use rhythm_core::{Note, Rhythm};
use tja::{TaikoNote, TaikoNoteVariant};
use crate::constant::{GUAGE_MISS_FACTOR, RANGE_GREAT, RANGE_MISS, RANGE_OK};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Debug)]
pub enum Hit {
Don,
Kat,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Judgement {
Great,
Ok,
Miss,
ComboHit,
Nothing,
}
#[derive(Clone, PartialEq, PartialOrd, Debug)]
pub struct CalculatedNote {
pub inner: TaikoNote,
pub idx: usize,
pub visible_start: f64,
pub visible_end: f64,
}
impl Eq for CalculatedNote {}
impl Ord for CalculatedNote {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.inner.start.partial_cmp(&other.inner.start).unwrap()
}
}
impl CalculatedNote {
pub fn visible(&self, time: f64) -> bool {
if self.inner.volume == 0 {
return false;
}
if self.variant() == TaikoNoteVariant::Invisible
|| self.variant() == TaikoNoteVariant::Unknown
{
return false;
}
return time > self.visible_start && time < self.visible_end;
}
pub fn position(&self, time: f64) -> Option<(f64, f64)> {
if !self.visible(time) {
return None;
}
if self.inner.variant == TaikoNoteVariant::Don
|| self.inner.variant == TaikoNoteVariant::Kat
{
let position =
1.0 - (time - self.visible_start) / (self.visible_end - self.visible_start);
Some((position, position))
} else {
let head = 1.0 - (time - self.visible_start) / (5.0 / self.inner.speed as f64 * 60.0);
let tail = 1.0
- (time - self.visible_start - self.inner.duration)
/ (5.0 / self.inner.speed as f64 * 60.0);
Some((head, tail))
}
}
}
impl Note for CalculatedNote {
fn start(&self) -> f64 {
self.inner.start
}
fn duration(&self) -> f64 {
self.inner.duration
}
fn volume(&self) -> u16 {
self.inner.volume
}
#[allow(refining_impl_trait)]
fn variant(&self) -> u16 {
self.inner.variant.into()
}
fn set_start(&mut self, start: f64) {
self.inner.start = start;
}
fn set_duration(&mut self, duration: f64) {
self.inner.duration = duration;
}
fn set_volume(&mut self, volume: u16) {
self.inner.volume = volume;
}
fn set_variant(&mut self, variant: impl Into<u16>) {
self.inner.variant = TaikoNoteVariant::from(variant.into());
}
}
#[derive(Clone, PartialEq, PartialOrd, Debug)]
pub struct GameSource {
pub difficulty: u8,
pub level: u8,
pub scoreinit: Option<i32>,
pub scorediff: Option<i32>,
pub notes: Vec<TaikoNote>,
}
#[derive(Clone, PartialEq, PartialOrd, Debug)]
pub struct InputState<H> {
pub time: f64,
pub hit: Option<H>,
}
#[derive(Clone, PartialEq, PartialOrd, Debug)]
pub struct OutputState {
pub finished: bool,
pub score: u32,
pub current_combo: u32,
pub max_combo: u32,
pub gauge: f64,
pub judgement: Option<Judgement>,
pub display: Vec<CalculatedNote>,
}
pub trait TaikoEngine<H> {
fn new(src: GameSource) -> Self;
fn forward(&mut self, input: InputState<H>) -> OutputState;
}
pub struct DefaultTaikoEngine {
rhythm: Rhythm<CalculatedNote>,
difficulty: u8,
level: u8,
scoreinit: i32,
score: u32,
current_combo: u32,
max_combo: u32,
gauge: f64,
current_time: f64,
total_notes: usize,
}
impl TaikoEngine<Hit> for DefaultTaikoEngine {
fn new(src: GameSource) -> Self {
let notes = src
.notes
.iter()
.enumerate()
.map(|(idx, note)| {
let (visible_start, visible_end) = if note.variant() == TaikoNoteVariant::Don
|| note.variant() == TaikoNoteVariant::Kat
|| note.variant() == TaikoNoteVariant::Both
{
let start = note.start - (4.5 * 60.0 / note.speed) as f64;
let end = note.start + note.duration + (0.5 * 60.0 / note.speed) as f64;
(start, end)
} else {
(0.0, 0.0)
};
let inner = match note.variant {
TaikoNoteVariant::Don | TaikoNoteVariant::Kat => {
let mut note = *note;
note.start -= RANGE_MISS;
note.duration = RANGE_MISS * 2.0;
note
}
_ => *note,
};
CalculatedNote {
inner,
idx,
visible_start,
visible_end,
}
})
.collect::<Vec<_>>();
let total_notes = notes
.iter()
.filter(|note| {
note.variant() == TaikoNoteVariant::Don || note.variant() == TaikoNoteVariant::Kat
})
.count();
let rhythm = Rhythm::new(notes.clone());
let scoreinit = src.scoreinit.unwrap_or(100_000 / total_notes as i32 * 10);
DefaultTaikoEngine {
rhythm,
difficulty: src.difficulty,
level: src.level,
scoreinit,
score: 0,
current_combo: 0,
max_combo: 0,
gauge: 0.0,
current_time: 0.0,
total_notes,
}
}
fn forward(&mut self, input: InputState<Hit>) -> OutputState {
let time_diff = input.time - self.current_time;
self.current_time = input.time;
let passed = self.rhythm.forward(time_diff);
let judgement = if let Some(hit) = input.hit {
match hit {
Hit::Don => {
if let Some((note, delta_from_start)) = self.rhythm.hit(TaikoNoteVariant::Don) {
if note.variant() == TaikoNoteVariant::Both {
Some(Judgement::ComboHit)
} else {
let delta = (delta_from_start - note.duration() / 2.0).abs();
if delta < RANGE_GREAT {
Some(Judgement::Great)
} else if delta < RANGE_OK {
Some(Judgement::Ok)
} else {
Some(Judgement::Miss)
}
}
} else {
Some(Judgement::Nothing)
}
}
Hit::Kat => {
if let Some((note, t)) = self.rhythm.hit(TaikoNoteVariant::Kat) {
if note.variant() == TaikoNoteVariant::Both {
Some(Judgement::ComboHit)
} else {
let delta = (t - note.duration() / 2.0).abs();
if delta < RANGE_GREAT {
Some(Judgement::Great)
} else if delta < RANGE_OK {
Some(Judgement::Ok)
} else {
Some(Judgement::Miss)
}
}
} else {
Some(Judgement::Nothing)
}
}
}
} else {
None
};
if passed.iter().any(|note| {
note.variant() == TaikoNoteVariant::Don || note.variant() == TaikoNoteVariant::Kat
}) {
self.current_combo = 0;
self.gauge -= (1.0 / self.total_notes as f64)
* GUAGE_MISS_FACTOR[self.difficulty as usize][self.level as usize];
}
match judgement {
Some(Judgement::Great) => {
self.score += self.scoreinit as u32;
self.current_combo += 1;
self.max_combo = self.max_combo.max(self.current_combo);
self.gauge += 1.0 / self.total_notes as f64;
}
Some(Judgement::Ok) => {
self.score += (self.scoreinit as u32) / 2;
self.current_combo += 1;
self.max_combo = self.max_combo.max(self.current_combo);
self.gauge += (1.0 / self.total_notes as f64)
* (if self.difficulty >= 3 { 0.5 } else { 0.75 });
}
Some(Judgement::Miss) => {
self.current_combo = 0;
self.gauge -= (1.0 / self.total_notes as f64)
* GUAGE_MISS_FACTOR[self.difficulty as usize][self.level as usize];
}
Some(Judgement::ComboHit) => {
self.score += 100;
}
_ => {}
};
self.gauge = self.gauge.max(0.0).min(1.0);
let display = self
.rhythm
.notes
.iter()
.filter(|note| note.visible(input.time))
.cloned()
.collect();
OutputState {
finished: self.rhythm.finished(),
score: self.score,
current_combo: self.current_combo,
max_combo: self.max_combo,
gauge: self.gauge,
judgement,
display,
}
}
}