diff --git a/.gitignore b/.gitignore index 8715492..7583399 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ Songs -__pycache__ \ No newline at end of file +__pycache__ +.venv +.ruff_cache +scores.db diff --git a/PyTaiko.py b/PyTaiko.py index 606d70b..9a89621 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -1,3 +1,5 @@ +import sqlite3 + import pyray as ray from libs.audio import audio @@ -16,7 +18,29 @@ class Screens: GAME = "GAME" RESULT = "RESULT" +def create_song_db(): + with sqlite3.connect('scores.db') as con: + cursor = con.cursor() + create_table_query = ''' + CREATE TABLE IF NOT EXISTS Scores ( + hash TEXT PRIMARY KEY, + en_name TEXT NOT NULL, + jp_name TEXT NOT NULL, + diff INTEGER, + score INTEGER, + good INTEGER, + ok INTEGER, + bad INTEGER, + drumroll INTEGER, + combo INTEGER + ); + ''' + cursor.execute(create_table_query) + con.commit() + print("Scores database created successfully") + def main(): + create_song_db() screen_width: int = get_config()["video"]["screen_width"] screen_height: int = get_config()["video"]["screen_height"] render_width, render_height = ray.get_render_width(), ray.get_render_height() diff --git a/config.toml b/config.toml index ca3330d..3bc1db0 100644 --- a/config.toml +++ b/config.toml @@ -14,7 +14,7 @@ right_don = ['J'] right_kat = ['K'] [audio] -device_type = 'ASIO' +device_type = 'WASAPI' asio_buffer = 6 [video] diff --git a/libs/animation.py b/libs/animation.py index 8447839..d3d68d6 100644 --- a/libs/animation.py +++ b/libs/animation.py @@ -86,8 +86,10 @@ class FadeAnimation(BaseAnimation): class MoveAnimation(BaseAnimation): def __init__(self, duration: float, total_distance: int = 0, start_position: int = 0, delay: float = 0.0, + reverse_delay: Optional[float] = None, ease_in: Optional[str] = None, ease_out: Optional[str] = None): super().__init__(duration, delay) + self.reverse_delay = reverse_delay self.total_distance = total_distance self.start_position = start_position self.ease_in = ease_in @@ -100,7 +102,14 @@ class MoveAnimation(BaseAnimation): elif elapsed_time >= self.delay + self.duration: self.attribute = self.start_position + self.total_distance - self.is_finished = True + if self.reverse_delay is not None: + self.start_ms = current_time_ms + self.delay = self.reverse_delay + self.start_position = self.start_position + self.total_distance + self.total_distance = -(self.total_distance) + self.reverse_delay = None + else: + self.is_finished = True else: progress = (elapsed_time - self.delay) / self.duration progress = self._apply_easing(progress, self.ease_in, self.ease_out) @@ -194,6 +203,7 @@ class Animation: duration: Length of the move in milliseconds start_position: The coordinates of the object before the move total_distance: The distance travelled from the start to end position + reverse_delay: If provided, move will play in reverse after this delay delay: Time to wait before starting the move ease_in: Control ease into the move ease_out: Control ease out of the move diff --git a/libs/tja.py b/libs/tja.py index 8564e44..bf4ee4b 100644 --- a/libs/tja.py +++ b/libs/tja.py @@ -1,3 +1,4 @@ +import hashlib import math from collections import deque from dataclasses import dataclass, field, fields @@ -375,6 +376,10 @@ class TJAParser: note = Balloon(note) note.count = int(balloon[balloon_index]) balloon_index += 1 + elif item == '8': + new_pixels_per_ms = play_note_list[-1].pixels_per_frame / (1000 / 60) + note.load_ms = note.hit_ms - (self.distance / new_pixels_per_ms) + note.pixels_per_frame = play_note_list[-1].pixels_per_frame self.current_ms += increment play_note_list.append(note) self.get_moji(play_note_list, ms_per_measure) @@ -389,3 +394,10 @@ class TJAParser: draw_note_list = deque(sorted(play_note_list, key=lambda n: n.load_ms)) bar_list = deque(sorted(bar_list, key=lambda b: b.load_ms)) return play_note_list, draw_note_list, bar_list + + def hash_note_data(self, notes: list): + n = hashlib.sha256() + for bar in notes: + for part in bar: + n.update(part.encode('utf-8')) + return n.hexdigest() diff --git a/scenes/game.py b/scenes/game.py index ae12641..0a75a53 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -1,6 +1,6 @@ import bisect import math -import random +import sqlite3 from pathlib import Path from typing import Optional @@ -8,9 +8,11 @@ import pyray as ray from libs.animation import Animation from libs.audio import audio +from libs.backgrounds import Background from libs.tja import Balloon, Drumroll, Note, TJAParser, calculate_base_score from libs.utils import ( OutlinedText, + draw_scaled_texture, get_config, get_current_ms, global_data, @@ -36,6 +38,8 @@ class GameScreen: self.start_delay = 1000 self.song_started = False + self.background = Background(width, height) + def load_textures(self): self.textures = load_all_textures_from_zip(Path('Graphics/lumendata/enso_system/common.zip')) zip_file = Path('Graphics/lumendata/enso_system/common.zip') @@ -77,25 +81,6 @@ class GameScreen: self.textures.update(load_all_textures_from_zip(Path('Graphics/lumendata/enso_system/base1p.zip'))) self.textures.update(load_all_textures_from_zip(Path('Graphics/lumendata/enso_system/don1p.zip'))) - self.bg_fever_name = 'bg_fever_a_' + str(random.randint(1, 4)).zfill(2) - self.bg_normal_name = 'bg_nomal_a_' + str(random.randint(1, 5)).zfill(2) - self.chibi_name = 'chibi_a_' + str(random.randint(1, 14)).zfill(2) - self.dance_name = 'dance_a_' + str(random.randint(1, 22)).zfill(2) - self.footer_name = 'dodai_a_' + str(random.randint(1, 3)).zfill(2) - self.donbg_name = 'donbg_a_' + str(random.randint(1, 6)).zfill(2) - self.fever_name = 'fever_a_' + str(random.randint(1, 4)).zfill(2) - self.renda_name = 'renda_a_' + str(random.randint(1, 3)).zfill(2) - - self.textures.update(load_all_textures_from_zip(Path(f'Graphics/lumendata/enso_original/{self.bg_fever_name}.zip'))) - self.textures.update(load_all_textures_from_zip(Path(f'Graphics/lumendata/enso_original/{self.bg_normal_name}.zip'))) - self.textures.update(load_all_textures_from_zip(Path(f'Graphics/lumendata/enso_original/{self.chibi_name}.zip'))) - self.textures.update(load_all_textures_from_zip(Path(f'Graphics/lumendata/enso_original/{self.dance_name}.zip'))) - self.textures.update(load_all_textures_from_zip(Path(f'Graphics/lumendata/enso_original/{self.footer_name}.zip'))) - self.textures.update(load_all_textures_from_zip(Path(f'Graphics/lumendata/enso_original/{self.donbg_name}_1p.zip'))) - self.textures.update(load_all_textures_from_zip(Path(f'Graphics/lumendata/enso_original/{self.donbg_name}_2p.zip'))) - self.textures.update(load_all_textures_from_zip(Path(f'Graphics/lumendata/enso_original/{self.fever_name}.zip'))) - self.textures.update(load_all_textures_from_zip(Path(f'Graphics/lumendata/enso_original/{self.renda_name}.zip'))) - self.result_transition_1 = load_texture_from_zip(Path('Graphics/lumendata/enso_result.zip'), 'retry_game_img00125.png') self.result_transition_2 = load_texture_from_zip(Path('Graphics/lumendata/enso_result.zip'), 'retry_game_img00126.png') @@ -154,6 +139,28 @@ class GameScreen: self.end_ms = 0 return 'RESULT' + def write_score(self): + if get_config()['general']['autoplay']: + return + with sqlite3.connect('scores.db') as con: + cursor = con.cursor() + hash = self.tja.hash_note_data(self.tja.data_to_notes(self.player_1.difficulty)[0]) + check_query = "SELECT score FROM Scores WHERE hash = ? LIMIT 1" + cursor.execute(check_query, (hash,)) + result = cursor.fetchone() + if result is None or session_data.result_score > result[0]: + insert_query = ''' + INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + ''' + data = (hash, self.tja.title, + self.tja.title_ja, self.player_1.difficulty, + session_data.result_score, session_data.result_good, + session_data.result_ok, session_data.result_bad, + session_data.result_total_drumroll, session_data.result_max_combo) + cursor.execute(insert_query, data) + con.commit() + def update(self): self.on_screen_start() self.current_ms = get_current_ms() - self.start_ms @@ -165,6 +172,8 @@ class GameScreen: self.song_started = True if self.movie is not None: self.movie.update() + else: + self.background.update(get_current_ms(), self.player_1.gauge.gauge_length > self.player_1.gauge.clear_start[min(self.player_1.difficulty, 3)]) self.player_1.update(self) if self.song_info is not None: @@ -176,6 +185,7 @@ class GameScreen: return self.on_screen_end() elif len(self.player_1.play_notes) == 0: session_data.result_score, session_data.result_good, session_data.result_ok, session_data.result_bad, session_data.result_max_combo, session_data.result_total_drumroll = self.player_1.get_result_score() + self.write_score() session_data.result_gauge_length = self.player_1.gauge.gauge_length if self.end_ms != 0: if get_current_ms() >= self.end_ms + 8533.34: @@ -184,25 +194,17 @@ class GameScreen: else: self.end_ms = get_current_ms() - def draw_background(self): - for i in range(0, self.width, self.textures[self.donbg_name + '_1p'][0].width): - ray.draw_texture(self.textures[self.donbg_name + '_1p'][0], i, 0, ray.WHITE) - ray.draw_texture(self.textures[self.bg_normal_name][0], 0, 360, ray.WHITE) - ray.draw_texture(self.textures[self.bg_normal_name][1], 0, 360, ray.fade(ray.WHITE, 0.25)) - ray.draw_texture(self.textures[self.footer_name][0], 0, self.height - self.textures[self.footer_name][0].height + 20, ray.WHITE) - def draw(self): if self.movie is not None: self.movie.draw() else: - self.draw_background() + self.background.draw() self.player_1.draw(self) if self.song_info is not None: self.song_info.draw(self) if self.result_transition is not None: self.result_transition.draw(self.width, self.height, self.result_transition_1, self.result_transition_2) - class Player: TIMING_GOOD = 25.0250015258789 TIMING_OK = 75.0750045776367 @@ -252,6 +254,7 @@ class Player: self.input_log: dict[float, tuple] = dict() self.gauge = Gauge(self.difficulty, metadata[-1][self.difficulty][0]) + self.gauge_hit_effect: list[GaugeHitEffect] = [] self.autoplay_hit_side = 'L' @@ -318,7 +321,7 @@ class Player: 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.index == current_note.index+1) + tail_note = next(note for note in self.draw_note_list if note.type == 8) 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: @@ -357,7 +360,8 @@ class Player: if self.combo > self.max_combo: self.max_combo = self.combo - self.draw_arc_list.append(NoteArc(note_type, get_current_ms(), self.player_number)) + self.draw_arc_list.append(NoteArc(note_type, get_current_ms(), self.player_number, note.type == 3 or note.type == 4) or note.type == 7) + #game_screen.background.chibis.append(game_screen.background.Chibi()) if note in self.current_notes_draw: index = self.current_notes_draw.index(note) @@ -365,7 +369,7 @@ class Player: def check_drumroll(self, game_screen: GameScreen, drum_type: int): note_type = game_screen.note_type_list[drum_type][0] - self.draw_arc_list.append(NoteArc(note_type, get_current_ms(), self.player_number)) + self.draw_arc_list.append(NoteArc(note_type, get_current_ms(), self.player_number, drum_type == 3 or drum_type == 4)) self.curr_drumroll_count += 1 self.total_drumroll += 1 self.score += 100 @@ -511,7 +515,12 @@ class Player: if self.lane_hit_effect is not None: self.lane_hit_effect.update(get_current_ms()) self.animation_manager(self.draw_drum_hit_list) - self.animation_manager(self.draw_arc_list) + for anim in self.draw_arc_list: + anim.update(get_current_ms()) + if anim.is_finished: + self.gauge_hit_effect.append(GaugeHitEffect(anim.texture, anim.is_big)) + self.draw_arc_list.remove(anim) + self.animation_manager(self.gauge_hit_effect) self.animation_manager(self.base_score_list) self.score_counter.update(get_current_ms(), self.score) self.autoplay_manager(game_screen) @@ -625,6 +634,8 @@ class Player: self.drumroll_counter.draw(game_screen) for anim in self.draw_arc_list: anim.draw(game_screen) + for anim in self.gauge_hit_effect: + anim.draw(game_screen) if self.balloon_anim is not None: self.balloon_anim.draw(game_screen) self.score_counter.draw(game_screen) @@ -725,9 +736,58 @@ class DrumHitEffect: elif self.side == 'R': ray.draw_texture(game_screen.textures['lane_obi'][17], x, y, self.color) +class GaugeHitEffect: + def __init__(self, note_texture: ray.Texture, big: bool): + self.note_texture = note_texture + self.is_big = big + self.texture_change = Animation.create_texture_change(116.67, textures=[(0, 33.33, 1), (33.33, 66.66, 2), (66.66, float('inf'), 3)]) + self.circle_fadein = Animation.create_fade(133, initial_opacity=0.0, final_opacity=1.0, delay=16.67) + self.resize = Animation.create_texture_resize(233, delay=self.texture_change.duration, initial_size=0.75, final_size=1.15) + self.fade_out = Animation.create_fade(66, delay=233) + self.test = Animation.create_fade(300, delay=116.67, initial_opacity=0.0, final_opacity=1.0) + self.color = ray.fade(ray.YELLOW, self.circle_fadein.attribute) + self.is_finished = False + def update(self, current_ms): + self.texture_change.update(current_ms) + self.circle_fadein.update(current_ms) + self.fade_out.update(current_ms) + self.resize.update(current_ms) + self.test.update(current_ms) + color = ray.YELLOW + if self.circle_fadein.is_finished: + color = ray.WHITE + self.color = ray.fade(color, min(self.fade_out.attribute, self.circle_fadein.attribute)) + if self.fade_out.is_finished: + self.is_finished = True + def draw(self, game_screen): + texture = game_screen.textures['onp_kiseki_don_1p'][self.texture_change.attribute] + color_map = {0.70: ray.WHITE, 0.80: ray.YELLOW, 0.90: ray.ORANGE, 1.00: ray.RED} + texture_color = ray.WHITE + for upper_bound, color in color_map.items(): + lower_bound = list(color_map.keys())[list(color_map.keys()).index(upper_bound) - 1] if list(color_map.keys()).index(upper_bound) > 0 else 0.70 + + if lower_bound <= self.resize.attribute <= upper_bound: + texture_color = color + elif self.resize.attribute >= upper_bound: + texture_color = ray.RED + original_x = 1223 + original_y = 164 + source = ray.Rectangle(0, 0, texture.width, texture.height) + dest_width = texture.width * self.resize.attribute + dest_height = texture.height * self.resize.attribute + dest = ray.Rectangle(original_x, original_y, dest_width, dest_height) + origin = ray.Vector2(dest_width / 2, dest_height / 2) + ray.draw_texture_pro(texture, source, dest, origin, self.test.attribute*100, ray.fade(texture_color, self.fade_out.attribute)) + ray.draw_texture(self.note_texture, 1187 - 29, 130 - 29, ray.fade(ray.WHITE, self.fade_out.attribute)) + if self.is_big: + ray.draw_texture(game_screen.textures['onp_kiseki_don_1p'][20], 1187 - 29, 130 - 29, self.color) + else: + ray.draw_texture(game_screen.textures['onp_kiseki_don_1p'][4], 1187 - 29, 130 - 29, self.color) + class NoteArc: - def __init__(self, note_texture: ray.Texture, current_ms: float, player_number: int): + def __init__(self, note_texture: ray.Texture, current_ms: float, player_number: int, big: bool): self.texture = note_texture + self.is_big = big self.arc_points = 22 self.create_ms = current_ms self.player_number = player_number @@ -886,7 +946,7 @@ class BalloonAnimation: if self.is_popped: ray.draw_texture(game_screen.textures['action_fusen_1p'][18], 460, 130, self.color) elif self.balloon_count >= 1: - balloon_index = (self.balloon_count - 1) * 7 // self.balloon_total + balloon_index = min(7, (self.balloon_count - 1) * 7 // self.balloon_total) ray.draw_texture(game_screen.textures['action_fusen_1p'][balloon_index+11], 460, 130, self.color) if self.balloon_count > 0: ray.draw_texture(game_screen.textures['action_fusen_1p'][0], 414, 40, ray.WHITE) @@ -1050,9 +1110,6 @@ class SongInfo: DISPLAY_DURATION = 1666 def __init__(self, song_name: str, genre: str): - self.fade_in = Animation.create_fade(self.FADE_DURATION, initial_opacity=0.0, final_opacity=1.0) - self.fade_out = Animation.create_fade(self.FADE_DURATION, delay=self.FADE_DURATION + self.DISPLAY_DURATION) - self.song_name = song_name self.genre = genre @@ -1060,6 +1117,9 @@ class SongInfo: self.song_title = OutlinedText( self.font, song_name, 40, ray.WHITE, ray.BLACK, outline_thickness=5 ) + self.fade_in = Animation.create_fade(self.FADE_DURATION, initial_opacity=0.0, final_opacity=1.0) + self.fade_out = Animation.create_fade(self.FADE_DURATION, delay=self.DISPLAY_DURATION) + self.fade_fake = Animation.create_fade(0, delay=self.DISPLAY_DURATION*2 + self.FADE_DURATION) def _load_font_for_text(self, text: str) -> ray.Font: codepoint_count = ray.ffi.new('int *', 0) @@ -1070,6 +1130,7 @@ class SongInfo: def update(self, current_ms: float): self.fade_in.update(current_ms) self.fade_out.update(current_ms) + self.fade_fake.update(current_ms) if not self.fade_in.is_finished: self.song_num_fade = ray.fade(ray.WHITE, self.fade_in.attribute) @@ -1078,12 +1139,13 @@ class SongInfo: self.song_num_fade = ray.fade(ray.WHITE, self.fade_out.attribute) self.song_name_fade = ray.fade(ray.WHITE, 1 - self.fade_out.attribute) - if self.fade_out.is_finished: - self._reset_animations() + if self.fade_fake.is_finished: + self._reset_animations(current_ms) - def _reset_animations(self): + def _reset_animations(self, current_ms: float): self.fade_in = Animation.create_fade(self.FADE_DURATION, initial_opacity=0.0, final_opacity=1.0) - self.fade_out = Animation.create_fade(self.FADE_DURATION, delay=self.FADE_DURATION + self.DISPLAY_DURATION) + self.fade_out = Animation.create_fade(self.FADE_DURATION, delay=self.DISPLAY_DURATION) + self.fade_fake = Animation.create_fade(0, delay=self.DISPLAY_DURATION*2 + self.FADE_DURATION) def draw(self, game_screen: GameScreen): song_texture_index = (global_data.songs_played % 4) + 8