Add branching. why not

This commit is contained in:
Anthony Samms
2025-10-11 18:49:55 -04:00
parent 78b1b31e0c
commit 34dd2adca7
5 changed files with 432 additions and 166 deletions

View File

@@ -4,10 +4,9 @@ import json
import sqlite3 import sqlite3
import sys import sys
import time import time
from collections import deque
from pathlib import Path from pathlib import Path
from libs.tja import TJAParser from libs.tja import NoteList, TJAParser
from libs.utils import get_config, global_data 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) tja_path_str = str(tja_path)
current_modified = tja_path.stat().st_mtime current_modified = tja_path.stat().st_mtime
tja = TJAParser(tja_path) tja = TJAParser(tja_path)
all_notes = deque() all_notes = NoteList()
all_bars = deque()
diff_hashes = dict() diff_hashes = dict()
for diff in tja.metadata.course_data: for diff in tja.metadata.course_data:
diff_notes, _, diff_bars = TJAParser.notes_to_position(TJAParser(tja.file_path), diff) diff_notes, _, _, _ = TJAParser.notes_to_position(TJAParser(tja.file_path), diff)
diff_hashes[diff] = tja.hash_note_data(diff_notes, diff_bars) diff_hashes[diff] = tja.hash_note_data(diff_notes)
all_notes.extend(diff_notes) all_notes.play_notes.extend(diff_notes.play_notes)
all_bars.extend(diff_bars) all_notes.bars.extend(diff_notes.bars)
if all_notes == []: if all_notes == []:
continue 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: if hash_val not in song_hashes:
song_hashes[hash_val] = [] song_hashes[hash_val] = []
@@ -222,14 +220,14 @@ def build_song_hashes(output_dir=Path("cache")):
def process_tja_file(tja_file): def process_tja_file(tja_file):
"""Process a single TJA file and return hash or None if error""" """Process a single TJA file and return hash or None if error"""
tja = TJAParser(tja_file) tja = TJAParser(tja_file)
all_notes = [] all_notes = NoteList()
for diff in tja.metadata.course_data: for diff in tja.metadata.course_data:
all_notes.extend( notes, _, _, _ = TJAParser.notes_to_position(TJAParser(tja.file_path), diff)
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 == []: if all_notes == []:
return '' return ''
hash = tja.hash_note_data(all_notes[0], all_notes[2]) hash = tja.hash_note_data(all_notes)
return hash return hash
def get_japanese_songs_for_version(csv_file_path, version_column): def get_japanese_songs_for_version(csv_file_path, version_column):

View File

@@ -33,10 +33,21 @@ class Note:
bpm: float = field(init=False) bpm: float = field(init=False)
gogo_time: bool = field(init=False) gogo_time: bool = field(init=False)
moji: int = 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): def __le__(self, other):
return self.hit_ms <= other.hit_ms 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): def __eq__(self, other):
return self.hit_ms == other.hit_ms return self.hit_ms == other.hit_ms
@@ -112,12 +123,32 @@ class Balloon(Note):
hash_string = str(field_values) hash_string = str(field_values)
return hash_string.encode('utf-8') 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 @dataclass
class CourseData: class CourseData:
level: int = 0 level: int = 0
balloon: list[int] = field(default_factory=lambda: []) balloon: list[int] = field(default_factory=lambda: [])
scoreinit: list[int] = field(default_factory=lambda: []) scoreinit: list[int] = field(default_factory=lambda: [])
scorediff: int = 0 scorediff: int = 0
is_branching: bool = False
@dataclass @dataclass
class TJAMetadata: class TJAMetadata:
@@ -141,16 +172,16 @@ class TJAEXData:
new: bool = False new: bool = False
def calculate_base_score(play_notes: deque[Note | Drumroll | Balloon]) -> int: def calculate_base_score(notes: NoteList) -> int:
total_notes = 0 total_notes = 0
balloon_count = 0 balloon_count = 0
drumroll_msec = 0 drumroll_msec = 0
for i in range(len(play_notes)): for i in range(len(notes.play_notes)):
note = play_notes[i] note = notes.play_notes[i]
if i < len(play_notes)-1: if i < len(notes.play_notes)-1:
next_note = play_notes[i+1] next_note = notes.play_notes[i+1]
else: else:
next_note = play_notes[len(play_notes)-1] next_note = notes.play_notes[len(notes.play_notes)-1]
if isinstance(note, Drumroll): if isinstance(note, Drumroll):
drumroll_msec += (next_note.hit_ms - note.hit_ms) drumroll_msec += (next_note.hit_ms - note.hit_ms)
elif isinstance(note, Balloon): elif isinstance(note, Balloon):
@@ -198,7 +229,9 @@ class TJAParser:
current_diff = None # Track which difficulty we're currently processing current_diff = None # Track which difficulty we're currently processing
for item in self.data: 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 continue
elif item.startswith('SUBTITLE'): elif item.startswith('SUBTITLE'):
region_code = 'en' region_code = 'en'
@@ -407,9 +440,10 @@ class TJAParser:
play_note_list[-3].moji = se_notes[1][2] play_note_list[-3].moji = se_notes[1][2]
def notes_to_position(self, diff: int): def notes_to_position(self, diff: int):
play_note_list: list[Note | Drumroll | Balloon] = [] master_notes = NoteList()
draw_note_list: list[Note | Drumroll | Balloon] = [] branch_m: list[NoteList] = []
bar_list: list[Note] = [] branch_e: list[NoteList] = []
branch_n: list[NoteList] = []
notes = self.data_to_notes(diff) notes = self.data_to_notes(diff)
balloon = self.metadata.course_data[diff].balloon.copy() balloon = self.metadata.course_data[diff].balloon.copy()
count = 0 count = 0
@@ -420,19 +454,127 @@ class TJAParser:
y_scroll_modifier = 0 y_scroll_modifier = 0
barline_display = True barline_display = True
gogo_time = False 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: 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)
barline_added = False barline_added = False
for part in bar: for part in bar:
if part.startswith('#BRANCHSTART'): 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 continue
if part == '#M': 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 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 continue
if '#LYRIC' in part: if '#LYRIC' in part:
continue continue
@@ -445,74 +587,15 @@ class TJAParser:
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:
# Extract the value after '#SCROLL ' scroll_value = part[7:]
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')
if 'i' in scroll_value: if 'i' in scroll_value:
# Handle different imaginary number formats normalized = scroll_value.replace('.i', 'j').replace('i', 'j')
if scroll_value == 'i': c = complex(normalized)
x_scroll_modifier = 0 x_scroll_modifier = c.real
y_scroll_modifier = 1 y_scroll_modifier = c.imag
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
else: else:
# Pure real number
x_scroll_modifier = float(scroll_value) x_scroll_modifier = float(scroll_value)
y_scroll_modifier = 0 y_scroll_modifier = 0.0
continue continue
elif '#BPMCHANGE' in part: elif '#BPMCHANGE' in part:
bpm = float(part[11:]) bpm = float(part[11:])
@@ -555,7 +638,11 @@ class TJAParser:
if barline_added: if barline_added:
bar_line.display = False 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 barline_added = True
#Empty bar is still a bar, otherwise start increment #Empty bar is still a bar, otherwise start increment
@@ -571,7 +658,7 @@ class TJAParser:
if item == '0' or (not item.isdigit()): if item == '0' or (not item.isdigit()):
self.current_ms += increment self.current_ms += increment
continue 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 self.current_ms += increment
continue continue
note = Note() note = Note()
@@ -600,33 +687,29 @@ class TJAParser:
note = Balloon(note) note = Balloon(note)
note.count = 1 if not balloon else balloon.pop(0) note.count = 1 if not balloon else balloon.pop(0)
elif item == '8': 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: if new_pixels_per_ms == 0:
note.load_ms = note.hit_ms note.load_ms = note.hit_ms
else: else:
note.load_ms = note.hit_ms - (self.distance / new_pixels_per_ms) 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 self.current_ms += increment
play_note_list.append(note) curr_note_list.append(note)
bisect.insort(draw_note_list, note, key=lambda x: x.load_ms) bisect.insort(curr_draw_list, note, key=lambda x: x.load_ms)
self.get_moji(play_note_list, ms_per_measure) self.get_moji(curr_note_list, ms_per_measure)
index += 1 index += 1
if len(play_note_list) > 3: if hasattr(curr_bar_list[-1], 'branch_params'):
if isinstance(play_note_list[-2], Drumroll) and play_note_list[-1].type != 8: print(curr_note_list[-1])
print(self.file_path, diff)
print(bar)
continue
raise Exception(f"{play_note_list[-2]}")
# https://stackoverflow.com/questions/72899/how-to-sort-a-list-of-dictionaries-by-a-value-of-the-dictionary-in-python # 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 # Sorting by load_ms is necessary for drawing, as some notes appear on the
# screen slower regardless of when they reach the judge circle # screen slower regardless of when they reach the judge circle
# Bars can be sorted like this because they don't need hit detection # 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() n = hashlib.sha256()
list1 = list(play_notes) list1 = notes.play_notes
list2 = list(bars) list2 = notes.bars
merged: list[Note | Drumroll | Balloon] = [] merged: list[Note | Drumroll | Balloon] = []
i = 0 i = 0
j = 0 j = 0
@@ -644,46 +727,47 @@ class TJAParser:
return n.hexdigest() return n.hexdigest()
def modifier_speed(notes: deque[Note | Balloon | Drumroll], bars, value: float): def modifier_speed(notes: NoteList, value: float):
notes = notes.copy() modded_notes = notes.draw_notes.copy()
for note in notes: modded_bars = notes.bars.copy()
for note in modded_notes:
note.pixels_per_frame_x *= value note.pixels_per_frame_x *= value
note.load_ms = note.hit_ms - (866 / get_pixels_per_ms(note.pixels_per_frame_x)) 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.pixels_per_frame_x *= value
bar.load_ms = bar.hit_ms - (866 / get_pixels_per_ms(bar.pixels_per_frame_x)) 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]): def modifier_display(notes: NoteList):
notes = notes.copy() modded_notes = notes.draw_notes.copy()
for note in notes: for note in modded_notes:
note.display = False note.display = False
return notes return modded_notes
def modifier_inverse(notes: deque[Note | Balloon | Drumroll]): def modifier_inverse(notes: NoteList):
notes = notes.copy() modded_notes = notes.play_notes.copy()
type_mapping = {1: 2, 2: 1, 3: 4, 4: 3} 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: if note.type in type_mapping:
note.type = type_mapping[note.type] 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 #value: 1 == kimagure, 2 == detarame
notes = notes.copy() modded_notes = notes.play_notes.copy()
percentage = int(len(notes) / 5) * value percentage = int(len(modded_notes) / 5) * value
selected_notes = random.sample(range(len(notes)), percentage) selected_notes = random.sample(range(len(modded_notes)), percentage)
type_mapping = {1: 2, 2: 1, 3: 4, 4: 3} type_mapping = {1: 2, 2: 1, 3: 4, 4: 3}
for i in selected_notes: for i in selected_notes:
if notes[i].type in type_mapping: if modded_notes[i].type in type_mapping:
notes[i].type = type_mapping[notes[i].type] modded_notes[i].type = type_mapping[modded_notes[i].type]
return notes 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: if global_data.modifiers.display:
draw_notes = modifier_display(draw_notes) draw_notes = modifier_display(notes)
if global_data.modifiers.inverse: if global_data.modifiers.inverse:
notes = modifier_inverse(notes) play_notes = modifier_inverse(notes)
notes = modifier_random(notes, global_data.modifiers.random) play_notes = modifier_random(notes, global_data.modifiers.random)
draw_notes, bars = modifier_speed(draw_notes, bars, global_data.modifiers.speed) draw_notes, bars = modifier_speed(notes, global_data.modifiers.speed)
return notes, draw_notes, bars return deque(play_notes), deque(draw_notes), deque(bars)

