feat: implement bpmchange, delay needs timeline bugfix

This commit is contained in:
mc08
2025-11-28 18:42:38 -08:00
parent 334a08685a
commit 719a9f3717
2 changed files with 81 additions and 131 deletions

View File

@@ -62,6 +62,8 @@ class TimelineObject:
cam_rotation: float = field(init=False) cam_rotation: float = field(init=False)
bpm: float = field(init=False) bpm: float = field(init=False)
bpmchange: float = field(init=False)
delay: float = field(init=False)
''' '''
gogo_time: bool = field(init=False) gogo_time: bool = field(init=False)
branch_params: str = field(init=False) branch_params: str = field(init=False)
@@ -109,8 +111,6 @@ class Note:
lyric: str = field(init=False) lyric: str = field(init=False)
sudden_appear_ms: float = field(init=False) sudden_appear_ms: float = field(init=False)
sudden_moving_ms: float = field(init=False) sudden_moving_ms: float = field(init=False)
bpmchange: float = field(init=False)
delay: float = field(init=False)
def __lt__(self, other): def __lt__(self, other):
return self.hit_ms < other.hit_ms return self.hit_ms < other.hit_ms
@@ -810,25 +810,10 @@ class TJAParser:
delay_last_note_ms = self.current_ms delay_last_note_ms = self.current_ms
def add_delay_bar(hit_ms: float, delay: float): def add_delay_bar(hit_ms: float, delay: float):
delay_bar = Note() delay_timeline = TimelineObject()
delay_bar.pixels_per_frame_x = get_pixels_per_frame(bpm * time_signature * x_scroll_modifier, time_signature*4, self.distance) delay_timeline.hit_ms = hit_ms
delay_bar.pixels_per_frame_y = get_pixels_per_frame(bpm * time_signature * y_scroll_modifier, time_signature*4, self.distance) delay_timeline.delay = delay
pixels_per_ms = get_pixels_per_ms(delay_bar.pixels_per_frame_x) bisect.insort(curr_timeline, delay_timeline, key=lambda x: x.hit_ms)
delay_bar.hit_ms = hit_ms
if pixels_per_ms == 0:
delay_bar.load_ms = delay_bar.hit_ms
else:
delay_bar.load_ms = delay_bar.hit_ms - (self.distance / pixels_per_ms)
delay_bar.type = 0
delay_bar.display = False
delay_bar.gogo_time = gogo_time
delay_bar.bpm = bpm
delay_bar.delay = delay
bisect.insort(curr_bar_list, delay_bar, key=lambda x: x.load_ms)
for bar in notes: for bar in notes:
bar_length = sum(len(part) for part in bar if '#' not in part) bar_length = sum(len(part) for part in bar if '#' not in part)
@@ -1272,30 +1257,15 @@ class TJAParser:
bpmchange = parsed_bpm / bpmchange_last_bpm bpmchange = parsed_bpm / bpmchange_last_bpm
bpmchange_last_bpm = parsed_bpm bpmchange_last_bpm = parsed_bpm
bpmchange_bar = Note() bpmchange_timeline = TimelineObject()
bpmchange_bar.pixels_per_frame_x = get_pixels_per_frame(bpm * time_signature * x_scroll_modifier, time_signature*4, self.distance) bpmchange_timeline.hit_ms = self.current_ms
bpmchange_bar.pixels_per_frame_y = get_pixels_per_frame(bpm * time_signature * y_scroll_modifier, time_signature*4, self.distance) bpmchange_timeline.bpmchange = bpmchange
pixels_per_ms = get_pixels_per_ms(bpmchange_bar.pixels_per_frame_x) bisect.insort(curr_timeline, bpmchange_timeline, key=lambda x: x.hit_ms)
bpmchange_bar.hit_ms = self.current_ms
if pixels_per_ms == 0:
bpmchange_bar.load_ms = bpmchange_bar.hit_ms
else:
bpmchange_bar.load_ms = bpmchange_bar.hit_ms - (self.distance / pixels_per_ms)
bpmchange_bar.type = 0
bpmchange_bar.display = False
bpmchange_bar.gogo_time = gogo_time
bpmchange_bar.bpm = bpm
bpmchange_bar.bpmchange = bpmchange
bisect.insort(curr_bar_list, bpmchange_bar, key=lambda x: x.load_ms)
else: else:
bpm = parsed_bpm timeline_obj = TimelineObject()
timeline_obj = TimelineObject() timeline_obj.hit_ms = self.current_ms
timeline_obj.hit_ms = self.current_ms timeline_obj.bpm = parsed_bpm
timeline_obj.bpm = bpm bisect.insort(curr_timeline, timeline_obj, key=lambda x: x.hit_ms)
bisect.insort(curr_timeline, timeline_obj, key=lambda x: x.hit_ms)
continue continue
elif '#BARLINEOFF' in part: elif '#BARLINEOFF' in part:
barline_display = False barline_display = False
@@ -1389,7 +1359,7 @@ class TJAParser:
continue continue
if delay_current != 0: if delay_current != 0:
logger.debug(delay_current) # logger.debug(delay_current)
add_delay_bar(delay_last_note_ms, delay_current) add_delay_bar(delay_last_note_ms, delay_current)
delay_current = 0 delay_current = 0

