From 9cc02741bc76cd224fcab790395b63096a336032 Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Wed, 29 Oct 2025 21:55:45 -0400 Subject: [PATCH] fix bugs --- PyTaiko.py | 6 +- libs/file_navigator.py | 258 +++++++++++++++++++++++++++++++++-------- libs/screen.py | 4 + libs/tja.py | 31 +++-- scenes/dan_select.py | 170 +++++++++++++++++++++++++++ scenes/game.py | 10 +- scenes/result.py | 4 +- scenes/song_select.py | 44 +++++-- 8 files changed, 441 insertions(+), 86 deletions(-) create mode 100644 scenes/dan_select.py diff --git a/PyTaiko.py b/PyTaiko.py index 74fa926..cf76c7e 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -29,6 +29,7 @@ from scenes.settings import SettingsScreen from scenes.song_select import SongSelectScreen from scenes.title import TitleScreen from scenes.two_player.song_select import TwoPlayerSongSelectScreen +from scenes.dan_select import DanSelectScreen logger = logging.getLogger(__name__) @@ -42,6 +43,7 @@ class Screens: RESULT = "RESULT" RESULT_2P = "RESULT_2P" SONG_SELECT_2P = "SONG_SELECT_2P" + DAN_SELECT = "DAN_SELECT" SETTINGS = "SETTINGS" DEV_MENU = "DEV_MENU" LOADING = "LOADING" @@ -156,6 +158,7 @@ def main(): result_screen_2p = TwoPlayerResultScreen('result') settings_screen = SettingsScreen('settings') dev_screen = DevScreen('dev') + dan_select_screen = DanSelectScreen('dan_select') screen_mapping = { Screens.ENTRY: entry_screen, @@ -168,6 +171,7 @@ def main(): Screens.RESULT_2P: result_screen_2p, Screens.SETTINGS: settings_screen, Screens.DEV_MENU: dev_screen, + Screens.DAN_SELECT: dan_select_screen, Screens.LOADING: load_screen } target = ray.load_render_texture(screen_width, screen_height) @@ -195,7 +199,7 @@ def main(): next_screen = screen.update() if screen.screen_init: ray.clear_background(ray.BLACK) - screen.draw() + screen._do_draw() if next_screen is not None: logger.info(f"Screen changed from {current_screen} to {next_screen}") diff --git a/libs/file_navigator.py b/libs/file_navigator.py index a0ebb2d..6c43ffe 100644 --- a/libs/file_navigator.py +++ b/libs/file_navigator.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass +import json import logging from pathlib import Path import random @@ -134,7 +136,8 @@ class SongBox: if not (-56 <= self.position <= 1280): self.reset() - def update(self, is_diff_select): + def update(self, is_diff_select: bool): + current_time = get_current_ms() self.is_diff_select = is_diff_select is_open_prev = self.is_open self.move_box() @@ -146,10 +149,10 @@ class SongBox: self.yellow_box.update(is_diff_select) if self.history_wait == 0: - self.history_wait = get_current_ms() + self.history_wait = current_time if self.score_history is None and {k: v for k, v in self.scores.items() if v is not None}: - self.score_history = ScoreHistory(self.scores, get_current_ms()) + self.score_history = ScoreHistory(self.scores, current_time) if not is_open_prev and self.is_open: if self.tja is not None or self.is_back: @@ -159,9 +162,9 @@ class SongBox: self.hori_name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5) self.open_anim.start() self.open_fade.start() - self.wait = get_current_ms() - if get_current_ms() >= self.history_wait + 3000: - self.history_wait = get_current_ms() + self.wait = current_time + if current_time >= self.history_wait + 3000: + self.history_wait = current_time if self.tja is None and self.texture_index != 17 and not audio.is_sound_playing('voice_enter'): audio.play_sound(f'genre_voice_{self.texture_index}', 'voice') elif not self.is_open and is_open_prev and audio.is_sound_playing(f'genre_voice_{self.texture_index}'): @@ -171,14 +174,14 @@ class SongBox: if self.box_texture is None and self.box_texture_path is not None: self.box_texture = ray.load_texture(self.box_texture_path) - self.open_anim.update(get_current_ms()) - self.open_fade.update(get_current_ms()) + self.open_anim.update(current_time) + self.open_fade.update(current_time) if self.name is None: self.name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5, vertical=True) if self.score_history is not None: - self.score_history.update(get_current_ms()) + self.score_history.update(current_time) def _draw_closed(self, x: int, y: int): @@ -195,6 +198,7 @@ class SongBox: if self.is_back: tex.draw_texture('box', 'back_text', x=x, y=y) + return elif self.name is not None: self.name.draw(outline_color=SongBox.OUTLINE_MAP.get(self.name_texture_index, ray.Color(101, 0, 82, 255)), x=x + 47 - int(self.name.texture.width / 2), y=y+35, y2=min(self.name.texture.height, 417)-self.name.texture.height) @@ -271,11 +275,12 @@ class SongBox: class YellowBox: """A song box when it is opened.""" - def __init__(self, name: OutlinedText, is_back: bool, tja: Optional[TJAParser] = None): + def __init__(self, name: Optional[OutlinedText], is_back: bool, tja: Optional[TJAParser] = None, is_dan: bool = False): self.is_diff_select = False self.name = name self.is_back = is_back self.tja = tja + self.is_dan = is_dan self.subtitle = None if self.tja is not None: subtitle_text = self.tja.metadata.subtitle.get(global_data.config['general']['language'], '') @@ -469,6 +474,8 @@ class YellowBox: def draw(self, song_box: SongBox, fade_override: Optional[float], is_ura: bool): self._draw_yellow_box() + if self.is_dan: + return if self.is_diff_select and self.tja is not None: self._draw_tja_data_diff(is_ura, song_box) else: @@ -481,6 +488,103 @@ class YellowBox: self._draw_text(song_box) +class DanBox: + def __init__(self, title: str, color: int, songs: list[tuple[TJAParser, int, int]], exams: list['Exam']): + self.position = -11111 + self.start_position = -1 + self.target_position = -1 + self.move = None + self.is_open = False + self.is_back = False + self.title = title + self.color = color + self.songs = songs + self.exams = exams + self.song_text: list[tuple[OutlinedText, OutlinedText]] = [] + self.name = None + self.yellow_box = None + + def move_box(self): + if self.position != self.target_position and self.move is None: + if self.position < self.target_position: + direction = 1 + else: + direction = -1 + if abs(self.target_position - self.position) > 250: + direction *= -1 + self.move = Animation.create_move(83.3*2, start_position=0, total_distance=300 * direction, ease_out='cubic') + self.move.start() + if self.is_open or self.target_position == BOX_CENTER + 150: + self.move.total_distance = 450 * direction + self.start_position = self.position + if self.move is not None: + self.move.update(get_current_ms()) + self.position = self.start_position + int(self.move.attribute) + if self.move.is_finished: + self.position = self.target_position + self.move = None + if not (-56 <= self.position <= 1280): + self.reset() + + def reset(self): + if self.name is None: + self.name.unload() + self.name = None + + def get_text(self): + if self.name is None: + self.name = OutlinedText(self.title, 40, ray.WHITE, outline_thickness=5, vertical=True) + if self.is_open and not self.song_text: + for song, genre, difficulty in self.songs: + title = song.metadata.title.get(global_data.config["general"]["language"], song.metadata.title["en"]) + subtitle = song.metadata.subtitle.get(global_data.config["general"]["language"], "") + title_text = OutlinedText(title, 40, ray.WHITE, outline_thickness=5, vertical=True) + font_size = 30 if len(subtitle) < 30 else 20 + subtitle_text = OutlinedText(subtitle, font_size, ray.WHITE, outline_thickness=5, vertical=True) + self.song_text.append((title_text, subtitle_text)) + + def update(self, is_diff_select: bool): + self.move_box() + self.get_text() + is_open_prev = self.is_open + self.is_open = self.position == BOX_CENTER + 150 + if not is_open_prev and self.is_open: + self.yellow_box = YellowBox(self.name, self.is_back, is_dan=True) + self.yellow_box.create_anim() + + if self.yellow_box is not None: + self.yellow_box.update(True) + + def _draw_closed(self, x: int, y: int): + tex.draw_texture('box', 'folder', frame=self.color, x=x) + if self.name is not None: + self.name.draw(outline_color=ray.BLACK, x=x + 47 - int(self.name.texture.width / 2), y=y+35, y2=min(self.name.texture.height, 417)-self.name.texture.height) + + def _draw_open(self, x: int, y: int, is_ura: bool): + if self.yellow_box is not None: + self.yellow_box.draw(x, y, False) + for i, song in enumerate(self.song_text): + title, subtitle = song + x = i * 140 + tex.draw_texture('yellow_box', 'genre_banner', x=x, frame=self.songs[i][1]) + tex.draw_texture('yellow_box', 'difficulty', x=x, frame=self.songs[i][2]) + tex.draw_texture('yellow_box', 'difficulty_x', x=x) + tex.draw_texture('yellow_box', 'difficulty_star', x=x) + level = self.songs[i][0].metadata.course_data[self.songs[i][2]].level + counter = str(level) + total_width = len(counter) * 10 + for i in range(len(counter)): + tex.draw_texture('yellow_box', 'difficulty_num', frame=int(counter[i]), x=x-(total_width // 2) + (i * 10)) + + title.draw(outline_color=ray.BLACK, x=665+x, y=127, y2=min(title.texture.height, 400)-title.texture.height) + subtitle.draw(outline_color=ray.BLACK, x=620+x, y=525-min(subtitle.texture.height, 400), y2=min(subtitle.texture.height, 400)-subtitle.texture.height) + + def draw(self, x: int, y: int, is_ura: bool): + if self.is_open: + self._draw_open(x, y, is_ura) + else: + self._draw_closed(x, y) + class GenreBG: """The background for a genre box.""" def __init__(self, start_box: SongBox, end_box: SongBox, title: OutlinedText, diff_sort: Optional[int]): @@ -651,6 +755,34 @@ class FileSystemItem: self.path = path self.name = name +def parse_box_def(path: Path): + """Parse box.def file for directory metadata""" + texture_index = SongBox.DEFAULT_INDEX + name = path.name + collection = None + encoding = test_encodings(path / "box.def") + + try: + with open(path / "box.def", 'r', encoding=encoding) as box_def: + for line in box_def: + line = line.strip() + if line.startswith("#GENRE:"): + genre = line.split(":", 1)[1].strip() + texture_index = FileSystemItem.GENRE_MAP.get(genre, SongBox.DEFAULT_INDEX) + if texture_index == SongBox.DEFAULT_INDEX: + texture_index = FileSystemItem.GENRE_MAP_2.get(genre, SongBox.DEFAULT_INDEX) + elif line.startswith("#TITLE:"): + name = line.split(":", 1)[1].strip() + elif line.startswith("#TITLEJA:"): + if global_data.config['general']['language'] == 'ja': + name = line.split(":", 1)[1].strip() + elif line.startswith("#COLLECTION"): + collection = line.split(":", 1)[1].strip() + except Exception as e: + logger.error(f"Error parsing box.def in {path}: {e}") + + return name, texture_index, collection + class Directory(FileSystemItem): """Represents a directory in the navigation system""" COLLECTIONS = [ @@ -690,6 +822,42 @@ class SongFile(FileSystemItem): self.box.hash = global_data.song_hashes[self.hash][0]["diff_hashes"] self.box.get_scores() +@dataclass +class Exam: + type: str + red: int + gold: int + range: str + +class DanCourse(FileSystemItem): + def __init__(self, path: Path, name: str): + super().__init__(path, name) + if name != "dan.json": + self.logging.error(f"Invalid dan course file: {path}") + with open(path, 'r') as f: + data = json.load(f) + title = data["title"] + color = data["color"] + songs = [] + for song in data["songs"]: + hash = song["hash"] + song_title = song["title"] + song_subtitle = song["subtitle"] + difficulty = song["difficulty"] + if hash in global_data.song_hashes: + path = Path(global_data.song_hashes[hash][0]["file_path"]) + if (path.parent.parent / "box.def").exists(): + _, genre_index, _ = parse_box_def(path.parent.parent) + songs.append((TJAParser(path), genre_index, difficulty)) + else: + pass + #do something with song_title, song_subtitle + exams = [] + for exam in data["exams"]: + exams.append(Exam(exam["type"], exam["red"], exam["gold"], exam["range"])) + + self.box = DanBox(title, color, songs, exams) + class FileNavigator: """Manages navigation through pre-generated Directory and SongFile objects""" def __init__(self): @@ -717,6 +885,7 @@ class FileNavigator: self.box_open = False self.genre_bg = None self.song_count = 0 + self.in_dan_select = False logger.info("FileNavigator initialized") def initialize(self, root_dirs: list[Path]): @@ -796,7 +965,7 @@ class FileNavigator: box_texture = None collection = None - name, texture_index, collection = self._parse_box_def(dir_path) + name, texture_index, collection = parse_box_def(dir_path) box_png_path = dir_path / "box.png" if box_png_path.exists(): box_texture = str(box_png_path) @@ -847,7 +1016,10 @@ class FileNavigator: # Create SongFile objects for tja_path in sorted(tja_files): song_key = str(tja_path) - if song_key not in self.all_song_files and tja_path in global_data.song_paths: + if song_key not in self.all_song_files and tja_path.name == "dan.json": + song_obj = DanCourse(tja_path, tja_path.name) + self.all_song_files[song_key] = song_obj + elif song_key not in self.all_song_files and tja_path in global_data.song_paths: song_obj = SongFile(tja_path, tja_path.name, texture_index) song_obj.box.get_scores() for course in song_obj.tja.metadata.course_data: @@ -872,10 +1044,10 @@ class FileNavigator: self.diff_sort_statistics[course][level][1] += 1 elif is_cleared: self.diff_sort_statistics[course][level][2] += 1 - self.song_count += 1 - global_data.song_progress = self.song_count / global_data.total_songs if song_obj.is_recent: self.new_items.append(SongFile(tja_path, tja_path.name, SongBox.DEFAULT_INDEX, name_texture_index=texture_index)) + self.song_count += 1 + global_data.song_progress = self.song_count / global_data.total_songs self.all_song_files[song_key] = song_obj if song_key in self.all_song_files: @@ -914,7 +1086,7 @@ class FileNavigator: # Determine if current directory has child directories with box.def has_children = False - if self.is_at_root(): + if self.is_at_root() or selected_item and selected_item.box.texture_index == 13: has_children = True # Root always has "children" (the root directories) else: has_children = any(item.is_dir() and (item / "box.def").exists() @@ -987,7 +1159,7 @@ class FileNavigator: temp_items.append(item) content_items = random.sample(temp_items, 10) - if content_items == [] or (selected_item is not None and selected_item.box.texture_index == 13): + if content_items == []: self.go_back() return i = 1 @@ -1060,8 +1232,7 @@ class FileNavigator: self.load_current_directory(selected_item=selected_item) - elif isinstance(selected_item, SongFile): - return selected_item + return selected_item def go_back(self): """Navigate back to the previous directory""" @@ -1109,6 +1280,8 @@ class FileNavigator: song_key = str(tja_path) if song_key in self.all_song_files: song_obj = self.all_song_files[song_key] + if not isinstance(song_obj, SongFile): + continue for diff in song_obj.box.scores: if diff not in all_scores: all_scores[diff] = [] @@ -1136,7 +1309,7 @@ class FileNavigator: tja_files: list[Path] = [] for path in directory.iterdir(): - if path.is_file() and path.suffix.lower() == ".tja": + if (path.is_file() and path.suffix.lower() == ".tja") or path.name == "dan.json": tja_files.append(path) elif path.is_dir(): # Only recurse into subdirectories that don't have box.def @@ -1163,34 +1336,6 @@ class FileNavigator: return tja_files - def _parse_box_def(self, path: Path): - """Parse box.def file for directory metadata""" - texture_index = SongBox.DEFAULT_INDEX - name = path.name - collection = None - encoding = test_encodings(path / "box.def") - - try: - with open(path / "box.def", 'r', encoding=encoding) as box_def: - for line in box_def: - line = line.strip() - if line.startswith("#GENRE:"): - genre = line.split(":", 1)[1].strip() - texture_index = FileSystemItem.GENRE_MAP.get(genre, SongBox.DEFAULT_INDEX) - if texture_index == SongBox.DEFAULT_INDEX: - texture_index = FileSystemItem.GENRE_MAP_2.get(genre, SongBox.DEFAULT_INDEX) - elif line.startswith("#TITLE:"): - name = line.split(":", 1)[1].strip() - elif line.startswith("#TITLEJA:"): - if global_data.config['general']['language'] == 'ja': - name = line.split(":", 1)[1].strip() - elif line.startswith("#COLLECTION"): - collection = line.split(":", 1)[1].strip() - except Exception as e: - logger.error(f"Error parsing box.def in {path}: {e}") - - return name, texture_index, collection - def _read_song_list(self, path: Path): """Read and process song_list.txt file""" tja_files: list[Path] = [] @@ -1251,13 +1396,24 @@ class FileNavigator: elif offset < -len(self.items) // 2: offset += len(self.items) - position = BOX_CENTER + (100 * offset) + # Adjust spacing based on dan select mode + base_spacing = 100 + center_offset = 150 + side_offset_l = 0 + side_offset_r = 300 + + if self.in_dan_select: + base_spacing = 150 + side_offset_l = 200 + side_offset_r = 500 + + position = BOX_CENTER + (base_spacing * offset) if position == BOX_CENTER: - position += 150 + position += center_offset elif position > BOX_CENTER: - position += 300 + position += side_offset_r else: - position -= 0 + position -= side_offset_l if item.box.position == -11111: item.box.position = position diff --git a/libs/screen.py b/libs/screen.py index c11e08f..31b349f 100644 --- a/libs/screen.py +++ b/libs/screen.py @@ -39,3 +39,7 @@ class Screen: def draw(self): pass + + def _do_draw(self): + if self.screen_init: + self.draw() diff --git a/libs/tja.py b/libs/tja.py index 1d0dee1..b917bd4 100644 --- a/libs/tja.py +++ b/libs/tja.py @@ -495,17 +495,15 @@ class TJAParser: def get_moji(self, play_note_list: list[Note], ms_per_measure: float) -> None: """ Assign 口唱歌 (note phoneticization) to notes. - Args: play_note_list (list[Note]): The list of notes to process. ms_per_measure (float): The duration of a measure in milliseconds. - Returns: None """ se_notes = { - 1: [0, 1, 2], # Note '1' has three possible sound effects - 2: [3, 4], # Note '2' has two possible sound effects + 1: 0, + 2: 3, 3: 5, 4: 6, 5: 7, @@ -514,10 +512,8 @@ class TJAParser: 8: 10, 9: 11 } - if len(play_note_list) <= 1: return - current_note = play_note_list[-1] if current_note.type == 1: current_note.moji = 0 @@ -525,44 +521,43 @@ class TJAParser: current_note.moji = 3 else: current_note.moji = se_notes[current_note.type] - prev_note = play_note_list[-2] - - if prev_note.type in {1, 2}: + if prev_note.type == 1: timing_threshold = ms_per_measure / 8 - 1 if current_note.hit_ms - prev_note.hit_ms <= timing_threshold: - prev_note.moji = se_notes[prev_note.type][1] + prev_note.moji = 1 else: - prev_note.moji = se_notes[prev_note.type][0] + prev_note.moji = 0 + elif prev_note.type == 2: + timing_threshold = ms_per_measure / 8 - 1 + if current_note.hit_ms - prev_note.hit_ms <= timing_threshold: + prev_note.moji = 4 + else: + prev_note.moji = 3 else: prev_note.moji = se_notes[prev_note.type] - if len(play_note_list) > 3: notes_minus_4 = play_note_list[-4] notes_minus_3 = play_note_list[-3] notes_minus_2 = play_note_list[-2] - consecutive_ones = ( notes_minus_4.type == 1 and notes_minus_3.type == 1 and notes_minus_2.type == 1 ) - if consecutive_ones: rapid_timing = ( notes_minus_3.hit_ms - notes_minus_4.hit_ms < (ms_per_measure / 8) and notes_minus_2.hit_ms - notes_minus_3.hit_ms < (ms_per_measure / 8) ) - if rapid_timing: if len(play_note_list) > 5: spacing_before = play_note_list[-4].hit_ms - play_note_list[-5].hit_ms >= (ms_per_measure / 8) spacing_after = play_note_list[-1].hit_ms - play_note_list[-2].hit_ms >= (ms_per_measure / 8) - if spacing_before and spacing_after: - play_note_list[-3].moji = se_notes[1][2] + play_note_list[-3].moji = 2 else: - play_note_list[-3].moji = se_notes[1][2] + play_note_list[-3].moji = 2 def notes_to_position(self, diff: int): """Parse a TJA's notes into a NoteList.""" diff --git a/scenes/dan_select.py b/scenes/dan_select.py new file mode 100644 index 0000000..0c32742 --- /dev/null +++ b/scenes/dan_select.py @@ -0,0 +1,170 @@ +import pyray as ray + +from libs.audio import audio +from libs.global_data import global_data +from libs.texture import tex +from libs.chara_2d import Chara2D +from libs.global_objects import AllNetIcon, CoinOverlay, Indicator, Nameplate, Timer +from libs.screen import Screen +from libs.file_navigator import navigator +from libs.utils import get_current_ms, is_l_don_pressed, is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed +from scenes.song_select import SongSelectScreen, State + + +class DanSelectScreen(Screen): + def on_screen_start(self): + super().on_screen_start() + self.navigator = navigator + self.navigator.in_dan_select = True + self.navigator.select_current_item() + self.coin_overlay = CoinOverlay() + self.allnet_indicator = AllNetIcon() + self.timer = Timer(60, get_current_ms(), self.navigator.select_current_item) + self.indicator = Indicator(Indicator.State.SELECT) + self.player = DanSelectPlayer(str(global_data.player_num)) + self.state = State.BROWSING + self.last_moved = 0 + + def on_screen_end(self, next_screen: str): + return super().on_screen_end(next_screen) + + def handle_input_browsing(self): + """Handle input for browsing songs.""" + action = self.player.handle_input_browsing(self.last_moved, self.navigator.items[self.navigator.selected_index] if self.navigator.items else None) + current_time = get_current_ms() + if action == "skip_left": + for _ in range(10): + self.navigator.navigate_left() + self.last_moved = current_time + elif action == "skip_right": + for _ in range(10): + self.navigator.navigate_right() + self.last_moved = current_time + elif action == "navigate_left": + self.navigator.navigate_left() + self.last_moved = current_time + elif action == "navigate_right": + self.navigator.navigate_right() + self.last_moved = current_time + elif action == "go_back": + self.navigator.go_back() + elif action == "select_song": + pass + + def handle_input(self, state, screen): + """Main input dispatcher. Delegates to state-specific handlers.""" + if state == State.BROWSING: + screen.handle_input_browsing() + elif state == State.SONG_SELECTED: + screen.handle_input_selected() + + def update(self): + super().update() + current_time = get_current_ms() + self.indicator.update(current_time) + self.timer.update(current_time) + for song in self.navigator.items: + song.box.update(False) + song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150 + self.player.update(current_time) + self.handle_input(self.state, self) + + def draw(self): + tex.draw_texture('global', 'bg') + tex.draw_texture('global', 'bg_header') + tex.draw_texture('global', 'bg_footer') + tex.draw_texture('global', 'footer') + for item in self.navigator.items: + box = item.box + if -156 <= box.position <= 1280 + 144: + if box.position <= 500: + box.draw(box.position, 95, False) + else: + box.draw(box.position, 95, False) + self.player.draw() + self.indicator.draw(410, 575) + self.timer.draw() + self.coin_overlay.draw() + tex.draw_texture('global', 'dan_select') + self.allnet_indicator.draw() + +class DanSelectPlayer: + def __init__(self, player_num: str): + self.player_num = player_num + self.selected_difficulty = -3 + self.prev_diff = -3 + self.selected_song = False + self.is_ura = False + self.ura_toggle = 0 + self.diff_select_move_right = False + self.neiro_selector = None + self.modifier_selector = None + + # Player-specific objects + self.chara = Chara2D(int(self.player_num) - 1, 100) + plate_info = global_data.config[f'nameplate_{self.player_num}p'] + self.nameplate = Nameplate(plate_info['name'], plate_info['title'], + int(self.player_num), plate_info['dan'], plate_info['gold']) + + def update(self, current_time): + """Update player state""" + self.nameplate.update(current_time) + self.chara.update(current_time, 100, False, False) + + def handle_input_browsing(self, last_moved, selected_item): + """Handle input for browsing songs. Returns action string or None.""" + current_time = get_current_ms() + + # Skip left (fast navigate) + if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT_CONTROL) or (is_l_kat_pressed(self.player_num) and current_time <= last_moved + 50): + audio.play_sound('skip', 'sound') + return "skip_left" + + # Skip right (fast navigate) + if ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT_CONTROL) or (is_r_kat_pressed(self.player_num) and current_time <= last_moved + 50): + audio.play_sound('skip', 'sound') + return "skip_right" + + # Navigate left + if is_l_kat_pressed(self.player_num): + audio.play_sound('kat', 'sound') + return "navigate_left" + + # Navigate right + if is_r_kat_pressed(self.player_num): + audio.play_sound('kat', 'sound') + return "navigate_right" + + # Select/Enter + if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num): + if selected_item is not None and selected_item.box.is_back: + audio.play_sound('cancel', 'sound') + return "go_back" + else: + return "select_song" + + return None + + def handle_input(self, state, screen): + """Main input dispatcher. Delegates to state-specific handlers.""" + if self.is_voice_playing(): + return + + if state == State.BROWSING: + screen.handle_input_browsing() + elif state == State.SONG_SELECTED: + screen.handle_input_selected() + + def handle_input_selected(self, current_item): + """Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None""" + if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num): + return "confirm" + return None + + def draw(self): + if self.player_num == '1': + self.nameplate.draw(30, 640) + self.chara.draw(x=-50, y=410) + else: + self.nameplate.draw(950, 640) + self.chara.draw(mirror=True, x=950, y=410) diff --git a/scenes/game.py b/scenes/game.py index 6c9f3b4..a4ad8e9 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -2159,7 +2159,6 @@ class Gauge: self.is_rainbow = False self.table = [ [ - None, {"clear_rate": 36.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 38.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 38.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, @@ -2167,7 +2166,6 @@ class Gauge: {"clear_rate": 44.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, ], [ - None, {"clear_rate": 45.939, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 45.939, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 48.676, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, @@ -2177,7 +2175,6 @@ class Gauge: {"clear_rate": 52.5, "ok_multiplier": 0.75, "bad_multiplier": -1.0}, ], [ - None, {"clear_rate": 54.325, "ok_multiplier": 0.75, "bad_multiplier": -0.75}, {"clear_rate": 54.325, "ok_multiplier": 0.75, "bad_multiplier": -0.75}, {"clear_rate": 50.774, "ok_multiplier": 0.75, "bad_multiplier": -1.0}, @@ -2188,7 +2185,6 @@ class Gauge: {"clear_rate": 48.120, "ok_multiplier": 0.75, "bad_multiplier": -1.25}, ], [ - None, {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6}, {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6}, {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6}, @@ -2209,7 +2205,7 @@ class Gauge: """Adds a good note to the gauge""" self.gauge_update_anim.start() self.previous_length = int(self.gauge_length) - self.gauge_length += (1 / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) + self.gauge_length += (1 / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level-1]["clear_rate"])) if self.gauge_length > self.gauge_max: self.gauge_length = self.gauge_max @@ -2217,14 +2213,14 @@ class Gauge: """Adds an ok note to the gauge""" self.gauge_update_anim.start() self.previous_length = int(self.gauge_length) - self.gauge_length += ((1 * self.table[self.difficulty][self.level]["ok_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) + self.gauge_length += ((1 * self.table[self.difficulty][self.level-1]["ok_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level-1]["clear_rate"])) if self.gauge_length > self.gauge_max: self.gauge_length = self.gauge_max def add_bad(self): """Adds a bad note to the gauge""" self.previous_length = int(self.gauge_length) - self.gauge_length += ((1 * self.table[self.difficulty][self.level]["bad_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) + self.gauge_length += ((1 * self.table[self.difficulty][self.level-1]["bad_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level-1]["clear_rate"])) if self.gauge_length < 0: self.gauge_length = 0 diff --git a/scenes/result.py b/scenes/result.py index 67fc91a..b8535ea 100644 --- a/scenes/result.py +++ b/scenes/result.py @@ -121,8 +121,8 @@ class ResultPlayer: self.score_animator = ScoreAnimator(session_data.result_score) plate_info = global_data.config[f'nameplate_{self.player_num}p'] self.nameplate = Nameplate(plate_info['name'], plate_info['title'], int(self.player_num), plate_info['dan'], plate_info['gold']) - self.score, self.good, self.ok, self.bad, self.max_combo, self.total_drumroll= '', '', '', '', '', '' - self.update_list = [['score', session_data.result_score], + self.score, self.good, self.ok, self.bad, self.max_combo, self.total_drumroll = '', '', '', '', '', '' + self.update_list: list[tuple[str, int]] = [['score', session_data.result_score], ['good', session_data.result_good], ['ok', session_data.result_ok], ['bad', session_data.result_bad], diff --git a/scenes/song_select.py b/scenes/song_select.py index f4c1b4b..eb83725 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -64,6 +64,7 @@ class SongSelectScreen(Screen): self.timer_selected = Timer(40, get_current_ms(), self._confirm_selection_wrapper) self.screen_init = True self.ura_switch_animation = UraSwitchAnimation() + self.dan_transition = DanTransition() self.player_1 = SongSelectPlayer(str(global_data.player_num), self.text_fade_in) @@ -88,7 +89,9 @@ class SongSelectScreen(Screen): self.reset_demo_music() self.finalize_song() self.player_1.nameplate.unload() - self.navigator.get_current_item().box.yellow_box.create_anim() + current_item = self.navigator.get_current_item() + if current_item.box.yellow_box is not None: + current_item.box.yellow_box.create_anim() return super().on_screen_end(next_screen) def reset_demo_music(self): @@ -130,8 +133,13 @@ class SongSelectScreen(Screen): self.text_fade_in.start() self.text_fade_out.start() elif action == "select_song": + current_song = self.navigator.get_current_item() + if isinstance(current_song, Directory) and current_song.box.texture_index == 13: + self.dan_transition.start() + audio.stop_sound('bgm') + return selected_song = self.navigator.select_current_item() - if selected_song: + if isinstance(selected_song, SongFile): self.state = State.SONG_SELECTED self.player_1.on_song_selected(selected_song) audio.play_sound('don', 'sound') @@ -240,6 +248,9 @@ class SongSelectScreen(Screen): self.indicator.update(current_time) self.blue_arrow_fade.update(current_time) self.blue_arrow_move.update(current_time) + self.dan_transition.update(current_time) + if self.dan_transition.is_finished: + return self.on_screen_end('DAN_SELECT') next_screen = self.update_players(current_time) @@ -353,6 +364,8 @@ class SongSelectScreen(Screen): if self.game_transition is not None: self.game_transition.draw() + if self.dan_transition.is_started: + self.dan_transition.draw() self.allnet_indicator.draw() class SongSelectPlayer: @@ -998,7 +1011,7 @@ class NeiroSelector: self.move_sideways.start() self.fade_sideways.start() self.text_2.unload() - self.text_2 = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE, ray.BLACK) + self.text_2 = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE) self.direction = -1 if self.selected_sound == len(self.sounds): return @@ -1013,7 +1026,7 @@ class NeiroSelector: self.move_sideways.start() self.fade_sideways.start() self.text_2.unload() - self.text_2 = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE, ray.BLACK) + self.text_2 = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE) self.direction = 1 if self.selected_sound == len(self.sounds): return @@ -1037,7 +1050,7 @@ class NeiroSelector: self.fade_sideways.update(current_ms) if self.move_sideways.is_finished: self.text.unload() - self.text = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE, ray.BLACK) + self.text = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE) self.is_finished = self.move.is_finished and self.is_confirmed def draw(self): @@ -1208,8 +1221,7 @@ class ModifierSelector: else: tex.draw_texture('modifier', 'mod_bg', y=move + (i*50), x=x) tex.draw_texture('modifier', 'mod_box', y=move + (i*50), x=x) - dest = ray.Rectangle(92 + x, 819 + move + (i*50), self.text_name[i].texture.width, self.text_name[i].texture.height) - self.text_name[i].draw(self.text_name[i].default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE) + self.text_name[i].draw(outline_color=ray.BLACK, x=92 + x, y=819 + move + (i*50)) current_mod = self.mods[i] current_value = getattr(global_data.modifiers[int(self.player_num)-1], current_mod.name) @@ -1255,3 +1267,21 @@ class ModifierSelector: if i == self.current_mod_index: tex.draw_texture('modifier', 'blue_arrow', y=move + (i*50), x=x-self.blue_arrow_move.attribute, fade=self.blue_arrow_fade.attribute) tex.draw_texture('modifier', 'blue_arrow', y=move + (i*50), x=x+110 + self.blue_arrow_move.attribute, mirror='horizontal', fade=self.blue_arrow_fade.attribute) + +class DanTransition: + def __init__(self): + self.slide_in = tex.get_animation(38) + self.is_finished = False + self.is_started = False + + def start(self): + self.slide_in.start() + self.is_started = True + + def update(self, current_time_ms: float): + self.slide_in.update(current_time_ms) + if self.slide_in.is_finished: + self.is_finished = True + + def draw(self): + tex.draw_texture('dan_transition', 'background', x2=self.slide_in.attribute)