taiko_core/
taiko.rs

1use rhythm_core::{Note, Rhythm};
2use tja::{TaikoNote, TaikoNoteVariant};
3
4use crate::constant::{GUAGE_MISS_FACTOR, RANGE_GREAT, RANGE_MISS, RANGE_OK};
5
6#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Debug)]
7pub enum Hit {
8    Don,
9    Kat,
10}
11
12#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
13pub enum Judgement {
14    Great,
15    Ok,
16    Miss,
17    ComboHit,
18    Nothing,
19}
20
21#[derive(Clone, PartialEq, PartialOrd, Debug)]
22pub struct CalculatedNote {
23    pub inner: TaikoNote,
24    pub idx: usize,
25    pub visible_start: f64,
26    pub visible_end: f64,
27}
28
29impl Eq for CalculatedNote {}
30
31impl Ord for CalculatedNote {
32    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
33        self.inner.start.partial_cmp(&other.inner.start).unwrap()
34    }
35}
36
37impl CalculatedNote {
38    pub fn visible(&self, time: f64) -> bool {
39        if self.inner.volume == 0 {
40            return false;
41        }
42
43        if self.variant() == TaikoNoteVariant::Invisible
44            || self.variant() == TaikoNoteVariant::Unknown
45        {
46            return false;
47        }
48
49        return time > self.visible_start && time < self.visible_end;
50    }
51
52    pub fn position(&self, time: f64) -> Option<(f64, f64)> {
53        if !self.visible(time) {
54            return None;
55        }
56
57        if self.inner.variant == TaikoNoteVariant::Don
58            || self.inner.variant == TaikoNoteVariant::Kat
59        {
60            let position =
61                1.0 - (time - self.visible_start) / (self.visible_end - self.visible_start);
62            Some((position, position))
63        } else {
64            let head = 1.0 - (time - self.visible_start) / (5.0 / self.inner.speed as f64 * 60.0);
65            let tail = 1.0
66                - (time - self.visible_start - self.inner.duration)
67                    / (5.0 / self.inner.speed as f64 * 60.0);
68
69            Some((head, tail))
70        }
71    }
72}
73
74impl Note for CalculatedNote {
75    fn start(&self) -> f64 {
76        self.inner.start
77    }
78
79    fn duration(&self) -> f64 {
80        self.inner.duration
81    }
82
83    fn volume(&self) -> u16 {
84        self.inner.volume
85    }
86
87    #[allow(refining_impl_trait)]
88    fn variant(&self) -> u16 {
89        self.inner.variant.into()
90    }
91
92    fn set_start(&mut self, start: f64) {
93        self.inner.start = start;
94    }
95
96    fn set_duration(&mut self, duration: f64) {
97        self.inner.duration = duration;
98    }
99
100    fn set_volume(&mut self, volume: u16) {
101        self.inner.volume = volume;
102    }
103
104    fn set_variant(&mut self, variant: impl Into<u16>) {
105        self.inner.variant = TaikoNoteVariant::from(variant.into());
106    }
107}
108
109#[derive(Clone, PartialEq, PartialOrd, Debug)]
110pub struct GameSource {
111    pub difficulty: u8,
112    pub level: u8,
113    pub scoreinit: Option<i32>,
114    pub scorediff: Option<i32>,
115    pub notes: Vec<TaikoNote>,
116}
117
118#[derive(Clone, PartialEq, PartialOrd, Debug)]
119pub struct InputState<H> {
120    /// The current time played in the music, in seconds.
121    pub time: f64,
122    /// Hit event that happened since the last frame.
123    pub hit: Option<H>,
124}
125
126#[derive(Clone, PartialEq, PartialOrd, Debug)]
127pub struct OutputState {
128    /// If the game is finished. (All notes are passed)
129    pub finished: bool,
130    /// The current score of the player.
131    pub score: u32,
132    /// The current combo of the player.
133    pub current_combo: u32,
134    /// The maximum combo of the player.
135    pub max_combo: u32,
136    /// The current soul gauge of the player.
137    pub gauge: f64,
138
139    /// The judgement of the hit in the last frame.
140    pub judgement: Option<Judgement>,
141
142    /// Display state
143    pub display: Vec<CalculatedNote>,
144}
145
146pub trait TaikoEngine<H> {
147    fn new(src: GameSource) -> Self;
148    fn forward(&mut self, input: InputState<H>) -> OutputState;
149}
150
151pub struct DefaultTaikoEngine {
152    rhythm: Rhythm<CalculatedNote>,
153
154    difficulty: u8,
155    level: u8,
156    scoreinit: i32,
157
158    score: u32,
159    current_combo: u32,
160    max_combo: u32,
161    gauge: f64,
162
163    current_time: f64,
164
165    total_notes: usize,
166}
167
168impl TaikoEngine<Hit> for DefaultTaikoEngine {
169    fn new(src: GameSource) -> Self {
170        let notes = src
171            .notes
172            .iter()
173            .enumerate()
174            .map(|(idx, note)| {
175                let (visible_start, visible_end) = if note.variant() == TaikoNoteVariant::Don
176                    || note.variant() == TaikoNoteVariant::Kat
177                    || note.variant() == TaikoNoteVariant::Both
178                {
179                    let start = note.start - (4.5 * 60.0 / note.speed) as f64;
180                    let end = note.start + note.duration + (0.5 * 60.0 / note.speed) as f64;
181                    (start, end)
182                } else {
183                    (0.0, 0.0)
184                };
185
186                let inner = match note.variant {
187                    TaikoNoteVariant::Don | TaikoNoteVariant::Kat => {
188                        let mut note = *note;
189                        note.start -= RANGE_MISS;
190                        note.duration = RANGE_MISS * 2.0;
191                        note
192                    }
193                    _ => *note,
194                };
195
196                CalculatedNote {
197                    inner,
198                    idx,
199                    visible_start,
200                    visible_end,
201                }
202            })
203            .collect::<Vec<_>>();
204        let total_notes = notes
205            .iter()
206            .filter(|note| {
207                note.variant() == TaikoNoteVariant::Don || note.variant() == TaikoNoteVariant::Kat
208            })
209            .count();
210        let rhythm = Rhythm::new(notes.clone());
211        let scoreinit = src.scoreinit.unwrap_or(100_000 / total_notes as i32 * 10);
212
213        DefaultTaikoEngine {
214            rhythm,
215            difficulty: src.difficulty,
216            level: src.level,
217            scoreinit,
218            score: 0,
219            current_combo: 0,
220            max_combo: 0,
221            gauge: 0.0,
222            current_time: 0.0,
223            total_notes,
224        }
225    }
226
227    fn forward(&mut self, input: InputState<Hit>) -> OutputState {
228        let time_diff = input.time - self.current_time;
229        self.current_time = input.time;
230        let passed = self.rhythm.forward(time_diff);
231
232        let judgement = if let Some(hit) = input.hit {
233            match hit {
234                Hit::Don => {
235                    if let Some((note, delta_from_start)) = self.rhythm.hit(TaikoNoteVariant::Don) {
236                        if note.variant() == TaikoNoteVariant::Both {
237                            Some(Judgement::ComboHit)
238                        } else {
239                            let delta = (delta_from_start - note.duration() / 2.0).abs();
240                            if delta < RANGE_GREAT {
241                                Some(Judgement::Great)
242                            } else if delta < RANGE_OK {
243                                Some(Judgement::Ok)
244                            } else {
245                                Some(Judgement::Miss)
246                            }
247                        }
248                    } else {
249                        Some(Judgement::Nothing)
250                    }
251                }
252                Hit::Kat => {
253                    if let Some((note, t)) = self.rhythm.hit(TaikoNoteVariant::Kat) {
254                        if note.variant() == TaikoNoteVariant::Both {
255                            Some(Judgement::ComboHit)
256                        } else {
257                            let delta = (t - note.duration() / 2.0).abs();
258                            if delta < RANGE_GREAT {
259                                Some(Judgement::Great)
260                            } else if delta < RANGE_OK {
261                                Some(Judgement::Ok)
262                            } else {
263                                Some(Judgement::Miss)
264                            }
265                        }
266                    } else {
267                        Some(Judgement::Nothing)
268                    }
269                }
270            }
271        } else {
272            None
273        };
274
275        // missed note, reset combo
276        if passed.iter().any(|note| {
277            note.variant() == TaikoNoteVariant::Don || note.variant() == TaikoNoteVariant::Kat
278        }) {
279            self.current_combo = 0;
280            self.gauge -= (1.0 / self.total_notes as f64)
281                * GUAGE_MISS_FACTOR[self.difficulty as usize][self.level as usize];
282        }
283
284        match judgement {
285            Some(Judgement::Great) => {
286                self.score += self.scoreinit as u32;
287
288                self.current_combo += 1;
289                self.max_combo = self.max_combo.max(self.current_combo);
290
291                self.gauge += 1.0 / self.total_notes as f64;
292            }
293            Some(Judgement::Ok) => {
294                self.score += (self.scoreinit as u32) / 2;
295
296                self.current_combo += 1;
297                self.max_combo = self.max_combo.max(self.current_combo);
298
299                self.gauge += (1.0 / self.total_notes as f64)
300                    * (if self.difficulty >= 3 { 0.5 } else { 0.75 });
301            }
302            Some(Judgement::Miss) => {
303                self.current_combo = 0;
304
305                self.gauge -= (1.0 / self.total_notes as f64)
306                    * GUAGE_MISS_FACTOR[self.difficulty as usize][self.level as usize];
307            }
308            Some(Judgement::ComboHit) => {
309                self.score += 100;
310            }
311            _ => {}
312        };
313
314        self.gauge = self.gauge.max(0.0).min(1.0);
315
316        let display = self
317            .rhythm
318            .notes
319            .iter()
320            .filter(|note| note.visible(input.time))
321            .cloned()
322            .collect();
323
324        OutputState {
325            finished: self.rhythm.finished(),
326            score: self.score,
327            current_combo: self.current_combo,
328            max_combo: self.max_combo,
329            gauge: self.gauge,
330            judgement,
331            display,
332        }
333    }
334}