From 802d9c5b37661299a9e04fd49cdead3ae5b75941 Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Wed, 19 Nov 2025 18:35:26 -0500 Subject: [PATCH] finally refactor the song select boxes --- .gitignore | 1 + docs/tja_compat.csv | 100 +++--- libs/audio/audio.c | 2 - libs/file_navigator.py | 534 +++++++++++++++---------------- libs/global_objects.py | 23 +- libs/texture.py | 14 +- libs/utils.py | 4 +- libs/video.py | 3 +- scenes/dan/dan_select.py | 19 +- scenes/dan/game_dan.py | 5 +- scenes/entry.py | 26 +- scenes/loading.py | 6 +- scenes/song_select.py | 41 ++- scenes/two_player/song_select.py | 10 +- 14 files changed, 401 insertions(+), 387 deletions(-) diff --git a/.gitignore b/.gitignore index ec4eb71..e37fcc9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ cache dev-config.toml libaudio.so latest.log +Graphics1080 diff --git a/docs/tja_compat.csv b/docs/tja_compat.csv index b9ed382..db21040 100644 --- a/docs/tja_compat.csv +++ b/docs/tja_compat.csv @@ -1,86 +1,86 @@ Command/Header,Support Status,Initial Sim Release TITLE,Supported,TaikoJiro v0.80 (initial release) -SUBTITLE,Supported,TaikoJiro v2.64 WAVE,Supported,TaikoJiro v0.80 (initial release) -DEMOSTART,Supported,TaikoJiro v2.37 OFFSET,Supported,TaikoJiro v0.80 (initial release) BPM,Supported,TaikoJiro v0.80 (initial release) -SCENEPRESET,Supported,OpenTaiko (0auBSQ) v0.6.0 -BGMOVIE,Supported,TJAPlayer2 for.PC ver.2016021300 -COURSE,Supported,TaikoJiro v1.76 LEVEL,Supported,TaikoJiro v0.80 (initial release) BALLOON,Supported,TaikoJiro +COURSE,Supported,TaikoJiro v1.76 +DEMOSTART,Supported,TaikoJiro v2.37 +SUBTITLE,Supported,TaikoJiro v2.64 +BGMOVIE,Supported,TJAPlayer2 for.PC ver.2016021300 +SCENEPRESET,Supported,OpenTaiko (0auBSQ) v0.6.0 #START / #END,Supported,TaikoJiro v0.80 (initial release) #BPMCHANGE,Supported,TaikoJiro v0.80 (initial release) #MEASURE,Supported,TaikoJiro #GOGOSTART / #GOGOEND,Supported,TaikoJiro #SCROLL,Supported,TaikoJiro -#BARLINEOFF / #BARLINEON,Supported,TaikoJiro v2.69 -#LYRIC,Supported,TJAPlayer2 for.PC +#DELAY,Supported,TaikoJiro v1.60 #SECTION,Supported,TaikoJiro v1.63 #BRANCHSTART / #BRANCHEND,Supported,TaikoJiro v1.63 #N / #E / #M,Supported,TaikoJiro v1.63 -#DELAY,Supported,TaikoJiro v1.60 +#BARLINEOFF / #BARLINEON,Supported,TaikoJiro v2.69 +#LYRIC,Supported,TJAPlayer2 for.PC #SUDDEN,Supported,TJAPlayer2 for.PC #JPOSSCROLL,Supported,TJAPlayer2 for.PC -ARTIST,Unsupported,Malody -MAKER,Unsupported,taiko-web ver.19.11.25 -NOTESDESIGNER,Unsupported,(Better)TaikoCatsCaffe -AUTHOR,Unsupported,Malody -GENRE,Unsupported,TJAPlayer2 for.PC -SIDE,Unsupported,TaikoJiro v2.49 -SIDEREV,Unsupported,TaikoJiro 2 v0.70 SONGVOL,Unsupported,TaikoJiro v1.66 SEVOL,Unsupported,TaikoJiro v1.66 +SCOREINIT,Unsupported,TaikoJiro v1.67 +SCOREDIFF,Unsupported,TaikoJiro v1.67 +STYLE,Unsupported,TaikoJiro v1.99 +GAME,Unsupported,TaikoJiro v2.13 +LIFE,Unsupported,TaikoJiro v2.19 +SIDE,Unsupported,TaikoJiro v2.49 +SCOREMODE,Unsupported,TaikoJiro v2.85 +SIDEREV,Unsupported,TaikoJiro 2 v0.70 +TOTAL,Unsupported,TaikoJiro v2.92 & TaikoJiro 2 v0.93 +GENRE,Unsupported,TJAPlayer2 for.PC +HIDDENBRANCH,Unsupported,TJAPlayer2 for.PC HEADSCROLL,Unsupported,TJAPlayer2 for.PC -PREIMAGE,Unsupported,OpenTaiko (0auBSQ) v0.5.4 -COVER,Unsupported,Malody +MOVIEOFFSET,Unsupported,TJAPlayer2 for.PC ver.2015081100 +BGIMAGE,Unsupported,TJAPlayer2 for.PC ver.2016021300 +EXAM,Unsupported,TJAPlayer3 v1.5.0 +GAUGEINCR,Unsupported,TJAPlayer3 v1.5.4 TAIKOWEBSKIN,Unsupported,taiko-web ver.19.02.03 +MAKER,Unsupported,taiko-web ver.19.11.25 +LYRICS: / LYRICFILE,Unsupported,taiko-web ver.20.03.31 +NOTESDESIGNER,Unsupported,(Better)TaikoCatsCaffe TOWERTYPE,Unsupported,OpenTaiko (0auBSQ) v0.5.0 DANTICK,Unsupported,OpenTaiko (0auBSQ) v0.5.1 DANTICKCOLOR,Unsupported,OpenTaiko (0auBSQ) v0.5.1 +PREIMAGE,Unsupported,OpenTaiko (0auBSQ) v0.5.4 SELECTBG,Unsupported,OpenTaiko (0auBSQ) v0.6.0 -BGIMAGE,Unsupported,TJAPlayer2 for.PC ver.2016021300 -BGOFFSET,Unsupported,Unknown -MOVIEOFFSET,Unsupported,TJAPlayer2 for.PC ver.2015081100 -BGA Headers,Unsupported,C-Taiko -LYRICS: / LYRICFILE,Unsupported,taiko-web ver.20.03.31 EXPLICIT,Unsupported,OpenTaiko (0auBSQ) v0.6.0 -GAME,Unsupported,TaikoJiro v2.13 -STYLE,Unsupported,TaikoJiro v1.99 -LIFE,Unsupported,TaikoJiro v2.19 -TOTAL,Unsupported,TaikoJiro v2.92 & TaikoJiro 2 v0.93 -GAUGEINCR,Unsupported,TJAPlayer3 v1.5.4 -EXAM,Unsupported,TJAPlayer3 v1.5.0 -SCOREMODE,Unsupported,TaikoJiro v2.85 -SCOREINIT,Unsupported,TaikoJiro v1.67 -SCOREDIFF,Unsupported,TaikoJiro v1.67 -HIDDENBRANCH,Unsupported,TJAPlayer2 for.PC +BGA Headers,Unsupported,C-Taiko +ARTIST,Unsupported,Malody +COVER,Unsupported,Malody +AUTHOR,Unsupported,Malody +BGOFFSET,Unsupported,Unknown -#BMSCROLL / #HBSCROLL / #NMSCROLL,Unsupported,TaikoJiro v1.91 -#PAPAMAMA,Unsupported,TJAPlayer3-f v1.7.2.0 -#DUMMYSTART / #DUMMYEND,Unsupported,TaikoManyGimmicks -#BARLINESCROLL,Unsupported,taiko-web (plugin Custom Barlines) -#HISPEED,Unsupported,TaikoManyGimmicks v0.6.1α -#DIRECTION,Unsupported,TJAPlayer2 for.PC -#BARLINE,Unsupported,taiko-web (plugin Custom Barlines) -#JUDGEDELAY,Unsupported,TaikoManyGimmicks -#NOTESPAWN,Unsupported,TaikoManyGimmicks -#ENABLEDORON / #DISABLEDORON,Unsupported,TJAPlayer3-Extended -#SENOTECHANGE,Unsupported,TJAPlayer3 v1.4.0 -#NOTESCHANGE,Unsupported,C-Taiko #LEVELHOLD,Unsupported,TaikoJiro v1.63 -#NEXTSONG,Unsupported,TJAPlayer3 v1.5.0 -#GAMETYPE,Unsupported,OpenTaiko (0auBSQ) v0.6.0 +#BMSCROLL / #HBSCROLL / #NMSCROLL,Unsupported,TaikoJiro v1.91 +#DIRECTION,Unsupported,TJAPlayer2 for.PC #HIDDEN,Unsupported,TJAPlayer2 for.PC -NOTE / BARLINE,Unsupported,TaikoManyGimmicks -#GRADATION,Unsupported,TaikoManyGimmicks v0.6α -#INCLUDE,Unsupported,TaikoManyGimmicks v0.6.1α -#SPLITLANE / #MERGELANE,Unsupported,OpenTaiko (0auBSQ) v0.6.0 +#SENOTECHANGE,Unsupported,TJAPlayer3 v1.4.0 +#NEXTSONG,Unsupported,TJAPlayer3 v1.5.0 +#PAPAMAMA,Unsupported,TJAPlayer3-f v1.7.2.0 +#ENABLEDORON / #DISABLEDORON,Unsupported,TJAPlayer3-Extended #OBJ / #CAM,Unsupported,TJAPlayer3-Extended #BORDERCOLOR,Unsupported,TJAPlayer3-Extended #CHANGETEXTURE / #RESETTEXTURE,Unsupported,TJAPlayer3-Extended #SETCONFIG,Unsupported,TJAPlayer3-Extended +#BARLINE,Unsupported,taiko-web (plugin Custom Barlines) +#BARLINESCROLL,Unsupported,taiko-web (plugin Custom Barlines) +#GAMETYPE,Unsupported,OpenTaiko (0auBSQ) v0.6.0 +#SPLITLANE / #MERGELANE,Unsupported,OpenTaiko (0auBSQ) v0.6.0 #BGAON / #BGAOFF,Unsupported,OpenTaiko (0auBSQ) v0.6.0 +#JUDGEDELAY,Unsupported,TaikoManyGimmicks +#NOTESPAWN,Unsupported,TaikoManyGimmicks +NOTE / BARLINE,Unsupported,TaikoManyGimmicks +#DUMMYSTART / #DUMMYEND,Unsupported,TaikoManyGimmicks +#GRADATION,Unsupported,TaikoManyGimmicks v0.6α +#INCLUDE,Unsupported,TaikoManyGimmicks v0.6.1α +#HISPEED,Unsupported,TaikoManyGimmicks v0.6.1α +#NOTESCHANGE,Unsupported,C-Taiko diff --git a/libs/audio/audio.c b/libs/audio/audio.c index 7d9beba..9f9759f 100644 --- a/libs/audio/audio.c +++ b/libs/audio/audio.c @@ -213,10 +213,8 @@ static int port_audio_callback(const void *inputBuffer, void *outputBuffer, } struct audio_buffer *audio_buffer = AUDIO.Buffer.first; - int active_buffers = 0; while (audio_buffer != NULL) { if (audio_buffer->playing && !audio_buffer->paused && audio_buffer->data != NULL) { - active_buffers++; unsigned int subBufferSizeFrames = audio_buffer->sizeInFrames / 2; unsigned long framesToMix = framesPerBuffer; float *buffer_data = (float *)audio_buffer->data; diff --git a/libs/file_navigator.py b/libs/file_navigator.py index f6ba42b..20f61d2 100644 --- a/libs/file_navigator.py +++ b/libs/file_navigator.py @@ -8,18 +8,17 @@ from libs.audio import audio from libs.animation import Animation, MoveAnimation from libs.global_data import Crown, Difficulty from libs.tja import TJAParser, test_encodings -from libs.texture import tex +from libs.texture import SCREEN_SCALE, SCREEN_WIDTH, tex from libs.utils import OutlinedText, get_current_ms, global_data from datetime import datetime, timedelta import sqlite3 import pyray as ray -BOX_CENTER = 444 +BOX_CENTER = 594 * SCREEN_SCALE logger = logging.getLogger(__name__) -class SongBox: - """A box for the song select screen.""" +class BaseBox(): OUTLINE_MAP = { 1: ray.Color(0, 77, 104, 255), 2: ray.Color(156, 64, 2, 255), @@ -38,61 +37,118 @@ class SongBox: } BACK_INDEX = 17 DEFAULT_INDEX = 9 - def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None, - tja_count: Optional[int] = None, box_texture: Optional[str] = None, name_texture_index: Optional[int] = None): + """Base class for all box types in the song select screen.""" + def __init__(self, name: str, texture_index: int): self.text_name = name self.texture_index = texture_index + self.position = float('inf') + self.start_position: float = -1 + self.target_position: float = -1 + self.open_anim = Animation.create_move(133, total_distance=150*SCREEN_SCALE, delay=83.33) + self.open_fade = Animation.create_fade(200, initial_opacity=0, final_opacity=1.0) + self.move = None + self.is_open = False + self.text_loaded = False + self.wait = 0 + + def load_text(self): + self.name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5, vertical=True) + + def move_box(self, current_time: float): + if self.position != self.target_position and self.move is None: + if self.position < self.target_position: + direction = 1 + else: + direction = -1 + if abs(self.target_position - self.position) > 250: + direction *= -1 + self.move = Animation.create_move(83.3, total_distance=100 * direction * SCREEN_SCALE, ease_out='cubic') + self.move.start() + if self.is_open or self.target_position == BOX_CENTER: + self.move.total_distance = 250 * direction + self.start_position = self.position + if self.move is not None: + self.move.update(current_time) + self.position = self.start_position + int(self.move.attribute) + if self.move.is_finished: + self.position = self.target_position + self.move = None + + def update(self, current_time: float, is_diff_select: bool): + self.is_diff_select = is_diff_select + self.open_anim.update(current_time) + self.open_fade.update(current_time) + + def _draw_closed(self, x: float, y: float): + tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x) + offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0 + tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=32, y=offset) + tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x) + if self.texture_index == SongBox.DEFAULT_INDEX: + tex.draw_texture('box', 'genre_overlay', x=x, y=y) + elif self.texture_index == 14: + tex.draw_texture('box', 'diff_overlay', x=x, y=y) + + def _draw_open(self, x: float, y: float, fade_override: Optional[float], is_ura: bool): + pass + + def draw(self, x: float, y: float, is_ura: bool, fade_override: Optional[float] = None): + if self.is_open and get_current_ms() >= self.wait + 83.33: + self._draw_open(x, y, fade_override, is_ura) + else: + self._draw_closed(x, y) + +class BackBox(BaseBox): + def __init__(self, name: str, texture_index: int): + super().__init__(name, texture_index) + self.yellow_box = None + + def load_text(self): + super().load_text() + self.text_loaded = True + + def update(self, current_time: float, is_diff_select: bool): + super().update(current_time, is_diff_select) + is_open_prev = self.is_open + self.move_box(current_time) + self.is_open = self.position == BOX_CENTER + + if self.yellow_box is not None: + self.yellow_box.update(is_diff_select) + + if not is_open_prev and self.is_open: + self.yellow_box = YellowBox(True) + self.yellow_box.create_anim() + self.wait = current_time + + def _draw_closed(self, x: float, y: float): + super()._draw_closed(x, y) + tex.draw_texture('box', 'back_text', x=x, y=y) + + def _draw_open(self, x: float, y: float, fade_override: Optional[float] = None, is_ura: bool = False): + if self.yellow_box is not None: + self.yellow_box.draw(self, fade_override, is_ura, self.name) + +class SongBox(BaseBox): + def __init__(self, name: str, texture_index: int, tja: TJAParser, name_texture_index: Optional[int] = None): + super().__init__(name, texture_index) if name_texture_index is None: self.name_texture_index = texture_index else: self.name_texture_index = name_texture_index - self.box_texture_path = box_texture - self.box_texture = None self.scores = dict() - self.crown = dict() - self.position = -11111 - self.start_position = -1 - self.target_position = -1 - self.is_open = False - self.is_back = self.texture_index == SongBox.BACK_INDEX - if self.is_back: - for i in range(1, SongBox.BACK_INDEX-1): - if audio.is_sound_playing(f'genre_voice_{i}'): - audio.stop_sound(f'genre_voice_{i}') - self.name = None - self.hori_name = None - self.yellow_box = None - self.open_anim = Animation.create_move(133, start_position=0, total_distance=150, delay=83.33) - self.open_fade = Animation.create_fade(200, initial_opacity=0, final_opacity=1.0) - self.move = None - self.wait = 0 - self.is_dir = is_dir - self.tja_count = tja_count - self.tja_count_text = None + self.hash = dict() self.score_history = None self.history_wait = 0 self.tja = tja - self.hash = dict() self.is_favorite = False + self.yellow_box = None - def reset(self): - if self.yellow_box is not None: - self.yellow_box.reset() - self.yellow_box.create_anim() - if self.name is not None: - self.name.unload() - self.name = None - if self.box_texture is not None: - ray.unload_texture(self.box_texture) - self.box_texture = None - if self.hori_name is not None: - self.hori_name.unload() - self.hori_name = None - self.is_open = False + def load_text(self): + super().load_text() + self.text_loaded = True def get_scores(self): - if self.tja is None: - return with sqlite3.connect('scores.db') as con: cursor = con.cursor() # Batch database query for all diffs at once @@ -116,37 +172,12 @@ class SongBox: self.scores[diff] = hash_to_score.get(diff_hash) self.score_history = None - def move_box(self): - if self.position != self.target_position and self.move is None: - if self.position < self.target_position: - direction = 1 - else: - direction = -1 - if abs(self.target_position - self.position) > 250: - direction *= -1 - self.move = Animation.create_move(83.3, start_position=0, total_distance=100 * direction, ease_out='cubic') - self.move.start() - if self.is_open or self.target_position == BOX_CENTER + 150: - self.move.total_distance = 250 * direction - self.start_position = self.position - if self.move is not None: - self.move.update(get_current_ms()) - self.position = self.start_position + int(self.move.attribute) - if self.move.is_finished: - self.position = self.target_position - self.move = None - if not (-56 <= self.position <= 1280): - self.reset() - - def update(self, is_diff_select: bool): - current_time = get_current_ms() - self.is_diff_select = is_diff_select + def update(self, current_time: float, is_diff_select: bool): + super().update(current_time, is_diff_select) is_open_prev = self.is_open - self.move_box() - self.is_open = self.position == BOX_CENTER + 150 + self.move_box(current_time) + self.is_open = self.position == BOX_CENTER - if not (-56 <= self.position <= 1280): - return if self.yellow_box is not None: self.yellow_box.update(is_diff_select) @@ -157,54 +188,21 @@ class SongBox: self.score_history = ScoreHistory(self.scores, current_time) if not is_open_prev and self.is_open: - if self.tja is not None or self.is_back: - self.yellow_box = YellowBox(self.name, self.is_back, tja=self.tja) - self.yellow_box.create_anim() - else: - self.hori_name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5) - self.open_anim.start() - self.open_fade.start() + self.yellow_box = YellowBox(False, tja=self.tja) + self.yellow_box.create_anim() self.wait = current_time if current_time >= self.history_wait + 3000: self.history_wait = current_time - if self.tja is None and self.texture_index != 17 and not audio.is_sound_playing('voice_enter'): - audio.play_sound(f'genre_voice_{self.texture_index}', 'voice') - elif not self.is_open and is_open_prev and self.texture_index != 17 and audio.is_sound_playing(f'genre_voice_{self.texture_index}'): - audio.stop_sound(f'genre_voice_{self.texture_index}') - if self.tja_count is not None and self.tja_count > 0 and self.tja_count_text is None: - self.tja_count_text = OutlinedText(str(self.tja_count), 35, ray.WHITE, outline_thickness=5)#, horizontal_spacing=1.2) - if self.box_texture is None and self.box_texture_path is not None: - self.box_texture = ray.load_texture(self.box_texture_path) - - self.open_anim.update(current_time) - self.open_fade.update(current_time) - - if self.name is None: - self.name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5, vertical=True) if self.score_history is not None: self.score_history.update(current_time) + def _draw_closed(self, x: float, y: float): + super()._draw_closed(x, y) - def _draw_closed(self, x: int, y: int): - tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x) - offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0 - tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=32, y=offset) - tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x) - if self.texture_index == SongBox.DEFAULT_INDEX: - tex.draw_texture('box', 'genre_overlay', x=x, y=y) - elif self.texture_index == 14: - tex.draw_texture('box', 'diff_overlay', x=x, y=y) - if not self.is_back and self.is_dir: - tex.draw_texture('box', 'folder_clip', frame=self.texture_index, x=x - (1 - offset), y=y) + self.name.draw(outline_color=SongBox.OUTLINE_MAP.get(self.name_texture_index, ray.Color(101, 0, 82, 255)), x=x + 47 - int(self.name.texture.width / 2), y=y+35, y2=min(self.name.texture.height, 417)-self.name.texture.height) - if self.is_back: - tex.draw_texture('box', 'back_text', x=x, y=y) - return - elif self.name is not None: - self.name.draw(outline_color=SongBox.OUTLINE_MAP.get(self.name_texture_index, ray.Color(101, 0, 82, 255)), x=x + 47 - int(self.name.texture.width / 2), y=y+35, y2=min(self.name.texture.height, 417)-self.name.texture.height) - - if self.tja is not None and self.tja.ex_data.new: + if self.tja.ex_data.new: tex.draw_texture('yellow_box', 'ex_data_new_song_balloon', x=x, y=y) valid_scores = {k: v for k, v in self.scores.items() if v is not None} if valid_scores: @@ -216,6 +214,58 @@ class SongBox: tex.draw_texture('yellow_box', 'crown_fc', x=x, y=y, frame=min(Difficulty.URA, highest_key)) elif score and score[5] >= Crown.CLEAR: tex.draw_texture('yellow_box', 'crown_clear', x=x, y=y, frame=min(Difficulty.URA, highest_key)) + + def _draw_open(self, x: float, y: float, fade_override=None, is_ura=False): + if self.yellow_box is not None: + self.yellow_box.draw(self, fade_override, is_ura, self.name) + + def draw_score_history(self): + if self.is_open and get_current_ms() >= self.wait + 83.33: + if self.score_history is not None and get_current_ms() >= self.history_wait + 3000: + self.score_history.draw() + +class FolderBox(BaseBox): + def __init__(self, name: str, texture_index: int, tja_count: int = 0, + box_texture: Optional[str] = None): + super().__init__(name, texture_index) + self.box_texture_path = Path(box_texture) if box_texture else None + self.is_back = self.texture_index == SongBox.BACK_INDEX + if self.is_back: + for i in range(1, SongBox.BACK_INDEX-1): + if audio.is_sound_playing(f'genre_voice_{i}'): + audio.stop_sound(f'genre_voice_{i}') + self.tja_count = tja_count + self.crown = dict() + + def load_text(self): + super().load_text() + self.hori_name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5) + self.box_texture = ray.load_texture(str(self.box_texture_path)) if self.box_texture_path and self.box_texture_path.exists() else None + self.tja_count_text = OutlinedText(str(self.tja_count), 35, ray.WHITE, outline_thickness=5) + self.text_loaded = True + + def update(self, current_time: float, is_diff_select: bool): + super().update(current_time, is_diff_select) + is_open_prev = self.is_open + self.move_box(current_time) + self.is_open = self.position == BOX_CENTER + + if not is_open_prev and self.is_open: + self.open_anim.start() + self.open_fade.start() + self.wait = current_time + if self.texture_index != SongBox.BACK_INDEX and not audio.is_sound_playing('voice_enter'): + audio.play_sound(f'genre_voice_{self.texture_index}', 'voice') + elif not self.is_open and is_open_prev and self.texture_index != 17 and audio.is_sound_playing(f'genre_voice_{self.texture_index}'): + audio.stop_sound(f'genre_voice_{self.texture_index}') + + def _draw_closed(self, x: float, y: float): + super()._draw_closed(x, y) + offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0 + tex.draw_texture('box', 'folder_clip', frame=self.texture_index, x=x - (1 - offset), y=y) + + self.name.draw(outline_color=SongBox.OUTLINE_MAP.get(self.texture_index, ray.Color(101, 0, 82, 255)), x=x + 47 - int(self.name.texture.width / 2), y=y+35, y2=min(self.name.texture.height, 417)-self.name.texture.height) + if self.crown: #Folder lamp highest_crown = max(self.crown) if self.crown[highest_crown] == 'DFC': @@ -225,11 +275,11 @@ class SongBox: else: tex.draw_texture('yellow_box', 'crown_clear', x=x, y=y, frame=min(Difficulty.URA, highest_crown)) - def _draw_open(self, x: int, y: int, fade_override: Optional[float]): + def _draw_open(self, x: float, y: float, fade_override: Optional[float], is_ura: bool): color = ray.WHITE if fade_override is not None: color = ray.fade(ray.WHITE, fade_override) - if self.hori_name is not None and self.open_anim.attribute >= 100: + if not self.is_back and self.open_anim.attribute >= 100: tex.draw_texture('box', 'folder_top_edge', x=x, y=y - self.open_anim.attribute, color=color, mirror='horizontal', frame=self.texture_index) tex.draw_texture('box', 'folder_top', x=x, y=y - self.open_anim.attribute, color=color, frame=self.texture_index) tex.draw_texture('box', 'folder_top_edge', x=x+268, y=y - self.open_anim.attribute, color=color, frame=self.texture_index) @@ -249,45 +299,30 @@ class SongBox: color = ray.WHITE if fade_override is not None: color = ray.fade(ray.WHITE, fade_override) - if self.tja_count_text is not None and self.texture_index != 14: + if self.texture_index != 14: tex.draw_texture('yellow_box', 'song_count_back', color=color, fade=0.5) tex.draw_texture('yellow_box', 'song_count_num', color=color) tex.draw_texture('yellow_box', 'song_count_songs', color=color) dest_width = min(124, self.tja_count_text.texture.width) self.tja_count_text.draw(outline_color=ray.BLACK, x=560 - (dest_width//2), y=126, x2=dest_width-self.tja_count_text.texture.width, color=color) - if self.texture_index != 9: + if self.texture_index != SongBox.DEFAULT_INDEX: tex.draw_texture('box', 'folder_graphic', color=color, frame=self.texture_index) tex.draw_texture('box', 'folder_text', color=color, frame=self.texture_index) elif self.box_texture is not None: - ray.draw_texture(self.box_texture, (x+48) - (self.box_texture.width//2), (y+240) - (self.box_texture.height//2), color) - - def draw_score_history(self): - if self.is_open and get_current_ms() >= self.wait + 83.33: - if self.score_history is not None and get_current_ms() >= self.history_wait + 3000: - self.score_history.draw() - - def draw(self, x: int, y: int, is_ura: bool, fade_override=None): - if self.is_open and get_current_ms() >= self.wait + 83.33: - if self.yellow_box is not None: - self.yellow_box.draw(self, fade_override, is_ura) - else: - self._draw_open(x, y, self.open_fade.attribute) - else: - self._draw_closed(x, y) + ray.draw_texture(self.box_texture, int((x+48) - (self.box_texture.width//2)), int((y+240) - (self.box_texture.height//2)), color) class YellowBox: """A song box when it is opened.""" - def __init__(self, name: Optional[OutlinedText], is_back: bool, tja: Optional[TJAParser] = None, is_dan: bool = False): + def __init__(self, is_back: bool, tja: Optional[TJAParser] = None, is_dan: bool = False): self.is_diff_select = False - self.name = name self.is_back = is_back self.tja = tja - self.is_dan = is_dan - self.subtitle = None if self.tja is not None: subtitle_text = self.tja.metadata.subtitle.get(global_data.config['general']['language'], '') font_size = 30 if len(subtitle_text) < 30 else 20 self.subtitle = OutlinedText(subtitle_text, font_size, ray.WHITE, outline_thickness=5, vertical=True) + self.is_dan = is_dan + self.subtitle = None self.left_out = tex.get_animation(9) self.right_out = tex.get_animation(10) @@ -318,11 +353,6 @@ class YellowBox: self.bottom_y = tex.textures['yellow_box']['yellow_box_bottom_right'].y[0] self.edge_height = tex.textures['yellow_box']['yellow_box_bottom_right'].height - def reset(self): - if self.subtitle is not None: - self.subtitle.unload() - self.subtitle = None - def create_anim(self): self.right_out_2.reset() self.top_y_out.reset() @@ -368,8 +398,8 @@ class YellowBox: self.center_height = self.center_h_out.attribute self.is_diff_select = is_diff_select - def _draw_tja_data(self, song_box, color, fade): - if self.tja is None: + def _draw_tja_data(self, song_box: SongBox, color: ray.Color, fade: float): + if not self.tja: return for diff in self.tja.metadata.course_data: if diff >= Difficulty.URA: @@ -406,16 +436,14 @@ class YellowBox: if self.tja.metadata.course_data[diff].is_branching and (get_current_ms() // 1000) % 2 == 0: tex.draw_texture('yellow_box', 'branch_indicator', x=(diff*60), color=color) - def _draw_tja_data_diff(self, is_ura: bool, song_box: Optional[SongBox] = None): - if self.tja is None: + def _draw_tja_data_diff(self, is_ura: bool, song_box: SongBox): + if not self.tja: return tex.draw_texture('diff_select', 'back', fade=self.fade_in.attribute) tex.draw_texture('diff_select', 'option', fade=self.fade_in.attribute) tex.draw_texture('diff_select', 'neiro', fade=self.fade_in.attribute) for diff in self.tja.metadata.course_data: - if song_box is None: - continue if diff >= Difficulty.URA: continue elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] is not None and song_box.scores[diff][4] == 2 and song_box.scores[diff][2] == 0) or (song_box.scores[diff][2] == 0 and song_box.scores[diff][3] == 0)): @@ -447,7 +475,7 @@ class YellowBox: name = 'branch_indicator_diff' tex.draw_texture('yellow_box', name, x=min(course, Difficulty.ONI)*115, fade=self.fade_in.attribute) - def _draw_text(self, song_box): + def _draw_text(self, song_box, name: OutlinedText): if not isinstance(self.right_out, MoveAnimation): return if not isinstance(self.right_out_2, MoveAnimation): @@ -457,9 +485,9 @@ class YellowBox: x = song_box.position + (self.right_out.attribute*0.85 - (self.right_out.start_position*0.85)) + self.right_out_2.attribute - self.right_out_2.start_position if self.is_back: tex.draw_texture('box', 'back_text_highlight', x=x) - elif self.name is not None: - texture = self.name.texture - self.name.draw(outline_color=ray.BLACK, x=x + 30, y=35 + self.top_y_out.attribute, y2=min(texture.height, 417)-texture.height, color=ray.WHITE) + else: + texture = name.texture + name.draw(outline_color=ray.BLACK, x=x + 30, y=35 + self.top_y_out.attribute, y2=min(texture.height, 417)-texture.height, color=ray.WHITE) if self.subtitle is not None: texture = self.subtitle.texture y = self.bottom_y - min(texture.height, 410) + 10 + self.top_y_out.attribute - self.top_y_out.start_position @@ -476,36 +504,31 @@ class YellowBox: tex.draw_texture('yellow_box', 'yellow_box_top', x=self.left_x + self.edge_height, y=self.top_y, x2=self.center_width) tex.draw_texture('yellow_box', 'yellow_box_center', x=self.left_x + self.edge_height, y=self.top_y + self.edge_height, x2=self.center_width, y2=self.center_height) - def draw(self, song_box: Optional[SongBox], fade_override: Optional[float], is_ura: bool): + def draw(self, song_box: Optional[SongBox | BackBox], fade_override: Optional[float], is_ura: bool, name: OutlinedText): self._draw_yellow_box() + fade = self.fade.attribute + if fade_override is not None: + fade = min(self.fade.attribute, fade_override) if self.is_dan: return - if self.is_diff_select and self.tja is not None: + if self.is_back: + tex.draw_texture('box', 'back_graphic', fade=fade) + return + if self.is_diff_select and isinstance(song_box, SongBox): self._draw_tja_data_diff(is_ura, song_box) - else: - fade = self.fade.attribute - if fade_override is not None: - fade = min(self.fade.attribute, fade_override) - if self.is_back: - tex.draw_texture('box', 'back_graphic', fade=fade) + elif isinstance(song_box, SongBox): self._draw_tja_data(song_box, ray.fade(ray.WHITE, fade), fade) - self._draw_text(song_box) + self._draw_text(song_box, name) -class DanBox: - def __init__(self, title: str, color: int, songs: list[tuple[TJAParser, int, int, int]], exams: list['Exam']): - self.position = -11111 - self.start_position = -1 - self.target_position = -1 - self.move = None - self.is_open = False - self.is_back = False - self.title = title - self.color = color +class DanBox(BaseBox): + def __init__(self, name, color: int, songs: list[tuple[TJAParser, int, int, int]], exams: list['Exam']): + super().__init__(name, color) self.songs = songs self.exams = exams self.song_text: list[tuple[OutlinedText, OutlinedText]] = [] self.total_notes = 0 + self.yellow_box = None for song, genre_index, difficulty, level in self.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) @@ -515,57 +538,26 @@ class DanBox: 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) - self.name = None - self.hori_name = None - self.yellow_box = None - def move_box(self): - if self.position != self.target_position and self.move is None: - if self.position < self.target_position: - direction = 1 - else: - direction = -1 - if abs(self.target_position - self.position) > 250: - direction *= -1 - self.move = Animation.create_move(83.3*2, start_position=0, total_distance=300 * direction, ease_out='cubic') - self.move.start() - if self.is_open or self.target_position == BOX_CENTER + 150: - self.move.total_distance = 450 * direction * -1 - self.start_position = self.position - if self.move is not None: - self.move.update(get_current_ms()) - self.position = self.start_position + int(self.move.attribute) - if self.move.is_finished: - self.position = self.target_position - self.move = None - if not (-56 <= self.position <= 1280): - self.reset() + def load_text(self): + super().load_text() + self.hori_name = OutlinedText(self.text_name, 40, ray.WHITE) + for song, genre, difficulty, level in self.songs: + title = song.metadata.title.get(global_data.config["general"]["language"], song.metadata.title["en"]) + subtitle = song.metadata.subtitle.get(global_data.config["general"]["language"], "") + title_text = OutlinedText(title, 40, ray.WHITE, vertical=True) + font_size = 30 if len(subtitle) < 30 else 20 + subtitle_text = OutlinedText(subtitle, font_size, ray.WHITE, vertical=True) + self.song_text.append((title_text, subtitle_text)) + self.text_loaded = True - def reset(self): - if self.name is not None: - self.name.unload() - self.name = None - - def get_text(self): - if self.name is None: - self.name = OutlinedText(self.title, 40, ray.WHITE, vertical=True) - self.hori_name = OutlinedText(self.title, 40, ray.WHITE) - if self.is_open and not self.song_text: - for song, genre, difficulty, level in self.songs: - title = song.metadata.title.get(global_data.config["general"]["language"], song.metadata.title["en"]) - subtitle = song.metadata.subtitle.get(global_data.config["general"]["language"], "") - title_text = OutlinedText(title, 40, ray.WHITE, vertical=True) - font_size = 30 if len(subtitle) < 30 else 20 - subtitle_text = OutlinedText(subtitle, font_size, ray.WHITE, vertical=True) - self.song_text.append((title_text, subtitle_text)) - - def update(self, is_diff_select: bool): - self.move_box() - self.get_text() + def update(self, current_time: float, is_diff_select: bool): + super().update(current_time, is_diff_select) is_open_prev = self.is_open - self.is_open = self.position == BOX_CENTER + 150 + self.move_box(current_time) + self.is_open = self.position == BOX_CENTER if not is_open_prev and self.is_open: - self.yellow_box = YellowBox(self.name, self.is_back, is_dan=True) + self.yellow_box = YellowBox(False, is_dan=True) self.yellow_box.create_anim() if self.yellow_box is not None: @@ -601,51 +593,49 @@ class DanBox: 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) + def _draw_closed(self, x: float, y: float): + tex.draw_texture('box', 'folder', frame=self.texture_index, x=x) if self.name is not None: self.name.draw(outline_color=ray.BLACK, x=x + 47 - int(self.name.texture.width / 2), y=y+35, y2=min(self.name.texture.height, 417)-self.name.texture.height) - def _draw_open(self, x: int, y: int, is_ura: bool): + def _draw_open(self, x: float, y: float, fade_override: Optional[float], is_ura: bool): + if fade_override is not None: + fade = fade_override + else: + fade = 1.0 if self.yellow_box is not None: - self.yellow_box.draw(None, None, False) + self.yellow_box.draw(None, None, False, self.name) for i, song in enumerate(self.song_text): title, subtitle = song x = i * 140 - tex.draw_texture('yellow_box', 'genre_banner', x=x, frame=self.songs[i][1]) - tex.draw_texture('yellow_box', 'difficulty', x=x, frame=self.songs[i][2]) - tex.draw_texture('yellow_box', 'difficulty_x', x=x) - tex.draw_texture('yellow_box', 'difficulty_star', x=x) + tex.draw_texture('yellow_box', 'genre_banner', x=x, frame=self.songs[i][1], fade=fade) + tex.draw_texture('yellow_box', 'difficulty', x=x, frame=self.songs[i][2], fade=fade) + tex.draw_texture('yellow_box', 'difficulty_x', x=x, fade=fade) + tex.draw_texture('yellow_box', 'difficulty_star', x=x, fade=fade) level = self.songs[i][0].metadata.course_data[self.songs[i][2]].level counter = str(level) total_width = len(counter) * 10 for i in range(len(counter)): - tex.draw_texture('yellow_box', 'difficulty_num', frame=int(counter[i]), x=x-(total_width // 2) + (i * 10)) + tex.draw_texture('yellow_box', 'difficulty_num', frame=int(counter[i]), x=x-(total_width // 2) + (i * 10), fade=fade) - title.draw(outline_color=ray.BLACK, x=665+x, y=127, y2=min(title.texture.height, 400)-title.texture.height) - subtitle.draw(outline_color=ray.BLACK, x=620+x, y=525-min(subtitle.texture.height, 400), y2=min(subtitle.texture.height, 400)-subtitle.texture.height) + title.draw(outline_color=ray.BLACK, x=665+x, y=127, y2=min(title.texture.height, 400)-title.texture.height, fade=fade) + subtitle.draw(outline_color=ray.BLACK, x=620+x, y=525-min(subtitle.texture.height, 400), y2=min(subtitle.texture.height, 400)-subtitle.texture.height, fade=fade) - tex.draw_texture('yellow_box', 'total_notes_bg') - tex.draw_texture('yellow_box', 'total_notes') + tex.draw_texture('yellow_box', 'total_notes_bg', fade=fade) + tex.draw_texture('yellow_box', 'total_notes', fade=fade) counter = str(self.total_notes) for i in range(len(counter)): - tex.draw_texture('yellow_box', 'total_notes_counter', frame=int(counter[i]), x=(i * 25)) + tex.draw_texture('yellow_box', 'total_notes_counter', frame=int(counter[i]), x=(i * 25), fade=fade) - tex.draw_texture('yellow_box', 'frame', frame=self.color) + tex.draw_texture('yellow_box', 'frame', frame=self.texture_index, fade=fade) 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.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, fade=fade) 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) - else: - self._draw_closed(x, y) - class GenreBG: """The background for a genre box.""" - def __init__(self, start_box: SongBox, end_box: SongBox, title: OutlinedText, diff_sort: Optional[int]): + def __init__(self, start_box: BaseBox, end_box: BaseBox, title: OutlinedText, diff_sort: Optional[int]): self.start_box = start_box self.end_box = end_box self.start_position = start_box.position @@ -694,7 +684,7 @@ class GenreBG: tex.draw_texture('box', 'folder_background_folder_edge', x=((offset+dest_width)//2)+20, y=y-2, fade=self.fade_in.attribute, frame=self.end_box.texture_index) if self.diff_num is not None: tex.draw_texture('diff_sort', 'star_num', frame=self.diff_num, x=-150 + (dest_width//2), y=-143) - self.title.draw(outline_color=ray.BLACK, x=(1280//2) - (dest_width//2)-(offset//2), y=y-60, x2=dest_width - self.title.texture.width, color=ray.fade(ray.WHITE, self.fade_in.attribute)) + self.title.draw(outline_color=ray.BLACK, x=(SCREEN_WIDTH//2) - (dest_width//2)-(offset//2), y=y-60, x2=dest_width - self.title.texture.width, color=ray.fade(ray.WHITE, self.fade_in.attribute)) class ScoreHistory: """The score information that appears while hovering over a song""" @@ -872,7 +862,10 @@ class Directory(FileSystemItem): elif self.to_root or self.back: texture_index = SongBox.BACK_INDEX - self.box = SongBox(name, texture_index, True, tja_count=tja_count, box_texture=box_texture) + if self.back: + self.box = BackBox(name, texture_index) + else: + self.box = FolderBox(name, texture_index, tja_count=tja_count, box_texture=box_texture) class SongFile(FileSystemItem): """Represents a song file (TJA) in the navigation system""" @@ -884,7 +877,7 @@ class SongFile(FileSystemItem): self.tja.ex_data.new = True title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) self.hash = global_data.song_paths[path] - self.box = SongBox(title, texture_index, False, tja=self.tja, name_texture_index=name_texture_index if name_texture_index is not None else texture_index) + self.box = SongBox(title, texture_index, self.tja, name_texture_index=name_texture_index if name_texture_index is not None else texture_index) self.box.hash = global_data.song_hashes[self.hash][0]["diff_hashes"] self.box.get_scores() @@ -904,7 +897,7 @@ class DanCourse(FileSystemItem): data = json.load(f) self.title = data["title"] self.color = data["color"] - self.charts = [] + self.charts: list[tuple[TJAParser, int, int, int]] = [] for chart in data["charts"]: hash = chart["hash"] #chart_title = chart["title"] @@ -1203,8 +1196,8 @@ class FileNavigator: if self.recent_folder is None: raise Exception("tried to enter recent folder without recents") self._generate_objects_recursive(self.recent_folder.path) - selected_item.box.tja_count_text = None - selected_item.box.tja_count = self._count_tja_files(self.recent_folder.path) + if not isinstance(selected_item.box, BackBox): + selected_item.box.tja_count = self._count_tja_files(self.recent_folder.path) content_items = self.directory_contents[dir_key] elif selected_item.collection == Directory.COLLECTIONS[2]: if self.favorite_folder is None: @@ -1212,8 +1205,8 @@ class FileNavigator: self._generate_objects_recursive(self.favorite_folder.path) tja_files = self._get_tja_files_for_directory(self.favorite_folder.path) self._calculate_directory_crowns(dir_key, tja_files) - selected_item.box.tja_count_text = None - selected_item.box.tja_count = self._count_tja_files(self.favorite_folder.path) + if not isinstance(selected_item.box, BackBox): + selected_item.box.tja_count = self._count_tja_files(self.favorite_folder.path) content_items = self.directory_contents[dir_key] self.in_favorites = True elif selected_item.collection == Directory.COLLECTIONS[3]: @@ -1245,7 +1238,7 @@ class FileNavigator: return i = 1 for item in content_items: - if isinstance(item, SongFile): + if isinstance(item, SongFile) and not has_children: if i % 10 == 0 and i != 0: back_dir = Directory(self.current_dir.parent, "", SongBox.BACK_INDEX, back=True) self.items.insert(self.selected_index+i, back_dir) @@ -1268,24 +1261,22 @@ class FileNavigator: 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 + if isinstance(item.box, FolderBox): item.box.crown = self._get_directory_crowns_cached(item_key) - else: - # Navigation items (back/to_root) - item.box.crown = dict() self.calculate_box_positions() - if (not has_children and start_box is not None - and end_box is not None and selected_item is not None - and selected_item.box.hori_name is not None): - hori_name = selected_item.box.hori_name - diff_sort = None - if selected_item.collection == Directory.COLLECTIONS[3]: - diff_sort = self.diff_sort_level - diffs = ['かんたん', 'ふつう', 'むずかしい', 'おに'] - hori_name = OutlinedText(diffs[min(Difficulty.ONI, self.diff_sort_diff)], 40, ray.WHITE, outline_thickness=5) - self.genre_bg = GenreBG(start_box, end_box, hori_name, diff_sort) + if selected_item and isinstance(selected_item.box, FolderBox): + if (not has_children and start_box is not None + and end_box is not None and selected_item is not None + and selected_item.box.hori_name is not None): + hori_name = selected_item.box.hori_name + diff_sort = None + if selected_item.collection == Directory.COLLECTIONS[3]: + diff_sort = self.diff_sort_level + diffs = ['かんたん', 'ふつう', 'むずかしい', 'おに'] + hori_name = OutlinedText(diffs[min(Difficulty.ONI, self.diff_sort_diff)], 40, ray.WHITE, outline_thickness=5) + self.genre_bg = GenreBG(start_box, end_box, hori_name, diff_sort) def select_current_item(self): """Select the currently highlighted item""" @@ -1488,15 +1479,18 @@ class FileNavigator: side_offset_l = 200 side_offset_r = 500 - position = BOX_CENTER + (base_spacing * offset) - if position == BOX_CENTER: + position = (BOX_CENTER - 150) + (base_spacing * offset) + if position == BOX_CENTER - 150: + #item.box.is_open = True position += center_offset - elif position > BOX_CENTER: + elif position > BOX_CENTER - 150: + #item.box.is_open = False position += side_offset_r else: position -= side_offset_l + #item.box.is_open = False - if item.box.position == -11111: + if item.box.position == float('inf'): item.box.position = position item.box.target_position = position else: @@ -1535,8 +1529,10 @@ class FileNavigator: def reset_items(self): """Reset the items in the song select scene""" - for item in self.items: - item.box.reset() + song = self.get_current_item() + if isinstance(song.box, SongBox): + if song.box.yellow_box is not None: + song.box.yellow_box.create_anim() def add_recent(self): """Add the current song to the recent list""" diff --git a/libs/global_objects.py b/libs/global_objects.py index 874be87..d943444 100644 --- a/libs/global_objects.py +++ b/libs/global_objects.py @@ -1,4 +1,5 @@ from enum import Enum +from typing import Callable import pyray as ray from libs.global_data import PlayerNum @@ -20,8 +21,8 @@ class Nameplate: is_rainbow (bool): Whether the player's nameplate background is rainbow. title_bg (int): The player's non-rainbow nameplate background. """ - self.name = OutlinedText(name, 22, ray.WHITE, outline_thickness=3.0) - self.title = OutlinedText(title, 20, ray.BLACK, outline_thickness=0) + self.name = OutlinedText(name, global_tex.skin_config["nameplate_text_name"].font_size, ray.WHITE, outline_thickness=3.0) + self.title = OutlinedText(title, global_tex.skin_config["nameplate_text_title"].font_size, ray.BLACK, outline_thickness=0) self.dan_index = dan self.player_num = player_num self.is_gold = is_gold @@ -60,7 +61,7 @@ class Nameplate: title_offset = 0 else: frame = self.title_bg - title_offset = 14 + title_offset = tex.skin_config["nameplate_title_offset"].x if self.is_rainbow: if 0 < self.rainbow_animation.attribute < 6: tex.draw_texture('nameplate', 'frame_top_rainbow', frame=self.rainbow_animation.attribute-1, x=x, y=y, fade=fade) @@ -75,12 +76,12 @@ class Nameplate: tex.draw_texture('nameplate', 'dan_emblem_gold', x=x, y=y, frame=self.dan_index, fade=fade) else: tex.draw_texture('nameplate', 'dan_emblem', x=x, y=y, frame=self.dan_index, fade=fade) - offset = 34 + offset = tex.skin_config["nameplate_dan_offset"].x if self.player_num != 0: tex.draw_texture('nameplate', f'{self.player_num}p', x=x, y=y, fade=fade) - self.name.draw(outline_color=ray.BLACK, x=x+136 - (min(255 - offset*4, self.name.texture.width)//2) + offset, y=y+24, x2=min(255 - offset*4, self.name.texture.width)-self.name.texture.width, color=ray.fade(ray.WHITE, fade)) - self.title.draw(x=x+136 - (min(255 - offset*2, self.title.texture.width)//2) + title_offset, y=y-3, x2=min(255 - offset*2, self.title.texture.width)-self.title.texture.width, color=ray.fade(ray.WHITE, fade)) + self.name.draw(outline_color=ray.BLACK, x=x+tex.skin_config["nameplate_text_name"].x - (min(tex.skin_config["nameplate_text_name"].width - offset*4, self.name.texture.width)//2) + offset, y=y+tex.skin_config["nameplate_text_name"].y, x2=min(tex.skin_config["nameplate_text_name"].width - offset*4, self.name.texture.width)-self.name.texture.width, color=ray.fade(ray.WHITE, fade)) + self.title.draw(x=x+tex.skin_config["nameplate_text_title"].x - (min(tex.skin_config["nameplate_text_title"].width - offset*2, self.title.texture.width)//2) + title_offset, y=y+tex.skin_config["nameplate_text_title"].y, x2=min(tex.skin_config["nameplate_text_title"].width - offset*2, self.title.texture.width)-self.title.texture.width, color=ray.fade(ray.WHITE, fade)) class Indicator: """Indicator class for displaying drum navigation.""" @@ -112,10 +113,10 @@ class Indicator: if self.state == Indicator.State.SELECT: tex.draw_texture('indicator', 'drum_kat', fade=min(fade, self.don_fade.attribute), x=x, y=y) - tex.draw_texture('indicator', 'drum_kat', fade=min(fade, self.don_fade.attribute), x=x+23, y=y, mirror='horizontal') - tex.draw_texture('indicator', 'drum_face', x=x+175, y=y, fade=fade) + tex.draw_texture('indicator', 'drum_kat', fade=min(fade, self.don_fade.attribute), x=x+tex.skin_config["indicator_kat_offset"].x, y=y, mirror='horizontal') + tex.draw_texture('indicator', 'drum_face', x=x+tex.skin_config["indicator_face_offset"].x, y=y, fade=fade) - tex.draw_texture('indicator', 'drum_don', fade=min(fade, self.don_fade.attribute), index=self.state.value, x=x+214, y=y) + tex.draw_texture('indicator', 'drum_don', fade=min(fade, self.don_fade.attribute), index=self.state.value, x=x+tex.skin_config["indicator_don_offset"].x, y=y) tex.draw_texture('indicator', 'blue_arrow', x=x-self.blue_arrow_move.attribute, y=y, fade=min(fade, self.blue_arrow_fade.attribute)) tex.draw_texture('indicator', 'blue_arrow', index=1, x=x+self.blue_arrow_move.attribute, y=y, mirror='horizontal', fade=min(fade, self.blue_arrow_fade.attribute)) else: @@ -169,7 +170,7 @@ class EntryOverlay: class Timer: """Timer class for displaying countdown timers.""" - def __init__(self, time: int, current_time_ms: float, confirm_func): + def __init__(self, time: int, current_time_ms: float, confirm_func: Callable): """ Initialize a Timer object. @@ -222,7 +223,7 @@ class Timer: else: tex.draw_texture('timer', 'bg') counter_name = 'counter_black' - margin = 40 + margin = tex.skin_config["timer_text_margin"].x total_width = len(self.counter) * margin for i, digit in enumerate(self.counter): tex.draw_texture('timer', counter_name, frame=int(digit), x=-(total_width//2)+(i*margin), scale=self.num_resize.attribute, center=True) diff --git a/libs/texture.py b/libs/texture.py index 6e6fec8..9b29f32 100644 --- a/libs/texture.py +++ b/libs/texture.py @@ -14,13 +14,17 @@ from libs.animation import BaseAnimation, parse_animations SCREEN_WIDTH = 1280 SCREEN_HEIGHT = 720 +SCREEN_SCALE = SCREEN_WIDTH / 1280 logger = logging.getLogger(__name__) -class Coordinates: - def __init__(self, x: float, y: float): +class SkinInfo: + def __init__(self, x: float, y: float, font_size: int, width: float, height: float): self.x = x self.y = y + self.width = width + self.height = height + self.font_size = font_size class Texture: """Texture class for managing textures and animations.""" @@ -47,12 +51,12 @@ class TextureWrapper: def __init__(self): self.textures: dict[str, dict[str, Texture]] = dict() self.animations: dict[int, BaseAnimation] = dict() - self.skin_config: dict[str, Coordinates] = dict() + self.skin_config: dict[str, SkinInfo] = dict() self.graphics_path = Path("Graphics") if (self.graphics_path / "skin_config.json").exists(): data = json.loads((self.graphics_path / "skin_config.json").read_text()) - self.skin_config: dict[str, Coordinates] = { - k: Coordinates(v['x'], v['y']) for k, v in data.items() + self.skin_config: dict[str, SkinInfo] = { + k: SkinInfo(v.get('x', 0), v.get('y', 0), v.get('font_size', 0), v.get('width', 0), v.get('height', 0)) for k, v in data.items() } def unload_textures(self): diff --git a/libs/utils.py b/libs/utils.py index 32b5600..714019b 100644 --- a/libs/utils.py +++ b/libs/utils.py @@ -18,7 +18,7 @@ from raylib import ( SHADER_UNIFORM_VEC4, ) -from libs.texture import TextureWrapper +from libs.texture import SCREEN_WIDTH, TextureWrapper logger = logging.getLogger(__name__) @@ -230,7 +230,7 @@ class OutlinedText: """ self.text = text self.hash = self._hash_text(text, font_size, color, vertical) - self.outline_thickness = outline_thickness + self.outline_thickness = outline_thickness * (SCREEN_WIDTH/1280) if self.hash in text_cache: self.texture = ray.load_texture(f'cache/image/{self.hash}.png') else: diff --git a/libs/video.py b/libs/video.py index d6b99fa..51457a1 100644 --- a/libs/video.py +++ b/libs/video.py @@ -6,6 +6,7 @@ from moviepy import VideoFileClip from libs.audio import audio from libs.utils import get_current_ms +from libs.texture import SCREEN_WIDTH, SCREEN_HEIGHT logger = logging.getLogger(__name__) @@ -119,7 +120,7 @@ class VideoPlayer: def draw(self): """Draw video frames to the raylib canvas""" if self.texture is not None: - ray.DrawTexture(self.texture, 0, 0, ray.WHITE) + ray.DrawTexturePro(self.texture, (0, 0, self.texture.width, self.texture.height), (0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), (0, 0), 0, ray.WHITE) def stop(self): """Stops the video, audio, and clears its buffer""" diff --git a/scenes/dan/dan_select.py b/scenes/dan/dan_select.py index 6b771b7..d2fb781 100644 --- a/scenes/dan/dan_select.py +++ b/scenes/dan/dan_select.py @@ -4,14 +4,14 @@ import pyray as ray from libs.audio import audio from libs.global_data import PlayerNum, global_data -from libs.texture import tex +from libs.texture import SCREEN_HEIGHT, SCREEN_WIDTH, 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 DanCourse, navigator +from libs.file_navigator import BackBox, 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 +from scenes.song_select import State logger = logging.getLogger(__name__) @@ -35,7 +35,7 @@ class DanSelectScreen(Screen): session_data = global_data.session_data[global_data.player_num] current_item = self.navigator.get_current_item() if isinstance(current_item, DanCourse): - session_data.selected_song = current_item.charts[0] + session_data.selected_song = current_item.charts[0][0].file_path session_data.selected_dan = current_item.charts session_data.selected_dan_exam = current_item.exams session_data.song_title = current_item.title @@ -88,8 +88,9 @@ class DanSelectScreen(Screen): 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 + if not song.box.text_loaded: + song.box.load_text() + song.box.update(current_time, False) self.player.update(current_time) res = self.handle_input(self.state, self) if res == 'go_back': @@ -102,13 +103,13 @@ class DanSelectScreen(Screen): tex.draw_texture('global', 'footer') for item in self.navigator.items: box = item.box - if -156 <= box.position <= 1280 + 144: + if -156 <= box.position <= SCREEN_WIDTH + 144: if box.position <= 500: 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))) + ray.draw_rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 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() @@ -169,7 +170,7 @@ class DanSelectPlayer: # Select/Enter if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num): - if selected_item is not None and selected_item.box.is_back: + if selected_item is not None and isinstance(selected_item.box, BackBox): audio.play_sound('cancel', 'sound') return "go_back" else: diff --git a/scenes/dan/game_dan.py b/scenes/dan/game_dan.py index 2ca3d31..1b4789a 100644 --- a/scenes/dan/game_dan.py +++ b/scenes/dan/game_dan.py @@ -185,7 +185,10 @@ class DanGameScreen(GameScreen): self.transition.update(current_time) self.current_ms = current_time - self.start_ms self.dan_transition.update(current_time) - self.start_song(current_time) + if self.transition.is_finished and self.dan_transition.is_finished: + self.start_song(self.current_ms) + else: + self.start_ms = current_time - self.tja.metadata.offset*1000 self.update_background(current_time) if self.song_music is not None: diff --git a/scenes/entry.py b/scenes/entry.py index 00e831a..bc1066b 100644 --- a/scenes/entry.py +++ b/scenes/entry.py @@ -104,7 +104,7 @@ class EntryScreen(Screen): self.state = State.SELECT_SIDE plate_info = global_data.config['nameplate_2p'] self.nameplate = Nameplate(plate_info['name'], plate_info['title'], PlayerNum.ALL, -1, False, False, 1) - self.chara = Chara2D(1, 100) + self.chara = Chara2D(1) self.side_select_fade.restart() self.side = 1 elif self.players[0] and self.players[0].player_num == PlayerNum.P2 and is_l_don_pressed(PlayerNum.P1) or is_r_don_pressed(PlayerNum.P1): @@ -291,19 +291,19 @@ class EntryPlayer: move_y = self.drum_move_1.attribute + self.drum_move_2.attribute if self.side == 0: # Left side (1P/red) - offset = 0 + offset = tex.skin_config["entry_drum_offset"].x tex.draw_texture('side_select', 'red_drum', x=move_x, y=move_y) - chara_x = move_x + offset + 170 + chara_x = move_x + offset + tex.skin_config["entry_chara_offset_l"].x chara_mirror = False else: # Right side (2P/blue) move_x *= -1 - offset = 620 + offset = tex.skin_config["entry_drum_offset"].y # bad practice to use y value as x value tex.draw_texture('side_select', 'blue_drum', x=move_x, y=move_y) - chara_x = move_x + offset + 130 + chara_x = move_x + offset + tex.skin_config["entry_chara_offset_r"].x chara_mirror = True # Draw character - chara_y = 570 + move_y + chara_y = tex.skin_config["entry_chara_offset_r"].y + move_y self.chara.draw(chara_x, chara_y, mirror=chara_mirror) # Draw cloud @@ -414,7 +414,7 @@ class Box: texture_left = tex.textures['mode_select']['box_highlight_left'].texture if isinstance(texture_left, list): raise Exception("highlight textures cannot be iterable") - tex.draw_texture('mode_select', 'box_highlight_center', x=self.left_x + texture_left.width, y=self.y, x2=self.right_x - self.left_x -15, color=color) + tex.draw_texture('mode_select', 'box_highlight_center', x=self.left_x + texture_left.width, y=self.y, x2=self.right_x - self.left_x + tex.skin_config["entry_box_highlight_offset"].x, color=color) tex.draw_texture('mode_select', 'box_highlight_left', x=self.left_x, y=self.y, color=color) tex.draw_texture('mode_select', 'box_highlight_right', x=self.right_x, y=self.y, color=color) @@ -422,7 +422,7 @@ class Box: text_x = self.x + (self.texture.width//2) - (self.text.texture.width//2) if self.is_selected: text_x += self.open.attribute - text_y = self.y + 20 + text_y = self.y + tex.skin_config["entry_box_text_offset"].y if self.is_selected: self.text.draw(outline_color=ray.BLACK, x=text_x, y=text_y, color=color) else: @@ -430,7 +430,7 @@ class Box: def draw(self, fade: float): color = ray.fade(ray.WHITE, fade) - ray.draw_texture(self.texture, self.x, self.y, color) + ray.draw_texture(self.texture, int(self.x), int(self.y), color) if self.is_selected and self.move.is_finished: self._draw_highlighted(color) self._draw_text(color) @@ -439,9 +439,9 @@ class BoxManager: """BoxManager class for the entry screen""" def __init__(self): self.box_titles: list[OutlinedText] = [ - OutlinedText('演奏ゲーム', 50, ray.WHITE, outline_thickness=5, vertical=True), - OutlinedText('特訓モード', 50, ray.WHITE, outline_thickness=5, vertical=True), - OutlinedText('ゲーム設定', 50, ray.WHITE, outline_thickness=5, vertical=True)] + OutlinedText('演奏ゲーム', tex.skin_config["entry_box_text"].font_size, ray.WHITE, outline_thickness=5, vertical=True), + OutlinedText('特訓モード', tex.skin_config["entry_box_text"].font_size, ray.WHITE, outline_thickness=5, vertical=True), + OutlinedText('ゲーム設定', tex.skin_config["entry_box_text"].font_size, ray.WHITE, outline_thickness=5, vertical=True)] self.box_locations = ["SONG_SELECT", "PRACTICE_SELECT", "SETTINGS"] self.num_boxes = len(self.box_titles) self.boxes = [Box(self.box_titles[i], self.box_locations[i]) for i in range(len(self.box_titles))] @@ -449,7 +449,7 @@ class BoxManager: self.fade_out = tex.get_animation(9) self.is_2p = False - spacing = 80 + spacing = tex.skin_config["entry_box_spacing"].x box_width = self.boxes[0].texture.width total_width = self.num_boxes * box_width + (self.num_boxes - 1) * spacing start_x = SCREEN_WIDTH//2 - total_width//2 diff --git a/scenes/loading.py b/scenes/loading.py index 160b363..c03951f 100644 --- a/scenes/loading.py +++ b/scenes/loading.py @@ -7,7 +7,7 @@ from libs.animation import Animation from libs.global_objects import AllNetIcon from libs.screen import Screen from libs.song_hash import build_song_hashes -from libs.texture import tex +from libs.texture import SCREEN_HEIGHT, SCREEN_WIDTH, tex from libs.utils import get_current_ms, global_data from libs.file_navigator import navigator @@ -17,8 +17,8 @@ logger = logging.getLogger(__name__) class LoadScreen(Screen): def __init__(self, name: str): super().__init__(name) - self.width = 1280 - self.height = 720 + self.width = SCREEN_WIDTH + self.height = SCREEN_HEIGHT self.songs_loaded = False self.navigator_started = False self.loading_complete = False diff --git a/scenes/song_select.py b/scenes/song_select.py index b6449ea..b2c0c26 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -5,7 +5,7 @@ from pathlib import Path import pyray as ray import logging -from libs.file_navigator import DanCourse, navigator +from libs.file_navigator import BackBox, DanCourse, navigator from libs.audio import audio from libs.chara_2d import Chara2D from libs.file_navigator import Directory, SongBox, SongFile @@ -79,21 +79,23 @@ class SongSelectScreen(Screen): self.navigator.mark_crowns_dirty_for_song(selected_song) curr_item = self.navigator.get_current_item() - curr_item.box.get_scores() + if isinstance(curr_item, SongFile): + curr_item.box.get_scores() self.navigator.add_recent() - def finalize_song(self): - global_data.session_data[global_data.player_num].selected_song = self.navigator.get_current_item().path + def finalize_song(self, current_item: SongFile): + global_data.session_data[global_data.player_num].selected_song = current_item.path global_data.session_data[global_data.player_num].selected_difficulty = self.player_1.selected_difficulty - global_data.session_data[global_data.player_num].genre_index = self.navigator.get_current_item().box.name_texture_index + global_data.session_data[global_data.player_num].genre_index = current_item.box.name_texture_index def on_screen_end(self, next_screen): self.screen_init = False self.reset_demo_music() - self.finalize_song() - self.player_1.nameplate.unload() current_item = self.navigator.get_current_item() - if current_item.box.yellow_box is not None: + if isinstance(current_item, SongFile): + self.finalize_song(current_item) + self.player_1.nameplate.unload() + if isinstance(current_item.box, SongBox) and current_item.box.yellow_box is not None: current_item.box.yellow_box.create_anim() return super().on_screen_end(next_screen) @@ -156,6 +158,8 @@ class SongSelectScreen(Screen): elif action == "add_favorite": self.navigator.add_favorite() current_box = self.navigator.get_current_item().box + if not isinstance(current_box, SongBox): + return current_box.is_favorite = not current_box.is_favorite def handle_input_selected(self): @@ -281,18 +285,19 @@ class SongSelectScreen(Screen): audio.update_music_stream(self.demo_song) if self.navigator.genre_bg is not None: - self.navigator.genre_bg.update(get_current_ms()) + self.navigator.genre_bg.update(current_time) if self.diff_sort_selector is not None: - self.diff_sort_selector.update(get_current_ms()) + self.diff_sort_selector.update(current_time) self.check_for_selection() for song in self.navigator.items: - song.box.update(self.state == State.SONG_SELECTED) - song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150 + song.box.update(current_time, self.state == State.SONG_SELECTED) + if not song.box.text_loaded: + song.box.load_text() if not isinstance(song, Directory) and song.box.is_open: - if self.demo_song is None and get_current_ms() >= song.box.wait + (83.33*3): + if self.demo_song is None and current_time >= song.box.wait + (83.33*3): song.box.get_scores() if song.tja.metadata.wave.exists() and song.tja.metadata.wave.is_file(): self.demo_song = audio.load_music_stream(song.tja.metadata.wave, 'demo_song') @@ -302,7 +307,7 @@ class SongSelectScreen(Screen): logger.info(f"Demo song loaded and playing for {song.tja.metadata.title}") if song.box.is_open: current_box = song.box - if not current_box.is_back and get_current_ms() >= song.box.wait + (83.33*3): + if not isinstance(current_box, BackBox) and current_time >= song.box.wait + (83.33*3): self.texture_index = current_box.texture_index if ray.is_key_pressed(global_data.config["keys"]["back_key"]): @@ -326,7 +331,7 @@ class SongSelectScreen(Screen): if self.navigator.genre_bg is not None and self.state == State.BROWSING: self.navigator.genre_bg.draw(95) - for item in self.navigator.items: + for i, item in enumerate(self.navigator.items): box = item.box if -156 <= box.position <= SCREEN_WIDTH + 144: if box.position <= 500: @@ -354,7 +359,9 @@ class SongSelectScreen(Screen): self.draw_players() if self.state == State.BROWSING and self.navigator.items != []: - self.navigator.get_current_item().box.draw_score_history() + curr_item = self.navigator.get_current_item() + if isinstance(curr_item, SongFile): + curr_item.box.draw_score_history() self.indicator.draw(410, 575) @@ -461,7 +468,7 @@ class SongSelectPlayer: # Select/Enter if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num): - if selected_item is not None and selected_item.box.is_back: + if selected_item is not None and isinstance(selected_item.box, BackBox): audio.play_sound('cancel', 'sound') return "go_back" elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[3]: diff --git a/scenes/two_player/song_select.py b/scenes/two_player/song_select.py index 3e6a371..5109bd7 100644 --- a/scenes/two_player/song_select.py +++ b/scenes/two_player/song_select.py @@ -1,5 +1,5 @@ import logging -from libs.file_navigator import SongFile +from libs.file_navigator import SongBox, SongFile from libs.global_data import PlayerNum from libs.transition import Transition from scenes.song_select import DiffSortSelect, SongSelectPlayer, SongSelectScreen, State @@ -14,9 +14,9 @@ class TwoPlayerSongSelectScreen(SongSelectScreen): self.player_1 = SongSelectPlayer(PlayerNum.P1, self.text_fade_in) self.player_2 = SongSelectPlayer(PlayerNum.P2, self.text_fade_in) - def finalize_song(self): - global_data.session_data[PlayerNum.P1].selected_song = self.navigator.get_current_item().path - global_data.session_data[PlayerNum.P1].genre_index = self.navigator.get_current_item().box.name_texture_index + def finalize_song(self, current_item): + global_data.session_data[PlayerNum.P1].selected_song = current_item.path + global_data.session_data[PlayerNum.P1].genre_index = current_item.box.name_texture_index logger.info(f"Finalized song selection: {global_data.session_data[PlayerNum.P1].selected_song}") def handle_input(self): @@ -81,6 +81,8 @@ class TwoPlayerSongSelectScreen(SongSelectScreen): elif action == "add_favorite": self.navigator.add_favorite() current_box = self.navigator.get_current_item().box + if not isinstance(current_box, SongBox): + return current_box.is_favorite = not current_box.is_favorite def handle_input_selected(self):