diff --git a/PyTaiko.py b/PyTaiko.py index 9145e25..5e7fdb0 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -37,6 +37,8 @@ class Screens: LOADING = "LOADING" def create_song_db(): + """Create the scores database if it doesn't exist + The migration will eventually be removed""" with sqlite3.connect('scores.db') as con: cursor = con.cursor() create_table_query = ''' diff --git a/libs/audio.py b/libs/audio.py index 6c7e300..c482ad5 100644 --- a/libs/audio.py +++ b/libs/audio.py @@ -111,6 +111,7 @@ except OSError as e: raise class AudioEngine: + """Initialize an audio engine for playing sounds and music.""" def __init__(self, device_type: int, sample_rate: float, buffer_size: int, volume_presets: dict[str, float]): self.device_type = device_type if sample_rate == -1: diff --git a/libs/background.py b/libs/background.py index c9c2ed8..2b91d71 100644 --- a/libs/background.py +++ b/libs/background.py @@ -12,17 +12,52 @@ from libs.bg_objects.renda import RendaController from libs.texture import TextureWrapper class Background: + """The background class for the game.""" COLLABS = { "A3": libs.bg_collabs.a3.Background, "ANIMAL": libs.bg_collabs.animal.Background, "BUTTOBURST": libs.bg_collabs.buttoburst.Background } def __init__(self, player_num: int, bpm: float, scene_preset: str = ''): + """ + Initialize the background class. + + Args: + player_num (int): The player number. + bpm (float): The beats per minute. + scene_preset (str): The scene preset. + """ self.tex_wrapper = TextureWrapper() self.tex_wrapper.load_animations('background') - if scene_preset == '': + if player_num == 3: + if scene_preset == '': + self.max_dancers = 5 + don_bg_num = random.randint(0, 5) + self.don_bg = DonBG.create(self.tex_wrapper, don_bg_num, 1) + self.don_bg_2 = DonBG.create(self.tex_wrapper, don_bg_num, 2) + self.renda = RendaController(self.tex_wrapper, random.randint(0, 2)) + self.chibi = ChibiController(self.tex_wrapper, random.randint(0, 13), bpm) + self.bg_normal = None + self.bg_fever = None + self.footer = None + self.fever = None + self.dancer = None + else: + collab_bg = Background.COLLABS[scene_preset](self.tex_wrapper, 1, bpm) + self.max_dancers = 5 + self.don_bg = collab_bg.don_bg + self.don_bg_2 = collab_bg.don_bg + self.bg_normal = None + self.bg_fever = None + self.footer = None + self.fever = None + self.dancer = None + self.renda = collab_bg.renda + self.chibi = collab_bg.chibi + elif scene_preset == '': self.max_dancers = 5 self.don_bg = DonBG.create(self.tex_wrapper, random.randint(0, 5), player_num) + self.don_bg_2 = None self.bg_normal = BGNormal.create(self.tex_wrapper, random.randint(0, 4)) self.bg_fever = BGFever.create(self.tex_wrapper, random.randint(0, 3)) self.footer = Footer(self.tex_wrapper, random.randint(0, 2)) @@ -34,6 +69,7 @@ class Background: collab_bg = Background.COLLABS[scene_preset](self.tex_wrapper, player_num, bpm) self.max_dancers = collab_bg.max_dancers self.don_bg = collab_bg.don_bg + self.don_bg_2 = None self.bg_normal = collab_bg.bg_normal self.bg_fever = collab_bg.bg_fever self.footer = collab_bg.footer @@ -45,50 +81,87 @@ class Background: self.is_rainbow = False self.last_milestone = 0 - def add_chibi(self, bad: bool): - self.chibi.add_chibi(bad) + def add_chibi(self, bad: bool, player_num: int): + """ + Add a chibi to the background. + + Args: + player_num (int): The player number. + bad (bool): Whether the chibi is bad. + """ + self.chibi.add_chibi(player_num, bad) def add_renda(self): + """ + Add a renda to the background. + """ self.renda.add_renda() def update(self, current_time_ms: float, bpm: float, gauge): - clear_threshold = gauge.clear_start[min(gauge.difficulty, 3)] - if gauge.gauge_length < clear_threshold: - current_milestone = min(self.max_dancers - 1, int(gauge.gauge_length / (clear_threshold / self.max_dancers))) - else: - current_milestone = self.max_dancers - if current_milestone > self.last_milestone and current_milestone < self.max_dancers: - self.dancer.add_dancer() - self.last_milestone = current_milestone - if not self.is_clear and gauge.is_clear: - self.bg_fever.start() - if not self.is_rainbow and gauge.is_rainbow and self.fever is not None: - self.fever.start() + """ + Update the background. + + Args: + current_time_ms (float): The current time in milliseconds. + bpm (float): The beats per minute. + gauge (Gauge): The gauge object. + """ + if self.dancer is not None: + clear_threshold = gauge.clear_start[min(gauge.difficulty, 3)] + if gauge.gauge_length < clear_threshold: + current_milestone = min(self.max_dancers - 1, int(gauge.gauge_length / (clear_threshold / self.max_dancers))) + else: + current_milestone = self.max_dancers + if current_milestone > self.last_milestone and current_milestone < self.max_dancers: + self.dancer.add_dancer() + self.last_milestone = current_milestone + if self.bg_fever is not None: + if not self.is_clear and gauge.is_clear: + self.bg_fever.start() + if not self.is_rainbow and gauge.is_rainbow and self.fever is not None: + self.fever.start() self.is_clear = gauge.is_clear self.is_rainbow = gauge.is_rainbow self.don_bg.update(current_time_ms, self.is_clear) - self.bg_normal.update(current_time_ms) - self.bg_fever.update(current_time_ms) + if self.don_bg_2 is not None: + self.don_bg_2.update(current_time_ms, self.is_clear) + if self.bg_normal is not None: + self.bg_normal.update(current_time_ms) + if self.bg_fever is not None: + self.bg_fever.update(current_time_ms) if self.fever is not None: self.fever.update(current_time_ms, bpm) - self.dancer.update(current_time_ms, bpm) + if self.dancer is not None: + self.dancer.update(current_time_ms, bpm) self.renda.update(current_time_ms) self.chibi.update(current_time_ms, bpm) + def draw(self): - if self.is_clear and not self.bg_fever.transitioned: - self.bg_normal.draw(self.tex_wrapper) - self.bg_fever.draw(self.tex_wrapper) - elif self.is_clear: - self.bg_fever.draw(self.tex_wrapper) - else: - self.bg_normal.draw(self.tex_wrapper) + """ + Draw the background. + """ + if self.bg_normal is not None: + if self.is_clear and not self.bg_fever.transitioned: + self.bg_normal.draw(self.tex_wrapper) + self.bg_fever.draw(self.tex_wrapper) + elif self.is_clear: + self.bg_fever.draw(self.tex_wrapper) + else: + self.bg_normal.draw(self.tex_wrapper) self.don_bg.draw(self.tex_wrapper) + if self.don_bg_2 is not None: + self.don_bg_2.draw(self.tex_wrapper, y=536) self.renda.draw() - self.dancer.draw(self.tex_wrapper) + if self.dancer is not None: + self.dancer.draw(self.tex_wrapper) if self.footer is not None: self.footer.draw(self.tex_wrapper) if self.is_rainbow and self.fever is not None: self.fever.draw(self.tex_wrapper) self.chibi.draw() + def unload(self): + """ + Unload the background. + """ self.tex_wrapper.unload_textures() diff --git a/libs/bg_objects/chibi.py b/libs/bg_objects/chibi.py index 287559a..6bc2c7e 100644 --- a/libs/bg_objects/chibi.py +++ b/libs/bg_objects/chibi.py @@ -7,18 +7,19 @@ import pyray as ray class Chibi: @staticmethod - def create(index: int, bpm: float, bad: bool, tex: TextureWrapper): + def create(index: int, bpm: float, bad: bool, tex: TextureWrapper, is_2p: bool): if bad: - return ChibiBad(index, bpm, tex) + return ChibiBad(index, bpm, tex, is_2p) map = [Chibi0, BaseChibi, Chibi2, BaseChibi, Chibi4, Chibi5, BaseChibi, BaseChibi, Chibi8, BaseChibi, BaseChibi, BaseChibi, BaseChibi, Chibi13] selected_obj = map[index] - return selected_obj(index, bpm, tex) + return selected_obj(index, bpm, tex, is_2p) class BaseChibi: - def __init__(self, index: int, bpm: float, tex: TextureWrapper): + def __init__(self, index: int, bpm: float, tex: TextureWrapper, is_2p: bool): self.name = 'chibi_' + str(index) self.bpm = bpm + self.is_2p = is_2p self.hori_move = Animation.create_move(60000 / self.bpm * 5, total_distance=1280) self.hori_move.start() self.vert_move = Animation.create_move(60000 / self.bpm / 2, total_distance=50, reverse_delay=0) @@ -42,10 +43,11 @@ class BaseChibi: self.texture_change.restart() def draw(self, tex: TextureWrapper): - tex.draw_texture(self.name, str(self.index), frame=self.texture_change.attribute, x=self.hori_move.attribute, y=-self.vert_move.attribute) + tex.draw_texture(self.name, str(self.index), frame=self.texture_change.attribute, x=self.hori_move.attribute, y=-self.vert_move.attribute+(self.is_2p*535)) class ChibiBad(BaseChibi): - def __init__(self, index: int, bpm: float, tex: TextureWrapper): + def __init__(self, index: int, bpm: float, tex: TextureWrapper, is_2p: bool): + self.is_2p = is_2p self.bpm = bpm self.index = random.randint(0, 2) self.keyframes = [3, 4] @@ -72,17 +74,17 @@ class ChibiBad(BaseChibi): def draw(self, tex: TextureWrapper): if not self.s_texture_change.is_finished: - tex.draw_texture('chibi_bad', '0', frame=self.s_texture_change.attribute, x=self.hori_move.attribute, y=self.vert_move.attribute, fade=self.fade_in.attribute) + tex.draw_texture('chibi_bad', '0', frame=self.s_texture_change.attribute, x=self.hori_move.attribute, y=self.vert_move.attribute+(self.is_2p*535), fade=self.fade_in.attribute) else: - tex.draw_texture('chibi_bad', '0', frame=self.texture_change.attribute, x=self.hori_move.attribute, y=self.vert_move.attribute) + tex.draw_texture('chibi_bad', '0', frame=self.texture_change.attribute, x=self.hori_move.attribute, y=self.vert_move.attribute+(self.is_2p*535)) class Chibi0(BaseChibi): def draw(self, tex: TextureWrapper): - tex.draw_texture(self.name, str(self.index), frame=self.texture_change.attribute, x=self.hori_move.attribute, y=self.vert_move.attribute) + tex.draw_texture(self.name, str(self.index), frame=self.texture_change.attribute, x=self.hori_move.attribute, y=self.vert_move.attribute+(self.is_2p*535)) class Chibi2(BaseChibi): - def __init__(self, index: int, bpm: float, tex: TextureWrapper): - super().__init__(index, bpm, tex) + def __init__(self, index: int, bpm: float, tex: TextureWrapper, is_2p: bool): + super().__init__(index, bpm, tex, is_2p) self.rotate = Animation.create_move(60000 / self.bpm / 2, total_distance=360, reverse_delay=0) self.rotate.start() @@ -94,23 +96,23 @@ class Chibi2(BaseChibi): def draw(self, tex: TextureWrapper): origin = ray.Vector2(64, 64) - tex.draw_texture(self.name, str(self.index), x=self.hori_move.attribute+origin.x, y=origin.y, origin=origin, rotation=self.rotate.attribute) + tex.draw_texture(self.name, str(self.index), x=self.hori_move.attribute+origin.x, y=origin.y+(self.is_2p*535), origin=origin, rotation=self.rotate.attribute) class Chibi4(BaseChibi): def draw(self, tex: TextureWrapper): - tex.draw_texture(self.name, str(self.index), frame=self.texture_change.attribute, x=self.hori_move.attribute) + tex.draw_texture(self.name, str(self.index), frame=self.texture_change.attribute, x=self.hori_move.attribute, y=(self.is_2p*535)) class Chibi5(BaseChibi): def draw(self, tex: TextureWrapper): - tex.draw_texture(self.name, str(self.index), frame=self.texture_change.attribute, x=self.hori_move.attribute) + tex.draw_texture(self.name, str(self.index), frame=self.texture_change.attribute, x=self.hori_move.attribute, y=(self.is_2p*535)) class Chibi8(BaseChibi): def draw(self, tex: TextureWrapper): - tex.draw_texture(self.name, str(self.index), frame=self.texture_change.attribute, x=self.hori_move.attribute) + tex.draw_texture(self.name, str(self.index), frame=self.texture_change.attribute, x=self.hori_move.attribute, y=(self.is_2p*535)) class Chibi13(BaseChibi): - def __init__(self, index: int, bpm: float, tex: TextureWrapper): - super().__init__(index, bpm, tex) + def __init__(self, index: int, bpm: float, tex: TextureWrapper, is_2p: bool): + super().__init__(index, bpm, tex, is_2p) duration = (60000 / self.bpm) self.scale = Animation.create_fade(duration, initial_opacity=1.0, final_opacity=0.75, delay=duration, reverse_delay=duration) self.scale.start() @@ -129,9 +131,9 @@ class Chibi13(BaseChibi): def draw(self, tex: TextureWrapper): tex.draw_texture(self.name, 'tail', frame=self.frame, x=self.hori_move.attribute, y=-self.vert_move.attribute) if self.scale.attribute == 0.75: - tex.draw_texture(self.name, str(self.index), frame=self.frame, x=self.hori_move.attribute, y=-self.vert_move.attribute) + tex.draw_texture(self.name, str(self.index), frame=self.frame, x=self.hori_move.attribute, y=-self.vert_move.attribute+(self.is_2p*535)) else: - tex.draw_texture(self.name, str(self.index), scale=self.scale.attribute, center=True, frame=self.frame, x=self.hori_move.attribute, y=-self.vert_move.attribute) + tex.draw_texture(self.name, str(self.index), scale=self.scale.attribute, center=True, frame=self.frame, x=self.hori_move.attribute, y=-self.vert_move.attribute+(self.is_2p*535)) class ChibiController: @@ -144,8 +146,8 @@ class ChibiController: tex.load_zip(path, f'chibi/{self.name}') tex.load_zip('background', 'chibi/chibi_bad') - def add_chibi(self, bad=False): - self.chibis.append(Chibi.create(self.index, self.bpm, bad, self.tex)) + def add_chibi(self, player_num: int, bad=False): + self.chibis.append(Chibi.create(self.index, self.bpm, bad, self.tex, player_num == 2)) def update(self, current_time_ms: float, bpm: float): self.bpm = bpm diff --git a/libs/bg_objects/don_bg.py b/libs/bg_objects/don_bg.py index 6f7b9ea..9fba140 100644 --- a/libs/bg_objects/don_bg.py +++ b/libs/bg_objects/don_bg.py @@ -31,17 +31,17 @@ class DonBG1(DonBGBase): def update(self, current_time_ms: float, is_clear: bool): super().update(current_time_ms, is_clear) self.overlay_move.update(current_time_ms) - def draw(self, tex: TextureWrapper): - self._draw_textures(tex, 1.0) + def draw(self, tex: TextureWrapper, y: float=0): + self._draw_textures(tex, 1.0, y) if self.is_clear: - self._draw_textures(tex, self.clear_fade.attribute) - def _draw_textures(self, tex: TextureWrapper, fade: float): + self._draw_textures(tex, self.clear_fade.attribute, y) + def _draw_textures(self, tex: TextureWrapper, fade: float, y: float): for i in range(5): - tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute) + tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=y) for i in range(6): - tex.draw_texture(self.name, 'overlay', frame=self.is_clear, fade=fade, x=(i*347)+self.move.attribute*(347/328), y=self.overlay_move.attribute) + tex.draw_texture(self.name, 'overlay', frame=self.is_clear, fade=fade, x=(i*347)+self.move.attribute*(347/328), y=self.overlay_move.attribute+y) for i in range(30): - tex.draw_texture(self.name, 'footer', frame=self.is_clear, fade=fade, x=(i*56)+self.move.attribute*((56/328)*3), y=self.overlay_move.attribute) + tex.draw_texture(self.name, 'footer', frame=self.is_clear, fade=fade, x=(i*56)+self.move.attribute*((56/328)*3), y=self.overlay_move.attribute+y) class DonBG2(DonBGBase): def __init__(self, tex: TextureWrapper, index: int, player_num: int, path: str): @@ -50,14 +50,14 @@ class DonBG2(DonBGBase): def update(self, current_time_ms: float, is_clear: bool): super().update(current_time_ms, is_clear) self.overlay_move.update(current_time_ms) - def draw(self, tex: TextureWrapper): - self._draw_textures(tex, 1.0) + def draw(self, tex: TextureWrapper, y: float = 0): + self._draw_textures(tex, 1.0, y) if self.is_clear: - self._draw_textures(tex, self.clear_fade.attribute) - def _draw_textures(self, tex: TextureWrapper, fade: float): + self._draw_textures(tex, self.clear_fade.attribute, y) + def _draw_textures(self, tex: TextureWrapper, fade: float, y: float): for i in range(5): - tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute) - tex.draw_texture(self.name, 'overlay', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=self.overlay_move.attribute) + tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=y) + tex.draw_texture(self.name, 'overlay', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=self.overlay_move.attribute+y) class DonBG3(DonBGBase): def __init__(self, tex: TextureWrapper, index: int, player_num: int, path: str): @@ -79,17 +79,17 @@ class DonBG3(DonBGBase): self.overlay_move.update(current_time_ms) self.overlay_move_2.update(current_time_ms) - def draw(self, tex: TextureWrapper): - self._draw_textures(tex, 1.0) + def draw(self, tex: TextureWrapper, y: float = 0): + self._draw_textures(tex, 1.0, y) if self.is_clear: - self._draw_textures(tex, self.clear_fade.attribute) + self._draw_textures(tex, self.clear_fade.attribute, y) - def _draw_textures(self, tex: TextureWrapper, fade: float): + def _draw_textures(self, tex: TextureWrapper, fade: float, y: float): for i in range(10): - tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*164)+self.move.attribute) - y = self.bounce_up.attribute - self.bounce_down.attribute + self.overlay_move.attribute + self.overlay_move_2.attribute + tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*164)+self.move.attribute, y=y) + y_pos = self.bounce_up.attribute - self.bounce_down.attribute + self.overlay_move.attribute + self.overlay_move_2.attribute for i in range(6): - tex.draw_texture(self.name, 'overlay', frame=self.is_clear, fade=fade, x=(i*328)+(self.move.attribute*2), y=y) + tex.draw_texture(self.name, 'overlay', frame=self.is_clear, fade=fade, x=(i*328)+(self.move.attribute*2), y=y_pos+y) class DonBG4(DonBGBase): def __init__(self, tex: TextureWrapper, index: int, player_num: int, path: str): @@ -98,15 +98,15 @@ class DonBG4(DonBGBase): def update(self, current_time_ms: float, is_clear: bool): super().update(current_time_ms, is_clear) self.overlay_move.update(current_time_ms) - def draw(self, tex: TextureWrapper): - self._draw_textures(tex, 1.0) + def draw(self, tex: TextureWrapper, y: float = 0): + self._draw_textures(tex, 1.0, y) if self.is_clear: - self._draw_textures(tex, self.clear_fade.attribute) + self._draw_textures(tex, self.clear_fade.attribute, y) - def _draw_textures(self, tex: TextureWrapper, fade: float): + def _draw_textures(self, tex: TextureWrapper, fade: float, y: float): for i in range(5): - tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute) - tex.draw_texture(self.name, 'overlay', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=self.overlay_move.attribute) + tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=y) + tex.draw_texture(self.name, 'overlay', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=self.overlay_move.attribute+y) class DonBG5(DonBGBase): def __init__(self, tex: TextureWrapper, index: int, player_num: int, path: str): @@ -126,16 +126,16 @@ class DonBG5(DonBGBase): self.bounce_down.restart() self.adjust.update(current_time_ms) - def draw(self, tex: TextureWrapper): - self._draw_textures(tex, 1.0) + def draw(self, tex: TextureWrapper, y: float = 0): + self._draw_textures(tex, 1.0, y) if self.is_clear: - self._draw_textures(tex, self.clear_fade.attribute) + self._draw_textures(tex, self.clear_fade.attribute, y) - def _draw_textures(self, tex: TextureWrapper, fade: float): + def _draw_textures(self, tex: TextureWrapper, fade: float, y: float): for i in range(5): - tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute) + tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=y) for i in range(6): - tex.draw_texture(self.name, 'overlay', frame=self.is_clear, fade=fade, x=(i*368)+(self.move.attribute * ((184/328)*2)), y=self.bounce_up.attribute - self.bounce_down.attribute - self.adjust.attribute) + tex.draw_texture(self.name, 'overlay', frame=self.is_clear, fade=fade, x=(i*368)+(self.move.attribute * ((184/328)*2)), y=self.bounce_up.attribute - self.bounce_down.attribute - self.adjust.attribute + y) class DonBG6(DonBGBase): def __init__(self, tex: TextureWrapper, index: int, player_num: int, path: str): @@ -144,15 +144,15 @@ class DonBG6(DonBGBase): def update(self, current_time_ms: float, is_clear: bool): super().update(current_time_ms, is_clear) self.overlay_move.update(current_time_ms) - def draw(self, tex: TextureWrapper): - self._draw_textures(tex, 1.0) + def draw(self, tex: TextureWrapper, y: float = 0): + self._draw_textures(tex, 1.0, y) if self.is_clear: - self._draw_textures(tex, self.clear_fade.attribute) + self._draw_textures(tex, self.clear_fade.attribute, y) - def _draw_textures(self, tex: TextureWrapper, fade: float): + def _draw_textures(self, tex: TextureWrapper, fade: float, y: float): for i in range(5): - tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute) + tex.draw_texture(self.name, 'background', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=y) for i in range(0, 6, 2): - tex.draw_texture(self.name, 'overlay_1', frame=self.is_clear, fade=fade, x=(i*264) + self.move.attribute*3, y=-self.move.attribute*0.85) + tex.draw_texture(self.name, 'overlay_1', frame=self.is_clear, fade=fade, x=(i*264) + self.move.attribute*3, y=-self.move.attribute*0.85+y) for i in range(5): - tex.draw_texture(self.name, 'overlay_2', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=self.overlay_move.attribute) + tex.draw_texture(self.name, 'overlay_2', frame=self.is_clear, fade=fade, x=(i*328)+self.move.attribute, y=self.overlay_move.attribute+y) diff --git a/libs/chara_2d.py b/libs/chara_2d.py index d6b8d18..5cc6f78 100644 --- a/libs/chara_2d.py +++ b/libs/chara_2d.py @@ -3,6 +3,14 @@ from libs.utils import global_tex class Chara2D: def __init__(self, index: int, bpm: float, path: str = 'chara'): + """ + Initialize a Chara2D object. + + Args: + index (int): The index of the character. + bpm (float): The beats per minute. + path (str, optional): The path to the character's textures. Defaults to 'chara'. + """ self.name = "chara_" + str(index) self.tex = global_tex self.anims = dict() @@ -27,6 +35,12 @@ class Chara2D: self.anims[name].start() def set_animation(self, name: str): + """ + Set the current animation for the character. + + Args: + name (str): The name of the animation to set. + """ if name == self.current_anim: return if self.current_anim in self.temp_anims: @@ -49,6 +63,15 @@ class Chara2D: self.current_anim = name self.anims[name].start() def update(self, current_time_ms: float, bpm: float, is_clear: bool, is_rainbow: bool): + """ + Update the character's animation state and appearance. + + Args: + current_time_ms (float): The current time in milliseconds. + bpm (float): The beats per minute. + is_clear (bool): Whether the gauge is in clear mode. + is_rainbow (bool): Whether the gauge is in rainbow mode. + """ if is_rainbow and not self.is_rainbow: self.is_rainbow = True self.set_animation('soul_in') @@ -76,6 +99,14 @@ class Chara2D: self.anims[self.current_anim].restart() def draw(self, x: float = 0, y: float = 0, mirror=False): + """ + Draw the character on the screen. + + Args: + x (float): The x-coordinate of the character's position. + y (float): The y-coordinate of the character's position. + mirror (bool): Whether to mirror the character horizontally. + """ if self.is_rainbow and self.current_anim not in {'soul_in', 'balloon_pop', 'balloon_popping'}: self.tex.draw_texture(self.name, self.current_anim + '_max', frame=self.anims[self.current_anim].attribute, x=x, y=y) else: diff --git a/libs/global_data.py b/libs/global_data.py index 088d3aa..37faa0b 100644 --- a/libs/global_data.py +++ b/libs/global_data.py @@ -1,8 +1,12 @@ from dataclasses import dataclass, field from pathlib import Path +from typing import Any @dataclass class Modifiers: + """ + Modifiers for the game. + """ auto: bool = False speed: float = 1.0 display: bool = False @@ -11,9 +15,25 @@ class Modifiers: @dataclass 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. + song_paths (dict[Path, str]): A dictionary mapping song paths to their hashes. + song_progress (float): The progress of the loading bar. + total_songs (int): The total number of songs. + hit_sound (int): The index of the hit sound currently used. + player_num (int): The player number. Either 1 or 2. + input_locked (int): The input lock status. 0 means unlocked, 1 or greater means locked. + modifiers (Modifiers): The modifiers for the game. + """ selected_song: Path = Path() songs_played: int = 0 - config: dict = field(default_factory=lambda: dict()) + config: dict[str, Any] = field(default_factory=lambda: dict()) song_hashes: dict[str, list[dict]] = field(default_factory=lambda: dict()) #Hash to path song_paths: dict[Path, str] = field(default_factory=lambda: dict()) #path to hash song_progress: float = 0.0 diff --git a/libs/global_objects.py b/libs/global_objects.py index 4569d51..dc580f5 100644 --- a/libs/global_objects.py +++ b/libs/global_objects.py @@ -6,19 +6,43 @@ from libs.audio import audio class Nameplate: + """Nameplate for displaying player information.""" def __init__(self, name: str, title: str, player_num: int, dan: int, is_gold: bool): + """Initialize a Nameplate object. + + Args: + name (str): The player's name. + title (str): The player's title. + player_num (int): The player's number. + dan (int): The player's dan level. + is_gold (bool): Whether the player's dan is gold. + """ self.name = OutlinedText(name, 22, ray.WHITE, ray.BLACK, outline_thickness=3.0) self.title = OutlinedText(title, 20, ray.BLACK, ray.WHITE, outline_thickness=0) self.dan_index = dan self.player_num = player_num self.is_gold = is_gold def update(self, current_time_ms: float): + """Update the Nameplate object. + + Args: + current_time_ms (float): The current time in milliseconds. + Currently unused as rainbow nameplates are not implemented. + """ pass def unload(self): + """Unload the Nameplate object.""" self.name.unload() self.title.unload() def draw(self, x: int, y: int, fade: float = 1.0): + """Draw the Nameplate object. + + Args: + x (int): The x-coordinate of the Nameplate object. + y (int): The y-coordinate of the Nameplate object. + fade (float): The fade value of the Nameplate object. + """ tex = global_tex tex.draw_texture('nameplate', 'shadow', x=x, y=y, fade=min(0.5, fade)) if self.player_num == -1: @@ -46,23 +70,28 @@ class Nameplate: self.title.draw(self.title.default_src, dest, ray.Vector2(0, 0), 0, ray.fade(ray.WHITE, fade)) class Indicator: + """Indicator class for displaying drum navigation.""" class State(Enum): + """Enum representing the different states of the indicator.""" SKIP = 0 SIDE = 1 SELECT = 2 WAIT = 3 def __init__(self, state: State): + """Initialize the indicator with the given state.""" self.state = state self.don_fade = global_tex.get_animation(6) self.blue_arrow_move = global_tex.get_animation(7) self.blue_arrow_fade = global_tex.get_animation(8) def update(self, current_time_ms: float): + """Update the indicator's animations.""" self.don_fade.update(current_time_ms) self.blue_arrow_move.update(current_time_ms) self.blue_arrow_fade.update(current_time_ms) def draw(self, x: int, y: int, fade=1.0): + """Draw the indicator at the given position with the given fade.""" tex = global_tex tex.draw_texture('indicator', 'background', x=x, y=y, fade=fade) tex.draw_texture('indicator', 'text', frame=self.state.value, x=x, y=y, fade=fade) @@ -80,29 +109,42 @@ class Indicator: tex.draw_texture('indicator', 'drum_don', fade=min(fade, self.don_fade.attribute), index=self.state.value, x=x, y=y) class CoinOverlay: + """Coin overlay for the game.""" def __init__(self): + """Initialize the coin overlay.""" pass def update(self, current_time_ms: float): + """Update the coin overlay. Unimplemented""" pass def draw(self, x: int = 0, y: int = 0): + """Draw the coin overlay. + Only draws free play for now.""" tex = global_tex tex.draw_texture('overlay', 'free_play', x=x, y=y) class AllNetIcon: + """All.Net status icon for the game.""" def __init__(self): + """Initialize the All.Net status icon.""" pass def update(self, current_time_ms: float): + """Update the All.Net status icon.""" pass def draw(self, x: int = 0, y: int = 0): + """Draw the All.Net status icon. Only drawn offline for now""" tex = global_tex tex.draw_texture('overlay', 'allnet_indicator', x=x, y=y, frame=0) class EntryOverlay: + """Banapass and Camera status icons""" def __init__(self): + """Initialize the Banapass and Camera status icons.""" self.online = False def update(self, current_time_ms: float): + """Update the Banapass and Camera status icons.""" pass def draw(self, x: int = 0, y: int = 0): + """Draw the Banapass and Camera status icons.""" tex = global_tex tex.draw_texture('overlay', 'banapass_or', x=x, y=y, frame=self.online) tex.draw_texture('overlay', 'banapass_card', x=x, y=y, frame=self.online) @@ -113,7 +155,16 @@ class EntryOverlay: tex.draw_texture('overlay', 'camera', x=x, y=y, frame=0) class Timer: + """Timer class for displaying countdown timers.""" def __init__(self, time: int, current_time_ms: float, confirm_func): + """ + Initialize a Timer object. + + Args: + time (int): The value to start counting down from. + current_time_ms (float): The current time in milliseconds. + confirm_func (function): The function to call when the timer finishes. + """ self.time = time self.last_time = current_time_ms self.counter = str(self.time) @@ -124,6 +175,7 @@ class Timer: self.is_finished = False self.is_frozen = get_config()["general"]["timer_frozen"] def update(self, current_time_ms: float): + """Update the timer's state.""" if self.time == 0 and not self.is_finished and not audio.is_sound_playing('voice_timer_0'): self.is_finished = True self.confirm_func() @@ -148,6 +200,7 @@ class Timer: elif self.time == 0: audio.play_sound('voice_timer_0', 'voice') def draw(self, x: int = 0, y: int = 0): + """Draw the timer on the screen.""" tex = global_tex if self.time < 10: tex.draw_texture('timer', 'bg_red') diff --git a/libs/song_hash.py b/libs/song_hash.py index cd6a54f..5a12ea7 100644 --- a/libs/song_hash.py +++ b/libs/song_hash.py @@ -23,6 +23,7 @@ class DiffHashesDecoder(json.JSONDecoder): super().__init__(object_hook=diff_hashes_object_hook, *args, **kwargs) def read_tjap3_score(input_file: Path): + """Read a TJAPlayer3 score.ini file and return the scores and clears.""" score_ini = configparser.ConfigParser() score_ini.read(input_file) scores = [int(score_ini['HiScore.Drums']['HiScore1']), @@ -50,6 +51,7 @@ def read_tjap3_score(input_file: Path): return scores, clears, None def build_song_hashes(output_dir=Path("cache")): + """Build a dictionary of song hashes and save it to a file.""" if not output_dir.exists(): output_dir.mkdir() song_hashes: dict[str, list[dict]] = dict() diff --git a/libs/texture.py b/libs/texture.py index 3108e84..baba051 100644 --- a/libs/texture.py +++ b/libs/texture.py @@ -14,6 +14,7 @@ SCREEN_WIDTH = 1280 SCREEN_HEIGHT = 720 class Texture: + """Texture class for managing textures and animations.""" def __init__(self, name: str, texture: Union[ray.Texture, list[ray.Texture]], init_vals: dict[str, int]): self.name = name self.texture = texture @@ -33,12 +34,14 @@ class Texture: self.controllable: list[bool] = [False] class TextureWrapper: + """Texture wrapper class for managing textures and animations.""" def __init__(self): self.textures: dict[str, dict[str, Texture]] = dict() self.animations: dict[int, BaseAnimation] = dict() self.graphics_path = Path("Graphics") def unload_textures(self): + """Unload all textures and animations.""" for zip in self.textures: for file in self.textures[zip]: tex_object = self.textures[zip][file] @@ -49,6 +52,8 @@ class TextureWrapper: ray.unload_texture(tex_object.texture) def get_animation(self, index: int, is_copy: bool = False): + """Get an animation by ID and returns a reference. + Returns a copy of the animation if is_copy is True.""" if index not in self.animations: raise Exception(f"Unable to find id {index} in loaded animations") if is_copy: @@ -83,12 +88,14 @@ class TextureWrapper: tex_object.controllable = [tex_mapping.get("controllable", False)] def load_animations(self, screen_name: str): + """Load animations for a screen.""" screen_path = self.graphics_path / screen_name if (screen_path / 'animation.json').exists(): with open(screen_path / 'animation.json') as json_file: self.animations = parse_animations(json.loads(json_file.read())) def load_zip(self, screen_name: str, subset: str): + """Load textures from a zip file.""" zip = (self.graphics_path / screen_name / subset).with_suffix('.zip') if screen_name in self.textures and subset in self.textures[screen_name]: return @@ -134,6 +141,7 @@ class TextureWrapper: raise Exception(f"Texture {tex_name} was not found in {zip}") def load_screen_textures(self, screen_name: str) -> None: + """Load textures for a screen.""" screen_path = self.graphics_path / screen_name if (screen_path / 'animation.json').exists(): with open(screen_path / 'animation.json') as json_file: @@ -165,6 +173,26 @@ class TextureWrapper: mirror: str = '', x: float = 0, y: float = 0, x2: float = 0, y2: float = 0, origin: ray.Vector2 = ray.Vector2(0,0), rotation: float = 0, fade: float = 1.1, index: int = 0, src: Optional[ray.Rectangle] = None) -> None: + """ + Wrapper function for raylib's draw_texture_pro(). + Parameters: + subset (str): The subset of textures to use. + texture (str): The name of the texture to draw. + color (ray.Color): The color to tint the texture. + frame (int): The frame of the texture to draw. Only used if the texture is animated. + scale (float): The scale factor to apply to the texture. + center (bool): Whether to center the texture. + mirror (str): The direction to mirror the texture, either 'horizontal' or 'vertical'. + x (float): An x-value added to the top-left corner of the texture. + y (float): The y-value added to the top-left corner of the texture. + x2 (float): The x-value added to the bottom-right corner of the texture. + y2 (float): The y-value added to the bottom-right corner of the texture. + origin (ray.Vector2): The origin point of the texture. + rotation (float): The rotation angle of the texture. + fade (float): The fade factor to apply to the texture. + index (int): The index of the position data for the texture. Only used if the texture has multiple positions. + src (Optional[ray.Rectangle]): The source rectangle of the texture. + """ mirror_x = -1 if mirror == 'horizontal' else 1 mirror_y = -1 if mirror == 'vertical' else 1 if fade != 1.1: diff --git a/libs/tja.py b/libs/tja.py index 53eee46..b043099 100644 --- a/libs/tja.py +++ b/libs/tja.py @@ -11,18 +11,36 @@ from libs.utils import get_pixels_per_frame, global_data, strip_comments @lru_cache(maxsize=64) -def get_ms_per_measure(bpm_val, time_sig): +def get_ms_per_measure(bpm_val: float, time_sig: float): + """Calculate the number of milliseconds per measure.""" #https://gist.github.com/KatieFrogs/e000f406bbc70a12f3c34a07303eec8b#measure if bpm_val == 0: return 0 return 60000 * (time_sig * 4) / bpm_val @lru_cache(maxsize=64) -def get_pixels_per_ms(pixels_per_frame): +def get_pixels_per_ms(pixels_per_frame: float): + """Calculate the number of pixels per millisecond.""" return pixels_per_frame / (1000 / 60) @dataclass() class Note: + """A note in a TJA file. + + Attributes: + type (int): The type (color) of the note. + hit_ms (float): The time at which the note should be hit. + load_ms (float): The time at which the note should be loaded. + pixels_per_frame_x (float): The number of pixels per frame in the x direction. + pixels_per_frame_y (float): The number of pixels per frame in the y direction. + display (bool): Whether the note should be displayed. + index (int): The index of the note. + bpm (float): The beats per minute of the song. + gogo_time (bool): Whether the note is a gogo time note. + moji (int): The text drawn below the note. + is_branch_start (bool): Whether the note is the start of a branch. + branch_params (str): The parameters (requirements) of the branch. + """ type: int = field(init=False) hit_ms: float = field(init=False) load_ms: float = field(init=False) @@ -78,6 +96,12 @@ class Note: @dataclass class Drumroll(Note): + """A drumroll note in a TJA file. + + Attributes: + _source_note (Note): The source note. + color (int): The color of the drumroll. (0-255 where 255 is red) + """ _source_note: Note color: int = field(init=False) @@ -94,6 +118,14 @@ class Drumroll(Note): @dataclass class Balloon(Note): + """A balloon note in a TJA file. + + Attributes: + _source_note (Note): The source note. + count (int): The number of hits it takes to pop. + popped (bool): Whether the balloon has been popped. + is_kusudama (bool): Whether the balloon is a kusudama. + """ _source_note: Note count: int = field(init=False) popped: bool = False @@ -125,6 +157,10 @@ class Balloon(Note): @dataclass class NoteList: + """A collection of notes + play_notes: A list of notes, drumrolls, and balloons that are played by the player + draw_notes: A list of notes, drumrolls, and balloons that are drawn by the player + bars: A list of bars""" play_notes: list[Note | Drumroll | Balloon] = field(default_factory=lambda: []) draw_notes: list[Note | Drumroll | Balloon] = field(default_factory=lambda: []) bars: list[Note] = field(default_factory=lambda: []) @@ -144,6 +180,13 @@ class NoteList: @dataclass class CourseData: + """A collection of course metadata + level: number of stars + balloon: list of balloon counts + scoreinit: Unused + scorediff: Unused + is_branching: whether the course has branches + """ level: int = 0 balloon: list[int] = field(default_factory=lambda: []) scoreinit: list[int] = field(default_factory=lambda: []) @@ -152,6 +195,19 @@ class CourseData: @dataclass class TJAMetadata: + """Metadata for a TJA file + title: dictionary for song titles, accessed by language code + subtitle: dictionary for song subtitles, accessed by language code + genre: genre of the song + wave: path to the song's audio file + demostart: start time of the preview + offset: offset of the song's audio file + bpm: beats per minute of the song + bgmovie: path to the song's background movie file + movieoffset: offset of the song's background movie file + scene_preset: background for the song + course_data: dictionary of course metadata, accessed by diff number + """ title: dict[str, str] = field(default_factory= lambda: {'en': ''}) subtitle: dict[str, str] = field(default_factory= lambda: {'en': ''}) genre: str = '' @@ -166,6 +222,11 @@ class TJAMetadata: @dataclass class TJAEXData: + """Extra data for TJA files + new_audio: Contains the word "-New Audio-" in any song title + old_audio: Contains the word "-Old Audio-" in any song title + limited_time: Contains the word "限定" in any song title or subtitle + new: If the TJA file has been created or modified in the last week""" new_audio: bool = False old_audio: bool = False limited_time: bool = False @@ -173,6 +234,14 @@ class TJAEXData: def calculate_base_score(notes: NoteList) -> int: + """Calculate the base score for a song based on the number of notes, balloons, and drumrolls. + + Args: + notes (NoteList): The list of notes in the song. + + Returns: + int: The base score for the song. + """ total_notes = 0 balloon_count = 0 drumroll_msec = 0 @@ -195,6 +264,14 @@ def calculate_base_score(notes: NoteList) -> int: return math.ceil((1000000 - (balloon_count * 100) - (16.920079999994086 * drumroll_msec / 1000 * 100)) / total_notes / 10) * 10 def test_encodings(file_path): + """Test the encoding of a file by trying different encodings. + + Args: + file_path (Path): The path to the file to test. + + Returns: + str: The encoding that successfully decoded the file. + """ encodings = ['utf-8-sig', 'shift-jis', 'utf-8'] final_encoding = None @@ -209,8 +286,28 @@ def test_encodings(file_path): class TJAParser: + """Parse a TJA file and extract metadata and data. + + Args: + path (Path): The path to the TJA file. + start_delay (int): The delay in milliseconds before the first note. + distance (int): The distance between notes. + + Attributes: + metadata (TJAMetadata): The metadata extracted from the TJA file. + ex_data (TJAEXData): The extended data extracted from the TJA file. + data (list): The data extracted from the TJA file. + """ DIFFS = {0: "easy", 1: "normal", 2: "hard", 3: "oni", 4: "edit", 5: "tower", 6: "dan"} def __init__(self, path: Path, start_delay: int = 0, distance: int = 866): + """ + Initialize a TJA object. + + Args: + path (Path): The path to the TJA file. + start_delay (int): The delay in milliseconds before the first note. + distance (int): The distance between notes. + """ self.file_path: Path = path encoding = test_encodings(self.file_path) @@ -226,6 +323,9 @@ class TJAParser: self.current_ms: float = start_delay def get_metadata(self): + """ + Extract metadata from the TJA file. + """ current_diff = None # Track which difficulty we're currently processing for item in self.data: @@ -332,6 +432,15 @@ class TJAParser: self.ex_data.limited_time = True def data_to_notes(self, diff) -> list[list[str]]: + """ + Convert the data to notes. + + Args: + diff (int): The difficulty level. + + Returns: + list[list[str]]: The notes. + """ diff_name = self.DIFFS.get(diff, "").lower() # Use enumerate for single iteration @@ -380,6 +489,16 @@ class TJAParser: return notes def get_moji(self, play_note_list: list[Note], ms_per_measure: float) -> None: + """ + Assign 口唱歌 (note phoneticization) to notes. + + Args: + play_note_list (list[Note]): The list of notes to process. + ms_per_measure (float): The duration of a measure in milliseconds. + + Returns: + None + """ se_notes = { 1: [0, 1, 2], # Note '1' has three possible sound effects 2: [3, 4], # Note '2' has two possible sound effects @@ -440,6 +559,7 @@ class TJAParser: play_note_list[-3].moji = se_notes[1][2] def notes_to_position(self, diff: int): + """Parse a TJA's notes into a NoteList.""" master_notes = NoteList() branch_m: list[NoteList] = [] branch_e: list[NoteList] = [] @@ -705,6 +825,7 @@ class TJAParser: return master_notes, branch_m, branch_e, branch_n def hash_note_data(self, notes: NoteList): + """Hashes the note data for the given NoteList.""" n = hashlib.sha256() list1 = notes.play_notes list2 = notes.bars @@ -726,6 +847,7 @@ class TJAParser: return n.hexdigest() def modifier_speed(notes: NoteList, value: float): + """Modifies the speed of the notes in the given NoteList.""" modded_notes = notes.draw_notes.copy() modded_bars = notes.bars.copy() for note in modded_notes: @@ -737,12 +859,14 @@ def modifier_speed(notes: NoteList, value: float): return modded_notes, modded_bars def modifier_display(notes: NoteList): + """Modifies the display of the notes in the given NoteList.""" modded_notes = notes.draw_notes.copy() for note in modded_notes: note.display = False return modded_notes def modifier_inverse(notes: NoteList): + """Inverts the type of the notes in the given NoteList.""" modded_notes = notes.play_notes.copy() type_mapping = {1: 2, 2: 1, 3: 4, 4: 3} for note in modded_notes: @@ -751,6 +875,8 @@ def modifier_inverse(notes: NoteList): return modded_notes def modifier_random(notes: NoteList, value: int): + """Randomly modifies the type of the notes in the given NoteList. + value: 1 == kimagure, 2 == detarame""" #value: 1 == kimagure, 2 == detarame modded_notes = notes.play_notes.copy() percentage = int(len(modded_notes) / 5) * value @@ -762,6 +888,7 @@ def modifier_random(notes: NoteList, value: int): return modded_notes def apply_modifiers(notes: NoteList): + """Applies all selected modifiers from global_data to the given NoteList.""" if global_data.modifiers.display: draw_notes = modifier_display(notes) if global_data.modifiers.inverse: diff --git a/libs/transition.py b/libs/transition.py index 3c6ae8f..6725d4f 100644 --- a/libs/transition.py +++ b/libs/transition.py @@ -4,7 +4,12 @@ from libs.utils import OutlinedText, global_tex class Transition: + """Transition class for the game.""" def __init__(self, title: str, subtitle: str, is_second: bool = False) -> None: + """Initialize the transition object. + title: str - The title of the chart. + subtitle: str - The subtitle of the chart. + is_second: bool - Whether this is the second half of the transition.""" self.is_finished = False self.rainbow_up = global_tex.get_animation(0) self.mini_up = global_tex.get_animation(1) @@ -16,6 +21,7 @@ class Transition: self.is_second = is_second def start(self): + """Start the transition effect.""" self.rainbow_up.start() self.mini_up.start() self.chara_down.start() @@ -23,6 +29,7 @@ class Transition: self.song_info_fade_out.start() def update(self, current_time_ms: float): + """Update the transition effect.""" self.rainbow_up.update(current_time_ms) self.chara_down.update(current_time_ms) self.mini_up.update(current_time_ms) @@ -30,7 +37,7 @@ class Transition: self.song_info_fade_out.update(current_time_ms) self.is_finished = self.song_info_fade.is_finished - def draw_song_info(self): + def _draw_song_info(self): color_1 = ray.fade(ray.WHITE, self.song_info_fade.attribute) color_2 = ray.fade(ray.WHITE, min(0.70, self.song_info_fade.attribute)) offset = 0 @@ -50,6 +57,7 @@ class Transition: self.subtitle.draw(self.subtitle.default_src, dest, ray.Vector2(0, 0), 0, color_1) def draw(self): + """Draw the transition effect.""" total_offset = 0 if self.is_second: total_offset = 816 @@ -65,4 +73,4 @@ class Transition: global_tex.draw_texture('rainbow_transition', 'chara_right', x=self.mini_up.attribute//2 + chara_offset, y=-self.mini_up.attribute + offset - total_offset) global_tex.draw_texture('rainbow_transition', 'chara_center', y=-self.rainbow_up.attribute + offset - total_offset) - self.draw_song_info() + self._draw_song_info() diff --git a/libs/utils.py b/libs/utils.py index 8ee5046..da98c63 100644 --- a/libs/utils.py +++ b/libs/utils.py @@ -39,6 +39,7 @@ def force_dedicated_gpu(): print(e) def rounded(num: float) -> int: + """Round a number to the nearest integer""" sign = 1 if (num >= 0) else -1 num = abs(num) result = int(num) @@ -47,9 +48,11 @@ def rounded(num: float) -> int: return sign * result def get_current_ms() -> int: + """Get the current time in milliseconds""" return rounded(time.time() * 1000) def strip_comments(code: str) -> str: + """Strip comments from a string of code""" result = '' index = 0 for line in code.splitlines(): @@ -63,6 +66,7 @@ def strip_comments(code: str) -> str: @lru_cache def get_pixels_per_frame(bpm: float, time_signature: float, distance: float) -> float: + """Calculate the number of pixels per frame""" if bpm == 0: return 0 beat_duration = 60 / bpm @@ -71,6 +75,7 @@ def get_pixels_per_frame(bpm: float, time_signature: float, distance: float) -> return (distance / total_frames) def get_config() -> dict[str, Any]: + """Get the configuration from the TOML file""" config_path = Path('dev-config.toml') if Path('dev-config.toml').exists() else Path('config.toml') with open(config_path, "r", encoding="utf-8") as f: @@ -79,6 +84,7 @@ def get_config() -> dict[str, Any]: return json.loads(json.dumps(config_file)) def save_config(config: dict[str, Any]) -> None: + """Save the configuration to the TOML file""" if Path('dev-config.toml').exists(): with open(Path('dev-config.toml'), "w", encoding="utf-8") as f: tomlkit.dump(config, f) @@ -87,6 +93,7 @@ def save_config(config: dict[str, Any]) -> None: tomlkit.dump(config, f) def is_l_don_pressed() -> bool: + """Check if the left don button is pressed""" if global_data.input_locked: return False keys = global_data.config["keys"]["left_don"] @@ -111,6 +118,7 @@ def is_l_don_pressed() -> bool: return False def is_r_don_pressed() -> bool: + """Check if the right don button is pressed""" if global_data.input_locked: return False keys = global_data.config["keys"]["right_don"] @@ -137,6 +145,7 @@ def is_r_don_pressed() -> bool: return False def is_l_kat_pressed() -> bool: + """Check if the left kat button is pressed""" if global_data.input_locked: return False keys = global_data.config["keys"]["left_kat"] @@ -163,6 +172,7 @@ def is_l_kat_pressed() -> bool: return False def is_r_kat_pressed() -> bool: + """Check if the right kat button is pressed""" if global_data.input_locked: return False keys = global_data.config["keys"]["right_kat"] @@ -190,6 +200,18 @@ def is_r_kat_pressed() -> bool: @dataclass class SessionData: + """Data class for storing session data. Wiped after the result screen. + selected_difficulty: The difficulty level selected by the user. + song_title: The title of the song being played. + genre_index: The index of the genre being played. + result_score: The score achieved in the game. + result_good: The number of good notes achieved in the game. + result_ok: The number of ok notes achieved in the game. + result_bad: The number of bad notes achieved in the game. + result_max_combo: The maximum combo achieved in the game. + result_total_drumroll: The total drumroll achieved in the game. + result_gauge_length: The length of the gauge achieved in the game. + prev_score: The previous score pulled from the database.""" selected_difficulty: int = 0 song_title: str = '' genre_index: int = 0 @@ -206,6 +228,7 @@ session_data = SessionData() global_tex = TextureWrapper() def reset_session(): + """Reset the session data.""" return SessionData() text_cache = set() @@ -218,7 +241,19 @@ for file in Path('cache/image').iterdir(): text_cache.add(file.stem) class OutlinedText: + """Create an outlined text object.""" def __init__(self, text: str, font_size: int, color: ray.Color, outline_color: ray.Color, outline_thickness=5.0, vertical=False): + """ + Create an outlined text object. + + Args: + text (str): The text to be displayed. + font_size (int): The size of the font. + color (ray.Color): The color of the text. + outline_color (ray.Color): The color of the outline. + outline_thickness (float): The thickness of the outline. + vertical (bool): Whether the text is vertical or not. + """ self.text = text self.hash = self._hash_text(text, font_size, color, vertical) self.outline_thickness = outline_thickness @@ -463,6 +498,16 @@ class OutlinedText: return texture def draw(self, src: ray.Rectangle, dest: ray.Rectangle, origin: ray.Vector2, rotation: float, color: ray.Color): + """ + Draw the outlined text object. + + Args: + src (ray.Rectangle): The source rectangle of the texture. + dest (ray.Rectangle): The destination rectangle of the texture. + origin (ray.Vector2): The origin of the texture. + rotation (float): The rotation of the texture. + color (ray.Color): The color of the text. + """ if isinstance(color, tuple): alpha_value = ray.ffi.new('float*', color[3] / 255.0) else: @@ -475,5 +520,11 @@ class OutlinedText: ray.end_shader_mode() def unload(self): + """ + Unload the outlined text object. + + Args: + None + """ ray.unload_shader(self.shader) ray.unload_texture(self.texture) diff --git a/scenes/entry.py b/scenes/entry.py index 28bb8f9..0bf2948 100644 --- a/scenes/entry.py +++ b/scenes/entry.py @@ -16,6 +16,7 @@ from libs.utils import ( class State: + """State enum for the entry screen""" SELECT_SIDE = 0 SELECT_MODE = 1 @@ -234,6 +235,7 @@ class EntryScreen: pass class Box: + """Box class for the entry screen""" def __init__(self, text: tuple[OutlinedText, OutlinedText], location: str): self.text, self.text_highlight = text self.location = location @@ -250,6 +252,7 @@ class Box: self.moving_right = False def set_positions(self, x: int): + """Set the positions of the box""" self.x = x self.static_x = self.x self.left_x = self.x @@ -277,11 +280,13 @@ class Box: self.open.update(current_time_ms) def move_left(self): + """Move the box left""" if not self.move.is_started: self.move.start() self.moving_left = True def move_right(self): + """Move the box right""" if not self.move.is_started: self.move.start() self.moving_right = True @@ -313,6 +318,7 @@ class Box: self._draw_text(color) class BoxManager: + """BoxManager class for the entry screen""" def __init__(self): self.box_titles: list[tuple[OutlinedText, OutlinedText]] = [ (OutlinedText('演奏ゲーム', 50, ray.WHITE, ray.Color(109, 68, 24, 255), outline_thickness=5, vertical=True), @@ -335,18 +341,23 @@ class BoxManager: box.move_right() def select_box(self): + """Select the currently selected box""" self.fade_out.start() def is_box_selected(self): + """Check if the box is selected""" return self.fade_out.is_started def is_finished(self): + """Check if the animation is finished""" return self.fade_out.is_finished def selected_box(self): + """Get the location of the currently selected box""" return self.boxes[self.selected_box_index].location def move_left(self): + """Move the cursor to the left""" prev_selection = self.selected_box_index if self.boxes[prev_selection].move.is_started and not self.boxes[prev_selection].move.is_finished: return @@ -358,6 +369,7 @@ class BoxManager: self.boxes[self.selected_box_index].move_right() def move_right(self): + """Move the cursor to the right""" prev_selection = self.selected_box_index if self.boxes[prev_selection].move.is_started and not self.boxes[prev_selection].move.is_finished: return diff --git a/scenes/game.py b/scenes/game.py index 4b23b02..58dfb71 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -51,6 +51,7 @@ class GameScreen: self.mask_shader = ray.load_shader("", "shader/mask.fs") def load_hitsounds(self): + """Load the hit sounds""" sounds_dir = Path("Sounds") if global_data.hit_sound == -1: self.sound_don = audio.load_sound(Path('none.wav'), 'hitsound_don') @@ -63,6 +64,7 @@ class GameScreen: self.sound_kat = audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound) / "ka.ogg", 'hitsound_kat') def init_tja(self, song: Path, difficulty: int): + """Initialize the TJA file""" self.tja = TJAParser(song, start_delay=self.start_delay, distance=SCREEN_WIDTH - GameScreen.JUDGE_X) if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists(): self.movie = VideoPlayer(self.tja.metadata.bgmovie) @@ -73,7 +75,9 @@ class GameScreen: if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None: self.song_music = audio.load_music_stream(self.tja.metadata.wave, 'song') - self.player_1 = Player(self.tja, global_data.player_num, difficulty) + #tja_copy = copy.deepcopy(self.tja) + self.player_1 = Player(self.tja, global_data.player_num, difficulty, False) + #self.player_2 = Player(tja_copy, 2, difficulty-1, True) self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) def on_screen_start(self): @@ -92,7 +96,10 @@ class GameScreen: subtitle = self.tja.metadata.subtitle.get(global_data.config['general']['language'].lower(), '') self.bpm = self.tja.metadata.bpm scene_preset = self.tja.metadata.scene_preset - self.background = Background(global_data.player_num, self.bpm, scene_preset=scene_preset) + if self.movie is None: + self.background = Background(global_data.player_num, self.bpm, scene_preset=scene_preset) + else: + self.background = None self.transition = Transition(session_data.song_title, subtitle, is_second=True) self.allnet_indicator = AllNetIcon() self.transition.start() @@ -111,6 +118,7 @@ class GameScreen: return next_screen def write_score(self): + """Write the score to the database""" if self.tja is None: return if global_data.modifiers.auto: @@ -178,7 +186,8 @@ class GameScreen: if self.song_music is not None: audio.update_music_stream(self.song_music) - self.player_1.update(self, current_time) + self.player_1.update(self.current_ms, current_time, self.background) + #self.player_2.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'): @@ -215,16 +224,20 @@ class GameScreen: audio.stop_music_stream(self.song_music) return self.on_screen_end('SONG_SELECT') + def draw_overlay(self): + self.song_info.draw() + self.transition.draw() + self.result_transition.draw() + self.allnet_indicator.draw() + def draw(self): if self.movie is not None: self.movie.draw() elif self.background is not None: self.background.draw() - self.player_1.draw(self) - self.song_info.draw() - self.transition.draw() - self.result_transition.draw() - self.allnet_indicator.draw() + self.player_1.draw(self.current_ms, self.start_ms, self.mask_shader) + #self.player_2.draw(self.current_ms, self.start_ms, self.mask_shader) + self.draw_overlay() class Player: TIMING_GOOD = 25.0250015258789 @@ -235,8 +248,8 @@ class Player: TIMING_OK_EASY = 108.441665649414 TIMING_BAD_EASY = 125.125 - def __init__(self, tja: TJAParser, player_number: int, difficulty: int): - + def __init__(self, tja: TJAParser, player_number: int, difficulty: int, is_2p: bool): + self.is_2p = is_2p self.player_number = str(player_number) self.difficulty = difficulty self.visual_offset = global_data.config["general"]["visual_offset"] @@ -303,8 +316,8 @@ class Player: self.balloon_anim: Optional[BalloonAnimation] = None self.kusudama_anim: Optional[KusudamaAnimation] = None self.base_score_list: list[ScoreCounterAnimation] = [] - self.combo_display = Combo(self.combo, 0) - self.score_counter = ScoreCounter(self.score) + self.combo_display = Combo(self.combo, 0, self.is_2p) + self.score_counter = ScoreCounter(self.score, self.is_2p) self.gogo_time: Optional[GogoTime] = None self.combo_announce = ComboAnnounce(self.combo, 0) self.branch_indicator = BranchIndicator() if tja and tja.metadata.course_data[self.difficulty].is_branching else None @@ -320,13 +333,14 @@ class Player: self.input_log: dict[float, tuple] = dict() stars = tja.metadata.course_data[self.difficulty].level - self.gauge = Gauge(self.player_number, self.difficulty, stars, self.total_notes) + self.gauge = Gauge(self.player_number, self.difficulty, stars, self.total_notes, self.is_2p) self.gauge_hit_effect: list[GaugeHitEffect] = [] self.autoplay_hit_side = 'L' self.last_subdivision = -1 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) self.draw_note_list.extend(branch_section.draw_notes) self.draw_bar_list.extend(branch_section.bars) @@ -343,13 +357,16 @@ class Player: self.other_notes = deque([note for note in total_other if note.hit_ms > timing_threshold]) def get_result_score(self): + """Returns the score, good count, ok count, bad count, max combo, and total drumroll""" return self.score, self.good_count, self.ok_count, self.bad_count, self.max_combo, self.total_drumroll def get_position_x(self, width: int, current_ms: float, load_ms: float, pixels_per_frame: float) -> int: + """Calculates the x-coordinate of a note based on its load time and current time""" time_diff = load_ms - current_ms return int(width + pixels_per_frame * 0.06 * time_diff - 64) - self.visual_offset def get_position_y(self, current_ms: float, load_ms: float, pixels_per_frame: float, pixels_per_frame_x) -> int: + """Calculates the y-coordinate of a note based on its load time and current time""" time_diff = load_ms - current_ms return int((pixels_per_frame * 0.06 * time_diff) + ((866 * pixels_per_frame) / pixels_per_frame_x)) @@ -368,6 +385,8 @@ class Player: animation_list[:] = remaining_animations def bar_manager(self, current_ms: float): + """Manages the bars and removes if necessary + Also sets branch conditions""" #Add bar to current_bars list if it is ready to be shown on screen if self.draw_bar_list and current_ms > self.draw_bar_list[0].load_ms: self.current_bars.append(self.draw_bar_list.popleft()) @@ -432,10 +451,14 @@ class Player: self.curr_branch_reqs = [e_req, m_req, branch_start_time, max(len(seen_notes), 1)] def play_note_manager(self, current_ms: float, background: Optional[Background]): + """Manages the play_notes and removes if necessary""" if self.don_notes and self.don_notes[0].hit_ms + Player.TIMING_BAD < current_ms: self.combo = 0 if background is not None: - background.add_chibi(True) + if self.is_2p: + background.add_chibi(True, 2) + else: + background.add_chibi(True, 1) self.bad_count += 1 self.gauge.add_bad() self.don_notes.popleft() @@ -445,7 +468,10 @@ class Player: if self.kat_notes and self.kat_notes[0].hit_ms + Player.TIMING_BAD < current_ms: self.combo = 0 if background is not None: - background.add_chibi(True) + if self.is_2p: + background.add_chibi(True, 2) + else: + background.add_chibi(True, 1) self.bad_count += 1 self.gauge.add_bad() self.kat_notes.popleft() @@ -475,6 +501,7 @@ class Player: self.is_balloon = True def draw_note_manager(self, current_ms: float): + """Manages the draw_notes and removes if necessary""" if self.draw_note_list and current_ms + 1000 >= self.draw_note_list[0].load_ms: current_note = self.draw_note_list.popleft() if 5 <= current_note.type <= 7: @@ -501,14 +528,13 @@ class Player: if position < GameScreen.JUDGE_X + 650: self.current_notes_draw.pop(0) - def note_manager(self, current_ms: float, background: Optional[Background], current_time: float): + def note_manager(self, current_ms: float, background: Optional[Background]): self.bar_manager(current_ms) self.play_note_manager(current_ms, background) self.draw_note_manager(current_ms) def note_correct(self, note: Note, current_time: float): - - # Remove from the appropriate separated list + """Removes a note from the appropriate separated list""" if note.type in {1, 3} and self.don_notes and self.don_notes[0] == note: self.don_notes.popleft() elif note.type in {2, 4} and self.kat_notes and self.kat_notes[0] == note: @@ -531,14 +557,15 @@ class Player: self.max_combo = self.combo if note.type != 9: - self.draw_arc_list.append(NoteArc(note.type, current_time, 1, note.type == 3 or note.type == 4 or note.type == 7, note.type == 7)) + self.draw_arc_list.append(NoteArc(note.type, current_time, self.is_2p + 1, note.type == 3 or note.type == 4 or note.type == 7, note.type == 7)) if note in self.current_notes_draw: index = self.current_notes_draw.index(note) self.current_notes_draw.pop(index) def check_drumroll(self, drum_type: int, background: Optional[Background], current_time: float): - self.draw_arc_list.append(NoteArc(drum_type, current_time, 1, drum_type == 3 or drum_type == 4, False)) + """Checks if a note has been hit during a drumroll""" + self.draw_arc_list.append(NoteArc(drum_type, current_time, self.is_2p + 1, drum_type == 3 or drum_type == 4, False)) self.curr_drumroll_count += 1 self.total_drumroll += 1 if self.is_branch and self.branch_condition == 'r': @@ -546,23 +573,24 @@ class Player: if background is not None: background.add_renda() self.score += 100 - self.base_score_list.append(ScoreCounterAnimation(self.player_number, 100)) + self.base_score_list.append(ScoreCounterAnimation(self.player_number, 100, self.is_2p)) if not isinstance(self.current_notes_draw[0], Drumroll): return self.current_notes_draw[0].color = max(0, 255 - (self.curr_drumroll_count * 10)) - def check_balloon(self, game_screen: GameScreen, drum_type: int, note: Balloon, current_time: float): + def check_balloon(self, drum_type: int, note: Balloon, current_time: float): + """Checks if the player has popped a balloon""" if drum_type != 1: return if note.is_kusudama: - self.check_kusudama(game_screen, note) + self.check_kusudama(note) return if self.balloon_anim is None: self.balloon_anim = BalloonAnimation(current_time, note.count) self.curr_balloon_count += 1 self.total_drumroll += 1 self.score += 100 - self.base_score_list.append(ScoreCounterAnimation(self.player_number, 100)) + self.base_score_list.append(ScoreCounterAnimation(self.player_number, 100, self.is_2p)) if self.curr_balloon_count == note.count: self.is_balloon = False note.popped = True @@ -571,20 +599,22 @@ class Player: self.note_correct(note, current_time) self.curr_balloon_count = 0 - def check_kusudama(self, game_screen: GameScreen, note: Balloon): + def check_kusudama(self, note: Balloon): + """Checks if the player has popped a kusudama""" if self.kusudama_anim is None: self.kusudama_anim = KusudamaAnimation(note.count) self.curr_balloon_count += 1 self.total_drumroll += 1 self.score += 100 - self.base_score_list.append(ScoreCounterAnimation(self.player_number, 100)) + self.base_score_list.append(ScoreCounterAnimation(self.player_number, 100, self.is_2p)) if self.curr_balloon_count == note.count: audio.play_sound('kusudama_pop', 'hitsound') self.is_balloon = False note.popped = True self.curr_balloon_count = 0 - def check_note(self, game_screen: GameScreen, drum_type: int, current_time: float): + def check_note(self, ms_from_start: float, drum_type: int, current_time: float, background: Optional[Background]): + """Checks if the player has hit a note""" if len(self.don_notes) == 0 and len(self.kat_notes) == 0 and len(self.other_notes) == 0: return @@ -599,11 +629,11 @@ class Player: curr_note = self.other_notes[0] if self.other_notes else None if self.is_drumroll: - self.check_drumroll(drum_type, game_screen.background, current_time) + self.check_drumroll(drum_type, background, current_time) elif self.is_balloon: if not isinstance(curr_note, Balloon): raise Exception("Balloon mode entered but current note is not balloon") - self.check_balloon(game_screen, drum_type, curr_note, current_time) + self.check_balloon(drum_type, curr_note, current_time) else: self.curr_drumroll_count = 0 @@ -619,37 +649,43 @@ class Player: return #If the note is too far away, stop checking - if game_screen.current_ms > (curr_note.hit_ms + bad_window_ms): + if ms_from_start > (curr_note.hit_ms + bad_window_ms): return big = curr_note.type == 3 or curr_note.type == 4 - if (curr_note.hit_ms - good_window_ms) <= game_screen.current_ms <= (curr_note.hit_ms + good_window_ms): - self.draw_judge_list.append(Judgement('GOOD', big, ms_display=game_screen.current_ms - curr_note.hit_ms)) - self.lane_hit_effect = LaneHitEffect('GOOD') + if (curr_note.hit_ms - good_window_ms) <= ms_from_start <= (curr_note.hit_ms + good_window_ms): + self.draw_judge_list.append(Judgement('GOOD', big, self.is_2p, ms_display=ms_from_start - curr_note.hit_ms)) + self.lane_hit_effect = LaneHitEffect('GOOD', self.is_2p) self.good_count += 1 self.score += self.base_score - self.base_score_list.append(ScoreCounterAnimation(self.player_number, self.base_score)) + self.base_score_list.append(ScoreCounterAnimation(self.player_number, self.base_score, self.is_2p)) self.note_correct(curr_note, current_time) self.gauge.add_good() if self.is_branch and self.branch_condition == 'p': self.branch_condition_count += 1 - if game_screen.background is not None: - game_screen.background.add_chibi(False) + if background is not None: + if self.is_2p: + background.add_chibi(False, 2) + else: + background.add_chibi(False, 1) - elif (curr_note.hit_ms - ok_window_ms) <= game_screen.current_ms <= (curr_note.hit_ms + ok_window_ms): - self.draw_judge_list.append(Judgement('OK', big, ms_display=game_screen.current_ms - curr_note.hit_ms)) + elif (curr_note.hit_ms - ok_window_ms) <= ms_from_start <= (curr_note.hit_ms + ok_window_ms): + self.draw_judge_list.append(Judgement('OK', big, self.is_2p, ms_display=ms_from_start - curr_note.hit_ms)) self.ok_count += 1 self.score += 10 * math.floor(self.base_score / 2 / 10) - self.base_score_list.append(ScoreCounterAnimation(self.player_number, 10 * math.floor(self.base_score / 2 / 10))) + self.base_score_list.append(ScoreCounterAnimation(self.player_number, 10 * math.floor(self.base_score / 2 / 10), self.is_2p)) self.note_correct(curr_note, current_time) self.gauge.add_ok() if self.is_branch and self.branch_condition == 'p': self.branch_condition_count += 0.5 - if game_screen.background is not None: - game_screen.background.add_chibi(False) + if background is not None: + if self.is_2p: + background.add_chibi(False, 2) + else: + background.add_chibi(False, 1) - elif (curr_note.hit_ms - bad_window_ms) <= game_screen.current_ms <= (curr_note.hit_ms + bad_window_ms): - self.draw_judge_list.append(Judgement('BAD', big, ms_display=game_screen.current_ms - curr_note.hit_ms)) + elif (curr_note.hit_ms - bad_window_ms) <= ms_from_start <= (curr_note.hit_ms + bad_window_ms): + self.draw_judge_list.append(Judgement('BAD', big, self.is_2p, ms_display=ms_from_start - curr_note.hit_ms)) self.bad_count += 1 self.combo = 0 # Remove from both the specific note list and the main play_notes list @@ -658,10 +694,14 @@ class Player: else: self.kat_notes.popleft() self.gauge.add_bad() - if game_screen.background is not None: - game_screen.background.add_chibi(True) + if background is not None: + if self.is_2p: + background.add_chibi(True, 2) + else: + background.add_chibi(True, 1) def drumroll_counter_manager(self, current_time: float): + """Manages drumroll counter behavior""" if self.is_drumroll and self.curr_drumroll_count > 0 and self.drumroll_counter is None: self.drumroll_counter = DrumrollCounter(current_time) @@ -672,6 +712,7 @@ class Player: self.drumroll_counter.update(current_time, self.curr_drumroll_count) def balloon_manager(self, current_time: float): + """Manages balloon and kusudama behavior""" if self.balloon_anim is not None: self.chara.set_animation('balloon_popping') self.balloon_anim.update(current_time, self.curr_balloon_count, not self.is_balloon) @@ -684,25 +725,26 @@ class Player: if self.kusudama_anim.is_finished: self.kusudama_anim = None - def handle_input(self, game_screen: GameScreen, current_time: float): + def handle_input(self, ms_from_start: float, current_time: float, background: Optional[Background]): input_checks = [ - (is_l_don_pressed, 'DON', 'L', game_screen.sound_don), - (is_r_don_pressed, 'DON', 'R', game_screen.sound_don), - (is_l_kat_pressed, 'KAT', 'L', game_screen.sound_kat), - (is_r_kat_pressed, 'KAT', 'R', game_screen.sound_kat) + (is_l_don_pressed, 'DON', 'L', 'hitsound_don'), + (is_r_don_pressed, 'DON', 'R', 'hitsound_don'), + (is_l_kat_pressed, 'KAT', 'L', 'hitsound_kat'), + (is_r_kat_pressed, 'KAT', 'R', 'hitsound_kat') ] for check_func, note_type, side, sound in input_checks: if check_func(): - self.lane_hit_effect = LaneHitEffect(note_type) - self.draw_drum_hit_list.append(DrumHitEffect(note_type, side)) + self.lane_hit_effect = LaneHitEffect(note_type, self.is_2p) + self.draw_drum_hit_list.append(DrumHitEffect(note_type, side, self.is_2p)) audio.play_sound(sound, 'hitsound') drum_value = 1 if note_type == 'DON' else 2 - self.check_note(game_screen, drum_value, current_time) - self.input_log[game_screen.current_ms] = (note_type, side) + self.check_note(ms_from_start, drum_value, current_time, background) + self.input_log[ms_from_start] = (note_type, side) - def autoplay_manager(self, game_screen: GameScreen, current_time: float): + def autoplay_manager(self, ms_from_start: float, current_time: float, background: Optional[Background]): + """Manages autoplay behavior""" if not global_data.modifiers.auto: return @@ -715,38 +757,39 @@ class Player: if bpm == 0: subdivision_in_ms = 0 else: - subdivision_in_ms = game_screen.current_ms // ((60000 * 4 / bpm) / 24) + subdivision_in_ms = ms_from_start // ((60000 * 4 / bpm) / 24) if subdivision_in_ms > self.last_subdivision: self.last_subdivision = subdivision_in_ms hit_type = 'DON' - self.lane_hit_effect = LaneHitEffect(hit_type) + self.lane_hit_effect = LaneHitEffect(hit_type, self.is_2p) self.autoplay_hit_side = 'R' if self.autoplay_hit_side == 'L' else 'L' - self.draw_drum_hit_list.append(DrumHitEffect(hit_type, self.autoplay_hit_side)) - audio.play_sound(game_screen.sound_don, 'hitsound') + self.draw_drum_hit_list.append(DrumHitEffect(hit_type, self.autoplay_hit_side, self.is_2p)) + audio.play_sound('hitsound_don', 'hitsound') note_type = 3 if note.type == 6 else 1 - self.check_note(game_screen, note_type, current_time) + self.check_note(ms_from_start, note_type, current_time, background) else: # Handle DON notes - while self.don_notes and game_screen.current_ms >= self.don_notes[0].hit_ms: + while self.don_notes and ms_from_start >= self.don_notes[0].hit_ms: note = self.don_notes[0] hit_type = 'DON' - self.lane_hit_effect = LaneHitEffect(hit_type) + self.lane_hit_effect = LaneHitEffect(hit_type, self.is_2p) self.autoplay_hit_side = 'R' if self.autoplay_hit_side == 'L' else 'L' - self.draw_drum_hit_list.append(DrumHitEffect(hit_type, self.autoplay_hit_side)) - audio.play_sound(game_screen.sound_don, 'hitsound') - self.check_note(game_screen, 1, current_time) + self.draw_drum_hit_list.append(DrumHitEffect(hit_type, self.autoplay_hit_side, self.is_2p)) + audio.play_sound('hitsound_don', 'hitsound') + self.check_note(ms_from_start, 1, current_time, background) # Handle KAT notes - while self.kat_notes and game_screen.current_ms >= self.kat_notes[0].hit_ms: + while self.kat_notes and ms_from_start >= self.kat_notes[0].hit_ms: note = self.kat_notes[0] hit_type = 'KAT' - self.lane_hit_effect = LaneHitEffect(hit_type) + self.lane_hit_effect = LaneHitEffect(hit_type, self.is_2p) self.autoplay_hit_side = 'R' if self.autoplay_hit_side == 'L' else 'L' - self.draw_drum_hit_list.append(DrumHitEffect(hit_type, self.autoplay_hit_side)) - audio.play_sound(game_screen.sound_kat, 'hitsound') - self.check_note(game_screen, 2, current_time) + self.draw_drum_hit_list.append(DrumHitEffect(hit_type, self.autoplay_hit_side, self.is_2p)) + audio.play_sound('hitsound_kat', 'hitsound') + self.check_note(ms_from_start, 2, current_time, background) def evaluate_branch(self, current_ms): + """Evaluates the branch condition and updates the branch status""" e_req, m_req, end_time, total_notes = self.curr_branch_reqs if current_ms >= end_time: self.is_branch = False @@ -775,8 +818,8 @@ class Player: self.branch_e.pop(0) self.branch_condition_count = 0 - def update(self, game_screen: GameScreen, current_time: float): - self.note_manager(game_screen.current_ms, game_screen.background, current_time) + def update(self, ms_from_start: float, current_time: float, background: Optional[Background]): + self.note_manager(ms_from_start, background) self.combo_display.update(current_time, self.combo) self.combo_announce.update(current_time) self.drumroll_counter_manager(current_time) @@ -793,7 +836,7 @@ class Player: for i, anim in enumerate(self.draw_arc_list): anim.update(current_time) if anim.is_finished: - self.gauge_hit_effect.append(GaugeHitEffect(anim.note_type, anim.is_big)) + self.gauge_hit_effect.append(GaugeHitEffect(anim.note_type, anim.is_big, self.is_2p)) finished_arcs.append(i) for i in reversed(finished_arcs): self.draw_arc_list.pop(i) @@ -801,8 +844,8 @@ class Player: self.animation_manager(self.gauge_hit_effect, current_time) self.animation_manager(self.base_score_list, current_time) self.score_counter.update(current_time, self.score) - self.autoplay_manager(game_screen, current_time) - self.handle_input(game_screen, current_time) + self.autoplay_manager(ms_from_start, current_time, background) + self.handle_input(ms_from_start, current_time, background) self.nameplate.update(current_time) self.gauge.update(current_time) if self.judge_counter is not None: @@ -813,7 +856,7 @@ class Player: self.ending_anim.update(current_time) if self.is_branch: - self.evaluate_branch(game_screen.current_ms) + self.evaluate_branch(ms_from_start) # Get the next note from any of the three lists for BPM and gogo time updates next_note = None @@ -832,7 +875,7 @@ class Player: self.bpm = next_note.bpm if next_note.gogo_time and not self.is_gogo_time: self.is_gogo_time = True - self.gogo_time = GogoTime() + self.gogo_time = GogoTime(self.is_2p) self.chara.set_animation('gogo_start') if not next_note.gogo_time and self.is_gogo_time: self.is_gogo_time = False @@ -841,6 +884,7 @@ class Player: self.chara.update(current_time, self.bpm, self.gauge.is_clear, self.gauge.is_rainbow) def draw_drumroll(self, current_ms: float, head: Drumroll, current_eighth: int): + """Draws a drumroll in the player's lane""" start_position = self.get_position_x(SCREEN_WIDTH, current_ms, head.load_ms, head.pixels_per_frame_x) tail = next((note for note in self.current_notes_draw[1:] if note.type == 8 and note.index > head.index), self.current_notes_draw[1]) is_big = int(head.type == 6) @@ -849,18 +893,19 @@ class Player: color = ray.Color(255, head.color, head.color, 255) if head.display: if length > 0: - tex.draw_texture('notes', "8", frame=is_big, x=start_position+64, y=192, x2=length-47, color=color) + tex.draw_texture('notes', "8", frame=is_big, x=start_position+64, y=192+(self.is_2p*176), x2=length-47, color=color) if is_big: - tex.draw_texture('notes', "drumroll_big_tail", x=end_position+64, y=192, color=color) + tex.draw_texture('notes', "drumroll_big_tail", x=end_position+64, y=192+(self.is_2p*176), color=color) else: - tex.draw_texture('notes', "drumroll_tail", x=end_position+64, y=192, color=color) - tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=192, color=color) + tex.draw_texture('notes', "drumroll_tail", x=end_position+64, y=192+(self.is_2p*176), color=color) + tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=192+(self.is_2p*176), color=color) - tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + 60, y=323, x2=length) - tex.draw_texture('notes', 'moji', frame=head.moji, x=(start_position - (168//2)) + 64, y=323) - tex.draw_texture('notes', 'moji', frame=tail.moji, x=(end_position - (168//2)) + 32, y=323) + tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + 60, y=323+(self.is_2p*176), x2=length) + tex.draw_texture('notes', 'moji', frame=head.moji, x=(start_position - (168//2)) + 64, y=323+(self.is_2p*176)) + tex.draw_texture('notes', 'moji', frame=tail.moji, x=(end_position - (168//2)) + 32, y=323+(self.is_2p*176)) def draw_balloon(self, current_ms: float, head: Balloon, current_eighth: int): + """Draws a balloon in the player's lane""" offset = 12 start_position = self.get_position_x(SCREEN_WIDTH, current_ms, head.load_ms, head.pixels_per_frame_x) tail = next((note for note in self.current_notes_draw[1:] if note.type == 8 and note.index > head.index), self.current_notes_draw[1]) @@ -873,10 +918,11 @@ class Player: else: position = start_position if head.display: - tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset, y=192) - tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+128, y=192) + tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset, y=192+(self.is_2p*176)) + tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+128, y=192+(self.is_2p*176)) def draw_bars(self, current_ms: float): + """Draw bars in the player's lane""" if not self.current_bars: return @@ -891,13 +937,14 @@ class Player: frame = 1 else: frame = 0 - bar_draws.append((str(bar.type), frame, x_position+60, y_position+190)) + bar_draws.append((str(bar.type), frame, x_position+60, y_position+190+(self.is_2p*176))) # Draw all bars in one batch for bar_type, frame, x, y in bar_draws: tex.draw_texture('notes', bar_type, frame=frame, x=x, y=y) def draw_notes(self, current_ms: float, start_ms: float): + """Draw notes in the player's lane""" if not self.current_notes_draw: return @@ -918,17 +965,17 @@ class Player: x_position = self.get_position_x(SCREEN_WIDTH, current_ms, note.load_ms, note.pixels_per_frame_x) y_position = self.get_position_y(current_ms, note.load_ms, note.pixels_per_frame_y, note.pixels_per_frame_x) self.draw_balloon(current_ms, note, current_eighth) - tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position) + tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position+(self.is_2p*176)) else: x_position = self.get_position_x(SCREEN_WIDTH, current_ms, note.load_ms, note.pixels_per_frame_x) y_position = self.get_position_y(current_ms, note.load_ms, note.pixels_per_frame_y, note.pixels_per_frame_x) if note.display: - tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position, y=y_position+192, center=True) - tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position) + tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position, y=y_position+192+(self.is_2p*176), center=True) + tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position+(self.is_2p*176)) def draw_modifiers(self): - # Batch modifier texture draws to reduce state changes + """Shows the currently selected modifiers""" modifiers_to_draw = ['mod_shinuchi'] # Speed modifiers @@ -951,19 +998,17 @@ class Player: # Draw all modifiers in one batch for modifier in modifiers_to_draw: - tex.draw_texture('lane', modifier) - - def draw(self, game_screen: GameScreen): - current_ms = game_screen.current_ms + tex.draw_texture('lane', modifier, index=self.is_2p) + def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader): # Group 1: Background and lane elements - tex.draw_texture('lane', 'lane_background') + tex.draw_texture('lane', 'lane_background', index=self.is_2p) if self.branch_indicator is not None: self.branch_indicator.draw() self.gauge.draw() if self.lane_hit_effect is not None: self.lane_hit_effect.draw() - tex.draw_texture('lane', 'lane_hit_circle') + tex.draw_texture('lane', 'lane_hit_circle', index=self.is_2p) # Group 2: Judgement and hit effects if self.gogo_time is not None: @@ -972,14 +1017,14 @@ class Player: anim.draw() # Group 3: Notes and bars (game content) - self.draw_bars(current_ms) - self.draw_notes(current_ms, game_screen.start_ms) + self.draw_bars(ms_from_start) + self.draw_notes(ms_from_start, start_ms) # Group 4: Lane covers and UI elements (batch similar textures) - tex.draw_texture('lane', f'{self.player_number}p_lane_cover') - tex.draw_texture('lane', 'drum') + tex.draw_texture('lane', f'{self.player_number}p_lane_cover', index=self.is_2p) + tex.draw_texture('lane', 'drum', index=self.is_2p) if global_data.modifiers.auto: - tex.draw_texture('lane', 'auto_icon') + tex.draw_texture('lane', 'auto_icon', index=self.is_2p) if self.ending_anim is not None: self.ending_anim.draw() @@ -987,24 +1032,27 @@ class Player: for anim in self.draw_drum_hit_list: anim.draw() for anim in self.draw_arc_list: - anim.draw(game_screen.mask_shader) + anim.draw(mask_shader) for anim in self.gauge_hit_effect: anim.draw() # Group 6: UI overlays self.combo_display.draw() self.combo_announce.draw() - tex.draw_texture('lane', 'lane_score_cover') - tex.draw_texture('lane', f'{self.player_number}p_icon') - tex.draw_texture('lane', 'lane_difficulty', frame=self.difficulty) + 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.judge_counter is not None: self.judge_counter.draw() # Group 7: Player-specific elements if not global_data.modifiers.auto: - self.nameplate.draw(-62, 285) + if self.is_2p: + self.nameplate.draw(-62, 285+461) + else: + self.nameplate.draw(-62, 285) self.draw_modifiers() - self.chara.draw() + self.chara.draw(y=(self.is_2p*536)) # Group 8: Special animations and counters if self.drumroll_counter is not None: @@ -1019,7 +1067,9 @@ class Player: #ray.draw_circle(game_screen.width//2, game_screen.height, 300, ray.ORANGE) class Judgement: - def __init__(self, type: str, big: bool, ms_display: Optional[float]=None): + """Shows the judgement of the player's hit""" + def __init__(self, type: str, big: bool, is_2p: bool, ms_display: Optional[float]=None): + self.is_2p = is_2p self.type = type self.big = big self.is_finished = False @@ -1051,25 +1101,27 @@ class Judgement: fade = self.fade_animation_2.attribute if self.type == 'GOOD': if self.big: - tex.draw_texture('hit_effect', 'hit_effect_good_big', fade=fade) - tex.draw_texture('hit_effect', 'outer_good_big', frame=index, fade=hit_fade) + tex.draw_texture('hit_effect', 'hit_effect_good_big', fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'outer_good_big', frame=index, fade=hit_fade, index=self.is_2p) else: - tex.draw_texture('hit_effect', 'hit_effect_good', fade=fade) - tex.draw_texture('hit_effect', 'outer_good', frame=index, fade=hit_fade) - tex.draw_texture('hit_effect', 'judge_good', y=y, fade=fade) + tex.draw_texture('hit_effect', 'hit_effect_good', fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'outer_good', frame=index, fade=hit_fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'judge_good', y=y, fade=fade, index=self.is_2p) elif self.type == 'OK': if self.big: - tex.draw_texture('hit_effect', 'hit_effect_ok_big', fade=fade) - tex.draw_texture('hit_effect', 'outer_ok_big', frame=index, fade=hit_fade) + tex.draw_texture('hit_effect', 'hit_effect_ok_big', fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'outer_ok_big', frame=index, fade=hit_fade, index=self.is_2p) else: - tex.draw_texture('hit_effect', 'hit_effect_ok', fade=fade) - tex.draw_texture('hit_effect', 'outer_ok', frame=index, fade=hit_fade) - tex.draw_texture('hit_effect', 'judge_ok', y=y, fade=fade) + tex.draw_texture('hit_effect', 'hit_effect_ok', fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'outer_ok', frame=index, fade=hit_fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'judge_ok', y=y, fade=fade, index=self.is_2p) elif self.type == 'BAD': - tex.draw_texture('hit_effect', 'judge_bad', y=y, fade=fade) + tex.draw_texture('hit_effect', 'judge_bad', y=y, fade=fade, index=self.is_2p) class LaneHitEffect: - def __init__(self, type: str): + """Display a gradient overlay when the player hits the drum""" + def __init__(self, type: str, is_2p: bool): + self.is_2p = is_2p self.type = type self.fade = tex.get_animation(0, is_copy=True) self.fade.start() @@ -1082,14 +1134,16 @@ class LaneHitEffect: def draw(self): if self.type == 'GOOD': - tex.draw_texture('lane', 'lane_hit_effect', frame=2, fade=self.fade.attribute) + tex.draw_texture('lane', 'lane_hit_effect', frame=2, index=self.is_2p, fade=self.fade.attribute) elif self.type == 'DON': - tex.draw_texture('lane', 'lane_hit_effect', frame=0, fade=self.fade.attribute) + tex.draw_texture('lane', 'lane_hit_effect', frame=0, index=self.is_2p, fade=self.fade.attribute) elif self.type == 'KAT': - tex.draw_texture('lane', 'lane_hit_effect', frame=1, fade=self.fade.attribute) + tex.draw_texture('lane', 'lane_hit_effect', frame=1, index=self.is_2p, fade=self.fade.attribute) class DrumHitEffect: - def __init__(self, type: str, side: str): + """Display the side of the drum hit""" + def __init__(self, type: str, side: str, is_2p: bool): + self.is_2p = is_2p self.type = type self.side = side self.is_finished = False @@ -1104,20 +1158,21 @@ class DrumHitEffect: def draw(self): if self.type == 'DON': if self.side == 'L': - tex.draw_texture('lane', 'drum_don_l', fade=self.fade.attribute) + tex.draw_texture('lane', 'drum_don_l', index=self.is_2p, fade=self.fade.attribute) elif self.side == 'R': - tex.draw_texture('lane', 'drum_don_r', fade=self.fade.attribute) + tex.draw_texture('lane', 'drum_don_r', index=self.is_2p, fade=self.fade.attribute) elif self.type == 'KAT': if self.side == 'L': - tex.draw_texture('lane', 'drum_kat_l', fade=self.fade.attribute) + tex.draw_texture('lane', 'drum_kat_l', index=self.is_2p, fade=self.fade.attribute) elif self.side == 'R': - tex.draw_texture('lane', 'drum_kat_r', fade=self.fade.attribute) + tex.draw_texture('lane', 'drum_kat_r', index=self.is_2p, fade=self.fade.attribute) class GaugeHitEffect: - # Pre-define color thresholds for better performance + """Effect when a note hits the gauge""" _COLOR_THRESHOLDS = [(0.70, ray.WHITE), (0.80, ray.YELLOW), (0.90, ray.ORANGE), (1.00, ray.RED)] - def __init__(self, note_type: int, big: bool): + def __init__(self, note_type: int, big: bool, is_2p: bool): + self.is_2p = is_2p self.note_type = note_type self.is_big = big self.texture_change = tex.get_animation(2, is_copy=True) @@ -1203,6 +1258,7 @@ class GaugeHitEffect: tex.draw_texture('gauge', 'hit_effect', frame=self.texture_change.attribute, x2=self.x2_pos, + y=(self.is_2p*435), y2=self.y2_pos, color=ray.fade(self.texture_color, fade_value), origin=self.origin, @@ -1211,13 +1267,14 @@ class GaugeHitEffect: # Note type texture tex.draw_texture('notes', str(self.note_type), - x=1158, y=101, + x=1158, y=101+(self.is_2p*435), fade=fade_value) # Circle effect texture (use cached texture name) - tex.draw_texture('gauge', self.circle_texture, color=self.color) + tex.draw_texture('gauge', self.circle_texture, color=self.color, y=(self.is_2p*435)) class NoteArc: + """Note arcing from the player to the gauge""" def __init__(self, note_type: int, current_ms: float, player_number: int, big: bool, is_balloon: bool): self.note_type = note_type self.is_big = big @@ -1234,6 +1291,9 @@ class NoteArc: curve_height = 425 self.start_x, self.start_y = 350, 192 self.end_x, self.end_y = 1158, 101 + if self.player_number == 2: + self.start_y += 176 + self.end_y += 435 self.explosion_x = self.start_x self.explosion_y = self.start_y @@ -1312,6 +1372,7 @@ class NoteArc: tex.draw_texture('notes', str(self.note_type), x=self.x_i, y=self.y_i) class DrumrollCounter: + """Displays a drumroll counter, stays alive until is_drumroll is false""" def __init__(self, current_ms: float): self.create_ms = current_ms self.is_finished = False @@ -1347,6 +1408,7 @@ class DrumrollCounter: tex.draw_texture('drumroll_counter', 'counter', color=color, frame=int(digit), x=-(total_width//2)+(i*52), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute) class BalloonAnimation: + """Draws a Balloon""" def __init__(self, current_ms: float, balloon_total: int): self.create_ms = current_ms self.is_finished = False @@ -1393,6 +1455,7 @@ class BalloonAnimation: tex.draw_texture('balloon', 'counter', frame=int(digit), color=self.color, x=-(total_width // 2) + (i * 52), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute) class KusudamaAnimation: + """Draws a Kusudama""" def __init__(self, balloon_total: int): self.balloon_total = balloon_total self.move_down = tex.get_animation(11) @@ -1458,8 +1521,10 @@ class KusudamaAnimation: tex.draw_texture('kusudama', 'counter', frame=int(digit), x=-(total_width // 2) + (i * 150), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute) class Combo: - def __init__(self, combo: int, current_ms: float): + """Displays the current combo""" + def __init__(self, combo: int, current_ms: float, is_2p: bool): self.combo = combo + self.is_2p = is_2p self.stretch_animation = tex.get_animation(5) self.color = [ray.fade(ray.WHITE, 1), ray.fade(ray.WHITE, 1), ray.fade(ray.WHITE, 1)] self.glimmer_dict = {0: 0, 1: 0, 2: 0} @@ -1511,22 +1576,24 @@ class Combo: if self.combo < 100: margin = 30 total_width = len(counter) * margin - tex.draw_texture('combo', 'combo') + tex.draw_texture('combo', 'combo', index=self.is_2p) for i, digit in enumerate(counter): - tex.draw_texture('combo', 'counter', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute) + tex.draw_texture('combo', 'counter', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute, index=self.is_2p) else: margin = 35 total_width = len(counter) * margin - tex.draw_texture('combo', 'combo_100') + tex.draw_texture('combo', 'combo_100', index=self.is_2p) for i, digit in enumerate(counter): - tex.draw_texture('combo', 'counter_100', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute) + tex.draw_texture('combo', 'counter_100', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute, index=self.is_2p) glimmer_positions = [(225, 210), (200, 230), (250, 230)] for j, (x, y) in enumerate(glimmer_positions): for i in range(3): - tex.draw_texture('combo', 'gleam', x=x+(i*30), y=y+self.glimmer_dict[j], color=self.color[j]) + tex.draw_texture('combo', 'gleam', x=x+(i*30), y=y+self.glimmer_dict[j] + (self.is_2p*176), color=self.color[j]) class ScoreCounter: - def __init__(self, score: int): + """Displays the total score""" + def __init__(self, score: int, is_2p: bool): + self.is_2p = is_2p self.score = score self.stretch = tex.get_animation(4) @@ -1547,7 +1614,7 @@ class ScoreCounter: self._cached_score_str = str(self.score) counter = self._cached_score_str - x, y = 150, 185 + x, y = 150, 185 + (self.is_2p*176) margin = 20 total_width = len(counter) * margin start_x = x - total_width @@ -1555,7 +1622,9 @@ class ScoreCounter: tex.draw_texture('lane', 'score_number', frame=int(digit), x=start_x + (i * margin), y=y - self.stretch.attribute, y2=self.stretch.attribute) class ScoreCounterAnimation: - def __init__(self, player_num: str, counter: int): + """Displays the score init being added to the total score""" + def __init__(self, player_num: str, counter: int, is_2p: bool): + self.is_2p = is_2p self.counter = counter self.fade_animation_1 = Animation.create_fade(50, initial_opacity=0.0, final_opacity=1.0) self.fade_animation_1.start() @@ -1617,10 +1686,11 @@ class ScoreCounterAnimation: tex.draw_texture('lane', 'score_number', frame=int(digit), x=start_x + (i * self.margin), - y=y, + y=y + (self.is_2p * 535), color=self.color) class SongInfo: + """Displays the song name and genre""" def __init__(self, song_name: str, genre: int): self.song_name = song_name self.genre = genre @@ -1642,6 +1712,7 @@ class SongInfo: tex.draw_texture('song_info', 'genre', fade=1 - self.fade.attribute, frame=self.genre) class ResultTransition: + """Displays the result transition animation""" def __init__(self, player_num: int): self.player_num = player_num self.move = global_tex.get_animation(5) @@ -1668,7 +1739,9 @@ class ResultTransition: x += 256 class GogoTime: - def __init__(self): + """Displays the Gogo Time fire and fireworks""" + def __init__(self, is_2p: bool): + self.is_2p = is_2p self.explosion_anim = tex.get_animation(23) self.fire_resize = tex.get_animation(24) self.fire_change = tex.get_animation(25) @@ -1682,12 +1755,13 @@ class GogoTime: self.fire_change.update(current_time_ms) def draw(self): - tex.draw_texture('gogo_time', 'fire', scale=self.fire_resize.attribute, frame=self.fire_change.attribute, fade=0.5, center=True) + tex.draw_texture('gogo_time', 'fire', scale=self.fire_resize.attribute, frame=self.fire_change.attribute, fade=0.5, center=True, index=self.is_2p) if not self.explosion_anim.is_finished: for i in range(5): tex.draw_texture('gogo_time', 'explosion', frame=self.explosion_anim.attribute, index=i) class ComboAnnounce: + """Displays the combo every 100 combos""" def __init__(self, combo: int, current_time_ms: float): self.combo = combo self.wait = current_time_ms @@ -1702,7 +1776,7 @@ class ComboAnnounce: self.is_finished = True self.fade.update(current_time_ms) - if not self.audio_played: + if not self.audio_played and self.combo >= 100: audio.play_sound(f'combo_{self.combo}_{global_data.player_num}p', 'voice') self.audio_played = True @@ -1735,6 +1809,7 @@ class ComboAnnounce: tex.draw_texture('combo', 'announce_text', x=-text_offset/2, fade=fade) class BranchIndicator: + """Displays the branch difficulty and changes""" def __init__(self): self.difficulty = 'normal' self.diff_2 = self.difficulty @@ -1781,6 +1856,7 @@ class BranchIndicator: tex.draw_texture('branch', self.difficulty, y=(self.diff_up.attribute * (self.direction*-1)) - (70*self.direction*-1), fade=1 - self.diff_fade.attribute) class FailAnimation: + """Animates the fail effect""" def __init__(self): self.bachio_fade_in = Animation.create_fade(150, initial_opacity=0.0, final_opacity=1.0) self.bachio_fade_in.start() @@ -1826,6 +1902,7 @@ class FailAnimation: tex.draw_texture('ending_anim', 'bachio_boom', index=1, fade=self.bachio_boom_fade_in.attribute, center=True, scale=self.bachio_boom_scale.attribute) class ClearAnimation: + """Animates the clear effect""" def __init__(self): self.bachio_fade_in = Animation.create_fade(150, initial_opacity=0.0, final_opacity=1.0) self.bachio_fade_in.start() @@ -1876,6 +1953,7 @@ class ClearAnimation: tex.draw_texture('ending_anim', 'bachio_r_' + self.name, x=self.bachio_move_out.attribute, frame=self.frame, fade=self.bachio_fade_in.attribute) class FCAnimation: + """Animates the full combo effect""" def __init__(self): self.bachio_fade_in = Animation.create_fade(150, initial_opacity=0.0, final_opacity=1.0) self.bachio_fade_in.start() @@ -1950,6 +2028,7 @@ class FCAnimation: tex.draw_texture('ending_anim', 'bachio_r_' + self.name, x=(self.bachio_move_out.attribute + self.bachio_move_out_2.attribute)*1.15, y=-self.bachio_move_up.attribute, frame=self.frame, fade=self.bachio_fade_in.attribute) class JudgeCounter: + """Counts the number of good, ok, bad, and drumroll notes in real time""" def __init__(self): self.good = 0 self.ok = 0 @@ -1994,7 +2073,9 @@ class JudgeCounter: class Gauge: - def __init__(self, player_num: str, difficulty: int, level: int, total_notes: int): + """The player's gauge""" + def __init__(self, player_num: str, difficulty: int, level: int, total_notes: int, is_2p: bool): + self.is_2p = is_2p self.player_num = player_num self.string_diff = "_hard" self.gauge_length = 0 @@ -2062,6 +2143,7 @@ class Gauge: 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) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) @@ -2069,6 +2151,7 @@ class Gauge: self.gauge_length = self.gauge_max 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 += ((1 * self.table[self.difficulty][self.level]["ok_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) @@ -2076,6 +2159,7 @@ class Gauge: self.gauge_length = self.gauge_max def add_bad(self): + """Adds a bad note to the gauge""" self.previous_length = int(self.gauge_length) self.gauge_length += ((1 * self.table[self.difficulty][self.level]["bad_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) if self.gauge_length < 0: diff --git a/scenes/result.py b/scenes/result.py index d64915d..bc49944 100644 --- a/scenes/result.py +++ b/scenes/result.py @@ -16,6 +16,9 @@ from libs.utils import ( class State: + """ + Enum representing the state of the result screen. + """ FAIL = 0 CLEAR = 1 RAINBOW = 2 @@ -81,6 +84,9 @@ class ResultScreen: return "SONG_SELECT" def update_score_animation(self): + """ + Update the score animation if a high score is achieved. + """ if self.is_skipped: if self.update_index == len(self.update_list) - 1: return @@ -161,6 +167,9 @@ class ResultScreen: self.chara.update(current_time, 100, False, False) def draw_score_info(self): + """ + Draw the score information. + """ if self.good != '': for i in range(len(str(self.good))): tex.draw_texture('score', 'judge_num', frame=int(str(self.good)[::-1][i]), x=943-(i*24), y=186) @@ -178,6 +187,9 @@ class ResultScreen: tex.draw_texture('score', 'judge_num', frame=int(str(self.total_drumroll)[::-1][i]), x=1217-(i*24), y=227) def draw_total_score(self): + """ + Draw the total score. + """ if not self.fade_in.is_finished: return tex.draw_texture('score', 'score_shinuchi') @@ -186,6 +198,7 @@ class ResultScreen: tex.draw_texture('score', 'score_num', x=-(i*21), frame=int(str(self.score)[::-1][i])) def draw_bottom_textures(self): + """Draw the bottom textures.""" if self.state == State.FAIL: tex.draw_texture('background', 'gradient_fail', fade=min(0.4, self.fade_in_bottom.attribute)) else: @@ -193,6 +206,7 @@ class ResultScreen: self.bottom_characters.draw() def draw_modifiers(self): + """Draw the modifiers if enabled.""" if global_data.modifiers.display: tex.draw_texture('score', 'mod_doron') if global_data.modifiers.inverse: @@ -260,6 +274,7 @@ class ResultScreen: pass class Crown: + """Represents a crown animation""" def __init__(self): self.resize = tex.get_animation(2) self.resize_fix = tex.get_animation(3) @@ -293,6 +308,7 @@ class Crown: tex.draw_texture('crown', 'gleam', frame=self.gleam.attribute) class BottomCharacters: + """Represents the bottom characters animation""" def __init__(self): self.move_up = tex.get_animation(7) self.move_down = tex.get_animation(8) @@ -370,6 +386,7 @@ class BottomCharacters: tex.draw_texture('bottom', 'chara_1', frame=self.chara_1_index, y=y) class FadeIn: + """A fade out disguised as a fade in""" def __init__(self): self.fadein = tex.get_animation(15) self.fadein.start() @@ -389,6 +406,7 @@ class FadeIn: x += 256 class ScoreAnimator: + """Animates a number from left to right""" def __init__(self, target_score): self.target_score = str(target_score) self.current_score_list = [[0,0] for _ in range(len(self.target_score))] @@ -396,6 +414,7 @@ class ScoreAnimator: self.is_finished = False def next_score(self) -> str: + """Returns the next number in the animation""" if self.digit_index == -1: self.is_finished = True return str(int(''.join([str(item[0]) for item in self.current_score_list]))) @@ -414,6 +433,7 @@ class ScoreAnimator: return str(int(ret_val)) class HighScoreIndicator: + """Indicates the difference between the old and new high score""" def __init__(self, old_score: int, new_score: int): self.score_diff = new_score - old_score self.move = tex.get_animation(18) @@ -432,6 +452,7 @@ class HighScoreIndicator: class Gauge: + """The gauge from the game screen, at 0.9x scale""" def __init__(self, player_num: str, gauge_length: int): self.player_num = player_num self.difficulty = min(2, session_data.selected_difficulty) diff --git a/scenes/song_select.py b/scenes/song_select.py index edcdffd..4248501 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -41,6 +41,7 @@ class SongSelectScreen: self.indicator = Indicator(Indicator.State.SELECT) def load_navigator(self): + """To be called on boot.""" self.navigator = FileNavigator(self.root_dir) def on_screen_start(self): @@ -119,6 +120,7 @@ class SongSelectScreen: return next_screen def reset_demo_music(self): + """Reset the preview music to the song select bgm.""" if self.demo_song is not None: audio.stop_music_stream(self.demo_song) audio.unload_music_stream(self.demo_song) @@ -127,6 +129,7 @@ class SongSelectScreen: self.navigator.get_current_item().box.wait = get_current_ms() def handle_input_browsing(self): + """Handle input for browsing songs.""" if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT_CONTROL) or (is_l_kat_pressed() and get_current_ms() <= self.last_moved + 50): self.reset_demo_music() for _ in range(10): @@ -188,7 +191,7 @@ class SongSelectScreen: audio.play_sound('add_favorite', 'sound') def handle_input_selected(self): - # Handle song selection confirmation or cancel + """Handle input for selecting difficulty.""" if self.neiro_selector is not None: if is_l_kat_pressed(): self.neiro_selector.move_left() @@ -222,6 +225,9 @@ class SongSelectScreen: self._confirm_selection() def get_current_song(): + """ + Returns the currently selected song. + """ selected_song = self.navigator.get_current_item() if isinstance(selected_song, Directory): raise Exception("Directory was chosen instead of song") @@ -247,6 +253,9 @@ class SongSelectScreen: self._toggle_ura_mode() def handle_input_diff_sort(self): + """ + Handle input for sorting difficulty. + """ if self.diff_sort_selector is None: raise Exception("Diff sort selector was not able to be created") if is_l_kat_pressed(): @@ -592,6 +601,7 @@ class SongSelectScreen: pass class SongBox: + """A box for the song select screen.""" OUTLINE_MAP = { 1: ray.Color(0, 77, 104, 255), 2: ray.Color(156, 64, 2, 255), @@ -851,6 +861,7 @@ class SongBox: self._draw_closed(x, y) class YellowBox: + """A song box when it is opened.""" def __init__(self, name: OutlinedText, is_back: bool, tja: Optional[TJAParser] = None): self.is_diff_select = False self.name = name @@ -1064,6 +1075,7 @@ class YellowBox: self._draw_text(song_box) class GenreBG: + """The background for a genre box.""" def __init__(self, start_box: SongBox, end_box: SongBox, title: OutlinedText, diff_sort: Optional[int]): self.start_box = start_box self.end_box = end_box @@ -1117,6 +1129,7 @@ class GenreBG: self.title.draw(self.title.default_src, dest, ray.Vector2(0, 0), 0, ray.fade(ray.WHITE, self.fade_in.attribute)) class UraSwitchAnimation: + """The animation for the ura switch.""" def __init__(self) -> None: self.texture_change = tex.get_animation(7) self.fade_out = tex.get_animation(8) @@ -1134,6 +1147,7 @@ class UraSwitchAnimation: tex.draw_texture('diff_select', 'ura_switch', frame=self.texture_change.attribute, fade=self.fade_out.attribute) class DiffSortSelect: + """The menu for selecting the difficulty sort and level sort.""" def __init__(self, statistics: dict[int, dict[int, list[int]]], prev_diff: int, prev_level: int): self.selected_box = -1 self.selected_level = 1 @@ -1368,6 +1382,7 @@ class DiffSortSelect: self.draw_diff_select() class NeiroSelector: + """The menu for selecting the game hitsounds.""" def __init__(self): self.selected_sound = global_data.hit_sound with open(Path("Sounds") / 'hit_sounds' / 'neiro_list.txt', encoding='utf-8-sig') as neiro_list: @@ -1477,6 +1492,7 @@ class NeiroSelector: self.text_2.draw(self.text_2.default_src, dest, ray.Vector2(0, 0), 0, ray.fade(ray.WHITE, 1 - self.fade_sideways.attribute)) class ModifierSelector: + """The menu for selecting the game modifiers.""" TEX_MAP = { "auto": "mod_auto", "speed": "mod_baisaku", @@ -1671,7 +1687,15 @@ class ModifierSelector: tex.draw_texture('modifier', 'blue_arrow', y=move + (i*50), x=x+110 + self.blue_arrow_move.attribute, mirror='horizontal', fade=self.blue_arrow_fade.attribute) class ScoreHistory: + """The score information that appears while hovering over a song""" def __init__(self, scores: dict[int, tuple[int, int, int, int]], current_ms): + """ + Initialize the score history with the given scores and current time. + + Args: + scores (dict[int, tuple[int, int, int, int]]): A dictionary of scores for each difficulty level. + current_ms (int): The current time in milliseconds. + """ self.scores = {k: v for k, v in scores.items() if v is not None} self.difficulty_keys = list(self.scores.keys()) self.curr_difficulty_index = 0 @@ -2164,8 +2188,6 @@ class FileNavigator: self.load_current_directory() self.box_open = False - # ... (rest of the methods remain the same: navigate_left, navigate_right, etc.) - def _count_tja_files(self, folder_path: Path): """Count TJA files in directory""" tja_count = 0 @@ -2389,10 +2411,12 @@ class FileNavigator: raise Exception("No current item available") def reset_items(self): + """Reset the items in the song select scene""" for item in self.items: item.box.reset() def add_recent(self): + """Add the current song to the recent list""" song = self.get_current_item() if isinstance(song, Directory): return @@ -2414,6 +2438,7 @@ class FileNavigator: print("Added recent: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) def add_favorite(self) -> bool: + """Add the current song to the favorites list""" song = self.get_current_item() if isinstance(song, Directory): return False diff --git a/scenes/title.py b/scenes/title.py index 782ed03..dbf00f0 100644 --- a/scenes/title.py +++ b/scenes/title.py @@ -60,6 +60,7 @@ class TitleScreen: return "ENTRY" def scene_manager(self, current_time): + """Manage the scene transitions""" if self.state == State.OP_VIDEO: if self.op_video is None: self.op_video = VideoPlayer(random.choice(self.op_video_list)) @@ -123,7 +124,9 @@ class TitleScreen: pass class WarningScreen: + """Warning screen for the game""" class X: + """Giant X behind the characters for the warning screen""" def __init__(self): self.resize = tex.get_animation(0) self.resize.start() @@ -149,6 +152,7 @@ class WarningScreen: tex.draw_texture('warning', 'x_red', fade=self.fadein.attribute, scale=self.resize.attribute, center=True) class BachiHit: + """Bachi hitting the player animation for the warning screen""" def __init__(self): self.resize = tex.get_animation(3) self.fadein = tex.get_animation(4) @@ -170,6 +174,7 @@ class WarningScreen: tex.draw_texture('warning', 'bachi') class Characters: + """Characters for the warning screen""" def __init__(self): self.shadow_fade = tex.get_animation(5) self.chara_0_frame = tex.get_animation(7) @@ -201,6 +206,7 @@ class WarningScreen: tex.draw_texture('warning', 'chara_1', frame=self.chara_1_frame.attribute, fade=fade, y=y_pos) class Board: + """Background Board for the warning screen""" def __init__(self): self.move_down = tex.get_animation(10) self.move_down.start()