From a1b1471bd51f997ed692b862cff2c88adead085b Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Mon, 10 Nov 2025 15:37:47 -0500 Subject: [PATCH] fix scrobbling --- libs/background.py | 14 +-- scenes/game.py | 61 +++++++----- scenes/practice/game.py | 202 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 246 insertions(+), 31 deletions(-) diff --git a/libs/background.py b/libs/background.py index 72c7982..a72b1e4 100644 --- a/libs/background.py +++ b/libs/background.py @@ -113,16 +113,17 @@ class Background: if self.renda is not None: self.renda.add_renda() - def update(self, current_time_ms: float, bpm: float, gauge_1p, gauge_2p = None): + def update(self, current_time_ms: float, bpm: float, gauge_1p = None, gauge_2p = None): """ Update the background. Args: current_time_ms (float): The current time in milliseconds. bpm (float): The beats per minute. - gauge (Gauge): The gauge object. + gauge_1p (Gauge): The gauge object for player 1. + gauge_2p (Gauge): The gauge object for player 2. """ - if self.dancer is not None: + if self.dancer is not None and gauge_1p is not None: clear_threshold = gauge_1p.clear_start[min(gauge_1p.difficulty, 3)] if gauge_1p.gauge_length < clear_threshold: current_milestone = min(self.max_dancers - 1, int(gauge_1p.gauge_length / (clear_threshold / self.max_dancers))) @@ -132,15 +133,16 @@ class Background: self.dancer.add_dancer() self.last_milestone = current_milestone logger.info(f"Dancer milestone reached: {current_milestone}/{self.max_dancers}") - if self.bg_fever is not None: + if self.bg_fever is not None and gauge_1p is not None: if not self.is_clear and gauge_1p.is_clear: self.bg_fever.start() logger.info("Fever started") if not self.is_rainbow and gauge_1p.is_rainbow and self.fever is not None: self.fever.start() logger.info("Rainbow fever started") - self.is_clear = gauge_1p.is_clear - self.is_rainbow = gauge_1p.is_rainbow + if gauge_1p is not None: + self.is_clear = gauge_1p.is_clear + self.is_rainbow = gauge_1p.is_rainbow self.don_bg.update(current_time_ms, self.is_clear) if self.don_bg_2 is not None and gauge_2p is not None: self.don_bg_2.update(current_time_ms, gauge_2p.is_clear) diff --git a/scenes/game.py b/scenes/game.py index da73b33..9124c93 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -202,6 +202,19 @@ class GameScreen(Screen): self.movie.start(current_time) self.song_started = True + def pause_song(self): + self.paused = not self.paused + if self.paused: + if self.song_music is not None: + self.audio_time = audio.get_music_time_played(self.song_music) + audio.stop_music_stream(self.song_music) + self.pause_time = get_current_ms() - self.start_ms + else: + if self.song_music is not None: + audio.play_music_stream(self.song_music, 'music') + audio.seek_music_stream(self.song_music, self.audio_time) + self.start_ms = get_current_ms() - self.pause_time + def global_keys(self): if ray.is_key_pressed(get_key_code(global_data.config["keys"]["restart_key"])): if self.song_music is not None: @@ -216,17 +229,7 @@ class GameScreen(Screen): return self.on_screen_end('SONG_SELECT') if ray.is_key_pressed(ray.KeyboardKey.KEY_SPACE): - self.paused = not self.paused - if self.paused: - if self.song_music is not None: - self.audio_time = audio.get_music_time_played(self.song_music) - audio.stop_music_stream(self.song_music) - self.pause_time = get_current_ms() - self.start_ms - else: - if self.song_music is not None: - audio.play_music_stream(self.song_music, 'music') - audio.seek_music_stream(self.song_music, self.audio_time) - self.start_ms = get_current_ms() - self.pause_time + self.pause_song() def spawn_ending_anims(self): if global_data.session_data[global_data.player_num-1].result_data.bad == 0: @@ -266,7 +269,8 @@ class GameScreen(Screen): elif self.current_ms >= self.player_1.end_time: session_data = global_data.session_data[global_data.player_num-1] session_data.result_data.score, session_data.result_data.good, session_data.result_data.ok, session_data.result_data.bad, session_data.result_data.max_combo, session_data.result_data.total_drumroll = self.player_1.get_result_score() - session_data.result_data.gauge_length = self.player_1.gauge.gauge_length + if self.player_1.gauge is not None: + session_data.result_data.gauge_length = self.player_1.gauge.gauge_length if self.end_ms != 0: if current_time >= self.end_ms + 1000: if self.player_1.ending_anim is None: @@ -524,7 +528,8 @@ class Player: else: background.add_chibi(True, 1) self.bad_count += 1 - self.gauge.add_bad() + if self.gauge is not None: + self.gauge.add_bad() self.don_notes.popleft() if self.is_branch and self.branch_condition == 'p': self.branch_condition_count -= 1 @@ -537,7 +542,8 @@ class Player: else: background.add_chibi(True, 1) self.bad_count += 1 - self.gauge.add_bad() + if self.gauge is not None: + self.gauge.add_bad() self.kat_notes.popleft() if self.is_branch and self.branch_condition == 'p': self.branch_condition_count -= 1 @@ -718,7 +724,8 @@ class Player: self.score += self.base_score self.base_score_list.append(ScoreCounterAnimation(self.player_number, self.base_score, self.is_2p)) self.note_correct(curr_note, current_time) - self.gauge.add_good() + if self.gauge is not None: + self.gauge.add_good() if self.is_branch and self.branch_condition == 'p': self.branch_condition_count += 1 if background is not None: @@ -733,7 +740,8 @@ class Player: self.score += 10 * math.floor(self.base_score / 2 / 10) self.base_score_list.append(ScoreCounterAnimation(self.player_number, 10 * math.floor(self.base_score / 2 / 10), self.is_2p)) self.note_correct(curr_note, current_time) - self.gauge.add_ok() + if self.gauge is not None: + self.gauge.add_ok() if self.is_branch and self.branch_condition == 'p': self.branch_condition_count += 0.5 if background is not None: @@ -751,7 +759,8 @@ class Player: self.don_notes.popleft() else: self.kat_notes.popleft() - self.gauge.add_bad() + if self.gauge is not None: + self.gauge.add_bad() if background is not None: if self.is_2p: background.add_chibi(True, 2) @@ -905,7 +914,8 @@ class Player: self.autoplay_manager(ms_from_start, current_time, background) self.handle_input(ms_from_start, current_time, background) self.nameplate.update(current_time) - self.gauge.update(current_time) + if self.gauge is not None: + self.gauge.update(current_time) if self.judge_counter is not None: self.judge_counter.update(self.good_count, self.ok_count, self.bad_count, self.total_drumroll) if self.branch_indicator is not None: @@ -939,7 +949,10 @@ class Player: self.is_gogo_time = False self.gogo_time = None self.chara.set_animation('gogo_stop') - self.chara.update(current_time, self.bpm, self.gauge.is_clear, self.gauge.is_rainbow) + if self.gauge is None: + self.chara.update(current_time, self.bpm, False, False) + else: + self.chara.update(current_time, self.bpm, self.gauge.is_clear, self.gauge.is_rainbow) def draw_drumroll(self, current_ms: float, head: Drumroll, current_eighth: int): """Draws a drumroll in the player's lane""" @@ -1058,12 +1071,17 @@ class Player: for modifier in modifiers_to_draw: tex.draw_texture('lane', modifier, index=self.is_2p) + def draw_note_types(self, ms_from_start: float, start_ms: float): + self.draw_bars(ms_from_start) + self.draw_notes(ms_from_start, start_ms) + def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader, dan_transition = None): # Group 1: Background and lane elements tex.draw_texture('lane', 'lane_background', index=self.is_2p) if self.branch_indicator is not None: self.branch_indicator.draw() - self.gauge.draw() + if self.gauge is not None: + self.gauge.draw() if self.lane_hit_effect is not None: self.lane_hit_effect.draw() tex.draw_texture('lane', 'lane_hit_circle', index=self.is_2p) @@ -1075,8 +1093,7 @@ class Player: anim.draw() # Group 3: Notes and bars (game content) - self.draw_bars(ms_from_start) - self.draw_notes(ms_from_start, start_ms) + self.draw_note_types(ms_from_start, start_ms) if dan_transition is not None: dan_transition.draw() diff --git a/scenes/practice/game.py b/scenes/practice/game.py index 5e7140e..0af6c6f 100644 --- a/scenes/practice/game.py +++ b/scenes/practice/game.py @@ -1,8 +1,18 @@ +from collections import deque import logging from pathlib import Path +import pyray as ray +import copy + +from libs.animation import Animation +from libs.audio import audio from libs.background import Background -from scenes.game import GameScreen, JudgeCounter +from libs.global_data import Modifiers, global_data +from libs.tja import Balloon, Drumroll, TJAParser, apply_modifiers +from libs.utils import get_current_ms +from libs.texture import tex +from scenes.game import GameScreen, JudgeCounter, Player, SCREEN_WIDTH logger = logging.getLogger(__name__) @@ -12,5 +22,191 @@ class PracticeGameScreen(GameScreen): self.background = Background(1, self.bpm, scene_preset='PRACTICE') def init_tja(self, song: Path): - super().init_tja(song) - self.player_1.judge_counter = JudgeCounter() + """Initialize the TJA file""" + self.tja = TJAParser(song, start_delay=self.start_delay, distance=SCREEN_WIDTH - GameScreen.JUDGE_X) + self.scrolling_tja = TJAParser(song, start_delay=self.start_delay, distance=SCREEN_WIDTH - GameScreen.JUDGE_X) + global_data.session_data[0].song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) + if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None: + self.song_music = audio.load_music_stream(self.tja.metadata.wave, 'song') + self.player_1 = PracticePlayer(self.tja, global_data.player_num, global_data.session_data[global_data.player_num-1].selected_difficulty, False, global_data.modifiers[0]) + notes, branch_m, branch_e, branch_n = self.tja.notes_to_position(self.player_1.difficulty) + _, self.scroll_note_list, self.bars = apply_modifiers(notes, self.player_1.modifiers) + self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) + self.scrobble_index = 0 + self.scrobble_time = self.bars[self.scrobble_index].hit_ms + self.scrobble_move = Animation.create_move(200, total_distance=0) + + def pause_song(self): + self.paused = not self.paused + self.player_1.paused = self.paused + if self.paused: + if self.song_music is not None: + audio.stop_music_stream(self.song_music) + self.pause_time = get_current_ms() - self.start_ms + first_bar_time = self.bars[0].hit_ms + nearest_bar_index = 0 + min_distance = float('inf') + for i, bar in enumerate(self.bars): + bar_relative_time = bar.hit_ms - first_bar_time + distance = abs(bar_relative_time - self.current_ms) + if distance < min_distance: + min_distance = distance + nearest_bar_index = i + self.scrobble_index = nearest_bar_index - 1 + self.scrobble_time = self.bars[self.scrobble_index].hit_ms + else: + resume_bar_index = max(0, self.scrobble_index) + previous_bar_index = max(0, self.scrobble_index - 1) + + first_bar_time = self.bars[0].hit_ms + resume_time = self.bars[resume_bar_index].hit_ms - first_bar_time + self.start_delay + start_time = self.bars[previous_bar_index].hit_ms - first_bar_time + + tja_copy = copy.deepcopy(self.scrolling_tja) + self.player_1.tja = tja_copy + self.player_1.reset_chart() + + self.player_1.don_notes = deque([note for note in self.player_1.don_notes if note.hit_ms >= resume_time]) + self.player_1.kat_notes = deque([note for note in self.player_1.kat_notes if note.hit_ms >= resume_time]) + self.player_1.other_notes = deque([note for note in self.player_1.other_notes if note.hit_ms >= resume_time]) + self.player_1.draw_note_list = deque([note for note in self.player_1.draw_note_list if note.hit_ms >= resume_time]) + self.player_1.draw_bar_list = deque([note for note in self.player_1.draw_bar_list if note.hit_ms >= resume_time]) + self.player_1.total_notes = len([note for note in self.player_1.play_notes if 0 < note.type < 5]) + + self.pause_time = start_time + if self.song_music is not None: + audio.play_music_stream(self.song_music, 'music') + audio.seek_music_stream(self.song_music, (self.pause_time - self.start_delay)/1000 - self.tja.metadata.offset) + self.start_ms = get_current_ms() - self.pause_time + + def global_keys(self): + super().global_keys() + if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT) or ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT): + audio.play_sound('kat', 'sound') + + old_index = self.scrobble_index + if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT): + self.scrobble_index = (self.scrobble_index - 1) if self.scrobble_index > 0 else len(self.bars) - 1 + elif ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT): + self.scrobble_index = (self.scrobble_index + 1) % len(self.bars) + + time_difference = self.bars[self.scrobble_index].load_ms - self.bars[old_index].load_ms + + self.scrobble_move = Animation.create_move(400, total_distance=time_difference, ease_out='quadratic') + self.scrobble_move.start() + + def update(self): + super().update() + self.scrobble_move.update(get_current_ms()) + if self.scrobble_move.is_finished: + self.scrobble_time = self.bars[self.scrobble_index].hit_ms + self.scrobble_move.reset() + + def get_position_x(self, width: int, current_ms: float, load_ms: float, pixels_per_frame: float) -> int: + """Calculates the x-coordinate of a note based on its load time and current time""" + if self.paused: + time_diff = load_ms - self.scrobble_time - self.scrobble_move.attribute + else: + time_diff = load_ms - current_ms + return int(width + pixels_per_frame * 0.06 * time_diff - 64) + + def get_position_y(self, current_ms: float, load_ms: float, pixels_per_frame: float, pixels_per_frame_x) -> int: + """Calculates the y-coordinate of a note based on its load time and current time""" + time_diff = load_ms - current_ms + return int((pixels_per_frame * 0.06 * time_diff) + ((866 * pixels_per_frame) / pixels_per_frame_x)) + + def draw_drumroll(self, current_ms: float, head: Drumroll, current_eighth: int, index: int): + """Draws a drumroll in the player's lane""" + start_position = self.get_position_x(SCREEN_WIDTH, current_ms, head.load_ms, head.pixels_per_frame_x) + tail = self.scroll_note_list[index + 1] + is_big = int(head.type == 6) + end_position = self.get_position_x(SCREEN_WIDTH, current_ms, tail.load_ms, tail.pixels_per_frame_x) + length = end_position - start_position + color = ray.Color(255, head.color, head.color, 255) + if head.display: + if length > 0: + tex.draw_texture('notes', "8", frame=is_big, x=start_position+64, y=192, x2=length-47, color=color) + if is_big: + tex.draw_texture('notes', "drumroll_big_tail", x=end_position+64, y=192, color=color) + else: + tex.draw_texture('notes', "drumroll_tail", x=end_position+64, y=192, color=color) + tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=192, color=color) + + tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + 60, y=323, x2=length) + tex.draw_texture('notes', 'moji', frame=head.moji, x=(start_position - (168//2)) + 64, y=323) + tex.draw_texture('notes', 'moji', frame=tail.moji, x=(end_position - (168//2)) + 32, y=323) + + def draw_balloon(self, current_ms: float, head: Balloon, current_eighth: int, index: int): + """Draws a balloon in the player's lane""" + offset = 12 + start_position = self.get_position_x(SCREEN_WIDTH, current_ms, head.load_ms, head.pixels_per_frame_x) + tail = self.scroll_note_list[index + 1] + end_position = self.get_position_x(SCREEN_WIDTH, current_ms, tail.load_ms, tail.pixels_per_frame_x) + pause_position = 349 + if current_ms >= tail.hit_ms: + position = end_position + elif current_ms >= head.hit_ms: + position = pause_position + else: + position = start_position + if head.display: + tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset, y=192) + tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+128, y=192) + + def draw(self): + super().draw() + if self.paused: + # Batch bar draws by pre-calculating positions + bar_draws = [] + for bar in reversed(self.bars): + if not bar.display: + continue + x_position = self.get_position_x(SCREEN_WIDTH, self.current_ms, bar.load_ms, bar.pixels_per_frame_x) + y_position = self.get_position_y(self.current_ms, bar.load_ms, bar.pixels_per_frame_y, bar.pixels_per_frame_x) + if x_position < 236 or x_position > SCREEN_WIDTH: + continue + if hasattr(bar, 'is_branch_start'): + frame = 1 + else: + frame = 0 + bar_draws.append((str(bar.type), frame, x_position+60, y_position+190)) + + # Draw all bars in one batch + for bar_type, frame, x, y in bar_draws: + tex.draw_texture('notes', bar_type, frame=frame, x=x, y=y) + + for note in reversed(self.scroll_note_list): + if note.type == 8: + continue + + if isinstance(note, Drumroll): + self.draw_drumroll(self.current_ms, note, 0, note.index) + elif isinstance(note, Balloon) and not note.is_kusudama: + x_position = self.get_position_x(SCREEN_WIDTH, self.current_ms, note.load_ms, note.pixels_per_frame_x) + y_position = self.get_position_y(self.current_ms, note.load_ms, note.pixels_per_frame_y, note.pixels_per_frame_x) + if x_position < 236 or x_position > SCREEN_WIDTH: + continue + self.draw_balloon(self.current_ms, note, 0, note.index) + tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position) + else: + x_position = self.get_position_x(SCREEN_WIDTH, self.current_ms, note.load_ms, note.pixels_per_frame_x) + y_position = self.get_position_y(self.current_ms, note.load_ms, note.pixels_per_frame_y, note.pixels_per_frame_x) + if x_position < 236 or x_position > SCREEN_WIDTH: + continue + + if note.display: + tex.draw_texture('notes', str(note.type), x=x_position, y=y_position+192, center=True) + tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position) + + +class PracticePlayer(Player): + def __init__(self, tja: TJAParser, player_number: int, difficulty: int, is_2p: bool, modifiers: Modifiers): + super().__init__(tja, player_number, difficulty, is_2p, modifiers) + self.judge_counter = JudgeCounter() + self.gauge = None + self.paused = False + + def draw_note_types(self, ms_from_start: float, start_ms: float): + if not self.paused: + self.draw_bars(ms_from_start) + self.draw_notes(ms_from_start, start_ms)