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 pub time: f64,
122 pub hit: Option<H>,
124}
125
126#[derive(Clone, PartialEq, PartialOrd, Debug)]
127pub struct OutputState {
128 pub finished: bool,
130 pub score: u32,
132 pub current_combo: u32,
134 pub max_combo: u32,
136 pub gauge: f64,
138
139 pub judgement: Option<Judgement>,
141
142 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 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}