View File

@@ -361,8 +361,6 @@ class Player:
self.combo_display = Combo(self.combo, 0, self.is_2p) self.combo_display = Combo(self.combo, 0, self.is_2p)
self.score_counter = ScoreCounter(self.score, self.is_2p) self.score_counter = ScoreCounter(self.score, self.is_2p)
self.gogo_time: Optional[GogoTime] = None self.gogo_time: Optional[GogoTime] = None
self.bpmchanges: deque[BPMChange]
self.delays: deque[Delay]
self.delay_start: Optional[float] = None self.delay_start: Optional[float] = None
self.delay_end: Optional[float] = None self.delay_end: Optional[float] = None
self.combo_announce = ComboAnnounce(self.combo, 0, player_num, self.is_2p) self.combo_announce = ComboAnnounce(self.combo, 0, player_num, self.is_2p)
@@ -408,35 +406,6 @@ class Player:
self.kat_notes = deque([note for note in self.play_notes if note.type in {NoteType.KAT, NoteType.KAT_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.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]) self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5])
# Collect bpmchange, delay (remove from bars, and pre-adjust their hit_ms)
self.bpmchanges = deque()
self.delays = deque()
new_draw_bar_list: deque[Note] = deque()
special_bars: deque[Note] = deque()
for note in self.draw_bar_list:
if hasattr(note, 'bpmchange') or hasattr(note, 'delay'):
special_bars.append(note)
else:
new_draw_bar_list.append(note)
self.draw_bar_list = new_draw_bar_list
special_bars_len = len(special_bars)
for i, note in enumerate(special_bars):
if hasattr(note, 'bpmchange'):
bpmchange = BPMChange(note.hit_ms, note.bpmchange)
self.bpmchanges.append(bpmchange)
for i2 in range(i + 1, special_bars_len):
bar = special_bars[i2]
bar.hit_ms = (bar.hit_ms - bpmchange.hit_ms) / bpmchange.bpmchange + bpmchange.hit_ms
if hasattr(note, 'delay'):
delay = Delay(note.hit_ms, note.delay)
self.delays.append(delay)
for i2 in range(i + 1, special_bars_len):
bar = special_bars[i2]
bar.hit_ms += delay.delay
total_notes = notes total_notes = notes
if self.branch_m: if self.branch_m:
for section in self.branch_m: for section in self.branch_m:
@@ -461,6 +430,26 @@ class Player:
self.bpm = 120 self.bpm = 120
if self.timeline and hasattr(self.timeline[self.timeline_index], 'bpm'): if self.timeline and hasattr(self.timeline[self.timeline_index], 'bpm'):
self.bpm = self.timeline[self.timeline_index].bpm self.bpm = self.timeline[self.timeline_index].bpm
# Handle HBSCROLL, BMSCROLL (pre-modify hit_ms, so that notes can't be literally hit, but are still visually different) - basically it applies the transformations of #BPMCHANGE and #DELAY to hit_ms, so that notes can't be hit even if its visaulyl
for i, o in enumerate(self.timeline):
if hasattr(o, 'bpmchange'):
hit_ms = o.hit_ms
bpmchange = o.bpmchange
for note in chain(self.play_notes, self.current_bars, self.draw_bar_list):
if note.hit_ms > hit_ms:
note.hit_ms = (note.hit_ms - hit_ms) / bpmchange + hit_ms
for i2 in range(i + 1, len(self.timeline)):
o2 = self.timeline[i2]
o2.hit_ms = (o2.hit_ms - hit_ms) / bpmchange + hit_ms
elif hasattr(o, 'delay'):
hit_ms = o.hit_ms
delay = o.delay
for note in chain(self.play_notes, self.current_bars, self.draw_bar_list):
if note.hit_ms > hit_ms:
note.hit_ms += delay
for i2 in range(i + 1, len(self.timeline)):
o2 = self.timeline[i2]
o2.hit_ms += delay
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"""
@@ -551,6 +540,46 @@ class Player:
if self.timeline_index < len(self.timeline) - 1: if self.timeline_index < len(self.timeline) - 1:
self.timeline_index += 1 self.timeline_index += 1
def handle_scroll_type_commands(self, current_ms: float):
if not self.timeline:
return
timeline_object = self.timeline[self.timeline_index]
should_advance = False
if hasattr(timeline_object, 'bpmchange') and timeline_object.hit_ms <= current_ms:
hit_ms = timeline_object.hit_ms
bpmchange = timeline_object.bpmchange
# Adjust notes (visually)
for note in chain(self.play_notes, self.current_bars, self.draw_bar_list):
# Already modified
# note.hit_ms = (note.hit_ms - hit_ms) / bpmchange + hit_ms
# time_diff * note.pixels_per_frame need to be the same before and after the adjustment
# that means time_diff should be divided by self.bpmchange.bpmchange
# current_ms = self.bpmchange.hit_ms
time_diff = note.load_ms - hit_ms
note.load_ms = time_diff / bpmchange + hit_ms
note.pixels_per_frame_x *= bpmchange
note.pixels_per_frame_y *= bpmchange
self.bpm *= bpmchange
should_advance = True
if hasattr(timeline_object, 'delay') and timeline_object.hit_ms <= current_ms:
hit_ms = timeline_object.hit_ms
delay = timeline_object.delay
if self.delay_start is not None:
logger.error('Needs fix: delay is currently active, but another delay is being activated')
else:
# Turn on delay visual
self.delay_start = hit_ms
self.delay_end = hit_ms + delay
should_advance = True
if should_advance and self.timeline_index < len(self.timeline) - 1:
self.timeline_index += 1
def update_bpm(self, current_ms: float): def update_bpm(self, current_ms: float):
if not self.timeline: if not self.timeline:
return return
@@ -1027,24 +1056,12 @@ class Player:
self.balloon_manager(current_time) self.balloon_manager(current_time)
if self.gogo_time is not None: if self.gogo_time is not None:
self.gogo_time.update(current_time) self.gogo_time.update(current_time)
if len(self.bpmchanges) != 0: if self.lane_hit_effect is not None:
bpmchange = self.bpmchanges[0] self.lane_hit_effect.update(current_time)
bpmchange_success = bpmchange.is_ready(ms_from_start) self.animation_manager(self.draw_drum_hit_list, current_time)
if bpmchange_success: self.get_judge_position(ms_from_start)
# Adjust notes self.handle_tjap3_extended_commands(ms_from_start)
for note in chain(self.play_notes, self.current_bars, self.draw_bar_list): self.handle_scroll_type_commands(ms_from_start)
note.bpm *= bpmchange.bpmchange
note.hit_ms = (note.hit_ms - bpmchange.hit_ms) / bpmchange.bpmchange + bpmchange.hit_ms
# time_diff * note.pixels_per_frame need to be the same before and after the adjustment
# that means time_diff should be divided by self.bpmchange.bpmchange
# current_ms = self.bpmchange.hit_ms
time_diff = note.load_ms - bpmchange.hit_ms
note.load_ms = time_diff / bpmchange.bpmchange + bpmchange.hit_ms
note.pixels_per_frame_x *= bpmchange.bpmchange
note.pixels_per_frame_y *= bpmchange.bpmchange
self.bpm *= bpmchange.bpmchange
self.bpmchanges.popleft()
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
@@ -1054,27 +1071,6 @@ 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
if len(self.delays) != 0:
delay = self.delays[0]
delay_success = delay.is_ready(ms_from_start)
if delay_success:
if self.delay_start is not None and self.delay_end is not None:
logger.error('Needs fix: delay is currently active, but another delay is being activated')
# Turn on delay visual
self.delay_start = delay.hit_ms
self.delay_end = delay.hit_ms + delay.delay
# Adjust notes
for note in chain(self.play_notes, self.current_bars, self.draw_bar_list):
# time_diff must be the same throughout the delay
# time_diff = note.load_ms - delay.hit_ms
note.hit_ms += delay.delay
# note.load_ms += delay.delay
self.delays.popleft()
if self.lane_hit_effect is not None:
self.lane_hit_effect.update(current_time)
self.animation_manager(self.draw_drum_hit_list, current_time)
self.get_judge_position(ms_from_start)
self.handle_tjap3_extended_commands(ms_from_start)
self.update_bpm(ms_from_start) self.update_bpm(ms_from_start)
# More efficient arc management # More efficient arc management
@@ -2060,22 +2056,6 @@ class GogoTime:
for i in range(5): for i in range(5):
tex.draw_texture('gogo_time', 'explosion', frame=self.explosion_anim.attribute, index=i) tex.draw_texture('gogo_time', 'explosion', frame=self.explosion_anim.attribute, index=i)
class BPMChange:
"""For BPM changes during HBSCROLL or BMSCROLL"""
def __init__(self, hit_ms: float, bpmchange: float):
self.hit_ms = hit_ms
self.bpmchange = bpmchange
def is_ready(self, ms_from_start: float):
return ms_from_start >= self.hit_ms
class Delay:
"""For delay during HBSCROLL or BMSCROLL"""
def __init__(self, hit_ms: float, delay: float):
self.hit_ms = hit_ms
self.delay = delay
def is_ready(self, ms_from_start: float):
return ms_from_start >= self.hit_ms
class ComboAnnounce: class ComboAnnounce:
"""Displays the combo every 100 combos""" """Displays the combo every 100 combos"""
def __init__(self, combo: int, current_time_ms: float, player_num: PlayerNum, is_2p: bool): def __init__(self, combo: int, current_time_ms: float, player_num: PlayerNum, is_2p: bool):