Skip to main content

rhythm_open_exchange/model/
note.rs

1//! Note types for VSRG.
2
3use rkyv::{Archive, Deserialize, Serialize};
4use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize};
5
6/// Type of note.
7#[derive(
8    Debug,
9    Clone,
10    Copy,
11    PartialEq,
12    Eq,
13    Archive,
14    Serialize,
15    Deserialize,
16    SerdeSerialize,
17    SerdeDeserialize,
18)]
19#[serde(tag = "type", content = "data")]
20pub enum NoteType {
21    /// Single tap note.
22    Tap,
23    /// Long note (hold) - must be held for the duration.
24    Hold { duration_us: i64 },
25    /// Burst/roll note - rapid tapping during the duration.
26    Burst { duration_us: i64 },
27    /// Mine - avoid hitting this note.
28    Mine,
29}
30
31/// A single note in the chart.
32#[derive(
33    Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize,
34)]
35pub struct Note {
36    /// Position in microseconds.
37    pub time_us: i64,
38    /// Type of note (tap, hold, burst, mine).
39    pub note_type: NoteType,
40    /// Optional index into `RoxChart.hitsounds` for keysounded notes.
41    pub hitsound_index: Option<u16>,
42    /// Column index (0-indexed).
43    pub column: u8,
44}
45
46impl Note {
47    /// Create a tap note.
48    #[must_use]
49    pub fn tap(time_us: i64, column: u8) -> Self {
50        Self {
51            time_us,
52            column,
53            note_type: NoteType::Tap,
54            hitsound_index: None,
55        }
56    }
57
58    /// Create a hold note.
59    #[must_use]
60    pub fn hold(time_us: i64, duration_us: i64, column: u8) -> Self {
61        Self {
62            time_us,
63            column,
64            note_type: NoteType::Hold { duration_us },
65            hitsound_index: None,
66        }
67    }
68
69    /// Create a burst/roll note.
70    #[must_use]
71    pub fn burst(time_us: i64, duration_us: i64, column: u8) -> Self {
72        Self {
73            time_us,
74            column,
75            note_type: NoteType::Burst { duration_us },
76            hitsound_index: None,
77        }
78    }
79
80    /// Create a mine note.
81    #[must_use]
82    pub fn mine(time_us: i64, column: u8) -> Self {
83        Self {
84            time_us,
85            column,
86            note_type: NoteType::Mine,
87            hitsound_index: None,
88        }
89    }
90
91    /// Check if this is a hold note.
92    #[must_use]
93    pub fn is_hold(&self) -> bool {
94        matches!(self.note_type, NoteType::Hold { .. })
95    }
96
97    /// Check if this is a burst note.
98    #[must_use]
99    pub fn is_burst(&self) -> bool {
100        matches!(self.note_type, NoteType::Burst { .. })
101    }
102
103    /// Check if this is a mine.
104    #[must_use]
105    pub fn is_mine(&self) -> bool {
106        matches!(self.note_type, NoteType::Mine)
107    }
108
109    /// Get the duration for holds/bursts, or 0 for taps/mines.
110    #[must_use]
111    pub fn duration_us(&self) -> i64 {
112        match self.note_type {
113            NoteType::Tap | NoteType::Mine => 0,
114            NoteType::Hold { duration_us } | NoteType::Burst { duration_us } => duration_us,
115        }
116    }
117
118    /// Get end time (start time + duration).
119    #[must_use]
120    pub fn end_time_us(&self) -> i64 {
121        self.time_us + self.duration_us()
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_note_tap_constructor() {
131        let note = Note::tap(1_000_000, 2);
132
133        assert_eq!(note.time_us, 1_000_000);
134        assert_eq!(note.column, 2);
135        assert!(matches!(note.note_type, NoteType::Tap));
136        assert!(note.hitsound_index.is_none());
137    }
138
139    #[test]
140    fn test_note_hold_constructor() {
141        let note = Note::hold(2_000_000, 500_000, 1);
142
143        assert_eq!(note.time_us, 2_000_000);
144        assert_eq!(note.column, 1);
145        assert!(matches!(
146            note.note_type,
147            NoteType::Hold {
148                duration_us: 500_000
149            }
150        ));
151    }
152
153    #[test]
154    fn test_note_burst_constructor() {
155        let note = Note::burst(3_000_000, 300_000, 3);
156
157        assert_eq!(note.time_us, 3_000_000);
158        assert_eq!(note.column, 3);
159        assert!(matches!(
160            note.note_type,
161            NoteType::Burst {
162                duration_us: 300_000
163            }
164        ));
165    }
166
167    #[test]
168    fn test_note_mine_constructor() {
169        let note = Note::mine(4_000_000, 0);
170
171        assert_eq!(note.time_us, 4_000_000);
172        assert_eq!(note.column, 0);
173        assert!(matches!(note.note_type, NoteType::Mine));
174    }
175
176    #[test]
177    fn test_note_is_hold() {
178        assert!(!Note::tap(0, 0).is_hold());
179        assert!(Note::hold(0, 100, 0).is_hold());
180        assert!(!Note::burst(0, 100, 0).is_hold());
181        assert!(!Note::mine(0, 0).is_hold());
182    }
183
184    #[test]
185    fn test_note_is_burst() {
186        assert!(!Note::tap(0, 0).is_burst());
187        assert!(!Note::hold(0, 100, 0).is_burst());
188        assert!(Note::burst(0, 100, 0).is_burst());
189        assert!(!Note::mine(0, 0).is_burst());
190    }
191
192    #[test]
193    fn test_note_is_mine() {
194        assert!(!Note::tap(0, 0).is_mine());
195        assert!(!Note::hold(0, 100, 0).is_mine());
196        assert!(!Note::burst(0, 100, 0).is_mine());
197        assert!(Note::mine(0, 0).is_mine());
198    }
199
200    #[test]
201    fn test_note_duration_us() {
202        assert_eq!(Note::tap(0, 0).duration_us(), 0);
203        assert_eq!(Note::hold(0, 500_000, 0).duration_us(), 500_000);
204        assert_eq!(Note::burst(0, 300_000, 0).duration_us(), 300_000);
205        assert_eq!(Note::mine(0, 0).duration_us(), 0);
206    }
207
208    #[test]
209    fn test_note_end_time_us() {
210        assert_eq!(Note::tap(1_000_000, 0).end_time_us(), 1_000_000);
211        assert_eq!(Note::hold(1_000_000, 500_000, 0).end_time_us(), 1_500_000);
212        assert_eq!(Note::burst(2_000_000, 300_000, 0).end_time_us(), 2_300_000);
213        assert_eq!(Note::mine(3_000_000, 0).end_time_us(), 3_000_000);
214    }
215}