Files
PyTaiko/scenes/song_select.py
2025-06-01 16:08:45 -04:00

1071 lines
48 KiB
Python

import sqlite3
from pathlib import Path
from typing import Optional
import pyray as ray
from libs import song_hash
from libs.animation import Animation
from libs.audio import audio
from libs.tja import TJAParser
from libs.utils import (
OutlinedText,
get_config,
get_current_ms,
global_data,
load_all_textures_from_zip,
session_data,
)
class SongSelectScreen:
BOX_CENTER = 444
def __init__(self, screen_width: int, screen_height: int):
self.screen_init = False
self.root_dir = get_config()["paths"]["tja_path"]
self.screen_width = screen_width
self.screen_height = screen_height
self.navigator = FileNavigator(self.root_dir)
def load_textures(self):
self.textures = load_all_textures_from_zip(Path('Graphics/lumendata/song_select.zip'))
self.textures['custom'] = [ray.load_texture('1.png'), ray.load_texture('2.png')]
def load_sounds(self):
sounds_dir = Path("Sounds")
self.sound_don = audio.load_sound(sounds_dir / "inst_00_don.wav")
self.sound_kat = audio.load_sound(sounds_dir / "inst_00_katsu.wav")
self.sound_skip = audio.load_sound(sounds_dir / 'song_select' / 'Skip.ogg')
#self.sound_select = audio.load_sound(sounds_dir / "song_select.wav")
#self.sound_cancel = audio.load_sound(sounds_dir / "cancel.wav")
def on_screen_start(self):
if not self.screen_init:
self.load_textures()
self.load_sounds()
self.selected_song = None
self.selected_difficulty = 0
self.game_transition = None
self.move_away = Animation.create_move(float('inf'))
self.diff_fade_out = Animation.create_fade(0, final_opacity=1.0)
self.background_move = Animation.create_move(15000, start_position=0, total_distance=1280)
self.state = "BROWSING"
self.text_fade_out = None
self.text_fade_in = None
self.texture_index = 784
self.last_texture_index = 784
self.background_fade_change = None
self.demo_song = None
for item in self.navigator.items:
item.box.reset()
self.navigator.get_current_item().box.get_scores()
self.screen_init = True
def on_screen_end(self):
self.screen_init = False
global_data.selected_song = self.navigator.get_current_item().path
session_data.selected_difficulty = self.selected_difficulty
self.reset_demo_music()
for zip in self.textures:
for texture in self.textures[zip]:
ray.unload_texture(texture)
return "GAME"
def reset_demo_music(self):
if self.demo_song is not None:
audio.stop_music_stream(self.demo_song)
audio.unload_music_stream(self.demo_song)
self.demo_song = None
self.navigator.get_current_item().box.wait = get_current_ms()
def handle_input(self):
if self.state == "BROWSING":
# Up/Down navigation
keys = get_config()["keybinds"]["left_kat"]
for key in keys:
if ray.is_key_pressed(ord(key)):
self.reset_demo_music()
self.navigator.navigate_left()
audio.play_sound(self.sound_kat)
keys = get_config()["keybinds"]["right_kat"]
for key in keys:
if ray.is_key_pressed(ord(key)):
self.reset_demo_music()
self.navigator.navigate_right()
audio.play_sound(self.sound_kat)
if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT_CONTROL):
self.reset_demo_music()
self.wait = get_current_ms()
for i in range(10):
self.navigator.navigate_left()
audio.play_sound(self.sound_skip)
if ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT_CONTROL):
self.reset_demo_music()
for i in range(10):
self.navigator.navigate_right()
audio.play_sound(self.sound_skip)
# Select/Enter
keys = get_config()["keybinds"]["left_don"] + get_config()["keybinds"]["right_don"]
for key in keys:
if ray.is_key_pressed(ord(key)):
selected_item = self.navigator.items[self.navigator.selected_index]
if selected_item is not None and selected_item.box.name == "Back":
self.navigator.go_back()
#audio.play_sound(self.sound_cancel)
else:
selected_song = self.navigator.select_current_item()
if selected_song:
self.state = "SONG_SELECTED"
audio.play_sound(self.sound_don)
self.move_away = Animation.create_move(233, total_distance=500)
self.diff_fade_out = Animation.create_fade(83)
elif self.state == "SONG_SELECTED":
# Handle song selection confirmation or cancel
keys = get_config()["keybinds"]["left_don"] + get_config()["keybinds"]["right_don"]
for key in keys:
if ray.is_key_pressed(ord(key)):
if self.selected_difficulty == -1:
self.selected_song = None
self.move_away = Animation.create_move(float('inf'))
self.diff_fade_out = Animation.create_fade(0, final_opacity=1.0)
self.text_fade_out = None
self.text_fade_in = None
self.state = "BROWSING"
for item in self.navigator.items:
item.box.reset()
else:
audio.play_sound(self.sound_don)
self.game_transition = Transition(self.screen_height)
keys = get_config()["keybinds"]["left_kat"]
for key in keys:
if ray.is_key_pressed(ord(key)):
audio.play_sound(self.sound_kat)
if self.selected_difficulty >= 0:
self.selected_difficulty = (self.selected_difficulty - 1)
keys = get_config()["keybinds"]["right_kat"]
for key in keys:
if ray.is_key_pressed(ord(key)):
audio.play_sound(self.sound_kat)
if self.selected_difficulty < 4:
self.selected_difficulty = (self.selected_difficulty + 1)
def update(self):
self.on_screen_start()
if self.background_move.is_finished:
self.background_move = Animation.create_move(15000, start_position=0, total_distance=1280)
self.background_move.update(get_current_ms())
if self.game_transition is not None:
self.game_transition.update(get_current_ms())
if self.game_transition.is_finished:
return self.on_screen_end()
self.handle_input()
if self.demo_song is not None:
audio.update_music_stream(self.demo_song)
if self.background_fade_change is None:
self.last_texture_index = self.texture_index
for song in self.navigator.items:
song.box.update(self.state == "SONG_SELECTED")
song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150
if not isinstance(song, Directory) and song.box.is_open:
if self.demo_song is None and get_current_ms() >= song.box.wait + (83.33*3):
song.box.get_scores()
self.demo_song = audio.load_music_stream(song.tja.metadata.wave)
audio.normalize_music_stream(self.demo_song, 0.1935)
audio.seek_music_stream(self.demo_song, song.tja.metadata.demostart)
audio.play_music_stream(self.demo_song)
if song.box.is_open:
current_box = song.box
if current_box.texture_index != 552 and get_current_ms() >= song.box.wait + (83.33*3):
self.texture_index = SongBox.BACKGROUND_MAP[current_box.texture_index]
if self.last_texture_index != self.texture_index and self.background_fade_change is None:
self.background_fade_change = Animation.create_fade(200)
self.move_away.update(get_current_ms())
self.diff_fade_out.update(get_current_ms())
if self.background_fade_change is not None:
self.background_fade_change.update(get_current_ms())
if self.background_fade_change.is_finished:
self.background_fade_change = None
if self.move_away.is_finished and self.text_fade_out is None:
self.text_fade_out = Animation.create_fade(33)
self.text_fade_in = Animation.create_fade(33, initial_opacity=0.0, final_opacity=1.0, delay=self.text_fade_out.duration)
if self.text_fade_out is not None:
self.text_fade_out.update(get_current_ms())
if self.text_fade_out.is_finished:
self.selected_song = True
if self.text_fade_in is not None:
self.text_fade_in.update(get_current_ms())
def draw_selector(self):
if self.selected_difficulty == -1:
ray.draw_texture(self.textures['song_select'][133], 314, 110, ray.WHITE)
else:
ray.draw_texture(self.textures['song_select'][140], 450 + (self.selected_difficulty * 115), 7, ray.WHITE)
ray.draw_texture(self.textures['song_select'][131], 461 + (self.selected_difficulty * 115), 132, ray.WHITE)
def draw(self):
# Draw file/directory list
texture_back = self.textures['song_select'][self.last_texture_index]
texture = self.textures['song_select'][self.texture_index]
for i in range(0, texture.width * 4, texture.width):
if self.background_fade_change is not None:
color = ray.fade(ray.WHITE, self.background_fade_change.attribute)
ray.draw_texture(texture_back, i - int(self.background_move.attribute), 0, color)
reverse_color = ray.fade(ray.WHITE, 1 - self.background_fade_change.attribute)
ray.draw_texture(texture, i - int(self.background_move.attribute), 0, reverse_color)
else:
ray.draw_texture(texture, i - int(self.background_move.attribute), 0, ray.WHITE)
for item in self.navigator.get_items():
box = item.box
if -156 <= box.position <= self.screen_width + 144:
if box.position <= 500:
box.draw(box.position - int(self.move_away.attribute), 95, self.textures, self.diff_fade_out.attribute)
else:
box.draw(box.position + int(self.move_away.attribute), 95, self.textures, self.diff_fade_out.attribute)
if self.selected_song and self.state == "SONG_SELECTED":
self.draw_selector()
fade = ray.WHITE
if self.text_fade_in is not None:
fade = ray.fade(ray.WHITE, self.text_fade_in.attribute)
ray.draw_texture(self.textures['song_select'][192], 5, 5, fade)
else:
fade = ray.WHITE
if self.text_fade_out is not None:
fade = ray.fade(ray.WHITE, self.text_fade_out.attribute)
ray.draw_texture(self.textures['song_select'][244], 5, 5, fade)
ray.draw_texture(self.textures['song_select'][394], 0, self.screen_height - self.textures['song_select'][394].height, ray.WHITE)
if self.game_transition is not None:
self.game_transition.draw(self.screen_height)
class SongBox:
OUTLINE_MAP = {
555: ray.Color(0, 77, 104, 255),
560: ray.Color(156, 64, 2, 255),
565: ray.Color(153, 4, 46, 255),
570: ray.Color(60, 104, 0, 255),
575: ray.Color(134, 88, 0, 255),
580: ray.Color(79, 40, 134, 255),
585: ray.Color(148, 24, 0, 255),
615: ray.Color(84, 101, 126, 255)
}
FOLDER_HEADER_MAP = {
555: 643,
560: 645,
565: 647,
570: 649,
575: 651,
580: 653,
585: 655,
615: 667,
620: 670
}
FULL_FOLDER_HEADER_MAP = {
555: 736,
560: 738,
565: 740,
570: 742,
575: 744,
580: 746,
585: 748,
615: 760,
620: 762,
}
BACKGROUND_MAP = {
555: 772,
560: 773,
565: 774,
570: 775,
575: 776,
580: 777,
585: 778,
615: 783,
620: 784
}
GENRE_CHAR_MAP = {
555: 507,
560: 509,
565: 511,
570: 513,
575: 515,
580: 517,
585: 519,
615: 532,
}
def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None, tja_count: Optional[int] = None):
self.text_name = name
self.texture_index = texture_index
self.scores = dict()
self.position = -11111
self.start_position = -1
self.target_position = -1
self.is_open = False
self.name = None
self.black_name = None
self.hori_name = None
self.yellow_box = None
self.open_anim = None
self.open_fade = None
self.move = None
self.wait = 0
self.is_dir = is_dir
self.tja_count = tja_count
self.tja_count_text = None
if self.tja_count is not None and self.tja_count != 0:
self.tja_count_text = OutlinedText(str(self.tja_count), 35, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, horizontal_spacing=1.2)
self.tja = tja
self.hash = dict()
self.update(False)
def reset(self):
if self.black_name is not None:
self.yellow_box = YellowBox(self.black_name, self.texture_index == 552, tja=self.tja)
self.open_anim = None
self.open_fade = None
def get_scores(self):
if self.tja is None:
return
with sqlite3.connect('scores.db') as con:
cursor = con.cursor()
diffs_to_compute = []
for diff in self.tja.metadata.course_data:
if diff not in self.hash:
diffs_to_compute.append(diff)
if diffs_to_compute:
for diff in diffs_to_compute:
notes, _, bars = self.tja.notes_to_position(diff)
self.hash[diff] = self.tja.hash_note_data(notes, bars)
# Batch database query for all diffs at once
if self.tja.metadata.course_data:
hash_values = [self.hash[diff] for diff in self.tja.metadata.course_data]
placeholders = ','.join('?' * len(hash_values))
batch_query = f"""
SELECT hash, score, good, ok, bad
FROM Scores
WHERE hash IN ({placeholders})
"""
cursor.execute(batch_query, hash_values)
hash_to_score = {row[0]: row[1:] for row in cursor.fetchall()}
for diff in self.tja.metadata.course_data:
diff_hash = self.hash[diff]
self.scores[diff] = hash_to_score.get(diff_hash)
def update(self, is_diff_select):
self.is_diff_select = is_diff_select
if self.yellow_box is not None:
self.yellow_box.update(is_diff_select)
is_open_prev = self.is_open
if self.position != self.target_position and self.move is None:
if self.position < self.target_position:
direction = 1
else:
direction = -1
if abs(self.target_position - self.position) > 250:
direction *= -1
self.move = Animation.create_move(66.67, start_position=0, total_distance=100 * direction)
if self.is_open or self.target_position == SongSelectScreen.BOX_CENTER + 150:
self.move.total_distance = 250 * direction
self.start_position = self.position
if self.move is not None:
self.move.update(get_current_ms())
self.position = self.start_position + int(self.move.attribute)
if self.move.is_finished:
self.position = self.target_position
self.move = None
self.is_open = self.position == SongSelectScreen.BOX_CENTER + 150
if not is_open_prev and self.is_open:
if self.black_name is None:
self.black_name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, vertical=True)
#print(f"loaded black name {self.text_name}")
if self.tja is not None or self.texture_index == 552:
self.yellow_box = YellowBox(self.black_name, self.texture_index == 552, tja=self.tja)
self.yellow_box.create_anim()
else:
self.hori_name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5)
#print(f"loaded hori name {self.text_name}")
self.open_anim = Animation.create_move(133, start_position=0, total_distance=150, delay=83.33)
self.open_fade = Animation.create_fade(200, initial_opacity=0, final_opacity=1.0)
self.wait = get_current_ms()
elif not self.is_open:
if self.black_name is not None:
self.black_name.unload()
self.black_name = None
if self.yellow_box is not None:
self.yellow_box = None
if self.hori_name is not None:
self.hori_name.unload()
self.hori_name = None
if self.open_anim is not None:
self.open_anim.update(get_current_ms())
if self.open_fade is not None:
self.open_fade.update(get_current_ms())
'''
if self.black_name is None:
self.black_name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, vertical=True)
if self.name is None:
self.name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), SongBox.OUTLINE_MAP.get(self.texture_index, ray.Color(101, 0, 82, 255)), outline_thickness=5, vertical=True)
'''
if self.name is None and -56 <= self.position <= 1280:
self.name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), SongBox.OUTLINE_MAP.get(self.texture_index, ray.Color(101, 0, 82, 255)), outline_thickness=5, vertical=True)
#print(f"loaded {self.text_name}")
elif self.name is not None and (self.position < -56 or self.position > 1280):
self.name.unload()
self.name = None
def _draw_closed(self, x: int, y: int, textures):
ray.draw_texture(textures['song_select'][self.texture_index+1], x, y, ray.WHITE)
offset = 0
if 555 <= self.texture_index <= 600:
offset = 1
for i in range(0, textures['song_select'][self.texture_index].width * 4, textures['song_select'][self.texture_index].width):
ray.draw_texture(textures['song_select'][self.texture_index], (x+32)+i, y - offset, ray.WHITE)
ray.draw_texture(textures['song_select'][self.texture_index+2], x+64, y, ray.WHITE)
if self.texture_index == 620:
ray.draw_texture(textures['song_select'][self.texture_index+3], x+12, y+16, ray.WHITE)
if self.texture_index != 552 and self.is_dir:
ray.draw_texture(textures['song_select'][SongBox.FOLDER_HEADER_MAP[self.texture_index]], x+4 - offset, y-6, ray.WHITE)
if self.texture_index == 552:
ray.draw_texture(textures['song_select'][422], x + 47 - int(textures['song_select'][422].width / 2), y+35, ray.WHITE)
elif self.name is not None:
src = ray.Rectangle(0, 0, self.name.texture.width, self.name.texture.height)
dest = ray.Rectangle(x + 47 - int(self.name.texture.width / 2), y+35, self.name.texture.width, min(self.name.texture.height, 417))
self.name.draw(src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
#ray.draw_text(str(self.position), x, y-25, 25, ray.GREEN)
def _draw_open(self, x: int, y: int, textures, fade_override):
if self.open_anim is not None:
color = ray.WHITE
if fade_override is not None:
color = ray.fade(ray.WHITE, fade_override)
if self.hori_name is not None and self.open_anim.attribute >= 100:
texture = textures['song_select'][SongBox.FULL_FOLDER_HEADER_MAP[self.texture_index]]
src = ray.Rectangle(0, 0, texture.width, texture.height)
dest = ray.Rectangle(x-115+48, (y-56) + 150 - int(self.open_anim.attribute), texture.width+220, texture.height)
ray.draw_texture_pro(texture, src, dest, ray.Vector2(0,0), 0, color)
texture = textures['song_select'][SongBox.FULL_FOLDER_HEADER_MAP[self.texture_index]+1]
src = ray.Rectangle(0, 0, -texture.width, texture.height)
dest = ray.Rectangle(x-115, y-56 + 150 - int(self.open_anim.attribute), texture.width, texture.height)
ray.draw_texture(texture, x+160, y-56 + 150 - int(self.open_anim.attribute), color)
ray.draw_texture_pro(texture, src, dest, ray.Vector2(0,0), 0, color)
src = ray.Rectangle(0, 0, self.hori_name.texture.width, self.hori_name.texture.height)
dest_width = min(300, self.hori_name.texture.width)
dest = ray.Rectangle((x + 48) - (dest_width//2), y-50 + 150 - int(self.open_anim.attribute), dest_width, self.hori_name.texture.height)
self.hori_name.draw(src, dest, ray.Vector2(0, 0), 0, color)
ray.draw_texture(textures['song_select'][self.texture_index+1], x - int(self.open_anim.attribute), y, ray.WHITE)
offset = 0
if 555 <= self.texture_index <= 600:
offset = 1
for i in range(0, textures['song_select'][self.texture_index].width * (5+int(self.open_anim.attribute / 4)), textures['song_select'][self.texture_index].width):
ray.draw_texture(textures['song_select'][self.texture_index], ((x- int(self.open_anim.attribute))+32)+i, y - offset, ray.WHITE)
ray.draw_texture(textures['song_select'][self.texture_index+2], x+64 + int(self.open_anim.attribute), y, ray.WHITE)
color = ray.WHITE
if self.texture_index == 620:
ray.draw_texture(textures['song_select'][self.texture_index+4], x+12 - 150, y+16, color)
if fade_override is not None:
color = ray.fade(ray.WHITE, min(0.5, fade_override))
ray.draw_texture(textures['song_select'][492], 470, 125, color)
color = ray.WHITE
if fade_override is not None:
color = ray.fade(ray.WHITE, fade_override)
if self.tja_count_text is not None:
ray.draw_texture(textures['song_select'][493], 475, 125, color)
ray.draw_texture(textures['song_select'][494], 600, 125, color)
src = ray.Rectangle(0, 0, self.tja_count_text.texture.width, self.tja_count_text.texture.height)
dest_width = min(124, self.tja_count_text.texture.width)
dest = ray.Rectangle(560 - (dest_width//2), 118, dest_width, self.tja_count_text.texture.height)
self.tja_count_text.draw(src, dest, ray.Vector2(0, 0), 0, color)
if self.texture_index in SongBox.GENRE_CHAR_MAP:
ray.draw_texture(textures['song_select'][SongBox.GENRE_CHAR_MAP[self.texture_index]+1], 650, 125, color)
ray.draw_texture(textures['song_select'][SongBox.GENRE_CHAR_MAP[self.texture_index]], 470, 180, color)
def draw(self, x: int, y: int, textures, fade_override=None):
if self.is_open and get_current_ms() >= self.wait + 83.33:
if self.yellow_box is not None:
self.yellow_box.draw(textures, self, fade_override)
else:
if self.open_fade is not None:
self._draw_open(x, y, textures, self.open_fade.attribute)
else:
self._draw_closed(x, y, textures)
class YellowBox:
def __init__(self, name: OutlinedText, is_back: bool, tja: Optional[TJAParser] = None):
self.is_diff_select = False
self.right_x = 803
self.left_x = 443
self.top_y = 96
self.bottom_y = 543
self.center_width = 332
self.center_height = 422
self.edge_height = 32
self.name = name
self.is_back = is_back
self.tja = tja
self.anim_created = False
self.left_out = Animation.create_move(83.33, total_distance=-152, delay=83.33)
self.right_out = Animation.create_move(83.33, total_distance=145, delay=83.33)
self.center_out = Animation.create_move(83.33, total_distance=300, delay=83.33)
self.fade = Animation.create_fade(83.33, initial_opacity=1.0, final_opacity=1.0, delay=83.33)
self.reset_animations()
def reset_animations(self):
self.fade_in = Animation.create_fade(float('inf'), initial_opacity=0.0, final_opacity=1.0, delay=83.33)
self.left_out_2 = Animation.create_move(float('inf'), total_distance=-213)
self.right_out_2 = Animation.create_move(float('inf'), total_distance=0)
self.center_out_2 = Animation.create_move(float('inf'), total_distance=423)
self.top_y_out = Animation.create_move(float('inf'), total_distance=-62)
self.center_h_out = Animation.create_move(float('inf'), total_distance=60)
def create_anim(self):
self.left_out = Animation.create_move(83.33, total_distance=-152, delay=83.33)
self.right_out = Animation.create_move(83.33, total_distance=145, delay=83.33)
self.center_out = Animation.create_move(83.33, total_distance=300, delay=83.33)
self.fade = Animation.create_fade(83.33, initial_opacity=0.0, final_opacity=1.0, delay=83.33)
def create_anim_2(self):
self.left_out_2 = Animation.create_move(116.67, total_distance=-213)
self.right_out_2 = Animation.create_move(116.67, total_distance=211)
self.center_out_2 = Animation.create_move(116.67, total_distance=423)
self.top_y_out = Animation.create_move(133.33, total_distance=-62, delay=self.left_out_2.duration)
self.center_h_out = Animation.create_move(133.33, total_distance=60, delay=self.left_out_2.duration)
self.fade_in = Animation.create_fade(116.67, initial_opacity=0.0, final_opacity=1.0, delay=self.left_out_2.duration + self.top_y_out.duration + 16.67)
def update(self, is_diff_select: bool):
self.left_out.update(get_current_ms())
self.right_out.update(get_current_ms())
self.center_out.update(get_current_ms())
self.fade.update(get_current_ms())
self.fade_in.update(get_current_ms())
self.left_out_2.update(get_current_ms())
self.right_out_2.update(get_current_ms())
self.center_out_2.update(get_current_ms())
self.top_y_out.update(get_current_ms())
self.center_h_out.update(get_current_ms())
self.is_diff_select = is_diff_select
if self.is_diff_select:
if not self.anim_created:
self.anim_created = True
self.create_anim_2()
self.right_x = 803 + int(self.right_out_2.attribute)
self.left_x = 443 + int(self.left_out_2.attribute)
self.top_y = 96 + int(self.top_y_out.attribute)
self.center_width = 332 + int(self.center_out_2.attribute)
self.center_height = 422 + int(self.center_h_out.attribute)
else:
self.anim_created = False
self.right_x = 658 + int(self.right_out.attribute)
self.left_x = 595 + int(self.left_out.attribute)
self.top_y = 96
self.center_width = 32 + int(self.center_out.attribute)
self.center_height = 422
def draw(self, textures: dict[str, list[ray.Texture]], song_box: SongBox, fade_override: Optional[float]):
# Draw corners
ray.draw_texture(textures['song_select'][235], self.right_x, self.bottom_y, ray.WHITE) # Bottom right
ray.draw_texture(textures['song_select'][236], self.left_x, self.bottom_y, ray.WHITE) # Bottom left
ray.draw_texture(textures['song_select'][237], self.right_x, self.top_y, ray.WHITE) # Top right
ray.draw_texture(textures['song_select'][238], self.left_x, self.top_y, ray.WHITE) # Top left
# Edges
# Bottom edge
texture = textures['song_select'][231]
src = ray.Rectangle(0, 0, texture.width, texture.height)
dest = ray.Rectangle(self.left_x + self.edge_height, self.bottom_y, self.center_width, texture.height)
ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
# Right edge
texture = textures['song_select'][232]
src = ray.Rectangle(0, 0, texture.width, texture.height)
dest = ray.Rectangle(self.right_x, self.top_y + self.edge_height, texture.width, self.center_height)
ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
# Left edge
texture = textures['song_select'][233]
src = ray.Rectangle(0, 0, texture.width, texture.height)
dest = ray.Rectangle(self.left_x, self.top_y + self.edge_height, texture.width, self.center_height)
ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
# Top edge
texture = textures['song_select'][234]
src = ray.Rectangle(0, 0, texture.width, texture.height)
dest = ray.Rectangle(self.left_x + self.edge_height, self.top_y, self.center_width, texture.height)
ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
# Center
texture = textures['song_select'][230]
src = ray.Rectangle(0, 0, texture.width, texture.height)
dest = ray.Rectangle(self.left_x + self.edge_height, self.top_y + self.edge_height, self.center_width, self.center_height)
ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
if self.is_diff_select:
#Back Button
color = ray.fade(ray.WHITE, self.fade_in.attribute)
ray.draw_texture(textures['song_select'][153], 314, 110, color)
#Difficulties
ray.draw_texture(textures['song_select'][154], 450, 90, color)
ray.draw_texture(textures['song_select'][182], 565, 90, color)
ray.draw_texture(textures['song_select'][185], 680, 90, color)
ray.draw_texture(textures['song_select'][188], 795, 90, color)
if self.tja is not None:
for course in self.tja.metadata.course_data:
for j in range(self.tja.metadata.course_data[course].level):
ray.draw_texture(textures['song_select'][155], 482+(course*115), 471+(j*-20), color)
else:
#Crowns
fade = self.fade.attribute
if fade_override is not None:
fade = min(self.fade.attribute, fade_override)
color = ray.fade(ray.WHITE, fade)
if self.is_back:
ray.draw_texture(textures['song_select'][421], 498, 250, color)
elif self.tja is not None:
for diff in self.tja.metadata.course_data:
if diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][3] == 0:
ray.draw_texture(textures['song_select'][160], 473 + (diff*60), 175, color)
ray.draw_texture(textures['song_select'][158], 473 + (diff*60), 175, ray.fade(color, min(fade, 0.25)))
#EX Data
if self.tja.ex_data.new_audio:
ray.draw_texture(textures['custom'][0], 458, 120, color)
elif self.tja.ex_data.old_audio:
ray.draw_texture(textures['custom'][1], 458, 120, color)
elif self.tja.ex_data.limited_time:
ray.draw_texture(textures['song_select'][418], 458, 120, color)
#Difficulties
ray.draw_texture(textures['song_select'][395], 458, 210, color)
ray.draw_texture(textures['song_select'][401], 518, 210, color)
ray.draw_texture(textures['song_select'][403], 578, 210, color)
ray.draw_texture(textures['song_select'][406], 638, 210, color)
#Stars
for course in self.tja.metadata.course_data:
for j in range(self.tja.metadata.course_data[course].level):
ray.draw_texture(textures['song_select'][396], 474+(course*60), 490+(j*-17), color)
else:
pass
if self.is_back:
texture = textures['song_select'][422]
x = int(((song_box.position + 47) - texture.width / 2) + (int(self.right_out.attribute)*0.85) + (int(self.right_out_2.attribute)))
y = self.top_y+35
ray.draw_texture(texture, x, y, ray.WHITE)
elif self.name is not None:
texture = self.name.texture
x = int(((song_box.position + 47) - texture.width / 2) + (int(self.right_out.attribute)*0.85) + (int(self.right_out_2.attribute)))
y = self.top_y+35
src = ray.Rectangle(0, 0, texture.width, texture.height)
dest = ray.Rectangle(x, y, texture.width, min(texture.height, 417))
self.name.draw(src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
class Transition:
def __init__(self, screen_height: int) -> None:
self.is_finished = False
self.rainbow_up = Animation.create_move(266, start_position=0, total_distance=screen_height + global_data.textures['scene_change_rainbow'][2].height, ease_in='cubic')
self.chara_down = None
def update(self, current_time_ms: float):
self.rainbow_up.update(current_time_ms)
if self.rainbow_up.is_finished and self.chara_down is None:
self.chara_down = Animation.create_move(33, start_position=0, total_distance=30)
if self.chara_down is not None:
self.chara_down.update(current_time_ms)
self.is_finished = self.chara_down.is_finished
def draw(self, screen_height: int):
ray.draw_texture(global_data.textures['scene_change_rainbow'][2], 0, screen_height - int(self.rainbow_up.attribute), ray.WHITE)
texture = global_data.textures['scene_change_rainbow'][0]
src = ray.Rectangle(0, 0, texture.width, texture.height)
dest = ray.Rectangle(0, screen_height - int(self.rainbow_up.attribute) + global_data.textures['scene_change_rainbow'][2].height, texture.width, screen_height)
ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
texture = global_data.textures['scene_change_rainbow'][3]
offset = 0
if self.chara_down is not None:
offset = int(self.chara_down.attribute)
ray.draw_texture(texture, 76, 816 - int(self.rainbow_up.attribute) + offset, ray.WHITE)
class FileSystemItem:
GENRE_MAP = {
'J-POP': 555,
'アニメ': 560,
'どうよう': 565,
'バラエティー': 570,
'クラシック': 575,
'ゲームミュージック': 580,
'ナムコオリジナル': 585,
'VOCALOID': 615,
}
"""Base class for files and directories in the navigation system"""
def __init__(self, path: Path, name: str):
self.path = path
self.selected = False
def is_selectable(self):
return True
class Directory(FileSystemItem):
"""Represents a directory in the navigation system"""
def __init__(self, path: Path, name: str, texture_index: int, has_box_def=False, to_root=False, back=False):
super().__init__(path, name)
self.has_box_def = has_box_def
self.to_root = to_root
self.back = back
if self.to_root or self.back:
texture_index = 552
tja_count = 0
if self.has_box_def:
tja_count = self.count_tja_files(path)
if (path / "song_list.txt").exists():
with open(path / "song_list.txt", 'r', encoding='utf-8-sig') as song_list_file:
tja_count += len(song_list_file.readlines())
self.box = SongBox(name, texture_index, True, tja_count=tja_count)
def count_tja_files(self, folder_path: Path):
tja_count = 0
#print(f"Scanning {folder_path}")
try:
items = folder_path.iterdir()
for item in items:
item_path = folder_path / item
if item_path.is_file():
if item.suffix == '.tja':
tja_count += 1
#print(f"Found: {item_path}")
elif item_path.is_dir():
tja_count += self.count_tja_files(item_path)
except PermissionError:
print(f"Permission denied accessing '{folder_path}'")
except Exception as e:
print(f"Error accessing '{folder_path}': {e}")
return tja_count
def get_display_name(self):
return self.box
class SongFile(FileSystemItem):
"""Represents a song file (TJA) in the navigation system"""
def __init__(self, path: Path, name: str, texture_index: int):
super().__init__(path, name)
self.tja = TJAParser(path)
title = self.tja.metadata.title.get(get_config()['general']['language'].lower(), self.tja.metadata.title['en'])
self.box = SongBox(title, texture_index, False, tja=self.tja)
def get_display_name(self):
return self.box
class FileNavigator:
"""Manages navigation through the file system"""
def __init__(self, root_dirs: list[str]):
# Handle both single path and list of paths
if isinstance(root_dirs, (list, tuple)):
self.root_dirs = [Path(p) if not isinstance(p, Path) else p for p in root_dirs]
else:
self.root_dirs = [Path(root_dirs) if not isinstance(root_dirs, Path) else root_dirs]
self.in_root_selection = True # Whether we're showing the root directory selection screen
self.current_dir = Path()
self.current_root_dir = Path()
self.items: list[Directory | SongFile] = []
self.selected_index = 0
self.history = [] # For tracking directory navigation history
self.load_root_directories()
def check_for_box_def(self, dir_path: Path):
"""Check if the directory contains a box.def file"""
box_def_path = dir_path / "box.def"
return box_def_path.exists()
def get_tja_folder_count(self, directory: Path):
return len(self.find_tja_files_recursive(directory))
def find_tja_files_recursive(self, directory: Path, box_def_dirs_only=True):
tja_files = []
try:
has_box_def = self.check_for_box_def(directory)
if box_def_dirs_only and has_box_def and directory != self.current_dir:
return []
for path in directory.iterdir():
if path.is_file() and path.suffix.lower() == ".tja":
tja_files.append(path)
elif path.is_dir():
sub_dir_has_box_def = self.check_for_box_def(path)
if not sub_dir_has_box_def:
tja_files.extend(self.find_tja_files_recursive(path, box_def_dirs_only))
except (PermissionError, OSError):
pass
return tja_files
def parse_box_def(self, path):
texture_index = 620
name = path.name
with open(path / "box.def", 'r', encoding='utf-8') as box_def:
for line in box_def:
if line.strip().startswith("#GENRE:"):
texture_index = FileSystemItem.GENRE_MAP[line.split(":")[1].strip()]
if line.strip().startswith("#TITLE:"):
name = line.split(":")[1].strip()
if line.strip().startswith("#TITLEJA:"):
if get_config()['general']['language'] == 'ja':
name = line.split(":")[1].strip()
return name, texture_index
def calculate_box_positions(self):
"""Dynamically calculate box positions based on current selection with wrap-around support"""
if not self.items:
return
num_items = len(self.items)
# Calculate positions for each item relative to the selected item
for i, item in enumerate(self.items):
# Calculate the circular distance from selected index
offset = i - self.selected_index
# Handle wrap-around by choosing the shortest circular distance
if offset > num_items // 2:
offset -= num_items
elif offset < -num_items // 2:
offset += num_items
# Calculate position based on offset
position = SongSelectScreen.BOX_CENTER + (100 * offset)
# Apply the same position adjustments as before
if position == SongSelectScreen.BOX_CENTER:
position += 150
elif position > SongSelectScreen.BOX_CENTER:
position += 300
else:
position -= 0
if item.box.position == -11111:
item.box.position = position
item.box.target_position = position
else:
item.box.target_position = position
def set_base_positions(self):
"""Set initial positions for all items"""
self.calculate_box_positions()
def load_root_directories(self):
"""Load the list of root directories as selectable items"""
self.items = []
self.in_root_selection = True
self.current_dir = Path()
self.current_root_dir = Path()
# Create directory items for each root
for root_path in self.root_dirs:
name = root_path.name if root_path.name else str(root_path)
has_box_def = self.check_for_box_def(root_path)
# Only add roots with box.def as directories
if has_box_def:
name, texture_index = self.parse_box_def(root_path)
self.items.append(Directory(root_path, name, texture_index, has_box_def=True))
else:
# For roots without box.def, add their TJA files directly to the root selection
tja_files = self.find_tja_files_recursive(root_path)
for tja_path in sorted(tja_files):
self.items.append(SongFile(tja_path, tja_path.name, 620))
# Reset selection
self.selected_index = 0 if self.items else -1
self.calculate_box_positions()
def load_current_directory(self):
"""Load all directories and TJA files in the current directory"""
self.items = []
self.selected_index = 0
if self.current_dir != self.current_root_dir:
self.items.append(Directory(self.current_dir.parent, "", 552, back=True))
elif not self.in_root_selection:
self.items.append(Directory(Path(), "", 552, to_root=True))
# Add only directories that contain box.def files
for path in sorted(self.current_dir.iterdir()):
if path.is_dir():
has_box_def = self.check_for_box_def(path)
if has_box_def:
name, texture_index = self.parse_box_def(path)
self.items.append(Directory(path, name, texture_index, has_box_def=True))
tja_files = []
if (self.current_dir / 'song_list.txt').exists():
updated_lines = []
file_updated = False
with open(self.current_dir / 'song_list.txt', 'r', encoding='utf-8-sig') as song_list:
for line in song_list:
hash, title, subtitle = line.strip().split('|')
original_hash = hash
if song_hash.song_hashes is not None:
if hash in song_hash.song_hashes:
tja_files.append(Path(song_hash.song_hashes[hash]["file_path"]))
else:
for key, value in song_hash.song_hashes.items():
if value["title"]["en"] == title and value["subtitle"]["en"][2:] == subtitle and Path(value["file_path"]).exists():
hash = key
tja_files.append(Path(song_hash.song_hashes[hash]["file_path"]))
break
if hash != original_hash:
file_updated = True
updated_lines.append(f"{hash}|{title}|{subtitle}")
if file_updated:
with open(self.current_dir / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list:
for line in updated_lines:
song_list.write(line + '\n')
else:
tja_files = self.find_tja_files_recursive(self.current_dir)
# Then add TJA files found
for i, tja_path in enumerate(sorted(tja_files)):
if i % 10 == 0 and i != 0:
if self.current_dir != self.current_root_dir:
self.items.append(Directory(self.current_dir.parent, "", 552, back=True))
elif not self.in_root_selection:
self.items.append(Directory(Path(), "", 552, to_root=True))
texture_index = 620
_, texture_index = self.parse_box_def(self.current_dir)
self.items.append(SongFile(tja_path, tja_path.name, texture_index))
self.calculate_box_positions()
def navigate_left(self):
"""Move selection left with wrap-around"""
if self.items:
self.selected_index = (self.selected_index - 1) % len(self.items)
self.calculate_box_positions()
def navigate_right(self):
"""Move selection right with wrap-around"""
if self.items:
self.selected_index = (self.selected_index + 1) % len(self.items)
self.calculate_box_positions()
def get_items(self):
"""Get visible items on screen - now returns all items since positions are dynamic"""
# With wrap-around, we might want to show all items or filter based on visibility
# For now, return all items since their positions are dynamically calculated
return self.items
def get_visible_items(self, screen_width=1280):
"""Get only the items that would be visible on screen"""
if not self.items:
return []
visible_items = []
center = SongSelectScreen.BOX_CENTER
half_screen = screen_width // 2
for item in self.items:
# Check if item's position is within the visible screen area
if abs(item.box.position - center) <= half_screen:
visible_items.append(item)
return visible_items
def select_current_item(self):
"""Select the currently highlighted item"""
if not self.items or self.selected_index >= len(self.items):
return
selected_item = self.items[self.selected_index]
if isinstance(selected_item, Directory):
if selected_item.to_root:
self.load_root_directories()
else:
if self.current_dir is not None:
self.history.append((self.current_dir, self.selected_index, self.in_root_selection, self.current_root_dir))
self.current_dir = selected_item.path
if self.in_root_selection:
self.current_root_dir = selected_item.path
self.in_root_selection = False
self.selected_index = 0
self.load_current_directory()
elif isinstance(selected_item, SongFile):
return selected_item
def go_back(self):
"""Navigate back to the previous directory"""
if self.history:
previous_dir, previous_index, previous_in_root, previous_root_dir = self.history.pop()
self.current_dir = previous_dir
self.selected_index = previous_index
self.in_root_selection = previous_in_root
self.current_root_dir = previous_root_dir
self.load_current_directory()
elif not self.in_root_selection:
# If we're not in history but also not in root selection, go back to root selection
self.load_root_directories()
def get_current_item(self):
"""Get the currently selected item"""
if self.items and 0 <= self.selected_index < len(self.items):
return self.items[self.selected_index]
raise Exception()