506 lines
17 KiB
Python
506 lines
17 KiB
Python
import math
|
|
from collections import defaultdict
|
|
|
|
import numpy as np
|
|
import osu_file_parser as osu_parser
|
|
|
|
# --- Constants ---
|
|
|
|
BREAK_ZERO_THRESHOLD_MS = 400
|
|
GRAPH_RESAMPLE_INTERVAL_MS = 100
|
|
SMOOTH_SIGMA_MS = 800
|
|
|
|
|
|
# --- Helper Functions ---
|
|
|
|
def gaussian_filter1d(data, sigma, mode="constant", cval=0.0):
|
|
kernel_radius = int(4 * sigma + 0.5)
|
|
x = np.arange(-kernel_radius, kernel_radius + 1)
|
|
kernel = np.exp(-0.5 * (x / sigma) ** 2)
|
|
kernel /= kernel.sum()
|
|
if mode == "constant":
|
|
padded = np.pad(data, kernel_radius, mode="constant", constant_values=cval)
|
|
else:
|
|
padded = np.pad(data, kernel_radius, mode=mode)
|
|
return np.convolve(padded, kernel, mode="valid")
|
|
|
|
|
|
def cumulative_sum(x, f):
|
|
"""Vectorised exact cumulative integral of piecewise-constant f on sorted x."""
|
|
F = np.zeros(len(x))
|
|
F[1:] = np.cumsum(f[:-1] * np.diff(x))
|
|
return F
|
|
|
|
|
|
def smooth_on_corners(x, f, window, scale=1.0, mode="sum"):
|
|
"""Vectorised sliding-window integral of piecewise-constant f."""
|
|
x = np.asarray(x, dtype=float)
|
|
f = np.asarray(f, dtype=float)
|
|
F = cumulative_sum(x, f)
|
|
|
|
a = np.clip(x - window, x[0], x[-1])
|
|
b = np.clip(x + window, x[0], x[-1])
|
|
|
|
def _query_vec(q_arr):
|
|
idx = np.searchsorted(x, q_arr) - 1
|
|
idx = np.clip(idx, 0, len(x) - 2)
|
|
return F[idx] + f[idx] * (q_arr - x[idx])
|
|
|
|
val = _query_vec(b) - _query_vec(a)
|
|
|
|
if mode == "avg":
|
|
span = b - a
|
|
return np.where(span > 0, val / span, 0.0)
|
|
return scale * val
|
|
|
|
|
|
def interp_values(new_x, old_x, old_vals):
|
|
return np.interp(new_x, old_x, old_vals)
|
|
|
|
|
|
def step_interp(new_x, old_x, old_vals):
|
|
indices = np.searchsorted(old_x, new_x, side="right") - 1
|
|
indices = np.clip(indices, 0, len(old_vals) - 1)
|
|
return old_vals[indices]
|
|
|
|
|
|
def rescale_high(sr):
|
|
if sr <= 9:
|
|
return sr
|
|
return 9 + (sr - 9) / 1.2
|
|
|
|
|
|
# --- Preprocessing ---
|
|
|
|
def preprocess_file(file_path, mod):
|
|
p_obj = osu_parser.parser(file_path)
|
|
p_obj.process()
|
|
p = p_obj.get_parsed_data()
|
|
|
|
note_seq = []
|
|
for i in range(len(p[1])):
|
|
k = p[1][i]
|
|
h = p[2][i]
|
|
if mod == "DT":
|
|
h = int(math.floor(h * 2 / 3))
|
|
elif mod == "HT":
|
|
h = int(math.floor(h * 4 / 3))
|
|
note_seq.append((k, h))
|
|
|
|
x = 0.3 * ((64.5 - math.ceil(p[5] * 3)) / 500) ** 0.5
|
|
x = min(x, 0.6 * (x - 0.09) + 0.09)
|
|
note_seq.sort(key=lambda tup: (tup[1], tup[0]))
|
|
|
|
note_dict = defaultdict(list)
|
|
for tup in note_seq:
|
|
note_dict[tup[0]].append(tup)
|
|
note_seq_by_column = sorted(note_dict.values(), key=lambda lst: lst[0][0])
|
|
|
|
K = p[0]
|
|
T = max(n[1] for n in note_seq) + 1
|
|
|
|
return x, K, T, note_seq, note_seq_by_column
|
|
|
|
|
|
# --- Corner Computation ---
|
|
|
|
def get_corners(T, note_seq):
|
|
corners_base = set()
|
|
for _, h in note_seq:
|
|
corners_base.update([h, h + 501, h - 499, h + 1])
|
|
corners_base.update([0, T])
|
|
corners_base = sorted(s for s in corners_base if 0 <= s <= T)
|
|
|
|
corners_A = set()
|
|
for _, h in note_seq:
|
|
corners_A.update([h, h + 1000, h - 1000])
|
|
corners_A.update([0, T])
|
|
corners_A = sorted(s for s in corners_A if 0 <= s <= T)
|
|
|
|
all_corners = sorted(set(corners_base) | set(corners_A))
|
|
return (
|
|
np.array(all_corners, dtype=float),
|
|
np.array(corners_base, dtype=float),
|
|
np.array(corners_A, dtype=float),
|
|
)
|
|
|
|
|
|
# --- Key Usage ---
|
|
|
|
def get_key_usage(K, T, note_seq, base_corners):
|
|
key_usage = {k: np.zeros(len(base_corners), dtype=bool) for k in range(K)}
|
|
for k, h in note_seq:
|
|
start = max(h - 150, 0)
|
|
end = min(h + 150, T - 1)
|
|
li = np.searchsorted(base_corners, start, side="left")
|
|
ri = np.searchsorted(base_corners, end, side="left")
|
|
key_usage[k][li:ri] = True
|
|
return key_usage
|
|
|
|
|
|
def get_key_usage_400(K, T, note_seq, base_corners):
|
|
key_usage_400 = {k: np.zeros(len(base_corners), dtype=float) for k in range(K)}
|
|
for k, h in note_seq:
|
|
start = max(h, 0)
|
|
li = np.searchsorted(base_corners, start - 400, side="left")
|
|
ri = np.searchsorted(base_corners, start + 400, side="left")
|
|
mid = np.searchsorted(base_corners, start, side="left")
|
|
|
|
key_usage_400[k][mid] += 3.75
|
|
for idx_range in [np.arange(li, mid), np.arange(mid + 1, ri)]:
|
|
key_usage_400[k][idx_range] += 3.75 - 3.75 / 400 ** 2 * (base_corners[idx_range] - start) ** 2
|
|
return key_usage_400
|
|
|
|
|
|
# --- Difficulty Components ---
|
|
|
|
def compute_anchor(K, key_usage_400, base_corners):
|
|
counts = np.stack([key_usage_400[k] for k in range(K)], axis=1)
|
|
counts = np.sort(counts, axis=1)[:, ::-1]
|
|
|
|
nonzero_mask = counts > 0
|
|
n_nz = nonzero_mask.sum(axis=1)
|
|
|
|
c0 = counts[:, :-1]
|
|
c1 = counts[:, 1:]
|
|
safe_c0 = np.where(c0 > 0, c0, 1.0)
|
|
ratio = np.where(c0 > 0, c1 / safe_c0, 0.0)
|
|
weight = 1 - 4 * (0.5 - ratio) ** 2
|
|
|
|
pair_valid = nonzero_mask[:, :-1] & nonzero_mask[:, 1:]
|
|
walk = np.sum(np.where(pair_valid, c0 * weight, 0.0), axis=1)
|
|
max_walk = np.sum(np.where(pair_valid, c0, 0.0), axis=1)
|
|
|
|
raw_anchor = np.where(n_nz > 1, walk / np.maximum(max_walk, 1e-9), 0.0)
|
|
return 1 + np.minimum(raw_anchor - 0.18, 5 * (raw_anchor - 0.22) ** 3)
|
|
|
|
|
|
def compute_Jbar(K, T, x, note_seq_by_column, base_corners):
|
|
def jack_nerfer(delta):
|
|
return 1 - 7e-5 * (0.15 + np.abs(delta - 0.08)) ** (-4)
|
|
|
|
J_ks = {k: np.zeros(len(base_corners)) for k in range(K)}
|
|
delta_ks = {k: np.full(len(base_corners), 1e9) for k in range(K)}
|
|
|
|
for k in range(K):
|
|
notes = note_seq_by_column[k]
|
|
if len(notes) < 2:
|
|
continue
|
|
starts = np.array([n[1] for n in notes[:-1]], dtype=float)
|
|
ends = np.array([n[1] for n in notes[1:]], dtype=float)
|
|
deltas = 0.001 * (ends - starts)
|
|
vals = deltas ** -1 * (deltas + 0.11 * x ** 0.25) ** -1 * jack_nerfer(deltas)
|
|
|
|
for start, end, delta, val in zip(starts, ends, deltas, vals):
|
|
li = np.searchsorted(base_corners, start, side="left")
|
|
ri = np.searchsorted(base_corners, end, side="left")
|
|
if ri > li:
|
|
J_ks[k][li:ri] = val
|
|
delta_ks[k][li:ri] = delta
|
|
|
|
Jbar_ks = {
|
|
k: smooth_on_corners(base_corners, J_ks[k], window=500, scale=0.001, mode="sum")
|
|
for k in range(K)
|
|
}
|
|
|
|
Jbar_stack = np.stack([Jbar_ks[k] for k in range(K)], axis=0)
|
|
delta_stack = np.stack([delta_ks[k] for k in range(K)], axis=0)
|
|
weights = 1.0 / delta_stack
|
|
num = np.sum(np.maximum(Jbar_stack, 0) ** 5 * weights, axis=0)
|
|
den = np.sum(weights, axis=0)
|
|
Jbar = (num / np.maximum(den, 1e-9)) ** 0.2
|
|
|
|
return delta_ks, Jbar
|
|
|
|
|
|
def compute_Xbar(K, T, x, note_seq_by_column, active_columns, base_corners):
|
|
cross_matrix = [
|
|
[-1],
|
|
[0.075, 0.075],
|
|
[0.125, 0.05, 0.125],
|
|
[0.125, 0.125, 0.125, 0.125],
|
|
[0.175, 0.25, 0.05, 0.25, 0.175],
|
|
[0.175, 0.25, 0.175, 0.175, 0.25, 0.175],
|
|
[0.225, 0.35, 0.25, 0.05, 0.25, 0.35, 0.225],
|
|
[0.225, 0.35, 0.25, 0.225, 0.225, 0.25, 0.35, 0.225],
|
|
[0.275, 0.45, 0.35, 0.25, 0.05, 0.25, 0.35, 0.45, 0.275],
|
|
[0.275, 0.45, 0.35, 0.25, 0.275, 0.275, 0.25, 0.35, 0.45, 0.275],
|
|
[0.325, 0.55, 0.45, 0.35, 0.25, 0.05, 0.25, 0.35, 0.45, 0.55, 0.325],
|
|
]
|
|
cross_coeff = cross_matrix[K]
|
|
X_ks = {k: np.zeros(len(base_corners)) for k in range(K + 1)}
|
|
fast_cross = {k: np.zeros(len(base_corners)) for k in range(K + 1)}
|
|
|
|
for k in range(K + 1):
|
|
if k == 0:
|
|
notes_in_pair = note_seq_by_column[0]
|
|
elif k == K:
|
|
notes_in_pair = note_seq_by_column[K - 1]
|
|
else:
|
|
notes_in_pair = sorted(
|
|
note_seq_by_column[k - 1] + note_seq_by_column[k], key=lambda t: t[1]
|
|
)
|
|
|
|
for i in range(1, len(notes_in_pair)):
|
|
start = notes_in_pair[i - 1][1]
|
|
end = notes_in_pair[i][1]
|
|
li = np.searchsorted(base_corners, start, side="left")
|
|
ri = np.searchsorted(base_corners, end, side="left")
|
|
if ri <= li:
|
|
continue
|
|
|
|
delta = 0.001 * (notes_in_pair[i][1] - notes_in_pair[i - 1][1])
|
|
val = 0.16 * max(x, delta) ** -2
|
|
|
|
left_inactive = (k - 1) not in active_columns[li] and (k - 1) not in active_columns[ri]
|
|
right_inactive = k not in active_columns[li] and k not in active_columns[ri]
|
|
if left_inactive or right_inactive:
|
|
val *= 1 - cross_coeff[k]
|
|
|
|
X_ks[k][li:ri] = val
|
|
fast_cross[k][li:ri] = max(0, 0.4 * max(delta, 0.06, 0.75 * x) ** -2 - 80)
|
|
|
|
X_base = np.array([
|
|
sum(X_ks[k][i] * cross_coeff[k] for k in range(K + 1)) +
|
|
sum(
|
|
np.sqrt(fast_cross[k][i] * cross_coeff[k] * fast_cross[k + 1][i] * cross_coeff[k + 1])
|
|
for k in range(K)
|
|
)
|
|
for i in range(len(base_corners))
|
|
])
|
|
|
|
return smooth_on_corners(base_corners, X_base, window=500, scale=0.001, mode="sum")
|
|
|
|
|
|
def compute_Pbar(K, T, x, note_seq, anchor, base_corners):
|
|
def stream_booster(delta):
|
|
bpm = np.clip(7.5 / delta, 0, 420)
|
|
primary = 0.10 / (1 + np.exp(-0.06 * (bpm - 175)))
|
|
secondary = np.where(
|
|
(bpm >= 200) & (bpm <= 350),
|
|
0.30 * (1 - np.exp(-0.02 * (bpm - 200))),
|
|
0.0,
|
|
)
|
|
return 1 + primary + secondary
|
|
|
|
P_step = np.zeros(len(base_corners))
|
|
|
|
for i in range(len(note_seq) - 1):
|
|
h_l = note_seq[i][1]
|
|
h_r = note_seq[i + 1][1]
|
|
delta_time = h_r - h_l
|
|
|
|
if delta_time < 1e-9:
|
|
spike = 1000 * (0.02 * (4 / x - 24)) ** 0.25
|
|
li = np.searchsorted(base_corners, h_l, side="left")
|
|
ri = np.searchsorted(base_corners, h_l, side="right")
|
|
if ri > li:
|
|
P_step[li:ri] += spike
|
|
continue
|
|
|
|
li = np.searchsorted(base_corners, h_l, side="left")
|
|
ri = np.searchsorted(base_corners, h_r, side="left")
|
|
if ri <= li:
|
|
continue
|
|
|
|
delta = 0.001 * delta_time
|
|
b_val = stream_booster(delta)
|
|
base_inc = (0.08 * x ** -1 * (1 - 24 * x ** -1 * (x / 6) ** 2)) ** 0.25
|
|
|
|
if delta < 2 * x / 3:
|
|
inc = delta ** -1 * (0.08 * x ** -1 * (1 - 24 * x ** -1 * (delta - x / 2) ** 2)) ** 0.25 * max(b_val, 1)
|
|
else:
|
|
inc = delta ** -1 * base_inc * max(b_val, 1)
|
|
|
|
seg_anchor = anchor[li:ri]
|
|
P_step[li:ri] += np.minimum(inc * seg_anchor, np.maximum(inc, inc * 2 - 10))
|
|
|
|
return smooth_on_corners(base_corners, P_step, window=500, scale=0.001, mode="sum")
|
|
|
|
|
|
def compute_Abar(K, T, x, note_seq_by_column, active_columns, delta_ks, A_corners, base_corners):
|
|
dks = {k: np.zeros(len(base_corners)) for k in range(K - 1)}
|
|
for i in range(len(base_corners)):
|
|
cols = active_columns[i]
|
|
for j in range(len(cols) - 1):
|
|
k0, k1 = cols[j], cols[j + 1]
|
|
dks[k0][i] = abs(delta_ks[k0][i] - delta_ks[k1][i]) + 0.4 * max(
|
|
0, max(delta_ks[k0][i], delta_ks[k1][i]) - 0.11
|
|
)
|
|
|
|
A_step = np.ones(len(A_corners))
|
|
bc_idx = np.clip(np.searchsorted(base_corners, A_corners), 0, len(base_corners) - 1)
|
|
|
|
for i in range(len(A_corners)):
|
|
idx = bc_idx[i]
|
|
cols = active_columns[idx]
|
|
for j in range(len(cols) - 1):
|
|
k0, k1 = cols[j], cols[j + 1]
|
|
d_val = dks[k0][idx]
|
|
dk0, dk1 = delta_ks[k0][idx], delta_ks[k1][idx]
|
|
if d_val < 0.02:
|
|
A_step[i] *= min(0.75 + 0.5 * max(dk0, dk1), 1)
|
|
elif d_val < 0.07:
|
|
A_step[i] *= min(0.65 + 5 * d_val + 0.5 * max(dk0, dk1), 1)
|
|
|
|
return smooth_on_corners(A_corners, A_step, window=250, mode="avg")
|
|
|
|
|
|
def compute_C_and_Ks(K, T, note_seq, key_usage, base_corners):
|
|
note_hit_times = np.array(sorted(n[1] for n in note_seq), dtype=float)
|
|
|
|
lo = np.searchsorted(note_hit_times, base_corners - 500, side="left")
|
|
hi = np.searchsorted(note_hit_times, base_corners + 500, side="left")
|
|
C_step = (hi - lo).astype(float)
|
|
|
|
Ks_step = np.maximum(
|
|
np.stack([key_usage[k] for k in range(K)], axis=0).sum(axis=0), 1
|
|
).astype(float)
|
|
|
|
return C_step, Ks_step
|
|
|
|
|
|
# --- Graph Post-Processing ---
|
|
|
|
def _apply_proximity_envelope(all_corners, D_all, note_seq):
|
|
if not note_seq:
|
|
return D_all.copy()
|
|
|
|
note_times = np.sort(np.array([float(h) for _, h in note_seq]))
|
|
PROXIMITY_FADE_MS = 500.0
|
|
|
|
idx = np.searchsorted(note_times, all_corners)
|
|
d_after = np.abs(note_times[np.clip(idx, 0, len(note_times) - 1)] - all_corners)
|
|
d_before = np.abs(note_times[np.clip(idx - 1, 0, len(note_times) - 1)] - all_corners)
|
|
d = np.minimum(d_after, d_before)
|
|
|
|
envelope = 0.5 * (1.0 + np.cos(np.pi * np.clip(d / PROXIMITY_FADE_MS, 0.0, 1.0)))
|
|
return D_all * envelope
|
|
|
|
|
|
def smooth_D_for_graph(all_corners, D_all, note_seq):
|
|
note_times = np.array(sorted(float(h) for _, h in note_seq), dtype=float)
|
|
|
|
t_start = float(all_corners[0])
|
|
t_end = float(all_corners[-1])
|
|
uniform_t = np.arange(t_start, t_end + GRAPH_RESAMPLE_INTERVAL_MS, GRAPH_RESAMPLE_INTERVAL_MS, dtype=float)
|
|
|
|
if len(note_times) > 0:
|
|
idx = np.searchsorted(note_times, uniform_t)
|
|
idx_after = np.clip(idx, 0, len(note_times) - 1)
|
|
idx_before = np.clip(idx - 1, 0, len(note_times) - 1)
|
|
dist = np.minimum(np.abs(uniform_t - note_times[idx_before]), np.abs(uniform_t - note_times[idx_after]))
|
|
break_mask = dist > BREAK_ZERO_THRESHOLD_MS
|
|
else:
|
|
break_mask = np.zeros(len(uniform_t), dtype=bool)
|
|
|
|
uniform_D = np.interp(uniform_t, all_corners, D_all)
|
|
uniform_D[break_mask] = 0.0
|
|
|
|
sigma_samples = SMOOTH_SIGMA_MS / GRAPH_RESAMPLE_INTERVAL_MS
|
|
uniform_result = gaussian_filter1d(uniform_D, sigma=sigma_samples, mode="constant", cval=0.0)
|
|
uniform_result[break_mask] = 0.0
|
|
|
|
return np.interp(all_corners, uniform_t, uniform_result)
|
|
|
|
|
|
# --- Main Entry Points ---
|
|
|
|
def calculate(file_path, mod):
|
|
x, K, T, note_seq, note_seq_by_column = preprocess_file(file_path, mod)
|
|
all_corners, base_corners, A_corners = get_corners(T, note_seq)
|
|
|
|
key_usage = get_key_usage(K, T, note_seq, base_corners)
|
|
active_columns = [[k for k in range(K) if key_usage[k][i]] for i in range(len(base_corners))]
|
|
key_usage_400 = get_key_usage_400(K, T, note_seq, base_corners)
|
|
anchor = compute_anchor(K, key_usage_400, base_corners)
|
|
|
|
delta_ks, Jbar = compute_Jbar(K, T, x, note_seq_by_column, base_corners)
|
|
Jbar = interp_values(all_corners, base_corners, Jbar)
|
|
|
|
Xbar = compute_Xbar(K, T, x, note_seq_by_column, active_columns, base_corners)
|
|
Xbar = interp_values(all_corners, base_corners, Xbar)
|
|
|
|
Pbar = compute_Pbar(K, T, x, note_seq, anchor, base_corners)
|
|
Pbar = interp_values(all_corners, base_corners, Pbar)
|
|
|
|
Abar = compute_Abar(K, T, x, note_seq_by_column, active_columns, delta_ks, A_corners, base_corners)
|
|
Abar = interp_values(all_corners, A_corners, Abar)
|
|
|
|
C_step, Ks_step = compute_C_and_Ks(K, T, note_seq, key_usage, base_corners)
|
|
C_arr = step_interp(all_corners, base_corners, C_step)
|
|
Ks_arr = step_interp(all_corners, base_corners, Ks_step)
|
|
|
|
S_all = (
|
|
(0.4 * (Abar ** (3 / Ks_arr) * np.minimum(Jbar, 8 + 0.85 * Jbar)) ** 1.5) +
|
|
(0.6 * (Abar ** (2 / 3) * (0.8 * Pbar)) ** 1.5)
|
|
) ** (2 / 3)
|
|
T_all = (Abar ** (3 / Ks_arr) * Xbar) / (Xbar + S_all + 1)
|
|
D_all = 2.7 * (S_all ** 0.5) * (T_all ** 1.5) + S_all * 0.27
|
|
|
|
gaps = np.empty_like(all_corners, dtype=float)
|
|
gaps[0] = (all_corners[1] - all_corners[0]) / 2.0
|
|
gaps[-1] = (all_corners[-1] - all_corners[-2]) / 2.0
|
|
gaps[1:-1] = (all_corners[2:] - all_corners[:-2]) / 2.0
|
|
|
|
effective_weights = C_arr * gaps
|
|
sorted_indices = np.argsort(D_all)
|
|
D_sorted = D_all[sorted_indices]
|
|
w_sorted = effective_weights[sorted_indices]
|
|
|
|
cum_weights = np.cumsum(w_sorted)
|
|
norm_cum_weights = cum_weights / cum_weights[-1]
|
|
|
|
target_percentiles = np.array([0.945, 0.935, 0.925, 0.915, 0.845, 0.835, 0.825, 0.815])
|
|
indices = np.searchsorted(norm_cum_weights, target_percentiles, side="left")
|
|
|
|
percentile_93 = np.mean(D_sorted[indices[:4]])
|
|
percentile_83 = np.mean(D_sorted[indices[4:8]])
|
|
weighted_mean = (np.sum(D_sorted ** 5 * w_sorted) / np.sum(w_sorted)) ** 0.2
|
|
|
|
SR = 0.88 * percentile_93 * 0.25 + 0.94 * percentile_83 * 0.2 + weighted_mean * 0.55
|
|
total_notes = len(note_seq)
|
|
SR *= total_notes / (total_notes + 60)
|
|
SR = rescale_high(SR) * 0.975
|
|
|
|
D_pre = _apply_proximity_envelope(all_corners, D_all, note_seq)
|
|
D_graph = smooth_D_for_graph(all_corners, D_pre, note_seq)
|
|
|
|
return (
|
|
SR,
|
|
all_corners,
|
|
D_graph,
|
|
{
|
|
"Pressing Intensity": Pbar,
|
|
"Unevenness": Abar,
|
|
"Same-Column Pressure": Jbar,
|
|
"Cross-Column Pressure": Xbar,
|
|
},
|
|
)
|
|
|
|
|
|
def factor_averages(times, factors):
|
|
times = np.asarray(times, dtype=float)
|
|
names = list(factors.keys())
|
|
matrix = np.stack([factors[n] for n in names], axis=0)
|
|
integrals = np.trapezoid(matrix, times, axis=1)
|
|
duration = times[-1] - times[0]
|
|
return {n: float(integrals[i] / duration) for i, n in enumerate(names)}
|
|
|
|
|
|
def parse_hitobjects(file_path, mod="NM"):
|
|
p_obj = osu_parser.parser(file_path)
|
|
p_obj.process()
|
|
p = p_obj.get_parsed_data()
|
|
|
|
hitobjects = []
|
|
for i in range(len(p[1])):
|
|
x = p[1][i]
|
|
time = p[2][i]
|
|
if mod == "DT":
|
|
time *= 2 / 3
|
|
elif mod == "HT":
|
|
time *= 4 / 3
|
|
hitobjects.append({"x": x, "time": time})
|
|
|
|
return hitobjects |