mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 19:50:12 +01:00
1598 lines
75 KiB
Python
1598 lines
75 KiB
Python
from dataclasses import dataclass
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
import random
|
|
from typing import Optional, Union
|
|
from libs.audio import audio
|
|
from libs.animation import Animation, MoveAnimation
|
|
from libs.global_data import Crown, Difficulty
|
|
from libs.tja import TJAParser, test_encodings
|
|
from libs.texture import tex
|
|
from libs.utils import OutlinedText, get_current_ms, global_data
|
|
from datetime import datetime, timedelta
|
|
import sqlite3
|
|
import pyray as ray
|
|
|
|
BOX_CENTER = 444
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class SongBox:
|
|
"""A box for the song select screen."""
|
|
OUTLINE_MAP = {
|
|
1: ray.Color(0, 77, 104, 255),
|
|
2: ray.Color(156, 64, 2, 255),
|
|
3: ray.Color(84, 101, 126, 255),
|
|
4: ray.Color(153, 4, 46, 255),
|
|
5: ray.Color(60, 104, 0, 255),
|
|
6: ray.Color(134, 88, 0, 255),
|
|
7: ray.Color(79, 40, 134, 255),
|
|
8: ray.Color(148, 24, 0, 255),
|
|
9: ray.Color(101, 0, 82, 255),
|
|
10: ray.Color(140, 39, 92, 255),
|
|
11: ray.Color(151, 57, 30, 255),
|
|
12: ray.Color(35, 123, 103, 255),
|
|
13: ray.Color(25, 68, 137, 255),
|
|
14: ray.Color(157, 13, 31, 255)
|
|
}
|
|
BACK_INDEX = 17
|
|
DEFAULT_INDEX = 9
|
|
def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None,
|
|
tja_count: Optional[int] = None, box_texture: Optional[str] = None, name_texture_index: Optional[int] = None):
|
|
self.text_name = name
|
|
self.texture_index = texture_index
|
|
if name_texture_index is None:
|
|
self.name_texture_index = texture_index
|
|
else:
|
|
self.name_texture_index = name_texture_index
|
|
self.box_texture_path = box_texture
|
|
self.box_texture = None
|
|
self.scores = dict()
|
|
self.crown = dict()
|
|
self.position = -11111
|
|
self.start_position = -1
|
|
self.target_position = -1
|
|
self.is_open = False
|
|
self.is_back = self.texture_index == SongBox.BACK_INDEX
|
|
if self.is_back:
|
|
for i in range(1, SongBox.BACK_INDEX-1):
|
|
if audio.is_sound_playing(f'genre_voice_{i}'):
|
|
audio.stop_sound(f'genre_voice_{i}')
|
|
self.name = None
|
|
self.hori_name = None
|
|
self.yellow_box = None
|
|
self.open_anim = Animation.create_move(133, start_position=0, total_distance=150, delay=83.33)
|
|
self.open_fade = Animation.create_fade(200, initial_opacity=0, final_opacity=1.0)
|
|
self.move = None
|
|
self.wait = 0
|
|
self.is_dir = is_dir
|
|
self.tja_count = tja_count
|
|
self.tja_count_text = None
|
|
self.score_history = None
|
|
self.history_wait = 0
|
|
self.tja = tja
|
|
self.hash = dict()
|
|
self.is_favorite = False
|
|
|
|
def reset(self):
|
|
if self.yellow_box is not None:
|
|
self.yellow_box.reset()
|
|
self.yellow_box.create_anim()
|
|
if self.name is not None:
|
|
self.name.unload()
|
|
self.name = None
|
|
if self.box_texture is not None:
|
|
ray.unload_texture(self.box_texture)
|
|
self.box_texture = None
|
|
if self.hori_name is not None:
|
|
self.hori_name.unload()
|
|
self.hori_name = None
|
|
self.is_open = False
|
|
|
|
def get_scores(self):
|
|
if self.tja is None:
|
|
return
|
|
with sqlite3.connect('scores.db') as con:
|
|
cursor = con.cursor()
|
|
# Batch database query for all diffs at once
|
|
if self.tja.metadata.course_data:
|
|
hash_values = [self.hash[diff] for diff in self.tja.metadata.course_data if diff in self.hash]
|
|
placeholders = ','.join('?' * len(hash_values))
|
|
|
|
batch_query = f"""
|
|
SELECT hash, score, good, ok, bad, drumroll, clear
|
|
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:
|
|
if diff not in self.hash:
|
|
continue
|
|
diff_hash = self.hash[diff]
|
|
self.scores[diff] = hash_to_score.get(diff_hash)
|
|
self.score_history = None
|
|
|
|
def move_box(self):
|
|
if self.position != self.target_position and self.move is None:
|
|
if self.position < self.target_position:
|
|
direction = 1
|
|
else:
|
|
direction = -1
|
|
if abs(self.target_position - self.position) > 250:
|
|
direction *= -1
|
|
self.move = Animation.create_move(83.3, start_position=0, total_distance=100 * direction, ease_out='cubic')
|
|
self.move.start()
|
|
if self.is_open or self.target_position == BOX_CENTER + 150:
|
|
self.move.total_distance = 250 * direction
|
|
self.start_position = self.position
|
|
if self.move is not None:
|
|
self.move.update(get_current_ms())
|
|
self.position = self.start_position + int(self.move.attribute)
|
|
if self.move.is_finished:
|
|
self.position = self.target_position
|
|
self.move = None
|
|
if not (-56 <= self.position <= 1280):
|
|
self.reset()
|
|
|
|
def update(self, is_diff_select: bool):
|
|
current_time = get_current_ms()
|
|
self.is_diff_select = is_diff_select
|
|
is_open_prev = self.is_open
|
|
self.move_box()
|
|
self.is_open = self.position == BOX_CENTER + 150
|
|
|
|
if not (-56 <= self.position <= 1280):
|
|
return
|
|
if self.yellow_box is not None:
|
|
self.yellow_box.update(is_diff_select)
|
|
|
|
if self.history_wait == 0:
|
|
self.history_wait = current_time
|
|
|
|
if self.score_history is None and {k: v for k, v in self.scores.items() if v is not None}:
|
|
self.score_history = ScoreHistory(self.scores, current_time)
|
|
|
|
if not is_open_prev and self.is_open:
|
|
if self.tja is not None or self.is_back:
|
|
self.yellow_box = YellowBox(self.name, self.is_back, tja=self.tja)
|
|
self.yellow_box.create_anim()
|
|
else:
|
|
self.hori_name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5)
|
|
self.open_anim.start()
|
|
self.open_fade.start()
|
|
self.wait = current_time
|
|
if current_time >= self.history_wait + 3000:
|
|
self.history_wait = current_time
|
|
if self.tja is None and self.texture_index != 17 and not audio.is_sound_playing('voice_enter'):
|
|
audio.play_sound(f'genre_voice_{self.texture_index}', 'voice')
|
|
elif not self.is_open and is_open_prev and self.texture_index != 17 and audio.is_sound_playing(f'genre_voice_{self.texture_index}'):
|
|
audio.stop_sound(f'genre_voice_{self.texture_index}')
|
|
if self.tja_count is not None and self.tja_count > 0 and self.tja_count_text is None:
|
|
self.tja_count_text = OutlinedText(str(self.tja_count), 35, ray.WHITE, outline_thickness=5)#, horizontal_spacing=1.2)
|
|
if self.box_texture is None and self.box_texture_path is not None:
|
|
self.box_texture = ray.load_texture(self.box_texture_path)
|
|
|
|
self.open_anim.update(current_time)
|
|
self.open_fade.update(current_time)
|
|
|
|
if self.name is None:
|
|
self.name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5, vertical=True)
|
|
|
|
if self.score_history is not None:
|
|
self.score_history.update(current_time)
|
|
|
|
|
|
def _draw_closed(self, x: int, y: int):
|
|
tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x)
|
|
offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0
|
|
tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=32, y=offset)
|
|
tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x)
|
|
if self.texture_index == SongBox.DEFAULT_INDEX:
|
|
tex.draw_texture('box', 'genre_overlay', x=x, y=y)
|
|
elif self.texture_index == 14:
|
|
tex.draw_texture('box', 'diff_overlay', x=x, y=y)
|
|
if not self.is_back and self.is_dir:
|
|
tex.draw_texture('box', 'folder_clip', frame=self.texture_index, x=x - (1 - offset), y=y)
|
|
|
|
if self.is_back:
|
|
tex.draw_texture('box', 'back_text', x=x, y=y)
|
|
return
|
|
elif self.name is not None:
|
|
self.name.draw(outline_color=SongBox.OUTLINE_MAP.get(self.name_texture_index, ray.Color(101, 0, 82, 255)), x=x + 47 - int(self.name.texture.width / 2), y=y+35, y2=min(self.name.texture.height, 417)-self.name.texture.height)
|
|
|
|
if self.tja is not None and self.tja.ex_data.new:
|
|
tex.draw_texture('yellow_box', 'ex_data_new_song_balloon', x=x, y=y)
|
|
valid_scores = {k: v for k, v in self.scores.items() if v is not None}
|
|
if valid_scores:
|
|
highest_key = max(valid_scores.keys())
|
|
score = self.scores[highest_key]
|
|
if score and score[5] == Crown.DFC:
|
|
tex.draw_texture('yellow_box', 'crown_dfc', x=x, y=y, frame=min(Difficulty.URA, highest_key))
|
|
elif score and score[5] == Crown.FC:
|
|
tex.draw_texture('yellow_box', 'crown_fc', x=x, y=y, frame=min(Difficulty.URA, highest_key))
|
|
elif score and score[5] >= Crown.CLEAR:
|
|
tex.draw_texture('yellow_box', 'crown_clear', x=x, y=y, frame=min(Difficulty.URA, highest_key))
|
|
if self.crown: #Folder lamp
|
|
highest_crown = max(self.crown)
|
|
if self.crown[highest_crown] == 'DFC':
|
|
tex.draw_texture('yellow_box', 'crown_dfc', x=x, y=y, frame=min(Difficulty.URA, highest_crown))
|
|
elif self.crown[highest_crown] == 'FC':
|
|
tex.draw_texture('yellow_box', 'crown_fc', x=x, y=y, frame=min(Difficulty.URA, highest_crown))
|
|
else:
|
|
tex.draw_texture('yellow_box', 'crown_clear', x=x, y=y, frame=min(Difficulty.URA, highest_crown))
|
|
|
|
def _draw_open(self, x: int, y: int, fade_override: Optional[float]):
|
|
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:
|
|
tex.draw_texture('box', 'folder_top_edge', x=x, y=y - self.open_anim.attribute, color=color, mirror='horizontal', frame=self.texture_index)
|
|
tex.draw_texture('box', 'folder_top', x=x, y=y - self.open_anim.attribute, color=color, frame=self.texture_index)
|
|
tex.draw_texture('box', 'folder_top_edge', x=x+268, y=y - self.open_anim.attribute, color=color, frame=self.texture_index)
|
|
dest_width = min(300, self.hori_name.texture.width)
|
|
self.hori_name.draw(outline_color=ray.BLACK, x=(x + 48) - (dest_width//2), y=y + 107 - self.open_anim.attribute, x2=dest_width-self.hori_name.texture.width, color=color)
|
|
|
|
tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x - self.open_anim.attribute)
|
|
offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0
|
|
tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x - self.open_anim.attribute, y=offset, x2=(self.open_anim.attribute*2)+32)
|
|
tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x + self.open_anim.attribute)
|
|
|
|
if self.texture_index == SongBox.DEFAULT_INDEX:
|
|
tex.draw_texture('box', 'genre_overlay_large', x=x, y=y, color=color)
|
|
elif self.texture_index == 14:
|
|
tex.draw_texture('box', 'diff_overlay_large', x=x, y=y, color=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 and self.texture_index != 14:
|
|
tex.draw_texture('yellow_box', 'song_count_back', color=color, fade=0.5)
|
|
tex.draw_texture('yellow_box', 'song_count_num', color=color)
|
|
tex.draw_texture('yellow_box', 'song_count_songs', color=color)
|
|
dest_width = min(124, self.tja_count_text.texture.width)
|
|
self.tja_count_text.draw(outline_color=ray.BLACK, x=560 - (dest_width//2), y=126, x2=dest_width-self.tja_count_text.texture.width, color=color)
|
|
if self.texture_index != 9:
|
|
tex.draw_texture('box', 'folder_graphic', color=color, frame=self.texture_index)
|
|
tex.draw_texture('box', 'folder_text', color=color, frame=self.texture_index)
|
|
elif self.box_texture is not None:
|
|
ray.draw_texture(self.box_texture, (x+48) - (self.box_texture.width//2), (y+240) - (self.box_texture.height//2), color)
|
|
|
|
def draw_score_history(self):
|
|
if self.is_open and get_current_ms() >= self.wait + 83.33:
|
|
if self.score_history is not None and get_current_ms() >= self.history_wait + 3000:
|
|
self.score_history.draw()
|
|
|
|
def draw(self, x: int, y: int, is_ura: bool, fade_override=None):
|
|
if self.is_open and get_current_ms() >= self.wait + 83.33:
|
|
if self.yellow_box is not None:
|
|
self.yellow_box.draw(self, fade_override, is_ura)
|
|
else:
|
|
self._draw_open(x, y, self.open_fade.attribute)
|
|
else:
|
|
self._draw_closed(x, y)
|
|
|
|
class YellowBox:
|
|
"""A song box when it is opened."""
|
|
def __init__(self, name: Optional[OutlinedText], is_back: bool, tja: Optional[TJAParser] = None, is_dan: bool = False):
|
|
self.is_diff_select = False
|
|
self.name = name
|
|
self.is_back = is_back
|
|
self.tja = tja
|
|
self.is_dan = is_dan
|
|
self.subtitle = None
|
|
if self.tja is not None:
|
|
subtitle_text = self.tja.metadata.subtitle.get(global_data.config['general']['language'], '')
|
|
font_size = 30 if len(subtitle_text) < 30 else 20
|
|
self.subtitle = OutlinedText(subtitle_text, font_size, ray.WHITE, outline_thickness=5, vertical=True)
|
|
|
|
self.left_out = tex.get_animation(9)
|
|
self.right_out = tex.get_animation(10)
|
|
self.center_out = tex.get_animation(11)
|
|
self.fade = tex.get_animation(12)
|
|
|
|
self.left_out.reset()
|
|
self.right_out.reset()
|
|
self.center_out.reset()
|
|
self.fade.reset()
|
|
|
|
self.left_out_2 = tex.get_animation(13)
|
|
self.right_out_2 = tex.get_animation(14)
|
|
self.center_out_2 = tex.get_animation(15)
|
|
self.top_y_out = tex.get_animation(16)
|
|
self.center_h_out = tex.get_animation(17)
|
|
self.fade_in = tex.get_animation(18)
|
|
|
|
self.right_out_2.reset()
|
|
self.top_y_out.reset()
|
|
self.center_h_out.reset()
|
|
|
|
self.right_x = self.right_out.attribute
|
|
self.left_x = self.left_out.attribute
|
|
self.center_width = self.center_out.attribute
|
|
self.top_y = self.top_y_out.attribute
|
|
self.center_height = self.center_h_out.attribute
|
|
self.bottom_y = tex.textures['yellow_box']['yellow_box_bottom_right'].y[0]
|
|
self.edge_height = tex.textures['yellow_box']['yellow_box_bottom_right'].height
|
|
|
|
def reset(self):
|
|
if self.subtitle is not None:
|
|
self.subtitle.unload()
|
|
self.subtitle = None
|
|
|
|
def create_anim(self):
|
|
self.right_out_2.reset()
|
|
self.top_y_out.reset()
|
|
self.center_h_out.reset()
|
|
self.left_out.start()
|
|
self.right_out.start()
|
|
self.center_out.start()
|
|
self.fade.start()
|
|
|
|
def create_anim_2(self):
|
|
self.left_out_2.start()
|
|
self.right_out_2.start()
|
|
self.center_out_2.start()
|
|
self.top_y_out.start()
|
|
self.center_h_out.start()
|
|
self.fade_in.start()
|
|
|
|
def update(self, is_diff_select: bool):
|
|
current_time = get_current_ms()
|
|
self.left_out.update(current_time)
|
|
self.right_out.update(current_time)
|
|
self.center_out.update(current_time)
|
|
self.fade.update(current_time)
|
|
self.fade_in.update(current_time)
|
|
self.left_out_2.update(current_time)
|
|
self.right_out_2.update(current_time)
|
|
self.center_out_2.update(current_time)
|
|
self.top_y_out.update(current_time)
|
|
self.center_h_out.update(current_time)
|
|
if is_diff_select and not self.is_diff_select:
|
|
self.create_anim_2()
|
|
if self.is_diff_select:
|
|
self.right_x = self.right_out_2.attribute
|
|
self.left_x = self.left_out_2.attribute
|
|
self.top_y = self.top_y_out.attribute
|
|
self.center_width = self.center_out_2.attribute
|
|
self.center_height = self.center_h_out.attribute
|
|
else:
|
|
self.right_x = self.right_out.attribute
|
|
self.left_x = self.left_out.attribute
|
|
self.center_width = self.center_out.attribute
|
|
self.top_y = self.top_y_out.attribute
|
|
self.center_height = self.center_h_out.attribute
|
|
self.is_diff_select = is_diff_select
|
|
|
|
def _draw_tja_data(self, song_box, color, fade):
|
|
if self.tja is None:
|
|
return
|
|
for diff in self.tja.metadata.course_data:
|
|
if diff >= Difficulty.URA:
|
|
continue
|
|
elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][4] is not None and song_box.scores[diff][4] == Crown.DFC:
|
|
tex.draw_texture('yellow_box', 's_crown_dfc', x=(diff*60), color=color)
|
|
elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][4] is not None and song_box.scores[diff][4] == Crown.FC:
|
|
tex.draw_texture('yellow_box', 's_crown_fc', x=(diff*60), color=color)
|
|
elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][4] is not None and song_box.scores[diff][4] >= Crown.CLEAR:
|
|
tex.draw_texture('yellow_box', 's_crown_clear', x=(diff*60), color=color)
|
|
tex.draw_texture('yellow_box', 's_crown_outline', x=(diff*60), fade=min(fade, 0.25))
|
|
|
|
if self.tja.ex_data.new_audio:
|
|
tex.draw_texture('yellow_box', 'ex_data_new_audio', color=color)
|
|
elif self.tja.ex_data.old_audio:
|
|
tex.draw_texture('yellow_box', 'ex_data_old_audio', color=color)
|
|
elif self.tja.ex_data.limited_time:
|
|
tex.draw_texture('yellow_box', 'ex_data_limited_time', color=color)
|
|
elif self.tja.ex_data.new:
|
|
tex.draw_texture('yellow_box', 'ex_data_new_song', color=color)
|
|
if song_box.is_favorite:
|
|
tex.draw_texture('yellow_box', f'favorite_{global_data.player_num}p', color=color)
|
|
|
|
for i in range(4):
|
|
tex.draw_texture('yellow_box', 'difficulty_bar', frame=i, x=(i*60), color=color)
|
|
if i not in self.tja.metadata.course_data:
|
|
tex.draw_texture('yellow_box', 'difficulty_bar_shadow', frame=i, x=(i*60), fade=min(fade, 0.25))
|
|
|
|
for diff in self.tja.metadata.course_data:
|
|
if diff >= Difficulty.URA:
|
|
continue
|
|
for j in range(self.tja.metadata.course_data[diff].level):
|
|
tex.draw_texture('yellow_box', 'star', x=(diff*60), y=(j*-17), color=color)
|
|
if self.tja.metadata.course_data[diff].is_branching and (get_current_ms() // 1000) % 2 == 0:
|
|
tex.draw_texture('yellow_box', 'branch_indicator', x=(diff*60), color=color)
|
|
|
|
def _draw_tja_data_diff(self, is_ura: bool, song_box: Optional[SongBox] = None):
|
|
if self.tja is None:
|
|
return
|
|
tex.draw_texture('diff_select', 'back', fade=self.fade_in.attribute)
|
|
tex.draw_texture('diff_select', 'option', fade=self.fade_in.attribute)
|
|
tex.draw_texture('diff_select', 'neiro', fade=self.fade_in.attribute)
|
|
|
|
for diff in self.tja.metadata.course_data:
|
|
if song_box is None:
|
|
continue
|
|
if diff >= Difficulty.URA:
|
|
continue
|
|
elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] is not None and song_box.scores[diff][4] == 2 and song_box.scores[diff][2] == 0) or (song_box.scores[diff][2] == 0 and song_box.scores[diff][3] == 0)):
|
|
tex.draw_texture('yellow_box', 's_crown_dfc', x=(diff*115)+8, y=-120, fade=self.fade_in.attribute)
|
|
elif diff in song_box.scores and song_box.scores[diff] is not None and ((song_box.scores[diff][4] is not None and song_box.scores[diff][4] == 2) or (song_box.scores[diff][3] == 0)):
|
|
tex.draw_texture('yellow_box', 's_crown_fc', x=(diff*115)+8, y=-120, fade=self.fade_in.attribute)
|
|
elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][4] is not None and song_box.scores[diff][4] >= 1:
|
|
tex.draw_texture('yellow_box', 's_crown_clear', x=(diff*115)+8, y=-120, fade=self.fade_in.attribute)
|
|
tex.draw_texture('yellow_box', 's_crown_outline', x=(diff*115)+8, y=-120, fade=min(self.fade_in.attribute, 0.25))
|
|
|
|
for i in range(4):
|
|
if i == Difficulty.ONI and is_ura:
|
|
tex.draw_texture('diff_select', 'diff_tower', frame=4, x=(i*115), fade=self.fade_in.attribute)
|
|
tex.draw_texture('diff_select', 'ura_oni_plate', fade=self.fade_in.attribute)
|
|
else:
|
|
tex.draw_texture('diff_select', 'diff_tower', frame=i, x=(i*115), fade=self.fade_in.attribute)
|
|
if i not in self.tja.metadata.course_data:
|
|
tex.draw_texture('diff_select', 'diff_tower_shadow', frame=i, x=(i*115), fade=min(self.fade_in.attribute, 0.25))
|
|
|
|
for course in self.tja.metadata.course_data:
|
|
if (course == Difficulty.URA and not is_ura) or (course == Difficulty.ONI and is_ura):
|
|
continue
|
|
for j in range(self.tja.metadata.course_data[course].level):
|
|
tex.draw_texture('yellow_box', 'star_ura', x=min(course, Difficulty.ONI)*115, y=(j*-20), fade=self.fade_in.attribute)
|
|
if self.tja.metadata.course_data[course].is_branching and (get_current_ms() // 1000) % 2 == 0:
|
|
if course == Difficulty.URA:
|
|
name = 'branch_indicator_ura'
|
|
else:
|
|
name = 'branch_indicator_diff'
|
|
tex.draw_texture('yellow_box', name, x=min(course, Difficulty.ONI)*115, fade=self.fade_in.attribute)
|
|
|
|
def _draw_text(self, song_box):
|
|
if not isinstance(self.right_out, MoveAnimation):
|
|
return
|
|
if not isinstance(self.right_out_2, MoveAnimation):
|
|
return
|
|
if not isinstance(self.top_y_out, MoveAnimation):
|
|
return
|
|
x = song_box.position + (self.right_out.attribute*0.85 - (self.right_out.start_position*0.85)) + self.right_out_2.attribute - self.right_out_2.start_position
|
|
if self.is_back:
|
|
tex.draw_texture('box', 'back_text_highlight', x=x)
|
|
elif self.name is not None:
|
|
texture = self.name.texture
|
|
self.name.draw(outline_color=ray.BLACK, x=x + 30, y=35 + self.top_y_out.attribute, y2=min(texture.height, 417)-texture.height, color=ray.WHITE)
|
|
if self.subtitle is not None:
|
|
texture = self.subtitle.texture
|
|
y = self.bottom_y - min(texture.height, 410) + 10 + self.top_y_out.attribute - self.top_y_out.start_position
|
|
self.subtitle.draw(outline_color=ray.BLACK, x=x-15, y=y, y2=min(texture.height, 410)-texture.height)
|
|
|
|
def _draw_yellow_box(self):
|
|
tex.draw_texture('yellow_box', 'yellow_box_bottom_right', x=self.right_x)
|
|
tex.draw_texture('yellow_box', 'yellow_box_bottom_left', x=self.left_x, y=self.bottom_y)
|
|
tex.draw_texture('yellow_box', 'yellow_box_top_right', x=self.right_x, y=self.top_y)
|
|
tex.draw_texture('yellow_box', 'yellow_box_top_left', x=self.left_x, y=self.top_y)
|
|
tex.draw_texture('yellow_box', 'yellow_box_bottom', x=self.left_x + self.edge_height, y=self.bottom_y, x2=self.center_width)
|
|
tex.draw_texture('yellow_box', 'yellow_box_right', x=self.right_x, y=self.top_y + self.edge_height, y2=self.center_height)
|
|
tex.draw_texture('yellow_box', 'yellow_box_left', x=self.left_x, y=self.top_y + self.edge_height, y2=self.center_height)
|
|
tex.draw_texture('yellow_box', 'yellow_box_top', x=self.left_x + self.edge_height, y=self.top_y, x2=self.center_width)
|
|
tex.draw_texture('yellow_box', 'yellow_box_center', x=self.left_x + self.edge_height, y=self.top_y + self.edge_height, x2=self.center_width, y2=self.center_height)
|
|
|
|
def draw(self, song_box: Optional[SongBox], fade_override: Optional[float], is_ura: bool):
|
|
self._draw_yellow_box()
|
|
if self.is_dan:
|
|
return
|
|
if self.is_diff_select and self.tja is not None:
|
|
self._draw_tja_data_diff(is_ura, song_box)
|
|
else:
|
|
fade = self.fade.attribute
|
|
if fade_override is not None:
|
|
fade = min(self.fade.attribute, fade_override)
|
|
if self.is_back:
|
|
tex.draw_texture('box', 'back_graphic', fade=fade)
|
|
self._draw_tja_data(song_box, ray.fade(ray.WHITE, fade), fade)
|
|
|
|
self._draw_text(song_box)
|
|
|
|
class DanBox:
|
|
def __init__(self, title: str, color: int, songs: list[tuple[TJAParser, int, int, int]], exams: list['Exam']):
|
|
self.position = -11111
|
|
self.start_position = -1
|
|
self.target_position = -1
|
|
self.move = None
|
|
self.is_open = False
|
|
self.is_back = False
|
|
self.title = title
|
|
self.color = color
|
|
self.songs = songs
|
|
self.exams = exams
|
|
self.song_text: list[tuple[OutlinedText, OutlinedText]] = []
|
|
self.total_notes = 0
|
|
for song, genre_index, difficulty, level in self.songs:
|
|
notes, branch_m, branch_e, branch_n = song.notes_to_position(difficulty)
|
|
self.total_notes += sum(1 for note in notes.play_notes if note.type < 5)
|
|
for branch in branch_m:
|
|
self.total_notes += sum(1 for note in branch.play_notes if note.type < 5)
|
|
for branch in branch_e:
|
|
self.total_notes += sum(1 for note in branch.play_notes if note.type < 5)
|
|
for branch in branch_n:
|
|
self.total_notes += sum(1 for note in branch.play_notes if note.type < 5)
|
|
self.name = None
|
|
self.hori_name = None
|
|
self.yellow_box = None
|
|
|
|
def move_box(self):
|
|
if self.position != self.target_position and self.move is None:
|
|
if self.position < self.target_position:
|
|
direction = 1
|
|
else:
|
|
direction = -1
|
|
if abs(self.target_position - self.position) > 250:
|
|
direction *= -1
|
|
self.move = Animation.create_move(83.3*2, start_position=0, total_distance=300 * direction, ease_out='cubic')
|
|
self.move.start()
|
|
if self.is_open or self.target_position == BOX_CENTER + 150:
|
|
self.move.total_distance = 450 * direction * -1
|
|
self.start_position = self.position
|
|
if self.move is not None:
|
|
self.move.update(get_current_ms())
|
|
self.position = self.start_position + int(self.move.attribute)
|
|
if self.move.is_finished:
|
|
self.position = self.target_position
|
|
self.move = None
|
|
if not (-56 <= self.position <= 1280):
|
|
self.reset()
|
|
|
|
def reset(self):
|
|
if self.name is not None:
|
|
self.name.unload()
|
|
self.name = None
|
|
|
|
def get_text(self):
|
|
if self.name is None:
|
|
self.name = OutlinedText(self.title, 40, ray.WHITE, vertical=True)
|
|
self.hori_name = OutlinedText(self.title, 40, ray.WHITE)
|
|
if self.is_open and not self.song_text:
|
|
for song, genre, difficulty, level in self.songs:
|
|
title = song.metadata.title.get(global_data.config["general"]["language"], song.metadata.title["en"])
|
|
subtitle = song.metadata.subtitle.get(global_data.config["general"]["language"], "")
|
|
title_text = OutlinedText(title, 40, ray.WHITE, vertical=True)
|
|
font_size = 30 if len(subtitle) < 30 else 20
|
|
subtitle_text = OutlinedText(subtitle, font_size, ray.WHITE, vertical=True)
|
|
self.song_text.append((title_text, subtitle_text))
|
|
|
|
def update(self, is_diff_select: bool):
|
|
self.move_box()
|
|
self.get_text()
|
|
is_open_prev = self.is_open
|
|
self.is_open = self.position == BOX_CENTER + 150
|
|
if not is_open_prev and self.is_open:
|
|
self.yellow_box = YellowBox(self.name, self.is_back, is_dan=True)
|
|
self.yellow_box.create_anim()
|
|
|
|
if self.yellow_box is not None:
|
|
self.yellow_box.update(True)
|
|
|
|
def _draw_exam_box(self):
|
|
tex.draw_texture('yellow_box', 'exam_box_bottom_right')
|
|
tex.draw_texture('yellow_box', 'exam_box_bottom_left')
|
|
tex.draw_texture('yellow_box', 'exam_box_top_right')
|
|
tex.draw_texture('yellow_box', 'exam_box_top_left')
|
|
tex.draw_texture('yellow_box', 'exam_box_bottom')
|
|
tex.draw_texture('yellow_box', 'exam_box_right')
|
|
tex.draw_texture('yellow_box', 'exam_box_left')
|
|
tex.draw_texture('yellow_box', 'exam_box_top')
|
|
tex.draw_texture('yellow_box', 'exam_box_center')
|
|
tex.draw_texture('yellow_box', 'exam_header')
|
|
|
|
for i, exam in enumerate(self.exams):
|
|
tex.draw_texture('yellow_box', 'judge_box', y=(i*83))
|
|
tex.draw_texture('yellow_box', 'exam_' + self.exams[i].type, y=(i*83))
|
|
counter = str(self.exams[i].red)
|
|
margin = 20
|
|
if self.exams[i].type == 'gauge':
|
|
tex.draw_texture('yellow_box', 'exam_percent', y=(i*83))
|
|
offset = -8
|
|
else:
|
|
offset = 0
|
|
for j in range(len(counter)):
|
|
tex.draw_texture('yellow_box', 'judge_num', frame=int(counter[j]), x=offset-(len(counter) - j) * margin, y=(i*83))
|
|
|
|
if self.exams[i].range == 'more':
|
|
tex.draw_texture('yellow_box', 'exam_more', x=(offset*-1.7), y=(i*83))
|
|
elif self.exams[i].range == 'less':
|
|
tex.draw_texture('yellow_box', 'exam_less', x=(offset*-1.7), y=(i*83))
|
|
|
|
def _draw_closed(self, x: int, y: int):
|
|
tex.draw_texture('box', 'folder', frame=self.color, x=x)
|
|
if self.name is not None:
|
|
self.name.draw(outline_color=ray.BLACK, x=x + 47 - int(self.name.texture.width / 2), y=y+35, y2=min(self.name.texture.height, 417)-self.name.texture.height)
|
|
|
|
def _draw_open(self, x: int, y: int, is_ura: bool):
|
|
if self.yellow_box is not None:
|
|
self.yellow_box.draw(None, None, False)
|
|
for i, song in enumerate(self.song_text):
|
|
title, subtitle = song
|
|
x = i * 140
|
|
tex.draw_texture('yellow_box', 'genre_banner', x=x, frame=self.songs[i][1])
|
|
tex.draw_texture('yellow_box', 'difficulty', x=x, frame=self.songs[i][2])
|
|
tex.draw_texture('yellow_box', 'difficulty_x', x=x)
|
|
tex.draw_texture('yellow_box', 'difficulty_star', x=x)
|
|
level = self.songs[i][0].metadata.course_data[self.songs[i][2]].level
|
|
counter = str(level)
|
|
total_width = len(counter) * 10
|
|
for i in range(len(counter)):
|
|
tex.draw_texture('yellow_box', 'difficulty_num', frame=int(counter[i]), x=x-(total_width // 2) + (i * 10))
|
|
|
|
title.draw(outline_color=ray.BLACK, x=665+x, y=127, y2=min(title.texture.height, 400)-title.texture.height)
|
|
subtitle.draw(outline_color=ray.BLACK, x=620+x, y=525-min(subtitle.texture.height, 400), y2=min(subtitle.texture.height, 400)-subtitle.texture.height)
|
|
|
|
tex.draw_texture('yellow_box', 'total_notes_bg')
|
|
tex.draw_texture('yellow_box', 'total_notes')
|
|
counter = str(self.total_notes)
|
|
for i in range(len(counter)):
|
|
tex.draw_texture('yellow_box', 'total_notes_counter', frame=int(counter[i]), x=(i * 25))
|
|
|
|
tex.draw_texture('yellow_box', 'frame', frame=self.color)
|
|
if self.hori_name is not None:
|
|
self.hori_name.draw(outline_color=ray.BLACK, x=434 - (self.hori_name.texture.width//2), y=84, x2=min(self.hori_name.texture.width, 275)-self.hori_name.texture.width)
|
|
|
|
self._draw_exam_box()
|
|
|
|
def draw(self, x: int, y: int, is_ura: bool):
|
|
if self.is_open:
|
|
self._draw_open(x, y, is_ura)
|
|
else:
|
|
self._draw_closed(x, y)
|
|
|
|
class GenreBG:
|
|
"""The background for a genre box."""
|
|
def __init__(self, start_box: SongBox, end_box: SongBox, title: OutlinedText, diff_sort: Optional[int]):
|
|
self.start_box = start_box
|
|
self.end_box = end_box
|
|
self.start_position = start_box.position
|
|
self.end_position = end_box.position
|
|
self.title = title
|
|
self.fade_in = Animation.create_fade(116, initial_opacity=0.0, final_opacity=1.0, ease_in='quadratic', delay=50)
|
|
self.fade_in.start()
|
|
self.diff_num = diff_sort
|
|
def update(self, current_ms):
|
|
self.start_position = self.start_box.position
|
|
self.end_position = self.end_box.position
|
|
self.fade_in.update(current_ms)
|
|
def draw(self, y):
|
|
offset = -150 if self.start_box.is_open else 0
|
|
|
|
tex.draw_texture('box', 'folder_background_edge', frame=self.end_box.texture_index, x=self.start_position+offset, y=y, mirror="horizontal", fade=self.fade_in.attribute)
|
|
|
|
|
|
extra_distance = 155 if self.end_box.is_open or (self.start_box.is_open and 844 <= self.end_position <= 1144) else 0
|
|
if self.start_position >= -56 and self.end_position < self.start_position:
|
|
x2 = self.start_position + 1400
|
|
x = self.start_position+offset
|
|
elif (self.start_position <= -56) and (self.end_position < self.start_position):
|
|
x = 0
|
|
x2 = 1280
|
|
else:
|
|
x2 = abs(self.end_position) - self.start_position + extra_distance + 57
|
|
x = self.start_position+offset
|
|
tex.draw_texture('box', 'folder_background', x=x, y=y, x2=x2, frame=self.end_box.texture_index)
|
|
|
|
|
|
if self.end_position < self.start_position and self.end_position >= -56:
|
|
x2 = min(self.end_position+75, 1280) + extra_distance
|
|
tex.draw_texture('box', 'folder_background', x=-18, y=y, x2=x2, frame=self.end_box.texture_index)
|
|
|
|
|
|
offset = 150 if self.end_box.is_open else 0
|
|
tex.draw_texture('box', 'folder_background_edge', x=self.end_position+80+offset, y=y, fade=self.fade_in.attribute, frame=self.end_box.texture_index)
|
|
|
|
if ((self.start_position <= 594 and self.end_position >= 594) or
|
|
((self.start_position <= 594 or self.end_position >= 594) and (self.start_position > self.end_position))):
|
|
offset = 100 if self.diff_num is not None else 0
|
|
dest_width = min(300, self.title.texture.width)
|
|
tex.draw_texture('box', 'folder_background_folder', x=-((offset+dest_width)//2), y=y-2, x2=dest_width+offset - 10, fade=self.fade_in.attribute, frame=self.end_box.texture_index)
|
|
tex.draw_texture('box', 'folder_background_folder_edge', x=-((offset+dest_width)//2), y=y-2, fade=self.fade_in.attribute, frame=self.end_box.texture_index, mirror="horizontal")
|
|
tex.draw_texture('box', 'folder_background_folder_edge', x=((offset+dest_width)//2)+20, y=y-2, fade=self.fade_in.attribute, frame=self.end_box.texture_index)
|
|
if self.diff_num is not None:
|
|
tex.draw_texture('diff_sort', 'star_num', frame=self.diff_num, x=-150 + (dest_width//2), y=-143)
|
|
self.title.draw(outline_color=ray.BLACK, x=(1280//2) - (dest_width//2)-(offset//2), y=y-60, x2=dest_width - self.title.texture.width, color=ray.fade(ray.WHITE, self.fade_in.attribute))
|
|
|
|
class ScoreHistory:
|
|
"""The score information that appears while hovering over a song"""
|
|
def __init__(self, scores: dict[int, tuple[int, int, int, int]], current_ms):
|
|
"""
|
|
Initialize the score history with the given scores and current time.
|
|
|
|
Args:
|
|
scores (dict[int, tuple[int, int, int, int]]): A dictionary of scores for each difficulty level.
|
|
current_ms (int): The current time in milliseconds.
|
|
"""
|
|
self.scores = {k: v for k, v in scores.items() if v is not None}
|
|
self.difficulty_keys = list(self.scores.keys())
|
|
self.curr_difficulty_index = 0
|
|
self.curr_difficulty_index = (self.curr_difficulty_index + 1) % len(self.difficulty_keys)
|
|
self.curr_difficulty = self.difficulty_keys[self.curr_difficulty_index]
|
|
self.curr_score = self.scores[self.curr_difficulty][0]
|
|
self.curr_score_su = self.scores[self.curr_difficulty][0]
|
|
self.last_ms = current_ms
|
|
self.long = True
|
|
|
|
def update(self, current_ms):
|
|
if current_ms >= self.last_ms + 1000:
|
|
self.last_ms = current_ms
|
|
self.curr_difficulty_index = (self.curr_difficulty_index + 1) % len(self.difficulty_keys)
|
|
self.curr_difficulty = self.difficulty_keys[self.curr_difficulty_index]
|
|
self.curr_score = self.scores[self.curr_difficulty][0]
|
|
self.curr_score_su = self.scores[self.curr_difficulty][0]
|
|
|
|
def draw_long(self):
|
|
tex.draw_texture('leaderboard','background_2')
|
|
tex.draw_texture('leaderboard','title', index=self.long)
|
|
if self.curr_difficulty == Difficulty.URA:
|
|
tex.draw_texture('leaderboard', 'shinuchi_ura', index=self.long)
|
|
else:
|
|
tex.draw_texture('leaderboard', 'shinuchi', index=self.long)
|
|
|
|
tex.draw_texture('leaderboard', 'pts', color=ray.WHITE, index=self.long)
|
|
tex.draw_texture('leaderboard', 'difficulty', frame=self.curr_difficulty, index=self.long)
|
|
|
|
for i in range(4):
|
|
tex.draw_texture('leaderboard', 'normal', index=self.long, y=50+(i*50))
|
|
|
|
tex.draw_texture('leaderboard', 'judge_good')
|
|
tex.draw_texture('leaderboard', 'judge_ok')
|
|
tex.draw_texture('leaderboard', 'judge_bad')
|
|
tex.draw_texture('leaderboard', 'judge_drumroll')
|
|
|
|
for j, counter in enumerate(self.scores[self.curr_difficulty]):
|
|
if j == Difficulty.TOWER:
|
|
continue
|
|
if counter is None:
|
|
continue
|
|
counter = str(counter)
|
|
margin = 24
|
|
for i in range(len(counter)):
|
|
if j == 0:
|
|
tex.draw_texture('leaderboard', 'counter', frame=int(counter[i]), x=-((len(counter) * 14) // 2) + (i * 14), color=ray.WHITE, index=self.long)
|
|
else:
|
|
tex.draw_texture('leaderboard', 'judge_num', frame=int(counter[i]), x=-(len(counter) - i) * margin, y=j*50)
|
|
|
|
def draw(self):
|
|
if self.long:
|
|
self.draw_long()
|
|
return
|
|
tex.draw_texture('leaderboard','background')
|
|
tex.draw_texture('leaderboard','title')
|
|
|
|
if self.curr_difficulty == Difficulty.URA:
|
|
tex.draw_texture('leaderboard', 'normal_ura')
|
|
tex.draw_texture('leaderboard', 'shinuchi_ura')
|
|
else:
|
|
tex.draw_texture('leaderboard', 'normal')
|
|
tex.draw_texture('leaderboard', 'shinuchi')
|
|
|
|
color = ray.BLACK
|
|
if self.curr_difficulty == Difficulty.URA:
|
|
color = ray.WHITE
|
|
tex.draw_texture('leaderboard','ura')
|
|
|
|
tex.draw_texture('leaderboard', 'pts', color=color)
|
|
tex.draw_texture('leaderboard', 'pts', y=50)
|
|
|
|
tex.draw_texture('leaderboard', 'difficulty', frame=self.curr_difficulty)
|
|
|
|
counter = str(self.curr_score)
|
|
total_width = len(counter) * 14
|
|
for i in range(len(counter)):
|
|
tex.draw_texture('leaderboard', 'counter', frame=int(counter[i]), x=-(total_width // 2) + (i * 14), color=color)
|
|
|
|
counter = str(self.curr_score_su)
|
|
total_width = len(counter) * 14
|
|
for i in range(len(counter)):
|
|
tex.draw_texture('leaderboard', 'counter', frame=int(counter[i]), x=-(total_width // 2) + (i * 14), y=50, color=ray.WHITE)
|
|
|
|
def parse_box_def(path: Path):
|
|
"""Parse box.def file for directory metadata"""
|
|
texture_index = SongBox.DEFAULT_INDEX
|
|
name = path.name
|
|
genre = ''
|
|
collection = None
|
|
encoding = test_encodings(path / "box.def")
|
|
|
|
try:
|
|
with open(path / "box.def", 'r', encoding=encoding) as box_def:
|
|
for line in box_def:
|
|
line = line.strip()
|
|
if line.startswith("#GENRE:"):
|
|
genre = line.split(":", 1)[1].strip()
|
|
texture_index = FileSystemItem.GENRE_MAP.get(genre, SongBox.DEFAULT_INDEX)
|
|
if texture_index == SongBox.DEFAULT_INDEX:
|
|
texture_index = FileSystemItem.GENRE_MAP_2.get(genre, SongBox.DEFAULT_INDEX)
|
|
elif line.startswith("#TITLE:"):
|
|
name = line.split(":", 1)[1].strip()
|
|
elif line.startswith("#TITLEJA:"):
|
|
if global_data.config['general']['language'] == 'ja':
|
|
name = line.split(":", 1)[1].strip()
|
|
elif line.startswith("#COLLECTION"):
|
|
collection = line.split(":", 1)[1].strip()
|
|
if name == '':
|
|
if genre:
|
|
name = genre
|
|
else:
|
|
name = path.name
|
|
except Exception as e:
|
|
logger.error(f"Error parsing box.def in {path}: {e}")
|
|
|
|
return name, texture_index, collection
|
|
|
|
class FileSystemItem:
|
|
GENRE_MAP = {
|
|
'J-POP': 1,
|
|
'アニメ': 2,
|
|
'VOCALOID': 3,
|
|
'どうよう': 4,
|
|
'バラエティー': 5,
|
|
'クラシック': 6,
|
|
'ゲームミュージック': 7,
|
|
'ナムコオリジナル': 8,
|
|
'RECOMMENDED': 10,
|
|
'FAVORITE': 11,
|
|
'RECENT': 12,
|
|
'段位道場': 13,
|
|
'DIFFICULTY': 14
|
|
}
|
|
GENRE_MAP_2 = {
|
|
'ボーカロイド': 3,
|
|
'バラエティ': 5
|
|
}
|
|
"""Base class for files and directories in the navigation system"""
|
|
def __init__(self, path: Path, name: str):
|
|
self.path = path
|
|
self.name = name
|
|
|
|
class Directory(FileSystemItem):
|
|
"""Represents a directory in the navigation system"""
|
|
COLLECTIONS = [
|
|
'NEW',
|
|
'RECENT',
|
|
'FAVORITE',
|
|
'DIFFICULTY',
|
|
'RECOMMENDED'
|
|
]
|
|
def __init__(self, path: Path, name: str, texture_index: int, has_box_def=False, to_root=False, back=False, tja_count=0, box_texture=None, collection=None):
|
|
super().__init__(path, name)
|
|
self.has_box_def = has_box_def
|
|
self.to_root = to_root
|
|
self.back = back
|
|
self.tja_count = tja_count
|
|
self.collection = None
|
|
if collection in Directory.COLLECTIONS:
|
|
self.collection = collection
|
|
if collection in FileSystemItem.GENRE_MAP:
|
|
texture_index = FileSystemItem.GENRE_MAP[collection]
|
|
elif self.to_root or self.back:
|
|
texture_index = SongBox.BACK_INDEX
|
|
|
|
self.box = SongBox(name, texture_index, True, tja_count=tja_count, box_texture=box_texture)
|
|
|
|
class SongFile(FileSystemItem):
|
|
"""Represents a song file (TJA) in the navigation system"""
|
|
def __init__(self, path: Path, name: str, texture_index: int, tja=None, name_texture_index: Optional[int]=None):
|
|
super().__init__(path, name)
|
|
self.is_recent = (datetime.now() - datetime.fromtimestamp(path.stat().st_mtime)) <= timedelta(days=7)
|
|
self.tja = tja or TJAParser(path)
|
|
if self.is_recent:
|
|
self.tja.ex_data.new = True
|
|
title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en'])
|
|
self.hash = global_data.song_paths[path]
|
|
self.box = SongBox(title, texture_index, False, tja=self.tja, name_texture_index=name_texture_index if name_texture_index is not None else texture_index)
|
|
self.box.hash = global_data.song_hashes[self.hash][0]["diff_hashes"]
|
|
self.box.get_scores()
|
|
|
|
@dataclass
|
|
class Exam:
|
|
type: str
|
|
red: int
|
|
gold: int
|
|
range: str
|
|
|
|
class DanCourse(FileSystemItem):
|
|
def __init__(self, path: Path, name: str):
|
|
super().__init__(path, name)
|
|
if name != "dan.json":
|
|
logger.error(f"Invalid dan course file: {path}")
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
self.title = data["title"]
|
|
self.color = data["color"]
|
|
self.charts = []
|
|
for chart in data["charts"]:
|
|
hash = chart["hash"]
|
|
#chart_title = chart["title"]
|
|
#chart_subtitle = chart["subtitle"]
|
|
difficulty = chart["difficulty"]
|
|
if hash in global_data.song_hashes:
|
|
path = Path(global_data.song_hashes[hash][0]["file_path"])
|
|
if (path.parent.parent / "box.def").exists():
|
|
_, genre_index, _ = parse_box_def(path.parent.parent)
|
|
else:
|
|
genre_index = 9
|
|
tja = TJAParser(path)
|
|
self.charts.append((tja, genre_index, difficulty, tja.metadata.course_data[difficulty].level))
|
|
else:
|
|
pass
|
|
#do something with song_title, song_subtitle
|
|
self.exams = []
|
|
for exam in data["exams"]:
|
|
self.exams.append(Exam(exam["type"], exam["value"][0], exam["value"][1], exam["range"]))
|
|
|
|
self.box = DanBox(self.title, self.color, self.charts, self.exams)
|
|
|
|
class FileNavigator:
|
|
"""Manages navigation through pre-generated Directory and SongFile objects"""
|
|
def __init__(self):
|
|
|
|
# Pre-generated objects storage
|
|
self.all_directories: dict[str, Directory] = {} # path -> Directory
|
|
self.all_song_files: dict[str, Union[SongFile, DanCourse]] = {} # path -> SongFile
|
|
self.directory_contents: dict[str, list[Union[Directory, SongFile]]] = {} # path -> list of items
|
|
|
|
# OPTION 2: Lazy crown calculation with caching
|
|
self.directory_crowns: dict[str, dict] = dict() # path -> crown list
|
|
self.crown_cache_dirty: set[str] = set() # directories that need crown recalculation
|
|
|
|
# Navigation state - simplified without root-specific state
|
|
self.current_dir = Path() # Empty path represents virtual root
|
|
self.items: list[Directory | SongFile] = []
|
|
self.new_items: list[Directory | SongFile] = []
|
|
self.favorite_folder: Optional[Directory] = None
|
|
self.recent_folder: Optional[Directory] = None
|
|
self.selected_index = 0
|
|
self.diff_sort_diff = Difficulty.URA
|
|
self.diff_sort_level = 10
|
|
self.diff_sort_statistics = dict()
|
|
self.history = []
|
|
self.box_open = False
|
|
self.genre_bg = None
|
|
self.song_count = 0
|
|
self.in_dan_select = False
|
|
logger.info("FileNavigator initialized")
|
|
|
|
def initialize(self, root_dirs: list[Path]):
|
|
self.root_dirs = [Path(p) if not isinstance(p, Path) else p for p in root_dirs]
|
|
self._generate_all_objects()
|
|
self._create_virtual_root()
|
|
self.load_current_directory()
|
|
logger.info(f"FileNavigator initialized with root_dirs: {self.root_dirs}")
|
|
|
|
def _create_virtual_root(self):
|
|
"""Create a virtual root directory containing all root directories"""
|
|
virtual_root_items = []
|
|
|
|
for root_path in self.root_dirs:
|
|
if not root_path.exists():
|
|
continue
|
|
|
|
root_key = str(root_path)
|
|
if root_key in self.all_directories:
|
|
# Root has box.def, add the directory itself
|
|
virtual_root_items.append(self.all_directories[root_key])
|
|
else:
|
|
# Root doesn't have box.def, add its immediate children with box.def
|
|
for child_path in sorted(root_path.iterdir()):
|
|
if child_path.is_dir():
|
|
child_key = str(child_path)
|
|
if child_key in self.all_directories:
|
|
virtual_root_items.append(self.all_directories[child_key])
|
|
|
|
# Also add direct TJA files from root
|
|
all_tja_files = self._find_tja_files_recursive(root_path)
|
|
for tja_path in sorted(all_tja_files):
|
|
song_key = str(tja_path)
|
|
if song_key in self.all_song_files:
|
|
virtual_root_items.append(self.all_song_files[song_key])
|
|
|
|
# Store virtual root contents (empty path key represents root)
|
|
self.directory_contents["."] = virtual_root_items
|
|
|
|
def _generate_all_objects(self):
|
|
"""Generate all Directory and SongFile objects in advance"""
|
|
logging.info("Generating all Directory and SongFile objects...")
|
|
|
|
# Generate objects for each root directory
|
|
for root_path in self.root_dirs:
|
|
if not root_path.exists():
|
|
logging.warning(f"Root directory does not exist: {root_path}")
|
|
continue
|
|
|
|
self._generate_objects_recursive(root_path)
|
|
|
|
if self.favorite_folder is not None:
|
|
song_list = self._read_song_list(self.favorite_folder.path)
|
|
for song_obj in song_list:
|
|
if str(song_obj) in self.all_song_files:
|
|
box = self.all_song_files[str(song_obj)].box
|
|
if isinstance(box, DanBox):
|
|
logger.warning(f"Cannot favorite DanCourse: {song_obj}")
|
|
else:
|
|
box.is_favorite = True
|
|
|
|
logging.info(f"Object generation complete. "
|
|
f"Directories: {len(self.all_directories)}, "
|
|
f"Songs: {len(self.all_song_files)}")
|
|
|
|
def _generate_objects_recursive(self, dir_path: Path):
|
|
"""Recursively generate Directory and SongFile objects for a directory"""
|
|
if not dir_path.is_dir():
|
|
return
|
|
|
|
dir_key = str(dir_path)
|
|
|
|
# Check for box.def
|
|
has_box_def = (dir_path / "box.def").exists()
|
|
|
|
# Only create Directory objects for directories with box.def
|
|
if has_box_def:
|
|
# Parse box.def if it exists
|
|
name = dir_path.name if dir_path.name else str(dir_path)
|
|
texture_index = SongBox.DEFAULT_INDEX
|
|
box_texture = None
|
|
collection = None
|
|
|
|
name, texture_index, collection = parse_box_def(dir_path)
|
|
box_png_path = dir_path / "box.png"
|
|
if box_png_path.exists():
|
|
box_texture = str(box_png_path)
|
|
|
|
# Count TJA files for this directory
|
|
tja_count = self._count_tja_files(dir_path)
|
|
if collection == Directory.COLLECTIONS[4]:
|
|
tja_count = 10
|
|
elif collection == Directory.COLLECTIONS[0]:
|
|
tja_count = len(self.new_items)
|
|
|
|
# Create Directory object
|
|
directory_obj = Directory(
|
|
dir_path, name, texture_index,
|
|
has_box_def=has_box_def,
|
|
tja_count=tja_count,
|
|
box_texture=box_texture,
|
|
collection=collection
|
|
)
|
|
if directory_obj.collection == Directory.COLLECTIONS[2]:
|
|
self.favorite_folder = directory_obj
|
|
elif directory_obj.collection == Directory.COLLECTIONS[1]:
|
|
self.recent_folder = directory_obj
|
|
self.all_directories[dir_key] = directory_obj
|
|
|
|
# Generate content list for this directory
|
|
content_items = []
|
|
|
|
# Add child directories that have box.def
|
|
child_dirs = []
|
|
for item_path in dir_path.iterdir():
|
|
if item_path.is_dir():
|
|
child_has_box_def = (item_path / "box.def").exists()
|
|
if child_has_box_def:
|
|
child_dirs.append(item_path)
|
|
# Recursively generate objects for child directory
|
|
self._generate_objects_recursive(item_path)
|
|
|
|
# Sort and add child directories
|
|
for child_path in sorted(child_dirs):
|
|
child_key = str(child_path)
|
|
if child_key in self.all_directories:
|
|
content_items.append(self.all_directories[child_key])
|
|
|
|
# Get TJA files for this directory
|
|
tja_files = self._get_tja_files_for_directory(dir_path)
|
|
|
|
# Create SongFile objects
|
|
for tja_path in sorted(tja_files):
|
|
song_key = str(tja_path)
|
|
if song_key not in self.all_song_files and tja_path.name == "dan.json":
|
|
valid_dan = True
|
|
with open(tja_path, 'r', encoding='utf-8') as file:
|
|
dan_data = json.load(file)
|
|
for chart in dan_data["charts"]:
|
|
hash = chart["hash"]
|
|
if hash not in global_data.song_hashes:
|
|
valid_dan = False
|
|
if valid_dan:
|
|
song_obj = DanCourse(tja_path, tja_path.name)
|
|
self.all_song_files[song_key] = song_obj
|
|
elif song_key not in self.all_song_files and tja_path in global_data.song_paths:
|
|
song_obj = SongFile(tja_path, tja_path.name, texture_index)
|
|
song_obj.box.get_scores()
|
|
for course in song_obj.tja.metadata.course_data:
|
|
level = song_obj.tja.metadata.course_data[course].level
|
|
|
|
scores = song_obj.box.scores.get(course)
|
|
if scores is not None:
|
|
is_cleared = scores[4] >= Crown.CLEAR if scores[4] is not None else False
|
|
is_full_combo = scores[4] == Crown.FC if scores[4] is not None else False
|
|
else:
|
|
is_cleared = False
|
|
is_full_combo = False
|
|
|
|
if course not in self.diff_sort_statistics:
|
|
self.diff_sort_statistics[course] = {}
|
|
|
|
if level not in self.diff_sort_statistics[course]:
|
|
self.diff_sort_statistics[course][level] = [1, int(is_full_combo), int(is_cleared)]
|
|
else:
|
|
self.diff_sort_statistics[course][level][0] += 1
|
|
if is_full_combo:
|
|
self.diff_sort_statistics[course][level][1] += 1
|
|
elif is_cleared:
|
|
self.diff_sort_statistics[course][level][2] += 1
|
|
if song_obj.is_recent:
|
|
self.new_items.append(SongFile(tja_path, tja_path.name, SongBox.DEFAULT_INDEX, name_texture_index=texture_index))
|
|
self.song_count += 1
|
|
global_data.song_progress = self.song_count / global_data.total_songs
|
|
self.all_song_files[song_key] = song_obj
|
|
|
|
if song_key in self.all_song_files:
|
|
content_items.append(self.all_song_files[song_key])
|
|
|
|
self.directory_contents[dir_key] = content_items
|
|
self.crown_cache_dirty.add(dir_key)
|
|
|
|
else:
|
|
# For directories without box.def, still process their children
|
|
for item_path in dir_path.iterdir():
|
|
if item_path.is_dir():
|
|
self._generate_objects_recursive(item_path)
|
|
|
|
# Create SongFile objects for TJA files in non-boxed directories
|
|
tja_files = self._find_tja_files_in_directory_only(dir_path)
|
|
for tja_path in tja_files:
|
|
song_key = str(tja_path)
|
|
if song_key not in self.all_song_files:
|
|
try:
|
|
song_obj = SongFile(tja_path, tja_path.name, SongBox.DEFAULT_INDEX)
|
|
self.song_count += 1
|
|
global_data.song_progress = self.song_count / global_data.total_songs
|
|
self.all_song_files[song_key] = song_obj
|
|
except Exception as e:
|
|
logger.error(f"Error creating SongFile for {tja_path}: {e}")
|
|
continue
|
|
|
|
def is_at_root(self) -> bool:
|
|
"""Check if currently at the virtual root"""
|
|
return self.current_dir == Path()
|
|
|
|
def load_current_directory(self, selected_item: Optional[Directory] = None):
|
|
"""Load pre-generated items for the current directory (unified for root and subdirs)"""
|
|
dir_key = str(self.current_dir)
|
|
|
|
# Determine if current directory has child directories with box.def
|
|
has_children = False
|
|
if self.is_at_root() or selected_item and selected_item.box.texture_index == 13:
|
|
has_children = True # Root always has "children" (the root directories)
|
|
else:
|
|
has_children = any(item.is_dir() and (item / "box.def").exists()
|
|
for item in self.current_dir.iterdir())
|
|
|
|
self.genre_bg = None
|
|
self.in_favorites = False
|
|
|
|
if has_children:
|
|
self.items = []
|
|
if not self.box_open:
|
|
self.selected_index = 0
|
|
|
|
start_box = None
|
|
end_box = None
|
|
|
|
# Add back navigation item (only if not at root)
|
|
if not self.is_at_root():
|
|
back_dir = Directory(self.current_dir.parent, "", SongBox.BACK_INDEX, back=True)
|
|
if not has_children:
|
|
start_box = back_dir.box
|
|
self.items.insert(self.selected_index, back_dir)
|
|
|
|
# Add pre-generated content for this directory
|
|
if dir_key in self.directory_contents:
|
|
content_items = self.directory_contents[dir_key]
|
|
|
|
# Handle special collections (same logic as before)
|
|
if isinstance(selected_item, Directory):
|
|
if selected_item.collection == Directory.COLLECTIONS[0]:
|
|
content_items = self.new_items
|
|
elif selected_item.collection == Directory.COLLECTIONS[1]:
|
|
if self.recent_folder is None:
|
|
raise Exception("tried to enter recent folder without recents")
|
|
self._generate_objects_recursive(self.recent_folder.path)
|
|
selected_item.box.tja_count_text = None
|
|
selected_item.box.tja_count = self._count_tja_files(self.recent_folder.path)
|
|
content_items = self.directory_contents[dir_key]
|
|
elif selected_item.collection == Directory.COLLECTIONS[2]:
|
|
if self.favorite_folder is None:
|
|
raise Exception("tried to enter favorite folder without favorites")
|
|
self._generate_objects_recursive(self.favorite_folder.path)
|
|
tja_files = self._get_tja_files_for_directory(self.favorite_folder.path)
|
|
self._calculate_directory_crowns(dir_key, tja_files)
|
|
selected_item.box.tja_count_text = None
|
|
selected_item.box.tja_count = self._count_tja_files(self.favorite_folder.path)
|
|
content_items = self.directory_contents[dir_key]
|
|
self.in_favorites = True
|
|
elif selected_item.collection == Directory.COLLECTIONS[3]:
|
|
content_items = []
|
|
parent_dir = selected_item.path.parent
|
|
for sibling_path in parent_dir.iterdir():
|
|
if sibling_path.is_dir() and sibling_path != selected_item.path:
|
|
sibling_key = str(sibling_path)
|
|
if sibling_key in self.directory_contents:
|
|
for item in self.directory_contents[sibling_key]:
|
|
if isinstance(item, SongFile) and item:
|
|
if self.diff_sort_diff in item.tja.metadata.course_data and item.tja.metadata.course_data[self.diff_sort_diff].level == self.diff_sort_level:
|
|
if item not in content_items:
|
|
content_items.append(item)
|
|
elif selected_item.collection == Directory.COLLECTIONS[4]:
|
|
parent_dir = selected_item.path.parent
|
|
temp_items = []
|
|
for sibling_path in parent_dir.iterdir():
|
|
if sibling_path.is_dir() and sibling_path != selected_item.path:
|
|
sibling_key = str(sibling_path)
|
|
if sibling_key in self.directory_contents:
|
|
for item in self.directory_contents[sibling_key]:
|
|
if not isinstance(item, Directory):
|
|
temp_items.append(item)
|
|
content_items = random.sample(temp_items, 10)
|
|
|
|
if content_items == []:
|
|
self.go_back()
|
|
return
|
|
i = 1
|
|
for item in content_items:
|
|
if isinstance(item, SongFile):
|
|
if i % 10 == 0 and i != 0:
|
|
back_dir = Directory(self.current_dir.parent, "", SongBox.BACK_INDEX, back=True)
|
|
self.items.insert(self.selected_index+i, back_dir)
|
|
i += 1
|
|
if not has_children:
|
|
if selected_item is not None:
|
|
item.box.texture_index = selected_item.box.texture_index
|
|
self.items.insert(self.selected_index+i, item)
|
|
else:
|
|
self.items.append(item)
|
|
i += 1
|
|
|
|
if not has_children:
|
|
self.box_open = True
|
|
end_box = content_items[-1].box
|
|
if selected_item in self.items:
|
|
self.items.remove(selected_item)
|
|
|
|
# Calculate crowns for directories
|
|
for item in self.items:
|
|
if isinstance(item, Directory):
|
|
item_key = str(item.path)
|
|
if item_key in self.directory_contents: # Only for real directories
|
|
item.box.crown = self._get_directory_crowns_cached(item_key)
|
|
else:
|
|
# Navigation items (back/to_root)
|
|
item.box.crown = dict()
|
|
|
|
self.calculate_box_positions()
|
|
|
|
if (not has_children and start_box is not None
|
|
and end_box is not None and selected_item is not None
|
|
and selected_item.box.hori_name is not None):
|
|
hori_name = selected_item.box.hori_name
|
|
diff_sort = None
|
|
if selected_item.collection == Directory.COLLECTIONS[3]:
|
|
diff_sort = self.diff_sort_level
|
|
diffs = ['かんたん', 'ふつう', 'むずかしい', 'おに']
|
|
hori_name = OutlinedText(diffs[min(Difficulty.ONI, self.diff_sort_diff)], 40, ray.WHITE, outline_thickness=5)
|
|
self.genre_bg = GenreBG(start_box, end_box, hori_name, diff_sort)
|
|
|
|
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 self.box_open:
|
|
self.go_back()
|
|
return
|
|
|
|
if selected_item.back:
|
|
# Handle back navigation
|
|
if self.current_dir.parent == Path():
|
|
# Going back to root
|
|
self.current_dir = Path()
|
|
else:
|
|
self.current_dir = self.current_dir.parent
|
|
else:
|
|
# Save current state to history
|
|
self.history.append((self.current_dir, self.selected_index))
|
|
self.current_dir = selected_item.path
|
|
logger.info(f"Entered Directory {selected_item.path}")
|
|
|
|
self.load_current_directory(selected_item=selected_item)
|
|
|
|
return selected_item
|
|
|
|
def go_back(self):
|
|
"""Navigate back to the previous directory"""
|
|
if self.history:
|
|
previous_dir, previous_index = self.history.pop()
|
|
self.current_dir = previous_dir
|
|
self.selected_index = previous_index
|
|
self.load_current_directory()
|
|
self.box_open = False
|
|
|
|
def _count_tja_files(self, folder_path: Path):
|
|
"""Count TJA files in directory"""
|
|
tja_count = 0
|
|
|
|
# Find all song_list.txt files recursively
|
|
song_list_files = list(folder_path.rglob("song_list.txt"))
|
|
|
|
if song_list_files:
|
|
# Process all song_list.txt files found
|
|
for song_list_path in song_list_files:
|
|
with open(song_list_path, 'r', encoding='utf-8-sig') as song_list_file:
|
|
tja_count += len([line for line in song_list_file.readlines() if line.strip()])
|
|
# Fallback: Use recursive counting of .tja files
|
|
tja_count += sum(1 for _ in folder_path.rglob("*.tja"))
|
|
|
|
return tja_count
|
|
|
|
def _get_directory_crowns_cached(self, dir_key: str) -> dict:
|
|
"""Get crowns for a directory, calculating only if needed"""
|
|
if dir_key in self.crown_cache_dirty or dir_key not in self.directory_crowns:
|
|
# Calculate crowns on-demand
|
|
dir_path = Path(dir_key)
|
|
tja_files = self._get_tja_files_for_directory(dir_path)
|
|
self._calculate_directory_crowns(dir_key, tja_files)
|
|
self.crown_cache_dirty.discard(dir_key)
|
|
|
|
return self.directory_crowns.get(dir_key, dict())
|
|
|
|
def _calculate_directory_crowns(self, dir_key: str, tja_files: list):
|
|
"""Pre-calculate crowns for a directory"""
|
|
all_scores = dict()
|
|
crowns = dict()
|
|
|
|
for tja_path in tja_files:
|
|
song_key = str(tja_path)
|
|
if song_key in self.all_song_files:
|
|
song_obj = self.all_song_files[song_key]
|
|
if not isinstance(song_obj, SongFile):
|
|
continue
|
|
for diff in song_obj.box.scores:
|
|
if diff not in all_scores:
|
|
all_scores[diff] = []
|
|
all_scores[diff].append(song_obj.box.scores[diff])
|
|
|
|
for diff in all_scores:
|
|
if all(score is not None and score[5] == Crown.DFC for score in all_scores[diff]):
|
|
crowns[diff] = 'DFC'
|
|
elif all(score is not None and score[5] == Crown.FC for score in all_scores[diff]):
|
|
crowns[diff] = 'FC'
|
|
elif all(score is not None and score[5] >= Crown.CLEAR for score in all_scores[diff]):
|
|
crowns[diff] = 'CLEAR'
|
|
|
|
self.directory_crowns[dir_key] = crowns
|
|
|
|
def _get_tja_files_for_directory(self, directory: Path):
|
|
"""Get TJA files for a specific directory"""
|
|
if (directory / 'song_list.txt').exists():
|
|
return self._read_song_list(directory)
|
|
else:
|
|
return self._find_tja_files_in_directory_only(directory)
|
|
|
|
def _find_tja_files_in_directory_only(self, directory: Path):
|
|
"""Find TJA files only in the specified directory, not recursively in subdirectories with box.def"""
|
|
tja_files: list[Path] = []
|
|
|
|
for path in directory.iterdir():
|
|
if (path.is_file() and path.suffix.lower() == ".tja") or path.name == "dan.json":
|
|
tja_files.append(path)
|
|
elif path.is_dir():
|
|
# Only recurse into subdirectories that don't have box.def
|
|
sub_dir_has_box_def = (path / "box.def").exists()
|
|
if not sub_dir_has_box_def:
|
|
tja_files.extend(self._find_tja_files_in_directory_only(path))
|
|
|
|
return tja_files
|
|
|
|
def _find_tja_files_recursive(self, directory: Path, box_def_dirs_only=True):
|
|
tja_files: list[Path] = []
|
|
|
|
has_box_def = (directory / "box.def").exists()
|
|
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 = (path / "box.def").exists()
|
|
if not sub_dir_has_box_def:
|
|
tja_files.extend(self._find_tja_files_recursive(path, box_def_dirs_only))
|
|
|
|
return tja_files
|
|
|
|
def _read_song_list(self, path: Path):
|
|
"""Read and process song_list.txt file"""
|
|
tja_files: list[Path] = []
|
|
updated_lines = []
|
|
file_updated = False
|
|
with open(path / 'song_list.txt', 'r', encoding='utf-8-sig') as song_list:
|
|
for line in song_list:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
|
|
parts = line.split('|')
|
|
if len(parts) < 3:
|
|
continue
|
|
|
|
hash_val, title, subtitle = parts[0], parts[1], parts[2]
|
|
original_hash = hash_val
|
|
|
|
if hash_val in global_data.song_hashes:
|
|
file_path = Path(global_data.song_hashes[hash_val][0]["file_path"])
|
|
if file_path.exists() and file_path not in tja_files:
|
|
tja_files.append(file_path)
|
|
else:
|
|
# Try to find by title and subtitle
|
|
for key, value in global_data.song_hashes.items():
|
|
for i in range(len(value)):
|
|
song = value[i]
|
|
if (song["title"]["en"] == title and
|
|
song["subtitle"]["en"] == subtitle and
|
|
Path(song["file_path"]).exists()):
|
|
hash_val = key
|
|
tja_files.append(Path(global_data.song_hashes[hash_val][i]["file_path"]))
|
|
break
|
|
|
|
if hash_val != original_hash:
|
|
file_updated = True
|
|
updated_lines.append(f"{hash_val}|{title}|{subtitle}")
|
|
|
|
# Write back updated song list if needed
|
|
if file_updated:
|
|
with open(path / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list:
|
|
for line in updated_lines:
|
|
logger.info(f"updated: {line}")
|
|
song_list.write(line + '\n')
|
|
|
|
return tja_files
|
|
|
|
def calculate_box_positions(self):
|
|
"""Dynamically calculate box positions based on current selection with wrap-around support"""
|
|
if not self.items:
|
|
return
|
|
|
|
for i, item in enumerate(self.items):
|
|
offset = i - self.selected_index
|
|
|
|
if offset > len(self.items) // 2:
|
|
offset -= len(self.items)
|
|
elif offset < -len(self.items) // 2:
|
|
offset += len(self.items)
|
|
|
|
# Adjust spacing based on dan select mode
|
|
base_spacing = 100
|
|
center_offset = 150
|
|
side_offset_l = 0
|
|
side_offset_r = 300
|
|
|
|
if self.in_dan_select:
|
|
base_spacing = 150
|
|
side_offset_l = 200
|
|
side_offset_r = 500
|
|
|
|
position = BOX_CENTER + (base_spacing * offset)
|
|
if position == BOX_CENTER:
|
|
position += center_offset
|
|
elif position > BOX_CENTER:
|
|
position += side_offset_r
|
|
else:
|
|
position -= side_offset_l
|
|
|
|
if item.box.position == -11111:
|
|
item.box.position = position
|
|
item.box.target_position = position
|
|
else:
|
|
item.box.target_position = position
|
|
|
|
def mark_crowns_dirty_for_song(self, song_file: SongFile):
|
|
"""Mark directories as needing crown recalculation when a song's score changes"""
|
|
song_path = song_file.path
|
|
|
|
# Find all directories that contain this song and mark them as dirty
|
|
for dir_key, content_items in self.directory_contents.items():
|
|
for item in content_items:
|
|
if isinstance(item, SongFile) and item.path == song_path:
|
|
self.crown_cache_dirty.add(dir_key)
|
|
break
|
|
|
|
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()
|
|
logger.info(f"Moved Left to {self.items[self.selected_index].path}")
|
|
|
|
def navigate_right(self):
|
|
"""Move selection right with wrap-around"""
|
|
if self.items:
|
|
self.selected_index = (self.selected_index + 1) % len(self.items)
|
|
self.calculate_box_positions()
|
|
logger.info(f"Moved Right to {self.items[self.selected_index].path}")
|
|
|
|
def get_current_item(self):
|
|
"""Get the currently selected item"""
|
|
if self.items and 0 <= self.selected_index < len(self.items):
|
|
return self.items[self.selected_index]
|
|
raise Exception("No current item available")
|
|
|
|
def reset_items(self):
|
|
"""Reset the items in the song select scene"""
|
|
for item in self.items:
|
|
item.box.reset()
|
|
|
|
def add_recent(self):
|
|
"""Add the current song to the recent list"""
|
|
song = self.get_current_item()
|
|
if isinstance(song, Directory):
|
|
return
|
|
if self.recent_folder is None:
|
|
return
|
|
|
|
recents_path = self.recent_folder.path / 'song_list.txt'
|
|
new_entry = f'{song.hash}|{song.tja.metadata.title["en"]}|{song.tja.metadata.subtitle["en"]}\n'
|
|
existing_entries = []
|
|
if recents_path.exists():
|
|
with open(recents_path, 'r', encoding='utf-8-sig') as song_list:
|
|
existing_entries = song_list.readlines()
|
|
existing_entries = [entry for entry in existing_entries if not entry.startswith(f'{song.hash}|')]
|
|
all_entries = [new_entry] + existing_entries
|
|
recent_entries = all_entries[:25]
|
|
with open(recents_path, 'w', encoding='utf-8-sig') as song_list:
|
|
song_list.writelines(recent_entries)
|
|
|
|
logger.info(f"Added Recent: {song.hash} {song.tja.metadata.title['en']} {song.tja.metadata.subtitle['en']}")
|
|
|
|
def add_favorite(self) -> bool:
|
|
"""Add the current song to the favorites list"""
|
|
song = self.get_current_item()
|
|
if isinstance(song, Directory):
|
|
return False
|
|
if self.favorite_folder is None:
|
|
return False
|
|
favorites_path = self.favorite_folder.path / 'song_list.txt'
|
|
lines = []
|
|
if not Path(favorites_path).exists():
|
|
Path(favorites_path).touch()
|
|
with open(favorites_path, 'r', encoding='utf-8-sig') as song_list:
|
|
for line in song_list:
|
|
line = line.strip()
|
|
if not line: # Skip empty lines
|
|
continue
|
|
hash, title, subtitle = line.split('|')
|
|
if song.hash == hash or (song.tja.metadata.title['en'] == title and song.tja.metadata.subtitle['en'] == subtitle):
|
|
if not self.in_favorites:
|
|
return False
|
|
else:
|
|
lines.append(line)
|
|
if self.in_favorites:
|
|
with open(favorites_path, 'w', encoding='utf-8-sig') as song_list:
|
|
for line in lines:
|
|
song_list.write(line + '\n')
|
|
logger.info(f"Removed Favorite: {song.hash} {song.tja.metadata.title['en']} {song.tja.metadata.subtitle['en']}")
|
|
else:
|
|
with open(favorites_path, 'a', encoding='utf-8-sig') as song_list:
|
|
song_list.write(f'{song.hash}|{song.tja.metadata.title["en"]}|{song.tja.metadata.subtitle["en"]}\n')
|
|
logger.info(f"Added Favorite: {song.hash} {song.tja.metadata.title['en']} {song.tja.metadata.subtitle['en']}")
|
|
return True
|
|
|
|
navigator = FileNavigator()
|