mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 11:40:13 +01:00
Add doc strings
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
131
libs/tja.py
131
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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user