From 655c2683cf65508c3bad89ea193ac0e4a51f9b19 Mon Sep 17 00:00:00 2001 From: Yonokid <37304577+Yonokid@users.noreply.github.com> Date: Sat, 3 Jan 2026 11:10:52 -0500 Subject: [PATCH 1/8] settings menu --- PyTaiko.py | 4 +- Skins/PyTaikoGreen | 2 +- scenes/settings.py | 389 +++++++++++++++++---------------------------- 3 files changed, 153 insertions(+), 242 deletions(-) diff --git a/PyTaiko.py b/PyTaiko.py index 35d4207..1ce1938 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -242,7 +242,7 @@ def init_audio(): def check_args(): if len(sys.argv) == 1: - return Screens.LOADING + return Screens.SETTINGS parser = argparse.ArgumentParser(description='Launch game with specified song file') parser.add_argument('song_path', type=str, help='Path to the TJA song file') @@ -265,7 +265,7 @@ def check_args(): selected_difficulty = args.difficulty else: selected_difficulty = max(tja.metadata.course_data.keys()) - current_screen = Screens.GAME_PRACTICE if args.practice else Screens.AI_GAME + current_screen = Screens.GAME_PRACTICE if args.practice else Screens.GAME global_data.session_data[PlayerNum.P1].selected_song = path global_data.session_data[PlayerNum.P1].selected_difficulty = selected_difficulty global_data.modifiers[PlayerNum.P1].auto = args.auto diff --git a/Skins/PyTaikoGreen b/Skins/PyTaikoGreen index ccd048a..5527b51 160000 --- a/Skins/PyTaikoGreen +++ b/Skins/PyTaikoGreen @@ -1 +1 @@ -Subproject commit ccd048aa2d65bd954c87476c178f3d4f0fa2872f +Subproject commit 5527b51f3a8faacad829a608a62a8652dd3a4d4e diff --git a/scenes/settings.py b/scenes/settings.py index a431ec0..0c11a28 100644 --- a/scenes/settings.py +++ b/scenes/settings.py @@ -1,12 +1,16 @@ +import json import logging import pyray as ray from libs.audio import audio from libs.config import save_config +from libs.global_objects import Indicator from libs.screen import Screen from libs.texture import tex from libs.utils import ( + OutlinedText, + get_current_ms, global_data, is_l_don_pressed, is_l_kat_pressed, @@ -16,262 +20,169 @@ from libs.utils import ( logger = logging.getLogger(__name__) +class BaseOptionBox: + def __init__(self, name: str, description: str): + self.name = OutlinedText(name, 30, ray.WHITE) + self.description = description + self.is_highlighted = False + + def draw(self): + if self.is_highlighted: + tex.draw_texture('background', 'title_highlight') + else: + tex.draw_texture('background', 'title') + text_x = tex.textures['background']['title'].x[0] + (tex.textures['background']['title'].width//2) - (self.name.texture.width//2) + text_y = tex.textures['background']['title'].y[0] + self.name.draw(outline_color=ray.BLACK, x=text_x, y=text_y) + +class Box: + """Box class for the entry screen""" + def __init__(self, text: OutlinedText, box_options: dict): + self.text = text + self.x = 10 * tex.screen_scale + self.y = -50 * tex.screen_scale + self.move = tex.get_animation(0) + self.is_selected = False + self.outline_color = ray.Color(109, 68, 24, 255) + self.direction = 1 + self.target_position = float('inf') + self.start_position = self.y + language = global_data.config["general"]["language"] + self.options = [BaseOptionBox(box_options[option]["name"][language], box_options[option]["description"][language]) for option in box_options] + + def __repr__(self): + return str(self.__dict__) + + def move_left(self): + """Move the box left""" + if self.y != self.target_position and self.target_position != float('inf'): + return False + self.move.start() + self.direction = 1 + self.start_position = self.y + self.target_position = self.y + (100 * tex.screen_scale * self.direction) + + if self.target_position >= 650: + self.target_position = -50 + (self.target_position - 650) + + return True + + def move_right(self): + """Move the box right""" + if self.y != self.target_position and self.target_position != float('inf'): + return False + self.move.start() + self.start_position = self.y + self.direction = -1 + self.target_position = self.y + (100 * tex.screen_scale * self.direction) + + if self.target_position < -50: + self.target_position = 650 + (self.target_position + 50) + + return True + + def update(self, current_time_ms: float, is_selected: bool): + self.move.update(current_time_ms) + self.is_selected = is_selected + if self.move.is_finished: + self.y = self.target_position + else: + self.y = self.start_position + (self.move.attribute * self.direction) + + def _draw_highlighted(self): + tex.draw_texture('box', 'box_highlight', x=self.x, y=self.y) + + def _draw_text(self): + text_x = self.x + (tex.textures['box']['box'].width//2) - (self.text.texture.width//2) + text_y = self.y + (tex.textures['box']['box'].height//2) - (self.text.texture.height//2) + if self.is_selected: + self.text.draw(outline_color=ray.BLACK, x=text_x, y=text_y) + else: + self.text.draw(outline_color=self.outline_color, x=text_x, y=text_y) + + def draw(self): + tex.draw_texture('box', 'box', x=self.x, y=self.y) + if self.is_selected: + self._draw_highlighted() + self._draw_text() + +class BoxManager: + """BoxManager class for the entry screen""" + def __init__(self, settings_template: dict): + language = global_data.config["general"]["language"] + self.boxes = [Box(OutlinedText(settings_template[config_name]["name"][language], tex.skin_config["entry_box_text"].font_size - int(5*tex.screen_scale), ray.WHITE, outline_thickness=5), settings_template[config_name]["options"]) for config_name in settings_template] + self.num_boxes = len(self.boxes) + self.selected_box_index = 3 + self.is_2p = False + + for i, box in enumerate(self.boxes): + box.y += 100*i + box.start_position += 100*i + + def move_left(self): + """Move the cursor to the left""" + moved = True + for box in self.boxes: + if not box.move_left(): + moved = False + + if moved: + self.selected_box_index = (self.selected_box_index - 1) % self.num_boxes + + def move_right(self): + """Move the cursor to the right""" + moved = True + for box in self.boxes: + if not box.move_right(): + moved = False + + if moved: + self.selected_box_index = (self.selected_box_index + 1) % self.num_boxes + + def update(self, current_time_ms: float): + for i, box in enumerate(self.boxes): + is_selected = i == self.selected_box_index + box.update(current_time_ms, is_selected) + + def draw(self): + for box in self.boxes: + box.draw() class SettingsScreen(Screen): def on_screen_start(self): super().on_screen_start() self.config = global_data.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.editing_gamepad = False + self.indicator = Indicator(Indicator.State.SELECT) + self.template = json.loads((tex.graphics_path / "settings_template.json").read_text(encoding='utf-8')) + self.box_manager = BoxManager(self.template) def on_screen_end(self, next_screen: str): save_config(self.config) global_data.config = self.config - audio.close_audio_device() - audio.device_type = global_data.config["audio"]["device_type"] - sample_rate = global_data.config["audio"]["sample_rate"] - if sample_rate < 0: - sample_rate = 44100 - audio.target_sample_rate = sample_rate - audio.buffer_size = global_data.config["audio"]["buffer_size"] - audio.volume_presets = global_data.config["volume"] audio.init_audio_device() logger.info("Settings saved and audio device re-initialized") return next_screen - 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] - logger.info(f"Toggled boolean setting: {section}.{key} -> {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, - 'sample_rate': 1000, - } - - step = step_sizes.get(key, 1) - new_value = current_value + (step * increment) - - if key == 'sample_rate': - valid_rates = [-1, 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] - - if key == 'buffer_size': - valid_sizes = [-1, 32, 64, 128, 256, 512, 1024] - current_idx = valid_sizes.index(current_value) if current_value in valid_sizes else 2 - new_idx = max(0, min(len(valid_sizes) - 1, current_idx + increment)) - new_value = valid_sizes[new_idx] - - self.config[section][key] = new_value - logger.info(f"Changed numeric setting: {section}.{key} -> {new_value}") - - def handle_string_cycle(self, section, key): - """Cycle through predefined string values""" - current_value = self.config[section][key] - - options = { - 'language': ['ja', 'en'], - } - - 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] - logger.info(f"Cycled string setting: {section}.{key} -> {self.config[section][key]}") - - def handle_key_binding(self, section, key): - """Handle key binding changes""" - self.editing_key = True - logger.info(f"Started key binding edit for: {section}.{key}") - - 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 - logger.info(f"Key binding updated: {current_header}.{setting_key} -> {new_key}") - elif key_pressed == global_data.config["keys"]["back_key"]: - self.editing_key = False - logger.info("Key binding edit cancelled") - - def handle_gamepad_binding(self, section, key): - self.editing_gamepad = True - logger.info(f"Started gamepad binding edit for: {section}.{key}") - - def update_gamepad_binding(self): - """Update gamepad binding based on input""" - button_pressed = ray.get_gamepad_button_pressed() - if button_pressed != 0: - 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] = [button_pressed] - self.editing_gamepad = False - logger.info(f"Gamepad binding updated: {current_header}.{setting_key} -> {button_pressed}") - if ray.is_key_pressed(global_data.config["keys"]["back_key"]): - self.editing_gamepad = False - logger.info("Gamepad binding edit cancelled") + def handle_input(self): + if is_l_kat_pressed(): + audio.play_sound('kat', 'sound') + self.box_manager.move_left() + elif is_r_kat_pressed(): + audio.play_sound('kat', 'sound') + self.box_manager.move_right() def update(self): super().update() - # Handle key binding editing - if self.editing_key: - self.update_key_binding() - return + self.handle_input() - if self.editing_gamepad: - self.update_gamepad_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()): - logger.info("Exiting settings screen") - return self.on_screen_end("ENTRY") - - # 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 - logger.info(f"Navigated to next section: {self.headers[self.header_index]}") - elif is_l_kat_pressed(): - self.header_index = (self.header_index - 1) % len(self.headers) - self.setting_index = 0 - logger.info(f"Navigated to previous section: {self.headers[self.header_index]}") - elif (is_l_don_pressed() or is_r_don_pressed()) and current_header != 'Exit': - self.in_setting_edit = True - logger.info(f"Entered section edit: {current_header}") - 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) - logger.info(f"Navigated to next setting: {settings[self.setting_index][0]}") - elif is_l_kat_pressed(): - self.setting_index = (self.setting_index - 1) % len(settings) - logger.info(f"Navigated to previous setting: {settings[self.setting_index][0]}") - 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 'keys' in current_header: - self.handle_key_binding(current_header, setting_key) - elif 'gamepad' in current_header: - self.handle_gamepad_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 isinstance(setting_value[0], int): - self.handle_gamepad_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 ('keys' not in current_header) and ('gamepad' not in current_header): - self.handle_string_cycle(current_header, setting_key) - - elif ray.is_key_pressed(global_data.config["keys"]["back_key"]): - self.in_setting_edit = False - logger.info("Exited section edit") + current_time = get_current_ms() + self.indicator.update(current_time) + self.box_manager.update(current_time) def draw(self): - ray.draw_rectangle(0, 0, tex.screen_width, tex.screen_height, ray.BLACK) - # Draw title - ray.draw_rectangle(0, 0, tex.screen_width, tex.screen_height, ray.BLACK) - 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) - if key == 'device_type' and not isinstance(value, list): - display_value = f'{display_value} ({audio.get_host_api_name(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) + tex.draw_texture('background', 'background') + self.box_manager.draw() + tex.draw_texture('background', 'footer') + self.indicator.draw(tex.skin_config['song_select_indicator'].x, tex.skin_config['song_select_indicator'].y) + tex.draw_texture('background', 'overlay', scale=0.70) From 1781960dccc59143120ac2359669b87d5343849e Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Sat, 3 Jan 2026 16:28:34 -0500 Subject: [PATCH 2/8] finish settings menu --- PyTaiko.py | 2 +- Skins/PyTaikoGreen | 2 +- scenes/settings.py | 368 +++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 341 insertions(+), 31 deletions(-) diff --git a/PyTaiko.py b/PyTaiko.py index 1ce1938..4302def 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -242,7 +242,7 @@ def init_audio(): def check_args(): if len(sys.argv) == 1: - return Screens.SETTINGS + return Screens.LOADING parser = argparse.ArgumentParser(description='Launch game with specified song file') parser.add_argument('song_path', type=str, help='Path to the TJA song file') diff --git a/Skins/PyTaikoGreen b/Skins/PyTaikoGreen index 5527b51..d870ca2 160000 --- a/Skins/PyTaikoGreen +++ b/Skins/PyTaikoGreen @@ -1 +1 @@ -Subproject commit 5527b51f3a8faacad829a608a62a8652dd3a4d4e +Subproject commit d870ca298076cb84a86cfcf9b64d93d99f4733fe diff --git a/scenes/settings.py b/scenes/settings.py index 0c11a28..aa9a512 100644 --- a/scenes/settings.py +++ b/scenes/settings.py @@ -3,8 +3,9 @@ import logging import pyray as ray +from libs.animation import Animation from libs.audio import audio -from libs.config import save_config +from libs.config import get_key_string, save_config from libs.global_objects import Indicator from libs.screen import Screen from libs.texture import tex @@ -21,34 +22,281 @@ from libs.utils import ( logger = logging.getLogger(__name__) class BaseOptionBox: - def __init__(self, name: str, description: str): + def __init__(self, name: str, description: str, path: str, values: dict): self.name = OutlinedText(name, 30, ray.WHITE) + self.setting_header, self.setting_name = path.split('/') self.description = description self.is_highlighted = False + self.value = global_data.config[self.setting_header][self.setting_name] + + def update(self, current_time): + pass + + def move_left(self): + pass + + def move_right(self): + pass + + def confirm(self): + global_data.config[self.setting_header][self.setting_name] = self.value + + def __repr__(self): + return str(self.__dict__) def draw(self): + tex.draw_texture('background', 'overlay', scale=0.70) if self.is_highlighted: tex.draw_texture('background', 'title_highlight') else: tex.draw_texture('background', 'title') text_x = tex.textures['background']['title'].x[0] + (tex.textures['background']['title'].width//2) - (self.name.texture.width//2) - text_y = tex.textures['background']['title'].y[0] + text_y = tex.textures['background']['title'].y[0] + self.name.texture.height//4 self.name.draw(outline_color=ray.BLACK, x=text_x, y=text_y) + ray.draw_text_ex(global_data.font, self.description, (450 * tex.screen_scale, 270 * tex.screen_scale), 25 * tex.screen_scale, 1, ray.BLACK) + +class BoolOptionBox(BaseOptionBox): + def __init__(self, name: str, description: str, path: str, values: dict): + super().__init__(name, description, path, values) + language = global_data.config["general"]["language"] + self.on_value = OutlinedText(values["true"].get(language, values["true"]["en"]), int(30 * tex.screen_scale), ray.WHITE) + self.off_value = OutlinedText(values["false"].get(language, values["false"]["en"]), int(30 * tex.screen_scale), ray.WHITE) + + def move_left(self): + self.value = False + + def move_right(self): + self.value = True + + def draw(self): + super().draw() + if not self.value: + tex.draw_texture('option', 'button_on', index=0) + else: + tex.draw_texture('option', 'button_off', index=0) + text_x = tex.textures["option"]["button_on"].x[0] + (tex.textures["option"]["button_on"].width//2) - (self.off_value.texture.width//2) + text_y = tex.textures["option"]["button_on"].y[0] + (tex.textures["option"]["button_on"].height//2) - (self.off_value.texture.height//2) + self.off_value.draw(outline_color=ray.BLACK, x=text_x, y=text_y) + if self.value: + tex.draw_texture('option', 'button_on', index=1) + else: + tex.draw_texture('option', 'button_off', index=1) + text_x = tex.textures["option"]["button_on"].x[1] + (tex.textures["option"]["button_on"].width//2) - (self.on_value.texture.width//2) + text_y = tex.textures["option"]["button_on"].y[1] + (tex.textures["option"]["button_on"].height//2) - (self.on_value.texture.height//2) + self.on_value.draw(outline_color=ray.BLACK, x=text_x, y=text_y) + +class IntOptionBox(BaseOptionBox): + def __init__(self, name: str, description: str, path: str, values: dict): + super().__init__(name, description, path, values) + self.value_text = OutlinedText(str(self.value), int(30 * tex.screen_scale), ray.WHITE) + self.flicker_fade = Animation.create_fade(400, initial_opacity=0.0, final_opacity=1.0, reverse_delay=0, loop=True) + self.flicker_fade.start() + language = global_data.config["general"]["language"] + self.value_list = [] + if values != dict(): + self.value_list = list(values.keys()) + self.value_index = 0 + self.values = values + self.value_text = OutlinedText(self.values[str(self.value)].get(language, self.values[str(self.value)]["en"]), int(30 * tex.screen_scale), ray.WHITE) + + def update(self, current_time): + self.flicker_fade.update(current_time) + + def move_left(self): + if self.value_list: + self.value_index = max(self.value_index - 1, 0) + self.value = int(self.value_list[self.value_index]) + self.value_text = OutlinedText(self.values[str(self.value)][global_data.config["general"]["language"]], int(30 * tex.screen_scale), ray.WHITE) + else: + self.value -= 1 + self.value_text = OutlinedText(str(self.value), int(30 * tex.screen_scale), ray.WHITE) + + def move_right(self): + if self.value_list: + self.value_index = min(self.value_index + 1, len(self.value_list) - 1) + self.value = int(self.value_list[self.value_index]) + self.value_text = OutlinedText(self.values[str(self.value)][global_data.config["general"]["language"]], int(30 * tex.screen_scale), ray.WHITE) + else: + self.value += 1 + self.value_text = OutlinedText(str(self.value), int(30 * tex.screen_scale), ray.WHITE) + + def draw(self): + super().draw() + tex.draw_texture('option', 'button_off', index=2) + if self.is_highlighted: + tex.draw_texture('option', 'button_on', index=2, fade=self.flicker_fade.attribute) + text_x = tex.textures["option"]["button_on"].x[2] + (tex.textures["option"]["button_on"].width//2) - (self.value_text.texture.width//2) + text_y = tex.textures["option"]["button_on"].y[2] + (tex.textures["option"]["button_on"].height//2) - (self.value_text.texture.height//2) + self.value_text.draw(outline_color=ray.BLACK, x=text_x, y=text_y) + +class StrOptionBox(BaseOptionBox): + def __init__(self, name: str, description: str, path: str, values: dict): + super().__init__(name, description, path, values) + self.flicker_fade = Animation.create_fade(400, initial_opacity=0.0, final_opacity=1.0, reverse_delay=0, loop=True) + self.flicker_fade.start() + self.value_list = [] + if values != dict(): + self.value_list = list(values.keys()) + self.value_index = 0 + self.values = values + language = global_data.config["general"]["language"] + self.value_text = OutlinedText(self.values[self.value].get(language, self.values[self.value]["en"]), int(30 * tex.screen_scale), ray.WHITE) + else: + self.string = self.value + self.value_text = OutlinedText(self.value, int(30 * tex.screen_scale), ray.WHITE) + + def update(self, current_time): + self.flicker_fade.update(current_time) + if self.is_highlighted and self.value_list == []: + if ray.is_key_pressed(ray.KeyboardKey.KEY_BACKSPACE): + self.string = self.string[:-1] + self.value_text = OutlinedText(self.string, int(30 * tex.screen_scale), ray.WHITE) + elif ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER): + self.value = self.string + self.is_highlighted = False + key = ray.get_char_pressed() + if key > 0: + self.string += chr(key) + self.value_text = OutlinedText(self.string, int(30 * tex.screen_scale), ray.WHITE) + + def move_left(self): + if self.value_list: + self.value_index = max(self.value_index - 1, 0) + self.value = self.value_list[self.value_index] + self.value_text = OutlinedText(self.values[self.value][global_data.config["general"]["language"]], int(30 * tex.screen_scale), ray.WHITE) + + def move_right(self): + if self.value_list: + self.value_index = min(self.value_index + 1, len(self.value_list) - 1) + self.value = self.value_list[self.value_index] + self.value_text = OutlinedText(self.values[self.value][global_data.config["general"]["language"]], int(30 * tex.screen_scale), ray.WHITE) + + def draw(self): + super().draw() + tex.draw_texture('option', 'button_off', index=2) + if self.is_highlighted: + tex.draw_texture('option', 'button_on', index=2, fade=self.flicker_fade.attribute) + text_x = tex.textures["option"]["button_on"].x[2] + (tex.textures["option"]["button_on"].width//2) - (self.value_text.texture.width//2) + text_y = tex.textures["option"]["button_on"].y[2] + (tex.textures["option"]["button_on"].height//2) - (self.value_text.texture.height//2) + self.value_text.draw(outline_color=ray.BLACK, x=text_x, y=text_y) + +class KeybindOptionBox(BaseOptionBox): + def __init__(self, name: str, description: str, path: str, values: dict): + super().__init__(name, description, path, values) + if isinstance(self.value, list): + text = ', '.join([get_key_string(key) for key in self.value]) + else: + text = get_key_string(self.value) + self.value_text = OutlinedText(text, int(30 * tex.screen_scale), ray.WHITE) + self.flicker_fade = Animation.create_fade(400, initial_opacity=0.0, final_opacity=1.0, reverse_delay=0, loop=True) + self.flicker_fade.start() + + def update(self, current_time): + self.flicker_fade.update(current_time) + if self.is_highlighted: + key = ray.get_key_pressed() + if key > 0: + self.value = key + audio.play_sound('don', 'sound') + self.value_text = OutlinedText(get_key_string(self.value), int(30 * tex.screen_scale), ray.WHITE) + self.is_highlighted = False + + def draw(self): + super().draw() + tex.draw_texture('option', 'button_off', index=2) + if self.is_highlighted: + tex.draw_texture('option', 'button_on', index=2, fade=self.flicker_fade.attribute) + text_x = tex.textures["option"]["button_on"].x[2] + (tex.textures["option"]["button_on"].width//2) - (self.value_text.texture.width//2) + text_y = tex.textures["option"]["button_on"].y[2] + (tex.textures["option"]["button_on"].height//2) - (self.value_text.texture.height//2) + self.value_text.draw(outline_color=ray.BLACK, x=text_x, y=text_y) + +class KeyBindControllerOptionBox(BaseOptionBox): + def __init__(self, name: str, description: str, path: str, values: dict): + super().__init__(name, description, path, values) + if isinstance(self.value, list): + text = ', '.join([str(key) for key in self.value]) + else: + text = str(self.value) + self.value_text = OutlinedText(text, int(30 * tex.screen_scale), ray.WHITE) + self.flicker_fade = Animation.create_fade(400, initial_opacity=0.0, final_opacity=1.0, reverse_delay=0, loop=True) + self.flicker_fade.start() + + def update(self, current_time): + self.flicker_fade.update(current_time) + if self.is_highlighted: + key = ray.get_gamepad_button_pressed() + if key > 0: + self.value = key + audio.play_sound('don', 'sound') + self.value_text = OutlinedText(str(self.value), int(30 * tex.screen_scale), ray.WHITE) + self.is_highlighted = False + + def draw(self): + super().draw() + tex.draw_texture('option', 'button_off', index=2) + if self.is_highlighted: + tex.draw_texture('option', 'button_on', index=2, fade=self.flicker_fade.attribute) + text_x = tex.textures["option"]["button_on"].x[2] + (tex.textures["option"]["button_on"].width//2) - (self.value_text.texture.width//2) + text_y = tex.textures["option"]["button_on"].y[2] + (tex.textures["option"]["button_on"].height//2) - (self.value_text.texture.height//2) + self.value_text.draw(outline_color=ray.BLACK, x=text_x, y=text_y) + +class FloatOptionBox(BaseOptionBox): + def __init__(self, name: str, description: str, path: str, values: dict): + super().__init__(name, description, path, values) + self.value_text = OutlinedText(str(int(self.value*100)) + "%", int(30 * tex.screen_scale), ray.WHITE) + self.flicker_fade = Animation.create_fade(400, initial_opacity=0.0, final_opacity=1.0, reverse_delay=0, loop=True) + self.flicker_fade.start() + + def update(self, current_time): + self.flicker_fade.update(current_time) + + def move_left(self): + self.value = ((self.value*100) - 1) / 100 + self.value_text = OutlinedText(str(int(self.value*100))+"%", int(30 * tex.screen_scale), ray.WHITE) + + def move_right(self): + self.value = ((self.value*100) + 1) / 100 + self.value_text = OutlinedText(str(int(self.value*100))+"%", int(30 * tex.screen_scale), ray.WHITE) + + def draw(self): + super().draw() + tex.draw_texture('option', 'button_off', index=2) + if self.is_highlighted: + tex.draw_texture('option', 'button_on', index=2, fade=self.flicker_fade.attribute) + text_x = tex.textures["option"]["button_on"].x[2] + (tex.textures["option"]["button_on"].width//2) - (self.value_text.texture.width//2) + text_y = tex.textures["option"]["button_on"].y[2] + (tex.textures["option"]["button_on"].height//2) - (self.value_text.texture.height//2) + self.value_text.draw(outline_color=ray.BLACK, x=text_x, y=text_y) class Box: """Box class for the entry screen""" - def __init__(self, text: OutlinedText, box_options: dict): - self.text = text + OPTION_BOX_MAP = { + "int": IntOptionBox, + "bool": BoolOptionBox, + "string": StrOptionBox, + "keybind": KeybindOptionBox, + "keybind_controller": KeyBindControllerOptionBox, + "float": FloatOptionBox + } + def __init__(self, name: str, text: str, box_options: dict): + self.name = name + self.text = OutlinedText(text, tex.skin_config["entry_box_text"].font_size - int(5*tex.screen_scale), ray.WHITE, outline_thickness=5) self.x = 10 * tex.screen_scale self.y = -50 * tex.screen_scale self.move = tex.get_animation(0) + self.blue_arrow_fade = tex.get_animation(1) + self.blue_arrow_move = tex.get_animation(2) self.is_selected = False + self.in_box = False self.outline_color = ray.Color(109, 68, 24, 255) self.direction = 1 self.target_position = float('inf') self.start_position = self.y + self.option_index = 0 language = global_data.config["general"]["language"] - self.options = [BaseOptionBox(box_options[option]["name"][language], box_options[option]["description"][language]) for option in box_options] + self.options = [Box.OPTION_BOX_MAP[ + box_options[option]["type"]](box_options[option]["name"].get(language, box_options[option]["name"]["en"]), + box_options[option]["description"].get(language, box_options[option]["description"]["en"]), box_options[option]["path"], + box_options[option]["values"]) for option in box_options] def __repr__(self): return str(self.__dict__) @@ -81,13 +329,41 @@ class Box: return True + def move_option_left(self): + if self.options[self.option_index].is_highlighted: + self.options[self.option_index].move_left() + return True + else: + if self.option_index == 0: + self.in_box = False + return False + self.option_index -= 1 + return True + + def move_option_right(self): + if self.options[self.option_index].is_highlighted: + self.options[self.option_index].move_right() + else: + self.option_index = min(self.option_index + 1, len(self.options) - 1) + + def select_option(self): + self.options[self.option_index].is_highlighted = not self.options[self.option_index].is_highlighted + self.options[self.option_index].confirm() + + def select(self): + self.in_box = True + def update(self, current_time_ms: float, is_selected: bool): self.move.update(current_time_ms) + self.blue_arrow_fade.update(current_time_ms) + self.blue_arrow_move.update(current_time_ms) self.is_selected = is_selected if self.move.is_finished: self.y = self.target_position else: self.y = self.start_position + (self.move.attribute * self.direction) + for option in self.options: + option.update(current_time_ms) def _draw_highlighted(self): tex.draw_texture('box', 'box_highlight', x=self.x, y=self.y) @@ -98,22 +374,31 @@ class Box: if self.is_selected: self.text.draw(outline_color=ray.BLACK, x=text_x, y=text_y) else: - self.text.draw(outline_color=self.outline_color, x=text_x, y=text_y) + if self.name == 'exit': + self.text.draw(outline_color=ray.RED, x=text_x, y=text_y) + else: + self.text.draw(outline_color=self.outline_color, x=text_x, y=text_y) def draw(self): tex.draw_texture('box', 'box', x=self.x, y=self.y) if self.is_selected: self._draw_highlighted() + if self.in_box: + self.options[self.option_index].draw() + if not self.options[self.option_index].is_highlighted: + tex.draw_texture('background', 'blue_arrow', index=0, x=-self.blue_arrow_move.attribute, fade=self.blue_arrow_fade.attribute) + if self.option_index != len(self.options) - 1: + tex.draw_texture('background', 'blue_arrow', index=1, x=self.blue_arrow_move.attribute, fade=self.blue_arrow_fade.attribute, mirror='horizontal') self._draw_text() class BoxManager: """BoxManager class for the entry screen""" def __init__(self, settings_template: dict): language = global_data.config["general"]["language"] - self.boxes = [Box(OutlinedText(settings_template[config_name]["name"][language], tex.skin_config["entry_box_text"].font_size - int(5*tex.screen_scale), ray.WHITE, outline_thickness=5), settings_template[config_name]["options"]) for config_name in settings_template] + self.boxes = [Box(config_name, settings_template[config_name]["name"].get(language, settings_template[config_name]["name"]["en"]), settings_template[config_name]["options"]) for config_name in settings_template] self.num_boxes = len(self.boxes) self.selected_box_index = 3 - self.is_2p = False + self.box_selected = False for i, box in enumerate(self.boxes): box.y += 100*i @@ -121,27 +406,45 @@ class BoxManager: def move_left(self): """Move the cursor to the left""" - moved = True - for box in self.boxes: - if not box.move_left(): - moved = False + if self.box_selected: + box = self.boxes[self.selected_box_index] + self.box_selected = box.move_option_left() + else: + moved = True + for box in self.boxes: + if not box.move_left(): + moved = False - if moved: - self.selected_box_index = (self.selected_box_index - 1) % self.num_boxes + if moved: + self.selected_box_index = (self.selected_box_index - 1) % self.num_boxes def move_right(self): """Move the cursor to the right""" - moved = True - for box in self.boxes: - if not box.move_right(): - moved = False + if self.box_selected: + box = self.boxes[self.selected_box_index] + box.move_option_right() + else: + moved = True + for box in self.boxes: + if not box.move_right(): + moved = False - if moved: - self.selected_box_index = (self.selected_box_index + 1) % self.num_boxes + if moved: + self.selected_box_index = (self.selected_box_index + 1) % self.num_boxes + + def select_box(self): + if self.boxes[self.selected_box_index].name == "exit": + return "exit" + if self.box_selected: + box = self.boxes[self.selected_box_index] + box.select_option() + else: + self.box_selected = True + self.boxes[self.selected_box_index].in_box = True def update(self, current_time_ms: float): for i, box in enumerate(self.boxes): - is_selected = i == self.selected_box_index + is_selected = i == self.selected_box_index and not self.box_selected box.update(current_time_ms, is_selected) def draw(self): @@ -151,17 +454,21 @@ class BoxManager: class SettingsScreen(Screen): def on_screen_start(self): super().on_screen_start() - self.config = global_data.config self.indicator = Indicator(Indicator.State.SELECT) self.template = json.loads((tex.graphics_path / "settings_template.json").read_text(encoding='utf-8')) self.box_manager = BoxManager(self.template) + audio.play_sound('bgm', 'music') def on_screen_end(self, next_screen: str): - save_config(self.config) - global_data.config = self.config + save_config(global_data.config) + audio.close_audio_device() + audio.device_type = global_data.config["audio"]["device_type"] + audio.target_sample_rate = global_data.config["audio"]["sample_rate"] + audio.buffer_size = global_data.config["audio"]["buffer_size"] + audio.volume_presets = global_data.config["volume"] audio.init_audio_device() logger.info("Settings saved and audio device re-initialized") - return next_screen + return super().on_screen_end(next_screen) def handle_input(self): if is_l_kat_pressed(): @@ -170,19 +477,22 @@ class SettingsScreen(Screen): elif is_r_kat_pressed(): audio.play_sound('kat', 'sound') self.box_manager.move_right() + elif is_l_don_pressed() or is_r_don_pressed(): + audio.play_sound('don', 'sound') + box_name = self.box_manager.select_box() + if box_name == 'exit': + return self.on_screen_end("ENTRY") def update(self): super().update() - self.handle_input() - current_time = get_current_ms() self.indicator.update(current_time) self.box_manager.update(current_time) + return self.handle_input() def draw(self): tex.draw_texture('background', 'background') self.box_manager.draw() tex.draw_texture('background', 'footer') self.indicator.draw(tex.skin_config['song_select_indicator'].x, tex.skin_config['song_select_indicator'].y) - tex.draw_texture('background', 'overlay', scale=0.70) From 1d901b22bf732bba77967c4be6e179d747527696 Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Sat, 3 Jan 2026 16:30:57 -0500 Subject: [PATCH 3/8] Update settings.py --- scenes/settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scenes/settings.py b/scenes/settings.py index aa9a512..5d4fc3f 100644 --- a/scenes/settings.py +++ b/scenes/settings.py @@ -6,7 +6,7 @@ import pyray as ray from libs.animation import Animation from libs.audio import audio from libs.config import get_key_string, save_config -from libs.global_objects import Indicator +from libs.global_objects import AllNetIcon, CoinOverlay, Indicator from libs.screen import Screen from libs.texture import tex from libs.utils import ( @@ -457,6 +457,8 @@ class SettingsScreen(Screen): self.indicator = Indicator(Indicator.State.SELECT) self.template = json.loads((tex.graphics_path / "settings_template.json").read_text(encoding='utf-8')) self.box_manager = BoxManager(self.template) + self.coin_overlay = CoinOverlay() + self.allnet_indicator = AllNetIcon() audio.play_sound('bgm', 'music') def on_screen_end(self, next_screen: str): @@ -496,3 +498,5 @@ class SettingsScreen(Screen): self.box_manager.draw() tex.draw_texture('background', 'footer') self.indicator.draw(tex.skin_config['song_select_indicator'].x, tex.skin_config['song_select_indicator'].y) + self.coin_overlay.draw() + self.allnet_indicator.draw() From f41ffd71d439cfaf005fb9aa3ce57876499edf1f Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Sat, 3 Jan 2026 16:44:29 -0500 Subject: [PATCH 4/8] Update settings.py --- scenes/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scenes/settings.py b/scenes/settings.py index 5d4fc3f..8ebd6d0 100644 --- a/scenes/settings.py +++ b/scenes/settings.py @@ -163,13 +163,13 @@ class StrOptionBox(BaseOptionBox): if self.value_list: self.value_index = max(self.value_index - 1, 0) self.value = self.value_list[self.value_index] - self.value_text = OutlinedText(self.values[self.value][global_data.config["general"]["language"]], int(30 * tex.screen_scale), ray.WHITE) + self.value_text = OutlinedText(self.values[self.value].get(global_data.config["general"]["language"], self.values[self.value]["en"]), int(30 * tex.screen_scale), ray.WHITE) def move_right(self): if self.value_list: self.value_index = min(self.value_index + 1, len(self.value_list) - 1) self.value = self.value_list[self.value_index] - self.value_text = OutlinedText(self.values[self.value][global_data.config["general"]["language"]], int(30 * tex.screen_scale), ray.WHITE) + self.value_text = OutlinedText(self.values[self.value].get(global_data.config["general"]["language"], self.values[self.value]["en"]), int(30 * tex.screen_scale), ray.WHITE) def draw(self): super().draw() From bcdb4859097190e0a14bcd7397334303eba47d5b Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Sat, 3 Jan 2026 16:45:57 -0500 Subject: [PATCH 5/8] Update settings.py --- scenes/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scenes/settings.py b/scenes/settings.py index 8ebd6d0..08ed188 100644 --- a/scenes/settings.py +++ b/scenes/settings.py @@ -106,7 +106,7 @@ class IntOptionBox(BaseOptionBox): if self.value_list: self.value_index = max(self.value_index - 1, 0) self.value = int(self.value_list[self.value_index]) - self.value_text = OutlinedText(self.values[str(self.value)][global_data.config["general"]["language"]], int(30 * tex.screen_scale), ray.WHITE) + self.value_text = OutlinedText(self.values[str(self.value)].get(global_data.config["general"]["language"], self.values[str(self.value)]["en"]), int(30 * tex.screen_scale), ray.WHITE) else: self.value -= 1 self.value_text = OutlinedText(str(self.value), int(30 * tex.screen_scale), ray.WHITE) @@ -115,7 +115,7 @@ class IntOptionBox(BaseOptionBox): if self.value_list: self.value_index = min(self.value_index + 1, len(self.value_list) - 1) self.value = int(self.value_list[self.value_index]) - self.value_text = OutlinedText(self.values[str(self.value)][global_data.config["general"]["language"]], int(30 * tex.screen_scale), ray.WHITE) + self.value_text = OutlinedText(self.values[str(self.value)].get(global_data.config["general"]["language"], self.values[str(self.value)]["en"]), int(30 * tex.screen_scale), ray.WHITE) else: self.value += 1 self.value_text = OutlinedText(str(self.value), int(30 * tex.screen_scale), ray.WHITE) From 0236c8676c0655cba52820d26d728af4eaebf92d Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Sat, 3 Jan 2026 19:14:11 -0500 Subject: [PATCH 6/8] Update PyTaiko.py --- PyTaiko.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/PyTaiko.py b/PyTaiko.py index 4302def..9a12a16 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -286,14 +286,15 @@ def check_discord_heartbeat(current_screen): def draw_fps(last_fps: int): curr_fps = ray.GetFPS() + pos = 20 * global_tex.screen_scale if curr_fps != 0 and curr_fps != last_fps: last_fps = curr_fps if last_fps < 30: - ray.DrawText(f'{last_fps} FPS'.encode('utf-8'), 20, 20, 20, ray.RED) + pyray.draw_text_ex(global_data.font, f'{last_fps} FPS', (pos, pos), pos, 1, pyray.RED) elif last_fps < 60: - ray.DrawText(f'{last_fps} FPS'.encode('utf-8'), 20, 20, 20, ray.YELLOW) + pyray.draw_text_ex(global_data.font, f'{last_fps} FPS', (pos, pos), pos, 1, pyray.YELLOW) else: - ray.DrawText(f'{last_fps} FPS'.encode('utf-8'), 20, 20, 20, ray.LIME) + pyray.draw_text_ex(global_data.font, f'{last_fps} FPS', (pos, pos), pos, 1, pyray.LIME) def draw_outer_border(screen_width: int, screen_height: int, last_color: pyray.Color): pyray.draw_rectangle(-screen_width, 0, screen_width, screen_height, last_color) From 8a595aebb75923af89ccef236eeeae7609f65548 Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Sat, 3 Jan 2026 19:14:15 -0500 Subject: [PATCH 7/8] Update PyTaikoGreen --- Skins/PyTaikoGreen | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Skins/PyTaikoGreen b/Skins/PyTaikoGreen index d870ca2..1829a25 160000 --- a/Skins/PyTaikoGreen +++ b/Skins/PyTaikoGreen @@ -1 +1 @@ -Subproject commit d870ca298076cb84a86cfcf9b64d93d99f4733fe +Subproject commit 1829a2538b2482593c6b7181dbbc22eda9b9b3a4 From 183eb75bbafaa3ce2199470ed452c92ad7ee1e9e Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Sun, 4 Jan 2026 10:54:30 -0500 Subject: [PATCH 8/8] minor bug fix --- config.toml | 2 +- scenes/game.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.toml b/config.toml index c28f1d4..23284dc 100644 --- a/config.toml +++ b/config.toml @@ -2,7 +2,7 @@ fps_counter = false audio_offset = 0 visual_offset = 0 -language = "ja" +language = "en" timer_frozen = true judge_counter = false nijiiro_notes = false diff --git a/scenes/game.py b/scenes/game.py index c4525af..b6bacb0 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -171,7 +171,7 @@ class GameScreen(Screen): existing_score = result[0] if result is not None else None existing_crown = result[1] if result is not None and len(result) > 1 and result[1] is not None else 0 crown = Crown.NONE - if session_data.result_data.bad and session_data.result_data.ok == 0: + if session_data.result_data.bad == 0 and session_data.result_data.ok == 0: crown = Crown.DFC elif session_data.result_data.bad == 0: crown = Crown.FC