View File

@@ -2,7 +2,7 @@ import pyray as ray
from libs.utils import get_current_ms from libs.utils import get_current_ms
from libs.texture import tex from libs.texture import tex
from scenes.game import ComboAnnounce from scenes.game import BranchIndicator
class DevScreen: class DevScreen:
@@ -16,7 +16,7 @@ class DevScreen:
if not self.screen_init: if not self.screen_init:
self.screen_init = True self.screen_init = True
tex.load_screen_textures('game') tex.load_screen_textures('game')
self.obj = ComboAnnounce(0, get_current_ms()) self.obj = BranchIndicator()
def on_screen_end(self, next_screen: str): def on_screen_end(self, next_screen: str):
self.screen_init = False self.screen_init = False
@@ -27,8 +27,14 @@ class DevScreen:
self.obj.update(get_current_ms()) self.obj.update(get_current_ms())
if ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER): if ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER):
return self.on_screen_end('GAME') return self.on_screen_end('GAME')
elif ray.is_key_pressed(ray.KeyboardKey.KEY_SPACE): elif ray.is_key_pressed(ray.KeyboardKey.KEY_UP):
self.obj = ComboAnnounce(100, get_current_ms()) 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): def draw(self):
ray.draw_rectangle(0, 0, 1280, 720, ray.GREEN) ray.draw_rectangle(0, 0, 1280, 720, ray.GREEN)

