mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 19:50:12 +01:00
feat: support #BPMCHANGE for BMSCROLL, HBSCROLL
This commit is contained in:
84
libs/tja.py
84
libs/tja.py
@@ -39,6 +39,11 @@ class NoteType(IntEnum):
|
|||||||
TAIL = 8
|
TAIL = 8
|
||||||
KUSUDAMA = 9
|
KUSUDAMA = 9
|
||||||
|
|
||||||
|
class ScrollType(IntEnum):
|
||||||
|
NMSCROLL = 0
|
||||||
|
BMSCROLL = 1
|
||||||
|
HBSCROLL = 2
|
||||||
|
|
||||||
@dataclass()
|
@dataclass()
|
||||||
class Note:
|
class Note:
|
||||||
"""A note in a TJA file.
|
"""A note in a TJA file.
|
||||||
@@ -56,6 +61,7 @@ class Note:
|
|||||||
moji (int): The text drawn below the note.
|
moji (int): The text drawn below the note.
|
||||||
is_branch_start (bool): Whether the note is the start of a branch.
|
is_branch_start (bool): Whether the note is the start of a branch.
|
||||||
branch_params (str): The parameters (requirements) of the branch.
|
branch_params (str): The parameters (requirements) of the branch.
|
||||||
|
bpmchange (float): If it exists, the bpm will be multiplied by it when the note passes the judgement circle
|
||||||
"""
|
"""
|
||||||
type: int = field(init=False)
|
type: int = field(init=False)
|
||||||
hit_ms: float = field(init=False)
|
hit_ms: float = field(init=False)
|
||||||
@@ -74,6 +80,7 @@ class Note:
|
|||||||
sudden_moving_ms: float = field(init=False)
|
sudden_moving_ms: float = field(init=False)
|
||||||
judge_pos_x: float = field(init=False)
|
judge_pos_x: float = field(init=False)
|
||||||
judge_pos_y: float = field(init=False)
|
judge_pos_y: float = field(init=False)
|
||||||
|
bpmchange: 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
|
||||||
@@ -509,6 +516,7 @@ class TJAParser:
|
|||||||
# Use enumerate for single iteration
|
# Use enumerate for single iteration
|
||||||
note_start = note_end = -1
|
note_start = note_end = -1
|
||||||
target_found = False
|
target_found = False
|
||||||
|
scroll_type = ScrollType.NMSCROLL
|
||||||
|
|
||||||
# Find the section boundaries
|
# Find the section boundaries
|
||||||
for i, line in enumerate(self.data):
|
for i, line in enumerate(self.data):
|
||||||
@@ -521,6 +529,15 @@ class TJAParser:
|
|||||||
elif line == "#END" and note_start != -1:
|
elif line == "#END" and note_start != -1:
|
||||||
note_end = i
|
note_end = i
|
||||||
break
|
break
|
||||||
|
elif '#NMSCROLL' in line:
|
||||||
|
scroll_type = ScrollType.NMSCROLL
|
||||||
|
continue
|
||||||
|
elif '#BMSCROLL' in line:
|
||||||
|
scroll_type = ScrollType.BMSCROLL
|
||||||
|
continue
|
||||||
|
elif '#HBSCROLL' in line:
|
||||||
|
scroll_type = ScrollType.HBSCROLL
|
||||||
|
continue
|
||||||
|
|
||||||
if note_start == -1 or note_end == -1:
|
if note_start == -1 or note_end == -1:
|
||||||
return []
|
return []
|
||||||
@@ -530,6 +547,14 @@ class TJAParser:
|
|||||||
bar = []
|
bar = []
|
||||||
section_data = self.data[note_start:note_end]
|
section_data = self.data[note_start:note_end]
|
||||||
|
|
||||||
|
# Prepend scroll type
|
||||||
|
if scroll_type == ScrollType.NMSCROLL:
|
||||||
|
bar.append('#NMSCROLL')
|
||||||
|
elif scroll_type == ScrollType.BMSCROLL:
|
||||||
|
bar.append('#BMSCROLL')
|
||||||
|
elif scroll_type == ScrollType.HBSCROLL:
|
||||||
|
bar.append('#HBSCROLL')
|
||||||
|
|
||||||
for line in section_data:
|
for line in section_data:
|
||||||
if line.startswith("#"):
|
if line.startswith("#"):
|
||||||
bar.append(line)
|
bar.append(line)
|
||||||
@@ -656,6 +681,8 @@ class TJAParser:
|
|||||||
is_section_start = False
|
is_section_start = False
|
||||||
section_bar = None
|
section_bar = None
|
||||||
lyric = ""
|
lyric = ""
|
||||||
|
scroll_type = ScrollType.NMSCROLL
|
||||||
|
bpmchange_last_bpm = bpm
|
||||||
for bar in notes:
|
for bar in notes:
|
||||||
#Length of the bar is determined by number of notes excluding commands
|
#Length of the bar is determined by number of notes excluding commands
|
||||||
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)
|
||||||
@@ -828,25 +855,60 @@ class TJAParser:
|
|||||||
|
|
||||||
continue
|
continue
|
||||||
elif '#NMSCROLL' in part:
|
elif '#NMSCROLL' in part:
|
||||||
|
scroll_type = ScrollType.NMSCROLL
|
||||||
|
continue
|
||||||
|
elif '#BMSCROLL' in part:
|
||||||
|
scroll_type = ScrollType.BMSCROLL
|
||||||
|
continue
|
||||||
|
elif '#HBSCROLL' in part:
|
||||||
|
scroll_type = ScrollType.HBSCROLL
|
||||||
continue
|
continue
|
||||||
elif '#MEASURE' in part:
|
elif '#MEASURE' in part:
|
||||||
divisor = part.find('/')
|
divisor = part.find('/')
|
||||||
time_signature = float(part[9:divisor]) / float(part[divisor+1:])
|
time_signature = float(part[9:divisor]) / float(part[divisor+1:])
|
||||||
continue
|
continue
|
||||||
elif '#SCROLL' in part:
|
elif '#SCROLL' in part:
|
||||||
scroll_value = part[7:]
|
if scroll_type != ScrollType.BMSCROLL:
|
||||||
if 'i' in scroll_value:
|
scroll_value = part[7:]
|
||||||
normalized = scroll_value.replace('.i', 'j').replace('i', 'j')
|
if 'i' in scroll_value:
|
||||||
normalized = normalized.replace(',', '')
|
normalized = scroll_value.replace('.i', 'j').replace('i', 'j')
|
||||||
c = complex(normalized)
|
normalized = normalized.replace(',', '')
|
||||||
x_scroll_modifier = c.real
|
c = complex(normalized)
|
||||||
y_scroll_modifier = c.imag
|
x_scroll_modifier = c.real
|
||||||
else:
|
y_scroll_modifier = c.imag
|
||||||
x_scroll_modifier = float(scroll_value)
|
else:
|
||||||
y_scroll_modifier = 0.0
|
x_scroll_modifier = float(scroll_value)
|
||||||
|
y_scroll_modifier = 0.0
|
||||||
continue
|
continue
|
||||||
elif '#BPMCHANGE' in part:
|
elif '#BPMCHANGE' in part:
|
||||||
bpm = float(part[11:])
|
parsed_bpm = float(part[11:])
|
||||||
|
if scroll_type == ScrollType.BMSCROLL or scroll_type == ScrollType.HBSCROLL:
|
||||||
|
# Do not modify bpm, it needs to be changed live by bpmchange
|
||||||
|
bpmchange = parsed_bpm / bpmchange_last_bpm
|
||||||
|
bpmchange_last_bpm = parsed_bpm
|
||||||
|
|
||||||
|
bpmchange_bar = Note()
|
||||||
|
bpmchange_bar.pixels_per_frame_x = get_pixels_per_frame(bpm * time_signature * x_scroll_modifier, time_signature*4, self.distance)
|
||||||
|
bpmchange_bar.pixels_per_frame_y = get_pixels_per_frame(bpm * time_signature * y_scroll_modifier, time_signature*4, self.distance)
|
||||||
|
pixels_per_ms = get_pixels_per_ms(bpmchange_bar.pixels_per_frame_x)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
bpmchange = None
|
||||||
|
else:
|
||||||
|
bpm = parsed_bpm
|
||||||
continue
|
continue
|
||||||
elif '#BARLINEOFF' in part:
|
elif '#BARLINEOFF' in part:
|
||||||
barline_display = False
|
barline_display = False
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import sqlite3
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
|
||||||
@@ -360,6 +361,7 @@ 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.combo_announce = ComboAnnounce(self.combo, 0, player_num, self.is_2p)
|
self.combo_announce = ComboAnnounce(self.combo, 0, player_num, self.is_2p)
|
||||||
self.branch_indicator = BranchIndicator(self.is_2p) if tja and tja.metadata.course_data[self.difficulty].is_branching else None
|
self.branch_indicator = BranchIndicator(self.is_2p) if tja and tja.metadata.course_data[self.difficulty].is_branching else None
|
||||||
self.ending_anim: Optional[FailAnimation | ClearAnimation | FCAnimation] = None
|
self.ending_anim: Optional[FailAnimation | ClearAnimation | FCAnimation] = None
|
||||||
@@ -403,6 +405,12 @@ 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])
|
||||||
|
|
||||||
|
self.bpmchanges = deque()
|
||||||
|
for note in self.draw_bar_list:
|
||||||
|
if hasattr(note, 'bpmchange'):
|
||||||
|
self.bpmchanges.append(BPMChange(note.hit_ms, note.bpmchange))
|
||||||
|
|
||||||
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:
|
||||||
@@ -943,6 +951,28 @@ 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:
|
||||||
|
bpmchange = self.bpmchanges[0]
|
||||||
|
bpmchange_success = bpmchange.is_ready(ms_from_start)
|
||||||
|
if bpmchange_success:
|
||||||
|
# Adjust notes
|
||||||
|
for note in chain(self.play_notes, self.current_bars, self.draw_bar_list):
|
||||||
|
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()
|
||||||
|
# Adjust later bpmchanges too
|
||||||
|
for bpmchange_bar in self.bpmchanges:
|
||||||
|
bpmchange_bar.hit_ms = (bpmchange_bar.hit_ms - bpmchange.hit_ms) / bpmchange.bpmchange + bpmchange.hit_ms
|
||||||
if self.lane_hit_effect is not None:
|
if self.lane_hit_effect is not None:
|
||||||
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)
|
||||||
@@ -1932,6 +1962,14 @@ 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 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):
|
||||||
|
|||||||
Reference in New Issue
Block a user