Soundtrack expanded to 35 pieces with a leitmotif variant generator and 8 new instruments

- generator: leitmotif as scale degrees, keys/modes, augmentation, diminution, inversion,
  fragment sequences, accompaniment and drum styles; hand-written title 2, boss themes, victory 2
- organ, bell, choir, sub bass, tom, crash, harp, timpani; 8-bit samples, unused instruments stripped
- src/tracks.rs generated with categories; music director with boss themes and room moods
- all pieces validated with libopenmpt; notes checked to stay in key

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 08:09:15 +01:00
1 parent 1e92e19089
commit 5158075d7b
42 files changed
+721 -98

No files matched your search

+61 -25
View File
@@ -11,6 +11,7 @@ mod render3d;
mod save;
mod settings;
mod sfx;
mod tracks;
mod sound;
mod ui;
mod xm;
@@ -73,35 +74,69 @@ fn apply_settings(s: &Settings, r3d: &mut R3d) {
audio::set_volumes(s.music_volume, s.volume, s.muted);
}
// Chooses the music for the current situation; build tracks rotate after two loops
// Chooses the music for the current situation. Each category rotates through its tracks;
// build and title music switch to the next track after two loops.
struct MusicDirector {
build: usize,
next: std::collections::HashMap<u8, usize>, // category -> next index within the category
current: Option<audio::Category>,
}
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.gate_defenders_alive();
let invasion = g.ents.iter().any(|e| e.alive && e.active_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();
fn new() -> MusicDirector {
MusicDirector { next: std::collections::HashMap::new(), current: None }
}
fn pick(&mut self, c: audio::Category) -> usize {
let list = audio::tracks_in(c);
let k = self.next.entry(c as u8).or_insert(0);
let t = list[*k % list.len()];
*k += 1;
t
}
fn update(&mut self, g: &Game, title: bool, cam: (f32, f32)) {
use audio::Category as C;
let alive = |f: &dyn Fn(&game::Ent) -> bool| g.ents.iter().any(|e| e.alive && f(e));
let near_room = |k: map::Kind| {
let (cx, cz) = (cam.0 as i32, cam.1 as i32);
let mut n = 0;
for y in cz - 5..=cz + 5 {
for x in cx - 5..=cx + 5 {
if g.map.get(x, y).map_or(false, |t| t.kind == k) {
n += 1;
}
}
}
BUILD[self.build]
n >= 4
};
let fade = if matches!(want, Track::Victory | Track::Defeat) { 1.0 } else { 2.5 };
audio::play_music(want, fade);
let want = if title {
C::Title
} else if g.state == 1 {
C::Victory
} else if g.state == 2 {
C::Defeat
} else if alive(&|e| e.active_hero() && e.kind == game::EK::Paladin && e.guard) {
C::BossWarden
} else if alive(&|e| e.active_hero() && e.kind == game::EK::Paladin) {
C::BossPaladin
} else if g.time - g.gate_hit_t < 12.0 || g.gate_defenders_alive() {
C::Assault
} else if alive(&|e| e.active_hero() && !e.guard) {
C::Battle
} else if near_room(map::Kind::Torture) {
C::Torture
} else if near_room(map::Kind::Temple) {
C::Temple
} else {
C::Build
};
let rotate = matches!(want, C::Build | C::Title) && self.current == Some(want) && audio::music_loops() >= 2;
if self.current != Some(want) || rotate || audio::current_track().is_none() {
self.current = Some(want);
let t = self.pick(want);
let fade = if matches!(want, C::Victory | C::Defeat) { 1.0 } else { 2.5 };
audio::play_music(t, fade);
}
}
}
@@ -175,7 +210,7 @@ async fn main() {
let gfx = Gfx::new();
let mut r3d = R3d::new();
let mut sounds = SoundBank::new();
let mut music = MusicDirector { build: 0 };
let mut music = MusicDirector::new();
let mut ui = Ui::new();
let mut menu = Menu::new();
@@ -542,7 +577,8 @@ async fn main() {
for ev in app.game.events.drain(..) {
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, .. })));
let on_title = menu.page == Some(Page::Title) || matches!(menu.page, Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. }) | Some(Page::NewGame));
music.update(&app.game, on_title, (app.cam.tx, app.cam.tz));
for m in app.game.msgs.drain(..) {
ui.log.push((m, now));
}