mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 11:40:13 +01:00
Add branching. why not
This commit is contained in:
@@ -4,10 +4,9 @@ import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import time
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
from libs.tja import TJAParser
|
||||
from libs.tja import NoteList, TJAParser
|
||||
from libs.utils import get_config, global_data
|
||||
|
||||
|
||||
@@ -113,20 +112,19 @@ def build_song_hashes(output_dir=Path("cache")):
|
||||
tja_path_str = str(tja_path)
|
||||
current_modified = tja_path.stat().st_mtime
|
||||
tja = TJAParser(tja_path)
|
||||
all_notes = deque()
|
||||
all_bars = deque()
|
||||
all_notes = NoteList()
|
||||
diff_hashes = dict()
|
||||
|
||||
for diff in tja.metadata.course_data:
|
||||
diff_notes, _, diff_bars = TJAParser.notes_to_position(TJAParser(tja.file_path), diff)
|
||||
diff_hashes[diff] = tja.hash_note_data(diff_notes, diff_bars)
|
||||
all_notes.extend(diff_notes)
|
||||
all_bars.extend(diff_bars)
|
||||
diff_notes, _, _, _ = TJAParser.notes_to_position(TJAParser(tja.file_path), diff)
|
||||
diff_hashes[diff] = tja.hash_note_data(diff_notes)
|
||||
all_notes.play_notes.extend(diff_notes.play_notes)
|
||||
all_notes.bars.extend(diff_notes.bars)
|
||||
|
||||
if all_notes == []:
|
||||
continue
|
||||
|
||||
hash_val = tja.hash_note_data(all_notes, all_bars)
|
||||
hash_val = tja.hash_note_data(all_notes)
|
||||
if hash_val not in song_hashes:
|
||||
song_hashes[hash_val] = []
|
||||
|
||||
@@ -222,14 +220,14 @@ def build_song_hashes(output_dir=Path("cache")):
|
||||
def process_tja_file(tja_file):
|
||||
"""Process a single TJA file and return hash or None if error"""
|
||||
tja = TJAParser(tja_file)
|
||||
all_notes = []
|
||||
all_notes = NoteList()
|
||||
for diff in tja.metadata.course_data:
|
||||
all_notes.extend(
|
||||
TJAParser.notes_to_position(TJAParser(tja.file_path), diff)
|
||||
)
|
||||
notes, _, _, _ = TJAParser.notes_to_position(TJAParser(tja.file_path), diff)
|
||||
all_notes.play_notes.extend(notes.play_notes)
|
||||
all_notes.bars.extend(notes.bars)
|
||||
if all_notes == []:
|
||||
return ''
|
||||
hash = tja.hash_note_data(all_notes[0], all_notes[2])
|
||||
hash = tja.hash_note_data(all_notes)
|
||||
return hash
|
||||
|
||||
def get_japanese_songs_for_version(csv_file_path, version_column):
|
||||
|
||||
328
libs/tja.py
328
libs/tja.py
@@ -33,10 +33,21 @@ class Note:
|
||||
bpm: float = field(init=False)
|
||||
gogo_time: bool = field(init=False)
|
||||
moji: int = field(init=False)
|
||||
is_branch_start: bool = field(init=False)
|
||||
branch_params: str = 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
|
||||
|
||||
@@ -112,12 +123,32 @@ class Balloon(Note):
|
||||
hash_string = str(field_values)
|
||||
return hash_string.encode('utf-8')
|
||||
|
||||
@dataclass
|
||||
class NoteList:
|
||||
play_notes: list[Note | Drumroll | Balloon] = field(default_factory=lambda: [])
|
||||
draw_notes: list[Note | Drumroll | Balloon] = field(default_factory=lambda: [])
|
||||
bars: list[Note] = field(default_factory=lambda: [])
|
||||
|
||||
def __add__(self, other: 'NoteList') -> 'NoteList':
|
||||
return NoteList(
|
||||
play_notes=self.play_notes + other.play_notes,
|
||||
draw_notes=self.draw_notes + other.draw_notes,
|
||||
bars=self.bars + other.bars
|
||||
)
|
||||
|
||||
def __iadd__(self, other: 'NoteList') -> 'NoteList':
|
||||
self.play_notes += other.play_notes
|
||||
self.draw_notes += other.draw_notes
|
||||
self.bars += other.bars
|
||||
return self
|
||||
|
||||
@dataclass
|
||||
class CourseData:
|
||||
level: int = 0
|
||||
balloon: list[int] = field(default_factory=lambda: [])
|
||||
scoreinit: list[int] = field(default_factory=lambda: [])
|
||||
scorediff: int = 0
|
||||
is_branching: bool = False
|
||||
|
||||
@dataclass
|
||||
class TJAMetadata:
|
||||
@@ -141,16 +172,16 @@ class TJAEXData:
|
||||
new: bool = False
|
||||
|
||||
|
||||
def calculate_base_score(play_notes: deque[Note | Drumroll | Balloon]) -> int:
|
||||
def calculate_base_score(notes: NoteList) -> int:
|
||||
total_notes = 0
|
||||
balloon_count = 0
|
||||
drumroll_msec = 0
|
||||
for i in range(len(play_notes)):
|
||||
note = play_notes[i]
|
||||
if i < len(play_notes)-1:
|
||||
next_note = play_notes[i+1]
|
||||
for i in range(len(notes.play_notes)):
|
||||
note = notes.play_notes[i]
|
||||
if i < len(notes.play_notes)-1:
|
||||
next_note = notes.play_notes[i+1]
|
||||
else:
|
||||
next_note = play_notes[len(play_notes)-1]
|
||||
next_note = notes.play_notes[len(notes.play_notes)-1]
|
||||
if isinstance(note, Drumroll):
|
||||
drumroll_msec += (next_note.hit_ms - note.hit_ms)
|
||||
elif isinstance(note, Balloon):
|
||||
@@ -198,7 +229,9 @@ class TJAParser:
|
||||
current_diff = None # Track which difficulty we're currently processing
|
||||
|
||||
for item in self.data:
|
||||
if item.startswith("#") or item[0].isdigit():
|
||||
if item.startswith('#BRANCH') and current_diff is not None:
|
||||
self.metadata.course_data[current_diff].is_branching = True
|
||||
elif item.startswith("#") or item[0].isdigit():
|
||||
continue
|
||||
elif item.startswith('SUBTITLE'):
|
||||
region_code = 'en'
|
||||
@@ -407,9 +440,10 @@ class TJAParser:
|
||||
play_note_list[-3].moji = se_notes[1][2]
|
||||
|
||||
def notes_to_position(self, diff: int):
|
||||
play_note_list: list[Note | Drumroll | Balloon] = []
|
||||
draw_note_list: list[Note | Drumroll | Balloon] = []
|
||||
bar_list: list[Note] = []
|
||||
master_notes = NoteList()
|
||||
branch_m: list[NoteList] = []
|
||||
branch_e: list[NoteList] = []
|
||||
branch_n: list[NoteList] = []
|
||||
notes = self.data_to_notes(diff)
|
||||
balloon = self.metadata.course_data[diff].balloon.copy()
|
||||
count = 0
|
||||
@@ -420,19 +454,127 @@ class TJAParser:
|
||||
y_scroll_modifier = 0
|
||||
barline_display = True
|
||||
gogo_time = False
|
||||
skip_branch = False
|
||||
curr_note_list = master_notes.play_notes
|
||||
curr_draw_list = master_notes.draw_notes
|
||||
curr_bar_list = master_notes.bars
|
||||
start_branch_ms = 0
|
||||
start_branch_bpm = bpm
|
||||
start_branch_time_sig = time_signature
|
||||
start_branch_x_scroll = x_scroll_modifier
|
||||
start_branch_y_scroll = y_scroll_modifier
|
||||
start_branch_barline = barline_display
|
||||
start_branch_gogo = gogo_time
|
||||
branch_balloon_count = 0
|
||||
is_branching = False
|
||||
for bar in notes:
|
||||
#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)
|
||||
barline_added = False
|
||||
for part in bar:
|
||||
if part.startswith('#BRANCHSTART'):
|
||||
skip_branch = True
|
||||
start_branch_ms = self.current_ms
|
||||
start_branch_bpm = bpm
|
||||
start_branch_time_sig = time_signature
|
||||
start_branch_x_scroll = x_scroll_modifier
|
||||
start_branch_y_scroll = y_scroll_modifier
|
||||
start_branch_barline = barline_display
|
||||
start_branch_gogo = gogo_time
|
||||
branch_balloon_count = count
|
||||
branch_params = part[13:]
|
||||
|
||||
if branch_params[0] == 'r':
|
||||
# Helper function to find and set drumroll branch params
|
||||
def set_drumroll_branch_params(note_list, bar_list):
|
||||
for i in range(len(note_list)-1, -1, -1):
|
||||
if 5 <= note_list[i].type <= 7 or note_list[i].type == 9:
|
||||
drumroll_ms = note_list[i].hit_ms
|
||||
for bar_idx in range(len(bar_list)-1, -1, -1):
|
||||
if bar_list[bar_idx].hit_ms <= drumroll_ms:
|
||||
bar_list[bar_idx].branch_params = branch_params
|
||||
return True
|
||||
break
|
||||
return False
|
||||
|
||||
# Always try to set in master notes
|
||||
set_drumroll_branch_params(master_notes.play_notes, master_notes.bars)
|
||||
|
||||
# If we have existing branches, also apply to them
|
||||
if branch_m and len(branch_m) > 0:
|
||||
set_drumroll_branch_params(branch_m[-1].play_notes, branch_m[-1].bars)
|
||||
if branch_e and len(branch_e) > 0:
|
||||
set_drumroll_branch_params(branch_e[-1].play_notes, branch_e[-1].bars)
|
||||
if branch_n and len(branch_n) > 0:
|
||||
set_drumroll_branch_params(branch_n[-1].play_notes, branch_n[-1].bars)
|
||||
else:
|
||||
if len(curr_bar_list) > 1:
|
||||
curr_bar_list[-2].branch_params = branch_params
|
||||
elif len(curr_bar_list) > 0:
|
||||
curr_bar_list[-1].branch_params = branch_params
|
||||
|
||||
if branch_m and len(branch_m[-1].bars) > 1:
|
||||
branch_m[-1].bars[-2].branch_params = branch_params
|
||||
elif branch_m and len(branch_m[-1].bars) > 0:
|
||||
branch_m[-1].bars[-1].branch_params = branch_params
|
||||
if branch_e and len(branch_e[-1].bars) > 1:
|
||||
branch_e[-1].bars[-2].branch_params = branch_params
|
||||
elif branch_e and len(branch_e[-1].bars) > 0:
|
||||
branch_e[-1].bars[-1].branch_params = branch_params
|
||||
if branch_n and len(branch_n[-1].bars) > 1:
|
||||
branch_n[-1].bars[-2].branch_params = branch_params
|
||||
elif branch_n and len(branch_n[-1].bars) > 0:
|
||||
branch_n[-1].bars[-1].branch_params = branch_params
|
||||
if branch_m and len(branch_m[-1].bars) > 0:
|
||||
branch_m[-1].bars[-1].branch_params = branch_params
|
||||
continue
|
||||
elif part.startswith('#BRANCHEND'):
|
||||
curr_note_list = master_notes.play_notes
|
||||
curr_draw_list = master_notes.draw_notes
|
||||
curr_bar_list = master_notes.bars
|
||||
continue
|
||||
if part == '#M':
|
||||
skip_branch = False
|
||||
branch_m.append(NoteList())
|
||||
curr_note_list = branch_m[-1].play_notes
|
||||
curr_draw_list = branch_m[-1].draw_notes
|
||||
curr_bar_list = branch_m[-1].bars
|
||||
self.current_ms = start_branch_ms
|
||||
bpm = start_branch_bpm
|
||||
time_signature = start_branch_time_sig
|
||||
x_scroll_modifier = start_branch_x_scroll
|
||||
y_scroll_modifier = start_branch_y_scroll
|
||||
barline_display = start_branch_barline
|
||||
gogo_time = start_branch_gogo
|
||||
count = branch_balloon_count
|
||||
is_branching = True
|
||||
continue
|
||||
if skip_branch:
|
||||
elif part == '#E':
|
||||
branch_e.append(NoteList())
|
||||
curr_note_list = branch_e[-1].play_notes
|
||||
curr_draw_list = branch_e[-1].draw_notes
|
||||
curr_bar_list = branch_e[-1].bars
|
||||
self.current_ms = start_branch_ms
|
||||
bpm = start_branch_bpm
|
||||
time_signature = start_branch_time_sig
|
||||
x_scroll_modifier = start_branch_x_scroll
|
||||
y_scroll_modifier = start_branch_y_scroll
|
||||
barline_display = start_branch_barline
|
||||
gogo_time = start_branch_gogo
|
||||
count = branch_balloon_count
|
||||
is_branching = True
|
||||
continue
|
||||
elif part == '#N':
|
||||
branch_n.append(NoteList())
|
||||
curr_note_list = branch_n[-1].play_notes
|
||||
curr_draw_list = branch_n[-1].draw_notes
|
||||
curr_bar_list = branch_n[-1].bars
|
||||
self.current_ms = start_branch_ms
|
||||
bpm = start_branch_bpm
|
||||
time_signature = start_branch_time_sig
|
||||
x_scroll_modifier = start_branch_x_scroll
|
||||
y_scroll_modifier = start_branch_y_scroll
|
||||
barline_display = start_branch_barline
|
||||
gogo_time = start_branch_gogo
|
||||
count = branch_balloon_count
|
||||
is_branching = True
|
||||
continue
|
||||
if '#LYRIC' in part:
|
||||
continue
|
||||
@@ -445,74 +587,15 @@ class TJAParser:
|
||||
time_signature = float(part[9:divisor]) / float(part[divisor+1:])
|
||||
continue
|
||||
elif '#SCROLL' in part:
|
||||
# Extract the value after '#SCROLL '
|
||||
scroll_value = part[7:].strip() # Remove '#SCROLL' and whitespace
|
||||
|
||||
# Initialize default values
|
||||
x_scroll_modifier = 0
|
||||
y_scroll_modifier = 0
|
||||
|
||||
# Handle empty value
|
||||
if not scroll_value:
|
||||
continue
|
||||
|
||||
# Check if it's a complex number (contains 'i')
|
||||
scroll_value = part[7:]
|
||||
if 'i' in scroll_value:
|
||||
# Handle different imaginary number formats
|
||||
if scroll_value == 'i':
|
||||
x_scroll_modifier = 0
|
||||
y_scroll_modifier = 1
|
||||
elif scroll_value == '-i':
|
||||
x_scroll_modifier = 0
|
||||
y_scroll_modifier = -1
|
||||
elif scroll_value.endswith('i') or scroll_value.endswith('.i'):
|
||||
# Remove the 'i' or '.i' suffix
|
||||
if scroll_value.endswith('.i'):
|
||||
complex_part = scroll_value[:-2]
|
||||
else:
|
||||
complex_part = scroll_value[:-1]
|
||||
|
||||
# Look for + or - that separates real and imaginary parts
|
||||
# Find the rightmost + or - (excluding position 0 for negative numbers)
|
||||
plus_pos = complex_part.rfind('+')
|
||||
minus_pos = complex_part.rfind('-')
|
||||
|
||||
separator_pos = -1
|
||||
if plus_pos > 0: # Ignore + at position 0
|
||||
separator_pos = plus_pos
|
||||
if minus_pos > 0 and minus_pos > separator_pos: # Ignore - at position 0
|
||||
separator_pos = minus_pos
|
||||
|
||||
if separator_pos > 0:
|
||||
# Complex number like '1+i', '3+4i', '2-5i', '-1+2i', etc.
|
||||
real_part = complex_part[:separator_pos]
|
||||
imag_part = complex_part[separator_pos:]
|
||||
|
||||
x_scroll_modifier = float(real_part) if real_part else 0
|
||||
|
||||
# Handle imaginary part
|
||||
if imag_part == '+' or imag_part == '':
|
||||
y_scroll_modifier = 1
|
||||
elif imag_part == '-':
|
||||
y_scroll_modifier = -1
|
||||
else:
|
||||
y_scroll_modifier = float(imag_part)
|
||||
else:
|
||||
# Pure imaginary like '5i', '-3i', '2.5i'
|
||||
if complex_part == '' or complex_part == '+':
|
||||
y_scroll_modifier = 1
|
||||
elif complex_part == '-':
|
||||
y_scroll_modifier = -1
|
||||
else:
|
||||
y_scroll_modifier = float(complex_part)
|
||||
x_scroll_modifier = 0
|
||||
else:
|
||||
# 'i' is somewhere in the middle - invalid format
|
||||
continue
|
||||
normalized = scroll_value.replace('.i', 'j').replace('i', 'j')
|
||||
c = complex(normalized)
|
||||
x_scroll_modifier = c.real
|
||||
y_scroll_modifier = c.imag
|
||||
else:
|
||||
# Pure real number
|
||||
x_scroll_modifier = float(scroll_value)
|
||||
y_scroll_modifier = 0
|
||||
y_scroll_modifier = 0.0
|
||||
continue
|
||||
elif '#BPMCHANGE' in part:
|
||||
bpm = float(part[11:])
|
||||
@@ -555,7 +638,11 @@ class TJAParser:
|
||||
if barline_added:
|
||||
bar_line.display = False
|
||||
|
||||
bisect.insort(bar_list, bar_line, key=lambda x: x.load_ms)
|
||||
if is_branching:
|
||||
bar_line.is_branch_start = True
|
||||
is_branching = False
|
||||
|
||||
bisect.insort(curr_bar_list, bar_line, key=lambda x: x.load_ms)
|
||||
barline_added = True
|
||||
|
||||
#Empty bar is still a bar, otherwise start increment
|
||||
@@ -571,7 +658,7 @@ class TJAParser:
|
||||
if item == '0' or (not item.isdigit()):
|
||||
self.current_ms += increment
|
||||
continue
|
||||
if item == '9' and play_note_list and play_note_list[-1].type == 9:
|
||||
if item == '9' and curr_note_list and curr_note_list[-1].type == 9:
|
||||
self.current_ms += increment
|
||||
continue
|
||||
note = Note()
|
||||
@@ -600,33 +687,29 @@ class TJAParser:
|
||||
note = Balloon(note)
|
||||
note.count = 1 if not balloon else balloon.pop(0)
|
||||
elif item == '8':
|
||||
new_pixels_per_ms = play_note_list[-1].pixels_per_frame_x / (1000 / 60)
|
||||
new_pixels_per_ms = curr_note_list[-1].pixels_per_frame_x / (1000 / 60)
|
||||
if new_pixels_per_ms == 0:
|
||||
note.load_ms = note.hit_ms
|
||||
else:
|
||||
note.load_ms = note.hit_ms - (self.distance / new_pixels_per_ms)
|
||||
note.pixels_per_frame_x = play_note_list[-1].pixels_per_frame_x
|
||||
note.pixels_per_frame_x = curr_note_list[-1].pixels_per_frame_x
|
||||
self.current_ms += increment
|
||||
play_note_list.append(note)
|
||||
bisect.insort(draw_note_list, note, key=lambda x: x.load_ms)
|
||||
self.get_moji(play_note_list, ms_per_measure)
|
||||
curr_note_list.append(note)
|
||||
bisect.insort(curr_draw_list, note, key=lambda x: x.load_ms)
|
||||
self.get_moji(curr_note_list, ms_per_measure)
|
||||
index += 1
|
||||
if len(play_note_list) > 3:
|
||||
if isinstance(play_note_list[-2], Drumroll) and play_note_list[-1].type != 8:
|
||||
print(self.file_path, diff)
|
||||
print(bar)
|
||||
continue
|
||||
raise Exception(f"{play_note_list[-2]}")
|
||||
if hasattr(curr_bar_list[-1], 'branch_params'):
|
||||
print(curr_note_list[-1])
|
||||
# https://stackoverflow.com/questions/72899/how-to-sort-a-list-of-dictionaries-by-a-value-of-the-dictionary-in-python
|
||||
# Sorting by load_ms is necessary for drawing, as some notes appear on the
|
||||
# screen slower regardless of when they reach the judge circle
|
||||
# Bars can be sorted like this because they don't need hit detection
|
||||
return deque(play_note_list), deque(draw_note_list), deque(bar_list)
|
||||
return master_notes, branch_m, branch_e, branch_n
|
||||
|
||||
def hash_note_data(self, play_notes: deque[Note | Drumroll | Balloon], bars: deque[Note]):
|
||||
def hash_note_data(self, notes: NoteList):
|
||||
n = hashlib.sha256()
|
||||
list1 = list(play_notes)
|
||||
list2 = list(bars)
|
||||
list1 = notes.play_notes
|
||||
list2 = notes.bars
|
||||
merged: list[Note | Drumroll | Balloon] = []
|
||||
i = 0
|
||||
j = 0
|
||||
@@ -644,46 +727,47 @@ class TJAParser:
|
||||
|
||||
return n.hexdigest()
|
||||
|
||||
def modifier_speed(notes: deque[Note | Balloon | Drumroll], bars, value: float):
|
||||
notes = notes.copy()
|
||||
for note in notes:
|
||||
def modifier_speed(notes: NoteList, value: float):
|
||||
modded_notes = notes.draw_notes.copy()
|
||||
modded_bars = notes.bars.copy()
|
||||
for note in modded_notes:
|
||||
note.pixels_per_frame_x *= value
|
||||
note.load_ms = note.hit_ms - (866 / get_pixels_per_ms(note.pixels_per_frame_x))
|
||||
for bar in bars:
|
||||
for bar in modded_bars:
|
||||
bar.pixels_per_frame_x *= value
|
||||
bar.load_ms = bar.hit_ms - (866 / get_pixels_per_ms(bar.pixels_per_frame_x))
|
||||
return notes, bars
|
||||
return modded_notes, modded_bars
|
||||
|
||||
def modifier_display(notes: deque[Note | Balloon | Drumroll]):
|
||||
notes = notes.copy()
|
||||
for note in notes:
|
||||
def modifier_display(notes: NoteList):
|
||||
modded_notes = notes.draw_notes.copy()
|
||||
for note in modded_notes:
|
||||
note.display = False
|
||||
return notes
|
||||
return modded_notes
|
||||
|
||||
def modifier_inverse(notes: deque[Note | Balloon | Drumroll]):
|
||||
notes = notes.copy()
|
||||
def modifier_inverse(notes: NoteList):
|
||||
modded_notes = notes.play_notes.copy()
|
||||
type_mapping = {1: 2, 2: 1, 3: 4, 4: 3}
|
||||
for note in notes:
|
||||
for note in modded_notes:
|
||||
if note.type in type_mapping:
|
||||
note.type = type_mapping[note.type]
|
||||
return notes
|
||||
return modded_notes
|
||||
|
||||
def modifier_random(notes: deque[Note | Balloon | Drumroll], value: int):
|
||||
def modifier_random(notes: NoteList, value: int):
|
||||
#value: 1 == kimagure, 2 == detarame
|
||||
notes = notes.copy()
|
||||
percentage = int(len(notes) / 5) * value
|
||||
selected_notes = random.sample(range(len(notes)), percentage)
|
||||
modded_notes = notes.play_notes.copy()
|
||||
percentage = int(len(modded_notes) / 5) * value
|
||||
selected_notes = random.sample(range(len(modded_notes)), percentage)
|
||||
type_mapping = {1: 2, 2: 1, 3: 4, 4: 3}
|
||||
for i in selected_notes:
|
||||
if notes[i].type in type_mapping:
|
||||
notes[i].type = type_mapping[notes[i].type]
|
||||
return notes
|
||||
if modded_notes[i].type in type_mapping:
|
||||
modded_notes[i].type = type_mapping[modded_notes[i].type]
|
||||
return modded_notes
|
||||
|
||||
def apply_modifiers(notes: deque[Note | Balloon | Drumroll], draw_notes: deque[Note | Balloon | Drumroll], bars: deque[Note]):
|
||||
def apply_modifiers(notes: NoteList):
|
||||
if global_data.modifiers.display:
|
||||
draw_notes = modifier_display(draw_notes)
|
||||
draw_notes = modifier_display(notes)
|
||||
if global_data.modifiers.inverse:
|
||||
notes = modifier_inverse(notes)
|
||||
notes = modifier_random(notes, global_data.modifiers.random)
|
||||
draw_notes, bars = modifier_speed(draw_notes, bars, global_data.modifiers.speed)
|
||||
return notes, draw_notes, bars
|
||||
play_notes = modifier_inverse(notes)
|
||||
play_notes = modifier_random(notes, global_data.modifiers.random)
|
||||
draw_notes, bars = modifier_speed(notes, global_data.modifiers.speed)
|
||||
return deque(play_notes), deque(draw_notes), deque(bars)
|
||||
|
||||
@@ -2,7 +2,7 @@ import pyray as ray
|
||||
|
||||
from libs.utils import get_current_ms
|
||||
from libs.texture import tex
|
||||
from scenes.game import ComboAnnounce
|
||||
from scenes.game import BranchIndicator
|
||||
|
||||
|
||||
class DevScreen:
|
||||
@@ -16,7 +16,7 @@ class DevScreen:
|
||||
if not self.screen_init:
|
||||
self.screen_init = True
|
||||
tex.load_screen_textures('game')
|
||||
self.obj = ComboAnnounce(0, get_current_ms())
|
||||
self.obj = BranchIndicator()
|
||||
|
||||
def on_screen_end(self, next_screen: str):
|
||||
self.screen_init = False
|
||||
@@ -27,8 +27,14 @@ class DevScreen:
|
||||
self.obj.update(get_current_ms())
|
||||
if ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER):
|
||||
return self.on_screen_end('GAME')
|
||||
elif ray.is_key_pressed(ray.KeyboardKey.KEY_SPACE):
|
||||
self.obj = ComboAnnounce(100, get_current_ms())
|
||||
elif ray.is_key_pressed(ray.KeyboardKey.KEY_UP):
|
||||
self.obj.level_up('master')
|
||||
elif ray.is_key_pressed(ray.KeyboardKey.KEY_DOWN):
|
||||
self.obj.level_down('expert')
|
||||
elif ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT):
|
||||
self.obj.level_up('expert')
|
||||
elif ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT):
|
||||
self.obj.level_down('normal')
|
||||
|
||||
def draw(self):
|
||||
ray.draw_rectangle(0, 0, 1280, 720, ray.GREEN)
|
||||
|
||||
199
scenes/game.py
199
scenes/game.py
@@ -17,6 +17,7 @@ from libs.tja import (
|
||||
Balloon,
|
||||
Drumroll,
|
||||
Note,
|
||||
NoteList,
|
||||
TJAParser,
|
||||
apply_modifiers,
|
||||
calculate_base_score,
|
||||
@@ -127,8 +128,8 @@ class GameScreen:
|
||||
return
|
||||
with sqlite3.connect('scores.db') as con:
|
||||
cursor = con.cursor()
|
||||
notes, _, bars = TJAParser.notes_to_position(TJAParser(self.tja.file_path), self.player_1.difficulty)
|
||||
hash = self.tja.hash_note_data(notes, bars)
|
||||
notes, _, _, _ = TJAParser.notes_to_position(TJAParser(self.tja.file_path), self.player_1.difficulty)
|
||||
hash = self.tja.hash_note_data(notes)
|
||||
check_query = "SELECT score FROM Scores WHERE hash = ? LIMIT 1"
|
||||
cursor.execute(check_query, (hash,))
|
||||
result = cursor.fetchone()
|
||||
@@ -231,16 +232,22 @@ class Player:
|
||||
self.visual_offset = global_data.config["general"]["visual_offset"]
|
||||
|
||||
if tja is not None:
|
||||
play_notes, self.draw_note_list, self.draw_bar_list = tja.notes_to_position(self.difficulty)
|
||||
play_notes, self.draw_note_list, self.draw_bar_list = apply_modifiers(play_notes, self.draw_note_list, self.draw_bar_list)
|
||||
notes, self.branch_m, self.branch_e, self.branch_n = tja.notes_to_position(self.difficulty)
|
||||
self.play_notes, self.draw_note_list, self.draw_bar_list = apply_modifiers(notes)
|
||||
else:
|
||||
play_notes, self.draw_note_list, self.draw_bar_list = deque(), deque(), deque()
|
||||
self.play_notes, self.draw_note_list, self.draw_bar_list = deque(), deque(), deque()
|
||||
notes = NoteList()
|
||||
|
||||
self.don_notes = deque([note for note in play_notes if note.type in {1, 3}])
|
||||
self.kat_notes = deque([note for note in play_notes if note.type in {2, 4}])
|
||||
self.other_notes = deque([note for note in play_notes if note.type not in {1, 2, 3, 4}])
|
||||
self.total_notes = len([note for note in play_notes if 0 < note.type < 5])
|
||||
self.base_score = calculate_base_score(play_notes)
|
||||
self.don_notes = deque([note for note in self.play_notes if note.type in {1, 3}])
|
||||
self.kat_notes = deque([note for note in self.play_notes if note.type in {2, 4}])
|
||||
self.other_notes = deque([note for note in self.play_notes if note.type not in {1, 2, 3, 4}])
|
||||
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.current_bars: list[Note] = []
|
||||
@@ -249,8 +256,12 @@ class Player:
|
||||
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 = play_notes[0].bpm if play_notes else 120
|
||||
self.bpm = self.play_notes[0].bpm if self.play_notes else 120
|
||||
|
||||
#Score management
|
||||
self.good_count = 0
|
||||
@@ -275,7 +286,8 @@ class Player:
|
||||
self.score_counter = ScoreCounter(self.score)
|
||||
self.gogo_time: Optional[GogoTime] = None
|
||||
self.combo_announce = ComboAnnounce(self.combo, 0)
|
||||
self.is_gogo_time = play_notes[0].gogo_time if play_notes else False
|
||||
self.branch_indicator = BranchIndicator() if tja and tja.metadata.course_data[self.difficulty].is_branching else None
|
||||
self.is_gogo_time = False
|
||||
plate_info = global_data.config['nameplate']
|
||||
self.nameplate = Nameplate(plate_info['name'], plate_info['title'], global_data.player_num, plate_info['dan'], plate_info['gold'])
|
||||
self.chara = Chara2D(player_number - 1, self.bpm)
|
||||
@@ -292,6 +304,22 @@ class Player:
|
||||
self.autoplay_hit_side = 'L'
|
||||
self.last_subdivision = -1
|
||||
|
||||
def merge_branch_section(self, branch_section: NoteList, current_ms: float):
|
||||
self.play_notes.extend(branch_section.play_notes)
|
||||
self.draw_note_list.extend(branch_section.draw_notes)
|
||||
self.draw_bar_list.extend(branch_section.bars)
|
||||
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_bar_list = deque(sorted(self.draw_bar_list, key=lambda x: x.load_ms))
|
||||
timing_threshold = current_ms - Player.TIMING_BAD
|
||||
total_don = [note for note in self.play_notes if note.type in {1, 3}]
|
||||
total_kat = [note for note in self.play_notes if note.type in {2, 4}]
|
||||
total_other = [note for note in self.play_notes if note.type not in {1, 2, 3, 4}]
|
||||
|
||||
self.don_notes = deque([note for note in total_don if note.hit_ms > timing_threshold])
|
||||
self.kat_notes = deque([note for note in total_kat if note.hit_ms > timing_threshold])
|
||||
self.other_notes = deque([note for note in total_other if note.hit_ms > timing_threshold])
|
||||
|
||||
def get_result_score(self):
|
||||
return self.score, self.good_count, self.ok_count, self.bad_count, self.max_combo, self.total_drumroll
|
||||
|
||||
@@ -334,7 +362,53 @@ class Player:
|
||||
if position >= removal_threshold:
|
||||
bars_to_keep.append(bar)
|
||||
self.current_bars = bars_to_keep
|
||||
if self.current_bars and hasattr(self.current_bars[-1], 'branch_params'):
|
||||
self.branch_condition, e_req, m_req = self.current_bars[-1].branch_params.split(',')
|
||||
delattr(self.current_bars[-1], 'branch_params')
|
||||
e_req = int(e_req)
|
||||
m_req = int(m_req)
|
||||
if not self.is_branch:
|
||||
self.is_branch = True
|
||||
if self.branch_condition == 'r':
|
||||
end_time = self.branch_m[0].bars[0].load_ms
|
||||
end_roll = -1
|
||||
|
||||
note_lists = [
|
||||
self.current_notes_draw,
|
||||
self.branch_n[0].draw_notes if self.branch_n else [],
|
||||
self.branch_e[0].draw_notes if self.branch_e else [],
|
||||
self.branch_m[0].draw_notes if self.branch_m else [],
|
||||
self.draw_note_list if self.draw_note_list else []
|
||||
]
|
||||
|
||||
end_roll = -1
|
||||
for notes in note_lists:
|
||||
for i in range(len(notes)-1, -1, -1):
|
||||
if notes[i].type == 8 and notes[i].hit_ms <= end_time:
|
||||
end_roll = notes[i].hit_ms
|
||||
break
|
||||
if end_roll != -1:
|
||||
break
|
||||
self.curr_branch_reqs = [e_req, m_req, end_roll, 0]
|
||||
elif self.branch_condition == 'p':
|
||||
start_time = self.current_bars[0].hit_ms if self.current_bars else self.current_bars[-1].hit_ms
|
||||
branch_start_time = self.branch_m[0].bars[0].load_ms
|
||||
|
||||
note_lists = [
|
||||
self.current_notes_draw,
|
||||
self.branch_n[0].draw_notes if self.branch_n else [],
|
||||
self.branch_e[0].draw_notes if self.branch_e else [],
|
||||
self.branch_m[0].draw_notes if self.branch_m else [],
|
||||
self.draw_note_list if self.draw_note_list else []
|
||||
]
|
||||
|
||||
seen_notes = set()
|
||||
for notes in note_lists:
|
||||
for note in notes:
|
||||
if note.type <= 4 and start_time <= note.hit_ms < branch_start_time:
|
||||
seen_notes.add(note)
|
||||
|
||||
self.curr_branch_reqs = [e_req, m_req, branch_start_time, len(seen_notes)]
|
||||
def play_note_manager(self, current_ms: float, background: Optional[Background]):
|
||||
if self.don_notes and self.don_notes[0].hit_ms + Player.TIMING_BAD < current_ms:
|
||||
self.combo = 0
|
||||
@@ -343,6 +417,8 @@ class Player:
|
||||
self.bad_count += 1
|
||||
self.gauge.add_bad()
|
||||
self.don_notes.popleft()
|
||||
if self.is_branch and self.branch_condition == 'p':
|
||||
self.branch_condition_count -= 1
|
||||
|
||||
if self.kat_notes and self.kat_notes[0].hit_ms + Player.TIMING_BAD < current_ms:
|
||||
self.combo = 0
|
||||
@@ -351,6 +427,8 @@ class Player:
|
||||
self.bad_count += 1
|
||||
self.gauge.add_bad()
|
||||
self.kat_notes.popleft()
|
||||
if self.is_branch and self.branch_condition == 'p':
|
||||
self.branch_condition_count -= 1
|
||||
|
||||
if not self.other_notes:
|
||||
return
|
||||
@@ -441,6 +519,8 @@ class Player:
|
||||
self.draw_arc_list.append(NoteArc(drum_type, current_time, 1, drum_type == 3 or drum_type == 4, False))
|
||||
self.curr_drumroll_count += 1
|
||||
self.total_drumroll += 1
|
||||
if self.is_branch and self.branch_condition == 'r':
|
||||
self.branch_condition_count += 1
|
||||
if background is not None:
|
||||
background.add_renda()
|
||||
self.score += 100
|
||||
@@ -529,6 +609,8 @@ class Player:
|
||||
self.base_score_list.append(ScoreCounterAnimation(self.player_number, self.base_score))
|
||||
self.note_correct(curr_note, current_time)
|
||||
self.gauge.add_good()
|
||||
if self.is_branch and self.branch_condition == 'p':
|
||||
self.branch_condition_count += 1
|
||||
if game_screen.background is not None:
|
||||
game_screen.background.add_chibi(False)
|
||||
|
||||
@@ -539,6 +621,8 @@ class Player:
|
||||
self.base_score_list.append(ScoreCounterAnimation(self.player_number, 10 * math.floor(self.base_score / 2 / 10)))
|
||||
self.note_correct(curr_note, current_time)
|
||||
self.gauge.add_ok()
|
||||
if self.is_branch and self.branch_condition == 'p':
|
||||
self.branch_condition_count += 0.5
|
||||
if game_screen.background is not None:
|
||||
game_screen.background.add_chibi(False)
|
||||
|
||||
@@ -640,6 +724,34 @@ class Player:
|
||||
audio.play_sound(game_screen.sound_kat)
|
||||
self.check_note(game_screen, 2, current_time)
|
||||
|
||||
def evaluate_branch(self, current_ms):
|
||||
e_req, m_req, end_time, total_notes = self.curr_branch_reqs
|
||||
if current_ms >= end_time:
|
||||
self.is_branch = False
|
||||
if self.branch_condition == 'p':
|
||||
self.branch_condition_count = min(int((self.branch_condition_count/total_notes)*100), 100)
|
||||
if self.branch_condition_count >= e_req and self.branch_condition_count < m_req:
|
||||
self.merge_branch_section(self.branch_e.pop(0), current_ms)
|
||||
if self.branch_indicator is not None and self.branch_indicator.difficulty != 'expert':
|
||||
if self.branch_indicator.difficulty == 'master':
|
||||
self.branch_indicator.level_down('expert')
|
||||
else:
|
||||
self.branch_indicator.level_up('expert')
|
||||
self.branch_m.pop(0)
|
||||
self.branch_n.pop(0)
|
||||
elif self.branch_condition_count >= m_req:
|
||||
self.merge_branch_section(self.branch_m.pop(0), current_ms)
|
||||
if self.branch_indicator is not None and self.branch_indicator.difficulty != 'master':
|
||||
self.branch_indicator.level_up('master')
|
||||
self.branch_n.pop(0)
|
||||
self.branch_e.pop(0)
|
||||
else:
|
||||
self.merge_branch_section(self.branch_n.pop(0), current_ms)
|
||||
if self.branch_indicator is not None and self.branch_indicator.difficulty != 'normal':
|
||||
self.branch_indicator.level_down('normal')
|
||||
self.branch_m.pop(0)
|
||||
self.branch_e.pop(0)
|
||||
self.branch_condition_count = 0
|
||||
|
||||
def update(self, game_screen: GameScreen, current_time: float):
|
||||
self.note_manager(game_screen.current_ms, game_screen.background, current_time)
|
||||
@@ -671,6 +783,11 @@ class Player:
|
||||
self.handle_input(game_screen, current_time)
|
||||
self.nameplate.update(current_time)
|
||||
self.gauge.update(current_time)
|
||||
if self.branch_indicator is not None:
|
||||
self.branch_indicator.update(current_time)
|
||||
|
||||
if self.is_branch:
|
||||
self.evaluate_branch(game_screen.current_ms)
|
||||
|
||||
# Get the next note from any of the three lists for BPM and gogo time updates
|
||||
next_note = None
|
||||
@@ -740,11 +857,15 @@ class Player:
|
||||
continue
|
||||
x_position = self.get_position_x(SCREEN_WIDTH, current_ms, bar.load_ms, bar.pixels_per_frame_x)
|
||||
y_position = self.get_position_y(current_ms, bar.load_ms, bar.pixels_per_frame_y, bar.pixels_per_frame_x)
|
||||
bar_draws.append((str(bar.type), x_position+60, y_position+190))
|
||||
if hasattr(bar, 'is_branch_start'):
|
||||
frame = 1
|
||||
else:
|
||||
frame = 0
|
||||
bar_draws.append((str(bar.type), frame, x_position+60, y_position+190))
|
||||
|
||||
# Draw all bars in one batch
|
||||
for bar_type, x, y in bar_draws:
|
||||
tex.draw_texture('notes', bar_type, x=x, y=y)
|
||||
for bar_type, frame, x, y in bar_draws:
|
||||
tex.draw_texture('notes', bar_type, frame=frame, x=x, y=y)
|
||||
|
||||
def draw_notes(self, current_ms: float, start_ms: float):
|
||||
if not self.current_notes_draw:
|
||||
@@ -807,6 +928,8 @@ class Player:
|
||||
|
||||
# Group 1: Background and lane elements
|
||||
tex.draw_texture('lane', 'lane_background')
|
||||
if self.branch_indicator is not None:
|
||||
self.branch_indicator.draw()
|
||||
self.gauge.draw()
|
||||
if self.lane_hit_effect is not None:
|
||||
self.lane_hit_effect.draw()
|
||||
@@ -1576,6 +1699,52 @@ class ComboAnnounce:
|
||||
tex.draw_texture('combo', 'announce_number', frame=self.combo // 100 - 1, x=0, fade=fade)
|
||||
tex.draw_texture('combo', 'announce_text', x=-text_offset/2, fade=fade)
|
||||
|
||||
class BranchIndicator:
|
||||
def __init__(self):
|
||||
self.difficulty = 'normal'
|
||||
self.diff_2 = self.difficulty
|
||||
self.diff_down = Animation.create_move(100, total_distance=20, ease_out='quadratic')
|
||||
self.diff_up = Animation.create_move(133, total_distance=70, delay=self.diff_down.duration, ease_out='quadratic')
|
||||
self.diff_fade = Animation.create_fade(133, delay=self.diff_down.duration)
|
||||
self.level_fade = Animation.create_fade(116, initial_opacity=0.0, final_opacity=1.0, reverse_delay=116*10)
|
||||
self.level_scale = Animation.create_texture_resize(116, initial_size=1.0, final_size=1.2, reverse_delay=0)
|
||||
self.direction = 1
|
||||
def update(self, current_time_ms):
|
||||
self.diff_down.update(current_time_ms)
|
||||
self.diff_up.update(current_time_ms)
|
||||
self.diff_fade.update(current_time_ms)
|
||||
self.level_fade.update(current_time_ms)
|
||||
self.level_scale.update(current_time_ms)
|
||||
def level_up(self, difficulty):
|
||||
self.diff_2 = self.difficulty
|
||||
self.difficulty = difficulty
|
||||
self.diff_down.start()
|
||||
self.diff_up.start()
|
||||
self.diff_fade.start()
|
||||
self.level_fade.start()
|
||||
self.level_scale.start()
|
||||
self.direction = 1
|
||||
def level_down(self, difficulty):
|
||||
self.diff_2 = self.difficulty
|
||||
self.difficulty = difficulty
|
||||
self.diff_down.start()
|
||||
self.diff_up.start()
|
||||
self.diff_fade.start()
|
||||
self.level_fade.start()
|
||||
self.level_scale.start()
|
||||
self.direction = -1
|
||||
def draw(self):
|
||||
if self.difficulty == 'expert':
|
||||
tex.draw_texture('branch', 'expert_bg', fade=min(0.5, 1 - self.diff_fade.attribute))
|
||||
if self.difficulty == 'master':
|
||||
tex.draw_texture('branch', 'master_bg', fade=min(0.5, 1 - self.diff_fade.attribute))
|
||||
if self.direction == -1:
|
||||
tex.draw_texture('branch', 'level_down', scale=self.level_scale.attribute, fade=self.level_fade.attribute, center=True)
|
||||
else:
|
||||
tex.draw_texture('branch', 'level_up', scale=self.level_scale.attribute, fade=self.level_fade.attribute, center=True)
|
||||
tex.draw_texture('branch', self.diff_2, y=(self.diff_down.attribute - self.diff_up.attribute) * self.direction, fade=self.diff_fade.attribute)
|
||||
tex.draw_texture('branch', self.difficulty, y=(self.diff_up.attribute * (self.direction*-1)) - (70*self.direction*-1), fade=1 - self.diff_fade.attribute)
|
||||
|
||||
class Gauge:
|
||||
def __init__(self, player_num: str, difficulty: int, level: int, total_notes: int):
|
||||
self.player_num = player_num
|
||||
|
||||
@@ -838,16 +838,17 @@ class YellowBox:
|
||||
self.fade_in.start()
|
||||
|
||||
def update(self, is_diff_select: bool):
|
||||
self.left_out.update(get_current_ms())
|
||||
self.right_out.update(get_current_ms())
|
||||
self.center_out.update(get_current_ms())
|
||||
self.fade.update(get_current_ms())
|
||||
self.fade_in.update(get_current_ms())
|
||||
self.left_out_2.update(get_current_ms())
|
||||
self.right_out_2.update(get_current_ms())
|
||||
self.center_out_2.update(get_current_ms())
|
||||
self.top_y_out.update(get_current_ms())
|
||||
self.center_h_out.update(get_current_ms())
|
||||
current_time = get_current_ms()
|
||||
self.left_out.update(current_time)
|
||||
self.right_out.update(current_time)
|
||||
self.center_out.update(current_time)
|
||||
self.fade.update(current_time)
|
||||
self.fade_in.update(current_time)
|
||||
self.left_out_2.update(current_time)
|
||||
self.right_out_2.update(current_time)
|
||||
self.center_out_2.update(current_time)
|
||||
self.top_y_out.update(current_time)
|
||||
self.center_h_out.update(current_time)
|
||||
if is_diff_select and not self.is_diff_select:
|
||||
self.create_anim_2()
|
||||
if self.is_diff_select:
|
||||
@@ -897,6 +898,8 @@ class YellowBox:
|
||||
continue
|
||||
for j in range(self.tja.metadata.course_data[diff].level):
|
||||
tex.draw_texture('yellow_box', 'star', x=(diff*60), y=(j*-17), color=color)
|
||||
if self.tja.metadata.course_data[diff].is_branching and (get_current_ms() // 1000) % 2 == 0:
|
||||
tex.draw_texture('yellow_box', 'branch_indicator', x=(diff*60), color=color)
|
||||
|
||||
def _draw_tja_data_diff(self, is_ura: bool):
|
||||
if self.tja is None:
|
||||
@@ -919,6 +922,12 @@ class YellowBox:
|
||||
continue
|
||||
for j in range(self.tja.metadata.course_data[course].level):
|
||||
tex.draw_texture('yellow_box', 'star_ura', x=min(course, 3)*115, y=(j*-20), fade=self.fade_in.attribute)
|
||||
if self.tja.metadata.course_data[course].is_branching and (get_current_ms() // 1000) % 2 == 0:
|
||||
if course == 4:
|
||||
name = 'branch_indicator_ura'
|
||||
else:
|
||||
name = 'branch_indicator_diff'
|
||||
tex.draw_texture('yellow_box', name, x=min(course, 3)*115, fade=self.fade_in.attribute)
|
||||
|
||||
def _draw_text(self, song_box):
|
||||
if not isinstance(self.right_out, MoveAnimation):
|
||||
@@ -2223,6 +2232,6 @@ class FileNavigator:
|
||||
print("Removed favorite:", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en'])
|
||||
else:
|
||||
with open(favorites_path, 'a', encoding='utf-8-sig') as song_list:
|
||||
song_list.write(f'{song.hash}|{song.tja.metadata.title['en']}|{song.tja.metadata.subtitle['en']}\n')
|
||||
song_list.write(f'{song.hash}|{song.tja.metadata.title["en"]}|{song.tja.metadata.subtitle["en"]}\n')
|
||||
print("Added favorite: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en'])
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user