This commit is contained in:
Anthony Samms
2025-10-29 21:55:45 -04:00
parent 8c2447c912
commit 9cc02741bc
8 changed files with 441 additions and 86 deletions

View File

@@ -29,6 +29,7 @@ from scenes.settings import SettingsScreen
from scenes.song_select import SongSelectScreen from scenes.song_select import SongSelectScreen
from scenes.title import TitleScreen from scenes.title import TitleScreen
from scenes.two_player.song_select import TwoPlayerSongSelectScreen from scenes.two_player.song_select import TwoPlayerSongSelectScreen
from scenes.dan_select import DanSelectScreen
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -42,6 +43,7 @@ class Screens:
RESULT = "RESULT" RESULT = "RESULT"
RESULT_2P = "RESULT_2P" RESULT_2P = "RESULT_2P"
SONG_SELECT_2P = "SONG_SELECT_2P" SONG_SELECT_2P = "SONG_SELECT_2P"
DAN_SELECT = "DAN_SELECT"
SETTINGS = "SETTINGS" SETTINGS = "SETTINGS"
DEV_MENU = "DEV_MENU" DEV_MENU = "DEV_MENU"
LOADING = "LOADING" LOADING = "LOADING"
@@ -156,6 +158,7 @@ def main():
result_screen_2p = TwoPlayerResultScreen('result') result_screen_2p = TwoPlayerResultScreen('result')
settings_screen = SettingsScreen('settings') settings_screen = SettingsScreen('settings')
dev_screen = DevScreen('dev') dev_screen = DevScreen('dev')
dan_select_screen = DanSelectScreen('dan_select')
screen_mapping = { screen_mapping = {
Screens.ENTRY: entry_screen, Screens.ENTRY: entry_screen,
@@ -168,6 +171,7 @@ def main():
Screens.RESULT_2P: result_screen_2p, Screens.RESULT_2P: result_screen_2p,
Screens.SETTINGS: settings_screen, Screens.SETTINGS: settings_screen,
Screens.DEV_MENU: dev_screen, Screens.DEV_MENU: dev_screen,
Screens.DAN_SELECT: dan_select_screen,
Screens.LOADING: load_screen Screens.LOADING: load_screen
} }
target = ray.load_render_texture(screen_width, screen_height) target = ray.load_render_texture(screen_width, screen_height)
@@ -195,7 +199,7 @@ def main():
next_screen = screen.update() next_screen = screen.update()
if screen.screen_init: if screen.screen_init:
ray.clear_background(ray.BLACK) ray.clear_background(ray.BLACK)
screen.draw() screen._do_draw()
if next_screen is not None: if next_screen is not None:
logger.info(f"Screen changed from {current_screen} to {next_screen}") logger.info(f"Screen changed from {current_screen} to {next_screen}")

View File

@@ -1,3 +1,5 @@
from dataclasses import dataclass
import json
import logging import logging
from pathlib import Path from pathlib import Path
import random import random
@@ -134,7 +136,8 @@ class SongBox:
if not (-56 <= self.position <= 1280): if not (-56 <= self.position <= 1280):
self.reset() self.reset()
def update(self, is_diff_select): def update(self, is_diff_select: bool):
current_time = get_current_ms()
self.is_diff_select = is_diff_select self.is_diff_select = is_diff_select
is_open_prev = self.is_open is_open_prev = self.is_open
self.move_box() self.move_box()
@@ -146,10 +149,10 @@ class SongBox:
self.yellow_box.update(is_diff_select) self.yellow_box.update(is_diff_select)
if self.history_wait == 0: if self.history_wait == 0:
self.history_wait = get_current_ms() 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}: 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, get_current_ms()) self.score_history = ScoreHistory(self.scores, current_time)
if not is_open_prev and self.is_open: if not is_open_prev and self.is_open:
if self.tja is not None or self.is_back: if self.tja is not None or self.is_back:
@@ -159,9 +162,9 @@ class SongBox:
self.hori_name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5) self.hori_name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5)
self.open_anim.start() self.open_anim.start()
self.open_fade.start() self.open_fade.start()
self.wait = get_current_ms() self.wait = current_time
if get_current_ms() >= self.history_wait + 3000: if current_time >= self.history_wait + 3000:
self.history_wait = get_current_ms() self.history_wait = current_time
if self.tja is None and self.texture_index != 17 and not audio.is_sound_playing('voice_enter'): 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') audio.play_sound(f'genre_voice_{self.texture_index}', 'voice')
elif not self.is_open and is_open_prev and audio.is_sound_playing(f'genre_voice_{self.texture_index}'): elif not self.is_open and is_open_prev and audio.is_sound_playing(f'genre_voice_{self.texture_index}'):
@@ -171,14 +174,14 @@ class SongBox:
if self.box_texture is None and self.box_texture_path is not None: 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.box_texture = ray.load_texture(self.box_texture_path)
self.open_anim.update(get_current_ms()) self.open_anim.update(current_time)
self.open_fade.update(get_current_ms()) self.open_fade.update(current_time)
if self.name is None: if self.name is None:
self.name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5, vertical=True) self.name = OutlinedText(self.text_name, 40, ray.WHITE, outline_thickness=5, vertical=True)
if self.score_history is not None: if self.score_history is not None:
self.score_history.update(get_current_ms()) self.score_history.update(current_time)
def _draw_closed(self, x: int, y: int): def _draw_closed(self, x: int, y: int):
@@ -195,6 +198,7 @@ class SongBox:
if self.is_back: if self.is_back:
tex.draw_texture('box', 'back_text', x=x, y=y) tex.draw_texture('box', 'back_text', x=x, y=y)
return
elif self.name is not None: 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) 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)
@@ -271,11 +275,12 @@ class SongBox:
class YellowBox: class YellowBox:
"""A song box when it is opened.""" """A song box when it is opened."""
def __init__(self, name: OutlinedText, is_back: bool, tja: Optional[TJAParser] = None): def __init__(self, name: Optional[OutlinedText], is_back: bool, tja: Optional[TJAParser] = None, is_dan: bool = False):
self.is_diff_select = False self.is_diff_select = False
self.name = name self.name = name
self.is_back = is_back self.is_back = is_back
self.tja = tja self.tja = tja
self.is_dan = is_dan
self.subtitle = None self.subtitle = None
if self.tja is not None: if self.tja is not None:
subtitle_text = self.tja.metadata.subtitle.get(global_data.config['general']['language'], '') subtitle_text = self.tja.metadata.subtitle.get(global_data.config['general']['language'], '')
@@ -469,6 +474,8 @@ class YellowBox:
def draw(self, song_box: SongBox, fade_override: Optional[float], is_ura: bool): def draw(self, song_box: SongBox, fade_override: Optional[float], is_ura: bool):
self._draw_yellow_box() self._draw_yellow_box()
if self.is_dan:
return
if self.is_diff_select and self.tja is not None: if self.is_diff_select and self.tja is not None:
self._draw_tja_data_diff(is_ura, song_box) self._draw_tja_data_diff(is_ura, song_box)
else: else:
@@ -481,6 +488,103 @@ class YellowBox:
self._draw_text(song_box) self._draw_text(song_box)
class DanBox:
def __init__(self, title: str, color: int, songs: list[tuple[TJAParser, 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.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
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 None:
self.name.unload()
self.name = None
def get_text(self):
if self.name is None:
self.name = OutlinedText(self.title, 40, ray.WHITE, outline_thickness=5, vertical=True)
if self.is_open and not self.song_text:
for song, genre, difficulty 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, outline_thickness=5, vertical=True)
font_size = 30 if len(subtitle) < 30 else 20
subtitle_text = OutlinedText(subtitle, font_size, ray.WHITE, outline_thickness=5, 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_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(x, y, 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)
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: class GenreBG:
"""The background for a genre box.""" """The background for a genre box."""
def __init__(self, start_box: SongBox, end_box: SongBox, title: OutlinedText, diff_sort: Optional[int]): def __init__(self, start_box: SongBox, end_box: SongBox, title: OutlinedText, diff_sort: Optional[int]):
@@ -651,6 +755,34 @@ class FileSystemItem:
self.path = path self.path = path
self.name = name self.name = name
def parse_box_def(path: Path):
"""Parse box.def file for directory metadata"""
texture_index = SongBox.DEFAULT_INDEX
name = path.name
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()
except Exception as e:
logger.error(f"Error parsing box.def in {path}: {e}")
return name, texture_index, collection
class Directory(FileSystemItem): class Directory(FileSystemItem):
"""Represents a directory in the navigation system""" """Represents a directory in the navigation system"""
COLLECTIONS = [ COLLECTIONS = [
@@ -690,6 +822,42 @@ class SongFile(FileSystemItem):
self.box.hash = global_data.song_hashes[self.hash][0]["diff_hashes"] self.box.hash = global_data.song_hashes[self.hash][0]["diff_hashes"]
self.box.get_scores() 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":
self.logging.error(f"Invalid dan course file: {path}")
with open(path, 'r') as f:
data = json.load(f)
title = data["title"]
color = data["color"]
songs = []
for song in data["songs"]:
hash = song["hash"]
song_title = song["title"]
song_subtitle = song["subtitle"]
difficulty = song["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)
songs.append((TJAParser(path), genre_index, difficulty))
else:
pass
#do something with song_title, song_subtitle
exams = []
for exam in data["exams"]:
exams.append(Exam(exam["type"], exam["red"], exam["gold"], exam["range"]))
self.box = DanBox(title, color, songs, exams)
class FileNavigator: class FileNavigator:
"""Manages navigation through pre-generated Directory and SongFile objects""" """Manages navigation through pre-generated Directory and SongFile objects"""
def __init__(self): def __init__(self):
@@ -717,6 +885,7 @@ class FileNavigator:
self.box_open = False self.box_open = False
self.genre_bg = None self.genre_bg = None
self.song_count = 0 self.song_count = 0
self.in_dan_select = False
logger.info("FileNavigator initialized") logger.info("FileNavigator initialized")
def initialize(self, root_dirs: list[Path]): def initialize(self, root_dirs: list[Path]):
@@ -796,7 +965,7 @@ class FileNavigator:
box_texture = None box_texture = None
collection = None collection = None
name, texture_index, collection = self._parse_box_def(dir_path) name, texture_index, collection = parse_box_def(dir_path)
box_png_path = dir_path / "box.png" box_png_path = dir_path / "box.png"
if box_png_path.exists(): if box_png_path.exists():
box_texture = str(box_png_path) box_texture = str(box_png_path)
@@ -847,7 +1016,10 @@ class FileNavigator:
# Create SongFile objects # Create SongFile objects
for tja_path in sorted(tja_files): for tja_path in sorted(tja_files):
song_key = str(tja_path) song_key = str(tja_path)
if song_key not in self.all_song_files and tja_path in global_data.song_paths: if song_key not in self.all_song_files and tja_path.name == "dan.json":
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 = SongFile(tja_path, tja_path.name, texture_index)
song_obj.box.get_scores() song_obj.box.get_scores()
for course in song_obj.tja.metadata.course_data: for course in song_obj.tja.metadata.course_data:
@@ -872,10 +1044,10 @@ class FileNavigator:
self.diff_sort_statistics[course][level][1] += 1 self.diff_sort_statistics[course][level][1] += 1
elif is_cleared: elif is_cleared:
self.diff_sort_statistics[course][level][2] += 1 self.diff_sort_statistics[course][level][2] += 1
self.song_count += 1
global_data.song_progress = self.song_count / global_data.total_songs
if song_obj.is_recent: if song_obj.is_recent:
self.new_items.append(SongFile(tja_path, tja_path.name, SongBox.DEFAULT_INDEX, name_texture_index=texture_index)) 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 self.all_song_files[song_key] = song_obj
if song_key in self.all_song_files: if song_key in self.all_song_files:
@@ -914,7 +1086,7 @@ class FileNavigator:
# Determine if current directory has child directories with box.def # Determine if current directory has child directories with box.def
has_children = False has_children = False
if self.is_at_root(): 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) has_children = True # Root always has "children" (the root directories)
else: else:
has_children = any(item.is_dir() and (item / "box.def").exists() has_children = any(item.is_dir() and (item / "box.def").exists()
@@ -987,7 +1159,7 @@ class FileNavigator:
temp_items.append(item) temp_items.append(item)
content_items = random.sample(temp_items, 10) content_items = random.sample(temp_items, 10)
if content_items == [] or (selected_item is not None and selected_item.box.texture_index == 13): if content_items == []:
self.go_back() self.go_back()
return return
i = 1 i = 1
@@ -1060,8 +1232,7 @@ class FileNavigator:
self.load_current_directory(selected_item=selected_item) self.load_current_directory(selected_item=selected_item)
elif isinstance(selected_item, SongFile): return selected_item
return selected_item
def go_back(self): def go_back(self):
"""Navigate back to the previous directory""" """Navigate back to the previous directory"""
@@ -1109,6 +1280,8 @@ class FileNavigator:
song_key = str(tja_path) song_key = str(tja_path)
if song_key in self.all_song_files: if song_key in self.all_song_files:
song_obj = self.all_song_files[song_key] song_obj = self.all_song_files[song_key]
if not isinstance(song_obj, SongFile):
continue
for diff in song_obj.box.scores: for diff in song_obj.box.scores:
if diff not in all_scores: if diff not in all_scores:
all_scores[diff] = [] all_scores[diff] = []
@@ -1136,7 +1309,7 @@ class FileNavigator:
tja_files: list[Path] = [] tja_files: list[Path] = []
for path in directory.iterdir(): for path in directory.iterdir():
if path.is_file() and path.suffix.lower() == ".tja": if (path.is_file() and path.suffix.lower() == ".tja") or path.name == "dan.json":
tja_files.append(path) tja_files.append(path)
elif path.is_dir(): elif path.is_dir():
# Only recurse into subdirectories that don't have box.def # Only recurse into subdirectories that don't have box.def
@@ -1163,34 +1336,6 @@ class FileNavigator:
return tja_files return tja_files
def _parse_box_def(self, path: Path):
"""Parse box.def file for directory metadata"""
texture_index = SongBox.DEFAULT_INDEX
name = path.name
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()
except Exception as e:
logger.error(f"Error parsing box.def in {path}: {e}")
return name, texture_index, collection
def _read_song_list(self, path: Path): def _read_song_list(self, path: Path):
"""Read and process song_list.txt file""" """Read and process song_list.txt file"""
tja_files: list[Path] = [] tja_files: list[Path] = []
@@ -1251,13 +1396,24 @@ class FileNavigator:
elif offset < -len(self.items) // 2: elif offset < -len(self.items) // 2:
offset += len(self.items) offset += len(self.items)
position = BOX_CENTER + (100 * offset) # 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: if position == BOX_CENTER:
position += 150 position += center_offset
elif position > BOX_CENTER: elif position > BOX_CENTER:
position += 300 position += side_offset_r
else: else:
position -= 0 position -= side_offset_l
if item.box.position == -11111: if item.box.position == -11111:
item.box.position = position item.box.position = position

View File

@@ -39,3 +39,7 @@ class Screen:
def draw(self): def draw(self):
pass pass
def _do_draw(self):
if self.screen_init:
self.draw()

View File

@@ -495,17 +495,15 @@ class TJAParser:
def get_moji(self, play_note_list: list[Note], ms_per_measure: float) -> None: def get_moji(self, play_note_list: list[Note], ms_per_measure: float) -> None:
""" """
Assign 口唱歌 (note phoneticization) to notes. Assign 口唱歌 (note phoneticization) to notes.
Args: Args:
play_note_list (list[Note]): The list of notes to process. play_note_list (list[Note]): The list of notes to process.
ms_per_measure (float): The duration of a measure in milliseconds. ms_per_measure (float): The duration of a measure in milliseconds.
Returns: Returns:
None None
""" """
se_notes = { se_notes = {
1: [0, 1, 2], # Note '1' has three possible sound effects 1: 0,
2: [3, 4], # Note '2' has two possible sound effects 2: 3,
3: 5, 3: 5,
4: 6, 4: 6,
5: 7, 5: 7,
@@ -514,10 +512,8 @@ class TJAParser:
8: 10, 8: 10,
9: 11 9: 11
} }
if len(play_note_list) <= 1: if len(play_note_list) <= 1:
return return
current_note = play_note_list[-1] current_note = play_note_list[-1]
if current_note.type == 1: if current_note.type == 1:
current_note.moji = 0 current_note.moji = 0
@@ -525,44 +521,43 @@ class TJAParser:
current_note.moji = 3 current_note.moji = 3
else: else:
current_note.moji = se_notes[current_note.type] current_note.moji = se_notes[current_note.type]
prev_note = play_note_list[-2] prev_note = play_note_list[-2]
if prev_note.type == 1:
if prev_note.type in {1, 2}:
timing_threshold = ms_per_measure / 8 - 1 timing_threshold = ms_per_measure / 8 - 1
if current_note.hit_ms - prev_note.hit_ms <= timing_threshold: if current_note.hit_ms - prev_note.hit_ms <= timing_threshold:
prev_note.moji = se_notes[prev_note.type][1] prev_note.moji = 1
else: else:
prev_note.moji = se_notes[prev_note.type][0] prev_note.moji = 0
elif prev_note.type == 2:
timing_threshold = ms_per_measure / 8 - 1
if current_note.hit_ms - prev_note.hit_ms <= timing_threshold:
prev_note.moji = 4
else:
prev_note.moji = 3
else: else:
prev_note.moji = se_notes[prev_note.type] prev_note.moji = se_notes[prev_note.type]
if len(play_note_list) > 3: if len(play_note_list) > 3:
notes_minus_4 = play_note_list[-4] notes_minus_4 = play_note_list[-4]
notes_minus_3 = play_note_list[-3] notes_minus_3 = play_note_list[-3]
notes_minus_2 = play_note_list[-2] notes_minus_2 = play_note_list[-2]
consecutive_ones = ( consecutive_ones = (
notes_minus_4.type == 1 and notes_minus_4.type == 1 and
notes_minus_3.type == 1 and notes_minus_3.type == 1 and
notes_minus_2.type == 1 notes_minus_2.type == 1
) )
if consecutive_ones: if consecutive_ones:
rapid_timing = ( rapid_timing = (
notes_minus_3.hit_ms - notes_minus_4.hit_ms < (ms_per_measure / 8) and notes_minus_3.hit_ms - notes_minus_4.hit_ms < (ms_per_measure / 8) and
notes_minus_2.hit_ms - notes_minus_3.hit_ms < (ms_per_measure / 8) notes_minus_2.hit_ms - notes_minus_3.hit_ms < (ms_per_measure / 8)
) )
if rapid_timing: if rapid_timing:
if len(play_note_list) > 5: if len(play_note_list) > 5:
spacing_before = play_note_list[-4].hit_ms - play_note_list[-5].hit_ms >= (ms_per_measure / 8) spacing_before = play_note_list[-4].hit_ms - play_note_list[-5].hit_ms >= (ms_per_measure / 8)
spacing_after = play_note_list[-1].hit_ms - play_note_list[-2].hit_ms >= (ms_per_measure / 8) spacing_after = play_note_list[-1].hit_ms - play_note_list[-2].hit_ms >= (ms_per_measure / 8)
if spacing_before and spacing_after: if spacing_before and spacing_after:
play_note_list[-3].moji = se_notes[1][2] play_note_list[-3].moji = 2
else: else:
play_note_list[-3].moji = se_notes[1][2] play_note_list[-3].moji = 2
def notes_to_position(self, diff: int): def notes_to_position(self, diff: int):
"""Parse a TJA's notes into a NoteList.""" """Parse a TJA's notes into a NoteList."""

170
scenes/dan_select.py Normal file
View File

@@ -0,0 +1,170 @@
import pyray as ray
from libs.audio import audio
from libs.global_data import global_data
from libs.texture import tex
from libs.chara_2d import Chara2D
from libs.global_objects import AllNetIcon, CoinOverlay, Indicator, Nameplate, Timer
from libs.screen import Screen
from libs.file_navigator import navigator
from libs.utils import get_current_ms, is_l_don_pressed, is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed
from scenes.song_select import SongSelectScreen, State
class DanSelectScreen(Screen):
def on_screen_start(self):
super().on_screen_start()
self.navigator = navigator
self.navigator.in_dan_select = True
self.navigator.select_current_item()
self.coin_overlay = CoinOverlay()
self.allnet_indicator = AllNetIcon()
self.timer = Timer(60, get_current_ms(), self.navigator.select_current_item)
self.indicator = Indicator(Indicator.State.SELECT)
self.player = DanSelectPlayer(str(global_data.player_num))
self.state = State.BROWSING
self.last_moved = 0
def on_screen_end(self, next_screen: str):
return super().on_screen_end(next_screen)
def handle_input_browsing(self):
"""Handle input for browsing songs."""
action = self.player.handle_input_browsing(self.last_moved, self.navigator.items[self.navigator.selected_index] if self.navigator.items else None)
current_time = get_current_ms()
if action == "skip_left":
for _ in range(10):
self.navigator.navigate_left()
self.last_moved = current_time
elif action == "skip_right":
for _ in range(10):
self.navigator.navigate_right()
self.last_moved = current_time
elif action == "navigate_left":
self.navigator.navigate_left()
self.last_moved = current_time
elif action == "navigate_right":
self.navigator.navigate_right()
self.last_moved = current_time
elif action == "go_back":
self.navigator.go_back()
elif action == "select_song":
pass
def handle_input(self, state, screen):
"""Main input dispatcher. Delegates to state-specific handlers."""
if state == State.BROWSING:
screen.handle_input_browsing()
elif state == State.SONG_SELECTED:
screen.handle_input_selected()
def update(self):
super().update()
current_time = get_current_ms()
self.indicator.update(current_time)
self.timer.update(current_time)
for song in self.navigator.items:
song.box.update(False)
song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150
self.player.update(current_time)
self.handle_input(self.state, self)
def draw(self):
tex.draw_texture('global', 'bg')
tex.draw_texture('global', 'bg_header')
tex.draw_texture('global', 'bg_footer')
tex.draw_texture('global', 'footer')
for item in self.navigator.items:
box = item.box
if -156 <= box.position <= 1280 + 144:
if box.position <= 500:
box.draw(box.position, 95, False)
else:
box.draw(box.position, 95, False)
self.player.draw()
self.indicator.draw(410, 575)
self.timer.draw()
self.coin_overlay.draw()
tex.draw_texture('global', 'dan_select')
self.allnet_indicator.draw()
class DanSelectPlayer:
def __init__(self, player_num: str):
self.player_num = player_num
self.selected_difficulty = -3
self.prev_diff = -3
self.selected_song = False
self.is_ura = False
self.ura_toggle = 0
self.diff_select_move_right = False
self.neiro_selector = None
self.modifier_selector = None
# Player-specific objects
self.chara = Chara2D(int(self.player_num) - 1, 100)
plate_info = global_data.config[f'nameplate_{self.player_num}p']
self.nameplate = Nameplate(plate_info['name'], plate_info['title'],
int(self.player_num), plate_info['dan'], plate_info['gold'])
def update(self, current_time):
"""Update player state"""
self.nameplate.update(current_time)
self.chara.update(current_time, 100, False, False)
def handle_input_browsing(self, last_moved, selected_item):
"""Handle input for browsing songs. Returns action string or None."""
current_time = get_current_ms()
# Skip left (fast navigate)
if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT_CONTROL) or (is_l_kat_pressed(self.player_num) and current_time <= last_moved + 50):
audio.play_sound('skip', 'sound')
return "skip_left"
# Skip right (fast navigate)
if ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT_CONTROL) or (is_r_kat_pressed(self.player_num) and current_time <= last_moved + 50):
audio.play_sound('skip', 'sound')
return "skip_right"
# Navigate left
if is_l_kat_pressed(self.player_num):
audio.play_sound('kat', 'sound')
return "navigate_left"
# Navigate right
if is_r_kat_pressed(self.player_num):
audio.play_sound('kat', 'sound')
return "navigate_right"
# Select/Enter
if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num):
if selected_item is not None and selected_item.box.is_back:
audio.play_sound('cancel', 'sound')
return "go_back"
else:
return "select_song"
return None
def handle_input(self, state, screen):
"""Main input dispatcher. Delegates to state-specific handlers."""
if self.is_voice_playing():
return
if state == State.BROWSING:
screen.handle_input_browsing()
elif state == State.SONG_SELECTED:
screen.handle_input_selected()
def handle_input_selected(self, current_item):
"""Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None"""
if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num):
return "confirm"
return None
def draw(self):
if self.player_num == '1':
self.nameplate.draw(30, 640)
self.chara.draw(x=-50, y=410)
else:
self.nameplate.draw(950, 640)
self.chara.draw(mirror=True, x=950, y=410)

View File

@@ -2159,7 +2159,6 @@ class Gauge:
self.is_rainbow = False self.is_rainbow = False
self.table = [ self.table = [
[ [
None,
{"clear_rate": 36.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 36.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
{"clear_rate": 38.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 38.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
{"clear_rate": 38.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 38.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
@@ -2167,7 +2166,6 @@ class Gauge:
{"clear_rate": 44.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 44.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
], ],
[ [
None,
{"clear_rate": 45.939, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 45.939, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
{"clear_rate": 45.939, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 45.939, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
{"clear_rate": 48.676, "ok_multiplier": 0.75, "bad_multiplier": -0.5}, {"clear_rate": 48.676, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
@@ -2177,7 +2175,6 @@ class Gauge:
{"clear_rate": 52.5, "ok_multiplier": 0.75, "bad_multiplier": -1.0}, {"clear_rate": 52.5, "ok_multiplier": 0.75, "bad_multiplier": -1.0},
], ],
[ [
None,
{"clear_rate": 54.325, "ok_multiplier": 0.75, "bad_multiplier": -0.75}, {"clear_rate": 54.325, "ok_multiplier": 0.75, "bad_multiplier": -0.75},
{"clear_rate": 54.325, "ok_multiplier": 0.75, "bad_multiplier": -0.75}, {"clear_rate": 54.325, "ok_multiplier": 0.75, "bad_multiplier": -0.75},
{"clear_rate": 50.774, "ok_multiplier": 0.75, "bad_multiplier": -1.0}, {"clear_rate": 50.774, "ok_multiplier": 0.75, "bad_multiplier": -1.0},
@@ -2188,7 +2185,6 @@ class Gauge:
{"clear_rate": 48.120, "ok_multiplier": 0.75, "bad_multiplier": -1.25}, {"clear_rate": 48.120, "ok_multiplier": 0.75, "bad_multiplier": -1.25},
], ],
[ [
None,
{"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6}, {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
{"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6}, {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
{"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6}, {"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
@@ -2209,7 +2205,7 @@ class Gauge:
"""Adds a good note to the gauge""" """Adds a good note to the gauge"""
self.gauge_update_anim.start() self.gauge_update_anim.start()
self.previous_length = int(self.gauge_length) self.previous_length = int(self.gauge_length)
self.gauge_length += (1 / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) self.gauge_length += (1 / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level-1]["clear_rate"]))
if self.gauge_length > self.gauge_max: if self.gauge_length > self.gauge_max:
self.gauge_length = self.gauge_max self.gauge_length = self.gauge_max
@@ -2217,14 +2213,14 @@ class Gauge:
"""Adds an ok note to the gauge""" """Adds an ok note to the gauge"""
self.gauge_update_anim.start() self.gauge_update_anim.start()
self.previous_length = int(self.gauge_length) self.previous_length = int(self.gauge_length)
self.gauge_length += ((1 * self.table[self.difficulty][self.level]["ok_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) self.gauge_length += ((1 * self.table[self.difficulty][self.level-1]["ok_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level-1]["clear_rate"]))
if self.gauge_length > self.gauge_max: if self.gauge_length > self.gauge_max:
self.gauge_length = self.gauge_max self.gauge_length = self.gauge_max
def add_bad(self): def add_bad(self):
"""Adds a bad note to the gauge""" """Adds a bad note to the gauge"""
self.previous_length = int(self.gauge_length) self.previous_length = int(self.gauge_length)
self.gauge_length += ((1 * self.table[self.difficulty][self.level]["bad_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])) self.gauge_length += ((1 * self.table[self.difficulty][self.level-1]["bad_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level-1]["clear_rate"]))
if self.gauge_length < 0: if self.gauge_length < 0:
self.gauge_length = 0 self.gauge_length = 0

View File

@@ -121,8 +121,8 @@ class ResultPlayer:
self.score_animator = ScoreAnimator(session_data.result_score) self.score_animator = ScoreAnimator(session_data.result_score)
plate_info = global_data.config[f'nameplate_{self.player_num}p'] plate_info = global_data.config[f'nameplate_{self.player_num}p']
self.nameplate = Nameplate(plate_info['name'], plate_info['title'], int(self.player_num), plate_info['dan'], plate_info['gold']) self.nameplate = Nameplate(plate_info['name'], plate_info['title'], int(self.player_num), plate_info['dan'], plate_info['gold'])
self.score, self.good, self.ok, self.bad, self.max_combo, self.total_drumroll= '', '', '', '', '', '' self.score, self.good, self.ok, self.bad, self.max_combo, self.total_drumroll = '', '', '', '', '', ''
self.update_list = [['score', session_data.result_score], self.update_list: list[tuple[str, int]] = [['score', session_data.result_score],
['good', session_data.result_good], ['good', session_data.result_good],
['ok', session_data.result_ok], ['ok', session_data.result_ok],
['bad', session_data.result_bad], ['bad', session_data.result_bad],

View File

@@ -64,6 +64,7 @@ class SongSelectScreen(Screen):
self.timer_selected = Timer(40, get_current_ms(), self._confirm_selection_wrapper) self.timer_selected = Timer(40, get_current_ms(), self._confirm_selection_wrapper)
self.screen_init = True self.screen_init = True
self.ura_switch_animation = UraSwitchAnimation() self.ura_switch_animation = UraSwitchAnimation()
self.dan_transition = DanTransition()
self.player_1 = SongSelectPlayer(str(global_data.player_num), self.text_fade_in) self.player_1 = SongSelectPlayer(str(global_data.player_num), self.text_fade_in)
@@ -88,7 +89,9 @@ class SongSelectScreen(Screen):
self.reset_demo_music() self.reset_demo_music()
self.finalize_song() self.finalize_song()
self.player_1.nameplate.unload() self.player_1.nameplate.unload()
self.navigator.get_current_item().box.yellow_box.create_anim() current_item = self.navigator.get_current_item()
if current_item.box.yellow_box is not None:
current_item.box.yellow_box.create_anim()
return super().on_screen_end(next_screen) return super().on_screen_end(next_screen)
def reset_demo_music(self): def reset_demo_music(self):
@@ -130,8 +133,13 @@ class SongSelectScreen(Screen):
self.text_fade_in.start() self.text_fade_in.start()
self.text_fade_out.start() self.text_fade_out.start()
elif action == "select_song": elif action == "select_song":
current_song = self.navigator.get_current_item()
if isinstance(current_song, Directory) and current_song.box.texture_index == 13:
self.dan_transition.start()
audio.stop_sound('bgm')
return
selected_song = self.navigator.select_current_item() selected_song = self.navigator.select_current_item()
if selected_song: if isinstance(selected_song, SongFile):
self.state = State.SONG_SELECTED self.state = State.SONG_SELECTED
self.player_1.on_song_selected(selected_song) self.player_1.on_song_selected(selected_song)
audio.play_sound('don', 'sound') audio.play_sound('don', 'sound')
@@ -240,6 +248,9 @@ class SongSelectScreen(Screen):
self.indicator.update(current_time) self.indicator.update(current_time)
self.blue_arrow_fade.update(current_time) self.blue_arrow_fade.update(current_time)
self.blue_arrow_move.update(current_time) self.blue_arrow_move.update(current_time)
self.dan_transition.update(current_time)
if self.dan_transition.is_finished:
return self.on_screen_end('DAN_SELECT')
next_screen = self.update_players(current_time) next_screen = self.update_players(current_time)
@@ -353,6 +364,8 @@ class SongSelectScreen(Screen):
if self.game_transition is not None: if self.game_transition is not None:
self.game_transition.draw() self.game_transition.draw()
if self.dan_transition.is_started:
self.dan_transition.draw()
self.allnet_indicator.draw() self.allnet_indicator.draw()
class SongSelectPlayer: class SongSelectPlayer:
@@ -998,7 +1011,7 @@ class NeiroSelector:
self.move_sideways.start() self.move_sideways.start()
self.fade_sideways.start() self.fade_sideways.start()
self.text_2.unload() self.text_2.unload()
self.text_2 = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE, ray.BLACK) self.text_2 = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE)
self.direction = -1 self.direction = -1
if self.selected_sound == len(self.sounds): if self.selected_sound == len(self.sounds):
return return
@@ -1013,7 +1026,7 @@ class NeiroSelector:
self.move_sideways.start() self.move_sideways.start()
self.fade_sideways.start() self.fade_sideways.start()
self.text_2.unload() self.text_2.unload()
self.text_2 = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE, ray.BLACK) self.text_2 = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE)
self.direction = 1 self.direction = 1
if self.selected_sound == len(self.sounds): if self.selected_sound == len(self.sounds):
return return
@@ -1037,7 +1050,7 @@ class NeiroSelector:
self.fade_sideways.update(current_ms) self.fade_sideways.update(current_ms)
if self.move_sideways.is_finished: if self.move_sideways.is_finished:
self.text.unload() self.text.unload()
self.text = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE, ray.BLACK) self.text = OutlinedText(self.sounds[self.selected_sound], 50, ray.WHITE)
self.is_finished = self.move.is_finished and self.is_confirmed self.is_finished = self.move.is_finished and self.is_confirmed
def draw(self): def draw(self):
@@ -1208,8 +1221,7 @@ class ModifierSelector:
else: else:
tex.draw_texture('modifier', 'mod_bg', y=move + (i*50), x=x) tex.draw_texture('modifier', 'mod_bg', y=move + (i*50), x=x)
tex.draw_texture('modifier', 'mod_box', y=move + (i*50), x=x) tex.draw_texture('modifier', 'mod_box', y=move + (i*50), x=x)
dest = ray.Rectangle(92 + x, 819 + move + (i*50), self.text_name[i].texture.width, self.text_name[i].texture.height) self.text_name[i].draw(outline_color=ray.BLACK, x=92 + x, y=819 + move + (i*50))
self.text_name[i].draw(self.text_name[i].default_src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
current_mod = self.mods[i] current_mod = self.mods[i]
current_value = getattr(global_data.modifiers[int(self.player_num)-1], current_mod.name) current_value = getattr(global_data.modifiers[int(self.player_num)-1], current_mod.name)
@@ -1255,3 +1267,21 @@ class ModifierSelector:
if i == self.current_mod_index: if i == self.current_mod_index:
tex.draw_texture('modifier', 'blue_arrow', y=move + (i*50), x=x-self.blue_arrow_move.attribute, fade=self.blue_arrow_fade.attribute) tex.draw_texture('modifier', 'blue_arrow', y=move + (i*50), x=x-self.blue_arrow_move.attribute, fade=self.blue_arrow_fade.attribute)
tex.draw_texture('modifier', 'blue_arrow', y=move + (i*50), x=x+110 + self.blue_arrow_move.attribute, mirror='horizontal', fade=self.blue_arrow_fade.attribute) tex.draw_texture('modifier', 'blue_arrow', y=move + (i*50), x=x+110 + self.blue_arrow_move.attribute, mirror='horizontal', fade=self.blue_arrow_fade.attribute)
class DanTransition:
def __init__(self):
self.slide_in = tex.get_animation(38)
self.is_finished = False
self.is_started = False
def start(self):
self.slide_in.start()
self.is_started = True
def update(self, current_time_ms: float):
self.slide_in.update(current_time_ms)
if self.slide_in.is_finished:
self.is_finished = True
def draw(self):
tex.draw_texture('dan_transition', 'background', x2=self.slide_in.attribute)