From ec69e3f2bd6f0aec2866c5595f985a9d8799ffb8 Mon Sep 17 00:00:00 2001 From: Yonokid <37304577+Yonokid@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:42:29 -0400 Subject: [PATCH] update to add every missing feature ever --- .github/workflows/python-app.yml | 2 +- .gitignore | 1 + PyTaiko.py | 29 +- config.toml | 15 +- libs/song_hash.py | 130 ++++- libs/tja.py | 112 ++-- libs/utils.py | 93 +++- pyproject.toml | 6 +- scenes/entry.py | 14 +- scenes/game.py | 108 ++-- scenes/result.py | 32 +- scenes/settings.py | 235 +++++++++ scenes/song_select.py | 853 +++++++++++++++++++++---------- scenes/title.py | 12 +- uv.lock | 170 ++++-- 15 files changed, 1340 insertions(+), 472 deletions(-) create mode 100644 scenes/settings.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 0b7a576..cfb8947 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -41,7 +41,7 @@ jobs: script-name: PyTaiko.py mode: app output-dir: . - include-module: raylib,moviepy,numpy,sounddevice + include-module: raylib,moviepy,numpy,sounddevice,tomlkit include-package: imageio_ffmpeg noinclude-setuptools-mode: nofollow noinclude-IPython-mode: nofollow diff --git a/.gitignore b/.gitignore index 36c3a6d..11bea41 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ pytaiko.build pytaiko.dist pytaiko.onefile-build pytaiko.exe +full.csv diff --git a/PyTaiko.py b/PyTaiko.py index a6aaeb4..c4c5dab 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -2,12 +2,20 @@ import sqlite3 from pathlib import Path import pyray as ray +from raylib.defines import ( + RL_FUNC_ADD, + RL_ONE, + RL_ONE_MINUS_SRC_ALPHA, + RL_SRC_ALPHA, +) +from libs import song_hash from libs.audio import audio from libs.utils import get_config, global_data, load_all_textures_from_zip from scenes.entry import EntryScreen from scenes.game import GameScreen from scenes.result import ResultScreen +from scenes.settings import SettingsScreen from scenes.song_select import SongSelectScreen from scenes.title import TitleScreen @@ -18,6 +26,7 @@ class Screens: SONG_SELECT = "SONG_SELECT" GAME = "GAME" RESULT = "RESULT" + SETTINGS = "SETTINGS" def create_song_db(): with sqlite3.connect('scores.db') as con: @@ -33,7 +42,8 @@ def create_song_db(): ok INTEGER, bad INTEGER, drumroll INTEGER, - combo INTEGER + combo INTEGER, + clear INTEGER ); ''' cursor.execute(create_table_query) @@ -42,6 +52,7 @@ def create_song_db(): def main(): create_song_db() + song_hash.song_hashes = song_hash.build_song_hashes() screen_width: int = get_config()["video"]["screen_width"] screen_height: int = get_config()["video"]["screen_height"] render_width, render_height = ray.get_render_width(), ray.get_render_height() @@ -57,12 +68,12 @@ def main(): ray.set_config_flags(ray.ConfigFlags.FLAG_MSAA_4X_HINT) ray.set_trace_log_level(ray.TraceLogLevel.LOG_ERROR) - ray.set_window_max_size(screen_width, screen_height) - ray.set_window_min_size(screen_width, screen_height) + #ray.set_window_max_size(screen_width, screen_height) + #ray.set_window_min_size(screen_width, screen_height) ray.init_window(screen_width, screen_height, "PyTaiko") if get_config()["video"]["borderless"]: ray.toggle_borderless_windowed() - ray.clear_window_state(ray.ConfigFlags.FLAG_WINDOW_TOPMOST) + #ray.clear_window_state(ray.ConfigFlags.FLAG_WINDOW_TOPMOST) if get_config()["video"]["fullscreen"]: ray.maximize_window() @@ -76,20 +87,20 @@ def main(): song_select_screen = SongSelectScreen(screen_width, screen_height) game_screen = GameScreen(screen_width, screen_height) result_screen = ResultScreen(screen_width, screen_height) + settings_screen = SettingsScreen(screen_width, screen_height) screen_mapping = { Screens.ENTRY: entry_screen, Screens.TITLE: title_screen, Screens.SONG_SELECT: song_select_screen, Screens.GAME: game_screen, - Screens.RESULT: result_screen + Screens.RESULT: result_screen, + Screens.SETTINGS: settings_screen } target = ray.load_render_texture(screen_width, screen_height) ray.set_texture_filter(target.texture, ray.TextureFilter.TEXTURE_FILTER_TRILINEAR) ray.gen_texture_mipmaps(target.texture) - #lmaooooooooooooo - #rl_set_blend_factors_separate(RL_SRC_ALPHA, RL_ONE_MINUS_SRC_ALPHA, RL_ONE, RL_ONE_MINUS_SRC_ALPHA, RL_FUNC_ADD, RL_FUNC_ADD) - ray.rl_set_blend_factors_separate(0x302, 0x303, 1, 0x303, 0x8006, 0x8006) + ray.rl_set_blend_factors_separate(RL_SRC_ALPHA, RL_ONE_MINUS_SRC_ALPHA, RL_ONE, RL_ONE_MINUS_SRC_ALPHA, RL_FUNC_ADD, RL_FUNC_ADD) ray.set_exit_key(ray.KeyboardKey.KEY_A) global_data.textures = load_all_textures_from_zip(Path('Graphics/lumendata/intermission.zip')) while not ray.window_should_close(): @@ -118,7 +129,7 @@ def main(): ray.draw_texture_pro( target.texture, ray.Rectangle(0, 0, target.texture.width, -target.texture.height), - ray.Rectangle(0, 0, dpi_scale[0], dpi_scale[1]), + ray.Rectangle(0, 0, screen_width, screen_height), ray.Vector2(0,0), 0, ray.WHITE diff --git a/config.toml b/config.toml index c094bc6..d931cb9 100644 --- a/config.toml +++ b/config.toml @@ -1,12 +1,13 @@ [general] fps_counter = false judge_offset = 0 -autoplay = true +visual_offset = 0 +autoplay = false sfx = true -language = 'ja' +language = "ja" [paths] -tja_path = ['E:/Taiko/ESE', 'Songs', 'E:/Taiko/VersionSort'] +tja_path = ['Songs'] video_path = 'Videos' [keybinds] @@ -16,9 +17,9 @@ right_don = ['J'] right_kat = ['K'] [audio] -device_type = 'ASIO' -buffer_size = 6 -sample_rate = 48000 +device_type = "Windows WASAPI" +buffer_size = 22 +sample_rate = 44100 exclusive = false [video] @@ -26,4 +27,4 @@ screen_width = 1280 screen_height = 720 fullscreen = false borderless = false -vsync = false +vsync = true diff --git a/libs/song_hash.py b/libs/song_hash.py index e65f603..65d8154 100644 --- a/libs/song_hash.py +++ b/libs/song_hash.py @@ -1,40 +1,48 @@ import json +import sys from pathlib import Path from typing import Optional import pandas as pd + from libs.tja import TJAParser from libs.utils import get_config song_hashes: Optional[dict] = None + def process_tja_file(tja_file): """Process a single TJA file and return hash or None if error""" tja = TJAParser(tja_file) all_notes = [] for diff in tja.metadata.course_data: - all_notes.extend(TJAParser.notes_to_position(TJAParser(tja.file_path), diff)) + all_notes.extend( + TJAParser.notes_to_position(TJAParser(tja.file_path), diff) + ) hash = tja.hash_note_data(all_notes[0], all_notes[2]) return hash -def build_song_hashes(output_file='cache/song_hashes.json'): + +def build_song_hashes(output_file="cache/song_hashes.json"): existing_hashes = {} output_path = Path(output_file) if output_path.exists(): try: - with open(output_file, 'r', encoding='utf-8') as f: + with open(output_file, "r", encoding="utf-8") as f: existing_hashes = json.load(f) except (json.JSONDecodeError, IOError) as e: - print(f"Warning: Could not load existing hashes from {output_file}: {e}") + print( + f"Warning: Could not load existing hashes from {output_file}: {e}" + ) existing_hashes = {} song_hashes = existing_hashes.copy() - tja_paths = get_config()['paths']['tja_path'] + tja_paths = get_config()["paths"]["tja_path"] all_tja_files = [] for root_dir in tja_paths: root_path = Path(root_dir) - all_tja_files.extend(root_path.rglob('*.tja')) + all_tja_files.extend(root_path.rglob("*.tja")) updated_count = 0 for tja_file in all_tja_files: @@ -45,14 +53,14 @@ def build_song_hashes(output_file='cache/song_hashes.json'): existing_hash = None for h, data in song_hashes.items(): - if data['file_path'] == str(tja_file): + if data["file_path"] == str(tja_file): existing_hash = h break if existing_hash is None: should_update = True else: - stored_modified = song_hashes[existing_hash].get('last_modified', 0) + stored_modified = song_hashes[existing_hash].get("last_modified", 0) if current_modified > stored_modified: should_update = True del song_hashes[existing_hash] @@ -61,17 +69,19 @@ def build_song_hashes(output_file='cache/song_hashes.json'): tja = TJAParser(tja_file) all_notes = [] for diff in tja.metadata.course_data: - all_notes.extend(TJAParser.notes_to_position(TJAParser(tja.file_path), diff)) + all_notes.extend( + TJAParser.notes_to_position(TJAParser(tja.file_path), diff) + ) hash_val = tja.hash_note_data(all_notes[0], all_notes[2]) song_hashes[hash_val] = { - 'file_path': str(tja_file), - 'last_modified': current_modified, - 'title': tja.metadata.title, - 'subtitle': tja.metadata.subtitle + "file_path": str(tja_file), + "last_modified": current_modified, + "title": tja.metadata.title, + "subtitle": tja.metadata.subtitle, } updated_count += 1 - with open(output_file, 'w', encoding='utf-8') as f: + with open(output_file, "w", encoding="utf-8") as f: json.dump(song_hashes, f, indent=2, ensure_ascii=False) print(f"Song hashes saved to {output_file}. Updated {updated_count} files.") @@ -80,29 +90,84 @@ def build_song_hashes(output_file='cache/song_hashes.json'): def get_japanese_songs_for_version(df, version_column): # Filter rows where the specified version column has 'YES' - version_songs = df[df[version_column] == 'YES'] + version_songs = df[df[version_column] != "NO"] # Extract Japanese titles (JPTITLE column) - japanese_titles = version_songs['TITLE 【TITLE2】\nJPTITLE/「TITLE2」 より'].tolist() + japanese_titles = version_songs[ + "TITLE 【TITLE2】\nJPTITLE/「TITLE2」 より" + ].tolist() - japanese_titles = [name.split('\n') for name in japanese_titles] - second_lines = [name[1] for name in japanese_titles if len(name) > 1] + japanese_titles = [name.split("\n") for name in japanese_titles] + second_lines = [ + name[1] if len(name) > 1 else name[0] for name in japanese_titles + ] all_tja_files = [] direct_tja_paths = dict() text_files = dict() - tja_paths = get_config()['paths']['tja_path'] + tja_paths = get_config()["paths"]["tja_path"] for root_dir in tja_paths: root_path = Path(root_dir) - all_tja_files.extend(root_path.rglob('*.tja')) + all_tja_files.extend(root_path.rglob("*.tja")) for tja in all_tja_files: tja_parse = TJAParser(tja) - direct_tja_paths[tja_parse.metadata.title.get('ja', tja_parse.metadata.title['en'])] = tja + tja_name = tja_parse.metadata.title.get( + "ja", tja_parse.metadata.title["en"] + ) + if "【双打】" in tja_name: + tja_name = tja_name.strip("【双打】") + tja_name = tja_name.strip() + if tja_name in direct_tja_paths: + direct_tja_paths[tja_name].append(tja) + else: + direct_tja_paths[tja_name] = [tja] for title in second_lines: + if "・・・" in title: + title = title.replace("・・・", "…") + if "..." in title: + title = title.replace("・・・", "…") + + # Find all matching keys + matches = [] + + # Check for exact title match if title in direct_tja_paths: - path = direct_tja_paths[title] - elif title.split('/')[0] in direct_tja_paths: - path = direct_tja_paths[title.split('/')[0]] + for path in direct_tja_paths[title]: + matches.append((title, path)) + + # Also check for partial matches with the first part before '/' + title_prefix = title.split("/")[0] + for key in direct_tja_paths: + if key.startswith(title_prefix) and key != title: + for path in direct_tja_paths[key]: + matches.append((key, path)) + + if not matches: + for key in direct_tja_paths: + if title.lower() in key.lower() or key.lower() in title.lower(): + for path in direct_tja_paths[key]: + matches.append((key, path)) + + if not matches: + from difflib import get_close_matches + + close_matches = get_close_matches( + title, direct_tja_paths.keys(), n=3, cutoff=0.6 + ) + for close_match in close_matches: + for path in direct_tja_paths[close_match]: + matches.append((close_match, path)) + + if len(matches) == 1: + path = matches[0][1] + elif len(matches) > 1: + print( + f"Multiple matches found for '{title.split('/')[0]} ({title.split('/')[1] if len(title.split('/')) > 1 else ''})':" + ) + for i, (key, path_val) in enumerate(matches, 1): + print(f"{i}. {key}: {path_val}") + choice = int(input("Choose number: ")) - 1 + path = matches[choice][1] else: path = Path(input(f"NOT FOUND {title}: ")) hash = process_tja_file(path) @@ -110,15 +175,24 @@ def get_japanese_songs_for_version(df, version_column): genre = Path(path).parent.parent.name if genre not in text_files: text_files[genre] = [] - text_files[genre].append(f"{hash}|{tja_parse.metadata.title['en'].strip()}|{tja_parse.metadata.subtitle['en'].strip()}") + text_files[genre].append( + f"{hash}|{tja_parse.metadata.title['en'].strip()}|{tja_parse.metadata.subtitle['en'].strip()}" + ) + print(f"Added {title}: {path}") for genre in text_files: if not Path(version_column).exists(): Path(version_column).mkdir() if not Path(f"{version_column}/{genre}").exists(): Path(f"{version_column}/{genre}").mkdir() - with open(Path(f"{version_column}/{genre}/song_list.txt"), 'w', encoding='utf-8-sig') as text_file: + with open( + Path(f"{version_column}/{genre}/song_list.txt"), + "w", + encoding="utf-8-sig", + ) as text_file: for item in text_files[genre]: - text_file.write(item + '\n') + text_file.write(item + "\n") return text_files -get_japanese_songs_for_version(pd.read_csv('full.csv'), 'AC12') + +if len(sys.argv) > 1: + get_japanese_songs_for_version(pd.read_csv("full.csv"), sys.argv[1]) diff --git a/libs/tja.py b/libs/tja.py index 86b0b00..7b5b744 100644 --- a/libs/tja.py +++ b/libs/tja.py @@ -1,12 +1,25 @@ +import bisect import hashlib import math from collections import deque from dataclasses import dataclass, field, fields +from functools import lru_cache from pathlib import Path from libs.utils import get_pixels_per_frame, strip_comments +@lru_cache(maxsize=64) +def get_ms_per_measure(bpm_val, time_sig): + #https://gist.github.com/KatieFrogs/e000f406bbc70a12f3c34a07303eec8b#measure + if bpm_val == 0: + return 0 + return 60000 * (time_sig * 4) / bpm_val + +@lru_cache(maxsize=64) +def get_pixels_per_ms(pixels_per_frame): + return pixels_per_frame / (1000 / 60) + @dataclass() class Note: type: int = field(init=False) @@ -150,6 +163,7 @@ def calculate_base_score(play_note_list: deque[Note | Drumroll | Balloon]) -> in return math.ceil(total_score / 10) * 10 class TJAParser: + DIFFS = {0: "easy", 1: "normal", 2: "hard", 3: "oni", 4: "edit", 5: "tower", 6: "dan"} def __init__(self, path: Path, start_delay: int = 0, distance: int = 866): self.file_path: Path = path @@ -260,59 +274,54 @@ class TJAParser: self.ex_data.limited_time = True def data_to_notes(self, diff): - note_start = -1 - note_end = -1 + diff_name = self.DIFFS.get(diff, "").lower() + + # Use enumerate for single iteration + note_start = note_end = -1 target_found = False - diffs = {0: "easy", 1: "normal", 2: "hard", 3: "oni", 4: "edit", 5: "tower", 6: "dan"} - # Get the name corresponding to this difficulty number - diff_name = diffs.get(diff, "").lower() - i = 0 - while i < len(self.data): - line = self.data[i] - - # Check if this is the start of a difficulty section + # Find the section boundaries + for i, line in enumerate(self.data): if line.startswith("COURSE:"): course_value = line[7:].strip().lower() - - # Match either the exact number or the name - if (course_value.isdigit() and int(course_value) == diff) or course_value == diff_name: - target_found = True - else: - target_found = False - - # If we found our target section, look for START and END markers - if target_found: - if line == "#START": + target_found = (course_value.isdigit() and int(course_value) == diff) or course_value == diff_name + elif target_found: + if note_start == -1 and line in ("#START", "#START P1"): note_start = i + 1 elif line == "#END" and note_start != -1: note_end = i - break # We found our complete section + break - i += 1 + if note_start == -1 or note_end == -1: + return [] + # Process the section with minimal string operations notes = [] bar = [] - #Check for measures and separate when comma exists - for i in range(note_start, note_end): - line = self.data[i] + section_data = self.data[note_start:note_end] + + for line in section_data: if line.startswith("#"): bar.append(line) + elif line == ',': + if not bar or all(item.startswith('#') for item in bar): + bar.append('') + notes.append(bar) + bar = [] else: - if line == ',': - if len(bar) == 0 or all(item.startswith('#') for item in bar): - bar.append('') + if line.endswith(','): + bar.append(line[:-1]) notes.append(bar) bar = [] else: - item = line.strip(',') - bar.append(item) - if item != line: - notes.append(bar) - bar = [] + bar.append(line) + + if bar: # Add remaining items + notes.append(bar) + return notes - def get_moji(self, play_note_list: deque[Note], ms_per_measure: float) -> None: + def get_moji(self, play_note_list: list[Note], ms_per_measure: float) -> None: se_notes = { 1: [0, 1, 2], # Note '1' has three possible sound effects 2: [3, 4], # Note '2' has two possible sound effects @@ -373,9 +382,9 @@ class TJAParser: play_note_list[-3].moji = se_notes[play_note_list[-3].moji][2] def notes_to_position(self, diff: int): - play_note_list: deque[Note | Drumroll | Balloon] = deque() - bar_list: deque[Note] = deque() - draw_note_list: deque[Note | Drumroll | Balloon] = deque() + play_note_list: list[Note | Drumroll | Balloon] = [] + draw_note_list: list[Note | Drumroll | Balloon] = [] + bar_list: list[Note] = [] notes = self.data_to_notes(diff) balloon = self.metadata.course_data[diff].balloon.copy() count = 0 @@ -431,18 +440,14 @@ class TJAParser: if skip_branch: continue - if bpm == 0: - ms_per_measure = 0 - else: - #https://gist.github.com/KatieFrogs/e000f406bbc70a12f3c34a07303eec8b#measure - ms_per_measure = 60000 * (time_signature*4) / bpm + ms_per_measure = get_ms_per_measure(bpm, time_signature) #Create note object bar_line = Note() #Determines how quickly the notes need to move across the screen to reach the judgment circle in time bar_line.pixels_per_frame = get_pixels_per_frame(bpm * time_signature * scroll_modifier, time_signature*4, self.distance) - pixels_per_ms = bar_line.pixels_per_frame / (1000 / 60) + pixels_per_ms = get_pixels_per_ms(bar_line.pixels_per_frame) bar_line.hit_ms = self.current_ms if pixels_per_ms == 0: @@ -455,7 +460,7 @@ class TJAParser: if barline_added: bar_line.display = False - bar_list.append(bar_line) + bisect.insort(bar_list, bar_line, key=lambda x: x.load_ms) barline_added = True #Empty bar is still a bar, otherwise start increment @@ -471,12 +476,11 @@ class TJAParser: continue note = Note() note.hit_ms = self.current_ms - if pixels_per_ms == 0: - note.load_ms = note.hit_ms - else: - note.load_ms = note.hit_ms - (self.distance / pixels_per_ms) - note.type = int(item) note.pixels_per_frame = bar_line.pixels_per_frame + pixels_per_ms = get_pixels_per_ms(note.pixels_per_frame) + note.load_ms = (note.hit_ms if pixels_per_ms == 0 + else note.hit_ms - (self.distance / pixels_per_ms)) + note.type = int(item) note.index = index note.bpm = bpm note.gogo_time = gogo_time @@ -489,10 +493,7 @@ class TJAParser: if balloon is None: raise Exception("Balloon note found, but no count was specified") note = Balloon(note) - if not balloon: - note.count = 1 - else: - note.count = balloon.pop(0) + note.count = 1 if not balloon else balloon.pop(0) elif item == '8': new_pixels_per_ms = play_note_list[-1].pixels_per_frame / (1000 / 60) if new_pixels_per_ms == 0: @@ -502,6 +503,7 @@ class TJAParser: note.pixels_per_frame = play_note_list[-1].pixels_per_frame self.current_ms += increment play_note_list.append(note) + bisect.insort(draw_note_list, note, key=lambda x: x.load_ms) self.get_moji(play_note_list, ms_per_measure) index += 1 if len(play_note_list) > 3: @@ -513,9 +515,7 @@ class TJAParser: # Sorting by load_ms is necessary for drawing, as some notes appear on the # screen slower regardless of when they reach the judge circle # Bars can be sorted like this because they don't need hit detection - draw_note_list = deque(sorted(play_note_list, key=lambda n: n.load_ms)) - bar_list = deque(sorted(bar_list, key=lambda b: b.load_ms)) - return play_note_list, draw_note_list, bar_list + return deque(play_note_list), deque(draw_note_list), deque(bar_list) def hash_note_data(self, play_notes: deque[Note | Drumroll | Balloon], bars: deque[Note]): n = hashlib.sha256() diff --git a/libs/utils.py b/libs/utils.py index 2dca94a..629e1b9 100644 --- a/libs/utils.py +++ b/libs/utils.py @@ -1,10 +1,12 @@ import hashlib +import math import os import tempfile import time -import tomllib +import tomlkit import zipfile from dataclasses import dataclass, field +from functools import lru_cache from pathlib import Path from typing import Any @@ -85,6 +87,7 @@ def strip_comments(code: str) -> str: index += 1 return result +@lru_cache def get_pixels_per_frame(bpm: float, time_signature: float, distance: float) -> float: if bpm == 0: return 0 @@ -94,10 +97,94 @@ def get_pixels_per_frame(bpm: float, time_signature: float, distance: float) -> return (distance / total_frames) def get_config() -> dict[str, Any]: - with open('config.toml', "rb") as f: - config_file = tomllib.load(f) + with open('config.toml', "r", encoding="utf-8") as f: + config_file = tomlkit.load(f) return config_file +def save_config(config: dict[str, Any]) -> None: + with open('config.toml', "w", encoding="utf-8") as f: + tomlkit.dump(config, f) + +def is_l_don_pressed() -> bool: + keys = get_config()["keybinds"]["left_don"] + for key in keys: + if ray.is_key_pressed(ord(key)): + return True + + if ray.is_gamepad_available(0): + if ray.is_gamepad_button_pressed(0, 16): + return True + + mid_x, mid_y = (1280//2, 720) + allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP} + if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()): + for i in range(min(ray.get_touch_point_count(), 10)): + tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y) + if math.dist(tap_pos, (mid_x, mid_y)) < 300 and tap_pos[0] <= mid_x: + return True + + return False + +def is_r_don_pressed() -> bool: + keys = get_config()["keybinds"]["right_don"] + for key in keys: + if ray.is_key_pressed(ord(key)): + return True + + if ray.is_gamepad_available(0): + if ray.is_gamepad_button_pressed(0, 17): + return True + + mid_x, mid_y = (1280//2, 720) + allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP} + if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()): + for i in range(min(ray.get_touch_point_count(), 10)): + tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y) + if math.dist(tap_pos, (mid_x, mid_y)) < 300 and tap_pos[0] > mid_x: + return True + + return False + +def is_l_kat_pressed() -> bool: + keys = get_config()["keybinds"]["left_kat"] + for key in keys: + if ray.is_key_pressed(ord(key)): + return True + + if ray.is_gamepad_available(0): + if ray.is_gamepad_button_pressed(0, 10): + return True + + mid_x, mid_y = (1280//2, 720) + allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP} + if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()): + for i in range(min(ray.get_touch_point_count(), 10)): + tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y) + if math.dist(tap_pos, (mid_x, mid_y)) >= 300 and tap_pos[0] <= mid_x: + return True + + return False + +def is_r_kat_pressed() -> bool: + keys = get_config()["keybinds"]["right_kat"] + for key in keys: + if ray.is_key_pressed(ord(key)): + return True + + if ray.is_gamepad_available(0): + if ray.is_gamepad_button_pressed(0, 12): + return True + + mid_x, mid_y = (1280//2, 720) + allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP} + if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()): + for i in range(min(ray.get_touch_point_count(), 10)): + tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y) + if math.dist(tap_pos, (mid_x, mid_y)) >= 300 and tap_pos[0] > mid_x: + return True + + return False + def draw_scaled_texture(texture: ray.Texture, x: int, y: int, scale: float, color: ray.Color) -> None: src_rect = ray.Rectangle(0, 0, texture.width, texture.height) dst_rect = ray.Rectangle(x, y, texture.width*scale, texture.height*scale) diff --git a/pyproject.toml b/pyproject.toml index 35c6318..0b3b1ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,11 +8,13 @@ dependencies = [ "numpy>=2.2.5", "opencv-python>=4.11.0.86", "pydub>=0.25.1", - "raylib>=5.5.0.2", - "raylib-dynamic>=5.5.0.2", "ruff>=0.11.7", "scipy>=1.15.2", "sounddevice>=0.5.1", "audioop-lts; python_version >= '3.13'", "moviepy>=2.1.2", + "raylib-sdl>=5.5.0.2", + "soundfile>=0.13.1", + "pandas>=2.3.0", + "tomlkit>=0.13.3", ] diff --git a/scenes/entry.py b/scenes/entry.py index 8b2ea65..b351d15 100644 --- a/scenes/entry.py +++ b/scenes/entry.py @@ -2,7 +2,7 @@ from pathlib import Path import pyray as ray -from libs.utils import get_config, load_texture_from_zip +from libs.utils import is_l_don_pressed, is_r_don_pressed, load_texture_from_zip class EntryScreen: @@ -18,16 +18,16 @@ class EntryScreen: if not self.screen_init: self.screen_init = True - def on_screen_end(self): + def on_screen_end(self, next_screen: str): self.screen_init = False - return "SONG_SELECT" + return next_screen def update(self): self.on_screen_start() - keys = get_config()["keybinds"]["left_don"] + get_config()["keybinds"]["right_don"] - for key in keys: - if ray.is_key_pressed(ord(key)): - return self.on_screen_end() + if is_l_don_pressed() or is_r_don_pressed(): + return self.on_screen_end("SONG_SELECT") + if ray.is_key_pressed(ray.KeyboardKey.KEY_F1): + return self.on_screen_end("SETTINGS") def draw(self): ray.draw_texture(self.texture_footer, 0, self.height - 151, ray.WHITE) diff --git a/scenes/game.py b/scenes/game.py index 567a094..38d022c 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -15,6 +15,10 @@ from libs.utils import ( get_config, get_current_ms, global_data, + is_l_don_pressed, + is_l_kat_pressed, + is_r_don_pressed, + is_r_kat_pressed, load_all_textures_from_zip, load_image_from_zip, load_texture_from_zip, @@ -38,6 +42,7 @@ class GameScreen: self.end_ms = 0 self.start_delay = 1000 self.song_started = False + self.prev_touch_count = 0 self.background = Background(width, height) @@ -150,14 +155,14 @@ class GameScreen: result = cursor.fetchone() if result is None or session_data.result_score > result[0]: insert_query = ''' - INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo) - VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo, clear) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); ''' data = (hash, self.tja.metadata.title['en'], - self.tja.metadata.title['ja'], self.player_1.difficulty, + self.tja.metadata.title.get('ja', ''), self.player_1.difficulty, session_data.result_score, session_data.result_good, session_data.result_ok, session_data.result_bad, - session_data.result_total_drumroll, session_data.result_max_combo) + session_data.result_total_drumroll, session_data.result_max_combo, int(self.player_1.gauge.gauge_length > self.player_1.gauge.clear_start[min(self.player_1.difficulty, 3)])) cursor.execute(insert_query, data) con.commit() @@ -222,6 +227,7 @@ class Player: self.player_number = player_number self.difficulty = difficulty + self.visual_offset = get_config()["general"]["visual_offset"] self.play_notes, self.draw_note_list, self.draw_bar_list = game_screen.tja.notes_to_position(self.difficulty) self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5]) @@ -259,7 +265,7 @@ class Player: self.input_log: dict[float, tuple] = dict() - self.gauge = Gauge(self.difficulty, game_screen.tja.metadata.course_data[self.difficulty].level) + self.gauge = Gauge(self.difficulty, game_screen.tja.metadata.course_data[self.difficulty].level, self.total_notes) self.gauge_hit_effect: list[GaugeHitEffect] = [] self.autoplay_hit_side = 'L' @@ -269,7 +275,7 @@ class Player: return self.score, self.good_count, self.ok_count, self.bad_count, self.total_drumroll, self.max_combo def get_position(self, game_screen: GameScreen, ms: float, pixels_per_frame: float) -> int: - return int(game_screen.width + pixels_per_frame * 60 / 1000 * (ms - game_screen.current_ms) - 64) + return int(game_screen.width + pixels_per_frame * 60 / 1000 * (ms - game_screen.current_ms) - 64) - self.visual_offset def animation_manager(self, animation_list: list): if len(animation_list) <= 0: @@ -305,6 +311,7 @@ class Player: if 0 < note.type <= 4: self.combo = 0 self.bad_count += 1 + self.gauge.add_bad() self.play_notes.popleft() elif note.type != 8: tail = self.play_notes[1] @@ -437,6 +444,7 @@ class Player: self.score += self.base_score self.base_score_list.append(ScoreCounterAnimation(self.base_score)) self.note_correct(game_screen, curr_note) + self.gauge.add_good() elif (curr_note.hit_ms - Player.TIMING_OK) <= game_screen.current_ms <= (curr_note.hit_ms + Player.TIMING_OK): self.draw_judge_list.append(Judgement('OK', big, ms_display=game_screen.current_ms - curr_note.hit_ms)) @@ -444,12 +452,14 @@ class Player: self.score += 10 * math.floor(self.base_score / 2 / 10) self.base_score_list.append(ScoreCounterAnimation(10 * math.floor(self.base_score / 2 / 10))) self.note_correct(game_screen, curr_note) + self.gauge.add_ok() elif (curr_note.hit_ms - Player.TIMING_BAD) <= game_screen.current_ms <= (curr_note.hit_ms + Player.TIMING_BAD): self.draw_judge_list.append(Judgement('BAD', big, ms_display=game_screen.current_ms - curr_note.hit_ms)) self.bad_count += 1 self.combo = 0 self.play_notes.popleft() + self.gauge.add_bad() def drumroll_counter_manager(self, game_screen: GameScreen): if self.is_drumroll and self.curr_drumroll_count > 0 and self.drumroll_counter is None: @@ -467,26 +477,23 @@ class Player: if self.balloon_anim.is_finished: self.balloon_anim = None - def key_manager(self, game_screen: GameScreen): - key_configs = [ - {"keys": get_config()["keybinds"]["left_don"], "type": "DON", "side": "L", "note_type": 1}, - {"keys": get_config()["keybinds"]["right_don"], "type": "DON", "side": "R", "note_type": 1}, - {"keys": get_config()["keybinds"]["left_kat"], "type": "KAT", "side": "L", "note_type": 2}, - {"keys": get_config()["keybinds"]["right_kat"], "type": "KAT", "side": "R", "note_type": 2} - ] - for config in key_configs: - for key in config["keys"]: - if ray.is_key_pressed(ord(key)): - hit_type = config["type"] - self.lane_hit_effect = LaneHitEffect(hit_type) - self.draw_drum_hit_list.append(DrumHitEffect(hit_type, config["side"])) + def handle_input(self, game_screen: GameScreen): + input_checks = [ + (is_l_don_pressed, 'DON', 'L', game_screen.sound_don), + (is_r_don_pressed, 'DON', 'R', game_screen.sound_don), + (is_l_kat_pressed, 'KAT', 'L', game_screen.sound_kat), + (is_r_kat_pressed, 'KAT', 'R', game_screen.sound_kat) + ] + for check_func, note_type, side, sound in input_checks: + if check_func(): + self.lane_hit_effect = LaneHitEffect(note_type) + self.draw_drum_hit_list.append(DrumHitEffect(note_type, side)) - sound = game_screen.sound_don if hit_type == "DON" else game_screen.sound_kat - if get_config()["general"]["sfx"]: - audio.play_sound(sound) + if get_config()["general"]["sfx"]: + audio.play_sound(sound) - self.check_note(game_screen, config["note_type"]) - self.input_log[game_screen.current_ms] = (hit_type, key) + self.check_note(game_screen, 1 if note_type == 'DON' else 2) + self.input_log[game_screen.current_ms] = (note_type, side) def autoplay_manager(self, game_screen: GameScreen): if not get_config()["general"]["autoplay"]: @@ -558,7 +565,7 @@ class Player: self.animation_manager(self.base_score_list) self.score_counter.update(get_current_ms(), self.score) self.autoplay_manager(game_screen) - self.key_manager(game_screen) + self.handle_input(game_screen) self.gauge.update(get_current_ms(), self.good_count, self.ok_count, self.bad_count, self.total_notes) @@ -686,6 +693,7 @@ class Player: self.score_counter.draw(game_screen) for anim in self.base_score_list: anim.draw(game_screen) + #ray.draw_circle(game_screen.width//2, game_screen.height, 300, ray.ORANGE) class Judgement: def __init__(self, type: str, big: bool, ms_display: Optional[float]=None): @@ -1232,8 +1240,10 @@ class ResultTransition: x += texture_2.width class Gauge: - def __init__(self, difficulty: int, level: int): + def __init__(self, difficulty: int, level: int, total_notes: int): self.gauge_length = 0 + self.previous_length = 0 + self.total_notes = total_notes self.difficulty = min(3, difficulty) self.clear_start = [0, 0, 68, 68] self.level = min(10, level) @@ -1285,16 +1295,29 @@ class Gauge: self.rainbow_fade_in = None self.rainbow_animation = None + def add_good(self): + self.gauge_update_anim = Animation.create_fade(450) + self.previous_length = int(self.gauge_length) + self.gauge_length += (1 / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) + if self.gauge_length > 87: + self.gauge_length = 87 + + def add_ok(self): + self.gauge_update_anim = Animation.create_fade(450) + self.previous_length = int(self.gauge_length) + self.gauge_length += ((1 * self.table[self.difficulty][self.level]["ok_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) + if self.gauge_length > 87: + self.gauge_length = 87 + + def add_bad(self): + self.previous_length = int(self.gauge_length) + self.gauge_length += ((1 * self.table[self.difficulty][self.level]["bad_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) + if self.gauge_length < 0: + self.gauge_length = 0 + def update(self, current_ms: float, good_count: int, ok_count: int, bad_count: int, total_notes: int): - gauge_length = int(((good_count + - (ok_count * self.table[self.difficulty][self.level]["ok_multiplier"] + - bad_count * self.table[self.difficulty][self.level]["bad_multiplier"])) / total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"]))) - previous_length = self.gauge_length - self.gauge_length = min(87, gauge_length) if self.gauge_length == 87 and self.rainbow_fade_in is None: self.rainbow_fade_in = Animation.create_fade(450, initial_opacity=0.0, final_opacity=1.0) - if self.gauge_length > previous_length: - self.gauge_update_anim = Animation.create_fade(450) if self.gauge_update_anim is not None: self.gauge_update_anim.update(current_ms) @@ -1314,12 +1337,13 @@ class Gauge: def draw(self, textures: list[ray.Texture]): ray.draw_texture(textures[0], 327, 132, ray.WHITE) ray.draw_texture(textures[1], 483, 124, ray.WHITE) - if self.gauge_length == 87 and self.rainbow_fade_in is not None and self.rainbow_animation is not None: + gauge_length = int(self.gauge_length) + if gauge_length == 87 and self.rainbow_fade_in is not None and self.rainbow_animation is not None: if 0 < self.rainbow_animation.attribute < 8: ray.draw_texture(textures[1 + int(self.rainbow_animation.attribute)], 483, 124, ray.fade(ray.WHITE, self.rainbow_fade_in.attribute)) ray.draw_texture(textures[2 + int(self.rainbow_animation.attribute)], 483, 124, ray.fade(ray.WHITE, self.rainbow_fade_in.attribute)) if self.rainbow_fade_in is None or not self.rainbow_fade_in.is_finished: - for i in range(self.gauge_length): + for i in range(gauge_length): if i == 68: ray.draw_texture(textures[16], 491 + (i*textures[13].width), 160 - 24, ray.WHITE) elif i > 68: @@ -1327,15 +1351,15 @@ class Gauge: ray.draw_texture(textures[20], 491 + (i*textures[13].width) + 2, 160, ray.WHITE) else: ray.draw_texture(textures[13], 491 + (i*textures[13].width), 160, ray.WHITE) - if self.gauge_update_anim is not None and self.gauge_length < 88: - if self.gauge_length == 69: - ray.draw_texture(textures[17], 491 + (self.gauge_length*textures[13].width) - 13, 160 - 8 - 24, ray.fade(ray.WHITE, self.gauge_update_anim.attribute)) - elif self.gauge_length > 69: - ray.draw_texture(textures[21], 491 + (self.gauge_length*textures[13].width) - 13, 160 - 8 - 22, ray.fade(ray.WHITE, self.gauge_update_anim.attribute)) + if self.gauge_update_anim is not None and gauge_length < 88 and gauge_length != self.previous_length: + if gauge_length == 69: + ray.draw_texture(textures[17], 491 + (gauge_length*textures[13].width) - 13, 160 - 8 - 24, ray.fade(ray.WHITE, self.gauge_update_anim.attribute)) + elif gauge_length > 69: + ray.draw_texture(textures[21], 491 + (gauge_length*textures[13].width) - 13, 160 - 8 - 22, ray.fade(ray.WHITE, self.gauge_update_anim.attribute)) else: - ray.draw_texture(textures[14], 491 + (self.gauge_length*textures[13].width) - 13, 160 - 8, ray.fade(ray.WHITE, self.gauge_update_anim.attribute)) + ray.draw_texture(textures[14], 491 + (gauge_length*textures[13].width) - 13, 160 - 8, ray.fade(ray.WHITE, self.gauge_update_anim.attribute)) ray.draw_texture(textures[10], 483, 124, ray.fade(ray.WHITE, 0.15)) - if self.gauge_length >= 69: + if gauge_length >= 69: ray.draw_texture(textures[18], 1038, 141, ray.WHITE) ray.draw_texture(textures[19], 1187, 130, ray.WHITE) else: diff --git a/scenes/result.py b/scenes/result.py index 64f9c80..06a6ada 100644 --- a/scenes/result.py +++ b/scenes/result.py @@ -8,9 +8,10 @@ from libs.audio import audio from libs.utils import ( OutlinedText, draw_scaled_texture, - get_config, get_current_ms, global_data, + is_l_don_pressed, + is_r_don_pressed, load_all_textures_from_zip, session_data, ) @@ -60,6 +61,7 @@ class ResultScreen: ['max_combo', session_data.result_max_combo]] self.update_index = 0 self.is_skipped = False + self.start_ms = get_current_ms() def on_screen_end(self): self.screen_init = False @@ -94,6 +96,14 @@ class ResultScreen: self.score_animator = ScoreAnimator(self.update_list[self.update_index][1]) self.score_delay += 16.67 * 3 + def handle_input(self): + if is_r_don_pressed() or is_l_don_pressed(): + if not self.is_skipped: + self.is_skipped = True + else: + if self.fade_out is None: + self.fade_out = FadeOut() + audio.play_sound(self.sound_don) def update(self): self.on_screen_start() @@ -108,16 +118,9 @@ class ResultScreen: if self.gauge.is_finished and self.score_delay is None: self.score_delay = get_current_ms() + 1883 - left_dons = get_config()["keybinds"]["left_don"] - right_dons = get_config()["keybinds"]["right_don"] - for don in left_dons + right_dons: - if ray.is_key_pressed(ord(don)): - if not self.is_skipped: - self.is_skipped = True - else: - if self.fade_out is None: - self.fade_out = FadeOut() - audio.play_sound(self.sound_don) + if get_current_ms() >= self.start_ms + 5000: + self.handle_input() + self.update_score_animation(self.is_skipped) if self.fade_out is not None: @@ -263,12 +266,13 @@ class Gauge: def draw(self, textures: list[ray.Texture]): color = ray.fade(ray.WHITE, self.gauge_fade_in.attribute) draw_scaled_texture(textures[217], 554, 109, (10/11), color) - if self.gauge_length == 87 and self.rainbow_animation is not None: + gauge_length = int(self.gauge_length) + if gauge_length == 87 and self.rainbow_animation is not None: if 0 < self.rainbow_animation.attribute < 8: draw_scaled_texture(textures[217 + int(self.rainbow_animation.attribute)], 554, 109, (10/11), color) draw_scaled_texture(textures[218 + int(self.rainbow_animation.attribute)], 554, 109, (10/11), color) else: - for i in range(self.gauge_length+1): + for i in range(gauge_length+1): width = int(i * 7.2) if i == 69: draw_scaled_texture(textures[192], 562 + width, 142 - 22, (10/11), color) @@ -285,7 +289,7 @@ class Gauge: draw_scaled_texture(textures[226], 554, 109, (10/11), ray.fade(ray.WHITE, min(0.15, self.gauge_fade_in.attribute))) draw_scaled_texture(textures[176], 1185, 116, (10/11), color) - if self.gauge_length >= 69: + if gauge_length >= 69: draw_scaled_texture(textures[194], 1058, 124, (10/11), color) draw_scaled_texture(textures[195], 1182, 115, (10/11), color) else: diff --git a/scenes/settings.py b/scenes/settings.py new file mode 100644 index 0000000..616e61c --- /dev/null +++ b/scenes/settings.py @@ -0,0 +1,235 @@ +import pyray as ray +import sounddevice as sd + +from libs.utils import ( + get_config, + is_l_don_pressed, + is_l_kat_pressed, + is_r_don_pressed, + is_r_kat_pressed, + save_config, +) + + +class SettingsScreen: + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self.screen_init = False + self.config = get_config() + self.headers = list(self.config.keys()) + self.headers.append('Exit') + self.header_index = 0 + self.setting_index = 0 + self.in_setting_edit = False + self.editing_key = False + self.temp_key_input = "" + + def on_screen_start(self): + if not self.screen_init: + self.screen_init = True + + def on_screen_end(self): + self.screen_init = False + save_config(self.config) + return "ENTRY" + + def get_current_settings(self): + """Get the current section's settings as a list""" + current_header = self.headers[self.header_index] + if current_header == 'Exit' or current_header not in self.config: + return [] + return list(self.config[current_header].items()) + + def handle_boolean_toggle(self, section, key): + """Toggle boolean values""" + self.config[section][key] = not self.config[section][key] + + def handle_numeric_change(self, section, key, increment): + """Handle numeric value changes""" + current_value = self.config[section][key] + + # Define step sizes for different settings + step_sizes = { + 'judge_offset': 1, + 'visual_offset': 1, + 'buffer_size': 1, + 'sample_rate': 1000, + } + + step = step_sizes.get(key, 1) + new_value = current_value + (step * increment) + + # Apply constraints for specific settings + if key == 'judge_offset': + new_value = max(-50, min(50, new_value)) + elif key == 'visual_offset': + new_value = max(-20, min(20, new_value)) + elif key == 'buffer_size': + new_value = max(1, min(32, new_value)) + elif key == 'sample_rate': + valid_rates = [22050, 44100, 48000, 88200, 96000] + current_idx = valid_rates.index(current_value) if current_value in valid_rates else 2 + new_idx = max(0, min(len(valid_rates) - 1, current_idx + increment)) + new_value = valid_rates[new_idx] + + self.config[section][key] = new_value + + def handle_string_cycle(self, section, key): + """Cycle through predefined string values""" + current_value = self.config[section][key] + + hostapis = sd.query_hostapis() + audio_devices = [] + if isinstance(hostapis, tuple): + for api in hostapis: + if isinstance(api, dict): + audio_devices.append(api['name']) + + options = { + 'language': ['ja', 'en'], + 'device_type': audio_devices + } + + if key in options: + values = options[key] + try: + current_idx = values.index(current_value) + new_idx = (current_idx + 1) % len(values) + self.config[section][key] = values[new_idx] + except ValueError: + self.config[section][key] = values[0] + + def handle_key_binding(self, section, key): + """Handle key binding changes""" + self.editing_key = True + self.temp_key_input = "" + + def update_key_binding(self): + """Update key binding based on input""" + key_pressed = ray.get_key_pressed() + if key_pressed != 0: + # Convert keycode to character + if 65 <= key_pressed <= 90: # A-Z + new_key = chr(key_pressed) + current_header = self.headers[self.header_index] + settings = self.get_current_settings() + if settings: + setting_key, _ = settings[self.setting_index] + self.config[current_header][setting_key] = [new_key] + self.editing_key = False + elif key_pressed == ray.KeyboardKey.KEY_ESCAPE: + self.editing_key = False + + def update(self): + self.on_screen_start() + + # Handle key binding editing + if self.editing_key: + self.update_key_binding() + return + + current_header = self.headers[self.header_index] + + # Exit handling + if current_header == 'Exit' and (is_l_don_pressed() or is_r_don_pressed()): + return self.on_screen_end() + + # Navigation between sections + if not self.in_setting_edit: + if is_r_kat_pressed(): + self.header_index = (self.header_index + 1) % len(self.headers) + self.setting_index = 0 + elif is_l_kat_pressed(): + self.header_index = (self.header_index - 1) % len(self.headers) + self.setting_index = 0 + elif (is_l_don_pressed() or is_r_don_pressed()) and current_header != 'Exit': + self.in_setting_edit = True + else: + # Navigation within settings + settings = self.get_current_settings() + if not settings: + self.in_setting_edit = False + return + + if is_r_kat_pressed(): + self.setting_index = (self.setting_index + 1) % len(settings) + elif is_l_kat_pressed(): + self.setting_index = (self.setting_index - 1) % len(settings) + elif is_r_don_pressed(): + # Modify setting value + setting_key, setting_value = settings[self.setting_index] + + if isinstance(setting_value, bool): + self.handle_boolean_toggle(current_header, setting_key) + elif isinstance(setting_value, (int, float)): + self.handle_numeric_change(current_header, setting_key, 1) + elif isinstance(setting_value, str): + if 'keybinds' in current_header: + self.handle_key_binding(current_header, setting_key) + else: + self.handle_string_cycle(current_header, setting_key) + elif isinstance(setting_value, list) and len(setting_value) > 0: + if isinstance(setting_value[0], str) and len(setting_value[0]) == 1: + # Key binding + self.handle_key_binding(current_header, setting_key) + elif is_l_don_pressed(): + # Modify setting value (reverse direction for numeric) + setting_key, setting_value = settings[self.setting_index] + + if isinstance(setting_value, bool): + self.handle_boolean_toggle(current_header, setting_key) + elif isinstance(setting_value, (int, float)): + self.handle_numeric_change(current_header, setting_key, -1) + elif isinstance(setting_value, str): + if 'keybinds' not in current_header: + self.handle_string_cycle(current_header, setting_key) + + elif ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): + self.in_setting_edit = False + + def draw(self): + # Draw title + ray.draw_text("SETTINGS", 20, 20, 30, ray.WHITE) + + # Draw section headers + current_header = self.headers[self.header_index] + for i, key in enumerate(self.headers): + color = ray.GREEN + if key == current_header: + color = ray.YELLOW if not self.in_setting_edit else ray.ORANGE + ray.draw_text(f'{key}', 20, i*25 + 70, 20, color) + + # Draw current section settings + if current_header != 'Exit' and current_header in self.config: + settings = self.get_current_settings() + + # Draw settings list + for i, (key, value) in enumerate(settings): + color = ray.GREEN + if self.in_setting_edit and i == self.setting_index: + color = ray.YELLOW + + # Format value display + if isinstance(value, list): + display_value = ', '.join(map(str, value)) + else: + display_value = str(value) + + ray.draw_text(f'{key}: {display_value}', 250, i*25 + 70, 20, color) + + # Draw instructions + y_offset = len(settings) * 25 + 150 + if not self.in_setting_edit: + ray.draw_text("Don/Kat: Navigate sections", 20, y_offset, 16, ray.GRAY) + ray.draw_text("L/R Don: Enter section", 20, y_offset + 20, 16, ray.GRAY) + else: + ray.draw_text("Don/Kat: Navigate settings", 20, y_offset, 16, ray.GRAY) + ray.draw_text("L/R Don: Modify value", 20, y_offset + 20, 16, ray.GRAY) + ray.draw_text("ESC: Back to sections", 20, y_offset + 40, 16, ray.GRAY) + + if self.editing_key: + ray.draw_text("Press a key to bind (ESC to cancel)", 20, y_offset + 60, 16, ray.RED) + else: + # Draw exit instruction + ray.draw_text("Press Don to exit settings", 250, 100, 20, ray.GREEN) diff --git a/scenes/song_select.py b/scenes/song_select.py index 8cb737c..3fe43b7 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -1,6 +1,7 @@ + import sqlite3 from pathlib import Path -from typing import Optional +from typing import Optional, Union import pyray as ray @@ -13,6 +14,10 @@ from libs.utils import ( get_config, get_current_ms, global_data, + is_l_don_pressed, + is_l_kat_pressed, + is_r_don_pressed, + is_r_kat_pressed, load_all_textures_from_zip, session_data, ) @@ -37,6 +42,8 @@ class SongSelectScreen: self.sound_don = audio.load_sound(sounds_dir / "inst_00_don.wav") self.sound_kat = audio.load_sound(sounds_dir / "inst_00_katsu.wav") self.sound_skip = audio.load_sound(sounds_dir / 'song_select' / 'Skip.ogg') + self.sound_ura_switch = audio.load_sound(sounds_dir / 'song_select' / 'SE_SELECT [4].ogg') + audio.set_sound_volume(self.sound_ura_switch, 0.25) #self.sound_select = audio.load_sound(sounds_dir / "song_select.wav") #self.sound_cancel = audio.load_sound(sounds_dir / "cancel.wav") @@ -45,7 +52,7 @@ class SongSelectScreen: self.load_textures() self.load_sounds() self.selected_song = None - self.selected_difficulty = 0 + self.selected_difficulty = -1 self.game_transition = None self.move_away = Animation.create_move(float('inf')) self.diff_fade_out = Animation.create_fade(0, final_opacity=1.0) @@ -61,6 +68,12 @@ class SongSelectScreen: item.box.reset() self.navigator.get_current_item().box.get_scores() self.screen_init = True + self.last_moved = get_current_ms() + self.ura_toggle = 0 + self.ura_switch_animation = None + self.is_ura = False + if str(global_data.selected_song) in self.navigator.all_song_files: + self.navigator.mark_crowns_dirty_for_song(self.navigator.all_song_files[str(global_data.selected_song)]) def on_screen_end(self): self.screen_init = False @@ -80,78 +93,105 @@ class SongSelectScreen: self.navigator.get_current_item().box.wait = get_current_ms() def handle_input(self): if self.state == "BROWSING": - # Up/Down navigation - keys = get_config()["keybinds"]["left_kat"] - for key in keys: - if ray.is_key_pressed(ord(key)): - self.reset_demo_music() - self.navigator.navigate_left() - audio.play_sound(self.sound_kat) - - keys = get_config()["keybinds"]["right_kat"] - for key in keys: - if ray.is_key_pressed(ord(key)): - self.reset_demo_music() - self.navigator.navigate_right() - audio.play_sound(self.sound_kat) - if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT_CONTROL): + if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT_CONTROL) or (is_l_kat_pressed() and get_current_ms() <= self.last_moved + 100): self.reset_demo_music() self.wait = get_current_ms() for i in range(10): self.navigator.navigate_left() audio.play_sound(self.sound_skip) - - if ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT_CONTROL): + 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 + 100): self.reset_demo_music() for i in range(10): self.navigator.navigate_right() audio.play_sound(self.sound_skip) + self.last_moved = get_current_ms() + elif is_l_kat_pressed(): + self.reset_demo_music() + self.navigator.navigate_left() + audio.play_sound(self.sound_kat) + self.last_moved = get_current_ms() + + elif is_r_kat_pressed(): + self.reset_demo_music() + self.navigator.navigate_right() + audio.play_sound(self.sound_kat) + self.last_moved = get_current_ms() # Select/Enter - keys = get_config()["keybinds"]["left_don"] + get_config()["keybinds"]["right_don"] - for key in keys: - if ray.is_key_pressed(ord(key)): - selected_item = self.navigator.items[self.navigator.selected_index] - if selected_item is not None and selected_item.box.name == "Back": - self.navigator.go_back() - #audio.play_sound(self.sound_cancel) - else: - selected_song = self.navigator.select_current_item() - if selected_song: - self.state = "SONG_SELECTED" - audio.play_sound(self.sound_don) - self.move_away = Animation.create_move(233, total_distance=500) - self.diff_fade_out = Animation.create_fade(83) + 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.texture_index == 552: + self.navigator.go_back() + #audio.play_sound(self.sound_cancel) + else: + selected_song = self.navigator.select_current_item() + if selected_song: + self.state = "SONG_SELECTED" + if 4 not in selected_song.tja.metadata.course_data: + self.is_ura = False + audio.play_sound(self.sound_don) + self.move_away = Animation.create_move(233, total_distance=500) + self.diff_fade_out = Animation.create_fade(83) elif self.state == "SONG_SELECTED": # Handle song selection confirmation or cancel - keys = get_config()["keybinds"]["left_don"] + get_config()["keybinds"]["right_don"] - for key in keys: - if ray.is_key_pressed(ord(key)): - if self.selected_difficulty == -1: - self.selected_song = None - self.move_away = Animation.create_move(float('inf')) - self.diff_fade_out = Animation.create_fade(0, final_opacity=1.0) - self.text_fade_out = None - self.text_fade_in = None - self.state = "BROWSING" - for item in self.navigator.items: - item.box.reset() - else: - audio.play_sound(self.sound_don) - self.game_transition = Transition(self.screen_height) - keys = get_config()["keybinds"]["left_kat"] - for key in keys: - if ray.is_key_pressed(ord(key)): - audio.play_sound(self.sound_kat) - if self.selected_difficulty >= 0: - self.selected_difficulty = (self.selected_difficulty - 1) - keys = get_config()["keybinds"]["right_kat"] - for key in keys: - if ray.is_key_pressed(ord(key)): - audio.play_sound(self.sound_kat) - if self.selected_difficulty < 4: - self.selected_difficulty = (self.selected_difficulty + 1) + if is_l_don_pressed() or is_r_don_pressed(): + if self.selected_difficulty == -1: + self.selected_song = None + self.move_away = Animation.create_move(float('inf')) + self.diff_fade_out = Animation.create_fade(0, final_opacity=1.0) + self.text_fade_out = None + self.text_fade_in = None + self.state = "BROWSING" + for item in self.navigator.items: + item.box.reset() + else: + audio.play_sound(self.sound_don) + self.game_transition = Transition(self.screen_height) + if is_l_kat_pressed(): + audio.play_sound(self.sound_kat) + selected_song = self.navigator.get_current_item() + if not isinstance(selected_song, Directory): + diffs = sorted([item for item in selected_song.tja.metadata.course_data]) + if self.is_ura and self.selected_difficulty == 4: + self.selected_difficulty = 2 + elif self.selected_difficulty == -1: + pass + elif self.selected_difficulty not in diffs: + self.selected_difficulty = min(diffs) + elif self.selected_difficulty == min(diffs): + self.selected_difficulty = -1 + elif self.selected_difficulty > min(diffs): + self.selected_difficulty = (diffs[diffs.index(self.selected_difficulty) - 1]) + else: + raise Exception("Directory was chosen instead of song") + if is_r_kat_pressed(): + audio.play_sound(self.sound_kat) + selected_song = self.navigator.get_current_item() + if not isinstance(selected_song, Directory): + diffs = sorted([item for item in selected_song.tja.metadata.course_data]) + if self.is_ura and self.selected_difficulty == 2: + self.selected_difficulty = 4 + if (self.selected_difficulty == 3 or self.selected_difficulty == 4) and 4 in diffs: + self.ura_toggle = (self.ura_toggle + 1) % 10 + if self.ura_toggle == 0: + self.is_ura = not self.is_ura + self.ura_switch_animation = UraSwitchAnimation(not self.is_ura) + audio.play_sound(self.sound_ura_switch) + self.selected_difficulty = 7 - self.selected_difficulty + elif self.selected_difficulty not in diffs: + self.selected_difficulty = min(diffs) + elif self.selected_difficulty < max(diffs): + self.selected_difficulty = (diffs[diffs.index(self.selected_difficulty) + 1]) + else: + raise Exception("Directory was chosen instead of song") + if ray.is_key_pressed(ray.KeyboardKey.KEY_TAB) and (self.selected_difficulty == 3 or self.selected_difficulty == 4): + self.ura_toggle = 0 + self.is_ura = not self.is_ura + self.ura_switch_animation = UraSwitchAnimation(not self.is_ura) + audio.play_sound(self.sound_ura_switch) + self.selected_difficulty = 7 - self.selected_difficulty def update(self): self.on_screen_start() @@ -163,8 +203,8 @@ class SongSelectScreen: self.game_transition.update(get_current_ms()) if self.game_transition.is_finished: return self.on_screen_end() - - self.handle_input() + else: + self.handle_input() if self.demo_song is not None: audio.update_music_stream(self.demo_song) @@ -209,12 +249,16 @@ class SongSelectScreen: if self.text_fade_in is not None: self.text_fade_in.update(get_current_ms()) + if self.ura_switch_animation is not None: + self.ura_switch_animation.update(get_current_ms()) + def draw_selector(self): if self.selected_difficulty == -1: ray.draw_texture(self.textures['song_select'][133], 314, 110, ray.WHITE) else: - ray.draw_texture(self.textures['song_select'][140], 450 + (self.selected_difficulty * 115), 7, ray.WHITE) - ray.draw_texture(self.textures['song_select'][131], 461 + (self.selected_difficulty * 115), 132, ray.WHITE) + difficulty = min(3, self.selected_difficulty) + ray.draw_texture(self.textures['song_select'][140], 450 + (difficulty * 115), 7, ray.WHITE) + ray.draw_texture(self.textures['song_select'][131], 461 + (difficulty * 115), 132, ray.WHITE) def draw(self): # Draw file/directory list @@ -229,13 +273,16 @@ class SongSelectScreen: else: ray.draw_texture(texture, i - int(self.background_move.attribute), 0, ray.WHITE) - for item in self.navigator.get_items(): + 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.textures, self.diff_fade_out.attribute) + box.draw(box.position - int(self.move_away.attribute), 95, self.textures, self.is_ura, fade_override=self.diff_fade_out.attribute) else: - box.draw(box.position + int(self.move_away.attribute), 95, self.textures, self.diff_fade_out.attribute) + box.draw(box.position + int(self.move_away.attribute), 95, self.textures, self.is_ura, fade_override=self.diff_fade_out.attribute) + + if self.ura_switch_animation is not None: + self.ura_switch_animation.draw(self.textures) if self.selected_song and self.state == "SONG_SELECTED": self.draw_selector() @@ -254,7 +301,6 @@ class SongSelectScreen: if self.game_transition is not None: self.game_transition.draw(self.screen_height) - class SongBox: OUTLINE_MAP = { 555: ray.Color(0, 77, 104, 255), @@ -309,15 +355,18 @@ class SongBox: 585: 519, 615: 532, } - def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None, tja_count: Optional[int] = None): + def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None, tja_count: Optional[int] = None, box_texture: Optional[ray.Texture] = None): self.text_name = name self.texture_index = texture_index + self.box_texture = box_texture self.scores = dict() + self.crown = dict() self.position = -11111 self.start_position = -1 self.target_position = -1 self.is_open = False self.name = None + self.subtitle = None self.black_name = None self.hori_name = None self.yellow_box = None @@ -336,7 +385,11 @@ class SongBox: def reset(self): if self.black_name is not None: - self.yellow_box = YellowBox(self.black_name, self.texture_index == 552, tja=self.tja) + if self.tja is not None: + subtitle = OutlinedText(self.tja.metadata.subtitle.get(get_config()['general']['language'], ''), 30, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, vertical=True) + else: + subtitle = None + self.yellow_box = YellowBox(self.black_name, self.texture_index == 552, tja=self.tja, subtitle=subtitle) self.open_anim = None self.open_fade = None @@ -354,7 +407,7 @@ class SongBox: if diffs_to_compute: for diff in diffs_to_compute: - notes, _, bars = self.tja.notes_to_position(diff) + notes, _, bars = TJAParser.notes_to_position(TJAParser(self.tja.file_path), diff) self.hash[diff] = self.tja.hash_note_data(notes, bars) # Batch database query for all diffs at once @@ -363,7 +416,7 @@ class SongBox: placeholders = ','.join('?' * len(hash_values)) batch_query = f""" - SELECT hash, score, good, ok, bad + SELECT hash, score, good, ok, bad, clear FROM Scores WHERE hash IN ({placeholders}) """ @@ -403,7 +456,11 @@ class SongBox: self.black_name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, vertical=True) #print(f"loaded black name {self.text_name}") if self.tja is not None or self.texture_index == 552: - self.yellow_box = YellowBox(self.black_name, self.texture_index == 552, tja=self.tja) + if self.tja is not None: + subtitle = OutlinedText(self.tja.metadata.subtitle.get(get_config()['general']['language'], ''), 30, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, vertical=True) + else: + subtitle = None + self.yellow_box = YellowBox(self.black_name, self.texture_index == 552, tja=self.tja, subtitle=subtitle) self.yellow_box.create_anim() else: self.hori_name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5) @@ -412,7 +469,6 @@ class SongBox: self.open_fade = Animation.create_fade(200, initial_opacity=0, final_opacity=1.0) self.wait = get_current_ms() - elif not self.is_open: if self.black_name is not None: self.black_name.unload() @@ -463,6 +519,20 @@ class SongBox: src = ray.Rectangle(0, 0, self.name.texture.width, self.name.texture.height) 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(src, dest, ray.Vector2(0, 0), 0, ray.WHITE) + + if self.scores: + highest_key = max(self.scores.keys()) + score = self.scores[highest_key] + if score and score[3] == 0: + ray.draw_texture(textures['song_select'][683+highest_key], x+20, y-30, ray.WHITE) + elif score and score[4] == 1: + ray.draw_texture(textures['song_select'][688+highest_key], x+20, y-30, ray.WHITE) + if self.crown: + highest_crown = max(self.crown) + if self.crown[highest_crown] == 'FC': + ray.draw_texture(textures['song_select'][683+highest_crown], x+20, y-30, ray.WHITE) + else: + ray.draw_texture(textures['song_select'][688+highest_crown], x+20, y-30, ray.WHITE) #ray.draw_text(str(self.position), x, y-25, 25, ray.GREEN) def _draw_open(self, x: int, y: int, textures, fade_override): @@ -484,7 +554,7 @@ class SongBox: src = ray.Rectangle(0, 0, self.hori_name.texture.width, self.hori_name.texture.height) dest_width = min(300, self.hori_name.texture.width) - dest = ray.Rectangle((x + 48) - (dest_width//2), y-50 + 150 - int(self.open_anim.attribute), dest_width, self.hori_name.texture.height) + dest = ray.Rectangle((x + 48) - (dest_width//2), y-52 + 150 - int(self.open_anim.attribute), dest_width, self.hori_name.texture.height) self.hori_name.draw(src, dest, ray.Vector2(0, 0), 0, color) @@ -518,11 +588,13 @@ class SongBox: if self.texture_index in SongBox.GENRE_CHAR_MAP: ray.draw_texture(textures['song_select'][SongBox.GENRE_CHAR_MAP[self.texture_index]+1], 650, 125, color) ray.draw_texture(textures['song_select'][SongBox.GENRE_CHAR_MAP[self.texture_index]], 470, 180, color) + elif self.box_texture is not None: + ray.draw_texture(self.box_texture, (x+48) - (self.box_texture.width//2), (y+130), color) - def draw(self, x: int, y: int, textures, fade_override=None): + def draw(self, x: int, y: int, textures, 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(textures, self, fade_override) + self.yellow_box.draw(textures, self, fade_override, is_ura) else: if self.open_fade is not None: self._draw_open(x, y, textures, self.open_fade.attribute) @@ -530,7 +602,7 @@ class SongBox: self._draw_closed(x, y, textures) class YellowBox: - def __init__(self, name: OutlinedText, is_back: bool, tja: Optional[TJAParser] = None): + def __init__(self, name: OutlinedText, is_back: bool, tja: Optional[TJAParser] = None, subtitle: Optional[OutlinedText] = None): self.is_diff_select = False self.right_x = 803 self.left_x = 443 @@ -540,6 +612,7 @@ class YellowBox: self.center_height = 422 self.edge_height = 32 self.name = name + self.subtitle = subtitle self.is_back = is_back self.tja = tja self.anim_created = False @@ -603,7 +676,7 @@ class YellowBox: self.center_width = 32 + int(self.center_out.attribute) self.center_height = 422 - def draw(self, textures: dict[str, list[ray.Texture]], song_box: SongBox, fade_override: Optional[float]): + def draw(self, textures: dict[str, list[ray.Texture]], song_box: SongBox, fade_override: Optional[float], is_ura: bool): # Draw corners ray.draw_texture(textures['song_select'][235], self.right_x, self.bottom_y, ray.WHITE) # Bottom right ray.draw_texture(textures['song_select'][236], self.left_x, self.bottom_y, ray.WHITE) # Bottom left @@ -642,21 +715,37 @@ class YellowBox: ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE) - if self.is_diff_select: + if self.is_diff_select and self.tja is not None: #Back Button color = ray.fade(ray.WHITE, self.fade_in.attribute) ray.draw_texture(textures['song_select'][153], 314, 110, color) #Difficulties ray.draw_texture(textures['song_select'][154], 450, 90, color) + if 0 not in self.tja.metadata.course_data: + ray.draw_texture(textures['song_select'][161], 450, 90, ray.fade(ray.WHITE, min(self.fade_in.attribute, 0.25))) ray.draw_texture(textures['song_select'][182], 565, 90, color) + if 1 not in self.tja.metadata.course_data: + ray.draw_texture(textures['song_select'][183], 565, 90, ray.fade(ray.WHITE, min(self.fade_in.attribute, 0.25))) ray.draw_texture(textures['song_select'][185], 680, 90, color) - ray.draw_texture(textures['song_select'][188], 795, 90, color) + if 2 not in self.tja.metadata.course_data: + ray.draw_texture(textures['song_select'][186], 680, 90, ray.fade(ray.WHITE, min(self.fade_in.attribute, 0.25))) + if is_ura: + ray.draw_texture(textures['song_select'][190], 795, 90, color) + ray.draw_texture(textures['song_select'][191], 807, 130, color) + else: + ray.draw_texture(textures['song_select'][188], 795, 90, color) + if 3 not in self.tja.metadata.course_data: + ray.draw_texture(textures['song_select'][189], 795, 90, ray.fade(ray.WHITE, min(self.fade_in.attribute, 0.25))) - if self.tja is not None: - for course in self.tja.metadata.course_data: - for j in range(self.tja.metadata.course_data[course].level): - ray.draw_texture(textures['song_select'][155], 482+(course*115), 471+(j*-20), color) + #Stars + for course in self.tja.metadata.course_data: + if course == 4 and not is_ura: + continue + if course == 3 and is_ura: + continue + for j in range(self.tja.metadata.course_data[course].level): + ray.draw_texture(textures['song_select'][155], 482+(min(course, 3)*115), 471+(j*-20), color) else: #Crowns @@ -668,9 +757,13 @@ class YellowBox: ray.draw_texture(textures['song_select'][421], 498, 250, color) elif self.tja is not None: for diff in self.tja.metadata.course_data: - if diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][3] == 0: + if diff == 4: + continue + elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][3] == 0: ray.draw_texture(textures['song_select'][160], 473 + (diff*60), 175, color) - ray.draw_texture(textures['song_select'][158], 473 + (diff*60), 175, ray.fade(color, min(fade, 0.25))) + elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][4] == 1: + ray.draw_texture(textures['song_select'][159], 473 + (diff*60), 175, color) + ray.draw_texture(textures['song_select'][158], 473 + (diff*60), 175, ray.fade(ray.WHITE, min(fade, 0.25))) #EX Data if self.tja.ex_data.new_audio: @@ -682,28 +775,58 @@ class YellowBox: #Difficulties ray.draw_texture(textures['song_select'][395], 458, 210, color) + if 0 not in self.tja.metadata.course_data: + ray.draw_texture(textures['song_select'][400], 458, 210, ray.fade(ray.WHITE, min(fade, 0.25))) ray.draw_texture(textures['song_select'][401], 518, 210, color) + if 1 not in self.tja.metadata.course_data: + ray.draw_texture(textures['song_select'][402], 518, 210, ray.fade(ray.WHITE, min(fade, 0.25))) ray.draw_texture(textures['song_select'][403], 578, 210, color) + if 2 not in self.tja.metadata.course_data: + ray.draw_texture(textures['song_select'][404], 578, 210, ray.fade(ray.WHITE, min(fade, 0.25))) ray.draw_texture(textures['song_select'][406], 638, 210, color) + if 3 not in self.tja.metadata.course_data: + ray.draw_texture(textures['song_select'][407], 638, 210, ray.fade(ray.WHITE, min(fade, 0.25))) #Stars - for course in self.tja.metadata.course_data: - for j in range(self.tja.metadata.course_data[course].level): - ray.draw_texture(textures['song_select'][396], 474+(course*60), 490+(j*-17), color) - else: - pass + for diff in self.tja.metadata.course_data: + if diff == 4: + continue + for j in range(self.tja.metadata.course_data[diff].level): + ray.draw_texture(textures['song_select'][396], 474+(diff*60), 490+(j*-17), color) if self.is_back: texture = textures['song_select'][422] - x = int(((song_box.position + 47) - texture.width / 2) + (int(self.right_out.attribute)*0.85) + (int(self.right_out_2.attribute))) + x = int(((song_box.position + 55) - texture.width / 2) + (int(self.right_out.attribute)*0.85) + (int(self.right_out_2.attribute))) y = self.top_y+35 ray.draw_texture(texture, x, y, ray.WHITE) elif self.name is not None: texture = self.name.texture - x = int(((song_box.position + 47) - texture.width / 2) + (int(self.right_out.attribute)*0.85) + (int(self.right_out_2.attribute))) + x = int(((song_box.position + 55) - texture.width / 2) + (int(self.right_out.attribute)*0.85) + (int(self.right_out_2.attribute))) y = self.top_y+35 src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle(x, y, texture.width, min(texture.height, 417)) self.name.draw(src, dest, ray.Vector2(0, 0), 0, ray.WHITE) + if self.subtitle is not None: + texture = self.subtitle.texture + x = int(((song_box.position + 10) - texture.width / 2) + (int(self.right_out.attribute)*0.85) + (int(self.right_out_2.attribute))) + y = self.bottom_y - min(texture.height, 410) + 10 + int(self.top_y_out.attribute) + src = ray.Rectangle(0, 0, texture.width, texture.height) + dest = ray.Rectangle(x, y, texture.width, min(texture.height, 410)) + self.subtitle.draw(src, dest, ray.Vector2(0, 0), 0, ray.WHITE) + +class UraSwitchAnimation: + def __init__(self, is_backwards: bool) -> None: + forwards_animation = ((0, 32, 166), (32, 80, 167), (80, 112, 168), (112, 133, 169)) + backwards_animation = ((0, 32, 169), (32, 80, 170), (80, 112, 171), (112, 133, 166)) + if is_backwards: + self.texture_change = Animation.create_texture_change(133, textures=backwards_animation) + else: + self.texture_change = Animation.create_texture_change(133, textures=forwards_animation) + self.fade_out = Animation.create_fade(166, delay=133) + def update(self, current_ms: float): + self.texture_change.update(current_ms) + self.fade_out.update(current_ms) + def draw(self, textures: dict[str, list[ray.Texture]]): + ray.draw_texture(textures['song_select'][self.texture_change.attribute], 815, 134, ray.fade(ray.WHITE, self.fade_out.attribute)) class Transition: def __init__(self, screen_height: int) -> None: @@ -747,71 +870,31 @@ class FileSystemItem: self.path = path self.selected = False - def is_selectable(self): - return True - - class Directory(FileSystemItem): """Represents a directory in the navigation system""" - def __init__(self, path: Path, name: str, texture_index: int, has_box_def=False, to_root=False, back=False): + 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): super().__init__(path, name) self.has_box_def = has_box_def self.to_root = to_root self.back = back + self.tja_count = tja_count + if self.to_root or self.back: texture_index = 552 - tja_count = 0 - if self.has_box_def: - tja_count = self.count_tja_files(path) - if (path / "song_list.txt").exists(): - with open(path / "song_list.txt", 'r', encoding='utf-8-sig') as song_list_file: - tja_count += len(song_list_file.readlines()) - self.box = SongBox(name, texture_index, True, tja_count=tja_count) - - def count_tja_files(self, folder_path: Path): - tja_count = 0 - - #print(f"Scanning {folder_path}") - try: - items = folder_path.iterdir() - - for item in items: - item_path = folder_path / item - - if item_path.is_file(): - if item.suffix == '.tja': - tja_count += 1 - #print(f"Found: {item_path}") - - elif item_path.is_dir(): - tja_count += self.count_tja_files(item_path) - - except PermissionError: - print(f"Permission denied accessing '{folder_path}'") - except Exception as e: - print(f"Error accessing '{folder_path}': {e}") - - return tja_count - - def get_display_name(self): - return self.box + 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): + def __init__(self, path: Path, name: str, texture_index: int, tja=None): super().__init__(path, name) - self.tja = TJAParser(path) + self.tja = tja or TJAParser(path) title = self.tja.metadata.title.get(get_config()['general']['language'].lower(), self.tja.metadata.title['en']) self.box = SongBox(title, texture_index, False, tja=self.tja) - - - def get_display_name(self): - return self.box - + self.box.get_scores() class FileNavigator: - """Manages navigation through the file system""" + """Manages navigation through pre-generated Directory and SongFile objects""" def __init__(self, root_dirs: list[str]): # Handle both single path and list of paths if isinstance(root_dirs, (list, tuple)): @@ -819,77 +902,323 @@ class FileNavigator: else: self.root_dirs = [Path(root_dirs) if not isinstance(root_dirs, Path) else root_dirs] - self.in_root_selection = True # Whether we're showing the root directory selection screen + # 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 + self.root_items: list[Union[Directory, SongFile]] = [] + + # 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 + self.in_root_selection = True self.current_dir = Path() self.current_root_dir = Path() self.items: list[Directory | SongFile] = [] self.selected_index = 0 - self.history = [] # For tracking directory navigation history + self.history = [] + + # Generate all objects upfront + self._generate_all_objects() self.load_root_directories() - def check_for_box_def(self, dir_path: Path): - """Check if the directory contains a box.def file""" - box_def_path = dir_path / "box.def" - return box_def_path.exists() + def _generate_all_objects(self): + """Generate all Directory and SongFile objects in advance""" + print("Generating all Directory and SongFile objects...") - def get_tja_folder_count(self, directory: Path): - return len(self.find_tja_files_recursive(directory)) + # First, 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 - def find_tja_files_recursive(self, directory: Path, box_def_dirs_only=True): + self._generate_objects_recursive(root_path, is_root=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, is_root=False): + """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() + + # Parse box.def if it exists + name = dir_path.name if dir_path.name else str(dir_path) + texture_index = 620 + box_texture = None + + if has_box_def: + name, texture_index = self._parse_box_def(dir_path) + box_png_path = dir_path / "box.png" + if box_png_path.exists(): + box_texture = ray.load_texture(str(box_png_path)) + + # Count TJA files for this directory + tja_count = self._count_tja_files(dir_path) + + # 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 + ) + 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 i, tja_path in enumerate(sorted(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, texture_index) + self.all_song_files[song_key] = song_obj + except Exception as e: + print(f"Error creating SongFile for {tja_path}: {e}") + continue + + content_items.append(self.all_song_files[song_key]) + + # Store content for this directory + self.directory_contents[dir_key] = content_items + + # OPTION 2: Mark directory for lazy crown calculation + self.crown_cache_dirty.add(dir_key) + + # If this is a root directory, add to root items + if is_root: + if has_box_def: + self.root_items.append(directory_obj) + else: + # For roots without box.def, add their TJA files directly + all_tja_files = self._find_tja_files_recursive(dir_path) + for tja_path in sorted(all_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, 620) + self.all_song_files[song_key] = song_obj + except Exception as e: + print(f"Error creating SongFile for {tja_path}: {e}") + continue + self.root_items.append(self.all_song_files[song_key]) + + def _count_tja_files(self, folder_path: Path): + """Count TJA files in directory (matching original logic)""" + 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: + try: + 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()]) + except (IOError, UnicodeDecodeError) as e: + # Handle potential file reading errors + print(f"Warning: Could not read {song_list_path}: {e}") + continue + else: + # 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 sorted(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[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: + # For directories with box.def, we want their direct TJA files + # Set box_def_dirs_only=False to ensure we get files from this directory + 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 = [] try: - has_box_def = self.check_for_box_def(directory) - 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 = self.check_for_box_def(path) + # 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_recursive(path, box_def_dirs_only)) + tja_files.extend(self._find_tja_files_in_directory_only(path)) except (PermissionError, OSError): pass return tja_files - def parse_box_def(self, path): + def _find_tja_files_recursive(self, directory: Path, box_def_dirs_only=True): + tja_files = [] + + try: + has_box_def = (directory / "box.def").exists() + # Fixed: Only skip if box_def_dirs_only is True AND has_box_def AND it's not the directory we're currently processing + # During object generation, we want to get files from directories with box.def + if box_def_dirs_only and has_box_def and directory != self.current_dir: + # This logic should only apply during navigation, not during object generation + # During object generation, we want to collect all TJA files + 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)) + except (PermissionError, OSError): + pass + + return tja_files + + def _parse_box_def(self, path: Path): + """Parse box.def file for directory metadata""" texture_index = 620 name = path.name - with open(path / "box.def", 'r', encoding='utf-8') as box_def: - for line in box_def: - if line.strip().startswith("#GENRE:"): - texture_index = FileSystemItem.GENRE_MAP[line.split(":")[1].strip()] - if line.strip().startswith("#TITLE:"): - name = line.split(":")[1].strip() - if line.strip().startswith("#TITLEJA:"): - if get_config()['general']['language'] == 'ja': - name = line.split(":")[1].strip() + + try: + with open(path / "box.def", 'r', encoding='utf-8') 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, 620) + elif line.startswith("#TITLE:"): + name = line.split(":", 1)[1].strip() + elif line.startswith("#TITLEJA:"): + if get_config()['general']['language'] == 'ja': + name = line.split(":", 1)[1].strip() + except Exception as e: + print(f"Error parsing box.def in {path}: {e}") + return name, texture_index + def _read_song_list(self, path: Path): + """Read and process song_list.txt file""" + tja_files = [] + 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 song_hash.song_hashes is not None: + if hash_val in song_hash.song_hashes: + file_path = Path(song_hash.song_hashes[hash_val]["file_path"]) + if file_path.exists(): + tja_files.append(file_path) + else: + # Try to find by title and subtitle + for key, value in song_hash.song_hashes.items(): + if (value["title"]["en"] == title and + value["subtitle"]["en"][2:] == subtitle and + Path(value["file_path"]).exists()): + hash_val = key + tja_files.append(Path(song_hash.song_hashes[hash_val]["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: + 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 - num_items = len(self.items) - - # Calculate positions for each item relative to the selected item for i, item in enumerate(self.items): - # Calculate the circular distance from selected index offset = i - self.selected_index - # Handle wrap-around by choosing the shortest circular distance - if offset > num_items // 2: - offset -= num_items - elif offset < -num_items // 2: - offset += num_items + if offset > len(self.items) // 2: + offset -= len(self.items) + elif offset < -len(self.items) // 2: + offset += len(self.items) - # Calculate position based on offset position = SongSelectScreen.BOX_CENTER + (100 * offset) - - # Apply the same position adjustments as before if position == SongSelectScreen.BOX_CENTER: position += 150 elif position > SongSelectScreen.BOX_CENTER: @@ -908,92 +1237,73 @@ class FileNavigator: self.calculate_box_positions() def load_root_directories(self): - """Load the list of root directories as selectable items""" - self.items = [] + """Load the pre-generated root directory items""" + self.items = self.root_items.copy() self.in_root_selection = True self.current_dir = Path() self.current_root_dir = Path() - # Create directory items for each root - for root_path in self.root_dirs: - name = root_path.name if root_path.name else str(root_path) - has_box_def = self.check_for_box_def(root_path) - # Only add roots with box.def as directories - if has_box_def: - name, texture_index = self.parse_box_def(root_path) - self.items.append(Directory(root_path, name, texture_index, has_box_def=True)) - else: - # For roots without box.def, add their TJA files directly to the root selection - tja_files = self.find_tja_files_recursive(root_path) - for tja_path in sorted(tja_files): - self.items.append(SongFile(tja_path, tja_path.name, 620)) - # Reset selection self.selected_index = 0 if self.items else -1 - self.calculate_box_positions() def load_current_directory(self): - """Load all directories and TJA files in the current directory""" + """Load pre-generated items for the current directory""" self.items = [] - self.selected_index = 0 + dir_key = str(self.current_dir) + + # Add back/to_root navigation items if self.current_dir != self.current_root_dir: - self.items.append(Directory(self.current_dir.parent, "", 552, back=True)) + back_dir = Directory(self.current_dir.parent, "", 552, back=True) + self.items.append(back_dir) elif not self.in_root_selection: - self.items.append(Directory(Path(), "", 552, to_root=True)) - # Add only directories that contain box.def files - for path in sorted(self.current_dir.iterdir()): - if path.is_dir(): - has_box_def = self.check_for_box_def(path) - if has_box_def: - name, texture_index = self.parse_box_def(path) - self.items.append(Directory(path, name, texture_index, has_box_def=True)) + to_root_dir = Directory(Path(), "", 552, to_root=True) + self.items.append(to_root_dir) - tja_files = [] - if (self.current_dir / 'song_list.txt').exists(): - updated_lines = [] - file_updated = False + # Add pre-generated content for this directory + if dir_key in self.directory_contents: + content_items = self.directory_contents[dir_key] - with open(self.current_dir / 'song_list.txt', 'r', encoding='utf-8-sig') as song_list: - for line in song_list: - hash, title, subtitle = line.strip().split('|') - original_hash = hash + # Handle the every-10-songs navigation logic + song_count = 0 + for item in content_items: + if isinstance(item, SongFile): + if song_count % 10 == 0 and song_count != 0 and song_count <= (len([x for x in content_items if isinstance(x, SongFile)]) - 10): + # Add navigation item + if self.current_dir != self.current_root_dir: + back_dir = Directory(self.current_dir.parent, "", 552, back=True) + self.items.append(back_dir) + elif not self.in_root_selection: + to_root_dir = Directory(Path(), "", 552, to_root=True) + self.items.append(to_root_dir) + song_count += 1 - if song_hash.song_hashes is not None: - if hash in song_hash.song_hashes: - tja_files.append(Path(song_hash.song_hashes[hash]["file_path"])) - else: - for key, value in song_hash.song_hashes.items(): - if value["title"]["en"] == title and value["subtitle"]["en"][2:] == subtitle and Path(value["file_path"]).exists(): - hash = key - tja_files.append(Path(song_hash.song_hashes[hash]["file_path"])) - break - if hash != original_hash: - file_updated = True - updated_lines.append(f"{hash}|{title}|{subtitle}") + self.items.append(item) - if file_updated: - with open(self.current_dir / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list: - for line in updated_lines: - song_list.write(line + '\n') - - else: - tja_files = self.find_tja_files_recursive(self.current_dir) - - # Then add TJA files found - for i, tja_path in enumerate(sorted(tja_files)): - if i % 10 == 0 and i != 0: - if self.current_dir != self.current_root_dir: - self.items.append(Directory(self.current_dir.parent, "", 552, back=True)) - elif not self.in_root_selection: - self.items.append(Directory(Path(), "", 552, to_root=True)) - texture_index = 620 - _, texture_index = self.parse_box_def(self.current_dir) - self.items.append(SongFile(tja_path, tja_path.name, texture_index)) + # OPTIMIZED: Use cached crowns (calculated on-demand) + 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() + 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: @@ -1006,12 +1316,6 @@ class FileNavigator: self.selected_index = (self.selected_index + 1) % len(self.items) self.calculate_box_positions() - def get_items(self): - """Get visible items on screen - now returns all items since positions are dynamic""" - # With wrap-around, we might want to show all items or filter based on visibility - # For now, return all items since their positions are dynamically calculated - return self.items - def get_visible_items(self, screen_width=1280): """Get only the items that would be visible on screen""" if not self.items: @@ -1022,7 +1326,6 @@ class FileNavigator: half_screen = screen_width // 2 for item in self.items: - # Check if item's position is within the visible screen area if abs(item.box.position - center) <= half_screen: visible_items.append(item) @@ -1039,14 +1342,17 @@ class FileNavigator: if selected_item.to_root: self.load_root_directories() else: + # Save current state to history if self.current_dir is not None: self.history.append((self.current_dir, self.selected_index, self.in_root_selection, self.current_root_dir)) + self.current_dir = selected_item.path if self.in_root_selection: self.current_root_dir = selected_item.path self.in_root_selection = False self.selected_index = 0 self.load_current_directory() + elif isinstance(selected_item, SongFile): return selected_item @@ -1058,13 +1364,48 @@ class FileNavigator: self.selected_index = previous_index self.in_root_selection = previous_in_root self.current_root_dir = previous_root_dir - self.load_current_directory() - elif not self.in_root_selection: - # If we're not in history but also not in root selection, go back to root selection - self.load_root_directories() + if self.in_root_selection: + self.load_root_directories() + else: + self.load_current_directory() 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() + raise Exception("No current item available") + + def regenerate_objects(self): + """Regenerate all objects (useful if files have changed on disk)""" + print("Regenerating all objects...") + + # Clear existing objects + self.all_directories.clear() + self.all_song_files.clear() + self.directory_contents.clear() + self.root_items.clear() + self.directory_crowns.clear() # Clear crown cache + self.crown_cache_dirty.clear() # Clear dirty flags + + # Regenerate everything + self._generate_all_objects() + + # Reset navigation state + self.current_dir = Path() + self.current_root_dir = Path() + self.history.clear() + self.load_root_directories() + + def get_stats(self): + """Get statistics about the pre-generated objects""" + song_count_by_dir = {} + for dir_path, items in self.directory_contents.items(): + song_count_by_dir[dir_path] = len([item for item in items if isinstance(item, SongFile)]) + + return { + 'total_directories': len(self.all_directories), + 'total_songs': len(self.all_song_files), + 'root_items': len(self.root_items), + 'directories_with_content': len(self.directory_contents), + 'songs_by_directory': song_count_by_dir + } diff --git a/scenes/title.py b/scenes/title.py index 442fdd7..c4d3fbc 100644 --- a/scenes/title.py +++ b/scenes/title.py @@ -3,12 +3,13 @@ from pathlib import Path import pyray as ray -from libs import song_hash from libs.animation import Animation from libs.audio import audio from libs.utils import ( get_config, get_current_ms, + is_l_don_pressed, + is_r_don_pressed, load_all_textures_from_zip, load_texture_from_zip, ) @@ -47,9 +48,6 @@ class TitleScreen: if not self.screen_init: self.screen_init = True self.load_textures() - - song_hash.song_hashes = song_hash.build_song_hashes() - self.scene = 'Opening Video' self.op_video = VideoPlayer(random.choice(self.op_video_list)) self.attract_video = VideoPlayer(random.choice(self.attract_video_list)) @@ -97,10 +95,8 @@ class TitleScreen: self.on_screen_start() self.scene_manager() - keys = get_config()["keybinds"]["left_don"] + get_config()["keybinds"]["right_don"] - for key in keys: - if ray.is_key_pressed(ord(key)): - return self.on_screen_end() + if is_l_don_pressed() or is_r_don_pressed(): + return self.on_screen_end() def draw(self): if self.scene == 'Opening Video': diff --git a/uv.lock b/uv.lock index a4ec863..811971c 100644 --- a/uv.lock +++ b/uv.lock @@ -143,15 +143,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, ] -[[package]] -name = "inflection" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" }, -] - [[package]] name = "moviepy" version = "2.1.2" @@ -235,6 +226,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" }, ] +[[package]] +name = "pandas" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload-time = "2025-06-05T03:27:54.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/1e/ba313812a699fe37bf62e6194265a4621be11833f5fce46d9eae22acb5d7/pandas-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8adff9f138fc614347ff33812046787f7d43b3cef7c0f0171b3340cae333f6ca", size = 11551836, upload-time = "2025-06-05T03:26:22.784Z" }, + { url = "https://files.pythonhosted.org/packages/1b/cc/0af9c07f8d714ea563b12383a7e5bde9479cf32413ee2f346a9c5a801f22/pandas-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5f08eb9a445d07720776df6e641975665c9ea12c9d8a331e0f6890f2dcd76ef", size = 10807977, upload-time = "2025-06-05T16:50:11.109Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3e/8c0fb7e2cf4a55198466ced1ca6a9054ae3b7e7630df7757031df10001fd/pandas-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa35c266c8cd1a67d75971a1912b185b492d257092bdd2709bbdebe574ed228d", size = 11788230, upload-time = "2025-06-05T03:26:27.417Z" }, + { url = "https://files.pythonhosted.org/packages/14/22/b493ec614582307faf3f94989be0f7f0a71932ed6f56c9a80c0bb4a3b51e/pandas-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a0cc77b0f089d2d2ffe3007db58f170dae9b9f54e569b299db871a3ab5bf46", size = 12370423, upload-time = "2025-06-05T03:26:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/9f/74/b012addb34cda5ce855218a37b258c4e056a0b9b334d116e518d72638737/pandas-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c06f6f144ad0a1bf84699aeea7eff6068ca5c63ceb404798198af7eb86082e33", size = 12990594, upload-time = "2025-06-06T00:00:13.934Z" }, + { url = "https://files.pythonhosted.org/packages/95/81/b310e60d033ab64b08e66c635b94076488f0b6ce6a674379dd5b224fc51c/pandas-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed16339bc354a73e0a609df36d256672c7d296f3f767ac07257801aa064ff73c", size = 13745952, upload-time = "2025-06-05T03:26:39.475Z" }, + { url = "https://files.pythonhosted.org/packages/25/ac/f6ee5250a8881b55bd3aecde9b8cfddea2f2b43e3588bca68a4e9aaf46c8/pandas-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:fa07e138b3f6c04addfeaf56cc7fdb96c3b68a3fe5e5401251f231fce40a0d7a", size = 11094534, upload-time = "2025-06-05T03:26:43.23Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865, upload-time = "2025-06-05T03:26:46.774Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154, upload-time = "2025-06-05T16:50:14.439Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180, upload-time = "2025-06-05T16:50:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493, upload-time = "2025-06-05T03:26:51.813Z" }, + { url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733, upload-time = "2025-06-06T00:00:18.651Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406, upload-time = "2025-06-05T03:26:55.992Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199, upload-time = "2025-06-05T03:26:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913, upload-time = "2025-06-05T03:27:02.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249, upload-time = "2025-06-05T16:50:20.17Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359, upload-time = "2025-06-05T03:27:06.431Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789, upload-time = "2025-06-05T03:27:09.875Z" }, + { url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734, upload-time = "2025-06-06T00:00:22.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381, upload-time = "2025-06-05T03:27:15.641Z" }, + { url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135, upload-time = "2025-06-05T03:27:24.131Z" }, + { url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356, upload-time = "2025-06-05T03:27:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674, upload-time = "2025-06-05T03:27:39.448Z" }, + { url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876, upload-time = "2025-06-05T03:27:43.652Z" }, + { url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182, upload-time = "2025-06-05T03:27:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686, upload-time = "2025-06-06T00:00:26.142Z" }, + { url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847, upload-time = "2025-06-05T03:27:51.465Z" }, +] + [[package]] name = "pillow" version = "10.4.0" @@ -315,12 +347,14 @@ dependencies = [ { name = "moviepy" }, { name = "numpy" }, { name = "opencv-python" }, + { name = "pandas" }, { name = "pydub" }, - { name = "raylib" }, - { name = "raylib-dynamic" }, + { name = "raylib-sdl" }, { name = "ruff" }, { name = "scipy" }, { name = "sounddevice" }, + { name = "soundfile" }, + { name = "tomlkit" }, ] [package.metadata] @@ -329,12 +363,26 @@ requires-dist = [ { name = "moviepy", specifier = ">=2.1.2" }, { name = "numpy", specifier = ">=2.2.5" }, { name = "opencv-python", specifier = ">=4.11.0.86" }, + { name = "pandas", specifier = ">=2.3.0" }, { name = "pydub", specifier = ">=0.25.1" }, - { name = "raylib", specifier = ">=5.5.0.2" }, - { name = "raylib-dynamic", specifier = ">=5.5.0.2" }, + { name = "raylib-sdl", specifier = ">=5.5.0.2" }, { name = "ruff", specifier = ">=0.11.7" }, { name = "scipy", specifier = ">=1.15.2" }, { name = "sounddevice", specifier = ">=0.5.1" }, + { name = "soundfile", specifier = ">=0.13.1" }, + { name = "tomlkit", specifier = ">=0.13.3" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] @@ -347,42 +395,40 @@ wheels = [ ] [[package]] -name = "raylib" -version = "5.5.0.2" +name = "pytz" +version = "2025.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/35/9bf3a2af73c55fd4310dcaec4f997c739888e0db9b4dfac71b7680810852/raylib-5.5.0.2.tar.gz", hash = "sha256:83c108ae3b4af40b53c93d1de2afbe309e986dd5efeb280ebe2e61c79956edb0", size = 181172, upload-time = "2024-11-26T11:12:02.791Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c4/ce21721b474eb8f65379f7315b382ccfe1d5df728eea4dcf287b874e7461/raylib-5.5.0.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:37eb0ec97fc6b08f989489a50e09b5dde519e1bb8eb17e4033ac82227b0e5eda", size = 1703742, upload-time = "2024-11-26T11:09:31.115Z" }, - { url = "https://files.pythonhosted.org/packages/23/61/138e305c82549869bb8cd41abe75571559eafbeab6aed1ce7d8fbe3ffd58/raylib-5.5.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bb9e506ecd3dbec6dba868eb036269837a8bde68220690842c3238239ee887ef", size = 1247449, upload-time = "2024-11-26T11:09:34.182Z" }, - { url = "https://files.pythonhosted.org/packages/85/e0/dc638c42d1a505f0992263d48e1434d82c21afdf376b06f549d2e281dfd4/raylib-5.5.0.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:70aa8bed67875a8cf25191f35263ef92d646bdfcb1f507915c81562a321f4931", size = 2184315, upload-time = "2024-11-26T11:09:36.715Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1a/49db57283a28fdc1ff0e4604911b7fff085128c2ac8bdd9efa8c5c47439d/raylib-5.5.0.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:0365e8c578f72f598795d9377fc70342f0d62aa193c2f304ca048b3e28866752", size = 2278139, upload-time = "2024-11-26T11:09:39.475Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8a/e1a690ab6889d4cb67346a2d32bad8b8e8b0f85ec826b00f76b0ad7e6ad6/raylib-5.5.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:5219be70e7fca03e9c4fddebf7e60e885d77137125c7a13f3800a947f8562a13", size = 1693944, upload-time = "2024-11-26T11:09:41.596Z" }, - { url = "https://files.pythonhosted.org/packages/69/2b/49bfa6833ad74ddf318d54ecafe73d535f583531469ecbd5b009d79667d1/raylib-5.5.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5233c529d9a0cfd469d88239c2182e55c5215a7755d83cc3d611148d3b9c9e67", size = 1706157, upload-time = "2024-11-26T11:09:43.6Z" }, - { url = "https://files.pythonhosted.org/packages/58/9c/8a3f4de0c81ad1228bf26410cfe3ecdc73011c59f18e542685ffc92c0120/raylib-5.5.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1f76204ffbc492722b571b12dbdc0dca89b10da76ddf48c12a3968d2db061dff", size = 1248027, upload-time = "2025-01-04T20:21:46.269Z" }, - { url = "https://files.pythonhosted.org/packages/7f/16/63baf1aae94832b9f5d15cafcee67bb6dd07a20cf64d40bac09903b79274/raylib-5.5.0.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:f8cc2e39f1d6b29211a97ec0ac818a5b04c43a40e747e4b4622101d48c711f9e", size = 2195374, upload-time = "2024-11-26T11:09:46.114Z" }, - { url = "https://files.pythonhosted.org/packages/70/bd/61a006b4e3ce4a6ca974cb0ceeb19f3816815ebabac650e9bf82767e65f6/raylib-5.5.0.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:f12da578a28da7f48481f46323e5aab8dd25461982b0e80d045782d6e69649f5", size = 2299593, upload-time = "2024-11-26T11:09:48.963Z" }, - { url = "https://files.pythonhosted.org/packages/f4/4f/59d554cc495bea8235b17cebfc76ed57aaa602c613b870159e31282fd4c1/raylib-5.5.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:b40234bbad9523fd6a2049640c76a98b4d6f0b8f4bd19bd33eaee55faf5e050d", size = 1696780, upload-time = "2024-11-26T11:09:50.787Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0a/78edc3ed1e2ca7e2ccea31ac5a8f4440a924662ee1042ecb76e08f465d8a/raylib-5.5.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2bfa9419a12eeb259f99c75c351db167968306295a5017cd4735241eaf2fa399", size = 1706104, upload-time = "2024-11-26T11:09:53.571Z" }, - { url = "https://files.pythonhosted.org/packages/48/fb/8b79a03c0d63bd6613d3e25df26d6c42fe36689f052abefa00a311be53fc/raylib-5.5.0.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:216456d0fa8da85c253a8596ca4b35c995bbf8af0fb8fa5244a4c6f5debfe294", size = 1248018, upload-time = "2024-11-26T11:09:56.257Z" }, - { url = "https://files.pythonhosted.org/packages/57/42/8ef2a8e21a2b4ba7ac4ea13b1dd9bc5d1084020ddca06ad0c6582c784a4e/raylib-5.5.0.2-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:4f1c33b7d2404e70dbf3dd8d71701efb24902adfcb553122bf3d9441ea4fd6f0", size = 2299336, upload-time = "2024-11-26T11:09:59.307Z" }, - { url = "https://files.pythonhosted.org/packages/b4/67/4c25526fde4dabf5cc60093202f8d07a7dc4b1bd29d7b84db55b443a527c/raylib-5.5.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:6b60c97a9f6cfaeef4f59ff162ea29f02c18282d129edebb62f132b2ad6bb935", size = 1696790, upload-time = "2024-11-26T11:10:01.812Z" }, - { url = "https://files.pythonhosted.org/packages/4a/22/2e02e3738ad041f5ec2830aecdfab411fc2960bfc3400e03b477284bfaf7/raylib-5.5.0.2-pp311-pypy311_pp73-macosx_10_13_x86_64.whl", hash = "sha256:bc45fe1c0aac50aa319a9a66d44bb2bd0dcd038a44d95978191ae7bfeb4a06d8", size = 1216231, upload-time = "2025-02-12T04:21:59.38Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7d/b29afedc4a706b12143f74f322cb32ad5a6f43e56aaca2a9fb89b0d94eee/raylib-5.5.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.whl", hash = "sha256:2242fd6079da5137e9863a447224f800adef6386ca8f59013a5d62cc5cadab2b", size = 1394928, upload-time = "2025-02-12T04:22:03.021Z" }, - { url = "https://files.pythonhosted.org/packages/b6/fa/2daf36d78078c6871b241168a36156169cfc8ea089faba5abe8edad304be/raylib-5.5.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e475a40764c9f83f9e66406bd86d85587eb923329a61ade463c3c59e1e880b16", size = 1564224, upload-time = "2025-02-12T04:22:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[package]] -name = "raylib-dynamic" +name = "raylib-sdl" version = "5.5.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, - { name = "inflection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/74/ee15f73ad900bc0ea013852626998f78ba1eec020f470bf9a6873f3725b4/raylib_dynamic-5.5.0.2.tar.gz", hash = "sha256:125c95e51754c56fc045052b22642e2d53839cb1aac9db9df99355744daae868", size = 3172948, upload-time = "2024-11-26T11:12:04.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/b6/24c241f86ed6101f277d600c8f69cd12368848f1e94ed82d21b4d9dfba11/raylib_sdl-5.5.0.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:265a312ea73d8987a4c58075db7f27e356e217d52b22383ccfacf07f6eeb3556", size = 2287593, upload-time = "2024-11-26T11:11:13.524Z" }, + { url = "https://files.pythonhosted.org/packages/db/22/cada7093a7fc22ba8a68c032850175e7480b0e4149e782ee6093dca04006/raylib_sdl-5.5.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8e86aa9ee0190cf02ae44bace9dc87379d171141a62daddd963dea40d40dbca7", size = 1745212, upload-time = "2025-01-04T20:21:04.281Z" }, + { url = "https://files.pythonhosted.org/packages/22/a2/387b20de1be19ece354a6c2cda30dc369948438dd824095b7da36ed974e7/raylib_sdl-5.5.0.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:3e2a91fef2525cc9a5a13fb3dbbd77e6c614b4d33f446c6047720516628b80d4", size = 2827982, upload-time = "2024-11-26T11:11:15.736Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b4/f96cfb79981ac8e692975ae06ca224043d92b386b7f8a80cb515a85775c3/raylib_sdl-5.5.0.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:5263010953358beffabca8f7f1dfedcccc0f931afb91cfb242fd7783c875d12c", size = 2967405, upload-time = "2024-11-26T11:11:18.039Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/c26425f317392a98c414c1f14974f511b1abf7f36d54069c80026ce505a3/raylib_sdl-5.5.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:4dcb099fa0bfa3048585d76445fe15cae3b2418c919f5777c54f429e1d3eb888", size = 1617306, upload-time = "2024-11-26T11:11:20.774Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9d/8cd7466084f93b125c052f1825bc505d2822c19539dc5b3a2ac6a588fcaa/raylib_sdl-5.5.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:48e374f5c20e618f4c7c5664ac9e4f39f53245ffb9b62d6d133b8676716363b0", size = 2290643, upload-time = "2024-11-26T11:11:23.67Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b0/1891f9712bed1083f1360d7f57f042a48bed110b5d5224256a33eeb9f302/raylib_sdl-5.5.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6cd980a3fabdbe88a6449acf7ab24686854dce258dea9ff5b0eb46b7eae981c0", size = 1746303, upload-time = "2024-11-26T11:11:26.032Z" }, + { url = "https://files.pythonhosted.org/packages/24/de/6848b61e1df0eda838a61cb9e7060bd10d4af9616974c4360257c65ff8ff/raylib_sdl-5.5.0.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a8e0fd9399188943c81dadf60d3d13198da1f9e0d84cc2aefa9e0c0d5c6266d6", size = 2840025, upload-time = "2024-11-26T11:11:29.705Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b8/d41eacab8666fb955877cb5e3d4afea7ba4d01f668d9e91aa1716c1230cb/raylib_sdl-5.5.0.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:92407aad5382958ec684c35d3a28c8fc091faca3b58fc5dd8b3495a1065520ff", size = 2985567, upload-time = "2024-11-26T11:11:32.887Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/d9f26daeda03ccd2d5b637fc3da25d039b39c27f1c2dde0ae9eeb2c40861/raylib_sdl-5.5.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ab31941d2247a043beb545f34ebef1844f1d9c5fac749032e991ee8c18439c5a", size = 1619555, upload-time = "2024-11-26T11:11:35.387Z" }, + { url = "https://files.pythonhosted.org/packages/06/9c/e3d596db50bf4fb35f39478f1f529446dded5a7142c0583dbd8b19d52fe4/raylib_sdl-5.5.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bc6fe38110da1c98c341ede7064277ff79fb1b2a607787bd772974b4e4105d9a", size = 2290691, upload-time = "2024-11-26T11:11:38.045Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d3/70d418057bb162d04df5cc5249ff83c38d02b55dbfe9363334518f2276b6/raylib_sdl-5.5.0.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:696ff487d3688a6c36c05c0cfcad1cd891351268739e5dfc9037692e958caa56", size = 1746153, upload-time = "2024-11-26T11:11:41.109Z" }, + { url = "https://files.pythonhosted.org/packages/56/8b/ccea24888331953eb43f2aa869d0a757093ebe88036d5234d1e83ef1785e/raylib_sdl-5.5.0.2-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:10672ac9c3b3bac2da95d99b1e128b7ebded8aff4f3325063544ea55c0712a62", size = 2985152, upload-time = "2024-11-26T11:11:44.078Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/3a5e650d8c8dc3670a50ca000b6bed8f10f86872090eaa2a3450595f1528/raylib_sdl-5.5.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:29d7888a7ff42273a91b44520b209d24f20a9fd5eae0d9ac18851e52797c5d8b", size = 1619604, upload-time = "2024-11-26T11:11:46.22Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2d/d531187e2c21d724fb07707dd71ac28d9e103d616ae595017f9b4679e8e2/raylib_sdl-5.5.0.2-pp311-pypy311_pp73-macosx_10_13_x86_64.whl", hash = "sha256:51017cb725fce7da0b8f220839f6db91e20bbb3ba58e7debe3b1fd67f4468188", size = 1809261, upload-time = "2025-02-12T04:22:11.465Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/81ec5b8e0c9b89e95b85d20fd4685368dabc0d7fedc8050efdfe3bbcc2f2/raylib_sdl-5.5.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.whl", hash = "sha256:9453c29fc28b08dcae8a6432aeb07f13fbf8504fb1351941df64b9fb8ba0bac3", size = 2180109, upload-time = "2025-02-12T04:22:14.472Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4f/8ba71611c74d6e3ff5e95d2935c1f5f98fc61183ebf70d4dfb09547a5767/raylib_sdl-5.5.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fd3841ab8d8d6ca24e4fbffc9514c58c0cf5bd29e3f2406006eba872416325b3", size = 1499920, upload-time = "2025-02-12T04:22:16.996Z" }, +] [[package]] name = "ruff" @@ -456,6 +502,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705, upload-time = "2025-02-17T00:34:43.619Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sounddevice" version = "0.5.1" @@ -471,6 +526,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/9b/15217b04f3b36d30de55fef542389d722de63f1ad81f9c72d8afc98cb6ab/sounddevice-0.5.1-py3-none-win_amd64.whl", hash = "sha256:4313b63f2076552b23ac3e0abd3bcfc0c1c6a696fc356759a13bd113c9df90f1", size = 363634, upload-time = "2024-10-12T09:40:11.065Z" }, ] +[[package]] +name = "soundfile" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + [[package]] name = "tqdm" version = "4.67.1" @@ -482,3 +565,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +]