add the majority of dan mode

This commit is contained in:
Anthony Samms
2025-10-30 23:09:16 -04:00
parent 3acc375aaf
commit 6c8466de8e
9 changed files with 508 additions and 86 deletions

View File

@@ -21,6 +21,7 @@ from libs.utils import (
from scenes.devtest import DevScreen from scenes.devtest import DevScreen
from scenes.entry import EntryScreen from scenes.entry import EntryScreen
from scenes.game import GameScreen from scenes.game import GameScreen
from scenes.game_dan import DanGameScreen
from scenes.two_player.game import TwoPlayerGameScreen from scenes.two_player.game import TwoPlayerGameScreen
from scenes.two_player.result import TwoPlayerResultScreen from scenes.two_player.result import TwoPlayerResultScreen
from scenes.loading import LoadScreen from scenes.loading import LoadScreen
@@ -44,6 +45,7 @@ class Screens:
RESULT_2P = "RESULT_2P" RESULT_2P = "RESULT_2P"
SONG_SELECT_2P = "SONG_SELECT_2P" SONG_SELECT_2P = "SONG_SELECT_2P"
DAN_SELECT = "DAN_SELECT" DAN_SELECT = "DAN_SELECT"
GAME_DAN = "GAME_DAN"
SETTINGS = "SETTINGS" SETTINGS = "SETTINGS"
DEV_MENU = "DEV_MENU" DEV_MENU = "DEV_MENU"
LOADING = "LOADING" LOADING = "LOADING"
@@ -159,6 +161,7 @@ def main():
settings_screen = SettingsScreen('settings') settings_screen = SettingsScreen('settings')
dev_screen = DevScreen('dev') dev_screen = DevScreen('dev')
dan_select_screen = DanSelectScreen('dan_select') dan_select_screen = DanSelectScreen('dan_select')
game_screen_dan = DanGameScreen('game_dan')
screen_mapping = { screen_mapping = {
Screens.ENTRY: entry_screen, Screens.ENTRY: entry_screen,
@@ -172,6 +175,7 @@ def main():
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.DAN_SELECT: dan_select_screen,
Screens.GAME_DAN: game_screen_dan,
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)

View File

@@ -504,13 +504,13 @@ class DanBox:
self.total_notes = 0 self.total_notes = 0
for song, genre_index, difficulty in self.songs: for song, genre_index, difficulty in self.songs:
notes, branch_m, branch_e, branch_n = song.notes_to_position(difficulty) notes, branch_m, branch_e, branch_n = song.notes_to_position(difficulty)
self.total_notes += len(notes.play_notes) self.total_notes += sum(1 for note in notes.play_notes if note.type < 5)
for branch in branch_m: for branch in branch_m:
self.total_notes += len(branch.play_notes) self.total_notes += sum(1 for note in branch.play_notes if note.type < 5)
for branch in branch_e: for branch in branch_e:
self.total_notes += len(branch.play_notes) self.total_notes += sum(1 for note in branch.play_notes if note.type < 5)
for branch in branch_n: for branch in branch_n:
self.total_notes += len(branch.play_notes) self.total_notes += sum(1 for note in branch.play_notes if note.type < 5)
self.name = None self.name = None
self.hori_name = None self.hori_name = None
self.yellow_box = None self.yellow_box = None
@@ -567,6 +567,36 @@ class DanBox:
if self.yellow_box is not None: if self.yellow_box is not None:
self.yellow_box.update(True) 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): def _draw_closed(self, x: int, y: int):
tex.draw_texture('box', 'folder', frame=self.color, x=x) tex.draw_texture('box', 'folder', frame=self.color, x=x)
if self.name is not None: if self.name is not None:
@@ -601,6 +631,8 @@ class DanBox:
if self.hori_name is not None: 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.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): def draw(self, x: int, y: int, is_ura: bool):
if self.is_open: if self.is_open:
self._draw_open(x, y, is_ura) self._draw_open(x, y, is_ura)
@@ -858,27 +890,27 @@ class DanCourse(FileSystemItem):
self.logging.error(f"Invalid dan course file: {path}") self.logging.error(f"Invalid dan course file: {path}")
with open(path, 'r') as f: with open(path, 'r') as f:
data = json.load(f) data = json.load(f)
title = data["title"] self.title = data["title"]
color = data["color"] self.color = data["color"]
songs = [] self.charts = []
for song in data["songs"]: for chart in data["charts"]:
hash = song["hash"] hash = chart["hash"]
song_title = song["title"] chart_title = chart["title"]
song_subtitle = song["subtitle"] chart_subtitle = chart["subtitle"]
difficulty = song["difficulty"] difficulty = chart["difficulty"]
if hash in global_data.song_hashes: if hash in global_data.song_hashes:
path = Path(global_data.song_hashes[hash][0]["file_path"]) path = Path(global_data.song_hashes[hash][0]["file_path"])
if (path.parent.parent / "box.def").exists(): if (path.parent.parent / "box.def").exists():
_, genre_index, _ = parse_box_def(path.parent.parent) _, genre_index, _ = parse_box_def(path.parent.parent)
songs.append((TJAParser(path), genre_index, difficulty)) self.charts.append((TJAParser(path), genre_index, difficulty))
else: else:
pass pass
#do something with song_title, song_subtitle #do something with song_title, song_subtitle
exams = [] self.exams = []
for exam in data["exams"]: for exam in data["exams"]:
exams.append(Exam(exam["type"], exam["red"], exam["gold"], exam["range"])) self.exams.append(Exam(exam["type"], exam["value"][0], exam["value"][1], exam["range"]))
self.box = DanBox(title, color, songs, exams) self.box = DanBox(self.title, self.color, self.charts, self.exams)
class FileNavigator: class FileNavigator:
"""Manages navigation through pre-generated Directory and SongFile objects""" """Manages navigation through pre-generated Directory and SongFile objects"""
@@ -1251,6 +1283,7 @@ class FileNavigator:
# Save current state to history # Save current state to history
self.history.append((self.current_dir, self.selected_index)) self.history.append((self.current_dir, self.selected_index))
self.current_dir = selected_item.path self.current_dir = selected_item.path
logger.info(f"Entered Directory {selected_item.path}")
self.load_current_directory(selected_item=selected_item) self.load_current_directory(selected_item=selected_item)
@@ -1459,12 +1492,14 @@ class FileNavigator:
if self.items: if self.items:
self.selected_index = (self.selected_index - 1) % len(self.items) self.selected_index = (self.selected_index - 1) % len(self.items)
self.calculate_box_positions() self.calculate_box_positions()
logger.info(f"Moved Left to {self.items[self.selected_index].path}")
def navigate_right(self): def navigate_right(self):
"""Move selection right with wrap-around""" """Move selection right with wrap-around"""
if self.items: if self.items:
self.selected_index = (self.selected_index + 1) % len(self.items) self.selected_index = (self.selected_index + 1) % len(self.items)
self.calculate_box_positions() self.calculate_box_positions()
logger.info(f"Moved Right to {self.items[self.selected_index].path}")
def get_current_item(self): def get_current_item(self):
"""Get the currently selected item""" """Get the currently selected item"""

View File

@@ -16,6 +16,10 @@ class Modifiers:
@dataclass @dataclass
class SessionData: class SessionData:
"""Data class for storing session data. Wiped after the result screen. """Data class for storing session data. Wiped after the result screen.
selected_song (Path): The currently selected song.
selected_dan (list[tuple[Any, int, int]]): The currently selected dan songs (TJA). TJAParser, Genre Index, Difficulty
selected_dan_exam: list[Exam]: list of dan requirements, contains Exam objects
dan_color: int: The emblem color of the selected dan
selected_difficulty: The difficulty level selected by the user. selected_difficulty: The difficulty level selected by the user.
song_title: The title of the song being played. song_title: The title of the song being played.
genre_index: The index of the genre being played. genre_index: The index of the genre being played.
@@ -27,6 +31,10 @@ class SessionData:
result_total_drumroll: The total drumroll achieved in the game. result_total_drumroll: The total drumroll achieved in the game.
result_gauge_length: The length of the gauge achieved in the game. result_gauge_length: The length of the gauge achieved in the game.
prev_score: The previous score pulled from the database.""" prev_score: The previous score pulled from the database."""
selected_song: Path = Path()
selected_dan: list[tuple[Any, int, int]] = field(default_factory=lambda: [])
selected_dan_exam: list[Any] = field(default_factory=lambda: [])
dan_color: int = 0
selected_difficulty: int = 0 selected_difficulty: int = 0
song_title: str = '' song_title: str = ''
genre_index: int = 0 genre_index: int = 0
@@ -45,7 +53,6 @@ class GlobalData:
Global data for the game. Should be accessed via the global_data variable. Global data for the game. Should be accessed via the global_data variable.
Attributes: Attributes:
selected_song (Path): The currently selected song.
songs_played (int): The number of songs played. songs_played (int): The number of songs played.
config (dict): The configuration settings. config (dict): The configuration settings.
song_hashes (dict[str, list[dict]]): A dictionary mapping song hashes to their metadata. song_hashes (dict[str, list[dict]]): A dictionary mapping song hashes to their metadata.
@@ -58,7 +65,6 @@ class GlobalData:
modifiers (list[Modifiers]): The modifiers for the game. modifiers (list[Modifiers]): The modifiers for the game.
session_data (list[SessionData]): Session data for both players. session_data (list[SessionData]): Session data for both players.
""" """
selected_song: Path = Path()
songs_played: int = 0 songs_played: int = 0
config: dict[str, Any] = field(default_factory=lambda: dict()) config: dict[str, Any] = field(default_factory=lambda: dict())
song_hashes: dict[str, list[dict]] = field(default_factory=lambda: dict()) #Hash to path song_hashes: dict[str, list[dict]] = field(default_factory=lambda: dict()) #Hash to path

View File

@@ -16,8 +16,12 @@ class Transition:
self.chara_down = global_tex.get_animation(2) self.chara_down = global_tex.get_animation(2)
self.song_info_fade = global_tex.get_animation(3) self.song_info_fade = global_tex.get_animation(3)
self.song_info_fade_out = global_tex.get_animation(4) self.song_info_fade_out = global_tex.get_animation(4)
self.title = OutlinedText(title, 40, ray.WHITE) if title == '' and subtitle == '':
self.subtitle = OutlinedText(subtitle, 30, ray.WHITE) self.title = ''
self.subtitle = ''
else:
self.title = OutlinedText(title, 40, ray.WHITE)
self.subtitle = OutlinedText(subtitle, 30, ray.WHITE)
self.is_second = is_second self.is_second = is_second
def start(self): def start(self):

View File

@@ -1,3 +1,5 @@
import logging
import pyray as ray import pyray as ray
from libs.audio import audio from libs.audio import audio
@@ -6,10 +8,12 @@ from libs.texture import tex
from libs.chara_2d import Chara2D from libs.chara_2d import Chara2D
from libs.global_objects import AllNetIcon, CoinOverlay, Indicator, Nameplate, Timer from libs.global_objects import AllNetIcon, CoinOverlay, Indicator, Nameplate, Timer
from libs.screen import Screen from libs.screen import Screen
from libs.file_navigator import navigator from libs.file_navigator import DanCourse, navigator
from libs.transition import Transition
from libs.utils import get_current_ms, is_l_don_pressed, is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed 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 from scenes.song_select import SongSelectScreen, State
logger = logging.getLogger(__name__)
class DanSelectScreen(Screen): class DanSelectScreen(Screen):
def on_screen_start(self): def on_screen_start(self):
@@ -23,9 +27,21 @@ class DanSelectScreen(Screen):
self.indicator = Indicator(Indicator.State.SELECT) self.indicator = Indicator(Indicator.State.SELECT)
self.player = DanSelectPlayer(str(global_data.player_num)) self.player = DanSelectPlayer(str(global_data.player_num))
self.state = State.BROWSING self.state = State.BROWSING
self.transition = Transition('', '')
self.last_moved = 0 self.last_moved = 0
def on_screen_end(self, next_screen: str): def on_screen_end(self, next_screen: str):
session_data = global_data.session_data[global_data.player_num-1]
current_item = self.navigator.get_current_item()
if isinstance(current_item, DanCourse):
session_data.selected_song = current_item.charts[0]
session_data.selected_dan = current_item.charts
session_data.selected_dan_exam = current_item.exams
session_data.song_title = current_item.title
session_data.dan_color = current_item.color
else:
self.navigator.in_dan_select = False
self.navigator.go_back()
return super().on_screen_end(next_screen) return super().on_screen_end(next_screen)
def handle_input_browsing(self): def handle_input_browsing(self):
@@ -47,27 +63,36 @@ class DanSelectScreen(Screen):
self.navigator.navigate_right() self.navigator.navigate_right()
self.last_moved = current_time self.last_moved = current_time
elif action == "go_back": elif action == "go_back":
self.navigator.go_back() return action
elif action == "select_song": elif action == "select_song":
pass self.state = State.SONG_SELECTED
def handle_input(self, state, screen): def handle_input(self, state, screen):
"""Main input dispatcher. Delegates to state-specific handlers.""" """Main input dispatcher. Delegates to state-specific handlers."""
if state == State.BROWSING: if state == State.BROWSING:
screen.handle_input_browsing() return screen.handle_input_browsing()
elif state == State.SONG_SELECTED: elif state == State.SONG_SELECTED:
screen.handle_input_selected() res = self.player.handle_input_selected()
if res == 'confirm':
self.transition.start()
elif res == 'cancel':
self.state = State.BROWSING
def update(self): def update(self):
super().update() super().update()
current_time = get_current_ms() current_time = get_current_ms()
self.indicator.update(current_time) self.indicator.update(current_time)
self.timer.update(current_time) self.timer.update(current_time)
self.transition.update(current_time)
if self.transition.is_finished:
return self.on_screen_end("GAME_DAN")
for song in self.navigator.items: for song in self.navigator.items:
song.box.update(False) song.box.update(False)
song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150 song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150
self.player.update(current_time) self.player.update(current_time)
self.handle_input(self.state, self) res = self.handle_input(self.state, self)
if res == 'go_back':
return self.on_screen_end("SONG_SELECT")
def draw(self): def draw(self):
tex.draw_texture('global', 'bg') tex.draw_texture('global', 'bg')
@@ -81,11 +106,14 @@ class DanSelectScreen(Screen):
box.draw(box.position, 95, False) box.draw(box.position, 95, False)
else: else:
box.draw(box.position, 95, False) box.draw(box.position, 95, False)
if self.state == State.SONG_SELECTED:
ray.draw_rectangle(0, 0, 1280, 720, ray.fade(ray.BLACK, min(0.5, self.player.confirmation_window.fade_in.attribute)))
self.player.draw() self.player.draw()
self.indicator.draw(410, 575) self.indicator.draw(410, 575)
self.timer.draw() self.timer.draw()
self.coin_overlay.draw() self.coin_overlay.draw()
tex.draw_texture('global', 'dan_select') tex.draw_texture('global', 'dan_select')
self.transition.draw()
self.allnet_indicator.draw() self.allnet_indicator.draw()
class DanSelectPlayer: class DanSelectPlayer:
@@ -95,10 +123,12 @@ class DanSelectPlayer:
self.prev_diff = -3 self.prev_diff = -3
self.selected_song = False self.selected_song = False
self.is_ura = False self.is_ura = False
self.is_confirmed = False
self.ura_toggle = 0 self.ura_toggle = 0
self.diff_select_move_right = False self.diff_select_move_right = False
self.neiro_selector = None self.neiro_selector = None
self.modifier_selector = None self.modifier_selector = None
self.confirmation_window = ConfirmationWindow()
# Player-specific objects # Player-specific objects
self.chara = Chara2D(int(self.player_num) - 1, 100) self.chara = Chara2D(int(self.player_num) - 1, 100)
@@ -110,6 +140,7 @@ class DanSelectPlayer:
"""Update player state""" """Update player state"""
self.nameplate.update(current_time) self.nameplate.update(current_time)
self.chara.update(current_time, 100, False, False) self.chara.update(current_time, 100, False, False)
self.confirmation_window.update(current_time, self.is_confirmed)
def handle_input_browsing(self, last_moved, selected_item): def handle_input_browsing(self, last_moved, selected_item):
"""Handle input for browsing songs. Returns action string or None.""" """Handle input for browsing songs. Returns action string or None."""
@@ -141,6 +172,7 @@ class DanSelectPlayer:
audio.play_sound('cancel', 'sound') audio.play_sound('cancel', 'sound')
return "go_back" return "go_back"
else: else:
self.confirmation_window.start()
return "select_song" return "select_song"
return None return None
@@ -153,12 +185,24 @@ class DanSelectPlayer:
if state == State.BROWSING: if state == State.BROWSING:
screen.handle_input_browsing() screen.handle_input_browsing()
elif state == State.SONG_SELECTED: elif state == State.SONG_SELECTED:
screen.handle_input_selected() res = screen.handle_input_selected()
def handle_input_selected(self, current_item): if res:
return res
def handle_input_selected(self):
"""Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None""" """Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None"""
if is_l_kat_pressed(self.player_num):
self.is_confirmed = False
if is_r_kat_pressed(self.player_num):
self.is_confirmed = True
if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num): if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num):
return "confirm" if self.is_confirmed:
return "confirm"
else:
self.confirmation_window = ConfirmationWindow()
return "cancel"
return None return None
def draw(self): def draw(self):
@@ -168,3 +212,28 @@ class DanSelectPlayer:
else: else:
self.nameplate.draw(950, 640) self.nameplate.draw(950, 640)
self.chara.draw(mirror=True, x=950, y=410) self.chara.draw(mirror=True, x=950, y=410)
self.confirmation_window.draw()
class ConfirmationWindow:
def __init__(self):
self.fade_in = tex.get_animation(8, is_copy=True)
self.side = 0
def start(self):
self.fade_in.start()
def update(self, current_time_ms: float, is_confirmed: bool):
self.fade_in.update(current_time_ms)
self.side = is_confirmed
def draw(self):
tex.draw_texture('confirm_box', 'bg', fade=self.fade_in.attribute)
tex.draw_texture('confirm_box', 'confirmation_text', fade=self.fade_in.attribute)
for i in range(2):
tex.draw_texture('confirm_box', 'selection_box', index=i, fade=self.fade_in.attribute)
tex.draw_texture('confirm_box', 'selection_box_highlight', index=self.side, fade=self.fade_in.attribute)
tex.draw_texture('confirm_box', 'selection_box_outline', index=self.side, fade=self.fade_in.attribute)
tex.draw_texture('confirm_box', 'yes', fade=self.fade_in.attribute)
tex.draw_texture('confirm_box', 'no', fade=self.fade_in.attribute)

View File

@@ -66,8 +66,8 @@ class GameScreen(Screen):
ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture0"), tex.textures['balloon']['rainbow_mask'].texture) ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture0"), tex.textures['balloon']['rainbow_mask'].texture)
ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture1"), tex.textures['balloon']['rainbow'].texture) ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture1"), tex.textures['balloon']['rainbow'].texture)
session_data = global_data.session_data[global_data.player_num-1] session_data = global_data.session_data[global_data.player_num-1]
self.init_tja(global_data.selected_song) self.init_tja(session_data.selected_song)
logger.info(f"TJA initialized for song: {global_data.selected_song}") logger.info(f"TJA initialized for song: {session_data.selected_song}")
self.load_hitsounds() self.load_hitsounds()
self.song_info = SongInfo(session_data.song_title, session_data.genre_index) self.song_info = SongInfo(session_data.song_title, session_data.genre_index)
self.result_transition = ResultTransition(global_data.player_num) self.result_transition = ResultTransition(global_data.player_num)
@@ -292,53 +292,14 @@ class Player:
def __init__(self, tja: TJAParser, player_number: int, difficulty: int, is_2p: bool, modifiers: Modifiers): def __init__(self, tja: TJAParser, player_number: int, difficulty: int, is_2p: bool, modifiers: Modifiers):
self.is_2p = is_2p self.is_2p = is_2p
self.is_dan = False
self.player_number = str(player_number) self.player_number = str(player_number)
self.difficulty = difficulty self.difficulty = difficulty
self.visual_offset = global_data.config["general"]["visual_offset"] self.visual_offset = global_data.config["general"]["visual_offset"]
self.modifiers = modifiers self.modifiers = modifiers
self.tja = tja
notes, self.branch_m, self.branch_e, self.branch_n = tja.notes_to_position(self.difficulty) self.reset_chart()
self.play_notes, self.draw_note_list, self.draw_bar_list = apply_modifiers(notes, self.modifiers)
self.end_time = 0
if self.play_notes:
self.end_time = self.play_notes[-1].hit_ms
if self.branch_m:
for section in self.branch_m:
if section.play_notes:
self.end_time = max(self.end_time, section.play_notes[-1].hit_ms)
if self.branch_e:
for section in self.branch_e:
if section.play_notes:
self.end_time = max(self.end_time, section.play_notes[-1].hit_ms)
if self.branch_n:
for section in self.branch_n:
if section.play_notes:
self.end_time = max(self.end_time, section.play_notes[-1].hit_ms)
self.don_notes = deque([note for note in self.play_notes if note.type in {1, 3}])
self.kat_notes = deque([note for note in self.play_notes if note.type in {2, 4}])
self.other_notes = deque([note for note in self.play_notes if note.type not in {1, 2, 3, 4}])
self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5])
total_notes = notes
if self.branch_m:
for section in self.branch_m:
self.total_notes += len([note for note in section.play_notes if 0 < note.type < 5])
total_notes += section
self.base_score = calculate_base_score(total_notes)
#Note management
self.current_bars: list[Note] = []
self.current_notes_draw: list[Note | Drumroll | Balloon] = []
self.is_drumroll = False
self.curr_drumroll_count = 0
self.is_balloon = False
self.curr_balloon_count = 0
self.is_branch = False
self.curr_branch_reqs = []
self.branch_condition_count = 0
self.branch_condition = ''
self.balloon_index = 0
self.bpm = self.play_notes[0].bpm if self.play_notes else 120
#Score management #Score management
self.good_count = 0 self.good_count = 0
@@ -382,6 +343,50 @@ class Player:
self.autoplay_hit_side = 'L' self.autoplay_hit_side = 'L'
self.last_subdivision = -1 self.last_subdivision = -1
def reset_chart(self):
notes, self.branch_m, self.branch_e, self.branch_n = self.tja.notes_to_position(self.difficulty)
self.play_notes, self.draw_note_list, self.draw_bar_list = apply_modifiers(notes, self.modifiers)
self.end_time = 0
if self.play_notes:
self.end_time = self.play_notes[-1].hit_ms
if self.branch_m:
for section in self.branch_m:
if section.play_notes:
self.end_time = max(self.end_time, section.play_notes[-1].hit_ms)
if self.branch_e:
for section in self.branch_e:
if section.play_notes:
self.end_time = max(self.end_time, section.play_notes[-1].hit_ms)
if self.branch_n:
for section in self.branch_n:
if section.play_notes:
self.end_time = max(self.end_time, section.play_notes[-1].hit_ms)
self.don_notes = deque([note for note in self.play_notes if note.type in {1, 3}])
self.kat_notes = deque([note for note in self.play_notes if note.type in {2, 4}])
self.other_notes = deque([note for note in self.play_notes if note.type not in {1, 2, 3, 4}])
self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5])
total_notes = notes
if self.branch_m:
for section in self.branch_m:
self.total_notes += len([note for note in section.play_notes if 0 < note.type < 5])
total_notes += section
self.base_score = calculate_base_score(total_notes)
#Note management
self.current_bars: list[Note] = []
self.current_notes_draw: list[Note | Drumroll | Balloon] = []
self.is_drumroll = False
self.curr_drumroll_count = 0
self.is_balloon = False
self.curr_balloon_count = 0
self.is_branch = False
self.curr_branch_reqs = []
self.branch_condition_count = 0
self.branch_condition = ''
self.balloon_index = 0
self.bpm = self.play_notes[0].bpm if self.play_notes else 120
def merge_branch_section(self, branch_section: NoteList, current_ms: float): def merge_branch_section(self, branch_section: NoteList, current_ms: float):
"""Merges the branch notes into the current notes""" """Merges the branch notes into the current notes"""
self.play_notes.extend(branch_section.play_notes) self.play_notes.extend(branch_section.play_notes)
@@ -1043,7 +1048,7 @@ class Player:
for modifier in modifiers_to_draw: for modifier in modifiers_to_draw:
tex.draw_texture('lane', modifier, index=self.is_2p) tex.draw_texture('lane', modifier, index=self.is_2p)
def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader): def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader, dan_transition = None):
# Group 1: Background and lane elements # Group 1: Background and lane elements
tex.draw_texture('lane', 'lane_background', index=self.is_2p) tex.draw_texture('lane', 'lane_background', index=self.is_2p)
if self.branch_indicator is not None: if self.branch_indicator is not None:
@@ -1062,9 +1067,13 @@ class Player:
# Group 3: Notes and bars (game content) # Group 3: Notes and bars (game content)
self.draw_bars(ms_from_start) self.draw_bars(ms_from_start)
self.draw_notes(ms_from_start, start_ms) self.draw_notes(ms_from_start, start_ms)
if dan_transition is not None:
dan_transition.draw()
# Group 4: Lane covers and UI elements (batch similar textures) # Group 4: Lane covers and UI elements (batch similar textures)
tex.draw_texture('lane', f'{self.player_number}p_lane_cover', index=self.is_2p) tex.draw_texture('lane', f'{self.player_number}p_lane_cover', index=self.is_2p)
if self.is_dan:
tex.draw_texture('lane', 'dan_lane_cover')
tex.draw_texture('lane', 'drum', index=self.is_2p) tex.draw_texture('lane', 'drum', index=self.is_2p)
if self.ending_anim is not None: if self.ending_anim is not None:
self.ending_anim.draw() self.ending_anim.draw()
@@ -1085,7 +1094,10 @@ class Player:
else: else:
tex.draw_texture('lane', 'lane_score_cover', index=self.is_2p) tex.draw_texture('lane', 'lane_score_cover', index=self.is_2p)
tex.draw_texture('lane', f'{self.player_number}p_icon', index=self.is_2p) tex.draw_texture('lane', f'{self.player_number}p_icon', index=self.is_2p)
tex.draw_texture('lane', 'lane_difficulty', frame=self.difficulty, index=self.is_2p) if self.is_dan:
tex.draw_texture('lane', 'lane_difficulty', frame=6)
else:
tex.draw_texture('lane', 'lane_difficulty', frame=self.difficulty, index=self.is_2p)
if self.judge_counter is not None: if self.judge_counter is not None:
self.judge_counter.draw() self.judge_counter.draw()

