diff --git a/libs/tja.py b/libs/tja.py index 586ca02..ae78d65 100644 --- a/libs/tja.py +++ b/libs/tja.py @@ -72,6 +72,8 @@ class Note: lyric: str = field(init=False) sudden_appear_ms: float = field(init=False) sudden_moving_ms: float = field(init=False) + judge_pos_x: float = field(init=False) + judge_pos_y: float = field(init=False) def __lt__(self, other): return self.hit_ms < other.hit_ms @@ -627,6 +629,10 @@ class TJAParser: index = 0 sudden_appear = 0 sudden_moving = 0 + judge_pos_x = 0 # Offset from default judgment position + judge_pos_y = 0 + judge_target_x = 0 # Target position for interpolation + judge_target_y = 0 time_signature = 4/4 bpm = self.metadata.bpm x_scroll_modifier = 1 @@ -758,6 +764,67 @@ class TJAParser: lyric = part[6:] continue if '#JPOSSCROLL' in part: + parts = part.split() + if len(parts) >= 4: + duration_ms = float(parts[1]) * 1000 + distance_str = parts[2] + direction_deg = float(parts[3]) + + delta_x = 0 + delta_y = 0 + + if 'i' in distance_str: + normalized = distance_str.replace('.i', 'j').replace('i', 'j') + normalized = normalized.replace(',', '') + c = complex(normalized) + direction_rad = math.radians(direction_deg) + cos_dir = math.cos(direction_rad) + sin_dir = math.sin(direction_rad) + delta_x = c.real * cos_dir - c.imag * sin_dir + delta_y = c.real * sin_dir + c.imag * cos_dir + else: + distance = float(distance_str) + direction_rad = math.radians(direction_deg) + delta_x = distance * math.cos(direction_rad) + delta_y = distance * math.sin(direction_rad) + + judge_target_x = judge_pos_x + delta_x + judge_target_y = judge_pos_y + delta_y + interpolation_interval_ms = 8 + num_steps = int(duration_ms / interpolation_interval_ms) + + for step in range(num_steps + 1): + t = step / max(num_steps, 1) # Interpolation factor (0 to 1) + interpolated_ms = self.current_ms + (step * interpolation_interval_ms) + + # Linear interpolation + interp_x = judge_pos_x + (delta_x * t) + interp_y = judge_pos_y + (delta_y * t) + + # Create invisible bar line to store position + jpos_bar = Note() + jpos_bar.pixels_per_frame_x = get_pixels_per_frame(bpm * time_signature * x_scroll_modifier, time_signature*4, self.distance) + jpos_bar.pixels_per_frame_y = get_pixels_per_frame(bpm * time_signature * y_scroll_modifier, time_signature*4, self.distance) + pixels_per_ms = get_pixels_per_ms(jpos_bar.pixels_per_frame_x) + + jpos_bar.hit_ms = interpolated_ms + if pixels_per_ms == 0: + jpos_bar.load_ms = jpos_bar.hit_ms + else: + jpos_bar.load_ms = jpos_bar.hit_ms - (self.distance / pixels_per_ms) + jpos_bar.type = 0 + jpos_bar.display = False + jpos_bar.gogo_time = gogo_time + jpos_bar.bpm = bpm + + jpos_bar.judge_pos_x = interp_x + jpos_bar.judge_pos_y = interp_y + + bisect.insort(curr_bar_list, jpos_bar, key=lambda x: x.load_ms) + + judge_pos_x = judge_target_x + judge_pos_y = judge_target_y + continue elif '#NMSCROLL' in part: continue diff --git a/scenes/game.py b/scenes/game.py index 4db9f8c..f9e88a0 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -336,6 +336,8 @@ class Player: self.total_drumroll = 0 self.arc_points = 25 + self.judge_x = 0 + self.judge_y = 0 self.draw_judge_list: list[Judgement] = [] self.lane_hit_effect: Optional[LaneHitEffect] = None @@ -442,6 +444,21 @@ class Player: time_diff = load_ms - current_ms return int((pixels_per_frame * 0.06 * time_diff) + ((866 * pixels_per_frame) / pixels_per_frame_x)) + def get_judge_position(self, current_ms: float): + """Get the current judgment circle position based on bar data""" + judge_x = 0 + judge_y = 0 + + # Find the most recent bar with judge position data + for bar in self.current_bars: + if hasattr(bar, 'judge_pos_x') and bar.hit_ms <= current_ms: + judge_x = bar.judge_pos_x + judge_y = bar.judge_pos_y + elif bar.hit_ms > current_ms: + break + + return judge_x, judge_y + def animation_manager(self, animation_list: list, current_time: float): if not animation_list: return @@ -636,7 +653,9 @@ class Player: self.max_combo = self.combo if note.type != NoteType.KUSUDAMA: - self.draw_arc_list.append(NoteArc(note.type, current_time, PlayerNum(self.is_2p + 1), note.type == NoteType.DON_L or note.type == NoteType.KAT_L or note.type == NoteType.BALLOON_HEAD, note.type == NoteType.BALLOON_HEAD)) + is_big = note.type == NoteType.DON_L or note.type == NoteType.KAT_L or note.type == NoteType.BALLOON_HEAD + is_balloon = note.type == NoteType.BALLOON_HEAD + self.draw_arc_list.append(NoteArc(note.type, current_time, PlayerNum(self.is_2p + 1), is_big, is_balloon, start_x=self.judge_x, start_y=self.judge_y)) if note in self.current_notes_draw: index = self.current_notes_draw.index(note) @@ -915,6 +934,7 @@ class Player: if self.lane_hit_effect is not None: self.lane_hit_effect.update(current_time) self.animation_manager(self.draw_drum_hit_list, current_time) + self.judge_x, self.judge_y = self.get_judge_position(ms_from_start) # More efficient arc management finished_arcs = [] @@ -975,31 +995,35 @@ class Player: def draw_drumroll(self, current_ms: float, head: Drumroll, current_eighth: int): """Draws a drumroll in the player's lane""" start_position = self.get_position_x(SCREEN_WIDTH, current_ms, head.load_ms, head.pixels_per_frame_x) + start_position += self.judge_x tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1]) is_big = int(head.type == NoteType.ROLL_HEAD_L) end_position = self.get_position_x(SCREEN_WIDTH, current_ms, tail.load_ms, tail.pixels_per_frame_x) + end_position += self.judge_x length = end_position - start_position color = ray.Color(255, head.color, head.color, 255) if head.display: if length > 0: - tex.draw_texture('notes', "8", frame=is_big, x=start_position+64, y=192+(self.is_2p*176), x2=length-47, color=color) + tex.draw_texture('notes', "8", frame=is_big, x=start_position+64, y=192+(self.is_2p*176)+self.judge_y, x2=length-47, color=color) if is_big: - tex.draw_texture('notes', "drumroll_big_tail", x=end_position+64, y=192+(self.is_2p*176), color=color) + tex.draw_texture('notes', "drumroll_big_tail", x=end_position+64, y=192+(self.is_2p*176)+self.judge_y, color=color) else: - tex.draw_texture('notes', "drumroll_tail", x=end_position+64, y=192+(self.is_2p*176), color=color) - tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=192+(self.is_2p*176), color=color) + tex.draw_texture('notes', "drumroll_tail", x=end_position+64, y=192+(self.is_2p*176)+self.judge_y, color=color) + tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=192+(self.is_2p*176)+self.judge_y, color=color) - tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + 60, y=323+(self.is_2p*176), x2=length) - tex.draw_texture('notes', 'moji', frame=head.moji, x=(start_position - (168//2)) + 64, y=323+(self.is_2p*176)) - tex.draw_texture('notes', 'moji', frame=tail.moji, x=(end_position - (168//2)) + 32, y=323+(self.is_2p*176)) + tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + 60, y=323+(self.is_2p*176)+self.judge_y, x2=length) + tex.draw_texture('notes', 'moji', frame=head.moji, x=(start_position - (168//2)) + 64, y=323+(self.is_2p*176)+self.judge_y, color=color) + tex.draw_texture('notes', 'moji', frame=tail.moji, x=(end_position - (168//2)) + 32, y=323+(self.is_2p*176)+self.judge_y, color=color) def draw_balloon(self, current_ms: float, head: Balloon, current_eighth: int): """Draws a balloon in the player's lane""" offset = 12 start_position = self.get_position_x(SCREEN_WIDTH, current_ms, head.load_ms, head.pixels_per_frame_x) + start_position += self.judge_x tail = next((note for note in self.current_notes_draw[1:] if note.type == NoteType.TAIL and note.index > head.index), self.current_notes_draw[1]) end_position = self.get_position_x(SCREEN_WIDTH, current_ms, tail.load_ms, tail.pixels_per_frame_x) - pause_position = 349 + end_position += self.judge_x + pause_position = 349 + self.judge_x if current_ms >= tail.hit_ms: position = end_position elif current_ms >= head.hit_ms: @@ -1007,8 +1031,8 @@ class Player: else: position = start_position if head.display: - tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset, y=192+(self.is_2p*176)) - tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+128, y=192+(self.is_2p*176)) + tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset, y=192+(self.is_2p*176)+self.judge_y) + tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+128, y=192+(self.is_2p*176)+self.judge_y) def draw_bars(self, current_ms: float): """Draw bars in the player's lane""" @@ -1020,6 +1044,8 @@ 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) + x_position += self.judge_x + y_position += self.judge_y if hasattr(bar, 'is_branch_start'): frame = 1 else: @@ -1063,7 +1089,8 @@ class Player: else: x_position = self.get_position_x(SCREEN_WIDTH, current_ms, note.load_ms, note.pixels_per_frame_x) y_position = self.get_position_y(current_ms, note.load_ms, note.pixels_per_frame_y, note.pixels_per_frame_x) - + x_position += self.judge_x + y_position += self.judge_y if isinstance(note, Drumroll): self.draw_drumroll(current_ms, note, current_eighth) elif isinstance(note, Balloon) and not note.is_kusudama: @@ -1167,13 +1194,13 @@ class Player: self.gauge.draw() if self.lane_hit_effect is not None: self.lane_hit_effect.draw() - tex.draw_texture('lane', 'lane_hit_circle', index=self.is_2p) + tex.draw_texture('lane', 'lane_hit_circle', x=self.judge_x, y=self.judge_y, index=self.is_2p) # Group 2: Judgement and hit effects if self.gogo_time is not None: - self.gogo_time.draw() + self.gogo_time.draw(self.judge_x, self.judge_y) for anim in self.draw_judge_list: - anim.draw() + anim.draw(self.judge_x, self.judge_y) # Group 3: Notes and bars (game content) self.draw_bars(ms_from_start) @@ -1208,29 +1235,29 @@ class Judgement: if self.fade_animation_2.is_finished: self.is_finished = True - def draw(self): + def draw(self, judge_x: float, judge_y: float): y = self.move_animation.attribute index = self.texture_animation.attribute hit_fade = self.fade_animation_1.attribute fade = self.fade_animation_2.attribute if self.type == 'GOOD': if self.big: - tex.draw_texture('hit_effect', 'hit_effect_good_big', fade=fade, index=self.is_2p) - tex.draw_texture('hit_effect', 'outer_good_big', frame=index, fade=hit_fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'hit_effect_good_big', x=judge_x, y=judge_y, fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'outer_good_big', x=judge_x, y=judge_y, frame=index, fade=hit_fade, index=self.is_2p) else: - tex.draw_texture('hit_effect', 'hit_effect_good', fade=fade, index=self.is_2p) - tex.draw_texture('hit_effect', 'outer_good', frame=index, fade=hit_fade, index=self.is_2p) - tex.draw_texture('hit_effect', 'judge_good', y=y, fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'hit_effect_good', x=judge_x, y=judge_y, fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'outer_good', x=judge_x, y=judge_y, frame=index, fade=hit_fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'judge_good', y=y+judge_y, x=judge_x, fade=fade, index=self.is_2p) elif self.type == 'OK': if self.big: - tex.draw_texture('hit_effect', 'hit_effect_ok_big', fade=fade, index=self.is_2p) - tex.draw_texture('hit_effect', 'outer_ok_big', frame=index, fade=hit_fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'hit_effect_ok_big', x=judge_x, y=judge_y, fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'outer_ok_big', x=judge_x, y=judge_y, frame=index, fade=hit_fade, index=self.is_2p) else: - tex.draw_texture('hit_effect', 'hit_effect_ok', fade=fade, index=self.is_2p) - tex.draw_texture('hit_effect', 'outer_ok', frame=index, fade=hit_fade, index=self.is_2p) - tex.draw_texture('hit_effect', 'judge_ok', y=y, fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'hit_effect_ok', x=judge_x, y=judge_y, fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'outer_ok', x=judge_x, y=judge_y, frame=index, fade=hit_fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'judge_ok', x=judge_x, y=y+judge_y, fade=fade, index=self.is_2p) elif self.type == 'BAD': - tex.draw_texture('hit_effect', 'judge_bad', y=y, fade=fade, index=self.is_2p) + tex.draw_texture('hit_effect', 'judge_bad', x=judge_x, y=y+judge_y, fade=fade, index=self.is_2p) class LaneHitEffect: """Display a gradient overlay when the player hits the drum""" @@ -1389,7 +1416,7 @@ class GaugeHitEffect: class NoteArc: """Note arcing from the player to the gauge""" - def __init__(self, note_type: int, current_ms: float, player_num: PlayerNum, big: bool, is_balloon: bool): + def __init__(self, note_type: int, current_ms: float, player_num: PlayerNum, big: bool, is_balloon: bool, start_x: float = 0, start_y: float = 0): self.note_type = note_type self.is_big = big self.is_balloon = is_balloon @@ -1403,7 +1430,7 @@ class NoteArc: self.points_per_explosion = 5 curve_height = 425 - self.start_x, self.start_y = 350, 192 + self.start_x, self.start_y = start_x + 350, start_y + 192 self.end_x, self.end_y = 1158, 101 if self.player_num == PlayerNum.P2: self.start_y += 176 @@ -1882,8 +1909,8 @@ class GogoTime: self.fire_resize.update(current_time_ms) self.fire_change.update(current_time_ms) - def draw(self): - tex.draw_texture('gogo_time', 'fire', scale=self.fire_resize.attribute, frame=self.fire_change.attribute, fade=0.5, center=True, index=self.is_2p) + def draw(self, judge_x: float, judge_y: float): + tex.draw_texture('gogo_time', 'fire', scale=self.fire_resize.attribute, frame=self.fire_change.attribute, fade=0.5, center=True, x=judge_x, y=judge_y, index=self.is_2p) if not self.explosion_anim.is_finished and not self.is_2p: for i in range(5): tex.draw_texture('gogo_time', 'explosion', frame=self.explosion_anim.attribute, index=i)