cleaning up

This commit is contained in:
Anthony Samms
2025-12-02 15:23:23 -05:00
parent d543a594af
commit f5896c7d63
6 changed files with 442 additions and 1322 deletions

View File

@@ -1,4 +1,3 @@
from scenes.game2 import GameScreen2
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
@@ -273,7 +272,7 @@ def main():
song_select_screen = SongSelectScreen('song_select') song_select_screen = SongSelectScreen('song_select')
song_select_screen_2p = TwoPlayerSongSelectScreen('song_select') song_select_screen_2p = TwoPlayerSongSelectScreen('song_select')
load_screen = LoadScreen('loading') load_screen = LoadScreen('loading')
game_screen = GameScreen2('game') game_screen = GameScreen('game')
game_screen_2p = TwoPlayerGameScreen('game') game_screen_2p = TwoPlayerGameScreen('game')
game_screen_practice = PracticeGameScreen('game') game_screen_practice = PracticeGameScreen('game')
practice_select_screen = PracticeSongSelectScreen('song_select') practice_select_screen = PracticeSongSelectScreen('song_select')

File diff suppressed because it is too large Load Diff

View File

@@ -1,287 +0,0 @@
import bisect
import hashlib
from dataclasses import dataclass, field, fields
from typing_extensions import Optional
from libs.tja import NoteList, ScrollType, TJAParser, TimelineObject, get_ms_per_measure
@dataclass()
class Note:
type: int = field(init=False)
hit_ms: float = field(init=False)
bpm: float = field(init=False)
scroll_x: float = field(init=False)
scroll_y: float = field(init=False)
display: bool = field(init=False)
index: int = field(init=False)
moji: int = field(init=False)
def __lt__(self, other):
return self.hit_ms < other.hit_ms
def __le__(self, other):
return self.hit_ms <= other.hit_ms
def __gt__(self, other):
return self.hit_ms > other.hit_ms
def __ge__(self, other):
return self.hit_ms >= other.hit_ms
def __eq__(self, other):
return self.hit_ms == other.hit_ms
def _get_hash_data(self) -> bytes:
hash_fields = ['type', 'hit_ms']
field_values = []
for field_name in sorted(hash_fields):
value = getattr(self, field_name, None)
field_values.append((field_name, value))
field_values.append(('__class__', self.__class__.__name__))
hash_string = str(field_values)
return hash_string.encode('utf-8')
def get_hash(self, algorithm='sha256') -> str:
"""Generate hash of the note"""
hash_obj = hashlib.new(algorithm)
hash_obj.update(self._get_hash_data())
return hash_obj.hexdigest()
def __hash__(self) -> int:
"""Make instances hashable for use in sets/dicts"""
return int(self.get_hash('md5')[:8], 16) # Use first 8 chars of MD5 as int
def __repr__(self):
return str(self.__dict__)
@dataclass
class Drumroll(Note):
"""A drumroll note in a TJA file.
Attributes:
_source_note (Note): The source note.
color (int): The color of the drumroll. (0-255 where 255 is red)
"""
_source_note: Note
color: int = field(init=False)
def __repr__(self):
return str(self.__dict__)
def __eq__(self, other):
return self.hit_ms == other.hit_ms
def __post_init__(self):
for field_name in [f.name for f in fields(Note)]:
if hasattr(self._source_note, field_name):
setattr(self, field_name, getattr(self._source_note, field_name))
@dataclass
class Balloon(Note):
"""A balloon note in a TJA file.
Attributes:
_source_note (Note): The source note.
count (int): The number of hits it takes to pop.
popped (bool): Whether the balloon has been popped.
is_kusudama (bool): Whether the balloon is a kusudama.
"""
_source_note: Note
count: int = field(init=False)
popped: bool = False
is_kusudama: bool = False
def __repr__(self):
return str(self.__dict__)
def __eq__(self, other):
return self.hit_ms == other.hit_ms
def __post_init__(self):
for field_name in [f.name for f in fields(Note)]:
if hasattr(self._source_note, field_name):
setattr(self, field_name, getattr(self._source_note, field_name))
def _get_hash_data(self) -> bytes:
"""Override to include source note and balloon-specific data"""
hash_fields = ['type', 'hit_ms', 'load_ms', 'count']
field_values = []
for field_name in sorted(hash_fields):
value = getattr(self, field_name, None)
field_values.append((field_name, value))
field_values.append(('__class__', self.__class__.__name__))
hash_string = str(field_values)
return hash_string.encode('utf-8')
@dataclass
class ParserState:
time_signature: float = 4/4
bpm: float = 120
bpmchange_last_bpm: float = 120
scroll_x_modifier: float = 1
scroll_y_modifier: float = 0
scroll_type: ScrollType = ScrollType.NMSCROLL
barline_display: bool = True
curr_note_list: list[Note | Drumroll | Balloon] = []
curr_draw_list: list[Note | Drumroll | Balloon] = []
curr_bar_list: list[Note] = []
curr_timeline: list[TimelineObject] = []
index: int = 0
balloons: list[int] = []
balloon_index: int = 0
prev_note: Optional[Note] = None
barline_added: bool = False
class TJAParser2(TJAParser):
def _build_command_registry(self):
"""Auto-discover command handlers based on naming convention."""
registry = {}
for name in dir(self):
if name.startswith('handle_'):
cmd_name = '#' + name[7:].upper()
registry[cmd_name] = getattr(self, name)
return registry
def handle_measure(self, part: str, state: ParserState):
numerator, denominator = part.split('/')
state.time_signature = float(numerator) / float(denominator)
def handle_scroll(self, part: str, state: ParserState):
if 'i' in part:
normalized = part.replace('.i', 'j').replace('i', 'j')
normalized = normalized.replace(',', '')
c = complex(normalized)
state.scroll_x_modifier = c.real
state.scroll_y_modifier = c.imag
else:
state.scroll_x_modifier = float(part)
state.scroll_y_modifier = 0.0
def handle_bpmchange(self, part: str, state: ParserState):
parsed_bpm = float(part)
if state.scroll_type == ScrollType.BMSCROLL or state.scroll_type == ScrollType.HBSCROLL:
# Do not modify bpm, it needs to be changed live by bpmchange
bpmchange = parsed_bpm / state.bpmchange_last_bpm
state.bpmchange_last_bpm = parsed_bpm
bpmchange_timeline = TimelineObject()
bpmchange_timeline.hit_ms = self.current_ms
bpmchange_timeline.bpmchange = bpmchange
state.curr_timeline.append(bpmchange_timeline)
else:
timeline_obj = TimelineObject()
timeline_obj.hit_ms = self.current_ms
timeline_obj.bpm = parsed_bpm
state.bpm = parsed_bpm
state.curr_timeline.append(timeline_obj)
def add_bar(self, state: ParserState):
bar_line = Note()
bar_line.hit_ms = self.current_ms
bar_line.type = 0
bar_line.display = state.barline_display
bar_line.bpm = state.bpm
bar_line.scroll_x = state.scroll_x_modifier
bar_line.scroll_y = state.scroll_y_modifier
if state.barline_added:
bar_line.display = False
return bar_line
def add_note(self, item: str, state: ParserState):
note = Note()
note.hit_ms = self.current_ms
note.display = True
note.type = int(item)
note.index = state.index
note.bpm = state.bpm
note.scroll_x = state.scroll_x_modifier
note.scroll_y = state.scroll_y_modifier
if item in {'5', '6'}:
note = Drumroll(note)
note.color = 255
elif item in {'7', '9'}:
state.balloon_index += 1
if state.balloons is None:
raise Exception("Balloon note found, but no count was specified")
if item == '9':
note = Balloon(note, is_kusudama=True)
else:
note = Balloon(note)
note.count = 1 if not state.balloons else state.balloons.pop(0)
elif item == '8':
if state.prev_note is None:
raise ValueError("No previous note found")
return note
def notes_to_position(self, diff: int):
"""Parse a TJA's notes into a NoteList."""
commands = self._build_command_registry()
master_notes = NoteList()
notes = self.data_to_notes(diff)
state = ParserState()
state.bpm = self.metadata.bpm
state.bpmchange_last_bpm = self.metadata.bpm
state.balloons = self.metadata.course_data[diff].balloon.copy()
state.curr_note_list = master_notes.play_notes
state.curr_draw_list = master_notes.draw_notes
state.curr_bar_list = master_notes.bars
state.curr_timeline = master_notes.timeline
init_bpm = TimelineObject()
init_bpm.hit_ms = self.current_ms
init_bpm.bpm = state.bpm
state.curr_timeline.append(init_bpm)
for bar in notes:
bar_length = sum(len(part) for part in bar if '#' not in part)
state.barline_added = False
for part in bar:
if part.startswith('#'):
for cmd_prefix, handler in commands.items():
if part.startswith(cmd_prefix):
value = part[len(cmd_prefix):].strip()
handler(value, state)
break
continue
elif len(part) > 0 and not part[0].isdigit():
continue
ms_per_measure = get_ms_per_measure(state.bpm, state.time_signature)
bar = self.add_bar(state)
state.curr_bar_list.append(bar)
state.barline_added = True
if len(part) == 0:
self.current_ms += ms_per_measure
increment = 0
else:
increment = ms_per_measure / bar_length
for item in part:
if item == '0' or (not item.isdigit()):
self.current_ms += increment
continue
note = self.add_note(item, state)
self.current_ms += increment
state.curr_note_list.append(note)
state.curr_draw_list.append(note)
self.get_moji(state.curr_note_list, ms_per_measure)
state.index += 1
state.prev_note = note
return master_notes, [master_notes], [master_notes], [master_notes]

