-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathchart.py
165 lines (119 loc) · 4.98 KB
/
chart.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
from itertools import tee, chain, groupby
from typing import Union, TypeVar, Iterator, Iterable, Optional
from .model import Chart, Slide, LaneLocated, Single, Directional, Connection, BPM, NoteBase, Command
_T = TypeVar('_T', bound=NoteBase)
def get_max_beat(chart: Chart) -> float:
"""Get the max beat of a chart."""
return max(
note.connections[-1].beat
if isinstance(note, Slide)
else note.beat
for note in chart.__root__
)
def get_notes_for_type(chart: Chart, note_type: Union[type[_T], tuple[type[NoteBase], ...]]) -> Iterator[_T]:
"""(Non-recursive) Get all notes of a given type(s)."""
yield from (note for note in chart.__root__ if isinstance(note, note_type))
def get_all_skill_notes(chart: Chart) -> Iterator[Single]:
"""Get all skill notes."""
yield from sorted(
filter(
lambda note: note.skill,
chain(
get_notes_for_type(chart, Single),
get_endpoints_for_slide(chart)
)
),
key=lambda note: note.beat
)
def get_endpoints_for_slide(chart: Chart) -> Iterator[Connection]:
"""Get all endpoints (head or tail) of a slide note."""
yield from chain.from_iterable(
(note.connections[0], note.connections[-1])
for note in get_notes_for_type(chart, Slide)
)
def is_note_should_black(note: LaneLocated) -> bool:
"""Check if a note should be rendered as black."""
return note.beat % 0.5 != 0
def is_note_flick(note: LaneLocated) -> bool:
"""Check if a note is a flick note."""
return note.flick is True
def is_note_skill(note: LaneLocated) -> bool:
"""Check if a note is a skill note."""
return note.skill is True
def get_note_beat(note: LaneLocated) -> float:
"""Get the beat of a note."""
return note.beat
def get_fever_command_tuple(command_list: list[Command]) -> tuple[Optional[Command], ...]:
"""Get the fever ready, start and end command from a list of system notes."""
fever_ready, fever_start, fever_end = None, None, None
for command in command_list:
if command.data == 'cmd_fever_ready.wav':
fever_ready = command
elif command.data == 'cmd_fever_start.wav':
fever_start = command
elif command.data == 'cmd_fever_end.wav':
fever_end = command
return fever_ready, fever_start, fever_end
def pairwise(iterable: Iterable[_T]) -> Iterator[tuple[_T, _T]]:
"""pairwise(ABCDEFG) --> AB BC CD DE EF FG. For Python 3.10+, use itertools.pairwise directly."""
a, b = tee(iterable)
next(b, None)
yield from zip(a, b)
def get_grouped_notes_by_beat(chart: Chart) -> Iterator[tuple[float, Iterator[LaneLocated]]]:
"""Group notes if they are on the same beat."""
notes = chain(
get_notes_for_type(chart, (Single, Directional)),
get_endpoints_for_slide(chart)
)
yield from groupby(sorted(notes, key=get_note_beat), get_note_beat)
def get_time_elapsed(bpms: list[BPM], beat: float) -> float:
"""Get the elapsed time of a beat."""
current_time = 0.0
current_bpm = bpms[0].bpm
current_beat = 0.0
for bpm in bpms:
if bpm.beat > beat:
break
current_time += (bpm.beat - current_beat) * 60 / current_bpm
current_bpm = bpm.bpm
current_beat = bpm.beat
current_time += (beat - current_beat) * 60 / current_bpm
return current_time
def get_beat_elapsed(bpms: list[BPM], time: float) -> float:
"""Get the elapsed beat of a time."""
current_time = 0.0
current_bpm = bpms[0].bpm
current_beat = 0.0
for bpm in bpms:
if current_time + (bpm.beat - current_beat) * 60 / current_bpm > time:
break
current_time += (bpm.beat - current_beat) * 60 / current_bpm
current_bpm = bpm.bpm
current_beat = bpm.beat
current_beat += (time - current_time) * current_bpm / 60
return current_beat
def get_combo_before(beat: float, note_list_single_directional: list[Union[Single, Directional]], note_list_slide: list[Slide]) -> int:
"""Get the total combo before given beat."""
return get_combo_between(0, beat, note_list_single_directional, note_list_slide)
def get_combo_between(
beat_start: float, beat_end: float,
note_list_single_directional: list[Union[Single, Directional]], note_list_slide: list[Slide]
) -> int:
"""Get the total combo between given beats. Include both ends."""
result = 0
# Single, Directional
result += sum(
beat_start <= single_or_directional.beat <= beat_end
for single_or_directional in note_list_single_directional
)
# Slide
result += sum(
beat_start <= connection.beat <= beat_end
for slide in note_list_slide
for connection in slide.connections if not connection.hidden
)
return result
def get_min_max_bpm(bpms: list[BPM]) -> tuple[float, float]:
"""Get the max and min BPM of a chart."""
bpms = sorted(bpms, key=lambda bpm: bpm.bpm)
return bpms[0].bpm, bpms[-1].bpm