mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 19:50:12 +01:00
Compare commits
49 Commits
latest
...
655c2683cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
655c2683cf | ||
|
|
e0b7f0a863 | ||
|
|
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 | ||
|
|
327c48aa1a | ||
|
|
3a18a507c0 | ||
|
|
2769503899 | ||
|
|
e719119764 | ||
|
|
174f322696 | ||
|
|
33125c6322 | ||
|
|
e97bd5bd4c | ||
|
|
88acfe5e5b | ||
|
|
73abcddf44 | ||
|
|
7ca8ff8c38 | ||
|
|
22778dbd3d | ||
|
|
d70e734661 | ||
|
|
2c09360bfd | ||
|
|
58d7043a50 | ||
|
|
27c58cc97e | ||
|
|
20c1f1141e | ||
|
|
4844792aaa | ||
|
|
201b37dda0 | ||
|
|
77886d34cb | ||
|
|
74080634de | ||
|
|
f7ab62ab1d | ||
|
|
5c7e759385 |
293
.github/workflows/python-app.yml
vendored
293
.github/workflows/python-app.yml
vendored
@@ -1,143 +1,186 @@
|
|||||||
name: PyTaiko
|
name: PyTaiko
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["main"]
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
issues: write
|
issues: write
|
||||||
repository-projects: write
|
repository-projects: write
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-22.04, windows-latest]
|
os: [ubuntu-22.04, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check-out repository
|
- name: Check-out repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install libaudio Dependencies (macOS)
|
- name: Install libaudio Dependencies (macOS)
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
run: |
|
run: |
|
||||||
brew update
|
brew update
|
||||||
brew install portaudio libsndfile speexdsp ccache
|
brew install portaudio libsndfile speexdsp ccache
|
||||||
|
|
||||||
- name: Install libaudio Dependencies (Windows)
|
- name: Install libaudio Dependencies (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
uses: msys2/setup-msys2@v2
|
uses: msys2/setup-msys2@v2
|
||||||
with:
|
with:
|
||||||
update: true
|
update: true
|
||||||
install: >-
|
install: >-
|
||||||
base-devel
|
base-devel
|
||||||
mingw-w64-x86_64-gcc
|
mingw-w64-x86_64-gcc
|
||||||
mingw-w64-x86_64-libsndfile
|
mingw-w64-x86_64-libsndfile
|
||||||
mingw-w64-x86_64-speexdsp
|
mingw-w64-x86_64-speexdsp
|
||||||
mingw-w64-x86_64-ccache
|
mingw-w64-x86_64-ccache
|
||||||
|
|
||||||
- name: Install libaudio Dependencies (Linux)
|
- name: Install libaudio Dependencies (Linux)
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
libsndfile1-dev \
|
libsndfile1-dev \
|
||||||
libspeexdsp-dev \
|
libspeexdsp-dev \
|
||||||
portaudio19-dev \
|
portaudio19-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
ccache \
|
ccache \
|
||||||
|
|
||||||
- name: Build libaudio (Windows)
|
- name: Build libaudio (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
shell: msys2 {0}
|
shell: msys2 {0}
|
||||||
run: |
|
run: |
|
||||||
cd libs/audio
|
cd libs/audio
|
||||||
make clean
|
make clean
|
||||||
make all
|
make all
|
||||||
make verify
|
make verify
|
||||||
|
|
||||||
- name: Build libaudio (Unix)
|
- name: Build libaudio (Unix)
|
||||||
if: runner.os != 'Windows'
|
if: runner.os != 'Windows'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd libs/audio
|
cd libs/audio
|
||||||
make clean
|
make clean
|
||||||
make all
|
make all
|
||||||
make verify
|
make verify
|
||||||
|
|
||||||
- name: Upload libaudio Artifacts
|
- name: Upload libaudio Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: libaudio-${{ runner.os }}-${{ runner.arch }}
|
name: libaudio-${{ runner.os }}-${{ runner.arch }}
|
||||||
path: |
|
path: |
|
||||||
libs/audio/libaudio.dll
|
libs/audio/libaudio.dll
|
||||||
libs/audio/libaudio.so
|
libs/audio/libaudio.so
|
||||||
libs/audio/libaudio.dylib
|
libs/audio/libaudio.dylib
|
||||||
libs/audio/*.a
|
libs/audio/*.a
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v4
|
uses: astral-sh/setup-uv@v4
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
run: uv python install
|
run: uv python install
|
||||||
|
|
||||||
- name: Build Executable
|
- name: Install dependencies
|
||||||
shell: bash
|
run: uv sync
|
||||||
run: |
|
|
||||||
uv run nuitka \
|
|
||||||
--lto=yes \
|
|
||||||
--mode=app \
|
|
||||||
--noinclude-setuptools-mode=nofollow \
|
|
||||||
--noinclude-IPython-mode=nofollow \
|
|
||||||
--assume-yes-for-downloads \
|
|
||||||
--windows-icon-from-ico=libs/icon.ico \
|
|
||||||
--macos-app-icon=libs/icon.icns \
|
|
||||||
--linux-icon=libs/icon.png \
|
|
||||||
PyTaiko.py
|
|
||||||
|
|
||||||
- name: Create Release Directory
|
- name: Copy libaudio to project root (Windows)
|
||||||
run: |
|
if: runner.os == 'Windows'
|
||||||
mkdir -p release
|
shell: bash
|
||||||
cp -r Skins Songs config.toml shader model release/
|
run: |
|
||||||
|
cp libs/audio/*.dll . 2>/dev/null || echo "libaudio not found"
|
||||||
|
|
||||||
if [ "${{ runner.os }}" == "Windows" ]; then
|
- name: Copy libaudio to project root (macOS)
|
||||||
cp libs/audio/*.dll release/ 2>/dev/null || echo "libaudio not found"
|
if: runner.os == 'macOS'
|
||||||
elif [ "${{ runner.os }}" == "macOS" ]; then
|
shell: bash
|
||||||
cp libs/audio/libaudio.dylib release/ 2>/dev/null || echo "libaudio not found"
|
run: |
|
||||||
else
|
cp libs/audio/libaudio.dylib . 2>/dev/null || echo "libaudio not found"
|
||||||
cp libs/audio/libaudio.so release/ 2>/dev/null || echo "libaudio not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${{ runner.os }}" == "Windows" ]; then
|
- name: Copy libaudio to project root (Linux)
|
||||||
cp *.exe release/ 2>/dev/null || echo "No .exe files found"
|
if: runner.os == 'Linux'
|
||||||
elif [ "${{ runner.os }}" == "macOS" ]; then
|
shell: bash
|
||||||
cp -r *.app release/ 2>/dev/null || echo "No .app bundles found"
|
run: |
|
||||||
else
|
cp libs/audio/libaudio.so . 2>/dev/null || echo "libaudio not found"
|
||||||
cp *.bin release/ 2>/dev/null || echo "No .bin files found"
|
|
||||||
fi
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Create Zip Archive
|
- name: Run tests (excluding audio tests)
|
||||||
run: |
|
run: uv run pytest test/libs/ -v --tb=short --color=yes --ignore=test/libs/test_audio.py
|
||||||
cd release
|
continue-on-error: false
|
||||||
if [ "${{ runner.os }}" == "Windows" ]; then
|
env:
|
||||||
7z a ../PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip *
|
PYTHONPATH: ${{ github.workspace }}
|
||||||
else
|
|
||||||
zip -r ../PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip *
|
|
||||||
fi
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Build Executable
|
||||||
uses: softprops/action-gh-release@v2
|
shell: bash
|
||||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
run: |
|
||||||
with:
|
uv run nuitka \
|
||||||
files: PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip
|
--lto=yes \
|
||||||
name: "PyTaiko [Rolling Release]"
|
--mode=app \
|
||||||
tag_name: "latest"
|
--noinclude-setuptools-mode=nofollow \
|
||||||
make_latest: true
|
--noinclude-IPython-mode=nofollow \
|
||||||
env:
|
--assume-yes-for-downloads \
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
--windows-icon-from-ico=libs/icon.ico \
|
||||||
|
--macos-app-icon=libs/icon.icns \
|
||||||
|
--linux-icon=libs/icon.png \
|
||||||
|
PyTaiko.py
|
||||||
|
|
||||||
|
- name: Create Release Directory
|
||||||
|
run: |
|
||||||
|
mkdir -p release
|
||||||
|
cp -r Skins Songs config.toml shader model release/
|
||||||
|
|
||||||
|
if [ "${{ runner.os }}" == "Windows" ]; then
|
||||||
|
cp libs/audio/*.dll release/ 2>/dev/null || echo "libaudio not found"
|
||||||
|
elif [ "${{ runner.os }}" == "macOS" ]; then
|
||||||
|
cp libs/audio/libaudio.dylib release/ 2>/dev/null || echo "libaudio not found"
|
||||||
|
else
|
||||||
|
cp libs/audio/libaudio.so release/ 2>/dev/null || echo "libaudio not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ runner.os }}" == "Windows" ]; then
|
||||||
|
cp *.exe release/ 2>/dev/null || echo "No .exe files found"
|
||||||
|
elif [ "${{ runner.os }}" == "macOS" ]; then
|
||||||
|
cp -r *.app release/ 2>/dev/null || echo "No .app bundles found"
|
||||||
|
else
|
||||||
|
cp *.bin release/ 2>/dev/null || echo "No .bin files found"
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Create Zip Archive
|
||||||
|
run: |
|
||||||
|
cd release
|
||||||
|
if [ "${{ runner.os }}" == "Windows" ]; then
|
||||||
|
7z a ../PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip *
|
||||||
|
else
|
||||||
|
zip -r ../PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip *
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload Build Artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: pytaiko-${{ runner.os }}-${{ runner.arch }}
|
||||||
|
path: PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
steps:
|
||||||
|
- name: Download All Artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: pytaiko-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: PyTaiko-*.zip
|
||||||
|
name: "PyTaiko [Rolling Release]"
|
||||||
|
tag_name: "latest"
|
||||||
|
make_latest: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
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
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,8 +5,8 @@ cache
|
|||||||
dev-config.toml
|
dev-config.toml
|
||||||
libaudio.so
|
libaudio.so
|
||||||
latest.log
|
latest.log
|
||||||
libaudio.dll
|
|
||||||
libaudio.dylib
|
libaudio.dylib
|
||||||
scores.db
|
scores.db
|
||||||
scores_gen3.db
|
scores_gen3.db
|
||||||
./libs/audio/audio.o
|
./libs/audio/audio.o
|
||||||
|
*.dll
|
||||||
|
|||||||
287
PyTaiko.py
287
PyTaiko.py
@@ -1,12 +1,13 @@
|
|||||||
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
import sqlite3
|
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 (
|
from raylib.defines import (
|
||||||
RL_FUNC_ADD,
|
RL_FUNC_ADD,
|
||||||
RL_ONE,
|
RL_ONE,
|
||||||
@@ -15,6 +16,7 @@ from raylib.defines import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
|
from libs.config import get_config
|
||||||
from libs.global_data import PlayerNum, ScoreMethod
|
from libs.global_data import PlayerNum, ScoreMethod
|
||||||
from libs.screen import Screen
|
from libs.screen import Screen
|
||||||
from libs.song_hash import DB_VERSION
|
from libs.song_hash import DB_VERSION
|
||||||
@@ -23,27 +25,26 @@ from libs.utils import (
|
|||||||
force_dedicated_gpu,
|
force_dedicated_gpu,
|
||||||
get_current_ms,
|
get_current_ms,
|
||||||
global_data,
|
global_data,
|
||||||
global_tex
|
global_tex,
|
||||||
)
|
)
|
||||||
from libs.config import get_config
|
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
|
||||||
from scenes.devtest import DevScreen
|
from scenes.devtest import DevScreen
|
||||||
from scenes.entry import EntryScreen
|
from scenes.entry import EntryScreen
|
||||||
from scenes.game import GameScreen
|
from scenes.game import GameScreen
|
||||||
from scenes.dan.game_dan import DanGameScreen
|
from scenes.loading import LoadScreen
|
||||||
from scenes.practice.game import PracticeGameScreen
|
from scenes.practice.game import PracticeGameScreen
|
||||||
from scenes.practice.song_select import PracticeSongSelectScreen
|
from scenes.practice.song_select import PracticeSongSelectScreen
|
||||||
from scenes.two_player.game import TwoPlayerGameScreen
|
|
||||||
from scenes.two_player.result import TwoPlayerResultScreen
|
|
||||||
from scenes.loading import LoadScreen
|
|
||||||
from scenes.result import ResultScreen
|
from scenes.result import ResultScreen
|
||||||
from scenes.settings import SettingsScreen
|
from scenes.settings import SettingsScreen
|
||||||
from scenes.song_select import SongSelectScreen
|
from scenes.song_select import SongSelectScreen
|
||||||
from scenes.title import TitleScreen
|
from scenes.title import TitleScreen
|
||||||
|
from scenes.two_player.game import TwoPlayerGameScreen
|
||||||
|
from scenes.two_player.result import TwoPlayerResultScreen
|
||||||
from scenes.two_player.song_select import TwoPlayerSongSelectScreen
|
from scenes.two_player.song_select import TwoPlayerSongSelectScreen
|
||||||
from scenes.dan.dan_select import DanSelectScreen
|
|
||||||
from scenes.dan.dan_result import DanResultScreen
|
|
||||||
|
|
||||||
from pypresence.presence import Presence
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
DISCORD_APP_ID = '1451423960401973353'
|
DISCORD_APP_ID = '1451423960401973353'
|
||||||
@@ -69,6 +70,8 @@ class Screens:
|
|||||||
DAN_RESULT = "DAN_RESULT"
|
DAN_RESULT = "DAN_RESULT"
|
||||||
PRACTICE_SELECT = "PRACTICE_SELECT"
|
PRACTICE_SELECT = "PRACTICE_SELECT"
|
||||||
GAME_PRACTICE = "GAME_PRACTICE"
|
GAME_PRACTICE = "GAME_PRACTICE"
|
||||||
|
AI_SELECT = "AI_SELECT"
|
||||||
|
AI_GAME = "AI_GAME"
|
||||||
SETTINGS = "SETTINGS"
|
SETTINGS = "SETTINGS"
|
||||||
DEV_MENU = "DEV_MENU"
|
DEV_MENU = "DEV_MENU"
|
||||||
LOADING = "LOADING"
|
LOADING = "LOADING"
|
||||||
@@ -163,12 +166,12 @@ def create_song_db():
|
|||||||
|
|
||||||
def update_camera_for_window_size(camera, virtual_width, virtual_height):
|
def update_camera_for_window_size(camera, virtual_width, virtual_height):
|
||||||
"""Update camera zoom, offset, scale, and rotation to maintain aspect ratio"""
|
"""Update camera zoom, offset, scale, and rotation to maintain aspect ratio"""
|
||||||
screen_width = ray.get_screen_width()
|
screen_width = ray.GetScreenWidth()
|
||||||
screen_height = ray.get_screen_height()
|
screen_height = ray.GetScreenHeight()
|
||||||
|
|
||||||
if screen_width == 0 or screen_height == 0:
|
if screen_width == 0 or screen_height == 0:
|
||||||
camera.zoom = 1.0
|
camera.zoom = 1.0
|
||||||
camera.offset = ray.Vector2(0, 0)
|
camera.offset = (0, 0)
|
||||||
camera.rotation = 0.0
|
camera.rotation = 0.0
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -188,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
|
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
|
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_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)
|
base_offset_y - zoom_offset_y - v_scale_offset_y + (global_data.camera.offset.y * scale)
|
||||||
)
|
)
|
||||||
|
|
||||||
camera.rotation = global_data.camera.rotation
|
camera.rotation = global_data.camera.rotation
|
||||||
|
|
||||||
def main():
|
def setup_logging():
|
||||||
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'
|
|
||||||
log_level = global_data.config["general"]["log_level"]
|
log_level = global_data.config["general"]["log_level"]
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
import io
|
import io
|
||||||
@@ -221,70 +217,20 @@ def main():
|
|||||||
handlers=[console_handler, file_handler]
|
handlers=[console_handler, file_handler]
|
||||||
)
|
)
|
||||||
sys.excepthook = handle_exception
|
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"]:
|
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")
|
logger.info("VSync enabled")
|
||||||
if global_data.config["video"]["target_fps"] != -1:
|
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']}")
|
logger.info(f"Target FPS set to {global_data.config['video']['target_fps']}")
|
||||||
ray.set_config_flags(ray.ConfigFlags.FLAG_MSAA_4X_HINT)
|
ray.SetConfigFlags(ray.FLAG_MSAA_4X_HINT)
|
||||||
ray.set_config_flags(ray.ConfigFlags.FLAG_WINDOW_RESIZABLE)
|
ray.SetConfigFlags(ray.FLAG_WINDOW_RESIZABLE)
|
||||||
ray.set_trace_log_level(ray.TraceLogLevel.LOG_WARNING)
|
ray.SetTraceLogLevel(ray.LOG_WARNING)
|
||||||
|
|
||||||
ray.init_window(screen_width, screen_height, "PyTaiko")
|
def init_audio():
|
||||||
logger.info(f"Window initialized: {screen_width}x{screen_height}")
|
audio.set_log_level((logger.level-1)//10)
|
||||||
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
|
|
||||||
|
|
||||||
if len(sys.argv) == 1:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
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,
|
|
||||||
help='Difficulty level (optional, defaults to max difficulty)')
|
|
||||||
parser.add_argument('--auto', action='store_true',
|
|
||||||
help='Enable auto mode')
|
|
||||||
parser.add_argument('--practice', action='store_true',
|
|
||||||
help='Start in practice mode')
|
|
||||||
args = parser.parse_args()
|
|
||||||
path = Path(args.song_path)
|
|
||||||
if not path.exists():
|
|
||||||
parser.error(f"Song file not found: {args.song_path}")
|
|
||||||
else:
|
|
||||||
path = Path(os.path.abspath(path))
|
|
||||||
tja = TJAParser(path)
|
|
||||||
if args.difficulty is not None:
|
|
||||||
if args.difficulty not in tja.metadata.course_data.keys():
|
|
||||||
parser.error(f"Invalid difficulty: {args.difficulty}. Available: {list(tja.metadata.course_data.keys())}")
|
|
||||||
selected_difficulty = args.difficulty
|
|
||||||
else:
|
|
||||||
selected_difficulty = max(tja.metadata.course_data.keys())
|
|
||||||
current_screen = Screens.GAME_PRACTICE if args.practice else Screens.GAME
|
|
||||||
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
|
|
||||||
|
|
||||||
logger.info(f"Initial screen: {current_screen}")
|
|
||||||
|
|
||||||
audio.set_log_level((log_level-1)//10)
|
|
||||||
old_stderr = os.dup(2)
|
old_stderr = os.dup(2)
|
||||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||||
os.dup2(devnull, 2)
|
os.dup2(devnull, 2)
|
||||||
@@ -294,6 +240,103 @@ def main():
|
|||||||
os.close(old_stderr)
|
os.close(old_stderr)
|
||||||
logger.info("Audio device initialized")
|
logger.info("Audio device initialized")
|
||||||
|
|
||||||
|
def check_args():
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
return Screens.SETTINGS
|
||||||
|
|
||||||
|
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,
|
||||||
|
help='Difficulty level (optional, defaults to max difficulty)')
|
||||||
|
parser.add_argument('--auto', action='store_true',
|
||||||
|
help='Enable auto mode')
|
||||||
|
parser.add_argument('--practice', action='store_true',
|
||||||
|
help='Start in practice mode')
|
||||||
|
args = parser.parse_args()
|
||||||
|
path = Path(args.song_path)
|
||||||
|
if not path.exists():
|
||||||
|
parser.error(f"Song file not found: {args.song_path}")
|
||||||
|
else:
|
||||||
|
path = Path(os.path.abspath(path))
|
||||||
|
tja = TJAParser(path)
|
||||||
|
if args.difficulty is not None:
|
||||||
|
if args.difficulty not in tja.metadata.course_data.keys():
|
||||||
|
parser.error(f"Invalid difficulty: {args.difficulty}. Available: {list(tja.metadata.course_data.keys())}")
|
||||||
|
selected_difficulty = args.difficulty
|
||||||
|
else:
|
||||||
|
selected_difficulty = max(tja.metadata.course_data.keys())
|
||||||
|
current_screen = Screens.GAME_PRACTICE if args.practice else Screens.GAME
|
||||||
|
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}")
|
||||||
|
|
||||||
create_song_db()
|
create_song_db()
|
||||||
|
|
||||||
title_screen = TitleScreen('title')
|
title_screen = TitleScreen('title')
|
||||||
@@ -305,6 +348,8 @@ def main():
|
|||||||
game_screen_2p = TwoPlayerGameScreen('game')
|
game_screen_2p = TwoPlayerGameScreen('game')
|
||||||
game_screen_practice = PracticeGameScreen('game')
|
game_screen_practice = PracticeGameScreen('game')
|
||||||
practice_select_screen = PracticeSongSelectScreen('song_select')
|
practice_select_screen = PracticeSongSelectScreen('song_select')
|
||||||
|
ai_select_screen = AISongSelectScreen('song_select')
|
||||||
|
ai_game_screen = AIBattleGameScreen('game')
|
||||||
result_screen = ResultScreen('result')
|
result_screen = ResultScreen('result')
|
||||||
result_screen_2p = TwoPlayerResultScreen('result')
|
result_screen_2p = TwoPlayerResultScreen('result')
|
||||||
settings_screen = SettingsScreen('settings')
|
settings_screen = SettingsScreen('settings')
|
||||||
@@ -322,6 +367,8 @@ def main():
|
|||||||
Screens.GAME: game_screen,
|
Screens.GAME: game_screen,
|
||||||
Screens.GAME_2P: game_screen_2p,
|
Screens.GAME_2P: game_screen_2p,
|
||||||
Screens.GAME_PRACTICE: game_screen_practice,
|
Screens.GAME_PRACTICE: game_screen_practice,
|
||||||
|
Screens.AI_SELECT: ai_select_screen,
|
||||||
|
Screens.AI_GAME: ai_game_screen,
|
||||||
Screens.RESULT: result_screen,
|
Screens.RESULT: result_screen,
|
||||||
Screens.RESULT_2P: result_screen_2p,
|
Screens.RESULT_2P: result_screen_2p,
|
||||||
Screens.SETTINGS: settings_screen,
|
Screens.SETTINGS: settings_screen,
|
||||||
@@ -332,51 +379,44 @@ def main():
|
|||||||
Screens.LOADING: load_screen
|
Screens.LOADING: load_screen
|
||||||
}
|
}
|
||||||
|
|
||||||
camera = ray.Camera2D()
|
camera = pyray.Camera2D()
|
||||||
camera.target = ray.Vector2(0, 0)
|
camera.target = pyray.Vector2(0, 0)
|
||||||
camera.rotation = 0.0
|
camera.rotation = 0.0
|
||||||
update_camera_for_window_size(camera, screen_width, screen_height)
|
update_camera_for_window_size(camera, screen_width, screen_height)
|
||||||
logger.info("Camera2D initialized")
|
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.rlSetBlendFactorsSeparate(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.SetExitKey(global_data.config["keys"]["exit_key"])
|
||||||
|
|
||||||
ray.hide_cursor()
|
ray.HideCursor()
|
||||||
logger.info("Cursor hidden")
|
logger.info("Cursor hidden")
|
||||||
last_fps = 1
|
last_fps = 1
|
||||||
last_color = ray.BLACK
|
last_color = pyray.BLACK
|
||||||
|
last_discord_check = 0
|
||||||
|
|
||||||
while not ray.window_should_close():
|
while not ray.WindowShouldClose():
|
||||||
if discord_connected:
|
current_time = get_current_ms()
|
||||||
if global_data.session_data[global_data.player_num].selected_song != Path():
|
if discord_connected and current_time > last_discord_check + 1000:
|
||||||
details = f"Playing Song: {global_data.session_data[global_data.player_num].song_title}"
|
check_discord_heartbeat(current_screen)
|
||||||
else:
|
last_discord_check = current_time
|
||||||
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"}]
|
|
||||||
)
|
|
||||||
|
|
||||||
if ray.is_key_pressed(global_data.config["keys"]["fullscreen_key"]):
|
if ray.IsKeyPressed(global_data.config["keys"]["fullscreen_key"]):
|
||||||
ray.toggle_fullscreen()
|
ray.ToggleFullscreen()
|
||||||
logger.info("Toggled fullscreen")
|
logger.info("Toggled fullscreen")
|
||||||
elif ray.is_key_pressed(global_data.config["keys"]["borderless_key"]):
|
elif ray.IsKeyPressed(global_data.config["keys"]["borderless_key"]):
|
||||||
ray.toggle_borderless_windowed()
|
ray.ToggleBorderlessWindowed()
|
||||||
logger.info("Toggled borderless windowed mode")
|
logger.info("Toggled borderless windowed mode")
|
||||||
|
|
||||||
update_camera_for_window_size(camera, screen_width, screen_height)
|
update_camera_for_window_size(camera, screen_width, screen_height)
|
||||||
|
|
||||||
ray.begin_drawing()
|
ray.BeginDrawing()
|
||||||
|
|
||||||
if global_data.camera.border_color != last_color:
|
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
|
last_color = global_data.camera.border_color
|
||||||
|
|
||||||
ray.begin_mode_2d(camera)
|
pyray.begin_mode_2d(camera)
|
||||||
ray.begin_blend_mode(ray.BlendMode.BLEND_CUSTOM_SEPARATE)
|
ray.BeginBlendMode(ray.BLEND_CUSTOM_SEPARATE)
|
||||||
|
|
||||||
screen = screen_mapping[current_screen]
|
screen = screen_mapping[current_screen]
|
||||||
|
|
||||||
@@ -390,26 +430,15 @@ def main():
|
|||||||
global_data.input_locked = 0
|
global_data.input_locked = 0
|
||||||
|
|
||||||
if global_data.config["general"]["fps_counter"]:
|
if global_data.config["general"]["fps_counter"]:
|
||||||
curr_fps = ray.get_fps()
|
draw_fps(last_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)
|
|
||||||
|
|
||||||
ray.draw_rectangle(-screen_width, 0, screen_width, screen_height, last_color)
|
draw_outer_border(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)
|
|
||||||
|
|
||||||
ray.end_blend_mode()
|
ray.EndBlendMode()
|
||||||
ray.end_mode_2d()
|
ray.EndMode2D()
|
||||||
ray.end_drawing()
|
ray.EndDrawing()
|
||||||
|
|
||||||
ray.close_window()
|
ray.CloseWindow()
|
||||||
audio.close_audio_device()
|
audio.close_audio_device()
|
||||||
if discord_connected:
|
if discord_connected:
|
||||||
RPC.close()
|
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://github.com/Yonokid/PyTaiko/stargazers)
|
||||||
[](https://discord.gg/XHcVYKW)
|
[](https://discord.gg/XHcVYKW)
|
||||||
[](https://github.com/Yonokid/PyTaiko/actions/workflows/python-app.yml)
|
[](https://github.com/Yonokid/PyTaiko/actions/workflows/python-app.yml)
|
||||||
|
[](https://github.com/Yonokid/PyTaiko/actions/workflows/tests.yml)
|
||||||
|
|
||||||
<img src="/docs/demo.gif">
|
<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>
|
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>
|
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)"
|
# Move to the place where you want to download Pytaiko, this example will be in the desktop
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
# Make sure the directory is not saved in iCloud! This will create an issue on macOS where the executable cannot compile.
|
||||||
source $HOME/.local/bin/env
|
cd ~/Desktop
|
||||||
|
|
||||||
|
# Installing dependencies
|
||||||
|
brew install python@3.12 uv git speexdsp libsndfile
|
||||||
|
|
||||||
|
# Downloading Pytaiko project
|
||||||
git clone https://github.com/Yonokid/PyTaiko
|
git clone https://github.com/Yonokid/PyTaiko
|
||||||
cd PyTaiko
|
cd PyTaiko
|
||||||
brew install libsndfile
|
|
||||||
brew install speexdsp
|
# Compiling audio libraries
|
||||||
cd libs/audio
|
cd libs/audio
|
||||||
make
|
make
|
||||||
mv libaudio.dylib ../../
|
mv libaudio.dylib ...
|
||||||
cd ../../
|
cd ...
|
||||||
|
|
||||||
|
# Running game (for testing)
|
||||||
uv run PyTaiko.py
|
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
|
## Installation
|
||||||
|
|
||||||
@@ -71,7 +83,7 @@ Download the latest release for your operating system from the [releases page](h
|
|||||||
2. Run `PyTaiko.exe`
|
2. Run `PyTaiko.exe`
|
||||||
|
|
||||||
#### macOS
|
#### 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
|
#### Linux
|
||||||
- Try running the compiled `PyTaiko.bin` binary
|
- Try running the compiled `PyTaiko.bin` binary
|
||||||
|
|||||||
Submodule Skins/PyTaikoGreen updated: cc253a534d...5527b51f3a
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
|
||||||
@@ -29,9 +29,7 @@ rainbow = false
|
|||||||
|
|
||||||
[paths]
|
[paths]
|
||||||
tja_path = ['Songs']
|
tja_path = ['Songs']
|
||||||
video_path = ['Videos']
|
skin = 'PyTaikoGreen'
|
||||||
#You can change this path to Graphics/GreenVer1080 for the 1080p skin
|
|
||||||
graphics_path = 'Graphics/GreenVer'
|
|
||||||
|
|
||||||
[keys]
|
[keys]
|
||||||
exit_key = 'Q'
|
exit_key = 'Q'
|
||||||
|
|||||||
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",
|
||||||
|
]
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import cffi
|
|
||||||
import platform
|
|
||||||
import logging
|
import logging
|
||||||
|
import platform
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from libs.config import VolumeConfig
|
import cffi
|
||||||
from libs.config import get_config
|
|
||||||
|
from libs.config import VolumeConfig, get_config
|
||||||
|
|
||||||
ffi = cffi.FFI()
|
ffi = cffi.FFI()
|
||||||
|
|
||||||
@@ -119,7 +119,8 @@ except OSError as e:
|
|||||||
|
|
||||||
class AudioEngine:
|
class AudioEngine:
|
||||||
"""Initialize an audio engine for playing sounds and music."""
|
"""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)
|
self.device_type = max(device_type, 0)
|
||||||
if sample_rate < 0:
|
if sample_rate < 0:
|
||||||
self.target_sample_rate = 44100
|
self.target_sample_rate = 44100
|
||||||
@@ -131,7 +132,10 @@ class AudioEngine:
|
|||||||
self.audio_device_ready = False
|
self.audio_device_ready = False
|
||||||
self.volume_presets = volume_presets
|
self.volume_presets = volume_presets
|
||||||
|
|
||||||
self.sounds_path = Path("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):
|
def set_log_level(self, level: int):
|
||||||
lib.set_log_level(level) # type: ignore
|
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)))
|
current_milestone = min(self.max_dancers - 1, int(gauge_1p.gauge_length / (clear_threshold / self.max_dancers)))
|
||||||
else:
|
else:
|
||||||
current_milestone = self.max_dancers
|
current_milestone = self.max_dancers
|
||||||
|
|
||||||
if current_milestone > self.last_milestone and current_milestone <= self.max_dancers:
|
if current_milestone > self.last_milestone and current_milestone <= self.max_dancers:
|
||||||
self.dancer.add_dancer()
|
self.dancer.add_dancer()
|
||||||
self.last_milestone = current_milestone
|
self.last_milestone = current_milestone
|
||||||
logger.info(f"Dancer milestone reached: {current_milestone}/{self.max_dancers}")
|
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 self.bg_fever is not None and gauge_1p is not None:
|
||||||
if not self.is_clear and gauge_1p.is_clear:
|
if not self.is_clear and gauge_1p.is_clear:
|
||||||
self.bg_fever.start()
|
self.bg_fever.start()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
from libs.bg_objects.bg_fever import BGFever4
|
from libs.bg_objects.bg_fever import BGFever4
|
||||||
from libs.bg_objects.bg_normal import BGNormal2
|
from libs.bg_objects.bg_normal import BGNormal2
|
||||||
@@ -9,6 +10,7 @@ from libs.bg_objects.renda import RendaController
|
|||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
|
|
||||||
|
|
||||||
class Background:
|
class Background:
|
||||||
def __init__(self, tex: TextureWrapper, player_num: PlayerNum, bpm: float, path: str, max_dancers: int):
|
def __init__(self, tex: TextureWrapper, player_num: PlayerNum, bpm: float, path: str, max_dancers: int):
|
||||||
self.tex_wrapper = tex
|
self.tex_wrapper = tex
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from libs.bg_objects.bg_fever import BGFeverBase
|
from libs.bg_objects.bg_fever import BGFeverBase
|
||||||
from libs.bg_objects.bg_normal import BGNormalBase
|
from libs.bg_objects.bg_normal import BGNormalBase
|
||||||
from libs.bg_objects.chibi import ChibiController
|
from libs.bg_objects.chibi import ChibiController
|
||||||
from libs.bg_objects.dancer import BaseDancer, BaseDancerGroup
|
from libs.bg_objects.dancer import BaseDancer, BaseDancerGroup
|
||||||
|
from libs.bg_objects.don_bg import DonBG4
|
||||||
from libs.bg_objects.fever import Fever3
|
from libs.bg_objects.fever import Fever3
|
||||||
from libs.bg_objects.footer import Footer
|
from libs.bg_objects.footer import Footer
|
||||||
from libs.bg_objects.renda import RendaController
|
from libs.bg_objects.renda import RendaController
|
||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
from libs.bg_objects.don_bg import DonBG4
|
|
||||||
|
|
||||||
|
|
||||||
class Background:
|
class Background:
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from libs.bg_objects.bg_fever import BGFeverBase
|
from libs.bg_objects.bg_fever import BGFeverBase
|
||||||
from libs.bg_objects.bg_normal import BGNormalBase
|
from libs.bg_objects.bg_normal import BGNormalBase
|
||||||
from libs.bg_objects.chibi import ChibiController
|
from libs.bg_objects.chibi import ChibiController
|
||||||
from libs.bg_objects.dancer import BaseDancer, BaseDancerGroup
|
from libs.bg_objects.dancer import BaseDancer, BaseDancerGroup
|
||||||
|
from libs.bg_objects.don_bg import DonBG4
|
||||||
from libs.bg_objects.fever import Fever3
|
from libs.bg_objects.fever import Fever3
|
||||||
from libs.bg_objects.renda import RendaController
|
from libs.bg_objects.renda import RendaController
|
||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
from libs.bg_objects.don_bg import DonBG4
|
|
||||||
|
|
||||||
|
|
||||||
class Background:
|
class Background:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from libs.bg_objects.footer import Footer
|
|||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
|
|
||||||
|
|
||||||
class Background:
|
class Background:
|
||||||
def __init__(self, tex: TextureWrapper, player_num: PlayerNum, bpm: float, path: str, max_dancers: int):
|
def __init__(self, tex: TextureWrapper, player_num: PlayerNum, bpm: float, path: str, max_dancers: int):
|
||||||
self.tex_wrapper = tex
|
self.tex_wrapper = tex
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
|
import pyray as ray
|
||||||
|
|
||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
from libs.bg_objects.bg_fever import BGFeverBase
|
from libs.bg_objects.bg_fever import BGFeverBase
|
||||||
from libs.bg_objects.bg_normal import BGNormalBase
|
from libs.bg_objects.bg_normal import BGNormalBase
|
||||||
from libs.bg_objects.chibi import ChibiController
|
from libs.bg_objects.chibi import ChibiController
|
||||||
from libs.bg_objects.dancer import BaseDancerGroup
|
from libs.bg_objects.dancer import BaseDancerGroup
|
||||||
|
from libs.bg_objects.don_bg import DonBGBase
|
||||||
from libs.bg_objects.fever import BaseFever
|
from libs.bg_objects.fever import BaseFever
|
||||||
from libs.bg_objects.footer import Footer
|
from libs.bg_objects.footer import Footer
|
||||||
from libs.bg_objects.renda import RendaController
|
from libs.bg_objects.renda import RendaController
|
||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
from libs.bg_objects.don_bg import DonBGBase
|
|
||||||
|
|
||||||
import pyray as ray
|
|
||||||
|
|
||||||
|
|
||||||
class Background:
|
class Background:
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ from libs.bg_objects.bg_fever import BGFeverBase
|
|||||||
from libs.bg_objects.bg_normal import BGNormalBase
|
from libs.bg_objects.bg_normal import BGNormalBase
|
||||||
from libs.bg_objects.chibi import ChibiController
|
from libs.bg_objects.chibi import ChibiController
|
||||||
from libs.bg_objects.dancer import BaseDancerGroup
|
from libs.bg_objects.dancer import BaseDancerGroup
|
||||||
|
from libs.bg_objects.don_bg import DonBGBase
|
||||||
from libs.bg_objects.footer import Footer
|
from libs.bg_objects.footer import Footer
|
||||||
from libs.bg_objects.renda import RendaController
|
from libs.bg_objects.renda import RendaController
|
||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
from libs.bg_objects.don_bg import DonBGBase
|
|
||||||
|
|
||||||
|
|
||||||
class Background:
|
class Background:
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
from libs.bg_objects.fever import Fever3
|
|
||||||
from libs.bg_objects.bg_fever import BGFeverBase
|
from libs.bg_objects.bg_fever import BGFeverBase
|
||||||
from libs.bg_objects.bg_normal import BGNormalBase
|
from libs.bg_objects.bg_normal import BGNormalBase
|
||||||
from libs.bg_objects.chibi import ChibiController
|
from libs.bg_objects.chibi import ChibiController
|
||||||
from libs.bg_objects.dancer import BaseDancer, BaseDancerGroup
|
from libs.bg_objects.dancer import BaseDancer, BaseDancerGroup
|
||||||
from libs.bg_objects.don_bg import DonBGBase
|
from libs.bg_objects.don_bg import DonBGBase
|
||||||
|
from libs.bg_objects.fever import Fever3
|
||||||
from libs.bg_objects.footer import Footer
|
from libs.bg_objects.footer import Footer
|
||||||
from libs.bg_objects.renda import RendaController
|
from libs.bg_objects.renda import RendaController
|
||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
|
|
||||||
|
|
||||||
class Background:
|
class Background:
|
||||||
def __init__(self, tex: TextureWrapper, player_num: PlayerNum, bpm: float, path: str, max_dancers: int):
|
def __init__(self, tex: TextureWrapper, player_num: PlayerNum, bpm: float, path: str, max_dancers: int):
|
||||||
self.tex_wrapper = tex
|
self.tex_wrapper = tex
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
|
import pyray as ray
|
||||||
|
|
||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
|
|
||||||
import pyray as ray
|
|
||||||
|
|
||||||
class Chibi:
|
class Chibi:
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import random
|
|||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
|
|
||||||
|
|
||||||
class Dancer:
|
class Dancer:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -161,6 +162,12 @@ class BaseDancerGroup():
|
|||||||
dancer.start()
|
dancer.start()
|
||||||
self.active_count += 1
|
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):
|
def update(self, current_time_ms: float, bpm: float):
|
||||||
for dancer in self.dancers:
|
for dancer in self.dancers:
|
||||||
dancer.update(current_time_ms, bpm)
|
dancer.update(current_time_ms, bpm)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
|
|
||||||
|
|
||||||
class Fever:
|
class Fever:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
|
|
||||||
|
|
||||||
class Footer:
|
class Footer:
|
||||||
def __init__(self, tex: TextureWrapper, index: int, path: str = 'background'):
|
def __init__(self, tex: TextureWrapper, index: int, path: str = 'background'):
|
||||||
self.index = index
|
self.index = index
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
|
import pyray as ray
|
||||||
|
|
||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
from libs.texture import TextureWrapper
|
from libs.texture import TextureWrapper
|
||||||
|
|
||||||
import pyray as ray
|
|
||||||
|
|
||||||
class Renda:
|
class Renda:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
from libs.utils import global_tex
|
from libs.utils import global_tex
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ class Chara2D:
|
|||||||
self.current_anim = self.past_anim
|
self.current_anim = self.past_anim
|
||||||
self.anims[self.current_anim].restart()
|
self.anims[self.current_anim].restart()
|
||||||
|
|
||||||
def draw(self, x: float = 0, y: float = 0, mirror=False):
|
def draw(self, x: float = 0, y: float = 0, mirror=False, scale=1.0):
|
||||||
"""
|
"""
|
||||||
Draw the character on the screen.
|
Draw the character on the screen.
|
||||||
|
|
||||||
@@ -116,9 +117,9 @@ class Chara2D:
|
|||||||
mirror (bool): Whether to mirror the character horizontally.
|
mirror (bool): Whether to mirror the character horizontally.
|
||||||
"""
|
"""
|
||||||
if self.is_rainbow and self.current_anim not in {'soul_in', 'balloon_pop', 'balloon_popping'}:
|
if self.is_rainbow and self.current_anim not in {'soul_in', 'balloon_pop', 'balloon_popping'}:
|
||||||
self.tex.draw_texture(self.name, self.current_anim + '_max', frame=self.anims[self.current_anim].attribute, x=x, y=y)
|
self.tex.draw_texture(self.name, self.current_anim + '_max', frame=self.anims[self.current_anim].attribute, x=x, y=y, scale=scale)
|
||||||
else:
|
else:
|
||||||
if mirror:
|
if mirror:
|
||||||
self.tex.draw_texture(self.name, self.current_anim, frame=self.anims[self.current_anim].attribute, x=x, y=y, mirror='horizontal')
|
self.tex.draw_texture(self.name, self.current_anim, frame=self.anims[self.current_anim].attribute, x=x, y=y, mirror='horizontal', scale=scale)
|
||||||
else:
|
else:
|
||||||
self.tex.draw_texture(self.name, self.current_anim, frame=self.anims[self.current_anim].attribute, x=x, y=y)
|
self.tex.draw_texture(self.name, self.current_anim, frame=self.anims[self.current_anim].attribute, x=x, y=y, scale=scale)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from pathlib import Path
|
|
||||||
import tomlkit
|
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
import tomlkit
|
||||||
|
|
||||||
|
|
||||||
class GeneralConfig(TypedDict):
|
class GeneralConfig(TypedDict):
|
||||||
fps_counter: bool
|
fps_counter: bool
|
||||||
@@ -30,8 +31,7 @@ class NameplateConfig(TypedDict):
|
|||||||
|
|
||||||
class PathsConfig(TypedDict):
|
class PathsConfig(TypedDict):
|
||||||
tja_path: list[Path]
|
tja_path: list[Path]
|
||||||
video_path: list[Path]
|
skin: Path
|
||||||
graphics_path: Path
|
|
||||||
|
|
||||||
class KeysConfig(TypedDict):
|
class KeysConfig(TypedDict):
|
||||||
exit_key: int
|
exit_key: int
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from enum import IntEnum
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
|
||||||
import random
|
import random
|
||||||
|
import sqlite3
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from enum import IntEnum
|
||||||
|
from pathlib import Path
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from raylib import SHADER_UNIFORM_VEC3
|
|
||||||
from libs.audio import audio
|
|
||||||
from libs.animation import Animation, MoveAnimation
|
|
||||||
from libs.global_data import Crown, Difficulty, ScoreMethod
|
|
||||||
from libs.tja import TJAParser, test_encodings
|
|
||||||
from libs.texture import tex
|
|
||||||
from libs.utils import OutlinedText, get_current_ms, global_data
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import sqlite3
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
from raylib import SHADER_UNIFORM_VEC3
|
||||||
|
|
||||||
|
from libs.animation import Animation, MoveAnimation
|
||||||
|
from libs.audio import audio
|
||||||
|
from libs.global_data import Crown, Difficulty, ScoreMethod
|
||||||
|
from libs.texture import tex
|
||||||
|
from libs.tja import TJAParser, test_encodings
|
||||||
|
from libs.utils import OutlinedText, get_current_ms, global_data
|
||||||
|
|
||||||
BOX_CENTER = 594 * tex.screen_scale
|
BOX_CENTER = 594 * tex.screen_scale
|
||||||
|
|
||||||
@@ -109,18 +110,22 @@ class BaseBox():
|
|||||||
else:
|
else:
|
||||||
self.fore_color = ray.Color(101, 0, 82, 255)
|
self.fore_color = ray.Color(101, 0, 82, 255)
|
||||||
self.position = float('inf')
|
self.position = float('inf')
|
||||||
self.start_position = -1.0
|
self.start_position = float('inf')
|
||||||
self.target_position = -1.0
|
self.target_position = float('inf')
|
||||||
self.open_anim = Animation.create_move(233, total_distance=150*tex.screen_scale, delay=50)
|
self.open_anim = Animation.create_move(233, total_distance=150*tex.screen_scale, delay=50)
|
||||||
self.open_fade = Animation.create_fade(200, initial_opacity=0, final_opacity=1.0)
|
self.open_fade = Animation.create_fade(200, initial_opacity=0, final_opacity=1.0)
|
||||||
self.move = None
|
self.move = Animation.create_move(133, total_distance=100 * tex.screen_scale, ease_out='cubic')
|
||||||
|
self.move.start()
|
||||||
self.shader = None
|
self.shader = None
|
||||||
self.is_open = False
|
self.is_open = False
|
||||||
self.text_loaded = False
|
self.text_loaded = False
|
||||||
self.wait = 0
|
self.wait = 0
|
||||||
|
|
||||||
def load_text(self):
|
def load_text(self):
|
||||||
self.name = OutlinedText(self.text_name, tex.skin_config["song_box_name"].font_size, ray.WHITE, outline_thickness=5, vertical=True)
|
font_size = tex.skin_config["song_box_name"].font_size
|
||||||
|
if len(self.text_name) >= 30:
|
||||||
|
font_size -= int(10 * tex.screen_scale)
|
||||||
|
self.name = OutlinedText(self.text_name, font_size, ray.WHITE, outline_thickness=5, vertical=True)
|
||||||
if self.back_color is not None:
|
if self.back_color is not None:
|
||||||
self.shader = ray.load_shader('shader/dummy.vs', 'shader/colortransform.fs')
|
self.shader = ray.load_shader('shader/dummy.vs', 'shader/colortransform.fs')
|
||||||
source_rgb = (142, 212, 30)
|
source_rgb = (142, 212, 30)
|
||||||
@@ -132,30 +137,22 @@ class BaseBox():
|
|||||||
ray.set_shader_value(self.shader, source_loc, source_color, SHADER_UNIFORM_VEC3)
|
ray.set_shader_value(self.shader, source_loc, source_color, SHADER_UNIFORM_VEC3)
|
||||||
ray.set_shader_value(self.shader, target_loc, target_color, SHADER_UNIFORM_VEC3)
|
ray.set_shader_value(self.shader, target_loc, target_color, SHADER_UNIFORM_VEC3)
|
||||||
|
|
||||||
def move_box(self, current_time: float):
|
def move_box(self, direction: int):
|
||||||
if self.position != self.target_position and self.move is None:
|
if self.position != self.target_position:
|
||||||
if self.position < self.target_position:
|
distance = abs(self.target_position - self.position)
|
||||||
direction = 1
|
self.move = Animation.create_move(133, total_distance=distance * tex.screen_scale * direction, ease_out='cubic')
|
||||||
else:
|
|
||||||
direction = -1
|
|
||||||
if abs(self.target_position - self.position) > 250 * tex.screen_scale:
|
|
||||||
direction *= -1
|
|
||||||
self.move = Animation.create_move(133, total_distance=100 * direction * tex.screen_scale, ease_out='cubic')
|
|
||||||
self.move.start()
|
|
||||||
if self.is_open or self.target_position == BOX_CENTER:
|
|
||||||
self.move.total_distance = int(250 * direction * tex.screen_scale)
|
|
||||||
self.start_position = self.position
|
self.start_position = self.position
|
||||||
if self.move is not None:
|
self.move.start()
|
||||||
self.move.update(current_time)
|
|
||||||
self.position = self.start_position + int(self.move.attribute)
|
|
||||||
if self.move.is_finished:
|
|
||||||
self.position = self.target_position
|
|
||||||
self.move = None
|
|
||||||
|
|
||||||
def update(self, current_time: float, is_diff_select: bool):
|
def update(self, current_time: float, is_diff_select: bool):
|
||||||
self.is_diff_select = is_diff_select
|
self.is_diff_select = is_diff_select
|
||||||
self.open_anim.update(current_time)
|
self.open_anim.update(current_time)
|
||||||
self.open_fade.update(current_time)
|
self.open_fade.update(current_time)
|
||||||
|
self.move.update(current_time)
|
||||||
|
if not self.move.is_finished:
|
||||||
|
self.position = self.start_position + int(self.move.attribute)
|
||||||
|
else:
|
||||||
|
self.position = self.target_position
|
||||||
|
|
||||||
def _draw_closed(self, x: float, y: float, outer_fade_override: float):
|
def _draw_closed(self, x: float, y: float, outer_fade_override: float):
|
||||||
if self.shader is not None and self.texture_index == TextureIndex.BLANK:
|
if self.shader is not None and self.texture_index == TextureIndex.BLANK:
|
||||||
@@ -192,7 +189,6 @@ class BackBox(BaseBox):
|
|||||||
def update(self, current_time: float, is_diff_select: bool):
|
def update(self, current_time: float, is_diff_select: bool):
|
||||||
super().update(current_time, is_diff_select)
|
super().update(current_time, is_diff_select)
|
||||||
is_open_prev = self.is_open
|
is_open_prev = self.is_open
|
||||||
self.move_box(current_time)
|
|
||||||
self.is_open = self.position == BOX_CENTER
|
self.is_open = self.position == BOX_CENTER
|
||||||
|
|
||||||
if self.yellow_box is not None:
|
if self.yellow_box is not None:
|
||||||
@@ -253,7 +249,6 @@ class SongBox(BaseBox):
|
|||||||
def update(self, current_time: float, is_diff_select: bool):
|
def update(self, current_time: float, is_diff_select: bool):
|
||||||
super().update(current_time, is_diff_select)
|
super().update(current_time, is_diff_select)
|
||||||
is_open_prev = self.is_open
|
is_open_prev = self.is_open
|
||||||
self.move_box(current_time)
|
|
||||||
self.is_open = self.position == BOX_CENTER
|
self.is_open = self.position == BOX_CENTER
|
||||||
|
|
||||||
if self.yellow_box is not None:
|
if self.yellow_box is not None:
|
||||||
@@ -324,7 +319,6 @@ class FolderBox(BaseBox):
|
|||||||
def update(self, current_time: float, is_diff_select: bool):
|
def update(self, current_time: float, is_diff_select: bool):
|
||||||
super().update(current_time, is_diff_select)
|
super().update(current_time, is_diff_select)
|
||||||
is_open_prev = self.is_open
|
is_open_prev = self.is_open
|
||||||
self.move_box(current_time)
|
|
||||||
self.is_open = self.position == BOX_CENTER
|
self.is_open = self.position == BOX_CENTER
|
||||||
|
|
||||||
if not is_open_prev and self.is_open:
|
if not is_open_prev and self.is_open:
|
||||||
@@ -652,7 +646,6 @@ class DanBox(BaseBox):
|
|||||||
def update(self, current_time: float, is_diff_select: bool):
|
def update(self, current_time: float, is_diff_select: bool):
|
||||||
super().update(current_time, is_diff_select)
|
super().update(current_time, is_diff_select)
|
||||||
is_open_prev = self.is_open
|
is_open_prev = self.is_open
|
||||||
self.move_box(current_time)
|
|
||||||
self.is_open = self.position == BOX_CENTER
|
self.is_open = self.position == BOX_CENTER
|
||||||
if not is_open_prev and self.is_open:
|
if not is_open_prev and self.is_open:
|
||||||
self.yellow_box = YellowBox(False, is_dan=True)
|
self.yellow_box = YellowBox(False, is_dan=True)
|
||||||
@@ -781,7 +774,8 @@ class GenreBG:
|
|||||||
if self.shader is not None and self.end_box.texture_index == TextureIndex.BLANK:
|
if self.shader is not None and self.end_box.texture_index == TextureIndex.BLANK:
|
||||||
ray.begin_shader_mode(self.shader)
|
ray.begin_shader_mode(self.shader)
|
||||||
offset = (tex.skin_config["genre_bg_offset"].x * -1) if self.start_box.is_open else 0
|
offset = (tex.skin_config["genre_bg_offset"].x * -1) if self.start_box.is_open else 0
|
||||||
|
if (344 * tex.screen_scale < self.start_box.position < 594 * tex.screen_scale):
|
||||||
|
offset = -self.start_position + 444 * tex.screen_scale
|
||||||
tex.draw_texture('box', 'folder_background_edge', frame=self.end_box.texture_index, x=self.start_position+offset, y=y, mirror="horizontal", fade=self.fade_in.attribute)
|
tex.draw_texture('box', 'folder_background_edge', frame=self.end_box.texture_index, x=self.start_position+offset, y=y, mirror="horizontal", fade=self.fade_in.attribute)
|
||||||
|
|
||||||
|
|
||||||
@@ -803,6 +797,8 @@ class GenreBG:
|
|||||||
tex.draw_texture('box', 'folder_background', x=tex.skin_config["genre_bg_folder_background"].x, y=y, x2=x2, frame=self.end_box.texture_index)
|
tex.draw_texture('box', 'folder_background', x=tex.skin_config["genre_bg_folder_background"].x, y=y, x2=x2, frame=self.end_box.texture_index)
|
||||||
|
|
||||||
|
|
||||||
|
if (594 * tex.screen_scale < self.end_box.position < 844 * tex.screen_scale):
|
||||||
|
offset = -self.end_position + 674 * tex.screen_scale
|
||||||
offset = tex.skin_config["genre_bg_offset"].x if self.end_box.is_open else 0
|
offset = tex.skin_config["genre_bg_offset"].x if self.end_box.is_open else 0
|
||||||
tex.draw_texture('box', 'folder_background_edge', x=self.end_position+tex.skin_config["genre_bg_folder_edge"].x+offset, y=y, fade=self.fade_in.attribute, frame=self.end_box.texture_index)
|
tex.draw_texture('box', 'folder_background_edge', x=self.end_position+tex.skin_config["genre_bg_folder_edge"].x+offset, y=y, fade=self.fade_in.attribute, frame=self.end_box.texture_index)
|
||||||
|
|
||||||
@@ -861,13 +857,11 @@ class ScoreHistory:
|
|||||||
tex.draw_texture('leaderboard', 'shinuchi_ura', index=self.long)
|
tex.draw_texture('leaderboard', 'shinuchi_ura', index=self.long)
|
||||||
else:
|
else:
|
||||||
tex.draw_texture('leaderboard', 'shinuchi', index=self.long)
|
tex.draw_texture('leaderboard', 'shinuchi', index=self.long)
|
||||||
|
tex.draw_texture('leaderboard', 'pts', color=ray.WHITE, index=self.long)
|
||||||
case ScoreMethod.GEN3:
|
case ScoreMethod.GEN3:
|
||||||
if self.curr_difficulty == Difficulty.URA:
|
|
||||||
tex.draw_texture('leaderboard', 'normal', index=self.long)
|
|
||||||
else:
|
|
||||||
tex.draw_texture('leaderboard', 'normal', index=self.long)
|
tex.draw_texture('leaderboard', 'normal', index=self.long)
|
||||||
|
tex.draw_texture('leaderboard', 'pts', color=ray.BLACK, index=self.long)
|
||||||
|
|
||||||
tex.draw_texture('leaderboard', 'pts', color=ray.WHITE, index=self.long)
|
|
||||||
tex.draw_texture('leaderboard', 'difficulty', frame=self.curr_difficulty, index=self.long)
|
tex.draw_texture('leaderboard', 'difficulty', frame=self.curr_difficulty, index=self.long)
|
||||||
|
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
@@ -887,7 +881,11 @@ class ScoreHistory:
|
|||||||
margin = tex.skin_config["score_info_counter_margin"].x
|
margin = tex.skin_config["score_info_counter_margin"].x
|
||||||
for i in range(len(counter)):
|
for i in range(len(counter)):
|
||||||
if j == 0:
|
if j == 0:
|
||||||
tex.draw_texture('leaderboard', 'counter', frame=int(counter[i]), x=-((len(counter) * tex.skin_config["score_info_counter_margin"].width) // 2) + (i * tex.skin_config["score_info_counter_margin"].width), color=ray.WHITE, index=self.long)
|
match global_data.config["general"]["score_method"]:
|
||||||
|
case ScoreMethod.SHINUCHI:
|
||||||
|
tex.draw_texture('leaderboard', 'counter', frame=int(counter[i]), x=-((len(counter) * tex.skin_config["score_info_counter_margin"].width) // 2) + (i * tex.skin_config["score_info_counter_margin"].width), color=ray.WHITE, index=self.long)
|
||||||
|
case ScoreMethod.GEN3:
|
||||||
|
tex.draw_texture('leaderboard', 'counter', frame=int(counter[i]), x=-((len(counter) * tex.skin_config["score_info_counter_margin"].width) // 2) + (i * tex.skin_config["score_info_counter_margin"].width), color=ray.BLACK, index=self.long)
|
||||||
else:
|
else:
|
||||||
tex.draw_texture('leaderboard', 'judge_num', frame=int(counter[i]), x=-(len(counter) - i) * margin, y=j*tex.skin_config["score_info_bg_offset"].y)
|
tex.draw_texture('leaderboard', 'judge_num', frame=int(counter[i]), x=-(len(counter) - i) * margin, y=j*tex.skin_config["score_info_bg_offset"].y)
|
||||||
|
|
||||||
@@ -1040,7 +1038,8 @@ class Directory(FileSystemItem):
|
|||||||
'RECENT',
|
'RECENT',
|
||||||
'FAVORITE',
|
'FAVORITE',
|
||||||
'DIFFICULTY',
|
'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):
|
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)
|
super().__init__(path, name)
|
||||||
@@ -1145,6 +1144,7 @@ class FileNavigator:
|
|||||||
self.genre_bg = None
|
self.genre_bg = None
|
||||||
self.song_count = 0
|
self.song_count = 0
|
||||||
self.in_dan_select = False
|
self.in_dan_select = False
|
||||||
|
self.current_search = ''
|
||||||
logger.info("FileNavigator initialized")
|
logger.info("FileNavigator initialized")
|
||||||
|
|
||||||
def initialize(self, root_dirs: list[Path]):
|
def initialize(self, root_dirs: list[Path]):
|
||||||
@@ -1346,6 +1346,90 @@ class FileNavigator:
|
|||||||
"""Check if currently at the virtual root"""
|
"""Check if currently at the virtual root"""
|
||||||
return self.current_dir == Path()
|
return self.current_dir == Path()
|
||||||
|
|
||||||
|
def load_new_items(self, selected_item, dir_key: str):
|
||||||
|
return self.new_items
|
||||||
|
|
||||||
|
def load_recent_items(self, selected_item, dir_key: str):
|
||||||
|
if self.recent_folder is None:
|
||||||
|
raise Exception("tried to enter recent folder without recents")
|
||||||
|
self._generate_objects_recursive(self.recent_folder.path)
|
||||||
|
if not isinstance(selected_item.box, BackBox):
|
||||||
|
selected_item.box.tja_count = self._count_tja_files(self.recent_folder.path)
|
||||||
|
return self.directory_contents[dir_key]
|
||||||
|
|
||||||
|
def load_favorite_items(self, selected_item, dir_key: str):
|
||||||
|
if self.favorite_folder is None:
|
||||||
|
raise Exception("tried to enter favorite folder without favorites")
|
||||||
|
self._generate_objects_recursive(self.favorite_folder.path)
|
||||||
|
tja_files = self._get_tja_files_for_directory(self.favorite_folder.path)
|
||||||
|
self._calculate_directory_crowns(dir_key, tja_files)
|
||||||
|
if not isinstance(selected_item.box, BackBox):
|
||||||
|
selected_item.box.tja_count = self._count_tja_files(self.favorite_folder.path)
|
||||||
|
self.in_favorites = True
|
||||||
|
return self.directory_contents[dir_key]
|
||||||
|
|
||||||
|
def load_diff_sort_items(self, selected_item, dir_key: str):
|
||||||
|
content_items = []
|
||||||
|
parent_dir = selected_item.path.parent
|
||||||
|
for sibling_path in parent_dir.iterdir():
|
||||||
|
if sibling_path.is_dir() and sibling_path != selected_item.path:
|
||||||
|
sibling_key = str(sibling_path)
|
||||||
|
if sibling_key in self.directory_contents:
|
||||||
|
for item in self.directory_contents[sibling_key]:
|
||||||
|
if isinstance(item, SongFile) and item:
|
||||||
|
if self.diff_sort_diff in item.tja.metadata.course_data and item.tja.metadata.course_data[self.diff_sort_diff].level == self.diff_sort_level:
|
||||||
|
if item not in content_items:
|
||||||
|
content_items.append(item)
|
||||||
|
return content_items
|
||||||
|
|
||||||
|
def load_recommended_items(self, selected_item, dir_key: str):
|
||||||
|
parent_dir = selected_item.path.parent
|
||||||
|
temp_items = []
|
||||||
|
for sibling_path in parent_dir.iterdir():
|
||||||
|
if sibling_path.is_dir() and sibling_path != selected_item.path:
|
||||||
|
sibling_key = str(sibling_path)
|
||||||
|
if sibling_key in self.directory_contents:
|
||||||
|
for item in self.directory_contents[sibling_key]:
|
||||||
|
if not isinstance(item, Directory) and isinstance(item, SongFile):
|
||||||
|
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):
|
def load_current_directory(self, selected_item: Optional[Directory] = None):
|
||||||
"""Load pre-generated items for the current directory (unified for root and subdirs)"""
|
"""Load pre-generated items for the current directory (unified for root and subdirs)"""
|
||||||
dir_key = str(self.current_dir)
|
dir_key = str(self.current_dir)
|
||||||
@@ -1383,47 +1467,17 @@ class FileNavigator:
|
|||||||
# Handle special collections (same logic as before)
|
# Handle special collections (same logic as before)
|
||||||
if isinstance(selected_item, Directory):
|
if isinstance(selected_item, Directory):
|
||||||
if selected_item.collection == Directory.COLLECTIONS[0]:
|
if selected_item.collection == Directory.COLLECTIONS[0]:
|
||||||
content_items = self.new_items
|
content_items = self.load_new_items(selected_item, dir_key)
|
||||||
elif selected_item.collection == Directory.COLLECTIONS[1]:
|
elif selected_item.collection == Directory.COLLECTIONS[1]:
|
||||||
if self.recent_folder is None:
|
content_items = self.load_recent_items(selected_item, dir_key)
|
||||||
raise Exception("tried to enter recent folder without recents")
|
|
||||||
self._generate_objects_recursive(self.recent_folder.path)
|
|
||||||
if not isinstance(selected_item.box, BackBox):
|
|
||||||
selected_item.box.tja_count = self._count_tja_files(self.recent_folder.path)
|
|
||||||
content_items = self.directory_contents[dir_key]
|
|
||||||
elif selected_item.collection == Directory.COLLECTIONS[2]:
|
elif selected_item.collection == Directory.COLLECTIONS[2]:
|
||||||
if self.favorite_folder is None:
|
content_items = self.load_favorite_items(selected_item, dir_key)
|
||||||
raise Exception("tried to enter favorite folder without favorites")
|
|
||||||
self._generate_objects_recursive(self.favorite_folder.path)
|
|
||||||
tja_files = self._get_tja_files_for_directory(self.favorite_folder.path)
|
|
||||||
self._calculate_directory_crowns(dir_key, tja_files)
|
|
||||||
if not isinstance(selected_item.box, BackBox):
|
|
||||||
selected_item.box.tja_count = self._count_tja_files(self.favorite_folder.path)
|
|
||||||
content_items = self.directory_contents[dir_key]
|
|
||||||
self.in_favorites = True
|
|
||||||
elif selected_item.collection == Directory.COLLECTIONS[3]:
|
elif selected_item.collection == Directory.COLLECTIONS[3]:
|
||||||
content_items = []
|
content_items = self.load_diff_sort_items(selected_item, dir_key)
|
||||||
parent_dir = selected_item.path.parent
|
|
||||||
for sibling_path in parent_dir.iterdir():
|
|
||||||
if sibling_path.is_dir() and sibling_path != selected_item.path:
|
|
||||||
sibling_key = str(sibling_path)
|
|
||||||
if sibling_key in self.directory_contents:
|
|
||||||
for item in self.directory_contents[sibling_key]:
|
|
||||||
if isinstance(item, SongFile) and item:
|
|
||||||
if self.diff_sort_diff in item.tja.metadata.course_data and item.tja.metadata.course_data[self.diff_sort_diff].level == self.diff_sort_level:
|
|
||||||
if item not in content_items:
|
|
||||||
content_items.append(item)
|
|
||||||
elif selected_item.collection == Directory.COLLECTIONS[4]:
|
elif selected_item.collection == Directory.COLLECTIONS[4]:
|
||||||
parent_dir = selected_item.path.parent
|
content_items = self.load_recommended_items(selected_item, dir_key)
|
||||||
temp_items = []
|
elif selected_item.collection == Directory.COLLECTIONS[5]:
|
||||||
for sibling_path in parent_dir.iterdir():
|
content_items = self.search_song(self.current_search)
|
||||||
if sibling_path.is_dir() and sibling_path != selected_item.path:
|
|
||||||
sibling_key = str(sibling_path)
|
|
||||||
if sibling_key in self.directory_contents:
|
|
||||||
for item in self.directory_contents[sibling_key]:
|
|
||||||
if not isinstance(item, Directory) and isinstance(item, SongFile):
|
|
||||||
temp_items.append(item)
|
|
||||||
content_items = random.sample(temp_items, min(10, len(temp_items)))
|
|
||||||
|
|
||||||
if content_items == []:
|
if content_items == []:
|
||||||
self.go_back()
|
self.go_back()
|
||||||
@@ -1494,7 +1548,7 @@ class FileNavigator:
|
|||||||
# Save current state to history
|
# Save current state to history
|
||||||
self.history.append((self.current_dir, self.selected_index))
|
self.history.append((self.current_dir, self.selected_index))
|
||||||
self.current_dir = selected_item.path
|
self.current_dir = selected_item.path
|
||||||
logger.info(f"Entered Directory {selected_item.path}")
|
logger.info(f"Entered Directory {selected_item.path} at index {self.selected_index}")
|
||||||
|
|
||||||
self.load_current_directory(selected_item=selected_item)
|
self.load_current_directory(selected_item=selected_item)
|
||||||
|
|
||||||
@@ -1744,19 +1798,19 @@ class FileNavigator:
|
|||||||
def navigate_left(self):
|
def navigate_left(self):
|
||||||
"""Move selection left with wrap-around"""
|
"""Move selection left with wrap-around"""
|
||||||
if self.items:
|
if self.items:
|
||||||
if self.items[0].box.move is not None and not self.items[0].box.move.is_finished:
|
|
||||||
return
|
|
||||||
self.selected_index = (self.selected_index - 1) % len(self.items)
|
self.selected_index = (self.selected_index - 1) % len(self.items)
|
||||||
self.calculate_box_positions()
|
self.calculate_box_positions()
|
||||||
|
for item in self.items:
|
||||||
|
item.box.move_box(1)
|
||||||
logger.info(f"Moved Left to {self.items[self.selected_index].path}")
|
logger.info(f"Moved Left to {self.items[self.selected_index].path}")
|
||||||
|
|
||||||
def navigate_right(self):
|
def navigate_right(self):
|
||||||
"""Move selection right with wrap-around"""
|
"""Move selection right with wrap-around"""
|
||||||
if self.items:
|
if self.items:
|
||||||
if self.items[0].box.move is not None and not self.items[0].box.move.is_finished:
|
|
||||||
return
|
|
||||||
self.selected_index = (self.selected_index + 1) % len(self.items)
|
self.selected_index = (self.selected_index + 1) % len(self.items)
|
||||||
self.calculate_box_positions()
|
self.calculate_box_positions()
|
||||||
|
for item in self.items:
|
||||||
|
item.box.move_box(-1)
|
||||||
logger.info(f"Moved Right to {self.items[self.selected_index].path}")
|
logger.info(f"Moved Right to {self.items[self.selected_index].path}")
|
||||||
|
|
||||||
def skip_left(self):
|
def skip_left(self):
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import pyray as ray
|
|||||||
|
|
||||||
from libs.config import Config
|
from libs.config import Config
|
||||||
|
|
||||||
|
|
||||||
class PlayerNum(IntEnum):
|
class PlayerNum(IntEnum):
|
||||||
ALL = 0
|
ALL = 0
|
||||||
P1 = 1
|
P1 = 1
|
||||||
P2 = 2
|
P2 = 2
|
||||||
TWO_PLAYER = 3
|
TWO_PLAYER = 3
|
||||||
DAN = 4
|
DAN = 4
|
||||||
|
AI = 5
|
||||||
|
|
||||||
class ScoreMethod():
|
class ScoreMethod():
|
||||||
GEN3 = "gen3"
|
GEN3 = "gen3"
|
||||||
@@ -43,6 +45,7 @@ class Modifiers:
|
|||||||
display: bool = False
|
display: bool = False
|
||||||
inverse: bool = False
|
inverse: bool = False
|
||||||
random: int = 0
|
random: int = 0
|
||||||
|
subdiff: int = 0
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DanResultSong:
|
class DanResultSong:
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
|
||||||
from libs.global_data import PlayerNum
|
|
||||||
from libs.utils import OutlinedText, global_tex
|
|
||||||
from libs.config import get_config
|
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
|
from libs.config import get_config
|
||||||
|
from libs.global_data import PlayerNum, global_data
|
||||||
|
from libs.utils import OutlinedText, global_tex
|
||||||
|
|
||||||
|
|
||||||
class Nameplate:
|
class Nameplate:
|
||||||
@@ -57,6 +58,9 @@ class Nameplate:
|
|||||||
"""
|
"""
|
||||||
tex = global_tex
|
tex = global_tex
|
||||||
tex.draw_texture('nameplate', 'shadow', x=x, y=y, fade=min(0.5, fade))
|
tex.draw_texture('nameplate', 'shadow', x=x, y=y, fade=min(0.5, fade))
|
||||||
|
if self.player_num == PlayerNum.AI:
|
||||||
|
tex.draw_texture('nameplate', 'ai', x=x, y=y)
|
||||||
|
return
|
||||||
if self.player_num == 0:
|
if self.player_num == 0:
|
||||||
frame = 2
|
frame = 2
|
||||||
title_offset = 0
|
title_offset = 0
|
||||||
@@ -98,6 +102,7 @@ class Indicator:
|
|||||||
self.don_fade = global_tex.get_animation(6)
|
self.don_fade = global_tex.get_animation(6)
|
||||||
self.blue_arrow_move = global_tex.get_animation(7)
|
self.blue_arrow_move = global_tex.get_animation(7)
|
||||||
self.blue_arrow_fade = global_tex.get_animation(8)
|
self.blue_arrow_fade = global_tex.get_animation(8)
|
||||||
|
self.select_text = OutlinedText(global_tex.skin_config["indicator_text"].text[global_data.config["general"]["language"]], global_tex.skin_config["indicator_text"].font_size, ray.WHITE, spacing=-3)
|
||||||
|
|
||||||
def update(self, current_time_ms: float):
|
def update(self, current_time_ms: float):
|
||||||
"""Update the indicator's animations."""
|
"""Update the indicator's animations."""
|
||||||
@@ -109,7 +114,8 @@ class Indicator:
|
|||||||
"""Draw the indicator at the given position with the given fade."""
|
"""Draw the indicator at the given position with the given fade."""
|
||||||
tex = global_tex
|
tex = global_tex
|
||||||
tex.draw_texture('indicator', 'background', x=x, y=y, fade=fade)
|
tex.draw_texture('indicator', 'background', x=x, y=y, fade=fade)
|
||||||
tex.draw_texture('indicator', 'text', frame=self.state.value, x=x, y=y, fade=fade)
|
tex.draw_texture('indicator', 'text', frame=self.state.value, x=x, y=y, fade=fade, color=ray.BLACK)
|
||||||
|
self.select_text.draw(ray.BLANK, x=x+global_tex.skin_config["indicator_text"].x, y=y, fade=fade)
|
||||||
tex.draw_texture('indicator', 'drum_face', index=self.state.value, x=x, y=y, fade=fade)
|
tex.draw_texture('indicator', 'drum_face', index=self.state.value, x=x, y=y, fade=fade)
|
||||||
if self.state == Indicator.State.SELECT:
|
if self.state == Indicator.State.SELECT:
|
||||||
tex.draw_texture('indicator', 'drum_kat', fade=min(fade, self.don_fade.attribute), x=x, y=y)
|
tex.draw_texture('indicator', 'drum_kat', fade=min(fade, self.don_fade.attribute), x=x, y=y)
|
||||||
@@ -127,15 +133,14 @@ class CoinOverlay:
|
|||||||
"""Coin overlay for the game."""
|
"""Coin overlay for the game."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize the coin overlay."""
|
"""Initialize the coin overlay."""
|
||||||
pass
|
self.free_play = OutlinedText(global_tex.skin_config["free_play"].text[global_data.config["general"]["language"]], global_tex.skin_config["free_play"].font_size, ray.WHITE, spacing=5, outline_thickness=4)
|
||||||
def update(self, current_time_ms: float):
|
def update(self, current_time_ms: float):
|
||||||
"""Update the coin overlay. Unimplemented"""
|
"""Update the coin overlay. Unimplemented"""
|
||||||
pass
|
pass
|
||||||
def draw(self, x: int = 0, y: int = 0):
|
def draw(self, x: int = 0, y: int = 0):
|
||||||
"""Draw the coin overlay.
|
"""Draw the coin overlay.
|
||||||
Only draws free play for now."""
|
Only draws free play for now."""
|
||||||
tex = global_tex
|
self.free_play.draw(ray.BLACK, x=global_tex.screen_width//2 - self.free_play.texture.width//2, y=global_tex.skin_config["free_play"].y)
|
||||||
tex.draw_texture('overlay', 'free_play', x=x, y=y)
|
|
||||||
|
|
||||||
class AllNetIcon:
|
class AllNetIcon:
|
||||||
"""All.Net status icon for the game."""
|
"""All.Net status icon for the game."""
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import configparser
|
import configparser
|
||||||
import logging
|
import csv
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
import csv
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from libs.config import get_config
|
||||||
from libs.global_data import Crown
|
from libs.global_data import Crown
|
||||||
from libs.tja import NoteList, TJAParser, test_encodings
|
from libs.tja import NoteList, TJAParser, test_encodings
|
||||||
from libs.utils import global_data
|
from libs.utils import global_data
|
||||||
from libs.config import get_config
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
DB_VERSION = 1
|
DB_VERSION = 1
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import raylib as ray
|
import raylib as ray
|
||||||
from pyray import Vector2, Rectangle, Color
|
from pyray import Color, Rectangle, Vector2
|
||||||
|
|
||||||
from libs.animation import BaseAnimation, parse_animations
|
from libs.animation import BaseAnimation, parse_animations
|
||||||
|
|
||||||
from libs.config import get_config
|
from libs.config import get_config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class SkinInfo:
|
class SkinInfo:
|
||||||
def __init__(self, x: float, y: float, font_size: int, width: float, height: float):
|
def __init__(self, x: float, y: float, font_size: int, width: float, height: float, text: dict[str, str]):
|
||||||
self.x = x
|
self.x = x
|
||||||
self.y = y
|
self.y = y
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.font_size = font_size
|
self.font_size = font_size
|
||||||
|
self.text = text
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.__dict__}"
|
return f"{self.__dict__}"
|
||||||
@@ -73,24 +71,31 @@ class TextureWrapper:
|
|||||||
self.textures: dict[str, dict[str, Texture | FramedTexture]] = dict()
|
self.textures: dict[str, dict[str, Texture | FramedTexture]] = dict()
|
||||||
self.animations: dict[int, BaseAnimation] = dict()
|
self.animations: dict[int, BaseAnimation] = dict()
|
||||||
self.skin_config: dict[str, SkinInfo] = dict()
|
self.skin_config: dict[str, SkinInfo] = dict()
|
||||||
self.graphics_path = Path(get_config()['paths']['graphics_path'])
|
self.graphics_path = Path(f'Skins/{get_config()['paths']['skin']}/Graphics')
|
||||||
self.parent_graphics_path = Path(get_config()['paths']['graphics_path'])
|
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():
|
if not (self.graphics_path / "skin_config.json").exists():
|
||||||
raise Exception("skin is missing a skin_config.json")
|
raise Exception("skin is missing a skin_config.json")
|
||||||
|
|
||||||
data = json.loads((self.graphics_path / "skin_config.json").read_text())
|
data = json.loads((self.graphics_path / "skin_config.json").read_text(encoding='utf-8'))
|
||||||
self.skin_config: dict[str, SkinInfo] = {
|
self.skin_config: dict[str, SkinInfo] = {
|
||||||
k: SkinInfo(v.get('x', 0), v.get('y', 0), v.get('font_size', 0), v.get('width', 0), v.get('height', 0)) for k, v in data.items()
|
k: SkinInfo(v.get('x', 0), v.get('y', 0), v.get('font_size', 0), v.get('width', 0), v.get('height', 0), v.get('text', dict())) for k, v in data.items()
|
||||||
}
|
}
|
||||||
self.screen_width = int(self.skin_config["screen"].width)
|
self.screen_width = int(self.skin_config["screen"].width)
|
||||||
self.screen_height = int(self.skin_config["screen"].height)
|
self.screen_height = int(self.skin_config["screen"].height)
|
||||||
self.screen_scale = self.screen_width / 1280
|
self.screen_scale = self.screen_width / 1280
|
||||||
if "parent" in data["screen"]:
|
if "parent" in data["screen"]:
|
||||||
parent = data["screen"]["parent"]
|
parent = data["screen"]["parent"]
|
||||||
self.parent_graphics_path = Path("Graphics") / parent
|
self.parent_graphics_path = Path("Skins") / parent
|
||||||
parent_data = json.loads((self.parent_graphics_path / "skin_config.json").read_text())
|
parent_data = json.loads((self.parent_graphics_path / "skin_config.json").read_text(encoding='utf-8'))
|
||||||
for k, v in parent_data.items():
|
for k, v in parent_data.items():
|
||||||
self.skin_config[k] = SkinInfo(v.get('x', 0) * self.screen_scale, v.get('y', 0) * self.screen_scale, v.get('font_size', 0) * self.screen_scale, v.get('width', 0) * self.screen_scale, v.get('height', 0) * self.screen_scale)
|
self.skin_config[k] = SkinInfo(v.get('x', 0) * self.screen_scale, v.get('y', 0) * self.screen_scale, v.get('font_size', 0) * self.screen_scale, v.get('width', 0) * self.screen_scale, v.get('height', 0) * self.screen_scale, v.get('text', dict()))
|
||||||
|
|
||||||
def unload_textures(self):
|
def unload_textures(self):
|
||||||
"""Unload all textures and animations."""
|
"""Unload all textures and animations."""
|
||||||
@@ -189,7 +194,7 @@ class TextureWrapper:
|
|||||||
if screen_name in self.textures and subset in self.textures[screen_name]:
|
if screen_name in self.textures and subset in self.textures[screen_name]:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
if not os.path.isfile(folder / 'texture.json'):
|
if not (folder / 'texture.json').exists():
|
||||||
raise Exception(f"texture.json file missing from {folder}")
|
raise Exception(f"texture.json file missing from {folder}")
|
||||||
|
|
||||||
with open(folder / 'texture.json') as json_file:
|
with open(folder / 'texture.json') as json_file:
|
||||||
@@ -205,7 +210,7 @@ class TextureWrapper:
|
|||||||
if tex_dir.is_dir():
|
if tex_dir.is_dir():
|
||||||
frames = [ray.LoadTexture(str(frame).encode(encoding)) for frame in sorted(tex_dir.iterdir(),
|
frames = [ray.LoadTexture(str(frame).encode(encoding)) for frame in sorted(tex_dir.iterdir(),
|
||||||
key=lambda x: int(x.stem)) if frame.is_file()]
|
key=lambda x: int(x.stem)) if frame.is_file()]
|
||||||
self.textures[folder.stem][tex_name] = Texture(tex_name, frames, tex_mapping)
|
self.textures[folder.stem][tex_name] = FramedTexture(tex_name, frames, tex_mapping)
|
||||||
self._read_tex_obj_data(tex_mapping, self.textures[folder.stem][tex_name])
|
self._read_tex_obj_data(tex_mapping, self.textures[folder.stem][tex_name])
|
||||||
elif tex_file.is_file():
|
elif tex_file.is_file():
|
||||||
tex = ray.LoadTexture(str(tex_file).encode(encoding))
|
tex = ray.LoadTexture(str(tex_file).encode(encoding))
|
||||||
@@ -230,7 +235,7 @@ class TextureWrapper:
|
|||||||
|
|
||||||
# Load zip files from child screen path only
|
# Load zip files from child screen path only
|
||||||
for zip_file in screen_path.iterdir():
|
for zip_file in screen_path.iterdir():
|
||||||
if zip_file.is_file() and zip_file.suffix == ".zip":
|
if zip_file.is_dir():
|
||||||
self.load_zip(screen_name, zip_file.stem)
|
self.load_zip(screen_name, zip_file.stem)
|
||||||
|
|
||||||
logger.info(f"Screen textures loaded for: {screen_name}")
|
logger.info(f"Screen textures loaded for: {screen_name}")
|
||||||
@@ -292,7 +297,7 @@ class TextureWrapper:
|
|||||||
else:
|
else:
|
||||||
ray.DrawTexturePro(tex_object.texture, source_rect, dest_rect, origin, rotation, final_color)
|
ray.DrawTexturePro(tex_object.texture, source_rect, dest_rect, origin, rotation, final_color)
|
||||||
if tex_object.controllable[index] or controllable:
|
if tex_object.controllable[index] or controllable:
|
||||||
self.control(tex_object)
|
self.control(tex_object, index)
|
||||||
|
|
||||||
def draw_texture(self, subset: str, texture: str, color: Color = Color(255, 255, 255, 255), frame: int = 0, scale: float = 1.0, center: bool = False,
|
def draw_texture(self, subset: str, texture: str, color: Color = Color(255, 255, 255, 255), frame: int = 0, scale: float = 1.0, center: bool = False,
|
||||||
mirror: str = '', x: float = 0, y: float = 0, x2: float = 0, y2: float = 0,
|
mirror: str = '', x: float = 0, y: float = 0, x2: float = 0, y2: float = 0,
|
||||||
|
|||||||
396
libs/tja.py
396
libs/tja.py
@@ -1,10 +1,10 @@
|
|||||||
from enum import IntEnum
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import math
|
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import random
|
import random
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from dataclasses import dataclass, field, fields
|
from dataclasses import dataclass, field, fields
|
||||||
|
from enum import IntEnum
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -1148,7 +1148,6 @@ def modifier_inverse(notes: NoteList):
|
|||||||
def modifier_random(notes: NoteList, value: int):
|
def modifier_random(notes: NoteList, value: int):
|
||||||
"""Randomly modifies the type of the notes in the given NoteList.
|
"""Randomly modifies the type of the notes in the given NoteList.
|
||||||
value: 1 == kimagure, 2 == detarame"""
|
value: 1 == kimagure, 2 == detarame"""
|
||||||
#value: 1 == kimagure, 2 == detarame
|
|
||||||
modded_notes = notes.play_notes.copy()
|
modded_notes = notes.play_notes.copy()
|
||||||
percentage = int(len(modded_notes) / 5) * value
|
percentage = int(len(modded_notes) / 5) * value
|
||||||
selected_notes = random.sample(range(len(modded_notes)), percentage)
|
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_inverse(notes)
|
||||||
play_notes = modifier_random(notes, modifiers.random)
|
play_notes = modifier_random(notes, modifiers.random)
|
||||||
draw_notes, bars = modifier_speed(notes, modifiers.speed)
|
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
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import string
|
||||||
import ctypes
|
import ctypes
|
||||||
import hashlib
|
import hashlib
|
||||||
import sys
|
import sys
|
||||||
@@ -135,7 +136,7 @@ for file in Path('cache/image').iterdir():
|
|||||||
|
|
||||||
class OutlinedText:
|
class OutlinedText:
|
||||||
"""Create an outlined text object."""
|
"""Create an outlined text object."""
|
||||||
def __init__(self, text: str, font_size: int, color: ray.Color, outline_thickness=5.0, vertical=False):
|
def __init__(self, text: str, font_size: int, color: ray.Color, outline_thickness=5.0, vertical=False, spacing=1):
|
||||||
"""
|
"""
|
||||||
Create an outlined text object.
|
Create an outlined text object.
|
||||||
|
|
||||||
@@ -158,7 +159,7 @@ class OutlinedText:
|
|||||||
if vertical:
|
if vertical:
|
||||||
self.texture = self._create_text_vertical(text, font_size, color, ray.BLANK, self.font)
|
self.texture = self._create_text_vertical(text, font_size, color, ray.BLANK, self.font)
|
||||||
else:
|
else:
|
||||||
self.texture = self._create_text_horizontal(text, font_size, color, ray.BLANK, self.font)
|
self.texture = self._create_text_horizontal(text, font_size, color, ray.BLANK, self.font, spacing=spacing)
|
||||||
ray.gen_texture_mipmaps(self.texture)
|
ray.gen_texture_mipmaps(self.texture)
|
||||||
ray.set_texture_filter(self.texture, ray.TextureFilter.TEXTURE_FILTER_TRILINEAR)
|
ray.set_texture_filter(self.texture, ray.TextureFilter.TEXTURE_FILTER_TRILINEAR)
|
||||||
outline_size = ray.ffi.new('float*', self.outline_thickness)
|
outline_size = ray.ffi.new('float*', self.outline_thickness)
|
||||||
@@ -200,7 +201,7 @@ class OutlinedText:
|
|||||||
if reload_font:
|
if reload_font:
|
||||||
codepoint_count = ray.ffi.new('int *', 0)
|
codepoint_count = ray.ffi.new('int *', 0)
|
||||||
codepoints = ray.load_codepoints(''.join(global_data.font_codepoints), codepoint_count)
|
codepoints = ray.load_codepoints(''.join(global_data.font_codepoints), codepoint_count)
|
||||||
global_data.font = ray.load_font_ex(str(Path('Graphics/Modified-DFPKanteiryu-XB.ttf')), 40, codepoints, len(global_data.font_codepoints))
|
global_data.font = ray.load_font_ex(str(Path(f'Skins/{global_data.config["paths"]["skin"]}/Graphics/Modified-DFPKanteiryu-XB.ttf')), 40, codepoints, len(global_data.font_codepoints))
|
||||||
logger.info(f"Reloaded font with {len(global_data.font_codepoints)} codepoints")
|
logger.info(f"Reloaded font with {len(global_data.font_codepoints)} codepoints")
|
||||||
return global_data.font
|
return global_data.font
|
||||||
|
|
||||||
@@ -358,19 +359,25 @@ class OutlinedText:
|
|||||||
ray.unload_image(image)
|
ray.unload_image(image)
|
||||||
return texture
|
return texture
|
||||||
|
|
||||||
def _create_text_horizontal(self, text: str, font_size: int, color: ray.Color, bg_color: ray.Color, font: Optional[ray.Font]=None, padding: int=10):
|
def _create_text_horizontal(self, text: str, font_size: int, color: ray.Color, bg_color: ray.Color, font: Optional[ray.Font]=None, padding: int=10, spacing: int=1):
|
||||||
if font:
|
if font:
|
||||||
text_size = ray.measure_text_ex(font, text, font_size, 0)
|
text_size = ray.measure_text_ex(font, text, font_size, spacing)
|
||||||
|
for char in text:
|
||||||
|
if char in string.whitespace:
|
||||||
|
text_size.x += 2
|
||||||
total_width = text_size.x + (padding * 2)
|
total_width = text_size.x + (padding * 2)
|
||||||
total_height = text_size.y + (padding * 2)
|
total_height = text_size.y + (padding * 2)
|
||||||
else:
|
else:
|
||||||
total_width = ray.measure_text(text, font_size) + (padding * 2)
|
total_width = ray.measure_text(text, font_size) + (padding * 2)
|
||||||
total_height = font_size + (padding * 2)
|
total_height = font_size + (padding * 2)
|
||||||
|
|
||||||
image = ray.gen_image_color(int(total_width), int(total_height), bg_color)
|
image = ray.gen_image_color(int(total_width), int(total_height), bg_color)
|
||||||
|
|
||||||
if font:
|
if font:
|
||||||
text_image = ray.image_text_ex(font, text, font_size, 0, color)
|
text_image = ray.image_text_ex(font, text, font_size, spacing, color)
|
||||||
else:
|
else:
|
||||||
text_image = ray.image_text(text, font_size, color)
|
text_image = ray.image_text(text, font_size, color)
|
||||||
|
|
||||||
text_x = padding
|
text_x = padding
|
||||||
text_y = padding
|
text_y = padding
|
||||||
ray.image_draw(image, text_image,
|
ray.image_draw(image, text_image,
|
||||||
@@ -378,7 +385,6 @@ class OutlinedText:
|
|||||||
ray.Rectangle(text_x, text_y, text_image.width, text_image.height),
|
ray.Rectangle(text_x, text_y, text_image.width, text_image.height),
|
||||||
ray.WHITE)
|
ray.WHITE)
|
||||||
ray.unload_image(text_image)
|
ray.unload_image(text_image)
|
||||||
|
|
||||||
ray.export_image(image, f'cache/image/{self.hash}.png')
|
ray.export_image(image, f'cache/image/{self.hash}.png')
|
||||||
texture = ray.load_texture_from_image(image)
|
texture = ray.load_texture_from_image(image)
|
||||||
ray.unload_image(image)
|
ray.unload_image(image)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from pathlib import Path
|
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import raylib as ray
|
|
||||||
import av
|
import av
|
||||||
|
import raylib as ray
|
||||||
|
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
from libs.utils import get_current_ms
|
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
|
from libs.utils import get_current_ms
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,35 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "pytaiko"
|
name = "pytaiko"
|
||||||
version = "1.0"
|
version = "1.1"
|
||||||
description = "Taiko no Tatsujin simulator written in python and raylib"
|
description = "Taiko no Tatsujin simulator written in python and raylib"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"av>=16.0.1",
|
"av>=16.0.1",
|
||||||
|
"pyinstrument>=5.1.1",
|
||||||
"pypresence>=4.6.1",
|
"pypresence>=4.6.1",
|
||||||
|
"pytest>=9.0.2",
|
||||||
"raylib-sdl>=5.5.0.2",
|
"raylib-sdl>=5.5.0.2",
|
||||||
"tomlkit>=0.13.3",
|
"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]
|
[tool.vulture]
|
||||||
exclude = ["*.git", ".github/", ".venv/", "cache/"]
|
exclude = ["*.git", ".github/", ".venv/", "cache/"]
|
||||||
paths = ["."]
|
paths = ["."]
|
||||||
@@ -18,4 +37,5 @@ paths = ["."]
|
|||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"nuitka>=2.8.4",
|
"nuitka>=2.8.4",
|
||||||
|
"pytest-cov>=6.0.0",
|
||||||
]
|
]
|
||||||
|
|||||||
541
scenes/ai_battle/game.py
Normal file
541
scenes/ai_battle/game.py
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from enum import Enum
|
||||||
|
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 AIDifficulty(Enum):
|
||||||
|
LVL_1 = (0.90, 0.10)
|
||||||
|
LVL_2 = (0.92, 0.08)
|
||||||
|
LVL_3 = (0.94, 0.06)
|
||||||
|
LVL_4 = (0.96, 0.04)
|
||||||
|
LVL_5 = (0.98, 0.02)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.value)
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.section_board = SectionBoard()
|
||||||
|
|
||||||
|
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
|
||||||
|
session_data = global_data.session_data[global_data.player_num]
|
||||||
|
session_data.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, session_data.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, session_data.selected_difficulty, True, ai_modifiers, AIDifficulty.LVL_2)
|
||||||
|
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)
|
||||||
|
self.precise_start = time.time() - self.tja.metadata.offset
|
||||||
|
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.section_board.num < 3 else (self.total_notes // 5) + (self.total_notes % 5) - 1
|
||||||
|
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
|
||||||
|
if self.background.contest_point >= 10:
|
||||||
|
self.section_board.wins[self.section_board.num] = True
|
||||||
|
else:
|
||||||
|
self.section_board.wins[self.section_board.num] = False
|
||||||
|
self.section_board.num += 1
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.update_audio(self.current_ms)
|
||||||
|
|
||||||
|
self.player_1.update(self.current_ms, current_time, None)
|
||||||
|
self.player_2.update(self.current_ms, current_time, None)
|
||||||
|
self.update_scoreboards()
|
||||||
|
self.section_board.update(current_time, self.player_1.good_count + self.player_1.ok_count + self.player_1.bad_count, self.total_notes)
|
||||||
|
|
||||||
|
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 self.player_1.modifiers.subdiff in [0, 1, 2, 3, 4, 5, 9, 13]:
|
||||||
|
self.write_score()
|
||||||
|
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.section_board.draw()
|
||||||
|
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 SectionBoard:
|
||||||
|
def __init__(self):
|
||||||
|
self.num = 0
|
||||||
|
self.wins: list[Optional[bool]] = [None] * 5
|
||||||
|
self.current_progress = 0
|
||||||
|
self.progress_bar_flash = Animation.create_fade(133, loop=True, reverse_delay=0)
|
||||||
|
self.section_highlight_flash = Animation.create_fade(350, loop=True, reverse_delay=0)
|
||||||
|
self.section_highlight_flash.start()
|
||||||
|
self.progress_bar_flash.start()
|
||||||
|
|
||||||
|
def update(self, current_time, player_notes, total_notes):
|
||||||
|
self.current_progress = player_notes / (total_notes // 5) if self.num < 3 else player_notes / ((total_notes // 5) + (total_notes % 5))
|
||||||
|
self.progress_bar_flash.update(current_time)
|
||||||
|
self.section_highlight_flash.update(current_time)
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
if self.current_progress < 0.75:
|
||||||
|
color = ray.GREEN
|
||||||
|
fade = 1.0
|
||||||
|
else:
|
||||||
|
color = ray.YELLOW
|
||||||
|
fade = self.progress_bar_flash.attribute
|
||||||
|
ray.draw_rectangle(int(177 * tex.screen_scale), int(160 * tex.screen_scale), int(148 * tex.screen_scale), int(20 * tex.screen_scale), ray.GRAY)
|
||||||
|
ray.draw_rectangle(int(177 * tex.screen_scale), int(160 * tex.screen_scale), int(self.current_progress * (148 * tex.screen_scale)), int(20 * tex.screen_scale), ray.fade(color, fade))
|
||||||
|
tex.draw_texture('ai_battle', 'progress_bar')
|
||||||
|
if self.num < len(self.wins):
|
||||||
|
tex.draw_texture('ai_battle', 'section_text', index=0, frame=self.num)
|
||||||
|
if self.num < len(self.wins) - 1:
|
||||||
|
tex.draw_texture('ai_battle', 'section_text', index=1, frame=self.num+1)
|
||||||
|
|
||||||
|
tex.draw_texture('ai_battle', 'sections')
|
||||||
|
if self.num < len(self.wins):
|
||||||
|
tex.draw_texture('ai_battle', 'section_highlight_green', index=self.num)
|
||||||
|
tex.draw_texture('ai_battle', 'section_highlight_white', index=self.num, fade=self.section_highlight_flash.attribute)
|
||||||
|
|
||||||
|
for i in range(len(self.wins)):
|
||||||
|
if self.wins[i] is not None:
|
||||||
|
if self.wins[i]:
|
||||||
|
tex.draw_texture('ai_battle', 'section_win', index=i)
|
||||||
|
else:
|
||||||
|
tex.draw_texture('ai_battle', 'section_lose', index=i)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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, ai_difficulty: AIDifficulty):
|
||||||
|
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, self.ok_percentage = ai_difficulty
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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.triangles_down = Animation.create_move(8500, total_distance=1152, loop=True)
|
||||||
|
self.contest_point_fade.start()
|
||||||
|
self.triangles_down.start()
|
||||||
|
|
||||||
|
def update(self, current_ms: float):
|
||||||
|
self.contest_point_fade.update(current_ms)
|
||||||
|
self.triangles_down.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', 'lower_triangles_1', y=self.triangles_down.attribute, fade=0.5)
|
||||||
|
tex.draw_texture('ai_battle', 'lower_triangles_2', y=self.triangles_down.attribute, fade=0.5)
|
||||||
|
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)
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
|
||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
|
from libs.audio import audio
|
||||||
from libs.chara_2d import Chara2D
|
from libs.chara_2d import Chara2D
|
||||||
from libs.global_data import PlayerNum, reset_session
|
from libs.global_data import PlayerNum, reset_session
|
||||||
from libs.audio import audio
|
|
||||||
from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate
|
from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate
|
||||||
from libs.screen import Screen
|
from libs.screen import Screen
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
@@ -13,7 +14,7 @@ from libs.utils import (
|
|||||||
get_current_ms,
|
get_current_ms,
|
||||||
global_data,
|
global_data,
|
||||||
is_l_don_pressed,
|
is_l_don_pressed,
|
||||||
is_r_don_pressed
|
is_r_don_pressed,
|
||||||
)
|
)
|
||||||
from scenes.game import Gauge
|
from scenes.game import Gauge
|
||||||
from scenes.result import Background
|
from scenes.result import Background
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
from libs.global_data import PlayerNum, global_data
|
|
||||||
from libs.texture import tex
|
|
||||||
from libs.chara_2d import Chara2D
|
from libs.chara_2d import Chara2D
|
||||||
from libs.global_objects import AllNetIcon, CoinOverlay, Indicator, Nameplate, Timer
|
|
||||||
from libs.screen import Screen
|
|
||||||
from libs.file_navigator import BackBox, DanCourse, navigator
|
from libs.file_navigator import BackBox, DanCourse, navigator
|
||||||
|
from libs.global_data import PlayerNum, global_data
|
||||||
|
from libs.global_objects import (
|
||||||
|
AllNetIcon,
|
||||||
|
CoinOverlay,
|
||||||
|
Indicator,
|
||||||
|
Nameplate,
|
||||||
|
Timer,
|
||||||
|
)
|
||||||
|
from libs.screen import Screen
|
||||||
|
from libs.texture import tex
|
||||||
from libs.transition import Transition
|
from libs.transition import Transition
|
||||||
from libs.utils import get_current_ms, is_l_don_pressed, is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed
|
from libs.utils import (
|
||||||
|
get_current_ms,
|
||||||
|
is_l_don_pressed,
|
||||||
|
is_l_kat_pressed,
|
||||||
|
is_r_don_pressed,
|
||||||
|
is_r_kat_pressed,
|
||||||
|
)
|
||||||
from scenes.song_select import State
|
from scenes.song_select import State
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
import copy
|
import copy
|
||||||
from typing import Optional, override
|
|
||||||
import pyray as ray
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional, override
|
||||||
|
|
||||||
|
import pyray as ray
|
||||||
|
|
||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
from libs.background import Background
|
from libs.background import Background
|
||||||
from libs.file_navigator import Exam
|
from libs.file_navigator import Exam
|
||||||
from libs.global_data import DanResultExam, DanResultSong, PlayerNum, global_data
|
from libs.global_data import (
|
||||||
|
DanResultExam,
|
||||||
|
DanResultSong,
|
||||||
|
PlayerNum,
|
||||||
|
global_data,
|
||||||
|
)
|
||||||
from libs.global_objects import AllNetIcon
|
from libs.global_objects import AllNetIcon
|
||||||
|
from libs.texture import tex
|
||||||
from libs.tja import TJAParser
|
from libs.tja import TJAParser
|
||||||
from libs.transition import Transition
|
from libs.transition import Transition
|
||||||
from libs.utils import OutlinedText, get_current_ms
|
from libs.utils import OutlinedText, get_current_ms
|
||||||
from libs.texture import tex
|
from scenes.game import (
|
||||||
from scenes.game import ClearAnimation, FCAnimation, FailAnimation, GameScreen, Gauge, ResultTransition, SongInfo
|
ClearAnimation,
|
||||||
|
FailAnimation,
|
||||||
|
FCAnimation,
|
||||||
|
GameScreen,
|
||||||
|
Gauge,
|
||||||
|
ResultTransition,
|
||||||
|
SongInfo,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
from libs.chara_2d import Chara2D
|
from libs.chara_2d import Chara2D
|
||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate, Indicator, EntryOverlay, Timer
|
from libs.global_objects import (
|
||||||
from libs.texture import tex
|
AllNetIcon,
|
||||||
|
CoinOverlay,
|
||||||
|
EntryOverlay,
|
||||||
|
Indicator,
|
||||||
|
Nameplate,
|
||||||
|
Timer,
|
||||||
|
)
|
||||||
from libs.screen import Screen
|
from libs.screen import Screen
|
||||||
|
from libs.texture import tex
|
||||||
from libs.utils import (
|
from libs.utils import (
|
||||||
OutlinedText,
|
OutlinedText,
|
||||||
get_current_ms,
|
get_current_ms,
|
||||||
@@ -48,6 +56,9 @@ class EntryScreen(Screen):
|
|||||||
self.chara = Chara2D(0)
|
self.chara = Chara2D(0)
|
||||||
self.announce_played = False
|
self.announce_played = False
|
||||||
self.players: list[Optional[EntryPlayer]] = [None, None]
|
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')
|
audio.play_sound('bgm', 'music')
|
||||||
|
|
||||||
def on_screen_end(self, next_screen: str):
|
def on_screen_end(self, next_screen: str):
|
||||||
@@ -156,7 +167,7 @@ class EntryScreen(Screen):
|
|||||||
tex.draw_texture('side_select', 'box_right', fade=fade)
|
tex.draw_texture('side_select', 'box_right', fade=fade)
|
||||||
tex.draw_texture('side_select', 'box_center', fade=fade)
|
tex.draw_texture('side_select', '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)
|
self.chara.draw(tex.skin_config["chara_entry"].x, tex.skin_config["chara_entry"].y)
|
||||||
|
|
||||||
@@ -172,7 +183,7 @@ class EntryScreen(Screen):
|
|||||||
else:
|
else:
|
||||||
tex.draw_texture('side_select', '2P_highlight', fade=fade)
|
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', '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)
|
self.nameplate.draw(tex.skin_config["nameplate_entry"].x, tex.skin_config["nameplate_entry"].y)
|
||||||
|
|
||||||
def draw_player_drum(self):
|
def draw_player_drum(self):
|
||||||
@@ -439,10 +450,11 @@ class BoxManager:
|
|||||||
"""BoxManager class for the entry screen"""
|
"""BoxManager class for the entry screen"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.box_titles: list[OutlinedText] = [
|
self.box_titles: list[OutlinedText] = [
|
||||||
OutlinedText('演奏ゲーム', tex.skin_config["entry_box_text"].font_size, ray.WHITE, outline_thickness=5, vertical=True),
|
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_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_box_text"].font_size, ray.WHITE, outline_thickness=5, vertical=True)]
|
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),
|
||||||
self.box_locations = ["SONG_SELECT", "PRACTICE_SELECT", "SETTINGS"]
|
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.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.boxes = [Box(self.box_titles[i], self.box_locations[i]) for i in range(len(self.box_titles))]
|
||||||
self.selected_box_index = 0
|
self.selected_box_index = 0
|
||||||
@@ -501,7 +513,7 @@ class BoxManager:
|
|||||||
def update(self, current_time_ms: float, is_2p: bool):
|
def update(self, current_time_ms: float, is_2p: bool):
|
||||||
self.is_2p = is_2p
|
self.is_2p = is_2p
|
||||||
if self.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):
|
for i, box in enumerate(self.boxes):
|
||||||
box.location = self.box_locations[i]
|
box.location = self.box_locations[i]
|
||||||
self.fade_out.update(current_time_ms)
|
self.fade_out.update(current_time_ms)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import bisect
|
import bisect
|
||||||
from enum import IntEnum
|
|
||||||
import math
|
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from enum import IntEnum
|
||||||
|
from itertools import chain
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
|
||||||
@@ -14,7 +15,13 @@ from libs.animation import Animation
|
|||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
from libs.background import Background
|
from libs.background import Background
|
||||||
from libs.chara_2d import Chara2D
|
from libs.chara_2d import Chara2D
|
||||||
from libs.global_data import Crown, Difficulty, Modifiers, PlayerNum, ScoreMethod
|
from libs.global_data import (
|
||||||
|
Crown,
|
||||||
|
Difficulty,
|
||||||
|
Modifiers,
|
||||||
|
PlayerNum,
|
||||||
|
ScoreMethod,
|
||||||
|
)
|
||||||
from libs.global_objects import AllNetIcon, Nameplate
|
from libs.global_objects import AllNetIcon, Nameplate
|
||||||
from libs.screen import Screen
|
from libs.screen import Screen
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
@@ -24,8 +31,8 @@ from libs.tja import (
|
|||||||
Note,
|
Note,
|
||||||
NoteList,
|
NoteList,
|
||||||
NoteType,
|
NoteType,
|
||||||
TJAParser,
|
|
||||||
TimelineObject,
|
TimelineObject,
|
||||||
|
TJAParser,
|
||||||
apply_modifiers,
|
apply_modifiers,
|
||||||
calculate_base_score,
|
calculate_base_score,
|
||||||
)
|
)
|
||||||
@@ -71,6 +78,7 @@ class GameScreen(Screen):
|
|||||||
self.paused = False
|
self.paused = False
|
||||||
self.pause_time = 0
|
self.pause_time = 0
|
||||||
self.audio_time = 0
|
self.audio_time = 0
|
||||||
|
self.last_resync = 0
|
||||||
self.movie = None
|
self.movie = None
|
||||||
self.song_music = None
|
self.song_music = None
|
||||||
if global_data.config["general"]["nijiiro_notes"]:
|
if global_data.config["general"]["nijiiro_notes"]:
|
||||||
@@ -116,7 +124,7 @@ class GameScreen(Screen):
|
|||||||
|
|
||||||
def load_hitsounds(self):
|
def load_hitsounds(self):
|
||||||
"""Load the hit sounds"""
|
"""Load the hit sounds"""
|
||||||
sounds_dir = Path("Sounds")
|
sounds_dir = Path(f"Skins/{global_data.config["paths"]["skin"]}/Sounds")
|
||||||
if global_data.hit_sound == -1:
|
if global_data.hit_sound == -1:
|
||||||
audio.load_sound(Path('none.wav'), 'hitsound_don_1p')
|
audio.load_sound(Path('none.wav'), 'hitsound_don_1p')
|
||||||
audio.load_sound(Path('none.wav'), 'hitsound_kat_1p')
|
audio.load_sound(Path('none.wav'), 'hitsound_kat_1p')
|
||||||
@@ -147,6 +155,7 @@ class GameScreen(Screen):
|
|||||||
|
|
||||||
self.player_1 = Player(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 = Player(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.start_ms = get_current_ms() - self.tja.metadata.offset*1000
|
self.start_ms = get_current_ms() - self.tja.metadata.offset*1000
|
||||||
|
self.precise_start = time.time() - self.tja.metadata.offset
|
||||||
|
|
||||||
def write_score(self):
|
def write_score(self):
|
||||||
"""Write the score to the database"""
|
"""Write the score to the database"""
|
||||||
@@ -243,6 +252,20 @@ class GameScreen(Screen):
|
|||||||
if self.background is not None:
|
if self.background is not None:
|
||||||
self.background.update(current_time, self.bpm, self.player_1.gauge, None)
|
self.background.update(current_time, self.bpm, self.player_1.gauge, None)
|
||||||
|
|
||||||
|
def update_audio(self, ms_from_start: float):
|
||||||
|
if not self.song_started:
|
||||||
|
return
|
||||||
|
if self.song_music is not None:
|
||||||
|
audio.update_music_stream(self.song_music)
|
||||||
|
'''
|
||||||
|
raw_audio_time = audio.get_music_time_played(self.song_music)
|
||||||
|
current_time = time.time() - self.precise_start
|
||||||
|
if abs(raw_audio_time - current_time) >= 0.006 and current_time > self.last_resync + 1000:
|
||||||
|
audio.seek_music_stream(self.song_music, current_time)
|
||||||
|
logger.info(f"Resyncing due to difference: {raw_audio_time} - {current_time} = {raw_audio_time - current_time}")
|
||||||
|
self.last_resync = current_time
|
||||||
|
'''
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
super().update()
|
super().update()
|
||||||
current_time = get_current_ms()
|
current_time = get_current_ms()
|
||||||
@@ -255,8 +278,7 @@ class GameScreen(Screen):
|
|||||||
self.start_ms = current_time - self.tja.metadata.offset*1000
|
self.start_ms = current_time - self.tja.metadata.offset*1000
|
||||||
self.update_background(current_time)
|
self.update_background(current_time)
|
||||||
|
|
||||||
if self.song_music is not None:
|
self.update_audio(self.current_ms)
|
||||||
audio.update_music_stream(self.song_music)
|
|
||||||
|
|
||||||
self.player_1.update(self.current_ms, current_time, self.background)
|
self.player_1.update(self.current_ms, current_time, self.background)
|
||||||
self.song_info.update(current_time)
|
self.song_info.update(current_time)
|
||||||
@@ -395,7 +417,7 @@ class Player:
|
|||||||
|
|
||||||
def reset_chart(self):
|
def reset_chart(self):
|
||||||
notes, self.branch_m, self.branch_e, self.branch_n = self.tja.notes_to_position(self.difficulty)
|
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.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}])
|
self.kat_notes = deque([note for note in self.play_notes if note.type in {NoteType.KAT, NoteType.KAT_L}])
|
||||||
@@ -443,12 +465,12 @@ class Player:
|
|||||||
self.bpm = 120
|
self.bpm = 120
|
||||||
if self.timeline and hasattr(self.timeline[self.timeline_index], 'bpm'):
|
if self.timeline and hasattr(self.timeline[self.timeline_index], 'bpm'):
|
||||||
self.bpm = 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):
|
for note in chain(self.draw_note_list, self.draw_bar_list):
|
||||||
self.get_load_time(note)
|
self.get_load_time(note)
|
||||||
if note.type == NoteType.TAIL:
|
if note.type == NoteType.TAIL:
|
||||||
note.load_ms = last_note.load_ms
|
note.load_ms = last_note.load_ms
|
||||||
note.unload_ms = last_note.unload_ms
|
last_note.unload_ms = note.unload_ms
|
||||||
last_note = note
|
last_note = note
|
||||||
|
|
||||||
self.draw_note_list = deque(sorted(self.draw_note_list, key=lambda n: n.load_ms))
|
self.draw_note_list = deque(sorted(self.draw_note_list, key=lambda n: n.load_ms))
|
||||||
@@ -484,6 +506,7 @@ class Player:
|
|||||||
for branch in (self.branch_m, self.branch_e, self.branch_n):
|
for branch in (self.branch_m, self.branch_e, self.branch_n):
|
||||||
if branch:
|
if branch:
|
||||||
for section in branch:
|
for section in branch:
|
||||||
|
section.play_notes, section.draw_notes, section.bars = apply_modifiers(section, self.modifiers)
|
||||||
if section.draw_notes:
|
if section.draw_notes:
|
||||||
for note in section.draw_notes:
|
for note in section.draw_notes:
|
||||||
self.get_load_time(note)
|
self.get_load_time(note)
|
||||||
@@ -1275,21 +1298,21 @@ class Player:
|
|||||||
modifiers_to_draw.append('mod_shinuchi')
|
modifiers_to_draw.append('mod_shinuchi')
|
||||||
|
|
||||||
# Speed modifiers
|
# Speed modifiers
|
||||||
if global_data.modifiers[self.player_num].speed >= 4:
|
if self.modifiers.speed >= 4:
|
||||||
modifiers_to_draw.append('mod_yonbai')
|
modifiers_to_draw.append('mod_yonbai')
|
||||||
elif global_data.modifiers[self.player_num].speed >= 3:
|
elif self.modifiers.speed >= 3:
|
||||||
modifiers_to_draw.append('mod_sanbai')
|
modifiers_to_draw.append('mod_sanbai')
|
||||||
elif global_data.modifiers[self.player_num].speed > 1:
|
elif self.modifiers.speed > 1:
|
||||||
modifiers_to_draw.append('mod_baisaku')
|
modifiers_to_draw.append('mod_baisaku')
|
||||||
|
|
||||||
# Other modifiers
|
# Other modifiers
|
||||||
if global_data.modifiers[self.player_num].display:
|
if self.modifiers.display:
|
||||||
modifiers_to_draw.append('mod_doron')
|
modifiers_to_draw.append('mod_doron')
|
||||||
if global_data.modifiers[self.player_num].inverse:
|
if self.modifiers.inverse:
|
||||||
modifiers_to_draw.append('mod_abekobe')
|
modifiers_to_draw.append('mod_abekobe')
|
||||||
if global_data.modifiers[self.player_num].random == 2:
|
if self.modifiers.random == 2:
|
||||||
modifiers_to_draw.append('mod_detarame')
|
modifiers_to_draw.append('mod_detarame')
|
||||||
elif global_data.modifiers[self.player_num].random == 1:
|
elif self.modifiers.random == 1:
|
||||||
modifiers_to_draw.append('mod_kimagure')
|
modifiers_to_draw.append('mod_kimagure')
|
||||||
|
|
||||||
# Draw all modifiers in one batch
|
# Draw all modifiers in one batch
|
||||||
@@ -1329,13 +1352,13 @@ class Player:
|
|||||||
self.judge_counter.draw()
|
self.judge_counter.draw()
|
||||||
|
|
||||||
# Group 7: Player-specific elements
|
# Group 7: Player-specific elements
|
||||||
if not self.modifiers.auto:
|
if self.modifiers.auto:
|
||||||
|
tex.draw_texture('lane', 'auto_icon', index=self.is_2p)
|
||||||
|
else:
|
||||||
if self.is_2p:
|
if self.is_2p:
|
||||||
self.nameplate.draw(tex.skin_config["game_nameplate_1p"].x, tex.skin_config["game_nameplate_1p"].y)
|
self.nameplate.draw(tex.skin_config["game_nameplate_1p"].x, tex.skin_config["game_nameplate_1p"].y)
|
||||||
else:
|
else:
|
||||||
self.nameplate.draw(tex.skin_config["game_nameplate_2p"].x, tex.skin_config["game_nameplate_2p"].y)
|
self.nameplate.draw(tex.skin_config["game_nameplate_2p"].x, tex.skin_config["game_nameplate_2p"].y)
|
||||||
else:
|
|
||||||
tex.draw_texture('lane', 'auto_icon', index=self.is_2p)
|
|
||||||
self.draw_modifiers()
|
self.draw_modifiers()
|
||||||
self.chara.draw(y=(self.is_2p*tex.skin_config["game_2p_offset"].y))
|
self.chara.draw(y=(self.is_2p*tex.skin_config["game_2p_offset"].y))
|
||||||
|
|
||||||
@@ -1353,6 +1376,8 @@ class Player:
|
|||||||
def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader, dan_transition = None):
|
def draw(self, ms_from_start: float, start_ms: float, mask_shader: ray.Shader, dan_transition = None):
|
||||||
# Group 1: Background and lane elements
|
# Group 1: Background and lane elements
|
||||||
tex.draw_texture('lane', 'lane_background', index=self.is_2p)
|
tex.draw_texture('lane', 'lane_background', index=self.is_2p)
|
||||||
|
if self.player_num == PlayerNum.AI:
|
||||||
|
tex.draw_texture('lane', 'ai_lane_background')
|
||||||
if self.branch_indicator is not None:
|
if self.branch_indicator is not None:
|
||||||
self.branch_indicator.draw()
|
self.branch_indicator.draw()
|
||||||
if self.gauge is not None:
|
if self.gauge is not None:
|
||||||
@@ -1760,7 +1785,7 @@ class BalloonAnimation:
|
|||||||
tex.draw_texture('balloon', 'pop', frame=7, color=self.color, y=self.is_2p*tex.skin_config["2p_offset"].y)
|
tex.draw_texture('balloon', 'pop', frame=7, color=self.color, y=self.is_2p*tex.skin_config["2p_offset"].y)
|
||||||
elif self.balloon_count >= 1:
|
elif self.balloon_count >= 1:
|
||||||
balloon_index = min(6, (self.balloon_count - 1) * 6 // self.balloon_total)
|
balloon_index = min(6, (self.balloon_count - 1) * 6 // self.balloon_total)
|
||||||
tex.draw_texture('balloon', 'pop', frame=balloon_index, color=self.color, index=self.player_num-1, y=self.is_2p*tex.skin_config["2p_offset"].y)
|
tex.draw_texture('balloon', 'pop', frame=balloon_index, color=self.color, index=self.is_2p, y=self.is_2p*tex.skin_config["2p_offset"].y)
|
||||||
if self.balloon_count > 0:
|
if self.balloon_count > 0:
|
||||||
tex.draw_texture('balloon', 'bubble', y=self.is_2p*(410 * tex.screen_scale), mirror='vertical' if self.is_2p else '')
|
tex.draw_texture('balloon', 'bubble', y=self.is_2p*(410 * tex.screen_scale), mirror='vertical' if self.is_2p else '')
|
||||||
counter = str(max(0, self.balloon_total - self.balloon_count + 1))
|
counter = str(max(0, self.balloon_total - self.balloon_count + 1))
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
|
||||||
import threading
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
|
||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
|
from libs.file_navigator import navigator
|
||||||
from libs.global_objects import AllNetIcon
|
from libs.global_objects import AllNetIcon
|
||||||
from libs.screen import Screen
|
from libs.screen import Screen
|
||||||
from libs.song_hash import build_song_hashes
|
from libs.song_hash import build_song_hashes
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
from libs.utils import get_current_ms, global_data
|
from libs.utils import get_current_ms, global_data
|
||||||
from libs.file_navigator import navigator
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ class LoadScreen(Screen):
|
|||||||
global_data.font_codepoints.add(character)
|
global_data.font_codepoints.add(character)
|
||||||
codepoint_count = ray.ffi.new('int *', 0)
|
codepoint_count = ray.ffi.new('int *', 0)
|
||||||
codepoints = ray.load_codepoints(''.join(global_data.font_codepoints), codepoint_count)
|
codepoints = ray.load_codepoints(''.join(global_data.font_codepoints), codepoint_count)
|
||||||
global_data.font = ray.load_font_ex(str(Path('Graphics/Modified-DFPKanteiryu-XB.ttf')), 40, codepoints, len(global_data.font_codepoints))
|
global_data.font = ray.load_font_ex(str(Path(f'Skins/{global_data.config["paths"]["skin"]}/Graphics/Modified-DFPKanteiryu-XB.ttf')), 40, codepoints, len(global_data.font_codepoints))
|
||||||
|
|
||||||
def _load_navigator(self):
|
def _load_navigator(self):
|
||||||
"""Background thread function to load navigator"""
|
"""Background thread function to load navigator"""
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
|
import copy
|
||||||
|
import logging
|
||||||
import math
|
import math
|
||||||
from collections import deque
|
from collections import deque
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
import copy
|
|
||||||
|
|
||||||
from libs.animation import Animation
|
from libs.animation import Animation
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
from libs.background import Background
|
from libs.background import Background
|
||||||
from libs.global_data import Modifiers, PlayerNum, global_data
|
from libs.global_data import Modifiers, PlayerNum, global_data
|
||||||
from libs.tja import Balloon, Drumroll, NoteType, TJAParser, TimelineObject, apply_modifiers
|
|
||||||
from libs.utils import get_current_ms, is_l_don_pressed, is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed
|
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
from scenes.game import DrumHitEffect, DrumType, GameScreen, JudgeCounter, LaneHitEffect, Player, Side
|
from libs.tja import (
|
||||||
|
Balloon,
|
||||||
|
Drumroll,
|
||||||
|
NoteType,
|
||||||
|
TimelineObject,
|
||||||
|
TJAParser,
|
||||||
|
apply_modifiers,
|
||||||
|
)
|
||||||
|
from libs.utils import (
|
||||||
|
get_current_ms,
|
||||||
|
is_l_don_pressed,
|
||||||
|
is_l_kat_pressed,
|
||||||
|
is_r_don_pressed,
|
||||||
|
is_r_kat_pressed,
|
||||||
|
)
|
||||||
|
from scenes.game import (
|
||||||
|
DrumHitEffect,
|
||||||
|
DrumType,
|
||||||
|
GameScreen,
|
||||||
|
JudgeCounter,
|
||||||
|
LaneHitEffect,
|
||||||
|
Player,
|
||||||
|
Side,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
|
||||||
from libs.global_data import Difficulty, PlayerNum, reset_session
|
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
from libs.chara_2d import Chara2D
|
from libs.chara_2d import Chara2D
|
||||||
|
from libs.global_data import Difficulty, PlayerNum, reset_session
|
||||||
from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate
|
from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate
|
||||||
from libs.screen import Screen
|
from libs.screen import Screen
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
@@ -12,7 +13,7 @@ from libs.utils import (
|
|||||||
get_current_ms,
|
get_current_ms,
|
||||||
global_data,
|
global_data,
|
||||||
is_l_don_pressed,
|
is_l_don_pressed,
|
||||||
is_r_don_pressed
|
is_r_don_pressed,
|
||||||
)
|
)
|
||||||
from scenes.game import ScoreMethod
|
from scenes.game import ScoreMethod
|
||||||
|
|
||||||
|
|||||||
@@ -1,276 +1,188 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
|
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
|
from libs.config import save_config
|
||||||
|
from libs.global_objects import Indicator
|
||||||
from libs.screen import Screen
|
from libs.screen import Screen
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
from libs.utils import (
|
from libs.utils import (
|
||||||
|
OutlinedText,
|
||||||
|
get_current_ms,
|
||||||
global_data,
|
global_data,
|
||||||
is_l_don_pressed,
|
is_l_don_pressed,
|
||||||
is_l_kat_pressed,
|
is_l_kat_pressed,
|
||||||
is_r_don_pressed,
|
is_r_don_pressed,
|
||||||
is_r_kat_pressed,
|
is_r_kat_pressed,
|
||||||
)
|
)
|
||||||
from libs.config import save_config
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class BaseOptionBox:
|
||||||
|
def __init__(self, name: str, description: str):
|
||||||
|
self.name = OutlinedText(name, 30, ray.WHITE)
|
||||||
|
self.description = description
|
||||||
|
self.is_highlighted = False
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
if self.is_highlighted:
|
||||||
|
tex.draw_texture('background', 'title_highlight')
|
||||||
|
else:
|
||||||
|
tex.draw_texture('background', 'title')
|
||||||
|
text_x = tex.textures['background']['title'].x[0] + (tex.textures['background']['title'].width//2) - (self.name.texture.width//2)
|
||||||
|
text_y = tex.textures['background']['title'].y[0]
|
||||||
|
self.name.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
|
||||||
|
|
||||||
|
class Box:
|
||||||
|
"""Box class for the entry screen"""
|
||||||
|
def __init__(self, text: OutlinedText, box_options: dict):
|
||||||
|
self.text = text
|
||||||
|
self.x = 10 * tex.screen_scale
|
||||||
|
self.y = -50 * tex.screen_scale
|
||||||
|
self.move = tex.get_animation(0)
|
||||||
|
self.is_selected = False
|
||||||
|
self.outline_color = ray.Color(109, 68, 24, 255)
|
||||||
|
self.direction = 1
|
||||||
|
self.target_position = float('inf')
|
||||||
|
self.start_position = self.y
|
||||||
|
language = global_data.config["general"]["language"]
|
||||||
|
self.options = [BaseOptionBox(box_options[option]["name"][language], box_options[option]["description"][language]) for option in box_options]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.__dict__)
|
||||||
|
|
||||||
|
def move_left(self):
|
||||||
|
"""Move the box left"""
|
||||||
|
if self.y != self.target_position and self.target_position != float('inf'):
|
||||||
|
return False
|
||||||
|
self.move.start()
|
||||||
|
self.direction = 1
|
||||||
|
self.start_position = self.y
|
||||||
|
self.target_position = self.y + (100 * tex.screen_scale * self.direction)
|
||||||
|
|
||||||
|
if self.target_position >= 650:
|
||||||
|
self.target_position = -50 + (self.target_position - 650)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def move_right(self):
|
||||||
|
"""Move the box right"""
|
||||||
|
if self.y != self.target_position and self.target_position != float('inf'):
|
||||||
|
return False
|
||||||
|
self.move.start()
|
||||||
|
self.start_position = self.y
|
||||||
|
self.direction = -1
|
||||||
|
self.target_position = self.y + (100 * tex.screen_scale * self.direction)
|
||||||
|
|
||||||
|
if self.target_position < -50:
|
||||||
|
self.target_position = 650 + (self.target_position + 50)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update(self, current_time_ms: float, is_selected: bool):
|
||||||
|
self.move.update(current_time_ms)
|
||||||
|
self.is_selected = is_selected
|
||||||
|
if self.move.is_finished:
|
||||||
|
self.y = self.target_position
|
||||||
|
else:
|
||||||
|
self.y = self.start_position + (self.move.attribute * self.direction)
|
||||||
|
|
||||||
|
def _draw_highlighted(self):
|
||||||
|
tex.draw_texture('box', 'box_highlight', x=self.x, y=self.y)
|
||||||
|
|
||||||
|
def _draw_text(self):
|
||||||
|
text_x = self.x + (tex.textures['box']['box'].width//2) - (self.text.texture.width//2)
|
||||||
|
text_y = self.y + (tex.textures['box']['box'].height//2) - (self.text.texture.height//2)
|
||||||
|
if self.is_selected:
|
||||||
|
self.text.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
|
||||||
|
else:
|
||||||
|
self.text.draw(outline_color=self.outline_color, x=text_x, y=text_y)
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
tex.draw_texture('box', 'box', x=self.x, y=self.y)
|
||||||
|
if self.is_selected:
|
||||||
|
self._draw_highlighted()
|
||||||
|
self._draw_text()
|
||||||
|
|
||||||
|
class BoxManager:
|
||||||
|
"""BoxManager class for the entry screen"""
|
||||||
|
def __init__(self, settings_template: dict):
|
||||||
|
language = global_data.config["general"]["language"]
|
||||||
|
self.boxes = [Box(OutlinedText(settings_template[config_name]["name"][language], tex.skin_config["entry_box_text"].font_size - int(5*tex.screen_scale), ray.WHITE, outline_thickness=5), settings_template[config_name]["options"]) for config_name in settings_template]
|
||||||
|
self.num_boxes = len(self.boxes)
|
||||||
|
self.selected_box_index = 3
|
||||||
|
self.is_2p = False
|
||||||
|
|
||||||
|
for i, box in enumerate(self.boxes):
|
||||||
|
box.y += 100*i
|
||||||
|
box.start_position += 100*i
|
||||||
|
|
||||||
|
def move_left(self):
|
||||||
|
"""Move the cursor to the left"""
|
||||||
|
moved = True
|
||||||
|
for box in self.boxes:
|
||||||
|
if not box.move_left():
|
||||||
|
moved = False
|
||||||
|
|
||||||
|
if moved:
|
||||||
|
self.selected_box_index = (self.selected_box_index - 1) % self.num_boxes
|
||||||
|
|
||||||
|
def move_right(self):
|
||||||
|
"""Move the cursor to the right"""
|
||||||
|
moved = True
|
||||||
|
for box in self.boxes:
|
||||||
|
if not box.move_right():
|
||||||
|
moved = False
|
||||||
|
|
||||||
|
if moved:
|
||||||
|
self.selected_box_index = (self.selected_box_index + 1) % self.num_boxes
|
||||||
|
|
||||||
|
def update(self, current_time_ms: float):
|
||||||
|
for i, box in enumerate(self.boxes):
|
||||||
|
is_selected = i == self.selected_box_index
|
||||||
|
box.update(current_time_ms, is_selected)
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
for box in self.boxes:
|
||||||
|
box.draw()
|
||||||
|
|
||||||
class SettingsScreen(Screen):
|
class SettingsScreen(Screen):
|
||||||
def on_screen_start(self):
|
def on_screen_start(self):
|
||||||
super().on_screen_start()
|
super().on_screen_start()
|
||||||
self.config = global_data.config
|
self.config = global_data.config
|
||||||
self.headers = list(self.config.keys())
|
self.indicator = Indicator(Indicator.State.SELECT)
|
||||||
self.headers.append('Exit')
|
self.template = json.loads((tex.graphics_path / "settings_template.json").read_text(encoding='utf-8'))
|
||||||
self.header_index = 0
|
self.box_manager = BoxManager(self.template)
|
||||||
self.setting_index = 0
|
|
||||||
self.in_setting_edit = False
|
|
||||||
self.editing_key = False
|
|
||||||
self.editing_gamepad = False
|
|
||||||
|
|
||||||
def on_screen_end(self, next_screen: str):
|
def on_screen_end(self, next_screen: str):
|
||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
global_data.config = self.config
|
global_data.config = self.config
|
||||||
audio.close_audio_device()
|
|
||||||
audio.device_type = global_data.config["audio"]["device_type"]
|
|
||||||
sample_rate = global_data.config["audio"]["sample_rate"]
|
|
||||||
if sample_rate < 0:
|
|
||||||
sample_rate = 44100
|
|
||||||
audio.target_sample_rate = sample_rate
|
|
||||||
audio.buffer_size = global_data.config["audio"]["buffer_size"]
|
|
||||||
audio.volume_presets = global_data.config["volume"]
|
|
||||||
audio.init_audio_device()
|
audio.init_audio_device()
|
||||||
logger.info("Settings saved and audio device re-initialized")
|
logger.info("Settings saved and audio device re-initialized")
|
||||||
return next_screen
|
return next_screen
|
||||||
|
|
||||||
def get_current_settings(self):
|
def handle_input(self):
|
||||||
"""Get the current section's settings as a list"""
|
if is_l_kat_pressed():
|
||||||
current_header = self.headers[self.header_index]
|
audio.play_sound('kat', 'sound')
|
||||||
if current_header == 'Exit' or current_header not in self.config:
|
self.box_manager.move_left()
|
||||||
return []
|
elif is_r_kat_pressed():
|
||||||
return list(self.config[current_header].items())
|
audio.play_sound('kat', 'sound')
|
||||||
|
self.box_manager.move_right()
|
||||||
def handle_boolean_toggle(self, section, key):
|
|
||||||
"""Toggle boolean values"""
|
|
||||||
self.config[section][key] = not self.config[section][key]
|
|
||||||
logger.info(f"Toggled boolean setting: {section}.{key} -> {self.config[section][key]}")
|
|
||||||
|
|
||||||
def handle_numeric_change(self, section, key, increment):
|
|
||||||
"""Handle numeric value changes"""
|
|
||||||
current_value = self.config[section][key]
|
|
||||||
|
|
||||||
# Define step sizes for different settings
|
|
||||||
step_sizes = {
|
|
||||||
'judge_offset': 1,
|
|
||||||
'visual_offset': 1,
|
|
||||||
'sample_rate': 1000,
|
|
||||||
}
|
|
||||||
|
|
||||||
step = step_sizes.get(key, 1)
|
|
||||||
new_value = current_value + (step * increment)
|
|
||||||
|
|
||||||
if key == 'sample_rate':
|
|
||||||
valid_rates = [-1, 22050, 44100, 48000, 88200, 96000]
|
|
||||||
current_idx = valid_rates.index(current_value) if current_value in valid_rates else 2
|
|
||||||
new_idx = max(0, min(len(valid_rates) - 1, current_idx + increment))
|
|
||||||
new_value = valid_rates[new_idx]
|
|
||||||
|
|
||||||
if key == 'buffer_size':
|
|
||||||
valid_sizes = [-1, 32, 64, 128, 256, 512, 1024]
|
|
||||||
current_idx = valid_sizes.index(current_value) if current_value in valid_sizes else 2
|
|
||||||
new_idx = max(0, min(len(valid_sizes) - 1, current_idx + increment))
|
|
||||||
new_value = valid_sizes[new_idx]
|
|
||||||
|
|
||||||
self.config[section][key] = new_value
|
|
||||||
logger.info(f"Changed numeric setting: {section}.{key} -> {new_value}")
|
|
||||||
|
|
||||||
def handle_string_cycle(self, section, key):
|
|
||||||
"""Cycle through predefined string values"""
|
|
||||||
current_value = self.config[section][key]
|
|
||||||
|
|
||||||
options = {
|
|
||||||
'language': ['ja', 'en'],
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in options:
|
|
||||||
values = options[key]
|
|
||||||
try:
|
|
||||||
current_idx = values.index(current_value)
|
|
||||||
new_idx = (current_idx + 1) % len(values)
|
|
||||||
self.config[section][key] = values[new_idx]
|
|
||||||
except ValueError:
|
|
||||||
self.config[section][key] = values[0]
|
|
||||||
logger.info(f"Cycled string setting: {section}.{key} -> {self.config[section][key]}")
|
|
||||||
|
|
||||||
def handle_key_binding(self, section, key):
|
|
||||||
"""Handle key binding changes"""
|
|
||||||
self.editing_key = True
|
|
||||||
logger.info(f"Started key binding edit for: {section}.{key}")
|
|
||||||
|
|
||||||
def update_key_binding(self):
|
|
||||||
"""Update key binding based on input"""
|
|
||||||
key_pressed = ray.get_key_pressed()
|
|
||||||
if key_pressed != 0:
|
|
||||||
# Convert keycode to character
|
|
||||||
if 65 <= key_pressed <= 90: # A-Z
|
|
||||||
new_key = chr(key_pressed)
|
|
||||||
current_header = self.headers[self.header_index]
|
|
||||||
settings = self.get_current_settings()
|
|
||||||
if settings:
|
|
||||||
setting_key, _ = settings[self.setting_index]
|
|
||||||
self.config[current_header][setting_key] = [new_key]
|
|
||||||
self.editing_key = False
|
|
||||||
logger.info(f"Key binding updated: {current_header}.{setting_key} -> {new_key}")
|
|
||||||
elif key_pressed == global_data.config["keys"]["back_key"]:
|
|
||||||
self.editing_key = False
|
|
||||||
logger.info("Key binding edit cancelled")
|
|
||||||
|
|
||||||
def handle_gamepad_binding(self, section, key):
|
|
||||||
self.editing_gamepad = True
|
|
||||||
logger.info(f"Started gamepad binding edit for: {section}.{key}")
|
|
||||||
|
|
||||||
def update_gamepad_binding(self):
|
|
||||||
"""Update gamepad binding based on input"""
|
|
||||||
button_pressed = ray.get_gamepad_button_pressed()
|
|
||||||
if button_pressed != 0:
|
|
||||||
current_header = self.headers[self.header_index]
|
|
||||||
settings = self.get_current_settings()
|
|
||||||
if settings:
|
|
||||||
setting_key, _ = settings[self.setting_index]
|
|
||||||
self.config[current_header][setting_key] = [button_pressed]
|
|
||||||
self.editing_gamepad = False
|
|
||||||
logger.info(f"Gamepad binding updated: {current_header}.{setting_key} -> {button_pressed}")
|
|
||||||
if ray.is_key_pressed(global_data.config["keys"]["back_key"]):
|
|
||||||
self.editing_gamepad = False
|
|
||||||
logger.info("Gamepad binding edit cancelled")
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
super().update()
|
super().update()
|
||||||
|
|
||||||
# Handle key binding editing
|
self.handle_input()
|
||||||
if self.editing_key:
|
|
||||||
self.update_key_binding()
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.editing_gamepad:
|
current_time = get_current_ms()
|
||||||
self.update_gamepad_binding()
|
self.indicator.update(current_time)
|
||||||
return
|
self.box_manager.update(current_time)
|
||||||
|
|
||||||
current_header = self.headers[self.header_index]
|
|
||||||
|
|
||||||
# Exit handling
|
|
||||||
if current_header == 'Exit' and (is_l_don_pressed() or is_r_don_pressed()):
|
|
||||||
logger.info("Exiting settings screen")
|
|
||||||
return self.on_screen_end("ENTRY")
|
|
||||||
|
|
||||||
# Navigation between sections
|
|
||||||
if not self.in_setting_edit:
|
|
||||||
if is_r_kat_pressed():
|
|
||||||
self.header_index = (self.header_index + 1) % len(self.headers)
|
|
||||||
self.setting_index = 0
|
|
||||||
logger.info(f"Navigated to next section: {self.headers[self.header_index]}")
|
|
||||||
elif is_l_kat_pressed():
|
|
||||||
self.header_index = (self.header_index - 1) % len(self.headers)
|
|
||||||
self.setting_index = 0
|
|
||||||
logger.info(f"Navigated to previous section: {self.headers[self.header_index]}")
|
|
||||||
elif (is_l_don_pressed() or is_r_don_pressed()) and current_header != 'Exit':
|
|
||||||
self.in_setting_edit = True
|
|
||||||
logger.info(f"Entered section edit: {current_header}")
|
|
||||||
else:
|
|
||||||
# Navigation within settings
|
|
||||||
settings = self.get_current_settings()
|
|
||||||
if not settings:
|
|
||||||
self.in_setting_edit = False
|
|
||||||
return
|
|
||||||
|
|
||||||
if is_r_kat_pressed():
|
|
||||||
self.setting_index = (self.setting_index + 1) % len(settings)
|
|
||||||
logger.info(f"Navigated to next setting: {settings[self.setting_index][0]}")
|
|
||||||
elif is_l_kat_pressed():
|
|
||||||
self.setting_index = (self.setting_index - 1) % len(settings)
|
|
||||||
logger.info(f"Navigated to previous setting: {settings[self.setting_index][0]}")
|
|
||||||
elif is_r_don_pressed():
|
|
||||||
# Modify setting value
|
|
||||||
setting_key, setting_value = settings[self.setting_index]
|
|
||||||
|
|
||||||
if isinstance(setting_value, bool):
|
|
||||||
self.handle_boolean_toggle(current_header, setting_key)
|
|
||||||
elif isinstance(setting_value, (int, float)):
|
|
||||||
self.handle_numeric_change(current_header, setting_key, 1)
|
|
||||||
elif isinstance(setting_value, str):
|
|
||||||
if 'keys' in current_header:
|
|
||||||
self.handle_key_binding(current_header, setting_key)
|
|
||||||
elif 'gamepad' in current_header:
|
|
||||||
self.handle_gamepad_binding(current_header, setting_key)
|
|
||||||
else:
|
|
||||||
self.handle_string_cycle(current_header, setting_key)
|
|
||||||
elif isinstance(setting_value, list) and len(setting_value) > 0:
|
|
||||||
if isinstance(setting_value[0], str) and len(setting_value[0]) == 1:
|
|
||||||
# Key binding
|
|
||||||
self.handle_key_binding(current_header, setting_key)
|
|
||||||
elif isinstance(setting_value[0], int):
|
|
||||||
self.handle_gamepad_binding(current_header, setting_key)
|
|
||||||
elif is_l_don_pressed():
|
|
||||||
# Modify setting value (reverse direction for numeric)
|
|
||||||
setting_key, setting_value = settings[self.setting_index]
|
|
||||||
|
|
||||||
if isinstance(setting_value, bool):
|
|
||||||
self.handle_boolean_toggle(current_header, setting_key)
|
|
||||||
elif isinstance(setting_value, (int, float)):
|
|
||||||
self.handle_numeric_change(current_header, setting_key, -1)
|
|
||||||
elif isinstance(setting_value, str):
|
|
||||||
if ('keys' not in current_header) and ('gamepad' not in current_header):
|
|
||||||
self.handle_string_cycle(current_header, setting_key)
|
|
||||||
|
|
||||||
elif ray.is_key_pressed(global_data.config["keys"]["back_key"]):
|
|
||||||
self.in_setting_edit = False
|
|
||||||
logger.info("Exited section edit")
|
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
ray.draw_rectangle(0, 0, tex.screen_width, tex.screen_height, ray.BLACK)
|
tex.draw_texture('background', 'background')
|
||||||
# Draw title
|
self.box_manager.draw()
|
||||||
ray.draw_rectangle(0, 0, tex.screen_width, tex.screen_height, ray.BLACK)
|
tex.draw_texture('background', 'footer')
|
||||||
ray.draw_text("SETTINGS", 20, 20, 30, ray.WHITE)
|
self.indicator.draw(tex.skin_config['song_select_indicator'].x, tex.skin_config['song_select_indicator'].y)
|
||||||
|
tex.draw_texture('background', 'overlay', scale=0.70)
|
||||||
# Draw section headers
|
|
||||||
current_header = self.headers[self.header_index]
|
|
||||||
for i, key in enumerate(self.headers):
|
|
||||||
color = ray.GREEN
|
|
||||||
if key == current_header:
|
|
||||||
color = ray.YELLOW if not self.in_setting_edit else ray.ORANGE
|
|
||||||
ray.draw_text(f'{key}', 20, i*25 + 70, 20, color)
|
|
||||||
|
|
||||||
# Draw current section settings
|
|
||||||
if current_header != 'Exit' and current_header in self.config:
|
|
||||||
settings = self.get_current_settings()
|
|
||||||
|
|
||||||
# Draw settings list
|
|
||||||
for i, (key, value) in enumerate(settings):
|
|
||||||
color = ray.GREEN
|
|
||||||
if self.in_setting_edit and i == self.setting_index:
|
|
||||||
color = ray.YELLOW
|
|
||||||
|
|
||||||
# Format value display
|
|
||||||
if isinstance(value, list):
|
|
||||||
display_value = ', '.join(map(str, value))
|
|
||||||
else:
|
|
||||||
display_value = str(value)
|
|
||||||
if key == 'device_type' and not isinstance(value, list):
|
|
||||||
display_value = f'{display_value} ({audio.get_host_api_name(value)})'
|
|
||||||
ray.draw_text(f'{key}: {display_value}', 250, i*25 + 70, 20, color)
|
|
||||||
|
|
||||||
# Draw instructions
|
|
||||||
y_offset = len(settings) * 25 + 150
|
|
||||||
if not self.in_setting_edit:
|
|
||||||
ray.draw_text("Don/Kat: Navigate sections", 20, y_offset, 16, ray.GRAY)
|
|
||||||
ray.draw_text("L/R Don: Enter section", 20, y_offset + 20, 16, ray.GRAY)
|
|
||||||
else:
|
|
||||||
ray.draw_text("Don/Kat: Navigate settings", 20, y_offset, 16, ray.GRAY)
|
|
||||||
ray.draw_text("L/R Don: Modify value", 20, y_offset + 20, 16, ray.GRAY)
|
|
||||||
ray.draw_text("ESC: Back to sections", 20, y_offset + 40, 16, ray.GRAY)
|
|
||||||
|
|
||||||
if self.editing_key:
|
|
||||||
ray.draw_text("Press a key to bind (ESC to cancel)", 20, y_offset + 60, 16, ray.RED)
|
|
||||||
else:
|
|
||||||
# Draw exit instruction
|
|
||||||
ray.draw_text("Press Don to exit settings", 250, 100, 20, ray.GREEN)
|
|
||||||
|
|||||||
@@ -1,18 +1,31 @@
|
|||||||
|
import logging
|
||||||
import random
|
import random
|
||||||
from dataclasses import fields
|
from dataclasses import fields
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pyray as ray
|
import pyray as ray
|
||||||
import logging
|
|
||||||
|
|
||||||
from raylib import SHADER_UNIFORM_VEC3
|
from raylib import SHADER_UNIFORM_VEC3
|
||||||
|
|
||||||
from libs.file_navigator import DEFAULT_COLORS, BackBox, DanCourse, GenreIndex, navigator
|
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
from libs.chara_2d import Chara2D
|
from libs.chara_2d import Chara2D
|
||||||
from libs.file_navigator import Directory, SongBox, SongFile
|
from libs.file_navigator import (
|
||||||
|
DEFAULT_COLORS,
|
||||||
|
BackBox,
|
||||||
|
DanCourse,
|
||||||
|
Directory,
|
||||||
|
GenreIndex,
|
||||||
|
SongBox,
|
||||||
|
SongFile,
|
||||||
|
navigator,
|
||||||
|
)
|
||||||
from libs.global_data import Difficulty, Modifiers, PlayerNum
|
from libs.global_data import Difficulty, Modifiers, PlayerNum
|
||||||
from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate, Indicator, Timer
|
from libs.global_objects import (
|
||||||
|
AllNetIcon,
|
||||||
|
CoinOverlay,
|
||||||
|
Indicator,
|
||||||
|
Nameplate,
|
||||||
|
Timer,
|
||||||
|
)
|
||||||
from libs.screen import Screen
|
from libs.screen import Screen
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
from libs.transition import Transition
|
from libs.transition import Transition
|
||||||
@@ -32,6 +45,7 @@ class State:
|
|||||||
BROWSING = 0
|
BROWSING = 0
|
||||||
SONG_SELECTED = 1
|
SONG_SELECTED = 1
|
||||||
DIFF_SORTING = 2
|
DIFF_SORTING = 2
|
||||||
|
SEARCHING = 3
|
||||||
|
|
||||||
class SongSelectScreen(Screen):
|
class SongSelectScreen(Screen):
|
||||||
BOX_CENTER = 444
|
BOX_CENTER = 444
|
||||||
@@ -56,6 +70,7 @@ class SongSelectScreen(Screen):
|
|||||||
self.game_transition = None
|
self.game_transition = None
|
||||||
self.demo_song = None
|
self.demo_song = None
|
||||||
self.diff_sort_selector = None
|
self.diff_sort_selector = None
|
||||||
|
self.search_box = None
|
||||||
self.coin_overlay = CoinOverlay()
|
self.coin_overlay = CoinOverlay()
|
||||||
self.allnet_indicator = AllNetIcon()
|
self.allnet_indicator = AllNetIcon()
|
||||||
self.indicator = Indicator(Indicator.State.SELECT)
|
self.indicator = Indicator(Indicator.State.SELECT)
|
||||||
@@ -69,6 +84,8 @@ class SongSelectScreen(Screen):
|
|||||||
self.dan_transition = DanTransition()
|
self.dan_transition = DanTransition()
|
||||||
self.shader = ray.load_shader('shader/dummy.vs', 'shader/colortransform.fs')
|
self.shader = ray.load_shader('shader/dummy.vs', 'shader/colortransform.fs')
|
||||||
self.color = None
|
self.color = None
|
||||||
|
song_format = tex.skin_config["song_num"].text[global_data.config["general"]["language"]]
|
||||||
|
self.song_num = OutlinedText(song_format.format(global_data.songs_played+1), tex.skin_config["song_num"].font_size, ray.WHITE)
|
||||||
self.load_shader_values(self.color)
|
self.load_shader_values(self.color)
|
||||||
|
|
||||||
session_data = global_data.session_data[global_data.player_num]
|
session_data = global_data.session_data[global_data.player_num]
|
||||||
@@ -109,7 +126,7 @@ class SongSelectScreen(Screen):
|
|||||||
self.screen_init = False
|
self.screen_init = False
|
||||||
self.reset_demo_music()
|
self.reset_demo_music()
|
||||||
current_item = self.navigator.get_current_item()
|
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.finalize_song(current_item)
|
||||||
self.player_1.nameplate.unload()
|
self.player_1.nameplate.unload()
|
||||||
if isinstance(current_item.box, SongBox) and current_item.box.yellow_box is not None:
|
if isinstance(current_item.box, SongBox) and current_item.box.yellow_box is not None:
|
||||||
@@ -152,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.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_in.start()
|
||||||
self.text_fade_out.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":
|
elif action == "select_song":
|
||||||
current_song = self.navigator.get_current_item()
|
current_song = self.navigator.get_current_item()
|
||||||
if isinstance(current_song, Directory) and current_song.box.genre_index == GenreIndex.DAN:
|
if isinstance(current_song, Directory) and current_song.box.genre_index == GenreIndex.DAN:
|
||||||
@@ -210,6 +232,21 @@ class SongSelectScreen(Screen):
|
|||||||
self.navigator.diff_sort_level = level
|
self.navigator.diff_sort_level = level
|
||||||
self.navigator.select_current_item()
|
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):
|
def _cancel_selection(self):
|
||||||
"""Reset to browsing state"""
|
"""Reset to browsing state"""
|
||||||
self.player_1.selected_song = False
|
self.player_1.selected_song = False
|
||||||
@@ -309,6 +346,9 @@ class SongSelectScreen(Screen):
|
|||||||
if self.diff_sort_selector is not None:
|
if self.diff_sort_selector is not None:
|
||||||
self.diff_sort_selector.update(current_time)
|
self.diff_sort_selector.update(current_time)
|
||||||
|
|
||||||
|
if self.search_box is not None:
|
||||||
|
self.search_box.update(current_time)
|
||||||
|
|
||||||
self.check_for_selection()
|
self.check_for_selection()
|
||||||
|
|
||||||
for song in self.navigator.items:
|
for song in self.navigator.items:
|
||||||
@@ -341,7 +381,7 @@ class SongSelectScreen(Screen):
|
|||||||
def draw_players(self):
|
def draw_players(self):
|
||||||
self.player_1.draw(self.state)
|
self.player_1.draw(self.state)
|
||||||
|
|
||||||
def draw(self):
|
def draw_background(self):
|
||||||
width = tex.textures['box']['background'].width
|
width = tex.textures['box']['background'].width
|
||||||
genre_index = self.genre_index
|
genre_index = self.genre_index
|
||||||
last_genre_index = self.last_genre_index
|
last_genre_index = self.last_genre_index
|
||||||
@@ -354,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)
|
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()
|
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()
|
self.draw_background_diffs()
|
||||||
|
|
||||||
if self.navigator.genre_bg is not None and self.state == State.BROWSING:
|
if self.navigator.genre_bg is not None and self.state == State.BROWSING:
|
||||||
@@ -371,6 +431,9 @@ class SongSelectScreen(Screen):
|
|||||||
if self.diff_sort_selector is not None:
|
if self.diff_sort_selector is not None:
|
||||||
self.diff_sort_selector.draw()
|
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):
|
if (self.player_1.selected_song and self.state == State.SONG_SELECTED):
|
||||||
tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute)
|
tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute)
|
||||||
elif self.state == State.DIFF_SORTING:
|
elif self.state == State.DIFF_SORTING:
|
||||||
@@ -385,21 +448,7 @@ class SongSelectScreen(Screen):
|
|||||||
if isinstance(curr_item, SongFile):
|
if isinstance(curr_item, SongFile):
|
||||||
curr_item.box.draw_score_history()
|
curr_item.box.draw_score_history()
|
||||||
|
|
||||||
self.indicator.draw(tex.skin_config['song_select_indicator'].x, tex.skin_config['song_select_indicator'].y)
|
self.draw_overlay()
|
||||||
|
|
||||||
tex.draw_texture('global', 'song_num_bg', fade=0.75)
|
|
||||||
tex.draw_texture('global', 'song_num', frame=global_data.songs_played % 4)
|
|
||||||
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()
|
|
||||||
|
|
||||||
class SongSelectPlayer:
|
class SongSelectPlayer:
|
||||||
def __init__(self, player_num: PlayerNum, text_fade_in):
|
def __init__(self, player_num: PlayerNum, text_fade_in):
|
||||||
@@ -413,6 +462,7 @@ class SongSelectPlayer:
|
|||||||
self.diff_select_move_right = False
|
self.diff_select_move_right = False
|
||||||
self.neiro_selector = None
|
self.neiro_selector = None
|
||||||
self.modifier_selector = None
|
self.modifier_selector = None
|
||||||
|
self.search_string = ''
|
||||||
|
|
||||||
# References to shared animations
|
# References to shared animations
|
||||||
self.diff_selector_move_1 = tex.get_animation(26, is_copy=True)
|
self.diff_selector_move_1 = tex.get_animation(26, is_copy=True)
|
||||||
@@ -478,13 +528,14 @@ class SongSelectPlayer:
|
|||||||
audio.play_sound('skip', 'sound')
|
audio.play_sound('skip', 'sound')
|
||||||
return "skip_right"
|
return "skip_right"
|
||||||
|
|
||||||
|
wheel = ray.get_mouse_wheel_move()
|
||||||
# Navigate left
|
# Navigate left
|
||||||
if is_l_kat_pressed(self.player_num):
|
if is_l_kat_pressed(self.player_num) or wheel > 0:
|
||||||
audio.play_sound('kat', 'sound')
|
audio.play_sound('kat', 'sound')
|
||||||
return "navigate_left"
|
return "navigate_left"
|
||||||
|
|
||||||
# Navigate right
|
# Navigate right
|
||||||
if is_r_kat_pressed(self.player_num):
|
if is_r_kat_pressed(self.player_num) or wheel < 0:
|
||||||
audio.play_sound('kat', 'sound')
|
audio.play_sound('kat', 'sound')
|
||||||
return "navigate_right"
|
return "navigate_right"
|
||||||
|
|
||||||
@@ -495,6 +546,8 @@ class SongSelectPlayer:
|
|||||||
return "go_back"
|
return "go_back"
|
||||||
elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[3]:
|
elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[3]:
|
||||||
return "diff_sort"
|
return "diff_sort"
|
||||||
|
elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[5]:
|
||||||
|
return "search"
|
||||||
else:
|
else:
|
||||||
return "select_song"
|
return "select_song"
|
||||||
|
|
||||||
@@ -522,6 +575,20 @@ class SongSelectPlayer:
|
|||||||
|
|
||||||
return None
|
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):
|
def handle_input(self, state, screen):
|
||||||
"""Main input dispatcher. Delegates to state-specific handlers."""
|
"""Main input dispatcher. Delegates to state-specific handlers."""
|
||||||
if self.is_voice_playing() or self.is_ready:
|
if self.is_voice_playing() or self.is_ready:
|
||||||
@@ -533,6 +600,8 @@ class SongSelectPlayer:
|
|||||||
screen.handle_input_selected()
|
screen.handle_input_selected()
|
||||||
elif state == State.DIFF_SORTING:
|
elif state == State.DIFF_SORTING:
|
||||||
screen.handle_input_diff_sort()
|
screen.handle_input_diff_sort()
|
||||||
|
elif state == State.SEARCHING:
|
||||||
|
screen.handle_input_search()
|
||||||
|
|
||||||
def handle_input_selected(self, current_item):
|
def handle_input_selected(self, current_item):
|
||||||
"""Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None"""
|
"""Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None"""
|
||||||
@@ -576,8 +645,6 @@ class SongSelectPlayer:
|
|||||||
if is_l_kat_pressed(self.player_num) or is_r_kat_pressed(self.player_num):
|
if is_l_kat_pressed(self.player_num) or is_r_kat_pressed(self.player_num):
|
||||||
audio.play_sound('kat', 'sound')
|
audio.play_sound('kat', 'sound')
|
||||||
selected_song = current_item
|
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)
|
diffs = sorted(selected_song.tja.metadata.course_data)
|
||||||
prev_diff = self.selected_difficulty
|
prev_diff = self.selected_difficulty
|
||||||
ret_val = None
|
ret_val = None
|
||||||
@@ -1011,12 +1078,37 @@ class DiffSortSelect:
|
|||||||
else:
|
else:
|
||||||
self.draw_diff_select()
|
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:
|
class NeiroSelector:
|
||||||
"""The menu for selecting the game hitsounds."""
|
"""The menu for selecting the game hitsounds."""
|
||||||
def __init__(self, player_num: PlayerNum):
|
def __init__(self, player_num: PlayerNum):
|
||||||
self.player_num = player_num
|
self.player_num = player_num
|
||||||
self.selected_sound = global_data.hit_sound[self.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 = neiro_list.readlines()
|
||||||
self.sounds.append('無音')
|
self.sounds.append('無音')
|
||||||
self.load_sound()
|
self.load_sound()
|
||||||
@@ -1037,9 +1129,9 @@ class NeiroSelector:
|
|||||||
if self.selected_sound == len(self.sounds):
|
if self.selected_sound == len(self.sounds):
|
||||||
return
|
return
|
||||||
if self.selected_sound == 0:
|
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:
|
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):
|
def move_left(self):
|
||||||
if self.move.is_started and not self.move.is_finished:
|
if self.move.is_started and not self.move.is_finished:
|
||||||
@@ -1131,16 +1223,23 @@ class ModifierSelector:
|
|||||||
"inverse": "mod_abekobe",
|
"inverse": "mod_abekobe",
|
||||||
"random": "mod_kimagure"
|
"random": "mod_kimagure"
|
||||||
}
|
}
|
||||||
NAME_MAP = {
|
NAME_MAP_JA = {
|
||||||
"auto": "オート",
|
"auto": "オート",
|
||||||
"speed": "はやさ",
|
"speed": "はやさ",
|
||||||
"display": "ドロン",
|
"display": "ドロン",
|
||||||
"inverse": "あべこべ",
|
"inverse": "あべこべ",
|
||||||
"random": "ランダム"
|
"random": "ランダム"
|
||||||
}
|
}
|
||||||
|
NAME_MAP_EN = {
|
||||||
|
"auto": "Auto",
|
||||||
|
"speed": "Speed",
|
||||||
|
"display": "Display",
|
||||||
|
"inverse": "Inverse",
|
||||||
|
"random": "Random"
|
||||||
|
}
|
||||||
def __init__(self, player_num: PlayerNum):
|
def __init__(self, player_num: PlayerNum):
|
||||||
self.player_num = player_num
|
self.player_num = player_num
|
||||||
self.mods = fields(Modifiers)
|
self.mods = fields(Modifiers)[:-1]
|
||||||
self.current_mod_index = 0
|
self.current_mod_index = 0
|
||||||
self.is_confirmed = False
|
self.is_confirmed = False
|
||||||
self.is_finished = False
|
self.is_finished = False
|
||||||
@@ -1152,19 +1251,24 @@ class ModifierSelector:
|
|||||||
self.fade_sideways = tex.get_animation(32, is_copy=True)
|
self.fade_sideways = tex.get_animation(32, is_copy=True)
|
||||||
self.direction = -1
|
self.direction = -1
|
||||||
audio.play_sound(f'voice_options_{self.player_num}p', 'sound')
|
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.language = global_data.config["general"]["language"]
|
||||||
self.text_true = OutlinedText('する', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
if self.language == 'en':
|
||||||
self.text_false = OutlinedText('しない', tex.skin_config["modifier_text"].font_size, ray.WHITE, outline_thickness=3.5)
|
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_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_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"].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
|
# 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_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"].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_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_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"].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):
|
def update(self, current_ms):
|
||||||
self.is_finished = self.is_confirmed and self.move.is_finished
|
self.is_finished = self.is_confirmed and self.move.is_finished
|
||||||
@@ -1205,20 +1309,20 @@ class ModifierSelector:
|
|||||||
if current_value:
|
if current_value:
|
||||||
self.text_true.unload()
|
self.text_true.unload()
|
||||||
self.text_true = self.text_true_2
|
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:
|
else:
|
||||||
self.text_false.unload()
|
self.text_false.unload()
|
||||||
self.text_false = self.text_false_2
|
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':
|
elif current_mod.name == 'random':
|
||||||
if current_value == 1:
|
if current_value == 1:
|
||||||
self.text_kimagure.unload()
|
self.text_kimagure.unload()
|
||||||
self.text_kimagure = self.text_kimagure_2
|
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:
|
elif current_value == 2:
|
||||||
self.text_detarame.unload()
|
self.text_detarame.unload()
|
||||||
self.text_detarame = self.text_detarame_2
|
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):
|
def left(self):
|
||||||
if self.is_confirmed:
|
if self.is_confirmed:
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pyray as ray
|
||||||
|
|
||||||
from libs.audio import audio
|
from libs.audio import audio
|
||||||
from libs.global_objects import AllNetIcon, CoinOverlay, EntryOverlay
|
from libs.global_objects import AllNetIcon, CoinOverlay, EntryOverlay
|
||||||
|
from libs.screen import Screen
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
from libs.utils import (
|
from libs.utils import (
|
||||||
|
OutlinedText,
|
||||||
get_current_ms,
|
get_current_ms,
|
||||||
global_data,
|
global_data,
|
||||||
global_tex,
|
global_tex,
|
||||||
@@ -13,7 +17,6 @@ from libs.utils import (
|
|||||||
is_r_don_pressed,
|
is_r_don_pressed,
|
||||||
)
|
)
|
||||||
from libs.video import VideoPlayer
|
from libs.video import VideoPlayer
|
||||||
from libs.screen import Screen
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -25,19 +28,11 @@ class State:
|
|||||||
class TitleScreen(Screen):
|
class TitleScreen(Screen):
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
#normalize to accept both stings and lists in toml
|
|
||||||
#maybe normalize centrally? but it's used only here
|
|
||||||
vp = global_data.config["paths"]["video_path"]
|
|
||||||
video_paths = [vp] if isinstance(vp, str) else vp
|
|
||||||
self.op_video_list = []
|
self.op_video_list = []
|
||||||
self.attract_video_list = []
|
self.attract_video_list = []
|
||||||
for base in video_paths:
|
base = Path(f"Skins/{global_data.config["paths"]["skin"]}/Videos")
|
||||||
base = Path(base)
|
self.op_video_list += list((base/"op_videos").glob("**/*.mp4"))
|
||||||
self.op_video_list += list((base/"op_videos").glob("**/*.mp4"))
|
self.attract_video_list += list((base/"attract_videos").glob("**/*.mp4"))
|
||||||
self.attract_video_list += list((base/"attract_videos").glob("**/*.mp4"))
|
|
||||||
self.coin_overlay = CoinOverlay()
|
|
||||||
self.allnet_indicator = AllNetIcon()
|
|
||||||
self.entry_overlay = EntryOverlay()
|
|
||||||
|
|
||||||
def on_screen_start(self):
|
def on_screen_start(self):
|
||||||
super().on_screen_start()
|
super().on_screen_start()
|
||||||
@@ -45,6 +40,10 @@ class TitleScreen(Screen):
|
|||||||
self.op_video = None
|
self.op_video = None
|
||||||
self.attract_video = None
|
self.attract_video = None
|
||||||
self.warning_board = None
|
self.warning_board = None
|
||||||
|
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, outline_thickness=4)
|
||||||
self.fade_out = tex.get_animation(13)
|
self.fade_out = tex.get_animation(13)
|
||||||
self.text_overlay_fade = tex.get_animation(14)
|
self.text_overlay_fade = tex.get_animation(14)
|
||||||
|
|
||||||
@@ -121,8 +120,8 @@ class TitleScreen(Screen):
|
|||||||
self.allnet_indicator.draw()
|
self.allnet_indicator.draw()
|
||||||
self.entry_overlay.draw(tex.skin_config["entry_overlay_title"].x, y=tex.skin_config["entry_overlay_title"].y)
|
self.entry_overlay.draw(tex.skin_config["entry_overlay_title"].x, y=tex.skin_config["entry_overlay_title"].y)
|
||||||
|
|
||||||
global_tex.draw_texture('overlay', 'hit_taiko_to_start', index=0, fade=self.text_overlay_fade.attribute)
|
self.hit_taiko_text.draw(ray.BLACK, x=tex.screen_width*0.25 - self.hit_taiko_text.texture.width//2, y=tex.skin_config["hit_taiko_to_start"].y, fade=self.text_overlay_fade.attribute)
|
||||||
global_tex.draw_texture('overlay', 'hit_taiko_to_start', index=1, fade=self.text_overlay_fade.attribute)
|
self.hit_taiko_text.draw(ray.BLACK, x=tex.screen_width*0.75 - self.hit_taiko_text.texture.width//2, y=tex.skin_config["hit_taiko_to_start"].y, fade=self.text_overlay_fade.attribute)
|
||||||
|
|
||||||
class WarningScreen:
|
class WarningScreen:
|
||||||
"""Warning screen for the game"""
|
"""Warning screen for the game"""
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import logging
|
|
||||||
import copy
|
import copy
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pyray as ray
|
||||||
|
|
||||||
|
from libs.audio import audio
|
||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.tja import TJAParser
|
from libs.tja import TJAParser
|
||||||
from libs.utils import get_current_ms
|
from libs.utils import get_current_ms, global_data
|
||||||
from libs.audio import audio
|
|
||||||
from libs.utils import global_data
|
|
||||||
from libs.video import VideoPlayer
|
from libs.video import VideoPlayer
|
||||||
import pyray as ray
|
from scenes.game import (
|
||||||
from scenes.game import ClearAnimation, FCAnimation, FailAnimation, GameScreen, Player, Background, ResultTransition
|
Background,
|
||||||
|
ClearAnimation,
|
||||||
|
FailAnimation,
|
||||||
|
FCAnimation,
|
||||||
|
GameScreen,
|
||||||
|
Player,
|
||||||
|
ResultTransition,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -23,7 +32,7 @@ class TwoPlayerGameScreen(GameScreen):
|
|||||||
|
|
||||||
def load_hitsounds(self):
|
def load_hitsounds(self):
|
||||||
"""Load the hit sounds"""
|
"""Load the hit sounds"""
|
||||||
sounds_dir = Path("Sounds")
|
sounds_dir = Path(f"Skins/{global_data.config["paths"]["skin"]}/Sounds")
|
||||||
|
|
||||||
# Load hitsounds for 1P
|
# Load hitsounds for 1P
|
||||||
if global_data.hit_sound[PlayerNum.P1] == -1:
|
if global_data.hit_sound[PlayerNum.P1] == -1:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.texture import tex
|
from libs.texture import tex
|
||||||
from libs.utils import get_current_ms
|
from libs.utils import get_current_ms
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from libs.audio import audio
|
||||||
from libs.file_navigator import SongBox, SongFile
|
from libs.file_navigator import SongBox, SongFile
|
||||||
from libs.global_data import PlayerNum
|
from libs.global_data import PlayerNum
|
||||||
from libs.transition import Transition
|
from libs.transition import Transition
|
||||||
from scenes.song_select import DiffSortSelect, SongSelectPlayer, SongSelectScreen, State
|
|
||||||
from libs.utils import get_current_ms, global_data
|
from libs.utils import get_current_ms, global_data
|
||||||
from libs.audio import audio
|
from scenes.song_select import (
|
||||||
|
DiffSortSelect,
|
||||||
|
SongSelectPlayer,
|
||||||
|
SongSelectScreen,
|
||||||
|
State,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
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
|
version = 1
|
||||||
revision = 3
|
revision = 2
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
[[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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "nuitka"
|
name = "nuitka"
|
||||||
version = "2.8.4"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.22"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pypresence"
|
name = "pypresence"
|
||||||
version = "4.6.1"
|
version = "4.6.1"
|
||||||
@@ -122,11 +260,13 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytaiko"
|
name = "pytaiko"
|
||||||
version = "1.0"
|
version = "1.1"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "av" },
|
{ name = "av" },
|
||||||
|
{ name = "pyinstrument" },
|
||||||
{ name = "pypresence" },
|
{ name = "pypresence" },
|
||||||
|
{ name = "pytest" },
|
||||||
{ name = "raylib-sdl" },
|
{ name = "raylib-sdl" },
|
||||||
{ name = "tomlkit" },
|
{ name = "tomlkit" },
|
||||||
]
|
]
|
||||||
@@ -134,18 +274,54 @@ dependencies = [
|
|||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "nuitka" },
|
{ name = "nuitka" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "av", specifier = ">=16.0.1" },
|
{ name = "av", specifier = ">=16.0.1" },
|
||||||
|
{ name = "pyinstrument", specifier = ">=5.1.1" },
|
||||||
{ name = "pypresence", specifier = ">=4.6.1" },
|
{ name = "pypresence", specifier = ">=4.6.1" },
|
||||||
|
{ name = "pytest", specifier = ">=9.0.2" },
|
||||||
{ name = "raylib-sdl", specifier = ">=5.5.0.2" },
|
{ name = "raylib-sdl", specifier = ">=5.5.0.2" },
|
||||||
{ name = "tomlkit", specifier = ">=0.13.3" },
|
{ name = "tomlkit", specifier = ">=0.13.3" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[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]]
|
[[package]]
|
||||||
name = "raylib-sdl"
|
name = "raylib-sdl"
|
||||||
|
|||||||
Reference in New Issue
Block a user