View File

@@ -135,7 +135,7 @@ class GameScreen(Screen):
def init_tja(self, song: Path): def init_tja(self, song: Path):
"""Initialize the TJA file""" """Initialize the TJA file"""
self.tja = TJAParser(song, start_delay=self.start_delay, distance=tex.screen_width - GameScreen.JUDGE_X) self.tja = TJAParser(song, start_delay=self.start_delay)
if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists(): if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists():
self.movie = VideoPlayer(self.tja.metadata.bgmovie) self.movie = VideoPlayer(self.tja.metadata.bgmovie)
self.movie.set_volume(0.0) self.movie.set_volume(0.0)
@@ -460,8 +460,8 @@ class Player:
self.draw_note_list.extend(branch_section.draw_notes) self.draw_note_list.extend(branch_section.draw_notes)
self.draw_bar_list.extend(branch_section.bars) self.draw_bar_list.extend(branch_section.bars)
self.play_notes = deque(sorted(self.play_notes)) self.play_notes = deque(sorted(self.play_notes))
self.draw_note_list = deque(sorted(self.draw_note_list, key=lambda x: x.load_ms)) self.draw_note_list = deque(sorted(self.draw_note_list, key=lambda x: x.hit_ms))
self.draw_bar_list = deque(sorted(self.draw_bar_list, key=lambda x: x.load_ms)) self.draw_bar_list = deque(sorted(self.draw_bar_list, key=lambda x: x.hit_ms))
total_don = [note for note in self.play_notes if note.type in {NoteType.DON, NoteType.DON_L}] total_don = [note for note in self.play_notes if note.type in {NoteType.DON, NoteType.DON_L}]
total_kat = [note for note in self.play_notes if note.type in {NoteType.KAT, NoteType.KAT_L}] total_kat = [note for note in self.play_notes if note.type in {NoteType.KAT, NoteType.KAT_L}]
total_other = [note for note in self.play_notes if note.type not in {NoteType.DON, NoteType.DON_L, NoteType.KAT, NoteType.KAT_L}] total_other = [note for note in self.play_notes if note.type not in {NoteType.DON, NoteType.DON_L, NoteType.KAT, NoteType.KAT_L}]
@@ -474,24 +474,16 @@ class Player:
"""Returns the score, good count, ok count, bad count, max combo, and total drumroll""" """Returns the score, good count, ok count, bad count, max combo, and total drumroll"""
return self.score, self.good_count, self.ok_count, self.bad_count, self.max_combo, self.total_drumroll return self.score, self.good_count, self.ok_count, self.bad_count, self.max_combo, self.total_drumroll
def get_position_x(self, width: int, current_ms: float, load_ms: float, pixels_per_frame: float) -> int: def get_position_x(self, note, current_ms):
"""Calculates the x-coordinate of a note based on its load time and current time""" speedx = note.bpm / 240000 * note.scroll_x * (tex.screen_width - GameScreen.JUDGE_X) * tex.screen_scale
# Override if delay active return GameScreen.JUDGE_X + (note.hit_ms - current_ms) * speedx
if self.delay_start:
current_ms = self.delay_start
time_diff = load_ms - current_ms
return int(width + pixels_per_frame * 0.06 * time_diff - (tex.textures["notes"]["1"].width//2)) - self.visual_offset
def get_position_y(self, current_ms: float, load_ms: float, pixels_per_frame: float, pixels_per_frame_x) -> int:
"""Calculates the y-coordinate of a note based on its load time and current time"""
# Override if delay active
if self.delay_start:
current_ms = self.delay_start
time_diff = load_ms - current_ms
if pixels_per_frame_x == 0:
return int(pixels_per_frame * 0.06 * time_diff)
return int((pixels_per_frame * 0.06 * time_diff) + ((self.tja.distance * pixels_per_frame) / pixels_per_frame_x))
def get_position_y(self, note, current_ms):
speedy = note.bpm / 240000 * note.scroll_y * (tex.screen_width - GameScreen.JUDGE_Y) * tex.screen_scale
return (note.hit_ms - current_ms) * speedy
'''
def handle_tjap3_extended_commands(self, current_ms: float): def handle_tjap3_extended_commands(self, current_ms: float):
if not self.timeline or self.timeline_index >= len(self.timeline): if not self.timeline or self.timeline_index >= len(self.timeline):
return return
@@ -531,6 +523,7 @@ class Player:
if should_advance: if should_advance:
self.timeline_index += 1 self.timeline_index += 1
'''
def get_judge_position(self, current_ms: float): def get_judge_position(self, current_ms: float):
"""Get the current judgment circle position based on bar data with on-demand interpolation""" """Get the current judgment circle position based on bar data with on-demand interpolation"""
@@ -539,7 +532,7 @@ class Player:
timeline_object = self.timeline[self.timeline_index] timeline_object = self.timeline[self.timeline_index]
if hasattr(timeline_object, 'delta_x'): if hasattr(timeline_object, 'judge_pos_x'):
if timeline_object.load_ms <= current_ms <= timeline_object.hit_ms: if timeline_object.load_ms <= current_ms <= timeline_object.hit_ms:
duration = timeline_object.hit_ms - timeline_object.load_ms duration = timeline_object.hit_ms - timeline_object.load_ms
if duration > 0: if duration > 0:
@@ -567,6 +560,7 @@ class Player:
timeline_object = self.timeline[self.timeline_index] timeline_object = self.timeline[self.timeline_index]
should_advance = False should_advance = False
'''
if hasattr(timeline_object, 'bpmchange') and timeline_object.hit_ms <= current_ms: if hasattr(timeline_object, 'bpmchange') and timeline_object.hit_ms <= current_ms:
hit_ms = timeline_object.hit_ms hit_ms = timeline_object.hit_ms
bpmchange = timeline_object.bpmchange bpmchange = timeline_object.bpmchange
@@ -584,6 +578,7 @@ class Player:
note.pixels_per_frame_y *= bpmchange note.pixels_per_frame_y *= bpmchange
self.bpm *= bpmchange self.bpm *= bpmchange
should_advance = True should_advance = True
'''
if hasattr(timeline_object, 'delay') and timeline_object.hit_ms <= current_ms: if hasattr(timeline_object, 'delay') and timeline_object.hit_ms <= current_ms:
hit_ms = timeline_object.hit_ms hit_ms = timeline_object.hit_ms
@@ -624,7 +619,7 @@ class Player:
"""Manages the bars and removes if necessary """Manages the bars and removes if necessary
Also sets branch conditions""" Also sets branch conditions"""
#Add bar to current_bars list if it is ready to be shown on screen #Add bar to current_bars list if it is ready to be shown on screen
if self.draw_bar_list and current_ms > self.draw_bar_list[0].load_ms: if self.draw_bar_list and current_ms >= self.draw_bar_list[0].hit_ms - 10000:
self.current_bars.append(self.draw_bar_list.popleft()) self.current_bars.append(self.draw_bar_list.popleft())
#If a bar is off screen, remove it #If a bar is off screen, remove it
@@ -632,10 +627,10 @@ class Player:
return return
# More efficient removal with early exit # More efficient removal with early exit
removal_threshold = GameScreen.JUDGE_X + (650 * tex.screen_scale) removal_threshold = GameScreen.JUDGE_X - (650 * tex.screen_scale)
bars_to_keep = [] bars_to_keep = []
for bar in self.current_bars: for bar in self.current_bars:
position = self.get_position_x(tex.screen_width, current_ms, bar.hit_ms, bar.pixels_per_frame_x) position = self.get_position_x(bar, current_ms)
if position >= removal_threshold: if position >= removal_threshold:
bars_to_keep.append(bar) bars_to_keep.append(bar)
self.current_bars = bars_to_keep self.current_bars = bars_to_keep
@@ -647,6 +642,7 @@ class Player:
logger.info(f'branch condition measures started with conditions {self.branch_condition}, {e_req}, {m_req}, {self.current_bars[-1].hit_ms}') logger.info(f'branch condition measures started with conditions {self.branch_condition}, {e_req}, {m_req}, {self.current_bars[-1].hit_ms}')
if not self.is_branch: if not self.is_branch:
self.is_branch = True self.is_branch = True
'''
if self.branch_condition == 'r': if self.branch_condition == 'r':
end_time = self.branch_m[0].bars[0].load_ms end_time = self.branch_m[0].bars[0].load_ms
end_roll = -1 end_roll = -1
@@ -686,6 +682,7 @@ class Player:
seen_notes.add(note) seen_notes.add(note)
self.curr_branch_reqs = [e_req, m_req, branch_start_time, max(len(seen_notes), 1)] self.curr_branch_reqs = [e_req, m_req, branch_start_time, max(len(seen_notes), 1)]
'''
def play_note_manager(self, current_ms: float, background: Optional[Background]): def play_note_manager(self, current_ms: float, background: Optional[Background]):
"""Manages the play_notes and removes if necessary""" """Manages the play_notes and removes if necessary"""
if self.don_notes and self.don_notes[0].hit_ms + Player.TIMING_BAD < current_ms: if self.don_notes and self.don_notes[0].hit_ms + Player.TIMING_BAD < current_ms:
@@ -745,7 +742,7 @@ class Player:
def draw_note_manager(self, current_ms: float): def draw_note_manager(self, current_ms: float):
"""Manages the draw_notes and removes if necessary""" """Manages the draw_notes and removes if necessary"""
if self.draw_note_list and current_ms + 1000 >= self.draw_note_list[0].load_ms: if self.draw_note_list and current_ms >= self.draw_note_list[0].hit_ms - 10000:
current_note = self.draw_note_list.popleft() current_note = self.draw_note_list.popleft()
if 5 <= current_note.type <= 7: if 5 <= current_note.type <= 7:
bisect.insort_left(self.current_notes_draw, current_note, key=lambda x: x.index) bisect.insort_left(self.current_notes_draw, current_note, key=lambda x: x.index)
@@ -767,16 +764,14 @@ class Player:
note = self.current_notes_draw[0] note = self.current_notes_draw[0]
if note.type in {NoteType.ROLL_HEAD, NoteType.ROLL_HEAD_L, NoteType.BALLOON_HEAD, NoteType.KUSUDAMA} and len(self.current_notes_draw) > 1: if note.type in {NoteType.ROLL_HEAD, NoteType.ROLL_HEAD_L, NoteType.BALLOON_HEAD, NoteType.KUSUDAMA} and len(self.current_notes_draw) > 1:
note = self.current_notes_draw[1] note = self.current_notes_draw[1]
if current_ms > note.hit_ms + 200: if self.get_position_x(note, current_ms) < GameScreen.JUDGE_X:
if note.type == NoteType.TAIL:
self.current_notes_draw.pop(0)
self.current_notes_draw.pop(0) self.current_notes_draw.pop(0)
def note_manager(self, current_ms: float, background: Optional[Background]): def note_manager(self, current_ms: float, background: Optional[Background]):
self.bar_manager(current_ms) self.bar_manager(current_ms)
self.play_note_manager(current_ms, background) self.play_note_manager(current_ms, background)
self.draw_note_manager(current_ms) self.draw_note_manager(current_ms)
self.handle_tjap3_extended_commands(current_ms) #self.handle_tjap3_extended_commands(current_ms)
def note_correct(self, note: Note, current_time: float): def note_correct(self, note: Note, current_time: float):
"""Removes a note from the appropriate separated list""" """Removes a note from the appropriate separated list"""
@@ -880,8 +875,8 @@ class Player:
if self.is_drumroll: if self.is_drumroll:
self.check_drumroll(drum_type, background, current_time) self.check_drumroll(drum_type, background, current_time)
elif self.is_balloon: elif self.is_balloon:
#if not isinstance(curr_note, Balloon): if not isinstance(curr_note, Balloon):
#raise Exception("Balloon mode entered but current note is not balloon") raise Exception("Balloon mode entered but current note is not balloon")
self.check_balloon(drum_type, curr_note, current_time) self.check_balloon(drum_type, curr_note, current_time)
else: else:
self.curr_drumroll_count = 0 self.curr_drumroll_count = 0
@@ -1082,8 +1077,8 @@ class Player:
self.lane_hit_effect.update(current_time) self.lane_hit_effect.update(current_time)
self.animation_manager(self.draw_drum_hit_list, current_time) self.animation_manager(self.draw_drum_hit_list, current_time)
self.get_judge_position(ms_from_start) self.get_judge_position(ms_from_start)
self.handle_tjap3_extended_commands(ms_from_start)
self.handle_scroll_type_commands(ms_from_start) self.handle_scroll_type_commands(ms_from_start)
'''
if self.delay_start is not None and self.delay_end is not None: if self.delay_start is not None and self.delay_end is not None:
# Currently, a delay is active: notes should be frozen at ms = delay_start # Currently, a delay is active: notes should be frozen at ms = delay_start
# Check if it ended # Check if it ended
@@ -1093,6 +1088,7 @@ class Player:
note.load_ms += delay note.load_ms += delay
self.delay_start = None self.delay_start = None
self.delay_end = None self.delay_end = None
'''
self.update_bpm(ms_from_start) self.update_bpm(ms_from_start)
# More efficient arc management # More efficient arc management
@@ -1123,30 +1119,6 @@ class Player:
if self.is_branch: if self.is_branch:
self.evaluate_branch(ms_from_start) self.evaluate_branch(ms_from_start)
# Get the next note from any of the three lists for BPM and gogo time updates
'''
next_note = None
candidates = []
if self.don_notes:
candidates.append(self.don_notes[0])
if self.kat_notes:
candidates.append(self.kat_notes[0])
if self.other_notes:
candidates.append(self.other_notes[0])
if candidates:
next_note = min(candidates, key=lambda note: note.load_ms)
if next_note:
if next_note.gogo_time and not self.is_gogo_time:
self.is_gogo_time = True
self.gogo_time = GogoTime(self.is_2p)
self.chara.set_animation('gogo_start')
if not next_note.gogo_time and self.is_gogo_time:
self.is_gogo_time = False
self.gogo_time = None
self.chara.set_animation('gogo_stop')
'''
if self.gauge is None: if self.gauge is None:
self.chara.update(current_time, self.bpm, False, False) self.chara.update(current_time, self.bpm, False, False)
else: else:
@@ -1154,25 +1126,22 @@ class Player:
def draw_drumroll(self, current_ms: float, head: Drumroll, current_eighth: int): def draw_drumroll(self, current_ms: float, head: Drumroll, current_eighth: int):
"""Draws a drumroll in the player's lane""" """Draws a drumroll in the player's lane"""
start_position = self.get_position_x(tex.screen_width, current_ms, head.load_ms, head.pixels_per_frame_x) start_position = self.get_position_x(head, current_ms)
start_position += self.judge_x
tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1]) tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1])
is_big = int(head.type == NoteType.ROLL_HEAD_L) is_big = int(head.type == NoteType.ROLL_HEAD_L)
end_position = self.get_position_x(tex.screen_width, current_ms, tail.load_ms, tail.pixels_per_frame_x) end_position = self.get_position_x(tail, current_ms)
end_position += self.judge_x
length = end_position - start_position length = end_position - start_position
color = ray.Color(255, head.color, head.color, 255) color = ray.Color(255, head.color, head.color, 255)
y = tex.skin_config["notes"].y + self.get_position_y(current_ms, head.load_ms, head.pixels_per_frame_y, head.pixels_per_frame_x) y = tex.skin_config["notes"].y + self.get_position_y(head, current_ms)
moji_y = tex.skin_config["moji"].y moji_y = tex.skin_config["moji"].y
moji_x = -(tex.textures["notes"]["moji"].width//2) + (tex.textures["notes"]["1"].width//2) moji_x = -(tex.textures["notes"]["moji"].width//2) + (tex.textures["notes"]["1"].width//2)
if head.display: if head.display:
if length > 0: tex.draw_texture('notes', "8", frame=is_big, x=start_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length+tex.skin_config["drumroll_width_offset"].width, color=color)
tex.draw_texture('notes', "8", frame=is_big, x=start_position+(tex.textures["notes"]["5"].width//2), y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length+tex.skin_config["drumroll_width_offset"].width, color=color) if is_big:
if is_big: tex.draw_texture('notes', "drumroll_big_tail", x=end_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
tex.draw_texture('notes', "drumroll_big_tail", x=end_position+tex.textures["notes"]["5"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color) else:
else: tex.draw_texture('notes', "drumroll_tail", x=end_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
tex.draw_texture('notes', "drumroll_tail", x=end_position+tex.textures["notes"]["5"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color) tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position - tex.textures["notes"]["1"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + tex.textures["notes"]["1"].width//2, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length) tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + tex.textures["notes"]["1"].width//2, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length)
tex.draw_texture('notes', 'moji', frame=head.moji, x=start_position + moji_x, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y) tex.draw_texture('notes', 'moji', frame=head.moji, x=start_position + moji_x, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
@@ -1181,13 +1150,11 @@ class Player:
def draw_balloon(self, current_ms: float, head: Balloon, current_eighth: int): def draw_balloon(self, current_ms: float, head: Balloon, current_eighth: int):
"""Draws a balloon in the player's lane""" """Draws a balloon in the player's lane"""
offset = tex.skin_config["balloon_offset"].x offset = tex.skin_config["balloon_offset"].x
start_position = self.get_position_x(tex.screen_width, current_ms, head.load_ms, head.pixels_per_frame_x) start_position = self.get_position_x(head, current_ms)
start_position += self.judge_x
tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1]) tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1])
end_position = self.get_position_x(tex.screen_width, current_ms, tail.load_ms, tail.pixels_per_frame_x) end_position = self.get_position_x(tail, current_ms)
end_position += self.judge_x pause_position = GameScreen.JUDGE_X
pause_position = tex.skin_config["balloon_pause_position"].x + self.judge_x y = tex.skin_config["notes"].y + self.get_position_y(head, current_ms)
y = tex.skin_config["notes"].y + self.get_position_y(current_ms, head.load_ms, head.pixels_per_frame_y, head.pixels_per_frame_x)
if current_ms >= tail.hit_ms: if current_ms >= tail.hit_ms:
position = end_position position = end_position
elif current_ms >= head.hit_ms: elif current_ms >= head.hit_ms:
@@ -1195,8 +1162,8 @@ class Player:
else: else:
position = start_position position = start_position
if head.display: if head.display:
tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y) tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset - tex.textures["notes"]["1"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+tex.textures["notes"]["10"].width, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y) tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+tex.textures["notes"]["10"].width - tex.textures["notes"]["1"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
def draw_bars(self, current_ms: float): def draw_bars(self, current_ms: float):
"""Draw bars in the player's lane""" """Draw bars in the player's lane"""
@@ -1206,29 +1173,19 @@ class Player:
for bar in reversed(self.current_bars): for bar in reversed(self.current_bars):
if not bar.display: if not bar.display:
continue continue
x_position = self.get_position_x(tex.screen_width, current_ms, bar.load_ms, bar.pixels_per_frame_x) x_position = self.get_position_x(bar, current_ms)
y_position = self.get_position_y(current_ms, bar.load_ms, bar.pixels_per_frame_y, bar.pixels_per_frame_x) y_position = self.get_position_y(bar, current_ms)
x_position += self.judge_x
y_position += self.judge_y
if hasattr(bar, 'is_branch_start'):
frame = 1
else:
frame = 0
if y_position != 0: if y_position != 0:
angle = math.degrees(math.atan2(bar.pixels_per_frame_y, bar.pixels_per_frame_x)) angle = math.degrees(math.atan2(bar.scroll_y, bar.scroll_x))
else: else:
angle = 0 angle = 0
tex.draw_texture('notes', str(bar.type), frame=frame, x=x_position+tex.skin_config["moji_drumroll"].x, y=y_position+tex.skin_config["moji_drumroll"].y+(self.is_2p*tex.skin_config["2p_offset"].y), rotation=angle) tex.draw_texture('notes', str(bar.type), x=x_position+tex.skin_config["moji_drumroll"].x- (tex.textures["notes"]["1"].width//2), y=y_position+tex.skin_config["moji_drumroll"].y+(self.is_2p*tex.skin_config["2p_offset"].y), rotation=angle)
def draw_notes(self, current_ms: float, start_ms: float): def draw_notes(self, current_ms: float):
"""Draw notes in the player's lane""" """Draw notes in the player's lane"""
if not self.current_notes_draw: if not self.current_notes_draw:
return return
eighth_in_ms = 0 if self.bpm == 0 else (60000 * 4 / self.bpm) / 8
current_eighth = 0
if self.combo >= 50 and eighth_in_ms != 0:
current_eighth = int((current_ms - start_ms) // eighth_in_ms)
for note in reversed(self.current_notes_draw): for note in reversed(self.current_notes_draw):
if self.balloon_anim is not None and note == self.current_notes_draw[0]: if self.balloon_anim is not None and note == self.current_notes_draw[0]:
@@ -1236,6 +1193,7 @@ class Player:
if note.type == NoteType.TAIL: if note.type == NoteType.TAIL:
continue continue
current_eighth = 0
if hasattr(note, 'sudden_appear_ms') and hasattr(note, 'sudden_moving_ms'): if hasattr(note, 'sudden_appear_ms') and hasattr(note, 'sudden_moving_ms'):
appear_ms = note.hit_ms - note.sudden_appear_ms appear_ms = note.hit_ms - note.sudden_appear_ms
moving_start_ms = note.hit_ms - note.sudden_moving_ms moving_start_ms = note.hit_ms - note.sudden_moving_ms
@@ -1247,14 +1205,11 @@ class Player:
effective_ms = moving_start_ms effective_ms = moving_start_ms
else: else:
effective_ms = current_ms effective_ms = current_ms
x_position = self.get_position_x(note, effective_ms)
x_position = self.get_position_x(tex.screen_width, effective_ms, note.load_ms, note.pixels_per_frame_x) y_position = self.get_position_y(note, effective_ms)
y_position = self.get_position_y(effective_ms, note.load_ms, note.pixels_per_frame_y, note.pixels_per_frame_x)
else: else:
x_position = self.get_position_x(tex.screen_width, current_ms, note.load_ms, note.pixels_per_frame_x) x_position = self.get_position_x(note, current_ms)
y_position = self.get_position_y(current_ms, note.load_ms, note.pixels_per_frame_y, note.pixels_per_frame_x) y_position = self.get_position_y(note, current_ms)
x_position += self.judge_x
y_position += self.judge_y
if isinstance(note, Drumroll): if isinstance(note, Drumroll):
self.draw_drumroll(current_ms, note, current_eighth) self.draw_drumroll(current_ms, note, current_eighth)
elif isinstance(note, Balloon) and not note.is_kusudama: elif isinstance(note, Balloon) and not note.is_kusudama:
@@ -1262,10 +1217,8 @@ class Player:
tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position, y=tex.skin_config["moji"].y + y_position+(self.is_2p*tex.skin_config["2p_offset"].y)) tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position, y=tex.skin_config["moji"].y + y_position+(self.is_2p*tex.skin_config["2p_offset"].y))
else: else:
if note.display: if note.display:
tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position, y=y_position+tex.skin_config["notes"].y+(self.is_2p*tex.skin_config["2p_offset"].y), center=True) tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position - (tex.textures["notes"]["1"].width//2), y=y_position+tex.skin_config["notes"].y+(self.is_2p*tex.skin_config["2p_offset"].y), center=True)
tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (tex.textures["notes"]["moji"].width//2) + (tex.textures["notes"]["1"].width//2), y=tex.skin_config["moji"].y + y_position+(self.is_2p*tex.skin_config["2p_offset"].y)) tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (tex.textures["notes"]["moji"].width//2), y=tex.skin_config["moji"].y + y_position+(self.is_2p*tex.skin_config["2p_offset"].y))
ray.draw_text(self.current_notes_draw[0].lyric, tex.screen_width//2 - (ray.measure_text(self.current_notes_draw[0].lyric, int(40 * tex.screen_scale))//2), tex.screen_height - int(50 * tex.screen_scale), int(40 * tex.screen_scale), ray.BLUE)
def draw_modifiers(self): def draw_modifiers(self):
@@ -1367,7 +1320,7 @@ 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)
if dan_transition is not None: if dan_transition is not None:
dan_transition.draw() dan_transition.draw()

View File

@@ -1,218 +0,0 @@
from collections import deque
from libs.tja2 import TJAParser2
import bisect
from enum import IntEnum
import math
import logging
from pathlib import Path
import pyray as ray
from libs.audio import audio
from libs.texture import tex
from libs.tja import calculate_base_score, NoteType
from libs.tja2 import (
Balloon,
Drumroll,
Note,
)
from libs.utils import (
get_current_ms,
global_data,
)
from libs.video import VideoPlayer
from scenes.game import GameScreen, Player
logger = logging.getLogger(__name__)
class DrumType(IntEnum):
DON = 1
KAT = 2
class Side(IntEnum):
LEFT = 1
RIGHT = 2
class Judgments(IntEnum):
GOOD = 0
OK = 1
BAD = 2
class GameScreen2(GameScreen):
def init_tja(self, song: Path):
"""Initialize the TJA file"""
self.tja = TJAParser2(song, start_delay=self.start_delay, distance=tex.screen_width - GameScreen.JUDGE_X)
if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists():
self.movie = VideoPlayer(self.tja.metadata.bgmovie)
self.movie.set_volume(0.0)
else:
self.movie = None
global_data.session_data[global_data.player_num].song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en'])
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 = Player2(self.tja, global_data.player_num, global_data.session_data[global_data.player_num].selected_difficulty, False, global_data.modifiers[global_data.player_num])
self.start_ms = get_current_ms() - self.tja.metadata.offset*1000
class Player2(Player):
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 = deque(notes.play_notes), deque(notes.draw_notes), deque(notes.bars)
self.don_notes = deque([note for note in self.play_notes if note.type in {NoteType.DON, NoteType.DON_L}])
self.kat_notes = deque([note for note in self.play_notes if note.type in {NoteType.KAT, NoteType.KAT_L}])
self.other_notes = deque([note for note in self.play_notes if note.type not in {NoteType.DON, NoteType.DON_L, NoteType.KAT, NoteType.KAT_L}])
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.timeline = notes.timeline
self.timeline_index = 0 # Range: [0, len(timeline)]
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 = 120
if self.timeline and hasattr(self.timeline[self.timeline_index], 'bpm'):
self.bpm = self.timeline[self.timeline_index].bpm
self.end_time = 0
if self.play_notes:
self.end_time = self.play_notes[-1].hit_ms
def get_position_x(self, note, current_ms):
speedx = note.bpm / 240000 * note.scroll_x * (tex.screen_width - GameScreen.JUDGE_X) * tex.screen_scale
return GameScreen.JUDGE_X + (note.hit_ms - current_ms) * speedx
def get_position_y(self, note, current_ms):
speedy = note.bpm / 240000 * note.scroll_y * (tex.screen_width - GameScreen.JUDGE_Y) * tex.screen_scale
return (note.hit_ms - current_ms) * speedy
def bar_manager(self, current_ms: float):
"""Manages the bars and removes if necessary
Also sets branch conditions"""
#Add bar to current_bars list if it is ready to be shown on screen
if self.draw_bar_list and current_ms >= self.draw_bar_list[0].hit_ms - 10000:
self.current_bars.append(self.draw_bar_list.popleft())
def draw_note_manager(self, current_ms: float):
"""Manages the draw_notes and removes if necessary"""
if self.draw_note_list and current_ms >= self.draw_note_list[0].hit_ms - 10000:
current_note = self.draw_note_list.popleft()
if 5 <= current_note.type <= 7:
bisect.insort_left(self.current_notes_draw, current_note, key=lambda x: x.index)
try:
tail_note = next((note for note in self.draw_note_list if note.type == NoteType.TAIL))
bisect.insort_left(self.current_notes_draw, tail_note, key=lambda x: x.index)
self.draw_note_list.remove(tail_note)
except Exception as e:
raise(e)
else:
bisect.insort_left(self.current_notes_draw, current_note, key=lambda x: x.index)
if not self.current_notes_draw:
return
if isinstance(self.current_notes_draw[0], Drumroll):
self.current_notes_draw[0].color = min(255, self.current_notes_draw[0].color + 1)
note = self.current_notes_draw[0]
if note.type in {NoteType.ROLL_HEAD, NoteType.ROLL_HEAD_L, NoteType.BALLOON_HEAD, NoteType.KUSUDAMA} and len(self.current_notes_draw) > 1:
note = self.current_notes_draw[1]
if self.get_position_x(note, current_ms) < GameScreen.JUDGE_X:
self.current_notes_draw.pop(0)
def draw_drumroll(self, current_ms: float, head: Drumroll, current_eighth: int):
"""Draws a drumroll in the player's lane"""
start_position = self.get_position_x(head, current_ms)
tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1])
is_big = int(head.type == NoteType.ROLL_HEAD_L)
end_position = self.get_position_x(tail, current_ms)
length = end_position - start_position
color = ray.Color(255, head.color, head.color, 255)
y = tex.skin_config["notes"].y + self.get_position_y(head, current_ms)
moji_y = tex.skin_config["moji"].y
moji_x = -(tex.textures["notes"]["moji"].width//2) + (tex.textures["notes"]["1"].width//2)
if head.display:
tex.draw_texture('notes', "8", frame=is_big, x=start_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length+tex.skin_config["drumroll_width_offset"].width, color=color)
if is_big:
tex.draw_texture('notes', "drumroll_big_tail", x=end_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
else:
tex.draw_texture('notes', "drumroll_tail", x=end_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position - tex.textures["notes"]["1"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + tex.textures["notes"]["1"].width//2, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length)
tex.draw_texture('notes', 'moji', frame=head.moji, x=start_position + moji_x, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
tex.draw_texture('notes', 'moji', frame=tail.moji, x=end_position + moji_x, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
def draw_balloon(self, current_ms: float, head: Balloon, current_eighth: int):
"""Draws a balloon in the player's lane"""
offset = tex.skin_config["balloon_offset"].x
start_position = self.get_position_x(head, current_ms)
tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1])
end_position = self.get_position_x(tail, current_ms)
pause_position = GameScreen.JUDGE_X
y = tex.skin_config["notes"].y + self.get_position_y(head, current_ms)
if current_ms >= tail.hit_ms:
position = end_position
elif current_ms >= head.hit_ms:
position = pause_position
else:
position = start_position
if head.display:
tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset - tex.textures["notes"]["1"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+tex.textures["notes"]["10"].width - tex.textures["notes"]["1"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
def draw_bars(self, current_ms: float):
"""Draw bars in the player's lane"""
if not self.current_bars:
return
for bar in reversed(self.current_bars):
if not bar.display:
continue
x_position = self.get_position_x(bar, current_ms)
y_position = self.get_position_y(bar, current_ms)
if y_position != 0:
angle = math.degrees(math.atan2(bar.scroll_y, bar.scroll_x))
else:
angle = 0
tex.draw_texture('notes', str(bar.type), x=x_position+tex.skin_config["moji_drumroll"].x- (tex.textures["notes"]["1"].width//2), y=y_position+tex.skin_config["moji_drumroll"].y+(self.is_2p*tex.skin_config["2p_offset"].y), rotation=angle)
def draw_notes(self, current_ms: float, start_ms: float):
"""Draw notes in the player's lane"""
if not self.current_notes_draw:
return
for note in reversed(self.current_notes_draw):
if self.balloon_anim is not None and note == self.current_notes_draw[0]:
continue
if note.type == NoteType.TAIL:
continue
current_eighth = 0
x_position = self.get_position_x(note, current_ms)
y_position = self.get_position_y(note, current_ms)
if isinstance(note, Drumroll):
self.draw_drumroll(current_ms, note, current_eighth)
elif isinstance(note, Balloon) and not note.is_kusudama:
self.draw_balloon(current_ms, note, current_eighth)
tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position, y=tex.skin_config["moji"].y + y_position+(self.is_2p*tex.skin_config["2p_offset"].y))
else:
if note.display:
tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position - (tex.textures["notes"]["1"].width//2), y=y_position+tex.skin_config["notes"].y+(self.is_2p*tex.skin_config["2p_offset"].y), center=True)
tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (tex.textures["notes"]["moji"].width//2), y=tex.skin_config["moji"].y + y_position+(self.is_2p*tex.skin_config["2p_offset"].y))

View File

@@ -345,7 +345,7 @@ class PracticePlayer(Player):
# Group 3: Notes and bars (game content) # Group 3: Notes and bars (game content)
if not self.paused: if not self.paused:
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)
class PracticeDrumHitEffect(DrumHitEffect): class PracticeDrumHitEffect(DrumHitEffect):
def __init__(self, type, side, is_2p, player_num: PlayerNum = PlayerNum.P1): def __init__(self, type, side, is_2p, player_num: PlayerNum = PlayerNum.P1):