vsrg 0.1.0

Data structures for vertical scrolling rhythm games
Documentation
//! Common fixtures for notes storage tests
//!
//! Includes:
//! - Note data types
//! - Note group
//! - World (groups + tempo/scroll tracks)
//! - Factory helpers.
//!
//! Each test suite should generate their own notes storage with
//! [`crate::generate_notes_storage!`] tailored to the tests.

use soa_rs::{SoaClone, Soars};

use crate::math::Easing;
use crate::notes::long::LongNoteData;
use crate::notes::short::ShortNoteData;
use crate::notes::{GroupId, Interval, NoteGroup, NoteGroups};
use crate::rhythm::{
    ClockTime, ScrollSpeedChangeEvent, ScrollSpeedTrack, ScrollSpeedTracks, Tempo,
    TempoChangeEvent, TempoTrack, TempoTracks, Time, TrackId,
};

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct TestShort {
    pub(crate) time: Time,
    pub(crate) gr_id: GroupId,
    pub(crate) pos: f32,
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct TestLong {
    pub(crate) time: Interval<Time>,
    pub(crate) gr_id: GroupId,
    pub(crate) pos: f32,
}

#[derive(Clone, Copy, Soars, SoaClone)]
pub(crate) struct NoteData(pub(crate) f32);

/// Runtime state component for test notes.
#[derive(Debug, PartialEq, Clone, Copy, Soars, SoaClone, Default)]
pub(crate) struct NoteRuntime(pub(crate) f32);

impl ShortNoteData for TestShort {
    type ValueData = NoteData;
    type RuntimeData = NoteRuntime;
    fn time(&self) -> Time {
        self.time
    }
    fn group_id(&self) -> &GroupId {
        &self.gr_id
    }
    fn value_data(&self) -> Self::ValueData {
        NoteData(self.pos)
    }
    fn reconstruct<'a>(
        time: Time,
        data: <Self::ValueData as soa_rs::Soars>::Ref<'a>,
        gr_id: GroupId,
    ) -> Self {
        Self {
            time,
            gr_id,
            pos: *data.0,
        }
    }
}

impl LongNoteData for TestLong {
    type ValueData = NoteData;
    type RuntimeData = NoteRuntime;
    fn start_time(&self) -> Time {
        self.time.start()
    }
    fn duration(&self) -> Time {
        generativity::make_guard!(guard);
        let (start, end) = self
            .time
            .start()
            .prove_same_unit(self.time.end(), guard)
            .expect("test interval endpoints use the same time unit");
        (end - start).as_time()
    }
    fn group_id(&self) -> &GroupId {
        &self.gr_id
    }
    fn value_data(&self) -> Self::ValueData {
        NoteData(self.pos)
    }
    fn reconstruct<'a>(
        start_time: Time,
        duration: Time,
        data: <Self::ValueData as soa_rs::Soars>::Ref<'a>,
        gr_id: GroupId,
    ) -> Self {
        generativity::make_guard!(guard);
        let (start, duration) = start_time
            .prove_same_unit(duration, guard)
            .expect("test note timing uses the same time unit");
        let time = Interval::try_new(start_time, (start + duration).as_time()).unwrap();
        Self {
            time,
            gr_id,
            pos: *data.0,
        }
    }
}

/// Note group where the named group `g{i}` uses the named tempo track `t{i}`
/// and the named scroll track `s{i}`.
#[derive(Debug, Clone)]
pub(crate) struct TestGroup {
    tempo_track_id: TrackId,
    scroll_track_id: TrackId,
}

impl NoteGroup for TestGroup {
    fn main_group() -> Self {
        Self {
            tempo_track_id: TrackId::Main,
            scroll_track_id: TrackId::Main,
        }
    }

    fn tempo_track_id(&self) -> &TrackId {
        &self.tempo_track_id
    }

    fn scroll_track_id(&self) -> &TrackId {
        &self.scroll_track_id
    }
}

/// Note groups and tempo/scroll tracks for a test, where the named group `g{i}` uses the named
/// tempo track `t{i}` and the named scroll track `s{i}`, alongside the main group and tracks.
#[derive(Debug, Clone)]
pub(crate) struct World {
    pub(crate) named_count: usize,
    pub(crate) note_groups: NoteGroups<TestGroup>,
    pub(crate) tempo_tracks: TempoTracks,
    pub(crate) scroll_tracks: ScrollSpeedTracks,
}

fn test_tempo_track(bpm: f64) -> TempoTrack {
    TempoTrack::with_events(vec![TempoChangeEvent {
        time: Time::Clock(ClockTime::ZERO),
        tempo: Tempo::from_bpm(bpm).unwrap(),
        ease: Easing::InConst,
    }])
    .unwrap()
}

impl World {
    pub(crate) fn new(named_count: usize) -> Self {
        let tempo_tracks = TempoTracks::with_tracks(
            test_tempo_track(60.0),
            (0..named_count).map(|i| {
                (
                    format!("t{i}").into(),
                    test_tempo_track(60.0 + 10.0 * i as f64),
                )
            }),
        );
        let scroll_tracks = ScrollSpeedTracks::with_tracks(
            ScrollSpeedTrack::new(TrackId::Main),
            (0..named_count).map(|i| {
                (
                    format!("s{i}").into(),
                    ScrollSpeedTrack::with_events(
                        TrackId::Named(format!("t{i}").into()),
                        vec![
                            ScrollSpeedChangeEvent::new(
                                Time::Clock(ClockTime::ZERO),
                                1.0 + i as f64,
                                Easing::InConst,
                            )
                            .unwrap(),
                        ],
                        &tempo_tracks,
                    ),
                )
            }),
        );
        let note_groups = NoteGroups::with_groups(
            TestGroup::main_group(),
            (0..named_count).map(|i| {
                (
                    format!("g{i}").into(),
                    TestGroup {
                        tempo_track_id: TrackId::Named(format!("t{i}").into()),
                        scroll_track_id: TrackId::Named(format!("s{i}").into()),
                    },
                )
            }),
        );

        Self {
            named_count,
            note_groups,
            tempo_tracks,
            scroll_tracks,
        }
    }
}

/// A short note at time zero in the main group.
pub(crate) fn short_data(pos: f32) -> TestShort {
    TestShort {
        time: Time::Clock(ClockTime::ZERO),
        gr_id: GroupId::Main,
        pos,
    }
}

/// A long note over [0s, 1s] in the main group.
pub(crate) fn long_data(pos: f32) -> TestLong {
    TestLong {
        time: Interval::try_new(
            Time::Clock(ClockTime::ZERO),
            Time::Clock(ClockTime::from_milis(1000.0).unwrap()),
        )
        .expect("test endpoints use the same time unit"),
        gr_id: GroupId::Main,
        pos,
    }
}