mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 03:30:13 +01:00
Compare commits
25 Commits
v1.1
...
7ef74a601b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ef74a601b | ||
|
|
102d82001f | ||
|
|
f278868a83 | ||
|
|
ac7c7abf82 | ||
|
|
f384de454f | ||
|
|
e5f788f30c | ||
|
|
b4fafa96b6 | ||
|
|
8e5f485734 | ||
|
|
1d39a4a373 | ||
|
|
775e603d4c | ||
|
|
90412af455 | ||
|
|
9f905c669d | ||
|
|
d88c671e63 | ||
|
|
b1f9c4f2ac | ||
|
|
7ca4050f1b | ||
|
|
9055505eb6 | ||
|
|
0fca2e5f3f | ||
|
|
83f376c1a7 | ||
|
|
65abde116e | ||
|
|
4ec426c34e | ||
|
|
a21ea9b7bc | ||
|
|
c36be89728 | ||
|
|
109719b7f5 | ||
|
|
7afb1da1cd | ||
|
|
fbcd181667 |
27
.github/workflows/python-app.yml
vendored
27
.github/workflows/python-app.yml
vendored
@@ -84,6 +84,33 @@ jobs:
|
||||
- name: Setup Python
|
||||
run: uv python install
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Copy libaudio to project root (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
cp libs/audio/*.dll . 2>/dev/null || echo "libaudio not found"
|
||||
|
||||
- name: Copy libaudio to project root (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
cp libs/audio/libaudio.dylib . 2>/dev/null || echo "libaudio not found"
|
||||
|
||||
- name: Copy libaudio to project root (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
cp libs/audio/libaudio.so . 2>/dev/null || echo "libaudio not found"
|
||||
|
||||
- name: Run tests (excluding audio tests)
|
||||
run: uv run pytest test/libs/ -v --tb=short --color=yes --ignore=test/libs/test_audio.py
|
||||
continue-on-error: false
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
|
||||
- name: Build Executable
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
129
.github/workflows/tests.yml
vendored
Normal file
129
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, develop]
|
||||
pull_request:
|
||||
branches: [main, master, develop]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest]
|
||||
python-version: ["3.12"]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Check-out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install libaudio Dependencies (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew update
|
||||
brew install portaudio libsndfile speexdsp ccache
|
||||
|
||||
- name: Install libaudio Dependencies (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
update: true
|
||||
install: >-
|
||||
base-devel
|
||||
mingw-w64-x86_64-gcc
|
||||
mingw-w64-x86_64-libsndfile
|
||||
mingw-w64-x86_64-speexdsp
|
||||
mingw-w64-x86_64-ccache
|
||||
|
||||
- name: Install libaudio Dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libsndfile1-dev \
|
||||
libspeexdsp-dev \
|
||||
portaudio19-dev \
|
||||
libpulse-dev \
|
||||
ccache
|
||||
|
||||
- name: Build libaudio (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
cd libs/audio
|
||||
make clean
|
||||
make all
|
||||
make verify
|
||||
|
||||
- name: Build libaudio (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
cd libs/audio
|
||||
make clean
|
||||
make all
|
||||
make verify
|
||||
|
||||
- name: Copy libaudio to project root (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
cp libs/audio/*.dll . 2>/dev/null || echo "libaudio not found"
|
||||
|
||||
- name: Copy libaudio to project root (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
cp libs/audio/libaudio.dylib . 2>/dev/null || echo "libaudio not found"
|
||||
|
||||
- name: Copy libaudio to project root (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
cp libs/audio/libaudio.so . 2>/dev/null || echo "libaudio not found"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Setup Python ${{ matrix.python-version }}
|
||||
run: uv python install ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Run tests (excluding audio tests)
|
||||
run: uv run pytest test/libs/ -v --tb=short --color=yes --ignore=test/libs/test_audio.py
|
||||
continue-on-error: false
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-${{ matrix.os }}-py${{ matrix.python-version }}
|
||||
path: |
|
||||
*.log
|
||||
temp/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
test-summary:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: always()
|
||||
steps:
|
||||
- name: Test Summary
|
||||
run: |
|
||||
echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All platform tests completed!" >> $GITHUB_STEP_SUMMARY
|
||||
214
PyTaiko.py
214
PyTaiko.py
@@ -5,7 +5,8 @@ import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pyray as ray
|
||||
import pyray
|
||||
import raylib as ray
|
||||
from pypresence.presence import Presence
|
||||
from raylib.defines import (
|
||||
RL_FUNC_ADD,
|
||||
@@ -26,6 +27,8 @@ from libs.utils import (
|
||||
global_data,
|
||||
global_tex,
|
||||
)
|
||||
from scenes.ai_battle.game import AIBattleGameScreen
|
||||
from scenes.ai_battle.song_select import AISongSelectScreen
|
||||
from scenes.dan.dan_result import DanResultScreen
|
||||
from scenes.dan.dan_select import DanSelectScreen
|
||||
from scenes.dan.game_dan import DanGameScreen
|
||||
@@ -67,6 +70,8 @@ class Screens:
|
||||
DAN_RESULT = "DAN_RESULT"
|
||||
PRACTICE_SELECT = "PRACTICE_SELECT"
|
||||
GAME_PRACTICE = "GAME_PRACTICE"
|
||||
AI_SELECT = "AI_SELECT"
|
||||
AI_GAME = "AI_GAME"
|
||||
SETTINGS = "SETTINGS"
|
||||
DEV_MENU = "DEV_MENU"
|
||||
LOADING = "LOADING"
|
||||
@@ -161,12 +166,12 @@ def create_song_db():
|
||||
|
||||
def update_camera_for_window_size(camera, virtual_width, virtual_height):
|
||||
"""Update camera zoom, offset, scale, and rotation to maintain aspect ratio"""
|
||||
screen_width = ray.get_screen_width()
|
||||
screen_height = ray.get_screen_height()
|
||||
screen_width = ray.GetScreenWidth()
|
||||
screen_height = ray.GetScreenHeight()
|
||||
|
||||
if screen_width == 0 or screen_height == 0:
|
||||
camera.zoom = 1.0
|
||||
camera.offset = ray.Vector2(0, 0)
|
||||
camera.offset = (0, 0)
|
||||
camera.rotation = 0.0
|
||||
return
|
||||
|
||||
@@ -186,21 +191,14 @@ def update_camera_for_window_size(camera, virtual_width, virtual_height):
|
||||
h_scale_offset_x = (virtual_width * scale * (h_scale - 1.0)) * 0.5
|
||||
v_scale_offset_y = (virtual_height * scale * (v_scale - 1.0)) * 0.5
|
||||
|
||||
camera.offset = ray.Vector2(
|
||||
camera.offset = (
|
||||
base_offset_x - zoom_offset_x - h_scale_offset_x + (global_data.camera.offset.x * scale),
|
||||
base_offset_y - zoom_offset_y - v_scale_offset_y + (global_data.camera.offset.y * scale)
|
||||
)
|
||||
|
||||
camera.rotation = global_data.camera.rotation
|
||||
|
||||
def main():
|
||||
force_dedicated_gpu()
|
||||
global_data.config = get_config()
|
||||
match global_data.config["general"]["score_method"]:
|
||||
case ScoreMethod.GEN3:
|
||||
global_data.score_db = 'scores_gen3.db'
|
||||
case ScoreMethod.SHINUCHI:
|
||||
global_data.score_db = 'scores.db'
|
||||
def setup_logging():
|
||||
log_level = global_data.config["general"]["log_level"]
|
||||
if sys.platform == 'win32':
|
||||
import io
|
||||
@@ -219,41 +217,33 @@ def main():
|
||||
handlers=[console_handler, file_handler]
|
||||
)
|
||||
sys.excepthook = handle_exception
|
||||
logger.info("Starting PyTaiko")
|
||||
|
||||
logger.debug(f"Loaded config: {global_data.config}")
|
||||
screen_width = global_tex.screen_width
|
||||
screen_height = global_tex.screen_height
|
||||
|
||||
def set_config_flags():
|
||||
if global_data.config["video"]["vsync"]:
|
||||
ray.set_config_flags(ray.ConfigFlags.FLAG_VSYNC_HINT)
|
||||
ray.SetConfigFlags(ray.FLAG_VSYNC_HINT)
|
||||
logger.info("VSync enabled")
|
||||
if global_data.config["video"]["target_fps"] != -1:
|
||||
ray.set_target_fps(global_data.config["video"]["target_fps"])
|
||||
ray.SetTargetFPS(global_data.config["video"]["target_fps"])
|
||||
logger.info(f"Target FPS set to {global_data.config['video']['target_fps']}")
|
||||
ray.set_config_flags(ray.ConfigFlags.FLAG_MSAA_4X_HINT)
|
||||
ray.set_config_flags(ray.ConfigFlags.FLAG_WINDOW_RESIZABLE)
|
||||
ray.set_trace_log_level(ray.TraceLogLevel.LOG_WARNING)
|
||||
ray.SetConfigFlags(ray.FLAG_MSAA_4X_HINT)
|
||||
ray.SetConfigFlags(ray.FLAG_WINDOW_RESIZABLE)
|
||||
ray.SetTraceLogLevel(ray.LOG_WARNING)
|
||||
|
||||
ray.init_window(screen_width, screen_height, "PyTaiko")
|
||||
logger.info(f"Window initialized: {screen_width}x{screen_height}")
|
||||
global_tex.load_screen_textures('global')
|
||||
logger.info("Global screen textures loaded")
|
||||
global_tex.load_zip('chara', 'chara_0')
|
||||
global_tex.load_zip('chara', 'chara_1')
|
||||
logger.info("Chara textures loaded")
|
||||
if global_data.config["video"]["borderless"]:
|
||||
ray.toggle_borderless_windowed()
|
||||
logger.info("Borderless window enabled")
|
||||
if global_data.config["video"]["fullscreen"]:
|
||||
ray.toggle_fullscreen()
|
||||
logger.info("Fullscreen enabled")
|
||||
|
||||
current_screen = Screens.LOADING
|
||||
def init_audio():
|
||||
audio.set_log_level((logger.level-1)//10)
|
||||
old_stderr = os.dup(2)
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, 2)
|
||||
os.close(devnull)
|
||||
audio.init_audio_device()
|
||||
os.dup2(old_stderr, 2)
|
||||
os.close(old_stderr)
|
||||
logger.info("Audio device initialized")
|
||||
|
||||
def check_args():
|
||||
if len(sys.argv) == 1:
|
||||
pass
|
||||
else:
|
||||
return Screens.LOADING
|
||||
|
||||
parser = argparse.ArgumentParser(description='Launch game with specified song file')
|
||||
parser.add_argument('song_path', type=str, help='Path to the TJA song file')
|
||||
parser.add_argument('difficulty', type=int, nargs='?', default=None,
|
||||
@@ -279,19 +269,74 @@ def main():
|
||||
global_data.session_data[PlayerNum.P1].selected_song = path
|
||||
global_data.session_data[PlayerNum.P1].selected_difficulty = selected_difficulty
|
||||
global_data.modifiers[PlayerNum.P1].auto = args.auto
|
||||
return current_screen
|
||||
|
||||
def check_discord_heartbeat(current_screen):
|
||||
if global_data.session_data[global_data.player_num].selected_song != Path():
|
||||
details = f"Playing Song: {global_data.session_data[global_data.player_num].song_title}"
|
||||
else:
|
||||
details = "Idling"
|
||||
RPC.update(
|
||||
state=f"In Screen {current_screen}",
|
||||
details=details,
|
||||
large_text="PyTaiko",
|
||||
start=get_current_ms()/1000,
|
||||
buttons=[{"label": "Play Now", "url": "https://github.com/Yonokid/PyTaiko"}]
|
||||
)
|
||||
|
||||
def draw_fps(last_fps: int):
|
||||
curr_fps = ray.GetFPS()
|
||||
if curr_fps != 0 and curr_fps != last_fps:
|
||||
last_fps = curr_fps
|
||||
if last_fps < 30:
|
||||
ray.DrawText(f'{last_fps} FPS'.encode('utf-8'), 20, 20, 20, ray.RED)
|
||||
elif last_fps < 60:
|
||||
ray.DrawText(f'{last_fps} FPS'.encode('utf-8'), 20, 20, 20, ray.YELLOW)
|
||||
else:
|
||||
ray.DrawText(f'{last_fps} FPS'.encode('utf-8'), 20, 20, 20, ray.LIME)
|
||||
|
||||
def draw_outer_border(screen_width: int, screen_height: int, last_color: pyray.Color):
|
||||
pyray.draw_rectangle(-screen_width, 0, screen_width, screen_height, last_color)
|
||||
pyray.draw_rectangle(screen_width, 0, screen_width, screen_height, last_color)
|
||||
pyray.draw_rectangle(0, -screen_height, screen_width, screen_height, last_color)
|
||||
pyray.draw_rectangle(0, screen_height, screen_width, screen_height, last_color)
|
||||
|
||||
def main():
|
||||
force_dedicated_gpu()
|
||||
|
||||
global_data.config = get_config()
|
||||
match global_data.config["general"]["score_method"]:
|
||||
case ScoreMethod.GEN3:
|
||||
global_data.score_db = 'scores_gen3.db'
|
||||
case ScoreMethod.SHINUCHI:
|
||||
global_data.score_db = 'scores.db'
|
||||
setup_logging()
|
||||
logger.info("Starting PyTaiko")
|
||||
logger.debug(f"Loaded config: {global_data.config}")
|
||||
screen_width = global_tex.screen_width
|
||||
screen_height = global_tex.screen_height
|
||||
|
||||
set_config_flags()
|
||||
ray.InitWindow(screen_width, screen_height, "PyTaiko".encode('utf-8'))
|
||||
|
||||
logger.info(f"Window initialized: {screen_width}x{screen_height}")
|
||||
global_tex.load_screen_textures('global')
|
||||
global_tex.load_zip('chara', 'chara_0')
|
||||
global_tex.load_zip('chara', 'chara_1')
|
||||
global_tex.load_zip('chara', 'chara_4')
|
||||
if global_data.config["video"]["borderless"]:
|
||||
ray.ToggleBorderlessWindowed()
|
||||
logger.info("Borderless window enabled")
|
||||
if global_data.config["video"]["fullscreen"]:
|
||||
ray.ToggleFullscreen()
|
||||
logger.info("Fullscreen enabled")
|
||||
|
||||
init_audio()
|
||||
|
||||
current_screen = check_args()
|
||||
|
||||
logger.info(f"Initial screen: {current_screen}")
|
||||
|
||||
audio.set_log_level((log_level-1)//10)
|
||||
old_stderr = os.dup(2)
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, 2)
|
||||
os.close(devnull)
|
||||
audio.init_audio_device()
|
||||
os.dup2(old_stderr, 2)
|
||||
os.close(old_stderr)
|
||||
logger.info("Audio device initialized")
|
||||
|
||||
create_song_db()
|
||||
|
||||
title_screen = TitleScreen('title')
|
||||
@@ -303,6 +348,8 @@ def main():
|
||||
game_screen_2p = TwoPlayerGameScreen('game')
|
||||
game_screen_practice = PracticeGameScreen('game')
|
||||
practice_select_screen = PracticeSongSelectScreen('song_select')
|
||||
ai_select_screen = AISongSelectScreen('song_select')
|
||||
ai_game_screen = AIBattleGameScreen('game')
|
||||
result_screen = ResultScreen('result')
|
||||
result_screen_2p = TwoPlayerResultScreen('result')
|
||||
settings_screen = SettingsScreen('settings')
|
||||
@@ -320,6 +367,8 @@ def main():
|
||||
Screens.GAME: game_screen,
|
||||
Screens.GAME_2P: game_screen_2p,
|
||||
Screens.GAME_PRACTICE: game_screen_practice,
|
||||
Screens.AI_SELECT: ai_select_screen,
|
||||
Screens.AI_GAME: ai_game_screen,
|
||||
Screens.RESULT: result_screen,
|
||||
Screens.RESULT_2P: result_screen_2p,
|
||||
Screens.SETTINGS: settings_screen,
|
||||
@@ -330,51 +379,41 @@ def main():
|
||||
Screens.LOADING: load_screen
|
||||
}
|
||||
|
||||
camera = ray.Camera2D()
|
||||
camera.target = ray.Vector2(0, 0)
|
||||
camera = pyray.Camera2D()
|
||||
camera.target = pyray.Vector2(0, 0)
|
||||
camera.rotation = 0.0
|
||||
update_camera_for_window_size(camera, screen_width, screen_height)
|
||||
logger.info("Camera2D initialized")
|
||||
|
||||
ray.rl_set_blend_factors_separate(RL_SRC_ALPHA, RL_ONE_MINUS_SRC_ALPHA, RL_ONE, RL_ONE_MINUS_SRC_ALPHA, RL_FUNC_ADD, RL_FUNC_ADD)
|
||||
ray.set_exit_key(global_data.config["keys"]["exit_key"])
|
||||
ray.rlSetBlendFactorsSeparate(RL_SRC_ALPHA, RL_ONE_MINUS_SRC_ALPHA, RL_ONE, RL_ONE_MINUS_SRC_ALPHA, RL_FUNC_ADD, RL_FUNC_ADD)
|
||||
ray.SetExitKey(global_data.config["keys"]["exit_key"])
|
||||
|
||||
ray.hide_cursor()
|
||||
ray.HideCursor()
|
||||
logger.info("Cursor hidden")
|
||||
last_fps = 1
|
||||
last_color = ray.BLACK
|
||||
last_color = pyray.BLACK
|
||||
|
||||
while not ray.window_should_close():
|
||||
while not ray.WindowShouldClose():
|
||||
if discord_connected:
|
||||
if global_data.session_data[global_data.player_num].selected_song != Path():
|
||||
details = f"Playing Song: {global_data.session_data[global_data.player_num].song_title}"
|
||||
else:
|
||||
details = "Idling"
|
||||
RPC.update(
|
||||
state=f"In Screen {current_screen}",
|
||||
details=details,
|
||||
large_text="PyTaiko",
|
||||
start=get_current_ms()/1000,
|
||||
buttons=[{"label": "Play Now", "url": "https://github.com/Yonokid/PyTaiko"}]
|
||||
)
|
||||
check_discord_heartbeat(current_screen)
|
||||
|
||||
if ray.is_key_pressed(global_data.config["keys"]["fullscreen_key"]):
|
||||
ray.toggle_fullscreen()
|
||||
if ray.IsKeyPressed(global_data.config["keys"]["fullscreen_key"]):
|
||||
ray.ToggleFullscreen()
|
||||
logger.info("Toggled fullscreen")
|
||||
elif ray.is_key_pressed(global_data.config["keys"]["borderless_key"]):
|
||||
ray.toggle_borderless_windowed()
|
||||
elif ray.IsKeyPressed(global_data.config["keys"]["borderless_key"]):
|
||||
ray.ToggleBorderlessWindowed()
|
||||
logger.info("Toggled borderless windowed mode")
|
||||
|
||||
update_camera_for_window_size(camera, screen_width, screen_height)
|
||||
|
||||
ray.begin_drawing()
|
||||
ray.BeginDrawing()
|
||||
|
||||
if global_data.camera.border_color != last_color:
|
||||
ray.clear_background(global_data.camera.border_color)
|
||||
pyray.clear_background(global_data.camera.border_color)
|
||||
last_color = global_data.camera.border_color
|
||||
|
||||
ray.begin_mode_2d(camera)
|
||||
ray.begin_blend_mode(ray.BlendMode.BLEND_CUSTOM_SEPARATE)
|
||||
pyray.begin_mode_2d(camera)
|
||||
ray.BeginBlendMode(ray.BLEND_CUSTOM_SEPARATE)
|
||||
|
||||
screen = screen_mapping[current_screen]
|
||||
|
||||
@@ -388,26 +427,15 @@ def main():
|
||||
global_data.input_locked = 0
|
||||
|
||||
if global_data.config["general"]["fps_counter"]:
|
||||
curr_fps = ray.get_fps()
|
||||
if curr_fps != 0 and curr_fps != last_fps:
|
||||
last_fps = curr_fps
|
||||
if last_fps < 30:
|
||||
ray.draw_text(f'{last_fps} FPS', 20, 20, 20, ray.RED)
|
||||
elif last_fps < 60:
|
||||
ray.draw_text(f'{last_fps} FPS', 20, 20, 20, ray.YELLOW)
|
||||
else:
|
||||
ray.draw_text(f'{last_fps} FPS', 20, 20, 20, ray.LIME)
|
||||
draw_fps(last_fps)
|
||||
|
||||
ray.draw_rectangle(-screen_width, 0, screen_width, screen_height, last_color)
|
||||
ray.draw_rectangle(screen_width, 0, screen_width, screen_height, last_color)
|
||||
ray.draw_rectangle(0, -screen_height, screen_width, screen_height, last_color)
|
||||
ray.draw_rectangle(0, screen_height, screen_width, screen_height, last_color)
|
||||
draw_outer_border(screen_width, screen_height, last_color)
|
||||
|
||||
ray.end_blend_mode()
|
||||
ray.end_mode_2d()
|
||||
ray.end_drawing()
|
||||
ray.EndBlendMode()
|
||||
ray.EndMode2D()
|
||||
ray.EndDrawing()
|
||||
|
||||
ray.close_window()
|
||||
ray.CloseWindow()
|
||||
audio.close_audio_device()
|
||||
if discord_connected:
|
||||
RPC.close()
|
||||
|
||||
30
README.md
30
README.md
@@ -4,10 +4,10 @@ A TJA player and Taiko simulator written in Python using the [raylib](https://ww
|
||||
|
||||

|
||||

|
||||
[](https://github.com/Yonokid/PyTaiko/releases)
|
||||
[](https://github.com/Yonokid/PyTaiko/stargazers)
|
||||
[](https://discord.gg/XHcVYKW)
|
||||
[](https://github.com/Yonokid/PyTaiko/actions/workflows/python-app.yml)
|
||||
[](https://github.com/Yonokid/PyTaiko/actions/workflows/tests.yml)
|
||||
|
||||
<img src="/docs/demo.gif">
|
||||
|
||||
@@ -47,18 +47,30 @@ Just make sure to use `/` and not `\`!<br>
|
||||
Q: I'm trying to play on Mac and it can't open!<br>
|
||||
A: Delete your installation and follow these commands in the terminal:<br>
|
||||
```
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
source $HOME/.local/bin/env
|
||||
# Move to the place where you want to download Pytaiko, this example will be in the desktop
|
||||
# Make sure the directory is not saved in iCloud! This will create an issue on macOS where the executable cannot compile.
|
||||
cd ~/Desktop
|
||||
|
||||
# Installing dependencies
|
||||
brew install python@3.12 uv git speexdsp libsndfile
|
||||
|
||||
# Downloading Pytaiko project
|
||||
git clone https://github.com/Yonokid/PyTaiko
|
||||
cd PyTaiko
|
||||
brew install libsndfile
|
||||
brew install speexdsp
|
||||
|
||||
# Compiling audio libraries
|
||||
cd libs/audio
|
||||
make
|
||||
mv libaudio.dylib ../../
|
||||
cd ../../
|
||||
mv libaudio.dylib ...
|
||||
cd ...
|
||||
|
||||
# Running game (for testing)
|
||||
uv run PyTaiko.py
|
||||
|
||||
# Creating executable
|
||||
|
||||
uv add nuitka
|
||||
uv run nuitka --mode=app --noinclude-setuptools-mode=nofollow --noinclude-IPython-mode=nofollow --assume-yes-for-downloads PyTaiko.py
|
||||
```
|
||||
## Installation
|
||||
|
||||
@@ -71,7 +83,7 @@ Download the latest release for your operating system from the [releases page](h
|
||||
2. Run `PyTaiko.exe`
|
||||
|
||||
#### macOS
|
||||
- Run with Python directly (see [Building from Source](#building-from-source))
|
||||
- Run with Python directly or self compile (see [Building from Source](#building-from-source))
|
||||
|
||||
#### Linux
|
||||
- Try running the compiled `PyTaiko.bin` binary
|
||||
|
||||
Submodule Skins/PyTaikoGreen updated: b9dbd95dc7...b62da05eda
4
Songs/18 Search/box.def
Normal file
4
Songs/18 Search/box.def
Normal file
@@ -0,0 +1,4 @@
|
||||
#TITLE:Search Song
|
||||
#TITLEJA:曲検索
|
||||
#COLLECTION:SEARCH
|
||||
#BACKCOLOR:#800000
|
||||
41
libs/__init__.py
Normal file
41
libs/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
PyTaiko Libraries Package
|
||||
|
||||
This package contains core libraries for PyTaiko.
|
||||
|
||||
Modules:
|
||||
animation: Animation system for game objects
|
||||
audio: Audio engine for sound and music playback
|
||||
background: Background rendering system
|
||||
chara_2d: 2D character animation system
|
||||
config: Configuration management
|
||||
file_navigator: File and song navigation UI
|
||||
global_data: Global data structures and enums
|
||||
global_objects: Global UI objects (nameplate, timer, etc.)
|
||||
screen: Base screen class for game states
|
||||
song_hash: Song hashing and database management
|
||||
texture: Texture loading and management
|
||||
tja: TJA chart file parser
|
||||
transition: Screen transition effects
|
||||
utils: Utility functions and helpers
|
||||
video: Video playback system
|
||||
"""
|
||||
|
||||
__version__ = "1.1"
|
||||
__all__ = [
|
||||
"animation",
|
||||
"audio",
|
||||
"background",
|
||||
"chara_2d",
|
||||
"config",
|
||||
"file_navigator",
|
||||
"global_data",
|
||||
"global_objects",
|
||||
"screen",
|
||||
"song_hash",
|
||||
"texture",
|
||||
"tja",
|
||||
"transition",
|
||||
"utils",
|
||||
"video",
|
||||
]
|
||||
@@ -119,7 +119,8 @@ except OSError as e:
|
||||
|
||||
class AudioEngine:
|
||||
"""Initialize an audio engine for playing sounds and music."""
|
||||
def __init__(self, device_type: int, sample_rate: float, buffer_size: int, volume_presets: VolumeConfig):
|
||||
def __init__(self, device_type: int, sample_rate: float, buffer_size: int,
|
||||
volume_presets: VolumeConfig, sounds_path: Path | None = None):
|
||||
self.device_type = max(device_type, 0)
|
||||
if sample_rate < 0:
|
||||
self.target_sample_rate = 44100
|
||||
@@ -131,7 +132,10 @@ class AudioEngine:
|
||||
self.audio_device_ready = False
|
||||
self.volume_presets = volume_presets
|
||||
|
||||
self.sounds_path = Path(f"Skins/{get_config()["paths"]["skin"]}/Sounds")
|
||||
if sounds_path is None:
|
||||
self.sounds_path = Path(f"Skins/{get_config()['paths']['skin']}/Sounds")
|
||||
else:
|
||||
self.sounds_path = sounds_path
|
||||
|
||||
def set_log_level(self, level: int):
|
||||
lib.set_log_level(level) # type: ignore
|
||||
|
||||
@@ -130,10 +130,17 @@ class Background:
|
||||
current_milestone = min(self.max_dancers - 1, int(gauge_1p.gauge_length / (clear_threshold / self.max_dancers)))
|
||||
else:
|
||||
current_milestone = self.max_dancers
|
||||
|
||||
if current_milestone > self.last_milestone and current_milestone <= self.max_dancers:
|
||||
self.dancer.add_dancer()
|
||||
self.last_milestone = current_milestone
|
||||
logger.info(f"Dancer milestone reached: {current_milestone}/{self.max_dancers}")
|
||||
elif current_milestone < self.last_milestone:
|
||||
dancers_to_remove = self.last_milestone - current_milestone
|
||||
for _ in range(dancers_to_remove):
|
||||
self.dancer.remove_dancer()
|
||||
self.last_milestone = current_milestone
|
||||
logger.info(f"Dancer milestones lost: {current_milestone}/{self.max_dancers} (removed {dancers_to_remove})")
|
||||
if self.bg_fever is not None and gauge_1p is not None:
|
||||
if not self.is_clear and gauge_1p.is_clear:
|
||||
self.bg_fever.start()
|
||||
|
||||
@@ -162,6 +162,12 @@ class BaseDancerGroup():
|
||||
dancer.start()
|
||||
self.active_count += 1
|
||||
|
||||
def remove_dancer(self):
|
||||
if self.active_count > 1:
|
||||
self.active_count -= 1
|
||||
position = self.spawn_positions[self.active_count]
|
||||
self.active_dancers[position] = None
|
||||
|
||||
def update(self, current_time_ms: float, bpm: float):
|
||||
for dancer in self.dancers:
|
||||
dancer.update(current_time_ms, bpm)
|
||||
|
||||
@@ -107,7 +107,7 @@ class Chara2D:
|
||||
self.current_anim = self.past_anim
|
||||
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.
|
||||
|
||||
@@ -117,9 +117,9 @@ class Chara2D:
|
||||
mirror (bool): Whether to mirror the character horizontally.
|
||||
"""
|
||||
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:
|
||||
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:
|
||||
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)
|
||||
|
||||
@@ -1038,7 +1038,8 @@ class Directory(FileSystemItem):
|
||||
'RECENT',
|
||||
'FAVORITE',
|
||||
'DIFFICULTY',
|
||||
'RECOMMENDED'
|
||||
'RECOMMENDED',
|
||||
'SEARCH'
|
||||
]
|
||||
def __init__(self, path: Path, name: str, back_color: Optional[tuple[int, int, int]], fore_color: Optional[tuple[int, int, int]], texture_index: TextureIndex, genre_index: GenreIndex, has_box_def=False, to_root=False, back=False, tja_count=0, box_texture=None, collection=None):
|
||||
super().__init__(path, name)
|
||||
@@ -1143,6 +1144,7 @@ class FileNavigator:
|
||||
self.genre_bg = None
|
||||
self.song_count = 0
|
||||
self.in_dan_select = False
|
||||
self.current_search = ''
|
||||
logger.info("FileNavigator initialized")
|
||||
|
||||
def initialize(self, root_dirs: list[Path]):
|
||||
@@ -1392,6 +1394,42 @@ class FileNavigator:
|
||||
temp_items.append(item)
|
||||
return random.sample(temp_items, min(10, len(temp_items)))
|
||||
|
||||
def _levenshtein_distance(self, s1: str, s2: str):
|
||||
# Create a matrix to store distances
|
||||
m, n = len(s1), len(s2)
|
||||
dp = [[0] * (n + 1) for _ in range(m + 1)]
|
||||
|
||||
# Initialize base cases
|
||||
for i in range(m + 1):
|
||||
dp[i][0] = i
|
||||
for j in range(n + 1):
|
||||
dp[0][j] = j
|
||||
|
||||
# Fill the matrix
|
||||
for i in range(1, m + 1):
|
||||
for j in range(1, n + 1):
|
||||
if s1[i-1] == s2[j-1]:
|
||||
dp[i][j] = dp[i-1][j-1] # No operation needed
|
||||
else:
|
||||
dp[i][j] = 1 + min(
|
||||
dp[i-1][j], # Deletion
|
||||
dp[i][j-1], # Insertion
|
||||
dp[i-1][j-1] # Substitution
|
||||
)
|
||||
|
||||
return dp[m][n]
|
||||
|
||||
def search_song(self, search_name: str):
|
||||
items = []
|
||||
for path, song in self.all_song_files.items():
|
||||
if self._levenshtein_distance(song.name[:-4].lower(), search_name.lower()) < 2:
|
||||
items.append(song)
|
||||
if isinstance(song, SongFile):
|
||||
if self._levenshtein_distance(song.tja.metadata.subtitle["en"].lower(), search_name.lower()) < 2:
|
||||
items.append(song)
|
||||
|
||||
return items
|
||||
|
||||
def load_current_directory(self, selected_item: Optional[Directory] = None):
|
||||
"""Load pre-generated items for the current directory (unified for root and subdirs)"""
|
||||
dir_key = str(self.current_dir)
|
||||
@@ -1438,6 +1476,8 @@ class FileNavigator:
|
||||
content_items = self.load_diff_sort_items(selected_item, dir_key)
|
||||
elif selected_item.collection == Directory.COLLECTIONS[4]:
|
||||
content_items = self.load_recommended_items(selected_item, dir_key)
|
||||
elif selected_item.collection == Directory.COLLECTIONS[5]:
|
||||
content_items = self.search_song(self.current_search)
|
||||
|
||||
if content_items == []:
|
||||
self.go_back()
|
||||
|
||||
@@ -14,6 +14,7 @@ class PlayerNum(IntEnum):
|
||||
P2 = 2
|
||||
TWO_PLAYER = 3
|
||||
DAN = 4
|
||||
AI = 5
|
||||
|
||||
class ScoreMethod():
|
||||
GEN3 = "gen3"
|
||||
@@ -44,6 +45,7 @@ class Modifiers:
|
||||
display: bool = False
|
||||
inverse: bool = False
|
||||
random: int = 0
|
||||
subdiff: int = 0
|
||||
|
||||
@dataclass
|
||||
class DanResultSong:
|
||||
|
||||
@@ -58,6 +58,9 @@ class Nameplate:
|
||||
"""
|
||||
tex = global_tex
|
||||
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:
|
||||
frame = 2
|
||||
title_offset = 0
|
||||
|
||||
@@ -72,6 +72,13 @@ class TextureWrapper:
|
||||
self.animations: dict[int, BaseAnimation] = dict()
|
||||
self.skin_config: dict[str, SkinInfo] = dict()
|
||||
self.graphics_path = Path(f'Skins/{get_config()['paths']['skin']}/Graphics')
|
||||
if not self.graphics_path.exists():
|
||||
logger.error("No skin has been configured")
|
||||
self.screen_width = 1280
|
||||
self.screen_height = 720
|
||||
self.screen_scale = 1.0
|
||||
self.skin_config = dict()
|
||||
return
|
||||
self.parent_graphics_path = Path(f'Skins/{get_config()['paths']['skin']}/Graphics')
|
||||
if not (self.graphics_path / "skin_config.json").exists():
|
||||
raise Exception("skin is missing a skin_config.json")
|
||||
@@ -290,7 +297,7 @@ class TextureWrapper:
|
||||
else:
|
||||
ray.DrawTexturePro(tex_object.texture, source_rect, dest_rect, origin, rotation, final_color)
|
||||
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,
|
||||
mirror: str = '', x: float = 0, y: float = 0, x2: float = 0, y2: float = 0,
|
||||
|
||||
392
libs/tja.py
392
libs/tja.py
@@ -1148,7 +1148,6 @@ def modifier_inverse(notes: NoteList):
|
||||
def modifier_random(notes: NoteList, value: int):
|
||||
"""Randomly modifies the type of the notes in the given NoteList.
|
||||
value: 1 == kimagure, 2 == detarame"""
|
||||
#value: 1 == kimagure, 2 == detarame
|
||||
modded_notes = notes.play_notes.copy()
|
||||
percentage = int(len(modded_notes) / 5) * value
|
||||
selected_notes = random.sample(range(len(modded_notes)), percentage)
|
||||
@@ -1166,4 +1165,393 @@ def apply_modifiers(notes: NoteList, modifiers: Modifiers):
|
||||
play_notes = modifier_inverse(notes)
|
||||
play_notes = modifier_random(notes, modifiers.random)
|
||||
draw_notes, bars = modifier_speed(notes, modifiers.speed)
|
||||
return deque(play_notes), deque(draw_notes), deque(bars)
|
||||
play_notes = modifier_difficulty(notes, modifiers.subdiff)
|
||||
draw_notes = modifier_difficulty(notes, modifiers.subdiff)
|
||||
return play_notes, draw_notes, bars
|
||||
|
||||
class Interval(IntEnum):
|
||||
UNKNOWN = 0
|
||||
QUARTER = 1
|
||||
EIGHTH = 2
|
||||
TWELFTH = 3
|
||||
SIXTEENTH = 4
|
||||
TWENTYFOURTH = 6
|
||||
THIRTYSECOND = 8
|
||||
|
||||
def modifier_difficulty(notes: NoteList, level: int):
|
||||
"""Modifies notes based on difficulty level according to the difficulty table.
|
||||
|
||||
Args:
|
||||
notes: The NoteList to modify
|
||||
level: The numerical difficulty level (1-13)
|
||||
|
||||
Returns:
|
||||
Modified list of notes
|
||||
"""
|
||||
# Levels with no changes: Easy (1), Normal (2-5), Hard (9), Oni (13)
|
||||
if level in [0, 1, 2, 3, 4, 5, 9, 13]:
|
||||
return notes.play_notes
|
||||
|
||||
modded_notes = notes.play_notes.copy()
|
||||
|
||||
# Helper function to calculate note interval category
|
||||
def get_note_interval_type(interval_ms: float, bpm: float, time_sig: float = 4.0) -> Interval:
|
||||
"""Classify note interval as 1/8, 1/16, 1/12, or 1/24 note."""
|
||||
if bpm == 0:
|
||||
return Interval.UNKNOWN
|
||||
|
||||
ms_per_measure = get_ms_per_measure(bpm, time_sig) / time_sig
|
||||
tolerance = 15 # ms tolerance for timing classification
|
||||
|
||||
eighth_note = ms_per_measure / 8
|
||||
sixteenth_note = ms_per_measure / 16
|
||||
twelfth_note = ms_per_measure / 12
|
||||
twentyfourth_note = ms_per_measure / 24
|
||||
thirtysecond_note = ms_per_measure / 32
|
||||
quarter_note = ms_per_measure / 4
|
||||
|
||||
if abs(interval_ms - eighth_note) < tolerance:
|
||||
return Interval.EIGHTH
|
||||
elif abs(interval_ms - sixteenth_note) < tolerance:
|
||||
return Interval.SIXTEENTH
|
||||
elif abs(interval_ms - twelfth_note) < tolerance:
|
||||
return Interval.TWELFTH
|
||||
elif abs(interval_ms - twentyfourth_note) < tolerance:
|
||||
return Interval.TWENTYFOURTH
|
||||
elif abs(interval_ms - thirtysecond_note) < tolerance:
|
||||
return Interval.THIRTYSECOND
|
||||
elif abs(interval_ms - quarter_note) < tolerance:
|
||||
return Interval.QUARTER
|
||||
return Interval.UNKNOWN
|
||||
|
||||
# Helper function to make notes single-color
|
||||
def make_single_color(note_indices: list[int]):
|
||||
"""Convert notes to single color (auto-detects majority color if not specified)."""
|
||||
don_count = 0
|
||||
kat_count = 0
|
||||
|
||||
for idx in note_indices:
|
||||
if idx < len(modded_notes):
|
||||
note_type = modded_notes[idx].type
|
||||
if note_type in [NoteType.DON, NoteType.DON_L]:
|
||||
don_count += 1
|
||||
elif note_type in [NoteType.KAT, NoteType.KAT_L]:
|
||||
kat_count += 1
|
||||
|
||||
# Use majority color, defaulting to DON if tied or no valid notes
|
||||
color = NoteType.DON if don_count >= kat_count else NoteType.KAT
|
||||
|
||||
# Convert all notes to the determined color
|
||||
for idx in note_indices:
|
||||
if idx < len(modded_notes):
|
||||
if modded_notes[idx].type in [NoteType.DON, NoteType.KAT]:
|
||||
modded_notes[idx].type = color
|
||||
elif modded_notes[idx].type in [NoteType.DON_L, NoteType.KAT_L]:
|
||||
modded_notes[idx].type = NoteType.DON_L if color == NoteType.DON else NoteType.KAT_L
|
||||
|
||||
# Helper function to find note streams
|
||||
def find_streams(interval_type: Interval) -> list[tuple[int, int]]:
|
||||
"""Find consecutive notes with the given interval type.
|
||||
Returns list of (start_index, length) tuples."""
|
||||
streams = []
|
||||
i = 0
|
||||
while i < len(modded_notes) - 1:
|
||||
if isinstance(modded_notes[i], (Drumroll, Balloon)):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
stream_start = i
|
||||
stream_length = 1
|
||||
|
||||
while i < len(modded_notes) - 1:
|
||||
if isinstance(modded_notes[i + 1], (Drumroll, Balloon)):
|
||||
break
|
||||
|
||||
interval = modded_notes[i + 1].hit_ms - modded_notes[i].hit_ms
|
||||
note_type = get_note_interval_type(interval, modded_notes[i].bpm)
|
||||
|
||||
if note_type == interval_type:
|
||||
stream_length += 1
|
||||
i += 1
|
||||
else:
|
||||
break
|
||||
|
||||
if stream_length >= 2: # At least 2 notes to form a stream
|
||||
streams.append((stream_start, stream_length))
|
||||
|
||||
i += 1
|
||||
|
||||
return streams
|
||||
|
||||
def find_2plus2_patterns(interval_type: Interval) -> list[int]:
|
||||
"""Find 2+2 patterns with the given interval type.
|
||||
A 2+2 pattern consists of:
|
||||
- 2 notes with the specified interval between them
|
||||
- A gap (size of the interval)
|
||||
- 2 more notes with the specified interval between them
|
||||
- A gap after (at least the size of the interval)
|
||||
|
||||
Returns list of starting indices for 2+2 patterns."""
|
||||
patterns = []
|
||||
i = 0
|
||||
|
||||
while i < len(modded_notes) - 3:
|
||||
if isinstance(modded_notes[i], (Drumroll, Balloon)):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Check if we have at least 4 notes ahead
|
||||
valid_notes_ahead = 0
|
||||
for j in range(i, min(i + 4, len(modded_notes))):
|
||||
if not isinstance(modded_notes[j], (Drumroll, Balloon)):
|
||||
valid_notes_ahead += 1
|
||||
|
||||
if valid_notes_ahead < 4:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Get the next 3 valid note indices (total 4 notes including current)
|
||||
note_indices = [i]
|
||||
j = i + 1
|
||||
while len(note_indices) < 4 and j < len(modded_notes):
|
||||
if not isinstance(modded_notes[j], (Drumroll, Balloon)):
|
||||
note_indices.append(j)
|
||||
j += 1
|
||||
|
||||
if len(note_indices) < 4:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Check intervals between the 4 notes
|
||||
interval1 = modded_notes[note_indices[1]].hit_ms - modded_notes[note_indices[0]].hit_ms
|
||||
interval2 = modded_notes[note_indices[2]].hit_ms - modded_notes[note_indices[1]].hit_ms
|
||||
interval3 = modded_notes[note_indices[3]].hit_ms - modded_notes[note_indices[2]].hit_ms
|
||||
|
||||
type1 = get_note_interval_type(interval1, modded_notes[note_indices[0]].bpm)
|
||||
type3 = get_note_interval_type(interval3, modded_notes[note_indices[2]].bpm)
|
||||
|
||||
# Check for 2+2 pattern:
|
||||
# - First interval matches our target type (between notes 0 and 1)
|
||||
# - Second interval is ~2x the target type (the gap, between notes 1 and 2)
|
||||
# - Third interval matches our target type (between notes 2 and 3)
|
||||
# - After the last note, there should be a gap (check next note)
|
||||
if type1 == interval_type and type3 == interval_type:
|
||||
# Check if middle interval is approximately 2x the note interval (represents the gap)
|
||||
ms_per_measure = get_ms_per_measure(modded_notes[note_indices[0]].bpm, 4.0) / 4.0
|
||||
target_interval = 0
|
||||
if interval_type == Interval.SIXTEENTH:
|
||||
target_interval = ms_per_measure / 16
|
||||
elif interval_type == Interval.EIGHTH:
|
||||
target_interval = ms_per_measure / 8
|
||||
elif interval_type == Interval.TWELFTH:
|
||||
target_interval = ms_per_measure / 12
|
||||
elif interval_type == Interval.TWENTYFOURTH:
|
||||
target_interval = ms_per_measure / 24
|
||||
|
||||
# The gap should be approximately 2x the note interval (with tolerance)
|
||||
expected_gap = target_interval * 2
|
||||
tolerance = 20 # ms tolerance for gap detection
|
||||
|
||||
if abs(interval2 - expected_gap) < tolerance:
|
||||
# Check if there's a gap after the 4th note
|
||||
if note_indices[3] + 1 < len(modded_notes):
|
||||
if not isinstance(modded_notes[note_indices[3] + 1], (Drumroll, Balloon)):
|
||||
interval_after = modded_notes[note_indices[3] + 1].hit_ms - modded_notes[note_indices[3]].hit_ms
|
||||
type_after = get_note_interval_type(interval_after, modded_notes[note_indices[3]].bpm)
|
||||
# Gap after should be at least the size of the interval
|
||||
if interval_after >= target_interval * 1.5 or type_after != interval_type:
|
||||
patterns.append(i)
|
||||
else:
|
||||
# End of notes, so pattern is valid
|
||||
patterns.append(i)
|
||||
|
||||
i += 1
|
||||
|
||||
return patterns
|
||||
|
||||
# Level 6 (Hard): 1/8 note streams become single-color; 1/8 note triplets become 1/4 notes
|
||||
if level == 6:
|
||||
streams = find_streams(Interval.EIGHTH)
|
||||
for start, length in streams:
|
||||
if length == 3:
|
||||
modded_notes[start + 1].type = NoteType.NONE
|
||||
elif length > 3:
|
||||
make_single_color(list(range(start, start + length)))
|
||||
|
||||
# Level 7 (Hard): 1/8 note 5-hit streams become 3-1 pattern; 7+ hits repeat 3-1-1 pattern
|
||||
elif level == 7:
|
||||
streams = find_streams(Interval.EIGHTH)
|
||||
for start, length in streams:
|
||||
if length == 5:
|
||||
modded_notes[start + 3].type = NoteType.NONE
|
||||
elif length >= 7:
|
||||
idx = start
|
||||
while idx < start + length:
|
||||
idx += 3
|
||||
if idx < start + length and idx < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 1
|
||||
|
||||
# Level 8 (Hard): 1/16 note triplets become 1/8 notes; 1/16 note 5-hit streams become 3+1 or 2+2
|
||||
elif level == 8:
|
||||
streams = find_streams(Interval.SIXTEENTH)
|
||||
for start, length in streams:
|
||||
if length == 3:
|
||||
modded_notes[start + 1].type = NoteType.NONE
|
||||
elif length == 5:
|
||||
#3+1 if start with don, 2+2 if start with kat
|
||||
if modded_notes[start].type in [NoteType.DON, NoteType.DON_L]:
|
||||
modded_notes[start + 3].type = NoteType.NONE
|
||||
else:
|
||||
modded_notes[start + 2].type = NoteType.NONE
|
||||
|
||||
# Level 10 (Oni):
|
||||
# 1/16 note 5-hit streams become 3+1
|
||||
# 1/16 note doubles become single-color
|
||||
# 2+2 hits become 2+1 hits (annoying)
|
||||
# 1/16 4+ hits become 8th doubles
|
||||
# 1/24ths are removed
|
||||
# 1/16th streams become triplet followed by interval below
|
||||
elif level == 10:
|
||||
streams = find_streams(Interval.THIRTYSECOND)
|
||||
for start, length in streams:
|
||||
idx = start + 1
|
||||
while idx < start + length:
|
||||
if idx < start + length and idx < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 2
|
||||
|
||||
streams = find_streams(Interval.TWENTYFOURTH)
|
||||
for start, length in streams:
|
||||
idx = start + 1
|
||||
while idx < start + length - 1:
|
||||
if idx < len(modded_notes) and idx + 1 < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
modded_notes[idx + 1].type = NoteType.NONE
|
||||
idx += 3
|
||||
streams = find_streams(Interval.SIXTEENTH)
|
||||
for start, length in streams:
|
||||
if length == 2:
|
||||
modded_notes[start].type = modded_notes[start + 1].type
|
||||
if length == 3:
|
||||
modded_notes[start + 1].type = NoteType.NONE
|
||||
if length == 4 or length == 5:
|
||||
modded_notes[start + 3].type = NoteType.NONE
|
||||
make_single_color(list(range(start, start + length)))
|
||||
elif length > 5:
|
||||
modded_notes[start + 3].type = NoteType.NONE
|
||||
idx = start + 5
|
||||
while idx < start + length:
|
||||
if idx < start + length and idx < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 2
|
||||
|
||||
streams_2_2 = find_2plus2_patterns(Interval.SIXTEENTH)
|
||||
for index in streams_2_2:
|
||||
modded_notes[index + 2].type = NoteType.NONE
|
||||
|
||||
# Level 11 (Oni):
|
||||
# Level 10 variation
|
||||
elif level == 11:
|
||||
streams = find_streams(Interval.THIRTYSECOND)
|
||||
for start, length in streams:
|
||||
idx = start + 1
|
||||
while idx < start + length:
|
||||
if idx < start + length and idx < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 2
|
||||
|
||||
streams = find_streams(Interval.TWENTYFOURTH)
|
||||
for start, length in streams:
|
||||
idx = start + 1
|
||||
while idx < start + length - 1:
|
||||
if idx < len(modded_notes) and idx + 1 < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
modded_notes[idx + 1].type = NoteType.NONE
|
||||
idx += 3
|
||||
|
||||
streams = find_streams(Interval.TWELFTH)
|
||||
for start, length in streams:
|
||||
idx = start + 1
|
||||
while idx < start + length - 1:
|
||||
if idx < len(modded_notes) and idx + 1 < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 3
|
||||
|
||||
streams = find_streams(Interval.SIXTEENTH)
|
||||
for start, length in streams:
|
||||
if length == 2:
|
||||
modded_notes[start].type = modded_notes[start + 1].type
|
||||
if length == 3:
|
||||
modded_notes[start + 1].type = NoteType.NONE
|
||||
if length == 4 or length == 5:
|
||||
modded_notes[start + 3].type = NoteType.NONE
|
||||
make_single_color(list(range(start, start + length)))
|
||||
elif length > 5:
|
||||
idx = start
|
||||
while idx < start + length:
|
||||
triplet_end = min(idx + 3, start + length)
|
||||
if triplet_end - idx >= 2:
|
||||
make_single_color(list(range(idx, triplet_end)))
|
||||
idx += 3
|
||||
if idx < start + length and idx < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 2
|
||||
if idx < start + length and idx < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 1
|
||||
# Level 12 (Oni):
|
||||
# Level 10 variation
|
||||
elif level == 12:
|
||||
streams = find_streams(Interval.THIRTYSECOND)
|
||||
for start, length in streams:
|
||||
idx = start + 1
|
||||
while idx < start + length:
|
||||
if idx < start + length and idx < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 2
|
||||
|
||||
streams = find_streams(Interval.TWENTYFOURTH)
|
||||
for start, length in streams:
|
||||
idx = start + 1
|
||||
while idx < start + length - 1:
|
||||
if idx < len(modded_notes) and idx + 1 < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
modded_notes[idx + 1].type = NoteType.NONE
|
||||
idx += 3
|
||||
|
||||
streams = find_streams(Interval.TWELFTH)
|
||||
for start, length in streams:
|
||||
if length <= 4:
|
||||
make_single_color(list(range(start, start + length)))
|
||||
else:
|
||||
idx = start + 1
|
||||
while idx < start + length - 1:
|
||||
if idx < len(modded_notes) and idx + 1 < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 3
|
||||
|
||||
streams = find_streams(Interval.SIXTEENTH)
|
||||
for start, length in streams:
|
||||
if length == 3:
|
||||
make_single_color(list(range(start, start + length)))
|
||||
if length == 4 or length == 5:
|
||||
modded_notes[start + 3].type = NoteType.NONE
|
||||
make_single_color(list(range(start, start + length)))
|
||||
elif length > 5:
|
||||
idx = start
|
||||
while idx < start + length:
|
||||
triplet_end = min(idx + 3, start + length)
|
||||
if triplet_end - idx >= 2:
|
||||
make_single_color(list(range(idx, triplet_end)))
|
||||
idx += 3
|
||||
if idx < start + length and idx < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 2
|
||||
if idx < start + length and idx < len(modded_notes):
|
||||
modded_notes[idx].type = NoteType.NONE
|
||||
idx += 1
|
||||
|
||||
filtered_notes = [note for note in modded_notes if note.type != NoteType.NONE]
|
||||
|
||||
return filtered_notes
|
||||
|
||||
@@ -6,11 +6,30 @@ readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"av>=16.0.1",
|
||||
"pyinstrument>=5.1.1",
|
||||
"pypresence>=4.6.1",
|
||||
"pytest>=9.0.2",
|
||||
"raylib-sdl>=5.5.0.2",
|
||||
"tomlkit>=0.13.3",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["test"]
|
||||
pythonpath = ["."]
|
||||
python_files = "test_*.py"
|
||||
python_classes = "Test*"
|
||||
python_functions = "test_*"
|
||||
addopts = [
|
||||
"-v",
|
||||
"--strict-markers",
|
||||
"--tb=short",
|
||||
"--color=yes",
|
||||
]
|
||||
markers = [
|
||||
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
||||
"integration: marks tests as integration tests",
|
||||
]
|
||||
|
||||
[tool.vulture]
|
||||
exclude = ["*.git", ".github/", ".venv/", "cache/"]
|
||||
paths = ["."]
|
||||
@@ -18,4 +37,5 @@ paths = ["."]
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"nuitka>=2.8.4",
|
||||
"pytest-cov>=6.0.0",
|
||||
]
|
||||
|
||||
470
scenes/ai_battle/game.py
Normal file
470
scenes/ai_battle/game.py
Normal file
@@ -0,0 +1,470 @@
|
||||
import copy
|
||||
import logging
|
||||
import random
|
||||
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 Difficulty, Modifiers, PlayerNum, global_data
|
||||
from libs.global_objects import Nameplate
|
||||
from libs.texture import tex
|
||||
from libs.tja import TJAParser
|
||||
from libs.utils import get_current_ms, global_tex
|
||||
from scenes.game import (
|
||||
DrumType,
|
||||
GameScreen,
|
||||
Gauge,
|
||||
Player,
|
||||
Side,
|
||||
SongInfo,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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):
|
||||
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('AI_SELECT')
|
||||
|
||||
if ray.is_key_pressed(global_data.config["keys"]["pause_key"]):
|
||||
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_scoreboards(self):
|
||||
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
|
||||
|
||||
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
|
||||
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, None)
|
||||
self.player_2.update(self.current_ms, current_time, None)
|
||||
self.update_scoreboards()
|
||||
|
||||
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'):
|
||||
return self.on_screen_end('AI_SELECT')
|
||||
elif self.current_ms >= self.player_1.end_time:
|
||||
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.gauge_length = int(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.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')
|
||||
else:
|
||||
self.end_ms = current_time
|
||||
|
||||
return self.global_keys()
|
||||
|
||||
def update_background(self, current_time):
|
||||
if self.player_1.don_notes == self.player_2.don_notes and self.player_1.kat_notes == self.player_2.kat_notes:
|
||||
self.background.update_values((self.player_1.good_count, self.player_1.ok_count), (self.player_2.good_count, self.player_2.ok_count))
|
||||
self.background.update(current_time)
|
||||
|
||||
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):
|
||||
self.contest_point_fade.update(current_ms)
|
||||
|
||||
def update_values(self, player_judge: tuple[int, int], ai_judge: tuple[int, int]):
|
||||
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) // 2) + 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)
|
||||
263
scenes/ai_battle/song_select.py
Normal file
263
scenes/ai_battle/song_select.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import logging
|
||||
|
||||
import pyray as ray
|
||||
|
||||
from libs.audio import audio
|
||||
from libs.chara_2d import Chara2D
|
||||
from libs.file_navigator import SongFile
|
||||
from libs.global_data import Difficulty, PlayerNum, global_data
|
||||
from libs.texture import tex
|
||||
from libs.utils import (
|
||||
is_l_don_pressed,
|
||||
is_l_kat_pressed,
|
||||
is_r_don_pressed,
|
||||
is_r_kat_pressed,
|
||||
)
|
||||
from scenes.song_select import (
|
||||
ModifierSelector,
|
||||
NeiroSelector,
|
||||
SongSelectPlayer,
|
||||
SongSelectScreen,
|
||||
State,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AISongSelectScreen(SongSelectScreen):
|
||||
def on_screen_start(self):
|
||||
super().on_screen_start()
|
||||
self.player_1 = AISongSelectPlayer(global_data.player_num, self.text_fade_in)
|
||||
global_data.modifiers[global_data.player_num].subdiff = 0
|
||||
self.ai_chara = Chara2D(PlayerNum.AI-1)
|
||||
|
||||
def update_players(self, current_time) -> str:
|
||||
self.player_1.update(current_time)
|
||||
self.ai_chara.update(current_time)
|
||||
if self.text_fade_out.is_finished:
|
||||
self.player_1.selected_song = True
|
||||
next_screen = "AI_GAME"
|
||||
return next_screen
|
||||
|
||||
def draw_background(self):
|
||||
tex.draw_texture('ai_battle', 'background')
|
||||
tex.draw_texture('ai_battle', 'background_overlay')
|
||||
tex.draw_texture('ai_battle', 'background_overlay_2')
|
||||
|
||||
def draw(self):
|
||||
self.draw_background()
|
||||
|
||||
if self.navigator.genre_bg is not None and self.state == State.BROWSING:
|
||||
self.navigator.genre_bg.draw(tex.skin_config["boxes"].y)
|
||||
|
||||
self.navigator.draw_boxes(self.move_away.attribute, self.player_1.is_ura, self.diff_fade_out.attribute)
|
||||
|
||||
if self.state == State.BROWSING:
|
||||
tex.draw_texture('global', 'arrow', index=0, x=-(self.blue_arrow_move.attribute*2), fade=self.blue_arrow_fade.attribute)
|
||||
tex.draw_texture('global', 'arrow', index=1, mirror='horizontal', x=self.blue_arrow_move.attribute*2, fade=self.blue_arrow_fade.attribute)
|
||||
tex.draw_texture('global', 'footer')
|
||||
|
||||
self.ura_switch_animation.draw()
|
||||
|
||||
if self.diff_sort_selector is not None:
|
||||
self.diff_sort_selector.draw()
|
||||
|
||||
if self.search_box is not None:
|
||||
self.search_box.draw()
|
||||
|
||||
if (self.player_1.selected_song and self.state == State.SONG_SELECTED):
|
||||
tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute)
|
||||
elif self.state == State.DIFF_SORTING:
|
||||
tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute)
|
||||
else:
|
||||
tex.draw_texture('global', 'song_select', fade=self.text_fade_out.attribute)
|
||||
|
||||
self.draw_players()
|
||||
self.ai_chara.draw(x=tex.skin_config["song_select_chara_2p"].x, y=tex.skin_config["song_select_chara_2p"].y, mirror=True)
|
||||
|
||||
if self.state == State.BROWSING and self.navigator.items != []:
|
||||
curr_item = self.navigator.get_current_item()
|
||||
if isinstance(curr_item, SongFile):
|
||||
curr_item.box.draw_score_history()
|
||||
|
||||
if self.player_1.subdiff_selector is not None and self.player_1.subdiff_selector.is_selected:
|
||||
ray.draw_rectangle(0, 0, tex.screen_width, tex.screen_height, ray.fade(ray.BLACK, 0.5))
|
||||
self.player_1.subdiff_selector.draw()
|
||||
|
||||
self.draw_overlay()
|
||||
|
||||
class AISongSelectPlayer(SongSelectPlayer):
|
||||
def __init__(self, player_num: PlayerNum, text_fade_in):
|
||||
super().__init__(player_num, text_fade_in)
|
||||
self.subdiff_selector = None
|
||||
|
||||
def update(self, current_time):
|
||||
super().update(current_time)
|
||||
if self.subdiff_selector is not None:
|
||||
self.subdiff_selector.update(current_time, self.selected_difficulty)
|
||||
|
||||
def on_song_selected(self, selected_song: SongFile):
|
||||
"""Called when a song is selected"""
|
||||
super().on_song_selected(selected_song)
|
||||
self.subdiff_selector = SubdiffSelector(self.player_num, min(selected_song.tja.metadata.course_data))
|
||||
|
||||
def handle_input_selected(self, current_item):
|
||||
"""Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None"""
|
||||
if self.neiro_selector is not None:
|
||||
if is_l_kat_pressed(self.player_num):
|
||||
self.neiro_selector.move_left()
|
||||
elif is_r_kat_pressed(self.player_num):
|
||||
self.neiro_selector.move_right()
|
||||
if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num):
|
||||
audio.play_sound('don', 'sound')
|
||||
self.neiro_selector.confirm()
|
||||
return None
|
||||
|
||||
if self.modifier_selector is not None:
|
||||
if is_l_kat_pressed(self.player_num):
|
||||
audio.play_sound('kat', 'sound')
|
||||
self.modifier_selector.left()
|
||||
elif is_r_kat_pressed(self.player_num):
|
||||
audio.play_sound('kat', 'sound')
|
||||
self.modifier_selector.right()
|
||||
if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num):
|
||||
audio.play_sound('don', 'sound')
|
||||
self.modifier_selector.confirm()
|
||||
return None
|
||||
|
||||
if self.subdiff_selector is not None and self.subdiff_selector.is_selected:
|
||||
if is_l_kat_pressed(self.player_num):
|
||||
audio.play_sound('kat', 'sound')
|
||||
self.subdiff_selector.move_left(self.selected_difficulty)
|
||||
elif is_r_kat_pressed(self.player_num):
|
||||
audio.play_sound('kat', 'sound')
|
||||
self.subdiff_selector.move_right(self.selected_difficulty)
|
||||
if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num):
|
||||
audio.play_sound('don', 'sound')
|
||||
self.subdiff_selector.confirm()
|
||||
self.is_ready = True
|
||||
return "confirm"
|
||||
return None
|
||||
|
||||
if is_l_don_pressed(self.player_num) or is_r_don_pressed(self.player_num):
|
||||
if self.selected_difficulty == -3:
|
||||
self.subdiff_selector = None
|
||||
return "cancel"
|
||||
elif self.selected_difficulty == -2:
|
||||
audio.play_sound('don', 'sound')
|
||||
self.modifier_selector = ModifierSelector(self.player_num)
|
||||
return None
|
||||
elif self.selected_difficulty == -1:
|
||||
audio.play_sound('don', 'sound')
|
||||
self.neiro_selector = NeiroSelector(self.player_num)
|
||||
return None
|
||||
else:
|
||||
audio.play_sound('don', 'sound')
|
||||
if self.subdiff_selector is not None:
|
||||
self.subdiff_selector.is_selected = True
|
||||
|
||||
if is_l_kat_pressed(self.player_num) or is_r_kat_pressed(self.player_num):
|
||||
audio.play_sound('kat', 'sound')
|
||||
selected_song = current_item
|
||||
diffs = sorted(selected_song.tja.metadata.course_data)
|
||||
prev_diff = self.selected_difficulty
|
||||
ret_val = None
|
||||
|
||||
if is_l_kat_pressed(self.player_num):
|
||||
ret_val = self._navigate_difficulty_left(diffs)
|
||||
elif is_r_kat_pressed(self.player_num):
|
||||
ret_val = self._navigate_difficulty_right(diffs)
|
||||
|
||||
if Difficulty.EASY <= self.selected_difficulty <= Difficulty.URA and self.selected_difficulty != prev_diff:
|
||||
self.selected_diff_bounce.start()
|
||||
self.selected_diff_fadein.start()
|
||||
|
||||
|
||||
return ret_val
|
||||
|
||||
if (ray.is_key_pressed(ray.KeyboardKey.KEY_TAB) and
|
||||
self.selected_difficulty in [Difficulty.ONI, Difficulty.URA]):
|
||||
return self._toggle_ura_mode()
|
||||
|
||||
return None
|
||||
|
||||
def draw(self, state: int, is_half: bool = False):
|
||||
if (self.selected_song and state == State.SONG_SELECTED):
|
||||
self.draw_selector(is_half)
|
||||
|
||||
offset = 0
|
||||
if self.subdiff_selector is not None:
|
||||
offset = -self.subdiff_selector.move.attribute*1.05
|
||||
self.nameplate.draw(tex.skin_config["song_select_nameplate_1p"].x, tex.skin_config["song_select_nameplate_1p"].y)
|
||||
self.chara.draw(x=tex.skin_config["song_select_chara_1p"].x, y=tex.skin_config["song_select_chara_1p"].y + (offset*0.6))
|
||||
|
||||
if self.subdiff_selector is not None:
|
||||
self.subdiff_selector.draw()
|
||||
|
||||
if self.neiro_selector is not None:
|
||||
self.neiro_selector.draw()
|
||||
|
||||
if self.modifier_selector is not None:
|
||||
self.modifier_selector.draw()
|
||||
|
||||
class SubdiffSelector:
|
||||
def __init__(self, player_num: PlayerNum, lowest_difficulty: int):
|
||||
self.player_num = player_num
|
||||
self.move = tex.get_animation(28, is_copy=True)
|
||||
self.blue_arrow_fade = tex.get_animation(29, is_copy=True)
|
||||
self.blue_arrow_move = tex.get_animation(30, is_copy=True)
|
||||
self.move.start()
|
||||
self.is_selected = False
|
||||
self.selected_index = 0
|
||||
subdiffs_easy = [('subdiff_easy', 0), ('subdiff_normal', 0), ('subdiff_normal', 1), ('subdiff_normal', 2)]
|
||||
subdiffs_normal = [('subdiff_normal', 0), ('subdiff_normal', 1), ('subdiff_normal', 2), ('subdiff_normal', 3)]
|
||||
subdiffs_hard = [('subdiff_hard', 0), ('subdiff_hard', 1), ('subdiff_hard', 2), ('subdiff_hard', 3)]
|
||||
subdiffs_oni = [('subdiff_oni', 0), ('subdiff_oni', 1), ('subdiff_oni', 2), ('subdiff_oni', 3)]
|
||||
self.levels = [
|
||||
[1, 2, 3, 4],
|
||||
[2, 3, 4, 5],
|
||||
[6, 7, 8, 9],
|
||||
[10, 11, 12, 13],
|
||||
[10, 11, 12, 13]
|
||||
]
|
||||
self.selected_level = 1
|
||||
|
||||
self.diff_map = {
|
||||
Difficulty.EASY: subdiffs_easy,
|
||||
Difficulty.NORMAL: subdiffs_normal,
|
||||
Difficulty.HARD: subdiffs_hard,
|
||||
Difficulty.ONI: subdiffs_oni,
|
||||
Difficulty.URA: subdiffs_oni
|
||||
}
|
||||
self.selected_subdiff = self.diff_map[Difficulty(lowest_difficulty)]
|
||||
|
||||
def update(self, current_ms: float, current_difficulty: int):
|
||||
self.move.update(current_ms)
|
||||
if current_difficulty in self.diff_map:
|
||||
self.selected_subdiff = self.diff_map[Difficulty(current_difficulty)]
|
||||
|
||||
def move_left(self, difficulty: int):
|
||||
self.selected_index = max(0, self.selected_index - 1)
|
||||
self.selected_level = self.levels[difficulty][self.selected_index]
|
||||
|
||||
def move_right(self, difficulty: int):
|
||||
self.selected_index = min(3, self.selected_index + 1)
|
||||
self.selected_level = self.levels[difficulty][self.selected_index]
|
||||
|
||||
def confirm(self):
|
||||
global_data.modifiers[self.player_num].subdiff = self.selected_level
|
||||
|
||||
def draw(self):
|
||||
y = -self.move.attribute*1.05
|
||||
if self.is_selected:
|
||||
tex.draw_texture('ai_battle', 'box_bg_blur', y=y)
|
||||
tex.draw_texture('ai_battle', 'box_2', y=y)
|
||||
tex.draw_texture('ai_battle', 'subdiff_select_text', y=y)
|
||||
else:
|
||||
tex.draw_texture('ai_battle', 'box_1', y=y)
|
||||
|
||||
tex.draw_texture('ai_battle', 'subdiff_selector', x=self.selected_index*tex.textures['ai_battle']['subdiff_easy'].width, y=y)
|
||||
for i, subdiff in enumerate(self.selected_subdiff):
|
||||
name, frame = subdiff
|
||||
tex.draw_texture('ai_battle', name, frame=frame, x=i*tex.textures['ai_battle'][name].width, y=y)
|
||||
|
||||
tex.draw_texture('ai_battle', 'bottom_text', y=y)
|
||||
@@ -56,6 +56,9 @@ class EntryScreen(Screen):
|
||||
self.chara = Chara2D(0)
|
||||
self.announce_played = False
|
||||
self.players: list[Optional[EntryPlayer]] = [None, None]
|
||||
|
||||
self.text_cancel = OutlinedText(tex.skin_config["entry_cancel"].text[global_data.config["general"]["language"]], tex.skin_config["entry_cancel"].font_size, ray.WHITE, outline_thickness=4, spacing=-4)
|
||||
self.text_question = OutlinedText(tex.skin_config["entry_question"].text[global_data.config["general"]["language"]], tex.skin_config["entry_question"].font_size, ray.WHITE, outline_thickness=4, spacing=-1)
|
||||
audio.play_sound('bgm', 'music')
|
||||
|
||||
def on_screen_end(self, next_screen: str):
|
||||
@@ -164,7 +167,7 @@ class EntryScreen(Screen):
|
||||
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.text_question.draw(outline_color=ray.BLACK, x=tex.skin_config["entry_question"].x-self.text_question.texture.width//2, y=tex.skin_config["entry_question"].y, fade=fade)
|
||||
|
||||
self.chara.draw(tex.skin_config["chara_entry"].x, tex.skin_config["chara_entry"].y)
|
||||
|
||||
@@ -180,7 +183,7 @@ class EntryScreen(Screen):
|
||||
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.text_cancel.draw(outline_color=ray.BLACK, x=tex.skin_config["entry_cancel"].x-self.text_cancel.texture.width//2, y=tex.skin_config["entry_cancel"].y, fade=fade)
|
||||
self.nameplate.draw(tex.skin_config["nameplate_entry"].x, tex.skin_config["nameplate_entry"].y)
|
||||
|
||||
def draw_player_drum(self):
|
||||
@@ -449,8 +452,9 @@ class BoxManager:
|
||||
self.box_titles: list[OutlinedText] = [
|
||||
OutlinedText(tex.skin_config["entry_game"].text[global_data.config["general"]["language"]], tex.skin_config["entry_box_text"].font_size, ray.WHITE, outline_thickness=5, vertical=True),
|
||||
OutlinedText(tex.skin_config["entry_practice"].text[global_data.config["general"]["language"]], tex.skin_config["entry_box_text"].font_size, ray.WHITE, outline_thickness=5, vertical=True),
|
||||
OutlinedText(tex.skin_config["entry_settings"].text[global_data.config["general"]["language"]], tex.skin_config["entry_box_text"].font_size, ray.WHITE, outline_thickness=5, vertical=True)]
|
||||
self.box_locations = ["SONG_SELECT", "PRACTICE_SELECT", "SETTINGS"]
|
||||
OutlinedText(tex.skin_config["entry_ai_battle"].text[global_data.config["general"]["language"]], tex.skin_config["entry_box_text"].font_size, ray.WHITE, outline_thickness=5, vertical=True),
|
||||
OutlinedText(tex.skin_config["entry_settings"].text[global_data.config["general"]["language"]], tex.skin_config["entry_box_text"].font_size, ray.WHITE, outline_thickness=5, vertical=True),]
|
||||
self.box_locations = ["SONG_SELECT", "PRACTICE_SELECT", "AI_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
|
||||
@@ -509,7 +513,7 @@ class BoxManager:
|
||||
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", "PRACTICE_SELECT", "SETTINGS"]
|
||||
self.box_locations = ["SONG_SELECT_2P", "PRACTICE_SELECT", "AI_SELECT", "SETTINGS"]
|
||||
for i, box in enumerate(self.boxes):
|
||||
box.location = self.box_locations[i]
|
||||
self.fade_out.update(current_time_ms)
|
||||
|
||||
@@ -401,7 +401,7 @@ class Player:
|
||||
|
||||
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.play_notes, self.draw_note_list, self.draw_bar_list = deque(apply_modifiers(notes, self.modifiers)[0]), deque(apply_modifiers(notes, self.modifiers)[1]), deque(apply_modifiers(notes, self.modifiers)[2])
|
||||
|
||||
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}])
|
||||
@@ -449,7 +449,7 @@ class Player:
|
||||
self.bpm = 120
|
||||
if self.timeline and hasattr(self.timeline[self.timeline_index], 'bpm'):
|
||||
self.bpm = self.timeline[self.timeline_index].bpm
|
||||
last_note = self.draw_note_list[0]
|
||||
last_note = self.draw_note_list[0] if self.draw_note_list else self.branch_m[0].draw_notes[0]
|
||||
for note in chain(self.draw_note_list, self.draw_bar_list):
|
||||
self.get_load_time(note)
|
||||
if note.type == NoteType.TAIL:
|
||||
@@ -490,6 +490,7 @@ class Player:
|
||||
for branch in (self.branch_m, self.branch_e, self.branch_n):
|
||||
if branch:
|
||||
for section in branch:
|
||||
section.play_notes, section.draw_notes, section.bars = apply_modifiers(section, self.modifiers)
|
||||
if section.draw_notes:
|
||||
for note in section.draw_notes:
|
||||
self.get_load_time(note)
|
||||
@@ -1281,21 +1282,21 @@ class Player:
|
||||
modifiers_to_draw.append('mod_shinuchi')
|
||||
|
||||
# Speed modifiers
|
||||
if global_data.modifiers[self.player_num].speed >= 4:
|
||||
if self.modifiers.speed >= 4:
|
||||
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')
|
||||
elif global_data.modifiers[self.player_num].speed > 1:
|
||||
elif self.modifiers.speed > 1:
|
||||
modifiers_to_draw.append('mod_baisaku')
|
||||
|
||||
# Other modifiers
|
||||
if global_data.modifiers[self.player_num].display:
|
||||
if self.modifiers.display:
|
||||
modifiers_to_draw.append('mod_doron')
|
||||
if global_data.modifiers[self.player_num].inverse:
|
||||
if self.modifiers.inverse:
|
||||
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')
|
||||
elif global_data.modifiers[self.player_num].random == 1:
|
||||
elif self.modifiers.random == 1:
|
||||
modifiers_to_draw.append('mod_kimagure')
|
||||
|
||||
# Draw all modifiers in one batch
|
||||
@@ -1335,13 +1336,13 @@ class Player:
|
||||
self.judge_counter.draw()
|
||||
|
||||
# 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:
|
||||
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))
|
||||
|
||||
@@ -1359,6 +1360,8 @@ class Player:
|
||||
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.player_num == PlayerNum.AI:
|
||||
tex.draw_texture('lane', 'ai_lane_background')
|
||||
if self.branch_indicator is not None:
|
||||
self.branch_indicator.draw()
|
||||
if self.gauge is not None:
|
||||
@@ -1766,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)
|
||||
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)
|
||||
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:
|
||||
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))
|
||||
|
||||
@@ -45,6 +45,7 @@ class State:
|
||||
BROWSING = 0
|
||||
SONG_SELECTED = 1
|
||||
DIFF_SORTING = 2
|
||||
SEARCHING = 3
|
||||
|
||||
class SongSelectScreen(Screen):
|
||||
BOX_CENTER = 444
|
||||
@@ -69,6 +70,7 @@ class SongSelectScreen(Screen):
|
||||
self.game_transition = None
|
||||
self.demo_song = None
|
||||
self.diff_sort_selector = None
|
||||
self.search_box = None
|
||||
self.coin_overlay = CoinOverlay()
|
||||
self.allnet_indicator = AllNetIcon()
|
||||
self.indicator = Indicator(Indicator.State.SELECT)
|
||||
@@ -124,7 +126,7 @@ class SongSelectScreen(Screen):
|
||||
self.screen_init = False
|
||||
self.reset_demo_music()
|
||||
current_item = self.navigator.get_current_item()
|
||||
if isinstance(current_item, SongFile):
|
||||
if isinstance(current_item, SongFile) and self.player_1.is_ready:
|
||||
self.finalize_song(current_item)
|
||||
self.player_1.nameplate.unload()
|
||||
if isinstance(current_item.box, SongBox) and current_item.box.yellow_box is not None:
|
||||
@@ -167,6 +169,11 @@ class SongSelectScreen(Screen):
|
||||
self.diff_sort_selector = DiffSortSelect(self.navigator.diff_sort_statistics, self.navigator.diff_sort_diff, self.navigator.diff_sort_level)
|
||||
self.text_fade_in.start()
|
||||
self.text_fade_out.start()
|
||||
elif action == "search":
|
||||
self.state = State.SEARCHING
|
||||
self.search_box = SearchBox()
|
||||
self.text_fade_in.start()
|
||||
self.text_fade_out.start()
|
||||
elif action == "select_song":
|
||||
current_song = self.navigator.get_current_item()
|
||||
if isinstance(current_song, Directory) and current_song.box.genre_index == GenreIndex.DAN:
|
||||
@@ -225,6 +232,21 @@ class SongSelectScreen(Screen):
|
||||
self.navigator.diff_sort_level = level
|
||||
self.navigator.select_current_item()
|
||||
|
||||
def handle_input_search(self):
|
||||
if self.search_box is None:
|
||||
raise Exception("search box was not able to be created")
|
||||
|
||||
result = self.player_1.handle_input_search()
|
||||
self.search_box.current_search = self.player_1.search_string
|
||||
|
||||
if result is not None:
|
||||
self.state = State.BROWSING
|
||||
self.search_box = None
|
||||
self.text_fade_out.reset()
|
||||
self.text_fade_in.reset()
|
||||
self.navigator.current_search = result
|
||||
self.navigator.select_current_item()
|
||||
|
||||
def _cancel_selection(self):
|
||||
"""Reset to browsing state"""
|
||||
self.player_1.selected_song = False
|
||||
@@ -324,6 +346,9 @@ class SongSelectScreen(Screen):
|
||||
if self.diff_sort_selector is not None:
|
||||
self.diff_sort_selector.update(current_time)
|
||||
|
||||
if self.search_box is not None:
|
||||
self.search_box.update(current_time)
|
||||
|
||||
self.check_for_selection()
|
||||
|
||||
for song in self.navigator.items:
|
||||
@@ -356,7 +381,7 @@ class SongSelectScreen(Screen):
|
||||
def draw_players(self):
|
||||
self.player_1.draw(self.state)
|
||||
|
||||
def draw(self):
|
||||
def draw_background(self):
|
||||
width = tex.textures['box']['background'].width
|
||||
genre_index = self.genre_index
|
||||
last_genre_index = self.last_genre_index
|
||||
@@ -369,6 +394,26 @@ class SongSelectScreen(Screen):
|
||||
tex.draw_texture('box', 'background', frame=genre_index, x=i-self.background_move.attribute, fade=1 - self.background_fade_change.attribute)
|
||||
ray.end_shader_mode()
|
||||
|
||||
def draw_overlay(self):
|
||||
self.indicator.draw(tex.skin_config['song_select_indicator'].x, tex.skin_config['song_select_indicator'].y)
|
||||
|
||||
tex.draw_texture('global', 'song_num_bg', fade=0.75, x=-(self.song_num.texture.width-127), x2=(self.song_num.texture.width-127))
|
||||
self.song_num.draw(ray.BLACK, x=tex.skin_config["song_num"].x-self.song_num.texture.width, y=tex.skin_config["song_num"].y)
|
||||
if self.state == State.BROWSING or self.state == State.DIFF_SORTING:
|
||||
self.timer_browsing.draw()
|
||||
elif self.state == State.SONG_SELECTED:
|
||||
self.timer_selected.draw()
|
||||
self.coin_overlay.draw()
|
||||
if self.game_transition is not None:
|
||||
self.game_transition.draw()
|
||||
|
||||
if self.dan_transition.is_started:
|
||||
self.dan_transition.draw()
|
||||
self.allnet_indicator.draw()
|
||||
|
||||
def draw(self):
|
||||
self.draw_background()
|
||||
|
||||
self.draw_background_diffs()
|
||||
|
||||
if self.navigator.genre_bg is not None and self.state == State.BROWSING:
|
||||
@@ -386,6 +431,9 @@ class SongSelectScreen(Screen):
|
||||
if self.diff_sort_selector is not None:
|
||||
self.diff_sort_selector.draw()
|
||||
|
||||
if self.search_box is not None:
|
||||
self.search_box.draw()
|
||||
|
||||
if (self.player_1.selected_song and self.state == State.SONG_SELECTED):
|
||||
tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute)
|
||||
elif self.state == State.DIFF_SORTING:
|
||||
@@ -400,21 +448,7 @@ class SongSelectScreen(Screen):
|
||||
if isinstance(curr_item, SongFile):
|
||||
curr_item.box.draw_score_history()
|
||||
|
||||
self.indicator.draw(tex.skin_config['song_select_indicator'].x, tex.skin_config['song_select_indicator'].y)
|
||||
|
||||
tex.draw_texture('global', 'song_num_bg', fade=0.75, x=-(self.song_num.texture.width-127), x2=(self.song_num.texture.width-127))
|
||||
self.song_num.draw(ray.BLACK, x=tex.skin_config["song_num"].x-self.song_num.texture.width, y=tex.skin_config["song_num"].y)
|
||||
if self.state == State.BROWSING or self.state == State.DIFF_SORTING:
|
||||
self.timer_browsing.draw()
|
||||
elif self.state == State.SONG_SELECTED:
|
||||
self.timer_selected.draw()
|
||||
self.coin_overlay.draw()
|
||||
if self.game_transition is not None:
|
||||
self.game_transition.draw()
|
||||
|
||||
if self.dan_transition.is_started:
|
||||
self.dan_transition.draw()
|
||||
self.allnet_indicator.draw()
|
||||
self.draw_overlay()
|
||||
|
||||
class SongSelectPlayer:
|
||||
def __init__(self, player_num: PlayerNum, text_fade_in):
|
||||
@@ -428,6 +462,7 @@ class SongSelectPlayer:
|
||||
self.diff_select_move_right = False
|
||||
self.neiro_selector = None
|
||||
self.modifier_selector = None
|
||||
self.search_string = ''
|
||||
|
||||
# References to shared animations
|
||||
self.diff_selector_move_1 = tex.get_animation(26, is_copy=True)
|
||||
@@ -511,6 +546,8 @@ class SongSelectPlayer:
|
||||
return "go_back"
|
||||
elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[3]:
|
||||
return "diff_sort"
|
||||
elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[5]:
|
||||
return "search"
|
||||
else:
|
||||
return "select_song"
|
||||
|
||||
@@ -538,6 +575,20 @@ class SongSelectPlayer:
|
||||
|
||||
return None
|
||||
|
||||
def handle_input_search(self):
|
||||
if ray.is_key_pressed(ray.KeyboardKey.KEY_BACKSPACE):
|
||||
self.search_string = self.search_string[:-1]
|
||||
elif ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER):
|
||||
result = self.search_string
|
||||
self.search_string = ''
|
||||
return result
|
||||
key = ray.get_char_pressed()
|
||||
|
||||
while key > 0:
|
||||
self.search_string += chr(key)
|
||||
key = ray.get_char_pressed()
|
||||
return None
|
||||
|
||||
def handle_input(self, state, screen):
|
||||
"""Main input dispatcher. Delegates to state-specific handlers."""
|
||||
if self.is_voice_playing() or self.is_ready:
|
||||
@@ -549,6 +600,8 @@ class SongSelectPlayer:
|
||||
screen.handle_input_selected()
|
||||
elif state == State.DIFF_SORTING:
|
||||
screen.handle_input_diff_sort()
|
||||
elif state == State.SEARCHING:
|
||||
screen.handle_input_search()
|
||||
|
||||
def handle_input_selected(self, current_item):
|
||||
"""Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None"""
|
||||
@@ -592,8 +645,6 @@ class SongSelectPlayer:
|
||||
if is_l_kat_pressed(self.player_num) or is_r_kat_pressed(self.player_num):
|
||||
audio.play_sound('kat', 'sound')
|
||||
selected_song = current_item
|
||||
if isinstance(selected_song, Directory):
|
||||
raise Exception("Directory was chosen instead of song")
|
||||
diffs = sorted(selected_song.tja.metadata.course_data)
|
||||
prev_diff = self.selected_difficulty
|
||||
ret_val = None
|
||||
@@ -1027,12 +1078,37 @@ class DiffSortSelect:
|
||||
else:
|
||||
self.draw_diff_select()
|
||||
|
||||
class SearchBox:
|
||||
def __init__(self):
|
||||
self.bg_resize = tex.get_animation(19)
|
||||
self.diff_fade_in = tex.get_animation(20)
|
||||
self.bg_resize.start()
|
||||
self.diff_fade_in.start()
|
||||
self.current_search = ''
|
||||
|
||||
def update(self, current_ms):
|
||||
self.bg_resize.update(current_ms)
|
||||
self.diff_fade_in.update(current_ms)
|
||||
|
||||
def draw(self):
|
||||
ray.draw_rectangle(0, 0, tex.screen_width, tex.screen_height, ray.fade(ray.BLACK, 0.6))
|
||||
tex.draw_texture('diff_sort', 'background', scale=self.bg_resize.attribute, center=True)
|
||||
background = tex.textures['diff_sort']['background']
|
||||
fade = self.diff_fade_in.attribute
|
||||
text_box_width, text_box_height = 400 * tex.screen_scale, 60 * tex.screen_scale
|
||||
x, y = background.width//2 + background.x[0] - text_box_width//2, background.height//2 + background.y[0] - text_box_height//2
|
||||
text_box = ray.Rectangle(x, y, text_box_width, text_box_height)
|
||||
ray.draw_rectangle_rec(text_box, ray.fade(ray.LIGHTGRAY, fade))
|
||||
ray.draw_rectangle_lines(int(text_box.x), int(text_box.y), int(text_box.width), int(text_box.height), ray.fade(ray.DARKGRAY, fade))
|
||||
text_size = ray.measure_text_ex(global_data.font, self.current_search, int(30 * tex.screen_scale), 1)
|
||||
ray.draw_text_ex(global_data.font, self.current_search, ray.Vector2(x + text_box_width//2 - text_size.x//2, y + text_box_height//2 - text_size.y//2), int(30 * tex.screen_scale), 1, ray.BLACK)
|
||||
|
||||
class NeiroSelector:
|
||||
"""The menu for selecting the game hitsounds."""
|
||||
def __init__(self, player_num: PlayerNum):
|
||||
self.player_num = player_num
|
||||
self.selected_sound = global_data.hit_sound[self.player_num]
|
||||
with open(Path("Sounds") / 'hit_sounds' / 'neiro_list.txt', encoding='utf-8-sig') as neiro_list:
|
||||
with open(Path('Skins') / global_data.config["paths"]["skin"] / Path("Sounds") / 'hit_sounds' / 'neiro_list.txt', encoding='utf-8-sig') as neiro_list:
|
||||
self.sounds = neiro_list.readlines()
|
||||
self.sounds.append('無音')
|
||||
self.load_sound()
|
||||
@@ -1053,9 +1129,9 @@ class NeiroSelector:
|
||||
if self.selected_sound == len(self.sounds):
|
||||
return
|
||||
if self.selected_sound == 0:
|
||||
self.curr_sound = audio.load_sound(Path("Sounds") / "hit_sounds" / str(self.selected_sound) / "don.wav", 'hit_sound')
|
||||
self.curr_sound = audio.load_sound(Path('Skins') / global_data.config["paths"]["skin"] / Path("Sounds") / "hit_sounds" / str(self.selected_sound) / "don.wav", 'hit_sound')
|
||||
else:
|
||||
self.curr_sound = audio.load_sound(Path("Sounds") / "hit_sounds" / str(self.selected_sound) / "don.ogg", 'hit_sound')
|
||||
self.curr_sound = audio.load_sound(Path('Skins') / global_data.config["paths"]["skin"] / Path("Sounds") / "hit_sounds" / str(self.selected_sound) / "don.ogg", 'hit_sound')
|
||||
|
||||
def move_left(self):
|
||||
if self.move.is_started and not self.move.is_finished:
|
||||
@@ -1147,16 +1223,23 @@ class ModifierSelector:
|
||||
"inverse": "mod_abekobe",
|
||||
"random": "mod_kimagure"
|
||||
}
|
||||
NAME_MAP = {
|
||||
NAME_MAP_JA = {
|
||||
"auto": "オート",
|
||||
"speed": "はやさ",
|
||||
"display": "ドロン",
|
||||
"inverse": "あべこべ",
|
||||
"random": "ランダム"
|
||||
}
|
||||
NAME_MAP_EN = {
|
||||
"auto": "Auto",
|
||||
"speed": "Speed",
|
||||
"display": "Display",
|
||||
"inverse": "Inverse",
|
||||
"random": "Random"
|
||||
}
|
||||
def __init__(self, player_num: PlayerNum):
|
||||
self.player_num = player_num
|
||||
self.mods = fields(Modifiers)
|
||||
self.mods = fields(Modifiers)[:-1]
|
||||
self.current_mod_index = 0
|
||||
self.is_confirmed = False
|
||||
self.is_finished = False
|
||||
@@ -1168,19 +1251,24 @@ class ModifierSelector:
|
||||
self.fade_sideways = tex.get_animation(32, is_copy=True)
|
||||
self.direction = -1
|
||||
audio.play_sound(f'voice_options_{self.player_num}p', 'sound')
|
||||
self.text_name = [OutlinedText(ModifierSelector.NAME_MAP[mod.name], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5) for mod in self.mods]
|
||||
self.text_true = OutlinedText('する', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_false = OutlinedText('しない', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.language = global_data.config["general"]["language"]
|
||||
if self.language == 'en':
|
||||
name_map = ModifierSelector.NAME_MAP_EN
|
||||
else:
|
||||
name_map = ModifierSelector.NAME_MAP_JA
|
||||
self.text_name = [OutlinedText(name_map[mod.name], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5) for mod in self.mods]
|
||||
self.text_true = OutlinedText(tex.skin_config["modifier_text_true"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_false = OutlinedText(tex.skin_config["modifier_text_false"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_speed = OutlinedText(str(global_data.modifiers[self.player_num].speed), tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_kimagure = OutlinedText('きまぐれ', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_detarame = OutlinedText('でたらめ', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_kimagure = OutlinedText(tex.skin_config["modifier_text_kimagure"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_detarame = OutlinedText(tex.skin_config["modifier_text_detarame"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
|
||||
# Secondary text objects for animation
|
||||
self.text_true_2 = OutlinedText('する', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_false_2 = OutlinedText('しない', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_true_2 = OutlinedText(tex.skin_config["modifier_text_true"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_false_2 = OutlinedText(tex.skin_config["modifier_text_false"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_speed_2 = OutlinedText(str(global_data.modifiers[self.player_num].speed), tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_kimagure_2 = OutlinedText('きまぐれ', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_detarame_2 = OutlinedText('でたらめ', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_kimagure_2 = OutlinedText(tex.skin_config["modifier_text_kimagure"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_detarame_2 = OutlinedText(tex.skin_config["modifier_text_detarame"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
|
||||
def update(self, current_ms):
|
||||
self.is_finished = self.is_confirmed and self.move.is_finished
|
||||
@@ -1221,20 +1309,20 @@ class ModifierSelector:
|
||||
if current_value:
|
||||
self.text_true.unload()
|
||||
self.text_true = self.text_true_2
|
||||
self.text_true_2 = OutlinedText('する', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_true_2 = OutlinedText(tex.skin_config["modifier_text_true"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
else:
|
||||
self.text_false.unload()
|
||||
self.text_false = self.text_false_2
|
||||
self.text_false_2 = OutlinedText('しない', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_false_2 = OutlinedText(tex.skin_config["modifier_text_false"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
elif current_mod.name == 'random':
|
||||
if current_value == 1:
|
||||
self.text_kimagure.unload()
|
||||
self.text_kimagure = self.text_kimagure_2
|
||||
self.text_kimagure_2 = OutlinedText('きまぐれ', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_kimagure_2 = OutlinedText(tex.skin_config["modifier_text_kimagure"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
elif current_value == 2:
|
||||
self.text_detarame.unload()
|
||||
self.text_detarame = self.text_detarame_2
|
||||
self.text_detarame_2 = OutlinedText('でたらめ', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
self.text_detarame_2 = OutlinedText(tex.skin_config["modifier_text_detarame"].text[self.language], tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
||||
|
||||
def left(self):
|
||||
if self.is_confirmed:
|
||||
|
||||
@@ -43,7 +43,7 @@ class TitleScreen(Screen):
|
||||
self.coin_overlay = CoinOverlay()
|
||||
self.allnet_indicator = AllNetIcon()
|
||||
self.entry_overlay = EntryOverlay()
|
||||
self.hit_taiko_text = OutlinedText(global_tex.skin_config["hit_taiko_to_start"].text[global_data.config["general"]["language"]], tex.skin_config["hit_taiko_to_start"].font_size, ray.WHITE, spacing=5)
|
||||
self.hit_taiko_text = OutlinedText(global_tex.skin_config["hit_taiko_to_start"].text[global_data.config["general"]["language"]], tex.skin_config["hit_taiko_to_start"].font_size, ray.WHITE, spacing=5, outline_thickness=4)
|
||||
self.fade_out = tex.get_animation(13)
|
||||
self.text_overlay_fade = tex.get_animation(14)
|
||||
|
||||
|
||||
695
test/libs/test_animation.py
Normal file
695
test/libs/test_animation.py
Normal file
@@ -0,0 +1,695 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from libs.animation import (
|
||||
Animation,
|
||||
BaseAnimation,
|
||||
FadeAnimation,
|
||||
MoveAnimation,
|
||||
TextStretchAnimation,
|
||||
TextureChangeAnimation,
|
||||
TextureResizeAnimation,
|
||||
parse_animations,
|
||||
)
|
||||
|
||||
|
||||
class TestBaseAnimation(unittest.TestCase):
|
||||
"""Test cases for the BaseAnimation class."""
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
@patch('libs.animation.global_data')
|
||||
def setUp(self, mock_global_data, mock_get_ms):
|
||||
"""Set up test fixtures."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
mock_global_data.input_locked = 0
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_initialization(self, mock_get_ms):
|
||||
"""Test basic initialization of BaseAnimation."""
|
||||
mock_get_ms.return_value = 100.0
|
||||
|
||||
anim = BaseAnimation(duration=1000.0, delay=100.0, loop=True, lock_input=True)
|
||||
|
||||
self.assertEqual(anim.duration, 1000.0)
|
||||
self.assertEqual(anim.delay, 100.0)
|
||||
self.assertEqual(anim.delay_saved, 100.0)
|
||||
self.assertEqual(anim.start_ms, 100.0)
|
||||
self.assertFalse(anim.is_finished)
|
||||
self.assertEqual(anim.attribute, 0)
|
||||
self.assertFalse(anim.is_started)
|
||||
self.assertTrue(anim.loop)
|
||||
self.assertTrue(anim.lock_input)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
@patch('libs.animation.global_data')
|
||||
def test_start(self, mock_global_data, mock_get_ms):
|
||||
"""Test starting an animation."""
|
||||
mock_get_ms.return_value = 200.0
|
||||
mock_global_data.input_locked = 0
|
||||
|
||||
anim = BaseAnimation(duration=1000.0, lock_input=True)
|
||||
anim.start()
|
||||
|
||||
self.assertTrue(anim.is_started)
|
||||
self.assertFalse(anim.is_finished)
|
||||
self.assertEqual(mock_global_data.input_locked, 1)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
@patch('libs.animation.global_data')
|
||||
def test_restart(self, mock_global_data, mock_get_ms):
|
||||
"""Test restarting an animation."""
|
||||
mock_get_ms.side_effect = [0.0, 500.0, 1000.0]
|
||||
mock_global_data.input_locked = 0
|
||||
|
||||
anim = BaseAnimation(duration=1000.0, delay=100.0, lock_input=True)
|
||||
anim.is_finished = True
|
||||
anim.delay = 0.0
|
||||
|
||||
anim.restart()
|
||||
|
||||
self.assertEqual(anim.start_ms, 500.0)
|
||||
self.assertFalse(anim.is_finished)
|
||||
self.assertEqual(anim.delay, 100.0)
|
||||
self.assertEqual(mock_global_data.input_locked, 1)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
@patch('libs.animation.global_data')
|
||||
def test_pause_unpause(self, mock_global_data, mock_get_ms):
|
||||
"""Test pausing and unpausing."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
mock_global_data.input_locked = 1
|
||||
|
||||
anim = BaseAnimation(duration=1000.0, lock_input=True)
|
||||
anim.is_started = True
|
||||
|
||||
anim.pause()
|
||||
self.assertFalse(anim.is_started)
|
||||
self.assertEqual(mock_global_data.input_locked, 0)
|
||||
|
||||
anim.unpause()
|
||||
self.assertTrue(anim.is_started)
|
||||
self.assertEqual(mock_global_data.input_locked, 1)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_loop_restarts(self, mock_get_ms):
|
||||
"""Test that looped animations restart when finished."""
|
||||
mock_get_ms.side_effect = [0.0, 100.0]
|
||||
|
||||
anim = BaseAnimation(duration=1000.0, loop=True)
|
||||
anim.is_finished = True
|
||||
|
||||
with patch.object(anim, 'restart') as mock_restart:
|
||||
anim.update(100.0)
|
||||
mock_restart.assert_called_once()
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
@patch('libs.animation.global_data')
|
||||
def test_input_lock_unlock(self, mock_global_data, mock_get_ms):
|
||||
"""Test input locking mechanism."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
mock_global_data.input_locked = 1
|
||||
|
||||
anim = BaseAnimation(duration=1000.0, lock_input=True)
|
||||
anim.is_finished = True
|
||||
anim.unlocked = False
|
||||
|
||||
anim.update(100.0)
|
||||
|
||||
self.assertTrue(anim.unlocked)
|
||||
self.assertEqual(mock_global_data.input_locked, 0)
|
||||
|
||||
def test_easing_functions(self):
|
||||
"""Test easing functions produce expected values."""
|
||||
anim = BaseAnimation(duration=1000.0)
|
||||
|
||||
# Test quadratic ease in
|
||||
self.assertAlmostEqual(anim._ease_in(0.5, "quadratic"), 0.25)
|
||||
self.assertAlmostEqual(anim._ease_in(1.0, "quadratic"), 1.0)
|
||||
|
||||
# Test cubic ease in
|
||||
self.assertAlmostEqual(anim._ease_in(0.5, "cubic"), 0.125)
|
||||
|
||||
# Test exponential ease in
|
||||
self.assertEqual(anim._ease_in(0.0, "exponential"), 0)
|
||||
|
||||
# Test quadratic ease out
|
||||
self.assertAlmostEqual(anim._ease_out(0.5, "quadratic"), 0.75)
|
||||
|
||||
# Test cubic ease out
|
||||
self.assertAlmostEqual(anim._ease_out(0.5, "cubic"), 0.875)
|
||||
|
||||
# Test exponential ease out
|
||||
self.assertEqual(anim._ease_out(1.0, "exponential"), 1)
|
||||
|
||||
|
||||
class TestFadeAnimation(unittest.TestCase):
|
||||
"""Test cases for the FadeAnimation class."""
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_initialization(self, mock_get_ms):
|
||||
"""Test fade animation initialization."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = FadeAnimation(
|
||||
duration=1000.0,
|
||||
initial_opacity=1.0,
|
||||
final_opacity=0.0,
|
||||
delay=100.0,
|
||||
ease_in="quadratic"
|
||||
)
|
||||
|
||||
self.assertEqual(anim.initial_opacity, 1.0)
|
||||
self.assertEqual(anim.final_opacity, 0.0)
|
||||
self.assertEqual(anim.attribute, 1.0)
|
||||
self.assertEqual(anim.ease_in, "quadratic")
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_fade_during_delay(self, mock_get_ms):
|
||||
"""Test that opacity stays at initial during delay."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = FadeAnimation(duration=1000.0, initial_opacity=1.0, final_opacity=0.0, delay=500.0)
|
||||
anim.start()
|
||||
|
||||
anim.update(250.0) # Within delay period
|
||||
|
||||
self.assertEqual(anim.attribute, 1.0)
|
||||
self.assertFalse(anim.is_finished)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_fade_progression(self, mock_get_ms):
|
||||
"""Test fade progresses correctly."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = FadeAnimation(duration=1000.0, initial_opacity=1.0, final_opacity=0.0)
|
||||
anim.start()
|
||||
|
||||
anim.update(500.0) # Halfway through
|
||||
|
||||
self.assertAlmostEqual(anim.attribute, 0.5, places=2)
|
||||
self.assertFalse(anim.is_finished)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_fade_completion(self, mock_get_ms):
|
||||
"""Test fade completes at final opacity."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = FadeAnimation(duration=1000.0, initial_opacity=1.0, final_opacity=0.0)
|
||||
anim.start()
|
||||
|
||||
anim.update(1000.0) # End of animation
|
||||
|
||||
self.assertEqual(anim.attribute, 0.0)
|
||||
self.assertTrue(anim.is_finished)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_fade_with_reverse_delay(self, mock_get_ms):
|
||||
"""Test fade reverses after reverse_delay."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = FadeAnimation(
|
||||
duration=1000.0,
|
||||
initial_opacity=1.0,
|
||||
final_opacity=0.0,
|
||||
reverse_delay=200.0
|
||||
)
|
||||
anim.start()
|
||||
|
||||
anim.update(1000.0) # Complete first fade
|
||||
|
||||
self.assertEqual(anim.attribute, 0.0)
|
||||
self.assertTrue(anim.is_reversing)
|
||||
self.assertEqual(anim.initial_opacity, 0.0)
|
||||
self.assertEqual(anim.final_opacity, 1.0)
|
||||
self.assertFalse(anim.is_finished)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_fade_with_easing(self, mock_get_ms):
|
||||
"""Test fade applies easing correctly."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = FadeAnimation(
|
||||
duration=1000.0,
|
||||
initial_opacity=0.0,
|
||||
final_opacity=1.0,
|
||||
ease_in="quadratic"
|
||||
)
|
||||
anim.start()
|
||||
|
||||
anim.update(500.0) # Halfway
|
||||
|
||||
# With quadratic ease in, at 0.5 progress we should have 0.25
|
||||
self.assertAlmostEqual(anim.attribute, 0.25, places=2)
|
||||
|
||||
|
||||
class TestMoveAnimation(unittest.TestCase):
|
||||
"""Test cases for the MoveAnimation class."""
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_initialization(self, mock_get_ms):
|
||||
"""Test move animation initialization."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = MoveAnimation(
|
||||
duration=1000.0,
|
||||
start_position=0,
|
||||
total_distance=100,
|
||||
delay=50.0
|
||||
)
|
||||
|
||||
self.assertEqual(anim.start_position, 0)
|
||||
self.assertEqual(anim.total_distance, 100)
|
||||
self.assertEqual(anim.delay, 50.0)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_move_during_delay(self, mock_get_ms):
|
||||
"""Test position stays at start during delay."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = MoveAnimation(duration=1000.0, start_position=50, total_distance=100, delay=200.0)
|
||||
anim.start()
|
||||
|
||||
anim.update(100.0) # Within delay
|
||||
|
||||
self.assertEqual(anim.attribute, 50)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_move_progression(self, mock_get_ms):
|
||||
"""Test move progresses correctly."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = MoveAnimation(duration=1000.0, start_position=0, total_distance=100)
|
||||
anim.start()
|
||||
|
||||
anim.update(500.0) # Halfway
|
||||
|
||||
self.assertAlmostEqual(anim.attribute, 50.0, places=2)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_move_completion(self, mock_get_ms):
|
||||
"""Test move completes at final position."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = MoveAnimation(duration=1000.0, start_position=0, total_distance=100)
|
||||
anim.start()
|
||||
|
||||
anim.update(1000.0)
|
||||
|
||||
self.assertEqual(anim.attribute, 100)
|
||||
self.assertTrue(anim.is_finished)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_move_with_reverse_delay(self, mock_get_ms):
|
||||
"""Test move reverses after reverse_delay."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = MoveAnimation(
|
||||
duration=1000.0,
|
||||
start_position=0,
|
||||
total_distance=100,
|
||||
reverse_delay=100.0
|
||||
)
|
||||
anim.start()
|
||||
|
||||
anim.update(1000.0) # Complete first move
|
||||
|
||||
self.assertEqual(anim.start_position, 100)
|
||||
self.assertEqual(anim.total_distance, -100)
|
||||
self.assertIsNone(anim.reverse_delay)
|
||||
self.assertFalse(anim.is_finished)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_move_with_easing(self, mock_get_ms):
|
||||
"""Test move applies easing."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = MoveAnimation(
|
||||
duration=1000.0,
|
||||
start_position=0,
|
||||
total_distance=100,
|
||||
ease_out="quadratic"
|
||||
)
|
||||
anim.start()
|
||||
|
||||
anim.update(500.0)
|
||||
|
||||
# With quadratic ease out, at 0.5 progress we should have 0.75
|
||||
self.assertAlmostEqual(anim.attribute, 75.0, places=2)
|
||||
|
||||
|
||||
class TestTextureChangeAnimation(unittest.TestCase):
|
||||
"""Test cases for the TextureChangeAnimation class."""
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_initialization(self, mock_get_ms):
|
||||
"""Test texture change animation initialization."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
textures = [(0.0, 100.0, 0), (100.0, 200.0, 1), (200.0, 300.0, 2)]
|
||||
anim = TextureChangeAnimation(duration=300.0, textures=textures)
|
||||
|
||||
self.assertEqual(anim.textures, textures)
|
||||
self.assertEqual(anim.attribute, 0) # First texture index
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_texture_change_progression(self, mock_get_ms):
|
||||
"""Test texture changes at correct times."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
textures = [(0.0, 100.0, 0), (100.0, 200.0, 1), (200.0, 300.0, 2)]
|
||||
anim = TextureChangeAnimation(duration=300.0, textures=textures)
|
||||
anim.start()
|
||||
|
||||
anim.update(50.0)
|
||||
self.assertEqual(anim.attribute, 0)
|
||||
|
||||
anim.update(150.0)
|
||||
self.assertEqual(anim.attribute, 1)
|
||||
|
||||
anim.update(250.0)
|
||||
self.assertEqual(anim.attribute, 2)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_texture_change_completion(self, mock_get_ms):
|
||||
"""Test texture change completes."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
textures = [(0.0, 100.0, 0), (100.0, 200.0, 1)]
|
||||
anim = TextureChangeAnimation(duration=200.0, textures=textures)
|
||||
anim.start()
|
||||
|
||||
anim.update(300.0) # Past duration
|
||||
|
||||
self.assertTrue(anim.is_finished)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_texture_change_with_delay(self, mock_get_ms):
|
||||
"""Test texture change respects delay."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
textures = [(0.0, 100.0, 0), (100.0, 200.0, 1)]
|
||||
anim = TextureChangeAnimation(duration=200.0, textures=textures, delay=100.0)
|
||||
anim.start()
|
||||
|
||||
anim.update(50.0) # During delay
|
||||
self.assertEqual(anim.attribute, 0)
|
||||
|
||||
anim.update(150.0) # 50ms into animation (after delay)
|
||||
self.assertEqual(anim.attribute, 0)
|
||||
|
||||
|
||||
class TestTextureResizeAnimation(unittest.TestCase):
|
||||
"""Test cases for the TextureResizeAnimation class."""
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_initialization(self, mock_get_ms):
|
||||
"""Test texture resize initialization."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = TextureResizeAnimation(
|
||||
duration=1000.0,
|
||||
initial_size=1.0,
|
||||
final_size=2.0
|
||||
)
|
||||
|
||||
self.assertEqual(anim.initial_size, 1.0)
|
||||
self.assertEqual(anim.final_size, 2.0)
|
||||
self.assertEqual(anim.attribute, 1.0)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_resize_progression(self, mock_get_ms):
|
||||
"""Test resize progresses correctly."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = TextureResizeAnimation(duration=1000.0, initial_size=1.0, final_size=2.0)
|
||||
anim.start()
|
||||
|
||||
anim.update(500.0) # Halfway
|
||||
|
||||
self.assertAlmostEqual(anim.attribute, 1.5, places=2)
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_resize_completion(self, mock_get_ms):
|
||||
"""Test resize completes."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = TextureResizeAnimation(duration=1000.0, initial_size=1.0, final_size=0.5)
|
||||
anim.start()
|
||||
|
||||
anim.update(1000.0)
|
||||
|
||||
self.assertEqual(anim.attribute, 0.5)
|
||||
self.assertTrue(anim.is_finished)
|
||||
|
||||
|
||||
class TestTextStretchAnimation(unittest.TestCase):
|
||||
"""Test cases for the TextStretchAnimation class."""
|
||||
|
||||
@patch('libs.animation.get_current_ms')
|
||||
def test_stretch_phases(self, mock_get_ms):
|
||||
"""Test text stretch animation phases."""
|
||||
mock_get_ms.return_value = 0.0
|
||||
|
||||
anim = TextStretchAnimation(duration=100.0)
|
||||
anim.start()
|
||||
|
||||
# Phase 1: Growing
|
||||
anim.update(50.0)
|
||||
self.assertGreater(anim.attribute, 2)
|
||||
|
||||
# Phase 2: Shrinking back
|
||||
anim.update(150.0)
|
||||
self.assertGreater(anim.attribute, 0)
|
||||
|
||||
# Phase 3: Finished
|
||||
anim.update(300.0)
|
||||
self.assertEqual(anim.attribute, 0)
|
||||
self.assertTrue(anim.is_finished)
|
||||
|
||||
|
||||
class TestAnimationFactory(unittest.TestCase):
|
||||
"""Test cases for the Animation factory class."""
|
||||
|
||||
def test_create_fade(self):
|
||||
"""Test factory creates fade animation."""
|
||||
anim = Animation.create_fade(1000.0, initial_opacity=1.0, final_opacity=0.0)
|
||||
|
||||
self.assertIsInstance(anim, FadeAnimation)
|
||||
self.assertEqual(anim.duration, 1000.0)
|
||||
self.assertEqual(anim.initial_opacity, 1.0)
|
||||
|
||||
def test_create_move(self):
|
||||
"""Test factory creates move animation."""
|
||||
anim = Animation.create_move(1000.0, start_position=0, total_distance=100)
|
||||
|
||||
self.assertIsInstance(anim, MoveAnimation)
|
||||
self.assertEqual(anim.duration, 1000.0)
|
||||
self.assertEqual(anim.total_distance, 100)
|
||||
|
||||
def test_create_texture_change(self):
|
||||
"""Test factory creates texture change animation."""
|
||||
textures = [(0.0, 100.0, 0)]
|
||||
anim = Animation.create_texture_change(1000.0, textures=textures)
|
||||
|
||||
self.assertIsInstance(anim, TextureChangeAnimation)
|
||||
self.assertEqual(anim.textures, textures)
|
||||
|
||||
def test_create_texture_resize(self):
|
||||
"""Test factory creates texture resize animation."""
|
||||
anim = Animation.create_texture_resize(1000.0, initial_size=1.0, final_size=2.0)
|
||||
|
||||
self.assertIsInstance(anim, TextureResizeAnimation)
|
||||
self.assertEqual(anim.initial_size, 1.0)
|
||||
|
||||
|
||||
class TestParseAnimations(unittest.TestCase):
|
||||
"""Test cases for parse_animations function."""
|
||||
|
||||
def test_parse_basic_animation(self):
|
||||
"""Test parsing a simple animation."""
|
||||
animation_json = [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "fade",
|
||||
"duration": 1000.0,
|
||||
"initial_opacity": 1.0,
|
||||
"final_opacity": 0.0
|
||||
}
|
||||
]
|
||||
|
||||
result = parse_animations(animation_json)
|
||||
|
||||
self.assertIn(1, result)
|
||||
self.assertIsInstance(result[1], FadeAnimation)
|
||||
self.assertEqual(result[1].duration, 1000.0)
|
||||
|
||||
def test_parse_multiple_animations(self):
|
||||
"""Test parsing multiple animations."""
|
||||
animation_json = [
|
||||
{"id": 1, "type": "fade", "duration": 1000.0},
|
||||
{"id": 2, "type": "move", "duration": 500.0, "total_distance": 50}
|
||||
]
|
||||
|
||||
result = parse_animations(animation_json)
|
||||
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertIsInstance(result[1], FadeAnimation)
|
||||
self.assertIsInstance(result[2], MoveAnimation)
|
||||
|
||||
def test_parse_with_reference(self):
|
||||
"""Test parsing animations with references."""
|
||||
animation_json = [
|
||||
{"id": 1, "type": "fade", "duration": 1000.0, "initial_opacity": 1.0},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "fade",
|
||||
"duration": {"reference_id": 1, "property": "duration"},
|
||||
"initial_opacity": 0.5
|
||||
}
|
||||
]
|
||||
|
||||
result = parse_animations(animation_json)
|
||||
|
||||
self.assertEqual(result[2].duration, 1000.0)
|
||||
self.assertEqual(result[2].initial_opacity, 0.5)
|
||||
|
||||
def test_parse_with_reference_and_init_val(self):
|
||||
"""Test parsing with reference and init_val modifier."""
|
||||
animation_json = [
|
||||
{"id": 1, "type": "fade", "duration": 1000.0},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "fade",
|
||||
"duration": {
|
||||
"reference_id": 1,
|
||||
"property": "duration",
|
||||
"init_val": 500.0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
result = parse_animations(animation_json)
|
||||
|
||||
self.assertEqual(result[2].duration, 1500.0)
|
||||
|
||||
def test_parse_missing_id_raises_error(self):
|
||||
"""Test that missing id raises exception."""
|
||||
animation_json = [
|
||||
{"type": "fade", "duration": 1000.0}
|
||||
]
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
parse_animations(animation_json)
|
||||
self.assertIn("requires id", str(context.exception))
|
||||
|
||||
def test_parse_missing_type_raises_error(self):
|
||||
"""Test that missing type raises exception."""
|
||||
animation_json = [
|
||||
{"id": 1, "duration": 1000.0}
|
||||
]
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
parse_animations(animation_json)
|
||||
self.assertIn("requires type", str(context.exception))
|
||||
|
||||
def test_parse_circular_reference_raises_error(self):
|
||||
"""Test that circular references are detected."""
|
||||
animation_json = [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "fade",
|
||||
"duration": {"reference_id": 2, "property": "duration"}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "fade",
|
||||
"duration": {"reference_id": 1, "property": "duration"}
|
||||
}
|
||||
]
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
parse_animations(animation_json)
|
||||
self.assertIn("Circular reference", str(context.exception))
|
||||
|
||||
def test_parse_unknown_type_raises_error(self):
|
||||
"""Test that unknown animation type raises exception."""
|
||||
animation_json = [
|
||||
{"id": 1, "type": "unknown_type", "duration": 1000.0}
|
||||
]
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
parse_animations(animation_json)
|
||||
self.assertIn("Unknown Animation type", str(context.exception))
|
||||
|
||||
def test_parse_missing_reference_property_raises_error(self):
|
||||
"""Test that missing reference property raises exception."""
|
||||
animation_json = [
|
||||
{"id": 1, "type": "fade", "duration": 1000.0},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "fade",
|
||||
"duration": {"reference_id": 1}
|
||||
}
|
||||
]
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
parse_animations(animation_json)
|
||||
self.assertIn("requires 'property'", str(context.exception))
|
||||
|
||||
def test_parse_nonexistent_reference_raises_error(self):
|
||||
"""Test that referencing nonexistent animation raises exception."""
|
||||
animation_json = [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "fade",
|
||||
"duration": {"reference_id": 999, "property": "duration"}
|
||||
}
|
||||
]
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
parse_animations(animation_json)
|
||||
self.assertIn("not found", str(context.exception))
|
||||
|
||||
def test_parse_ignores_comments(self):
|
||||
"""Test that comments are ignored during parsing."""
|
||||
animation_json = [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "fade",
|
||||
"duration": 1000.0,
|
||||
"comment": "This is a fade animation"
|
||||
}
|
||||
]
|
||||
|
||||
result = parse_animations(animation_json)
|
||||
|
||||
self.assertIn(1, result)
|
||||
self.assertIsInstance(result[1], FadeAnimation)
|
||||
|
||||
def test_parse_nested_references(self):
|
||||
"""Test parsing nested reference chains."""
|
||||
animation_json = [
|
||||
{"id": 1, "type": "fade", "duration": 1000.0},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "fade",
|
||||
"duration": {"reference_id": 1, "property": "duration"}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "fade",
|
||||
"duration": {
|
||||
"reference_id": 2,
|
||||
"property": "duration",
|
||||
"init_val": 500.0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
result = parse_animations(animation_json)
|
||||
|
||||
self.assertEqual(result[3].duration, 1500.0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
518
test/libs/test_audio.py
Normal file
518
test/libs/test_audio.py
Normal file
@@ -0,0 +1,518 @@
|
||||
import shutil
|
||||
import struct
|
||||
import unittest
|
||||
import wave
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from libs.audio import AudioEngine, audio
|
||||
from libs.config import VolumeConfig
|
||||
|
||||
DEFAULT_CONFIG = VolumeConfig(sound=0.8, music=0.7, voice=0.6, hitsound=0.5, attract_mode=0.4)
|
||||
|
||||
class TestAudioEngine(unittest.TestCase):
|
||||
"""Integration tests using the audio library."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up test fixtures once for all tests."""
|
||||
# Create temporary directory for test audio files
|
||||
cls.test_dir = Path().cwd() / Path("temp")
|
||||
cls.test_dir.mkdir(exist_ok=True)
|
||||
cls.sounds_dir = Path(cls.test_dir) / "Sounds"
|
||||
cls.sounds_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Create test WAV files
|
||||
cls._create_test_wav(cls.sounds_dir / "don.wav")
|
||||
cls._create_test_wav(cls.sounds_dir / "ka.wav")
|
||||
cls._create_test_wav(cls.sounds_dir / "test_sound.wav")
|
||||
cls._create_test_wav(cls.sounds_dir / "test_music.wav", duration=2.0)
|
||||
|
||||
# Create screen sounds directory
|
||||
cls.screen_sounds = cls.sounds_dir / "menu"
|
||||
cls.screen_sounds.mkdir()
|
||||
cls._create_test_wav(cls.screen_sounds / "click.wav")
|
||||
cls._create_test_wav(cls.screen_sounds / "hover.wav")
|
||||
|
||||
# Create global sounds directory
|
||||
cls.global_sounds = cls.sounds_dir / "global"
|
||||
cls.global_sounds.mkdir()
|
||||
cls._create_test_wav(cls.global_sounds / "confirm.wav")
|
||||
|
||||
cls.volume_presets = DEFAULT_CONFIG
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Clean up test files."""
|
||||
shutil.rmtree(cls.test_dir)
|
||||
|
||||
@staticmethod
|
||||
def _create_test_wav(filepath, duration=0.1, frequency=440):
|
||||
"""Create a simple test WAV file."""
|
||||
sample_rate = 44100
|
||||
num_samples = int(sample_rate * duration)
|
||||
|
||||
with wave.open(str(filepath), 'w') as wav_file:
|
||||
wav_file.setnchannels(1) # Mono
|
||||
wav_file.setsampwidth(2) # 16-bit
|
||||
wav_file.setframerate(sample_rate)
|
||||
|
||||
for i in range(num_samples):
|
||||
# Generate a simple sine wave
|
||||
value = int(32767.0 * 0.3 *
|
||||
(i % (sample_rate // frequency)) /
|
||||
(sample_rate // frequency))
|
||||
wav_file.writeframes(struct.pack('h', value))
|
||||
|
||||
def setUp(self):
|
||||
"""Set up each test."""
|
||||
self.mock_config_path = self.sounds_dir
|
||||
# Store original audio singleton state to avoid test pollution
|
||||
self._original_audio_sounds_path = audio.sounds_path
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down each test."""
|
||||
# Restore original audio singleton state
|
||||
audio.sounds_path = self._original_audio_sounds_path
|
||||
# Clear any sounds or music loaded during tests
|
||||
if hasattr(audio, 'sounds') and isinstance(audio.sounds, dict):
|
||||
audio.sounds.clear()
|
||||
if hasattr(audio, 'music_streams') and isinstance(audio.music_streams, dict):
|
||||
audio.music_streams.clear()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_initialization(self, mock_config):
|
||||
"""Test AudioEngine initialization."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
|
||||
engine = AudioEngine(
|
||||
device_type=0,
|
||||
sample_rate=44100.0,
|
||||
buffer_size=512,
|
||||
volume_presets=self.volume_presets,
|
||||
sounds_path=self.sounds_dir
|
||||
)
|
||||
|
||||
self.assertEqual(engine.device_type, 0)
|
||||
self.assertEqual(engine.target_sample_rate, 44100.0)
|
||||
self.assertEqual(engine.buffer_size, 512)
|
||||
self.assertEqual(engine.volume_presets, self.volume_presets)
|
||||
self.assertFalse(engine.audio_device_ready)
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_init_and_close_audio_device(self, mock_config):
|
||||
"""Test initializing and closing audio device."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
# Initialize
|
||||
success = engine.init_audio_device()
|
||||
self.assertTrue(success)
|
||||
self.assertTrue(engine.audio_device_ready)
|
||||
self.assertTrue(engine.is_audio_device_ready())
|
||||
|
||||
# Close
|
||||
engine.close_audio_device()
|
||||
self.assertFalse(engine.audio_device_ready)
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_master_volume(self, mock_config):
|
||||
"""Test master volume control."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
# Set and get master volume
|
||||
engine.set_master_volume(0.75)
|
||||
volume = engine.get_master_volume()
|
||||
self.assertAlmostEqual(volume, 0.75, places=2)
|
||||
|
||||
# Test clamping
|
||||
engine.set_master_volume(1.5)
|
||||
volume = engine.get_master_volume()
|
||||
self.assertLessEqual(volume, 1.0)
|
||||
|
||||
engine.set_master_volume(-0.5)
|
||||
volume = engine.get_master_volume()
|
||||
self.assertGreaterEqual(volume, 0.0)
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_load_and_unload_sound(self, mock_config):
|
||||
"""Test loading and unloading sounds."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
# Load sound
|
||||
sound_path = self.sounds_dir / "test_sound.wav"
|
||||
sound_id = engine.load_sound(sound_path, "test")
|
||||
|
||||
self.assertEqual(sound_id, "test")
|
||||
self.assertIn("test", engine.sounds)
|
||||
|
||||
# Unload sound
|
||||
engine.unload_sound("test")
|
||||
self.assertNotIn("test", engine.sounds)
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_load_nonexistent_sound(self, mock_config):
|
||||
"""Test loading a non-existent sound file."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
sound_id = engine.load_sound(Path("nonexistent.wav"), "bad")
|
||||
self.assertEqual(sound_id, "")
|
||||
self.assertNotIn("bad", engine.sounds)
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_play_and_stop_sound(self, mock_config):
|
||||
"""Test playing and stopping sounds."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
# Load and play sound
|
||||
sound_path = self.sounds_dir / "test_sound.wav"
|
||||
engine.load_sound(sound_path, "test")
|
||||
|
||||
engine.play_sound("test", "sound")
|
||||
|
||||
# Give it a moment to start
|
||||
import time
|
||||
time.sleep(0.05)
|
||||
|
||||
# Check if playing (might not be if audio is very short)
|
||||
# Just verify no exceptions were raised
|
||||
is_playing = engine.is_sound_playing("test")
|
||||
self.assertIsInstance(is_playing, bool)
|
||||
|
||||
# Stop sound
|
||||
engine.stop_sound("test")
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_play_don_and_kat(self, mock_config):
|
||||
"""Test playing the special don and kat sounds."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
# Play don
|
||||
engine.play_sound("don", "sound")
|
||||
is_playing = engine.is_sound_playing("don")
|
||||
self.assertIsInstance(is_playing, bool)
|
||||
engine.stop_sound("don")
|
||||
|
||||
# Play kat
|
||||
engine.play_sound("kat", "sound")
|
||||
is_playing = engine.is_sound_playing("kat")
|
||||
self.assertIsInstance(is_playing, bool)
|
||||
engine.stop_sound("kat")
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_sound_volume_control(self, mock_config):
|
||||
"""Test setting sound volume."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
sound_path = self.sounds_dir / "test_sound.wav"
|
||||
engine.load_sound(sound_path, "test")
|
||||
|
||||
# Set volume (should not raise exception)
|
||||
engine.set_sound_volume("test", 0.5)
|
||||
engine.play_sound("test", "")
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_sound_pan_control(self, mock_config):
|
||||
"""Test setting sound pan."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
sound_path = self.sounds_dir / "test_sound.wav"
|
||||
engine.load_sound(sound_path, "test")
|
||||
|
||||
# Set pan (should not raise exception)
|
||||
engine.set_sound_pan("test", -0.5) # Left
|
||||
engine.set_sound_pan("test", 0.5) # Right
|
||||
engine.set_sound_pan("test", 0.0) # Center
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_load_screen_sounds(self, mock_config):
|
||||
"""Test loading sounds for a screen."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
engine.load_screen_sounds("menu")
|
||||
|
||||
# Check that screen sounds were loaded
|
||||
self.assertIn("click", engine.sounds)
|
||||
self.assertIn("hover", engine.sounds)
|
||||
|
||||
# Check that global sounds were loaded
|
||||
self.assertIn("confirm", engine.sounds)
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_unload_all_sounds(self, mock_config):
|
||||
"""Test unloading all sounds."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
# Load multiple sounds
|
||||
engine.load_sound(self.sounds_dir / "test_sound.wav", "s1")
|
||||
engine.load_sound(self.sounds_dir / "test_sound.wav", "s2")
|
||||
engine.load_sound(self.sounds_dir / "test_sound.wav", "s3")
|
||||
|
||||
self.assertEqual(len(engine.sounds), 3)
|
||||
|
||||
# Unload all
|
||||
engine.unload_all_sounds()
|
||||
self.assertEqual(len(engine.sounds), 0)
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_load_and_play_music_stream(self, mock_config):
|
||||
"""Test loading and playing music streams."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
music_path = self.sounds_dir / "test_music.wav"
|
||||
music_id = engine.load_music_stream(music_path, "bgm")
|
||||
print(music_id)
|
||||
|
||||
self.assertEqual(music_id, "bgm")
|
||||
self.assertIn("bgm", engine.music_streams)
|
||||
|
||||
# Play music
|
||||
engine.play_music_stream("bgm", "music")
|
||||
|
||||
# Update music stream
|
||||
engine.update_music_stream("bgm")
|
||||
|
||||
# Check if playing
|
||||
is_playing = engine.is_music_stream_playing("bgm")
|
||||
self.assertIsInstance(is_playing, bool)
|
||||
|
||||
# Stop music
|
||||
engine.stop_music_stream("bgm")
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_music_time_functions(self, mock_config):
|
||||
"""Test getting music time length and played."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
music_path = self.sounds_dir / "test_music.wav"
|
||||
music_file = engine.load_music_stream(music_path, "bgm")
|
||||
|
||||
# Get time length
|
||||
length = engine.get_music_time_length(music_file)
|
||||
self.assertGreater(length, 0.0)
|
||||
self.assertLess(length, 10.0) # Should be around 2 seconds
|
||||
|
||||
# Play and get time played
|
||||
engine.play_music_stream(music_file, "music")
|
||||
engine.update_music_stream(music_file)
|
||||
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
engine.update_music_stream(music_file)
|
||||
|
||||
played = engine.get_music_time_played(music_file)
|
||||
self.assertGreaterEqual(played, 0.0)
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_music_volume_control(self, mock_config):
|
||||
"""Test setting music volume."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
music_path = self.sounds_dir / "test_music.wav"
|
||||
engine.load_music_stream(music_path, "bgm")
|
||||
|
||||
# Set volume (should not raise exception)
|
||||
engine.set_music_volume("bgm", 0.6)
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_seek_music_stream(self, mock_config):
|
||||
"""Test seeking in music stream."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
music_path = self.sounds_dir / "test_music.wav"
|
||||
engine.load_music_stream(music_path, "bgm")
|
||||
|
||||
# Seek to position (should not raise exception)
|
||||
engine.seek_music_stream("bgm", 0.5)
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_unload_music_stream(self, mock_config):
|
||||
"""Test unloading music stream."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
music_path = self.sounds_dir / "test_music.wav"
|
||||
engine.load_music_stream(music_path, "bgm")
|
||||
|
||||
self.assertIn("bgm", engine.music_streams)
|
||||
|
||||
engine.unload_music_stream("bgm")
|
||||
self.assertNotIn("bgm", engine.music_streams)
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_unload_all_music(self, mock_config):
|
||||
"""Test unloading all music streams."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
# Load multiple music streams
|
||||
music_path = self.sounds_dir / "test_music.wav"
|
||||
engine.load_music_stream(music_path, "bgm1")
|
||||
engine.load_music_stream(music_path, "bgm2")
|
||||
|
||||
self.assertEqual(len(engine.music_streams), 2)
|
||||
|
||||
engine.unload_all_music()
|
||||
self.assertEqual(len(engine.music_streams), 0)
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_host_api_functions(self, mock_config):
|
||||
"""Test host API query functions."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
engine.init_audio_device()
|
||||
|
||||
# List host APIs (should not crash)
|
||||
engine.list_host_apis()
|
||||
|
||||
# Get host API name
|
||||
name = engine.get_host_api_name(0)
|
||||
self.assertIsInstance(name, str)
|
||||
|
||||
@patch('libs.audio.get_config')
|
||||
def test_full_lifecycle(self, mock_config):
|
||||
"""Test complete audio engine lifecycle."""
|
||||
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||
|
||||
|
||||
if engine.init_audio_device():
|
||||
try:
|
||||
# Load sounds and music
|
||||
engine.load_sound(self.sounds_dir / "test_sound.wav", "sfx")
|
||||
engine.load_music_stream(self.sounds_dir / "test_music.wav", "bgm")
|
||||
|
||||
# Set volumes
|
||||
engine.set_master_volume(0.8)
|
||||
engine.set_sound_volume("sfx", 0.7)
|
||||
engine.set_music_volume("bgm", 0.6)
|
||||
|
||||
# Play audio
|
||||
engine.play_sound("sfx", "sound")
|
||||
engine.play_music_stream("bgm", "music")
|
||||
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
|
||||
# Update music
|
||||
engine.update_music_stream("bgm")
|
||||
|
||||
# Stop
|
||||
engine.stop_sound("sfx")
|
||||
engine.stop_music_stream("bgm")
|
||||
|
||||
# Cleanup
|
||||
engine.unload_sound("sfx")
|
||||
engine.unload_music_stream("bgm")
|
||||
|
||||
finally:
|
||||
engine.close_audio_device()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run tests
|
||||
unittest.main(verbosity=2)
|
||||
551
test/libs/test_global_data.py
Normal file
551
test/libs/test_global_data.py
Normal file
@@ -0,0 +1,551 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from libs.global_data import (
|
||||
Camera,
|
||||
Crown,
|
||||
DanResultData,
|
||||
DanResultExam,
|
||||
DanResultSong,
|
||||
Difficulty,
|
||||
GlobalData,
|
||||
Modifiers,
|
||||
PlayerNum,
|
||||
ResultData,
|
||||
ScoreMethod,
|
||||
SessionData,
|
||||
global_data,
|
||||
reset_session,
|
||||
)
|
||||
|
||||
|
||||
class TestPlayerNum(unittest.TestCase):
|
||||
"""Test cases for the PlayerNum enum."""
|
||||
|
||||
def test_player_num_values(self):
|
||||
"""Test PlayerNum enum values."""
|
||||
self.assertEqual(PlayerNum.ALL, 0)
|
||||
self.assertEqual(PlayerNum.P1, 1)
|
||||
self.assertEqual(PlayerNum.P2, 2)
|
||||
self.assertEqual(PlayerNum.TWO_PLAYER, 3)
|
||||
self.assertEqual(PlayerNum.DAN, 4)
|
||||
|
||||
def test_player_num_is_int_enum(self):
|
||||
"""Test that PlayerNum values are integers."""
|
||||
self.assertIsInstance(PlayerNum.P1, int)
|
||||
self.assertIsInstance(PlayerNum.P2, int)
|
||||
|
||||
|
||||
class TestScoreMethod(unittest.TestCase):
|
||||
"""Test cases for the ScoreMethod class."""
|
||||
|
||||
def test_score_method_constants(self):
|
||||
"""Test ScoreMethod constants."""
|
||||
self.assertEqual(ScoreMethod.GEN3, "gen3")
|
||||
self.assertEqual(ScoreMethod.SHINUCHI, "shinuchi")
|
||||
|
||||
|
||||
class TestDifficulty(unittest.TestCase):
|
||||
"""Test cases for the Difficulty enum."""
|
||||
|
||||
def test_difficulty_values(self):
|
||||
"""Test Difficulty enum values."""
|
||||
self.assertEqual(Difficulty.EASY, 0)
|
||||
self.assertEqual(Difficulty.NORMAL, 1)
|
||||
self.assertEqual(Difficulty.HARD, 2)
|
||||
self.assertEqual(Difficulty.ONI, 3)
|
||||
self.assertEqual(Difficulty.URA, 4)
|
||||
self.assertEqual(Difficulty.TOWER, 5)
|
||||
self.assertEqual(Difficulty.DAN, 6)
|
||||
|
||||
def test_difficulty_ordering(self):
|
||||
"""Test that difficulty levels are ordered correctly."""
|
||||
self.assertLess(Difficulty.EASY, Difficulty.NORMAL)
|
||||
self.assertLess(Difficulty.NORMAL, Difficulty.HARD)
|
||||
self.assertLess(Difficulty.HARD, Difficulty.ONI)
|
||||
self.assertLess(Difficulty.ONI, Difficulty.URA)
|
||||
|
||||
|
||||
class TestCrown(unittest.TestCase):
|
||||
"""Test cases for the Crown enum."""
|
||||
|
||||
def test_crown_values(self):
|
||||
"""Test Crown enum values."""
|
||||
self.assertEqual(Crown.NONE, 0)
|
||||
self.assertEqual(Crown.CLEAR, 1)
|
||||
self.assertEqual(Crown.FC, 2)
|
||||
self.assertEqual(Crown.DFC, 3)
|
||||
|
||||
def test_crown_ordering(self):
|
||||
"""Test crown achievement ordering."""
|
||||
self.assertLess(Crown.NONE, Crown.CLEAR)
|
||||
self.assertLess(Crown.CLEAR, Crown.FC)
|
||||
self.assertLess(Crown.FC, Crown.DFC)
|
||||
|
||||
|
||||
class TestModifiers(unittest.TestCase):
|
||||
"""Test cases for the Modifiers dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default modifier values."""
|
||||
mods = Modifiers()
|
||||
|
||||
self.assertFalse(mods.auto)
|
||||
self.assertEqual(mods.speed, 1.0)
|
||||
self.assertFalse(mods.display)
|
||||
self.assertFalse(mods.inverse)
|
||||
self.assertEqual(mods.random, 0)
|
||||
|
||||
def test_custom_values(self):
|
||||
"""Test custom modifier values."""
|
||||
mods = Modifiers(auto=True, speed=2.0, display=True, inverse=True, random=3)
|
||||
|
||||
self.assertTrue(mods.auto)
|
||||
self.assertEqual(mods.speed, 2.0)
|
||||
self.assertTrue(mods.display)
|
||||
self.assertTrue(mods.inverse)
|
||||
self.assertEqual(mods.random, 3)
|
||||
|
||||
def test_speed_multiplier(self):
|
||||
"""Test different speed multiplier values."""
|
||||
mods1 = Modifiers(speed=0.5)
|
||||
mods2 = Modifiers(speed=1.5)
|
||||
mods3 = Modifiers(speed=3.0)
|
||||
|
||||
self.assertEqual(mods1.speed, 0.5)
|
||||
self.assertEqual(mods2.speed, 1.5)
|
||||
self.assertEqual(mods3.speed, 3.0)
|
||||
|
||||
|
||||
class TestDanResultSong(unittest.TestCase):
|
||||
"""Test cases for the DanResultSong dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default DanResultSong values."""
|
||||
song = DanResultSong()
|
||||
|
||||
self.assertEqual(song.selected_difficulty, 0)
|
||||
self.assertEqual(song.diff_level, 0)
|
||||
self.assertEqual(song.song_title, "default_title")
|
||||
self.assertEqual(song.genre_index, 0)
|
||||
self.assertEqual(song.good, 0)
|
||||
self.assertEqual(song.ok, 0)
|
||||
self.assertEqual(song.bad, 0)
|
||||
self.assertEqual(song.drumroll, 0)
|
||||
|
||||
def test_custom_values(self):
|
||||
"""Test custom DanResultSong values."""
|
||||
song = DanResultSong(
|
||||
selected_difficulty=3,
|
||||
diff_level=10,
|
||||
song_title="Test Song",
|
||||
genre_index=5,
|
||||
good=100,
|
||||
ok=20,
|
||||
bad=5,
|
||||
drumroll=15
|
||||
)
|
||||
|
||||
self.assertEqual(song.selected_difficulty, 3)
|
||||
self.assertEqual(song.diff_level, 10)
|
||||
self.assertEqual(song.song_title, "Test Song")
|
||||
self.assertEqual(song.genre_index, 5)
|
||||
self.assertEqual(song.good, 100)
|
||||
self.assertEqual(song.ok, 20)
|
||||
self.assertEqual(song.bad, 5)
|
||||
self.assertEqual(song.drumroll, 15)
|
||||
|
||||
|
||||
class TestDanResultExam(unittest.TestCase):
|
||||
"""Test cases for the DanResultExam class."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default DanResultExam values."""
|
||||
exam = DanResultExam()
|
||||
|
||||
self.assertEqual(exam.progress, 0)
|
||||
self.assertEqual(exam.counter_value, 0)
|
||||
self.assertEqual(exam.bar_texture, "exam_red")
|
||||
self.assertFalse(exam.failed)
|
||||
|
||||
def test_custom_values(self):
|
||||
"""Test custom DanResultExam values."""
|
||||
exam = DanResultExam()
|
||||
exam.progress = 0.75
|
||||
exam.counter_value = 150
|
||||
exam.bar_texture = "exam_gold"
|
||||
exam.failed = True
|
||||
|
||||
self.assertEqual(exam.progress, 0.75)
|
||||
self.assertEqual(exam.counter_value, 150)
|
||||
self.assertEqual(exam.bar_texture, "exam_gold")
|
||||
self.assertTrue(exam.failed)
|
||||
|
||||
|
||||
class TestDanResultData(unittest.TestCase):
|
||||
"""Test cases for the DanResultData dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default DanResultData values."""
|
||||
data = DanResultData()
|
||||
|
||||
self.assertEqual(data.dan_color, 0)
|
||||
self.assertEqual(data.dan_title, "default_title")
|
||||
self.assertEqual(data.score, 0)
|
||||
self.assertEqual(data.gauge_length, 0.0)
|
||||
self.assertEqual(data.max_combo, 0)
|
||||
self.assertEqual(data.songs, [])
|
||||
self.assertEqual(data.exams, [])
|
||||
self.assertEqual(data.exam_data, [])
|
||||
|
||||
def test_with_songs(self):
|
||||
"""Test DanResultData with songs."""
|
||||
song1 = DanResultSong(song_title="Song 1")
|
||||
song2 = DanResultSong(song_title="Song 2")
|
||||
|
||||
data = DanResultData(songs=[song1, song2])
|
||||
|
||||
self.assertEqual(len(data.songs), 2)
|
||||
self.assertEqual(data.songs[0].song_title, "Song 1")
|
||||
self.assertEqual(data.songs[1].song_title, "Song 2")
|
||||
|
||||
def test_with_exam_data(self):
|
||||
"""Test DanResultData with exam data."""
|
||||
exam1 = DanResultExam()
|
||||
exam1.progress = 0.5
|
||||
exam2 = DanResultExam()
|
||||
exam2.progress = 1.0
|
||||
|
||||
data = DanResultData(exam_data=[exam1, exam2])
|
||||
|
||||
self.assertEqual(len(data.exam_data), 2)
|
||||
self.assertEqual(data.exam_data[0].progress, 0.5)
|
||||
self.assertEqual(data.exam_data[1].progress, 1.0)
|
||||
|
||||
|
||||
class TestResultData(unittest.TestCase):
|
||||
"""Test cases for the ResultData dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default ResultData values."""
|
||||
data = ResultData()
|
||||
|
||||
self.assertEqual(data.score, 0)
|
||||
self.assertEqual(data.good, 0)
|
||||
self.assertEqual(data.ok, 0)
|
||||
self.assertEqual(data.bad, 0)
|
||||
self.assertEqual(data.max_combo, 0)
|
||||
self.assertEqual(data.total_drumroll, 0)
|
||||
self.assertEqual(data.gauge_length, 0)
|
||||
self.assertEqual(data.prev_score, 0)
|
||||
|
||||
def test_custom_values(self):
|
||||
"""Test custom ResultData values."""
|
||||
data = ResultData(
|
||||
score=500000,
|
||||
good=150,
|
||||
ok=30,
|
||||
bad=10,
|
||||
max_combo=120,
|
||||
total_drumroll=45,
|
||||
gauge_length=0.85,
|
||||
prev_score=450000
|
||||
)
|
||||
|
||||
self.assertEqual(data.score, 500000)
|
||||
self.assertEqual(data.good, 150)
|
||||
self.assertEqual(data.ok, 30)
|
||||
self.assertEqual(data.bad, 10)
|
||||
self.assertEqual(data.max_combo, 120)
|
||||
self.assertEqual(data.total_drumroll, 45)
|
||||
self.assertEqual(data.gauge_length, 0.85)
|
||||
self.assertEqual(data.prev_score, 450000)
|
||||
|
||||
def test_total_notes(self):
|
||||
"""Test calculating total notes from result data."""
|
||||
data = ResultData(good=100, ok=50, bad=10)
|
||||
total = data.good + data.ok + data.bad
|
||||
|
||||
self.assertEqual(total, 160)
|
||||
|
||||
|
||||
class TestSessionData(unittest.TestCase):
|
||||
"""Test cases for the SessionData dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default SessionData values."""
|
||||
session = SessionData()
|
||||
|
||||
self.assertEqual(session.selected_song, Path())
|
||||
self.assertEqual(session.song_hash, "")
|
||||
self.assertEqual(session.selected_dan, [])
|
||||
self.assertEqual(session.selected_dan_exam, [])
|
||||
self.assertEqual(session.dan_color, 0)
|
||||
self.assertEqual(session.selected_difficulty, 0)
|
||||
self.assertEqual(session.song_title, "default_title")
|
||||
self.assertEqual(session.genre_index, 0)
|
||||
self.assertIsInstance(session.result_data, ResultData)
|
||||
self.assertIsInstance(session.dan_result_data, DanResultData)
|
||||
|
||||
def test_custom_song_selection(self):
|
||||
"""Test custom song selection."""
|
||||
song_path = Path("Songs/TestSong/song.tja")
|
||||
session = SessionData(
|
||||
selected_song=song_path,
|
||||
song_hash="abc123",
|
||||
selected_difficulty=3,
|
||||
song_title="Test Song"
|
||||
)
|
||||
|
||||
self.assertEqual(session.selected_song, song_path)
|
||||
self.assertEqual(session.song_hash, "abc123")
|
||||
self.assertEqual(session.selected_difficulty, 3)
|
||||
self.assertEqual(session.song_title, "Test Song")
|
||||
|
||||
def test_dan_selection(self):
|
||||
"""Test dan course selection."""
|
||||
dan_songs = [(Mock(), 0, 3, 10), (Mock(), 1, 3, 10)]
|
||||
dan_exams = [Mock(), Mock(), Mock()]
|
||||
|
||||
session = SessionData(
|
||||
selected_dan=dan_songs,
|
||||
selected_dan_exam=dan_exams,
|
||||
dan_color=2
|
||||
)
|
||||
|
||||
self.assertEqual(len(session.selected_dan), 2)
|
||||
self.assertEqual(len(session.selected_dan_exam), 3)
|
||||
self.assertEqual(session.dan_color, 2)
|
||||
|
||||
def test_result_data_independence(self):
|
||||
"""Test that each session has independent result data."""
|
||||
session1 = SessionData()
|
||||
session2 = SessionData()
|
||||
|
||||
session1.result_data.score = 100000
|
||||
|
||||
self.assertEqual(session1.result_data.score, 100000)
|
||||
self.assertEqual(session2.result_data.score, 0)
|
||||
|
||||
|
||||
class TestCamera(unittest.TestCase):
|
||||
"""Test cases for the Camera class."""
|
||||
|
||||
@patch('libs.global_data.ray')
|
||||
def test_default_values(self, mock_ray):
|
||||
"""Test default Camera values."""
|
||||
mock_ray.Vector2 = Mock(return_value=Mock())
|
||||
mock_ray.BLACK = Mock()
|
||||
|
||||
camera = Camera()
|
||||
|
||||
self.assertEqual(camera.zoom, 1.0)
|
||||
self.assertEqual(camera.h_scale, 1.0)
|
||||
self.assertEqual(camera.v_scale, 1.0)
|
||||
self.assertEqual(camera.rotation, 0.0)
|
||||
|
||||
|
||||
class TestGlobalData(unittest.TestCase):
|
||||
"""Test cases for the GlobalData dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default GlobalData values."""
|
||||
data = GlobalData()
|
||||
|
||||
self.assertEqual(data.songs_played, 0)
|
||||
self.assertIsInstance(data.camera, Camera)
|
||||
self.assertEqual(data.song_hashes, {})
|
||||
self.assertEqual(data.song_paths, {})
|
||||
self.assertEqual(data.score_db, "")
|
||||
self.assertEqual(data.song_progress, 0.0)
|
||||
self.assertEqual(data.total_songs, 0)
|
||||
self.assertEqual(data.hit_sound, [0, 0, 0])
|
||||
self.assertEqual(data.player_num, PlayerNum.P1)
|
||||
self.assertEqual(data.input_locked, 0)
|
||||
|
||||
def test_modifiers_list(self):
|
||||
"""Test that modifiers list has correct size."""
|
||||
data = GlobalData()
|
||||
|
||||
self.assertEqual(len(data.modifiers), 3)
|
||||
self.assertIsInstance(data.modifiers[0], Modifiers)
|
||||
self.assertIsInstance(data.modifiers[1], Modifiers)
|
||||
self.assertIsInstance(data.modifiers[2], Modifiers)
|
||||
|
||||
def test_session_data_list(self):
|
||||
"""Test that session data list has correct size."""
|
||||
data = GlobalData()
|
||||
|
||||
self.assertEqual(len(data.session_data), 3)
|
||||
self.assertIsInstance(data.session_data[0], SessionData)
|
||||
self.assertIsInstance(data.session_data[1], SessionData)
|
||||
self.assertIsInstance(data.session_data[2], SessionData)
|
||||
|
||||
def test_song_hashes_dict(self):
|
||||
"""Test song_hashes dictionary operations."""
|
||||
data = GlobalData()
|
||||
|
||||
data.song_hashes["hash1"] = [{"path": "Songs/Song1"}]
|
||||
data.song_hashes["hash2"] = [{"path": "Songs/Song2"}]
|
||||
|
||||
self.assertEqual(len(data.song_hashes), 2)
|
||||
self.assertIn("hash1", data.song_hashes)
|
||||
self.assertIn("hash2", data.song_hashes)
|
||||
|
||||
def test_song_paths_dict(self):
|
||||
"""Test song_paths dictionary operations."""
|
||||
data = GlobalData()
|
||||
|
||||
path1 = Path("Songs/Song1/song.tja")
|
||||
path2 = Path("Songs/Song2/song.tja")
|
||||
|
||||
data.song_paths[path1] = "hash1"
|
||||
data.song_paths[path2] = "hash2"
|
||||
|
||||
self.assertEqual(len(data.song_paths), 2)
|
||||
self.assertEqual(data.song_paths[path1], "hash1")
|
||||
self.assertEqual(data.song_paths[path2], "hash2")
|
||||
|
||||
def test_input_locked_counter(self):
|
||||
"""Test input_locked as a counter."""
|
||||
data = GlobalData()
|
||||
|
||||
self.assertEqual(data.input_locked, 0)
|
||||
|
||||
data.input_locked += 1
|
||||
self.assertEqual(data.input_locked, 1)
|
||||
|
||||
data.input_locked += 1
|
||||
self.assertEqual(data.input_locked, 2)
|
||||
|
||||
data.input_locked -= 1
|
||||
self.assertEqual(data.input_locked, 1)
|
||||
|
||||
def test_songs_played_counter(self):
|
||||
"""Test songs_played counter."""
|
||||
data = GlobalData()
|
||||
|
||||
self.assertEqual(data.songs_played, 0)
|
||||
|
||||
data.songs_played += 1
|
||||
self.assertEqual(data.songs_played, 1)
|
||||
|
||||
data.songs_played += 1
|
||||
self.assertEqual(data.songs_played, 2)
|
||||
|
||||
def test_hit_sound_indices(self):
|
||||
"""Test hit_sound indices list."""
|
||||
data = GlobalData()
|
||||
|
||||
self.assertEqual(data.hit_sound, [0, 0, 0])
|
||||
|
||||
data.hit_sound[0] = 1
|
||||
data.hit_sound[1] = 2
|
||||
data.hit_sound[2] = 3
|
||||
|
||||
self.assertEqual(data.hit_sound, [1, 2, 3])
|
||||
|
||||
|
||||
class TestGlobalDataSingleton(unittest.TestCase):
|
||||
"""Test cases for the global_data singleton."""
|
||||
|
||||
def test_global_data_exists(self):
|
||||
"""Test that global_data instance exists."""
|
||||
self.assertIsInstance(global_data, GlobalData)
|
||||
|
||||
def test_global_data_modifiable(self):
|
||||
"""Test that global_data can be modified."""
|
||||
original_songs_played = global_data.songs_played
|
||||
global_data.songs_played += 1
|
||||
|
||||
self.assertEqual(global_data.songs_played, original_songs_played + 1)
|
||||
|
||||
# Reset for other tests
|
||||
global_data.songs_played = original_songs_played
|
||||
|
||||
|
||||
class TestResetSession(unittest.TestCase):
|
||||
"""Test cases for reset_session function."""
|
||||
|
||||
def test_reset_session_clears_p1_data(self):
|
||||
"""Test that reset_session clears player 1 data."""
|
||||
global_data.session_data[1].result_data.score = 100000
|
||||
global_data.session_data[1].song_title = "Test Song"
|
||||
|
||||
reset_session()
|
||||
|
||||
self.assertIsInstance(global_data.session_data[1], SessionData)
|
||||
self.assertEqual(global_data.session_data[1].song_title, "default_title")
|
||||
|
||||
def test_reset_session_clears_p2_data(self):
|
||||
"""Test that reset_session clears player 2 data."""
|
||||
global_data.session_data[2].result_data.score = 50000
|
||||
global_data.session_data[2].selected_difficulty = 3
|
||||
|
||||
reset_session()
|
||||
|
||||
self.assertIsInstance(global_data.session_data[2], SessionData)
|
||||
self.assertEqual(global_data.session_data[2].selected_difficulty, 0)
|
||||
|
||||
def test_reset_session_preserves_index_0(self):
|
||||
"""Test that reset_session doesn't affect index 0."""
|
||||
original_data = global_data.session_data[0]
|
||||
original_data.song_title = "Should Not Change"
|
||||
|
||||
reset_session()
|
||||
|
||||
self.assertEqual(global_data.session_data[0].song_title, "Should Not Change")
|
||||
|
||||
def test_reset_session_creates_new_instances(self):
|
||||
"""Test that reset_session creates new SessionData instances."""
|
||||
old_p1_session = global_data.session_data[1]
|
||||
old_p2_session = global_data.session_data[2]
|
||||
|
||||
reset_session()
|
||||
|
||||
self.assertIsNot(global_data.session_data[1], old_p1_session)
|
||||
self.assertIsNot(global_data.session_data[2], old_p2_session)
|
||||
|
||||
|
||||
class TestDataclassIntegration(unittest.TestCase):
|
||||
"""Integration tests for dataclass interactions."""
|
||||
|
||||
def test_session_with_result_data(self):
|
||||
"""Test SessionData with populated ResultData."""
|
||||
session = SessionData()
|
||||
session.result_data.score = 750000
|
||||
session.result_data.good = 200
|
||||
session.result_data.max_combo = 180
|
||||
|
||||
self.assertEqual(session.result_data.score, 750000)
|
||||
self.assertEqual(session.result_data.good, 200)
|
||||
self.assertEqual(session.result_data.max_combo, 180)
|
||||
|
||||
def test_session_with_dan_result_data(self):
|
||||
"""Test SessionData with populated DanResultData."""
|
||||
session = SessionData()
|
||||
session.dan_result_data.dan_title = "10th Dan"
|
||||
session.dan_result_data.dan_color = 5
|
||||
|
||||
song1 = DanResultSong(song_title="Dan Song 1")
|
||||
song2 = DanResultSong(song_title="Dan Song 2")
|
||||
session.dan_result_data.songs = [song1, song2]
|
||||
|
||||
self.assertEqual(session.dan_result_data.dan_title, "10th Dan")
|
||||
self.assertEqual(len(session.dan_result_data.songs), 2)
|
||||
|
||||
def test_modifiers_independent_per_player(self):
|
||||
"""Test that each player has independent modifiers."""
|
||||
data = GlobalData()
|
||||
|
||||
data.modifiers[1].speed = 2.0
|
||||
data.modifiers[2].speed = 1.5
|
||||
|
||||
self.assertEqual(data.modifiers[1].speed, 2.0)
|
||||
self.assertEqual(data.modifiers[2].speed, 1.5)
|
||||
self.assertEqual(data.modifiers[0].speed, 1.0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
289
test/libs/test_global_objects.py
Normal file
289
test/libs/test_global_objects.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from libs.global_data import PlayerNum
|
||||
from libs.global_objects import (
|
||||
AllNetIcon,
|
||||
CoinOverlay,
|
||||
EntryOverlay,
|
||||
Indicator,
|
||||
Nameplate,
|
||||
Timer,
|
||||
)
|
||||
|
||||
|
||||
class TestNameplate(unittest.TestCase):
|
||||
"""Test cases for the Nameplate class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
# Mock global_tex and its methods
|
||||
self.mock_tex = Mock()
|
||||
self.mock_tex.skin_config = {
|
||||
"nameplate_text_name": Mock(font_size=20, x=100, y=50, width=200),
|
||||
"nameplate_text_title": Mock(font_size=16, x=100, y=80, width=150),
|
||||
"nameplate_title_offset": Mock(x=10),
|
||||
"nameplate_dan_offset": Mock(x=20)
|
||||
}
|
||||
self.mock_tex.get_animation = Mock(return_value=Mock(start=Mock(), update=Mock(), is_finished=False))
|
||||
|
||||
@patch('libs.global_objects.global_tex')
|
||||
@patch('libs.global_objects.OutlinedText')
|
||||
def test_initialization_basic(self, mock_text, mock_global_tex):
|
||||
"""Test basic nameplate initialization."""
|
||||
mock_global_tex.skin_config = self.mock_tex.skin_config
|
||||
|
||||
nameplate = Nameplate("TestPlayer", "TestTitle", PlayerNum.P1, 5, False, False, 0)
|
||||
|
||||
self.assertEqual(nameplate.dan_index, 5)
|
||||
self.assertEqual(nameplate.player_num, 1)
|
||||
self.assertFalse(nameplate.is_gold)
|
||||
self.assertFalse(nameplate.is_rainbow)
|
||||
self.assertEqual(nameplate.title_bg, 0)
|
||||
|
||||
@patch('libs.global_objects.global_tex')
|
||||
@patch('libs.global_objects.OutlinedText')
|
||||
def test_initialization_rainbow(self, mock_text, mock_global_tex):
|
||||
"""Test rainbow nameplate initialization."""
|
||||
mock_global_tex.skin_config = self.mock_tex.skin_config
|
||||
mock_animation = Mock()
|
||||
mock_global_tex.get_animation.return_value = mock_animation
|
||||
|
||||
nameplate = Nameplate("Player", "Title", PlayerNum.P1, 3, False, True, 0)
|
||||
|
||||
self.assertTrue(nameplate.is_rainbow)
|
||||
mock_global_tex.get_animation.assert_called_once_with(12)
|
||||
mock_animation.start.assert_called_once()
|
||||
|
||||
@patch('libs.global_objects.global_tex')
|
||||
def test_update_rainbow_animation(self, mock_global_tex):
|
||||
"""Test rainbow animation update logic."""
|
||||
mock_animation = Mock(is_finished=False, update=Mock())
|
||||
mock_global_tex.get_animation.return_value = mock_animation
|
||||
mock_global_tex.skin_config = self.mock_tex.skin_config
|
||||
|
||||
with patch('libs.global_objects.OutlinedText'):
|
||||
nameplate = Nameplate("P", "T", PlayerNum.P1, 0, False, True, 0)
|
||||
nameplate.update(1000.0)
|
||||
|
||||
mock_animation.update.assert_called_once_with(1000.0)
|
||||
|
||||
@patch('libs.global_objects.global_tex')
|
||||
def test_update_rainbow_restart(self, mock_global_tex):
|
||||
"""Test rainbow animation restarts when finished."""
|
||||
mock_animation = Mock(is_finished=True, update=Mock(), restart=Mock())
|
||||
mock_global_tex.get_animation.return_value = mock_animation
|
||||
mock_global_tex.skin_config = self.mock_tex.skin_config
|
||||
|
||||
with patch('libs.global_objects.OutlinedText'):
|
||||
nameplate = Nameplate("P", "T", PlayerNum.P1, 0, False, True, 0)
|
||||
nameplate.update(1000.0)
|
||||
|
||||
mock_animation.restart.assert_called_once()
|
||||
|
||||
@patch('libs.global_objects.global_tex')
|
||||
def test_unload(self, mock_global_tex):
|
||||
"""Test nameplate resource cleanup."""
|
||||
mock_global_tex.skin_config = self.mock_tex.skin_config
|
||||
|
||||
with patch('libs.global_objects.OutlinedText') as mock_text:
|
||||
mock_name = Mock()
|
||||
mock_title = Mock()
|
||||
mock_text.side_effect = [mock_name, mock_title]
|
||||
|
||||
nameplate = Nameplate("P", "T", PlayerNum.P1, 0, False, False, 0)
|
||||
nameplate.unload()
|
||||
|
||||
mock_name.unload.assert_called_once()
|
||||
mock_title.unload.assert_called_once()
|
||||
|
||||
|
||||
class TestIndicator(unittest.TestCase):
|
||||
"""Test cases for the Indicator class."""
|
||||
|
||||
@patch('libs.global_objects.global_tex')
|
||||
@patch('libs.global_objects.OutlinedText')
|
||||
def test_initialization(self, mock_text, mock_global_tex):
|
||||
"""Test indicator initialization with different states."""
|
||||
mock_global_tex.get_animation.return_value = Mock()
|
||||
mock_global_tex.skin_config = {"indicator_text": Mock(text={"en": "Select"}, font_size=20)}
|
||||
|
||||
with patch('libs.global_objects.global_data') as mock_data:
|
||||
mock_data.config = {"general": {"language": "en"}}
|
||||
|
||||
indicator = Indicator(Indicator.State.SELECT)
|
||||
|
||||
self.assertEqual(indicator.state, Indicator.State.SELECT)
|
||||
self.assertEqual(mock_global_tex.get_animation.call_count, 3)
|
||||
|
||||
@patch('libs.global_objects.global_tex')
|
||||
def test_update_animations(self, mock_global_tex):
|
||||
"""Test that all animations update correctly."""
|
||||
mock_don_fade = Mock()
|
||||
mock_arrow_move = Mock()
|
||||
mock_arrow_fade = Mock()
|
||||
mock_global_tex.get_animation.side_effect = [mock_don_fade, mock_arrow_move, mock_arrow_fade]
|
||||
mock_global_tex.skin_config = {"indicator_text": Mock(text={"en": "S"}, font_size=20)}
|
||||
|
||||
with patch('libs.global_objects.global_data.config', {"general": {"language": "en"}}):
|
||||
with patch('libs.global_objects.OutlinedText'):
|
||||
indicator = Indicator(Indicator.State.SKIP)
|
||||
indicator.update(500.0)
|
||||
|
||||
mock_don_fade.update.assert_called_once_with(500.0)
|
||||
mock_arrow_move.update.assert_called_once_with(500.0)
|
||||
mock_arrow_fade.update.assert_called_once_with(500.0)
|
||||
|
||||
|
||||
class TestTimer(unittest.TestCase):
|
||||
"""Test cases for the Timer class."""
|
||||
|
||||
@patch('libs.global_objects.get_config')
|
||||
@patch('libs.global_objects.global_tex')
|
||||
def test_initialization(self, mock_tex, mock_config):
|
||||
"""Test timer initialization."""
|
||||
mock_config.return_value = {"general": {"timer_frozen": False}}
|
||||
mock_tex.get_animation.return_value = Mock()
|
||||
mock_func = Mock()
|
||||
|
||||
timer = Timer(30, 0.0, mock_func)
|
||||
|
||||
self.assertEqual(timer.time, 30)
|
||||
self.assertEqual(timer.counter, "30")
|
||||
self.assertFalse(timer.is_finished)
|
||||
self.assertFalse(timer.is_frozen)
|
||||
|
||||
@patch('libs.global_objects.audio')
|
||||
@patch('libs.global_objects.get_config')
|
||||
@patch('libs.global_objects.global_tex')
|
||||
def test_countdown_normal(self, mock_tex, mock_config, mock_audio):
|
||||
"""Test normal countdown behavior."""
|
||||
mock_config.return_value = {"general": {"timer_frozen": False}}
|
||||
mock_tex.get_animation.return_value = Mock(update=Mock(), start=Mock())
|
||||
mock_func = Mock()
|
||||
|
||||
timer = Timer(15, 0.0, mock_func)
|
||||
timer.update(1000.0)
|
||||
|
||||
self.assertEqual(timer.time, 14)
|
||||
self.assertEqual(timer.counter, "14")
|
||||
|
||||
@patch('libs.global_objects.audio')
|
||||
@patch('libs.global_objects.get_config')
|
||||
@patch('libs.global_objects.global_tex')
|
||||
def test_countdown_below_ten(self, mock_tex, mock_config, mock_audio):
|
||||
"""Test countdown triggers animations below 10."""
|
||||
mock_config.return_value = {"general": {"timer_frozen": False}}
|
||||
mock_animation = Mock(update=Mock(), start=Mock())
|
||||
mock_tex.get_animation.return_value = mock_animation
|
||||
mock_func = Mock()
|
||||
|
||||
timer = Timer(10, 0.0, mock_func)
|
||||
timer.update(1000.0)
|
||||
|
||||
self.assertEqual(timer.time, 9)
|
||||
mock_audio.play_sound.assert_called_with('timer_blip', 'sound')
|
||||
self.assertEqual(mock_animation.start.call_count, 3)
|
||||
|
||||
@patch('libs.global_objects.audio')
|
||||
@patch('libs.global_objects.get_config')
|
||||
@patch('libs.global_objects.global_tex')
|
||||
def test_voice_triggers(self, mock_tex, mock_config, mock_audio):
|
||||
"""Test voice announcements at specific times."""
|
||||
mock_config.return_value = {"general": {"timer_frozen": False}}
|
||||
mock_tex.get_animation.return_value = Mock(update=Mock(), start=Mock())
|
||||
mock_func = Mock()
|
||||
|
||||
# Test 10 second voice
|
||||
timer = Timer(11, 0.0, mock_func)
|
||||
timer.update(1000.0)
|
||||
mock_audio.play_sound.assert_called_with('voice_timer_10', 'voice')
|
||||
|
||||
# Test 5 second voice
|
||||
timer = Timer(6, 0.0, mock_func)
|
||||
timer.update(1000.0)
|
||||
mock_audio.play_sound.assert_called_with('voice_timer_5', 'voice')
|
||||
|
||||
@patch('libs.global_objects.audio')
|
||||
@patch('libs.global_objects.get_config')
|
||||
@patch('libs.global_objects.global_tex')
|
||||
def test_timer_finish_callback(self, mock_tex, mock_config, mock_audio):
|
||||
"""Test callback is triggered when timer reaches zero."""
|
||||
mock_config.return_value = {"general": {"timer_frozen": False}}
|
||||
mock_tex.get_animation.return_value = Mock(update=Mock(), start=Mock())
|
||||
mock_audio.is_sound_playing.return_value = False
|
||||
mock_func = Mock()
|
||||
|
||||
timer = Timer(1, 0.0, mock_func)
|
||||
timer.update(1000.0)
|
||||
timer.update(2000.0)
|
||||
|
||||
mock_func.assert_called_once()
|
||||
self.assertTrue(timer.is_finished)
|
||||
|
||||
@patch('libs.global_objects.get_config')
|
||||
@patch('libs.global_objects.global_tex')
|
||||
def test_timer_frozen(self, mock_tex, mock_config):
|
||||
"""Test frozen timer doesn't count down."""
|
||||
mock_config.return_value = {"general": {"timer_frozen": True}}
|
||||
mock_tex.get_animation.return_value = Mock(update=Mock())
|
||||
mock_func = Mock()
|
||||
|
||||
timer = Timer(10, 0.0, mock_func)
|
||||
initial_time = timer.time
|
||||
timer.update(1000.0)
|
||||
|
||||
self.assertEqual(timer.time, initial_time)
|
||||
|
||||
|
||||
class TestCoinOverlay(unittest.TestCase):
|
||||
"""Test cases for the CoinOverlay class."""
|
||||
|
||||
@patch('libs.global_objects.global_tex')
|
||||
@patch('libs.global_objects.global_data')
|
||||
@patch('libs.global_objects.OutlinedText')
|
||||
def test_initialization(self, mock_text, mock_data, mock_tex):
|
||||
"""Test coin overlay initialization."""
|
||||
mock_tex.skin_config = {
|
||||
"free_play": Mock(text={"en": "Free Play"}, font_size=24, y=100)
|
||||
}
|
||||
mock_data.config = {"general": {"language": "en"}}
|
||||
|
||||
_ = CoinOverlay()
|
||||
|
||||
mock_text.assert_called_once()
|
||||
|
||||
|
||||
class TestAllNetIcon(unittest.TestCase):
|
||||
"""Test cases for the AllNetIcon class."""
|
||||
|
||||
@patch('libs.global_objects.get_config')
|
||||
def test_initialization_offline(self, mock_config):
|
||||
"""Test AllNet icon initializes offline."""
|
||||
mock_config.return_value = {"general": {"fake_online": False}}
|
||||
|
||||
icon = AllNetIcon()
|
||||
|
||||
self.assertFalse(icon.online)
|
||||
|
||||
@patch('libs.global_objects.get_config')
|
||||
def test_initialization_online(self, mock_config):
|
||||
"""Test AllNet icon initializes online."""
|
||||
mock_config.return_value = {"general": {"fake_online": True}}
|
||||
|
||||
icon = AllNetIcon()
|
||||
|
||||
self.assertTrue(icon.online)
|
||||
|
||||
|
||||
class TestEntryOverlay(unittest.TestCase):
|
||||
"""Test cases for the EntryOverlay class."""
|
||||
|
||||
@patch('libs.global_objects.get_config')
|
||||
def test_initialization(self, mock_config):
|
||||
"""Test entry overlay initialization."""
|
||||
mock_config.return_value = {"general": {"fake_online": False}}
|
||||
|
||||
overlay = EntryOverlay()
|
||||
|
||||
self.assertFalse(overlay.online)
|
||||
333
test/libs/test_screen.py
Normal file
333
test/libs/test_screen.py
Normal file
@@ -0,0 +1,333 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from libs.screen import Screen
|
||||
|
||||
|
||||
class TestScreen(unittest.TestCase):
|
||||
"""Test cases for the Screen class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.screen_name = "test_screen"
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_initialization(self, mock_audio, mock_tex):
|
||||
"""Test screen initialization."""
|
||||
screen = Screen(self.screen_name)
|
||||
|
||||
self.assertEqual(screen.screen_name, self.screen_name)
|
||||
self.assertFalse(screen.screen_init)
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_on_screen_start(self, mock_audio, mock_tex):
|
||||
"""Test on_screen_start loads textures and sounds."""
|
||||
screen = Screen(self.screen_name)
|
||||
screen.on_screen_start()
|
||||
|
||||
mock_tex.load_screen_textures.assert_called_once_with(self.screen_name)
|
||||
mock_audio.load_screen_sounds.assert_called_once_with(self.screen_name)
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_do_screen_start_first_call(self, mock_audio, mock_tex):
|
||||
"""Test _do_screen_start initializes screen on first call."""
|
||||
screen = Screen(self.screen_name)
|
||||
|
||||
self.assertFalse(screen.screen_init)
|
||||
screen._do_screen_start()
|
||||
|
||||
self.assertTrue(screen.screen_init)
|
||||
mock_tex.load_screen_textures.assert_called_once_with(self.screen_name)
|
||||
mock_audio.load_screen_sounds.assert_called_once_with(self.screen_name)
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_do_screen_start_subsequent_calls(self, mock_audio, mock_tex):
|
||||
"""Test _do_screen_start doesn't reinitialize on subsequent calls."""
|
||||
screen = Screen(self.screen_name)
|
||||
|
||||
screen._do_screen_start()
|
||||
screen._do_screen_start()
|
||||
screen._do_screen_start()
|
||||
|
||||
# Should only be called once despite multiple calls
|
||||
mock_tex.load_screen_textures.assert_called_once()
|
||||
mock_audio.load_screen_sounds.assert_called_once()
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_on_screen_end(self, mock_audio, mock_tex):
|
||||
"""Test on_screen_end unloads resources and returns next screen."""
|
||||
screen = Screen(self.screen_name)
|
||||
screen.screen_init = True
|
||||
|
||||
next_screen = "next_screen"
|
||||
result = screen.on_screen_end(next_screen)
|
||||
|
||||
self.assertEqual(result, next_screen)
|
||||
self.assertFalse(screen.screen_init)
|
||||
mock_audio.unload_all_sounds.assert_called_once()
|
||||
mock_audio.unload_all_music.assert_called_once()
|
||||
mock_tex.unload_textures.assert_called_once()
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_on_screen_end_unload_order(self, mock_audio, mock_tex):
|
||||
"""Test that resources are unloaded in correct order."""
|
||||
screen = Screen(self.screen_name)
|
||||
screen.screen_init = True
|
||||
|
||||
manager = Mock()
|
||||
manager.attach_mock(mock_audio.unload_all_sounds, 'unload_sounds')
|
||||
manager.attach_mock(mock_audio.unload_all_music, 'unload_music')
|
||||
manager.attach_mock(mock_tex.unload_textures, 'unload_textures')
|
||||
|
||||
screen.on_screen_end("next")
|
||||
|
||||
# Verify order: sounds, music, then textures
|
||||
calls = manager.mock_calls
|
||||
self.assertEqual(calls[0][0], 'unload_sounds')
|
||||
self.assertEqual(calls[1][0], 'unload_music')
|
||||
self.assertEqual(calls[2][0], 'unload_textures')
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_update_not_initialized(self, mock_audio, mock_tex):
|
||||
"""Test update initializes screen if not already initialized."""
|
||||
screen = Screen(self.screen_name)
|
||||
|
||||
self.assertFalse(screen.screen_init)
|
||||
screen.update()
|
||||
|
||||
self.assertTrue(screen.screen_init)
|
||||
mock_tex.load_screen_textures.assert_called_once()
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_update_already_initialized(self, mock_audio, mock_tex):
|
||||
"""Test update doesn't reinitialize if already initialized."""
|
||||
screen = Screen(self.screen_name)
|
||||
screen.screen_init = True
|
||||
|
||||
screen.update()
|
||||
|
||||
# Should not load again
|
||||
mock_tex.load_screen_textures.assert_not_called()
|
||||
mock_audio.load_screen_sounds.assert_not_called()
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_update_returns_value(self, mock_audio, mock_tex):
|
||||
"""Test update returns value from _do_screen_start."""
|
||||
screen = Screen(self.screen_name)
|
||||
|
||||
with patch.object(screen, '_do_screen_start', return_value="test_value"):
|
||||
result = screen.update()
|
||||
self.assertEqual(result, "test_value")
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_draw_default_implementation(self, mock_audio, mock_tex):
|
||||
"""Test draw has empty default implementation."""
|
||||
screen = Screen(self.screen_name)
|
||||
|
||||
# Should not raise any errors
|
||||
screen.draw()
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_do_draw_when_initialized(self, mock_audio, mock_tex):
|
||||
"""Test _do_draw calls draw when screen is initialized."""
|
||||
screen = Screen(self.screen_name)
|
||||
screen.screen_init = True
|
||||
|
||||
with patch.object(screen, 'draw') as mock_draw:
|
||||
screen._do_draw()
|
||||
mock_draw.assert_called_once()
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_do_draw_when_not_initialized(self, mock_audio, mock_tex):
|
||||
"""Test _do_draw doesn't call draw when screen is not initialized."""
|
||||
screen = Screen(self.screen_name)
|
||||
screen.screen_init = False
|
||||
|
||||
with patch.object(screen, 'draw') as mock_draw:
|
||||
screen._do_draw()
|
||||
mock_draw.assert_not_called()
|
||||
|
||||
|
||||
class TestScreenSubclass(unittest.TestCase):
|
||||
"""Test cases for Screen subclass behavior."""
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_subclass_custom_on_screen_start(self, mock_audio, mock_tex):
|
||||
"""Test that subclass can override on_screen_start."""
|
||||
class CustomScreen(Screen):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self.custom_init_called = False
|
||||
|
||||
def on_screen_start(self):
|
||||
super().on_screen_start()
|
||||
self.custom_init_called = True
|
||||
|
||||
screen = CustomScreen("custom")
|
||||
screen.on_screen_start()
|
||||
|
||||
self.assertTrue(screen.custom_init_called)
|
||||
mock_tex.load_screen_textures.assert_called_once_with("custom")
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_subclass_custom_update(self, mock_audio, mock_tex):
|
||||
"""Test that subclass can override update."""
|
||||
class CustomScreen(Screen):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self.update_count = 0
|
||||
|
||||
def update(self):
|
||||
result = super().update()
|
||||
self.update_count += 1
|
||||
return result
|
||||
|
||||
screen = CustomScreen("custom")
|
||||
screen.update()
|
||||
screen.update()
|
||||
|
||||
self.assertEqual(screen.update_count, 2)
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_subclass_custom_draw(self, mock_audio, mock_tex):
|
||||
"""Test that subclass can override draw."""
|
||||
class CustomScreen(Screen):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self.draw_called = False
|
||||
|
||||
def draw(self):
|
||||
self.draw_called = True
|
||||
|
||||
screen = CustomScreen("custom")
|
||||
screen.screen_init = True
|
||||
screen._do_draw()
|
||||
|
||||
self.assertTrue(screen.draw_called)
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_subclass_custom_on_screen_end(self, mock_audio, mock_tex):
|
||||
"""Test that subclass can override on_screen_end."""
|
||||
class CustomScreen(Screen):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self.cleanup_called = False
|
||||
|
||||
def on_screen_end(self, next_screen):
|
||||
self.cleanup_called = True
|
||||
return super().on_screen_end(next_screen)
|
||||
|
||||
screen = CustomScreen("custom")
|
||||
screen.screen_init = True
|
||||
result = screen.on_screen_end("next")
|
||||
|
||||
self.assertTrue(screen.cleanup_called)
|
||||
self.assertEqual(result, "next")
|
||||
|
||||
|
||||
class TestScreenLifecycle(unittest.TestCase):
|
||||
"""Test cases for complete screen lifecycle."""
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_full_lifecycle(self, mock_audio, mock_tex):
|
||||
"""Test complete screen lifecycle from start to end."""
|
||||
screen = Screen("lifecycle_test")
|
||||
|
||||
# Initial state
|
||||
self.assertFalse(screen.screen_init)
|
||||
|
||||
# Start screen
|
||||
screen.update()
|
||||
self.assertTrue(screen.screen_init)
|
||||
mock_tex.load_screen_textures.assert_called_once_with("lifecycle_test")
|
||||
mock_audio.load_screen_sounds.assert_called_once_with("lifecycle_test")
|
||||
|
||||
# Multiple updates don't reinitialize
|
||||
screen.update()
|
||||
screen.update()
|
||||
self.assertEqual(mock_tex.load_screen_textures.call_count, 1)
|
||||
|
||||
# Draw while initialized
|
||||
with patch.object(screen, 'draw') as mock_draw:
|
||||
screen._do_draw()
|
||||
mock_draw.assert_called_once()
|
||||
|
||||
# End screen
|
||||
result = screen.on_screen_end("next_screen")
|
||||
self.assertEqual(result, "next_screen")
|
||||
self.assertFalse(screen.screen_init)
|
||||
mock_audio.unload_all_sounds.assert_called_once()
|
||||
mock_audio.unload_all_music.assert_called_once()
|
||||
mock_tex.unload_textures.assert_called_once()
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_multiple_screen_transitions(self, mock_audio, mock_tex):
|
||||
"""Test transitioning between multiple screens."""
|
||||
screen1 = Screen("screen1")
|
||||
screen2 = Screen("screen2")
|
||||
screen3 = Screen("screen3")
|
||||
|
||||
# Initialize first screen
|
||||
screen1.update()
|
||||
self.assertTrue(screen1.screen_init)
|
||||
|
||||
# Transition to second screen
|
||||
next_name = screen1.on_screen_end("screen2")
|
||||
self.assertEqual(next_name, "screen2")
|
||||
self.assertFalse(screen1.screen_init)
|
||||
|
||||
screen2.update()
|
||||
self.assertTrue(screen2.screen_init)
|
||||
|
||||
# Transition to third screen
|
||||
next_name = screen2.on_screen_end("screen3")
|
||||
self.assertEqual(next_name, "screen3")
|
||||
self.assertFalse(screen2.screen_init)
|
||||
|
||||
screen3.update()
|
||||
self.assertTrue(screen3.screen_init)
|
||||
|
||||
@patch('libs.screen.tex')
|
||||
@patch('libs.screen.audio')
|
||||
def test_screen_reinitialize_after_end(self, mock_audio, mock_tex):
|
||||
"""Test that screen can be reinitialized after ending."""
|
||||
screen = Screen("reinit_test")
|
||||
|
||||
# First initialization
|
||||
screen.update()
|
||||
self.assertTrue(screen.screen_init)
|
||||
|
||||
# End screen
|
||||
screen.on_screen_end("next")
|
||||
self.assertFalse(screen.screen_init)
|
||||
|
||||
# Reinitialize
|
||||
mock_tex.load_screen_textures.reset_mock()
|
||||
mock_audio.load_screen_sounds.reset_mock()
|
||||
|
||||
screen.update()
|
||||
self.assertTrue(screen.screen_init)
|
||||
mock_tex.load_screen_textures.assert_called_once()
|
||||
mock_audio.load_screen_sounds.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
289
test/libs/test_texture.py
Normal file
289
test/libs/test_texture.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from libs.texture import (
|
||||
FramedTexture,
|
||||
SkinInfo,
|
||||
Texture,
|
||||
TextureWrapper,
|
||||
)
|
||||
|
||||
|
||||
class TestSkinInfo(unittest.TestCase):
|
||||
"""Test cases for the SkinInfo dataclass."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test SkinInfo initialization."""
|
||||
skin_info = SkinInfo(
|
||||
x=100.0,
|
||||
y=200.0,
|
||||
font_size=24,
|
||||
width=300.0,
|
||||
height=100.0,
|
||||
text={"en": "Test", "ja": "テスト"}
|
||||
)
|
||||
|
||||
self.assertEqual(skin_info.x, 100.0)
|
||||
self.assertEqual(skin_info.y, 200.0)
|
||||
self.assertEqual(skin_info.font_size, 24)
|
||||
self.assertEqual(skin_info.width, 300.0)
|
||||
self.assertEqual(skin_info.height, 100.0)
|
||||
self.assertEqual(skin_info.text, {"en": "Test", "ja": "テスト"})
|
||||
|
||||
def test_repr(self):
|
||||
"""Test SkinInfo string representation."""
|
||||
skin_info = SkinInfo(
|
||||
x=100.0,
|
||||
y=200.0,
|
||||
font_size=24,
|
||||
width=300.0,
|
||||
height=100.0,
|
||||
text={"en": "Test"}
|
||||
)
|
||||
|
||||
repr_str = repr(skin_info)
|
||||
self.assertIn("100.0", repr_str)
|
||||
self.assertIn("200.0", repr_str)
|
||||
|
||||
|
||||
class TestTexture(unittest.TestCase):
|
||||
"""Test cases for the Texture class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_texture = Mock()
|
||||
self.mock_texture.width = 100
|
||||
self.mock_texture.height = 50
|
||||
|
||||
@patch('libs.texture.ray')
|
||||
def test_initialization_single_texture(self, mock_ray):
|
||||
"""Test Texture initialization with single texture."""
|
||||
texture = Texture(
|
||||
name="test_texture",
|
||||
texture=self.mock_texture,
|
||||
init_vals={}
|
||||
)
|
||||
|
||||
self.assertEqual(texture.name, "test_texture")
|
||||
self.assertEqual(texture.texture, self.mock_texture)
|
||||
self.assertEqual(texture.width, 100)
|
||||
self.assertEqual(texture.height, 50)
|
||||
self.assertEqual(texture.x, [0])
|
||||
self.assertEqual(texture.y, [0])
|
||||
|
||||
@patch('libs.texture.ray')
|
||||
def test_initialization_with_init_vals(self, mock_ray):
|
||||
"""Test Texture initialization with init_vals."""
|
||||
init_vals = {"x": 10, "y": 20}
|
||||
texture = Texture(
|
||||
name="test",
|
||||
texture=self.mock_texture,
|
||||
init_vals=init_vals
|
||||
)
|
||||
|
||||
self.assertEqual(texture.init_vals, init_vals)
|
||||
self.assertEqual(texture.name, "test")
|
||||
|
||||
@patch('libs.texture.ray')
|
||||
def test_default_values(self, mock_ray):
|
||||
"""Test Texture default values."""
|
||||
texture = Texture(name="test", texture=self.mock_texture, init_vals={})
|
||||
|
||||
self.assertEqual(texture.x, [0])
|
||||
self.assertEqual(texture.y, [0])
|
||||
self.assertEqual(texture.x2, [100])
|
||||
self.assertEqual(texture.y2, [50])
|
||||
self.assertEqual(texture.controllable, [False])
|
||||
|
||||
@patch('libs.texture.ray')
|
||||
def test_repr(self, mock_ray):
|
||||
"""Test Texture string representation."""
|
||||
texture = Texture(name="test", texture=self.mock_texture, init_vals={})
|
||||
|
||||
repr_str = repr(texture)
|
||||
self.assertIn("test", repr_str)
|
||||
|
||||
|
||||
class TestFramedTexture(unittest.TestCase):
|
||||
"""Test cases for the FramedTexture class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_textures = [Mock() for _ in range(4)]
|
||||
for tex in self.mock_textures:
|
||||
tex.width = 200
|
||||
tex.height = 100
|
||||
|
||||
@patch('libs.texture.ray')
|
||||
def test_initialization(self, mock_ray):
|
||||
"""Test FramedTexture initialization."""
|
||||
framed = FramedTexture(
|
||||
name="test_framed",
|
||||
texture=self.mock_textures,
|
||||
init_vals={}
|
||||
)
|
||||
|
||||
self.assertEqual(framed.name, "test_framed")
|
||||
self.assertEqual(framed.texture, self.mock_textures)
|
||||
self.assertEqual(framed.width, 200)
|
||||
self.assertEqual(framed.height, 100)
|
||||
self.assertEqual(framed.x, [0])
|
||||
self.assertEqual(framed.y, [0])
|
||||
|
||||
@patch('libs.texture.ray')
|
||||
def test_default_values(self, mock_ray):
|
||||
"""Test FramedTexture default values."""
|
||||
framed = FramedTexture(
|
||||
name="test",
|
||||
texture=self.mock_textures,
|
||||
init_vals={}
|
||||
)
|
||||
|
||||
self.assertEqual(framed.x, [0])
|
||||
self.assertEqual(framed.y, [0])
|
||||
self.assertEqual(framed.x2, [200])
|
||||
self.assertEqual(framed.y2, [100])
|
||||
|
||||
|
||||
class TestTextureWrapper(unittest.TestCase):
|
||||
"""Test cases for the TextureWrapper class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_texture = Mock()
|
||||
self.mock_texture.width = 100
|
||||
self.mock_texture.height = 50
|
||||
|
||||
@patch('libs.texture.get_config')
|
||||
@patch('libs.texture.Path')
|
||||
def test_initialization(self, mock_path_cls, mock_get_config):
|
||||
"""Test TextureWrapper initialization."""
|
||||
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
|
||||
|
||||
# Mock the skin_config.json file
|
||||
mock_path_instance = Mock()
|
||||
mock_config_path = Mock()
|
||||
mock_config_path.exists.return_value = True
|
||||
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
|
||||
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
|
||||
mock_path_cls.return_value = mock_path_instance
|
||||
|
||||
wrapper = TextureWrapper()
|
||||
|
||||
self.assertEqual(wrapper.screen_width, 1280)
|
||||
self.assertEqual(wrapper.screen_height, 720)
|
||||
self.assertIsInstance(wrapper.textures, dict)
|
||||
|
||||
@patch('libs.texture.get_config')
|
||||
@patch('libs.texture.Path')
|
||||
def test_get_animation(self, mock_path_cls, mock_get_config):
|
||||
"""Test getting animation from list."""
|
||||
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
|
||||
|
||||
# Mock the skin_config.json file
|
||||
mock_path_instance = Mock()
|
||||
mock_config_path = Mock()
|
||||
mock_config_path.exists.return_value = True
|
||||
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
|
||||
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
|
||||
mock_path_cls.return_value = mock_path_instance
|
||||
|
||||
mock_animation = Mock()
|
||||
|
||||
wrapper = TextureWrapper()
|
||||
wrapper.animations = {0: mock_animation}
|
||||
|
||||
result = wrapper.get_animation(0)
|
||||
|
||||
self.assertEqual(result, mock_animation)
|
||||
|
||||
@patch('libs.texture.get_config')
|
||||
@patch('libs.texture.Path')
|
||||
@patch('libs.texture.copy.deepcopy')
|
||||
def test_get_animation_copy(self, mock_deepcopy, mock_path_cls, mock_get_config):
|
||||
"""Test getting animation copy."""
|
||||
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
|
||||
|
||||
# Mock the skin_config.json file
|
||||
mock_path_instance = Mock()
|
||||
mock_config_path = Mock()
|
||||
mock_config_path.exists.return_value = True
|
||||
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
|
||||
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
|
||||
mock_path_cls.return_value = mock_path_instance
|
||||
|
||||
mock_animation = Mock()
|
||||
mock_copy = Mock()
|
||||
mock_deepcopy.return_value = mock_copy
|
||||
|
||||
wrapper = TextureWrapper()
|
||||
wrapper.animations = {0: mock_animation}
|
||||
|
||||
result = wrapper.get_animation(0, is_copy=True)
|
||||
|
||||
mock_deepcopy.assert_called_once_with(mock_animation)
|
||||
self.assertEqual(result, mock_copy)
|
||||
|
||||
@patch('libs.texture.get_config')
|
||||
@patch('libs.texture.Path')
|
||||
@patch('libs.texture.ray')
|
||||
def test_read_tex_obj_data(self, mock_ray, mock_path_cls, mock_get_config):
|
||||
"""Test reading texture object data from JSON."""
|
||||
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
|
||||
|
||||
# Mock the skin_config.json file
|
||||
mock_path_instance = Mock()
|
||||
mock_config_path = Mock()
|
||||
mock_config_path.exists.return_value = True
|
||||
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
|
||||
|
||||
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
|
||||
mock_path_cls.return_value = mock_path_instance
|
||||
|
||||
wrapper = TextureWrapper()
|
||||
|
||||
# Create a mock texture object
|
||||
mock_texture = Mock()
|
||||
mock_texture.x = [0]
|
||||
mock_texture.y = [0]
|
||||
|
||||
# Test with a dictionary mapping
|
||||
tex_mapping = {"x": 10, "y": 20}
|
||||
wrapper._read_tex_obj_data(tex_mapping, mock_texture)
|
||||
|
||||
# Verify the texture attributes were updated (they are lists)
|
||||
self.assertEqual(mock_texture.x, [10])
|
||||
self.assertEqual(mock_texture.y, [20])
|
||||
|
||||
@patch('libs.texture.get_config')
|
||||
@patch('libs.texture.Path')
|
||||
def test_read_tex_obj_data_not_exists(self, mock_path_cls, mock_get_config):
|
||||
"""Test reading texture data with empty mapping."""
|
||||
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
|
||||
|
||||
# Mock the skin_config.json file
|
||||
mock_path_instance = Mock()
|
||||
mock_config_path = Mock()
|
||||
mock_config_path.exists.return_value = True
|
||||
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
|
||||
|
||||
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
|
||||
mock_path_cls.return_value = mock_path_instance
|
||||
|
||||
wrapper = TextureWrapper()
|
||||
|
||||
# Create a mock texture object
|
||||
mock_texture = Mock()
|
||||
mock_texture.x = [0]
|
||||
mock_texture.y = [0]
|
||||
|
||||
# Test with empty mapping (should not modify texture)
|
||||
tex_mapping = {}
|
||||
wrapper._read_tex_obj_data(tex_mapping, mock_texture)
|
||||
|
||||
# Verify the texture attributes remained unchanged
|
||||
self.assertEqual(mock_texture.x, [0])
|
||||
self.assertEqual(mock_texture.y, [0])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
182
uv.lock
generated
182
uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 2
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
@@ -83,6 +83,85 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nuitka"
|
||||
version = "2.8.4"
|
||||
@@ -102,6 +181,24 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634, upload-time = "2022-01-26T14:38:48.677Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
@@ -111,6 +208,47 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstrument"
|
||||
version = "5.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/ce/824ee634994e612156f7b84eaf50b8523c676ebfed8d8dd12939a82f4c15/pyinstrument-5.1.1.tar.gz", hash = "sha256:bc401cda990b3c1cfe8e0e0473cbd605df3c63b73478a89ac4ab108f2184baa8", size = 264730, upload-time = "2025-08-12T11:35:43.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/d4/b94f47aa7d301f6cdf5924bb75caacd0d0a1852bd4e876e3a64fc5798dad/pyinstrument-5.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:45af421c60c943a7f1619afabeba4951d4cc16b4206490d7d5b7ef5a4e2dfd42", size = 130315, upload-time = "2025-08-12T11:34:52.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/42/1bc2f28e139f69a0918d5d5dc1d59e65c640d4da9dd153fa48c2a8a87dd9/pyinstrument-5.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2603db3d745a65de66c96929ab9b0fcce050511eb24e32856ea2458785b8917f", size = 122805, upload-time = "2025-08-12T11:34:54.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/85/2f0c9115cd8a01e0a18d0650d9f3f20ff71e8ca17bd4af60dd3a0cb76f8a/pyinstrument-5.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fe32492100efaa1b0a488c237fe420fdaf141646733a31a97f96c4e1fa6bbf8", size = 148210, upload-time = "2025-08-12T11:34:55.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/62/3c73a63e6913378cc7e9ffb5af1e50836511eee83b7c7bf252fad7ec24e4/pyinstrument-5.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:999b5373f8b1e846357923063ae5c9275ad8a85ed4e0a42960a349288d1f5007", size = 146995, upload-time = "2025-08-12T11:34:57.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/8b/d21f4b6d8849881e9572967818e3e6d2dcb212e7dfa89e4e356d359db32b/pyinstrument-5.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58a2f69052178ec624e4df0cf546eda48b3a381572ac1cb3272b4c163888af9d", size = 147029, upload-time = "2025-08-12T11:34:58.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/4d/1e43cecf2bcf4a3dd1100f4fc7a3da6438a65d0b95ca7b8ab5d094ea7c0b/pyinstrument-5.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9bbc00d2e258edbefeb39b61ad4636099b08acd1effdd40d76883a13e7bf5a", size = 146668, upload-time = "2025-08-12T11:34:59.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/48/00322b48e7adb665d04303b487454eb0c13a76ec0af8da20f452098fcc12/pyinstrument-5.1.1-cp313-cp313-win32.whl", hash = "sha256:cf2d8933e2aeaa02d4cb6279d83ef11ee882fb243fff96e3378153a730aadd6e", size = 124288, upload-time = "2025-08-12T11:35:00.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/14/d56515a110f74799aefc7489c1578ce4d99a4d731309559a427f954e7abc/pyinstrument-5.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:2402683a92617066b13a6d48f904396dcd15938016875b392534df027660eed4", size = 125041, upload-time = "2025-08-12T11:35:01.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/2b/e4bdcabb5ae67de2ec3fa1f6e4eb4ae707b0bf460f895d4594792cdc919b/pyinstrument-5.1.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:688acba1c00cad73e43254e610f8e384a53ced3b0dbb5268fb44636e2b99663e", size = 130358, upload-time = "2025-08-12T11:35:03.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/36/616f8db63997c096d3fb65e657cdf5bd2a63b53ed24a14750770dc500979/pyinstrument-5.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:116f5ad8cec4d6f5626305d7c1a104f5845a084bfb4b192d231eb8c41ea81f9a", size = 122827, upload-time = "2025-08-12T11:35:04.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/7a/4f5d2bbc7c2466d46eb5ff47c6e667464eead47140e01a64be45215a59d4/pyinstrument-5.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d139d12a637001d3884344330054ce8335b2c8165dc3dd239726e1b358576bd", size = 147947, upload-time = "2025-08-12T11:35:05.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/c9b0081c0e52789a910390ce44e54c1318999d74386f15d92d0deb522aff/pyinstrument-5.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc5b87b1e27bec94457fed8d03c755a3c09edb4f35d975dbdffd77d863173254", size = 146702, upload-time = "2025-08-12T11:35:07.202Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/1b/745ed7997da22ae68ff21b8f28e5e3a97b220335dce4ee7cf46d5eb17b32/pyinstrument-5.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15f4a2ed9562efab34b555e1208955cf9681b2272489d7a59cd0e289344ada2e", size = 146836, upload-time = "2025-08-12T11:35:08.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/f0/05cefdcf79d1901f9d179e7f55f3acaadbc5fee7af955cebb3f555280638/pyinstrument-5.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1cb0c79bfa2b2b5734213429c9d7f455e5af664cfde785c69a5780f6c532c1fd", size = 146463, upload-time = "2025-08-12T11:35:09.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/cb/6a6f33316be3c7b8247f8ca0e418a2b6fb68d64c227169b7dbee50009366/pyinstrument-5.1.1-cp314-cp314-win32.whl", hash = "sha256:3b9f1216ae4848a8983dc405e1a42e46e75bd8ae96aaba328d4358b8fc80a7a0", size = 124950, upload-time = "2025-08-12T11:35:11.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/ea/99caeb29f446f57d077a83c7c5f2b7c27c1719984d425f679bf2ec1eb6b0/pyinstrument-5.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:26971d4a17e0d5d4f6737e71c9de7a7ce5c83ab7daf078c6bf330be41d65273b", size = 125720, upload-time = "2025-08-12T11:35:12.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d0/953b75d634565ef34f8ed559f2e4af7cd1f2d5f5b578092e8f1d8199e4b1/pyinstrument-5.1.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:62362843884d654401ec4c25fed35f4b4ded077d96b3396f1e791c31e4203d3e", size = 131258, upload-time = "2025-08-12T11:35:13.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/a4/4ec87cfd0974d79b2fcd72b3e20336fc65b96a5b08f2eb2867bf71b27b82/pyinstrument-5.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f2d640230b71c6d9ac8f27a9c5cd07fc8a6acad9196d1e48d9c33658b176fb80", size = 123276, upload-time = "2025-08-12T11:35:14.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/f8/6a210989c8ede85f91b7e4ba5d9730492f1d081762570c06c750d787536c/pyinstrument-5.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f54f7292c63461c75ddf193f5e733803e463ccbc54f2fb7c9591337ddea7d10", size = 155767, upload-time = "2025-08-12T11:35:16.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/a8/5ac81ffbfe36d2e5c3332a9452746a21540987da0d9491db751a905bba13/pyinstrument-5.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c156eb442f9f22960ae16bd195051863d5e8a68b877926e88bbaf8bbdc1456d1", size = 153423, upload-time = "2025-08-12T11:35:17.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/55/5620c2a61403cde044e81e33056c14fbf5793eea33f67f2223d61abec9ae/pyinstrument-5.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:caadaf67ad5926c46af784316024793c909b9e9ee550475855fd32171c4bd033", size = 153542, upload-time = "2025-08-12T11:35:18.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/83/a8f22466652250a847dfdf58f9a2717b470fdbbcb075c7f730bf608041a6/pyinstrument-5.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88ef2e8f483a5e1501d79a7ebdab592a597467810ed24d8db09ab6f568e938d3", size = 152337, upload-time = "2025-08-12T11:35:19.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/a6/cd4590da14deaeda6315519c26064874bbb9648a1358b80e8a8ca5d4add0/pyinstrument-5.1.1-cp314-cp314t-win32.whl", hash = "sha256:265bc4389f82e6521777bfab426a62a15c4940955e86f75db79a44e7349f9757", size = 125621, upload-time = "2025-08-12T11:35:21.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/30/177102e798539368aef25688a6a171d66ec92e6f16b6b651a89045a2bd13/pyinstrument-5.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fa254f269a72a007b5d02c18cd4b67081e0efabbd33e18acdbd5e3be905afa06", size = 126528, upload-time = "2025-08-12T11:35:22.578Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pypresence"
|
||||
version = "4.6.1"
|
||||
@@ -122,11 +260,13 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytaiko"
|
||||
version = "1.0"
|
||||
version = "1.1"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "av" },
|
||||
{ name = "pyinstrument" },
|
||||
{ name = "pypresence" },
|
||||
{ name = "pytest" },
|
||||
{ name = "raylib-sdl" },
|
||||
{ name = "tomlkit" },
|
||||
]
|
||||
@@ -134,18 +274,54 @@ dependencies = [
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "nuitka" },
|
||||
{ name = "pytest-cov" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "av", specifier = ">=16.0.1" },
|
||||
{ name = "pyinstrument", specifier = ">=5.1.1" },
|
||||
{ name = "pypresence", specifier = ">=4.6.1" },
|
||||
{ name = "pytest", specifier = ">=9.0.2" },
|
||||
{ name = "raylib-sdl", specifier = ">=5.5.0.2" },
|
||||
{ name = "tomlkit", specifier = ">=0.13.3" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "nuitka", specifier = ">=2.8.4" }]
|
||||
dev = [
|
||||
{ name = "nuitka", specifier = ">=2.8.4" },
|
||||
{ name = "pytest-cov", specifier = ">=6.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raylib-sdl"
|
||||
|
||||
Reference in New Issue
Block a user