From 6c8466de8e5eb247ab97106476c524c2d2481788 Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Thu, 30 Oct 2025 23:09:16 -0400 Subject: [PATCH] add the majority of dan mode --- PyTaiko.py | 4 + libs/file_navigator.py | 67 +++++-- libs/global_data.py | 10 +- libs/transition.py | 8 +- scenes/dan_select.py | 87 ++++++++- scenes/game.py | 104 ++++++----- scenes/game_dan.py | 303 ++++++++++++++++++++++++++++++- scenes/song_select.py | 7 +- scenes/two_player/song_select.py | 4 +- 9 files changed, 508 insertions(+), 86 deletions(-) diff --git a/PyTaiko.py b/PyTaiko.py index cf76c7e..b852841 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -21,6 +21,7 @@ from libs.utils import ( from scenes.devtest import DevScreen from scenes.entry import EntryScreen from scenes.game import GameScreen +from scenes.game_dan import DanGameScreen from scenes.two_player.game import TwoPlayerGameScreen from scenes.two_player.result import TwoPlayerResultScreen from scenes.loading import LoadScreen @@ -44,6 +45,7 @@ class Screens: RESULT_2P = "RESULT_2P" SONG_SELECT_2P = "SONG_SELECT_2P" DAN_SELECT = "DAN_SELECT" + GAME_DAN = "GAME_DAN" SETTINGS = "SETTINGS" DEV_MENU = "DEV_MENU" LOADING = "LOADING" @@ -159,6 +161,7 @@ def main(): settings_screen = SettingsScreen('settings') dev_screen = DevScreen('dev') dan_select_screen = DanSelectScreen('dan_select') + game_screen_dan = DanGameScreen('game_dan') screen_mapping = { Screens.ENTRY: entry_screen, @@ -172,6 +175,7 @@ def main(): Screens.SETTINGS: settings_screen, Screens.DEV_MENU: dev_screen, Screens.DAN_SELECT: dan_select_screen, + Screens.GAME_DAN: game_screen_dan, Screens.LOADING: load_screen } target = ray.load_render_texture(screen_width, screen_height) diff --git a/libs/file_navigator.py b/libs/file_navigator.py index 0440e36..a13fcc2 100644 --- a/libs/file_navigator.py +++ b/libs/file_navigator.py @@ -504,13 +504,13 @@ class DanBox: self.total_notes = 0 for song, genre_index, difficulty in self.songs: notes, branch_m, branch_e, branch_n = song.notes_to_position(difficulty) - self.total_notes += len(notes.play_notes) + self.total_notes += sum(1 for note in notes.play_notes if note.type < 5) for branch in branch_m: - self.total_notes += len(branch.play_notes) + self.total_notes += sum(1 for note in branch.play_notes if note.type < 5) for branch in branch_e: - self.total_notes += len(branch.play_notes) + self.total_notes += sum(1 for note in branch.play_notes if note.type < 5) for branch in branch_n: - self.total_notes += len(branch.play_notes) + self.total_notes += sum(1 for note in branch.play_notes if note.type < 5) self.name = None self.hori_name = None self.yellow_box = None @@ -567,6 +567,36 @@ class DanBox: if self.yellow_box is not None: self.yellow_box.update(True) + def _draw_exam_box(self): + tex.draw_texture('yellow_box', 'exam_box_bottom_right') + tex.draw_texture('yellow_box', 'exam_box_bottom_left') + tex.draw_texture('yellow_box', 'exam_box_top_right') + tex.draw_texture('yellow_box', 'exam_box_top_left') + tex.draw_texture('yellow_box', 'exam_box_bottom') + tex.draw_texture('yellow_box', 'exam_box_right') + tex.draw_texture('yellow_box', 'exam_box_left') + tex.draw_texture('yellow_box', 'exam_box_top') + tex.draw_texture('yellow_box', 'exam_box_center') + tex.draw_texture('yellow_box', 'exam_header') + + for i, exam in enumerate(self.exams): + tex.draw_texture('yellow_box', 'judge_box', y=(i*83)) + tex.draw_texture('yellow_box', 'exam_' + self.exams[i].type, y=(i*83)) + counter = str(self.exams[i].red) + margin = 20 + if self.exams[i].type == 'gauge': + tex.draw_texture('yellow_box', 'exam_percent', y=(i*83)) + offset = -8 + else: + offset = 0 + for j in range(len(counter)): + tex.draw_texture('yellow_box', 'judge_num', frame=int(counter[j]), x=offset-(len(counter) - j) * margin, y=(i*83)) + + if self.exams[i].range == 'more': + tex.draw_texture('yellow_box', 'exam_more', x=(offset*-1.7), y=(i*83)) + elif self.exams[i].range == 'less': + tex.draw_texture('yellow_box', 'exam_less', x=(offset*-1.7), y=(i*83)) + def _draw_closed(self, x: int, y: int): tex.draw_texture('box', 'folder', frame=self.color, x=x) if self.name is not None: @@ -601,6 +631,8 @@ class DanBox: if self.hori_name is not None: self.hori_name.draw(outline_color=ray.BLACK, x=434 - (self.hori_name.texture.width//2), y=84, x2=min(self.hori_name.texture.width, 275)-self.hori_name.texture.width) + self._draw_exam_box() + def draw(self, x: int, y: int, is_ura: bool): if self.is_open: self._draw_open(x, y, is_ura) @@ -858,27 +890,27 @@ class DanCourse(FileSystemItem): self.logging.error(f"Invalid dan course file: {path}") with open(path, 'r') as f: data = json.load(f) - title = data["title"] - color = data["color"] - songs = [] - for song in data["songs"]: - hash = song["hash"] - song_title = song["title"] - song_subtitle = song["subtitle"] - difficulty = song["difficulty"] + self.title = data["title"] + self.color = data["color"] + self.charts = [] + for chart in data["charts"]: + hash = chart["hash"] + chart_title = chart["title"] + chart_subtitle = chart["subtitle"] + difficulty = chart["difficulty"] if hash in global_data.song_hashes: path = Path(global_data.song_hashes[hash][0]["file_path"]) if (path.parent.parent / "box.def").exists(): _, genre_index, _ = parse_box_def(path.parent.parent) - songs.append((TJAParser(path), genre_index, difficulty)) + self.charts.append((TJAParser(path), genre_index, difficulty)) else: pass #do something with song_title, song_subtitle - exams = [] + self.exams = [] for exam in data["exams"]: - exams.append(Exam(exam["type"], exam["red"], exam["gold"], exam["range"])) + self.exams.append(Exam(exam["type"], exam["value"][0], exam["value"][1], exam["range"])) - self.box = DanBox(title, color, songs, exams) + self.box = DanBox(self.title, self.color, self.charts, self.exams) class FileNavigator: """Manages navigation through pre-generated Directory and SongFile objects""" @@ -1251,6 +1283,7 @@ class FileNavigator: # Save current state to history self.history.append((self.current_dir, self.selected_index)) self.current_dir = selected_item.path + logger.info(f"Entered Directory {selected_item.path}") self.load_current_directory(selected_item=selected_item) @@ -1459,12 +1492,14 @@ class FileNavigator: if self.items: self.selected_index = (self.selected_index - 1) % len(self.items) self.calculate_box_positions() + logger.info(f"Moved Left to {self.items[self.selected_index].path}") def navigate_right(self): """Move selection right with wrap-around""" if self.items: self.selected_index = (self.selected_index + 1) % len(self.items) self.calculate_box_positions() + logger.info(f"Moved Right to {self.items[self.selected_index].path}") def get_current_item(self): """Get the currently selected item""" diff --git a/libs/global_data.py b/libs/global_data.py index 14d1d03..a5d5a30 100644 --- a/libs/global_data.py +++ b/libs/global_data.py @@ -16,6 +16,10 @@ class Modifiers: @dataclass class SessionData: """Data class for storing session data. Wiped after the result screen. + selected_song (Path): The currently selected song. + selected_dan (list[tuple[Any, int, int]]): The currently selected dan songs (TJA). TJAParser, Genre Index, Difficulty + selected_dan_exam: list[Exam]: list of dan requirements, contains Exam objects + dan_color: int: The emblem color of the selected dan selected_difficulty: The difficulty level selected by the user. song_title: The title of the song being played. genre_index: The index of the genre being played. @@ -27,6 +31,10 @@ class SessionData: result_total_drumroll: The total drumroll achieved in the game. result_gauge_length: The length of the gauge achieved in the game. prev_score: The previous score pulled from the database.""" + selected_song: Path = Path() + selected_dan: list[tuple[Any, int, int]] = field(default_factory=lambda: []) + selected_dan_exam: list[Any] = field(default_factory=lambda: []) + dan_color: int = 0 selected_difficulty: int = 0 song_title: str = '' genre_index: int = 0 @@ -45,7 +53,6 @@ class GlobalData: Global data for the game. Should be accessed via the global_data variable. Attributes: - selected_song (Path): The currently selected song. songs_played (int): The number of songs played. config (dict): The configuration settings. song_hashes (dict[str, list[dict]]): A dictionary mapping song hashes to their metadata. @@ -58,7 +65,6 @@ class GlobalData: modifiers (list[Modifiers]): The modifiers for the game. session_data (list[SessionData]): Session data for both players. """ - selected_song: Path = Path() songs_played: int = 0 config: dict[str, Any] = field(default_factory=lambda: dict()) song_hashes: dict[str, list[dict]] = field(default_factory=lambda: dict()) #Hash to path diff --git a/libs/transition.py b/libs/transition.py index 2983e1b..8e36c0e 100644 --- a/libs/transition.py +++ b/libs/transition.py @@ -16,8 +16,12 @@ class Transition: self.chara_down = global_tex.get_animation(2) self.song_info_fade = global_tex.get_animation(3) self.song_info_fade_out = global_tex.get_animation(4) - self.title = OutlinedText(title, 40, ray.WHITE) - self.subtitle = OutlinedText(subtitle, 30, ray.WHITE) + if title == '' and subtitle == '': + self.title = '' + self.subtitle = '' + else: + self.title = OutlinedText(title, 40, ray.WHITE) + self.subtitle = OutlinedText(subtitle, 30, ray.WHITE) self.is_second = is_second def start(self): diff --git a/scenes/dan_select.py b/scenes/dan_select.py index 0c32742..3487b83 100644 --- a/scenes/dan_select.py +++ b/scenes/dan_select.py @@ -1,3 +1,5 @@ + +import logging import pyray as ray from libs.audio import audio @@ -6,10 +8,12 @@ from libs.texture import tex from libs.chara_2d import Chara2D from libs.global_objects import AllNetIcon, CoinOverlay, Indicator, Nameplate, Timer from libs.screen import Screen -from libs.file_navigator import navigator +from libs.file_navigator import DanCourse, navigator +from libs.transition import Transition from libs.utils import get_current_ms, is_l_don_pressed, is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed from scenes.song_select import SongSelectScreen, State +logger = logging.getLogger(__name__) class DanSelectScreen(Screen): def on_screen_start(self): @@ -23,9 +27,21 @@ class DanSelectScreen(Screen): self.indicator = Indicator(Indicator.State.SELECT) self.player = DanSelectPlayer(str(global_data.player_num)) self.state = State.BROWSING + self.transition = Transition('', '') self.last_moved = 0 def on_screen_end(self, next_screen: str): + session_data = global_data.session_data[global_data.player_num-1] + current_item = self.navigator.get_current_item() + if isinstance(current_item, DanCourse): + session_data.selected_song = current_item.charts[0] + session_data.selected_dan = current_item.charts + session_data.selected_dan_exam = current_item.exams + session_data.song_title = current_item.title + session_data.dan_color = current_item.color + else: + self.navigator.in_dan_select = False + self.navigator.go_back() return super().on_screen_end(next_screen) def handle_input_browsing(self): @@ -47,27 +63,36 @@ class DanSelectScreen(Screen): self.navigator.navigate_right() self.last_moved = current_time elif action == "go_back": - self.navigator.go_back() + return action elif action == "select_song": - pass + self.state = State.SONG_SELECTED def handle_input(self, state, screen): """Main input dispatcher. Delegates to state-specific handlers.""" if state == State.BROWSING: - screen.handle_input_browsing() + return screen.handle_input_browsing() elif state == State.SONG_SELECTED: - screen.handle_input_selected() + res = self.player.handle_input_selected() + if res == 'confirm': + self.transition.start() + elif res == 'cancel': + self.state = State.BROWSING def update(self): super().update() current_time = get_current_ms() self.indicator.update(current_time) self.timer.update(current_time) + self.transition.update(current_time) + if self.transition.is_finished: + return self.on_screen_end("GAME_DAN") for song in self.navigator.items: song.box.update(False) song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150 self.player.update(current_time) - self.handle_input(self.state, self) + res = self.handle_input(self.state, self) + if res == 'go_back': + return self.on_screen_end("SONG_SELECT") def draw(self): tex.draw_texture('global', 'bg') @@ -81,11 +106,14 @@ class DanSelectScreen(Screen): box.draw(box.position, 95, False) else: box.draw(box.position, 95, False) + if self.state == State.SONG_SELECTED: + ray.draw_rectangle(0, 0, 1280, 720, ray.fade(ray.BLACK, min(0.5, self.player.confirmation_window.fade_in.attribute))) self.player.draw() self.indicator.draw(410, 575) self.timer.draw() self.coin_overlay.draw() tex.draw_texture('global', 'dan_select') + self.transition.draw() self.allnet_indicator.draw() class DanSelectPlayer: @@ -95,10 +123,12 @@ class DanSelectPlayer: self.prev_diff = -3 self.selected_song = False self.is_ura = False + self.is_confirmed = False self.ura_toggle = 0 self.diff_select_move_right = False self.neiro_selector = None self.modifier_selector = None + self.confirmation_window = ConfirmationWindow() # Player-specific objects self.chara = Chara2D(int(self.player_num) - 1, 100) @@ -110,6 +140,7 @@ class DanSelectPlayer: """Update player state""" self.nameplate.update(current_time) self.chara.update(current_time, 100, False, False) + self.confirmation_window.update(current_time, self.is_confirmed) def handle_input_browsing(self, last_moved, selected_item): """Handle input for browsing songs. Returns action string or None.""" @@ -141,6 +172,7 @@ class DanSelectPlayer: audio.play_sound('cancel', 'sound') return "go_back" else: + self.confirmation_window.start() return "select_song" return None @@ -153,12 +185,24 @@ class DanSelectPlayer: if state == State.BROWSING: screen.handle_input_browsing() elif state == State.SONG_SELECTED: - screen.handle_input_selected() + res = screen.handle_input_selected() - def handle_input_selected(self, current_item): + if res: + return res + + def handle_input_selected(self): """Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None""" + if is_l_kat_pressed(self.player_num): + self.is_confirmed = False + if is_r_kat_pressed(self.player_num): + self.is_confirmed = True + if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num): - return "confirm" + if self.is_confirmed: + return "confirm" + else: + self.confirmation_window = ConfirmationWindow() + return "cancel" return None def draw(self): @@ -168,3 +212,28 @@ class DanSelectPlayer: else: self.nameplate.draw(950, 640) self.chara.draw(mirror=True, x=950, y=410) + + self.confirmation_window.draw() + +class ConfirmationWindow: + def __init__(self): + self.fade_in = tex.get_animation(8, is_copy=True) + self.side = 0 + + def start(self): + self.fade_in.start() + + def update(self, current_time_ms: float, is_confirmed: bool): + self.fade_in.update(current_time_ms) + self.side = is_confirmed + + def draw(self): + tex.draw_texture('confirm_box', 'bg', fade=self.fade_in.attribute) + tex.draw_texture('confirm_box', 'confirmation_text', fade=self.fade_in.attribute) + for i in range(2): + tex.draw_texture('confirm_box', 'selection_box', index=i, fade=self.fade_in.attribute) + + tex.draw_texture('confirm_box', 'selection_box_highlight', index=self.side, fade=self.fade_in.attribute) + tex.draw_texture('confirm_box', 'selection_box_outline', index=self.side, fade=self.fade_in.attribute) + tex.draw_texture('confirm_box', 'yes', fade=self.fade_in.attribute) + tex.draw_texture('confirm_box', 'no', fade=self.fade_in.attribute) diff --git a/scenes/game.py b/scenes/game.py index c11ca68..96c0db0 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -66,8 +66,8 @@ class GameScreen(Screen): ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture0"), tex.textures['balloon']['rainbow_mask'].texture) ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture1"), tex.textures['balloon']['rainbow'].texture) session_data = global_data.session_data[global_data.player_num-1] - self.init_tja(global_data.selected_song) - logger.info(f"TJA initialized for song: {global_data.selected_song}") + self.init_tja(session_data.selected_song) + logger.info(f"TJA initialized for song: {session_data.selected_song}") self.load_hitsounds() self.song_info = SongInfo(session_data.song_title, session_data.genre_index) self.result_transition = ResultTransition(global_data.player_num) @@ -292,53 +292,14 @@ class Player: def __init__(self, tja: TJAParser, player_number: int, difficulty: int, is_2p: bool, modifiers: Modifiers): self.is_2p = is_2p + self.is_dan = False self.player_number = str(player_number) self.difficulty = difficulty self.visual_offset = global_data.config["general"]["visual_offset"] self.modifiers = modifiers + self.tja = tja - notes, self.branch_m, self.branch_e, self.branch_n = tja.notes_to_position(self.difficulty) - self.play_notes, self.draw_note_list, self.draw_bar_list = apply_modifiers(notes, self.modifiers) - self.end_time = 0 - if self.play_notes: - self.end_time = self.play_notes[-1].hit_ms - if self.branch_m: - for section in self.branch_m: - if section.play_notes: - self.end_time = max(self.end_time, section.play_notes[-1].hit_ms) - if self.branch_e: - for section in self.branch_e: - if section.play_notes: - self.end_time = max(self.end_time, section.play_notes[-1].hit_ms) - if self.branch_n: - for section in self.branch_n: - if section.play_notes: - self.end_time = max(self.end_time, section.play_notes[-1].hit_ms) - - self.don_notes = deque([note for note in self.play_notes if note.type in {1, 3}]) - self.kat_notes = deque([note for note in self.play_notes if note.type in {2, 4}]) - self.other_notes = deque([note for note in self.play_notes if note.type not in {1, 2, 3, 4}]) - self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5]) - total_notes = notes - if self.branch_m: - for section in self.branch_m: - self.total_notes += len([note for note in section.play_notes if 0 < note.type < 5]) - total_notes += section - self.base_score = calculate_base_score(total_notes) - - #Note management - self.current_bars: list[Note] = [] - self.current_notes_draw: list[Note | Drumroll | Balloon] = [] - self.is_drumroll = False - self.curr_drumroll_count = 0 - self.is_balloon = False - self.curr_balloon_count = 0 - self.is_branch = False - self.curr_branch_reqs = [] - self.branch_condition_count = 0 - self.branch_condition = '' - self.balloon_index = 0 - self.bpm = self.play_notes[0].bpm if self.play_notes else 120 + self.reset_chart() #Score management self.good_count = 0 @@ -382,6 +343,50 @@ class Player: self.autoplay_hit_side = 'L' self.last_subdivision = -1 + def reset_chart(self): + notes, self.branch_m, self.branch_e, self.branch_n = self.tja.notes_to_position(self.difficulty) + self.play_notes, self.draw_note_list, self.draw_bar_list = apply_modifiers(notes, self.modifiers) + self.end_time = 0 + if self.play_notes: + self.end_time = self.play_notes[-1].hit_ms + if self.branch_m: + for section in self.branch_m: + if section.play_notes: + self.end_time = max(self.end_time, section.play_notes[-1].hit_ms) + if self.branch_e: + for section in self.branch_e: + if section.play_notes: + self.end_time = max(self.end_time, section.play_notes[-1].hit_ms) + if self.branch_n: + for section in self.branch_n: + if section.play_notes: + self.end_time = max(self.end_time, section.play_notes[-1].hit_ms) + + self.don_notes = deque([note for note in self.play_notes if note.type in {1, 3}]) + self.kat_notes = deque([note for note in self.play_notes if note.type in {2, 4}]) + self.other_notes = deque([note for note in self.play_notes if note.type not in {1, 2, 3, 4}]) + self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5]) + total_notes = notes + if self.branch_m: + for section in self.branch_m: + self.total_notes += len([note for note in section.play_notes if 0 < note.type < 5]) + total_notes += section + self.base_score = calculate_base_score(total_notes) + + #Note management + self.current_bars: list[Note] = [] + self.current_notes_draw: list[Note | Drumroll | Balloon] = [] + self.is_drumroll = False + self.curr_drumroll_count = 0 + self.is_balloon = False + self.curr_balloon_count = 0 + self.is_branch = False + self.curr_branch_reqs = [] + self.branch_condition_count = 0 + self.branch_condition = '' + self.balloon_index = 0 + self.bpm = self.play_notes[0].bpm if self.play_notes else 120 + def merge_branch_section(self, branch_section: NoteList, current_ms: float): """Merges the branch notes into the current notes""" self.play_notes.extend(branch_section.play_notes) @@ -1043,7 +1048,7 @@ class Player: for modifier in modifiers_to_draw: tex.draw_texture('lane', modifier, index=self.is_2p) - def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader): + def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader, dan_transition = None): # Group 1: Background and lane elements tex.draw_texture('lane', 'lane_background', index=self.is_2p) if self.branch_indicator is not None: @@ -1062,9 +1067,13 @@ class Player: # Group 3: Notes and bars (game content) self.draw_bars(ms_from_start) self.draw_notes(ms_from_start, start_ms) + if dan_transition is not None: + dan_transition.draw() # Group 4: Lane covers and UI elements (batch similar textures) tex.draw_texture('lane', f'{self.player_number}p_lane_cover', index=self.is_2p) + if self.is_dan: + tex.draw_texture('lane', 'dan_lane_cover') tex.draw_texture('lane', 'drum', index=self.is_2p) if self.ending_anim is not None: self.ending_anim.draw() @@ -1085,7 +1094,10 @@ class Player: else: tex.draw_texture('lane', 'lane_score_cover', index=self.is_2p) tex.draw_texture('lane', f'{self.player_number}p_icon', index=self.is_2p) - tex.draw_texture('lane', 'lane_difficulty', frame=self.difficulty, index=self.is_2p) + if self.is_dan: + tex.draw_texture('lane', 'lane_difficulty', frame=6) + else: + tex.draw_texture('lane', 'lane_difficulty', frame=self.difficulty, index=self.is_2p) if self.judge_counter is not None: self.judge_counter.draw() diff --git a/scenes/game_dan.py b/scenes/game_dan.py index 3d0110e..3e444c3 100644 --- a/scenes/game_dan.py +++ b/scenes/game_dan.py @@ -1,28 +1,105 @@ +from typing import override +import pyray as ray import logging +from libs.animation import Animation from libs.audio import audio from libs.background import Background +from libs.file_navigator import Exam from libs.global_data import global_data +from libs.global_objects import AllNetIcon +from libs.tja import TJAParser from libs.transition import Transition -from scenes.game import GameScreen, SongInfo +from libs.utils import OutlinedText, get_current_ms +from libs.texture import tex +from scenes.game import GameScreen, ResultTransition, SongInfo logger = logging.getLogger(__name__) +SCREEN_WIDTH = 1280 +SCREEN_HEIGHT = 720 + class DanGameScreen(GameScreen): JUDGE_X = 414 + + @override def on_screen_start(self): - super().on_screen_start() - self.init_tja(global_data.selected_song) - logger.info(f"TJA initialized for song: {global_data.selected_song}") - self.song_info = SongInfo(session_data.song_title, session_data.genre_index) + self.mask_shader = ray.load_shader("shader/outline.vs", "shader/mask.fs") + self.current_ms = 0 + self.end_ms = 0 + self.start_delay = 4000 + self.song_started = False + self.song_music = None + self.song_index = 0 + tex.unload_textures() + tex.load_screen_textures('game') + audio.load_screen_sounds('game') + if global_data.config["general"]["nijiiro_notes"]: + # drop original + if "notes" in tex.textures: + del tex.textures["notes"] + # load nijiiro, rename "notes" + # to leave hardcoded 'notes' in calls below + tex.load_zip("game", "notes_nijiiro") + tex.textures["notes"] = tex.textures.pop("notes_nijiiro") + logger.info("Loaded nijiiro notes textures") + ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture0"), tex.textures['balloon']['rainbow_mask'].texture) + ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture1"), tex.textures['balloon']['rainbow'].texture) + session_data = global_data.session_data[global_data.player_num-1] + songs = session_data.selected_dan + self.exams = session_data.selected_dan_exam + self.total_notes = 0 + for song, genre_index, difficulty in songs: + notes, branch_m, branch_e, branch_n = song.notes_to_position(difficulty) + self.total_notes += sum(1 for note in notes.play_notes if note.type < 5) + for branch in branch_m: + self.total_notes += sum(1 for note in branch.play_notes if note.type < 5) + for branch in branch_e: + self.total_notes += sum(1 for note in branch.play_notes if note.type < 5) + for branch in branch_n: + self.total_notes += sum(1 for note in branch.play_notes if note.type < 5) + song, genre_index, difficulty = songs[self.song_index] + session_data.selected_difficulty = difficulty + self.hori_name = OutlinedText(session_data.song_title, 40, ray.WHITE) + self.init_tja(song.file_path) + self.color = session_data.dan_color + self.player_1.is_dan = True + self.player_1.gauge = DanGauge(str(global_data.player_num), self.total_notes) + logger.info(f"TJA initialized for song: {song.file_path}") + self.load_hitsounds() + self.song_info = SongInfo(song.metadata.title.get(global_data.config["general"]["language"], "en"), genre_index) + self.result_transition = ResultTransition(4) + self.bpm = self.tja.metadata.bpm self.background = Background(global_data.player_num, self.bpm, scene_preset='DAN') self.transition = Transition('', '', is_second=True) self.transition.start() + self.dan_transition = DanTransition() + self.dan_transition.start() + self.allnet_indicator = AllNetIcon() + + def change_song(self): + session_data = global_data.session_data[global_data.player_num-1] + songs = session_data.selected_dan + song, genre_index, difficulty = songs[self.song_index] + session_data.selected_difficulty = difficulty + self.player_1.difficulty = difficulty + self.tja = TJAParser(song.file_path, start_delay=self.start_delay, distance=SCREEN_WIDTH - GameScreen.JUDGE_X) + audio.unload_music_stream(self.song_music) + self.song_music = None + self.song_started = False + + if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None: + self.song_music = audio.load_music_stream(self.tja.metadata.wave, 'song') + self.player_1.tja = self.tja + self.player_1.reset_chart() + self.dan_transition.start() + self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) def update(self): - super().update() + super(GameScreen, self).update() current_time = get_current_ms() self.transition.update(current_time) self.current_ms = current_time - self.start_ms + self.dan_transition.update(current_time) self.start_song(current_time) self.update_background(current_time) @@ -31,4 +108,218 @@ class DanGameScreen(GameScreen): self.player_1.update(self.current_ms, current_time, self.background) self.song_info.update(current_time) + self.result_transition.update(current_time) + if self.result_transition.is_finished and not audio.is_sound_playing('result_transition'): + logger.info("Result transition finished, moving to RESULT screen") + return self.on_screen_end('RESULT') + elif self.current_ms >= self.player_1.end_time + 1000: + session_data = global_data.session_data[global_data.player_num-1] + if self.song_index == len(session_data.selected_dan) - 1: + if self.end_ms != 0: + if current_time >= self.end_ms + 1000: + if self.player_1.ending_anim is None: + self.spawn_ending_anims() + if current_time >= self.end_ms + 8533.34: + if not self.result_transition.is_started: + self.result_transition.start() + audio.play_sound('result_transition', 'voice') + logger.info("Result transition started and voice played") + else: + self.end_ms = current_time + else: + self.song_index += 1 + self.dan_transition.start() + self.change_song() + return self.global_keys() + + def draw_dan_info(self): + tex.draw_texture('dan_info', 'total_notes') + counter = str(self.total_notes - self.player_1.good_count - self.player_1.ok_count - self.player_1.bad_count) + self._draw_counter(counter, margin=45, texture='total_notes_counter') + + for i, exam in enumerate(self.exams): + y_offset = i * 94 + tex.draw_texture('dan_info', 'exam_bg', y=y_offset) + tex.draw_texture('dan_info', 'exam_overlay_1', y=y_offset) + + # Get progress based on exam type + progress = self._get_exam_progress(exam) / exam.red + if exam.range == 'less': + progress = 1 - progress + self._draw_progress_bar(progress, y_offset) + # Draw exam type and counter + counter = str(exam.red) + self._draw_counter(counter, margin=22, texture='value_counter', index=0, y=y_offset) + tex.draw_texture('dan_info', f'exam_{exam.type}', y=y_offset, x=-len(counter)*20) + + if exam.range == 'less': + tex.draw_texture('dan_info', 'exam_less', y=y_offset) + elif exam.range == 'more': + tex.draw_texture('dan_info', 'exam_more', y=y_offset) + + tex.draw_texture('dan_info', 'exam_overlay_2', y=y_offset) + if exam.range == 'less': + counter = str(max(0, exam.red - self._get_exam_progress(exam))) + elif exam.range == 'more': + counter = str(max(0, self._get_exam_progress(exam))) + self._draw_counter(counter, margin=22, texture='value_counter', index=1, y=y_offset) + if exam.type == 'gauge': + tex.draw_texture('dan_info', 'exam_percent', y=y_offset, index=1) + + tex.draw_texture('dan_info', 'frame', frame=self.color) + if self.hori_name is not None: + self.hori_name.draw(outline_color=ray.BLACK, x=154 - (self.hori_name.texture.width//2), + y=392, x2=min(self.hori_name.texture.width, 275)-self.hori_name.texture.width) + + def _draw_counter(self, counter, margin, texture, index=None, y=0): + """Helper to draw digit counters""" + for j in range(len(counter)): + kwargs = {'frame': int(counter[j]), 'x': -(len(counter) - j) * margin, 'y': y} + if index is not None: + kwargs['index'] = index + tex.draw_texture('dan_info', texture, **kwargs) + + def _get_exam_progress(self, exam: Exam) -> int: + """Get progress value based on exam type""" + type_mapping = { + 'gauge': (self.player_1.gauge.gauge_length / self.player_1.gauge.gauge_max) * 100, + 'judgeperfect': self.player_1.good_count, + 'judgegood': self.player_1.ok_count, + 'judgebad': self.player_1.bad_count, + 'score': self.player_1.score, + 'combo': self.player_1.max_combo + } + return int(type_mapping.get(exam.type, 0)) + + def _draw_progress_bar(self, progress, y_offset): + """Draw the progress bar with appropriate color""" + progress = max(0, progress) # Clamp to 0 minimum + progress = min(progress, 1) # Clamp to 1 maximum + + if progress == 1: + texture = 'exam_max' + elif progress >= 0.5: + texture = 'exam_gold' + else: + texture = 'exam_red' + + tex.draw_texture('dan_info', texture, x2=940*progress, y=y_offset) + + @override + def draw(self): + self.background.draw() + self.draw_dan_info() + self.player_1.draw(self.current_ms, self.start_ms, self.mask_shader, dan_transition=self.dan_transition) + self.draw_overlay() + + +class DanTransition: + def __init__(self): + self.move = tex.get_animation(26) + self.is_finished = False + + def start(self): + self.move.start() + self.is_finished = False + + def update(self, current_time): + self.move.update(current_time) + self.is_finished = self.move.is_finished + + def draw(self): + tex.draw_texture('dan', 'transition', index=0, x=self.move.attribute, mirror='horizontal') + tex.draw_texture('dan', 'transition', index=1, x=-self.move.attribute) + + +class DanGauge: + """The player's gauge""" + def __init__(self, player_num: str, total_notes: int): + self.player_num = player_num + self.string_diff = "_hard" + self.gauge_length = 0 + self.previous_length = 0 + self.visual_length = 0 + self.total_notes = total_notes + self.gauge_max = 89 + self.tamashii_fire_change = tex.get_animation(25) + self.is_clear = False + self.is_rainbow = False + self.gauge_update_anim = tex.get_animation(10) + self.rainbow_fade_in = None + self.rainbow_animation = None + + def add_good(self): + """Adds a good note to the gauge""" + self.gauge_update_anim.start() + self.previous_length = int(self.gauge_length) + self.gauge_length += (1 / (self.total_notes * (self.gauge_max / 100))) * 100 + if self.gauge_length > self.gauge_max: + self.gauge_length = self.gauge_max + + if int(self.gauge_length * 8) % 8 == 0: + self.visual_length = int(self.gauge_length * 8) + + def add_ok(self): + """Adds an ok note to the gauge""" + self.gauge_update_anim.start() + self.previous_length = int(self.gauge_length) + self.gauge_length += (0.5 / (self.total_notes * (self.gauge_max / 100))) * 100 + if self.gauge_length > self.gauge_max: + self.gauge_length = self.gauge_max + + if int(self.gauge_length * 8) % 8 == 0: + self.visual_length = int(self.gauge_length * 8) + + def add_bad(self): + """Adds a bad note to the gauge""" + self.previous_length = int(self.gauge_length) + self.gauge_length -= (2 / (self.total_notes * (self.gauge_max / 100))) * 100 + if self.gauge_length < 0: + self.gauge_length = 0 + + if int(self.gauge_length * 8) % 8 == 0: + self.visual_length = int(self.gauge_length * 8) + + def update(self, current_ms: float): + self.is_rainbow = self.gauge_length == self.gauge_max + self.is_clear = self.is_rainbow + if self.gauge_length == self.gauge_max and self.rainbow_fade_in is None: + self.rainbow_fade_in = Animation.create_fade(450, initial_opacity=0.0, final_opacity=1.0) + self.rainbow_fade_in.start() + self.gauge_update_anim.update(current_ms) + self.tamashii_fire_change.update(current_ms) + + if self.rainbow_fade_in is not None: + self.rainbow_fade_in.update(current_ms) + + if self.rainbow_animation is None: + self.rainbow_animation = Animation.create_texture_change((16.67*8) * 3, textures=[((16.67 * 3) * i, (16.67 * 3) * (i + 1), i) for i in range(8)]) + self.rainbow_animation.start() + else: + self.rainbow_animation.update(current_ms) + if self.rainbow_animation.is_finished or self.gauge_length < 87: + self.rainbow_animation = None + + def draw(self): + tex.draw_texture('gauge_dan', 'border') + tex.draw_texture('gauge_dan', f'{self.player_num}p_unfilled') + tex.draw_texture('gauge_dan', f'{self.player_num}p_bar', x2=self.visual_length-8) + + # Rainbow effect for full gauge + if self.gauge_length == self.gauge_max and self.rainbow_fade_in is not None and self.rainbow_animation is not None: + if 0 < self.rainbow_animation.attribute < 8: + tex.draw_texture('gauge_dan', 'rainbow', frame=self.rainbow_animation.attribute-1, fade=self.rainbow_fade_in.attribute) + tex.draw_texture('gauge_dan', 'rainbow', frame=self.rainbow_animation.attribute, fade=self.rainbow_fade_in.attribute) + if self.gauge_update_anim is not None and self.visual_length <= self.gauge_max and self.visual_length > self.previous_length: + tex.draw_texture('gauge_dan', f'{self.player_num}p_bar_fade', x=self.visual_length-8, fade=self.gauge_update_anim.attribute) + tex.draw_texture('gauge_dan', 'overlay', fade=0.15) + + # Draw clear status indicators + if self.is_rainbow: + tex.draw_texture('gauge_dan', 'tamashii_fire', scale=0.75, center=True, frame=self.tamashii_fire_change.attribute) + tex.draw_texture('gauge_dan', 'tamashii') + if self.is_rainbow and self.tamashii_fire_change.attribute in (0, 1, 4, 5): + tex.draw_texture('gauge_dan', 'tamashii_overlay', fade=0.5) + else: + tex.draw_texture('gauge_dan', 'tamashii_dark') diff --git a/scenes/song_select.py b/scenes/song_select.py index eb83725..6eabe68 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -66,21 +66,22 @@ class SongSelectScreen(Screen): self.ura_switch_animation = UraSwitchAnimation() self.dan_transition = DanTransition() + session_data = global_data.session_data[global_data.player_num-1] self.player_1 = SongSelectPlayer(str(global_data.player_num), self.text_fade_in) if self.navigator.items == []: logger.warning("No navigator items found, returning to ENTRY screen") return self.on_screen_end("ENTRY") - 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)]) + if str(session_data.selected_song) in self.navigator.all_song_files: + self.navigator.mark_crowns_dirty_for_song(self.navigator.all_song_files[str(session_data.selected_song)]) curr_item = self.navigator.get_current_item() curr_item.box.get_scores() self.navigator.add_recent() def finalize_song(self): - global_data.selected_song = self.navigator.get_current_item().path + global_data.session_data[global_data.player_num-1].selected_song = self.navigator.get_current_item().path global_data.session_data[global_data.player_num-1].selected_difficulty = self.player_1.selected_difficulty global_data.session_data[global_data.player_num-1].genre_index = self.navigator.get_current_item().box.name_texture_index diff --git a/scenes/two_player/song_select.py b/scenes/two_player/song_select.py index bd468d9..7a6de4e 100644 --- a/scenes/two_player/song_select.py +++ b/scenes/two_player/song_select.py @@ -14,9 +14,9 @@ class TwoPlayerSongSelectScreen(SongSelectScreen): self.player_2 = SongSelectPlayer('2', self.text_fade_in) def finalize_song(self): - global_data.selected_song = self.navigator.get_current_item().path + global_data.session_data[0].selected_song = self.navigator.get_current_item().path global_data.session_data[0].genre_index = self.navigator.get_current_item().box.name_texture_index - logger.info(f"Finalized song selection: {global_data.selected_song}") + logger.info(f"Finalized song selection: {global_data.session_data[0].selected_song}") def handle_input_browsing(self): """Handle input for browsing songs."""