View File

@@ -1,28 +1,105 @@
from typing import override
import pyray as ray
import logging import logging
from libs.animation import Animation
from libs.audio import audio from libs.audio import audio
from libs.background import Background from libs.background import Background
from libs.file_navigator import Exam
from libs.global_data import global_data from libs.global_data import global_data
from libs.global_objects import AllNetIcon
from libs.tja import TJAParser
from libs.transition import Transition from libs.transition import Transition
from scenes.game import GameScreen, SongInfo from libs.utils import OutlinedText, get_current_ms
from libs.texture import tex
from scenes.game import GameScreen, ResultTransition, SongInfo
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
class DanGameScreen(GameScreen): class DanGameScreen(GameScreen):
JUDGE_X = 414 JUDGE_X = 414
@override
def on_screen_start(self): def on_screen_start(self):
super().on_screen_start() self.mask_shader = ray.load_shader("shader/outline.vs", "shader/mask.fs")
self.init_tja(global_data.selected_song) self.current_ms = 0
logger.info(f"TJA initialized for song: {global_data.selected_song}") self.end_ms = 0
self.song_info = SongInfo(session_data.song_title, session_data.genre_index) self.start_delay = 4000
self.song_started = False
self.song_music = None
self.song_index = 0
tex.unload_textures()
tex.load_screen_textures('game')
audio.load_screen_sounds('game')
if global_data.config["general"]["nijiiro_notes"]:
# drop original
if "notes" in tex.textures:
del tex.textures["notes"]
# load nijiiro, rename "notes"
# to leave hardcoded 'notes' in calls below
tex.load_zip("game", "notes_nijiiro")
tex.textures["notes"] = tex.textures.pop("notes_nijiiro")
logger.info("Loaded nijiiro notes textures")
ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture0"), tex.textures['balloon']['rainbow_mask'].texture)
ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture1"), tex.textures['balloon']['rainbow'].texture)
session_data = global_data.session_data[global_data.player_num-1]
songs = session_data.selected_dan
self.exams = session_data.selected_dan_exam
self.total_notes = 0
for song, genre_index, difficulty in 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)
song, genre_index, difficulty = songs[self.song_index]
session_data.selected_difficulty = difficulty
self.hori_name = OutlinedText(session_data.song_title, 40, ray.WHITE)
self.init_tja(song.file_path)
self.color = session_data.dan_color
self.player_1.is_dan = True
self.player_1.gauge = DanGauge(str(global_data.player_num), self.total_notes)
logger.info(f"TJA initialized for song: {song.file_path}")
self.load_hitsounds()
self.song_info = SongInfo(song.metadata.title.get(global_data.config["general"]["language"], "en"), genre_index)
self.result_transition = ResultTransition(4)
self.bpm = self.tja.metadata.bpm
self.background = Background(global_data.player_num, self.bpm, scene_preset='DAN') self.background = Background(global_data.player_num, self.bpm, scene_preset='DAN')
self.transition = Transition('', '', is_second=True) self.transition = Transition('', '', is_second=True)
self.transition.start() self.transition.start()
self.dan_transition = DanTransition()
self.dan_transition.start()
self.allnet_indicator = AllNetIcon()
def change_song(self):
session_data = global_data.session_data[global_data.player_num-1]
songs = session_data.selected_dan
song, genre_index, difficulty = songs[self.song_index]
session_data.selected_difficulty = difficulty
self.player_1.difficulty = difficulty
self.tja = TJAParser(song.file_path, start_delay=self.start_delay, distance=SCREEN_WIDTH - GameScreen.JUDGE_X)
audio.unload_music_stream(self.song_music)
self.song_music = None
self.song_started = False
if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None:
self.song_music = audio.load_music_stream(self.tja.metadata.wave, 'song')
self.player_1.tja = self.tja
self.player_1.reset_chart()
self.dan_transition.start()
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)
def update(self): def update(self):
super().update() super(GameScreen, self).update()
current_time = get_current_ms() current_time = get_current_ms()
self.transition.update(current_time) self.transition.update(current_time)
self.current_ms = current_time - self.start_ms self.current_ms = current_time - self.start_ms
self.dan_transition.update(current_time)
self.start_song(current_time) self.start_song(current_time)
self.update_background(current_time) self.update_background(current_time)
@@ -31,4 +108,218 @@ class DanGameScreen(GameScreen):
self.player_1.update(self.current_ms, current_time, self.background) self.player_1.update(self.current_ms, current_time, self.background)
self.song_info.update(current_time) self.song_info.update(current_time)
self.result_transition.update(current_time)
if self.result_transition.is_finished and not audio.is_sound_playing('result_transition'):
logger.info("Result transition finished, moving to RESULT screen")
return self.on_screen_end('RESULT')
elif self.current_ms >= self.player_1.end_time + 1000:
session_data = global_data.session_data[global_data.player_num-1]
if self.song_index == len(session_data.selected_dan) - 1:
if self.end_ms != 0:
if current_time >= self.end_ms + 1000:
if self.player_1.ending_anim is None:
self.spawn_ending_anims()
if current_time >= self.end_ms + 8533.34:
if not self.result_transition.is_started:
self.result_transition.start()
audio.play_sound('result_transition', 'voice')
logger.info("Result transition started and voice played")
else:
self.end_ms = current_time
else:
self.song_index += 1
self.dan_transition.start()
self.change_song()
return self.global_keys() return self.global_keys()
def draw_dan_info(self):
tex.draw_texture('dan_info', 'total_notes')
counter = str(self.total_notes - self.player_1.good_count - self.player_1.ok_count - self.player_1.bad_count)
self._draw_counter(counter, margin=45, texture='total_notes_counter')
for i, exam in enumerate(self.exams):
y_offset = i * 94
tex.draw_texture('dan_info', 'exam_bg', y=y_offset)
tex.draw_texture('dan_info', 'exam_overlay_1', y=y_offset)
# Get progress based on exam type
progress = self._get_exam_progress(exam) / exam.red
if exam.range == 'less':
progress = 1 - progress
self._draw_progress_bar(progress, y_offset)
# Draw exam type and counter
counter = str(exam.red)
self._draw_counter(counter, margin=22, texture='value_counter', index=0, y=y_offset)
tex.draw_texture('dan_info', f'exam_{exam.type}', y=y_offset, x=-len(counter)*20)
if exam.range == 'less':
tex.draw_texture('dan_info', 'exam_less', y=y_offset)
elif exam.range == 'more':
tex.draw_texture('dan_info', 'exam_more', y=y_offset)
tex.draw_texture('dan_info', 'exam_overlay_2', y=y_offset)
if exam.range == 'less':
counter = str(max(0, exam.red - self._get_exam_progress(exam)))
elif exam.range == 'more':
counter = str(max(0, self._get_exam_progress(exam)))
self._draw_counter(counter, margin=22, texture='value_counter', index=1, y=y_offset)
if exam.type == 'gauge':
tex.draw_texture('dan_info', 'exam_percent', y=y_offset, index=1)
tex.draw_texture('dan_info', 'frame', frame=self.color)
if self.hori_name is not None:
self.hori_name.draw(outline_color=ray.BLACK, x=154 - (self.hori_name.texture.width//2),
y=392, x2=min(self.hori_name.texture.width, 275)-self.hori_name.texture.width)
def _draw_counter(self, counter, margin, texture, index=None, y=0):
"""Helper to draw digit counters"""
for j in range(len(counter)):
kwargs = {'frame': int(counter[j]), 'x': -(len(counter) - j) * margin, 'y': y}
if index is not None:
kwargs['index'] = index
tex.draw_texture('dan_info', texture, **kwargs)
def _get_exam_progress(self, exam: Exam) -> int:
"""Get progress value based on exam type"""
type_mapping = {
'gauge': (self.player_1.gauge.gauge_length / self.player_1.gauge.gauge_max) * 100,
'judgeperfect': self.player_1.good_count,
'judgegood': self.player_1.ok_count,
'judgebad': self.player_1.bad_count,
'score': self.player_1.score,
'combo': self.player_1.max_combo
}
return int(type_mapping.get(exam.type, 0))
def _draw_progress_bar(self, progress, y_offset):
"""Draw the progress bar with appropriate color"""
progress = max(0, progress) # Clamp to 0 minimum
progress = min(progress, 1) # Clamp to 1 maximum
if progress == 1:
texture = 'exam_max'
elif progress >= 0.5:
texture = 'exam_gold'
else:
texture = 'exam_red'
tex.draw_texture('dan_info', texture, x2=940*progress, y=y_offset)
@override
def draw(self):
self.background.draw()
self.draw_dan_info()
self.player_1.draw(self.current_ms, self.start_ms, self.mask_shader, dan_transition=self.dan_transition)
self.draw_overlay()
class DanTransition:
def __init__(self):
self.move = tex.get_animation(26)
self.is_finished = False
def start(self):
self.move.start()
self.is_finished = False
def update(self, current_time):
self.move.update(current_time)
self.is_finished = self.move.is_finished
def draw(self):
tex.draw_texture('dan', 'transition', index=0, x=self.move.attribute, mirror='horizontal')
tex.draw_texture('dan', 'transition', index=1, x=-self.move.attribute)
class DanGauge:
"""The player's gauge"""
def __init__(self, player_num: str, total_notes: int):
self.player_num = player_num
self.string_diff = "_hard"
self.gauge_length = 0
self.previous_length = 0
self.visual_length = 0
self.total_notes = total_notes
self.gauge_max = 89
self.tamashii_fire_change = tex.get_animation(25)
self.is_clear = False
self.is_rainbow = False
self.gauge_update_anim = tex.get_animation(10)
self.rainbow_fade_in = None
self.rainbow_animation = None
def add_good(self):
"""Adds a good note to the gauge"""
self.gauge_update_anim.start()
self.previous_length = int(self.gauge_length)
self.gauge_length += (1 / (self.total_notes * (self.gauge_max / 100))) * 100
if self.gauge_length > self.gauge_max:
self.gauge_length = self.gauge_max
if int(self.gauge_length * 8) % 8 == 0:
self.visual_length = int(self.gauge_length * 8)
def add_ok(self):
"""Adds an ok note to the gauge"""
self.gauge_update_anim.start()
self.previous_length = int(self.gauge_length)
self.gauge_length += (0.5 / (self.total_notes * (self.gauge_max / 100))) * 100
if self.gauge_length > self.gauge_max:
self.gauge_length = self.gauge_max
if int(self.gauge_length * 8) % 8 == 0:
self.visual_length = int(self.gauge_length * 8)
def add_bad(self):
"""Adds a bad note to the gauge"""
self.previous_length = int(self.gauge_length)
self.gauge_length -= (2 / (self.total_notes * (self.gauge_max / 100))) * 100
if self.gauge_length < 0:
self.gauge_length = 0
if int(self.gauge_length * 8) % 8 == 0:
self.visual_length = int(self.gauge_length * 8)
def update(self, current_ms: float):
self.is_rainbow = self.gauge_length == self.gauge_max
self.is_clear = self.is_rainbow
if self.gauge_length == self.gauge_max and self.rainbow_fade_in is None:
self.rainbow_fade_in = Animation.create_fade(450, initial_opacity=0.0, final_opacity=1.0)
self.rainbow_fade_in.start()
self.gauge_update_anim.update(current_ms)
self.tamashii_fire_change.update(current_ms)
if self.rainbow_fade_in is not None:
self.rainbow_fade_in.update(current_ms)
if self.rainbow_animation is None:
self.rainbow_animation = Animation.create_texture_change((16.67*8) * 3, textures=[((16.67 * 3) * i, (16.67 * 3) * (i + 1), i) for i in range(8)])
self.rainbow_animation.start()
else:
self.rainbow_animation.update(current_ms)
if self.rainbow_animation.is_finished or self.gauge_length < 87:
self.rainbow_animation = None
def draw(self):
tex.draw_texture('gauge_dan', 'border')
tex.draw_texture('gauge_dan', f'{self.player_num}p_unfilled')
tex.draw_texture('gauge_dan', f'{self.player_num}p_bar', x2=self.visual_length-8)
# Rainbow effect for full gauge
if self.gauge_length == self.gauge_max and self.rainbow_fade_in is not None and self.rainbow_animation is not None:
if 0 < self.rainbow_animation.attribute < 8:
tex.draw_texture('gauge_dan', 'rainbow', frame=self.rainbow_animation.attribute-1, fade=self.rainbow_fade_in.attribute)
tex.draw_texture('gauge_dan', 'rainbow', frame=self.rainbow_animation.attribute, fade=self.rainbow_fade_in.attribute)
if self.gauge_update_anim is not None and self.visual_length <= self.gauge_max and self.visual_length > self.previous_length:
tex.draw_texture('gauge_dan', f'{self.player_num}p_bar_fade', x=self.visual_length-8, fade=self.gauge_update_anim.attribute)
tex.draw_texture('gauge_dan', 'overlay', fade=0.15)
# Draw clear status indicators
if self.is_rainbow:
tex.draw_texture('gauge_dan', 'tamashii_fire', scale=0.75, center=True, frame=self.tamashii_fire_change.attribute)
tex.draw_texture('gauge_dan', 'tamashii')
if self.is_rainbow and self.tamashii_fire_change.attribute in (0, 1, 4, 5):
tex.draw_texture('gauge_dan', 'tamashii_overlay', fade=0.5)
else:
tex.draw_texture('gauge_dan', 'tamashii_dark')

View File

@@ -66,21 +66,22 @@ class SongSelectScreen(Screen):
self.ura_switch_animation = UraSwitchAnimation() self.ura_switch_animation = UraSwitchAnimation()
self.dan_transition = DanTransition() self.dan_transition = DanTransition()
session_data = global_data.session_data[global_data.player_num-1]
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)
if self.navigator.items == []: if self.navigator.items == []:
logger.warning("No navigator items found, returning to ENTRY screen") logger.warning("No navigator items found, returning to ENTRY screen")
return self.on_screen_end("ENTRY") return self.on_screen_end("ENTRY")
if str(global_data.selected_song) in self.navigator.all_song_files: if str(session_data.selected_song) in self.navigator.all_song_files:
self.navigator.mark_crowns_dirty_for_song(self.navigator.all_song_files[str(global_data.selected_song)]) self.navigator.mark_crowns_dirty_for_song(self.navigator.all_song_files[str(session_data.selected_song)])
curr_item = self.navigator.get_current_item() curr_item = self.navigator.get_current_item()
curr_item.box.get_scores() curr_item.box.get_scores()
self.navigator.add_recent() self.navigator.add_recent()
def finalize_song(self): def finalize_song(self):
global_data.selected_song = self.navigator.get_current_item().path global_data.session_data[global_data.player_num-1].selected_song = self.navigator.get_current_item().path
global_data.session_data[global_data.player_num-1].selected_difficulty = self.player_1.selected_difficulty global_data.session_data[global_data.player_num-1].selected_difficulty = self.player_1.selected_difficulty
global_data.session_data[global_data.player_num-1].genre_index = self.navigator.get_current_item().box.name_texture_index global_data.session_data[global_data.player_num-1].genre_index = self.navigator.get_current_item().box.name_texture_index

View File

@@ -14,9 +14,9 @@ class TwoPlayerSongSelectScreen(SongSelectScreen):
self.player_2 = SongSelectPlayer('2', self.text_fade_in) self.player_2 = SongSelectPlayer('2', self.text_fade_in)
def finalize_song(self): def finalize_song(self):
global_data.selected_song = self.navigator.get_current_item().path global_data.session_data[0].selected_song = self.navigator.get_current_item().path
global_data.session_data[0].genre_index = self.navigator.get_current_item().box.name_texture_index global_data.session_data[0].genre_index = self.navigator.get_current_item().box.name_texture_index
logger.info(f"Finalized song selection: {global_data.selected_song}") logger.info(f"Finalized song selection: {global_data.session_data[0].selected_song}")
def handle_input_browsing(self): def handle_input_browsing(self):
"""Handle input for browsing songs.""" """Handle input for browsing songs."""