Holiday music: a dark carol for Christmas, a spring tune for Easter, a harpsichord dance for Halloween
During a holiday the title music plays its piece, the build music every fourth time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
65ed76865d
commit
9800ce7156
8 files changed
+319
-8
No files matched your search
+66
-1
@@ -24,6 +24,10 @@ pub enum Category {
|
||||
Defeat,
|
||||
Surface, // above ground: flute, harp, birdsong
|
||||
SurfaceBattle, // fighting above ground
|
||||
// the holidays' pieces (see holiday_category)
|
||||
Christmas,
|
||||
Easter,
|
||||
Halloween,
|
||||
}
|
||||
|
||||
// indices of all tracks of a category, in play order
|
||||
@@ -34,7 +38,27 @@ pub fn tracks_in(c: Category) -> Vec<usize> {
|
||||
// the calm tracks the music player offers (no battle, victory or defeat music)
|
||||
pub fn player_tracks() -> Vec<usize> {
|
||||
use Category as C;
|
||||
TRACKS.iter().enumerate().filter(|(_, t)| matches!(t.1, C::Title | C::Build | C::Surface | C::Torture | C::Temple)).map(|(i, _)| i).collect()
|
||||
TRACKS.iter().enumerate().filter(|(_, t)| matches!(t.1, C::Title | C::Build | C::Surface | C::Torture | C::Temple | C::Christmas | C::Easter | C::Halloween)).map(|(i, _)| i).collect()
|
||||
}
|
||||
|
||||
// the music of a holiday (holiday::current()), if it has one
|
||||
pub fn holiday_music(holiday: u8) -> Option<Category> {
|
||||
match holiday {
|
||||
crate::holiday::CHRISTMAS => Some(Category::Christmas),
|
||||
crate::holiday::EASTER => Some(Category::Easter),
|
||||
crate::holiday::HALLOWEEN => Some(Category::Halloween),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// the category actually played for `want` during a holiday: the title music always becomes the
|
||||
// holiday's piece, the build music every fourth time (`build_picks`: build tracks picked so far)
|
||||
pub fn holiday_category(want: Category, holiday: u8, build_picks: usize) -> Category {
|
||||
match (want, holiday_music(holiday)) {
|
||||
(Category::Title, Some(h)) => h,
|
||||
(Category::Build, Some(h)) if build_picks % 4 == 3 => h,
|
||||
_ => want,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_id(t: usize) -> &'static str {
|
||||
@@ -366,6 +390,47 @@ mod tests {
|
||||
assert_eq!(track_title(track_by_id("build13").unwrap()), "Underground River");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_holiday_has_its_piece_for_the_title_and_now_and_then_the_build_music() {
|
||||
use crate::holiday::{CHRISTMAS, EASTER, HALLOWEEN, NONE};
|
||||
for (h, c, id, title) in [
|
||||
(CHRISTMAS, Category::Christmas, "holiday_christmas", "Frostbound Carol"),
|
||||
(EASTER, Category::Easter, "holiday_easter", "Eggs in the Moss"),
|
||||
(HALLOWEEN, Category::Halloween, "holiday_halloween", "Crypt of Lanterns"),
|
||||
] {
|
||||
let t = track_by_id(id).unwrap();
|
||||
assert_eq!(tracks_in(c), vec![t]);
|
||||
assert_eq!(track_title(t), title);
|
||||
assert!(player_tracks().contains(&t));
|
||||
assert_eq!(holiday_category(Category::Title, h, 0), c);
|
||||
assert_eq!(holiday_category(Category::Title, h, 5), c);
|
||||
let builds: Vec<Category> = (0..8).map(|k| holiday_category(Category::Build, h, k)).collect();
|
||||
assert_eq!(builds.iter().filter(|&&b| b == c).count(), 2, "now and then, not always");
|
||||
assert!(builds.iter().all(|&b| b == c || b == Category::Build));
|
||||
for other in [Category::Battle, Category::Surface, Category::Victory, Category::Torture] {
|
||||
assert_eq!(holiday_category(other, h, 3), other);
|
||||
}
|
||||
// the piece plays: audible, finite and not clipping
|
||||
let m = std::rc::Rc::new(xm::load(TRACKS[t].2).unwrap());
|
||||
let mut p = Player::new(m, 22050.0);
|
||||
let mut buf = vec![0.0f32; 2 * 22050];
|
||||
let mut peak = 0.0f32;
|
||||
for _ in 0..20 {
|
||||
buf.iter_mut().for_each(|v| *v = 0.0);
|
||||
p.render(&mut buf, MUSIC_GAIN);
|
||||
for &v in &buf {
|
||||
assert!(v.is_finite());
|
||||
peak = peak.max(v.abs());
|
||||
}
|
||||
}
|
||||
assert!(peak > 0.05 && peak < 1.0, "{} peak {}", id, peak);
|
||||
}
|
||||
for k in 0..8 {
|
||||
assert_eq!(holiday_category(Category::Title, NONE, k), Category::Title);
|
||||
assert_eq!(holiday_category(Category::Build, NONE, k), Category::Build);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_crossfade_and_sfx() {
|
||||
dc_audio_init(48000.0);
|
||||
|
||||
+10
-3
@@ -492,11 +492,12 @@ fn apply_settings(s: &Settings, r3d: &mut R3d) {
|
||||
struct MusicDirector {
|
||||
next: std::collections::HashMap<u8, usize>, // category -> next index within the category
|
||||
current: Option<audio::Category>,
|
||||
build_picks: usize, // build tracks picked so far (during a holiday every fourth is its piece)
|
||||
}
|
||||
|
||||
impl MusicDirector {
|
||||
fn new() -> MusicDirector {
|
||||
MusicDirector { next: std::collections::HashMap::new(), current: None }
|
||||
MusicDirector { next: std::collections::HashMap::new(), current: None, build_picks: 0 }
|
||||
}
|
||||
|
||||
fn pick(&mut self, c: audio::Category) -> usize {
|
||||
@@ -573,11 +574,17 @@ impl MusicDirector {
|
||||
}
|
||||
let rotate = matches!(want, C::Build | C::Title | C::Surface) && self.current == Some(want) && audio::music_loops() >= 2;
|
||||
// after the chosen track was let go, the director picks again
|
||||
let foreign = chosen.is_none() && audio::current_track().map_or(false, |t| crate::tracks::TRACKS.get(t).map_or(false, |x| x.1 != want)) && calm && self.current == Some(want);
|
||||
// (a holiday's piece belongs to the title and build music)
|
||||
let holiday = holiday::current();
|
||||
let fits = |c: C| c == want || (matches!(want, C::Title | C::Build) && audio::holiday_music(holiday) == Some(c));
|
||||
let foreign = chosen.is_none() && audio::current_track().map_or(false, |t| crate::tracks::TRACKS.get(t).map_or(false, |x| !fits(x.1))) && calm && self.current == Some(want);
|
||||
if self.current != Some(want) || rotate || foreign || audio::current_track().is_none() {
|
||||
let before = self.current;
|
||||
self.current = Some(want);
|
||||
let t = self.pick(want);
|
||||
let t = self.pick(audio::holiday_category(want, holiday, self.build_picks));
|
||||
if want == C::Build {
|
||||
self.build_picks += 1;
|
||||
}
|
||||
// between the dungeon and the surface the music changes slowly
|
||||
let fade = if matches!(want, C::Victory | C::Defeat) {
|
||||
1.0
|
||||
|
||||
+4
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::audio::Category;
|
||||
|
||||
pub const TRACKS: [(&str, Category, &[u8]); 47] = [
|
||||
pub const TRACKS: [(&str, Category, &[u8]); 50] = [
|
||||
("title", Category::Title, include_bytes!("../assets/music/title.xm")),
|
||||
("title2", Category::Title, include_bytes!("../assets/music/title2.xm")),
|
||||
("build1", Category::Build, include_bytes!("../assets/music/build1.xm")),
|
||||
@@ -51,4 +51,7 @@ pub const TRACKS: [(&str, Category, &[u8]); 47] = [
|
||||
("victory", Category::Victory, include_bytes!("../assets/music/victory.xm")),
|
||||
("victory2", Category::Victory, include_bytes!("../assets/music/victory2.xm")),
|
||||
("defeat", Category::Defeat, include_bytes!("../assets/music/defeat.xm")),
|
||||
("holiday_christmas", Category::Christmas, include_bytes!("../assets/music/holiday_christmas.xm")),
|
||||
("holiday_easter", Category::Easter, include_bytes!("../assets/music/holiday_easter.xm")),
|
||||
("holiday_halloween", Category::Halloween, include_bytes!("../assets/music/holiday_halloween.xm")),
|
||||
];
|
||||
@@ -1130,7 +1130,8 @@ mod tests {
|
||||
fn render_all_songs() {
|
||||
for (k, (name, _, data)) in crate::tracks::TRACKS.iter().enumerate() {
|
||||
let m = Rc::new(load(data).expect("load"));
|
||||
assert!(m.channels == 8 || (m.channels == 10 && name.starts_with("surface")), "{} {}", name, m.channels);
|
||||
// birds and wind above ground and in the spring
|
||||
assert!(m.channels == 8 || (m.channels == 10 && (name.starts_with("surface") || *name == "holiday_easter")), "{} {}", name, m.channels);
|
||||
let mut p = Player::new(m, 48000.0);
|
||||
let mut buf = vec![0.0f32; 48000 * 2];
|
||||
let mut peak = 0.0f32;
|
||||
|
||||
Reference in new issue
Block a user