From 8b7ba3acbd852f7dd7375302b7d58a1f54743a46 Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Fri, 24 Oct 2025 00:08:53 -0400 Subject: [PATCH] Add 2P --- PyTaiko.py | 10 +- config.toml | 8 +- libs/file_navigator.py | 1327 ++++++++++++++++++ libs/global_data.py | 41 +- libs/utils.py | 68 +- scenes/entry.py | 307 +++-- scenes/game.py | 58 +- scenes/loading.py | 8 +- scenes/result.py | 421 +++--- scenes/song_select.py | 2138 +++++++----------------------- scenes/two_player/game.py | 79 +- scenes/two_player/result.py | 36 + scenes/two_player/song_select.py | 166 +++ 13 files changed, 2597 insertions(+), 2070 deletions(-) create mode 100644 libs/file_navigator.py create mode 100644 scenes/two_player/result.py create mode 100644 scenes/two_player/song_select.py diff --git a/PyTaiko.py b/PyTaiko.py index 6e42251..45d1941 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -20,11 +20,13 @@ from scenes.devtest import DevScreen from scenes.entry import EntryScreen from scenes.game import GameScreen from scenes.two_player.game import TwoPlayerGameScreen +from scenes.two_player.result import TwoPlayerResultScreen from scenes.loading import LoadScreen from scenes.result import ResultScreen from scenes.settings import SettingsScreen from scenes.song_select import SongSelectScreen from scenes.title import TitleScreen +from scenes.two_player.song_select import TwoPlayerSongSelectScreen class Screens: @@ -34,6 +36,8 @@ class Screens: GAME = "GAME" GAME_2P = "GAME_2P" RESULT = "RESULT" + RESULT_2P = "RESULT_2P" + SONG_SELECT_2P = "SONG_SELECT_2P" SETTINGS = "SETTINGS" DEV_MENU = "DEV_MENU" LOADING = "LOADING" @@ -107,10 +111,12 @@ def main(): title_screen = TitleScreen() entry_screen = EntryScreen() song_select_screen = SongSelectScreen() - load_screen = LoadScreen(song_select_screen) + song_select_screen_2p = TwoPlayerSongSelectScreen() + load_screen = LoadScreen() game_screen = GameScreen() game_screen_2p = TwoPlayerGameScreen() result_screen = ResultScreen() + result_screen_2p = TwoPlayerResultScreen() settings_screen = SettingsScreen() dev_screen = DevScreen() @@ -118,9 +124,11 @@ def main(): Screens.ENTRY: entry_screen, Screens.TITLE: title_screen, Screens.SONG_SELECT: song_select_screen, + Screens.SONG_SELECT_2P: song_select_screen_2p, Screens.GAME: game_screen, Screens.GAME_2P: game_screen_2p, Screens.RESULT: result_screen, + Screens.RESULT_2P: result_screen_2p, Screens.SETTINGS: settings_screen, Screens.DEV_MENU: dev_screen, Screens.LOADING: load_screen diff --git a/config.toml b/config.toml index c3188cc..3348efd 100644 --- a/config.toml +++ b/config.toml @@ -8,12 +8,18 @@ touch_enabled = false timer_frozen = true judge_counter = false -[nameplate] +[nameplate_1p] name = 'どんちゃん' title = 'ドンだーデビュー!' dan = -1 gold = false +[nameplate_2p] +name = 'かつちゃん' +title = 'ドンだーデビュー!' +dan = -1 +gold = false + [paths] tja_path = ['Songs'] video_path = ['Videos'] diff --git a/libs/file_navigator.py b/libs/file_navigator.py new file mode 100644 index 0000000..0337e15 --- /dev/null +++ b/libs/file_navigator.py @@ -0,0 +1,1327 @@ +from pathlib import Path +import random +from typing import Optional, Union +from libs.audio import audio +from libs.animation import Animation, MoveAnimation +from libs.tja import TJAParser, test_encodings +from libs.texture import tex +from libs.utils import OutlinedText, get_current_ms, global_data +from datetime import datetime, timedelta +import sqlite3 +import pyray as ray + +BOX_CENTER = 444 + +class SongBox: + """A box for the song select screen.""" + OUTLINE_MAP = { + 1: ray.Color(0, 77, 104, 255), + 2: ray.Color(156, 64, 2, 255), + 3: ray.Color(84, 101, 126, 255), + 4: ray.Color(153, 4, 46, 255), + 5: ray.Color(60, 104, 0, 255), + 6: ray.Color(134, 88, 0, 255), + 7: ray.Color(79, 40, 134, 255), + 8: ray.Color(148, 24, 0, 255), + 9: ray.Color(101, 0, 82, 255), + 10: ray.Color(140, 39, 92, 255), + 11: ray.Color(151, 57, 30, 255), + 12: ray.Color(35, 123, 103, 255), + 13: ray.Color(25, 68, 137, 255), + 14: ray.Color(157, 13, 31, 255) + } + BACK_INDEX = 17 + DEFAULT_INDEX = 9 + def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None, + tja_count: Optional[int] = None, box_texture: Optional[str] = None, name_texture_index: Optional[int] = None): + self.text_name = name + self.texture_index = texture_index + if name_texture_index is None: + self.name_texture_index = texture_index + else: + self.name_texture_index = name_texture_index + self.box_texture_path = box_texture + self.box_texture = None + self.scores = dict() + self.crown = dict() + self.position = -11111 + self.start_position = -1 + self.target_position = -1 + self.is_open = False + self.is_back = self.texture_index == SongBox.BACK_INDEX + if self.is_back: + for i in range(1, 16): + if audio.is_sound_playing(f'genre_voice_{i}'): + audio.stop_sound(f'genre_voice_{i}') + self.name = None + self.black_name = None + self.hori_name = None + self.yellow_box = None + self.open_anim = Animation.create_move(133, start_position=0, total_distance=150, delay=83.33) + self.open_fade = Animation.create_fade(200, initial_opacity=0, final_opacity=1.0) + self.move = None + self.wait = 0 + self.is_dir = is_dir + self.tja_count = tja_count + self.tja_count_text = None + self.score_history = None + self.history_wait = 0 + self.tja = tja + self.hash = dict() + self.is_favorite = False + + def reset(self): + if self.yellow_box is not None: + self.yellow_box.reset() + self.yellow_box.create_anim() + if self.name is not None: + self.name.unload() + self.name = None + if self.box_texture is not None: + ray.unload_texture(self.box_texture) + self.box_texture = None + if self.black_name is not None: + self.black_name.unload() + self.black_name = None + if self.hori_name is not None: + self.hori_name.unload() + self.hori_name = None + self.is_open = False + + def get_scores(self): + if self.tja is None: + return + with sqlite3.connect('scores.db') as con: + cursor = con.cursor() + # Batch database query for all diffs at once + if self.tja.metadata.course_data: + hash_values = [self.hash[diff] for diff in self.tja.metadata.course_data if diff in self.hash] + placeholders = ','.join('?' * len(hash_values)) + + batch_query = f""" + SELECT hash, score, good, ok, bad, clear + FROM Scores + WHERE hash IN ({placeholders}) + """ + cursor.execute(batch_query, hash_values) + + hash_to_score = {row[0]: row[1:] for row in cursor.fetchall()} + + for diff in self.tja.metadata.course_data: + if diff not in self.hash: + continue + diff_hash = self.hash[diff] + self.scores[diff] = hash_to_score.get(diff_hash) + + 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, start_position=0, total_distance=100 * direction, ease_out='cubic') + self.move.start() + if self.is_open or self.target_position == BOX_CENTER + 150: + self.move.total_distance = 250 * 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 + + def update(self, is_diff_select): + self.is_diff_select = is_diff_select + is_open_prev = self.is_open + self.move_box() + self.is_open = self.position == BOX_CENTER + 150 + + if not (-56 <= self.position <= 1280): + return + if self.yellow_box is not None: + self.yellow_box.update(is_diff_select) + + if self.history_wait == 0: + self.history_wait = get_current_ms() + + 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()) + + if not is_open_prev and self.is_open: + if self.black_name is None: + self.black_name = OutlinedText(self.text_name, 40, ray.WHITE, ray.BLACK, outline_thickness=5, vertical=True) + if self.tja is not None or self.is_back: + self.yellow_box = YellowBox(self.black_name, self.is_back, tja=self.tja) + self.yellow_box.create_anim() + else: + self.hori_name = OutlinedText(self.text_name, 40, ray.WHITE, ray.BLACK, 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() + 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}'): + audio.stop_sound(f'genre_voice_{self.texture_index}') + if self.tja_count is not None and self.tja_count > 0 and self.tja_count_text is None: + self.tja_count_text = OutlinedText(str(self.tja_count), 35, ray.WHITE, ray.BLACK, outline_thickness=5)#, horizontal_spacing=1.2) + 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()) + + if self.name is None: + self.name = OutlinedText(self.text_name, 40, ray.WHITE, SongBox.OUTLINE_MAP.get(self.name_texture_index, ray.Color(101, 0, 82, 255)), outline_thickness=5, vertical=True) + + if self.score_history is not None: + self.score_history.update(get_current_ms()) + + + def _draw_closed(self, x: int, y: int): + tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x) + offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0 + tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=32, y=offset) + tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x) + if self.texture_index == SongBox.DEFAULT_INDEX: + tex.draw_texture('box', 'genre_overlay', x=x, y=y) + elif self.texture_index == 14: + tex.draw_texture('box', 'diff_overlay', x=x, y=y) + if not self.is_back and self.is_dir: + tex.draw_texture('box', 'folder_clip', frame=self.texture_index, x=x - (1 - offset), y=y) + + if self.is_back: + tex.draw_texture('box', 'back_text', x=x, y=y) + elif self.name is not None: + dest = ray.Rectangle(x + 47 - int(self.name.texture.width / 2), y+35, self.name.texture.width, min(self.name.texture.height, 417)) + self.name.draw(self.name.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE) + + if self.tja is not None and self.tja.ex_data.new: + tex.draw_texture('yellow_box', 'ex_data_new_song_balloon', x=x, y=y) + valid_scores = {k: v for k, v in self.scores.items() if v is not None} + if valid_scores: + highest_key = max(valid_scores.keys()) + score = self.scores[highest_key] + if score and ((score[4] == 2 and score[2] == 0) or (score[2] == 0 and score[3] == 0)): + tex.draw_texture('yellow_box', 'crown_dfc', x=x, y=y, frame=min(4, highest_key)) + elif score and ((score[4] == 2) or (score[3] == 0)): + tex.draw_texture('yellow_box', 'crown_fc', x=x, y=y, frame=min(4, highest_key)) + elif score and score[4] >= 1: + tex.draw_texture('yellow_box', 'crown_clear', x=x, y=y, frame=min(4, highest_key)) + if self.crown: #Folder lamp + highest_crown = max(self.crown) + if self.crown[highest_crown] == 'DFC': + tex.draw_texture('yellow_box', 'crown_dfc', x=x, y=y, frame=min(4, highest_crown)) + elif self.crown[highest_crown] == 'FC': + tex.draw_texture('yellow_box', 'crown_fc', x=x, y=y, frame=min(4, highest_crown)) + else: + tex.draw_texture('yellow_box', 'crown_clear', x=x, y=y, frame=min(4, highest_crown)) + + def _draw_open(self, x: int, y: int, fade_override: Optional[float]): + color = ray.WHITE + if fade_override is not None: + color = ray.fade(ray.WHITE, fade_override) + if self.hori_name is not None and self.open_anim.attribute >= 100: + tex.draw_texture('box', 'folder_top_edge', x=x, y=y - self.open_anim.attribute, color=color, mirror='horizontal', frame=self.texture_index) + tex.draw_texture('box', 'folder_top', x=x, y=y - self.open_anim.attribute, color=color, frame=self.texture_index) + tex.draw_texture('box', 'folder_top_edge', x=x+268, y=y - self.open_anim.attribute, color=color, frame=self.texture_index) + dest_width = min(300, self.hori_name.texture.width) + dest = ray.Rectangle((x + 48) - (dest_width//2), y + 107 - self.open_anim.attribute, dest_width, self.hori_name.texture.height) + self.hori_name.draw(self.hori_name.default_src, dest, ray.Vector2(0, 0), 0, color) + + tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x - self.open_anim.attribute) + offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0 + tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x - self.open_anim.attribute, y=offset, x2=(self.open_anim.attribute*2)+32) + tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x + self.open_anim.attribute) + + if self.texture_index == SongBox.DEFAULT_INDEX: + tex.draw_texture('box', 'genre_overlay_large', x=x, y=y, color=color) + elif self.texture_index == 14: + tex.draw_texture('box', 'diff_overlay_large', x=x, y=y, color=color) + + color = ray.WHITE + if fade_override is not None: + color = ray.fade(ray.WHITE, fade_override) + if self.tja_count_text is not None and self.texture_index != 14: + tex.draw_texture('yellow_box', 'song_count_back', color=color, fade=0.5) + tex.draw_texture('yellow_box', 'song_count_num', color=color) + tex.draw_texture('yellow_box', 'song_count_songs', color=color) + dest_width = min(124, self.tja_count_text.texture.width) + dest = ray.Rectangle(560 - (dest_width//2), 126, dest_width, self.tja_count_text.texture.height) + self.tja_count_text.draw(self.tja_count_text.default_src, dest, ray.Vector2(0, 0), 0, color) + if self.texture_index != 9: + tex.draw_texture('box', 'folder_graphic', color=color, frame=self.texture_index) + tex.draw_texture('box', 'folder_text', color=color, frame=self.texture_index) + elif self.box_texture is not None: + ray.draw_texture(self.box_texture, (x+48) - (self.box_texture.width//2), (y+240) - (self.box_texture.height//2), color) + + def draw_score_history(self): + if self.is_open and get_current_ms() >= self.wait + 83.33: + if self.score_history is not None and get_current_ms() >= self.history_wait + 3000: + self.score_history.draw() + def draw(self, x: int, y: int, is_ura: bool, fade_override=None): + if self.is_open and get_current_ms() >= self.wait + 83.33: + if self.yellow_box is not None: + self.yellow_box.draw(self, fade_override, is_ura) + else: + self._draw_open(x, y, self.open_fade.attribute) + else: + self._draw_closed(x, y) + +class YellowBox: + """A song box when it is opened.""" + def __init__(self, name: OutlinedText, is_back: bool, tja: Optional[TJAParser] = None): + self.is_diff_select = False + self.name = name + self.is_back = is_back + self.tja = tja + self.subtitle = None + if self.tja is not None: + subtitle_text = self.tja.metadata.subtitle.get(global_data.config['general']['language'], '') + font_size = 30 if len(subtitle_text) < 30 else 20 + self.subtitle = OutlinedText(subtitle_text, font_size, ray.WHITE, ray.BLACK, outline_thickness=5, vertical=True) + + self.left_out = tex.get_animation(9) + self.right_out = tex.get_animation(10) + self.center_out = tex.get_animation(11) + self.fade = tex.get_animation(12) + + self.left_out.reset() + self.right_out.reset() + self.center_out.reset() + self.fade.reset() + + self.left_out_2 = tex.get_animation(13) + self.right_out_2 = tex.get_animation(14) + self.center_out_2 = tex.get_animation(15) + self.top_y_out = tex.get_animation(16) + self.center_h_out = tex.get_animation(17) + self.fade_in = tex.get_animation(18) + + self.right_out_2.reset() + self.top_y_out.reset() + self.center_h_out.reset() + + self.right_x = self.right_out.attribute + self.left_x = self.left_out.attribute + self.center_width = self.center_out.attribute + self.top_y = self.top_y_out.attribute + self.center_height = self.center_h_out.attribute + self.bottom_y = tex.textures['yellow_box']['yellow_box_bottom_right'].y[0] + self.edge_height = tex.textures['yellow_box']['yellow_box_bottom_right'].height + + def reset(self): + if self.subtitle is not None: + self.subtitle.unload() + self.subtitle = None + + def create_anim(self): + self.right_out_2.reset() + self.top_y_out.reset() + self.center_h_out.reset() + self.left_out.start() + self.right_out.start() + self.center_out.start() + self.fade.start() + + def create_anim_2(self): + self.left_out_2.start() + self.right_out_2.start() + self.center_out_2.start() + self.top_y_out.start() + self.center_h_out.start() + self.fade_in.start() + + def update(self, is_diff_select: bool): + current_time = get_current_ms() + self.left_out.update(current_time) + self.right_out.update(current_time) + self.center_out.update(current_time) + self.fade.update(current_time) + self.fade_in.update(current_time) + self.left_out_2.update(current_time) + self.right_out_2.update(current_time) + self.center_out_2.update(current_time) + self.top_y_out.update(current_time) + self.center_h_out.update(current_time) + if is_diff_select and not self.is_diff_select: + self.create_anim_2() + if self.is_diff_select: + self.right_x = self.right_out_2.attribute + self.left_x = self.left_out_2.attribute + self.top_y = self.top_y_out.attribute + self.center_width = self.center_out_2.attribute + self.center_height = self.center_h_out.attribute + else: + self.right_x = self.right_out.attribute + self.left_x = self.left_out.attribute + self.center_width = self.center_out.attribute + self.top_y = self.top_y_out.attribute + self.center_height = self.center_h_out.attribute + self.is_diff_select = is_diff_select + + def _draw_tja_data(self, song_box, color, fade): + if self.tja is None: + return + for diff in self.tja.metadata.course_data: + if diff >= 4: + continue + elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] == 2 and song_box.scores[diff][2] == 0) or (song_box.scores[diff][2] == 0 and song_box.scores[diff][3] == 0)): + tex.draw_texture('yellow_box', 's_crown_dfc', x=(diff*60), color=color) + elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] == 2) or (song_box.scores[diff][3] == 0)): + tex.draw_texture('yellow_box', 's_crown_fc', x=(diff*60), color=color) + elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][4] >= 1: + tex.draw_texture('yellow_box', 's_crown_clear', x=(diff*60), color=color) + tex.draw_texture('yellow_box', 's_crown_outline', x=(diff*60), fade=min(fade, 0.25)) + + if self.tja.ex_data.new_audio: + tex.draw_texture('yellow_box', 'ex_data_new_audio', color=color) + elif self.tja.ex_data.old_audio: + tex.draw_texture('yellow_box', 'ex_data_old_audio', color=color) + elif self.tja.ex_data.limited_time: + tex.draw_texture('yellow_box', 'ex_data_limited_time', color=color) + elif self.tja.ex_data.new: + tex.draw_texture('yellow_box', 'ex_data_new_song', color=color) + if song_box.is_favorite: + tex.draw_texture('yellow_box', f'favorite_{global_data.player_num}p', color=color) + + for i in range(4): + tex.draw_texture('yellow_box', 'difficulty_bar', frame=i, x=(i*60), color=color) + if i not in self.tja.metadata.course_data: + tex.draw_texture('yellow_box', 'difficulty_bar_shadow', frame=i, x=(i*60), fade=min(fade, 0.25)) + + for diff in self.tja.metadata.course_data: + if diff >= 4: + continue + for j in range(self.tja.metadata.course_data[diff].level): + tex.draw_texture('yellow_box', 'star', x=(diff*60), y=(j*-17), color=color) + if self.tja.metadata.course_data[diff].is_branching and (get_current_ms() // 1000) % 2 == 0: + tex.draw_texture('yellow_box', 'branch_indicator', x=(diff*60), color=color) + + def _draw_tja_data_diff(self, is_ura: bool, song_box): + if self.tja is None: + return + tex.draw_texture('diff_select', 'back', fade=self.fade_in.attribute) + tex.draw_texture('diff_select', 'option', fade=self.fade_in.attribute) + tex.draw_texture('diff_select', 'neiro', fade=self.fade_in.attribute) + + for diff in self.tja.metadata.course_data: + if diff >= 4: + continue + elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] == 2 and song_box.scores[diff][2] == 0) or (song_box.scores[diff][2] == 0 and song_box.scores[diff][3] == 0)): + tex.draw_texture('yellow_box', 's_crown_dfc', x=(diff*115)+8, y=-120, fade=self.fade_in.attribute) + elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] == 2) or (song_box.scores[diff][3] == 0)): + tex.draw_texture('yellow_box', 's_crown_fc', x=(diff*115)+8, y=-120, fade=self.fade_in.attribute) + elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][4] >= 1: + tex.draw_texture('yellow_box', 's_crown_clear', x=(diff*115)+8, y=-120, fade=self.fade_in.attribute) + tex.draw_texture('yellow_box', 's_crown_outline', x=(diff*115)+8, y=-120, fade=min(self.fade_in.attribute, 0.25)) + + for i in range(4): + if i == 3 and is_ura: + tex.draw_texture('diff_select', 'diff_tower', frame=4, x=(i*115), fade=self.fade_in.attribute) + tex.draw_texture('diff_select', 'ura_oni_plate', fade=self.fade_in.attribute) + else: + tex.draw_texture('diff_select', 'diff_tower', frame=i, x=(i*115), fade=self.fade_in.attribute) + if i not in self.tja.metadata.course_data: + tex.draw_texture('diff_select', 'diff_tower_shadow', frame=i, x=(i*115), fade=min(self.fade_in.attribute, 0.25)) + + for course in self.tja.metadata.course_data: + if (course == 4 and not is_ura) or (course == 3 and is_ura): + continue + for j in range(self.tja.metadata.course_data[course].level): + tex.draw_texture('yellow_box', 'star_ura', x=min(course, 3)*115, y=(j*-20), fade=self.fade_in.attribute) + if self.tja.metadata.course_data[course].is_branching and (get_current_ms() // 1000) % 2 == 0: + if course == 4: + name = 'branch_indicator_ura' + else: + name = 'branch_indicator_diff' + tex.draw_texture('yellow_box', name, x=min(course, 3)*115, fade=self.fade_in.attribute) + + def _draw_text(self, song_box): + if not isinstance(self.right_out, MoveAnimation): + return + if not isinstance(self.right_out_2, MoveAnimation): + return + if not isinstance(self.top_y_out, MoveAnimation): + return + x = song_box.position + (self.right_out.attribute*0.85 - (self.right_out.start_position*0.85)) + self.right_out_2.attribute - self.right_out_2.start_position + if self.is_back: + tex.draw_texture('box', 'back_text_highlight', x=x) + elif self.name is not None: + texture = self.name.texture + dest = ray.Rectangle(x + 30, 35 + self.top_y_out.attribute, texture.width, min(texture.height, 417)) + self.name.draw(self.name.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE) + if self.subtitle is not None: + texture = self.subtitle.texture + y = self.bottom_y - min(texture.height, 410) + 10 + self.top_y_out.attribute - self.top_y_out.start_position + dest = ray.Rectangle(x - 15, y, texture.width, min(texture.height, 410)) + self.subtitle.draw(self.subtitle.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE) + + def _draw_yellow_box(self): + tex.draw_texture('yellow_box', 'yellow_box_bottom_right', x=self.right_x) + tex.draw_texture('yellow_box', 'yellow_box_bottom_left', x=self.left_x, y=self.bottom_y) + tex.draw_texture('yellow_box', 'yellow_box_top_right', x=self.right_x, y=self.top_y) + tex.draw_texture('yellow_box', 'yellow_box_top_left', x=self.left_x, y=self.top_y) + tex.draw_texture('yellow_box', 'yellow_box_bottom', x=self.left_x + self.edge_height, y=self.bottom_y, x2=self.center_width) + tex.draw_texture('yellow_box', 'yellow_box_right', x=self.right_x, y=self.top_y + self.edge_height, y2=self.center_height) + tex.draw_texture('yellow_box', 'yellow_box_left', x=self.left_x, y=self.top_y + self.edge_height, y2=self.center_height) + tex.draw_texture('yellow_box', 'yellow_box_top', x=self.left_x + self.edge_height, y=self.top_y, x2=self.center_width) + tex.draw_texture('yellow_box', 'yellow_box_center', x=self.left_x + self.edge_height, y=self.top_y + self.edge_height, x2=self.center_width, y2=self.center_height) + + def draw(self, song_box: SongBox, fade_override: Optional[float], is_ura: bool): + self._draw_yellow_box() + if self.is_diff_select and self.tja is not None: + self._draw_tja_data_diff(is_ura, song_box) + else: + fade = self.fade.attribute + if fade_override is not None: + fade = min(self.fade.attribute, fade_override) + if self.is_back: + tex.draw_texture('box', 'back_graphic', fade=fade) + self._draw_tja_data(song_box, ray.fade(ray.WHITE, fade), fade) + + self._draw_text(song_box) + +class GenreBG: + """The background for a genre box.""" + def __init__(self, start_box: SongBox, end_box: SongBox, title: OutlinedText, diff_sort: Optional[int]): + self.start_box = start_box + self.end_box = end_box + self.start_position = start_box.position + self.end_position = end_box.position + self.title = title + self.fade_in = Animation.create_fade(116, initial_opacity=0.0, final_opacity=1.0, ease_in='quadratic', delay=50) + self.fade_in.start() + self.diff_num = diff_sort + def update(self, current_ms): + self.start_position = self.start_box.position + self.end_position = self.end_box.position + self.fade_in.update(current_ms) + def draw(self, y): + offset = -150 if self.start_box.is_open else 0 + + tex.draw_texture('box', 'folder_background_edge', frame=self.end_box.texture_index, x=self.start_position+offset, y=y, mirror="horizontal", fade=self.fade_in.attribute) + + + extra_distance = 155 if self.end_box.is_open or (self.start_box.is_open and 844 <= self.end_position <= 1144) else 0 + if self.start_position >= -56 and self.end_position < self.start_position: + x2 = self.start_position + 1400 + x = self.start_position+offset + elif (self.start_position <= -56) and (self.end_position < self.start_position): + x = 0 + x2 = 1280 + else: + x2 = abs(self.end_position) - self.start_position + extra_distance + 57 + x = self.start_position+offset + tex.draw_texture('box', 'folder_background', x=x, y=y, x2=x2, frame=self.end_box.texture_index) + + + if self.end_position < self.start_position and self.end_position >= -56: + x2 = min(self.end_position+75, 1280) + extra_distance + tex.draw_texture('box', 'folder_background', x=-18, y=y, x2=x2, frame=self.end_box.texture_index) + + + offset = 150 if self.end_box.is_open else 0 + tex.draw_texture('box', 'folder_background_edge', x=self.end_position+80+offset, y=y, fade=self.fade_in.attribute, frame=self.end_box.texture_index) + + if ((self.start_position <= 594 and self.end_position >= 594) or + ((self.start_position <= 594 or self.end_position >= 594) and (self.start_position > self.end_position))): + offset = 100 if self.diff_num is not None else 0 + dest_width = min(300, self.title.texture.width) + tex.draw_texture('box', 'folder_background_folder', x=-((offset+dest_width)//2), y=y-2, x2=dest_width+offset - 10, fade=self.fade_in.attribute, frame=self.end_box.texture_index) + tex.draw_texture('box', 'folder_background_folder_edge', x=-((offset+dest_width)//2), y=y-2, fade=self.fade_in.attribute, frame=self.end_box.texture_index, mirror="horizontal") + tex.draw_texture('box', 'folder_background_folder_edge', x=((offset+dest_width)//2)+20, y=y-2, fade=self.fade_in.attribute, frame=self.end_box.texture_index) + if self.diff_num is not None: + tex.draw_texture('diff_sort', 'star_num', frame=self.diff_num, x=-150 + (dest_width//2), y=-143) + dest = ray.Rectangle((1280//2) - (dest_width//2)-(offset//2), y-60, dest_width, self.title.texture.height) + self.title.draw(self.title.default_src, dest, ray.Vector2(0, 0), 0, ray.fade(ray.WHITE, self.fade_in.attribute)) + +class ScoreHistory: + """The score information that appears while hovering over a song""" + def __init__(self, scores: dict[int, tuple[int, int, int, int]], current_ms): + """ + Initialize the score history with the given scores and current time. + + Args: + scores (dict[int, tuple[int, int, int, int]]): A dictionary of scores for each difficulty level. + current_ms (int): The current time in milliseconds. + """ + self.scores = {k: v for k, v in scores.items() if v is not None} + self.difficulty_keys = list(self.scores.keys()) + self.curr_difficulty_index = 0 + self.curr_difficulty_index = (self.curr_difficulty_index + 1) % len(self.difficulty_keys) + self.curr_difficulty = self.difficulty_keys[self.curr_difficulty_index] + self.curr_score = self.scores[self.curr_difficulty][0] + self.curr_score_su = self.scores[self.curr_difficulty][0] + self.last_ms = current_ms + + def update(self, current_ms): + if current_ms >= self.last_ms + 1000: + self.last_ms = current_ms + self.curr_difficulty_index = (self.curr_difficulty_index + 1) % len(self.difficulty_keys) + self.curr_difficulty = self.difficulty_keys[self.curr_difficulty_index] + self.curr_score = self.scores[self.curr_difficulty][0] + self.curr_score_su = self.scores[self.curr_difficulty][0] + + def draw(self): + tex.draw_texture('leaderboard','background') + tex.draw_texture('leaderboard','title') + + if self.curr_difficulty == 4: + tex.draw_texture('leaderboard', 'normal_ura') + tex.draw_texture('leaderboard', 'shinuchi_ura') + else: + tex.draw_texture('leaderboard', 'normal') + tex.draw_texture('leaderboard', 'shinuchi') + + color = ray.BLACK + if self.curr_difficulty == 4: + color = ray.WHITE + tex.draw_texture('leaderboard','ura') + + tex.draw_texture('leaderboard', 'pts', color=color) + tex.draw_texture('leaderboard', 'pts', y=50) + + tex.draw_texture('leaderboard', 'difficulty', frame=self.curr_difficulty) + + counter = str(self.curr_score) + total_width = len(counter) * 14 + for i in range(len(counter)): + tex.draw_texture('leaderboard', 'counter', frame=int(counter[i]), x=-(total_width // 2) + (i * 14), color=color) + + counter = str(self.curr_score_su) + total_width = len(counter) * 14 + for i in range(len(counter)): + tex.draw_texture('leaderboard', 'counter', frame=int(counter[i]), x=-(total_width // 2) + (i * 14), y=50, color=ray.WHITE) + +class FileSystemItem: + GENRE_MAP = { + 'J-POP': 1, + 'アニメ': 2, + 'VOCALOID': 3, + 'どうよう': 4, + 'バラエティー': 5, + 'クラシック': 6, + 'ゲームミュージック': 7, + 'ナムコオリジナル': 8, + 'RECOMMENDED': 10, + 'FAVORITE': 11, + 'RECENT': 12, + '段位道場': 13, + 'DIFFICULTY': 14 + } + GENRE_MAP_2 = { + 'ボーカロイド': 3, + 'バラエティ': 5 + } + """Base class for files and directories in the navigation system""" + def __init__(self, path: Path, name: str): + self.path = path + self.name = name + +class Directory(FileSystemItem): + """Represents a directory in the navigation system""" + COLLECTIONS = [ + 'NEW', + 'RECENT', + 'FAVORITE', + 'DIFFICULTY', + 'RECOMMENDED' + ] + def __init__(self, path: Path, name: str, texture_index: int, has_box_def=False, to_root=False, back=False, tja_count=0, box_texture=None, collection=None): + super().__init__(path, name) + self.has_box_def = has_box_def + self.to_root = to_root + self.back = back + self.tja_count = tja_count + self.collection = None + if collection in Directory.COLLECTIONS: + self.collection = collection + if collection in FileSystemItem.GENRE_MAP: + texture_index = FileSystemItem.GENRE_MAP[collection] + elif self.to_root or self.back: + texture_index = SongBox.BACK_INDEX + + self.box = SongBox(name, texture_index, True, tja_count=tja_count, box_texture=box_texture) + +class SongFile(FileSystemItem): + """Represents a song file (TJA) in the navigation system""" + def __init__(self, path: Path, name: str, texture_index: int, tja=None, name_texture_index: Optional[int]=None): + super().__init__(path, name) + self.is_recent = (datetime.now() - datetime.fromtimestamp(path.stat().st_mtime)) <= timedelta(days=7) + self.tja = tja or TJAParser(path) + if self.is_recent: + self.tja.ex_data.new = True + title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) + self.hash = global_data.song_paths[path] + self.box = SongBox(title, texture_index, False, tja=self.tja, name_texture_index=name_texture_index if name_texture_index is not None else texture_index) + self.box.hash = global_data.song_hashes[self.hash][0]["diff_hashes"] + self.box.get_scores() + +class FileNavigator: + """Manages navigation through pre-generated Directory and SongFile objects""" + def __init__(self): + + # Pre-generated objects storage + self.all_directories: dict[str, Directory] = {} # path -> Directory + self.all_song_files: dict[str, SongFile] = {} # path -> SongFile + self.directory_contents: dict[str, list[Union[Directory, SongFile]]] = {} # path -> list of items + + # OPTION 2: Lazy crown calculation with caching + self.directory_crowns: dict[str, dict] = dict() # path -> crown list + self.crown_cache_dirty: set[str] = set() # directories that need crown recalculation + + # Navigation state - simplified without root-specific state + self.current_dir = Path() # Empty path represents virtual root + self.items: list[Directory | SongFile] = [] + self.new_items: list[Directory | SongFile] = [] + self.favorite_folder: Optional[Directory] = None + self.recent_folder: Optional[Directory] = None + self.selected_index = 0 + self.diff_sort_diff = 4 + self.diff_sort_level = 10 + self.diff_sort_statistics = dict() + self.history = [] + self.box_open = False + self.genre_bg = None + self.song_count = 0 + + def initialize(self, root_dirs: list[Path]): + self.root_dirs = [Path(p) if not isinstance(p, Path) else p for p in root_dirs] + self._generate_all_objects() + self._create_virtual_root() + self.load_current_directory() + + def _create_virtual_root(self): + """Create a virtual root directory containing all root directories""" + virtual_root_items = [] + + for root_path in self.root_dirs: + if not root_path.exists(): + continue + + root_key = str(root_path) + if root_key in self.all_directories: + # Root has box.def, add the directory itself + virtual_root_items.append(self.all_directories[root_key]) + else: + # Root doesn't have box.def, add its immediate children with box.def + for child_path in sorted(root_path.iterdir()): + if child_path.is_dir(): + child_key = str(child_path) + if child_key in self.all_directories: + virtual_root_items.append(self.all_directories[child_key]) + + # Also add direct TJA files from root + all_tja_files = self._find_tja_files_recursive(root_path) + for tja_path in sorted(all_tja_files): + song_key = str(tja_path) + if song_key in self.all_song_files: + virtual_root_items.append(self.all_song_files[song_key]) + + # Store virtual root contents (empty path key represents root) + self.directory_contents["."] = virtual_root_items + + def _generate_all_objects(self): + """Generate all Directory and SongFile objects in advance""" + print("Generating all Directory and SongFile objects...") + + # Generate objects for each root directory + for root_path in self.root_dirs: + if not root_path.exists(): + print(f"Root directory does not exist: {root_path}") + continue + + self._generate_objects_recursive(root_path) + + if self.favorite_folder is not None: + song_list = self._read_song_list(self.favorite_folder.path) + for song_obj in song_list: + if str(song_obj) in self.all_song_files: + self.all_song_files[str(song_obj)].box.is_favorite = True + + print(f"Object generation complete. " + f"Directories: {len(self.all_directories)}, " + f"Songs: {len(self.all_song_files)}") + + def _generate_objects_recursive(self, dir_path: Path): + """Recursively generate Directory and SongFile objects for a directory""" + if not dir_path.is_dir(): + return + + dir_key = str(dir_path) + + # Check for box.def + has_box_def = (dir_path / "box.def").exists() + + # Only create Directory objects for directories with box.def + if has_box_def: + # Parse box.def if it exists + name = dir_path.name if dir_path.name else str(dir_path) + texture_index = SongBox.DEFAULT_INDEX + box_texture = None + collection = None + + name, texture_index, collection = self._parse_box_def(dir_path) + box_png_path = dir_path / "box.png" + if box_png_path.exists(): + box_texture = str(box_png_path) + + # Count TJA files for this directory + tja_count = self._count_tja_files(dir_path) + if collection == Directory.COLLECTIONS[4]: + tja_count = 10 + elif collection == Directory.COLLECTIONS[0]: + tja_count = len(self.new_items) + + # Create Directory object + directory_obj = Directory( + dir_path, name, texture_index, + has_box_def=has_box_def, + tja_count=tja_count, + box_texture=box_texture, + collection=collection + ) + if directory_obj.collection == Directory.COLLECTIONS[2]: + self.favorite_folder = directory_obj + elif directory_obj.collection == Directory.COLLECTIONS[1]: + self.recent_folder = directory_obj + self.all_directories[dir_key] = directory_obj + + # Generate content list for this directory + content_items = [] + + # Add child directories that have box.def + child_dirs = [] + for item_path in dir_path.iterdir(): + if item_path.is_dir(): + child_has_box_def = (item_path / "box.def").exists() + if child_has_box_def: + child_dirs.append(item_path) + # Recursively generate objects for child directory + self._generate_objects_recursive(item_path) + + # Sort and add child directories + for child_path in sorted(child_dirs): + child_key = str(child_path) + if child_key in self.all_directories: + content_items.append(self.all_directories[child_key]) + + # Get TJA files for this directory + tja_files = self._get_tja_files_for_directory(dir_path) + + # 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: + song_obj = SongFile(tja_path, tja_path.name, texture_index) + song_obj.box.get_scores() + for course in song_obj.tja.metadata.course_data: + level = song_obj.tja.metadata.course_data[course].level + + scores = song_obj.box.scores.get(course) + if scores is not None: + is_cleared = scores[4] >= 1 + is_full_combo = (scores[4] == 2) or (scores[3] == 0) + else: + is_cleared = False + is_full_combo = False + + if course not in self.diff_sort_statistics: + self.diff_sort_statistics[course] = {} + + if level not in self.diff_sort_statistics[course]: + self.diff_sort_statistics[course][level] = [1, int(is_full_combo), int(is_cleared)] + else: + self.diff_sort_statistics[course][level][0] += 1 + if is_full_combo: + 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.all_song_files[song_key] = song_obj + + if song_key in self.all_song_files: + content_items.append(self.all_song_files[song_key]) + + self.directory_contents[dir_key] = content_items + self.crown_cache_dirty.add(dir_key) + + else: + # For directories without box.def, still process their children + for item_path in dir_path.iterdir(): + if item_path.is_dir(): + self._generate_objects_recursive(item_path) + + # Create SongFile objects for TJA files in non-boxed directories + tja_files = self._find_tja_files_in_directory_only(dir_path) + for tja_path in tja_files: + song_key = str(tja_path) + if song_key not in self.all_song_files: + try: + song_obj = SongFile(tja_path, tja_path.name, SongBox.DEFAULT_INDEX) + self.song_count += 1 + global_data.song_progress = self.song_count / global_data.total_songs + self.all_song_files[song_key] = song_obj + except Exception as e: + print(f"Error creating SongFile for {tja_path}: {e}") + continue + + def is_at_root(self) -> bool: + """Check if currently at the virtual root""" + return self.current_dir == Path() + + def load_current_directory(self, selected_item: Optional[Directory] = None): + """Load pre-generated items for the current directory (unified for root and subdirs)""" + dir_key = str(self.current_dir) + + # Determine if current directory has child directories with box.def + has_children = False + if self.is_at_root(): + has_children = True # Root always has "children" (the root directories) + else: + has_children = any(item.is_dir() and (item / "box.def").exists() + for item in self.current_dir.iterdir()) + + self.genre_bg = None + self.in_favorites = False + + if has_children: + self.items = [] + if not self.box_open: + self.selected_index = 0 + + start_box = None + end_box = None + + # Add back navigation item (only if not at root) + if not self.is_at_root(): + back_dir = Directory(self.current_dir.parent, "", SongBox.BACK_INDEX, back=True) + if not has_children: + start_box = back_dir.box + self.items.insert(self.selected_index, back_dir) + + # Add pre-generated content for this directory + if dir_key in self.directory_contents: + content_items = self.directory_contents[dir_key] + + # Handle special collections (same logic as before) + if isinstance(selected_item, Directory): + if selected_item.collection == Directory.COLLECTIONS[0]: + content_items = self.new_items + elif selected_item.collection == Directory.COLLECTIONS[1]: + if self.recent_folder is None: + raise Exception("tried to enter recent folder without recents") + self._generate_objects_recursive(self.recent_folder.path) + selected_item.box.tja_count_text = None + selected_item.box.tja_count = self._count_tja_files(self.recent_folder.path) + content_items = self.directory_contents[dir_key] + elif selected_item.collection == Directory.COLLECTIONS[2]: + if self.favorite_folder is None: + raise Exception("tried to enter favorite folder without favorites") + self._generate_objects_recursive(self.favorite_folder.path) + tja_files = self._get_tja_files_for_directory(self.favorite_folder.path) + self._calculate_directory_crowns(dir_key, tja_files) + selected_item.box.tja_count_text = None + selected_item.box.tja_count = self._count_tja_files(self.favorite_folder.path) + content_items = self.directory_contents[dir_key] + self.in_favorites = True + elif selected_item.collection == Directory.COLLECTIONS[3]: + content_items = [] + parent_dir = selected_item.path.parent + for sibling_path in parent_dir.iterdir(): + if sibling_path.is_dir() and sibling_path != selected_item.path: + sibling_key = str(sibling_path) + if sibling_key in self.directory_contents: + for item in self.directory_contents[sibling_key]: + if isinstance(item, SongFile) and item: + if self.diff_sort_diff in item.tja.metadata.course_data and item.tja.metadata.course_data[self.diff_sort_diff].level == self.diff_sort_level: + if item not in content_items: + content_items.append(item) + elif selected_item.collection == Directory.COLLECTIONS[4]: + parent_dir = selected_item.path.parent + temp_items = [] + for sibling_path in parent_dir.iterdir(): + if sibling_path.is_dir() and sibling_path != selected_item.path: + sibling_key = str(sibling_path) + if sibling_key in self.directory_contents: + for item in self.directory_contents[sibling_key]: + if not isinstance(item, Directory): + 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): + self.go_back() + return + i = 1 + for item in content_items: + if isinstance(item, SongFile): + if i % 10 == 0 and i != 0: + back_dir = Directory(self.current_dir.parent, "", SongBox.BACK_INDEX, back=True) + self.items.insert(self.selected_index+i, back_dir) + i += 1 + if not has_children: + if selected_item is not None: + item.box.texture_index = selected_item.box.texture_index + self.items.insert(self.selected_index+i, item) + else: + self.items.append(item) + i += 1 + + if not has_children: + self.box_open = True + end_box = content_items[-1].box + if selected_item in self.items: + self.items.remove(selected_item) + + # Calculate crowns for directories + for item in self.items: + if isinstance(item, Directory): + item_key = str(item.path) + if item_key in self.directory_contents: # Only for real directories + item.box.crown = self._get_directory_crowns_cached(item_key) + else: + # Navigation items (back/to_root) + item.box.crown = dict() + + self.calculate_box_positions() + + if (not has_children and start_box is not None + and end_box is not None and selected_item is not None + and selected_item.box.hori_name is not None): + hori_name = selected_item.box.hori_name + diff_sort = None + if selected_item.collection == Directory.COLLECTIONS[3]: + diff_sort = self.diff_sort_level + diffs = ['かんたん', 'ふつう', 'むずかしい', 'おに'] + hori_name = OutlinedText(diffs[min(3, self.diff_sort_diff)], 40, ray.WHITE, ray.BLACK, outline_thickness=5) + self.genre_bg = GenreBG(start_box, end_box, hori_name, diff_sort) + + def select_current_item(self): + """Select the currently highlighted item""" + if not self.items or self.selected_index >= len(self.items): + return + + selected_item = self.items[self.selected_index] + + if isinstance(selected_item, Directory): + if self.box_open: + self.go_back() + return + + if selected_item.back: + # Handle back navigation + if self.current_dir.parent == Path(): + # Going back to root + self.current_dir = Path() + else: + self.current_dir = self.current_dir.parent + else: + # Save current state to history + self.history.append((self.current_dir, self.selected_index)) + self.current_dir = selected_item.path + + self.load_current_directory(selected_item=selected_item) + + elif isinstance(selected_item, SongFile): + return selected_item + + def go_back(self): + """Navigate back to the previous directory""" + if self.history: + previous_dir, previous_index = self.history.pop() + self.current_dir = previous_dir + self.selected_index = previous_index + self.load_current_directory() + self.box_open = False + + def _count_tja_files(self, folder_path: Path): + """Count TJA files in directory""" + tja_count = 0 + + # Find all song_list.txt files recursively + song_list_files = list(folder_path.rglob("song_list.txt")) + + if song_list_files: + # Process all song_list.txt files found + for song_list_path in song_list_files: + with open(song_list_path, 'r', encoding='utf-8-sig') as song_list_file: + tja_count += len([line for line in song_list_file.readlines() if line.strip()]) + # Fallback: Use recursive counting of .tja files + tja_count += sum(1 for _ in folder_path.rglob("*.tja")) + + return tja_count + + def _get_directory_crowns_cached(self, dir_key: str) -> dict: + """Get crowns for a directory, calculating only if needed""" + if dir_key in self.crown_cache_dirty or dir_key not in self.directory_crowns: + # Calculate crowns on-demand + dir_path = Path(dir_key) + tja_files = self._get_tja_files_for_directory(dir_path) + self._calculate_directory_crowns(dir_key, tja_files) + self.crown_cache_dirty.discard(dir_key) + + return self.directory_crowns.get(dir_key, dict()) + + def _calculate_directory_crowns(self, dir_key: str, tja_files: list): + """Pre-calculate crowns for a directory""" + all_scores = dict() + crowns = dict() + + for tja_path in tja_files: + song_key = str(tja_path) + if song_key in self.all_song_files: + song_obj = self.all_song_files[song_key] + for diff in song_obj.box.scores: + if diff not in all_scores: + all_scores[diff] = [] + all_scores[diff].append(song_obj.box.scores[diff]) + + for diff in all_scores: + if all(score is not None and ((score[4] == 2 and score[2] == 0) or (score[2] == 0 and score[3] == 0)) for score in all_scores[diff]): + crowns[diff] = 'DFC' + elif all(score is not None and ((score[4] == 2) or (score[3] == 0)) for score in all_scores[diff]): + crowns[diff] = 'FC' + elif all(score is not None and score[4] >= 1 for score in all_scores[diff]): + crowns[diff] = 'CLEAR' + + self.directory_crowns[dir_key] = crowns + + def _get_tja_files_for_directory(self, directory: Path): + """Get TJA files for a specific directory""" + if (directory / 'song_list.txt').exists(): + return self._read_song_list(directory) + else: + return self._find_tja_files_in_directory_only(directory) + + def _find_tja_files_in_directory_only(self, directory: Path): + """Find TJA files only in the specified directory, not recursively in subdirectories with box.def""" + tja_files: list[Path] = [] + + for path in directory.iterdir(): + if path.is_file() and path.suffix.lower() == ".tja": + tja_files.append(path) + elif path.is_dir(): + # Only recurse into subdirectories that don't have box.def + sub_dir_has_box_def = (path / "box.def").exists() + if not sub_dir_has_box_def: + tja_files.extend(self._find_tja_files_in_directory_only(path)) + + return tja_files + + def _find_tja_files_recursive(self, directory: Path, box_def_dirs_only=True): + tja_files: list[Path] = [] + + has_box_def = (directory / "box.def").exists() + if box_def_dirs_only and has_box_def and directory != self.current_dir: + return [] + + for path in directory.iterdir(): + if path.is_file() and path.suffix.lower() == ".tja": + tja_files.append(path) + elif path.is_dir(): + sub_dir_has_box_def = (path / "box.def").exists() + if not sub_dir_has_box_def: + tja_files.extend(self._find_tja_files_recursive(path, box_def_dirs_only)) + + 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: + print(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] = [] + updated_lines = [] + file_updated = False + with open(path / 'song_list.txt', 'r', encoding='utf-8-sig') as song_list: + for line in song_list: + line = line.strip() + if not line: + continue + + parts = line.split('|') + if len(parts) < 3: + continue + + hash_val, title, subtitle = parts[0], parts[1], parts[2] + original_hash = hash_val + + if hash_val in global_data.song_hashes: + file_path = Path(global_data.song_hashes[hash_val][0]["file_path"]) + if file_path.exists() and file_path not in tja_files: + tja_files.append(file_path) + else: + # Try to find by title and subtitle + for key, value in global_data.song_hashes.items(): + for i in range(len(value)): + song = value[i] + if (song["title"]["en"] == title and + song["subtitle"]["en"] == subtitle and + Path(song["file_path"]).exists()): + hash_val = key + tja_files.append(Path(global_data.song_hashes[hash_val][i]["file_path"])) + break + + if hash_val != original_hash: + file_updated = True + updated_lines.append(f"{hash_val}|{title}|{subtitle}") + + # Write back updated song list if needed + if file_updated: + with open(path / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list: + for line in updated_lines: + print("updated", line) + song_list.write(line + '\n') + + return tja_files + + def calculate_box_positions(self): + """Dynamically calculate box positions based on current selection with wrap-around support""" + if not self.items: + return + + for i, item in enumerate(self.items): + offset = i - self.selected_index + + if offset > len(self.items) // 2: + offset -= len(self.items) + elif offset < -len(self.items) // 2: + offset += len(self.items) + + position = BOX_CENTER + (100 * offset) + if position == BOX_CENTER: + position += 150 + elif position > BOX_CENTER: + position += 300 + else: + position -= 0 + + if item.box.position == -11111: + item.box.position = position + item.box.target_position = position + else: + item.box.target_position = position + + def mark_crowns_dirty_for_song(self, song_file: SongFile): + """Mark directories as needing crown recalculation when a song's score changes""" + song_path = song_file.path + + # Find all directories that contain this song and mark them as dirty + for dir_key, content_items in self.directory_contents.items(): + for item in content_items: + if isinstance(item, SongFile) and item.path == song_path: + self.crown_cache_dirty.add(dir_key) + break + + def navigate_left(self): + """Move selection left with wrap-around""" + if self.items: + self.selected_index = (self.selected_index - 1) % len(self.items) + self.calculate_box_positions() + + def navigate_right(self): + """Move selection right with wrap-around""" + if self.items: + self.selected_index = (self.selected_index + 1) % len(self.items) + self.calculate_box_positions() + + def get_current_item(self): + """Get the currently selected item""" + if self.items and 0 <= self.selected_index < len(self.items): + return self.items[self.selected_index] + raise Exception("No current item available") + + def reset_items(self): + """Reset the items in the song select scene""" + for item in self.items: + item.box.reset() + + def add_recent(self): + """Add the current song to the recent list""" + song = self.get_current_item() + if isinstance(song, Directory): + return + if self.recent_folder is None: + return + + recents_path = self.recent_folder.path / 'song_list.txt' + new_entry = f'{song.hash}|{song.tja.metadata.title["en"]}|{song.tja.metadata.subtitle["en"]}\n' + existing_entries = [] + if recents_path.exists(): + with open(recents_path, 'r', encoding='utf-8-sig') as song_list: + existing_entries = song_list.readlines() + existing_entries = [entry for entry in existing_entries if not entry.startswith(f'{song.hash}|')] + all_entries = [new_entry] + existing_entries + recent_entries = all_entries[:25] + with open(recents_path, 'w', encoding='utf-8-sig') as song_list: + song_list.writelines(recent_entries) + + print("Added recent: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) + + def add_favorite(self) -> bool: + """Add the current song to the favorites list""" + song = self.get_current_item() + if isinstance(song, Directory): + return False + if self.favorite_folder is None: + return False + favorites_path = self.favorite_folder.path / 'song_list.txt' + lines = [] + if not Path(favorites_path).exists(): + Path(favorites_path).touch() + with open(favorites_path, 'r', encoding='utf-8-sig') as song_list: + for line in song_list: + line = line.strip() + if not line: # Skip empty lines + continue + hash, title, subtitle = line.split('|') + if song.hash == hash or (song.tja.metadata.title['en'] == title and song.tja.metadata.subtitle['en'] == subtitle): + if not self.in_favorites: + return False + else: + lines.append(line) + if self.in_favorites: + with open(favorites_path, 'w', encoding='utf-8-sig') as song_list: + for line in lines: + song_list.write(line + '\n') + print("Removed favorite:", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) + else: + with open(favorites_path, 'a', encoding='utf-8-sig') as song_list: + song_list.write(f'{song.hash}|{song.tja.metadata.title["en"]}|{song.tja.metadata.subtitle["en"]}\n') + print("Added favorite: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) + return True + +navigator = FileNavigator() diff --git a/libs/global_data.py b/libs/global_data.py index 37faa0b..14d1d03 100644 --- a/libs/global_data.py +++ b/libs/global_data.py @@ -13,6 +13,32 @@ class Modifiers: inverse: bool = False random: int = 0 +@dataclass +class SessionData: + """Data class for storing session data. Wiped after the result screen. + selected_difficulty: The difficulty level selected by the user. + song_title: The title of the song being played. + genre_index: The index of the genre being played. + result_score: The score achieved in the game. + result_good: The number of good notes achieved in the game. + result_ok: The number of ok notes achieved in the game. + result_bad: The number of bad notes achieved in the game. + result_max_combo: The maximum combo achieved in the game. + result_total_drumroll: The total drumroll achieved in the game. + result_gauge_length: The length of the gauge achieved in the game. + prev_score: The previous score pulled from the database.""" + selected_difficulty: int = 0 + song_title: str = '' + genre_index: int = 0 + result_score: int = 0 + result_good: int = 0 + result_ok: int = 0 + result_bad: int = 0 + result_max_combo: int = 0 + result_total_drumroll: int = 0 + result_gauge_length: int = 0 + prev_score: int = 0 + @dataclass class GlobalData: """ @@ -26,10 +52,11 @@ class GlobalData: song_paths (dict[Path, str]): A dictionary mapping song paths to their hashes. song_progress (float): The progress of the loading bar. total_songs (int): The total number of songs. - hit_sound (int): The index of the hit sound currently used. + hit_sound (list[int]): The indices of the hit sounds currently used. player_num (int): The player number. Either 1 or 2. input_locked (int): The input lock status. 0 means unlocked, 1 or greater means locked. - modifiers (Modifiers): The modifiers for the game. + modifiers (list[Modifiers]): The modifiers for the game. + session_data (list[SessionData]): Session data for both players. """ selected_song: Path = Path() songs_played: int = 0 @@ -38,9 +65,15 @@ class GlobalData: song_paths: dict[Path, str] = field(default_factory=lambda: dict()) #path to hash song_progress: float = 0.0 total_songs: int = 0 - hit_sound: int = 0 + hit_sound: list[int] = field(default_factory=lambda: [0, 0]) player_num: int = 1 input_locked: int = 0 - modifiers: Modifiers = field(default_factory=lambda: Modifiers()) + modifiers: list[Modifiers] = field(default_factory=lambda: [Modifiers(), Modifiers()]) + session_data: list[SessionData] = field(default_factory=lambda: [SessionData(), SessionData()]) global_data = GlobalData() + +def reset_session(): + """Reset the session data.""" + global_data.session_data[0] = SessionData() + global_data.session_data[1] = SessionData() diff --git a/libs/utils.py b/libs/utils.py index 7f62623..03a0509 100644 --- a/libs/utils.py +++ b/libs/utils.py @@ -4,7 +4,6 @@ import math import sys import time import json -from dataclasses import dataclass from libs.global_data import global_data from functools import lru_cache from pathlib import Path @@ -92,11 +91,16 @@ def save_config(config: dict[str, Any]) -> None: with open(Path('config.toml'), "w", encoding="utf-8") as f: tomlkit.dump(config, f) -def is_l_don_pressed() -> bool: +def is_l_don_pressed(player_num: str = '0') -> bool: """Check if the left don button is pressed""" if global_data.input_locked: return False - keys = global_data.config["keys_1p"]["left_don"] + if player_num == '0': + keys = global_data.config["keys_1p"]["left_don"] + global_data.config["keys_2p"]["left_don"] + elif player_num == '1': + keys = global_data.config["keys_1p"]["left_don"] + elif player_num == '2': + keys = global_data.config["keys_2p"]["left_don"] gamepad_buttons = global_data.config["gamepad"]["left_don"] for key in keys: if ray.is_key_pressed(ord(key)): @@ -117,11 +121,16 @@ def is_l_don_pressed() -> bool: return False -def is_r_don_pressed() -> bool: +def is_r_don_pressed(player_num: str = '0') -> bool: """Check if the right don button is pressed""" if global_data.input_locked: return False - keys = global_data.config["keys_1p"]["right_don"] + if player_num == '0': + keys = global_data.config["keys_1p"]["right_don"] + global_data.config["keys_2p"]["right_don"] + elif player_num == '1': + keys = global_data.config["keys_1p"]["right_don"] + elif player_num == '2': + keys = global_data.config["keys_2p"]["right_don"] gamepad_buttons = global_data.config["gamepad"]["right_don"] for key in keys: if ray.is_key_pressed(ord(key)): @@ -144,11 +153,16 @@ def is_r_don_pressed() -> bool: return False -def is_l_kat_pressed() -> bool: +def is_l_kat_pressed(player_num: str = '0') -> bool: """Check if the left kat button is pressed""" if global_data.input_locked: return False - keys = global_data.config["keys_1p"]["left_kat"] + if player_num == '0': + keys = global_data.config["keys_1p"]["left_kat"] + global_data.config["keys_2p"]["left_kat"] + elif player_num == '1': + keys = global_data.config["keys_1p"]["left_kat"] + elif player_num == '2': + keys = global_data.config["keys_2p"]["left_kat"] gamepad_buttons = global_data.config["gamepad"]["left_kat"] for key in keys: if ray.is_key_pressed(ord(key)): @@ -171,11 +185,16 @@ def is_l_kat_pressed() -> bool: return False -def is_r_kat_pressed() -> bool: +def is_r_kat_pressed(player_num: str = '0') -> bool: """Check if the right kat button is pressed""" if global_data.input_locked: return False - keys = global_data.config["keys_1p"]["right_kat"] + if player_num == '0': + keys = global_data.config["keys_1p"]["right_kat"] + global_data.config["keys_2p"]["right_kat"] + elif player_num == '1': + keys = global_data.config["keys_1p"]["right_kat"] + elif player_num == '2': + keys = global_data.config["keys_2p"]["right_kat"] gamepad_buttons = global_data.config["gamepad"]["right_kat"] for key in keys: if ray.is_key_pressed(ord(key)): @@ -198,39 +217,8 @@ def is_r_kat_pressed() -> bool: return False -@dataclass -class SessionData: - """Data class for storing session data. Wiped after the result screen. - selected_difficulty: The difficulty level selected by the user. - song_title: The title of the song being played. - genre_index: The index of the genre being played. - result_score: The score achieved in the game. - result_good: The number of good notes achieved in the game. - result_ok: The number of ok notes achieved in the game. - result_bad: The number of bad notes achieved in the game. - result_max_combo: The maximum combo achieved in the game. - result_total_drumroll: The total drumroll achieved in the game. - result_gauge_length: The length of the gauge achieved in the game. - prev_score: The previous score pulled from the database.""" - selected_difficulty: int = 0 - song_title: str = '' - genre_index: int = 0 - result_score: int = 0 - result_good: int = 0 - result_ok: int = 0 - result_bad: int = 0 - result_max_combo: int = 0 - result_total_drumroll: int = 0 - result_gauge_length: int = 0 - prev_score: int = 0 - -session_data = SessionData() global_tex = TextureWrapper() -def reset_session(): - """Reset the session data.""" - return SessionData() - text_cache = set() if not Path('cache/image').exists(): if not Path('cache').exists(): diff --git a/scenes/entry.py b/scenes/entry.py index 0bf2948..e51e921 100644 --- a/scenes/entry.py +++ b/scenes/entry.py @@ -29,11 +29,14 @@ class EntryScreen: tex.load_screen_textures('entry') audio.load_screen_sounds('entry') self.side = 1 + self.is_2p = False self.box_manager = BoxManager() self.state = State.SELECT_SIDE - plate_info = global_data.config['nameplate'] + + # Initial nameplate for side selection + plate_info = global_data.config['nameplate_1p'] self.nameplate = Nameplate(plate_info['name'], plate_info['title'], -1, -1, False) - self.indicator = Indicator(Indicator.State.SELECT) + self.coin_overlay = CoinOverlay() self.allnet_indicator = AllNetIcon() self.entry_overlay = EntryOverlay() @@ -41,95 +44,97 @@ class EntryScreen: self.screen_init = True self.side_select_fade = tex.get_animation(0) self.bg_flicker = tex.get_animation(1) - self.drum_move_1 = tex.get_animation(2) - self.drum_move_2 = tex.get_animation(3) - self.drum_move_3 = tex.get_animation(4) - self.cloud_resize = tex.get_animation(5) - self.cloud_resize_loop = tex.get_animation(6) - self.cloud_texture_change = tex.get_animation(7) - self.cloud_fade = tex.get_animation(8) - self.nameplate_fadein = tex.get_animation(12) self.side_select_fade.start() self.chara = Chara2D(0, 100) self.announce_played = False + self.players = [None, None] audio.play_sound('bgm', 'music') def on_screen_end(self, next_screen: str): self.screen_init = False audio.stop_sound('bgm') self.nameplate.unload() + for player in self.players: + if player: + player.unload() tex.unload_textures() audio.unload_all_sounds() audio.unload_all_music() return next_screen def handle_input(self): - if self.box_manager.is_box_selected(): - return if self.state == State.SELECT_SIDE: if is_l_don_pressed() or is_r_don_pressed(): if self.side == 1: return self.on_screen_end("TITLE") global_data.player_num = round((self.side/3) + 1) - self.drum_move_1.start() - self.drum_move_2.start() - self.drum_move_3.start() - self.cloud_resize.start() - self.cloud_resize_loop.start() - self.cloud_texture_change.start() - self.cloud_fade.start() + + if self.players[0]: + self.players[1] = EntryPlayer(global_data.player_num, self.side, self.box_manager) + self.players[1].start_animations() + global_data.player_num = 1 + self.is_2p = True + else: + self.players[0] = EntryPlayer(global_data.player_num, self.side, self.box_manager) + self.players[0].start_animations() + self.is_2p = False + audio.play_sound('cloud', 'sound') audio.play_sound(f'entry_start_{global_data.player_num}p', 'voice') - plate_info = global_data.config['nameplate'] - self.nameplate.unload() - self.nameplate = Nameplate(plate_info['name'], plate_info['title'], global_data.player_num, plate_info['dan'], plate_info['gold']) - self.nameplate_fadein.start() self.state = State.SELECT_MODE - if self.side == 2: - self.chara = Chara2D(1, 100) + audio.play_sound('don', 'sound') + if is_l_kat_pressed(): + audio.play_sound('kat', 'sound') + if self.players[0] and self.players[0].player_num == 1: + self.side = 1 + elif self.players[0] and self.players[0].player_num == 2: + self.side = 0 else: - self.chara = Chara2D(0, 100) - audio.play_sound('don', 'sound') - if is_l_kat_pressed(): - audio.play_sound('kat', 'sound') - self.side = max(0, self.side - 1) + self.side = max(0, self.side - 1) if is_r_kat_pressed(): audio.play_sound('kat', 'sound') - self.side = min(2, self.side + 1) + if self.players[0] and self.players[0].player_num == 1: + self.side = 2 + elif self.players[0] and self.players[0].player_num == 2: + self.side = 1 + else: + self.side = min(2, self.side + 1) elif self.state == State.SELECT_MODE: - if is_l_don_pressed() or is_r_don_pressed(): + for player in self.players: + if player: + player.handle_input() + if self.players[0] and self.players[0].player_num == 1 and is_l_don_pressed('2') or is_r_don_pressed('2'): audio.play_sound('don', 'sound') - self.box_manager.select_box() - if is_l_kat_pressed(): - audio.play_sound('kat', 'sound') - self.box_manager.move_left() - if is_r_kat_pressed(): - audio.play_sound('kat', 'sound') - self.box_manager.move_right() + self.state = State.SELECT_SIDE + plate_info = global_data.config['nameplate_2p'] + self.nameplate = Nameplate(plate_info['name'], plate_info['title'], -1, -1, False) + self.chara = Chara2D(1, 100) + self.side_select_fade.restart() + self.side = 1 + elif self.players[0] and self.players[0].player_num == 2 and is_l_don_pressed('1') or is_r_don_pressed('1'): + audio.play_sound('don', 'sound') + self.state = State.SELECT_SIDE + self.side_select_fade.restart() + self.side = 1 def update(self): self.on_screen_start() current_time = get_current_ms() self.side_select_fade.update(current_time) self.bg_flicker.update(current_time) - self.drum_move_1.update(current_time) - self.drum_move_2.update(current_time) - self.drum_move_3.update(current_time) - self.cloud_resize.update(current_time) - self.cloud_texture_change.update(current_time) - self.cloud_fade.update(current_time) - self.cloud_resize_loop.update(current_time) - self.box_manager.update(current_time) - self.nameplate_fadein.update(current_time) - self.nameplate.update(current_time) - self.indicator.update(current_time) + self.box_manager.update(current_time, self.is_2p) self.timer.update(current_time) + self.nameplate.update(current_time) self.chara.update(current_time, 100, False, False) + for player in self.players: + if player: + player.update(current_time) if self.box_manager.is_finished(): return self.on_screen_end(self.box_manager.selected_box()) - if self.cloud_fade.is_finished and not audio.is_sound_playing(f'entry_start_{global_data.player_num}p') and not self.announce_played: - audio.play_sound('select_mode', 'voice') - self.announce_played = True + for player in self.players: + if player and player.cloud_fade.is_finished and not audio.is_sound_playing(f'entry_start_{global_data.player_num}p') and not self.announce_played: + audio.play_sound('select_mode', 'voice') + self.announce_played = True return self.handle_input() def draw_background(self): @@ -141,12 +146,6 @@ class EntryScreen: tex.draw_texture('background', 'shops_right') tex.draw_texture('background', 'lights', scale=2.0, fade=self.bg_flicker.attribute) - def draw_footer(self): - tex.draw_texture('side_select', 'footer') - if self.state == State.SELECT_SIDE or self.side != 0: - tex.draw_texture('side_select', 'footer_left') - if self.state == State.SELECT_SIDE or self.side != 2: - tex.draw_texture('side_select', 'footer_right') def draw_side_select(self, fade): tex.draw_texture('side_select', 'box_top_left', fade=fade) @@ -180,46 +179,38 @@ class EntryScreen: self.nameplate.draw(500, 185) def draw_player_drum(self): - move_x = self.drum_move_3.attribute - move_y = self.drum_move_1.attribute + self.drum_move_2.attribute - if self.side == 0: - offset = 0 - tex.draw_texture('side_select', 'red_drum', x=move_x, y=move_y) - else: - move_x *= -1 - offset = 620 - tex.draw_texture('side_select', 'blue_drum', x=move_x, y=move_y) - - scale = self.cloud_resize.attribute - if self.cloud_resize.is_finished: - scale = max(1, self.cloud_resize_loop.attribute) - if self.side == 2: - self.chara.draw(move_x + offset + 130, 570 + move_y, mirror=True) - else: - self.chara.draw(move_x + offset + 170, 570 + move_y) - tex.draw_texture('side_select', 'cloud', x=move_x + offset, y=move_y, frame=self.cloud_texture_change.attribute, fade=self.cloud_fade.attribute, scale=scale, center=True) + for player in self.players: + if player: + player.draw_drum() def draw_mode_select(self): - self.draw_player_drum() - if not self.cloud_texture_change.is_finished: - return + for player in self.players: + if player and not player.is_cloud_animation_finished(): + return self.box_manager.draw() def draw(self): self.draw_background() + self.draw_player_drum() if self.state == State.SELECT_SIDE: self.draw_side_select(self.side_select_fade.attribute) elif self.state == State.SELECT_MODE: self.draw_mode_select() - self.draw_footer() + tex.draw_texture('side_select', 'footer') + if self.players[0] and self.players[1]: + pass + elif not self.players[0]: + tex.draw_texture('side_select', 'footer_left') + tex.draw_texture('side_select', 'footer_right') + elif self.players[0] and self.players[0].player_num == 1: + tex.draw_texture('side_select', 'footer_right') + elif self.players[0] and self.players[0].player_num == 2: + tex.draw_texture('side_select', 'footer_left') - if self.state == State.SELECT_MODE: - if self.side == 0: - self.nameplate.draw(30, 640, fade=self.nameplate_fadein.attribute) - self.indicator.draw(50, 575, fade=self.nameplate_fadein.attribute) - else: - self.nameplate.draw(950, 640, fade=self.nameplate_fadein.attribute) - self.indicator.draw(770, 575, fade=self.nameplate_fadein.attribute) + + for player in self.players: + if player: + player.draw_nameplate_and_indicator(fade=player.nameplate_fadein.attribute) tex.draw_texture('global', 'player_entry') @@ -231,8 +222,136 @@ class EntryScreen: self.coin_overlay.draw() self.allnet_indicator.draw() - def draw_3d(self): - pass +class EntryPlayer: + """Player-specific state and rendering for the entry screen""" + def __init__(self, player_num: int, side: int, box_manager: 'BoxManager'): + """ + Initialize a player for the entry screen + Args: + player_num: 1 or 2 (player number) + side: 0 for left (1P), 2 for right (2P) + box_manager: Reference to the box manager for input handling + """ + self.player_num = player_num + self.side = side + self.box_manager = box_manager + + # Load player-specific resources + plate_info = global_data.config[f'nameplate_{self.player_num}p'] + self.nameplate = Nameplate( + plate_info['name'], + plate_info['title'], + player_num, + plate_info['dan'], + plate_info['gold'] + ) + self.indicator = Indicator(Indicator.State.SELECT) + + # Character (0 for red/1P, 1 for blue/2P) + chara_id = 0 if side == 0 else 1 + self.chara = Chara2D(chara_id, 100) + + # Animations + self.drum_move_1 = tex.get_animation(2) + self.drum_move_2 = tex.get_animation(3) + self.drum_move_3 = tex.get_animation(4) + self.cloud_resize = tex.get_animation(5) + self.cloud_resize_loop = tex.get_animation(6) + self.cloud_texture_change = tex.get_animation(7) + self.cloud_fade = tex.get_animation(8) + self.nameplate_fadein = tex.get_animation(12) + + def start_animations(self): + """Start all player entry animations""" + self.drum_move_1.start() + self.drum_move_2.start() + self.drum_move_3.start() + self.cloud_resize.start() + self.cloud_resize_loop.start() + self.cloud_texture_change.start() + self.cloud_fade.start() + self.nameplate_fadein.start() + + def update(self, current_time: float): + """Update player animations and state""" + self.drum_move_1.update(current_time) + self.drum_move_2.update(current_time) + self.drum_move_3.update(current_time) + self.cloud_resize.update(current_time) + self.cloud_texture_change.update(current_time) + self.cloud_fade.update(current_time) + self.cloud_resize_loop.update(current_time) + self.nameplate_fadein.update(current_time) + self.nameplate.update(current_time) + self.indicator.update(current_time) + self.chara.update(current_time, 100, False, False) + + def draw_drum(self): + """Draw the player's drum with animations""" + move_x = self.drum_move_3.attribute + move_y = self.drum_move_1.attribute + self.drum_move_2.attribute + + if self.side == 0: # Left side (1P/red) + offset = 0 + tex.draw_texture('side_select', 'red_drum', x=move_x, y=move_y) + chara_x = move_x + offset + 170 + chara_mirror = False + else: # Right side (2P/blue) + move_x *= -1 + offset = 620 + tex.draw_texture('side_select', 'blue_drum', x=move_x, y=move_y) + chara_x = move_x + offset + 130 + chara_mirror = True + + # Draw character + chara_y = 570 + move_y + self.chara.draw(chara_x, chara_y, mirror=chara_mirror) + + # Draw cloud + scale = self.cloud_resize.attribute + if self.cloud_resize.is_finished: + scale = max(1, self.cloud_resize_loop.attribute) + tex.draw_texture( + 'side_select', 'cloud', + x=move_x + offset, + y=move_y, + frame=self.cloud_texture_change.attribute, + fade=self.cloud_fade.attribute, + scale=scale, + center=True + ) + + def draw_nameplate_and_indicator(self, fade: float = 1.0): + """Draw nameplate and indicator at player-specific position""" + if self.side == 0: # Left side + self.nameplate.draw(30, 640, fade=fade) + self.indicator.draw(50, 575, fade=fade) + else: # Right side + self.nameplate.draw(950, 640, fade=fade) + self.indicator.draw(770, 575, fade=fade) + + def is_cloud_animation_finished(self) -> bool: + """Check if cloud texture change animation is finished""" + return self.cloud_texture_change.is_finished + + def unload(self): + """Unload player resources""" + self.nameplate.unload() + + def handle_input(self): + """Handle player input for mode selection""" + if self.box_manager.is_box_selected(): + return + + if is_l_don_pressed(str(self.player_num)) or is_r_don_pressed(str(self.player_num)): + audio.play_sound('don', 'sound') + self.box_manager.select_box() + if is_l_kat_pressed(str(self.player_num)): + audio.play_sound('kat', 'sound') + self.box_manager.move_left() + if is_r_kat_pressed(str(self.player_num)): + audio.play_sound('kat', 'sound') + self.box_manager.move_right() class Box: """Box class for the entry screen""" @@ -330,6 +449,7 @@ class BoxManager: self.boxes = [Box(self.box_titles[i], self.box_locations[i]) for i in range(len(self.box_titles))] self.selected_box_index = 0 self.fade_out = tex.get_animation(9) + self.is_2p = False spacing = 80 box_width = self.boxes[0].texture.width @@ -380,7 +500,12 @@ class BoxManager: self.boxes[self.selected_box_index-1].move_left() self.boxes[self.selected_box_index].move_left() - def update(self, current_time_ms: float): + def update(self, current_time_ms: float, is_2p: bool): + self.is_2p = is_2p + if self.is_2p: + self.box_locations = ["SONG_SELECT_2P", "SETTINGS"] + for i, box in enumerate(self.boxes): + box.location = self.box_locations[i] self.fade_out.update(current_time_ms) for i, box in enumerate(self.boxes): is_selected = i == self.selected_box_index diff --git a/scenes/game.py b/scenes/game.py index 3f1c3fb..159a0f8 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -33,8 +33,7 @@ from libs.utils import ( is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed, - rounded, - session_data, + rounded ) from libs.video import VideoPlayer @@ -57,20 +56,14 @@ class GameScreen: if global_data.hit_sound == -1: audio.load_sound(Path('none.wav'), 'hitsound_don_1p') audio.load_sound(Path('none.wav'), 'hitsound_kat_1p') - audio.load_sound(Path('none.wav'), 'hitsound_don_2p') - audio.load_sound(Path('none.wav'), 'hitsound_kat_2p') if global_data.hit_sound == 0: - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "don.wav", 'hitsound_don_1p') - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "ka.wav", 'hitsound_kat_1p') - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "don.wav", 'hitsound_don_2p') - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "ka.wav", 'hitsound_kat_2p') + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.wav", 'hitsound_don_1p') + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.wav", 'hitsound_kat_1p') else: - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "don.ogg", 'hitsound_don_1p') - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "ka.ogg", 'hitsound_kat_1p') - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "don.ogg", 'hitsound_don_2p') - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "ka.ogg", 'hitsound_kat_2p') + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.ogg", 'hitsound_don_1p') + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.ogg", 'hitsound_kat_1p') - def init_tja(self, song: Path, difficulty: int): + def init_tja(self, song: Path): """Initialize the TJA file""" self.tja = TJAParser(song, start_delay=self.start_delay, distance=SCREEN_WIDTH - GameScreen.JUDGE_X) if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists(): @@ -78,11 +71,11 @@ class GameScreen: self.movie.set_volume(0.0) else: self.movie = None - session_data.song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) + 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 = Player(self.tja, global_data.player_num, difficulty, False, global_data.modifiers) + self.player_1 = Player(self.tja, global_data.player_num, global_data.session_data[global_data.player_num-1].selected_difficulty, False, global_data.modifiers[0]) self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) def on_screen_start(self): @@ -94,7 +87,8 @@ class GameScreen: audio.load_screen_sounds('game') ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture0"), tex.textures['balloon']['rainbow_mask'].texture) ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture1"), tex.textures['balloon']['rainbow'].texture) - self.init_tja(global_data.selected_song, session_data.selected_difficulty) + session_data = global_data.session_data[global_data.player_num-1] + self.init_tja(global_data.selected_song) self.load_hitsounds() self.song_info = SongInfo(session_data.song_title, session_data.genre_index) self.result_transition = ResultTransition(global_data.player_num) @@ -126,9 +120,10 @@ class GameScreen: """Write the score to the database""" if self.tja is None: return - if global_data.modifiers.auto: + if global_data.modifiers[global_data.player_num-1].auto: return with sqlite3.connect('scores.db') as con: + session_data = global_data.session_data[global_data.player_num-1] cursor = con.cursor() notes, _, _, _ = TJAParser.notes_to_position(TJAParser(self.tja.file_path), self.player_1.difficulty) hash = self.tja.hash_note_data(notes) @@ -180,7 +175,7 @@ class GameScreen: if ray.is_key_pressed(ray.KeyboardKey.KEY_F1): if self.song_music is not None: audio.stop_music_stream(self.song_music) - self.init_tja(global_data.selected_song, session_data.selected_difficulty) + self.init_tja(global_data.selected_song) audio.play_sound('restart', 'sound') self.song_started = False @@ -190,7 +185,7 @@ class GameScreen: return self.on_screen_end('SONG_SELECT') def spawn_ending_anims(self): - if session_data.result_bad == 0: + if global_data.session_data[global_data.player_num-1].result_bad == 0: self.player_1.ending_anim = FCAnimation(self.player_1.is_2p) elif self.player_1.gauge.is_clear: self.player_1.ending_anim = ClearAnimation(self.player_1.is_2p) @@ -223,6 +218,7 @@ class GameScreen: if self.result_transition.is_finished and not audio.is_sound_playing('result_transition'): return self.on_screen_end('RESULT') elif self.current_ms >= self.player_1.end_time: + session_data = global_data.session_data[global_data.player_num-1] session_data.result_score, session_data.result_good, session_data.result_ok, session_data.result_bad, session_data.result_max_combo, session_data.result_total_drumroll = self.player_1.get_result_score() session_data.result_gauge_length = self.player_1.gauge.gauge_length if self.end_ms != 0: @@ -338,7 +334,7 @@ class Player: self.branch_indicator = BranchIndicator(self.is_2p) if tja and tja.metadata.course_data[self.difficulty].is_branching else None self.ending_anim: Optional[FailAnimation | ClearAnimation | FCAnimation] = None self.is_gogo_time = False - plate_info = global_data.config['nameplate'] + plate_info = global_data.config[f'nameplate_{self.is_2p+1}p'] self.nameplate = Nameplate(plate_info['name'], plate_info['title'], global_data.player_num, plate_info['dan'], plate_info['gold']) self.chara = Chara2D(player_number - 1, self.bpm) if global_data.config['general']['judge_counter']: @@ -748,7 +744,7 @@ class Player: (is_r_kat_pressed, 'KAT', 'R', f'hitsound_kat_{self.player_number}p') ] for check_func, note_type, side, sound in input_checks: - if check_func(): + if check_func(self.player_number): self.lane_hit_effect = LaneHitEffect(note_type, self.is_2p) self.draw_drum_hit_list.append(DrumHitEffect(note_type, side, self.is_2p)) @@ -994,21 +990,21 @@ class Player: modifiers_to_draw = ['mod_shinuchi'] # Speed modifiers - if global_data.modifiers.speed >= 4: + if global_data.modifiers[int(self.player_number)-1].speed >= 4: modifiers_to_draw.append('mod_yonbai') - elif global_data.modifiers.speed >= 3: + elif global_data.modifiers[int(self.player_number)-1].speed >= 3: modifiers_to_draw.append('mod_sanbai') - elif global_data.modifiers.speed > 1: + elif global_data.modifiers[int(self.player_number)-1].speed > 1: modifiers_to_draw.append('mod_baisaku') # Other modifiers - if global_data.modifiers.display: + if global_data.modifiers[int(self.player_number)-1].display: modifiers_to_draw.append('mod_doron') - if global_data.modifiers.inverse: + if global_data.modifiers[int(self.player_number)-1].inverse: modifiers_to_draw.append('mod_abekobe') - if global_data.modifiers.random == 2: + if global_data.modifiers[int(self.player_number)-1].random == 2: modifiers_to_draw.append('mod_detarame') - elif global_data.modifiers.random == 1: + elif global_data.modifiers[int(self.player_number)-1].random == 1: modifiers_to_draw.append('mod_kimagure') # Draw all modifiers in one batch @@ -1038,8 +1034,6 @@ class Player: # Group 4: Lane covers and UI elements (batch similar textures) tex.draw_texture('lane', f'{self.player_number}p_lane_cover', index=self.is_2p) tex.draw_texture('lane', 'drum', index=self.is_2p) - if global_data.modifiers.auto: - tex.draw_texture('lane', 'auto_icon', index=self.is_2p) if self.ending_anim is not None: self.ending_anim.draw() @@ -1064,11 +1058,13 @@ class Player: self.judge_counter.draw() # Group 7: Player-specific elements - if not global_data.modifiers.auto: + if not self.modifiers.auto: if self.is_2p: self.nameplate.draw(-62, 371) else: self.nameplate.draw(-62, 285) + else: + tex.draw_texture('lane', 'auto_icon', index=self.is_2p) self.draw_modifiers() self.chara.draw(y=(self.is_2p*536)) diff --git a/scenes/loading.py b/scenes/loading.py index 0a6c66c..ed7e09a 100644 --- a/scenes/loading.py +++ b/scenes/loading.py @@ -7,18 +7,18 @@ from libs.global_objects import AllNetIcon from libs.song_hash import build_song_hashes from libs.texture import tex from libs.utils import get_current_ms, global_data -from scenes.song_select import SongSelectScreen +from libs.file_navigator import navigator class LoadScreen: - def __init__(self, song_select_screen: SongSelectScreen): + def __init__(self): self.width = 1280 self.height = 720 self.screen_init = False self.songs_loaded = False self.navigator_started = False self.loading_complete = False - self.song_select_screen = song_select_screen + self.navigator = navigator # Progress bar settings self.progress_bar_width = self.width * 0.43 @@ -40,7 +40,7 @@ class LoadScreen: def _load_navigator(self): """Background thread function to load navigator""" - self.song_select_screen.load_navigator() + self.navigator.initialize(global_data.config["paths"]["tja_path"]) self.loading_complete = True def on_screen_start(self): diff --git a/scenes/result.py b/scenes/result.py index bc49944..79eebe1 100644 --- a/scenes/result.py +++ b/scenes/result.py @@ -1,6 +1,6 @@ import pyray as ray -from libs import utils +from libs.global_data import reset_session from libs.audio import audio from libs.chara_2d import Chara2D from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate @@ -10,8 +10,7 @@ from libs.utils import ( get_current_ms, global_data, is_l_don_pressed, - is_r_don_pressed, - session_data, + is_r_don_pressed ) @@ -34,60 +33,127 @@ class ResultScreen: tex.load_screen_textures('result') audio.load_screen_sounds('result') self.screen_init = True - self.song_info = OutlinedText(session_data.song_title, 40, ray.WHITE, ray.BLACK, outline_thickness=5) + self.song_info = OutlinedText(global_data.session_data[0].song_title, 40, ray.WHITE, ray.BLACK, outline_thickness=5) audio.play_sound('bgm', 'music') - self.fade_in = FadeIn() + self.fade_in = FadeIn(str(global_data.player_num)) self.fade_out = tex.get_animation(0) - self.fade_in_bottom = tex.get_animation(1) - self.gauge = None - self.score_delay = None - self.bottom_characters = BottomCharacters() - self.crown = None - self.state = None - self.high_score_indicator = None - plate_info = global_data.config['nameplate'] - self.nameplate = Nameplate(plate_info['name'], plate_info['title'], global_data.player_num, plate_info['dan'], plate_info['gold']) - self.chara = Chara2D(global_data.player_num - 1, 100) - self.score_animator = ScoreAnimator(session_data.result_score) self.coin_overlay = CoinOverlay() self.allnet_indicator = AllNetIcon() - self.score = '' - self.good = '' - self.ok = '' - self.bad = '' - self.max_combo = '' - self.total_drumroll = '' - self.update_list = [['score', session_data.result_score], - ['good', session_data.result_good], - ['ok', session_data.result_ok], - ['bad', session_data.result_bad], - ['max_combo', session_data.result_max_combo], - ['total_drumroll', session_data.result_total_drumroll]] - self.update_index = 0 - self.is_skipped = False self.start_ms = get_current_ms() - if session_data.result_ok == 0 and session_data.result_bad == 0: - self.crown_type = 'crown_dfc' - elif session_data.result_bad == 0: - self.crown_type = 'crown_fc' - else: - self.crown_type = 'crown_clear' + self.is_skipped = False + self.background = Background(str(global_data.player_num), self.width) + self.player_1 = ResultPlayer(str(global_data.player_num), False, False) - def on_screen_end(self): + def on_screen_end(self, next_screen: str): self.screen_init = False global_data.songs_played += 1 tex.unload_textures() audio.stop_sound('bgm') audio.unload_all_sounds() audio.unload_all_music() - utils.session_data = utils.reset_session() - return "SONG_SELECT" + reset_session() + return next_screen - def update_score_animation(self): + def handle_input(self): + if is_r_don_pressed() or is_l_don_pressed(): + if not self.is_skipped: + self.is_skipped = True + else: + self.fade_out.start() + audio.play_sound('don', 'sound') + + def update(self): + self.on_screen_start() + current_time = get_current_ms() + self.fade_in.update(current_time) + self.player_1.update(current_time, self.fade_in.is_finished, self.is_skipped) + + if current_time >= self.start_ms + 5000 and not self.fade_out.is_started: + self.handle_input() + + self.fade_out.update(current_time) + if self.fade_out.is_finished: + self.fade_out.update(current_time) + return self.on_screen_end("SONG_SELECT") + + def draw_overlay(self): + self.fade_in.draw() + ray.draw_rectangle(0, 0, self.width, self.height, ray.fade(ray.BLACK, self.fade_out.attribute)) + self.coin_overlay.draw() + self.allnet_indicator.draw() + + def draw_song_info(self): + tex.draw_texture('song_info', 'song_num', frame=global_data.songs_played%4) + dest = ray.Rectangle(1252 - self.song_info.texture.width, 35 - self.song_info.texture.height / 2, self.song_info.texture.width, self.song_info.texture.height) + self.song_info.draw(self.song_info.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE) + + def draw(self): + self.background.draw() + self.draw_song_info() + self.player_1.draw() + self.draw_overlay() + + +class Background: + def __init__(self, player_num: str, width: int): + self.player_num = player_num + self.width = width + def draw(self): + x = 0 + if self.player_num == '3': + while x < self.width: + tex.draw_texture('background', 'background_1p', x=x, y=-360) + tex.draw_texture('background', 'background_2p', x=x, y=360) + tex.draw_texture('background', 'footer_1p', x=x, y=-72) + tex.draw_texture('background', 'footer_2p', x=x, y=648) + x += 256 + else: + while x < self.width: + tex.draw_texture('background', f'background_{self.player_num}p', x=x, y=-360) + tex.draw_texture('background', f'background_{self.player_num}p', x=x, y=360) + tex.draw_texture('background', f'footer_{self.player_num}p', x=x, y=-72) + tex.draw_texture('background', f'footer_{self.player_num}p', x=x, y=648) + x += 256 + tex.draw_texture('background', 'result_text') + +class ResultPlayer: + def __init__(self, player_num: str, has_2p: bool, is_2p: bool): + self.player_num = player_num + self.has_2p = has_2p + self.is_2p = is_2p + self.fade_in_finished = False + self.fade_in_bottom = tex.get_animation(1, is_copy=True) + self.bottom_characters = BottomCharacters() + self.gauge = None + self.score_delay = None + self.crown = None + self.state = None + self.high_score_indicator = None + self.chara = Chara2D(int(self.player_num) - 1, 100) + session_data = global_data.session_data[int(self.player_num)-1] + 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], + ['good', session_data.result_good], + ['ok', session_data.result_ok], + ['bad', session_data.result_bad], + ['max_combo', session_data.result_max_combo], + ['total_drumroll', session_data.result_total_drumroll]] + self.update_index = 0 + if session_data.result_ok == 0 and session_data.result_bad == 0: + self.crown_type = 'crown_dfc' + elif session_data.result_bad == 0: + self.crown_type = 'crown_fc' + else: + self.crown_type = 'crown_clear' + + def update_score_animation(self, is_skipped: bool): """ Update the score animation if a high score is achieved. """ - if self.is_skipped: + if is_skipped: if self.update_index == len(self.update_list) - 1: return setattr(self, self.update_list[self.update_index][0], self.update_list[self.update_index][1]) @@ -109,146 +175,102 @@ class ResultScreen: self.score_animator = ScoreAnimator(self.update_list[self.update_index][1]) self.score_delay += 16.67 * 3 if self.update_index > 0 and self.high_score_indicator is None: + session_data = global_data.session_data[int(self.player_num)-1] if session_data.result_score > session_data.prev_score: - self.high_score_indicator = HighScoreIndicator(session_data.prev_score, session_data.result_score) + self.high_score_indicator = HighScoreIndicator(session_data.prev_score, session_data.result_score, self.is_2p) - def handle_input(self): - if is_r_don_pressed() or is_l_don_pressed(): - if not self.is_skipped: - self.is_skipped = True - else: - self.fade_out.start() - audio.play_sound('don', 'sound') - - def update(self): - self.on_screen_start() - current_time = get_current_ms() - self.fade_in.update(current_time) - if self.fade_in.is_finished and self.gauge is None: - self.gauge = Gauge(str(global_data.player_num), session_data.result_gauge_length) + def update(self, current_ms: float, fade_in_finished: bool, is_skipped: bool): + self.fade_in_finished = fade_in_finished + if self.fade_in_finished and self.gauge is None: + self.gauge = Gauge(self.player_num, global_data.session_data[int(self.player_num)-1].result_gauge_length, self.is_2p) self.bottom_characters.start() - self.bottom_characters.update(self.state) + self.update_score_animation(is_skipped) if self.bottom_characters.is_finished and self.crown is None: if self.gauge is not None and self.gauge.gauge_length > 69: - self.crown = Crown() + self.crown = Crown(self.is_2p) + if self.high_score_indicator is not None: + self.high_score_indicator.update(current_ms) + + self.fade_in_bottom.update(current_ms) + self.nameplate.update(current_ms) if self.gauge is not None: - self.gauge.update(current_time) + self.gauge.update(current_ms) if self.gauge.is_finished and self.score_delay is None: - self.score_delay = current_time + 1883 + self.score_delay = current_ms + 1883 if self.score_delay is not None: - if current_time > self.score_delay and not self.fade_in_bottom.is_started: + if current_ms > self.score_delay and not self.fade_in_bottom.is_started: self.fade_in_bottom.start() if self.gauge is not None: self.state = self.gauge.state - if self.high_score_indicator is not None: - self.high_score_indicator.update(current_time) - - self.fade_in_bottom.update(current_time) - - if current_time >= self.start_ms + 5000 and not self.fade_out.is_started: - self.handle_input() - - self.update_score_animation() - - self.fade_out.update(current_time) - if self.fade_out.is_finished: - self.fade_out.update(current_time) - return self.on_screen_end() - if self.crown is not None: - self.crown.update(current_time) + self.crown.update(current_ms) - self.nameplate.update(current_time) - self.chara.update(current_time, 100, False, False) + self.chara.update(current_ms, 100, False, False) def draw_score_info(self): - """ - Draw the score information. - """ - if self.good != '': - for i in range(len(str(self.good))): - tex.draw_texture('score', 'judge_num', frame=int(str(self.good)[::-1][i]), x=943-(i*24), y=186) - if self.ok != '': - for i in range(len(str(self.ok))): - tex.draw_texture('score', 'judge_num', frame=int(str(self.ok)[::-1][i]), x=943-(i*24), y=227) - if self.bad != '': - for i in range(len(str(self.bad))): - tex.draw_texture('score', 'judge_num', frame=int(str(self.bad)[::-1][i]), x=943-(i*24), y=267) - if self.max_combo != '': - for i in range(len(str(self.max_combo))): - tex.draw_texture('score', 'judge_num', frame=int(str(self.max_combo)[::-1][i]), x=1217-(i*24), y=186) - if self.total_drumroll != '': - for i in range(len(str(self.total_drumroll))): - tex.draw_texture('score', 'judge_num', frame=int(str(self.total_drumroll)[::-1][i]), x=1217-(i*24), y=227) + """Draw the score information.""" + for j, score in enumerate([self.good, self.ok, self.bad, self.max_combo, self.total_drumroll]): + if score == '': + continue + score_str = str(score)[::-1] + for i, digit in enumerate(score_str): + tex.draw_texture('score', 'judge_num', frame=int(digit), x=-(i*24), index=j+(self.is_2p*5)) def draw_total_score(self): """ Draw the total score. """ - if not self.fade_in.is_finished: + if not self.fade_in_finished: return - tex.draw_texture('score', 'score_shinuchi') + tex.draw_texture('score', 'score_shinuchi', index=self.is_2p) if self.score != '': for i in range(len(str(self.score))): - tex.draw_texture('score', 'score_num', x=-(i*21), frame=int(str(self.score)[::-1][i])) - - def draw_bottom_textures(self): - """Draw the bottom textures.""" - if self.state == State.FAIL: - tex.draw_texture('background', 'gradient_fail', fade=min(0.4, self.fade_in_bottom.attribute)) - else: - tex.draw_texture('background', 'gradient_clear', fade=min(0.4, self.fade_in_bottom.attribute)) - self.bottom_characters.draw() + tex.draw_texture('score', 'score_num', x=-(i*21), frame=int(str(self.score)[::-1][i]), index=self.is_2p) def draw_modifiers(self): """Draw the modifiers if enabled.""" - if global_data.modifiers.display: - tex.draw_texture('score', 'mod_doron') - if global_data.modifiers.inverse: - tex.draw_texture('score', 'mod_abekobe') - if global_data.modifiers.random == 1: - tex.draw_texture('score', 'mod_kimagure') - elif global_data.modifiers.random == 2: - tex.draw_texture('score', 'mod_detarame') - if global_data.modifiers.speed >= 4: - tex.draw_texture('score', 'mod_yonbai') - elif global_data.modifiers.speed >= 3: - tex.draw_texture('score', 'mod_sanbai') - elif global_data.modifiers.speed > 1: - tex.draw_texture('score', 'mod_baisaku') + if global_data.modifiers[int(self.player_num)-1].display: + tex.draw_texture('score', 'mod_doron', index=self.is_2p) + if global_data.modifiers[int(self.player_num)-1].inverse: + tex.draw_texture('score', 'mod_abekobe', index=self.is_2p) + if global_data.modifiers[int(self.player_num)-1].random == 1: + tex.draw_texture('score', 'mod_kimagure', index=self.is_2p) + elif global_data.modifiers[int(self.player_num)-1].random == 2: + tex.draw_texture('score', 'mod_detarame', index=self.is_2p) + if global_data.modifiers[int(self.player_num)-1].speed >= 4: + tex.draw_texture('score', 'mod_yonbai', index=self.is_2p) + elif global_data.modifiers[int(self.player_num)-1].speed >= 3: + tex.draw_texture('score', 'mod_sanbai', index=self.is_2p) + elif global_data.modifiers[int(self.player_num)-1].speed > 1: + tex.draw_texture('score', 'mod_baisaku', index=self.is_2p) def draw(self): - x = 0 - while x < self.width: - tex.draw_texture('background', f'background_{str(global_data.player_num)}p', x=x, y=-360) - tex.draw_texture('background', f'background_{str(global_data.player_num)}p', x=x, y=360) - tex.draw_texture('background', f'footer_{str(global_data.player_num)}p', x=x, y=-72) - tex.draw_texture('background', f'footer_{str(global_data.player_num)}p', x=x, y=648) - x += 256 + if self.is_2p: + if self.state == State.FAIL: + tex.draw_texture('background', 'gradient_fail', fade=min(0.4, self.fade_in_bottom.attribute)) + elif self.state == State.CLEAR: + tex.draw_texture('background', 'gradient_clear', fade=min(0.4, self.fade_in_bottom.attribute)) + else: + y = -288 if self.has_2p else 0 + if self.state == State.FAIL: + tex.draw_texture('background', 'gradient_fail', fade=min(0.4, self.fade_in_bottom.attribute), y=y) + elif self.state == State.CLEAR: + tex.draw_texture('background', 'gradient_clear', fade=min(0.4, self.fade_in_bottom.attribute), y=y) + tex.draw_texture('score', 'overlay', color=ray.fade(ray.WHITE, 0.75), index=self.is_2p) + tex.draw_texture('score', 'difficulty', frame=global_data.session_data[int(self.player_num)-1].selected_difficulty, index=self.is_2p) + if not self.has_2p: + self.bottom_characters.draw() - tex.draw_texture('background', 'result_text') - tex.draw_texture('song_info', 'song_num', frame=global_data.songs_played%4) - dest = ray.Rectangle(1252 - self.song_info.texture.width, 35 - self.song_info.texture.height / 2, self.song_info.texture.width, self.song_info.texture.height) - self.song_info.draw(self.song_info.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE) - - tex.draw_texture('score', 'overlay', color=ray.fade(ray.WHITE, 0.75)) - tex.draw_texture('score', 'difficulty', frame=session_data.selected_difficulty) - - self.draw_bottom_textures() - - if self.gauge is not None: - self.gauge.draw() - - tex.draw_texture('score', 'judge_good') - tex.draw_texture('score', 'judge_ok') - tex.draw_texture('score', 'judge_bad') - tex.draw_texture('score', 'max_combo') - tex.draw_texture('score', 'drumroll') + tex.draw_texture('score', 'judge_good', index=self.is_2p) + tex.draw_texture('score', 'judge_ok', index=self.is_2p) + tex.draw_texture('score', 'judge_bad', index=self.is_2p) + tex.draw_texture('score', 'max_combo', index=self.is_2p) + tex.draw_texture('score', 'drumroll', index=self.is_2p) self.draw_score_info() self.draw_total_score() @@ -256,31 +278,25 @@ class ResultScreen: if self.crown is not None: self.crown.draw(self.crown_type) - self.nameplate.draw(265, 80) - self.draw_modifiers() if self.high_score_indicator is not None: self.high_score_indicator.draw() - self.chara.draw(y=100) - - self.fade_in.draw() - ray.draw_rectangle(0, 0, self.width, self.height, ray.fade(ray.BLACK, self.fade_out.attribute)) - self.coin_overlay.draw() - self.allnet_indicator.draw() - - def draw_3d(self): - pass + self.chara.draw(y=100+(self.is_2p*360)) + if self.gauge is not None: + self.gauge.draw() + self.nameplate.draw(265, 80+(self.is_2p*300)) class Crown: """Represents a crown animation""" - def __init__(self): - self.resize = tex.get_animation(2) - self.resize_fix = tex.get_animation(3) - self.white_fadein = tex.get_animation(4) - self.gleam = tex.get_animation(5) - self.fadein = tex.get_animation(6) + def __init__(self, is_2p: bool): + self.is_2p = is_2p + self.resize = tex.get_animation(2, is_copy=True) + self.resize_fix = tex.get_animation(3, is_copy=True) + self.white_fadein = tex.get_animation(4, is_copy=True) + self.gleam = tex.get_animation(5, is_copy=True) + self.fadein = tex.get_animation(6, is_copy=True) self.resize.start() self.resize_fix.start() self.white_fadein.start() @@ -302,10 +318,10 @@ class Crown: scale = self.resize.attribute if self.resize.is_finished: scale = self.resize_fix.attribute - tex.draw_texture('crown', crown_name, scale=scale, center=True) - tex.draw_texture('crown', 'crown_fade', fade=self.white_fadein.attribute) + tex.draw_texture('crown', crown_name, scale=scale, center=True, index=self.is_2p) + tex.draw_texture('crown', 'crown_fade', fade=self.white_fadein.attribute, index=self.is_2p) if self.gleam.attribute >= 0: - tex.draw_texture('crown', 'gleam', frame=self.gleam.attribute) + tex.draw_texture('crown', 'gleam', frame=self.gleam.attribute, index=self.is_2p) class BottomCharacters: """Represents the bottom characters animation""" @@ -387,10 +403,11 @@ class BottomCharacters: class FadeIn: """A fade out disguised as a fade in""" - def __init__(self): + def __init__(self, player_num: str): self.fadein = tex.get_animation(15) self.fadein.start() self.is_finished = False + self.player_num = player_num def update(self, current_ms: float): self.fadein.update(current_ms) @@ -398,12 +415,20 @@ class FadeIn: def draw(self): x = 0 - while x < 1280: - tex.draw_texture('background', f'background_{str(global_data.player_num)}p', x=x, y=-360, fade=self.fadein.attribute) - tex.draw_texture('background', f'background_{str(global_data.player_num)}p', x=x, y=360, fade=self.fadein.attribute) - tex.draw_texture('background', f'footer_{str(global_data.player_num)}p', x=x, y=-72, fade=self.fadein.attribute) - tex.draw_texture('background', f'footer_{str(global_data.player_num)}p', x=x, y=648, fade=self.fadein.attribute) - x += 256 + if self.player_num == '3': + while x < 1280: + tex.draw_texture('background', 'background_1p', x=x, y=-360, fade=self.fadein.attribute) + tex.draw_texture('background', 'background_2p', x=x, y=360, fade=self.fadein.attribute) + tex.draw_texture('background', 'footer_1p', x=x, y=-72, fade=self.fadein.attribute) + tex.draw_texture('background', 'footer_2p', x=x, y=648, fade=self.fadein.attribute) + x += 256 + else: + while x < 1280: + tex.draw_texture('background', f'background_{self.player_num}p', x=x, y=-360, fade=self.fadein.attribute) + tex.draw_texture('background', f'background_{self.player_num}p', x=x, y=360, fade=self.fadein.attribute) + tex.draw_texture('background', f'footer_{self.player_num}p', x=x, y=-72, fade=self.fadein.attribute) + tex.draw_texture('background', f'footer_{self.player_num}p', x=x, y=648, fade=self.fadein.attribute) + x += 256 class ScoreAnimator: """Animates a number from left to right""" @@ -434,7 +459,8 @@ class ScoreAnimator: class HighScoreIndicator: """Indicates the difference between the old and new high score""" - def __init__(self, old_score: int, new_score: int): + def __init__(self, old_score: int, new_score: int, is_2p: bool): + self.is_2p = is_2p self.score_diff = new_score - old_score self.move = tex.get_animation(18) self.fade = tex.get_animation(19) @@ -446,16 +472,17 @@ class HighScoreIndicator: self.fade.update(current_ms) def draw(self): - tex.draw_texture('score', 'high_score', y=self.move.attribute, fade=self.fade.attribute) + tex.draw_texture('score', 'high_score', y=self.move.attribute, fade=self.fade.attribute, index=self.is_2p) for i in range(len(str(self.score_diff))): - tex.draw_texture('score', 'high_score_num', x=-(i*14), frame=int(str(self.score_diff)[::-1][i]), y=self.move.attribute, fade=self.fade.attribute) + tex.draw_texture('score', 'high_score_num', x=-(i*14), frame=int(str(self.score_diff)[::-1][i]), y=self.move.attribute, fade=self.fade.attribute, index=self.is_2p) class Gauge: """The gauge from the game screen, at 0.9x scale""" - def __init__(self, player_num: str, gauge_length: int): + def __init__(self, player_num: str, gauge_length: int, is_2p: bool): + self.is_2p = is_2p self.player_num = player_num - self.difficulty = min(2, session_data.selected_difficulty) + self.difficulty = min(2, global_data.session_data[int(player_num)-1].selected_difficulty) self.gauge_length = gauge_length self.clear_start = [69, 69, 69] self.gauge_max = 87 @@ -488,37 +515,37 @@ class Gauge: def draw(self): scale = 10/11 - tex.draw_texture('gauge', f'{self.player_num}p_unfilled' + self.string_diff, scale=scale, fade=self.gauge_fade_in.attribute) + tex.draw_texture('gauge', f'{self.player_num}p_unfilled' + self.string_diff, scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) gauge_length = int(self.gauge_length) if gauge_length == self.gauge_max: if 0 < self.rainbow_animation.attribute < 8: - tex.draw_texture('gauge', 'rainbow' + self.string_diff, frame=self.rainbow_animation.attribute-1, scale=scale, fade=self.gauge_fade_in.attribute) - tex.draw_texture('gauge', 'rainbow' + self.string_diff, frame=self.rainbow_animation.attribute, scale=scale, fade=self.gauge_fade_in.attribute) + tex.draw_texture('gauge', 'rainbow' + self.string_diff, frame=self.rainbow_animation.attribute-1, scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) + tex.draw_texture('gauge', 'rainbow' + self.string_diff, frame=self.rainbow_animation.attribute, scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) else: for i in range(gauge_length+1): width = int(i * 7.2) if i == self.clear_start[self.difficulty] - 1: - tex.draw_texture('gauge', 'bar_clear_transition', x=width, scale=scale, fade=self.gauge_fade_in.attribute) + tex.draw_texture('gauge', 'bar_clear_transition', x=width, scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) elif i > self.clear_start[self.difficulty] - 1: if i % 5 == 0: - tex.draw_texture('gauge', 'bar_clear_top', x=width, scale=scale, fade=self.gauge_fade_in.attribute) - tex.draw_texture('gauge', 'bar_clear_bottom', x=width, scale=scale, fade=self.gauge_fade_in.attribute) - tex.draw_texture('gauge', 'bar_clear_top', x=width+1, scale=scale, fade=self.gauge_fade_in.attribute) - tex.draw_texture('gauge', 'bar_clear_bottom', x=width+1, scale=scale, fade=self.gauge_fade_in.attribute) + tex.draw_texture('gauge', 'bar_clear_top', x=width, scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) + tex.draw_texture('gauge', 'bar_clear_bottom', x=width, scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) + tex.draw_texture('gauge', 'bar_clear_top', x=width+1, scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) + tex.draw_texture('gauge', 'bar_clear_bottom', x=width+1, scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) else: if i % 5 == 0: - tex.draw_texture('gauge', f'{self.player_num}p_bar', x=width, scale=scale, fade=self.gauge_fade_in.attribute) - tex.draw_texture('gauge', f'{self.player_num}p_bar', x=width+1, scale=scale, fade=self.gauge_fade_in.attribute) - tex.draw_texture('gauge', 'overlay' + self.string_diff, scale=scale, fade=min(0.15, self.gauge_fade_in.attribute)) - tex.draw_texture('gauge', 'footer', scale=scale, fade=self.gauge_fade_in.attribute) + tex.draw_texture('gauge', f'{self.player_num}p_bar', x=width, scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) + tex.draw_texture('gauge', f'{self.player_num}p_bar', x=width+1, scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) + tex.draw_texture('gauge', 'overlay' + self.string_diff, scale=scale, fade=min(0.15, self.gauge_fade_in.attribute), index=self.is_2p) + tex.draw_texture('gauge', 'footer', scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) if gauge_length >= self.clear_start[self.difficulty]: - tex.draw_texture('gauge', 'clear', scale=scale, fade=self.gauge_fade_in.attribute, index=self.difficulty) + tex.draw_texture('gauge', 'clear', scale=scale, fade=self.gauge_fade_in.attribute, index=self.difficulty+(self.is_2p*3)) if self.state == State.RAINBOW: - tex.draw_texture('gauge', 'tamashii_fire', scale=0.75 * scale, center=True, frame=self.tamashii_fire_change.attribute, fade=self.gauge_fade_in.attribute) - tex.draw_texture('gauge', 'tamashii', scale=scale, fade=self.gauge_fade_in.attribute) + tex.draw_texture('gauge', 'tamashii_fire', scale=0.75 * scale, center=True, frame=self.tamashii_fire_change.attribute, fade=self.gauge_fade_in.attribute, index=self.is_2p) + tex.draw_texture('gauge', 'tamashii', scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) if self.state == State.RAINBOW and self.tamashii_fire_change.attribute in (0, 1, 4, 5): - tex.draw_texture('gauge', 'tamashii_overlay', scale=scale, fade=min(0.5, self.gauge_fade_in.attribute)) + tex.draw_texture('gauge', 'tamashii_overlay', scale=scale, fade=min(0.5, self.gauge_fade_in.attribute), index=self.is_2p) else: - tex.draw_texture('gauge', 'clear_dark', scale=scale, fade=self.gauge_fade_in.attribute, index=self.difficulty) - tex.draw_texture('gauge', 'tamashii_dark', scale=scale, fade=self.gauge_fade_in.attribute) + tex.draw_texture('gauge', 'clear_dark', scale=scale, fade=self.gauge_fade_in.attribute, index=self.difficulty+(self.is_2p*3)) + tex.draw_texture('gauge', 'tamashii_dark', scale=scale, fade=self.gauge_fade_in.attribute, index=self.is_2p) diff --git a/scenes/song_select.py b/scenes/song_select.py index 4248501..8616253 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -1,19 +1,16 @@ import random -import sqlite3 from dataclasses import fields -from datetime import datetime, timedelta from pathlib import Path -from typing import Optional, Union import pyray as ray -from libs.animation import Animation, MoveAnimation +from libs.file_navigator import navigator from libs.audio import audio from libs.chara_2d import Chara2D +from libs.file_navigator import Directory, SongBox, SongFile from libs.global_data import Modifiers from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate, Indicator, Timer from libs.texture import tex -from libs.tja import TJAParser, test_encodings from libs.transition import Transition from libs.utils import ( OutlinedText, @@ -23,10 +20,8 @@ from libs.utils import ( is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed, - session_data, ) - class State: BROWSING = 0 SONG_SELECTED = 1 @@ -36,13 +31,9 @@ class SongSelectScreen: BOX_CENTER = 444 def __init__(self, screen_width: int = 1280): self.screen_init = False - self.root_dir = global_data.config["paths"]["tja_path"] self.screen_width = screen_width self.indicator = Indicator(Indicator.State.SELECT) - - def load_navigator(self): - """To be called on boot.""" - self.navigator = FileNavigator(self.root_dir) + self.navigator = navigator def on_screen_start(self): if not self.screen_init: @@ -58,41 +49,25 @@ class SongSelectScreen: self.text_fade_out = tex.get_animation(3) self.text_fade_in = tex.get_animation(4) self.background_fade_change = tex.get_animation(5) - self.diff_selector_move_1 = tex.get_animation(26) - self.diff_selector_move_2 = tex.get_animation(27) self.blue_arrow_fade = tex.get_animation(29) self.blue_arrow_move = tex.get_animation(30) self.blue_arrow_fade.start() self.blue_arrow_move.start() - self.selected_diff_bounce = tex.get_animation(33) - self.selected_diff_fadein = tex.get_animation(34) - self.selected_diff_highlight_fade = tex.get_animation(35) - self.selected_diff_text_resize = tex.get_animation(36) - self.selected_diff_text_fadein = tex.get_animation(37) - self.diff_select_move_right = False self.state = State.BROWSING - self.selected_difficulty = -3 - self.prev_diff = -3 - self.selected_song = None self.game_transition = None self.demo_song = None self.diff_sort_selector = None - self.neiro_selector = None - self.modifier_selector = None - self.chara = Chara2D(global_data.player_num - 1, 100) self.coin_overlay = CoinOverlay() self.allnet_indicator = AllNetIcon() self.texture_index = SongBox.DEFAULT_INDEX self.last_texture_index = SongBox.DEFAULT_INDEX self.last_moved = get_current_ms() self.timer_browsing = Timer(100, get_current_ms(), self.navigator.select_current_item) - self.timer_selected = Timer(40, get_current_ms(), self._confirm_selection) - self.ura_toggle = 0 - self.is_ura = False + self.timer_selected = Timer(40, get_current_ms(), self._confirm_selection_wrapper) self.screen_init = True self.ura_switch_animation = UraSwitchAnimation() - plate_info = global_data.config['nameplate'] - self.nameplate = self.nameplate = Nameplate(plate_info['name'], plate_info['title'], global_data.player_num, plate_info['dan'], plate_info['gold']) + + self.player_1 = SongSelectPlayer(str(global_data.player_num), self.text_fade_in) if self.navigator.items == []: return self.on_screen_end("ENTRY") @@ -105,18 +80,20 @@ class SongSelectScreen: curr_item.box.get_scores() self.navigator.add_recent() + def finalize_song(self): + global_data.selected_song = self.navigator.get_current_item().path + global_data.session_data[global_data.player_num-1].selected_difficulty = self.player_1.selected_difficulty + global_data.session_data[global_data.player_num-1].genre_index = self.navigator.get_current_item().box.name_texture_index + def on_screen_end(self, next_screen): self.screen_init = False - if self.navigator.items != []: - global_data.selected_song = self.navigator.get_current_item().path - session_data.selected_difficulty = self.selected_difficulty - session_data.genre_index = self.navigator.get_current_item().box.name_texture_index - self.reset_demo_music() - self.navigator.reset_items() - audio.unload_all_sounds() - audio.unload_all_music() - tex.unload_textures() - self.nameplate.unload() + self.reset_demo_music() + self.navigator.reset_items() + self.finalize_song() + audio.unload_all_sounds() + audio.unload_all_music() + tex.unload_textures() + self.player_1.nameplate.unload() return next_screen def reset_demo_music(self): @@ -130,127 +107,60 @@ class SongSelectScreen: def handle_input_browsing(self): """Handle input for browsing songs.""" - if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT_CONTROL) or (is_l_kat_pressed() and get_current_ms() <= self.last_moved + 50): + action = self.player_1.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": self.reset_demo_music() for _ in range(10): self.navigator.navigate_left() - audio.play_sound('skip', 'sound') - self.last_moved = get_current_ms() - elif ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT_CONTROL) or (is_r_kat_pressed() and get_current_ms() <= self.last_moved + 50): + self.last_moved = current_time + elif action == "skip_right": self.reset_demo_music() for _ in range(10): self.navigator.navigate_right() - audio.play_sound('skip', 'sound') - self.last_moved = get_current_ms() - elif is_l_kat_pressed(): + self.last_moved = current_time + elif action == "navigate_left": self.reset_demo_music() self.navigator.navigate_left() - audio.play_sound('kat', 'sound') - self.last_moved = get_current_ms() - - elif is_r_kat_pressed(): + self.last_moved = current_time + elif action == "navigate_right": self.reset_demo_music() self.navigator.navigate_right() - audio.play_sound('kat', 'sound') - self.last_moved = get_current_ms() - - # Select/Enter - if is_l_don_pressed() or is_r_don_pressed(): - selected_item = self.navigator.items[self.navigator.selected_index] - if selected_item is not None and selected_item.box.is_back: - self.navigator.go_back() - audio.play_sound('cancel', 'sound') - elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[3]: - self.state = State.DIFF_SORTING - self.diff_sort_selector = DiffSortSelect(self.navigator.diff_sort_statistics, self.navigator.diff_sort_diff, self.navigator.diff_sort_level) - self.text_fade_in.start() + self.last_moved = current_time + elif action == "go_back": + self.navigator.go_back() + elif action == "diff_sort": + self.state = State.DIFF_SORTING + self.diff_sort_selector = DiffSortSelect(self.navigator.diff_sort_statistics, self.navigator.diff_sort_diff, self.navigator.diff_sort_level) + self.text_fade_in.start() + self.text_fade_out.start() + elif action == "select_song": + selected_song = self.navigator.select_current_item() + if selected_song: + self.state = State.SONG_SELECTED + self.player_1.on_song_selected(selected_song) + audio.play_sound('don', 'sound') + audio.play_sound('voice_select_diff', 'voice') + self.move_away.start() + self.diff_fade_out.start() self.text_fade_out.start() - else: - selected_song = self.navigator.select_current_item() - if selected_song: - self.state = State.SONG_SELECTED - if 4 not in selected_song.tja.metadata.course_data: - self.is_ura = False - elif (4 in selected_song.tja.metadata.course_data and - 3 not in selected_song.tja.metadata.course_data): - self.is_ura = True - audio.play_sound('don', 'sound') - audio.play_sound('voice_select_diff', 'voice') - self.move_away.start() - self.diff_fade_out.start() - self.text_fade_out.start() - self.text_fade_in.start() - self.selected_diff_bounce.start() - self.selected_diff_fadein.start() - - if ray.is_key_pressed(ray.KeyboardKey.KEY_SPACE): - success = self.navigator.add_favorite() + self.text_fade_in.start() + self.player_1.selected_diff_bounce.start() + self.player_1.selected_diff_fadein.start() + elif action == "add_favorite": + self.navigator.add_favorite() current_box = self.navigator.get_current_item().box current_box.is_favorite = not current_box.is_favorite - if success: - audio.play_sound('add_favorite', 'sound') def handle_input_selected(self): """Handle input for selecting difficulty.""" - if self.neiro_selector is not None: - if is_l_kat_pressed(): - self.neiro_selector.move_left() - elif is_r_kat_pressed(): - self.neiro_selector.move_right() - if is_l_don_pressed() or is_r_don_pressed(): - audio.play_sound('don', 'sound') - self.neiro_selector.confirm() - return - if self.modifier_selector is not None: - if is_l_kat_pressed(): - audio.play_sound('kat', 'sound') - self.modifier_selector.left() - elif is_r_kat_pressed(): - audio.play_sound('kat', 'sound') - self.modifier_selector.right() - if is_l_don_pressed() or is_r_don_pressed(): - audio.play_sound('don', 'sound') - self.modifier_selector.confirm() - return - if is_l_don_pressed() or is_r_don_pressed(): - if self.selected_difficulty == -3: - self._cancel_selection() - elif self.selected_difficulty == -2: - audio.play_sound('don', 'sound') - self.modifier_selector = ModifierSelector() - elif self.selected_difficulty == -1: - audio.play_sound('don', 'sound') - self.neiro_selector = NeiroSelector() - else: - self._confirm_selection() - - def get_current_song(): - """ - Returns the currently selected song. - """ - selected_song = self.navigator.get_current_item() - if isinstance(selected_song, Directory): - raise Exception("Directory was chosen instead of song") - return selected_song - - if is_l_kat_pressed() or is_r_kat_pressed(): - audio.play_sound('kat', 'sound') - selected_song = get_current_song() - diffs = sorted(selected_song.tja.metadata.course_data) - prev_diff = self.selected_difficulty - - if is_l_kat_pressed(): - self._navigate_difficulty_left(diffs) - else: # is_r_kat_pressed() - self._navigate_difficulty_right(diffs) - - if 0 <= self.selected_difficulty <= 4 and self.selected_difficulty != prev_diff: - self.selected_diff_bounce.start() - self.selected_diff_fadein.start() - - if (ray.is_key_pressed(ray.KeyboardKey.KEY_TAB) and - self.selected_difficulty in [3, 4]): - self._toggle_ura_mode() + result = self.player_1.handle_input_selected(self.navigator.get_current_item()) + if result == "cancel": + self._cancel_selection() + elif result == "confirm": + self._confirm_selection() + elif result == "ura_toggle": + self.ura_switch_animation.start(not self.player_1.is_ura) def handle_input_diff_sort(self): """ @@ -258,18 +168,11 @@ class SongSelectScreen: """ if self.diff_sort_selector is None: raise Exception("Diff sort selector was not able to be created") - if is_l_kat_pressed(): - self.diff_sort_selector.input_left() - audio.play_sound('kat', 'sound') - if is_r_kat_pressed(): - self.diff_sort_selector.input_right() - audio.play_sound('kat', 'sound') - if is_l_don_pressed() or is_r_don_pressed(): - tuple = self.diff_sort_selector.input_select() - audio.play_sound('don', 'sound') - if tuple is None: - return - diff, level = tuple + + result = self.player_1.handle_input_diff_sort(self.diff_sort_selector) + + if result is not None: + diff, level = result self.diff_sort_selector = None self.state = State.BROWSING self.text_fade_out.reset() @@ -282,24 +185,370 @@ class SongSelectScreen: def _cancel_selection(self): """Reset to browsing state""" - self.selected_song = None + self.player_1.selected_song = None self.move_away.reset() self.diff_fade_out.reset() self.text_fade_out.reset() self.text_fade_in.reset() self.state = State.BROWSING self.timer_browsing = Timer(100, get_current_ms(), self.navigator.select_current_item) - self.timer_selected = Timer(40, get_current_ms(), self._confirm_selection) + self.timer_selected = Timer(40, get_current_ms(), self._confirm_selection_wrapper) self.navigator.reset_items() + def _confirm_selection_wrapper(self): + """Wrapper for timer callback""" + self._confirm_selection() def _confirm_selection(self): """Confirm song selection and create game transition""" audio.play_sound('don', 'sound') audio.play_sound(f'voice_start_song_{global_data.player_num}p', 'voice') - self.selected_diff_highlight_fade.start() - self.selected_diff_text_resize.start() - self.selected_diff_text_fadein.start() + self.player_1.selected_diff_highlight_fade.start() + self.player_1.selected_diff_text_resize.start() + self.player_1.selected_diff_text_fadein.start() + + def handle_input(self): + self.player_1.handle_input(self.state, self) + + def update_players(self, current_time): + self.player_1.update(current_time) + if self.text_fade_out.is_finished: + self.player_1.selected_song = True + return "GAME" + + def check_for_selection(self): + if self.player_1.selected_diff_highlight_fade.is_finished and not audio.is_sound_playing(f'voice_start_song_{global_data.player_num}p') and self.game_transition is None: + selected_song = self.navigator.get_current_item() + if not isinstance(selected_song, SongFile): + raise Exception("picked directory") + + title = selected_song.tja.metadata.title.get( + global_data.config['general']['language'], '') + subtitle = selected_song.tja.metadata.subtitle.get( + global_data.config['general']['language'], '') + self.game_transition = Transition(title, subtitle) + self.game_transition.start() + + def update(self): + ret_val = self.on_screen_start() + if ret_val is not None: + return ret_val + current_time = get_current_ms() + self.background_move.update(current_time) + self.move_away.update(current_time) + self.diff_fade_out.update(current_time) + self.background_fade_change.update(current_time) + self.text_fade_out.update(current_time) + self.text_fade_in.update(current_time) + self.ura_switch_animation.update(current_time) + self.indicator.update(current_time) + self.blue_arrow_fade.update(current_time) + self.blue_arrow_move.update(current_time) + + next_screen = self.update_players(current_time) + + if self.state == State.BROWSING or self.state == State.DIFF_SORTING: + self.timer_browsing.update(current_time) + elif self.state == State.SONG_SELECTED: + self.timer_selected.update(current_time) + + if self.last_texture_index != self.texture_index: + if not self.background_fade_change.is_started: + self.background_fade_change.start() + if self.background_fade_change.is_finished: + self.last_texture_index = self.texture_index + self.background_fade_change.reset() + + if self.game_transition is not None: + self.game_transition.update(current_time) + if self.game_transition.is_finished: + return self.on_screen_end(next_screen) + else: + self.handle_input() + + if self.demo_song is not None: + audio.update_music_stream(self.demo_song) + + if self.navigator.genre_bg is not None: + self.navigator.genre_bg.update(get_current_ms()) + + if self.diff_sort_selector is not None: + self.diff_sort_selector.update(get_current_ms()) + + self.check_for_selection() + + for song in self.navigator.items: + song.box.update(self.state == State.SONG_SELECTED) + song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150 + if not isinstance(song, Directory) and song.box.is_open: + if self.demo_song is None and get_current_ms() >= song.box.wait + (83.33*3): + song.box.get_scores() + if song.tja.metadata.wave.exists() and song.tja.metadata.wave.is_file(): + self.demo_song = audio.load_music_stream(song.tja.metadata.wave, 'demo_song') + audio.play_music_stream(self.demo_song) + audio.seek_music_stream(self.demo_song, song.tja.metadata.demostart) + audio.stop_sound('bgm') + if song.box.is_open: + current_box = song.box + if not current_box.is_back and get_current_ms() >= song.box.wait + (83.33*3): + self.texture_index = current_box.texture_index + + if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): + return self.on_screen_end('ENTRY') + + def draw_background_diffs(self): + self.player_1.draw_background_diffs(self.state) + + def draw_players(self): + self.player_1.draw(self.state) + + def draw(self): + width = tex.textures['box']['background'].width + for i in range(0, width * 4, width): + tex.draw_texture('box', 'background', frame=self.last_texture_index, x=i-self.background_move.attribute) + tex.draw_texture('box', 'background', frame=self.texture_index, x=i-self.background_move.attribute, fade=1 - self.background_fade_change.attribute) + + self.draw_background_diffs() + + if self.navigator.genre_bg is not None and self.state == State.BROWSING: + self.navigator.genre_bg.draw(95) + + for item in self.navigator.items: + box = item.box + if -156 <= box.position <= self.screen_width + 144: + if box.position <= 500: + box.draw(box.position - int(self.move_away.attribute), 95, self.player_1.is_ura, fade_override=self.diff_fade_out.attribute) + else: + box.draw(box.position + int(self.move_away.attribute), 95, self.player_1.is_ura, fade_override=self.diff_fade_out.attribute) + + if self.state == State.BROWSING: + tex.draw_texture('global', 'arrow', index=0, x=-(self.blue_arrow_move.attribute*2), fade=self.blue_arrow_fade.attribute) + tex.draw_texture('global', 'arrow', index=1, mirror='horizontal', x=self.blue_arrow_move.attribute*2, fade=self.blue_arrow_fade.attribute) + tex.draw_texture('global', 'footer') + + self.ura_switch_animation.draw() + + if self.diff_sort_selector is not None: + self.diff_sort_selector.draw() + + if (self.player_1.selected_song and self.state == State.SONG_SELECTED): + tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute) + elif self.state == State.DIFF_SORTING: + tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute) + else: + tex.draw_texture('global', 'song_select', fade=self.text_fade_out.attribute) + + self.draw_players() + + if self.state == State.BROWSING and self.navigator.items != []: + self.navigator.get_current_item().box.draw_score_history() + + self.indicator.draw(410, 575) + + tex.draw_texture('global', 'song_num_bg', fade=0.75) + tex.draw_texture('global', 'song_num', frame=global_data.songs_played % 4) + if self.state == State.BROWSING or self.state == State.DIFF_SORTING: + self.timer_browsing.draw() + elif self.state == State.SONG_SELECTED: + self.timer_selected.draw() + self.coin_overlay.draw() + if self.game_transition is not None: + self.game_transition.draw() + + self.allnet_indicator.draw() + +class SongSelectPlayer: + def __init__(self, player_num: str, text_fade_in): + self.player_num = player_num + self.selected_difficulty = -3 + self.prev_diff = -3 + self.selected_song = None + self.is_ura = False + self.ura_toggle = 0 + self.diff_select_move_right = False + self.neiro_selector = None + self.modifier_selector = None + + # References to shared animations + self.diff_selector_move_1 = tex.get_animation(26, is_copy=True) + self.diff_selector_move_2 = tex.get_animation(27, is_copy=True) + self.text_fade_in = text_fade_in + self.selected_diff_bounce = tex.get_animation(33, is_copy=True) + self.selected_diff_fadein = tex.get_animation(34, is_copy=True) + self.selected_diff_highlight_fade = tex.get_animation(35, is_copy=True) + self.selected_diff_text_resize = tex.get_animation(36, is_copy=True) + self.selected_diff_text_fadein = tex.get_animation(37, is_copy=True) + + # 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.selected_diff_bounce.update(current_time) + self.selected_diff_fadein.update(current_time) + self.selected_diff_highlight_fade.update(current_time) + self.selected_diff_text_resize.update(current_time) + self.selected_diff_text_fadein.update(current_time) + self.diff_selector_move_1.update(current_time) + self.diff_selector_move_2.update(current_time) + self.nameplate.update(current_time) + self.chara.update(current_time, 100, False, False) + + if self.neiro_selector is not None: + self.neiro_selector.update(current_time) + if self.neiro_selector.is_finished: + self.neiro_selector = None + + if self.modifier_selector is not None: + self.modifier_selector.update(current_time) + if self.modifier_selector.is_finished: + self.modifier_selector = None + + def is_voice_playing(self): + """Check if player voice is playing""" + return audio.is_sound_playing(f'voice_start_song_{self.player_num}p') + + def on_song_selected(self, selected_song): + """Called when a song is selected""" + if 4 not in selected_song.tja.metadata.course_data: + self.is_ura = False + elif (4 in selected_song.tja.metadata.course_data and + 3 not in selected_song.tja.metadata.course_data): + self.is_ura = True + + 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" + elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[3]: + return "diff_sort" + else: + return "select_song" + + # Add favorite + if ray.is_key_pressed(ray.KeyboardKey.KEY_SPACE): + audio.play_sound('add_favorite', 'sound') + return "add_favorite" + + return None + + def handle_input_diff_sort(self, diff_sort_selector): + """Handle input for difficulty sorting. Returns (diff, level) tuple or None.""" + if is_l_kat_pressed(self.player_num): + diff_sort_selector.input_left() + audio.play_sound('kat', 'sound') + + if is_r_kat_pressed(self.player_num): + diff_sort_selector.input_right() + audio.play_sound('kat', 'sound') + + if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num): + result = diff_sort_selector.input_select() + audio.play_sound('don', 'sound') + return result + + 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() + elif state == State.DIFF_SORTING: + screen.handle_input_diff_sort() + + def handle_input_selected(self, current_item): + """Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None""" + if self.neiro_selector is not None: + if is_l_kat_pressed(self.player_num): + self.neiro_selector.move_left() + elif is_r_kat_pressed(self.player_num): + self.neiro_selector.move_right() + if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num): + audio.play_sound('don', 'sound') + self.neiro_selector.confirm() + return None + + if self.modifier_selector is not None: + if is_l_kat_pressed(self.player_num): + audio.play_sound('kat', 'sound') + self.modifier_selector.left() + elif is_r_kat_pressed(self.player_num): + audio.play_sound('kat', 'sound') + self.modifier_selector.right() + if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num): + audio.play_sound('don', 'sound') + self.modifier_selector.confirm() + return None + + if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num): + if self.selected_difficulty == -3: + return "cancel" + elif self.selected_difficulty == -2: + audio.play_sound('don', 'sound') + self.modifier_selector = ModifierSelector(self.player_num) + return None + elif self.selected_difficulty == -1: + audio.play_sound('don', 'sound') + self.neiro_selector = NeiroSelector(self.player_num) + return None + else: + return "confirm" + + if is_l_kat_pressed(self.player_num) or is_r_kat_pressed(self.player_num): + audio.play_sound('kat', 'sound') + selected_song = current_item + if isinstance(selected_song, Directory): + raise Exception("Directory was chosen instead of song") + diffs = sorted(selected_song.tja.metadata.course_data) + prev_diff = self.selected_difficulty + + if is_l_kat_pressed(self.player_num): + self._navigate_difficulty_left(diffs) + else: # is_r_kat_pressed() + self._navigate_difficulty_right(diffs) + + if 0 <= self.selected_difficulty <= 4 and self.selected_difficulty != prev_diff: + self.selected_diff_bounce.start() + self.selected_diff_fadein.start() + + if (ray.is_key_pressed(ray.KeyboardKey.KEY_TAB) and + self.selected_difficulty in [3, 4]): + return self._toggle_ura_mode() + + return None def _navigate_difficulty_left(self, diffs): """Navigate difficulty selection leftward""" @@ -357,163 +606,48 @@ class SongSelectScreen: self.diff_selector_move_1.start() def _toggle_ura_mode(self): - """Toggle between ura and normal mode""" + """Toggle between ura and normal mode. Returns 'ura_toggle' to signal screen to play animation""" self.ura_toggle = 0 self.is_ura = not self.is_ura - self.ura_switch_animation.start(not self.is_ura) audio.play_sound('ura_switch', 'sound') self.selected_difficulty = 7 - self.selected_difficulty + return "ura_toggle" - def handle_input(self): - if audio.is_sound_playing(f'voice_start_song_{global_data.player_num}p'): - return - if self.state == State.BROWSING: - self.handle_input_browsing() - elif self.state == State.SONG_SELECTED: - self.handle_input_selected() - elif self.state == State.DIFF_SORTING: - self.handle_input_diff_sort() - - def update(self): - ret_val = self.on_screen_start() - if ret_val is not None: - return ret_val - current_time = get_current_ms() - self.background_move.update(current_time) - self.move_away.update(current_time) - self.diff_fade_out.update(current_time) - self.background_fade_change.update(current_time) - self.text_fade_out.update(current_time) - self.text_fade_in.update(current_time) - self.ura_switch_animation.update(current_time) - self.diff_selector_move_1.update(current_time) - self.diff_selector_move_2.update(current_time) - self.nameplate.update(current_time) - self.indicator.update(current_time) - self.selected_diff_bounce.update(current_time) - self.selected_diff_fadein.update(current_time) - self.selected_diff_highlight_fade.update(current_time) - self.selected_diff_text_resize.update(current_time) - self.selected_diff_text_fadein.update(current_time) - self.blue_arrow_fade.update(current_time) - self.blue_arrow_move.update(current_time) - self.chara.update(current_time, 100, False, False) - - if self.state == State.BROWSING or self.state == State.DIFF_SORTING: - self.timer_browsing.update(current_time) - elif self.state == State.SONG_SELECTED: - self.timer_selected.update(current_time) - - if self.text_fade_out.is_finished: - self.selected_song = True - - if self.last_texture_index != self.texture_index: - if not self.background_fade_change.is_started: - self.background_fade_change.start() - if self.background_fade_change.is_finished: - self.last_texture_index = self.texture_index - self.background_fade_change.reset() - - if self.game_transition is not None: - self.game_transition.update(get_current_ms()) - if self.game_transition.is_finished: - return self.on_screen_end("GAME") - else: - self.handle_input() - - if self.demo_song is not None: - audio.update_music_stream(self.demo_song) - - if self.navigator.genre_bg is not None: - self.navigator.genre_bg.update(get_current_ms()) - - if self.diff_sort_selector is not None: - self.diff_sort_selector.update(get_current_ms()) - - if self.neiro_selector is not None: - self.neiro_selector.update(get_current_ms()) - if self.neiro_selector.is_finished: - self.neiro_selector = None - - if self.modifier_selector is not None: - self.modifier_selector.update(get_current_ms()) - if self.modifier_selector.is_finished: - self.modifier_selector = None - - if self.selected_diff_highlight_fade.is_finished and not audio.is_sound_playing(f'voice_start_song_{global_data.player_num}p') and self.game_transition is None: - selected_song = self.navigator.get_current_item() - if not isinstance(selected_song, SongFile): - raise Exception("picked directory") - - title = selected_song.tja.metadata.title.get( - global_data.config['general']['language'], '') - subtitle = selected_song.tja.metadata.subtitle.get( - global_data.config['general']['language'], '') - self.game_transition = Transition(title, subtitle) - self.game_transition.start() - - for song in self.navigator.items: - song.box.update(self.state == State.SONG_SELECTED) - song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150 - if not isinstance(song, Directory) and song.box.is_open: - if self.demo_song is None and get_current_ms() >= song.box.wait + (83.33*3): - song.box.get_scores() - if song.tja.metadata.wave.exists() and song.tja.metadata.wave.is_file(): - self.demo_song = audio.load_music_stream(song.tja.metadata.wave, 'demo_song') - audio.play_music_stream(self.demo_song) - audio.seek_music_stream(self.demo_song, song.tja.metadata.demostart) - audio.stop_sound('bgm') - if song.box.is_open: - current_box = song.box - if not current_box.is_back and get_current_ms() >= song.box.wait + (83.33*3): - self.texture_index = current_box.texture_index - - if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): - return self.on_screen_end('ENTRY') - - def draw_selector(self): + def draw_selector(self, state): fade = 0.5 if (self.neiro_selector is not None or self.modifier_selector is not None) else self.text_fade_in.attribute direction = 1 if self.diff_select_move_right else -1 if self.selected_difficulty <= -1 or self.prev_diff == -1: if self.prev_diff == -1 and self.selected_difficulty >= 0: if not self.diff_selector_move_2.is_finished: - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_balloon', x=((self.prev_diff+3) * 70) - 220 + (self.diff_selector_move_2.attribute * direction), fade=fade) - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_outline_back', x=((self.prev_diff+3) * 70) + (self.diff_selector_move_2.attribute * direction)) + tex.draw_texture('diff_select', f'{self.player_num}p_balloon', x=((self.prev_diff+3) * 70) - 220 + (self.diff_selector_move_2.attribute * direction), fade=fade) + tex.draw_texture('diff_select', f'{self.player_num}p_outline_back', x=((self.prev_diff+3) * 70) + (self.diff_selector_move_2.attribute * direction)) else: difficulty = min(3, self.selected_difficulty) - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_balloon', x=(difficulty * 115), fade=fade) - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_outline', x=(difficulty * 115)) + tex.draw_texture('diff_select', f'{self.player_num}p_balloon', x=(difficulty * 115), fade=fade) + tex.draw_texture('diff_select', f'{self.player_num}p_outline', x=(difficulty * 115)) elif not self.diff_selector_move_2.is_finished: - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_outline_back', x=((self.prev_diff+3) * 70) + (self.diff_selector_move_2.attribute * direction)) + tex.draw_texture('diff_select', f'{self.player_num}p_outline_back', x=((self.prev_diff+3) * 70) + (self.diff_selector_move_2.attribute * direction)) if self.selected_difficulty != -3: - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_balloon', x=((self.prev_diff+3) * 70) - 220 + (self.diff_selector_move_2.attribute * direction), fade=fade) + tex.draw_texture('diff_select', f'{self.player_num}p_balloon', x=((self.prev_diff+3) * 70) - 220 + (self.diff_selector_move_2.attribute * direction), fade=fade) else: - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_outline_back', x=((self.selected_difficulty+3) * 70)) + tex.draw_texture('diff_select', f'{self.player_num}p_outline_back', x=((self.selected_difficulty+3) * 70)) if self.selected_difficulty != -3: - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_balloon', x=((self.selected_difficulty+3) * 70) - 220, fade=fade) + tex.draw_texture('diff_select', f'{self.player_num}p_balloon', x=((self.selected_difficulty+3) * 70) - 220, fade=fade) else: if self.prev_diff == -1: return if not self.diff_selector_move_1.is_finished: difficulty = min(3, self.prev_diff) - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_balloon', x=(difficulty * 115) + (self.diff_selector_move_1.attribute * direction), fade=fade) - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_outline', x=(difficulty * 115) + (self.diff_selector_move_1.attribute * direction)) + tex.draw_texture('diff_select', f'{self.player_num}p_balloon', x=(difficulty * 115) + (self.diff_selector_move_1.attribute * direction), fade=fade) + tex.draw_texture('diff_select', f'{self.player_num}p_outline', x=(difficulty * 115) + (self.diff_selector_move_1.attribute * direction)) else: difficulty = min(3, self.selected_difficulty) - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_balloon', x=(difficulty * 115), fade=fade) - tex.draw_texture('diff_select', f'{str(global_data.player_num)}p_outline', x=(difficulty * 115)) + tex.draw_texture('diff_select', f'{self.player_num}p_balloon', x=(difficulty * 115), fade=fade) + tex.draw_texture('diff_select', f'{self.player_num}p_outline', x=(difficulty * 115)) - def draw(self): - width = tex.textures['box']['background'].width - for i in range(0, width * 4, width): - tex.draw_texture('box', 'background', frame=self.last_texture_index, x=i-self.background_move.attribute) - tex.draw_texture('box', 'background', frame=self.texture_index, x=i-self.background_move.attribute, fade=1 - self.background_fade_change.attribute) - - if self.navigator.genre_bg is not None and self.state == State.BROWSING: - self.navigator.genre_bg.draw(95) - - if (self.selected_song and self.state == State.SONG_SELECTED and self.selected_difficulty >= 0): - if global_data.player_num == 2: + def draw_background_diffs(self, state): + if (self.selected_song and state == State.SONG_SELECTED and self.selected_difficulty >= 0): + if self.player_num == '2': tex.draw_texture('global', 'background_diff', frame=self.selected_difficulty, fade=min(0.5, self.selected_diff_fadein.attribute), x=1025, y=-self.selected_diff_bounce.attribute, y2=self.selected_diff_bounce.attribute) if self.selected_diff_highlight_fade.is_reversing or self.selected_diff_highlight_fade.is_finished: tex.draw_texture('global', 'background_diff', frame=self.selected_difficulty, x=1025, y=-self.selected_diff_bounce.attribute, y2=self.selected_diff_bounce.attribute) @@ -528,31 +662,9 @@ class SongSelectScreen: tex.draw_texture('global', 'bg_diff_text_bg', fade=min(0.5, self.selected_diff_text_fadein.attribute), scale=self.selected_diff_text_resize.attribute, center=True) tex.draw_texture('global', 'bg_diff_text', frame=min(3, self.selected_difficulty), fade=self.selected_diff_text_fadein.attribute, scale=self.selected_diff_text_resize.attribute, center=True) - for item in self.navigator.items: - box = item.box - if -156 <= box.position <= self.screen_width + 144: - if box.position <= 500: - box.draw(box.position - int(self.move_away.attribute), 95, self.is_ura, fade_override=self.diff_fade_out.attribute) - else: - box.draw(box.position + int(self.move_away.attribute), 95, self.is_ura, fade_override=self.diff_fade_out.attribute) - - if self.state == State.BROWSING: - tex.draw_texture('global', 'arrow', index=0, x=-(self.blue_arrow_move.attribute*2), fade=self.blue_arrow_fade.attribute) - tex.draw_texture('global', 'arrow', index=1, mirror='horizontal', x=self.blue_arrow_move.attribute*2, fade=self.blue_arrow_fade.attribute) - tex.draw_texture('global', 'footer') - - self.ura_switch_animation.draw() - - if self.diff_sort_selector is not None: - self.diff_sort_selector.draw() - - if (self.selected_song and self.state == State.SONG_SELECTED): - self.draw_selector() - tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute) - elif self.state == State.DIFF_SORTING: - tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute) - else: - tex.draw_texture('global', 'song_select', fade=self.text_fade_out.attribute) + def draw(self, state): + if (self.selected_song and state == State.SONG_SELECTED): + self.draw_selector(state) offset = 0 if self.neiro_selector is not None: @@ -567,567 +679,19 @@ class SongSelectScreen: offset += -370 else: offset *= -1 - if self.nameplate.player_num == 1: + if self.player_num == '1': self.nameplate.draw(30, 640) self.chara.draw(x=-50, y=410 + (offset*0.6)) else: self.nameplate.draw(950, 640) self.chara.draw(mirror=True, x=950, y=410 + (offset*0.6)) - if self.state == State.BROWSING and self.navigator.items != []: - self.navigator.get_current_item().box.draw_score_history() - - self.indicator.draw(410, 575) - if self.neiro_selector is not None: self.neiro_selector.draw() if self.modifier_selector is not None: self.modifier_selector.draw() - tex.draw_texture('global', 'song_num_bg', fade=0.75) - tex.draw_texture('global', 'song_num', frame=global_data.songs_played % 4) - if self.state == State.BROWSING or self.state == State.DIFF_SORTING: - self.timer_browsing.draw() - elif self.state == State.SONG_SELECTED: - self.timer_selected.draw() - self.coin_overlay.draw() - if self.game_transition is not None: - self.game_transition.draw() - - self.allnet_indicator.draw() - - def draw_3d(self): - pass - -class SongBox: - """A box for the song select screen.""" - OUTLINE_MAP = { - 1: ray.Color(0, 77, 104, 255), - 2: ray.Color(156, 64, 2, 255), - 3: ray.Color(84, 101, 126, 255), - 4: ray.Color(153, 4, 46, 255), - 5: ray.Color(60, 104, 0, 255), - 6: ray.Color(134, 88, 0, 255), - 7: ray.Color(79, 40, 134, 255), - 8: ray.Color(148, 24, 0, 255), - 9: ray.Color(101, 0, 82, 255), - 10: ray.Color(140, 39, 92, 255), - 11: ray.Color(151, 57, 30, 255), - 12: ray.Color(35, 123, 103, 255), - 13: ray.Color(25, 68, 137, 255), - 14: ray.Color(157, 13, 31, 255) - } - BACK_INDEX = 17 - DEFAULT_INDEX = 9 - def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None, - tja_count: Optional[int] = None, box_texture: Optional[str] = None, name_texture_index: Optional[int] = None): - self.text_name = name - self.texture_index = texture_index - if name_texture_index is None: - self.name_texture_index = texture_index - else: - self.name_texture_index = name_texture_index - self.box_texture_path = box_texture - self.box_texture = None - self.scores = dict() - self.crown = dict() - self.position = -11111 - self.start_position = -1 - self.target_position = -1 - self.is_open = False - self.is_back = self.texture_index == SongBox.BACK_INDEX - if self.is_back: - for i in range(1, 16): - if audio.is_sound_playing(f'genre_voice_{i}'): - audio.stop_sound(f'genre_voice_{i}') - self.name = None - self.black_name = None - self.hori_name = None - self.yellow_box = None - self.open_anim = Animation.create_move(133, start_position=0, total_distance=150, delay=83.33) - self.open_fade = Animation.create_fade(200, initial_opacity=0, final_opacity=1.0) - self.move = None - self.wait = 0 - self.is_dir = is_dir - self.tja_count = tja_count - self.tja_count_text = None - self.score_history = None - self.history_wait = 0 - self.tja = tja - self.hash = dict() - self.is_favorite = False - - def reset(self): - if self.yellow_box is not None: - self.yellow_box.reset() - self.yellow_box.create_anim() - if self.name is not None: - self.name.unload() - self.name = None - if self.box_texture is not None: - ray.unload_texture(self.box_texture) - self.box_texture = None - if self.black_name is not None: - self.black_name.unload() - self.black_name = None - if self.hori_name is not None: - self.hori_name.unload() - self.hori_name = None - self.is_open = False - - def get_scores(self): - if self.tja is None: - return - with sqlite3.connect('scores.db') as con: - cursor = con.cursor() - # Batch database query for all diffs at once - if self.tja.metadata.course_data: - hash_values = [self.hash[diff] for diff in self.tja.metadata.course_data if diff in self.hash] - placeholders = ','.join('?' * len(hash_values)) - - batch_query = f""" - SELECT hash, score, good, ok, bad, clear - FROM Scores - WHERE hash IN ({placeholders}) - """ - cursor.execute(batch_query, hash_values) - - hash_to_score = {row[0]: row[1:] for row in cursor.fetchall()} - - for diff in self.tja.metadata.course_data: - if diff not in self.hash: - continue - diff_hash = self.hash[diff] - self.scores[diff] = hash_to_score.get(diff_hash) - - 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, start_position=0, total_distance=100 * direction, ease_out='cubic') - self.move.start() - if self.is_open or self.target_position == SongSelectScreen.BOX_CENTER + 150: - self.move.total_distance = 250 * 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 - - def update(self, is_diff_select): - self.is_diff_select = is_diff_select - is_open_prev = self.is_open - self.move_box() - self.is_open = self.position == SongSelectScreen.BOX_CENTER + 150 - - if not (-56 <= self.position <= 1280): - return - if self.yellow_box is not None: - self.yellow_box.update(is_diff_select) - - if self.history_wait == 0: - self.history_wait = get_current_ms() - - 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()) - - if not is_open_prev and self.is_open: - if self.black_name is None: - self.black_name = OutlinedText(self.text_name, 40, ray.WHITE, ray.BLACK, outline_thickness=5, vertical=True) - if self.tja is not None or self.is_back: - self.yellow_box = YellowBox(self.black_name, self.is_back, tja=self.tja) - self.yellow_box.create_anim() - else: - self.hori_name = OutlinedText(self.text_name, 40, ray.WHITE, ray.BLACK, 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() - 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}'): - audio.stop_sound(f'genre_voice_{self.texture_index}') - if self.tja_count is not None and self.tja_count > 0 and self.tja_count_text is None: - self.tja_count_text = OutlinedText(str(self.tja_count), 35, ray.WHITE, ray.BLACK, outline_thickness=5)#, horizontal_spacing=1.2) - 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()) - - if self.name is None: - self.name = OutlinedText(self.text_name, 40, ray.WHITE, SongBox.OUTLINE_MAP.get(self.name_texture_index, ray.Color(101, 0, 82, 255)), outline_thickness=5, vertical=True) - - if self.score_history is not None: - self.score_history.update(get_current_ms()) - - - def _draw_closed(self, x: int, y: int): - tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x) - offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0 - tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=32, y=offset) - tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x) - if self.texture_index == SongBox.DEFAULT_INDEX: - tex.draw_texture('box', 'genre_overlay', x=x, y=y) - elif self.texture_index == 14: - tex.draw_texture('box', 'diff_overlay', x=x, y=y) - if not self.is_back and self.is_dir: - tex.draw_texture('box', 'folder_clip', frame=self.texture_index, x=x - (1 - offset), y=y) - - if self.is_back: - tex.draw_texture('box', 'back_text', x=x, y=y) - elif self.name is not None: - dest = ray.Rectangle(x + 47 - int(self.name.texture.width / 2), y+35, self.name.texture.width, min(self.name.texture.height, 417)) - self.name.draw(self.name.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE) - - if self.tja is not None and self.tja.ex_data.new: - tex.draw_texture('yellow_box', 'ex_data_new_song_balloon', x=x, y=y) - valid_scores = {k: v for k, v in self.scores.items() if v is not None} - if valid_scores: - highest_key = max(valid_scores.keys()) - score = self.scores[highest_key] - if score and ((score[4] == 2 and score[2] == 0) or (score[2] == 0 and score[3] == 0)): - tex.draw_texture('yellow_box', 'crown_dfc', x=x, y=y, frame=min(4, highest_key)) - elif score and ((score[4] == 2) or (score[3] == 0)): - tex.draw_texture('yellow_box', 'crown_fc', x=x, y=y, frame=min(4, highest_key)) - elif score and score[4] >= 1: - tex.draw_texture('yellow_box', 'crown_clear', x=x, y=y, frame=min(4, highest_key)) - if self.crown: #Folder lamp - highest_crown = max(self.crown) - if self.crown[highest_crown] == 'DFC': - tex.draw_texture('yellow_box', 'crown_dfc', x=x, y=y, frame=min(4, highest_crown)) - elif self.crown[highest_crown] == 'FC': - tex.draw_texture('yellow_box', 'crown_fc', x=x, y=y, frame=min(4, highest_crown)) - else: - tex.draw_texture('yellow_box', 'crown_clear', x=x, y=y, frame=min(4, highest_crown)) - - def _draw_open(self, x: int, y: int, fade_override: Optional[float]): - color = ray.WHITE - if fade_override is not None: - color = ray.fade(ray.WHITE, fade_override) - if self.hori_name is not None and self.open_anim.attribute >= 100: - tex.draw_texture('box', 'folder_top_edge', x=x, y=y - self.open_anim.attribute, color=color, mirror='horizontal', frame=self.texture_index) - tex.draw_texture('box', 'folder_top', x=x, y=y - self.open_anim.attribute, color=color, frame=self.texture_index) - tex.draw_texture('box', 'folder_top_edge', x=x+268, y=y - self.open_anim.attribute, color=color, frame=self.texture_index) - dest_width = min(300, self.hori_name.texture.width) - dest = ray.Rectangle((x + 48) - (dest_width//2), y + 107 - self.open_anim.attribute, dest_width, self.hori_name.texture.height) - self.hori_name.draw(self.hori_name.default_src, dest, ray.Vector2(0, 0), 0, color) - - tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x - self.open_anim.attribute) - offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0 - tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x - self.open_anim.attribute, y=offset, x2=(self.open_anim.attribute*2)+32) - tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x + self.open_anim.attribute) - - if self.texture_index == SongBox.DEFAULT_INDEX: - tex.draw_texture('box', 'genre_overlay_large', x=x, y=y, color=color) - elif self.texture_index == 14: - tex.draw_texture('box', 'diff_overlay_large', x=x, y=y, color=color) - - color = ray.WHITE - if fade_override is not None: - color = ray.fade(ray.WHITE, fade_override) - if self.tja_count_text is not None and self.texture_index != 14: - tex.draw_texture('yellow_box', 'song_count_back', color=color, fade=0.5) - tex.draw_texture('yellow_box', 'song_count_num', color=color) - tex.draw_texture('yellow_box', 'song_count_songs', color=color) - dest_width = min(124, self.tja_count_text.texture.width) - dest = ray.Rectangle(560 - (dest_width//2), 126, dest_width, self.tja_count_text.texture.height) - self.tja_count_text.draw(self.tja_count_text.default_src, dest, ray.Vector2(0, 0), 0, color) - if self.texture_index != 9: - tex.draw_texture('box', 'folder_graphic', color=color, frame=self.texture_index) - tex.draw_texture('box', 'folder_text', color=color, frame=self.texture_index) - elif self.box_texture is not None: - ray.draw_texture(self.box_texture, (x+48) - (self.box_texture.width//2), (y+240) - (self.box_texture.height//2), color) - - def draw_score_history(self): - if self.is_open and get_current_ms() >= self.wait + 83.33: - if self.score_history is not None and get_current_ms() >= self.history_wait + 3000: - self.score_history.draw() - def draw(self, x: int, y: int, is_ura: bool, fade_override=None): - if self.is_open and get_current_ms() >= self.wait + 83.33: - if self.yellow_box is not None: - self.yellow_box.draw(self, fade_override, is_ura) - else: - self._draw_open(x, y, self.open_fade.attribute) - else: - self._draw_closed(x, y) - -class YellowBox: - """A song box when it is opened.""" - def __init__(self, name: OutlinedText, is_back: bool, tja: Optional[TJAParser] = None): - self.is_diff_select = False - self.name = name - self.is_back = is_back - self.tja = tja - self.subtitle = None - if self.tja is not None: - subtitle_text = self.tja.metadata.subtitle.get(global_data.config['general']['language'], '') - font_size = 30 if len(subtitle_text) < 30 else 20 - self.subtitle = OutlinedText(subtitle_text, font_size, ray.WHITE, ray.BLACK, outline_thickness=5, vertical=True) - - self.left_out = tex.get_animation(9) - self.right_out = tex.get_animation(10) - self.center_out = tex.get_animation(11) - self.fade = tex.get_animation(12) - - self.left_out.reset() - self.right_out.reset() - self.center_out.reset() - self.fade.reset() - - self.left_out_2 = tex.get_animation(13) - self.right_out_2 = tex.get_animation(14) - self.center_out_2 = tex.get_animation(15) - self.top_y_out = tex.get_animation(16) - self.center_h_out = tex.get_animation(17) - self.fade_in = tex.get_animation(18) - - self.right_out_2.reset() - self.top_y_out.reset() - self.center_h_out.reset() - - self.right_x = self.right_out.attribute - self.left_x = self.left_out.attribute - self.center_width = self.center_out.attribute - self.top_y = self.top_y_out.attribute - self.center_height = self.center_h_out.attribute - self.bottom_y = tex.textures['yellow_box']['yellow_box_bottom_right'].y[0] - self.edge_height = tex.textures['yellow_box']['yellow_box_bottom_right'].height - - def reset(self): - if self.subtitle is not None: - self.subtitle.unload() - self.subtitle = None - - def create_anim(self): - self.right_out_2.reset() - self.top_y_out.reset() - self.center_h_out.reset() - self.left_out.start() - self.right_out.start() - self.center_out.start() - self.fade.start() - - def create_anim_2(self): - self.left_out_2.start() - self.right_out_2.start() - self.center_out_2.start() - self.top_y_out.start() - self.center_h_out.start() - self.fade_in.start() - - def update(self, is_diff_select: bool): - current_time = get_current_ms() - self.left_out.update(current_time) - self.right_out.update(current_time) - self.center_out.update(current_time) - self.fade.update(current_time) - self.fade_in.update(current_time) - self.left_out_2.update(current_time) - self.right_out_2.update(current_time) - self.center_out_2.update(current_time) - self.top_y_out.update(current_time) - self.center_h_out.update(current_time) - if is_diff_select and not self.is_diff_select: - self.create_anim_2() - if self.is_diff_select: - self.right_x = self.right_out_2.attribute - self.left_x = self.left_out_2.attribute - self.top_y = self.top_y_out.attribute - self.center_width = self.center_out_2.attribute - self.center_height = self.center_h_out.attribute - else: - self.right_x = self.right_out.attribute - self.left_x = self.left_out.attribute - self.center_width = self.center_out.attribute - self.top_y = self.top_y_out.attribute - self.center_height = self.center_h_out.attribute - self.is_diff_select = is_diff_select - - def _draw_tja_data(self, song_box, color, fade): - if self.tja is None: - return - for diff in self.tja.metadata.course_data: - if diff >= 4: - continue - elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] == 2 and song_box.scores[diff][2] == 0) or (song_box.scores[diff][2] == 0 and song_box.scores[diff][3] == 0)): - tex.draw_texture('yellow_box', 's_crown_dfc', x=(diff*60), color=color) - elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] == 2) or (song_box.scores[diff][3] == 0)): - tex.draw_texture('yellow_box', 's_crown_fc', x=(diff*60), color=color) - elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][4] >= 1: - tex.draw_texture('yellow_box', 's_crown_clear', x=(diff*60), color=color) - tex.draw_texture('yellow_box', 's_crown_outline', x=(diff*60), fade=min(fade, 0.25)) - - if self.tja.ex_data.new_audio: - tex.draw_texture('yellow_box', 'ex_data_new_audio', color=color) - elif self.tja.ex_data.old_audio: - tex.draw_texture('yellow_box', 'ex_data_old_audio', color=color) - elif self.tja.ex_data.limited_time: - tex.draw_texture('yellow_box', 'ex_data_limited_time', color=color) - elif self.tja.ex_data.new: - tex.draw_texture('yellow_box', 'ex_data_new_song', color=color) - if song_box.is_favorite: - tex.draw_texture('yellow_box', f'favorite_{global_data.player_num}p', color=color) - - for i in range(4): - tex.draw_texture('yellow_box', 'difficulty_bar', frame=i, x=(i*60), color=color) - if i not in self.tja.metadata.course_data: - tex.draw_texture('yellow_box', 'difficulty_bar_shadow', frame=i, x=(i*60), fade=min(fade, 0.25)) - - for diff in self.tja.metadata.course_data: - if diff >= 4: - continue - for j in range(self.tja.metadata.course_data[diff].level): - tex.draw_texture('yellow_box', 'star', x=(diff*60), y=(j*-17), color=color) - if self.tja.metadata.course_data[diff].is_branching and (get_current_ms() // 1000) % 2 == 0: - tex.draw_texture('yellow_box', 'branch_indicator', x=(diff*60), color=color) - - def _draw_tja_data_diff(self, is_ura: bool, song_box): - if self.tja is None: - return - tex.draw_texture('diff_select', 'back', fade=self.fade_in.attribute) - tex.draw_texture('diff_select', 'option', fade=self.fade_in.attribute) - tex.draw_texture('diff_select', 'neiro', fade=self.fade_in.attribute) - - for diff in self.tja.metadata.course_data: - if diff >= 4: - continue - elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] == 2 and song_box.scores[diff][2] == 0) or (song_box.scores[diff][2] == 0 and song_box.scores[diff][3] == 0)): - tex.draw_texture('yellow_box', 's_crown_dfc', x=(diff*115)+8, y=-120, fade=self.fade_in.attribute) - elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] == 2) or (song_box.scores[diff][3] == 0)): - tex.draw_texture('yellow_box', 's_crown_fc', x=(diff*115)+8, y=-120, fade=self.fade_in.attribute) - elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][4] >= 1: - tex.draw_texture('yellow_box', 's_crown_clear', x=(diff*115)+8, y=-120, fade=self.fade_in.attribute) - tex.draw_texture('yellow_box', 's_crown_outline', x=(diff*115)+8, y=-120, fade=min(self.fade_in.attribute, 0.25)) - - for i in range(4): - if i == 3 and is_ura: - tex.draw_texture('diff_select', 'diff_tower', frame=4, x=(i*115), fade=self.fade_in.attribute) - tex.draw_texture('diff_select', 'ura_oni_plate', fade=self.fade_in.attribute) - else: - tex.draw_texture('diff_select', 'diff_tower', frame=i, x=(i*115), fade=self.fade_in.attribute) - if i not in self.tja.metadata.course_data: - tex.draw_texture('diff_select', 'diff_tower_shadow', frame=i, x=(i*115), fade=min(self.fade_in.attribute, 0.25)) - - for course in self.tja.metadata.course_data: - if (course == 4 and not is_ura) or (course == 3 and is_ura): - continue - for j in range(self.tja.metadata.course_data[course].level): - tex.draw_texture('yellow_box', 'star_ura', x=min(course, 3)*115, y=(j*-20), fade=self.fade_in.attribute) - if self.tja.metadata.course_data[course].is_branching and (get_current_ms() // 1000) % 2 == 0: - if course == 4: - name = 'branch_indicator_ura' - else: - name = 'branch_indicator_diff' - tex.draw_texture('yellow_box', name, x=min(course, 3)*115, fade=self.fade_in.attribute) - - def _draw_text(self, song_box): - if not isinstance(self.right_out, MoveAnimation): - return - if not isinstance(self.right_out_2, MoveAnimation): - return - if not isinstance(self.top_y_out, MoveAnimation): - return - x = song_box.position + (self.right_out.attribute*0.85 - (self.right_out.start_position*0.85)) + self.right_out_2.attribute - self.right_out_2.start_position - if self.is_back: - tex.draw_texture('box', 'back_text_highlight', x=x) - elif self.name is not None: - texture = self.name.texture - dest = ray.Rectangle(x + 30, 35 + self.top_y_out.attribute, texture.width, min(texture.height, 417)) - self.name.draw(self.name.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE) - if self.subtitle is not None: - texture = self.subtitle.texture - y = self.bottom_y - min(texture.height, 410) + 10 + self.top_y_out.attribute - self.top_y_out.start_position - dest = ray.Rectangle(x - 15, y, texture.width, min(texture.height, 410)) - self.subtitle.draw(self.subtitle.default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE) - - def _draw_yellow_box(self): - tex.draw_texture('yellow_box', 'yellow_box_bottom_right', x=self.right_x) - tex.draw_texture('yellow_box', 'yellow_box_bottom_left', x=self.left_x, y=self.bottom_y) - tex.draw_texture('yellow_box', 'yellow_box_top_right', x=self.right_x, y=self.top_y) - tex.draw_texture('yellow_box', 'yellow_box_top_left', x=self.left_x, y=self.top_y) - tex.draw_texture('yellow_box', 'yellow_box_bottom', x=self.left_x + self.edge_height, y=self.bottom_y, x2=self.center_width) - tex.draw_texture('yellow_box', 'yellow_box_right', x=self.right_x, y=self.top_y + self.edge_height, y2=self.center_height) - tex.draw_texture('yellow_box', 'yellow_box_left', x=self.left_x, y=self.top_y + self.edge_height, y2=self.center_height) - tex.draw_texture('yellow_box', 'yellow_box_top', x=self.left_x + self.edge_height, y=self.top_y, x2=self.center_width) - tex.draw_texture('yellow_box', 'yellow_box_center', x=self.left_x + self.edge_height, y=self.top_y + self.edge_height, x2=self.center_width, y2=self.center_height) - - def draw(self, song_box: SongBox, fade_override: Optional[float], is_ura: bool): - self._draw_yellow_box() - if self.is_diff_select and self.tja is not None: - self._draw_tja_data_diff(is_ura, song_box) - else: - fade = self.fade.attribute - if fade_override is not None: - fade = min(self.fade.attribute, fade_override) - if self.is_back: - tex.draw_texture('box', 'back_graphic', fade=fade) - self._draw_tja_data(song_box, ray.fade(ray.WHITE, fade), fade) - - self._draw_text(song_box) - -class GenreBG: - """The background for a genre box.""" - def __init__(self, start_box: SongBox, end_box: SongBox, title: OutlinedText, diff_sort: Optional[int]): - self.start_box = start_box - self.end_box = end_box - self.start_position = start_box.position - self.end_position = end_box.position - self.title = title - self.fade_in = Animation.create_fade(116, initial_opacity=0.0, final_opacity=1.0, ease_in='quadratic', delay=50) - self.fade_in.start() - self.diff_num = diff_sort - def update(self, current_ms): - self.start_position = self.start_box.position - self.end_position = self.end_box.position - self.fade_in.update(current_ms) - def draw(self, y): - offset = -150 if self.start_box.is_open else 0 - - tex.draw_texture('box', 'folder_background_edge', frame=self.end_box.texture_index, x=self.start_position+offset, y=y, mirror="horizontal", fade=self.fade_in.attribute) - - - extra_distance = 155 if self.end_box.is_open or (self.start_box.is_open and 844 <= self.end_position <= 1144) else 0 - if self.start_position >= -56 and self.end_position < self.start_position: - x2 = self.start_position + 1400 - x = self.start_position+offset - elif (self.start_position <= -56) and (self.end_position < self.start_position): - x = 0 - x2 = 1280 - else: - x2 = abs(self.end_position) - self.start_position + extra_distance + 57 - x = self.start_position+offset - tex.draw_texture('box', 'folder_background', x=x, y=y, x2=x2, frame=self.end_box.texture_index) - - - if self.end_position < self.start_position and self.end_position >= -56: - x2 = min(self.end_position+75, 1280) + extra_distance - tex.draw_texture('box', 'folder_background', x=-18, y=y, x2=x2, frame=self.end_box.texture_index) - - - offset = 150 if self.end_box.is_open else 0 - tex.draw_texture('box', 'folder_background_edge', x=self.end_position+80+offset, y=y, fade=self.fade_in.attribute, frame=self.end_box.texture_index) - - if ((self.start_position <= 594 and self.end_position >= 594) or - ((self.start_position <= 594 or self.end_position >= 594) and (self.start_position > self.end_position))): - offset = 100 if self.diff_num is not None else 0 - dest_width = min(300, self.title.texture.width) - tex.draw_texture('box', 'folder_background_folder', x=-((offset+dest_width)//2), y=y-2, x2=dest_width+offset - 10, fade=self.fade_in.attribute, frame=self.end_box.texture_index) - tex.draw_texture('box', 'folder_background_folder_edge', x=-((offset+dest_width)//2), y=y-2, fade=self.fade_in.attribute, frame=self.end_box.texture_index, mirror="horizontal") - tex.draw_texture('box', 'folder_background_folder_edge', x=((offset+dest_width)//2)+20, y=y-2, fade=self.fade_in.attribute, frame=self.end_box.texture_index) - if self.diff_num is not None: - tex.draw_texture('diff_sort', 'star_num', frame=self.diff_num, x=-150 + (dest_width//2), y=-143) - dest = ray.Rectangle((1280//2) - (dest_width//2)-(offset//2), y-60, dest_width, self.title.texture.height) - self.title.draw(self.title.default_src, dest, ray.Vector2(0, 0), 0, ray.fade(ray.WHITE, self.fade_in.attribute)) - class UraSwitchAnimation: """The animation for the ura switch.""" def __init__(self) -> None: @@ -1383,13 +947,14 @@ class DiffSortSelect: class NeiroSelector: """The menu for selecting the game hitsounds.""" - def __init__(self): - self.selected_sound = global_data.hit_sound + def __init__(self, player_num: str): + self.player_num = player_num + self.selected_sound = global_data.hit_sound[int(self.player_num)-1] with open(Path("Sounds") / 'hit_sounds' / 'neiro_list.txt', encoding='utf-8-sig') as neiro_list: self.sounds = neiro_list.readlines() self.sounds.append('無音') self.load_sound() - audio.play_sound(f'voice_hitsound_select_{global_data.player_num}p', 'voice') + audio.play_sound(f'voice_hitsound_select_{self.player_num}p', 'voice') self.is_finished = False self.is_confirmed = False self.move = tex.get_animation(28) @@ -1444,9 +1009,9 @@ class NeiroSelector: if self.move.is_started and not self.move.is_finished: return if self.selected_sound == len(self.sounds): - global_data.hit_sound = -1 + global_data.hit_sound[int(self.player_num)-1] = -1 else: - global_data.hit_sound = self.selected_sound + global_data.hit_sound[int(self.player_num)-1] = self.selected_sound self.is_confirmed = True self.move.restart() @@ -1466,9 +1031,9 @@ class NeiroSelector: y = -370 + self.move.attribute else: y = -self.move.attribute - x = (global_data.player_num - 1) * 800 + x = (int(self.player_num) - 1) * 800 tex.draw_texture('neiro', 'background', x=x, y=y) - tex.draw_texture('neiro', f'{global_data.player_num}p', x=x, y=y) + tex.draw_texture('neiro', f'{self.player_num}p', x=x, y=y) tex.draw_texture('neiro', 'divisor', x=x, y=y) tex.draw_texture('neiro', 'music_note', y=y, x=x+(self.move_sideways.attribute*self.direction), fade=self.fade_sideways.attribute) tex.draw_texture('neiro', 'music_note', y=y, x=x+(self.direction*-100) + (self.move_sideways.attribute*self.direction), fade=1 - self.fade_sideways.attribute) @@ -1507,7 +1072,8 @@ class ModifierSelector: "inverse": "あべこべ", "random": "ランダム" } - def __init__(self): + def __init__(self, player_num: str): + self.player_num = player_num self.mods = fields(Modifiers) self.current_mod_index = 0 self.is_confirmed = False @@ -1519,18 +1085,18 @@ class ModifierSelector: self.move_sideways = tex.get_animation(31) self.fade_sideways = tex.get_animation(32) self.direction = -1 - audio.play_sound(f'voice_options_{global_data.player_num}p', 'sound') + audio.play_sound(f'voice_options_{self.player_num}p', 'sound') self.text_name = [OutlinedText(ModifierSelector.NAME_MAP[mod.name], 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) for mod in self.mods] self.text_true = OutlinedText('する', 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) self.text_false = OutlinedText('しない', 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) - self.text_speed = OutlinedText(str(global_data.modifiers.speed), 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) + self.text_speed = OutlinedText(str(global_data.modifiers[int(self.player_num)-1].speed), 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) self.text_kimagure = OutlinedText('きまぐれ', 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) self.text_detarame = OutlinedText('でたらめ', 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) # Secondary text objects for animation self.text_true_2 = OutlinedText('する', 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) self.text_false_2 = OutlinedText('しない', 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) - self.text_speed_2 = OutlinedText(str(global_data.modifiers.speed), 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) + self.text_speed_2 = OutlinedText(str(global_data.modifiers[int(self.player_num)-1].speed), 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) self.text_kimagure_2 = OutlinedText('きまぐれ', 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) self.text_detarame_2 = OutlinedText('でたらめ', 30, ray.WHITE, ray.BLACK, outline_thickness=3.5) @@ -1546,7 +1112,7 @@ class ModifierSelector: self.fade_sideways.update(current_ms) if self.move_sideways.is_finished and not self.is_confirmed: current_mod = self.mods[self.current_mod_index] - current_value = getattr(global_data.modifiers, current_mod.name) + current_value = getattr(global_data.modifiers[int(self.player_num)-1], current_mod.name) if current_mod.name == 'speed': self.text_speed.unload() @@ -1567,7 +1133,7 @@ class ModifierSelector: # Update secondary text objects for the new values current_mod = self.mods[self.current_mod_index] - current_value = getattr(global_data.modifiers, current_mod.name) + current_value = getattr(global_data.modifiers[int(self.player_num)-1], current_mod.name) if current_mod.name == 'speed': self.text_speed_2.unload() @@ -1577,30 +1143,30 @@ class ModifierSelector: if self.is_confirmed: return current_mod = self.mods[self.current_mod_index] - current_value = getattr(global_data.modifiers, current_mod.name) + current_value = getattr(global_data.modifiers[int(self.player_num)-1], current_mod.name) if current_mod.type is bool: - setattr(global_data.modifiers, current_mod.name, not current_value) + setattr(global_data.modifiers[int(self.player_num)-1], current_mod.name, not current_value) self._start_text_animation(-1) elif current_mod.name == 'speed': - setattr(global_data.modifiers, current_mod.name, max(0.1, (current_value*10 - 1))/10) + setattr(global_data.modifiers[int(self.player_num)-1], current_mod.name, max(0.1, (current_value*10 - 1))/10) self._start_text_animation(-1) elif current_mod.name == 'random': - setattr(global_data.modifiers, current_mod.name, max(0, current_value-1)) + setattr(global_data.modifiers[int(self.player_num)-1], current_mod.name, max(0, current_value-1)) self._start_text_animation(-1) def right(self): if self.is_confirmed: return current_mod = self.mods[self.current_mod_index] - current_value = getattr(global_data.modifiers, current_mod.name) + current_value = getattr(global_data.modifiers[int(self.player_num)-1], current_mod.name) if current_mod.type is bool: - setattr(global_data.modifiers, current_mod.name, not current_value) + setattr(global_data.modifiers[int(self.player_num)-1], current_mod.name, not current_value) self._start_text_animation(1) elif current_mod.name == 'speed': - setattr(global_data.modifiers, current_mod.name, (current_value*10 + 1)/10) + setattr(global_data.modifiers[int(self.player_num)-1], current_mod.name, (current_value*10 + 1)/10) self._start_text_animation(1) elif current_mod.name == 'random': - setattr(global_data.modifiers, current_mod.name, (current_value+1) % 3) + setattr(global_data.modifiers[int(self.player_num)-1], current_mod.name, (current_value+1) % 3) self._start_text_animation(1) def _draw_animated_text(self, text_primary, text_secondary, x, y, should_animate): @@ -1626,9 +1192,9 @@ class ModifierSelector: move = self.move.attribute - 370 else: move = -self.move.attribute - x = (global_data.player_num - 1) * 800 + x = (int(self.player_num) - 1) * 800 tex.draw_texture('modifier', 'top', y=move, x=x) - tex.draw_texture('modifier', f'{global_data.player_num}p', y=move, x=x) + tex.draw_texture('modifier', f'{self.player_num}p', y=move, x=x) tex.draw_texture('modifier', 'bottom', y=move + (len(self.mods)*50), x=x) for i in range(len(self.mods)): @@ -1642,7 +1208,7 @@ class ModifierSelector: self.text_name[i].draw(self.text_name[i].default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE) current_mod = self.mods[i] - current_value = getattr(global_data.modifiers, current_mod.name) + current_value = getattr(global_data.modifiers[int(self.player_num)-1], current_mod.name) is_current_mod = (i == self.current_mod_index) if current_mod.type is bool: @@ -1685,787 +1251,3 @@ 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 ScoreHistory: - """The score information that appears while hovering over a song""" - def __init__(self, scores: dict[int, tuple[int, int, int, int]], current_ms): - """ - Initialize the score history with the given scores and current time. - - Args: - scores (dict[int, tuple[int, int, int, int]]): A dictionary of scores for each difficulty level. - current_ms (int): The current time in milliseconds. - """ - self.scores = {k: v for k, v in scores.items() if v is not None} - self.difficulty_keys = list(self.scores.keys()) - self.curr_difficulty_index = 0 - self.curr_difficulty_index = (self.curr_difficulty_index + 1) % len(self.difficulty_keys) - self.curr_difficulty = self.difficulty_keys[self.curr_difficulty_index] - self.curr_score = self.scores[self.curr_difficulty][0] - self.curr_score_su = self.scores[self.curr_difficulty][0] - self.last_ms = current_ms - - def update(self, current_ms): - if current_ms >= self.last_ms + 1000: - self.last_ms = current_ms - self.curr_difficulty_index = (self.curr_difficulty_index + 1) % len(self.difficulty_keys) - self.curr_difficulty = self.difficulty_keys[self.curr_difficulty_index] - self.curr_score = self.scores[self.curr_difficulty][0] - self.curr_score_su = self.scores[self.curr_difficulty][0] - - def draw(self): - tex.draw_texture('leaderboard','background') - tex.draw_texture('leaderboard','title') - - if self.curr_difficulty == 4: - tex.draw_texture('leaderboard', 'normal_ura') - tex.draw_texture('leaderboard', 'shinuchi_ura') - else: - tex.draw_texture('leaderboard', 'normal') - tex.draw_texture('leaderboard', 'shinuchi') - - color = ray.BLACK - if self.curr_difficulty == 4: - color = ray.WHITE - tex.draw_texture('leaderboard','ura') - - tex.draw_texture('leaderboard', 'pts', color=color) - tex.draw_texture('leaderboard', 'pts', y=50) - - tex.draw_texture('leaderboard', 'difficulty', frame=self.curr_difficulty) - - counter = str(self.curr_score) - total_width = len(counter) * 14 - for i in range(len(counter)): - tex.draw_texture('leaderboard', 'counter', frame=int(counter[i]), x=-(total_width // 2) + (i * 14), color=color) - - counter = str(self.curr_score_su) - total_width = len(counter) * 14 - for i in range(len(counter)): - tex.draw_texture('leaderboard', 'counter', frame=int(counter[i]), x=-(total_width // 2) + (i * 14), y=50, color=ray.WHITE) - -class FileSystemItem: - GENRE_MAP = { - 'J-POP': 1, - 'アニメ': 2, - 'VOCALOID': 3, - 'どうよう': 4, - 'バラエティー': 5, - 'クラシック': 6, - 'ゲームミュージック': 7, - 'ナムコオリジナル': 8, - 'RECOMMENDED': 10, - 'FAVORITE': 11, - 'RECENT': 12, - '段位道場': 13, - 'DIFFICULTY': 14 - } - GENRE_MAP_2 = { - 'ボーカロイド': 3, - 'バラエティ': 5 - } - """Base class for files and directories in the navigation system""" - def __init__(self, path: Path, name: str): - self.path = path - self.name = name - -class Directory(FileSystemItem): - """Represents a directory in the navigation system""" - COLLECTIONS = [ - 'NEW', - 'RECENT', - 'FAVORITE', - 'DIFFICULTY', - 'RECOMMENDED' - ] - def __init__(self, path: Path, name: str, texture_index: int, has_box_def=False, to_root=False, back=False, tja_count=0, box_texture=None, collection=None): - super().__init__(path, name) - self.has_box_def = has_box_def - self.to_root = to_root - self.back = back - self.tja_count = tja_count - self.collection = None - if collection in Directory.COLLECTIONS: - self.collection = collection - if collection in FileSystemItem.GENRE_MAP: - texture_index = FileSystemItem.GENRE_MAP[collection] - elif self.to_root or self.back: - texture_index = SongBox.BACK_INDEX - - self.box = SongBox(name, texture_index, True, tja_count=tja_count, box_texture=box_texture) - -class SongFile(FileSystemItem): - """Represents a song file (TJA) in the navigation system""" - def __init__(self, path: Path, name: str, texture_index: int, tja=None, name_texture_index: Optional[int]=None): - super().__init__(path, name) - self.is_recent = (datetime.now() - datetime.fromtimestamp(path.stat().st_mtime)) <= timedelta(days=7) - self.tja = tja or TJAParser(path) - if self.is_recent: - self.tja.ex_data.new = True - title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) - self.hash = global_data.song_paths[path] - self.box = SongBox(title, texture_index, False, tja=self.tja, name_texture_index=name_texture_index if name_texture_index is not None else texture_index) - self.box.hash = global_data.song_hashes[self.hash][0]["diff_hashes"] - self.box.get_scores() - -class FileNavigator: - """Manages navigation through pre-generated Directory and SongFile objects""" - def __init__(self, root_dirs: list[str]): - self.root_dirs = [Path(p) if not isinstance(p, Path) else p for p in root_dirs] - - # Pre-generated objects storage - self.all_directories: dict[str, Directory] = {} # path -> Directory - self.all_song_files: dict[str, SongFile] = {} # path -> SongFile - self.directory_contents: dict[str, list[Union[Directory, SongFile]]] = {} # path -> list of items - - # OPTION 2: Lazy crown calculation with caching - self.directory_crowns: dict[str, dict] = dict() # path -> crown list - self.crown_cache_dirty: set[str] = set() # directories that need crown recalculation - - # Navigation state - simplified without root-specific state - self.current_dir = Path() # Empty path represents virtual root - self.items: list[Directory | SongFile] = [] - self.new_items: list[Directory | SongFile] = [] - self.favorite_folder: Optional[Directory] = None - self.recent_folder: Optional[Directory] = None - self.selected_index = 0 - self.diff_sort_diff = 4 - self.diff_sort_level = 10 - self.diff_sort_statistics = dict() - self.history = [] - self.box_open = False - self.genre_bg = None - self.song_count = 0 - - # Generate all objects upfront - self._generate_all_objects() - self._create_virtual_root() - self.load_current_directory() - - def _create_virtual_root(self): - """Create a virtual root directory containing all root directories""" - virtual_root_items = [] - - for root_path in self.root_dirs: - if not root_path.exists(): - continue - - root_key = str(root_path) - if root_key in self.all_directories: - # Root has box.def, add the directory itself - virtual_root_items.append(self.all_directories[root_key]) - else: - # Root doesn't have box.def, add its immediate children with box.def - for child_path in sorted(root_path.iterdir()): - if child_path.is_dir(): - child_key = str(child_path) - if child_key in self.all_directories: - virtual_root_items.append(self.all_directories[child_key]) - - # Also add direct TJA files from root - all_tja_files = self._find_tja_files_recursive(root_path) - for tja_path in sorted(all_tja_files): - song_key = str(tja_path) - if song_key in self.all_song_files: - virtual_root_items.append(self.all_song_files[song_key]) - - # Store virtual root contents (empty path key represents root) - self.directory_contents["."] = virtual_root_items - - def _generate_all_objects(self): - """Generate all Directory and SongFile objects in advance""" - print("Generating all Directory and SongFile objects...") - - # Generate objects for each root directory - for root_path in self.root_dirs: - if not root_path.exists(): - print(f"Root directory does not exist: {root_path}") - continue - - self._generate_objects_recursive(root_path) - - if self.favorite_folder is not None: - song_list = self._read_song_list(self.favorite_folder.path) - for song_obj in song_list: - if str(song_obj) in self.all_song_files: - self.all_song_files[str(song_obj)].box.is_favorite = True - - print(f"Object generation complete. " - f"Directories: {len(self.all_directories)}, " - f"Songs: {len(self.all_song_files)}") - - def _generate_objects_recursive(self, dir_path: Path): - """Recursively generate Directory and SongFile objects for a directory""" - if not dir_path.is_dir(): - return - - dir_key = str(dir_path) - - # Check for box.def - has_box_def = (dir_path / "box.def").exists() - - # Only create Directory objects for directories with box.def - if has_box_def: - # Parse box.def if it exists - name = dir_path.name if dir_path.name else str(dir_path) - texture_index = SongBox.DEFAULT_INDEX - box_texture = None - collection = None - - name, texture_index, collection = self._parse_box_def(dir_path) - box_png_path = dir_path / "box.png" - if box_png_path.exists(): - box_texture = str(box_png_path) - - # Count TJA files for this directory - tja_count = self._count_tja_files(dir_path) - if collection == Directory.COLLECTIONS[4]: - tja_count = 10 - elif collection == Directory.COLLECTIONS[0]: - tja_count = len(self.new_items) - - # Create Directory object - directory_obj = Directory( - dir_path, name, texture_index, - has_box_def=has_box_def, - tja_count=tja_count, - box_texture=box_texture, - collection=collection - ) - if directory_obj.collection == Directory.COLLECTIONS[2]: - self.favorite_folder = directory_obj - elif directory_obj.collection == Directory.COLLECTIONS[1]: - self.recent_folder = directory_obj - self.all_directories[dir_key] = directory_obj - - # Generate content list for this directory - content_items = [] - - # Add child directories that have box.def - child_dirs = [] - for item_path in dir_path.iterdir(): - if item_path.is_dir(): - child_has_box_def = (item_path / "box.def").exists() - if child_has_box_def: - child_dirs.append(item_path) - # Recursively generate objects for child directory - self._generate_objects_recursive(item_path) - - # Sort and add child directories - for child_path in sorted(child_dirs): - child_key = str(child_path) - if child_key in self.all_directories: - content_items.append(self.all_directories[child_key]) - - # Get TJA files for this directory - tja_files = self._get_tja_files_for_directory(dir_path) - - # 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: - song_obj = SongFile(tja_path, tja_path.name, texture_index) - song_obj.box.get_scores() - for course in song_obj.tja.metadata.course_data: - level = song_obj.tja.metadata.course_data[course].level - - scores = song_obj.box.scores.get(course) - if scores is not None: - is_cleared = scores[4] >= 1 - is_full_combo = (scores[4] == 2) or (scores[3] == 0) - else: - is_cleared = False - is_full_combo = False - - if course not in self.diff_sort_statistics: - self.diff_sort_statistics[course] = {} - - if level not in self.diff_sort_statistics[course]: - self.diff_sort_statistics[course][level] = [1, int(is_full_combo), int(is_cleared)] - else: - self.diff_sort_statistics[course][level][0] += 1 - if is_full_combo: - 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.all_song_files[song_key] = song_obj - - if song_key in self.all_song_files: - content_items.append(self.all_song_files[song_key]) - - self.directory_contents[dir_key] = content_items - self.crown_cache_dirty.add(dir_key) - - else: - # For directories without box.def, still process their children - for item_path in dir_path.iterdir(): - if item_path.is_dir(): - self._generate_objects_recursive(item_path) - - # Create SongFile objects for TJA files in non-boxed directories - tja_files = self._find_tja_files_in_directory_only(dir_path) - for tja_path in tja_files: - song_key = str(tja_path) - if song_key not in self.all_song_files: - try: - song_obj = SongFile(tja_path, tja_path.name, SongBox.DEFAULT_INDEX) - self.song_count += 1 - global_data.song_progress = self.song_count / global_data.total_songs - self.all_song_files[song_key] = song_obj - except Exception as e: - print(f"Error creating SongFile for {tja_path}: {e}") - continue - - def is_at_root(self) -> bool: - """Check if currently at the virtual root""" - return self.current_dir == Path() - - def load_current_directory(self, selected_item: Optional[Directory] = None): - """Load pre-generated items for the current directory (unified for root and subdirs)""" - dir_key = str(self.current_dir) - - # Determine if current directory has child directories with box.def - has_children = False - if self.is_at_root(): - has_children = True # Root always has "children" (the root directories) - else: - has_children = any(item.is_dir() and (item / "box.def").exists() - for item in self.current_dir.iterdir()) - - self.genre_bg = None - self.in_favorites = False - - if has_children: - self.items = [] - if not self.box_open: - self.selected_index = 0 - - start_box = None - end_box = None - - # Add back navigation item (only if not at root) - if not self.is_at_root(): - back_dir = Directory(self.current_dir.parent, "", SongBox.BACK_INDEX, back=True) - if not has_children: - start_box = back_dir.box - self.items.insert(self.selected_index, back_dir) - - # Add pre-generated content for this directory - if dir_key in self.directory_contents: - content_items = self.directory_contents[dir_key] - - # Handle special collections (same logic as before) - if isinstance(selected_item, Directory): - if selected_item.collection == Directory.COLLECTIONS[0]: - content_items = self.new_items - elif selected_item.collection == Directory.COLLECTIONS[1]: - if self.recent_folder is None: - raise Exception("tried to enter recent folder without recents") - self._generate_objects_recursive(self.recent_folder.path) - selected_item.box.tja_count_text = None - selected_item.box.tja_count = self._count_tja_files(self.recent_folder.path) - content_items = self.directory_contents[dir_key] - elif selected_item.collection == Directory.COLLECTIONS[2]: - if self.favorite_folder is None: - raise Exception("tried to enter favorite folder without favorites") - self._generate_objects_recursive(self.favorite_folder.path) - tja_files = self._get_tja_files_for_directory(self.favorite_folder.path) - self._calculate_directory_crowns(dir_key, tja_files) - selected_item.box.tja_count_text = None - selected_item.box.tja_count = self._count_tja_files(self.favorite_folder.path) - content_items = self.directory_contents[dir_key] - self.in_favorites = True - elif selected_item.collection == Directory.COLLECTIONS[3]: - content_items = [] - parent_dir = selected_item.path.parent - for sibling_path in parent_dir.iterdir(): - if sibling_path.is_dir() and sibling_path != selected_item.path: - sibling_key = str(sibling_path) - if sibling_key in self.directory_contents: - for item in self.directory_contents[sibling_key]: - if isinstance(item, SongFile) and item: - if self.diff_sort_diff in item.tja.metadata.course_data and item.tja.metadata.course_data[self.diff_sort_diff].level == self.diff_sort_level: - if item not in content_items: - content_items.append(item) - elif selected_item.collection == Directory.COLLECTIONS[4]: - parent_dir = selected_item.path.parent - temp_items = [] - for sibling_path in parent_dir.iterdir(): - if sibling_path.is_dir() and sibling_path != selected_item.path: - sibling_key = str(sibling_path) - if sibling_key in self.directory_contents: - for item in self.directory_contents[sibling_key]: - if not isinstance(item, Directory): - 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): - self.go_back() - return - i = 1 - for item in content_items: - if isinstance(item, SongFile): - if i % 10 == 0 and i != 0: - back_dir = Directory(self.current_dir.parent, "", SongBox.BACK_INDEX, back=True) - self.items.insert(self.selected_index+i, back_dir) - i += 1 - if not has_children: - if selected_item is not None: - item.box.texture_index = selected_item.box.texture_index - self.items.insert(self.selected_index+i, item) - else: - self.items.append(item) - i += 1 - - if not has_children: - self.box_open = True - end_box = content_items[-1].box - if selected_item in self.items: - self.items.remove(selected_item) - - # Calculate crowns for directories - for item in self.items: - if isinstance(item, Directory): - item_key = str(item.path) - if item_key in self.directory_contents: # Only for real directories - item.box.crown = self._get_directory_crowns_cached(item_key) - else: - # Navigation items (back/to_root) - item.box.crown = dict() - - self.calculate_box_positions() - - if (not has_children and start_box is not None - and end_box is not None and selected_item is not None - and selected_item.box.hori_name is not None): - hori_name = selected_item.box.hori_name - diff_sort = None - if selected_item.collection == Directory.COLLECTIONS[3]: - diff_sort = self.diff_sort_level - diffs = ['かんたん', 'ふつう', 'むずかしい', 'おに'] - hori_name = OutlinedText(diffs[min(3, self.diff_sort_diff)], 40, ray.WHITE, ray.BLACK, outline_thickness=5) - self.genre_bg = GenreBG(start_box, end_box, hori_name, diff_sort) - - def select_current_item(self): - """Select the currently highlighted item""" - if not self.items or self.selected_index >= len(self.items): - return - - selected_item = self.items[self.selected_index] - - if isinstance(selected_item, Directory): - if self.box_open: - self.go_back() - return - - if selected_item.back: - # Handle back navigation - if self.current_dir.parent == Path(): - # Going back to root - self.current_dir = Path() - else: - self.current_dir = self.current_dir.parent - else: - # Save current state to history - self.history.append((self.current_dir, self.selected_index)) - self.current_dir = selected_item.path - - self.load_current_directory(selected_item=selected_item) - - elif isinstance(selected_item, SongFile): - return selected_item - - def go_back(self): - """Navigate back to the previous directory""" - if self.history: - previous_dir, previous_index = self.history.pop() - self.current_dir = previous_dir - self.selected_index = previous_index - self.load_current_directory() - self.box_open = False - - def _count_tja_files(self, folder_path: Path): - """Count TJA files in directory""" - tja_count = 0 - - # Find all song_list.txt files recursively - song_list_files = list(folder_path.rglob("song_list.txt")) - - if song_list_files: - # Process all song_list.txt files found - for song_list_path in song_list_files: - with open(song_list_path, 'r', encoding='utf-8-sig') as song_list_file: - tja_count += len([line for line in song_list_file.readlines() if line.strip()]) - # Fallback: Use recursive counting of .tja files - tja_count += sum(1 for _ in folder_path.rglob("*.tja")) - - return tja_count - - def _get_directory_crowns_cached(self, dir_key: str) -> dict: - """Get crowns for a directory, calculating only if needed""" - if dir_key in self.crown_cache_dirty or dir_key not in self.directory_crowns: - # Calculate crowns on-demand - dir_path = Path(dir_key) - tja_files = self._get_tja_files_for_directory(dir_path) - self._calculate_directory_crowns(dir_key, tja_files) - self.crown_cache_dirty.discard(dir_key) - - return self.directory_crowns.get(dir_key, dict()) - - def _calculate_directory_crowns(self, dir_key: str, tja_files: list): - """Pre-calculate crowns for a directory""" - all_scores = dict() - crowns = dict() - - for tja_path in tja_files: - song_key = str(tja_path) - if song_key in self.all_song_files: - song_obj = self.all_song_files[song_key] - for diff in song_obj.box.scores: - if diff not in all_scores: - all_scores[diff] = [] - all_scores[diff].append(song_obj.box.scores[diff]) - - for diff in all_scores: - if all(score is not None and ((score[4] == 2 and score[2] == 0) or (score[2] == 0 and score[3] == 0)) for score in all_scores[diff]): - crowns[diff] = 'DFC' - elif all(score is not None and ((score[4] == 2) or (score[3] == 0)) for score in all_scores[diff]): - crowns[diff] = 'FC' - elif all(score is not None and score[4] >= 1 for score in all_scores[diff]): - crowns[diff] = 'CLEAR' - - self.directory_crowns[dir_key] = crowns - - def _get_tja_files_for_directory(self, directory: Path): - """Get TJA files for a specific directory""" - if (directory / 'song_list.txt').exists(): - return self._read_song_list(directory) - else: - return self._find_tja_files_in_directory_only(directory) - - def _find_tja_files_in_directory_only(self, directory: Path): - """Find TJA files only in the specified directory, not recursively in subdirectories with box.def""" - tja_files: list[Path] = [] - - for path in directory.iterdir(): - if path.is_file() and path.suffix.lower() == ".tja": - tja_files.append(path) - elif path.is_dir(): - # Only recurse into subdirectories that don't have box.def - sub_dir_has_box_def = (path / "box.def").exists() - if not sub_dir_has_box_def: - tja_files.extend(self._find_tja_files_in_directory_only(path)) - - return tja_files - - def _find_tja_files_recursive(self, directory: Path, box_def_dirs_only=True): - tja_files: list[Path] = [] - - has_box_def = (directory / "box.def").exists() - if box_def_dirs_only and has_box_def and directory != self.current_dir: - return [] - - for path in directory.iterdir(): - if path.is_file() and path.suffix.lower() == ".tja": - tja_files.append(path) - elif path.is_dir(): - sub_dir_has_box_def = (path / "box.def").exists() - if not sub_dir_has_box_def: - tja_files.extend(self._find_tja_files_recursive(path, box_def_dirs_only)) - - 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: - print(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] = [] - updated_lines = [] - file_updated = False - with open(path / 'song_list.txt', 'r', encoding='utf-8-sig') as song_list: - for line in song_list: - line = line.strip() - if not line: - continue - - parts = line.split('|') - if len(parts) < 3: - continue - - hash_val, title, subtitle = parts[0], parts[1], parts[2] - original_hash = hash_val - - if hash_val in global_data.song_hashes: - file_path = Path(global_data.song_hashes[hash_val][0]["file_path"]) - if file_path.exists() and file_path not in tja_files: - tja_files.append(file_path) - else: - # Try to find by title and subtitle - for key, value in global_data.song_hashes.items(): - for i in range(len(value)): - song = value[i] - if (song["title"]["en"] == title and - song["subtitle"]["en"] == subtitle and - Path(song["file_path"]).exists()): - hash_val = key - tja_files.append(Path(global_data.song_hashes[hash_val][i]["file_path"])) - break - - if hash_val != original_hash: - file_updated = True - updated_lines.append(f"{hash_val}|{title}|{subtitle}") - - # Write back updated song list if needed - if file_updated: - with open(path / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list: - for line in updated_lines: - print("updated", line) - song_list.write(line + '\n') - - return tja_files - - def calculate_box_positions(self): - """Dynamically calculate box positions based on current selection with wrap-around support""" - if not self.items: - return - - for i, item in enumerate(self.items): - offset = i - self.selected_index - - if offset > len(self.items) // 2: - offset -= len(self.items) - elif offset < -len(self.items) // 2: - offset += len(self.items) - - position = SongSelectScreen.BOX_CENTER + (100 * offset) - if position == SongSelectScreen.BOX_CENTER: - position += 150 - elif position > SongSelectScreen.BOX_CENTER: - position += 300 - else: - position -= 0 - - if item.box.position == -11111: - item.box.position = position - item.box.target_position = position - else: - item.box.target_position = position - - def mark_crowns_dirty_for_song(self, song_file: SongFile): - """Mark directories as needing crown recalculation when a song's score changes""" - song_path = song_file.path - - # Find all directories that contain this song and mark them as dirty - for dir_key, content_items in self.directory_contents.items(): - for item in content_items: - if isinstance(item, SongFile) and item.path == song_path: - self.crown_cache_dirty.add(dir_key) - break - - def navigate_left(self): - """Move selection left with wrap-around""" - if self.items: - self.selected_index = (self.selected_index - 1) % len(self.items) - self.calculate_box_positions() - - def navigate_right(self): - """Move selection right with wrap-around""" - if self.items: - self.selected_index = (self.selected_index + 1) % len(self.items) - self.calculate_box_positions() - - def get_current_item(self): - """Get the currently selected item""" - if self.items and 0 <= self.selected_index < len(self.items): - return self.items[self.selected_index] - raise Exception("No current item available") - - def reset_items(self): - """Reset the items in the song select scene""" - for item in self.items: - item.box.reset() - - def add_recent(self): - """Add the current song to the recent list""" - song = self.get_current_item() - if isinstance(song, Directory): - return - if self.recent_folder is None: - return - - recents_path = self.recent_folder.path / 'song_list.txt' - new_entry = f'{song.hash}|{song.tja.metadata.title["en"]}|{song.tja.metadata.subtitle["en"]}\n' - existing_entries = [] - if recents_path.exists(): - with open(recents_path, 'r', encoding='utf-8-sig') as song_list: - existing_entries = song_list.readlines() - existing_entries = [entry for entry in existing_entries if not entry.startswith(f'{song.hash}|')] - all_entries = [new_entry] + existing_entries - recent_entries = all_entries[:25] - with open(recents_path, 'w', encoding='utf-8-sig') as song_list: - song_list.writelines(recent_entries) - - print("Added recent: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) - - def add_favorite(self) -> bool: - """Add the current song to the favorites list""" - song = self.get_current_item() - if isinstance(song, Directory): - return False - if self.favorite_folder is None: - return False - favorites_path = self.favorite_folder.path / 'song_list.txt' - lines = [] - if not Path(favorites_path).exists(): - Path(favorites_path).touch() - with open(favorites_path, 'r', encoding='utf-8-sig') as song_list: - for line in song_list: - line = line.strip() - if not line: # Skip empty lines - continue - hash, title, subtitle = line.split('|') - if song.hash == hash or (song.tja.metadata.title['en'] == title and song.tja.metadata.subtitle['en'] == subtitle): - if not self.in_favorites: - return False - else: - lines.append(line) - if self.in_favorites: - with open(favorites_path, 'w', encoding='utf-8-sig') as song_list: - for line in lines: - song_list.write(line + '\n') - print("Removed favorite:", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) - else: - with open(favorites_path, 'a', encoding='utf-8-sig') as song_list: - song_list.write(f'{song.hash}|{song.tja.metadata.title["en"]}|{song.tja.metadata.subtitle["en"]}\n') - print("Added favorite: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) - return True diff --git a/scenes/two_player/game.py b/scenes/two_player/game.py index cfccef2..e00ebd8 100644 --- a/scenes/two_player/game.py +++ b/scenes/two_player/game.py @@ -1,12 +1,12 @@ import copy from pathlib import Path -from libs.global_data import Modifiers from libs.tja import TJAParser from libs.utils import get_current_ms from libs.audio import audio -from libs.utils import global_data, session_data +from libs.utils import global_data from libs.video import VideoPlayer -from scenes.game import ClearAnimation, FCAnimation, FailAnimation, GameScreen, Player, Background, SCREEN_WIDTH +import pyray as ray +from scenes.game import ClearAnimation, FCAnimation, FailAnimation, GameScreen, Player, Background, SCREEN_WIDTH, ResultTransition class TwoPlayerGameScreen(GameScreen): def on_screen_start(self): @@ -16,27 +16,52 @@ class TwoPlayerGameScreen(GameScreen): if self.background is not None: self.background.unload() self.background = Background(3, self.bpm, scene_preset=scene_preset) + self.result_transition = ResultTransition(3) def load_hitsounds(self): """Load the hit sounds""" sounds_dir = Path("Sounds") - if global_data.hit_sound == -1: + + # Load hitsounds for 1P + if global_data.hit_sound[0] == -1: audio.load_sound(Path('none.wav'), 'hitsound_don_1p') audio.load_sound(Path('none.wav'), 'hitsound_kat_1p') - if global_data.hit_sound == 0: - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "don.wav", 'hitsound_don_1p') - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "ka.wav", 'hitsound_kat_1p') - audio.set_sound_pan('hitsound_don_1p', 1.0) - audio.set_sound_pan('hitsound_kat_1p', 1.0) - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "don_2p.wav", 'hitsound_don_2p') - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "ka_2p.wav", 'hitsound_kat_2p') - audio.set_sound_pan('hitsound_don_2p', 0.0) - audio.set_sound_pan('hitsound_kat_2p', 0.0) + elif global_data.hit_sound[0] == 0: + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.wav", 'hitsound_don_1p') + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.wav", 'hitsound_kat_1p') else: - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "don.ogg", 'hitsound_don_1p') - audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "ka.ogg", 'hitsound_kat_1p') + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.ogg", 'hitsound_don_1p') + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.ogg", 'hitsound_kat_1p') + audio.set_sound_pan('hitsound_don_1p', 1.0) + audio.set_sound_pan('hitsound_kat_1p', 1.0) - def init_tja(self, song: Path, difficulty: int): + # Load hitsounds for 2P + if global_data.hit_sound[1] == -1: + audio.load_sound(Path('none.wav'), 'hitsound_don_2p') + audio.load_sound(Path('none.wav'), 'hitsound_kat_2p') + elif global_data.hit_sound[1] == 0: + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "don_2p.wav", 'hitsound_don_2p') + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "ka_2p.wav", 'hitsound_kat_2p') + else: + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "don.ogg", 'hitsound_don_2p') + audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "ka.ogg", 'hitsound_kat_2p') + audio.set_sound_pan('hitsound_don_2p', 0.0) + audio.set_sound_pan('hitsound_kat_2p', 0.0) + + def global_keys(self): + if ray.is_key_pressed(ray.KeyboardKey.KEY_F1): + if self.song_music is not None: + audio.stop_music_stream(self.song_music) + self.init_tja(global_data.selected_song) + audio.play_sound('restart', 'sound') + self.song_started = False + + if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): + if self.song_music is not None: + audio.stop_music_stream(self.song_music) + return self.on_screen_end('SONG_SELECT_2P') + + def init_tja(self, song: Path): """Initialize the TJA file""" self.tja = TJAParser(song, start_delay=self.start_delay, distance=SCREEN_WIDTH - GameScreen.JUDGE_X) if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists(): @@ -44,24 +69,28 @@ class TwoPlayerGameScreen(GameScreen): self.movie.set_volume(0.0) else: self.movie = None - session_data.song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) + 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') tja_copy = copy.deepcopy(self.tja) - self.player_1 = Player(self.tja, 1, difficulty, False, global_data.modifiers) - self.player_2 = Player(tja_copy, 2, difficulty-1, True, Modifiers()) + self.player_1 = Player(self.tja, 1, global_data.session_data[0].selected_difficulty, False, global_data.modifiers[0]) + self.player_2 = Player(tja_copy, 2, global_data.session_data[1].selected_difficulty, True, global_data.modifiers[1]) self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) def spawn_ending_anims(self): - if session_data.result_bad == 0: + if global_data.session_data[0].result_bad == 0: self.player_1.ending_anim = FCAnimation(self.player_1.is_2p) - self.player_2.ending_anim = FCAnimation(self.player_2.is_2p) elif self.player_1.gauge.is_clear: self.player_1.ending_anim = ClearAnimation(self.player_1.is_2p) - self.player_2.ending_anim = ClearAnimation(self.player_2.is_2p) elif not self.player_1.gauge.is_clear: self.player_1.ending_anim = FailAnimation(self.player_1.is_2p) + + if global_data.session_data[1].result_bad == 0: + self.player_2.ending_anim = FCAnimation(self.player_2.is_2p) + elif self.player_2.gauge.is_clear: + self.player_2.ending_anim = ClearAnimation(self.player_2.is_2p) + elif not self.player_2.gauge.is_clear: self.player_2.ending_anim = FailAnimation(self.player_2.is_2p) def update(self): @@ -80,10 +109,14 @@ class TwoPlayerGameScreen(GameScreen): self.song_info.update(current_time) self.result_transition.update(current_time) if self.result_transition.is_finished and not audio.is_sound_playing('result_transition'): - return self.on_screen_end('RESULT') + return self.on_screen_end('RESULT_2P') elif self.current_ms >= self.player_1.end_time: + session_data = global_data.session_data[0] session_data.result_score, session_data.result_good, session_data.result_ok, session_data.result_bad, session_data.result_max_combo, session_data.result_total_drumroll = self.player_1.get_result_score() session_data.result_gauge_length = self.player_1.gauge.gauge_length + session_data = global_data.session_data[1] + session_data.result_score, session_data.result_good, session_data.result_ok, session_data.result_bad, session_data.result_max_combo, session_data.result_total_drumroll = self.player_2.get_result_score() + session_data.result_gauge_length = self.player_2.gauge.gauge_length if self.end_ms != 0: if current_time >= self.end_ms + 1000: if self.player_1.ending_anim is None: diff --git a/scenes/two_player/result.py b/scenes/two_player/result.py new file mode 100644 index 0000000..3c84065 --- /dev/null +++ b/scenes/two_player/result.py @@ -0,0 +1,36 @@ +from libs.utils import get_current_ms +from scenes.result import Background, FadeIn, ResultPlayer, ResultScreen + +class TwoPlayerResultScreen(ResultScreen): + def __init__(self): + super().__init__() + + def on_screen_start(self): + if not self.screen_init: + super().on_screen_start() + self.background = Background('3', self.width) + self.fade_in = FadeIn('3') + self.player_1 = ResultPlayer('1', True, False) + self.player_2 = ResultPlayer('2', True, True) + + def update(self): + self.on_screen_start() + current_time = get_current_ms() + self.fade_in.update(current_time) + self.player_1.update(current_time, self.fade_in.is_finished, self.is_skipped) + self.player_2.update(current_time, self.fade_in.is_finished, self.is_skipped) + + if current_time >= self.start_ms + 5000 and not self.fade_out.is_started: + self.handle_input() + + self.fade_out.update(current_time) + if self.fade_out.is_finished: + self.fade_out.update(current_time) + return self.on_screen_end("SONG_SELECT_2P") + + def draw(self): + self.background.draw() + self.draw_song_info() + self.player_1.draw() + self.player_2.draw() + self.draw_overlay() diff --git a/scenes/two_player/song_select.py b/scenes/two_player/song_select.py new file mode 100644 index 0000000..d5fe6a1 --- /dev/null +++ b/scenes/two_player/song_select.py @@ -0,0 +1,166 @@ +from libs.file_navigator import SongFile +from libs.transition import Transition +from scenes.song_select import DiffSortSelect, SongSelectPlayer, SongSelectScreen, State +from libs.utils import get_current_ms, global_data +from libs.audio import audio + +class TwoPlayerSongSelectScreen(SongSelectScreen): + def on_screen_start(self): + if not self.screen_init: + super().on_screen_start() + self.player_1 = SongSelectPlayer('1', self.text_fade_in) + self.player_2 = SongSelectPlayer('2', self.text_fade_in) + + def finalize_song(self): + global_data.selected_song = self.navigator.get_current_item().path + global_data.session_data[0].genre_index = self.navigator.get_current_item().box.name_texture_index + + def handle_input_browsing(self): + """Handle input for browsing songs.""" + action = self.player_1.handle_input_browsing(self.last_moved, self.navigator.items[self.navigator.selected_index] if self.navigator.items else None) + if action is None: + action = self.player_2.handle_input_browsing(self.last_moved, self.navigator.items[self.navigator.selected_index] if self.navigator.items else None) + if action is None: + return + + current_time = get_current_ms() + if action == "skip_left": + self.reset_demo_music() + for _ in range(10): + self.navigator.navigate_left() + self.last_moved = current_time + elif action == "skip_right": + self.reset_demo_music() + for _ in range(10): + self.navigator.navigate_right() + self.last_moved = current_time + elif action == "navigate_left": + self.reset_demo_music() + self.navigator.navigate_left() + self.last_moved = current_time + elif action == "navigate_right": + self.reset_demo_music() + self.navigator.navigate_right() + self.last_moved = current_time + elif action == "go_back": + self.navigator.go_back() + elif action == "diff_sort": + self.state = State.DIFF_SORTING + self.diff_sort_selector = DiffSortSelect(self.navigator.diff_sort_statistics, self.navigator.diff_sort_diff, self.navigator.diff_sort_level) + self.text_fade_in.start() + self.text_fade_out.start() + elif action == "select_song": + selected_song = self.navigator.select_current_item() + if selected_song: + self.state = State.SONG_SELECTED + self.player_1.on_song_selected(selected_song) + audio.play_sound('don', 'sound') + audio.play_sound('voice_select_diff', 'voice') + self.move_away.start() + self.diff_fade_out.start() + self.text_fade_out.start() + self.text_fade_in.start() + self.player_1.selected_diff_bounce.start() + self.player_1.selected_diff_fadein.start() + self.player_2.selected_diff_bounce.start() + self.player_2.selected_diff_fadein.start() + elif action == "add_favorite": + self.navigator.add_favorite() + current_box = self.navigator.get_current_item().box + current_box.is_favorite = not current_box.is_favorite + + def handle_input_selected(self): + """Handle input for selecting difficulty.""" + p2_result = False + result = self.player_1.handle_input_selected(self.navigator.get_current_item()) + if result is None: + result = self.player_2.handle_input_selected(self.navigator.get_current_item()) + if result is None: + return + p2_result = True + if result is not None: + print(result, p2_result) + if result == "cancel": + self._cancel_selection() + elif result == "confirm": + if p2_result: + self._confirm_selection(2) + else: + self._confirm_selection(1) + elif result == "ura_toggle": + if p2_result: + self.ura_switch_animation.start(not self.player_2.is_ura) + else: + self.ura_switch_animation.start(not self.player_1.is_ura) + + def handle_input_diff_sort(self): + """ + Handle input for sorting difficulty. + """ + if self.diff_sort_selector is None: + raise Exception("Diff sort selector was not able to be created") + + result = self.player_1.handle_input_diff_sort(self.diff_sort_selector) + + if result is not None: + diff, level = result + self.diff_sort_selector = None + self.state = State.BROWSING + self.text_fade_out.reset() + self.text_fade_in.reset() + if diff != -1: + if level != -1: + self.navigator.diff_sort_diff = diff + self.navigator.diff_sort_level = level + self.navigator.select_current_item() + + def _cancel_selection(self): + """Reset to browsing state""" + super()._cancel_selection() + self.player_2.selected_song = None + + def _confirm_selection(self, player_selected: int): + """Confirm song selection and create game transition""" + audio.play_sound('don', 'sound') + audio.play_sound(f'voice_start_song_{global_data.player_num}p', 'voice') + if player_selected == 1: + global_data.session_data[0].selected_difficulty = self.player_1.selected_difficulty + self.player_1.selected_diff_highlight_fade.start() + self.player_1.selected_diff_text_resize.start() + self.player_1.selected_diff_text_fadein.start() + elif player_selected == 2: + global_data.session_data[1].selected_difficulty = self.player_2.selected_difficulty + self.player_2.selected_diff_highlight_fade.start() + self.player_2.selected_diff_text_resize.start() + self.player_2.selected_diff_text_fadein.start() + + def check_for_selection(self): + if (self.player_1.selected_diff_highlight_fade.is_finished and + self.player_2.selected_diff_highlight_fade.is_finished and + not audio.is_sound_playing(f'voice_start_song_{global_data.player_num}p') and self.game_transition is None): + selected_song = self.navigator.get_current_item() + if not isinstance(selected_song, SongFile): + raise Exception("picked directory") + + title = selected_song.tja.metadata.title.get( + global_data.config['general']['language'], '') + subtitle = selected_song.tja.metadata.subtitle.get( + global_data.config['general']['language'], '') + self.game_transition = Transition(title, subtitle) + self.game_transition.start() + + def update_players(self, current_time): + self.player_1.update(current_time) + self.player_2.update(current_time) + if self.text_fade_out.is_finished: + self.player_1.selected_song = True + self.player_2.selected_song = True + return "GAME_2P" + + def draw_background_diffs(self): + self.player_1.draw_background_diffs(self.state) + self.player_2.draw_background_diffs(self.state) + + def draw_players(self): + self.player_1.draw(self.state) + self.player_2.draw(self.state)