preliminary AI battle support

This commit is contained in:
Yonokid
2025-12-31 00:29:52 -05:00
parent f384de454f
commit ac7c7abf82
8 changed files with 431 additions and 28 deletions

View File

@@ -323,6 +323,7 @@ def main():
global_tex.load_screen_textures('global') global_tex.load_screen_textures('global')
global_tex.load_zip('chara', 'chara_0') global_tex.load_zip('chara', 'chara_0')
global_tex.load_zip('chara', 'chara_1') global_tex.load_zip('chara', 'chara_1')
global_tex.load_zip('chara', 'chara_4')
if global_data.config["video"]["borderless"]: if global_data.config["video"]["borderless"]:
ray.ToggleBorderlessWindowed() ray.ToggleBorderlessWindowed()
logger.info("Borderless window enabled") logger.info("Borderless window enabled")

View File

@@ -107,7 +107,7 @@ class Chara2D:
self.current_anim = self.past_anim self.current_anim = self.past_anim
self.anims[self.current_anim].restart() self.anims[self.current_anim].restart()
def draw(self, x: float = 0, y: float = 0, mirror=False): def draw(self, x: float = 0, y: float = 0, mirror=False, scale=1.0):
""" """
Draw the character on the screen. Draw the character on the screen.
@@ -117,9 +117,9 @@ class Chara2D:
mirror (bool): Whether to mirror the character horizontally. mirror (bool): Whether to mirror the character horizontally.
""" """
if self.is_rainbow and self.current_anim not in {'soul_in', 'balloon_pop', 'balloon_popping'}: if self.is_rainbow and self.current_anim not in {'soul_in', 'balloon_pop', 'balloon_popping'}:
self.tex.draw_texture(self.name, self.current_anim + '_max', frame=self.anims[self.current_anim].attribute, x=x, y=y) self.tex.draw_texture(self.name, self.current_anim + '_max', frame=self.anims[self.current_anim].attribute, x=x, y=y, scale=scale)
else: else:
if mirror: if mirror:
self.tex.draw_texture(self.name, self.current_anim, frame=self.anims[self.current_anim].attribute, x=x, y=y, mirror='horizontal') self.tex.draw_texture(self.name, self.current_anim, frame=self.anims[self.current_anim].attribute, x=x, y=y, mirror='horizontal', scale=scale)
else: else:
self.tex.draw_texture(self.name, self.current_anim, frame=self.anims[self.current_anim].attribute, x=x, y=y) self.tex.draw_texture(self.name, self.current_anim, frame=self.anims[self.current_anim].attribute, x=x, y=y, scale=scale)

View File

@@ -14,6 +14,7 @@ class PlayerNum(IntEnum):
P2 = 2 P2 = 2
TWO_PLAYER = 3 TWO_PLAYER = 3
DAN = 4 DAN = 4
AI = 5
class ScoreMethod(): class ScoreMethod():
GEN3 = "gen3" GEN3 = "gen3"

View File

@@ -58,6 +58,9 @@ class Nameplate:
""" """
tex = global_tex tex = global_tex
tex.draw_texture('nameplate', 'shadow', x=x, y=y, fade=min(0.5, fade)) tex.draw_texture('nameplate', 'shadow', x=x, y=y, fade=min(0.5, fade))
if self.player_num == PlayerNum.AI:
tex.draw_texture('nameplate', 'ai', x=x, y=y)
return
if self.player_num == 0: if self.player_num == 0:
frame = 2 frame = 2
title_offset = 0 title_offset = 0

View File

@@ -297,7 +297,7 @@ class TextureWrapper:
else: else:
ray.DrawTexturePro(tex_object.texture, source_rect, dest_rect, origin, rotation, final_color) ray.DrawTexturePro(tex_object.texture, source_rect, dest_rect, origin, rotation, final_color)
if tex_object.controllable[index] or controllable: if tex_object.controllable[index] or controllable:
self.control(tex_object) self.control(tex_object, index)
def draw_texture(self, subset: str, texture: str, color: Color = Color(255, 255, 255, 255), frame: int = 0, scale: float = 1.0, center: bool = False, def draw_texture(self, subset: str, texture: str, color: Color = Color(255, 255, 255, 255), frame: int = 0, scale: float = 1.0, center: bool = False,
mirror: str = '', x: float = 0, y: float = 0, x2: float = 0, y2: float = 0, mirror: str = '', x: float = 0, y: float = 0, x2: float = 0, y2: float = 0,

View File

@@ -1,15 +1,38 @@
import copy
import logging import logging
import random
from pathlib import Path
from typing import Optional
import pyray as ray import pyray as ray
from libs.animation import Animation
from libs.audio import audio from libs.audio import audio
from libs.global_data import global_data from libs.background import Background
from libs.utils import get_current_ms from libs.chara_2d import Chara2D
from scenes.game import GameScreen from libs.global_data import Difficulty, Modifiers, PlayerNum, global_data
from libs.global_objects import Nameplate
from libs.texture import tex
from libs.tja import Note, TJAParser
from libs.utils import get_current_ms, global_tex
from scenes.game import (
DrumType,
GameScreen,
Gauge,
Player,
Side,
SongInfo,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AIBattleGameScreen(GameScreen): class AIBattleGameScreen(GameScreen):
def on_screen_start(self):
super().on_screen_start()
session_data = global_data.session_data[global_data.player_num]
self.song_info = SongInfoAI(session_data.song_title, session_data.genre_index)
self.background = AIBackground(session_data.selected_difficulty)
def global_keys(self): def global_keys(self):
if ray.is_key_pressed(global_data.config["keys"]["restart_key"]): if ray.is_key_pressed(global_data.config["keys"]["restart_key"]):
if self.song_music is not None: if self.song_music is not None:
@@ -26,11 +49,64 @@ class AIBattleGameScreen(GameScreen):
if ray.is_key_pressed(global_data.config["keys"]["pause_key"]): if ray.is_key_pressed(global_data.config["keys"]["pause_key"]):
self.pause_song() self.pause_song()
def load_hitsounds(self):
"""Load the hit sounds"""
sounds_dir = Path(f"Skins/{global_data.config["paths"]["skin"]}/Sounds")
# Load hitsounds for 1P
if global_data.hit_sound[global_data.player_num] == -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")
elif global_data.hit_sound[global_data.player_num] == 0:
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[global_data.player_num]) / "don.wav", 'hitsound_don_1p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[global_data.player_num]) / "ka.wav", 'hitsound_kat_1p')
logger.info("Loaded wav hit sounds for 1P")
else:
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[global_data.player_num]) / "don.ogg", 'hitsound_don_1p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[global_data.player_num]) / "ka.ogg", 'hitsound_kat_1p')
logger.info("Loaded ogg hit sounds for 1P")
audio.set_sound_pan('hitsound_don_1p', 0.0)
audio.set_sound_pan('hitsound_kat_1p', 0.0)
# Load hitsounds for 2P
if global_data.hit_sound[global_data.player_num] == -1:
audio.load_sound(Path('none.wav'), 'hitsound_don_5p')
audio.load_sound(Path('none.wav'), 'hitsound_kat_5p')
logger.info("Loaded default (none) hit sounds for 5P")
elif global_data.hit_sound[global_data.player_num] == 0:
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[global_data.player_num]) / "don_2p.wav", 'hitsound_don_5p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[global_data.player_num]) / "ka_2p.wav", 'hitsound_kat_5p')
logger.info("Loaded wav hit sounds for 5P")
else:
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[global_data.player_num]) / "don.ogg", 'hitsound_don_5p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[global_data.player_num]) / "ka.ogg", 'hitsound_kat_5p')
logger.info("Loaded ogg hit sounds for 5P")
audio.set_sound_pan('hitsound_don_5p', 1.0)
audio.set_sound_pan('hitsound_kat_5p', 1.0)
def init_tja(self, song: Path):
"""Initialize the TJA file"""
self.tja = TJAParser(song, start_delay=self.start_delay)
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')
tja_copy = copy.deepcopy(self.tja)
self.player_1 = PlayerNoChara(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.player_1.gauge = AIGauge(self.player_1.player_num, self.player_1.difficulty, self.tja.metadata.course_data[self.player_1.difficulty].level, self.player_1.total_notes, self.player_1.is_2p)
ai_modifiers = copy.deepcopy(global_data.modifiers[global_data.player_num])
ai_modifiers.auto = True
self.player_2 = AIPlayer(tja_copy, PlayerNum.AI, global_data.session_data[global_data.player_num].selected_difficulty, True, ai_modifiers)
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)
self.total_notes = len(self.player_1.don_notes) + len(self.player_1.kat_notes)
logger.info(f"TJA initialized for two-player song: {song}")
def update(self): def update(self):
super(GameScreen, self).update() super(GameScreen, self).update()
current_time = get_current_ms() current_time = get_current_ms()
self.transition.update(current_time) self.transition.update(current_time)
if not self.paused:
self.current_ms = current_time - self.start_ms self.current_ms = current_time - self.start_ms
if self.transition.is_finished: if self.transition.is_finished:
self.start_song(self.current_ms) self.start_song(self.current_ms)
@@ -41,17 +117,24 @@ class AIBattleGameScreen(GameScreen):
if self.song_music is not None: if self.song_music is not None:
audio.update_music_stream(self.song_music) audio.update_music_stream(self.song_music)
self.player_1.update(self.current_ms, current_time, self.background) self.player_1.update(self.current_ms, current_time, None)
self.player_2.update(self.current_ms, current_time, None)
section_notes = self.total_notes // 5
if self.player_1.good_count + self.player_1.ok_count + self.player_1.bad_count == section_notes:
self.player_2.good_percentage = self.player_1.good_count / section_notes
self.player_2.ok_percentage = self.player_1.ok_count / section_notes
logger.info(f"AI Good Percentage: {self.player_2.good_percentage}, AI OK Percentage: {self.player_2.ok_percentage}")
self.player_1.good_count, self.player_1.ok_count, self.player_1.bad_count = 0, 0, 0
self.player_2.good_count, self.player_2.ok_count, self.player_2.bad_count = 0, 0, 0
self.song_info.update(current_time) self.song_info.update(current_time)
self.result_transition.update(current_time) self.result_transition.update(current_time)
if self.result_transition.is_finished and not audio.is_sound_playing('result_transition'): 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('AI_SELECT') return self.on_screen_end('AI_SELECT')
elif self.current_ms >= self.player_1.end_time: elif self.current_ms >= self.player_1.end_time:
session_data = global_data.session_data[global_data.player_num] session_data = global_data.session_data[PlayerNum.P1]
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() 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 = int(self.player_1.gauge.gauge_length)
session_data.result_data.gauge_length = self.player_1.gauge.gauge_length
if self.end_ms != 0: if self.end_ms != 0:
if current_time >= self.end_ms + 1000: if current_time >= self.end_ms + 1000:
if self.player_1.ending_anim is None: if self.player_1.ending_anim is None:
@@ -60,8 +143,321 @@ class AIBattleGameScreen(GameScreen):
if not self.result_transition.is_started: if not self.result_transition.is_started:
self.result_transition.start() self.result_transition.start()
audio.play_sound('result_transition', 'voice') audio.play_sound('result_transition', 'voice')
logger.info("Result transition started and voice played")
else: else:
self.end_ms = current_time self.end_ms = current_time
return self.global_keys() return self.global_keys()
def update_background(self, current_time):
self.background.update(current_time, (self.player_1.good_count, self.player_1.ok_count), (self.player_2.good_count, self.player_2.ok_count))
def draw(self):
if self.movie is not None:
self.movie.draw()
elif self.background is not None:
self.background.draw(self.player_1.chara, self.player_2.chara)
self.player_1.draw(self.current_ms, self.start_ms, self.mask_shader)
self.player_2.draw(self.current_ms, self.start_ms, self.mask_shader)
self.draw_overlay()
class PlayerNoChara(Player):
def __init__(self, tja: TJAParser, player_num: PlayerNum, difficulty: int, is_2p: bool, modifiers: Modifiers):
super().__init__(tja, player_num, difficulty, is_2p, modifiers)
self.stretch_animation = [tex.get_animation(5, is_copy=True) for _ in range(4)]
def update(self, ms_from_start: float, current_time: float, background: Optional[Background]):
good_count, ok_count, bad_count, total_drumroll = self.good_count, self.ok_count, self.bad_count, self.total_drumroll
super().update(ms_from_start, current_time, background)
for ani in self.stretch_animation:
ani.update(current_time)
if good_count != self.good_count:
self.stretch_animation[0].start()
if ok_count != self.ok_count:
self.stretch_animation[1].start()
if bad_count != self.bad_count:
self.stretch_animation[2].start()
if total_drumroll != self.total_drumroll:
self.stretch_animation[3].start()
def draw_overlays(self, mask_shader: ray.Shader):
tex.draw_texture('lane', f'{self.player_num}p_lane_cover', index=self.is_2p)
tex.draw_texture('lane', 'drum', index=self.is_2p)
if self.ending_anim is not None:
self.ending_anim.draw()
for anim in self.draw_drum_hit_list:
anim.draw()
for anim in self.draw_arc_list:
anim.draw(mask_shader)
# Group 6: UI overlays
self.combo_display.draw()
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)
tex.draw_texture('lane', 'lane_difficulty', frame=self.difficulty, index=self.is_2p)
# Group 7: Player-specific elements
if self.modifiers.auto:
tex.draw_texture('lane', 'auto_icon', index=self.is_2p)
else:
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)
self.draw_modifiers()
tex.draw_texture('ai_battle', 'scoreboard')
for j, counter in enumerate([self.good_count, self.ok_count, self.bad_count, self.total_drumroll]):
margin = tex.textures["ai_battle"]["scoreboard_num"].width//2
total_width = len(str(counter)) * margin
for i, digit in enumerate(str(counter)):
tex.draw_texture('ai_battle', 'scoreboard_num', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation[j].attribute, y2=self.stretch_animation[j].attribute, index=j, controllable=True)
# 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()
class AIPlayer(Player):
def __init__(self, tja: TJAParser, player_num: PlayerNum, difficulty: int, is_2p: bool, modifiers: Modifiers):
super().__init__(tja, player_num, difficulty, is_2p, modifiers)
self.stretch_animation = [tex.get_animation(5, is_copy=True) for _ in range(4)]
self.chara = Chara2D(player_num - 1, self.bpm)
self.judge_counter = None
self.gauge = None
self.gauge_hit_effect = []
plate_info = global_data.config[f'nameplate_{self.is_2p+1}p']
self.nameplate = Nameplate(plate_info['name'], plate_info['title'], PlayerNum.AI, plate_info['dan'], plate_info['gold'], plate_info['rainbow'], plate_info['title_bg'])
self.good_percentage = 0.90
self.ok_percentage = 0.07
def update(self, ms_from_start: float, current_time: float, background: Optional[Background]):
good_count, ok_count, bad_count, total_drumroll = self.good_count, self.ok_count, self.bad_count, self.total_drumroll
super().update(ms_from_start, current_time, background)
for ani in self.stretch_animation:
ani.update(current_time)
if good_count != self.good_count:
self.stretch_animation[0].start()
if ok_count != self.ok_count:
self.stretch_animation[1].start()
if bad_count != self.bad_count:
self.stretch_animation[2].start()
if total_drumroll != self.total_drumroll:
self.stretch_animation[3].start()
def autoplay_manager(self, ms_from_start: float, current_time: float, background: Optional[Background]):
"""Manages autoplay behavior with randomized accuracy"""
if not self.modifiers.auto:
return
if self.is_drumroll or self.is_balloon:
if self.bpm == 0:
subdivision_in_ms = 0
else:
subdivision_in_ms = ms_from_start // ((60000 * 4 / self.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:
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
self._adjust_timing(self.don_notes, DrumType.DON, 'don',
ms_from_start, current_time, background,
good_window_ms, ok_window_ms, bad_window_ms)
self._adjust_timing(self.kat_notes, DrumType.KAT, 'kat',
ms_from_start, current_time, background,
good_window_ms, ok_window_ms, bad_window_ms)
def _adjust_timing(self, notes, hit_type, sound_type, ms_from_start,
current_time, background, good_window_ms, ok_window_ms, bad_window_ms):
"""Process autoplay for a specific note type"""
while notes and ms_from_start >= notes[0].hit_ms:
note = notes[0]
rand = random.random()
if rand < (self.good_percentage):
timing_offset = random.uniform(-good_window_ms * 0.5, good_window_ms * 0.5)
elif rand < (self.good_percentage + self.ok_percentage):
timing_offset = random.choice([
random.uniform(-ok_window_ms, -good_window_ms),
random.uniform(good_window_ms, ok_window_ms)
])
else:
timing_offset = random.choice([
random.uniform(-bad_window_ms * 1.5, -bad_window_ms),
random.uniform(bad_window_ms, bad_window_ms * 1.5)
])
adjusted_ms = note.hit_ms + timing_offset
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_{sound_type}_{self.player_num}p', 'hitsound')
self.check_note(adjusted_ms, hit_type, current_time, background)
def draw_overlays(self, mask_shader: ray.Shader):
# Group 4: Lane covers and UI elements (batch similar textures)
tex.draw_texture('lane', 'ai_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)
# Group 6: UI overlays
self.combo_display.draw()
if self.judge_counter is not None:
self.judge_counter.draw()
# Group 7: Player-specific elements
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)
tex.draw_texture('ai_battle', 'scoreboard_ai')
for j, counter in enumerate([self.good_count, self.ok_count, self.bad_count, self.total_drumroll]):
margin = tex.textures["ai_battle"]["scoreboard_num"].width//2
total_width = len(str(counter)) * margin
for i, digit in enumerate(str(counter)):
tex.draw_texture('ai_battle', 'scoreboard_num', frame=int(digit), x=-(total_width // 2) + (i * margin), y=-self.stretch_animation[j].attribute, y2=self.stretch_animation[j].attribute, index=j+4, controllable=True)
# 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()
class AIGauge(Gauge):
def draw(self):
scale = 0.5
x, y = 10 * tex.screen_scale, 15 * tex.screen_scale
tex.draw_texture('gauge_ai', f'{self.player_num}p_unfilled' + self.string_diff, scale=scale, x=x, y=y)
gauge_length = int(self.gauge_length)
clear_point = self.clear_start[self.difficulty]
bar_width = tex.textures["gauge_ai"][f"{self.player_num}p_bar"].width * scale
tex.draw_texture('gauge_ai', f'{self.player_num}p_bar', x2=min(gauge_length*bar_width, (clear_point - 1)*bar_width)-bar_width, scale=scale, x=x, y=y)
if gauge_length >= clear_point - 1:
tex.draw_texture('gauge_ai', 'bar_clear_transition', x=((clear_point - 1)*bar_width)+x, scale=scale, y=y)
if gauge_length > clear_point:
tex.draw_texture('gauge_ai', 'bar_clear_top', x=((clear_point) * bar_width)+x, x2=(gauge_length-clear_point)*bar_width, scale=scale, y=y)
tex.draw_texture('gauge_ai', 'bar_clear_bottom', x=((clear_point) * bar_width)+x, x2=(gauge_length-clear_point)*bar_width, scale=scale, y=y)
# 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_ai', 'rainbow' + self.string_diff, frame=self.rainbow_animation.attribute-1, fade=self.rainbow_fade_in.attribute, scale=scale, x=x, y=y)
tex.draw_texture('gauge_ai', 'rainbow' + self.string_diff, frame=self.rainbow_animation.attribute, fade=self.rainbow_fade_in.attribute, scale=scale, x=x, y=y)
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_ai', 'bar_clear_transition_fade', x=(gauge_length*bar_width)+x, fade=self.gauge_update_anim.attribute, scale=scale, y=y)
elif gauge_length > self.clear_start[self.difficulty]:
tex.draw_texture('gauge_ai', 'bar_clear_fade', x=(gauge_length*bar_width)+x, fade=self.gauge_update_anim.attribute, scale=scale, y=y)
else:
tex.draw_texture('gauge_ai', f'{self.player_num}p_bar_fade', x=(gauge_length*bar_width)+x, fade=self.gauge_update_anim.attribute, scale=scale, y=y)
tex.draw_texture('gauge_ai', 'overlay' + self.string_diff, fade=0.15, scale=scale, x=x, y=y)
# Draw clear status indicators
tex.draw_texture('gauge_ai', 'footer', scale=scale, x=x, y=y)
if gauge_length >= clear_point-1:
tex.draw_texture('gauge_ai', 'clear', index=min(2, self.difficulty), scale=scale, x=x, y=y)
if self.is_rainbow:
tex.draw_texture('gauge_ai', 'tamashii_fire', scale=0.75 * scale, center=True, frame=self.tamashii_fire_change.attribute, index=self.is_2p)
tex.draw_texture('gauge_ai', 'tamashii', scale=scale, x=x, y=y)
if self.is_rainbow and self.tamashii_fire_change.attribute in (0, 1, 4, 5):
tex.draw_texture('gauge_ai', 'tamashii_overlay', fade=0.5, scale=scale, x=x, y=y)
else:
tex.draw_texture('gauge_ai', 'clear_dark', index=min(2, self.difficulty), scale=scale, x=x, y=y)
tex.draw_texture('gauge_ai', 'tamashii_dark', scale=scale, x=x, y=y)
class SongInfoAI(SongInfo):
"""Displays the song name and genre"""
def draw(self):
y = 600 * tex.screen_scale
tex.draw_texture('song_info', 'song_num', fade=self.fade.attribute, frame=global_data.songs_played % 4, y=y)
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+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, y=y)
class AIBackground:
def __init__(self, difficulty: int):
self.contest_point = 10
self.total_tiles = 19
self.difference = 0
self.difficulty = min(difficulty, 3)
self.multipliers = [
[5, 3],
[5, 3],
[3, 2],
[3, 1]
]
self.contest_point_fade = Animation.create_fade(166, initial_opacity=0.0, final_opacity=1.0, reverse_delay=166, delay=166, loop=True)
self.contest_point_fade.start()
def update(self, current_ms: float, player_judge: tuple[int, int], ai_judge: tuple[int, int]):
self.contest_point_fade.update(current_ms)
player_total = (player_judge[0] * self.multipliers[self.difficulty][0]) + (player_judge[1] * self.multipliers[self.difficulty][1])
ai_total = (ai_judge[0] * self.multipliers[self.difficulty][0]) + (ai_judge[1] * self.multipliers[self.difficulty][1])
self.contest_point = player_total - ai_total + 10
self.contest_point = min(max(1, self.contest_point), self.total_tiles - 1)
def unload(self):
pass
def draw_lower(self):
tex.draw_texture('ai_battle', 'bg_lower')
tile_width = tex.textures['ai_battle']['red_tile_lower'].width
for i in range(self.contest_point):
tex.draw_texture('ai_battle', 'red_tile_lower', frame=i, x=(i*tile_width))
for i in range(self.total_tiles - self.contest_point):
tex.draw_texture('ai_battle', 'blue_tile_lower', frame=i, x=(((self.total_tiles - 1) - i)*tile_width))
tex.draw_texture('ai_battle', 'highlight_tile_lower', x=self.contest_point * tile_width, fade=self.contest_point_fade.attribute)
def draw_upper(self, chara_1: Chara2D, chara_2: Chara2D):
tex.draw_texture('ai_battle', 'bg_upper')
for i in range(self.contest_point):
tex.draw_texture('ai_battle', 'red_tile_upper', frame=i, index=i)
for i in range(self.total_tiles - self.contest_point):
tex.draw_texture('ai_battle', 'blue_tile_upper', frame=i, index=(self.total_tiles - 1) - i)
tex.draw_texture('ai_battle', 'bg_outline_upper')
if self.contest_point > 9:
frame = self.total_tiles - self.contest_point
mirror = 'horizontal'
else:
frame = self.contest_point - 1
mirror = ''
tex.draw_texture('ai_battle', 'highlight_tile_upper', frame=frame, index=self.contest_point-1, mirror=mirror, fade=self.contest_point_fade.attribute)
tile_width = tex.textures['ai_battle']['red_tile_lower'].width
offset = 60
chara_1.draw(x=tile_width*self.contest_point - (global_tex.textures['chara_0']['normal'].width//2) - offset, y=40, scale=0.5)
chara_2.draw(x=tile_width*self.contest_point - (global_tex.textures['chara_4']['normal'].width//2) + offset*1.3, y=40, scale=0.5, mirror=True)
def draw(self, chara_1: Chara2D, chara_2: Chara2D):
self.draw_lower()
self.draw_upper(chara_1, chara_2)

View File

@@ -1282,21 +1282,21 @@ class Player:
modifiers_to_draw.append('mod_shinuchi') modifiers_to_draw.append('mod_shinuchi')
# Speed modifiers # Speed modifiers
if global_data.modifiers[self.player_num].speed >= 4: if self.modifiers.speed >= 4:
modifiers_to_draw.append('mod_yonbai') modifiers_to_draw.append('mod_yonbai')
elif global_data.modifiers[self.player_num].speed >= 3: elif self.modifiers.speed >= 3:
modifiers_to_draw.append('mod_sanbai') modifiers_to_draw.append('mod_sanbai')
elif global_data.modifiers[self.player_num].speed > 1: elif self.modifiers.speed > 1:
modifiers_to_draw.append('mod_baisaku') modifiers_to_draw.append('mod_baisaku')
# Other modifiers # Other modifiers
if global_data.modifiers[self.player_num].display: if self.modifiers.display:
modifiers_to_draw.append('mod_doron') modifiers_to_draw.append('mod_doron')
if global_data.modifiers[self.player_num].inverse: if self.modifiers.inverse:
modifiers_to_draw.append('mod_abekobe') modifiers_to_draw.append('mod_abekobe')
if global_data.modifiers[self.player_num].random == 2: if self.modifiers.random == 2:
modifiers_to_draw.append('mod_detarame') modifiers_to_draw.append('mod_detarame')
elif global_data.modifiers[self.player_num].random == 1: elif self.modifiers.random == 1:
modifiers_to_draw.append('mod_kimagure') modifiers_to_draw.append('mod_kimagure')
# Draw all modifiers in one batch # Draw all modifiers in one batch
@@ -1336,13 +1336,13 @@ class Player:
self.judge_counter.draw() self.judge_counter.draw()
# Group 7: Player-specific elements # Group 7: Player-specific elements
if not self.modifiers.auto: if self.modifiers.auto:
tex.draw_texture('lane', 'auto_icon', index=self.is_2p)
else:
if self.is_2p: if self.is_2p:
self.nameplate.draw(tex.skin_config["game_nameplate_1p"].x, tex.skin_config["game_nameplate_1p"].y) self.nameplate.draw(tex.skin_config["game_nameplate_1p"].x, tex.skin_config["game_nameplate_1p"].y)
else: else:
self.nameplate.draw(tex.skin_config["game_nameplate_2p"].x, tex.skin_config["game_nameplate_2p"].y) 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.draw_modifiers()
self.chara.draw(y=(self.is_2p*tex.skin_config["game_2p_offset"].y)) self.chara.draw(y=(self.is_2p*tex.skin_config["game_2p_offset"].y))
@@ -1360,6 +1360,8 @@ class Player:
def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader, dan_transition = None): def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader, dan_transition = None):
# Group 1: Background and lane elements # Group 1: Background and lane elements
tex.draw_texture('lane', 'lane_background', index=self.is_2p) tex.draw_texture('lane', 'lane_background', index=self.is_2p)
if self.player_num == PlayerNum.AI:
tex.draw_texture('lane', 'ai_lane_background')
if self.branch_indicator is not None: if self.branch_indicator is not None:
self.branch_indicator.draw() self.branch_indicator.draw()
if self.gauge is not None: if self.gauge is not None:
@@ -1767,7 +1769,7 @@ class BalloonAnimation:
tex.draw_texture('balloon', 'pop', frame=7, color=self.color, y=self.is_2p*tex.skin_config["2p_offset"].y) 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: elif self.balloon_count >= 1:
balloon_index = min(6, (self.balloon_count - 1) * 6 // self.balloon_total) 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) tex.draw_texture('balloon', 'pop', frame=balloon_index, color=self.color, index=self.is_2p, y=self.is_2p*tex.skin_config["2p_offset"].y)
if self.balloon_count > 0: 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 '') 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)) counter = str(max(0, self.balloon_total - self.balloon_count + 1))