diff --git a/assets/spritesheet-1024.png.import b/assets/spritesheet-1024.png.import index 5ea26cf..57c16c6 100644 --- a/assets/spritesheet-1024.png.import +++ b/assets/spritesheet-1024.png.import @@ -28,6 +28,7 @@ process/fix_alpha_border=true process/premult_alpha=false process/HDR_as_SRGB=false process/invert_color=false +process/normal_map_invert_y=false stream=false size_limit=0 detect_3d=true diff --git a/assets/spritesheet.svg.import b/assets/spritesheet.svg.import index 24f4358..d53c490 100644 --- a/assets/spritesheet.svg.import +++ b/assets/spritesheet.svg.import @@ -28,6 +28,7 @@ process/fix_alpha_border=true process/premult_alpha=false process/HDR_as_SRGB=false process/invert_color=false +process/normal_map_invert_y=false stream=false size_limit=0 detect_3d=true diff --git a/mainmenu_theme.tres b/mainmenu_theme.tres index 71bf9b6..469bd36 100644 --- a/mainmenu_theme.tres +++ b/mainmenu_theme.tres @@ -2,6 +2,12 @@ [ext_resource path="res://assets/fonts/Sniglet-Regular.ttf" type="DynamicFontData" id=1] +[sub_resource type="DynamicFont" id=1] +size = 48 +outline_size = 4 +outline_color = Color( 0, 0, 0, 1 ) +font_data = ExtResource( 1 ) + [sub_resource type="StyleBoxFlat" id=3] content_margin_left = 24.0 content_margin_right = 24.0 @@ -69,12 +75,6 @@ corner_radius_bottom_left = 16 shadow_color = Color( 0.423529, 0.670588, 1, 0.372549 ) shadow_size = 6 -[sub_resource type="DynamicFont" id=1] -size = 48 -outline_size = 4 -outline_color = Color( 0, 0, 0, 1 ) -font_data = ExtResource( 1 ) - [resource] default_font = SubResource( 1 ) Button/colors/font_color = Color( 0.88, 0.88, 0.88, 1 ) @@ -82,7 +82,7 @@ Button/colors/font_color_disabled = Color( 0.9, 0.9, 0.9, 0.2 ) Button/colors/font_color_hover = Color( 0.827451, 0.788235, 1, 1 ) Button/colors/font_color_pressed = Color( 1, 1, 1, 1 ) Button/constants/hseparation = 2 -Button/fonts/font = null +Button/fonts/font = SubResource( 1 ) Button/styles/disabled = SubResource( 3 ) Button/styles/focus = SubResource( 4 ) Button/styles/hover = SubResource( 2 ) diff --git a/scripts/NoteHandler.gd b/scripts/NoteHandler.gd index dd3eec0..11c60eb 100644 --- a/scripts/NoteHandler.gd +++ b/scripts/NoteHandler.gd @@ -1,5 +1,5 @@ extends Control -var RadialMeshTools := preload('res://scripts/RadialMeshTools.gd') +const RadialMeshTools := preload('res://scripts/RadialMeshTools.gd') var screen_height := 1080 @@ -164,93 +164,105 @@ func check_hold_release(col): do_hold_release(note) # Separate function since there's no need to 'consume' releases #---------------------------------------------------------------------------------------------------------------------------------------------- -const arr_div := Vector3(2.0, float(Rules.COLS), TAU) +func draw_note(mesh, note, position, t): + var output = null + var scale := 1.0 + + if position < GameTheme.INNER_NOTE_CIRCLE_RATIO: + scale *= position/GameTheme.INNER_NOTE_CIRCLE_RATIO + position = GameTheme.INNER_NOTE_CIRCLE_RATIO + + var note_center = (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position * GameTheme.receptor_ring_radius) + var color: PoolColorArray + + match note.type: + Note.NOTE_TAP: + color = GameTheme.color_array_tap(clamp((note.time_death-t)/Note.DEATH_DELAY, 0.0, 1.0), note.double_hit) + RadialMeshTools.make_tap_mesh(mesh, note_center, scale, color) + + Note.NOTE_STAR: + color = GameTheme.color_array_star(clamp((note.time_death-t)/Note.DEATH_DELAY, 0.0, 1.0), note.double_hit) + var angle = fmod(t/note.duration, 1.0)*TAU + RadialMeshTools.make_star_mesh(mesh, note_center, scale, angle, 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 * 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 + if position_rel > 0: + var note_rel_center := (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position_rel * GameTheme.receptor_ring_radius) + output = [position_rel, note.column] + if position_rel < GameTheme.INNER_NOTE_CIRCLE_RATIO: + position_rel = GameTheme.INNER_NOTE_CIRCLE_RATIO + var note_center_rel = (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position_rel * GameTheme.receptor_ring_radius) + RadialMeshTools.make_hold_mesh(mesh, note_center, note_center_rel, scale, GameTheme.RADIAL_COL_ANGLES[note.column], color) + + Note.NOTE_SLIDE: + var trail_alpha := 1.0 + if position < GameTheme.INNER_NOTE_CIRCLE_RATIO: + trail_alpha = 0.0 + elif position < 1.0: + trail_alpha = min(1.0, (position-GameTheme.INNER_NOTE_CIRCLE_RATIO)/(1-GameTheme.INNER_NOTE_CIRCLE_RATIO*2)) + else: + var trail_progress : float = clamp((t - note.time_hit - GameTheme.SLIDE_DELAY)/(note.duration - GameTheme.SLIDE_DELAY), 0.0, 1.0) + var star_pos : Vector2 = note.get_position(trail_progress) * GameTheme.receptor_ring_radius + var star_angle : float = note.get_angle(trail_progress) + RadialMeshTools.make_star_mesh(mesh, star_pos, 1.33, star_angle) + 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*GameTheme.slide_trail_alpha) # TODO: somehow factor this out? + + return output + + +#---------------------------------------------------------------------------------------------------------------------------------------------- +const arr_div := Vector3(2.0, float(Rules.COLS), TAU) # Scaling factors to avoid LDR processing clipping our values func _draw(): var mesh := ArrayMesh.new() - var noteline_data : Image = noteline_array_image.get_rect(Rect2(0, 0, 16, 16)) - noteline_data.lock() - var i := 0 - var j := 0 + var hit_spots := [] + var release_spots := [] for note in active_notes: var position : float = (t+GameTheme.note_forecast_beats-note.time_hit)/GameTheme.note_forecast_beats - var scale := 1.0 - if note.hittable: - noteline_data.set_pixel( - i%16, i/16, Color( - position/arr_div.x, - float(note.column)/arr_div.y, - GameTheme.RADIAL_COL_ANGLES[note.column]/arr_div.z - ) - ) - i += 1 + hit_spots.append([position, note.column]) + var release = draw_note(mesh, note, position, t) + if release: + release_spots.append([position, note.column]) - if position < GameTheme.INNER_NOTE_CIRCLE_RATIO: - scale *= position/GameTheme.INNER_NOTE_CIRCLE_RATIO - position = GameTheme.INNER_NOTE_CIRCLE_RATIO - - var note_center = (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position * GameTheme.receptor_ring_radius) - var color: PoolColorArray - match note.type: - Note.NOTE_TAP: - color = GameTheme.color_array_tap(clamp((note.time_death-t)/Note.DEATH_DELAY, 0.0, 1.0), note.double_hit) - RadialMeshTools.make_tap_mesh(mesh, note_center, scale, color) - Note.NOTE_STAR: - color = GameTheme.color_array_star(clamp((note.time_death-t)/Note.DEATH_DELAY, 0.0, 1.0), note.double_hit) - var angle = fmod(t/note.duration, 1.0)*TAU - RadialMeshTools.make_star_mesh(mesh, note_center, scale, angle, 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 * 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 - if position_rel > 0: - var note_rel_center := (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position_rel * GameTheme.receptor_ring_radius) - noteline_data.set_pixel( - j%16, 15, Color( - position_rel/arr_div.x, - float(note.column)/arr_div.y, - GameTheme.RADIAL_COL_ANGLES[note.column]/arr_div.z - ) - ) - j += 1 - if position_rel < GameTheme.INNER_NOTE_CIRCLE_RATIO: - position_rel = GameTheme.INNER_NOTE_CIRCLE_RATIO - var note_center_rel = (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position_rel * GameTheme.receptor_ring_radius) - RadialMeshTools.make_hold_mesh(mesh, note_center, note_center_rel, scale, GameTheme.RADIAL_COL_ANGLES[note.column], color) - Note.NOTE_SLIDE: - var trail_alpha := 1.0 - if position < GameTheme.INNER_NOTE_CIRCLE_RATIO: - trail_alpha = 0.0 - elif position < 1.0: - trail_alpha = min(1.0, (position-GameTheme.INNER_NOTE_CIRCLE_RATIO)/(1-GameTheme.INNER_NOTE_CIRCLE_RATIO*2)) - else: - var trail_progress : float = clamp((t - note.time_hit - GameTheme.SLIDE_DELAY)/(note.duration - GameTheme.SLIDE_DELAY), 0.0, 1.0) - var star_pos : Vector2 = note.get_position(trail_progress) * GameTheme.receptor_ring_radius - var star_angle : float = note.get_angle(trail_progress) - RadialMeshTools.make_star_mesh(mesh, star_pos, 1.33, star_angle) - 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*GameTheme.slide_trail_alpha) + # In the absense of shader uniform arrays, we have to send our data via a texture + var noteline_data : Image = noteline_array_image.get_rect(Rect2(0, 0, 16, 16)) + noteline_data.lock() + var i := 0 + for spot in hit_spots: + noteline_data.set_pixel(i%16, i/16, Color(spot[0]/arr_div.x, float(spot[1])/arr_div.y, GameTheme.RADIAL_COL_ANGLES[spot[1]]/arr_div.z)) + i += 1 + i = 0 + for spot in release_spots: + noteline_data.set_pixel(i%16, 15, Color(spot[0]/arr_div.x, float(spot[1])/arr_div.y, GameTheme.RADIAL_COL_ANGLES[spot[1]]/arr_div.z)) + i += 1 noteline_data.unlock() + var noteline_data_tex := ImageTexture.new() noteline_data_tex.create_from_image(noteline_data, 0) notelines.set_texture(noteline_data_tex) + # The mesh that we've added all our note vertices and UVs to meshinstance.set_mesh(mesh) + # Another mesh for judgement texts var textmesh := ArrayMesh.new() for text in active_judgement_texts: RadialMeshTools.make_judgement_text(textmesh, RadialMeshTools.TextJudgement[text.judgement], text.col, (t-text.time)/GameTheme.judge_text_duration) @@ -288,12 +300,14 @@ func _init(): GameTheme.init_radial_values() initialise_scores() + func set_time(seconds: float): var msecs = OS.get_ticks_msec() time_zero_msec = msecs - (seconds * 1000) time = seconds t = game_time(time) + func make_noteline_mesh(vertices := 32) -> ArrayMesh: assert(vertices > 3) var rec_scale1 = (float(screen_height)/float(GameTheme.receptor_ring_radius))*0.5 @@ -318,6 +332,7 @@ func make_noteline_mesh(vertices := 32) -> ArrayMesh: mesh_playfield.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_FAN, arrays) return mesh_playfield + # Called when the node enters the scene tree for the first time. func _ready(): notelines.set_mesh(make_noteline_mesh()) @@ -334,6 +349,7 @@ func _ready(): meshinstance.material.set_shader_param('screen_size', get_viewport().get_size()) meshinstance.set_texture(GameTheme.tex_notes) + func load_track(song_key: String, difficulty_key: String): self.song_key = song_key set_time(-3.0) @@ -360,31 +376,39 @@ func load_track(song_key: String, difficulty_key: String): initialise_scores() # Remove old score + func stop(): MusicPlayer.stop() VideoPlayer.stop() # running = false next_note_to_load = 10000000 # Hacky but whatever + func intro_click(): SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, GameTheme.snd_count_in) + func get_realtime_precise() -> float: # Usually we only update the gametime once per process loop, but for input callbacks it's good to have msec precision return (OS.get_ticks_msec() - time_zero_msec)/1000.0 + func game_time(realtime: float) -> float: return realtime * bpm / 60.0 + func real_time(gametime: float) -> float: return gametime * 60.0 / bpm + func video_start_time() -> float: return -sync_offset_video + func audio_start_time() -> float: return -sync_offset_audio + # Called every frame. 'delta' is the elapsed time since the previous frame. var timers_set := false func _process(delta):