mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 11:40:13 +01:00
2385 lines
118 KiB
Python
2385 lines
118 KiB
Python
import bisect
|
|
from enum import IntEnum
|
|
import math
|
|
import logging
|
|
import sqlite3
|
|
from collections import deque
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import pyray as ray
|
|
|
|
from libs.animation import Animation
|
|
from libs.audio import audio
|
|
from libs.background import Background
|
|
from libs.chara_2d import Chara2D
|
|
from libs.global_data import Crown, Difficulty, Modifiers, PlayerNum
|
|
from libs.global_objects import AllNetIcon, Nameplate
|
|
from libs.screen import Screen
|
|
from libs.texture import tex
|
|
from libs.tja import (
|
|
Balloon,
|
|
Drumroll,
|
|
Note,
|
|
NoteList,
|
|
NoteType,
|
|
TJAParser,
|
|
apply_modifiers,
|
|
calculate_base_score,
|
|
)
|
|
from libs.transition import Transition
|
|
from libs.utils import (
|
|
OutlinedText,
|
|
get_current_ms,
|
|
global_data,
|
|
global_tex,
|
|
is_l_don_pressed,
|
|
is_l_kat_pressed,
|
|
is_r_don_pressed,
|
|
is_r_kat_pressed,
|
|
rounded,
|
|
)
|
|
from libs.video import VideoPlayer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class DrumType(IntEnum):
|
|
DON = 1
|
|
KAT = 2
|
|
|
|
class Side(IntEnum):
|
|
LEFT = 1
|
|
RIGHT = 2
|
|
|
|
class Judgments(IntEnum):
|
|
GOOD = 0
|
|
OK = 1
|
|
BAD = 2
|
|
|
|
class GameScreen(Screen):
|
|
JUDGE_X = 414 * tex.screen_scale
|
|
def on_screen_start(self):
|
|
super().on_screen_start()
|
|
self.mask_shader = ray.load_shader("shader/outline.vs", "shader/mask.fs")
|
|
self.current_ms = 0
|
|
self.end_ms = 0
|
|
self.start_delay = 1000
|
|
self.song_started = False
|
|
self.paused = False
|
|
self.pause_time = 0
|
|
self.audio_time = 0
|
|
self.movie = None
|
|
self.song_music = None
|
|
if global_data.config["general"]["nijiiro_notes"]:
|
|
# drop original
|
|
if "notes" in tex.textures:
|
|
del tex.textures["notes"]
|
|
# load nijiiro, rename "notes"
|
|
# to leave hardcoded 'notes' in calls below
|
|
tex.load_zip("game", "notes_nijiiro")
|
|
tex.textures["notes"] = tex.textures.pop("notes_nijiiro")
|
|
logger.info("Loaded nijiiro notes textures")
|
|
ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture0"), tex.textures['balloon']['rainbow_mask'].texture)
|
|
ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture1"), tex.textures['balloon']['rainbow'].texture)
|
|
session_data = global_data.session_data[global_data.player_num]
|
|
self.init_tja(session_data.selected_song)
|
|
logger.info(f"TJA initialized for song: {session_data.selected_song}")
|
|
self.load_hitsounds()
|
|
self.song_info = SongInfo(session_data.song_title, session_data.genre_index)
|
|
self.result_transition = ResultTransition(global_data.player_num)
|
|
subtitle = self.tja.metadata.subtitle.get(global_data.config['general']['language'].lower(), '')
|
|
self.bpm = self.tja.metadata.bpm
|
|
scene_preset = self.tja.metadata.scene_preset
|
|
if self.movie is None:
|
|
self.background = Background(global_data.player_num, self.bpm, scene_preset=scene_preset)
|
|
logger.info("Background initialized")
|
|
else:
|
|
self.background = None
|
|
logger.info("Movie initialized")
|
|
self.transition = Transition(session_data.song_title, subtitle, is_second=True)
|
|
self.allnet_indicator = AllNetIcon()
|
|
self.transition.start()
|
|
|
|
def on_screen_end(self, next_screen):
|
|
self.song_started = False
|
|
self.end_ms = 0
|
|
if self.movie is not None:
|
|
self.movie.stop()
|
|
logger.info("Movie stopped")
|
|
if self.background is not None:
|
|
self.background.unload()
|
|
logger.info("Background unloaded")
|
|
return super().on_screen_end(next_screen)
|
|
|
|
def load_hitsounds(self):
|
|
"""Load the hit sounds"""
|
|
sounds_dir = Path("Sounds")
|
|
if global_data.hit_sound == -1:
|
|
audio.load_sound(Path('none.wav'), 'hitsound_don_1p')
|
|
audio.load_sound(Path('none.wav'), 'hitsound_kat_1p')
|
|
logger.info("Loaded default (none) hit sounds for 1P")
|
|
if global_data.hit_sound == 0:
|
|
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[PlayerNum.P1]) / "don.wav", 'hitsound_don_1p')
|
|
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[PlayerNum.P1]) / "ka.wav", 'hitsound_kat_1p')
|
|
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[PlayerNum.P2]) / "don.wav", 'hitsound_don_2p')
|
|
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[PlayerNum.P2]) / "ka.wav", 'hitsound_kat_2p')
|
|
logger.info("Loaded wav hit sounds for 1P and 2P")
|
|
else:
|
|
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[PlayerNum.P1]) / "don.ogg", 'hitsound_don_1p')
|
|
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[PlayerNum.P1]) / "ka.ogg", 'hitsound_kat_1p')
|
|
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[PlayerNum.P2]) / "don.ogg", 'hitsound_don_2p')
|
|
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[PlayerNum.P2]) / "ka.ogg", 'hitsound_kat_2p')
|
|
logger.info("Loaded ogg hit sounds for 1P and 2P")
|
|
|
|
def init_tja(self, song: Path):
|
|
"""Initialize the TJA file"""
|
|
self.tja = TJAParser(song, start_delay=self.start_delay, distance=tex.screen_width - GameScreen.JUDGE_X)
|
|
if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists():
|
|
self.movie = VideoPlayer(self.tja.metadata.bgmovie)
|
|
self.movie.set_volume(0.0)
|
|
else:
|
|
self.movie = None
|
|
global_data.session_data[global_data.player_num].song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en'])
|
|
if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None:
|
|
self.song_music = audio.load_music_stream(self.tja.metadata.wave, 'song')
|
|
|
|
self.player_1 = Player(self.tja, global_data.player_num, global_data.session_data[global_data.player_num].selected_difficulty, False, global_data.modifiers[global_data.player_num])
|
|
self.start_ms = get_current_ms() - self.tja.metadata.offset*1000
|
|
|
|
def get_song_hash(self, song: Path):
|
|
notes, branch_m, branch_e, branch_n = TJAParser.notes_to_position(TJAParser(song), self.player_1.difficulty)
|
|
if branch_m:
|
|
for branch in branch_m:
|
|
notes.play_notes.extend(branch.play_notes)
|
|
notes.draw_notes.extend(branch.draw_notes)
|
|
notes.bars.extend(branch.bars)
|
|
if branch_e:
|
|
for branch in branch_e:
|
|
notes.play_notes.extend(branch.play_notes)
|
|
notes.draw_notes.extend(branch.draw_notes)
|
|
notes.bars.extend(branch.bars)
|
|
if branch_n:
|
|
for branch in branch_n:
|
|
notes.play_notes.extend(branch.play_notes)
|
|
notes.draw_notes.extend(branch.draw_notes)
|
|
notes.bars.extend(branch.bars)
|
|
hash = self.tja.hash_note_data(notes)
|
|
return hash
|
|
|
|
def write_score(self):
|
|
"""Write the score to the database"""
|
|
if global_data.modifiers[global_data.player_num].auto:
|
|
return
|
|
with sqlite3.connect('scores.db') as con:
|
|
session_data = global_data.session_data[global_data.player_num]
|
|
cursor = con.cursor()
|
|
hash = self.get_song_hash(session_data.selected_song)
|
|
check_query = "SELECT score, clear FROM Scores WHERE hash = ? LIMIT 1"
|
|
cursor.execute(check_query, (hash,))
|
|
result = cursor.fetchone()
|
|
existing_score = result[0] if result is not None else None
|
|
existing_crown = result[1] if result is not None and len(result) > 1 and result[1] is not None else 0
|
|
crown = Crown.NONE
|
|
if session_data.result_data.bad and session_data.result_data.ok == 0:
|
|
crown = Crown.DFC
|
|
elif session_data.result_data.bad == 0:
|
|
crown = Crown.FC
|
|
elif self.player_1.gauge.is_clear:
|
|
crown = Crown.CLEAR
|
|
logger.info(f"Existing score: {existing_score}, Existing crown: {existing_crown}, New score: {session_data.result_data.score}, New crown: {crown}")
|
|
if result is None or (existing_score is not None and session_data.result_data.score > existing_score):
|
|
insert_query = '''
|
|
INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo, clear)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
|
'''
|
|
data = (hash, self.tja.metadata.title['en'],
|
|
self.tja.metadata.title.get('ja', ''), self.player_1.difficulty,
|
|
session_data.result_data.score, session_data.result_data.good,
|
|
session_data.result_data.ok, session_data.result_data.bad,
|
|
session_data.result_data.total_drumroll, session_data.result_data.max_combo, crown)
|
|
cursor.execute(insert_query, data)
|
|
session_data.result_data.prev_score = existing_score if existing_score is not None else 0
|
|
logger.info(f"Wrote score {session_data.result_data.score} for {self.tja.metadata.title['en']}")
|
|
con.commit()
|
|
if result is None or (existing_crown is not None and crown > existing_crown):
|
|
cursor.execute("UPDATE Scores SET clear = ? WHERE hash = ?", (crown, hash))
|
|
con.commit()
|
|
|
|
def start_song(self, ms_from_start):
|
|
if (ms_from_start >= self.tja.metadata.offset*1000 + self.start_delay - global_data.config["general"]["audio_offset"]) and not self.song_started:
|
|
if self.song_music is not None:
|
|
audio.play_music_stream(self.song_music, 'music')
|
|
logger.info(f"Song started at {ms_from_start}")
|
|
if self.movie is not None:
|
|
self.movie.start(get_current_ms())
|
|
self.song_started = True
|
|
|
|
def pause_song(self):
|
|
self.paused = not self.paused
|
|
if self.paused:
|
|
if self.song_music is not None:
|
|
self.audio_time = audio.get_music_time_played(self.song_music)
|
|
audio.stop_music_stream(self.song_music)
|
|
self.pause_time = get_current_ms() - self.start_ms
|
|
else:
|
|
if self.song_music is not None:
|
|
audio.play_music_stream(self.song_music, 'music')
|
|
audio.seek_music_stream(self.song_music, self.audio_time)
|
|
self.start_ms = get_current_ms() - self.pause_time
|
|
|
|
def global_keys(self):
|
|
if ray.is_key_pressed(global_data.config["keys"]["restart_key"]):
|
|
if self.song_music is not None:
|
|
audio.stop_music_stream(self.song_music)
|
|
self.init_tja(global_data.session_data[global_data.player_num].selected_song)
|
|
audio.play_sound('restart', 'sound')
|
|
self.song_started = False
|
|
|
|
if ray.is_key_pressed(global_data.config["keys"]["back_key"]):
|
|
if self.song_music is not None:
|
|
audio.stop_music_stream(self.song_music)
|
|
return self.on_screen_end('SONG_SELECT')
|
|
|
|
if ray.is_key_pressed(global_data.config["keys"]["pause_key"]):
|
|
self.pause_song()
|
|
|
|
def spawn_ending_anims(self):
|
|
if global_data.session_data[global_data.player_num].result_data.bad == 0:
|
|
self.player_1.ending_anim = FCAnimation(self.player_1.is_2p)
|
|
elif self.player_1.gauge.is_clear:
|
|
self.player_1.ending_anim = ClearAnimation(self.player_1.is_2p)
|
|
elif not self.player_1.gauge.is_clear:
|
|
self.player_1.ending_anim = FailAnimation(self.player_1.is_2p)
|
|
|
|
def update_background(self, current_time):
|
|
if self.movie is not None:
|
|
self.movie.update()
|
|
else:
|
|
if len(self.player_1.current_bars) > 0:
|
|
self.bpm = self.player_1.bpm
|
|
if self.background is not None:
|
|
self.background.update(current_time, self.bpm, self.player_1.gauge, None)
|
|
|
|
def update(self):
|
|
super().update()
|
|
current_time = get_current_ms()
|
|
self.transition.update(current_time)
|
|
if not self.paused:
|
|
self.current_ms = current_time - self.start_ms
|
|
if self.transition.is_finished:
|
|
self.start_song(self.current_ms)
|
|
else:
|
|
self.start_ms = current_time - self.tja.metadata.offset*1000
|
|
self.update_background(current_time)
|
|
|
|
if self.song_music is not None:
|
|
audio.update_music_stream(self.song_music)
|
|
|
|
self.player_1.update(self.current_ms, current_time, self.background)
|
|
self.song_info.update(current_time)
|
|
self.result_transition.update(current_time)
|
|
if self.result_transition.is_finished and not audio.is_sound_playing('result_transition'):
|
|
logger.info("Result transition finished, moving to RESULT screen")
|
|
return self.on_screen_end('RESULT')
|
|
elif self.current_ms >= self.player_1.end_time:
|
|
session_data = global_data.session_data[global_data.player_num]
|
|
session_data.result_data.score, session_data.result_data.good, session_data.result_data.ok, session_data.result_data.bad, session_data.result_data.max_combo, session_data.result_data.total_drumroll = self.player_1.get_result_score()
|
|
if self.player_1.gauge is not None:
|
|
session_data.result_data.gauge_length = self.player_1.gauge.gauge_length
|
|
if self.end_ms != 0:
|
|
if current_time >= self.end_ms + 1000:
|
|
if self.player_1.ending_anim is None:
|
|
self.write_score()
|
|
logger.info("Score written and ending animations spawned")
|
|
self.spawn_ending_anims()
|
|
if current_time >= self.end_ms + 8533.34:
|
|
if not self.result_transition.is_started:
|
|
self.result_transition.start()
|
|
audio.play_sound('result_transition', 'voice')
|
|
logger.info("Result transition started and voice played")
|
|
else:
|
|
self.end_ms = current_time
|
|
|
|
return self.global_keys()
|
|
|
|
def draw_overlay(self):
|
|
self.song_info.draw()
|
|
self.transition.draw()
|
|
self.result_transition.draw()
|
|
self.allnet_indicator.draw()
|
|
|
|
def draw(self):
|
|
if self.movie is not None:
|
|
self.movie.draw()
|
|
elif self.background is not None:
|
|
self.background.draw()
|
|
self.player_1.draw(self.current_ms, self.start_ms, self.mask_shader)
|
|
self.draw_overlay()
|
|
|
|
class Player:
|
|
TIMING_GOOD = 25.0250015258789
|
|
TIMING_OK = 75.0750045776367
|
|
TIMING_BAD = 108.441665649414
|
|
|
|
TIMING_GOOD_EASY = 41.7083358764648
|
|
TIMING_OK_EASY = 108.441665649414
|
|
TIMING_BAD_EASY = 125.125
|
|
|
|
def __init__(self, tja: TJAParser, player_num: PlayerNum, difficulty: int, is_2p: bool, modifiers: Modifiers):
|
|
self.is_2p = is_2p
|
|
self.is_dan = False
|
|
self.player_num = player_num
|
|
self.difficulty = difficulty
|
|
self.visual_offset = global_data.config["general"]["visual_offset"]
|
|
self.modifiers = modifiers
|
|
self.tja = tja
|
|
|
|
self.reset_chart()
|
|
|
|
#Score management
|
|
self.good_count = 0
|
|
self.ok_count = 0
|
|
self.bad_count = 0
|
|
self.combo = 0
|
|
self.score = 0
|
|
self.max_combo = 0
|
|
self.total_drumroll = 0
|
|
|
|
self.arc_points = 25
|
|
self.judge_x = 0
|
|
self.judge_y = 0
|
|
|
|
self.draw_judge_list: list[Judgment] = []
|
|
self.lane_hit_effect: Optional[LaneHitEffect] = None
|
|
self.draw_arc_list: list[NoteArc] = []
|
|
self.draw_drum_hit_list: list[DrumHitEffect] = []
|
|
self.drumroll_counter: Optional[DrumrollCounter] = None
|
|
self.balloon_anim: Optional[BalloonAnimation] = None
|
|
self.kusudama_anim: Optional[KusudamaAnimation] = None
|
|
self.base_score_list: list[ScoreCounterAnimation] = []
|
|
self.combo_display = Combo(self.combo, 0, self.is_2p)
|
|
self.score_counter = ScoreCounter(self.score, self.is_2p)
|
|
self.gogo_time: Optional[GogoTime] = None
|
|
self.combo_announce = ComboAnnounce(self.combo, 0, player_num, self.is_2p)
|
|
self.branch_indicator = BranchIndicator(self.is_2p) if tja and tja.metadata.course_data[self.difficulty].is_branching else None
|
|
self.ending_anim: Optional[FailAnimation | ClearAnimation | FCAnimation] = None
|
|
self.is_gogo_time = False
|
|
plate_info = global_data.config[f'nameplate_{self.is_2p+1}p']
|
|
self.nameplate = Nameplate(plate_info['name'], plate_info['title'], global_data.player_num, plate_info['dan'], plate_info['gold'], plate_info['rainbow'], plate_info['title_bg'])
|
|
self.chara = Chara2D(player_num - 1, self.bpm)
|
|
if global_data.config['general']['judge_counter']:
|
|
self.judge_counter = JudgeCounter()
|
|
else:
|
|
self.judge_counter = None
|
|
|
|
self.input_log: dict[float, str] = dict()
|
|
stars = tja.metadata.course_data[self.difficulty].level
|
|
self.gauge = Gauge(self.player_num, self.difficulty, stars, self.total_notes, self.is_2p)
|
|
self.gauge_hit_effect: list[GaugeHitEffect] = []
|
|
|
|
self.autoplay_hit_side = Side.LEFT
|
|
self.last_subdivision = -1
|
|
|
|
def reset_chart(self):
|
|
notes, self.branch_m, self.branch_e, self.branch_n = self.tja.notes_to_position(self.difficulty)
|
|
self.play_notes, self.draw_note_list, self.draw_bar_list = apply_modifiers(notes, self.modifiers)
|
|
self.end_time = 0
|
|
if self.play_notes:
|
|
self.end_time = self.play_notes[-1].hit_ms
|
|
if self.branch_m:
|
|
for section in self.branch_m:
|
|
if section.play_notes:
|
|
self.end_time = max(self.end_time, section.play_notes[-1].hit_ms)
|
|
if self.branch_e:
|
|
for section in self.branch_e:
|
|
if section.play_notes:
|
|
self.end_time = max(self.end_time, section.play_notes[-1].hit_ms)
|
|
if self.branch_n:
|
|
for section in self.branch_n:
|
|
if section.play_notes:
|
|
self.end_time = max(self.end_time, section.play_notes[-1].hit_ms)
|
|
|
|
self.don_notes = deque([note for note in self.play_notes if note.type in {NoteType.DON, NoteType.DON_L}])
|
|
self.kat_notes = deque([note for note in self.play_notes if note.type in {NoteType.KAT, NoteType.KAT_L}])
|
|
self.other_notes = deque([note for note in self.play_notes if note.type not in {NoteType.DON, NoteType.DON_L, NoteType.KAT, NoteType.KAT_L}])
|
|
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] = []
|
|
self.current_notes_draw: list[Note | Drumroll | Balloon] = []
|
|
self.is_drumroll = False
|
|
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 = self.play_notes[0].bpm if self.play_notes else 120
|
|
|
|
def merge_branch_section(self, branch_section: NoteList, current_ms: float):
|
|
"""Merges the branch notes into the current notes"""
|
|
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))
|
|
total_don = [note for note in self.play_notes if note.type in {NoteType.DON, NoteType.DON_L}]
|
|
total_kat = [note for note in self.play_notes if note.type in {NoteType.KAT, NoteType.KAT_L}]
|
|
total_other = [note for note in self.play_notes if note.type not in {NoteType.DON, NoteType.DON_L, NoteType.KAT, NoteType.KAT_L}]
|
|
|
|
self.don_notes = deque([note for note in total_don if note.hit_ms > current_ms])
|
|
self.kat_notes = deque([note for note in total_kat if note.hit_ms > current_ms])
|
|
self.other_notes = deque([note for note in total_other if note.hit_ms > current_ms])
|
|
|
|
def get_result_score(self):
|
|
"""Returns the score, good count, ok count, bad count, max combo, and total drumroll"""
|
|
return self.score, self.good_count, self.ok_count, self.bad_count, self.max_combo, self.total_drumroll
|
|
|
|
def get_position_x(self, width: int, current_ms: float, load_ms: float, pixels_per_frame: float) -> int:
|
|
"""Calculates the x-coordinate of a note based on its load time and current time"""
|
|
time_diff = load_ms - current_ms
|
|
return int(width + pixels_per_frame * 0.06 * time_diff - (tex.textures["notes"]["1"].width//2)) - self.visual_offset
|
|
|
|
def get_position_y(self, current_ms: float, load_ms: float, pixels_per_frame: float, pixels_per_frame_x) -> int:
|
|
"""Calculates the y-coordinate of a note based on its load time and current time"""
|
|
time_diff = load_ms - current_ms
|
|
return int((pixels_per_frame * 0.06 * time_diff) + ((self.tja.distance * 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 * tex.screen_scale
|
|
judge_y = bar.judge_pos_y * tex.screen_scale
|
|
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
|
|
|
|
# More efficient: use list comprehension to filter out finished animations
|
|
remaining_animations = []
|
|
for animation in animation_list:
|
|
animation.update(current_time)
|
|
if not animation.is_finished:
|
|
remaining_animations.append(animation)
|
|
|
|
# Replace the original list contents
|
|
animation_list[:] = remaining_animations
|
|
|
|
def bar_manager(self, current_ms: float):
|
|
"""Manages the bars and removes if necessary
|
|
Also sets branch conditions"""
|
|
#Add bar to current_bars list if it is ready to be shown on screen
|
|
if self.draw_bar_list and current_ms > self.draw_bar_list[0].load_ms:
|
|
self.current_bars.append(self.draw_bar_list.popleft())
|
|
|
|
#If a bar is off screen, remove it
|
|
if not self.current_bars:
|
|
return
|
|
|
|
# More efficient removal with early exit
|
|
removal_threshold = GameScreen.JUDGE_X + (650 * tex.screen_scale)
|
|
bars_to_keep = []
|
|
for bar in self.current_bars:
|
|
position = self.get_position_x(tex.screen_width, current_ms, bar.hit_ms, bar.pixels_per_frame_x)
|
|
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 = float(e_req)
|
|
m_req = float(m_req)
|
|
logger.info(f'branch condition measures started with conditions {self.branch_condition}, {e_req}, {m_req}, {self.current_bars[-1].hit_ms}')
|
|
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.other_notes,
|
|
self.branch_m[0].play_notes if self.branch_m else [],
|
|
self.branch_e[0].play_notes if self.branch_e else [],
|
|
self.branch_n[0].play_notes if self.branch_n else [],
|
|
]
|
|
|
|
end_roll = -1
|
|
for notes in note_lists:
|
|
for i in range(len(notes)-1, -1, -1):
|
|
if notes[i].type == NoteType.TAIL 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, 1]
|
|
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, max(len(seen_notes), 1)]
|
|
def play_note_manager(self, current_ms: float, background: Optional[Background]):
|
|
"""Manages the play_notes and removes if necessary"""
|
|
if self.don_notes and self.don_notes[0].hit_ms + Player.TIMING_BAD < current_ms:
|
|
self.combo = 0
|
|
if background is not None:
|
|
if self.is_2p:
|
|
background.add_chibi(True, 2)
|
|
else:
|
|
background.add_chibi(True, 1)
|
|
self.bad_count += 1
|
|
self.input_log[self.don_notes[0].index] = 'BAD'
|
|
if self.gauge is not None:
|
|
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
|
|
if background is not None:
|
|
if self.is_2p:
|
|
background.add_chibi(True, 2)
|
|
else:
|
|
background.add_chibi(True, 1)
|
|
self.bad_count += 1
|
|
self.input_log[self.kat_notes[0].index] = 'BAD'
|
|
if self.gauge is not None:
|
|
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
|
|
|
|
note = self.other_notes[0]
|
|
if (note.hit_ms <= current_ms):
|
|
if note.type == NoteType.ROLL_HEAD or note.type == NoteType.ROLL_HEAD_L:
|
|
self.is_drumroll = True
|
|
elif note.type == NoteType.BALLOON_HEAD or note.type == NoteType.KUSUDAMA:
|
|
self.is_balloon = True
|
|
elif note.type == NoteType.TAIL:
|
|
self.other_notes.popleft()
|
|
self.is_drumroll = False
|
|
self.is_balloon = False
|
|
self.curr_balloon_count = 0
|
|
self.curr_drumroll_count = 0
|
|
return
|
|
tail = self.other_notes[1]
|
|
if tail.hit_ms <= current_ms:
|
|
self.other_notes.popleft()
|
|
self.other_notes.popleft()
|
|
self.is_drumroll = False
|
|
self.is_balloon = False
|
|
self.curr_balloon_count = 0
|
|
self.curr_drumroll_count = 0
|
|
|
|
def draw_note_manager(self, current_ms: float):
|
|
"""Manages the draw_notes and removes if necessary"""
|
|
if self.draw_note_list and current_ms + 1000 >= self.draw_note_list[0].load_ms:
|
|
current_note = self.draw_note_list.popleft()
|
|
if 5 <= current_note.type <= 7:
|
|
bisect.insort_left(self.current_notes_draw, current_note, key=lambda x: x.index)
|
|
try:
|
|
tail_note = next((note for note in self.draw_note_list if note.type == NoteType.TAIL))
|
|
bisect.insort_left(self.current_notes_draw, tail_note, key=lambda x: x.index)
|
|
self.draw_note_list.remove(tail_note)
|
|
except Exception as e:
|
|
raise(e)
|
|
else:
|
|
bisect.insort_left(self.current_notes_draw, current_note, key=lambda x: x.index)
|
|
|
|
if not self.current_notes_draw:
|
|
return
|
|
|
|
if isinstance(self.current_notes_draw[0], Drumroll):
|
|
self.current_notes_draw[0].color = min(255, self.current_notes_draw[0].color + 1)
|
|
|
|
note = self.current_notes_draw[0]
|
|
if note.type in {NoteType.ROLL_HEAD, NoteType.ROLL_HEAD_L, NoteType.BALLOON_HEAD, NoteType.KUSUDAMA} and len(self.current_notes_draw) > 1:
|
|
note = self.current_notes_draw[1]
|
|
position = self.get_position_x(tex.screen_width, current_ms, note.hit_ms, note.pixels_per_frame_x)
|
|
if position < GameScreen.JUDGE_X + (650 * tex.screen_scale):
|
|
self.current_notes_draw.pop(0)
|
|
|
|
def note_manager(self, current_ms: float, background: Optional[Background]):
|
|
self.bar_manager(current_ms)
|
|
self.play_note_manager(current_ms, background)
|
|
self.draw_note_manager(current_ms)
|
|
|
|
def note_correct(self, note: Note, current_time: float):
|
|
"""Removes a note from the appropriate separated list"""
|
|
if note.type in {NoteType.DON, NoteType.DON_L} and self.don_notes and self.don_notes[0] == note:
|
|
self.don_notes.popleft()
|
|
elif note.type in {NoteType.KAT, NoteType.KAT_L} and self.kat_notes and self.kat_notes[0] == note:
|
|
self.kat_notes.popleft()
|
|
elif note.type not in {NoteType.DON, NoteType.DON_L, NoteType.KAT, NoteType.KAT_L} and self.other_notes and self.other_notes[0] == note:
|
|
self.other_notes.popleft()
|
|
|
|
index = note.index
|
|
if note.type == NoteType.BALLOON_HEAD:
|
|
if self.other_notes:
|
|
self.other_notes.popleft()
|
|
|
|
if note.type < 7:
|
|
self.combo += 1
|
|
if self.combo % 10 == 0:
|
|
self.chara.set_animation('10_combo')
|
|
if self.combo % 100 == 0:
|
|
self.combo_announce = ComboAnnounce(self.combo, current_time, self.player_num, self.is_2p)
|
|
if self.combo > self.max_combo:
|
|
self.max_combo = self.combo
|
|
|
|
if note.type != NoteType.KUSUDAMA:
|
|
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)
|
|
self.current_notes_draw.pop(index)
|
|
|
|
def check_drumroll(self, drum_type: DrumType, background: Optional[Background], current_time: float):
|
|
"""Checks if a note has been hit during a drumroll"""
|
|
self.draw_arc_list.append(NoteArc(drum_type, current_time, PlayerNum(self.is_2p + 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
|
|
self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p))
|
|
if not isinstance(self.current_notes_draw[0], Drumroll):
|
|
return
|
|
self.current_notes_draw[0].color = max(0, 255 - (self.curr_drumroll_count * 10))
|
|
|
|
def check_balloon(self, drum_type: DrumType, note: Balloon, current_time: float):
|
|
"""Checks if the player has popped a balloon"""
|
|
if drum_type != DrumType.DON:
|
|
return
|
|
if note.is_kusudama:
|
|
self.check_kusudama(note)
|
|
return
|
|
if self.balloon_anim is None:
|
|
self.balloon_anim = BalloonAnimation(current_time, note.count, self.player_num, self.is_2p)
|
|
self.curr_balloon_count += 1
|
|
self.total_drumroll += 1
|
|
self.score += 100
|
|
self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p))
|
|
if self.curr_balloon_count == note.count:
|
|
self.is_balloon = False
|
|
note.popped = True
|
|
self.balloon_anim.update(current_time, self.curr_balloon_count, note.popped)
|
|
audio.play_sound('balloon_pop', 'hitsound')
|
|
self.note_correct(note, current_time)
|
|
self.curr_balloon_count = 0
|
|
|
|
def check_kusudama(self, note: Balloon):
|
|
"""Checks if the player has popped a kusudama"""
|
|
if self.kusudama_anim is None:
|
|
self.kusudama_anim = KusudamaAnimation(note.count)
|
|
self.curr_balloon_count += 1
|
|
self.total_drumroll += 1
|
|
self.score += 100
|
|
self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p))
|
|
if self.curr_balloon_count == note.count:
|
|
audio.play_sound('kusudama_pop', 'hitsound')
|
|
self.is_balloon = False
|
|
note.popped = True
|
|
self.curr_balloon_count = 0
|
|
|
|
def check_note(self, ms_from_start: float, drum_type: DrumType, current_time: float, background: Optional[Background]):
|
|
"""Checks if the player has hit a note"""
|
|
if len(self.don_notes) == 0 and len(self.kat_notes) == 0 and len(self.other_notes) == 0:
|
|
return
|
|
|
|
if self.difficulty < Difficulty.NORMAL:
|
|
good_window_ms = Player.TIMING_GOOD_EASY
|
|
ok_window_ms = Player.TIMING_OK_EASY
|
|
bad_window_ms = Player.TIMING_BAD_EASY
|
|
else:
|
|
good_window_ms = Player.TIMING_GOOD
|
|
ok_window_ms = Player.TIMING_OK
|
|
bad_window_ms = Player.TIMING_BAD
|
|
|
|
curr_note = self.other_notes[0] if self.other_notes else None
|
|
if self.is_drumroll:
|
|
self.check_drumroll(drum_type, background, current_time)
|
|
elif self.is_balloon:
|
|
if not isinstance(curr_note, Balloon):
|
|
raise Exception("Balloon mode entered but current note is not balloon")
|
|
self.check_balloon(drum_type, curr_note, current_time)
|
|
else:
|
|
self.curr_drumroll_count = 0
|
|
|
|
if drum_type == DrumType.DON:
|
|
if not self.don_notes:
|
|
return
|
|
curr_note = self.don_notes[0]
|
|
else:
|
|
if not self.kat_notes:
|
|
return
|
|
curr_note = self.kat_notes[0]
|
|
|
|
#If the note is too far away, stop checking
|
|
if ms_from_start > (curr_note.hit_ms + bad_window_ms):
|
|
return
|
|
|
|
big = curr_note.type == NoteType.DON_L or curr_note.type == NoteType.KAT_L
|
|
if (curr_note.hit_ms - good_window_ms) <= ms_from_start <= (curr_note.hit_ms + good_window_ms):
|
|
self.draw_judge_list.append(Judgment(Judgments.GOOD, big, self.is_2p))
|
|
self.lane_hit_effect = LaneHitEffect(Judgments.GOOD, self.is_2p)
|
|
self.good_count += 1
|
|
self.score += self.base_score
|
|
self.base_score_list.append(ScoreCounterAnimation(self.player_num, self.base_score, self.is_2p))
|
|
self.input_log[curr_note.index] = 'GOOD'
|
|
self.note_correct(curr_note, current_time)
|
|
if self.gauge is not None:
|
|
self.gauge.add_good()
|
|
if self.is_branch and self.branch_condition == 'p':
|
|
self.branch_condition_count += 1
|
|
if background is not None:
|
|
if self.is_2p:
|
|
background.add_chibi(False, 2)
|
|
else:
|
|
background.add_chibi(False, 1)
|
|
|
|
elif (curr_note.hit_ms - ok_window_ms) <= ms_from_start <= (curr_note.hit_ms + ok_window_ms):
|
|
self.draw_judge_list.append(Judgment(Judgments.OK, big, self.is_2p))
|
|
self.ok_count += 1
|
|
self.score += 10 * math.floor(self.base_score / 2 / 10)
|
|
self.base_score_list.append(ScoreCounterAnimation(self.player_num, 10 * math.floor(self.base_score / 2 / 10), self.is_2p))
|
|
self.input_log[curr_note.index] = 'OK'
|
|
self.note_correct(curr_note, current_time)
|
|
if self.gauge is not None:
|
|
self.gauge.add_ok()
|
|
if self.is_branch and self.branch_condition == 'p':
|
|
self.branch_condition_count += 0.5
|
|
if background is not None:
|
|
if self.is_2p:
|
|
background.add_chibi(False, 2)
|
|
else:
|
|
background.add_chibi(False, 1)
|
|
|
|
elif (curr_note.hit_ms - bad_window_ms) <= ms_from_start <= (curr_note.hit_ms + bad_window_ms):
|
|
self.input_log[curr_note.index] = 'BAD'
|
|
self.draw_judge_list.append(Judgment(Judgments.BAD, big, self.is_2p))
|
|
self.bad_count += 1
|
|
self.combo = 0
|
|
if drum_type == DrumType.DON:
|
|
note = self.don_notes.popleft()
|
|
else:
|
|
note = self.kat_notes.popleft()
|
|
if note in self.current_notes_draw:
|
|
self.current_notes_draw.remove(note)
|
|
if self.gauge is not None:
|
|
self.gauge.add_bad()
|
|
if background is not None:
|
|
if self.is_2p:
|
|
background.add_chibi(True, 2)
|
|
else:
|
|
background.add_chibi(True, 1)
|
|
|
|
def drumroll_counter_manager(self, current_time: float):
|
|
"""Manages drumroll counter behavior"""
|
|
if self.is_drumroll and self.curr_drumroll_count > 0 and self.drumroll_counter is None:
|
|
self.drumroll_counter = DrumrollCounter(self.is_2p)
|
|
|
|
if self.drumroll_counter is not None:
|
|
if self.drumroll_counter.is_finished and not self.is_drumroll:
|
|
self.drumroll_counter = None
|
|
else:
|
|
self.drumroll_counter.update(current_time, self.curr_drumroll_count)
|
|
|
|
def balloon_manager(self, current_time: float):
|
|
"""Manages balloon and kusudama behavior"""
|
|
if self.balloon_anim is not None:
|
|
self.chara.set_animation('balloon_popping')
|
|
self.balloon_anim.update(current_time, self.curr_balloon_count, not self.is_balloon)
|
|
if self.balloon_anim.is_finished:
|
|
self.balloon_anim = None
|
|
self.chara.set_animation('balloon_pop')
|
|
if self.kusudama_anim is not None:
|
|
self.kusudama_anim.update(current_time, not self.is_balloon)
|
|
self.kusudama_anim.update_count(self.curr_balloon_count)
|
|
if self.kusudama_anim.is_finished:
|
|
self.kusudama_anim = None
|
|
|
|
def spawn_hit_effects(self, drum_type: DrumType, side: Side):
|
|
self.lane_hit_effect = LaneHitEffect(drum_type, self.is_2p)
|
|
self.draw_drum_hit_list.append(DrumHitEffect(drum_type, side, self.is_2p))
|
|
|
|
def handle_input(self, ms_from_start: float, current_time: float, background: Optional[Background]):
|
|
input_checks = [
|
|
(is_l_don_pressed, DrumType.DON, Side.LEFT, f'hitsound_don_{self.player_num}p'),
|
|
(is_r_don_pressed, DrumType.DON, Side.RIGHT, f'hitsound_don_{self.player_num}p'),
|
|
(is_l_kat_pressed, DrumType.KAT, Side.LEFT, f'hitsound_kat_{self.player_num}p'),
|
|
(is_r_kat_pressed, DrumType.KAT, Side.RIGHT, f'hitsound_kat_{self.player_num}p')
|
|
]
|
|
for check_func, drum_type, side, sound in input_checks:
|
|
if check_func(self.player_num):
|
|
self.spawn_hit_effects(drum_type, side)
|
|
audio.play_sound(sound, 'hitsound')
|
|
self.check_note(ms_from_start, drum_type, current_time, background)
|
|
|
|
def autoplay_manager(self, ms_from_start: float, current_time: float, background: Optional[Background]):
|
|
"""Manages autoplay behavior"""
|
|
if not self.modifiers.auto:
|
|
return
|
|
|
|
# Handle drumroll and balloon hits
|
|
if self.is_drumroll or self.is_balloon:
|
|
if not self.other_notes:
|
|
return
|
|
note = self.other_notes[0]
|
|
bpm = note.bpm
|
|
if bpm == 0:
|
|
subdivision_in_ms = 0
|
|
else:
|
|
subdivision_in_ms = ms_from_start // ((60000 * 4 / bpm) / 24)
|
|
if subdivision_in_ms > self.last_subdivision:
|
|
self.last_subdivision = subdivision_in_ms
|
|
hit_type = DrumType.DON
|
|
self.autoplay_hit_side = Side.RIGHT if self.autoplay_hit_side == Side.LEFT else Side.LEFT
|
|
self.spawn_hit_effects(hit_type, self.autoplay_hit_side)
|
|
audio.play_sound(f'hitsound_don_{self.player_num}p', 'hitsound')
|
|
self.check_note(ms_from_start, hit_type, current_time, background)
|
|
else:
|
|
# Handle DON notes
|
|
while self.don_notes and ms_from_start >= self.don_notes[0].hit_ms:
|
|
note = self.don_notes[0]
|
|
hit_type = DrumType.DON
|
|
self.autoplay_hit_side = Side.RIGHT if self.autoplay_hit_side == Side.LEFT else Side.LEFT
|
|
self.spawn_hit_effects(hit_type, self.autoplay_hit_side)
|
|
audio.play_sound(f'hitsound_don_{self.player_num}p', 'hitsound')
|
|
self.check_note(ms_from_start, hit_type, current_time, background)
|
|
|
|
# Handle KAT notes
|
|
while self.kat_notes and ms_from_start >= self.kat_notes[0].hit_ms:
|
|
note = self.kat_notes[0]
|
|
hit_type = DrumType.KAT
|
|
self.autoplay_hit_side = Side.RIGHT if self.autoplay_hit_side == Side.LEFT else Side.LEFT
|
|
self.spawn_hit_effects(hit_type, self.autoplay_hit_side)
|
|
audio.play_sound(f'hitsound_kat_{self.player_num}p', 'hitsound')
|
|
self.check_note(ms_from_start, hit_type, current_time, background)
|
|
|
|
def evaluate_branch(self, current_ms):
|
|
"""Evaluates the branch condition and updates the branch status"""
|
|
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 = max(min((self.branch_condition_count/total_notes)*100, 100), 0)
|
|
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)
|
|
if self.branch_indicator is not None:
|
|
logger.info(f"Branch set to {self.branch_indicator.difficulty} based on conditions {self.branch_condition_count}, {e_req, m_req}")
|
|
self.branch_condition_count = 0
|
|
|
|
def update(self, ms_from_start: float, current_time: float, background: Optional[Background]):
|
|
self.note_manager(ms_from_start, background)
|
|
self.combo_display.update(current_time, self.combo)
|
|
self.combo_announce.update(current_time)
|
|
self.drumroll_counter_manager(current_time)
|
|
self.animation_manager(self.draw_judge_list, current_time)
|
|
self.balloon_manager(current_time)
|
|
if self.gogo_time is not None:
|
|
self.gogo_time.update(current_time)
|
|
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 = []
|
|
for i, anim in enumerate(self.draw_arc_list):
|
|
anim.update(current_time)
|
|
if anim.is_finished:
|
|
self.gauge_hit_effect.append(GaugeHitEffect(anim.note_type, anim.is_big, self.is_2p))
|
|
finished_arcs.append(i)
|
|
for i in reversed(finished_arcs):
|
|
self.draw_arc_list.pop(i)
|
|
|
|
self.animation_manager(self.gauge_hit_effect, current_time)
|
|
self.animation_manager(self.base_score_list, current_time)
|
|
self.score_counter.update(current_time, self.score)
|
|
self.autoplay_manager(ms_from_start, current_time, background)
|
|
self.handle_input(ms_from_start, current_time, background)
|
|
self.nameplate.update(current_time)
|
|
if self.gauge is not None:
|
|
self.gauge.update(current_time)
|
|
if self.judge_counter is not None:
|
|
self.judge_counter.update(self.good_count, self.ok_count, self.bad_count, self.total_drumroll)
|
|
if self.branch_indicator is not None:
|
|
self.branch_indicator.update(current_time)
|
|
if self.ending_anim is not None:
|
|
self.ending_anim.update(current_time)
|
|
|
|
if self.is_branch:
|
|
self.evaluate_branch(ms_from_start)
|
|
|
|
# Get the next note from any of the three lists for BPM and gogo time updates
|
|
next_note = None
|
|
candidates = []
|
|
if self.don_notes:
|
|
candidates.append(self.don_notes[0])
|
|
if self.kat_notes:
|
|
candidates.append(self.kat_notes[0])
|
|
if self.other_notes:
|
|
candidates.append(self.other_notes[0])
|
|
|
|
if candidates:
|
|
next_note = min(candidates, key=lambda note: note.load_ms)
|
|
|
|
if next_note:
|
|
self.bpm = next_note.bpm
|
|
if next_note.gogo_time and not self.is_gogo_time:
|
|
self.is_gogo_time = True
|
|
self.gogo_time = GogoTime(self.is_2p)
|
|
self.chara.set_animation('gogo_start')
|
|
if not next_note.gogo_time and self.is_gogo_time:
|
|
self.is_gogo_time = False
|
|
self.gogo_time = None
|
|
self.chara.set_animation('gogo_stop')
|
|
if self.gauge is None:
|
|
self.chara.update(current_time, self.bpm, False, False)
|
|
else:
|
|
self.chara.update(current_time, self.bpm, self.gauge.is_clear, self.gauge.is_rainbow)
|
|
|
|
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(tex.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(tex.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)
|
|
y = tex.skin_config["notes"].y
|
|
moji_y = tex.skin_config["moji"].y
|
|
moji_x = tex.skin_config["moji"].x
|
|
if head.display:
|
|
if length > 0:
|
|
tex.draw_texture('notes', "8", frame=is_big, x=start_position+(tex.textures["notes"]["8"].width//2), y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length+tex.skin_config["drumroll_width_offset"].width, color=color)
|
|
if is_big:
|
|
tex.draw_texture('notes', "drumroll_big_tail", x=end_position+tex.textures["notes"]["drumroll_big_tail"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
|
|
else:
|
|
tex.draw_texture('notes', "drumroll_tail", x=end_position+tex.textures["notes"]["drumroll_tail"].width//2, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
|
|
tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, color=color)
|
|
|
|
tex.draw_texture('notes', 'moji_drumroll_mid', x=start_position + tex.skin_config["moji_drumroll"].x, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y, x2=length)
|
|
tex.draw_texture('notes', 'moji', frame=head.moji, x=start_position - moji_x, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
|
|
tex.draw_texture('notes', 'moji', frame=tail.moji, x=end_position - tex.skin_config["moji_drumroll"].width, y=moji_y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
|
|
|
|
def draw_balloon(self, current_ms: float, head: Balloon, current_eighth: int):
|
|
"""Draws a balloon in the player's lane"""
|
|
offset = tex.skin_config["balloon_offset"].x
|
|
start_position = self.get_position_x(tex.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(tex.screen_width, current_ms, tail.load_ms, tail.pixels_per_frame_x)
|
|
end_position += self.judge_x
|
|
pause_position = tex.skin_config["balloon_pause_position"].x + self.judge_x
|
|
if current_ms >= tail.hit_ms:
|
|
position = end_position
|
|
elif current_ms >= head.hit_ms:
|
|
position = pause_position
|
|
else:
|
|
position = start_position
|
|
if head.display:
|
|
tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=position-offset, y=tex.skin_config["notes"].y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
|
|
tex.draw_texture('notes', '10', frame=current_eighth % 2, x=position-offset+tex.textures["notes"]["10"].width, y=tex.skin_config["notes"].y+(self.is_2p*tex.skin_config["2p_offset"].y)+self.judge_y)
|
|
|
|
def draw_bars(self, current_ms: float):
|
|
"""Draw bars in the player's lane"""
|
|
if not self.current_bars:
|
|
return
|
|
|
|
for bar in reversed(self.current_bars):
|
|
if not bar.display:
|
|
continue
|
|
x_position = self.get_position_x(tex.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:
|
|
frame = 0
|
|
if y_position != 0:
|
|
angle = math.degrees(math.atan2(bar.pixels_per_frame_y, bar.pixels_per_frame_x))
|
|
else:
|
|
angle = 0
|
|
tex.draw_texture('notes', str(bar.type), frame=frame, x=x_position+tex.skin_config["moji_drumroll"].x, y=y_position+tex.skin_config["moji_drumroll"].y+(self.is_2p*tex.skin_config["2p_offset"].y), rotation=angle)
|
|
|
|
|
|
def draw_notes(self, current_ms: float, start_ms: float):
|
|
"""Draw notes in the player's lane"""
|
|
if not self.current_notes_draw:
|
|
return
|
|
eighth_in_ms = 0 if self.bpm == 0 else (60000 * 4 / self.bpm) / 8
|
|
current_eighth = 0
|
|
if self.combo >= 50 and eighth_in_ms != 0:
|
|
current_eighth = int((current_ms - start_ms) // eighth_in_ms)
|
|
|
|
for note in reversed(self.current_notes_draw):
|
|
if self.balloon_anim is not None and note == self.current_notes_draw[0]:
|
|
continue
|
|
if note.type == NoteType.TAIL:
|
|
continue
|
|
|
|
if hasattr(note, 'sudden_appear_ms') and hasattr(note, 'sudden_moving_ms'):
|
|
appear_ms = note.hit_ms - note.sudden_appear_ms
|
|
moving_start_ms = note.hit_ms - note.sudden_moving_ms
|
|
|
|
if current_ms < appear_ms:
|
|
continue
|
|
|
|
if current_ms < moving_start_ms:
|
|
effective_ms = moving_start_ms
|
|
else:
|
|
effective_ms = current_ms
|
|
|
|
x_position = self.get_position_x(tex.screen_width, effective_ms, note.load_ms, note.pixels_per_frame_x)
|
|
y_position = self.get_position_y(effective_ms, note.load_ms, note.pixels_per_frame_y, note.pixels_per_frame_x)
|
|
else:
|
|
x_position = self.get_position_x(tex.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:
|
|
self.draw_balloon(current_ms, note, current_eighth)
|
|
tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position, y=tex.skin_config["moji"].y + y_position+(self.is_2p*tex.skin_config["2p_offset"].y))
|
|
else:
|
|
if note.display:
|
|
tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position, y=y_position+tex.skin_config["notes"].y+(self.is_2p*tex.skin_config["2p_offset"].y), center=True)
|
|
tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (tex.textures["notes"]["moji"].width//2) + (tex.textures["notes"]["1"].width//2), y=tex.skin_config["moji"].y + y_position+(self.is_2p*tex.skin_config["2p_offset"].y))
|
|
|
|
ray.draw_text(self.current_notes_draw[0].lyric, tex.screen_width//2 - (ray.measure_text(self.current_notes_draw[0].lyric, int(40 * tex.screen_scale))//2), tex.screen_height - int(50 * tex.screen_scale), int(40 * tex.screen_scale), ray.BLUE)
|
|
|
|
|
|
def draw_modifiers(self):
|
|
"""Shows the currently selected modifiers"""
|
|
modifiers_to_draw = ['mod_shinuchi']
|
|
|
|
# Speed modifiers
|
|
if global_data.modifiers[self.player_num].speed >= 4:
|
|
modifiers_to_draw.append('mod_yonbai')
|
|
elif global_data.modifiers[self.player_num].speed >= 3:
|
|
modifiers_to_draw.append('mod_sanbai')
|
|
elif global_data.modifiers[self.player_num].speed > 1:
|
|
modifiers_to_draw.append('mod_baisaku')
|
|
|
|
# Other modifiers
|
|
if global_data.modifiers[self.player_num].display:
|
|
modifiers_to_draw.append('mod_doron')
|
|
if global_data.modifiers[self.player_num].inverse:
|
|
modifiers_to_draw.append('mod_abekobe')
|
|
if global_data.modifiers[self.player_num].random == 2:
|
|
modifiers_to_draw.append('mod_detarame')
|
|
elif global_data.modifiers[self.player_num].random == 1:
|
|
modifiers_to_draw.append('mod_kimagure')
|
|
|
|
# Draw all modifiers in one batch
|
|
for modifier in modifiers_to_draw:
|
|
tex.draw_texture('lane', modifier, index=self.is_2p)
|
|
|
|
def draw_overlays(self, mask_shader: ray.Shader):
|
|
# Group 4: Lane covers and UI elements (batch similar textures)
|
|
tex.draw_texture('lane', f'{self.player_num}p_lane_cover', index=self.is_2p)
|
|
if self.is_dan:
|
|
tex.draw_texture('lane', 'dan_lane_cover')
|
|
tex.draw_texture('lane', 'drum', index=self.is_2p)
|
|
if self.ending_anim is not None:
|
|
self.ending_anim.draw()
|
|
|
|
# Group 5: Hit effects and animations
|
|
for anim in self.draw_drum_hit_list:
|
|
anim.draw()
|
|
for anim in self.draw_arc_list:
|
|
anim.draw(mask_shader)
|
|
for anim in self.gauge_hit_effect:
|
|
anim.draw()
|
|
|
|
# Group 6: UI overlays
|
|
self.combo_display.draw()
|
|
self.combo_announce.draw()
|
|
if self.is_2p:
|
|
tex.draw_texture('lane', 'lane_score_cover', index=self.is_2p, mirror='vertical')
|
|
else:
|
|
tex.draw_texture('lane', 'lane_score_cover', index=self.is_2p)
|
|
tex.draw_texture('lane', f'{self.player_num}p_icon', index=self.is_2p)
|
|
if self.is_dan:
|
|
tex.draw_texture('lane', 'lane_difficulty', frame=6)
|
|
else:
|
|
tex.draw_texture('lane', 'lane_difficulty', frame=self.difficulty, index=self.is_2p)
|
|
if self.judge_counter is not None:
|
|
self.judge_counter.draw()
|
|
|
|
# Group 7: Player-specific elements
|
|
if not self.modifiers.auto:
|
|
if self.is_2p:
|
|
self.nameplate.draw(tex.skin_config["game_nameplate_1p"].x, tex.skin_config["game_nameplate_1p"].y)
|
|
else:
|
|
self.nameplate.draw(tex.skin_config["game_nameplate_2p"].x, tex.skin_config["game_nameplate_2p"].y)
|
|
else:
|
|
tex.draw_texture('lane', 'auto_icon', index=self.is_2p)
|
|
self.draw_modifiers()
|
|
self.chara.draw(y=(self.is_2p*tex.skin_config["game_2p_offset"].y))
|
|
|
|
# Group 8: Special animations and counters
|
|
if self.drumroll_counter is not None:
|
|
self.drumroll_counter.draw()
|
|
if self.balloon_anim is not None:
|
|
self.balloon_anim.draw()
|
|
if self.kusudama_anim is not None:
|
|
self.kusudama_anim.draw()
|
|
self.score_counter.draw()
|
|
for anim in self.base_score_list:
|
|
anim.draw()
|
|
|
|
def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader, dan_transition = None):
|
|
# Group 1: Background and lane elements
|
|
tex.draw_texture('lane', 'lane_background', index=self.is_2p)
|
|
if self.branch_indicator is not None:
|
|
self.branch_indicator.draw()
|
|
if self.gauge is not None:
|
|
self.gauge.draw()
|
|
if self.lane_hit_effect is not None:
|
|
self.lane_hit_effect.draw()
|
|
tex.draw_texture('lane', 'lane_hit_circle', x=self.judge_x, y=self.judge_y, index=self.is_2p)
|
|
|
|
# Group 2: judgment and hit effects
|
|
if self.gogo_time is not None:
|
|
self.gogo_time.draw(self.judge_x, self.judge_y)
|
|
for anim in self.draw_judge_list:
|
|
anim.draw(self.judge_x, self.judge_y)
|
|
|
|
# Group 3: Notes and bars (game content)
|
|
self.draw_bars(ms_from_start)
|
|
self.draw_notes(ms_from_start, start_ms)
|
|
if dan_transition is not None:
|
|
dan_transition.draw()
|
|
|
|
self.draw_overlays(mask_shader)
|
|
|
|
class Judgment:
|
|
"""Shows the judgment of the player's hit"""
|
|
def __init__(self, type: Judgments, big: bool, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.type = type
|
|
self.big = big
|
|
self.is_finished = False
|
|
|
|
self.fade_animation_1 = tex.get_animation(27, is_copy=True)
|
|
self.fade_animation_2 = tex.get_animation(28, is_copy=True)
|
|
self.move_animation = tex.get_animation(29, is_copy=True)
|
|
self.texture_animation = tex.get_animation(30, is_copy=True)
|
|
self.move_animation.start()
|
|
self.fade_animation_2.start()
|
|
self.fade_animation_1.start()
|
|
self.texture_animation.start()
|
|
|
|
def update(self, current_ms):
|
|
animations = [self.fade_animation_1, self.fade_animation_2, self.move_animation, self.texture_animation]
|
|
for anim in animations:
|
|
anim.update(current_ms)
|
|
|
|
if self.fade_animation_2.is_finished:
|
|
self.is_finished = True
|
|
|
|
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 == Judgments.GOOD:
|
|
if self.big:
|
|
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', 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 == Judgments.OK:
|
|
if self.big:
|
|
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', 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 == Judgments.BAD:
|
|
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"""
|
|
def __init__(self, type: Judgments | DrumType, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.type = type
|
|
self.fade = tex.get_animation(0, is_copy=True)
|
|
self.fade.start()
|
|
self.is_finished = False
|
|
|
|
def update(self, current_ms: float):
|
|
self.fade.update(current_ms)
|
|
if self.fade.is_finished:
|
|
self.is_finished = True
|
|
|
|
def draw(self):
|
|
if self.type == Judgments.GOOD:
|
|
tex.draw_texture('lane', 'lane_hit_effect', frame=2, index=self.is_2p, fade=self.fade.attribute)
|
|
elif self.type == DrumType.DON:
|
|
tex.draw_texture('lane', 'lane_hit_effect', frame=0, index=self.is_2p, fade=self.fade.attribute)
|
|
elif self.type == DrumType.KAT:
|
|
tex.draw_texture('lane', 'lane_hit_effect', frame=1, index=self.is_2p, fade=self.fade.attribute)
|
|
|
|
class DrumHitEffect:
|
|
"""Display the side of the drum hit"""
|
|
def __init__(self, type: DrumType, side: Side, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.type = type
|
|
self.side = side
|
|
self.is_finished = False
|
|
self.fade = tex.get_animation(1, is_copy=True)
|
|
self.fade.start()
|
|
|
|
def update(self, current_ms: float):
|
|
self.fade.update(current_ms)
|
|
if self.fade.is_finished:
|
|
self.is_finished = True
|
|
|
|
def draw(self):
|
|
if self.type == DrumType.DON:
|
|
if self.side == Side.LEFT:
|
|
tex.draw_texture('lane', 'drum_don_l', index=self.is_2p, fade=self.fade.attribute)
|
|
elif self.side == Side.RIGHT:
|
|
tex.draw_texture('lane', 'drum_don_r', index=self.is_2p, fade=self.fade.attribute)
|
|
elif self.type == DrumType.KAT:
|
|
if self.side == Side.LEFT:
|
|
tex.draw_texture('lane', 'drum_kat_l', index=self.is_2p, fade=self.fade.attribute)
|
|
elif self.side == Side.RIGHT:
|
|
tex.draw_texture('lane', 'drum_kat_r', index=self.is_2p, fade=self.fade.attribute)
|
|
|
|
class GaugeHitEffect:
|
|
"""Effect when a note hits the gauge"""
|
|
_COLOR_THRESHOLDS = [(0.70, ray.WHITE), (0.80, ray.YELLOW), (0.90, ray.ORANGE), (1.00, ray.RED)]
|
|
|
|
def __init__(self, note_type: int, big: bool, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.note_type = note_type
|
|
self.is_big = big
|
|
self.texture_change = tex.get_animation(2, is_copy=True)
|
|
self.circle_fadein = tex.get_animation(31, is_copy=True)
|
|
self.resize = tex.get_animation(32, is_copy=True)
|
|
self.fade_out = tex.get_animation(33, is_copy=True)
|
|
self.rotation = tex.get_animation(34, is_copy=True)
|
|
self.texture_change.start()
|
|
self.circle_fadein.start()
|
|
self.resize.start()
|
|
self.fade_out.start()
|
|
self.rotation.start()
|
|
self.color = ray.fade(ray.YELLOW, self.circle_fadein.attribute)
|
|
self.is_finished = False
|
|
|
|
self.width = tex.textures["gauge"]["hit_effect"].width
|
|
|
|
self.texture_color = ray.WHITE
|
|
self.dest_width = self.width * tex.screen_scale
|
|
self.dest_height = self.width * tex.screen_scale
|
|
self.origin = ray.Vector2(self.width//2, self.width//2)
|
|
self.rotation_angle = 0
|
|
self.x2_pos = -self.width
|
|
self.y2_pos = -self.width
|
|
|
|
# Cache for texture selection
|
|
self.circle_texture = 'hit_effect_circle_big' if self.is_big else 'hit_effect_circle'
|
|
self._last_resize_value = -1
|
|
self._cached_texture_color = ray.WHITE
|
|
|
|
def _get_texture_color_for_resize(self, resize_value):
|
|
"""Calculate texture color based on resize attribute value with caching"""
|
|
# Use cached value if resize hasn't changed significantly
|
|
if abs(resize_value - self._last_resize_value) < 0.01:
|
|
return self._cached_texture_color
|
|
|
|
self._last_resize_value = resize_value
|
|
|
|
if resize_value >= 1.00:
|
|
self._cached_texture_color = ray.RED
|
|
else:
|
|
# Use pre-defined thresholds for faster lookup
|
|
self._cached_texture_color = ray.WHITE
|
|
for threshold, color in self._COLOR_THRESHOLDS:
|
|
if resize_value <= threshold:
|
|
self._cached_texture_color = color
|
|
break
|
|
|
|
return self._cached_texture_color
|
|
|
|
def update(self, current_ms):
|
|
# Update all animations
|
|
self.texture_change.update(current_ms)
|
|
self.circle_fadein.update(current_ms)
|
|
self.fade_out.update(current_ms)
|
|
self.resize.update(current_ms)
|
|
self.rotation.update(current_ms)
|
|
|
|
# Update circle color with optimized calculation
|
|
base_color = ray.WHITE if self.circle_fadein.is_finished else ray.YELLOW
|
|
fade_value = min(self.fade_out.attribute, self.circle_fadein.attribute)
|
|
self.color = ray.fade(base_color, fade_value)
|
|
|
|
# Pre-compute drawing values only when resize changes significantly
|
|
resize_val = self.resize.attribute
|
|
if abs(resize_val - getattr(self, '_last_resize_calc', -1)) > 0.005:
|
|
self._last_resize_calc = resize_val
|
|
self.texture_color = self._get_texture_color_for_resize(resize_val)
|
|
self.dest_width = self.width * resize_val
|
|
self.dest_height = self.width * resize_val
|
|
self.origin = ray.Vector2(self.dest_width / 2, self.dest_height / 2)
|
|
self.x2_pos = -self.width + (self.width * resize_val)
|
|
self.y2_pos = -self.width + (self.width * resize_val)
|
|
|
|
self.rotation_angle = self.rotation.attribute * 100
|
|
|
|
# Check if finished
|
|
if self.fade_out.is_finished:
|
|
self.is_finished = True
|
|
|
|
def draw(self):
|
|
fade_value = self.fade_out.attribute
|
|
|
|
# Main hit effect texture
|
|
tex.draw_texture('gauge', 'hit_effect',
|
|
frame=self.texture_change.attribute,
|
|
x2=self.x2_pos,
|
|
index=self.is_2p,
|
|
y2=self.y2_pos,
|
|
color=ray.fade(self.texture_color, fade_value),
|
|
origin=self.origin,
|
|
rotation=self.rotation_angle,
|
|
center=True)
|
|
|
|
# Note type texture
|
|
pos_data = tex.skin_config["gauge_hit_effect_note"]
|
|
tex.draw_texture('notes', str(self.note_type),
|
|
x=pos_data.x, y=pos_data.y+(self.is_2p*(pos_data.height)),
|
|
fade=fade_value)
|
|
|
|
# Circle effect texture (use cached texture name)
|
|
tex.draw_texture('gauge', self.circle_texture, color=self.color, index=self.is_2p)
|
|
|
|
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, start_x: float = 0, start_y: float = 0):
|
|
self.note_type = note_type
|
|
self.is_big = big
|
|
self.is_balloon = is_balloon
|
|
self.arc_points = 100
|
|
self.arc_duration = 22
|
|
self.current_progress = 0
|
|
self.create_ms = current_ms
|
|
self.player_num = player_num
|
|
|
|
self.explosion_point_index = 0
|
|
self.points_per_explosion = 5
|
|
|
|
curve_height = 425 * tex.screen_scale
|
|
self.start_x, self.start_y = start_x + (350 * tex.screen_scale), start_y + (192 * tex.screen_scale)
|
|
self.end_x, self.end_y = 1158 * tex.screen_scale, 101 * tex.screen_scale
|
|
if self.player_num == PlayerNum.P2:
|
|
self.start_y += (176 * tex.screen_scale)
|
|
self.end_y += (372 * tex.screen_scale)
|
|
self.explosion_x = self.start_x
|
|
self.explosion_y = self.start_y
|
|
|
|
if self.player_num == PlayerNum.P1:
|
|
# Control point influences the curve shape
|
|
self.control_x = (self.start_x + self.end_x) // 2
|
|
self.control_y = min(self.start_y, self.end_y) - curve_height # Arc upward
|
|
else:
|
|
self.control_x = (self.start_x + self.end_x) // 2
|
|
self.control_y = max(self.start_y, self.end_y) + curve_height # Arc downward
|
|
|
|
self.x_i = self.start_x
|
|
self.y_i = self.start_y
|
|
self.is_finished = False
|
|
self.arc_points_cache = []
|
|
for i in range(self.arc_points + 1):
|
|
t = i / self.arc_points
|
|
t_inv = 1.0 - t
|
|
x = int(t_inv * t_inv * self.start_x + 2 * t_inv * t * self.control_x + t * t * self.end_x)
|
|
y = int(t_inv * t_inv * self.start_y + 2 * t_inv * t * self.control_y + t * t * self.end_y)
|
|
self.arc_points_cache.append((x, y))
|
|
|
|
self.explosion_x, self.explosion_y = self.arc_points_cache[0]
|
|
self.explosion_anim = tex.get_animation(22)
|
|
self.explosion_anim.start()
|
|
|
|
def update(self, current_ms: float):
|
|
ms_since_call = (current_ms - self.create_ms) / 16.67
|
|
ms_since_call = max(0, min(ms_since_call, self.arc_duration))
|
|
|
|
self.current_progress = ms_since_call / self.arc_duration
|
|
if self.current_progress >= 1.0:
|
|
self.is_finished = True
|
|
self.x_i, self.y_i = self.arc_points_cache[-1]
|
|
return
|
|
|
|
point_index = int(self.current_progress * self.arc_points)
|
|
if point_index < len(self.arc_points_cache):
|
|
self.x_i, self.y_i = self.arc_points_cache[point_index]
|
|
else:
|
|
self.x_i, self.y_i = self.arc_points_cache[-1]
|
|
|
|
self.explosion_anim.update(current_ms)
|
|
if self.explosion_anim.is_finished:
|
|
self.explosion_point_index = min(
|
|
self.explosion_point_index + self.points_per_explosion,
|
|
len(self.arc_points_cache) - 1
|
|
)
|
|
|
|
self.explosion_x, self.explosion_y = self.arc_points_cache[self.explosion_point_index*4]
|
|
self.explosion_anim.restart()
|
|
|
|
def draw(self, mask_shader: ray.Shader):
|
|
if self.is_balloon:
|
|
rainbow = tex.textures['balloon']['rainbow']
|
|
if self.player_num == PlayerNum.P2:
|
|
rainbow_height = -rainbow.height
|
|
else:
|
|
rainbow_height = rainbow.height
|
|
trail_length_ratio = 0.5
|
|
trail_start_progress = max(0, self.current_progress - trail_length_ratio)
|
|
trail_end_progress = self.current_progress
|
|
|
|
if trail_end_progress > trail_start_progress:
|
|
crop_start_x = int(trail_start_progress * rainbow.width)
|
|
crop_end_x = int(trail_end_progress * rainbow.width)
|
|
crop_width = crop_end_x - crop_start_x
|
|
|
|
if crop_width > 0:
|
|
src = ray.Rectangle(crop_start_x, 0, crop_width, rainbow_height)
|
|
mirror = 'vertical' if self.player_num == PlayerNum.P2 else ''
|
|
y = (435 * tex.screen_scale) if self.player_num == PlayerNum.P2 else 0
|
|
ray.begin_shader_mode(mask_shader)
|
|
tex.draw_texture('balloon', 'rainbow_mask', src=src, x=crop_start_x, x2=-rainbow.width + crop_width, mirror=mirror, y=y)
|
|
ray.end_shader_mode()
|
|
|
|
tex.draw_texture('balloon', 'explosion', x=self.explosion_x, y=self.explosion_y-(30 * tex.screen_scale), frame=self.explosion_anim.attribute)
|
|
'''
|
|
elif self.is_big:
|
|
tex.draw_texture('hit_effect', 'explosion', x=self.explosion_x, y=self.explosion_y-30, frame=self.explosion_anim.attribute)
|
|
'''
|
|
tex.draw_texture('notes', str(self.note_type), x=self.x_i, y=self.y_i)
|
|
|
|
class DrumrollCounter:
|
|
"""Displays a drumroll counter, stays alive until is_drumroll is false"""
|
|
def __init__(self, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.is_finished = False
|
|
self.drumroll_count = 0
|
|
self.fade_animation = tex.get_animation(8)
|
|
self.fade_animation.start()
|
|
self.stretch_animation = tex.get_animation(9)
|
|
|
|
def update_count(self, count: int):
|
|
if self.drumroll_count != count:
|
|
self.drumroll_count = count
|
|
self.stretch_animation.start()
|
|
self.fade_animation.start()
|
|
|
|
def update(self, current_ms: float, drumroll_count: int):
|
|
self.stretch_animation.update(current_ms)
|
|
self.fade_animation.update(current_ms)
|
|
|
|
if drumroll_count != 0:
|
|
self.update_count(drumroll_count)
|
|
if self.fade_animation.is_finished:
|
|
self.is_finished = True
|
|
|
|
def draw(self):
|
|
color = ray.fade(ray.WHITE, self.fade_animation.attribute)
|
|
tex.draw_texture('drumroll_counter', 'bubble', color=color, index=self.is_2p)
|
|
counter = str(self.drumroll_count)
|
|
total_width = len(counter) * tex.skin_config["drumroll_counter_margin"].x
|
|
for i, digit in enumerate(counter):
|
|
tex.draw_texture('drumroll_counter', 'counter', color=color, index=self.is_2p, frame=int(digit), x=-(total_width//2)+(i*tex.skin_config["drumroll_counter_margin"].x), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute)
|
|
|
|
class BalloonAnimation:
|
|
"""Draws a Balloon"""
|
|
def __init__(self, current_ms: float, balloon_total: int, player_num: PlayerNum, is_2p: bool):
|
|
self.player_num = player_num
|
|
self.is_2p = is_2p
|
|
self.create_ms = current_ms
|
|
self.is_finished = False
|
|
self.total_duration = 83.33
|
|
self.color = ray.fade(ray.WHITE, 1.0)
|
|
self.balloon_count = 0
|
|
self.balloon_total = balloon_total
|
|
self.is_popped = False
|
|
self.stretch_animation = tex.get_animation(6)
|
|
self.fade_animation = tex.get_animation(7)
|
|
self.fade_animation.start()
|
|
|
|
def update_count(self, balloon_count: int):
|
|
if self.balloon_count != balloon_count:
|
|
self.balloon_count = balloon_count
|
|
self.stretch_animation.start()
|
|
|
|
def update(self, current_ms: float, balloon_count: int, is_popped: bool):
|
|
self.update_count(balloon_count)
|
|
self.stretch_animation.update(current_ms)
|
|
self.is_popped = is_popped
|
|
|
|
elapsed_time = current_ms - self.create_ms
|
|
if self.is_popped:
|
|
self.fade_animation.update(current_ms)
|
|
self.color = ray.fade(ray.WHITE, self.fade_animation.attribute)
|
|
else:
|
|
self.total_duration = elapsed_time + 166
|
|
self.fade_animation.delay = self.total_duration - 166
|
|
if self.fade_animation.is_finished:
|
|
self.is_finished = True
|
|
|
|
def draw(self):
|
|
if self.is_popped:
|
|
tex.draw_texture('balloon', 'pop', frame=7, color=self.color, y=self.is_2p*tex.skin_config["2p_offset"].y)
|
|
elif self.balloon_count >= 1:
|
|
balloon_index = min(6, (self.balloon_count - 1) * 6 // self.balloon_total)
|
|
tex.draw_texture('balloon', 'pop', frame=balloon_index, color=self.color, index=self.player_num-1, y=self.is_2p*tex.skin_config["2p_offset"].y)
|
|
if self.balloon_count > 0:
|
|
tex.draw_texture('balloon', 'bubble', y=self.is_2p*(410 * tex.screen_scale), mirror='vertical' if self.is_2p else '')
|
|
counter = str(max(0, self.balloon_total - self.balloon_count + 1))
|
|
total_width = len(counter) * tex.skin_config["drumroll_counter_margin"].x
|
|
for i, digit in enumerate(counter):
|
|
tex.draw_texture('balloon', 'counter', frame=int(digit), color=self.color, x=-(total_width // 2) + (i * tex.skin_config["drumroll_counter_margin"].x), y=-self.stretch_animation.attribute+(self.is_2p*435), y2=self.stretch_animation.attribute)
|
|
|
|
class KusudamaAnimation:
|
|
"""Draws a Kusudama"""
|
|
def __init__(self, balloon_total: int):
|
|
self.balloon_total = balloon_total
|
|
self.move_down = tex.get_animation(11)
|
|
self.move_up = tex.get_animation(12)
|
|
self.renda_move_up = tex.get_animation(13)
|
|
self.renda_move_down = tex.get_animation(18)
|
|
self.renda_fade_in = tex.get_animation(14)
|
|
self.renda_fade_out = tex.get_animation(20)
|
|
self.stretch_animation = tex.get_animation(15)
|
|
self.breathing = tex.get_animation(16)
|
|
self.renda_breathe = tex.get_animation(17)
|
|
self.open = tex.get_animation(19)
|
|
self.fade_out = tex.get_animation(21)
|
|
self.balloon_count = 0
|
|
self.is_popped = False
|
|
self.is_finished = False
|
|
self.move_down.start()
|
|
self.move_up.start()
|
|
self.renda_move_up.start()
|
|
self.renda_move_down.start()
|
|
self.renda_fade_in.start()
|
|
|
|
self.open.reset()
|
|
self.renda_fade_out.reset()
|
|
self.fade_out.reset()
|
|
|
|
def update_count(self, balloon_count: int):
|
|
if self.balloon_count != balloon_count:
|
|
self.balloon_count = balloon_count
|
|
self.stretch_animation.start()
|
|
self.breathing.start()
|
|
|
|
def update(self, current_ms, is_popped: bool):
|
|
if is_popped and not self.is_popped:
|
|
self.is_popped = True
|
|
self.open.start()
|
|
self.renda_fade_out.start()
|
|
self.fade_out.start()
|
|
self.move_down.update(current_ms)
|
|
self.move_up.update(current_ms)
|
|
self.renda_move_up.update(current_ms)
|
|
self.renda_move_down.update(current_ms)
|
|
self.renda_fade_in.update(current_ms)
|
|
self.renda_fade_out.update(current_ms)
|
|
self.fade_out.update(current_ms)
|
|
self.stretch_animation.update(current_ms)
|
|
self.breathing.update(current_ms)
|
|
self.renda_breathe.update(current_ms)
|
|
self.open.update(current_ms)
|
|
self.is_finished = self.fade_out.is_finished
|
|
def draw(self):
|
|
y = self.move_down.attribute - self.move_up.attribute
|
|
renda_y = -self.renda_move_up.attribute + self.renda_move_down.attribute + self.renda_breathe.attribute
|
|
tex.draw_texture('kusudama', 'kusudama', frame=self.open.attribute, y=y, scale=self.breathing.attribute, center=True, fade=self.fade_out.attribute)
|
|
tex.draw_texture('kusudama', 'renda', y=renda_y, fade=min(self.renda_fade_in.attribute, self.renda_fade_out.attribute))
|
|
|
|
if self.move_up.is_finished and not self.is_popped:
|
|
counter = str(max(0, self.balloon_total - self.balloon_count))
|
|
if counter == '0':
|
|
return
|
|
total_width = len(counter) * tex.skin_config["kusudama_counter_margin"].x
|
|
for i, digit in enumerate(counter):
|
|
tex.draw_texture('kusudama', 'counter', frame=int(digit), x=-(total_width // 2) + (i * tex.skin_config["kusudama_counter_margin"].x), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute)
|
|
|
|
class Combo:
|
|
"""Displays the current combo"""
|
|
def __init__(self, combo: int, current_ms: float, is_2p: bool):
|
|
self.combo = combo
|
|
self.is_2p = is_2p
|
|
self.stretch_animation = tex.get_animation(5, is_copy=True)
|
|
self.color = [ray.fade(ray.WHITE, 1), ray.fade(ray.WHITE, 1), ray.fade(ray.WHITE, 1)]
|
|
self.glimmer_dict = {0: 0, 1: 0, 2: 0}
|
|
self.total_time = 250
|
|
self.cycle_time = self.total_time * 2
|
|
self.start_times = [
|
|
current_ms,
|
|
current_ms + (2 / 3) * self.cycle_time,
|
|
current_ms + (4 / 3) * self.cycle_time
|
|
]
|
|
|
|
def update_count(self, combo: int):
|
|
if self.combo != combo:
|
|
self.combo = combo
|
|
self.stretch_animation.start()
|
|
|
|
def update(self, current_ms: float, combo: int):
|
|
self.update_count(combo)
|
|
self.stretch_animation.update(current_ms)
|
|
|
|
for i in range(3):
|
|
elapsed_time = current_ms - self.start_times[i]
|
|
if elapsed_time > self.cycle_time:
|
|
cycles_completed = elapsed_time // self.cycle_time
|
|
self.start_times[i] += cycles_completed * self.cycle_time
|
|
elapsed_time = current_ms - self.start_times[i]
|
|
if elapsed_time <= self.total_time:
|
|
self.glimmer_dict[i] = -int(elapsed_time // 16.67)
|
|
fade_start_time = self.total_time - 164
|
|
if elapsed_time >= fade_start_time:
|
|
fade = 1 - (elapsed_time - fade_start_time) / 164
|
|
else:
|
|
fade = 1
|
|
else:
|
|
self.glimmer_dict[i] = 0
|
|
fade = 0
|
|
self.color[i] = ray.fade(ray.WHITE, fade)
|
|
|
|
def draw(self):
|
|
if self.combo < 3:
|
|
return
|
|
|
|
# Cache string conversion
|
|
if self.combo != getattr(self, '_cached_combo_value', -1):
|
|
self._cached_combo_value = self.combo
|
|
self._cached_combo_str = str(self.combo)
|
|
counter = self._cached_combo_str
|
|
|
|
if self.combo < 100:
|
|
margin = tex.skin_config["combo_margin"].x
|
|
total_width = len(counter) * margin
|
|
tex.draw_texture('combo', 'combo', index=self.is_2p)
|
|
for i, digit in enumerate(counter):
|
|
tex.draw_texture('combo', 'counter', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute, index=self.is_2p)
|
|
else:
|
|
margin = tex.skin_config["combo_margin"].y
|
|
total_width = len(counter) * margin
|
|
tex.draw_texture('combo', 'combo_100', index=self.is_2p)
|
|
for i, digit in enumerate(counter):
|
|
tex.draw_texture('combo', 'counter_100', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation.attribute, y2=self.stretch_animation.attribute, index=self.is_2p)
|
|
glimmer_positions = [(225 * tex.screen_scale, 210 * tex.screen_scale), (200 * tex.screen_scale, 230 * tex.screen_scale), (250 * tex.screen_scale, 230 * tex.screen_scale)]
|
|
for j, (x, y) in enumerate(glimmer_positions):
|
|
for i in range(3):
|
|
tex.draw_texture('combo', 'gleam', x=x+(i*tex.skin_config["combo_margin"].x), y=y+self.glimmer_dict[j] + (self.is_2p*tex.skin_config["2p_offset"].y), color=self.color[j])
|
|
|
|
class ScoreCounter:
|
|
"""Displays the total score"""
|
|
def __init__(self, score: int, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.score = score
|
|
self.stretch = tex.get_animation(4, is_copy=True)
|
|
|
|
def update_count(self, score: int):
|
|
if self.score != score:
|
|
self.score = score
|
|
self.stretch.start()
|
|
|
|
def update(self, current_ms: float, score: int):
|
|
self.update_count(score)
|
|
if self.score > 0:
|
|
self.stretch.update(current_ms)
|
|
|
|
def draw(self):
|
|
# Cache string conversion
|
|
if self.score != getattr(self, '_cached_score_value', -1):
|
|
self._cached_score_value = self.score
|
|
self._cached_score_str = str(self.score)
|
|
counter = self._cached_score_str
|
|
|
|
x, y = 150 * tex.screen_scale, (185 * tex.screen_scale) + (self.is_2p*310*tex.screen_scale)
|
|
margin = tex.skin_config["score_counter_margin"].x
|
|
total_width = len(counter) * margin
|
|
start_x = x - total_width
|
|
for i, digit in enumerate(counter):
|
|
tex.draw_texture('lane', 'score_number', frame=int(digit), x=start_x + (i * margin), y=y - self.stretch.attribute, y2=self.stretch.attribute)
|
|
|
|
class ScoreCounterAnimation:
|
|
"""Displays the score init being added to the total score"""
|
|
def __init__(self, player_num: PlayerNum, counter: int, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.counter = counter
|
|
self.direction = -1 if self.is_2p else 1
|
|
self.fade_animation_1 = tex.get_animation(35, is_copy=True)
|
|
self.move_animation_1 = tex.get_animation(36, is_copy=True)
|
|
self.fade_animation_2 = tex.get_animation(37, is_copy=True)
|
|
self.move_animation_2 = tex.get_animation(38, is_copy=True)
|
|
self.move_animation_3 = tex.get_animation(39, is_copy=True)
|
|
self.move_animation_4 = tex.get_animation(40, is_copy=True)
|
|
self.fade_animation_1.start()
|
|
self.move_animation_1.start()
|
|
self.fade_animation_2.start()
|
|
self.move_animation_2.start()
|
|
self.move_animation_3.start()
|
|
self.move_animation_4.start()
|
|
|
|
if player_num == PlayerNum.P2:
|
|
self.base_color = ray.Color(84, 250, 238, 255)
|
|
else:
|
|
self.base_color = ray.Color(254, 102, 0, 255)
|
|
self.color = ray.fade(self.base_color, 1.0)
|
|
self.is_finished = False
|
|
|
|
# Cache string and layout calculations
|
|
self.counter_str = str(counter)
|
|
self.margin = tex.skin_config["score_counter_margin"].x
|
|
self.total_width = len(self.counter_str) * self.margin
|
|
self.y_pos_list = []
|
|
|
|
def update(self, current_ms: float):
|
|
self.fade_animation_1.update(current_ms)
|
|
self.move_animation_1.update(current_ms)
|
|
self.move_animation_2.update(current_ms)
|
|
self.move_animation_3.update(current_ms)
|
|
self.move_animation_4.update(current_ms)
|
|
self.fade_animation_2.update(current_ms)
|
|
|
|
fade_value = self.fade_animation_2.attribute if self.fade_animation_1.is_finished else self.fade_animation_1.attribute
|
|
self.color = ray.fade(self.base_color, fade_value)
|
|
|
|
if self.fade_animation_2.is_finished:
|
|
self.is_finished = True
|
|
|
|
# Cache y positions
|
|
self.y_pos_list = [self.move_animation_4.attribute + i*5 for i in range(1, len(self.counter_str)+1)]
|
|
|
|
def draw(self):
|
|
x = self.move_animation_2.attribute if self.move_animation_1.is_finished else self.move_animation_1.attribute
|
|
if x == 0:
|
|
return
|
|
|
|
start_x = x - self.total_width
|
|
|
|
for i, digit in enumerate(self.counter_str):
|
|
if self.move_animation_3.is_finished:
|
|
y = self.y_pos_list[i]
|
|
elif self.move_animation_2.is_finished:
|
|
y = self.move_animation_3.attribute
|
|
else:
|
|
y = 148 * tex.screen_scale
|
|
|
|
y_offset = y * self.direction
|
|
|
|
tex.draw_texture('lane', 'score_number',
|
|
frame=int(digit),
|
|
x=start_x + (i * self.margin),
|
|
y=y_offset + (self.is_2p * 680 * tex.screen_scale),
|
|
color=self.color)
|
|
|
|
class SongInfo:
|
|
"""Displays the song name and genre"""
|
|
def __init__(self, song_name: str, genre: int):
|
|
self.song_name = song_name
|
|
self.genre = genre
|
|
self.song_title = OutlinedText(song_name, tex.skin_config["song_info"].font_size, ray.WHITE, outline_thickness=5)
|
|
self.fade = tex.get_animation(3)
|
|
|
|
def update(self, current_ms: float):
|
|
self.fade.update(current_ms)
|
|
|
|
def draw(self):
|
|
tex.draw_texture('song_info', 'song_num', fade=self.fade.attribute, frame=global_data.songs_played % 4)
|
|
|
|
text_x = tex.skin_config["song_info"].x - self.song_title.texture.width
|
|
text_y = tex.skin_config["song_info"].y - self.song_title.texture.height//2
|
|
self.song_title.draw(outline_color=ray.BLACK, x=text_x, y=text_y, color=ray.fade(ray.WHITE, 1 - self.fade.attribute))
|
|
|
|
if self.genre < 9:
|
|
tex.draw_texture('song_info', 'genre', fade=1 - self.fade.attribute, frame=self.genre)
|
|
|
|
class ResultTransition:
|
|
"""Displays the result transition animation"""
|
|
def __init__(self, player_num: PlayerNum):
|
|
self.player_num = player_num
|
|
self.move = global_tex.get_animation(5)
|
|
self.move.reset()
|
|
self.is_finished = False
|
|
self.is_started = False
|
|
|
|
def start(self):
|
|
self.move.start()
|
|
|
|
def update(self, current_ms: float):
|
|
self.move.update(current_ms)
|
|
self.is_started = self.move.is_started
|
|
self.is_finished = self.move.is_finished
|
|
|
|
def draw(self):
|
|
x = 0
|
|
while x < tex.screen_width:
|
|
tex_height = global_tex.textures['result_transition']['1p_shutter_footer'].height
|
|
if self.player_num == PlayerNum.TWO_PLAYER:
|
|
global_tex.draw_texture('result_transition', '1p_shutter', frame=0, x=x, y=-tex.screen_height + self.move.attribute)
|
|
global_tex.draw_texture('result_transition', '2p_shutter', frame=0, x=x, y=tex.screen_height - self.move.attribute)
|
|
global_tex.draw_texture('result_transition', '1p_shutter_footer', x=x, y=-tex_height + self.move.attribute)
|
|
global_tex.draw_texture('result_transition', '2p_shutter_footer', x=x, y=tex.screen_height + tex_height - self.move.attribute)
|
|
else:
|
|
global_tex.draw_texture('result_transition', f'{self.player_num}p_shutter', frame=0, x=x, y=-tex.screen_height + self.move.attribute)
|
|
global_tex.draw_texture('result_transition', f'{self.player_num}p_shutter', frame=0, x=x, y=tex.screen_height - self.move.attribute)
|
|
global_tex.draw_texture('result_transition', f'{self.player_num}p_shutter_footer', x=x, y=-tex_height + self.move.attribute)
|
|
global_tex.draw_texture('result_transition', f'{self.player_num}p_shutter_footer', x=x, y=tex.screen_height + tex_height - self.move.attribute)
|
|
x += tex.screen_width // 5
|
|
|
|
class GogoTime:
|
|
"""Displays the Gogo Time fire and fireworks"""
|
|
def __init__(self, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.explosion_anim = tex.get_animation(23, is_copy=True)
|
|
self.fire_resize = tex.get_animation(24, is_copy=True)
|
|
self.fire_change = tex.get_animation(25, is_copy=True)
|
|
|
|
self.explosion_anim.start()
|
|
self.fire_resize.start()
|
|
self.fire_change.start()
|
|
def update(self, current_time_ms: float):
|
|
self.explosion_anim.update(current_time_ms)
|
|
self.fire_resize.update(current_time_ms)
|
|
self.fire_change.update(current_time_ms)
|
|
|
|
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)
|
|
|
|
class ComboAnnounce:
|
|
"""Displays the combo every 100 combos"""
|
|
def __init__(self, combo: int, current_time_ms: float, player_num: PlayerNum, is_2p: bool):
|
|
self.player_num = player_num
|
|
self.is_2p = is_2p
|
|
self.combo = combo
|
|
self.wait = current_time_ms
|
|
self.fade = tex.get_animation(65)
|
|
self.fade.start()
|
|
self.is_finished = False
|
|
self.audio_played = False
|
|
|
|
def update(self, current_time_ms: float):
|
|
if current_time_ms >= self.wait + 1666.67 and not self.is_finished:
|
|
self.fade.start()
|
|
self.is_finished = True
|
|
|
|
self.fade.update(current_time_ms)
|
|
if not self.audio_played and self.combo >= 100:
|
|
audio.play_sound(f'combo_{self.combo}_{self.player_num}p', 'voice')
|
|
self.audio_played = True
|
|
|
|
def draw(self):
|
|
if self.combo == 0:
|
|
return
|
|
if not self.is_finished:
|
|
fade = 1 - self.fade.attribute
|
|
else:
|
|
fade = self.fade.attribute
|
|
tex.draw_texture('combo', f'announce_bg_{self.player_num}p', fade=fade, index=self.is_2p)
|
|
|
|
if self.combo >= 1000:
|
|
thousands = self.combo // 1000
|
|
remaining_hundreds = (self.combo % 1000) // 100
|
|
thousands_offset = -110
|
|
hundreds_offset = 20
|
|
if self.combo % 1000 == 0:
|
|
tex.draw_texture('combo', 'announce_number', frame=thousands-1, x=-23 * tex.screen_scale, fade=fade, index=self.is_2p)
|
|
tex.draw_texture('combo', 'announce_add', frame=0, x=435 * tex.screen_scale, fade=fade, index=self.is_2p)
|
|
else:
|
|
if thousands <= 5:
|
|
tex.draw_texture('combo', 'announce_add', frame=thousands, x=429 * tex.screen_scale + thousands_offset, fade=fade, index=self.is_2p)
|
|
if remaining_hundreds > 0:
|
|
tex.draw_texture('combo', 'announce_number', frame=remaining_hundreds-1, x=hundreds_offset, fade=fade, index=self.is_2p)
|
|
text_offset = -30 * tex.screen_scale
|
|
else:
|
|
text_offset = 0
|
|
tex.draw_texture('combo', 'announce_number', frame=self.combo // 100 - 1, x=0, fade=fade, index=self.is_2p)
|
|
tex.draw_texture('combo', 'announce_text', x=-text_offset/2, fade=fade, index=self.is_2p)
|
|
|
|
class BranchIndicator:
|
|
"""Displays the branch difficulty and changes"""
|
|
def __init__(self, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.difficulty = 'normal'
|
|
self.diff_2 = self.difficulty
|
|
self.diff_down = tex.get_animation(41)
|
|
self.diff_up = tex.get_animation(42)
|
|
self.diff_fade = tex.get_animation(43)
|
|
self.level_fade = tex.get_animation(44)
|
|
self.level_scale = tex.get_animation(45)
|
|
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), index=self.is_2p)
|
|
if self.difficulty == 'master':
|
|
tex.draw_texture('branch', 'master_bg', fade=min(0.5, 1 - self.diff_fade.attribute), index=self.is_2p)
|
|
if self.direction == -1:
|
|
tex.draw_texture('branch', 'level_down', scale=self.level_scale.attribute, fade=self.level_fade.attribute, center=True, index=self.is_2p)
|
|
else:
|
|
tex.draw_texture('branch', 'level_up', scale=self.level_scale.attribute, fade=self.level_fade.attribute, center=True, index=self.is_2p)
|
|
tex.draw_texture('branch', self.diff_2, y=(self.diff_down.attribute - self.diff_up.attribute) * self.direction, fade=self.diff_fade.attribute, index=self.is_2p)
|
|
tex.draw_texture('branch', self.difficulty, y=(self.diff_up.attribute * (self.direction*-1)) - ((70 * tex.screen_scale)*self.direction*-1), fade=1 - self.diff_fade.attribute, index=self.is_2p)
|
|
|
|
class FailAnimation:
|
|
"""Animates the fail effect"""
|
|
def __init__(self, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.bachio_fade_in = tex.get_animation(46)
|
|
self.bachio_texture_change = tex.get_animation(47)
|
|
self.bachio_fall = tex.get_animation(48)
|
|
self.bachio_move_out = tex.get_animation(49)
|
|
self.bachio_boom_fade_in = tex.get_animation(50)
|
|
self.bachio_boom_scale = tex.get_animation(51)
|
|
self.bachio_up = tex.get_animation(52)
|
|
self.bachio_down = tex.get_animation(53)
|
|
self.text_fade_in = tex.get_animation(54)
|
|
self.text_fade_in.start()
|
|
self.bachio_fade_in.start()
|
|
self.bachio_texture_change.start()
|
|
self.bachio_fall.start()
|
|
self.bachio_move_out.start()
|
|
self.bachio_boom_fade_in.start()
|
|
self.bachio_boom_scale.start()
|
|
self.bachio_up.start()
|
|
self.bachio_down.start()
|
|
self.name = 'in'
|
|
self.frame = self.bachio_texture_change.attribute
|
|
audio.play_sound('fail', 'sound')
|
|
def update(self, current_time_ms: float):
|
|
self.bachio_fade_in.update(current_time_ms)
|
|
self.bachio_texture_change.update(current_time_ms)
|
|
self.bachio_fall.update(current_time_ms)
|
|
self.bachio_move_out.update(current_time_ms)
|
|
self.bachio_boom_fade_in.update(current_time_ms)
|
|
self.bachio_boom_scale.update(current_time_ms)
|
|
self.bachio_up.update(current_time_ms)
|
|
self.bachio_down.update(current_time_ms)
|
|
self.text_fade_in.update(current_time_ms)
|
|
if self.bachio_texture_change.is_finished:
|
|
self.name = 'fall'
|
|
self.frame = self.bachio_fall.attribute
|
|
else:
|
|
self.frame = self.bachio_texture_change.attribute
|
|
def draw(self):
|
|
tex.draw_texture('ending_anim', 'fail', fade=self.text_fade_in.attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'bachio_l_' + self.name, x=-self.bachio_move_out.attribute - (self.bachio_up.attribute/2), y=self.bachio_down.attribute - self.bachio_up.attribute, frame=self.frame, fade=self.bachio_fade_in.attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'bachio_r_' + self.name, x=self.bachio_move_out.attribute + (self.bachio_up.attribute/2), y=self.bachio_down.attribute - self.bachio_up.attribute, frame=self.frame, fade=self.bachio_fade_in.attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'bachio_boom', index=0, fade=self.bachio_boom_fade_in.attribute, center=True, scale=self.bachio_boom_scale.attribute, y=(self.is_2p*tex.skin_config["2p_offset"].y))
|
|
tex.draw_texture('ending_anim', 'bachio_boom', index=1, fade=self.bachio_boom_fade_in.attribute, center=True, scale=self.bachio_boom_scale.attribute, y=(self.is_2p*tex.skin_config["2p_offset"].y))
|
|
|
|
class ClearAnimation:
|
|
"""Animates the clear effect"""
|
|
def __init__(self, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.bachio_fade_in = tex.get_animation(46)
|
|
self.bachio_fade_in.start()
|
|
self.bachio_texture_change = tex.get_animation(47)
|
|
self.bachio_texture_change.start()
|
|
self.bachio_out = tex.get_animation(55)
|
|
self.bachio_out.start()
|
|
self.bachio_move_out = tex.get_animation(66)
|
|
self.bachio_move_out.start()
|
|
self.clear_separate_fade_in = [Animation.create_fade(100, initial_opacity=0.0, final_opacity=1.0, delay=i*50) for i in range(5)]
|
|
for fade in self.clear_separate_fade_in:
|
|
fade.start()
|
|
self.clear_separate_stretch = [Animation.create_text_stretch(200, delay=i*50) for i in range(5)]
|
|
for stretch in self.clear_separate_stretch:
|
|
stretch.start()
|
|
self.clear_highlight_fade_in = tex.get_animation(56)
|
|
self.clear_highlight_fade_in.start()
|
|
self.draw_clear_full = False
|
|
self.name = 'in'
|
|
self.frame = 0
|
|
audio.play_sound('clear', 'sound')
|
|
|
|
def update(self, current_time_ms: float):
|
|
self.bachio_fade_in.update(current_time_ms)
|
|
self.bachio_texture_change.update(current_time_ms)
|
|
self.bachio_out.update(current_time_ms)
|
|
self.bachio_move_out.update(current_time_ms)
|
|
self.clear_highlight_fade_in.update(current_time_ms)
|
|
if self.clear_highlight_fade_in.attribute == 1.0:
|
|
self.draw_clear_full = True
|
|
for fade in self.clear_separate_fade_in:
|
|
fade.update(current_time_ms)
|
|
for stretch in self.clear_separate_stretch:
|
|
stretch.update(current_time_ms)
|
|
if self.bachio_texture_change.is_finished:
|
|
self.name = 'out'
|
|
self.frame = self.bachio_out.attribute
|
|
else:
|
|
self.frame = self.bachio_texture_change.attribute
|
|
def draw(self):
|
|
if self.draw_clear_full:
|
|
tex.draw_texture('ending_anim', 'clear', index=self.is_2p)
|
|
else:
|
|
for i in range(4, -1, -1):
|
|
tex.draw_texture('ending_anim', 'clear_separated', frame=i, fade=self.clear_separate_fade_in[i].attribute, x=i*60 * tex.screen_scale, y=-self.clear_separate_stretch[i].attribute, y2=self.clear_separate_stretch[i].attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'clear_highlight', fade=self.clear_highlight_fade_in.attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'bachio_l_' + self.name, x=-self.bachio_move_out.attribute, frame=self.frame, fade=self.bachio_fade_in.attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'bachio_r_' + self.name, x=self.bachio_move_out.attribute, frame=self.frame, fade=self.bachio_fade_in.attribute, index=self.is_2p)
|
|
|
|
class FCAnimation:
|
|
"""Animates the full combo effect"""
|
|
def __init__(self, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.bachio_fade_in = tex.get_animation(46)
|
|
self.bachio_fade_in.start()
|
|
self.bachio_texture_change = tex.get_animation(47)
|
|
self.bachio_texture_change.start()
|
|
self.bachio_out = tex.get_animation(55)
|
|
self.bachio_out.start()
|
|
self.bachio_move_out = tex.get_animation(49)
|
|
self.bachio_move_out.start()
|
|
self.clear_separate_fade_in = [Animation.create_fade(100, initial_opacity=0.0, final_opacity=1.0, delay=i*50) for i in range(5)]
|
|
for fade in self.clear_separate_fade_in:
|
|
fade.start()
|
|
self.clear_separate_stretch = [Animation.create_text_stretch(200, delay=i*50) for i in range(5)]
|
|
for stretch in self.clear_separate_stretch:
|
|
stretch.start()
|
|
self.clear_highlight_fade_in = tex.get_animation(56)
|
|
self.clear_highlight_fade_in.start()
|
|
self.fc_highlight_up = tex.get_animation(57)
|
|
self.fc_highlight_up.start()
|
|
self.fc_highlight_fade_out = tex.get_animation(58)
|
|
self.bachio_move_out_2 = tex.get_animation(59)
|
|
self.bachio_move_up = tex.get_animation(60)
|
|
self.fan_fade_in = tex.get_animation(61)
|
|
self.fan_texture_change = tex.get_animation(62)
|
|
self.draw_clear_full = False
|
|
self.name = 'in'
|
|
self.frame = 0
|
|
audio.play_sound('full_combo', 'sound')
|
|
|
|
def update(self, current_time_ms: float):
|
|
self.bachio_fade_in.update(current_time_ms)
|
|
self.bachio_texture_change.update(current_time_ms)
|
|
self.bachio_out.update(current_time_ms)
|
|
self.bachio_move_out.update(current_time_ms)
|
|
self.clear_highlight_fade_in.update(current_time_ms)
|
|
self.fc_highlight_up.update(current_time_ms)
|
|
self.fc_highlight_fade_out.update(current_time_ms)
|
|
self.bachio_move_out_2.update(current_time_ms)
|
|
self.bachio_move_up.update(current_time_ms)
|
|
self.fan_fade_in.update(current_time_ms)
|
|
self.fan_texture_change.update(current_time_ms)
|
|
if self.fc_highlight_up.is_finished and not self.fc_highlight_fade_out.is_started:
|
|
self.fc_highlight_fade_out.start()
|
|
self.bachio_move_out_2.start()
|
|
self.bachio_move_up.start()
|
|
self.fan_fade_in.start()
|
|
self.fan_texture_change.start()
|
|
audio.play_sound('full_combo_voice', 'voice')
|
|
if self.clear_highlight_fade_in.attribute == 1.0:
|
|
self.draw_clear_full = True
|
|
for fade in self.clear_separate_fade_in:
|
|
fade.update(current_time_ms)
|
|
for stretch in self.clear_separate_stretch:
|
|
stretch.update(current_time_ms)
|
|
if self.bachio_texture_change.is_finished:
|
|
self.name = 'out'
|
|
self.frame = self.bachio_out.attribute
|
|
else:
|
|
self.frame = self.bachio_texture_change.attribute
|
|
def draw(self):
|
|
if self.draw_clear_full:
|
|
tex.draw_texture('ending_anim', 'full_combo_overlay', y=-self.fc_highlight_up.attribute, fade=0.5, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'full_combo', y=-self.fc_highlight_up.attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'full_combo_highlight', y=-self.fc_highlight_up.attribute, fade=self.fc_highlight_fade_out.attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'fan_l', frame=self.fan_texture_change.attribute, fade=self.fan_fade_in.attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'fan_r', frame=self.fan_texture_change.attribute, fade=self.fan_fade_in.attribute, index=self.is_2p)
|
|
else:
|
|
for i in range(4, -1, -1):
|
|
tex.draw_texture('ending_anim', 'clear_separated', frame=i, fade=self.clear_separate_fade_in[i].attribute, x=i*60 * tex.screen_scale, y=-self.clear_separate_stretch[i].attribute, y2=self.clear_separate_stretch[i].attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'clear_highlight', fade=self.clear_highlight_fade_in.attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'bachio_l_' + self.name, x=(-self.bachio_move_out.attribute - self.bachio_move_out_2.attribute)*1.15, y=-self.bachio_move_up.attribute, frame=self.frame, fade=self.bachio_fade_in.attribute, index=self.is_2p)
|
|
tex.draw_texture('ending_anim', 'bachio_r_' + self.name, x=(self.bachio_move_out.attribute + self.bachio_move_out_2.attribute)*1.15, y=-self.bachio_move_up.attribute, frame=self.frame, fade=self.bachio_fade_in.attribute, index=self.is_2p)
|
|
|
|
class JudgeCounter:
|
|
"""Counts the number of good, ok, bad, and drumroll notes in real time"""
|
|
def __init__(self):
|
|
self.good = 0
|
|
self.ok = 0
|
|
self.bad = 0
|
|
self.drumrolls = 0
|
|
self.orange = ray.Color(253, 161, 0, 255)
|
|
self.white = ray.WHITE
|
|
def update(self, good: int, ok: int, bad: int, drumrolls: int):
|
|
self.good = good
|
|
self.ok = ok
|
|
self.bad = bad
|
|
self.drumrolls = drumrolls
|
|
def draw_counter(self, counter: float, x: float, y: float, margin: float, color: ray.Color):
|
|
counter_str = str(rounded(counter))
|
|
counter_len = len(counter_str)
|
|
for i, digit in enumerate(counter_str):
|
|
tex.draw_texture('judge_counter', 'counter', frame=int(digit), x=x - (counter_len - i) * margin, y=y, color=color)
|
|
def draw(self):
|
|
tex.draw_texture('judge_counter', 'bg')
|
|
tex.draw_texture('judge_counter', 'total_percent')
|
|
tex.draw_texture('judge_counter', 'judgments')
|
|
tex.draw_texture('judge_counter', 'drumrolls')
|
|
|
|
for i in range(4):
|
|
tex.draw_texture('judge_counter', 'percent', index=i, color=self.orange)
|
|
|
|
total_notes = self.good + self.ok + self.bad
|
|
if total_notes == 0:
|
|
total_notes = 1
|
|
margin = tex.skin_config["judge_counter_margin"].x
|
|
self.draw_counter(self.good / total_notes * 100, tex.skin_config["judge_counter_1"].x, tex.skin_config["judge_counter_1"].y, margin, self.orange)
|
|
self.draw_counter(self.ok / total_notes * 100, tex.skin_config["judge_counter_1"].x, tex.skin_config["judge_counter_3"].y, margin, self.orange)
|
|
self.draw_counter(self.bad / total_notes * 100, tex.skin_config["judge_counter_1"].x, tex.skin_config["judge_counter_4"].x, margin, self.orange)
|
|
self.draw_counter((self.good + self.ok) / total_notes * 100, tex.skin_config["judge_counter_3"].x, tex.skin_config["judge_counter_4"].y, margin, self.orange)
|
|
self.draw_counter(self.good, tex.skin_config["judge_counter_2"].x, tex.skin_config["judge_counter_1"].y, margin, self.white)
|
|
self.draw_counter(self.ok, tex.skin_config["judge_counter_2"].x, tex.skin_config["judge_counter_3"].y, margin, self.white)
|
|
self.draw_counter(self.bad, tex.skin_config["judge_counter_2"].x, tex.skin_config["judge_counter_4"].x, margin, self.white)
|
|
self.draw_counter(self.drumrolls, tex.skin_config["judge_counter_2"].x, tex.skin_config["judge_counter_4"].width, margin, self.white)
|
|
|
|
class Gauge:
|
|
"""The player's gauge"""
|
|
def __init__(self, player_num: PlayerNum, difficulty: int, level: int, total_notes: int, is_2p: bool):
|
|
self.is_2p = is_2p
|
|
self.player_num = player_num
|
|
self.string_diff = "_hard"
|
|
self.gauge_length = 0
|
|
self.previous_length = 0
|
|
self.total_notes = total_notes
|
|
self.difficulty = min(Difficulty.ONI, difficulty)
|
|
self.clear_start = [52, 60, 69, 69]
|
|
self.gauge_max = 87
|
|
self.level = min(10, level)
|
|
self.tamashii_fire_change = tex.get_animation(25)
|
|
if self.difficulty == Difficulty.HARD:
|
|
self.string_diff = "_hard"
|
|
elif self.difficulty == Difficulty.NORMAL:
|
|
self.string_diff = "_normal"
|
|
elif self.difficulty == Difficulty.EASY:
|
|
self.string_diff = "_easy"
|
|
self.is_clear = False
|
|
self.is_rainbow = False
|
|
self.table = [
|
|
[
|
|
{"clear_rate": 36.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
|
|
{"clear_rate": 38.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
|
|
{"clear_rate": 38.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
|
|
{"clear_rate": 44.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
|
|
{"clear_rate": 44.0, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
|
|
],
|
|
[
|
|
{"clear_rate": 45.939, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
|
|
{"clear_rate": 45.939, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
|
|
{"clear_rate": 48.676, "ok_multiplier": 0.75, "bad_multiplier": -0.5},
|
|
{"clear_rate": 49.232, "ok_multiplier": 0.75, "bad_multiplier": -0.75},
|
|
{"clear_rate": 52.5, "ok_multiplier": 0.75, "bad_multiplier": -1.0},
|
|
{"clear_rate": 52.5, "ok_multiplier": 0.75, "bad_multiplier": -1.0},
|
|
{"clear_rate": 52.5, "ok_multiplier": 0.75, "bad_multiplier": -1.0},
|
|
],
|
|
[
|
|
{"clear_rate": 54.325, "ok_multiplier": 0.75, "bad_multiplier": -0.75},
|
|
{"clear_rate": 54.325, "ok_multiplier": 0.75, "bad_multiplier": -0.75},
|
|
{"clear_rate": 50.774, "ok_multiplier": 0.75, "bad_multiplier": -1.0},
|
|
{"clear_rate": 48.410, "ok_multiplier": 0.75, "bad_multiplier": -1.17},
|
|
{"clear_rate": 47.246, "ok_multiplier": 0.75, "bad_multiplier": -1.25},
|
|
{"clear_rate": 48.120, "ok_multiplier": 0.75, "bad_multiplier": -1.25},
|
|
{"clear_rate": 48.120, "ok_multiplier": 0.75, "bad_multiplier": -1.25},
|
|
{"clear_rate": 48.120, "ok_multiplier": 0.75, "bad_multiplier": -1.25},
|
|
],
|
|
[
|
|
{"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
|
|
{"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
|
|
{"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
|
|
{"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
|
|
{"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
|
|
{"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
|
|
{"clear_rate": 56.603, "ok_multiplier": 0.5, "bad_multiplier": -1.6},
|
|
{"clear_rate": 56.0, "ok_multiplier": 0.5, "bad_multiplier": -2.0},
|
|
{"clear_rate": 61.428, "ok_multiplier": 0.5, "bad_multiplier": -2.0},
|
|
{"clear_rate": 61.428, "ok_multiplier": 0.5, "bad_multiplier": -2.0},
|
|
]
|
|
]
|
|
self.gauge_update_anim = tex.get_animation(10)
|
|
self.rainbow_fade_in = None
|
|
self.rainbow_animation = tex.get_animation(64)
|
|
|
|
def add_good(self):
|
|
"""Adds a good note to the gauge"""
|
|
self.gauge_update_anim.start()
|
|
self.previous_length = int(self.gauge_length)
|
|
self.gauge_length += (1 / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level-1]["clear_rate"]))
|
|
if self.gauge_length > self.gauge_max:
|
|
self.gauge_length = self.gauge_max
|
|
|
|
def add_ok(self):
|
|
"""Adds an ok note to the gauge"""
|
|
self.gauge_update_anim.start()
|
|
self.previous_length = int(self.gauge_length)
|
|
self.gauge_length += ((1 * self.table[self.difficulty][self.level-1]["ok_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level-1]["clear_rate"]))
|
|
if self.gauge_length > self.gauge_max:
|
|
self.gauge_length = self.gauge_max
|
|
|
|
def add_bad(self):
|
|
"""Adds a bad note to the gauge"""
|
|
self.previous_length = int(self.gauge_length)
|
|
self.gauge_length += ((1 * self.table[self.difficulty][self.level-1]["bad_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level-1]["clear_rate"]))
|
|
if self.gauge_length < 0:
|
|
self.gauge_length = 0
|
|
|
|
def update(self, current_ms: float):
|
|
self.is_clear = self.gauge_length > self.clear_start[min(self.difficulty, Difficulty.HARD)]-1
|
|
self.is_rainbow = self.gauge_length == self.gauge_max
|
|
if self.gauge_length == self.gauge_max and self.rainbow_fade_in is None:
|
|
self.rainbow_fade_in = tex.get_animation(63)
|
|
self.rainbow_fade_in.start()
|
|
self.gauge_update_anim.update(current_ms)
|
|
self.tamashii_fire_change.update(current_ms)
|
|
|
|
if self.rainbow_fade_in is not None:
|
|
self.rainbow_fade_in.update(current_ms)
|
|
|
|
self.rainbow_animation.update(current_ms)
|
|
|
|
def draw(self):
|
|
mirror = 'vertical' if self.is_2p else ''
|
|
tex.draw_texture('gauge', 'border' + self.string_diff, index=self.is_2p, mirror=mirror)
|
|
tex.draw_texture('gauge', f'{self.player_num}p_unfilled' + self.string_diff, index=self.is_2p, mirror=mirror)
|
|
gauge_length = int(self.gauge_length)
|
|
clear_point = self.clear_start[self.difficulty]
|
|
bar_width = tex.textures["gauge"][f"{self.player_num}p_bar"].width
|
|
tex.draw_texture('gauge', f'{self.player_num}p_bar', x2=min(gauge_length*bar_width, (clear_point - 1)*bar_width)-bar_width, index=self.is_2p)
|
|
if gauge_length >= clear_point - 1:
|
|
tex.draw_texture('gauge', 'bar_clear_transition', x=(clear_point - 1)*bar_width, index=self.is_2p, mirror=mirror)
|
|
if gauge_length > clear_point:
|
|
tex.draw_texture('gauge', 'bar_clear_top', x=(clear_point) * bar_width, x2=(gauge_length-clear_point)*bar_width, index=self.is_2p, mirror=mirror)
|
|
tex.draw_texture('gauge', 'bar_clear_bottom', x=(clear_point) * bar_width, x2=(gauge_length-clear_point)*bar_width, index=self.is_2p)
|
|
|
|
# Rainbow effect for full gauge
|
|
if gauge_length == self.gauge_max and self.rainbow_fade_in is not None:
|
|
if 0 < self.rainbow_animation.attribute < 8:
|
|
tex.draw_texture('gauge', 'rainbow' + self.string_diff, frame=self.rainbow_animation.attribute-1, fade=self.rainbow_fade_in.attribute, index=self.is_2p, mirror=mirror)
|
|
tex.draw_texture('gauge', 'rainbow' + self.string_diff, frame=self.rainbow_animation.attribute, fade=self.rainbow_fade_in.attribute, index=self.is_2p, mirror=mirror)
|
|
if self.gauge_update_anim is not None and gauge_length <= self.gauge_max and gauge_length > self.previous_length:
|
|
if gauge_length == self.clear_start[self.difficulty]:
|
|
tex.draw_texture('gauge', 'bar_clear_transition_fade', x=gauge_length*bar_width, fade=self.gauge_update_anim.attribute, index=self.is_2p, mirror=mirror)
|
|
elif gauge_length > self.clear_start[self.difficulty]:
|
|
tex.draw_texture('gauge', 'bar_clear_fade', x=gauge_length*bar_width, fade=self.gauge_update_anim.attribute, index=self.is_2p)
|
|
else:
|
|
tex.draw_texture('gauge', f'{self.player_num}p_bar_fade', x=gauge_length*bar_width, fade=self.gauge_update_anim.attribute, index=self.is_2p)
|
|
tex.draw_texture('gauge', 'overlay' + self.string_diff, fade=0.15, index=self.is_2p, mirror=mirror)
|
|
|
|
# Draw clear status indicators
|
|
if gauge_length >= clear_point-1:
|
|
tex.draw_texture('gauge', 'clear', index=min(2, self.difficulty)+(self.is_2p*3))
|
|
if self.is_rainbow:
|
|
tex.draw_texture('gauge', 'tamashii_fire', scale=0.75, center=True, frame=self.tamashii_fire_change.attribute, index=self.is_2p)
|
|
tex.draw_texture('gauge', 'tamashii', index=self.is_2p)
|
|
if self.is_rainbow and self.tamashii_fire_change.attribute in (0, 1, 4, 5):
|
|
tex.draw_texture('gauge', 'tamashii_overlay', fade=0.5, index=self.is_2p)
|
|
else:
|
|
tex.draw_texture('gauge', 'clear_dark', index=min(2, self.difficulty)+(self.is_2p*3))
|
|
tex.draw_texture('gauge', 'tamashii_dark', index=self.is_2p)
|