minor fixes

This commit is contained in:
Anthony Samms
2025-11-02 18:29:46 -05:00
parent a1bc58b5ea
commit f93accd4a1
16 changed files with 60 additions and 55 deletions

243
scenes/dan/dan_select.py Normal file
View File

@@ -0,0 +1,243 @@
import logging
import pyray as ray
from libs.audio import audio
from libs.global_data import global_data
from libs.texture import tex
from libs.chara_2d import Chara2D
from libs.global_objects import AllNetIcon, CoinOverlay, Indicator, Nameplate, Timer
from libs.screen import Screen
from libs.file_navigator import DanCourse, navigator
from libs.transition import Transition
from libs.utils import get_current_ms, is_l_don_pressed, is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed
from scenes.song_select import SongSelectScreen, State
logger = logging.getLogger(__name__)
class DanSelectScreen(Screen):
def on_screen_start(self):
super().on_screen_start()
self.navigator = navigator
self.navigator.in_dan_select = True
self.coin_overlay = CoinOverlay()
self.allnet_indicator = AllNetIcon()
self.timer = Timer(60, get_current_ms(), self.navigator.select_current_item)
self.indicator = Indicator(Indicator.State.SELECT)
self.player = DanSelectPlayer(str(global_data.player_num))
self.state = State.BROWSING
self.transition = Transition('', '')
self.last_moved = 0
audio.play_sound('bgm', 'music')
audio.play_sound('dan_select', 'voice')
def on_screen_end(self, next_screen: str):
session_data = global_data.session_data[global_data.player_num-1]
current_item = self.navigator.get_current_item()
if isinstance(current_item, DanCourse):
session_data.selected_song = current_item.charts[0]
session_data.selected_dan = current_item.charts
session_data.selected_dan_exam = current_item.exams
session_data.song_title = current_item.title
session_data.dan_color = current_item.color
else:
self.navigator.in_dan_select = False
self.navigator.go_back()
return super().on_screen_end(next_screen)
def handle_input_browsing(self):
"""Handle input for browsing songs."""
action = self.player.handle_input_browsing(self.last_moved, self.navigator.items[self.navigator.selected_index] if self.navigator.items else None)
current_time = get_current_ms()
if action == "skip_left":
for _ in range(10):
self.navigator.navigate_left()
self.last_moved = current_time
elif action == "skip_right":
for _ in range(10):
self.navigator.navigate_right()
self.last_moved = current_time
elif action == "navigate_left":
self.navigator.navigate_left()
self.last_moved = current_time
elif action == "navigate_right":
self.navigator.navigate_right()
self.last_moved = current_time
elif action == "go_back":
return action
elif action == "select_song":
self.state = State.SONG_SELECTED
def handle_input(self, state, screen):
"""Main input dispatcher. Delegates to state-specific handlers."""
if state == State.BROWSING:
return screen.handle_input_browsing()
elif state == State.SONG_SELECTED:
res = self.player.handle_input_selected()
if res == 'confirm':
self.transition.start()
elif res == 'cancel':
self.state = State.BROWSING
def update(self):
super().update()
current_time = get_current_ms()
self.indicator.update(current_time)
self.timer.update(current_time)
self.transition.update(current_time)
if self.transition.is_finished:
return self.on_screen_end("GAME_DAN")
for song in self.navigator.items:
song.box.update(False)
song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150
self.player.update(current_time)
res = self.handle_input(self.state, self)
if res == 'go_back':
return self.on_screen_end("SONG_SELECT")
def draw(self):
tex.draw_texture('global', 'bg')
tex.draw_texture('global', 'bg_header')
tex.draw_texture('global', 'bg_footer')
tex.draw_texture('global', 'footer')
for item in self.navigator.items:
box = item.box
if -156 <= box.position <= 1280 + 144:
if box.position <= 500:
box.draw(box.position, 95, False)
else:
box.draw(box.position, 95, False)
if self.state == State.SONG_SELECTED:
ray.draw_rectangle(0, 0, 1280, 720, ray.fade(ray.BLACK, min(0.5, self.player.confirmation_window.fade_in.attribute)))
self.player.draw()
self.indicator.draw(410, 575)
self.timer.draw()
self.coin_overlay.draw()
tex.draw_texture('global', 'dan_select')
self.transition.draw()
self.allnet_indicator.draw()
class DanSelectPlayer:
def __init__(self, player_num: str):
self.player_num = player_num
self.selected_difficulty = -3
self.prev_diff = -3
self.selected_song = False
self.is_ura = False
self.is_confirmed = False
self.ura_toggle = 0
self.diff_select_move_right = False
self.neiro_selector = None
self.modifier_selector = None
self.confirmation_window = ConfirmationWindow()
# Player-specific objects
self.chara = Chara2D(int(self.player_num) - 1, 100)
plate_info = global_data.config[f'nameplate_{self.player_num}p']
self.nameplate = Nameplate(plate_info['name'], plate_info['title'],
int(self.player_num), plate_info['dan'], plate_info['gold'], plate_info['title_bg'])
def update(self, current_time):
"""Update player state"""
self.nameplate.update(current_time)
self.chara.update(current_time, 100, False, False)
self.confirmation_window.update(current_time, self.is_confirmed)
def handle_input_browsing(self, last_moved, selected_item):
"""Handle input for browsing songs. Returns action string or None."""
current_time = get_current_ms()
# Skip left (fast navigate)
if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT_CONTROL) or (is_l_kat_pressed(self.player_num) and current_time <= last_moved + 50):
audio.play_sound('skip', 'sound')
return "skip_left"
# Skip right (fast navigate)
if ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT_CONTROL) or (is_r_kat_pressed(self.player_num) and current_time <= last_moved + 50):
audio.play_sound('skip', 'sound')
return "skip_right"
# Navigate left
if is_l_kat_pressed(self.player_num):
audio.play_sound('kat', 'sound')
return "navigate_left"
# Navigate right
if is_r_kat_pressed(self.player_num):
audio.play_sound('kat', 'sound')
return "navigate_right"
# Select/Enter
if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num):
if selected_item is not None and selected_item.box.is_back:
audio.play_sound('cancel', 'sound')
return "go_back"
else:
self.confirmation_window.start()
audio.play_sound('don', 'sound')
audio.play_sound('confirm_box', 'sound')
audio.play_sound('dan_confirm', 'voice')
return "select_song"
return None
def handle_input(self, state, screen):
"""Main input dispatcher. Delegates to state-specific handlers."""
if state == State.BROWSING:
screen.handle_input_browsing()
elif state == State.SONG_SELECTED:
res = screen.handle_input_selected()
if res:
return res
def handle_input_selected(self):
"""Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None"""
if is_l_kat_pressed(self.player_num):
audio.play_sound('kat', 'sound')
self.is_confirmed = False
if is_r_kat_pressed(self.player_num):
audio.play_sound('kat', 'sound')
self.is_confirmed = True
if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num):
if self.is_confirmed:
audio.play_sound('don', 'sound')
return "confirm"
else:
self.confirmation_window = ConfirmationWindow()
return "cancel"
return None
def draw(self):
if self.player_num == '1':
self.nameplate.draw(30, 640)
self.chara.draw(x=-50, y=410)
else:
self.nameplate.draw(950, 640)
self.chara.draw(mirror=True, x=950, y=410)
self.confirmation_window.draw()
class ConfirmationWindow:
def __init__(self):
self.fade_in = tex.get_animation(8, is_copy=True)
self.side = 0
def start(self):
self.fade_in.start()
def update(self, current_time_ms: float, is_confirmed: bool):
self.fade_in.update(current_time_ms)
self.side = is_confirmed
def draw(self):
tex.draw_texture('confirm_box', 'bg', fade=self.fade_in.attribute)
tex.draw_texture('confirm_box', 'confirmation_text', fade=self.fade_in.attribute)
for i in range(2):
tex.draw_texture('confirm_box', 'selection_box', index=i, fade=self.fade_in.attribute)
tex.draw_texture('confirm_box', 'selection_box_highlight', index=self.side, fade=self.fade_in.attribute)
tex.draw_texture('confirm_box', 'selection_box_outline', index=self.side, fade=self.fade_in.attribute)
tex.draw_texture('confirm_box', 'yes', fade=self.fade_in.attribute)
tex.draw_texture('confirm_box', 'no', fade=self.fade_in.attribute)

420
scenes/dan/game_dan.py Normal file
View File

@@ -0,0 +1,420 @@
import copy
from typing import override
import pyray as ray
import logging
from libs.animation import Animation
from libs.audio import audio
from libs.background import Background
from libs.file_navigator import Exam
from libs.global_data import global_data
from libs.global_objects import AllNetIcon
from libs.tja import TJAParser
from libs.transition import Transition
from libs.utils import OutlinedText, get_current_ms
from libs.texture import tex
from scenes.game import ClearAnimation, FCAnimation, FailAnimation, GameScreen, Gauge, ResultTransition, SongInfo
logger = logging.getLogger(__name__)
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
class DanGameScreen(GameScreen):
JUDGE_X = 414
@override
def on_screen_start(self):
self.mask_shader = ray.load_shader("shader/outline.vs", "shader/mask.fs")
self.current_ms = 0
self.end_ms = 0
self.start_delay = 4000
self.song_started = False
self.song_music = None
self.song_index = 0
tex.unload_textures()
tex.load_screen_textures('game')
audio.load_screen_sounds('game')
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)
self.hori_name = OutlinedText(global_data.session_data[global_data.player_num-1].song_title, 40, ray.WHITE)
self.init_dan()
self.background = Background(global_data.player_num, self.bpm, scene_preset='DAN')
self.transition = Transition('', '', is_second=True)
self.transition.start()
self.dan_transition = DanTransition()
self.dan_transition.start()
self.allnet_indicator = AllNetIcon()
self.result_transition = ResultTransition(4)
self.load_hitsounds()
def init_dan(self):
session_data = global_data.session_data[global_data.player_num-1]
songs = copy.deepcopy(session_data.selected_dan)
self.exams = copy.deepcopy(session_data.selected_dan_exam)
self.total_notes = 0
for song, genre_index, difficulty in songs:
notes, branch_m, branch_e, branch_n = song.notes_to_position(difficulty)
self.total_notes += sum(1 for note in notes.play_notes if note.type < 5)
for branch in branch_m:
self.total_notes += sum(1 for note in branch.play_notes if note.type < 5)
for branch in branch_e:
self.total_notes += sum(1 for note in branch.play_notes if note.type < 5)
for branch in branch_n:
self.total_notes += sum(1 for note in branch.play_notes if note.type < 5)
song, genre_index, difficulty = songs[self.song_index]
session_data.selected_difficulty = difficulty
self.init_tja(song.file_path)
self.color = session_data.dan_color
self.player_1.is_dan = True
self.player_1.gauge = DanGauge(str(global_data.player_num), self.total_notes)
self.song_info = SongInfo(song.metadata.title.get(global_data.config["general"]["language"], "en"), genre_index)
self.bpm = self.tja.metadata.bpm
logger.info(f"TJA initialized for song: {song.file_path}")
self.dan_info_cache = None
self.exam_failed = [False] * len(self.exams)
def change_song(self):
session_data = global_data.session_data[global_data.player_num-1]
songs = session_data.selected_dan
song, genre_index, difficulty = songs[self.song_index]
session_data.selected_difficulty = difficulty
self.player_1.difficulty = difficulty
self.tja = TJAParser(song.file_path, start_delay=self.start_delay, distance=SCREEN_WIDTH - GameScreen.JUDGE_X)
if self.song_music is not None:
audio.unload_music_stream(self.song_music)
self.song_music = None
self.song_started = False
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.tja = self.tja
self.player_1.reset_chart()
self.dan_transition.start()
self.song_info = SongInfo(self.tja.metadata.title.get(global_data.config["general"]["language"], "en"), genre_index)
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)
def _calculate_dan_info(self):
"""Calculate all dan info data for drawing"""
remaining_notes = self.total_notes - self.player_1.good_count - self.player_1.ok_count - self.player_1.bad_count
exam_data = []
for exam in self.exams:
progress_value = self._get_exam_progress(exam)
progress = progress_value / exam.red
if exam.range == 'less':
progress = 1 - progress
counter_value = max(0, exam.red - progress_value)
elif exam.range == 'more':
counter_value = max(0, progress_value)
else:
counter_value = max(0, progress_value)
# Clamp progress
progress = max(0, min(progress, 1))
# Determine progress bar texture
if progress == 1:
bar_texture = 'exam_max'
elif progress >= 0.5:
bar_texture = 'exam_gold'
else:
bar_texture = 'exam_red'
exam_data.append({
'exam': exam,
'progress': progress,
'bar_texture': bar_texture,
'counter_value': counter_value,
'red_value': exam.red
})
return {
'remaining_notes': remaining_notes,
'exam_data': exam_data
}
def _get_exam_progress(self, exam: Exam) -> int:
"""Get progress value based on exam type"""
type_mapping = {
'gauge': (self.player_1.gauge.gauge_length / self.player_1.gauge.gauge_max) * 100,
'judgeperfect': self.player_1.good_count,
'judgegood': self.player_1.ok_count + self.player_1.bad_count,
'judgebad': self.player_1.bad_count,
'score': self.player_1.score,
'combo': self.player_1.max_combo
}
return int(type_mapping.get(exam.type, 0))
@override
def global_keys(self):
if ray.is_key_pressed(ray.KeyboardKey.KEY_F1):
if self.song_music is not None:
audio.stop_music_stream(self.song_music)
audio.seek_music_stream(self.song_music, 0)
self.song_started = False
audio.play_sound('restart', 'sound')
self.init_dan()
if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE):
if self.song_music is not None:
audio.stop_music_stream(self.song_music)
return self.on_screen_end('DAN_SELECT')
@override
def spawn_ending_anims(self):
if self.player_1.gauge.is_clear and not any(self.exam_failed):
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)
@override
def update(self):
super(GameScreen, self).update()
current_time = get_current_ms()
self.transition.update(current_time)
self.current_ms = current_time - self.start_ms
self.dan_transition.update(current_time)
self.start_song(current_time)
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)
self.dan_info_cache = self._calculate_dan_info()
self._check_exam_failures()
if self.result_transition.is_finished and not audio.is_sound_playing('dan_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 + 1000:
session_data = global_data.session_data[global_data.player_num-1]
if self.song_index == len(session_data.selected_dan) - 1:
if self.end_ms != 0:
if current_time >= self.end_ms + 1000:
if self.player_1.ending_anim is None:
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('dan_transition', 'voice')
logger.info("Result transition started and voice played")
else:
self.end_ms = current_time
else:
self.song_index += 1
self.dan_transition.start()
self.change_song()
return self.global_keys()
def _check_exam_failures(self):
for i, exam in enumerate(self.exams):
progress_value = self._get_exam_progress(exam)
if self.exam_failed[i]:
continue
if exam.range == 'more':
if progress_value < exam.red and self.end_ms != 0:
self.exam_failed[i] = True
audio.play_sound('exam_failed', 'sound')
logger.info(f"Exam {i} ({exam.type}) failed: {progress_value} < {exam.red}")
elif exam.range == 'less':
counter_value = max(0, exam.red - progress_value)
if counter_value == 0:
self.exam_failed[i] = True
audio.play_sound('dan_failed', 'sound')
logger.info(f"Exam {i} ({exam.type}) failed: counter reached 0")
def draw_dan_info(self):
if self.dan_info_cache is None:
return
cache = self.dan_info_cache
# Draw total notes counter
tex.draw_texture('dan_info', 'total_notes')
counter = str(cache['remaining_notes'])
self._draw_counter(counter, margin=45, texture='total_notes_counter')
# Draw exam info
for i, exam_info in enumerate(cache['exam_data']):
y_offset = i * 94
exam = exam_info['exam']
tex.draw_texture('dan_info', 'exam_bg', y=y_offset)
tex.draw_texture('dan_info', 'exam_overlay_1', y=y_offset)
# Draw progress bar
tex.draw_texture('dan_info', exam_info['bar_texture'], x2=940*exam_info['progress'], y=y_offset)
# Draw exam type and red value counter
red_counter = str(exam_info['red_value'])
self._draw_counter(red_counter, margin=22, texture='value_counter', index=0, y=y_offset)
tex.draw_texture('dan_info', f'exam_{exam.type}', y=y_offset, x=-len(red_counter)*20)
# Draw range indicator
if exam.range == 'less':
tex.draw_texture('dan_info', 'exam_less', y=y_offset)
elif exam.range == 'more':
tex.draw_texture('dan_info', 'exam_more', y=y_offset)
# Draw current value counter
tex.draw_texture('dan_info', 'exam_overlay_2', y=y_offset)
value_counter = str(exam_info['counter_value'])
self._draw_counter(value_counter, margin=22, texture='value_counter', index=1, y=y_offset)
if exam.type == 'gauge':
tex.draw_texture('dan_info', 'exam_percent', y=y_offset, index=1)
if self.exam_failed[i]:
tex.draw_texture('dan_info', 'exam_bg', fade=0.5, y=y_offset)
tex.draw_texture('dan_info', 'exam_failed', y=y_offset)
# Draw frame and title
tex.draw_texture('dan_info', 'frame', frame=self.color)
if self.hori_name is not None:
self.hori_name.draw(outline_color=ray.BLACK, x=154 - (self.hori_name.texture.width//2),
y=392, x2=min(self.hori_name.texture.width, 275)-self.hori_name.texture.width)
def _draw_counter(self, counter, margin, texture, index=None, y=0):
"""Helper to draw digit counters"""
for j in range(len(counter)):
kwargs = {'frame': int(counter[j]), 'x': -(len(counter) - j) * margin, 'y': y}
if index is not None:
kwargs['index'] = index
tex.draw_texture('dan_info', texture, **kwargs)
@override
def draw(self):
self.background.draw()
self.draw_dan_info()
self.player_1.draw(self.current_ms, self.start_ms, self.mask_shader, dan_transition=self.dan_transition)
self.draw_overlay()
class DanTransition:
def __init__(self):
self.move = tex.get_animation(26)
self.is_finished = False
def start(self):
self.move.start()
self.is_finished = False
def update(self, current_time):
self.move.update(current_time)
self.is_finished = self.move.is_finished
def draw(self):
tex.draw_texture('dan', 'transition', index=0, x=self.move.attribute, mirror='horizontal')
tex.draw_texture('dan', 'transition', index=1, x=-self.move.attribute)
class DanGauge(Gauge):
"""The player's gauge"""
def __init__(self, player_num: str, total_notes: int):
self.player_num = player_num
self.string_diff = "_hard"
self.gauge_length = 0
self.previous_length = 0
self.visual_length = 0
self.total_notes = total_notes
self.gauge_max = 89
self.tamashii_fire_change = tex.get_animation(25)
self.is_clear = False
self.is_rainbow = False
self.gauge_update_anim = tex.get_animation(10)
self.rainbow_fade_in = None
self.rainbow_animation = None
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 * (self.gauge_max / 100))) * 100
if self.gauge_length > self.gauge_max:
self.gauge_length = self.gauge_max
if int(self.gauge_length * 8) % 8 == 0:
self.visual_length = int(self.gauge_length * 8)
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 += (0.5 / (self.total_notes * (self.gauge_max / 100))) * 100
if self.gauge_length > self.gauge_max:
self.gauge_length = self.gauge_max
if int(self.gauge_length * 8) % 8 == 0:
self.visual_length = int(self.gauge_length * 8)
def add_bad(self):
"""Adds a bad note to the gauge"""
self.previous_length = int(self.gauge_length)
self.gauge_length -= (2 / (self.total_notes * (self.gauge_max / 100))) * 100
if self.gauge_length < 0:
self.gauge_length = 0
if int(self.gauge_length * 8) % 8 == 0:
self.visual_length = int(self.gauge_length * 8)
def update(self, current_ms: float):
self.is_rainbow = self.gauge_length == self.gauge_max
self.is_clear = self.is_rainbow
if self.gauge_length == self.gauge_max and self.rainbow_fade_in is None:
self.rainbow_fade_in = Animation.create_fade(450, initial_opacity=0.0, final_opacity=1.0)
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)
if self.rainbow_animation is None:
self.rainbow_animation = Animation.create_texture_change((16.67*8) * 3, textures=[((16.67 * 3) * i, (16.67 * 3) * (i + 1), i) for i in range(8)])
self.rainbow_animation.start()
else:
self.rainbow_animation.update(current_ms)
if self.rainbow_animation.is_finished or self.gauge_length < 87:
self.rainbow_animation = None
def draw(self):
tex.draw_texture('gauge_dan', 'border')
tex.draw_texture('gauge_dan', f'{self.player_num}p_unfilled')
tex.draw_texture('gauge_dan', f'{self.player_num}p_bar', x2=self.visual_length-8)
# Rainbow effect for full gauge
if self.gauge_length == self.gauge_max and self.rainbow_fade_in is not None and self.rainbow_animation is not None:
if 0 < self.rainbow_animation.attribute < 8:
tex.draw_texture('gauge_dan', 'rainbow', frame=self.rainbow_animation.attribute-1, fade=self.rainbow_fade_in.attribute)
tex.draw_texture('gauge_dan', 'rainbow', frame=self.rainbow_animation.attribute, fade=self.rainbow_fade_in.attribute)
if self.gauge_update_anim is not None and self.visual_length <= self.gauge_max and self.visual_length > self.previous_length:
tex.draw_texture('gauge_dan', f'{self.player_num}p_bar_fade', x=self.visual_length-8, fade=self.gauge_update_anim.attribute)
tex.draw_texture('gauge_dan', 'overlay', fade=0.15)
# Draw clear status indicators
if self.is_rainbow:
tex.draw_texture('gauge_dan', 'tamashii_fire', scale=0.75, center=True, frame=self.tamashii_fire_change.attribute)
tex.draw_texture('gauge_dan', 'tamashii')
if self.is_rainbow and self.tamashii_fire_change.attribute in (0, 1, 4, 5):
tex.draw_texture('gauge_dan', 'tamashii_overlay', fade=0.5)
else:
tex.draw_texture('gauge_dan', 'tamashii_dark')