Minor refactoring
This commit is contained in:
parent
205f78fd84
commit
09f42d2cb4
8 changed files with 371 additions and 311 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
/songs
|
||||||
|
|
|
@ -26,6 +26,15 @@ impl EventHandler for Game {
|
||||||
if self.imgui.want_capture_mouse() {
|
if self.imgui.want_capture_mouse() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.seeker_drag {
|
||||||
|
use super::seeker::BOUNDS;
|
||||||
|
let jump_percent = (x - BOUNDS.x) / BOUNDS.w;
|
||||||
|
if let Some(song) = &self.song {
|
||||||
|
let pos = jump_percent as f64 * song.length().unwrap();
|
||||||
|
song.set_position(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +56,7 @@ impl EventHandler for Game {
|
||||||
MouseButton::Left => {
|
MouseButton::Left => {
|
||||||
use super::seeker::BOUNDS;
|
use super::seeker::BOUNDS;
|
||||||
if rect_contains(&BOUNDS, x, y) {
|
if rect_contains(&BOUNDS, x, y) {
|
||||||
|
self.seeker_drag = true;
|
||||||
let jump_percent = (x - BOUNDS.x) / BOUNDS.w;
|
let jump_percent = (x - BOUNDS.x) / BOUNDS.w;
|
||||||
if let Some(song) = &self.song {
|
if let Some(song) = &self.song {
|
||||||
let pos = jump_percent as f64 * song.length().unwrap();
|
let pos = jump_percent as f64 * song.length().unwrap();
|
||||||
|
@ -68,7 +78,16 @@ impl EventHandler for Game {
|
||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
) -> GameResult {
|
) -> GameResult {
|
||||||
|
// let go of the seeker
|
||||||
|
if self.seeker_drag {
|
||||||
|
self.seeker_drag = false;
|
||||||
|
}
|
||||||
|
|
||||||
self.imgui.update_mouse_up(btn);
|
self.imgui.update_mouse_up(btn);
|
||||||
|
if self.imgui.want_capture_mouse() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
match btn {
|
match btn {
|
||||||
MouseButton::Left => {
|
MouseButton::Left => {
|
||||||
if let Some((px, py)) = self.left_drag_start {
|
if let Some((px, py)) = self.left_drag_start {
|
||||||
|
|
205
src/game/hitobjects.rs
Normal file
205
src/game/hitobjects.rs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use ggez::{
|
||||||
|
graphics::{Color, DrawParam},
|
||||||
|
Context,
|
||||||
|
};
|
||||||
|
use libosu::prelude::*;
|
||||||
|
|
||||||
|
use crate::{beatmap::STACK_DISTANCE, hitobject::HitObjectExt};
|
||||||
|
|
||||||
|
use super::{Game, PLAYFIELD_BOUNDS};
|
||||||
|
|
||||||
|
pub struct DrawInfo<'a> {
|
||||||
|
hit_object: &'a HitObjectExt,
|
||||||
|
fade_opacity: f64,
|
||||||
|
end_time: f64,
|
||||||
|
color: Color,
|
||||||
|
/// Whether or not the circle (slider head for sliders) should appear to have already
|
||||||
|
/// been hit in the editor after the object's time has already come.
|
||||||
|
circle_is_hit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Game {
|
||||||
|
pub(super) fn draw_hitobjects(&mut self, ctx: &mut Context, current_time: f64) -> Result<()> {
|
||||||
|
// figure out what objects will be visible on the screen at the current instant
|
||||||
|
// 1.5 cus editor so people can see objects for longer durations
|
||||||
|
let mut playfield_hitobjects = Vec::new();
|
||||||
|
let preempt = 1.5
|
||||||
|
* self
|
||||||
|
.beatmap
|
||||||
|
.inner
|
||||||
|
.difficulty
|
||||||
|
.approach_preempt()
|
||||||
|
.as_seconds();
|
||||||
|
let fade_in = 1.5
|
||||||
|
* self
|
||||||
|
.beatmap
|
||||||
|
.inner
|
||||||
|
.difficulty
|
||||||
|
.approach_fade_time()
|
||||||
|
.as_seconds();
|
||||||
|
let fade_out_time = 0.75; // TODO: figure out what this number actually is
|
||||||
|
let fade_out_offset = 0.0; // TODO: figure out what this number actually is
|
||||||
|
|
||||||
|
// TODO: tighten this loop even more by binary searching for the start of the timeline and
|
||||||
|
// playfield hitobjects rather than looping through the entire beatmap, better yet, just
|
||||||
|
// keeping track of the old index will probably be much faster
|
||||||
|
for ho in self.beatmap.hit_objects.iter().rev() {
|
||||||
|
let ho_time = ho.inner.start_time.as_seconds();
|
||||||
|
let color = self.combo_colors[ho.color_idx];
|
||||||
|
|
||||||
|
// draw in timeline
|
||||||
|
self.draw_hitobject_to_timeline(ctx, current_time, ho)?;
|
||||||
|
|
||||||
|
// draw hitobject in playfield
|
||||||
|
let end_time;
|
||||||
|
match ho.inner.kind {
|
||||||
|
HitObjectKind::Circle => end_time = ho_time,
|
||||||
|
HitObjectKind::Slider(_) => {
|
||||||
|
let duration = self.beatmap.inner.get_slider_duration(&ho.inner).unwrap();
|
||||||
|
end_time = ho_time + duration;
|
||||||
|
}
|
||||||
|
HitObjectKind::Spinner(SpinnerInfo {
|
||||||
|
end_time: spinner_end,
|
||||||
|
}) => end_time = spinner_end.as_seconds(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let fade_opacity = if current_time <= ho_time - fade_in {
|
||||||
|
// before the hitobject's time arrives, it fades in
|
||||||
|
// TODO: calculate ease
|
||||||
|
(current_time - (ho_time - preempt)) / fade_in
|
||||||
|
} else if current_time < ho_time + fade_out_time {
|
||||||
|
// while the object is on screen the opacity should be 1
|
||||||
|
1.0
|
||||||
|
} else if current_time < end_time + fade_out_offset {
|
||||||
|
// after the hitobject's time, it fades out
|
||||||
|
// TODO: calculate ease
|
||||||
|
((end_time + fade_out_offset) - current_time) / fade_out_time
|
||||||
|
} else {
|
||||||
|
// completely faded out
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let circle_is_hit = current_time > ho_time;
|
||||||
|
|
||||||
|
if ho_time - preempt <= current_time && current_time <= end_time + fade_out_offset {
|
||||||
|
playfield_hitobjects.push(DrawInfo {
|
||||||
|
hit_object: ho,
|
||||||
|
fade_opacity,
|
||||||
|
end_time,
|
||||||
|
color,
|
||||||
|
circle_is_hit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cs_scale = PLAYFIELD_BOUNDS.w / 640.0;
|
||||||
|
let osupx_scale_x = PLAYFIELD_BOUNDS.w / 512.0;
|
||||||
|
let osupx_scale_y = PLAYFIELD_BOUNDS.h / 384.0;
|
||||||
|
let cs_osupx = self.beatmap.inner.difficulty.circle_size_osupx();
|
||||||
|
let cs_real = cs_osupx * cs_scale;
|
||||||
|
|
||||||
|
for draw_info in playfield_hitobjects.iter() {
|
||||||
|
let ho = draw_info.hit_object;
|
||||||
|
let ho_time = ho.inner.start_time.as_seconds();
|
||||||
|
let stacking = ho.stacking as f32 * STACK_DISTANCE as f32;
|
||||||
|
let pos = [
|
||||||
|
PLAYFIELD_BOUNDS.x + osupx_scale_x * (ho.inner.pos.x as f32 - stacking),
|
||||||
|
PLAYFIELD_BOUNDS.y + osupx_scale_y * (ho.inner.pos.y as f32 - stacking),
|
||||||
|
];
|
||||||
|
let mut color = draw_info.color;
|
||||||
|
color.a = 0.6 * draw_info.fade_opacity as f32;
|
||||||
|
|
||||||
|
let mut slider_info = None;
|
||||||
|
if let HitObjectKind::Slider(info) = &ho.inner.kind {
|
||||||
|
let mut control_points = vec![ho.inner.pos];
|
||||||
|
control_points.extend(&info.control_points);
|
||||||
|
|
||||||
|
Game::render_slider_body(
|
||||||
|
&mut self.slider_cache,
|
||||||
|
info,
|
||||||
|
control_points.as_ref(),
|
||||||
|
ctx,
|
||||||
|
PLAYFIELD_BOUNDS,
|
||||||
|
&self.beatmap.inner,
|
||||||
|
color,
|
||||||
|
)?;
|
||||||
|
slider_info = Some((info, control_points));
|
||||||
|
|
||||||
|
let end_pos = ho.inner.end_pos();
|
||||||
|
let end_pos = [
|
||||||
|
PLAYFIELD_BOUNDS.x + osupx_scale_x * end_pos.x as f32,
|
||||||
|
PLAYFIELD_BOUNDS.y + osupx_scale_y * end_pos.y as f32,
|
||||||
|
];
|
||||||
|
self.skin.hitcircle.draw(
|
||||||
|
ctx,
|
||||||
|
(cs_real * 2.0, cs_real * 2.0),
|
||||||
|
DrawParam::default().dest(end_pos).color(color),
|
||||||
|
)?;
|
||||||
|
self.skin.hitcircleoverlay.draw(
|
||||||
|
ctx,
|
||||||
|
(cs_real * 2.0, cs_real * 2.0),
|
||||||
|
DrawParam::default().dest(end_pos),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw main hitcircle
|
||||||
|
let faded_color = Color::new(1.0, 1.0, 1.0, 0.6 * draw_info.fade_opacity as f32);
|
||||||
|
self.skin.hitcircle.draw(
|
||||||
|
ctx,
|
||||||
|
(cs_real * 2.0, cs_real * 2.0),
|
||||||
|
DrawParam::default().dest(pos).color(color),
|
||||||
|
)?;
|
||||||
|
self.skin.hitcircleoverlay.draw(
|
||||||
|
ctx,
|
||||||
|
(cs_real * 2.0, cs_real * 2.0),
|
||||||
|
DrawParam::default().dest(pos).color(faded_color),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// draw numbers
|
||||||
|
self.draw_numbers_on_circle(ctx, ho.number, pos, cs_real, faded_color)?;
|
||||||
|
|
||||||
|
if let Some((info, control_points)) = slider_info {
|
||||||
|
let spline = self.slider_cache.get(&control_points).unwrap();
|
||||||
|
Game::render_slider_wireframe(ctx, &control_points, PLAYFIELD_BOUNDS, faded_color)?;
|
||||||
|
|
||||||
|
if current_time > ho_time && current_time < draw_info.end_time {
|
||||||
|
let elapsed_time = current_time - ho_time;
|
||||||
|
let total_duration = draw_info.end_time - ho_time;
|
||||||
|
let single_duration = total_duration / info.num_repeats as f64;
|
||||||
|
let finished_repeats = (elapsed_time / single_duration).floor();
|
||||||
|
let this_repeat_time = elapsed_time - finished_repeats * single_duration;
|
||||||
|
let mut travel_percent = this_repeat_time / single_duration;
|
||||||
|
|
||||||
|
// reverse direction on odd trips
|
||||||
|
if finished_repeats as u32 % 2 == 1 {
|
||||||
|
travel_percent = 1.0 - travel_percent;
|
||||||
|
}
|
||||||
|
let travel_length = travel_percent * info.pixel_length;
|
||||||
|
let pos = spline.point_at_length(travel_length);
|
||||||
|
let ball_pos = [
|
||||||
|
PLAYFIELD_BOUNDS.x + osupx_scale_x * pos.x as f32,
|
||||||
|
PLAYFIELD_BOUNDS.y + osupx_scale_y * pos.y as f32,
|
||||||
|
];
|
||||||
|
self.skin.sliderb.draw_frame(
|
||||||
|
ctx,
|
||||||
|
(cs_real * 1.8, cs_real * 1.8),
|
||||||
|
DrawParam::default().dest(ball_pos).color(color),
|
||||||
|
(travel_percent / 0.25) as usize,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_time < ho_time {
|
||||||
|
let time_diff = ho_time - current_time;
|
||||||
|
let approach_r = cs_real * (1.0 + 2.0 * time_diff as f32 / 0.75);
|
||||||
|
self.skin.approachcircle.draw(
|
||||||
|
ctx,
|
||||||
|
(approach_r * 2.0, approach_r * 2.0),
|
||||||
|
DrawParam::default().dest(pos).color(color),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
257
src/game/mod.rs
257
src/game/mod.rs
|
@ -1,10 +1,12 @@
|
||||||
mod background;
|
mod background;
|
||||||
mod events;
|
mod events;
|
||||||
mod grid;
|
mod grid;
|
||||||
|
mod hitobjects;
|
||||||
mod numbers;
|
mod numbers;
|
||||||
mod seeker;
|
mod seeker;
|
||||||
mod sliders;
|
mod sliders;
|
||||||
mod timeline;
|
mod timeline;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
@ -22,23 +24,23 @@ use ggez::{
|
||||||
Context,
|
Context,
|
||||||
};
|
};
|
||||||
use image::io::Reader as ImageReader;
|
use image::io::Reader as ImageReader;
|
||||||
use imgui::{Window, MenuItem};
|
|
||||||
use imgui_winit_support::WinitPlatform;
|
|
||||||
use libosu::{
|
use libosu::{
|
||||||
beatmap::Beatmap,
|
beatmap::Beatmap,
|
||||||
hitobject::{HitObjectKind, SliderSplineKind, SpinnerInfo},
|
hitobject::{HitObjectKind, SliderSplineKind},
|
||||||
math::Point,
|
math::Point,
|
||||||
spline::Spline,
|
spline::Spline,
|
||||||
timing::{Millis, TimingPoint, TimingPointKind},
|
timing::{Millis, TimingPoint, TimingPointKind},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::audio::{AudioEngine, Sound};
|
use crate::audio::{AudioEngine, Sound};
|
||||||
use crate::beatmap::{BeatmapExt, STACK_DISTANCE};
|
use crate::beatmap::BeatmapExt;
|
||||||
use crate::hitobject::HitObjectExt;
|
use crate::hitobject::HitObjectExt;
|
||||||
use crate::imgui_wrapper::ImGuiWrapper;
|
use crate::imgui_wrapper::ImGuiWrapper;
|
||||||
use crate::skin::Skin;
|
use crate::skin::Skin;
|
||||||
use crate::utils::{self, rect_contains};
|
use crate::utils::{self, rect_contains};
|
||||||
|
|
||||||
|
use self::ui::UiState;
|
||||||
|
|
||||||
pub const PLAYFIELD_BOUNDS: Rect = Rect::new(112.0, 122.0, 800.0, 600.0);
|
pub const PLAYFIELD_BOUNDS: Rect = Rect::new(112.0, 122.0, 800.0, 600.0);
|
||||||
pub const DEFAULT_COLORS: &[(f32, f32, f32)] = &[
|
pub const DEFAULT_COLORS: &[(f32, f32, f32)] = &[
|
||||||
(1.0, 0.75, 0.0),
|
(1.0, 0.75, 0.0),
|
||||||
|
@ -71,9 +73,11 @@ pub struct Game {
|
||||||
beatmap: BeatmapExt,
|
beatmap: BeatmapExt,
|
||||||
pub skin: Skin,
|
pub skin: Skin,
|
||||||
background_image: Option<Image>,
|
background_image: Option<Image>,
|
||||||
|
ui_state: Option<UiState>,
|
||||||
|
|
||||||
frame: usize,
|
frame: usize,
|
||||||
slider_cache: SliderCache,
|
slider_cache: SliderCache,
|
||||||
|
seeker_drag: bool,
|
||||||
seeker_cache: Option<CanvasGeneric<GlBackendSpec>>,
|
seeker_cache: Option<CanvasGeneric<GlBackendSpec>>,
|
||||||
combo_colors: Vec<Color>,
|
combo_colors: Vec<Color>,
|
||||||
selected_objects: Vec<usize>,
|
selected_objects: Vec<usize>,
|
||||||
|
@ -103,8 +107,10 @@ impl Game {
|
||||||
beatmap,
|
beatmap,
|
||||||
song: None,
|
song: None,
|
||||||
skin,
|
skin,
|
||||||
|
ui_state: Some(UiState::default()),
|
||||||
frame: 0,
|
frame: 0,
|
||||||
slider_cache: SliderCache::default(),
|
slider_cache: SliderCache::default(),
|
||||||
|
seeker_drag: false,
|
||||||
seeker_cache: None,
|
seeker_cache: None,
|
||||||
combo_colors: DEFAULT_COLORS
|
combo_colors: DEFAULT_COLORS
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -224,199 +230,17 @@ impl Game {
|
||||||
graphics::queue_text(ctx, &text, [0.0, 0.0], Some(Color::WHITE));
|
graphics::queue_text(ctx, &text, [0.0, 0.0], Some(Color::WHITE));
|
||||||
graphics::draw_queued_text(ctx, DrawParam::default(), None, FilterMode::Linear)?;
|
graphics::draw_queued_text(ctx, DrawParam::default(), None, FilterMode::Linear)?;
|
||||||
|
|
||||||
struct DrawInfo<'a> {
|
|
||||||
hit_object: &'a HitObjectExt,
|
|
||||||
fade_opacity: f64,
|
|
||||||
end_time: f64,
|
|
||||||
color: Color,
|
|
||||||
/// Whether or not the circle (slider head for sliders) should appear to have already
|
|
||||||
/// been hit in the editor after the object's time has already come.
|
|
||||||
circle_is_hit: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
// figure out what objects will be visible on the screen at the current instant
|
|
||||||
// 1.5 cus editor so people can see objects for longer durations
|
|
||||||
let mut playfield_hitobjects = Vec::new();
|
|
||||||
let preempt = 1.5
|
|
||||||
* self
|
|
||||||
.beatmap
|
|
||||||
.inner
|
|
||||||
.difficulty
|
|
||||||
.approach_preempt()
|
|
||||||
.as_seconds();
|
|
||||||
let fade_in = 1.5
|
|
||||||
* self
|
|
||||||
.beatmap
|
|
||||||
.inner
|
|
||||||
.difficulty
|
|
||||||
.approach_fade_time()
|
|
||||||
.as_seconds();
|
|
||||||
let fade_out_time = fade_in; // TODO: figure out what this number actually is
|
|
||||||
let fade_out_offset = preempt; // TODO: figure out what this number actually is
|
|
||||||
|
|
||||||
// TODO: tighten this loop even more by binary searching for the start of the timeline and
|
|
||||||
// playfield hitobjects rather than looping through the entire beatmap, better yet, just
|
|
||||||
// keeping track of the old index will probably be much faster
|
|
||||||
for ho in self.beatmap.hit_objects.iter().rev() {
|
|
||||||
let ho_time = ho.inner.start_time.as_seconds();
|
|
||||||
let color = self.combo_colors[ho.color_idx];
|
|
||||||
|
|
||||||
// draw in timeline
|
|
||||||
self.draw_hitobject_to_timeline(ctx, time, ho)?;
|
|
||||||
|
|
||||||
// draw hitobject in playfield
|
|
||||||
let end_time;
|
|
||||||
match ho.inner.kind {
|
|
||||||
HitObjectKind::Circle => end_time = ho_time,
|
|
||||||
HitObjectKind::Slider(_) => {
|
|
||||||
let duration = self.beatmap.inner.get_slider_duration(&ho.inner).unwrap();
|
|
||||||
end_time = ho_time + duration;
|
|
||||||
}
|
|
||||||
HitObjectKind::Spinner(SpinnerInfo {
|
|
||||||
end_time: spinner_end,
|
|
||||||
}) => end_time = spinner_end.as_seconds(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let fade_opacity = if time <= ho_time - fade_in {
|
|
||||||
// before the hitobject's time arrives, it fades in
|
|
||||||
// TODO: calculate ease
|
|
||||||
(time - (ho_time - preempt)) / fade_in
|
|
||||||
} else if time < ho_time + fade_out_time {
|
|
||||||
// while the object is on screen the opacity should be 1
|
|
||||||
1.0
|
|
||||||
} else if time < end_time + fade_out_offset {
|
|
||||||
// after the hitobject's time, it fades out
|
|
||||||
// TODO: calculate ease
|
|
||||||
((end_time + fade_out_offset) - time) / fade_out_time
|
|
||||||
} else {
|
|
||||||
// completely faded out
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
let circle_is_hit = time > ho_time;
|
|
||||||
|
|
||||||
if ho_time - preempt <= time && time <= end_time + fade_out_offset {
|
|
||||||
playfield_hitobjects.push(DrawInfo {
|
|
||||||
hit_object: ho,
|
|
||||||
fade_opacity,
|
|
||||||
end_time,
|
|
||||||
color,
|
|
||||||
circle_is_hit,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.draw_timeline(ctx, time)?;
|
self.draw_timeline(ctx, time)?;
|
||||||
|
|
||||||
let cs_scale = PLAYFIELD_BOUNDS.w / 640.0;
|
self.draw_hitobjects(ctx, time)?;
|
||||||
let osupx_scale_x = PLAYFIELD_BOUNDS.w / 512.0;
|
|
||||||
let osupx_scale_y = PLAYFIELD_BOUNDS.h / 384.0;
|
|
||||||
let cs_osupx = self.beatmap.inner.difficulty.circle_size_osupx();
|
|
||||||
let cs_real = cs_osupx * cs_scale;
|
|
||||||
|
|
||||||
for draw_info in playfield_hitobjects.iter() {
|
|
||||||
let ho = draw_info.hit_object;
|
|
||||||
let ho_time = ho.inner.start_time.as_seconds();
|
|
||||||
let stacking = ho.stacking as f32 * STACK_DISTANCE as f32;
|
|
||||||
let pos = [
|
|
||||||
PLAYFIELD_BOUNDS.x + osupx_scale_x * (ho.inner.pos.x as f32 - stacking),
|
|
||||||
PLAYFIELD_BOUNDS.y + osupx_scale_y * (ho.inner.pos.y as f32 - stacking),
|
|
||||||
];
|
|
||||||
let mut color = draw_info.color;
|
|
||||||
color.a = 0.6 * draw_info.fade_opacity as f32;
|
|
||||||
|
|
||||||
let mut slider_info = None;
|
|
||||||
if let HitObjectKind::Slider(info) = &ho.inner.kind {
|
|
||||||
let mut control_points = vec![ho.inner.pos];
|
|
||||||
control_points.extend(&info.control_points);
|
|
||||||
|
|
||||||
Game::render_slider_body(
|
|
||||||
&mut self.slider_cache,
|
|
||||||
info,
|
|
||||||
control_points.as_ref(),
|
|
||||||
ctx,
|
|
||||||
PLAYFIELD_BOUNDS,
|
|
||||||
&self.beatmap.inner,
|
|
||||||
color,
|
|
||||||
)?;
|
|
||||||
slider_info = Some((info, control_points));
|
|
||||||
|
|
||||||
let end_pos = ho.inner.end_pos();
|
|
||||||
let end_pos = [
|
|
||||||
PLAYFIELD_BOUNDS.x + osupx_scale_x * end_pos.x as f32,
|
|
||||||
PLAYFIELD_BOUNDS.y + osupx_scale_y * end_pos.y as f32,
|
|
||||||
];
|
|
||||||
self.skin.hitcircle.draw(
|
|
||||||
ctx,
|
|
||||||
(cs_real * 2.0, cs_real * 2.0),
|
|
||||||
DrawParam::default().dest(end_pos).color(color),
|
|
||||||
)?;
|
|
||||||
self.skin.hitcircleoverlay.draw(
|
|
||||||
ctx,
|
|
||||||
(cs_real * 2.0, cs_real * 2.0),
|
|
||||||
DrawParam::default().dest(end_pos),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw main hitcircle
|
|
||||||
let faded_color = Color::new(1.0, 1.0, 1.0, 0.6 * draw_info.fade_opacity as f32);
|
|
||||||
self.skin.hitcircle.draw(
|
|
||||||
ctx,
|
|
||||||
(cs_real * 2.0, cs_real * 2.0),
|
|
||||||
DrawParam::default().dest(pos).color(color),
|
|
||||||
)?;
|
|
||||||
self.skin.hitcircleoverlay.draw(
|
|
||||||
ctx,
|
|
||||||
(cs_real * 2.0, cs_real * 2.0),
|
|
||||||
DrawParam::default().dest(pos).color(faded_color),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// draw numbers
|
|
||||||
self.draw_numbers_on_circle(ctx, ho.number, pos, cs_real, faded_color)?;
|
|
||||||
|
|
||||||
if let Some((info, control_points)) = slider_info {
|
|
||||||
let spline = self.slider_cache.get(&control_points).unwrap();
|
|
||||||
Game::render_slider_wireframe(ctx, &control_points, PLAYFIELD_BOUNDS, faded_color)?;
|
|
||||||
|
|
||||||
if time > ho_time && time < draw_info.end_time {
|
|
||||||
let elapsed_time = time - ho_time;
|
|
||||||
let total_duration = draw_info.end_time - ho_time;
|
|
||||||
let single_duration = total_duration / info.num_repeats as f64;
|
|
||||||
let finished_repeats = (elapsed_time / single_duration).floor();
|
|
||||||
let this_repeat_time = elapsed_time - finished_repeats * single_duration;
|
|
||||||
let mut travel_percent = this_repeat_time / single_duration;
|
|
||||||
|
|
||||||
// reverse direction on odd trips
|
|
||||||
if finished_repeats as u32 % 2 == 1 {
|
|
||||||
travel_percent = 1.0 - travel_percent;
|
|
||||||
}
|
|
||||||
let travel_length = travel_percent * info.pixel_length;
|
|
||||||
let pos = spline.point_at_length(travel_length);
|
|
||||||
let ball_pos = [
|
|
||||||
PLAYFIELD_BOUNDS.x + osupx_scale_x * pos.x as f32,
|
|
||||||
PLAYFIELD_BOUNDS.y + osupx_scale_y * pos.y as f32,
|
|
||||||
];
|
|
||||||
self.skin.sliderb.draw_frame(
|
|
||||||
ctx,
|
|
||||||
(cs_real * 1.8, cs_real * 1.8),
|
|
||||||
DrawParam::default().dest(ball_pos).color(color),
|
|
||||||
(travel_percent / 0.25) as usize,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if time < ho_time {
|
|
||||||
let time_diff = ho_time - time;
|
|
||||||
let approach_r = cs_real * (1.0 + 2.0 * time_diff as f32 / 0.75);
|
|
||||||
self.skin.approachcircle.draw(
|
|
||||||
ctx,
|
|
||||||
(approach_r * 2.0, approach_r * 2.0),
|
|
||||||
DrawParam::default().dest(pos).color(color),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.draw_seeker(ctx)?;
|
self.draw_seeker(ctx)?;
|
||||||
|
|
||||||
|
// TODO: don't duplicate these from hitobjects.rs
|
||||||
|
let cs_scale = PLAYFIELD_BOUNDS.w / 640.0;
|
||||||
|
let cs_osupx = self.beatmap.inner.difficulty.circle_size_osupx();
|
||||||
|
let cs_real = cs_osupx * cs_scale;
|
||||||
|
|
||||||
// draw whatever tool user is using
|
// draw whatever tool user is using
|
||||||
let (mx, my) = self.mouse_pos;
|
let (mx, my) = self.mouse_pos;
|
||||||
let pos_x = (mx - PLAYFIELD_BOUNDS.x) / PLAYFIELD_BOUNDS.w * 512.0;
|
let pos_x = (mx - PLAYFIELD_BOUNDS.x) / PLAYFIELD_BOUNDS.w * 512.0;
|
||||||
|
@ -520,53 +344,10 @@ impl Game {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut show = true;
|
if let Some(mut state) = self.ui_state.take() {
|
||||||
self.imgui.render(ctx, 1.0, |ui| {
|
self.draw_ui(ctx, &mut state)?;
|
||||||
if let Some(menu_bar) = ui.begin_main_menu_bar() {
|
self.ui_state = Some(state);
|
||||||
if let Some(menu) = ui.begin_menu("File") {
|
|
||||||
MenuItem::new("Save <C-s>").build(ui);
|
|
||||||
MenuItem::new("Create Difficulty").build(ui);
|
|
||||||
ui.separator();
|
|
||||||
MenuItem::new("Revert to Saved <C-l>").build(ui);
|
|
||||||
MenuItem::new("Export...").build(ui);
|
|
||||||
ui.separator();
|
|
||||||
MenuItem::new("Open Song Folder").build(ui);
|
|
||||||
MenuItem::new("Exit <Esc>").build(ui);
|
|
||||||
menu.end();
|
|
||||||
}
|
}
|
||||||
if let Some(menu) = ui.begin_menu("Edit") {
|
|
||||||
menu.end();
|
|
||||||
}
|
|
||||||
if let Some(menu) = ui.begin_menu("View") {
|
|
||||||
menu.end();
|
|
||||||
}
|
|
||||||
if let Some(menu) = ui.begin_menu("Compose") {
|
|
||||||
menu.end();
|
|
||||||
}
|
|
||||||
if let Some(menu) = ui.begin_menu("Design") {
|
|
||||||
menu.end();
|
|
||||||
}
|
|
||||||
if let Some(menu) = ui.begin_menu("Timing") {
|
|
||||||
menu.end();
|
|
||||||
}
|
|
||||||
if let Some(menu) = ui.begin_menu("Web") {
|
|
||||||
menu.end();
|
|
||||||
}
|
|
||||||
if let Some(menu) = ui.begin_menu("Help") {
|
|
||||||
menu.end();
|
|
||||||
}
|
|
||||||
menu_bar.end();
|
|
||||||
}
|
|
||||||
// Window
|
|
||||||
// Window::new("Hello world")
|
|
||||||
// .size([300.0, 600.0], imgui::Condition::FirstUseEver)
|
|
||||||
// .position([50.0, 50.0], imgui::Condition::FirstUseEver)
|
|
||||||
// .build(&ui, || {
|
|
||||||
// // Your window stuff here!
|
|
||||||
// ui.text("Hi from this label!");
|
|
||||||
// });
|
|
||||||
// ui.show_demo_window(&mut show);
|
|
||||||
});
|
|
||||||
|
|
||||||
graphics::present(ctx)?;
|
graphics::present(ctx)?;
|
||||||
self.frame += 1;
|
self.frame += 1;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use ggez::{
|
use ggez::{
|
||||||
conf::NumSamples,
|
graphics::{self, Color, DrawMode, DrawParam, FillOptions, Mesh, Rect},
|
||||||
graphics::{self, Canvas, Color, DrawMode, DrawParam, FillOptions, Mesh, Rect},
|
|
||||||
mint::Point2,
|
mint::Point2,
|
||||||
Context,
|
Context,
|
||||||
};
|
};
|
||||||
|
@ -13,18 +12,6 @@ pub const BOUNDS: Rect = Rect::new(46.0, 732.0, 932.0, 36.0);
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
pub(super) fn draw_seeker(&mut self, ctx: &mut Context) -> Result<()> {
|
pub(super) fn draw_seeker(&mut self, ctx: &mut Context) -> Result<()> {
|
||||||
if self.seeker_cache.is_none() {
|
|
||||||
println!("drawing seeker");
|
|
||||||
let format = graphics::get_window_color_format(ctx);
|
|
||||||
let canvas = Canvas::new(
|
|
||||||
ctx,
|
|
||||||
BOUNDS.w as u16,
|
|
||||||
BOUNDS.h as u16,
|
|
||||||
NumSamples::Sixteen,
|
|
||||||
format,
|
|
||||||
)?;
|
|
||||||
graphics::set_canvas(ctx, Some(&canvas));
|
|
||||||
|
|
||||||
let rect = Mesh::new_rectangle(
|
let rect = Mesh::new_rectangle(
|
||||||
ctx,
|
ctx,
|
||||||
DrawMode::Fill(FillOptions::default()),
|
DrawMode::Fill(FillOptions::default()),
|
||||||
|
@ -33,11 +20,12 @@ impl Game {
|
||||||
)?;
|
)?;
|
||||||
graphics::draw(ctx, &rect, DrawParam::default())?;
|
graphics::draw(ctx, &rect, DrawParam::default())?;
|
||||||
|
|
||||||
let line_y = BOUNDS.h / 2.0;
|
// draw the main timeline of the seeker
|
||||||
|
let line_y = BOUNDS.y + BOUNDS.h / 2.0;
|
||||||
let line = Mesh::new_line(
|
let line = Mesh::new_line(
|
||||||
ctx,
|
ctx,
|
||||||
&[
|
&[
|
||||||
Point2::from([0.0, line_y]),
|
Point2::from([BOUNDS.x, line_y]),
|
||||||
Point2::from([BOUNDS.w, line_y]),
|
Point2::from([BOUNDS.w, line_y]),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
|
@ -48,6 +36,7 @@ impl Game {
|
||||||
if let Some(song) = &self.song {
|
if let Some(song) = &self.song {
|
||||||
let len = song.length()?;
|
let len = song.length()?;
|
||||||
|
|
||||||
|
// draw timing points
|
||||||
for timing_point in self.beatmap.inner.timing_points.iter() {
|
for timing_point in self.beatmap.inner.timing_points.iter() {
|
||||||
let color = match timing_point.kind {
|
let color = match timing_point.kind {
|
||||||
TimingPointKind::Inherited(_) => Color::new(0.0, 0.8, 0.0, 0.4),
|
TimingPointKind::Inherited(_) => Color::new(0.0, 0.8, 0.0, 0.4),
|
||||||
|
@ -55,24 +44,28 @@ impl Game {
|
||||||
};
|
};
|
||||||
|
|
||||||
let percent = timing_point.time.as_seconds() / len;
|
let percent = timing_point.time.as_seconds() / len;
|
||||||
let x = percent as f32 * BOUNDS.w;
|
let x = BOUNDS.x + percent as f32 * BOUNDS.w;
|
||||||
|
|
||||||
let line = Mesh::new_line(
|
let line = Mesh::new_line(
|
||||||
ctx,
|
ctx,
|
||||||
&[Point2::from([x, 0.0]), Point2::from([x, BOUNDS.h / 2.0])],
|
&[
|
||||||
|
Point2::from([x, BOUNDS.y]),
|
||||||
|
Point2::from([x, BOUNDS.y + BOUNDS.h / 2.0]),
|
||||||
|
],
|
||||||
1.0,
|
1.0,
|
||||||
color,
|
color,
|
||||||
)?;
|
)?;
|
||||||
graphics::draw(ctx, &line, DrawParam::default())?;
|
graphics::draw(ctx, &line, DrawParam::default())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// draw the knob for current position
|
||||||
let percent = song.position()? / len;
|
let percent = song.position()? / len;
|
||||||
let x = percent as f32 * BOUNDS.w;
|
let x = BOUNDS.x + percent as f32 * BOUNDS.w;
|
||||||
let line = Mesh::new_line(
|
let line = Mesh::new_line(
|
||||||
ctx,
|
ctx,
|
||||||
&[
|
&[
|
||||||
Point2::from([x, 0.2 * BOUNDS.h]),
|
Point2::from([x, BOUNDS.y + 0.2 * BOUNDS.h]),
|
||||||
Point2::from([x, 0.8 * BOUNDS.h]),
|
Point2::from([x, BOUNDS.y + 0.8 * BOUNDS.h]),
|
||||||
],
|
],
|
||||||
4.0,
|
4.0,
|
||||||
Color::WHITE,
|
Color::WHITE,
|
||||||
|
@ -80,19 +73,6 @@ impl Game {
|
||||||
graphics::draw(ctx, &line, DrawParam::default())?;
|
graphics::draw(ctx, &line, DrawParam::default())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
graphics::set_canvas(ctx, None);
|
|
||||||
self.seeker_cache = Some(canvas);
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(canvas) = &self.seeker_cache {
|
|
||||||
graphics::draw(
|
|
||||||
ctx,
|
|
||||||
canvas,
|
|
||||||
DrawParam::default()
|
|
||||||
.dest([BOUNDS.x, BOUNDS.y])
|
|
||||||
.scale([1.0, 10.0]),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
77
src/game/ui.rs
Normal file
77
src/game/ui.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use ggez::Context;
|
||||||
|
use imgui::{Condition, MenuItem, TabBar, TabItem, Window};
|
||||||
|
|
||||||
|
use super::Game;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct UiState {
|
||||||
|
song_setup_selected: bool,
|
||||||
|
song_setup_artist: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Game {
|
||||||
|
pub(super) fn draw_ui(&mut self, ctx: &mut Context, state: &mut UiState) -> Result<()> {
|
||||||
|
self.imgui.render(ctx, 1.0, |ui| {
|
||||||
|
// menu bar
|
||||||
|
if let Some(menu_bar) = ui.begin_main_menu_bar() {
|
||||||
|
if let Some(menu) = ui.begin_menu("File") {
|
||||||
|
MenuItem::new("Save <C-s>").build(ui);
|
||||||
|
MenuItem::new("Create Difficulty").build(ui);
|
||||||
|
ui.separator();
|
||||||
|
MenuItem::new("Song Setup").build_with_ref(ui, &mut state.song_setup_selected);
|
||||||
|
MenuItem::new("Revert to Saved <C-l>").build(ui);
|
||||||
|
ui.separator();
|
||||||
|
MenuItem::new("Open Song Folder").build(ui);
|
||||||
|
MenuItem::new("Exit <Esc>").build(ui);
|
||||||
|
menu.end();
|
||||||
|
}
|
||||||
|
if let Some(menu) = ui.begin_menu("Edit") {
|
||||||
|
menu.end();
|
||||||
|
}
|
||||||
|
if let Some(menu) = ui.begin_menu("View") {
|
||||||
|
menu.end();
|
||||||
|
}
|
||||||
|
if let Some(menu) = ui.begin_menu("Compose") {
|
||||||
|
menu.end();
|
||||||
|
}
|
||||||
|
if let Some(menu) = ui.begin_menu("Design") {
|
||||||
|
menu.end();
|
||||||
|
}
|
||||||
|
if let Some(menu) = ui.begin_menu("Timing") {
|
||||||
|
menu.end();
|
||||||
|
}
|
||||||
|
if let Some(menu) = ui.begin_menu("Web") {
|
||||||
|
menu.end();
|
||||||
|
}
|
||||||
|
if let Some(menu) = ui.begin_menu("Help") {
|
||||||
|
menu.end();
|
||||||
|
}
|
||||||
|
menu_bar.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.song_setup_selected {
|
||||||
|
Window::new("Song Setup")
|
||||||
|
.size([80.0, 120.0], Condition::Appearing)
|
||||||
|
.build(&ui, || {
|
||||||
|
TabBar::new("song_setup").build(&ui, || {
|
||||||
|
TabItem::new("General").build(&ui, || {
|
||||||
|
ui.group(|| {
|
||||||
|
ui.text("Artist");
|
||||||
|
ui.same_line();
|
||||||
|
ui.input_text("", &mut state.song_setup_artist).build();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
TabItem::new("Difficulty").build(&ui, || {});
|
||||||
|
TabItem::new("Audio").build(&ui, || {});
|
||||||
|
TabItem::new("Colors").build(&ui, || {});
|
||||||
|
TabItem::new("Design").build(&ui, || {});
|
||||||
|
TabItem::new("Advanced").build(&ui, || {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ use ggez::Context;
|
||||||
use gfx_core::{handle::RenderTargetView, memory::Typed};
|
use gfx_core::{handle::RenderTargetView, memory::Typed};
|
||||||
use gfx_device_gl;
|
use gfx_device_gl;
|
||||||
|
|
||||||
use imgui::{Context as ImContext, FontId, Key, Ui, Window, FontSource};
|
use imgui::{Context as ImContext, FontId, FontSource, Key, Ui};
|
||||||
use imgui_gfx_renderer::*;
|
use imgui_gfx_renderer::*;
|
||||||
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
|
@ -20,11 +20,8 @@ use std::path::PathBuf;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use ggez::{
|
use ggez::{
|
||||||
conf::{WindowMode, WindowSetup},
|
conf::{WindowMode, WindowSetup},
|
||||||
event, graphics, ContextBuilder,
|
event, ContextBuilder,
|
||||||
};
|
};
|
||||||
use imgui::{Context as ImContext, FontConfig, FontSource};
|
|
||||||
use imgui_gfx_renderer::{Renderer, Shaders};
|
|
||||||
use imgui_winit_support::{HiDpiMode, WinitPlatform};
|
|
||||||
use imgui_wrapper::ImGuiWrapper;
|
use imgui_wrapper::ImGuiWrapper;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue