import logging from typing import Optional import pyray as ray from libs.audio import audio from libs.chara_2d import Chara2D from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate, Indicator, EntryOverlay, Timer from libs.texture import tex from libs.screen import Screen from libs.utils import ( OutlinedText, get_current_ms, global_data, is_l_don_pressed, is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed, ) logger = logging.getLogger(__name__) class State: """State enum for the entry screen""" SELECT_SIDE = 0 SELECT_MODE = 1 class EntryScreen(Screen): def on_screen_start(self): super().on_screen_start() self.side = 1 self.is_2p = False self.box_manager = BoxManager() self.state = State.SELECT_SIDE # Initial nameplate for side selection plate_info = global_data.config['nameplate_1p'] self.nameplate = Nameplate(plate_info['name'], plate_info['title'], -1, -1, False, False, 0) self.coin_overlay = CoinOverlay() self.allnet_indicator = AllNetIcon() self.entry_overlay = EntryOverlay() self.timer = Timer(60, get_current_ms(), self.box_manager.select_box) self.screen_init = True self.side_select_fade = tex.get_animation(0) self.bg_flicker = tex.get_animation(1) self.side_select_fade.start() self.chara = Chara2D(0, 100) self.announce_played = False self.players: list[Optional[EntryPlayer]] = [None, None] audio.play_sound('bgm', 'music') def on_screen_end(self, next_screen: str): audio.stop_sound('bgm') self.nameplate.unload() for player in self.players: if player: player.unload() return super().on_screen_end(next_screen) def handle_input(self): if self.state == State.SELECT_SIDE: if is_l_don_pressed() or is_r_don_pressed(): if self.side == 1: return self.on_screen_end("TITLE") global_data.player_num = round((self.side/3) + 1) if self.players[0]: self.players[1] = EntryPlayer(global_data.player_num, self.side, self.box_manager) self.players[1].start_animations() global_data.player_num = 1 self.is_2p = True else: self.players[0] = EntryPlayer(global_data.player_num, self.side, self.box_manager) self.players[0].start_animations() self.is_2p = False audio.play_sound('cloud', 'sound') audio.play_sound(f'entry_start_{global_data.player_num}p', 'voice') self.state = State.SELECT_MODE audio.play_sound('don', 'sound') if is_l_kat_pressed(): audio.play_sound('kat', 'sound') if self.players[0] and self.players[0].player_num == 1: self.side = 1 elif self.players[0] and self.players[0].player_num == 2: self.side = 0 else: self.side = max(0, self.side - 1) if is_r_kat_pressed(): audio.play_sound('kat', 'sound') if self.players[0] and self.players[0].player_num == 1: self.side = 2 elif self.players[0] and self.players[0].player_num == 2: self.side = 1 else: self.side = min(2, self.side + 1) elif self.state == State.SELECT_MODE: for player in self.players: if player: player.handle_input() if self.players[0] and self.players[0].player_num == 1 and is_l_don_pressed('2') or is_r_don_pressed('2'): audio.play_sound('don', 'sound') self.state = State.SELECT_SIDE plate_info = global_data.config['nameplate_2p'] self.nameplate = Nameplate(plate_info['name'], plate_info['title'], -1, -1, False, False, 1) self.chara = Chara2D(1, 100) self.side_select_fade.restart() self.side = 1 elif self.players[0] and self.players[0].player_num == 2 and is_l_don_pressed('1') or is_r_don_pressed('1'): audio.play_sound('don', 'sound') self.state = State.SELECT_SIDE self.side_select_fade.restart() self.side = 1 def update(self): super().update() current_time = get_current_ms() self.side_select_fade.update(current_time) self.bg_flicker.update(current_time) self.box_manager.update(current_time, self.is_2p) self.timer.update(current_time) self.nameplate.update(current_time) self.chara.update(current_time, 100, False, False) for player in self.players: if player: player.update(current_time) if self.box_manager.is_finished(): logger.info(f"Box selection finished, transitioning to {self.box_manager.selected_box()}") return self.on_screen_end(self.box_manager.selected_box()) for player in self.players: if player and player.cloud_fade.is_finished and not audio.is_sound_playing(f'entry_start_{global_data.player_num}p') and not self.announce_played: audio.play_sound('select_mode', 'voice') self.announce_played = True return self.handle_input() def draw_background(self): tex.draw_texture('background', 'bg') tex.draw_texture('background', 'tower') tex.draw_texture('background', 'shops_center') tex.draw_texture('background', 'people') tex.draw_texture('background', 'shops_left') tex.draw_texture('background', 'shops_right') tex.draw_texture('background', 'lights', scale=2.0, fade=self.bg_flicker.attribute) def draw_side_select(self, fade): tex.draw_texture('side_select', 'box_top_left', fade=fade) tex.draw_texture('side_select', 'box_top_right', fade=fade) tex.draw_texture('side_select', 'box_bottom_left', fade=fade) tex.draw_texture('side_select', 'box_bottom_right', fade=fade) tex.draw_texture('side_select', 'box_top', fade=fade) tex.draw_texture('side_select', 'box_bottom', fade=fade) tex.draw_texture('side_select', 'box_left', fade=fade) tex.draw_texture('side_select', 'box_right', fade=fade) tex.draw_texture('side_select', 'box_center', fade=fade) tex.draw_texture('side_select', 'question', fade=fade) self.chara.draw(480, 240) tex.draw_texture('side_select', '1P', fade=fade) tex.draw_texture('side_select', 'cancel', fade=fade) tex.draw_texture('side_select', '2P', fade=fade) if self.side == 0: tex.draw_texture('side_select', '1P_highlight', fade=fade) tex.draw_texture('side_select', '1P2P_outline', index=0, fade=fade, mirror='horizontal') elif self.side == 1: tex.draw_texture('side_select', 'cancel_highlight', fade=fade) tex.draw_texture('side_select', 'cancel_outline', fade=fade) else: tex.draw_texture('side_select', '2P_highlight', fade=fade) tex.draw_texture('side_select', '1P2P_outline', index=1, fade=fade) tex.draw_texture('side_select', 'cancel_text', fade=fade) self.nameplate.draw(500, 185) def draw_player_drum(self): for player in self.players: if player: player.draw_drum() def draw_mode_select(self): for player in self.players: if player and not player.is_cloud_animation_finished(): return self.box_manager.draw() def draw(self): self.draw_background() self.draw_player_drum() if self.state == State.SELECT_SIDE: self.draw_side_select(self.side_select_fade.attribute) elif self.state == State.SELECT_MODE: self.draw_mode_select() tex.draw_texture('side_select', 'footer') if self.players[0] and self.players[1]: pass elif not self.players[0]: tex.draw_texture('side_select', 'footer_left') tex.draw_texture('side_select', 'footer_right') elif self.players[0] and self.players[0].player_num == 1: tex.draw_texture('side_select', 'footer_right') elif self.players[0] and self.players[0].player_num == 2: tex.draw_texture('side_select', 'footer_left') for player in self.players: if player: player.draw_nameplate_and_indicator(fade=player.nameplate_fadein.attribute) tex.draw_texture('global', 'player_entry') if self.box_manager.is_finished(): ray.draw_rectangle(0, 0, 1280, 720, ray.BLACK) self.timer.draw() self.entry_overlay.draw(y=-10) self.coin_overlay.draw() self.allnet_indicator.draw() class EntryPlayer: """Player-specific state and rendering for the entry screen""" def __init__(self, player_num: int, side: int, box_manager: 'BoxManager'): """ Initialize a player for the entry screen Args: player_num: 1 or 2 (player number) side: 0 for left (1P), 2 for right (2P) box_manager: Reference to the box manager for input handling """ self.player_num = player_num self.side = side self.box_manager = box_manager # Load player-specific resources plate_info = global_data.config[f'nameplate_{self.player_num}p'] self.nameplate = Nameplate( plate_info['name'], plate_info['title'], player_num, plate_info['dan'], plate_info['gold'], plate_info['rainbow'], plate_info['title_bg'] ) self.indicator = Indicator(Indicator.State.SELECT) # Character (0 for red/1P, 1 for blue/2P) chara_id = 0 if side == 0 else 1 self.chara = Chara2D(chara_id, 100) # Animations self.drum_move_1 = tex.get_animation(2) self.drum_move_2 = tex.get_animation(3) self.drum_move_3 = tex.get_animation(4) self.cloud_resize = tex.get_animation(5) self.cloud_resize_loop = tex.get_animation(6) self.cloud_texture_change = tex.get_animation(7) self.cloud_fade = tex.get_animation(8) self.nameplate_fadein = tex.get_animation(12) def start_animations(self): """Start all player entry animations""" self.drum_move_1.start() self.drum_move_2.start() self.drum_move_3.start() self.cloud_resize.start() self.cloud_resize_loop.start() self.cloud_texture_change.start() self.cloud_fade.start() self.nameplate_fadein.start() def update(self, current_time: float): """Update player animations and state""" self.drum_move_1.update(current_time) self.drum_move_2.update(current_time) self.drum_move_3.update(current_time) self.cloud_resize.update(current_time) self.cloud_texture_change.update(current_time) self.cloud_fade.update(current_time) self.cloud_resize_loop.update(current_time) self.nameplate_fadein.update(current_time) self.nameplate.update(current_time) self.indicator.update(current_time) self.chara.update(current_time, 100, False, False) def draw_drum(self): """Draw the player's drum with animations""" move_x = self.drum_move_3.attribute move_y = self.drum_move_1.attribute + self.drum_move_2.attribute if self.side == 0: # Left side (1P/red) offset = 0 tex.draw_texture('side_select', 'red_drum', x=move_x, y=move_y) chara_x = move_x + offset + 170 chara_mirror = False else: # Right side (2P/blue) move_x *= -1 offset = 620 tex.draw_texture('side_select', 'blue_drum', x=move_x, y=move_y) chara_x = move_x + offset + 130 chara_mirror = True # Draw character chara_y = 570 + move_y self.chara.draw(chara_x, chara_y, mirror=chara_mirror) # Draw cloud scale = self.cloud_resize.attribute if self.cloud_resize.is_finished: scale = max(1, self.cloud_resize_loop.attribute) tex.draw_texture( 'side_select', 'cloud', x=move_x + offset, y=move_y, frame=self.cloud_texture_change.attribute, fade=self.cloud_fade.attribute, scale=scale, center=True ) def draw_nameplate_and_indicator(self, fade: float = 1.0): """Draw nameplate and indicator at player-specific position""" if self.side == 0: # Left side self.nameplate.draw(30, 640, fade=fade) self.indicator.draw(50, 575, fade=fade) else: # Right side self.nameplate.draw(950, 640, fade=fade) self.indicator.draw(770, 575, fade=fade) def is_cloud_animation_finished(self) -> bool: """Check if cloud texture change animation is finished""" return self.cloud_texture_change.is_finished def unload(self): """Unload player resources""" self.nameplate.unload() def handle_input(self): """Handle player input for mode selection""" if self.box_manager.is_box_selected(): return if is_l_don_pressed(str(self.player_num)) or is_r_don_pressed(str(self.player_num)): audio.play_sound('don', 'sound') self.box_manager.select_box() if is_l_kat_pressed(str(self.player_num)): audio.play_sound('kat', 'sound') self.box_manager.move_left() if is_r_kat_pressed(str(self.player_num)): audio.play_sound('kat', 'sound') self.box_manager.move_right() class Box: """Box class for the entry screen""" def __init__(self, text: OutlinedText, location: str): self.text = text self.location = location self.box_tex_obj = tex.textures['mode_select']['box'] if isinstance(self.box_tex_obj.texture, list): raise Exception("Box texture cannot be iterable") self.texture = self.box_tex_obj.texture self.x = self.box_tex_obj.x[0] self.y = self.box_tex_obj.y[0] self.move = tex.get_animation(10) self.open = tex.get_animation(11) self.is_selected = False self.moving_left = False self.moving_right = False def set_positions(self, x: int): """Set the positions of the box""" self.x = x self.static_x = self.x self.left_x = self.x self.static_left = self.left_x self.right_x = self.left_x + tex.textures['mode_select']['box'].width - tex.textures['mode_select']['box_highlight_right'].width self.static_right = self.right_x def update(self, current_time_ms: float, is_selected: bool): self.move.update(current_time_ms) if self.moving_left: self.x = self.static_x - int(self.move.attribute) elif self.moving_right: self.x = self.static_x + int(self.move.attribute) if self.move.is_finished: self.moving_left = False self.moving_right = False self.static_x = self.x if is_selected and not self.is_selected: self.open.start() self.is_selected = is_selected if self.is_selected: self.left_x = self.static_left - int(self.open.attribute) self.right_x = self.static_right + int(self.open.attribute) self.open.update(current_time_ms) def move_left(self): """Move the box left""" if not self.move.is_started: self.move.start() self.moving_left = True def move_right(self): """Move the box right""" if not self.move.is_started: self.move.start() self.moving_right = True def _draw_highlighted(self, color): texture_left = tex.textures['mode_select']['box_highlight_left'].texture if isinstance(texture_left, list): raise Exception("highlight textures cannot be iterable") tex.draw_texture('mode_select', 'box_highlight_center', x=self.left_x + texture_left.width, y=self.y, x2=self.right_x - self.left_x -15, color=color) tex.draw_texture('mode_select', 'box_highlight_left', x=self.left_x, y=self.y, color=color) tex.draw_texture('mode_select', 'box_highlight_right', x=self.right_x, y=self.y, color=color) def _draw_text(self, color): text_x = self.x + (self.texture.width//2) - (self.text.texture.width//2) if self.is_selected: text_x += self.open.attribute text_y = self.y + 20 if self.is_selected: self.text.draw(outline_color=ray.BLACK, x=text_x, y=text_y, color=color) else: self.text.draw(outline_color=ray.Color(109, 68, 24, 255), x=text_x, y=text_y, color=color) def draw(self, fade: float): color = ray.fade(ray.WHITE, fade) ray.draw_texture(self.texture, self.x, self.y, color) if self.is_selected and self.move.is_finished: self._draw_highlighted(color) self._draw_text(color) class BoxManager: """BoxManager class for the entry screen""" def __init__(self): self.box_titles: list[OutlinedText] = [ OutlinedText('演奏ゲーム', 50, ray.WHITE, outline_thickness=5, vertical=True), OutlinedText('ゲーム設定', 50, ray.WHITE, outline_thickness=5, vertical=True)] self.box_locations = ["SONG_SELECT", "SETTINGS"] self.num_boxes = len(self.box_titles) self.boxes = [Box(self.box_titles[i], self.box_locations[i]) for i in range(len(self.box_titles))] self.selected_box_index = 0 self.fade_out = tex.get_animation(9) self.is_2p = False spacing = 80 box_width = self.boxes[0].texture.width total_width = self.num_boxes * box_width + (self.num_boxes - 1) * spacing start_x = 640 - total_width//2 for i, box in enumerate(self.boxes): box.set_positions(start_x + i * (box_width + spacing)) if i > 0: box.move_right() def select_box(self): """Select the currently selected box""" self.fade_out.start() def is_box_selected(self): """Check if the box is selected""" return self.fade_out.is_started def is_finished(self): """Check if the animation is finished""" return self.fade_out.is_finished def selected_box(self): """Get the location of the currently selected box""" return self.boxes[self.selected_box_index].location def move_left(self): """Move the cursor to the left""" prev_selection = self.selected_box_index if self.boxes[prev_selection].move.is_started and not self.boxes[prev_selection].move.is_finished: return self.selected_box_index = max(0, self.selected_box_index - 1) if prev_selection == self.selected_box_index: return if self.selected_box_index != self.selected_box_index - 1: self.boxes[self.selected_box_index+1].move_right() self.boxes[self.selected_box_index].move_right() def move_right(self): """Move the cursor to the right""" prev_selection = self.selected_box_index if self.boxes[prev_selection].move.is_started and not self.boxes[prev_selection].move.is_finished: return self.selected_box_index = min(self.num_boxes - 1, self.selected_box_index + 1) if prev_selection == self.selected_box_index: return if self.selected_box_index != 0: self.boxes[self.selected_box_index-1].move_left() self.boxes[self.selected_box_index].move_left() def update(self, current_time_ms: float, is_2p: bool): self.is_2p = is_2p if self.is_2p: self.box_locations = ["SONG_SELECT_2P", "SETTINGS"] for i, box in enumerate(self.boxes): box.location = self.box_locations[i] self.fade_out.update(current_time_ms) for i, box in enumerate(self.boxes): is_selected = i == self.selected_box_index box.update(current_time_ms, is_selected) def draw(self): for box in self.boxes: box.draw(self.fade_out.attribute)