View File

@@ -17,6 +17,7 @@ from libs.tja import (
Balloon, Balloon,
Drumroll, Drumroll,
Note, Note,
NoteList,
TJAParser, TJAParser,
apply_modifiers, apply_modifiers,
calculate_base_score, calculate_base_score,
@@ -127,8 +128,8 @@ class GameScreen:
return return
with sqlite3.connect('scores.db') as con: with sqlite3.connect('scores.db') as con:
cursor = con.cursor() cursor = con.cursor()
notes, _, bars = TJAParser.notes_to_position(TJAParser(self.tja.file_path), self.player_1.difficulty) notes, _, _, _ = TJAParser.notes_to_position(TJAParser(self.tja.file_path), self.player_1.difficulty)
hash = self.tja.hash_note_data(notes, bars) hash = self.tja.hash_note_data(notes)
check_query = "SELECT score FROM Scores WHERE hash = ? LIMIT 1" check_query = "SELECT score FROM Scores WHERE hash = ? LIMIT 1"
cursor.execute(check_query, (hash,)) cursor.execute(check_query, (hash,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -231,16 +232,22 @@ class Player:
self.visual_offset = global_data.config["general"]["visual_offset"] self.visual_offset = global_data.config["general"]["visual_offset"]
if tja is not None: if tja is not None:
play_notes, self.draw_note_list, self.draw_bar_list = tja.notes_to_position(self.difficulty) notes, self.branch_m, self.branch_e, self.branch_n = 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) self.play_notes, self.draw_note_list, self.draw_bar_list = apply_modifiers(notes)
else: 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.don_notes = deque([note for note in self.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.kat_notes = deque([note for note in self.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.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 play_notes if 0 < note.type < 5]) self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5])
self.base_score = calculate_base_score(play_notes) 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 #Note management
self.current_bars: list[Note] = [] self.current_bars: list[Note] = []
@@ -249,8 +256,12 @@ class Player:
self.curr_drumroll_count = 0 self.curr_drumroll_count = 0
self.is_balloon = False self.is_balloon = False
self.curr_balloon_count = 0 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.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 #Score management
self.good_count = 0 self.good_count = 0
@@ -275,7 +286,8 @@ class Player:
self.score_counter = ScoreCounter(self.score) self.score_counter = ScoreCounter(self.score)
self.gogo_time: Optional[GogoTime] = None self.gogo_time: Optional[GogoTime] = None
self.combo_announce = ComboAnnounce(self.combo, 0) 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'] 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.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) self.chara = Chara2D(player_number - 1, self.bpm)
@@ -292,6 +304,22 @@ class Player:
self.autoplay_hit_side = 'L' self.autoplay_hit_side = 'L'
self.last_subdivision = -1 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): def get_result_score(self):
return self.score, self.good_count, self.ok_count, self.bad_count, self.max_combo, self.total_drumroll 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: if position >= removal_threshold:
bars_to_keep.append(bar) bars_to_keep.append(bar)
self.current_bars = bars_to_keep 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]): 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: if self.don_notes and self.don_notes[0].hit_ms + Player.TIMING_BAD < current_ms:
self.combo = 0 self.combo = 0
@@ -343,6 +417,8 @@ class Player:
self.bad_count += 1 self.bad_count += 1
self.gauge.add_bad() self.gauge.add_bad()
self.don_notes.popleft() 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: if self.kat_notes and self.kat_notes[0].hit_ms + Player.TIMING_BAD < current_ms:
self.combo = 0 self.combo = 0
@@ -351,6 +427,8 @@ class Player:
self.bad_count += 1 self.bad_count += 1
self.gauge.add_bad() self.gauge.add_bad()
self.kat_notes.popleft() self.kat_notes.popleft()
if self.is_branch and self.branch_condition == 'p':
self.branch_condition_count -= 1
if not self.other_notes: if not self.other_notes:
return 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.draw_arc_list.append(NoteArc(drum_type, current_time, 1, drum_type == 3 or drum_type == 4, False))
self.curr_drumroll_count += 1 self.curr_drumroll_count += 1
self.total_drumroll += 1 self.total_drumroll += 1
if self.is_branch and self.branch_condition == 'r':
self.branch_condition_count += 1
if background is not None: if background is not None:
background.add_renda() background.add_renda()
self.score += 100 self.score += 100
@@ -529,6 +609,8 @@ class Player:
self.base_score_list.append(ScoreCounterAnimation(self.player_number, self.base_score)) self.base_score_list.append(ScoreCounterAnimation(self.player_number, self.base_score))
self.note_correct(curr_note, current_time) self.note_correct(curr_note, current_time)
self.gauge.add_good() 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: if game_screen.background is not None:
game_screen.background.add_chibi(False) 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.base_score_list.append(ScoreCounterAnimation(self.player_number, 10 * math.floor(self.base_score / 2 / 10)))
self.note_correct(curr_note, current_time) self.note_correct(curr_note, current_time)
self.gauge.add_ok() 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: if game_screen.background is not None:
game_screen.background.add_chibi(False) game_screen.background.add_chibi(False)
@@ -640,6 +724,34 @@ class Player:
audio.play_sound(game_screen.sound_kat) audio.play_sound(game_screen.sound_kat)
self.check_note(game_screen, 2, current_time) 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): def update(self, game_screen: GameScreen, current_time: float):
self.note_manager(game_screen.current_ms, game_screen.background, current_time) 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.handle_input(game_screen, current_time)
self.nameplate.update(current_time) self.nameplate.update(current_time)
self.gauge.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 # Get the next note from any of the three lists for BPM and gogo time updates
next_note = None next_note = None
@@ -740,11 +857,15 @@ class Player:
continue continue
x_position = self.get_position_x(SCREEN_WIDTH, current_ms, bar.load_ms, bar.pixels_per_frame_x) 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) 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 # Draw all bars in one batch
for bar_type, x, y in bar_draws: for bar_type, frame, x, y in bar_draws:
tex.draw_texture('notes', bar_type, x=x, y=y) tex.draw_texture('notes', bar_type, frame=frame, x=x, y=y)
def draw_notes(self, current_ms: float, start_ms: float): def draw_notes(self, current_ms: float, start_ms: float):
if not self.current_notes_draw: if not self.current_notes_draw:
@@ -807,6 +928,8 @@ class Player:
# Group 1: Background and lane elements # Group 1: Background and lane elements
tex.draw_texture('lane', 'lane_background') tex.draw_texture('lane', 'lane_background')
if self.branch_indicator is not None:
self.branch_indicator.draw()
self.gauge.draw() self.gauge.draw()
if self.lane_hit_effect is not None: if self.lane_hit_effect is not None:
self.lane_hit_effect.draw() 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_number', frame=self.combo // 100 - 1, x=0, fade=fade)
tex.draw_texture('combo', 'announce_text', x=-text_offset/2, 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: class Gauge:
def __init__(self, player_num: str, difficulty: int, level: int, total_notes: int): def __init__(self, player_num: str, difficulty: int, level: int, total_notes: int):
self.player_num = player_num self.player_num = player_num

View File

@@ -838,16 +838,17 @@ class YellowBox:
self.fade_in.start() self.fade_in.start()
def update(self, is_diff_select: bool): def update(self, is_diff_select: bool):
self.left_out.update(get_current_ms()) current_time = get_current_ms()
self.right_out.update(get_current_ms()) self.left_out.update(current_time)
self.center_out.update(get_current_ms()) self.right_out.update(current_time)
self.fade.update(get_current_ms()) self.center_out.update(current_time)
self.fade_in.update(get_current_ms()) self.fade.update(current_time)
self.left_out_2.update(get_current_ms()) self.fade_in.update(current_time)
self.right_out_2.update(get_current_ms()) self.left_out_2.update(current_time)
self.center_out_2.update(get_current_ms()) self.right_out_2.update(current_time)
self.top_y_out.update(get_current_ms()) self.center_out_2.update(current_time)
self.center_h_out.update(get_current_ms()) self.top_y_out.update(current_time)
self.center_h_out.update(current_time)
if is_diff_select and not self.is_diff_select: if is_diff_select and not self.is_diff_select:
self.create_anim_2() self.create_anim_2()
if self.is_diff_select: if self.is_diff_select:
@@ -897,6 +898,8 @@ class YellowBox:
continue continue
for j in range(self.tja.metadata.course_data[diff].level): 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) 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): def _draw_tja_data_diff(self, is_ura: bool):
if self.tja is None: if self.tja is None:
@@ -919,6 +922,12 @@ class YellowBox:
continue continue
for j in range(self.tja.metadata.course_data[course].level): 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) 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): def _draw_text(self, song_box):
if not isinstance(self.right_out, MoveAnimation): 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']) print("Removed favorite:", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en'])
else: else:
with open(favorites_path, 'a', encoding='utf-8-sig') as song_list: 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']) print("Added favorite: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en'])
return True return True