From 794e9dd4a05c28ac028b3841c281ecb4e48f3527 Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Wed, 11 Dec 2019 23:55:25 +1030 Subject: [PATCH] Hold+Slide release scoring! Practically feature-complete! --- FileLoader.gd | 18 ++++++- Menu.gd | 50 ++++++++++++-------- Note.gd | 4 ++ NoteHandler.gd | 125 +++++++++++++++++++++++++++++++++++++++++-------- Rules.gd | 2 +- 5 files changed, 158 insertions(+), 41 deletions(-) diff --git a/FileLoader.gd b/FileLoader.gd index 3aa042b..1aa4407 100644 --- a/FileLoader.gd +++ b/FileLoader.gd @@ -113,4 +113,20 @@ func load_folder(folder): return result_json.error var result = result_json.result result.directory = folder - return result \ No newline at end of file + return result + + +func load_ogg(filename) -> AudioStreamOGGVorbis: + var audiostream = AudioStreamOGGVorbis.new() + var oggfile = File.new() + oggfile.open(filename, File.READ) + audiostream.set_data(oggfile.get_buffer(oggfile.get_len())) + oggfile.close() + return audiostream + +func load_image(filename) -> ImageTexture: + var tex = ImageTexture.new() + var img = Image.new() + img.load(filename) + tex.create_from_image(img) + return tex \ No newline at end of file diff --git a/Menu.gd b/Menu.gd index 9dc7df5..e408922 100644 --- a/Menu.gd +++ b/Menu.gd @@ -44,7 +44,7 @@ func scan_library(): if dir.file_exists(key + "/song.json"): song_defs[key] = FileLoader.load_folder("%s/%s" % [rootdir, key]) print("Loaded song directory: %s" % key) - song_images[key] = load("%s/%s/%s" % [rootdir, key, song_defs[key]["tile_filename"]]) + song_images[key] = FileLoader.load_image("%s/%s/%s" % [rootdir, key, song_defs[key]["tile_filename"]]) if song_defs[key]["genre"] in genres: genres[song_defs[key]["genre"]].append(key) else: @@ -67,7 +67,11 @@ func save_score(): data.song_key = scorescreen_song_key var json = JSON.print(data) var file = File.new() - var err = file.open(rootdir + "/{year}{month}{day}T{hour}{minute}{second}.json".format(scorescreen_datetime), File.WRITE) +# var filename = rootdir + "/{year}{month}{day}T{hour}{minute}{second}.json".format(scorescreen_datetime) + # So uh. Can't zero-pad using the string.format() method. This sucks. + var dt = scorescreen_datetime + var filename = rootdir + "/%04d%02d%02dT%02d%02d%02d.json"%[dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second] + var err = file.open(filename, File.WRITE) if err != OK: print(err) return err @@ -109,7 +113,7 @@ func load_score(filename): func _ready(): scan_library() $"/root/main/NoteHandler".connect("finished_song", self, "finished_song") - load_score("20191210T235010.json") + load_score("20191211T234131.json") # For testing purposes # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta): @@ -229,11 +233,11 @@ func _draw_score_screen(center: Vector2) -> Array: var y = center.y - 200 var x_songtile = x - 120 var x_score = x + 120 - var x2 = x - 360 - var x_spacing = 116 - var y_spacing = 48 + var x2 = x - 370 + var x_spacing = 124 + var y_spacing = 42 var y1 = y - var y2 = y + size + y_spacing*2 + var y2 = y + size + y_spacing*1.5 var tex_judgement_text = $"/root/main/NoteHandler".tex_judgement_text var judgement_text_scale = 0.667 @@ -242,10 +246,12 @@ func _draw_score_screen(center: Vector2) -> Array: draw_songtile(song_key, Vector2(x_songtile-size/2.0, y), size, false, selected_difficulty, 3) draw_string_centered(TitleFont, Vector2(x_songtile, y+size), song_defs[song_key]["title"], Color(0.95, 0.95, 1.0)) - var notestrs = ["Tap", "Hold", "Slide"] + var notestrs = ["Taps:", "Holds Hit:", "Released:", "Slides Hit:", "Slid:"] + var notetypes = [0, 1, -1, 2, -2] + var note_spacing = [0.0, 1.25, 2.25, 3.5, 4.5] var judgestrs = Array(Rules.JUDGEMENT_STRINGS + ["Miss"]) var judge_scores = [1.0, 0.9, 0.75, 0.5, 0.0] - var notetype_weights = [1.0, 2.0, 2.0] + var notetype_weights = [1.0, 1.0, 1.0, 1.0, 1.0] var notecount_total = 0 var notecount_early = 0 var notecount_late = 0 @@ -260,24 +266,30 @@ func _draw_score_screen(center: Vector2) -> Array: for i in len(notestrs): # For each note type, make a row and print scores - draw_string_centered(TitleFont, Vector2(x2, y2+y_spacing*(i+1)), notestrs[i]+"s:", Color(0.95, 0.95, 1.0)) + var idx = notetypes[i] var note_score = 0 var note_count = 0 +# var y_row = y2+y_spacing*(i+1) + var y_row = y2 + y_spacing * (note_spacing[i]+1) + draw_string_centered(TitleFont, Vector2(x2, y_row), notestrs[i], Color(0.95, 0.95, 1.0)) for j in len(judgestrs): var score if j == 0: - score = scorescreen_score_data[i][0] + score = scorescreen_score_data[idx][0] elif j >= len(judgestrs)-1: - score = scorescreen_score_data[i]["MISS"] + score = scorescreen_score_data[idx]["MISS"] else: - score = scorescreen_score_data[i][j] + scorescreen_score_data[i][-j] - notecount_early += scorescreen_score_data[i][-j] - notecount_late += scorescreen_score_data[i][j] - draw_string_centered(TitleFont, Vector2(x2+x_spacing*(j+1), y2+y_spacing*(i+1)), str(score), Color(0.95, 0.95, 1.0)) + score = scorescreen_score_data[idx][j] + scorescreen_score_data[idx][-j] + notecount_early += scorescreen_score_data[idx][-j] + notecount_late += scorescreen_score_data[idx][j] + if (j >= len(judgestrs)-1) and (idx == -1): + draw_string_centered(TitleFont, Vector2(x2+x_spacing*(j+1), y_row), "^", Color(0.95, 0.95, 1.0)) + else: + draw_string_centered(TitleFont, Vector2(x2+x_spacing*(j+1), y_row), str(score), Color(0.95, 0.95, 1.0)) notecount_total += score # Kinda redundant, will probably refactor eventually note_count += score note_score += score * judge_scores[j] - draw_string_centered(TitleFont, Vector2(x2+x_spacing*(len(judgestrs)+1), y2+y_spacing*(i+1)), "%2.2f%%"%(note_score/note_count*100.0), Color(0.95, 0.95, 1.0)) + draw_string_centered(TitleFont, Vector2(x2+x_spacing*(len(judgestrs)+1), y_row), "%2.2f%%"%(note_score/note_count*100.0), Color(0.95, 0.95, 1.0)) total_score += note_score * notetype_weights[i] total_scoremax += note_count * notetype_weights[i] @@ -295,8 +307,8 @@ func _draw_score_screen(center: Vector2) -> Array: $ScoreText.score_sub = "%2.3f%%"%(overall_score*100.0) $ScoreText.update() - draw_string_centered(TitleFont, Vector2(x, y2+y_spacing*4), "Early : Late", Color(0.95, 0.95, 1.0)) - draw_string_centered(TitleFont, Vector2(x, y2+y_spacing*5), "%3d%% : %3d%%"%[notecount_early*100/notecount_total, notecount_late*100/notecount_total], Color(0.95, 0.95, 1.0)) + draw_string_centered(TitleFont, Vector2(x, y2+y_spacing*7), "Early : Late", Color(0.95, 0.95, 1.0)) + draw_string_centered(TitleFont, Vector2(x, y2+y_spacing*8), "%3d%% : %3d%%"%[notecount_early*100/notecount_total, notecount_late*100/notecount_total], Color(0.95, 0.95, 1.0)) var rect_songselect := Rect2(-100.0, 300.0, 400.0, 100.0) draw_rect(rect_songselect, Color.red) diff --git a/Note.gd b/Note.gd index 9dfeb57..619f970 100644 --- a/Note.gd +++ b/Note.gd @@ -6,6 +6,7 @@ extends Node enum {NOTE_TAP, NOTE_HOLD, NOTE_SLIDE, NOTE_ARROW, NOTE_TOUCH, NOTE_TOUCH_HOLD} enum SlideType {CHORD, ARC_CW, ARC_ACW} const DEATH_DELAY := 1.0 # This is touchy with the judgement windows and variable bpm. +const RELEASE_SCORE_TYPES := [NOTE_HOLD, NOTE_SLIDE, NOTE_TOUCH_HOLD] class NoteBase: var time_hit: float @@ -25,6 +26,7 @@ class NoteTap extends NoteBase: class NoteHold extends NoteBase: var type := NOTE_HOLD var time_release: float + var time_released := INF var duration: float var is_held: bool func _init(time_hit: float, duration: float, column: int): @@ -42,6 +44,8 @@ class NoteSlide extends NoteBase: var column_release: int var slide_type: int var slide_id: int + var progress := INF + var missed_slide := false var values: Dictionary func _init(time_hit: float, duration: float, column: int, column_release: int, slide_type: int): diff --git a/NoteHandler.gd b/NoteHandler.gd index b44c70d..c9521a2 100644 --- a/NoteHandler.gd +++ b/NoteHandler.gd @@ -62,6 +62,7 @@ var next_note_to_load := 0 var active_judgement_texts := [] var scores := {} +var active_slide_trails := [] var slide_trail_meshes := {} var slide_trail_mesh_instances := {} @@ -116,6 +117,11 @@ func initialise_scores(): scores[type] = {} for key in TextJudgement: scores[type][key] = 0 + # Release types + for type in [Note.NOTE_HOLD, Note.NOTE_SLIDE]: + scores[-type] = {} + for key in TextJudgement: + scores[-type][key] = 0 func make_text_mesh(mesh: ArrayMesh, text_id: int, pos: Vector2, angle: float, alpha:=1.0, scale:=1.0): var r := GameTheme.judge_text_size2 * scale @@ -270,8 +276,24 @@ func activate_note(note, judgement): Note.NOTE_HOLD: note.is_held = true Note.NOTE_SLIDE: - pass # Set up slide trail? - return + # Set up slide trail? + active_slide_trails.append(note) + note.progress = 0.0 + +func activate_note_release(note, judgement): + # Only for Hold, Slide + SFXPlayer.play(SFXPlayer.Type.NON_POSITIONAL, self, snd_judgement[judgement], db_judgement[judgement], pitch_judgement[judgement]) + scores[-note.type][judgement] += 1 + + match note.type: + Note.NOTE_HOLD: + note.is_held = false + note.time_released = t + active_judgement_texts.append({col=note.column, judgement=judgement, time=t}) + Note.NOTE_SLIDE: + active_judgement_texts.append({col=note.column_release, judgement=judgement, time=t}) + Note.NOTE_TOUCH_HOLD: + pass func button_pressed(col): for note in active_notes: @@ -283,37 +305,65 @@ func button_pressed(col): var hit_delta = get_realtime_precise() - real_time(note.time_hit) # Judgement times are in seconds not gametime if hit_delta >= 0.0: if hit_delta > Rules.JUDGEMENT_TIMES_POST[-1]: - continue # missed + continue # missed, don't consume input for i in Rules.JUDGEMENT_TIERS: if hit_delta <= Rules.JUDGEMENT_TIMES_POST[i]: activate_note(note, i) - return + return # Consume input because one press shouldn't trigger two notes else: if -hit_delta > Rules.JUDGEMENT_TIMES_PRE[-1]: - continue # too far away + continue # too far away, don't consume input for i in Rules.JUDGEMENT_TIERS: - if -hit_delta <= Rules.JUDGEMENT_TIMES_POST[i]: + if -hit_delta <= Rules.JUDGEMENT_TIMES_PRE[i]: activate_note(note, -i) return - func touchbutton_pressed(col): button_pressed(col) + +func do_hold_release(note): + var hit_delta = get_realtime_precise() - real_time(note.time_release) # Judgement times are in seconds not gametime + if hit_delta >= 0.0: + for i in Rules.JUDGEMENT_TIERS-1: + if hit_delta <= Rules.JUDGEMENT_TIMES_RELEASE_POST[i]: + activate_note_release(note, i) + return + activate_note_release(note, Rules.JUDGEMENT_TIERS-1) # No "miss" for releasing, only worst judgement. + return + else: + for i in Rules.JUDGEMENT_TIERS-1: + if -hit_delta <= Rules.JUDGEMENT_TIMES_RELEASE_PRE[i]: + activate_note_release(note, -i) + return + activate_note_release(note, Rules.JUDGEMENT_TIERS-1) # No "miss" for releasing, only worst judgement. + return + +func do_slide_release(note): + var hit_delta = get_realtime_precise() - real_time(note.time_release) # Judgement times are in seconds not gametime + if hit_delta >= 0.0: + for i in Rules.JUDGEMENT_TIERS: + if hit_delta <= Rules.JUDGEMENT_TIMES_SLIDE_POST[i]: + activate_note_release(note, i) + return + else: + for i in Rules.JUDGEMENT_TIERS: + if -hit_delta <= Rules.JUDGEMENT_TIMES_SLIDE_PRE[i]: + activate_note_release(note, -i) + return + func check_hold_release(col): for note in active_notes: if note.column != col: continue if note.type == Note.NOTE_HOLD: if note.is_held == true: - note.is_held = false - pass + do_hold_release(note) # Separate function since there's no need to "consume" releases func button_released(col): # We only care about hold release. # For that particular case, we want both to be unheld. if $"/root/main/InputHandler".touchbuttons_pressed[col] == 0: check_hold_release(col) - func touchbutton_released(col): if $"/root/main/InputHandler".buttons_pressed[col] == 0: check_hold_release(col) @@ -345,10 +395,14 @@ func _draw(): make_tap_mesh(mesh, note_center, scale, color) Note.NOTE_HOLD: if note.is_held: + position = (t+GameTheme.note_forecast_beats-note.time_release)/GameTheme.note_forecast_beats color = GameTheme.COLOR_ARRAY_HOLD_HELD - note_center = GameTheme.RADIAL_UNIT_VECTORS[note.column] * GameTheme.receptor_ring_radius + note_center = GameTheme.RADIAL_UNIT_VECTORS[note.column] * GameTheme.receptor_ring_radius * max(position, 1.0) elif position > 1.0: color = GameTheme.COLOR_ARRAY_DOUBLE_MISS_8 if note.double_hit else GameTheme.COLOR_ARRAY_HOLD_MISS + if note.time_released != INF: + position = (t+GameTheme.note_forecast_beats-note.time_released)/GameTheme.note_forecast_beats + note_center = GameTheme.RADIAL_UNIT_VECTORS[note.column] * GameTheme.receptor_ring_radius * position else: color = GameTheme.COLOR_ARRAY_DOUBLE_8 if note.double_hit else GameTheme.COLOR_ARRAY_HOLD var position_rel : float = (t+GameTheme.note_forecast_beats-note.time_release)/GameTheme.note_forecast_beats @@ -374,7 +428,8 @@ func _draw(): var star_pos : Vector2 = note.get_position(trail_progress) var star_angle : float = note.get_angle(trail_progress) make_star_mesh(mesh, star_pos, 1.33, star_angle, color) -# slide_trail_mesh_instances[note.slide_id].material.set_shader_param("trail_progress", trail_progress) + if note.progress != INF: + slide_trail_mesh_instances[note.slide_id].material.set_shader_param("trail_progress", note.progress) if t > note.time_release: trail_alpha = max(1 - (t - note.time_release)/Note.DEATH_DELAY, 0.0) slide_trail_mesh_instances[note.slide_id].material.set_shader_param("base_alpha", trail_alpha*0.88) @@ -392,6 +447,29 @@ func _draw(): make_judgement_text(textmesh, TextJudgement[text.judgement], text.col, (t-text.time)/GameTheme.judge_text_duration) $JudgeText.set_mesh(textmesh) + +func _input(event): + var pos + if event is InputEventScreenTouch: + if event.pressed: + pos = event.position - get_global_transform_with_canvas().get_origin() + else: + return + elif event is InputEventScreenDrag: + pos = event.position - get_global_transform_with_canvas().get_origin() + else: + return + + for i in range(len(active_slide_trails)-1, -1, -1): + var note = active_slide_trails[i] + var center = note.get_position(note.progress) + if (pos - center).length_squared() < 10000.0: + note.progress += 0.09 + if note.progress >= 1.0: + do_slide_release(note) + active_slide_trails.remove(i) + + func _init(): Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN) GameTheme.init_radial_values() @@ -440,14 +518,7 @@ func load_track(data: Dictionary, difficulty_idx: int): bpm = data.bpm_values[0] sync_offset_audio = data.audio_offsets[0] sync_offset_video = data.video_offsets[0] - var audiostream = AudioStreamOGGVorbis.new() -# var asb = load(data.directory + "/" + data.audio_filelist[0]) -# audiostream.set_data(asb.get_data()) - # Unbelievably stupid bug, infuriating workaround - var oggfile = File.new() - oggfile.open(data.directory + "/" + data.audio_filelist[0], File.READ) - audiostream.set_data(oggfile.get_buffer(oggfile.get_len())) - oggfile.close() + var audiostream = FileLoader.load_ogg(data.directory + "/" + data.audio_filelist[0]) var videostream = load(data.directory + "/" + data.video_filelist[0]) $"/root/main/music".set_stream(audiostream) @@ -533,13 +604,26 @@ func _process(delta): if note.type == Note.NOTE_SLIDE: $SlideTrailHandler.remove_child(slide_trail_mesh_instances[note.slide_id]) slide_trail_mesh_instances.erase(note.slide_id) + var idx = active_slide_trails.find(note) + if idx >= 0: + active_slide_trails.remove(idx) + active_judgement_texts.append({col=note.column_release, judgement="MISS", time=t}) + scores[-Note.NOTE_SLIDE]["MISS"] += 1 + note.missed_slide = true + SFXPlayer.play(SFXPlayer.Type.NON_POSITIONAL, self, snd_judgement["MISS"], db_judgement["MISS"]) active_notes.remove(i) elif note.time_activated == INF: if ((t-note.time_hit) > miss_time) and not note.missed: active_judgement_texts.append({col=note.column, judgement="MISS", time=t}) scores[note.type]["MISS"] += 1 + if Note.RELEASE_SCORE_TYPES.has(note.type): + scores[-note.type]["MISS"] += 1 note.missed = true SFXPlayer.play(SFXPlayer.Type.NON_POSITIONAL, self, snd_judgement["MISS"], db_judgement["MISS"]) + if note.type == Note.NOTE_SLIDE: + # Even if you miss the hit you can still slide, we're so nice + active_slide_trails.append(note) + note.progress = 0.0 # Clean out expired judgement texts # By design they will always be in order so we can ignore anything past the first index @@ -574,6 +658,7 @@ func _process(delta): # next_note_to_load = 0 if (len(active_notes) < 1) and (next_note_to_load >= len(all_notes)) and not get_node("/root/main/music").is_playing(): self.running = false + self.timers_set = false emit_signal("finished_song", song_key, scores) # Redraw diff --git a/Rules.gd b/Rules.gd index 17037b5..e3583eb 100644 --- a/Rules.gd +++ b/Rules.gd @@ -13,7 +13,7 @@ const JUDGEMENT_TIMES_PRE := [0.040, 0.090, 0.125, 0.150] const JUDGEMENT_TIMES_POST := [0.040, 0.090, 0.125, 0.150] const JUDGEMENT_TIMES_RELEASE_PRE := [0.040, 0.090, 0.125, 0.150] const JUDGEMENT_TIMES_RELEASE_POST := [0.090, 0.140, 0.175, 0.200] # Small grace period -const JUDGEMENT_TIMES_SLIDE_PRE := [0.090, 0.140, 0.175, 0.200] # Small grace period, sort-of +const JUDGEMENT_TIMES_SLIDE_PRE := [0.090, 0.240, 0.375, 60.000] # Small grace period, sort-of. Just be generous, really. const JUDGEMENT_TIMES_SLIDE_POST := [0.090, 0.140, 0.175, 0.200] const SCORE_STRINGS = ["SSS", "SS", "S", "A⁺", "A", "B⁺", "B", "C⁺", "C", "F"]