diff --git a/data-struct-lib/src/song.rs b/data-struct-lib/src/song.rs index 3e2d544..c74be95 100644 --- a/data-struct-lib/src/song.rs +++ b/data-struct-lib/src/song.rs @@ -41,7 +41,7 @@ pub struct GameNote { #[serde(skip, default = "JangguFace::default")] pub face: JangguFace, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct GameChart { pub artist: String, pub delay: u64, @@ -241,10 +241,11 @@ impl GameSong { .unwrap() .to_string(); if let Some(video_filename) = deserialized.video_filename { - deserialized.video_filename = Some(Path::join(path, video_filename) - .to_str() - .unwrap() - .to_string() + deserialized.video_filename = Some( + Path::join(path, video_filename) + .to_str() + .unwrap() + .to_string(), ); } deserialized.cover_image_filename = Path::join(path, deserialized.cover_image_filename) diff --git a/game/src/game/game_player.rs b/game/src/game/game_player.rs index ac5c54f..aa569c6 100644 --- a/game/src/game/game_player.rs +++ b/game/src/game/game_player.rs @@ -1,60 +1,34 @@ -pub mod draw_gameplay_ui; +pub mod chart_player; +pub mod chart_player_ui; +pub mod effect_sound_player; pub mod game_result; pub mod janggu_state_with_tick; -pub mod judge_and_display_notes; -pub mod load_hit_sounds; pub mod timing_judge; use std::{path::Path, thread}; use kira::{ clock::ClockSpeed, - manager::{backend::DefaultBackend, AudioManager, AudioManagerSettings}, sound::static_sound::{StaticSoundData, StaticSoundSettings}, tween::Tween, }; use num_rational::Rational64; use sdl2::{image::LoadTexture, pixels::PixelFormatEnum}; -use crate::{ - create_streaming_iyuv_texture, - game::{ - common::{event_loop_common, render_common}, - game_common_context, - game_player::judge_and_display_notes::display_notes_and_judge, - }, +use crate::game::{ + common::{event_loop_common, render_common}, + game_common_context, }; use self::{ - draw_gameplay_ui::{DisplayedSongNote, UIContent}, - game_result::GameResult, + chart_player::ChartPlayer, effect_sound_player::EffectSoundPlayer, game_result::GameResult, janggu_state_with_tick::JangguStateWithTick, - judge_and_display_notes::EffectSoundHandles, - load_hit_sounds::load_hit_sounds, - timing_judge::{NoteAccuracy, TimingJudge}, }; -use bidrum_data_struct_lib::{janggu::JangguFace, song::GameSong}; +use bidrum_data_struct_lib::song::GameSong; use super::render_video::VideoFileRenderer; -pub fn is_input_effect_needed(state: &JangguStateWithTick, tick: i128) -> [Option; 2] { - const TIME_DELTA: i128 = 150; - let mut faces = [None, None]; - if let Some(_) = state.궁채.face { - if state.궁채.keydown_timing - tick < TIME_DELTA { - faces[0] = state.궁채.face; - } - } - if let Some(_) = state.열채.face { - if state.열채.keydown_timing - tick < TIME_DELTA { - faces[1] = state.열채.face; - } - } - - faces -} - pub(crate) fn play_song( common_context: &mut game_common_context::GameCommonContext, song: &GameSong, @@ -77,8 +51,7 @@ pub(crate) fn play_song( let song_path_string = song.audio_filename.clone(); // Load hit sound data - let hit_sounds = load_hit_sounds(); - let mut effect_sound_handles = EffectSoundHandles::new(); + let mut effect_sounds = EffectSoundPlayer::new(); // to receive coin input while loading the audio file, // loading should be done in separated thread. @@ -129,7 +102,6 @@ pub(crate) fn play_song( // get judge and create timing judge let chart = song.get_chart(level).unwrap(); - let mut timing_judge = TimingJudge::new(&chart); // start the clock. clock.start().expect("Failed to start clock"); @@ -169,13 +141,10 @@ pub(crate) fn play_song( .expect("Failed to load play background image."); // variables for displaying accuracy - let mut accuracy: Option = None; - let mut accuracy_tick: Option = None; let mut janggu_state_with_tick = JangguStateWithTick::new(); - let mut processed_note_ids = Vec::::new(); - let mut gameplay_ui_resources = draw_gameplay_ui::GamePlayUIResources::new(&texture_creator); + let mut chart_player = ChartPlayer::new(chart, &texture_creator); 'running: loop { let tick_now = clock.time().ticks as i128 - start_tick.ticks as i128; @@ -222,20 +191,16 @@ pub(crate) fn play_song( let input_now = common_context.read_janggu_state(); janggu_state_with_tick.update(input_now, tick_now); + effect_sounds.play_janggu_sound(&janggu_state_with_tick, &mut common_context.audio_manager); + // display notes and accuracy if tick_now >= 0 { - display_notes_and_judge( - common_context, - &chart, - &mut timing_judge, - &janggu_state_with_tick, - &mut gameplay_ui_resources, - &mut processed_note_ids, - &mut accuracy, - &mut accuracy_tick, - &hit_sounds, - &mut effect_sound_handles, + chart_player.judge(&janggu_state_with_tick, tick_now); + chart_player.draw( tick_now, + &mut common_context.canvas, + common_context.game_initialized_at.elapsed().as_millis(), + &janggu_state_with_tick, ); } @@ -258,5 +223,5 @@ pub(crate) fn play_song( // If video_file_renderer is not None, stop playing video video_file_renderer.stop_decoding(); } - return Some(timing_judge.get_game_result()); + return Some(chart_player.game_result()); } diff --git a/game/src/game/game_player/chart_player.rs b/game/src/game/game_player/chart_player.rs new file mode 100644 index 0000000..e373669 --- /dev/null +++ b/game/src/game/game_player/chart_player.rs @@ -0,0 +1,189 @@ +use bidrum_data_struct_lib::{ + janggu::JangguFace, + song::{GameChart, GameNote}, +}; +use sdl2::{render::Canvas, video::Window}; + +use crate::game::game_player::{ + chart_player_ui::displayed_song_note::DisplayedSongNote, timing_judge::NoteAccuracy, +}; + +use super::{ + chart_player_ui::{disappearing_note_effect::DisapearingNoteEffect, ChartPlayerUI}, + game_result::GameResult, + janggu_state_with_tick::JangguStateWithTick, + timing_judge::TimingJudge, +}; + +struct ProcessedNote { + processed_note_id: u64, + processed_at_tick: i128, + accuracy: NoteAccuracy, +} + +pub struct ChartPlayer<'a> { + chart: GameChart, + timing_judge: TimingJudge, + ui: ChartPlayerUI<'a>, + processed_notes: Vec, + accuracy: Option<(NoteAccuracy, i128)>, +} + +impl ChartPlayer<'_> { + pub fn new( + chart: GameChart, + texture_creator: &sdl2::render::TextureCreator, + ) -> ChartPlayer { + ChartPlayer { + chart: chart.clone(), + timing_judge: TimingJudge::new(&chart), + ui: ChartPlayerUI::new(texture_creator), + processed_notes: vec![], + accuracy: None, + } + } + + fn append_disappearing_notes(&mut self, tick: i128, note_ids: Vec) { + let left_face = self.chart.left_face.clone(); + let right_face = self.chart.right_face.clone(); + let faces = [left_face, right_face].concat(); + let disappearing_notes = faces.iter().filter(|i| { + note_ids.contains(&i.id) + && self.processed_notes.iter().any(|j| { + j.processed_note_id == i.id + && j.processed_at_tick.abs_diff(tick.into()) + < DisapearingNoteEffect::effect_duration() + }) + }); + + for i in disappearing_notes { + self.ui + .disappearing_note_effects + .push_note(self.get_display_note(i, tick), tick as i128) + } + } + + pub fn judge(&mut self, janggu: &JangguStateWithTick, tick: i128) { + let new_accuracies = self.timing_judge.judge(janggu, tick as u64); + + if !new_accuracies.is_empty() { + self.accuracy = Some(( + new_accuracies.iter().map(|x| x.accuracy).max().unwrap(), + tick, + )); + for i in new_accuracies.clone() { + self.processed_notes.push(ProcessedNote { + processed_note_id: i.note_id, + processed_at_tick: tick, + accuracy: i.accuracy, + }); + + if !matches!(i.accuracy, NoteAccuracy::Miss) {} + } + } + + self.append_disappearing_notes( + tick, + new_accuracies + .iter() + .filter(|x| !matches!(x.accuracy, NoteAccuracy::Miss)) + .map(|x| x.note_id) + .collect(), + ); + } + + fn get_display_note(&self, note: &GameNote, tick: i128) -> DisplayedSongNote { + DisplayedSongNote { + face: note.face, + stick: note.stick, + distance: note.get_position( + self.chart.bpm, + self.chart.delay, + self.chart.bpm * 2, + tick as u64, + ), + } + } + + fn processed_note_ids(&self) -> Vec { + self.processed_notes + .iter() + .map(|x| x.processed_note_id) + .collect() + } + + fn get_display_notes(&self, tick_now: u64) -> Vec { + let mut display_notes = Vec::::new(); + if tick_now >= 0 { + // get positions of the notes + for i in &self.chart.left_face { + if !self.processed_note_ids().contains(&i.id) { + display_notes.push(DisplayedSongNote { + face: JangguFace::궁편, + stick: i.stick, + distance: i.get_position( + self.chart.bpm, + self.chart.delay, + self.chart.bpm * 2, + tick_now, + ), + }); + } + } + + for i in &self.chart.right_face { + if !self.processed_note_ids().contains(&i.id) { + display_notes.push(DisplayedSongNote { + face: JangguFace::열편, + stick: i.stick, + distance: i.get_position( + self.chart.bpm, + self.chart.delay, + self.chart.bpm * 2, + tick_now, + ), + }); + } + } + } + + display_notes + } + + pub fn draw( + &mut self, + tick: i128, + canvas: &mut Canvas, + overall_tick: u128, + janggu_state_with_tick: &JangguStateWithTick, + ) { + // judgement is visible for only 800 ms + const ACCURACY_DISPLAY_DURATION: u32 = 800; + + // set ui accuracy effect + self.ui.accuracy = None; + self.ui.accuracy_time_progress = None; + if let Some(accuracy) = self.accuracy { + if accuracy.1.abs_diff(tick) > ACCURACY_DISPLAY_DURATION.into() { + self.accuracy = None; + } else { + self.ui.accuracy = Some(accuracy.0); + self.ui.accuracy_time_progress = + Some(accuracy.1.abs_diff(tick) as f32 / ACCURACY_DISPLAY_DURATION as f32) + } + } + + // draw game play ui + if tick >= 0 { + self.ui.disappearing_note_effects.update_base_tick(tick); + self.ui.input_effect.update(janggu_state_with_tick, tick); + self.ui.notes = self.get_display_notes(tick as u64); + } + self.ui.overall_effect_tick = overall_tick; + self.ui.draw(canvas); + } + + pub fn game_result(&self) -> GameResult { + self.timing_judge.get_game_result() + } +} diff --git a/game/src/game/game_player/chart_player_ui.rs b/game/src/game/game_player/chart_player_ui.rs new file mode 100644 index 0000000..0fbe115 --- /dev/null +++ b/game/src/game/game_player/chart_player_ui.rs @@ -0,0 +1,388 @@ +pub mod disappearing_note_effect; +pub mod displayed_song_note; +pub mod input_effect; +mod resources; + +use ezing::expo_out; +use num_rational::Rational32; +use sdl2::{ + pixels::Color, + rect::Rect, + render::{Canvas, TextureCreator}, + video::{Window, WindowContext}, +}; + +use bidrum_data_struct_lib::janggu::{JangguFace, JangguStick}; + +use self::{ + disappearing_note_effect::DisapearingNoteEffect, displayed_song_note::DisplayedSongNote, + input_effect::InputEffect, resources::ChartPlayerUIResources, +}; + +use super::timing_judge::NoteAccuracy; + +pub struct ChartPlayerUI<'a> { + pub notes: Vec, + pub accuracy: Option, + pub accuracy_time_progress: Option, + pub input_effect: InputEffect, + pub overall_effect_tick: u128, + pub disappearing_note_effects: DisapearingNoteEffect, + resources: ChartPlayerUIResources<'a>, +} + +/// draws chart player ui into canvas +impl ChartPlayerUI<'_> { + /// creates new GamePlayUI + pub fn new(texture_creator: &TextureCreator) -> ChartPlayerUI { + ChartPlayerUI::with_resources(ChartPlayerUIResources::new(texture_creator)) + } + + pub fn with_resources(resources: ChartPlayerUIResources) -> ChartPlayerUI { + return ChartPlayerUI { + notes: vec![], + accuracy: None, + accuracy_time_progress: None, + input_effect: InputEffect::new(), + overall_effect_tick: 0, + disappearing_note_effects: DisapearingNoteEffect::new(), + resources: resources, + }; + } + + /// renders game play ui with notes + pub fn draw(&mut self, canvas: &mut Canvas) { + // loads texture of judgement line + let judgement_line_texture = &self.resources.judgement_line_texture; + + // enable alpha blending + canvas.set_blend_mode(sdl2::render::BlendMode::Blend); + + // load textures + let note_textures = &mut self.resources.note_textures; + let accuracy_textures = &mut self.resources.accuray_textures; + let janggu_texture = &self.resources.janggu_texture; + + // get note size + let right_stick_note_height = 90; + let right_stick_note_width = (note_textures.right_stick.query().width as f32 + / note_textures.right_stick.query().height as f32 + * right_stick_note_height as f32) as u32; + let left_stick_note_height = (note_textures.left_stick.query().height as f32 + * (right_stick_note_height as f32 / note_textures.right_stick.query().height as f32)) + as u32; + let left_stick_note_width = (note_textures.left_stick.query().width as f32 + / note_textures.left_stick.query().height as f32 + * left_stick_note_height as f32) as u32; + let max_stick_note_height = std::cmp::max(left_stick_note_height, right_stick_note_height); + + // calculate background height + let background_padding = 15; + let background_border_height = 5 as u32; + let background_height_without_border = max_stick_note_height + background_padding * 2; + let background_height_with_border = + background_height_without_border + background_border_height * 2; + + // calculate janggu width + let janggu_width_min = ((janggu_texture.query().width as f32 + / janggu_texture.query().height as f32) + * background_height_with_border as f32) as u32; + let janggu_width_max = janggu_width_min + 20; + + // calculate janggu icon size + let janggu_width = janggu_width_min + + ((janggu_width_max - janggu_width_min) as f64 * { + if self.input_effect.left_face.keydown_timing.is_none() + && self.input_effect.right_face.keydown_timing.is_none() + { + 0.0 + } else { + // animation duration + let animation_duration = 250; + + // last keydown timing + let last_keydown_timing = self + .input_effect + .left_face + .keydown_timing + .unwrap_or(0) + .max(self.input_effect.right_face.keydown_timing.unwrap_or(0)); + + // elapsed time since last keydown timing + let delta = self.input_effect.base_tick - last_keydown_timing; + + // return easing value + if delta < animation_duration { + 1.0 - ezing::bounce_inout(delta as f64 / animation_duration as f64) + } else { + 0.0 + } + } + }) as u32; + let janggu_height = ((janggu_texture.query().height as f32 + / janggu_texture.query().width as f32) + * janggu_width as f32) as u32; + + // get viewport + let viewport = canvas.viewport(); + + // draw backgrounds + let background_width = (viewport.width() - janggu_width_min) / 2; + let background_y = + (canvas.viewport().height() as i32 - (background_height_without_border as i32)) / 2; + for background_x in [ + 0, /* x coordinate of left background */ + background_width as i32 + janggu_width_min as i32, /* x coordinate of right background */ + ] { + let background_alpha = { + // is the face hitted? + let hitting = if background_x == 0 { + self.input_effect.left_face.pressed + } else { + self.input_effect.right_face.pressed + }; + + // base which changed by whether it's hitted or not + let base = if hitting { 200 } else { 100 }; + + // effect that changes by time + let effect_by_time = (50.0 * { + ezing::quad_out({ + let blinking_duration = 300; + let total_duration = 1000; + assert!(0 < blinking_duration && blinking_duration <= total_duration); + + let remainder = (self.overall_effect_tick % total_duration) as f64; + if remainder < blinking_duration as f64 { + (blinking_duration as f64 - remainder) / blinking_duration as f64 + } else { + 0.0 + } + }) + }) as u8; + + base + effect_by_time + }; + canvas.set_draw_color(Color::RGBA(200, 200, 200, background_alpha)); + canvas + .fill_rect(Rect::new( + background_x, + background_y, + background_width, + background_height_without_border, + )) + .unwrap(); + + // draw border, too + canvas.set_draw_color(Color::RGBA(120, 120, 120, background_alpha)); + canvas + .fill_rect(Rect::new( + background_x, + background_y - background_border_height as i32, + background_width, + background_border_height, + )) + .unwrap(); + canvas + .fill_rect(Rect::new( + background_x, + background_y + background_height_without_border as i32, + background_width, + background_border_height, + )) + .unwrap(); + } + + // draw judgement line + let judgement_line_height = max_stick_note_height; + let judgement_line_padding_px = 20; + let judgement_line_width = ((judgement_line_texture.query().width as f32 + / judgement_line_texture.query().height as f32) + * max_stick_note_height as f32) as u32; + let judgeline_line_ypos = background_y + background_padding as i32; + let judgement_line_xposes = [ + background_width as i32 - judgement_line_width as i32 - judgement_line_padding_px, /* left judgement line */ + background_width as i32 + janggu_width_min as i32 + judgement_line_padding_px, /* right judgement line */ + ]; + for judgement_line_xpos in judgement_line_xposes { + canvas + .copy( + &judgement_line_texture, + None, + Rect::new( + judgement_line_xpos, + judgeline_line_ypos, + judgement_line_width, + judgement_line_height, + ), + ) + .unwrap(); + } + + // load textures for the notes and accuracy + let note_width_max = std::cmp::max(left_stick_note_width, right_stick_note_width); + + // draw note + let mut draw_note = |i: &DisplayedSongNote, disappearing_effect: Option| { + let note_texture = match i.stick { + JangguStick::궁채 => &mut note_textures.left_stick, + JangguStick::열채 => &mut note_textures.right_stick, + }; + let note_width = match i.stick { + JangguStick::궁채 => left_stick_note_width, + JangguStick::열채 => right_stick_note_width, + }; + let note_height = match i.stick { + JangguStick::궁채 => left_stick_note_height, + JangguStick::열채 => right_stick_note_height, + }; + let note_ypos = background_y + + (background_height_without_border as i32 - note_height as i32) as i32 / 2 + + if let Some(disappearing_effect_progress) = disappearing_effect { + ((background_height_with_border - note_height + 20) as f32 + * -ezing::circ_out(disappearing_effect_progress)) as i32 + } else { + 0 + }; + + /* + * note_xpos judgement_line_xpos + * +--------- +---- + * - - distance_between_centers - - + * - *----------------------------------------------------* - + * - - - - + * ---------- ----- + */ + let distance_between_centers = i.distance * note_width_max as f64; + let note_xpos = (match i.face { + JangguFace::궁편 => judgement_line_xposes[0], + JangguFace::열편 => judgement_line_xposes[1], + } as f64 + + (judgement_line_width / 2) as f64 + + distance_between_centers + * match i.face { + JangguFace::궁편 => -1.0, + JangguFace::열편 => 1.0, + } + - (note_width / 2) as f64) as i32; + + // Do not render note if the note is on janggu icon + let near_center_edge_x_pos = match i.face { + JangguFace::궁편 => note_xpos + note_width as i32, + JangguFace::열편 => note_xpos, + }; + let distance_with_center = match i.face { + JangguFace::궁편 => (viewport.width() / 2) as i32 - near_center_edge_x_pos, + JangguFace::열편 => near_center_edge_x_pos - (viewport.width() / 2) as i32, + }; + if distance_with_center <= (janggu_width_min / 2) as i32 { + return; + } + + // draw note + if let Some(disappearing_effect_progress) = disappearing_effect { + note_texture.set_alpha_mod( + (255.0 * (1.0 - ezing::circ_out(disappearing_effect_progress))) as u8, + ); + } else { + note_texture.set_alpha_mod(255); + } + + canvas + .copy( + note_texture, + None, + Rect::new(note_xpos, note_ypos, note_width, note_height), + ) + .unwrap(); + }; + + // draw right-stick first, and then draw left-stick. + for i in &self.notes { + if matches!(i.stick, JangguStick::열채) { + draw_note(i, None); + } + } + for i in &self.notes { + if matches!(i.stick, JangguStick::궁채) { + draw_note(i, None); + } + } + + // draw disappearing notes, too + let note_disappearing_duration = DisapearingNoteEffect::effect_duration(); + for i in &self.disappearing_note_effects.notes { + let tick_delta = i.tick.abs_diff(self.disappearing_note_effects.base_tick); + if (matches!(i.note.stick, JangguStick::열채) + && tick_delta < note_disappearing_duration) + { + draw_note( + &i.note, + Some(tick_delta as f32 / note_disappearing_duration as f32), + ) + } + } + for i in &self.disappearing_note_effects.notes { + let tick_delta = i.tick.abs_diff(self.disappearing_note_effects.base_tick); + if (matches!(i.note.stick, JangguStick::궁채) + && tick_delta < note_disappearing_duration) + { + draw_note( + &i.note, + Some(tick_delta as f32 / note_disappearing_duration as f32), + ) + } + } + + // draw janggu icon on center + canvas + .copy( + &janggu_texture, + None, + Rect::new( + (viewport.width() - janggu_width) as i32 / 2, + (viewport.height() - janggu_height) as i32 / 2, + janggu_width, + janggu_height, + ), + ) + .expect("Failed to draw janggu icon"); + + // draw note accuracy + if let Some(accuracy) = self.accuracy { + let accuracy_texture = match accuracy { + NoteAccuracy::Overchaos => &mut accuracy_textures.overchaos, + NoteAccuracy::Perfect => &mut accuracy_textures.perfect, + NoteAccuracy::Great => &mut accuracy_textures.great, + NoteAccuracy::Good => &mut accuracy_textures.good, + NoteAccuracy::Bad => &mut accuracy_textures.bad, + NoteAccuracy::Miss => &mut accuracy_textures.miss, + }; + + let width = 120; + let height = (Rational32::new( + accuracy_texture.query().height as i32 * width as i32, + accuracy_texture.query().width as i32, + )) + .to_integer(); + let x = (viewport.width() - width) as i32 / 2; + let y_start = + (viewport.height() - background_height_with_border) as i32 / 2 - (height / 2); + let y_end = y_start - height as i32 - 10; + let y = y_start + + ((y_end - y_start) as f32 * expo_out(self.accuracy_time_progress.unwrap())) + as i32; + + accuracy_texture + .set_alpha_mod((expo_out(self.accuracy_time_progress.unwrap()) * 255.0) as u8); + + canvas + .copy( + &accuracy_texture, + None, + Rect::new(x, y, width as u32, height as u32), + ) + .unwrap(); + } + } +} diff --git a/game/src/game/game_player/chart_player_ui/disappearing_note_effect.rs b/game/src/game/game_player/chart_player_ui/disappearing_note_effect.rs new file mode 100644 index 0000000..e0feeb3 --- /dev/null +++ b/game/src/game/game_player/chart_player_ui/disappearing_note_effect.rs @@ -0,0 +1,36 @@ +use super::displayed_song_note::DisplayedSongNote; + +#[derive(Clone)] +pub struct DisapearingNoteEffectItem { + pub(super) tick: i128, + pub(super) note: DisplayedSongNote, +} + +#[derive(Clone)] +pub struct DisapearingNoteEffect { + pub(super) base_tick: i128, + pub(super) notes: Vec, +} +impl DisapearingNoteEffect { + pub fn effect_duration() -> u128 { + 150 + } + + pub fn new() -> DisapearingNoteEffect { + DisapearingNoteEffect { + base_tick: 0, + notes: vec![], + } + } + + pub fn push_note(&mut self, note: DisplayedSongNote, tick: i128) { + self.notes.push(DisapearingNoteEffectItem { + tick: tick, + note: note, + }); + } + + pub fn update_base_tick(&mut self, tick: i128) { + self.base_tick = tick; + } +} diff --git a/game/src/game/game_player/chart_player_ui/displayed_song_note.rs b/game/src/game/game_player/chart_player_ui/displayed_song_note.rs new file mode 100644 index 0000000..6f4a149 --- /dev/null +++ b/game/src/game/game_player/chart_player_ui/displayed_song_note.rs @@ -0,0 +1,8 @@ +use bidrum_data_struct_lib::janggu::{JangguFace, JangguStick}; + +#[derive(Clone)] +pub struct DisplayedSongNote { + pub distance: f64, + pub face: JangguFace, + pub stick: JangguStick, +} diff --git a/game/src/game/game_player/chart_player_ui/input_effect.rs b/game/src/game/game_player/chart_player_ui/input_effect.rs new file mode 100644 index 0000000..8e013d8 --- /dev/null +++ b/game/src/game/game_player/chart_player_ui/input_effect.rs @@ -0,0 +1,77 @@ +use bidrum_data_struct_lib::janggu::JangguFace; + +use crate::game::game_player::janggu_state_with_tick::JangguStateWithTick; + +#[derive(Clone)] +pub struct InputEffectItem { + pub(super) pressed: bool, + pub(super) keydown_timing: Option, +} + +#[derive(Clone)] +pub struct InputEffect { + pub(super) left_face: InputEffectItem, + pub(super) right_face: InputEffectItem, + pub(super) base_tick: i128, +} + +impl InputEffect { + pub fn new() -> InputEffect { + return InputEffect { + base_tick: 0, + left_face: InputEffectItem { + pressed: false, + keydown_timing: None, + }, + right_face: InputEffectItem { + pressed: false, + keydown_timing: None, + }, + }; + } + pub fn update(&mut self, janggu: &JangguStateWithTick, tick_now: i128) { + self.base_tick = tick_now; + + // Process left face + self.left_face.pressed = false; + if janggu.궁채.face.is_some_and(|x| x == JangguFace::궁편) { + self.left_face.pressed = true; + self.left_face.keydown_timing = + Some(if let Some(prev) = self.left_face.keydown_timing { + prev.max(janggu.궁채.keydown_timing) + } else { + janggu.궁채.keydown_timing + }) + } + if janggu.열채.face.is_some_and(|x| x == JangguFace::궁편) { + self.left_face.pressed = true; + self.left_face.keydown_timing = + Some(if let Some(prev) = self.left_face.keydown_timing { + prev.max(janggu.열채.keydown_timing) + } else { + janggu.열채.keydown_timing + }) + } + + // Process right face + self.right_face.pressed = false; + if janggu.궁채.face.is_some_and(|x| x == JangguFace::열편) { + self.right_face.pressed = true; + self.right_face.keydown_timing = + Some(if let Some(prev) = self.right_face.keydown_timing { + prev.max(janggu.궁채.keydown_timing) + } else { + janggu.궁채.keydown_timing + }) + } + if janggu.열채.face.is_some_and(|x| x == JangguFace::열편) { + self.right_face.pressed = true; + self.right_face.keydown_timing = + Some(if let Some(prev) = self.right_face.keydown_timing { + prev.max(janggu.열채.keydown_timing) + } else { + janggu.열채.keydown_timing + }) + } + } +} diff --git a/game/src/game/game_player/chart_player_ui/resources.rs b/game/src/game/game_player/chart_player_ui/resources.rs new file mode 100644 index 0000000..bd83314 --- /dev/null +++ b/game/src/game/game_player/chart_player_ui/resources.rs @@ -0,0 +1,66 @@ +use sdl2::{ + image::LoadTexture, + render::{Texture, TextureCreator}, + video::WindowContext, +}; + +pub struct NoteTextures<'a> { + pub left_stick: Texture<'a>, + pub right_stick: Texture<'a>, +} +pub struct AccuracyTextures<'a> { + pub overchaos: Texture<'a>, + pub perfect: Texture<'a>, + pub great: Texture<'a>, + pub good: Texture<'a>, + pub bad: Texture<'a>, + pub miss: Texture<'a>, +} +pub struct ChartPlayerUIResources<'a> { + pub judgement_line_texture: Texture<'a>, + pub note_textures: NoteTextures<'a>, + pub accuray_textures: AccuracyTextures<'a>, + pub janggu_texture: Texture<'a>, +} + +fn load_note_textures( + texture_creator: &TextureCreator, +) -> Result { + Ok(NoteTextures { + left_stick: texture_creator.load_texture("assets/img/note/left_stick.png")?, + right_stick: texture_creator.load_texture("assets/img/note/right_stick.png")?, + }) +} + +fn load_accuracy_textures( + texture_creator: &TextureCreator, +) -> Result { + Ok(AccuracyTextures { + overchaos: texture_creator.load_texture("assets/img/accuracy/overchaos.png")?, + perfect: texture_creator.load_texture("assets/img/accuracy/perfect.png")?, + great: texture_creator.load_texture("assets/img/accuracy/great.png")?, + good: texture_creator.load_texture("assets/img/accuracy/good.png")?, + bad: texture_creator.load_texture("assets/img/accuracy/bad.png")?, + miss: texture_creator.load_texture("assets/img/accuracy/miss.png")?, + }) +} + +impl ChartPlayerUIResources<'_> { + pub fn new(texture_creator: &TextureCreator) -> ChartPlayerUIResources { + let judgement_line_texture = texture_creator + .load_texture("assets/img/play_ui/note_guideline.png") + .expect("Failed to load note guideline image"); + let note_textures = load_note_textures(texture_creator).unwrap(); + let janggu_texture = texture_creator + .load_texture("assets/img/play_ui/janggu.png") + .expect("Failed to load janggu image"); + let accuray_textures = load_accuracy_textures(texture_creator).unwrap(); + + return ChartPlayerUIResources { + judgement_line_texture: judgement_line_texture, + note_textures: note_textures, + janggu_texture: janggu_texture, + accuray_textures: accuray_textures, + }; + } +} diff --git a/game/src/game/game_player/draw_gameplay_ui.rs b/game/src/game/game_player/draw_gameplay_ui.rs deleted file mode 100644 index b500742..0000000 --- a/game/src/game/game_player/draw_gameplay_ui.rs +++ /dev/null @@ -1,331 +0,0 @@ -use ezing::expo_out; -use num_rational::Rational32; -use sdl2::{ - image::LoadTexture, - pixels::Color, - rect::Rect, - render::{Canvas, Texture, TextureCreator}, - video::{Window, WindowContext}, -}; - -use bidrum_data_struct_lib::janggu::{JangguFace, JangguStick}; - -use super::timing_judge::{JudgeResult, NoteAccuracy}; - -struct NoteTextures<'a> { - left_stick: Texture<'a>, - right_stick: Texture<'a>, -} -struct AccuracyTextures<'a> { - overchaos: Texture<'a>, - perfect: Texture<'a>, - great: Texture<'a>, - good: Texture<'a>, - bad: Texture<'a>, - miss: Texture<'a>, -} -pub struct DisplayedSongNote { - pub(crate) distance: f64, - pub(crate) face: JangguFace, - pub(crate) stick: JangguStick, -} - -pub struct UIContent { - pub(crate) accuracy: Option, - pub(crate) accuracy_time_progress: Option, - pub(crate) input_effect: [Option; 2], -} - -fn load_note_textures( - texture_creator: &TextureCreator, -) -> Result { - Ok(NoteTextures { - left_stick: texture_creator.load_texture("assets/img/note/left_stick.png")?, - right_stick: texture_creator.load_texture("assets/img/note/right_stick.png")?, - }) -} - -fn load_accuracy_textures( - texture_creator: &TextureCreator, -) -> Result { - Ok(AccuracyTextures { - overchaos: texture_creator.load_texture("assets/img/accuracy/overchaos.png")?, - perfect: texture_creator.load_texture("assets/img/accuracy/perfect.png")?, - great: texture_creator.load_texture("assets/img/accuracy/great.png")?, - good: texture_creator.load_texture("assets/img/accuracy/good.png")?, - bad: texture_creator.load_texture("assets/img/accuracy/bad.png")?, - miss: texture_creator.load_texture("assets/img/accuracy/miss.png")?, - }) -} - -pub struct GamePlayUIResources<'a> { - judgement_line_texture: Texture<'a>, - note_textures: NoteTextures<'a>, - accuray_textures: AccuracyTextures<'a>, - janggu_texture: Texture<'a>, -} - -impl GamePlayUIResources<'_> { - pub fn new(texture_creator: &TextureCreator) -> GamePlayUIResources { - let judgement_line_texture = texture_creator - .load_texture("assets/img/play_ui/note_guideline.png") - .expect("Failed to load note guideline image"); - let note_textures = load_note_textures(texture_creator).unwrap(); - let janggu_texture = texture_creator - .load_texture("assets/img/play_ui/janggu.png") - .expect("Failed to load janggu image"); - let accuray_textures = load_accuracy_textures(texture_creator).unwrap(); - - return GamePlayUIResources { - judgement_line_texture: judgement_line_texture, - note_textures: note_textures, - janggu_texture: janggu_texture, - accuray_textures: accuray_textures, - }; - } -} - -/// renders game play ui with notes -pub fn draw_gameplay_ui( - canvas: &mut Canvas, - notes: Vec, - other: UIContent, - resources: &mut GamePlayUIResources, -) { - // loads texture of judgement line - let judgement_line_texture = &resources.judgement_line_texture; - - // enable alpha blending - canvas.set_blend_mode(sdl2::render::BlendMode::Blend); - - // load textures - let note_textures = &resources.note_textures; - let accuracy_textures = &mut resources.accuray_textures; - let janggu_texture = &resources.janggu_texture; - - // get note size - let right_stick_note_height = 90; - let right_stick_note_width = (note_textures.right_stick.query().width as f32 - / note_textures.right_stick.query().height as f32 - * right_stick_note_height as f32) as u32; - let left_stick_note_height = (note_textures.left_stick.query().height as f32 - * (right_stick_note_height as f32 / note_textures.right_stick.query().height as f32)) - as u32; - let left_stick_note_width = (note_textures.left_stick.query().width as f32 - / note_textures.left_stick.query().height as f32 - * left_stick_note_height as f32) as u32; - let max_stick_note_height = std::cmp::max(left_stick_note_height, right_stick_note_height); - - // calculate background height - let background_padding = 15; - let background_border_height = 5 as u32; - let background_height_without_border = max_stick_note_height + background_padding * 2; - let background_height_with_border = - background_height_without_border + background_border_height * 2; - - // calculate janggu width - let janggu_width = ((janggu_texture.query().width as f32 - / janggu_texture.query().height as f32) - * background_height_with_border as f32) as u32; - - // draw janggu on center - let viewport = canvas.viewport(); - canvas.copy( - &janggu_texture, - None, - Rect::new( - (viewport.width() - janggu_width) as i32 / 2, - (viewport.height() - background_height_with_border) as i32 / 2, - janggu_width, - background_height_with_border, - ), - ); - - // draw backgrounds - let background_width = (viewport.width() - janggu_width) / 2; - let background_y = - (canvas.viewport().height() as i32 - (background_height_without_border as i32)) / 2; - for background_x in [ - 0, /* x coordinate of left background */ - background_width as i32 + janggu_width as i32, /* x coordinate of right background */ - ] { - let background_alpha = if other.input_effect.iter().any(|i| { - i.is_some_and(|x| { - if background_x == 0 { - x == JangguFace::궁편 - } else { - x == JangguFace::열편 - } - }) - }) { - 200 - } else { - 150 - }; - canvas.set_draw_color(Color::RGBA(200, 200, 200, background_alpha)); - canvas - .fill_rect(Rect::new( - background_x, - background_y, - background_width, - background_height_without_border, - )) - .unwrap(); - - // draw border, too - canvas.set_draw_color(Color::RGBA(120, 120, 120, background_alpha)); - canvas - .fill_rect(Rect::new( - background_x, - background_y - background_border_height as i32, - background_width, - background_border_height, - )) - .unwrap(); - canvas - .fill_rect(Rect::new( - background_x, - background_y + background_height_without_border as i32, - background_width, - background_border_height, - )) - .unwrap(); - } - - // draw judgement line - let judgement_line_height = max_stick_note_height; - let judgement_line_padding_px = 20; - let judgement_line_width = ((judgement_line_texture.query().width as f32 - / judgement_line_texture.query().height as f32) - * max_stick_note_height as f32) as u32; - let judgeline_line_ypos = background_y + background_padding as i32; - let judgement_line_xposes = [ - background_width as i32 - judgement_line_width as i32 - judgement_line_padding_px, /* left judgement line */ - background_width as i32 + janggu_width as i32 + judgement_line_padding_px, /* right judgement line */ - ]; - for judgement_line_xpos in judgement_line_xposes { - canvas - .copy( - &judgement_line_texture, - None, - Rect::new( - judgement_line_xpos, - judgeline_line_ypos, - judgement_line_width, - judgement_line_height, - ), - ) - .unwrap(); - } - - // load textures for the notes and accuracy - let note_width_max = std::cmp::max(left_stick_note_width, right_stick_note_width); - - // draw note - let mut draw_note = |i: &DisplayedSongNote| { - let note_texture = match i.stick { - JangguStick::궁채 => ¬e_textures.left_stick, - JangguStick::열채 => ¬e_textures.right_stick, - }; - let note_width = match i.stick { - JangguStick::궁채 => left_stick_note_width, - JangguStick::열채 => right_stick_note_width, - }; - let note_height = match i.stick { - JangguStick::궁채 => left_stick_note_height, - JangguStick::열채 => right_stick_note_height, - }; - let note_ypos = background_y - + (background_height_without_border as i32 - note_height as i32) as i32 / 2; - - /* - * note_xpos judgement_line_xpos - * +--------- +---- - * - - distance_between_centers - - - * - *----------------------------------------------------* - - * - - - - - * ---------- ----- - */ - let distance_between_centers = i.distance * note_width_max as f64; - let note_xpos = (match i.face { - JangguFace::궁편 => judgement_line_xposes[0], - JangguFace::열편 => judgement_line_xposes[1], - } as f64 - + (judgement_line_width / 2) as f64 - + distance_between_centers - * match i.face { - JangguFace::궁편 => -1.0, - JangguFace::열편 => 1.0, - } - - (note_width / 2) as f64) as i32; - - // Do not render note if the note is on janggu icon - let near_center_edge_x_pos = match i.face { - JangguFace::궁편 => note_xpos + note_width as i32, - JangguFace::열편 => note_xpos, - }; - let distance_with_center = match i.face { - JangguFace::궁편 => (viewport.width() / 2) as i32 - near_center_edge_x_pos, - JangguFace::열편 => near_center_edge_x_pos - (viewport.width() / 2) as i32, - }; - if distance_with_center <= (janggu_width / 2) as i32 { - return; - } - - // draw note - canvas - .copy( - note_texture, - None, - Rect::new(note_xpos, note_ypos, note_width, note_height), - ) - .unwrap(); - }; - - // draw right-stick first, and then draw left-stick. - for i in ¬es { - if matches!(i.stick, JangguStick::열채) { - draw_note(i); - } - } - for i in ¬es { - if matches!(i.stick, JangguStick::궁채) { - draw_note(i); - } - } - - // draw note accuracy - if let Some(accuracy) = other.accuracy { - let accuracy_texture = match accuracy { - NoteAccuracy::Overchaos => &mut accuracy_textures.overchaos, - NoteAccuracy::Perfect => &mut accuracy_textures.perfect, - NoteAccuracy::Great => &mut accuracy_textures.great, - NoteAccuracy::Good => &mut accuracy_textures.good, - NoteAccuracy::Bad => &mut accuracy_textures.bad, - NoteAccuracy::Miss => &mut accuracy_textures.miss, - }; - - let width = 120; - let height = (Rational32::new( - accuracy_texture.query().height as i32 * width as i32, - accuracy_texture.query().width as i32, - )) - .to_integer(); - let x = (viewport.width() - width) as i32 / 2; - let y_start = (viewport.height() - background_height_with_border) as i32 / 2 - (height / 2); - let y_end = y_start - height as i32 - 10; - let y = y_start - + ((y_end - y_start) as f32 * expo_out(other.accuracy_time_progress.unwrap())) as i32; - - accuracy_texture - .set_alpha_mod((expo_out(other.accuracy_time_progress.unwrap()) * 255.0) as u8); - - canvas - .copy( - &accuracy_texture, - None, - Rect::new(x, y, width as u32, height as u32), - ) - .unwrap(); - } -} diff --git a/game/src/game/game_player/effect_sound_player.rs b/game/src/game/game_player/effect_sound_player.rs new file mode 100644 index 0000000..ab54a93 --- /dev/null +++ b/game/src/game/game_player/effect_sound_player.rs @@ -0,0 +1,88 @@ +use kira::{ + manager::AudioManager, + sound::{ + static_sound::{StaticSoundData, StaticSoundHandle, StaticSoundSettings}, + PlaybackState, + }, +}; + +use super::janggu_state_with_tick::JangguStateWithTick; + +fn load_hit_sounds() -> [StaticSoundData; 2] { + [ + StaticSoundData::from_file( + "assets/sound/janggu_hit/kung.wav", + StaticSoundSettings::default(), + ) + .expect("Failed to load kung sound"), + StaticSoundData::from_file( + "assets/sound/janggu_hit/deok.wav", + StaticSoundSettings::default(), + ) + .expect("Failed to load deok sound"), + ] +} + +struct EffectSoundHandles { + left_stick: Option, + right_stick: Option, +} + +impl EffectSoundHandles { + pub fn new() -> EffectSoundHandles { + EffectSoundHandles { + left_stick: None, + right_stick: None, + } + } +} +pub struct EffectSoundPlayer { + hit_sounds: [StaticSoundData; 2], + effect_sound_play_handles: EffectSoundHandles, +} + +impl EffectSoundPlayer { + pub fn new() -> EffectSoundPlayer { + EffectSoundPlayer { + effect_sound_play_handles: EffectSoundHandles::new(), + hit_sounds: load_hit_sounds(), + } + } + + pub fn play_janggu_sound( + &mut self, + janggu_state_with_tick: &JangguStateWithTick, + audio_manager: &mut AudioManager, + ) { + let kung_sound_data = self.hit_sounds[0].clone(); + let deok_sound_data = self.hit_sounds[1].clone(); + + if janggu_state_with_tick.궁채.is_keydown_now { + let play_sound = if let Some(handle) = &mut self.effect_sound_play_handles.left_stick { + !matches!(handle.state(), PlaybackState::Playing) || handle.position() > 0.01 + } else { + true + }; + if play_sound { + let new_handle = audio_manager + .play(kung_sound_data.clone()) + .expect("Failed to play kung sound"); + self.effect_sound_play_handles.left_stick = Some(new_handle); + } + } + if janggu_state_with_tick.열채.is_keydown_now { + let play_sound = if let Some(handle) = &mut self.effect_sound_play_handles.right_stick { + !matches!(handle.state(), PlaybackState::Playing) || handle.position() > 0.01 + } else { + true + }; + + if play_sound { + let new_handle = audio_manager + .play(deok_sound_data.clone()) + .expect("Failed to play deok sound"); + self.effect_sound_play_handles.right_stick = Some(new_handle); + } + } + } +} diff --git a/game/src/game/game_player/judge_and_display_notes.rs b/game/src/game/game_player/judge_and_display_notes.rs deleted file mode 100644 index d2d5d9a..0000000 --- a/game/src/game/game_player/judge_and_display_notes.rs +++ /dev/null @@ -1,166 +0,0 @@ -use std::time::Duration; - -use bidrum_data_struct_lib::{ - janggu::JangguFace, - song::{GameChart, GameSong}, -}; -use kira::{ - sound::{ - static_sound::{StaticSoundData, StaticSoundHandle}, - PlaybackState, - }, - tween::Tween, - StartTime, -}; - -use crate::game::{ - game_common_context, - game_player::{ - draw_gameplay_ui::{DisplayedSongNote, UIContent}, - is_input_effect_needed, - timing_judge::NoteAccuracy, - }, -}; - -use super::{ - draw_gameplay_ui::{self, GamePlayUIResources}, - janggu_state_with_tick::JangguStateWithTick, - timing_judge::TimingJudge, -}; - -pub struct EffectSoundHandles { - left_stick: Option, - right_stick: Option, -} - -impl EffectSoundHandles { - pub fn new() -> EffectSoundHandles { - EffectSoundHandles { - left_stick: None, - right_stick: None, - } - } -} - -pub(crate) fn display_notes_and_judge( - common_context: &mut game_common_context::GameCommonContext, - chart: &GameChart, - timing_judge: &mut TimingJudge, - janggu_state_with_tick: &JangguStateWithTick, - gameplay_ui_resources: &mut GamePlayUIResources, - processed_note_ids: &mut Vec, - accuracy: &mut Option, - accuracy_tick: &mut Option, - hit_sounds: &[StaticSoundData; 2], - effect_sound_handles: &mut EffectSoundHandles, - tick_now: i128, -) { - let kung_sound_data = hit_sounds[0].clone(); - let deok_sound_data = hit_sounds[1].clone(); - - // display notes and accuracy - let mut display_notes = Vec::::new(); - if tick_now >= 0 { - // get positions of the notes - for i in &chart.left_face { - if !processed_note_ids.contains(&i.id) { - display_notes.push(DisplayedSongNote { - face: JangguFace::궁편, - stick: i.stick, - distance: i.get_position( - chart.bpm, - chart.delay, - chart.bpm * 2, - (tick_now) as u64, - ), - }); - } - } - - for i in &chart.right_face { - if !processed_note_ids.contains(&i.id) { - display_notes.push(DisplayedSongNote { - face: JangguFace::열편, - stick: i.stick, - distance: i.get_position( - chart.bpm, - chart.delay, - chart.bpm * 2, - (tick_now) as u64, - ), - }); - } - } - - // make judgement - let new_accuracies = timing_judge.judge(&janggu_state_with_tick, tick_now as u64); - - // play hit sound when use git janggu - if janggu_state_with_tick.궁채.is_keydown_now { - let play_sound = if let Some(handle) = &mut effect_sound_handles.left_stick { - !matches!(handle.state(), PlaybackState::Playing) || handle.position() > 0.01 - } else { - true - }; - if play_sound { - let new_handle = common_context - .audio_manager - .play(kung_sound_data.clone()) - .expect("Failed to play kung sound"); - effect_sound_handles.left_stick = Some(new_handle); - } - } - if janggu_state_with_tick.열채.is_keydown_now { - let play_sound = if let Some(handle) = &mut effect_sound_handles.right_stick { - !matches!(handle.state(), PlaybackState::Playing) || handle.position() > 0.01 - } else { - true - }; - - if play_sound { - let new_handle = common_context - .audio_manager - .play(deok_sound_data.clone()) - .expect("Failed to play deok sound"); - effect_sound_handles.right_stick = Some(new_handle); - } - } - - // if any judgement is made, display it - if !new_accuracies.is_empty() { - *accuracy_tick = Some(tick_now); - *accuracy = new_accuracies.iter().map(|x| x.accuracy).max(); - for i in new_accuracies { - processed_note_ids.push(i.note_id); - } - } - } - - // judgement is visible for only 800 ms - const ACCURACY_DISPLAY_DURATION: u32 = 800; - if let Some(accuracy_tick_unwrapped) = *accuracy_tick { - if tick_now - accuracy_tick_unwrapped > ACCURACY_DISPLAY_DURATION as i128 { - *accuracy_tick = None; - } - } - - // draw game play ui - draw_gameplay_ui::draw_gameplay_ui( - &mut common_context.canvas, - display_notes, - UIContent { - accuracy: if let Some(_) = accuracy_tick { - *accuracy - } else { - None - }, - accuracy_time_progress: if let Some(accuracy_time_unwrapped) = *accuracy_tick { - Some((tick_now - accuracy_time_unwrapped) as f32 / ACCURACY_DISPLAY_DURATION as f32) - } else { - None - }, - input_effect: is_input_effect_needed(&janggu_state_with_tick, tick_now), - }, - gameplay_ui_resources, - ); -} diff --git a/game/src/game/game_player/load_hit_sounds.rs b/game/src/game/game_player/load_hit_sounds.rs deleted file mode 100644 index 61c606d..0000000 --- a/game/src/game/game_player/load_hit_sounds.rs +++ /dev/null @@ -1,16 +0,0 @@ -use kira::sound::static_sound::{StaticSoundData, StaticSoundSettings}; - -pub fn load_hit_sounds() -> [StaticSoundData; 2] { - [ - StaticSoundData::from_file( - "assets/sound/janggu_hit/kung.wav", - StaticSoundSettings::default(), - ) - .expect("Failed to load kung sound"), - StaticSoundData::from_file( - "assets/sound/janggu_hit/deok.wav", - StaticSoundSettings::default(), - ) - .expect("Failed to load deok sound"), - ] -} diff --git a/game/src/game/game_player/timing_judge.rs b/game/src/game/game_player/timing_judge.rs index 76b0012..7ea0ad6 100644 --- a/game/src/game/game_player/timing_judge.rs +++ b/game/src/game/game_player/timing_judge.rs @@ -52,6 +52,7 @@ pub(crate) struct TimingJudge { max_health: u64, } +#[derive(Clone)] pub(crate) struct JudgeResult { pub accuracy: NoteAccuracy, pub note_id: u64, diff --git a/game/src/game/tutorial.rs b/game/src/game/tutorial.rs index e53f7e2..0f59c83 100644 --- a/game/src/game/tutorial.rs +++ b/game/src/game/tutorial.rs @@ -20,9 +20,7 @@ use super::{ common::{event_loop_common, render_common}, game_common_context::GameCommonContext, game_player::{ - self, - draw_gameplay_ui::{self, GamePlayUIResources, UIContent}, - is_input_effect_needed, + chart_player_ui::ChartPlayerUI, janggu_state_with_tick::{self, JangguStateWithTick}, }, render_video::VideoFileRenderer, @@ -254,9 +252,6 @@ fn ask_for_tutorial(common_context: &mut GameCommonContext) -> bool { } fn do_tutorial(common_context: &mut GameCommonContext) { - let texture_creator = common_context.canvas.texture_creator(); - let mut gameplay_ui_resources = - game_player::draw_gameplay_ui::GamePlayUIResources::new(&texture_creator); let mut janggu_state = janggu_state_with_tick::JangguStateWithTick::new(); let started = std::time::Instant::now(); @@ -270,36 +265,25 @@ fn do_tutorial(common_context: &mut GameCommonContext) { let mut janggu_state_and_start_time = (&mut janggu_state, started); - do_tutorial_greetings( - common_context, - &mut gameplay_ui_resources, - &mut janggu_state_and_start_time, - ); + do_tutorial_greetings(common_context, &mut janggu_state_and_start_time); do_learn_stick_note( common_context, - &mut gameplay_ui_resources, &mut janggu_state_and_start_time, JangguStick::궁채, ); do_learn_stick_note( common_context, - &mut gameplay_ui_resources, &mut janggu_state_and_start_time, JangguStick::열채, ); - do_tutorial_ending( - common_context, - &mut gameplay_ui_resources, - &mut janggu_state_and_start_time, - ); + do_tutorial_ending(common_context, &mut janggu_state_and_start_time); } pub(self) fn display_tutorial_messages( common_context: &mut GameCommonContext, - game_ui_resources: &mut GamePlayUIResources, messages: &[(Texture, StaticSoundData)], janggu_state_and_tutorial_start_time: &mut (&mut JangguStateWithTick, Instant), ) { @@ -308,6 +292,8 @@ pub(self) fn display_tutorial_messages( let mut message_index = 0; let mut message_started_at = started_at.clone(); let message_gap = std::time::Duration::from_secs(1); + let texture_creator = common_context.canvas.texture_creator(); + let mut chart_player_ui = ChartPlayerUI::new(&texture_creator); loop { let tick = janggu_state_and_tutorial_start_time.1.elapsed().as_millis() as i128; for event in common_context.event_pump.poll_iter() { @@ -319,16 +305,7 @@ pub(self) fn display_tutorial_messages( .update(common_context.read_janggu_state(), tick); common_context.canvas.clear(); - draw_gameplay_ui::draw_gameplay_ui( - &mut common_context.canvas, - vec![], - UIContent { - accuracy: None, - accuracy_time_progress: None, - input_effect: is_input_effect_needed(janggu_state_and_tutorial_start_time.0, tick), - }, - game_ui_resources, - ); + chart_player_ui.draw(&mut common_context.canvas); if message_index >= messages.len() { return; diff --git a/game/src/game/tutorial/ending.rs b/game/src/game/tutorial/ending.rs index 0456312..5181e43 100644 --- a/game/src/game/tutorial/ending.rs +++ b/game/src/game/tutorial/ending.rs @@ -3,16 +3,12 @@ use std::{path, time::Instant}; use kira::sound::static_sound::StaticSoundSettings; use sdl2::image::LoadTexture; -use crate::game::{ - game_common_context::GameCommonContext, - game_player::{draw_gameplay_ui::GamePlayUIResources, janggu_state_with_tick}, -}; +use crate::game::{game_common_context::GameCommonContext, game_player::janggu_state_with_tick}; use super::display_tutorial_messages; pub(crate) fn do_tutorial_ending( common_context: &mut GameCommonContext, - game_ui_resources: &mut GamePlayUIResources, janggu_state_and_tutorial_start_time: &mut ( &mut janggu_state_with_tick::JangguStateWithTick, Instant, @@ -33,7 +29,6 @@ pub(crate) fn do_tutorial_ending( display_tutorial_messages( common_context, - game_ui_resources, &[message], janggu_state_and_tutorial_start_time, ); diff --git a/game/src/game/tutorial/greetings.rs b/game/src/game/tutorial/greetings.rs index 3fbc164..bd88fcf 100644 --- a/game/src/game/tutorial/greetings.rs +++ b/game/src/game/tutorial/greetings.rs @@ -9,17 +9,13 @@ use sdl2::{image::LoadTexture, render::Texture}; use crate::game::{ common::{event_loop_common, render_common}, game_common_context::GameCommonContext, - game_player::{ - draw_gameplay_ui::{self, GamePlayUIResources, UIContent}, - is_input_effect_needed, janggu_state_with_tick, - }, + game_player::{chart_player_ui::ChartPlayerUI, janggu_state_with_tick}, }; use super::display_tutorial_messages; pub(crate) fn do_tutorial_greetings( common_context: &mut GameCommonContext, - game_ui_resources: &mut GamePlayUIResources, janggu_state_and_tutorial_start_time: &mut ( &mut janggu_state_with_tick::JangguStateWithTick, Instant, @@ -42,6 +38,7 @@ pub(crate) fn do_tutorial_greetings( let started_at = std::time::Instant::now(); let message_start_delay = Duration::from_secs(1); + let mut chart_player_ui = ChartPlayerUI::new(&texture_creator); loop { if started_at.elapsed() > message_start_delay { break; @@ -56,24 +53,15 @@ pub(crate) fn do_tutorial_greetings( .update(common_context.read_janggu_state(), tick); common_context.canvas.clear(); - draw_gameplay_ui::draw_gameplay_ui( - &mut common_context.canvas, - vec![], - UIContent { - accuracy: None, - accuracy_time_progress: None, - input_effect: is_input_effect_needed(janggu_state_and_tutorial_start_time.0, tick), - }, - game_ui_resources, - ); - // First message is not started + chart_player_ui.draw(&mut common_context.canvas); + + render_common(common_context); common_context.canvas.present(); continue; } display_tutorial_messages( common_context, - game_ui_resources, &messages, janggu_state_and_tutorial_start_time, ) diff --git a/game/src/game/tutorial/learn_stick_note.rs b/game/src/game/tutorial/learn_stick_note.rs index b091cc5..c48fcc0 100644 --- a/game/src/game/tutorial/learn_stick_note.rs +++ b/game/src/game/tutorial/learn_stick_note.rs @@ -1,36 +1,31 @@ use std::{ ops::Sub, - path::{self, Path}, + path::{self}, time::{Duration, Instant}, }; use bidrum_data_struct_lib::{ - janggu::{self, JangguFace, JangguStick}, - song::{GameChart, GameNote}, + janggu::{JangguFace, JangguStick}, + song::GameChart, }; -use ffmpeg_next::subtitle::Text; use kira::sound::static_sound::{StaticSoundData, StaticSoundSettings}; use sdl2::{image::LoadTexture, rect::Rect, render::Texture}; use crate::game::{ - common::{self, event_loop_common, render_common}, + common::event_loop_common, game_common_context::GameCommonContext, game_player::{ - draw_gameplay_ui::{self, DisplayedSongNote, GamePlayUIResources, UIContent}, - is_input_effect_needed, + chart_player::ChartPlayer, + chart_player_ui::{displayed_song_note::DisplayedSongNote, ChartPlayerUI}, + effect_sound_player::EffectSoundPlayer, janggu_state_with_tick::JangguStateWithTick, - judge_and_display_notes::{display_notes_and_judge, EffectSoundHandles}, - load_hit_sounds::load_hit_sounds, - timing_judge, }, - start, }; use super::{display_tutorial_messages, get_message_image_asset_dst_rect}; fn display_animated_example_note( common_context: &mut GameCommonContext, - game_ui_resources: &mut GamePlayUIResources, janggu_state_and_tutorial_start_time: &mut (&mut JangguStateWithTick, Instant), message: &(Texture, StaticSoundData), stick: JangguStick, @@ -75,6 +70,8 @@ fn display_animated_example_note( common_context.audio_manager.play(message.1.clone()); let voice_started_at = Instant::now(); + let mut chart_player_ui = ChartPlayerUI::new(&texture_creator); + loop { for event in common_context.event_pump.poll_iter() { event_loop_common(&event, &mut common_context.coins); @@ -117,25 +114,23 @@ fn display_animated_example_note( / note_duration.as_millis() as f64)); }) .filter(|i| *i >= 0.0); - draw_gameplay_ui::draw_gameplay_ui( - &mut common_context.canvas, - note_positions - .clone() - .map(|position| -> DisplayedSongNote { - return DisplayedSongNote { - distance: position, - face: pane, - stick: stick, - }; - }) - .collect(), - UIContent { - accuracy: None, - accuracy_time_progress: None, - input_effect: is_input_effect_needed(janggu_state_and_tutorial_start_time.0, tick), - }, - game_ui_resources, - ); + + chart_player_ui.notes = note_positions + .clone() + .map(|position| -> DisplayedSongNote { + return DisplayedSongNote { + distance: position, + face: pane, + stick: stick, + }; + }) + .collect(); + chart_player_ui + .input_effect + .update(janggu_state_and_tutorial_start_time.0, tick); + chart_player_ui.overall_effect_tick = + common_context.game_initialized_at.elapsed().as_millis(); + chart_player_ui.draw(&mut common_context.canvas); // Display janggu animation let frame_index = if let Some(min_note_position) = @@ -160,15 +155,13 @@ fn display_animated_example_note( fn display_tryitout_notes( common_context: &mut GameCommonContext, - game_ui_resources: &mut GamePlayUIResources, stick: JangguStick, pane: JangguFace, ) { let texture_creator = common_context.canvas.texture_creator(); - // Load hit sounds - let hit_sounds = load_hit_sounds(); - let mut hit_sound_handles = EffectSoundHandles::new(); + // Load effect sounds + let mut effect_sound_player = EffectSoundPlayer::new(); // Load janggu-hitting instruction animation frames let animation_frames = [1, 2, 3, 4, 5, 6].map(|idx| -> Texture { @@ -212,12 +205,10 @@ fn display_tryitout_notes( GameChart::create_example_chart_for_tutorial(stick, pane, note_count, note_gap, chart_bpm); // Prepare tutorial play - let mut processed_notes = vec![]; - let mut judge = timing_judge::TimingJudge::new(&chart); let mut judged_all_at = None; let tryitout_tutorial_started_at = Instant::now(); - let mut accuracy = None; - let mut accuracy_tick = None; + + let mut chart_player = ChartPlayer::new(chart.clone(), &texture_creator); let mut janggu_state = JangguStateWithTick::new(); janggu_state.update(common_context.read_janggu_state(), 0); @@ -228,7 +219,7 @@ fn display_tryitout_notes( } // If tutorial ends, return - if judge.get_game_result().total_judged_note_count() == note_count + if chart_player.game_result().total_judged_note_count() == note_count && judged_all_at.is_none() { judged_all_at = Some(Instant::now()) @@ -243,19 +234,16 @@ fn display_tryitout_notes( // Clear canvas common_context.canvas.clear(); - // Display UI - display_notes_and_judge( - common_context, - &chart, - &mut judge, - &janggu_state, - game_ui_resources, - &mut processed_notes, - &mut accuracy, - &mut accuracy_tick, - &hit_sounds, - &mut hit_sound_handles, + // Play effect sound + effect_sound_player.play_janggu_sound(&janggu_state, &mut common_context.audio_manager); + + // Judge and display UI + chart_player.judge(&janggu_state, tick.into()); + chart_player.draw( tick.into(), + &mut common_context.canvas, + common_context.game_initialized_at.elapsed().as_millis(), + &janggu_state, ); // Display janggu animation @@ -288,7 +276,6 @@ fn display_tryitout_notes( pub(crate) fn do_learn_stick_note( common_context: &mut GameCommonContext, - game_ui_resources: &mut GamePlayUIResources, janggu_state_and_tutorial_start_time: &mut (&mut JangguStateWithTick, Instant), stick: JangguStick, ) { @@ -320,14 +307,12 @@ pub(crate) fn do_learn_stick_note( // Display two messages, Telling how the note looks like, at first display_tutorial_messages( common_context, - game_ui_resources, &messages[..2], janggu_state_and_tutorial_start_time, ); display_animated_example_note( common_context, - game_ui_resources, janggu_state_and_tutorial_start_time, &messages[2], stick, @@ -339,14 +324,12 @@ pub(crate) fn do_learn_stick_note( display_tutorial_messages( common_context, - game_ui_resources, &messages[3..4], janggu_state_and_tutorial_start_time, ); display_tryitout_notes( common_context, - game_ui_resources, stick, match stick { JangguStick::궁채 => JangguFace::궁편, @@ -356,7 +339,6 @@ pub(crate) fn do_learn_stick_note( display_animated_example_note( common_context, - game_ui_resources, janggu_state_and_tutorial_start_time, &messages[4], stick, @@ -368,14 +350,12 @@ pub(crate) fn do_learn_stick_note( display_tutorial_messages( common_context, - game_ui_resources, &messages[5..6], janggu_state_and_tutorial_start_time, ); display_tryitout_notes( common_context, - game_ui_resources, stick, match stick { JangguStick::궁채 => JangguFace::열편,