Compare commits
13 Commits
48e707cd64
...
F-B
| Author | SHA1 | Date | |
|---|---|---|---|
| 4895990729 | |||
| 6686f5ae15 | |||
| 8d58bf683f | |||
|
|
6d7be5c45c | ||
| 92c1261f6f | |||
| 9a2a7dbee6 | |||
| f91e3c9089 | |||
| 84a0c2b7e0 | |||
| 9f2d753500 | |||
| a77534c72b | |||
| 1ca7a3f610 | |||
| 4da81d16dc | |||
| 3d611a9c46 |
@@ -0,0 +1,20 @@
|
|||||||
|
I will optimize the loading process by implementing a **Multi-Threaded Worker Loader**. This involves creating a pool of Web Workers to fetch assets (JavaScript, Audio, Images, Views) in parallel, offloading the network initiation and handling from the main thread.
|
||||||
|
|
||||||
|
### Plan:
|
||||||
|
1. **Create `public/src/js/loader-worker.js`**:
|
||||||
|
* This worker will handle `fetch` requests for different resource types (`text`, `blob`, `arraybuffer`).
|
||||||
|
* It will transfer the data back to the main thread (using zero-copy transfer for `ArrayBuffer`).
|
||||||
|
|
||||||
|
2. **Modify `public/src/js/loader.js`**:
|
||||||
|
* **Initialize Worker Pool**: Create a pool of workers (defaulting to 4) in the `Loader` class.
|
||||||
|
* **Implement `workerFetch(url, type)`**: A method to distribute fetch tasks to the worker pool.
|
||||||
|
* **Override `ajax(url, ...)`**: Intercept requests for static assets (`src/`, `assets/`, etc.) and route them through `workerFetch`. Keep API calls (`api/`) on the main thread to ensure session stability.
|
||||||
|
* **Update `loadScript(url)`**: Change it to fetch the script content via `workerFetch` and inject it using a `<script>` tag with inline content. This ensures JS files are also loaded via the "multi-process" mechanism.
|
||||||
|
* **Update `loadSound` and `RemoteFile` logic**: Since `RemoteFile` uses `loader.ajax`, routing `ajax` to workers will automatically parallelize audio loading.
|
||||||
|
|
||||||
|
### Technical Details:
|
||||||
|
* **Concurrency**: 4 Workers will be used to maximize throughput without overloading the browser's connection limit per domain.
|
||||||
|
* **Resource Types**:
|
||||||
|
* **JS/Views**: Fetched as `text`.
|
||||||
|
* **Images**: Fetched as `blob` -> `URL.createObjectURL`.
|
||||||
|
* **Audio**: Fetched as `arraybuffer` -> `AudioContext.decodeAudioData`.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
I will implement the requested changes to remove the delete functionality and redirect the upload interface while keeping the upload API intact.
|
||||||
|
|
||||||
|
### 1. Frontend: Song Selection Menu (`public/src/js/songselect.js`)
|
||||||
|
* **Remove Delete Button**: I will remove the "Delete" (削除) button configuration from the `difficultyMenu` buttons array (around lines 313-319). This removes the option from the UI.
|
||||||
|
* **Redirect Upload Action**: I will modify the handler for the "upload" action (around lines 954-958). Instead of redirecting to the local `/upload/` page, it will redirect to `https://zizhipu.taiko.asia`.
|
||||||
|
|
||||||
|
### 2. Backend: API Security (`app.py`)
|
||||||
|
* **Disable Delete API**: I will modify the `/api/delete` route to return a 403 Forbidden error (or simply pass), ensuring that songs cannot be deleted even if someone calls the API directly.
|
||||||
|
* **Keep Upload API**: The `/api/upload` route will remain unchanged, preserving the ability to upload songs via API as requested.
|
||||||
76
README.md
76
README.md
@@ -33,6 +33,55 @@ http://<服务器IP>/
|
|||||||
- 创建 `systemd` 服务,使用 `gunicorn` 直接监听 `0.0.0.0:80`
|
- 创建 `systemd` 服务,使用 `gunicorn` 直接监听 `0.0.0.0:80`
|
||||||
- 开放防火墙 `80/tcp`(如系统启用了 `ufw`)
|
- 开放防火墙 `80/tcp`(如系统启用了 `ufw`)
|
||||||
|
|
||||||
|
## 全命令部署(使用 Docker 部署 MongoDB)
|
||||||
|
|
||||||
|
适用系统:Ubuntu 20.04+/22.04+/24.04(需 root),MongoDB 通过 Docker 启动,其余步骤照常。
|
||||||
|
|
||||||
|
1. 安装 Docker 并启动:
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y docker.io
|
||||||
|
sudo systemctl enable --now docker
|
||||||
|
```
|
||||||
|
2. 启动 MongoDB 容器(持久化到 `/srv/taiko-web-mongo`,监听 `27017`):
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /srv/taiko-web-mongo
|
||||||
|
sudo docker run -d \
|
||||||
|
--name taiko-web-mongo \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-v /srv/taiko-web-mongo:/data/db \
|
||||||
|
-p 27017:27017 \
|
||||||
|
mongo:6
|
||||||
|
```
|
||||||
|
如需开启认证,可加上:
|
||||||
|
```bash
|
||||||
|
-e MONGO_INITDB_ROOT_USERNAME=<用户名> -e MONGO_INITDB_ROOT_PASSWORD=<强密码>
|
||||||
|
```
|
||||||
|
并在应用侧通过环境变量指定 Host:
|
||||||
|
```bash
|
||||||
|
export TAIKO_WEB_MONGO_HOST=127.0.0.1:27017
|
||||||
|
```
|
||||||
|
3. 安装并启动 Redis(照常):
|
||||||
|
```bash
|
||||||
|
sudo apt install -y redis-server
|
||||||
|
sudo systemctl enable --now redis-server
|
||||||
|
```
|
||||||
|
4. 准备项目与虚拟环境(照常):
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /srv/taiko-web
|
||||||
|
sudo rsync -a --delete --exclude '.git' --exclude '.venv' . /srv/taiko-web/
|
||||||
|
sudo python3 -m venv /srv/taiko-web/.venv
|
||||||
|
sudo /srv/taiko-web/.venv/bin/pip install -U pip
|
||||||
|
sudo /srv/taiko-web/.venv/bin/pip install -r /srv/taiko-web/requirements.txt
|
||||||
|
sudo cp /srv/taiko-web/config.example.py /srv/taiko-web/config.py
|
||||||
|
```
|
||||||
|
5. 赋予 80 端口绑定权限并启动:
|
||||||
|
```bash
|
||||||
|
sudo setcap 'cap_net_bind_service=+ep' /srv/taiko-web/.venv/bin/python3
|
||||||
|
export TAIKO_WEB_MONGO_HOST=${TAIKO_WEB_MONGO_HOST:-127.0.0.1:27017}
|
||||||
|
sudo /srv/taiko-web/.venv/bin/gunicorn -b 0.0.0.0:80 app:app
|
||||||
|
```
|
||||||
|
|
||||||
## 手动部署(可选)
|
## 手动部署(可选)
|
||||||
|
|
||||||
1. 安装依赖:
|
1. 安装依赖:
|
||||||
@@ -94,3 +143,30 @@ docker run --detach \
|
|||||||
---
|
---
|
||||||
|
|
||||||
如需将监听接口改为仅内网或增加并发工作数(例如 `--workers 4`),可在 `setup.sh` 或 `systemd` 服务中调整。
|
如需将监听接口改为仅内网或增加并发工作数(例如 `--workers 4`),可在 `setup.sh` 或 `systemd` 服务中调整。
|
||||||
|
## 歌曲类型(Type)
|
||||||
|
|
||||||
|
- 可选枚举:
|
||||||
|
- 01 Pop
|
||||||
|
- 02 Anime
|
||||||
|
- 03 Vocaloid
|
||||||
|
- 04 Children and Folk
|
||||||
|
- 05 Variety
|
||||||
|
- 06 Classical
|
||||||
|
- 07 Game Music
|
||||||
|
- 08 Live Festival Mode
|
||||||
|
- 09 Namco Original
|
||||||
|
- 10 Taiko Towers
|
||||||
|
- 11 Dan Dojo
|
||||||
|
|
||||||
|
### 上传要求
|
||||||
|
- 上传表单新增必填字段 `song_type`,取值为上述枚举之一
|
||||||
|
- 成功后将写入 MongoDB `songs.song_type`
|
||||||
|
|
||||||
|
### API 扩展
|
||||||
|
- `GET /api/songs?type=<歌曲类型>` 按类型过滤返回启用歌曲
|
||||||
|
- 示例:`/api/songs?type=02%20Anime`
|
||||||
|
- 返回项包含 `song_type` 字段
|
||||||
|
|
||||||
|
### 前端切换
|
||||||
|
- 在歌曲选择页顶部显示当前歌曲类型标签
|
||||||
|
- 使用左右跳转(Shift+左右或肩键)自动切换类型并刷新列表
|
||||||
|
|||||||
196
app.py
196
app.py
@@ -13,6 +13,7 @@ import requests
|
|||||||
import schema
|
import schema
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
# -- カスタム --
|
# -- カスタム --
|
||||||
import traceback
|
import traceback
|
||||||
@@ -33,6 +34,7 @@ from flask_session import Session
|
|||||||
from flask_wtf.csrf import CSRFProtect, generate_csrf, CSRFError
|
from flask_wtf.csrf import CSRFProtect, generate_csrf, CSRFError
|
||||||
from ffmpy import FFmpeg
|
from ffmpy import FFmpeg
|
||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
|
from pymongo.errors import DuplicateKeyError
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
|
||||||
def take_config(name, required=False):
|
def take_config(name, required=False):
|
||||||
@@ -44,6 +46,19 @@ def take_config(name, required=False):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
SONG_TYPES = [
|
||||||
|
"01 Pop",
|
||||||
|
"02 Anime",
|
||||||
|
"03 Vocaloid",
|
||||||
|
"04 Children and Folk",
|
||||||
|
"05 Variety",
|
||||||
|
"06 Classical",
|
||||||
|
"07 Game Music",
|
||||||
|
"08 Live Festival Mode",
|
||||||
|
"09 Namco Original",
|
||||||
|
"10 Taiko Towers",
|
||||||
|
"11 Dan Dojo",
|
||||||
|
]
|
||||||
|
|
||||||
def get_remote_address() -> str:
|
def get_remote_address() -> str:
|
||||||
return flask.request.headers.get("CF-Connecting-IP") or flask.request.headers.get("X-Forwarded-For") or flask.request.remote_addr or "127.0.0.1"
|
return flask.request.headers.get("CF-Connecting-IP") or flask.request.headers.get("X-Forwarded-For") or flask.request.remote_addr or "127.0.0.1"
|
||||||
@@ -89,7 +104,15 @@ sess.init_app(app)
|
|||||||
db = client[take_config('MONGO', required=True)['database']]
|
db = client[take_config('MONGO', required=True)['database']]
|
||||||
db.users.create_index('username', unique=True)
|
db.users.create_index('username', unique=True)
|
||||||
db.songs.create_index('id', unique=True)
|
db.songs.create_index('id', unique=True)
|
||||||
|
db.songs.create_index('song_type')
|
||||||
db.scores.create_index('username')
|
db.scores.create_index('username')
|
||||||
|
db.leaderboard.create_index([
|
||||||
|
('song_id', 1),
|
||||||
|
('difficulty', 1),
|
||||||
|
('month', 1),
|
||||||
|
('score', -1)
|
||||||
|
])
|
||||||
|
db.leaderboard.create_index('username')
|
||||||
|
|
||||||
|
|
||||||
class HashException(Exception):
|
class HashException(Exception):
|
||||||
@@ -479,7 +502,13 @@ def route_api_preview():
|
|||||||
@app.route(basedir + 'api/songs')
|
@app.route(basedir + 'api/songs')
|
||||||
@app.cache.cached(timeout=15)
|
@app.cache.cached(timeout=15)
|
||||||
def route_api_songs():
|
def route_api_songs():
|
||||||
songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False}))
|
type_q = flask.request.args.get('type')
|
||||||
|
query = {'enabled': True}
|
||||||
|
if type_q:
|
||||||
|
if type_q not in SONG_TYPES:
|
||||||
|
return abort(400)
|
||||||
|
query['song_type'] = type_q
|
||||||
|
songs = list(db.songs.find(query, {'_id': False, 'enabled': False}))
|
||||||
for song in songs:
|
for song in songs:
|
||||||
if song['maker_id']:
|
if song['maker_id']:
|
||||||
if song['maker_id'] == 0:
|
if song['maker_id'] == 0:
|
||||||
@@ -725,6 +754,133 @@ def route_api_scores_get():
|
|||||||
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don})
|
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(basedir + 'api/leaderboard/submit', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def route_api_leaderboard_submit():
|
||||||
|
data = request.get_json()
|
||||||
|
if not schema.validate(data, schema.leaderboard_submit):
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
username = session.get('username')
|
||||||
|
user = db.users.find_one({'username': username})
|
||||||
|
|
||||||
|
current_month = datetime.now().strftime('%Y-%m')
|
||||||
|
|
||||||
|
# Handle song_id type
|
||||||
|
song_id = data['song_id']
|
||||||
|
|
||||||
|
query = {
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': data['difficulty'],
|
||||||
|
'month': current_month,
|
||||||
|
'username': username
|
||||||
|
}
|
||||||
|
|
||||||
|
existing = db.leaderboard.find_one(query)
|
||||||
|
|
||||||
|
new_record = False
|
||||||
|
rank_info = None
|
||||||
|
|
||||||
|
if not existing or data['score'] > existing['score']:
|
||||||
|
entry = {
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': data['difficulty'],
|
||||||
|
'month': current_month,
|
||||||
|
'username': username,
|
||||||
|
'display_name': user['display_name'],
|
||||||
|
'don': get_db_don(user),
|
||||||
|
'score': data['score'],
|
||||||
|
'good': data.get('good', 0),
|
||||||
|
'ok': data.get('ok', 0),
|
||||||
|
'bad': data.get('bad', 0),
|
||||||
|
'maxCombo': data.get('maxCombo', 0),
|
||||||
|
'timestamp': datetime.now()
|
||||||
|
}
|
||||||
|
db.leaderboard.update_one(query, {'$set': entry}, upsert=True)
|
||||||
|
new_record = True
|
||||||
|
|
||||||
|
# Get rank
|
||||||
|
count = db.leaderboard.count_documents({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': data['difficulty'],
|
||||||
|
'month': current_month,
|
||||||
|
'score': {'$gt': data['score']}
|
||||||
|
})
|
||||||
|
rank = count + 1
|
||||||
|
rank_info = rank
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'ok',
|
||||||
|
'new_record': new_record,
|
||||||
|
'rank': rank_info
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(basedir + 'api/leaderboard/get')
|
||||||
|
def route_api_leaderboard_get():
|
||||||
|
song_id = request.args.get('song_id')
|
||||||
|
difficulty = request.args.get('difficulty')
|
||||||
|
|
||||||
|
if not song_id or not difficulty:
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
# Try convert song_id to int if it looks like one
|
||||||
|
if re.match('^[0-9]+$', str(song_id)):
|
||||||
|
song_id = int(song_id)
|
||||||
|
|
||||||
|
current_month = datetime.now().strftime('%Y-%m')
|
||||||
|
|
||||||
|
query = {
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'month': current_month
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get top 50
|
||||||
|
leaderboard = list(db.leaderboard.find(query, {'_id': False}).sort('score', -1).limit(50))
|
||||||
|
|
||||||
|
user_rank = None
|
||||||
|
if session.get('username'):
|
||||||
|
username = session.get('username')
|
||||||
|
user_entry = db.leaderboard.find_one({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'month': current_month,
|
||||||
|
'username': username
|
||||||
|
}, {'_id': False})
|
||||||
|
|
||||||
|
if user_entry:
|
||||||
|
# Calculate rank
|
||||||
|
count = db.leaderboard.count_documents({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'month': current_month,
|
||||||
|
'score': {'$gt': user_entry['score']}
|
||||||
|
})
|
||||||
|
user_rank = {
|
||||||
|
'rank': count + 1,
|
||||||
|
'entry': user_entry
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'ok',
|
||||||
|
'leaderboard': leaderboard,
|
||||||
|
'user_rank': user_rank
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(basedir + 'api/leaderboard/reset', methods=['POST'])
|
||||||
|
@admin_required(level=100)
|
||||||
|
def route_api_leaderboard_reset():
|
||||||
|
current_month = datetime.now().strftime('%Y-%m')
|
||||||
|
result = db.leaderboard.delete_many({'month': {'$ne': current_month}})
|
||||||
|
return jsonify({
|
||||||
|
'status': 'ok',
|
||||||
|
'deleted_count': result.deleted_count,
|
||||||
|
'current_month': current_month
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route(basedir + 'privacy')
|
@app.route(basedir + 'privacy')
|
||||||
def route_api_privacy():
|
def route_api_privacy():
|
||||||
last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt')))
|
last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt')))
|
||||||
@@ -852,17 +1008,32 @@ def upload_file():
|
|||||||
db_entry['enabled'] = True
|
db_entry['enabled'] = True
|
||||||
pprint.pprint(db_entry)
|
pprint.pprint(db_entry)
|
||||||
|
|
||||||
# mongoDBにデータをぶち込む
|
# 必要な歌曲类型
|
||||||
client['taiko']["songs"].insert_one(db_entry)
|
song_type = flask.request.form.get('song_type')
|
||||||
|
if not song_type or song_type not in SONG_TYPES:
|
||||||
|
return flask.jsonify({'error': 'invalid_song_type'})
|
||||||
|
db_entry['song_type'] = song_type
|
||||||
|
|
||||||
|
# mongoDBにデータをぶち込む(重複IDは部分更新で上書きし、_id を不変に保つ)
|
||||||
|
coll = client['taiko']["songs"]
|
||||||
|
try:
|
||||||
|
coll.insert_one(db_entry)
|
||||||
|
except DuplicateKeyError:
|
||||||
|
coll.update_one({"id": db_entry["id"]}, {"$set": db_entry}, upsert=True)
|
||||||
# キャッシュ削除(/api/songs)
|
# キャッシュ削除(/api/songs)
|
||||||
try:
|
try:
|
||||||
app.cache.delete_memoized(route_api_songs)
|
app.cache.delete_memoized(route_api_songs)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ディレクトリを作成
|
base_env = os.getenv("TAIKO_WEB_SONGS_DIR")
|
||||||
target_dir = pathlib.Path(os.getenv("TAIKO_WEB_SONGS_DIR", "public/songs")) / generated_id
|
if base_env:
|
||||||
target_dir.mkdir(parents=True,exist_ok=True)
|
base_dir = pathlib.Path(base_env)
|
||||||
|
else:
|
||||||
|
base_dir = pathlib.Path(__file__).resolve().parent / "public" / "songs"
|
||||||
|
base_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
target_dir = base_dir / generated_id
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# TJAを保存
|
# TJAを保存
|
||||||
(target_dir / "main.tja").write_bytes(tja_data)
|
(target_dir / "main.tja").write_bytes(tja_data)
|
||||||
@@ -875,19 +1046,8 @@ def upload_file():
|
|||||||
return flask.jsonify({'success': True})
|
return flask.jsonify({'success': True})
|
||||||
|
|
||||||
@app.route("/api/delete", methods=["POST"])
|
@app.route("/api/delete", methods=["POST"])
|
||||||
@limiter.limit("1 per day")
|
|
||||||
def delete():
|
def delete():
|
||||||
id = flask.request.get_json().get('id')
|
return flask.jsonify({ "success": False, "reason": "Deletion is disabled" }), 403
|
||||||
client["taiko"]["songs"].delete_one({ "id": id })
|
|
||||||
|
|
||||||
parent_dir = pathlib.Path(os.getenv("TAIKO_WEB_SONGS_DIR", "public/songs"))
|
|
||||||
target_dir = parent_dir / id
|
|
||||||
if not (target_dir.resolve().parents and parent_dir.resolve() in target_dir.resolve().parents):
|
|
||||||
return flask.jsonify({ "success": False, "reason": "PARENT IS NOT ALLOWED" })
|
|
||||||
|
|
||||||
shutil.rmtree(target_dir)
|
|
||||||
|
|
||||||
return "成功しました。"
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import argparse
|
import argparse
|
||||||
|
|||||||
BIN
flask_session/2029240f6d1128be89ddc32729463129
Normal file
BIN
flask_session/2029240f6d1128be89ddc32729463129
Normal file
Binary file not shown.
@@ -6,6 +6,7 @@ var assets = {
|
|||||||
"parseosu.js",
|
"parseosu.js",
|
||||||
"titlescreen.js",
|
"titlescreen.js",
|
||||||
"scoresheet.js",
|
"scoresheet.js",
|
||||||
|
"leaderboard_ui.js",
|
||||||
"songselect.js",
|
"songselect.js",
|
||||||
"keyboard.js",
|
"keyboard.js",
|
||||||
"gameinput.js",
|
"gameinput.js",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class ImportSongs{
|
class ImportSongs{
|
||||||
constructor(...args){
|
constructor(...args){
|
||||||
this.init(...args)
|
this.init(...args)
|
||||||
}
|
}
|
||||||
@@ -322,6 +322,10 @@
|
|||||||
songTitle = songTitle.slice(0, uraPos)
|
songTitle = songTitle.slice(0, uraPos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(id === "cn" && !meta["titlecn"] && meta.titlezh){
|
||||||
|
titleLang.cn = meta.titlezh
|
||||||
|
titleLangAdded = true
|
||||||
|
}
|
||||||
if(meta["title" + id]){
|
if(meta["title" + id]){
|
||||||
titleLang[id] = meta["title" + id]
|
titleLang[id] = meta["title" + id]
|
||||||
titleLangAdded = true
|
titleLangAdded = true
|
||||||
@@ -329,6 +333,10 @@
|
|||||||
titleLang[id] = this.songTitle[songTitle][id] + ura
|
titleLang[id] = this.songTitle[songTitle][id] + ura
|
||||||
titleLangAdded = true
|
titleLangAdded = true
|
||||||
}
|
}
|
||||||
|
if(id === "cn" && !meta["subtitlecn"] && meta.subtitlezh){
|
||||||
|
subtitleLang.cn = meta.subtitlezh
|
||||||
|
subtitleLangAdded = true
|
||||||
|
}
|
||||||
if(meta["subtitle" + id]){
|
if(meta["subtitle" + id]){
|
||||||
subtitleLang[id] = meta["subtitle" + id]
|
subtitleLang[id] = meta["subtitle" + id]
|
||||||
subtitleLangAdded = true
|
subtitleLangAdded = true
|
||||||
|
|||||||
286
public/src/js/leaderboard_ui.js
Normal file
286
public/src/js/leaderboard_ui.js
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
class LeaderboardUI {
|
||||||
|
constructor(songSelect) {
|
||||||
|
this.songSelect = songSelect
|
||||||
|
this.canvasCache = new CanvasCache()
|
||||||
|
this.nameplateCache = new CanvasCache()
|
||||||
|
this.visible = false
|
||||||
|
this.loading = false
|
||||||
|
this.leaderboardData = []
|
||||||
|
this.userRank = null
|
||||||
|
this.scroll = 0
|
||||||
|
this.maxScroll = 0
|
||||||
|
this.songId = null
|
||||||
|
this.difficulty = null
|
||||||
|
this.font = strings.font
|
||||||
|
this.numbersFont = "TnT, Meiryo, sans-serif"
|
||||||
|
|
||||||
|
// UI Constants
|
||||||
|
this.width = 800
|
||||||
|
this.height = 500
|
||||||
|
this.itemHeight = 60
|
||||||
|
this.headerHeight = 80
|
||||||
|
this.padding = 20
|
||||||
|
|
||||||
|
this.closeBtn = {
|
||||||
|
x: 0, y: 0, w: 60, h: 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show(songId, difficulty) {
|
||||||
|
this.visible = true
|
||||||
|
this.loading = true
|
||||||
|
this.songId = songId
|
||||||
|
this.difficulty = difficulty
|
||||||
|
this.leaderboardData = []
|
||||||
|
this.userRank = null
|
||||||
|
this.scroll = 0
|
||||||
|
|
||||||
|
this.fetchData(songId, difficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.visible = false
|
||||||
|
this.songSelect.leaderboardActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData(songId, difficulty) {
|
||||||
|
loader.ajax(gameConfig.basedir + `api/leaderboard/get?song_id=${songId}&difficulty=${difficulty}`).then(response => {
|
||||||
|
const data = JSON.parse(response)
|
||||||
|
if (data.status === 'ok') {
|
||||||
|
this.leaderboardData = data.leaderboard
|
||||||
|
this.userRank = data.userRank
|
||||||
|
this.loading = false
|
||||||
|
|
||||||
|
// Calculate max scroll
|
||||||
|
const totalHeight = this.leaderboardData.length * this.itemHeight
|
||||||
|
const viewHeight = this.height - this.headerHeight - this.padding * 2
|
||||||
|
this.maxScroll = Math.max(0, totalHeight - viewHeight)
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
console.error("Leaderboard fetch error:", e)
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx, winW, winH, pixelRatio) {
|
||||||
|
if (!this.visible) return
|
||||||
|
|
||||||
|
const x = (winW - this.width) / 2
|
||||||
|
const y = (winH - this.height) / 2
|
||||||
|
|
||||||
|
// Draw Overlay
|
||||||
|
ctx.fillStyle = "rgba(0, 0, 0, 0.7)"
|
||||||
|
ctx.fillRect(0, 0, winW, winH)
|
||||||
|
|
||||||
|
// Draw Window
|
||||||
|
this.songSelect.draw.roundedRect({
|
||||||
|
ctx: ctx,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
w: this.width,
|
||||||
|
h: this.height,
|
||||||
|
radius: 20
|
||||||
|
})
|
||||||
|
ctx.fillStyle = "#fff"
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Header
|
||||||
|
ctx.fillStyle = "#ff6600"
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x + 20, y)
|
||||||
|
ctx.lineTo(x + this.width - 20, y)
|
||||||
|
ctx.quadraticCurveTo(x + this.width, y, x + this.width, y + 20)
|
||||||
|
ctx.lineTo(x + this.width, y + this.headerHeight)
|
||||||
|
ctx.lineTo(x, y + this.headerHeight)
|
||||||
|
ctx.lineTo(x, y + 20)
|
||||||
|
ctx.quadraticCurveTo(x, y, x + 20, y)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Title
|
||||||
|
const diffText = strings[this.difficulty === "ura" ? "oni" : this.difficulty]
|
||||||
|
const titleText = strings.leaderboardTitle.replace("%s", diffText)
|
||||||
|
|
||||||
|
this.songSelect.draw.layeredText({
|
||||||
|
ctx: ctx,
|
||||||
|
text: titleText,
|
||||||
|
fontSize: 40,
|
||||||
|
fontFamily: this.font,
|
||||||
|
x: x + this.width / 2,
|
||||||
|
y: y + 54,
|
||||||
|
align: "center",
|
||||||
|
width: this.width - 100
|
||||||
|
}, [
|
||||||
|
{ outline: "#fff", letterBorder: 2 },
|
||||||
|
{ fill: "#000" }
|
||||||
|
])
|
||||||
|
|
||||||
|
// Close Button
|
||||||
|
this.closeBtn.x = x + this.width - 50
|
||||||
|
this.closeBtn.y = y - 10
|
||||||
|
ctx.fillStyle = "#ff0000"
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(this.closeBtn.x, this.closeBtn.y, 20, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.strokeStyle = "#fff"
|
||||||
|
ctx.lineWidth = 3
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.fillStyle = "#fff"
|
||||||
|
ctx.font = "bold 24px Arial"
|
||||||
|
ctx.textAlign = "center"
|
||||||
|
ctx.textBaseline = "middle"
|
||||||
|
ctx.fillText("X", this.closeBtn.x, this.closeBtn.y)
|
||||||
|
|
||||||
|
// Content Area
|
||||||
|
const contentX = x + this.padding
|
||||||
|
const contentY = y + this.headerHeight + this.padding
|
||||||
|
const contentW = this.width - this.padding * 2
|
||||||
|
const contentH = this.height - this.headerHeight - this.padding * 2
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.rect(contentX, contentY, contentW, contentH)
|
||||||
|
ctx.clip()
|
||||||
|
|
||||||
|
if (this.loading) {
|
||||||
|
ctx.fillStyle = "#000"
|
||||||
|
ctx.font = `30px ${this.font}`
|
||||||
|
ctx.textAlign = "center"
|
||||||
|
ctx.fillText(strings.loadingLeaderboard, x + this.width / 2, y + this.height / 2)
|
||||||
|
} else if (this.leaderboardData.length === 0) {
|
||||||
|
ctx.fillStyle = "#666"
|
||||||
|
ctx.font = `30px ${this.font}`
|
||||||
|
ctx.textAlign = "center"
|
||||||
|
ctx.fillText(strings.noScores, x + this.width / 2, y + this.height / 2)
|
||||||
|
} else {
|
||||||
|
// Draw List
|
||||||
|
let currentY = contentY - this.scroll
|
||||||
|
|
||||||
|
// Header Row
|
||||||
|
/*
|
||||||
|
ctx.fillStyle = "#eee"
|
||||||
|
ctx.fillRect(contentX, contentY, contentW, 40)
|
||||||
|
ctx.fillStyle = "#000"
|
||||||
|
ctx.font = `bold 20px ${this.font}`
|
||||||
|
ctx.textAlign = "left"
|
||||||
|
ctx.fillText(strings.rank, contentX + 20, contentY + 28)
|
||||||
|
ctx.fillText(strings.playerName, contentX + 100, contentY + 28)
|
||||||
|
ctx.textAlign = "right"
|
||||||
|
ctx.fillText(strings.score, contentX + contentW - 20, contentY + 28)
|
||||||
|
currentY += 40
|
||||||
|
*/
|
||||||
|
|
||||||
|
this.leaderboardData.forEach((entry, index) => {
|
||||||
|
if (currentY + this.itemHeight < contentY) {
|
||||||
|
currentY += this.itemHeight
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (currentY > contentY + contentH) return
|
||||||
|
|
||||||
|
const isUser = this.userRank && this.userRank.entry.username === entry.username
|
||||||
|
|
||||||
|
// Background
|
||||||
|
if (index % 2 === 0) {
|
||||||
|
ctx.fillStyle = isUser ? "#fff8e1" : "#f9f9f9"
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = isUser ? "#fff3cd" : "#fff"
|
||||||
|
}
|
||||||
|
ctx.fillRect(contentX, currentY, contentW, this.itemHeight)
|
||||||
|
|
||||||
|
// Rank
|
||||||
|
const rank = index + 1
|
||||||
|
let rankColor = "#000"
|
||||||
|
if (rank === 1) rankColor = "#ffd700"
|
||||||
|
else if (rank === 2) rankColor = "#c0c0c0"
|
||||||
|
else if (rank === 3) rankColor = "#cd7f32"
|
||||||
|
|
||||||
|
if (rank <= 3) {
|
||||||
|
ctx.fillStyle = rankColor
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(contentX + 40, currentY + this.itemHeight / 2, 18, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.fillStyle = "#fff"
|
||||||
|
ctx.font = "bold 20px Arial"
|
||||||
|
ctx.textAlign = "center"
|
||||||
|
ctx.textBaseline = "middle"
|
||||||
|
ctx.fillText(rank, contentX + 40, currentY + this.itemHeight / 2)
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = "#000"
|
||||||
|
ctx.font = "20px Arial"
|
||||||
|
ctx.textAlign = "center"
|
||||||
|
ctx.textBaseline = "middle"
|
||||||
|
ctx.fillText(rank, contentX + 40, currentY + this.itemHeight / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name
|
||||||
|
ctx.fillStyle = "#000"
|
||||||
|
ctx.font = `24px ${this.font}`
|
||||||
|
ctx.textAlign = "left"
|
||||||
|
ctx.fillText(entry.display_name, contentX + 90, currentY + this.itemHeight / 2)
|
||||||
|
|
||||||
|
// Score
|
||||||
|
ctx.font = `bold 28px ${this.numbersFont}`
|
||||||
|
ctx.textAlign = "right"
|
||||||
|
ctx.fillStyle = "#ff4500"
|
||||||
|
ctx.fillText(entry.score.toLocaleString(), contentX + contentW - 20, currentY + this.itemHeight / 2 + 2)
|
||||||
|
|
||||||
|
// Max Combo (small)
|
||||||
|
ctx.font = `16px ${this.font}`
|
||||||
|
ctx.fillStyle = "#666"
|
||||||
|
ctx.fillText(`${strings.maxCombo}: ${entry.maxCombo}`, contentX + contentW - 180, currentY + this.itemHeight / 2 + 5)
|
||||||
|
|
||||||
|
currentY += this.itemHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
|
||||||
|
// Draw User Rank at Bottom if exists
|
||||||
|
if (this.userRank) {
|
||||||
|
const footerY = y + this.height - 50
|
||||||
|
ctx.fillStyle = "#333"
|
||||||
|
ctx.fillRect(x, footerY, this.width, 50)
|
||||||
|
|
||||||
|
ctx.fillStyle = "#fff"
|
||||||
|
ctx.font = `20px ${this.font}`
|
||||||
|
ctx.textAlign = "left"
|
||||||
|
ctx.textBaseline = "middle"
|
||||||
|
|
||||||
|
const rankText = strings.yourRank.replace("%s", this.userRank.rank)
|
||||||
|
ctx.fillText(rankText, x + 20, footerY + 25)
|
||||||
|
|
||||||
|
ctx.textAlign = "right"
|
||||||
|
ctx.font = `bold 24px ${this.numbersFont}`
|
||||||
|
ctx.fillText(this.userRank.entry.score.toLocaleString(), x + this.width - 20, footerY + 25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseMove(x, y) {
|
||||||
|
// Handle hover if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseDown(x, y) {
|
||||||
|
if (!this.visible) return false
|
||||||
|
|
||||||
|
// Check Close Button
|
||||||
|
const dx = x - this.closeBtn.x
|
||||||
|
const dy = y - this.closeBtn.y
|
||||||
|
if (dx * dx + dy * dy < 20 * 20) {
|
||||||
|
this.hide()
|
||||||
|
assets.sounds["se_cancel"].play()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check window click (consume event)
|
||||||
|
// Adjust coordinates
|
||||||
|
const winX = (innerWidth * (window.devicePixelRatio || 1) - this.width) / 2
|
||||||
|
/* Note: simple check logic here, might need more precise logic mapping similar to mouseWheel */
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel(delta) {
|
||||||
|
if (!this.visible) return
|
||||||
|
|
||||||
|
this.scroll += delta * 50
|
||||||
|
this.scroll = Math.max(0, Math.min(this.scroll, this.maxScroll))
|
||||||
|
}
|
||||||
|
}
|
||||||
26
public/src/js/loader-worker.js
Normal file
26
public/src/js/loader-worker.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
self.addEventListener('message', async e => {
|
||||||
|
const { id, url, type } = e.data
|
||||||
|
try{
|
||||||
|
const response = await fetch(url)
|
||||||
|
if(!response.ok){
|
||||||
|
throw new Error(response.status + " " + response.statusText)
|
||||||
|
}
|
||||||
|
let data
|
||||||
|
if(type === "arraybuffer"){
|
||||||
|
data = await response.arrayBuffer()
|
||||||
|
}else if(type === "blob"){
|
||||||
|
data = await response.blob()
|
||||||
|
}else{
|
||||||
|
data = await response.text()
|
||||||
|
}
|
||||||
|
self.postMessage({
|
||||||
|
id: id,
|
||||||
|
data: data
|
||||||
|
}, type === "arraybuffer" ? [data] : undefined)
|
||||||
|
}catch(e){
|
||||||
|
self.postMessage({
|
||||||
|
id: id,
|
||||||
|
error: e.toString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -11,6 +11,8 @@ class Loader{
|
|||||||
this.errorMessages = []
|
this.errorMessages = []
|
||||||
this.songSearchGradient = "linear-gradient(to top, rgba(245, 246, 252, 0.08), #ff5963), "
|
this.songSearchGradient = "linear-gradient(to top, rgba(245, 246, 252, 0.08), #ff5963), "
|
||||||
|
|
||||||
|
this.initWorkers()
|
||||||
|
|
||||||
var promises = []
|
var promises = []
|
||||||
|
|
||||||
promises.push(this.ajax("src/views/loader.html").then(page => {
|
promises.push(this.ajax("src/views/loader.html").then(page => {
|
||||||
@@ -23,6 +25,78 @@ class Loader{
|
|||||||
|
|
||||||
Promise.all(promises).then(this.run.bind(this))
|
Promise.all(promises).then(this.run.bind(this))
|
||||||
}
|
}
|
||||||
|
initWorkers(){
|
||||||
|
this.workers = []
|
||||||
|
this.workerQueue = []
|
||||||
|
this.workerCallbacks = {}
|
||||||
|
this.workerId = 0
|
||||||
|
var concurrency = navigator.hardwareConcurrency || 4
|
||||||
|
for(var i = 0; i < concurrency; i++){
|
||||||
|
var worker = new Worker("src/js/loader-worker.js")
|
||||||
|
worker.onmessage = this.onWorkerMessage.bind(this)
|
||||||
|
this.workers.push({
|
||||||
|
worker: worker,
|
||||||
|
active: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onWorkerMessage(e){
|
||||||
|
var data = e.data
|
||||||
|
var callback = this.workerCallbacks[data.id]
|
||||||
|
if(callback){
|
||||||
|
delete this.workerCallbacks[data.id]
|
||||||
|
if(data.error){
|
||||||
|
callback.reject(data.error)
|
||||||
|
}else{
|
||||||
|
callback.resolve(data.data)
|
||||||
|
}
|
||||||
|
this.workerFree(callback.workerIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workerFetch(url, type){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
var id = ++this.workerId
|
||||||
|
this.workerQueue.push({
|
||||||
|
id: id,
|
||||||
|
url: new URL(url, location.href).href,
|
||||||
|
type: type,
|
||||||
|
resolve: resolve,
|
||||||
|
reject: reject
|
||||||
|
})
|
||||||
|
this.workerRun()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
workerRun(){
|
||||||
|
if(this.workerQueue.length === 0){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var workerIndex = -1
|
||||||
|
var minActive = Infinity
|
||||||
|
for(var i = 0; i < this.workers.length; i++){
|
||||||
|
if(this.workers[i].active < minActive){
|
||||||
|
minActive = this.workers[i].active
|
||||||
|
workerIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(workerIndex !== -1){
|
||||||
|
var task = this.workerQueue.shift()
|
||||||
|
var workerObj = this.workers[workerIndex]
|
||||||
|
workerObj.active++
|
||||||
|
this.workerCallbacks[task.id] = task
|
||||||
|
task.workerIndex = workerIndex
|
||||||
|
workerObj.worker.postMessage({
|
||||||
|
id: task.id,
|
||||||
|
url: task.url,
|
||||||
|
type: task.type
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workerFree(index){
|
||||||
|
if(this.workers[index]){
|
||||||
|
this.workers[index].active--
|
||||||
|
this.workerRun()
|
||||||
|
}
|
||||||
|
}
|
||||||
run(){
|
run(){
|
||||||
this.promises = []
|
this.promises = []
|
||||||
this.loaderDiv = document.querySelector("#loader")
|
this.loaderDiv = document.querySelector("#loader")
|
||||||
@@ -538,6 +612,17 @@ class Loader{
|
|||||||
return css.join("\n")
|
return css.join("\n")
|
||||||
}
|
}
|
||||||
ajax(url, customRequest, customResponse){
|
ajax(url, customRequest, customResponse){
|
||||||
|
if(!customResponse && (url.startsWith("src/") || url.startsWith("assets/") || url.indexOf("img/") !== -1 || url.indexOf("audio/") !== -1 || url.indexOf("fonts/") !== -1 || url.indexOf("views/") !== -1)){
|
||||||
|
var type = "text"
|
||||||
|
if(customRequest){
|
||||||
|
var reqStub = {}
|
||||||
|
customRequest(reqStub)
|
||||||
|
if(reqStub.responseType){
|
||||||
|
type = reqStub.responseType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.workerFetch(url, type)
|
||||||
|
}
|
||||||
var request = new XMLHttpRequest()
|
var request = new XMLHttpRequest()
|
||||||
request.open("GET", url)
|
request.open("GET", url)
|
||||||
var promise = pageEvents.load(request)
|
var promise = pageEvents.load(request)
|
||||||
@@ -557,12 +642,13 @@ class Loader{
|
|||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
loadScript(url){
|
loadScript(url){
|
||||||
var script = document.createElement("script")
|
|
||||||
var url = url + this.queryString
|
var url = url + this.queryString
|
||||||
var promise = pageEvents.load(script)
|
return this.workerFetch(url, "text").then(code => {
|
||||||
script.src = url
|
var script = document.createElement("script")
|
||||||
document.head.appendChild(script)
|
code += "\n//# sourceURL=" + url
|
||||||
return promise
|
script.text = code
|
||||||
|
document.head.appendChild(script)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
getCsrfToken(){
|
getCsrfToken(){
|
||||||
return this.ajax("api/csrftoken").then(response => {
|
return this.ajax("api/csrftoken").then(response => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
class Scoresheet{
|
class Scoresheet {
|
||||||
constructor(...args){
|
constructor(...args) {
|
||||||
this.init(...args)
|
this.init(...args)
|
||||||
}
|
}
|
||||||
init(controller, results, multiplayer, touchEnabled){
|
init(controller, results, multiplayer, touchEnabled) {
|
||||||
this.controller = controller
|
this.controller = controller
|
||||||
this.resultsObj = results
|
this.resultsObj = results
|
||||||
this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0]
|
this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0]
|
||||||
@@ -11,12 +11,12 @@ class Scoresheet{
|
|||||||
this.results[player0] = {}
|
this.results[player0] = {}
|
||||||
this.rules = []
|
this.rules = []
|
||||||
this.rules[player0] = this.controller.game.rules
|
this.rules[player0] = this.controller.game.rules
|
||||||
if(multiplayer){
|
if (multiplayer) {
|
||||||
this.player.push(p2.player === 2 ? 0 : 1)
|
this.player.push(p2.player === 2 ? 0 : 1)
|
||||||
this.results[this.player[1]] = p2.results
|
this.results[this.player[1]] = p2.results
|
||||||
this.rules[this.player[1]] = this.controller.syncWith.game.rules
|
this.rules[this.player[1]] = this.controller.syncWith.game.rules
|
||||||
}
|
}
|
||||||
for(var i in results){
|
for (var i in results) {
|
||||||
this.results[player0][i] = results[i] === null ? null : results[i].toString()
|
this.results[player0][i] = results[i] === null ? null : results[i].toString()
|
||||||
}
|
}
|
||||||
this.multiplayer = multiplayer
|
this.multiplayer = multiplayer
|
||||||
@@ -26,10 +26,10 @@ class Scoresheet{
|
|||||||
this.ctx = this.canvas.getContext("2d")
|
this.ctx = this.canvas.getContext("2d")
|
||||||
var resolution = settings.getItem("resolution")
|
var resolution = settings.getItem("resolution")
|
||||||
var noSmoothing = resolution === "low" || resolution === "lowest"
|
var noSmoothing = resolution === "low" || resolution === "lowest"
|
||||||
if(noSmoothing){
|
if (noSmoothing) {
|
||||||
this.ctx.imageSmoothingEnabled = false
|
this.ctx.imageSmoothingEnabled = false
|
||||||
}
|
}
|
||||||
if(resolution === "lowest"){
|
if (resolution === "lowest") {
|
||||||
this.canvas.style.imageRendering = "pixelated"
|
this.canvas.style.imageRendering = "pixelated"
|
||||||
}
|
}
|
||||||
this.game = document.getElementById("game")
|
this.game = document.getElementById("game")
|
||||||
@@ -78,12 +78,12 @@ class Scoresheet{
|
|||||||
assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689)
|
assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689)
|
||||||
|
|
||||||
this.session = p2.session
|
this.session = p2.session
|
||||||
if(this.session){
|
if (this.session) {
|
||||||
if(p2.getMessage("songsel")){
|
if (p2.getMessage("songsel")) {
|
||||||
this.toSongsel(true)
|
this.toSongsel(true)
|
||||||
}
|
}
|
||||||
pageEvents.add(p2, "message", response => {
|
pageEvents.add(p2, "message", response => {
|
||||||
if(response.type === "songsel"){
|
if (response.type === "songsel") {
|
||||||
this.toSongsel(true)
|
this.toSongsel(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -100,51 +100,51 @@ class Scoresheet{
|
|||||||
touchEvents: controller.view.touchEvents
|
touchEvents: controller.view.touchEvents
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
keyDown(pressed){
|
keyDown(pressed) {
|
||||||
if(pressed && this.redrawing){
|
if (pressed && this.redrawing) {
|
||||||
this.toNext()
|
this.toNext()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mouseDown(event){
|
mouseDown(event) {
|
||||||
if(event.type === "touchstart"){
|
if (event.type === "touchstart") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.canvas.style.cursor = ""
|
this.canvas.style.cursor = ""
|
||||||
this.state.pointerLocked = true
|
this.state.pointerLocked = true
|
||||||
}else{
|
} else {
|
||||||
this.state.pointerLocked = false
|
this.state.pointerLocked = false
|
||||||
if(event.which !== 1){
|
if (event.which !== 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.toNext()
|
this.toNext()
|
||||||
}
|
}
|
||||||
toNext(){
|
toNext() {
|
||||||
var elapsed = this.getMS() - this.state.screenMS
|
var elapsed = this.getMS() - this.state.screenMS
|
||||||
if(this.state.screen === "fadeIn" && elapsed >= this.state.startDelay){
|
if (this.state.screen === "fadeIn" && elapsed >= this.state.startDelay) {
|
||||||
this.toScoresShown()
|
this.toScoresShown()
|
||||||
}else if(this.state.screen === "scoresShown" && elapsed >= 1000){
|
} else if (this.state.screen === "scoresShown" && elapsed >= 1000) {
|
||||||
this.toSongsel()
|
this.toSongsel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toScoresShown(){
|
toScoresShown() {
|
||||||
if(!p2.session){
|
if (!p2.session) {
|
||||||
this.state.screen = "scoresShown"
|
this.state.screen = "scoresShown"
|
||||||
this.state.screenMS = this.getMS()
|
this.state.screenMS = this.getMS()
|
||||||
this.controller.playSound("neiro_1_don", 0, true)
|
this.controller.playSound("neiro_1_don", 0, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toSongsel(fromP2){
|
toSongsel(fromP2) {
|
||||||
if(!p2.session || fromP2){
|
if (!p2.session || fromP2) {
|
||||||
snd.musicGain.fadeOut(0.5)
|
snd.musicGain.fadeOut(0.5)
|
||||||
this.state.screen = "fadeOut"
|
this.state.screen = "fadeOut"
|
||||||
this.state.screenMS = this.getMS()
|
this.state.screenMS = this.getMS()
|
||||||
if(!fromP2){
|
if (!fromP2) {
|
||||||
this.controller.playSound("neiro_1_don", 0, true)
|
this.controller.playSound("neiro_1_don", 0, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startRedraw(){
|
startRedraw() {
|
||||||
this.redrawing = true
|
this.redrawing = true
|
||||||
requestAnimationFrame(this.redrawBind)
|
requestAnimationFrame(this.redrawBind)
|
||||||
this.winW = null
|
this.winW = null
|
||||||
@@ -152,7 +152,7 @@ class Scoresheet{
|
|||||||
|
|
||||||
pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.mouseDown.bind(this))
|
pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.mouseDown.bind(this))
|
||||||
|
|
||||||
if(!this.multiplayer){
|
if (!this.multiplayer) {
|
||||||
this.tetsuoHana = document.createElement("div")
|
this.tetsuoHana = document.createElement("div")
|
||||||
this.tetsuoHana.id = "tetsuohana"
|
this.tetsuoHana.id = "tetsuohana"
|
||||||
var flowersBg = "url('" + assets.image["results_flowers"].src + "')"
|
var flowersBg = "url('" + assets.image["results_flowers"].src + "')"
|
||||||
@@ -160,12 +160,12 @@ class Scoresheet{
|
|||||||
var tetsuoHanaBg = "url('" + assets.image["results_tetsuohana" + (debugObj.state === "closed" ? "" : "2")].src + "')"
|
var tetsuoHanaBg = "url('" + assets.image["results_tetsuohana" + (debugObj.state === "closed" ? "" : "2")].src + "')"
|
||||||
var id = ["flowers1", "flowers2", "mikoshi", "tetsuo", "hana"]
|
var id = ["flowers1", "flowers2", "mikoshi", "tetsuo", "hana"]
|
||||||
var bg = [flowersBg, flowersBg, mikoshiBg, tetsuoHanaBg, tetsuoHanaBg]
|
var bg = [flowersBg, flowersBg, mikoshiBg, tetsuoHanaBg, tetsuoHanaBg]
|
||||||
for(var i = 0; i < id.length; i++){
|
for (var i = 0; i < id.length; i++) {
|
||||||
if(id[i] === "mikoshi"){
|
if (id[i] === "mikoshi") {
|
||||||
var divOut = document.createElement("div")
|
var divOut = document.createElement("div")
|
||||||
divOut.id = id[i] + "-out"
|
divOut.id = id[i] + "-out"
|
||||||
this.tetsuoHana.appendChild(divOut)
|
this.tetsuoHana.appendChild(divOut)
|
||||||
}else{
|
} else {
|
||||||
var divOut = this.tetsuoHana
|
var divOut = this.tetsuoHana
|
||||||
}
|
}
|
||||||
var div = document.createElement("div")
|
var div = document.createElement("div")
|
||||||
@@ -180,16 +180,16 @@ class Scoresheet{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redraw(){
|
redraw() {
|
||||||
if(!this.redrawRunning){
|
if (!this.redrawRunning) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if(this.redrawing){
|
if (this.redrawing) {
|
||||||
requestAnimationFrame(this.redrawBind)
|
requestAnimationFrame(this.redrawBind)
|
||||||
}
|
}
|
||||||
var ms = this.getMS()
|
var ms = this.getMS()
|
||||||
|
|
||||||
if(!this.redrawRunning){
|
if (!this.redrawRunning) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,11 +200,11 @@ class Scoresheet{
|
|||||||
var winH = lastHeight
|
var winH = lastHeight
|
||||||
this.pixelRatio = window.devicePixelRatio || 1
|
this.pixelRatio = window.devicePixelRatio || 1
|
||||||
var resolution = settings.getItem("resolution")
|
var resolution = settings.getItem("resolution")
|
||||||
if(resolution === "medium"){
|
if (resolution === "medium") {
|
||||||
this.pixelRatio *= 0.75
|
this.pixelRatio *= 0.75
|
||||||
}else if(resolution === "low"){
|
} else if (resolution === "low") {
|
||||||
this.pixelRatio *= 0.5
|
this.pixelRatio *= 0.5
|
||||||
}else if(resolution === "lowest"){
|
} else if (resolution === "lowest") {
|
||||||
this.pixelRatio *= 0.25
|
this.pixelRatio *= 0.25
|
||||||
}
|
}
|
||||||
winW *= this.pixelRatio
|
winW *= this.pixelRatio
|
||||||
@@ -213,8 +213,8 @@ class Scoresheet{
|
|||||||
var ratioY = winH / 720
|
var ratioY = winH / 720
|
||||||
var ratio = (ratioX < ratioY ? ratioX : ratioY)
|
var ratio = (ratioX < ratioY ? ratioX : ratioY)
|
||||||
|
|
||||||
if(this.redrawing){
|
if (this.redrawing) {
|
||||||
if(this.winW !== winW || this.winH !== winH){
|
if (this.winW !== winW || this.winH !== winH) {
|
||||||
this.canvas.width = Math.max(1, winW)
|
this.canvas.width = Math.max(1, winW)
|
||||||
this.canvas.height = Math.max(1, winH)
|
this.canvas.height = Math.max(1, winH)
|
||||||
ctx.scale(ratio, ratio)
|
ctx.scale(ratio, ratio)
|
||||||
@@ -224,37 +224,37 @@ class Scoresheet{
|
|||||||
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
|
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
|
||||||
this.nameplateCache.resize(274, 134, ratio + 0.2)
|
this.nameplateCache.resize(274, 134, ratio + 0.2)
|
||||||
|
|
||||||
if(!this.multiplayer){
|
if (!this.multiplayer) {
|
||||||
this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio)
|
this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio)
|
||||||
if(this.tetsuoHanaClass === "dance"){
|
if (this.tetsuoHanaClass === "dance") {
|
||||||
this.tetsuoHana.classList.remove("dance", "dance2")
|
this.tetsuoHana.classList.remove("dance", "dance2")
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
this.tetsuoHana.classList.add("dance2")
|
this.tetsuoHana.classList.add("dance2")
|
||||||
},50)
|
}, 50)
|
||||||
}else if(this.tetsuoHanaClass === "failed"){
|
} else if (this.tetsuoHanaClass === "failed") {
|
||||||
this.tetsuoHana.classList.remove("failed")
|
this.tetsuoHana.classList.remove("failed")
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
this.tetsuoHana.classList.add("failed")
|
this.tetsuoHana.classList.add("failed")
|
||||||
},50)
|
}, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else if(!document.hasFocus() && this.state.screen === "scoresShown"){
|
} else if (!document.hasFocus() && this.state.screen === "scoresShown") {
|
||||||
if(this.state["countup0"]){
|
if (this.state["countup0"]) {
|
||||||
this.stopSound("se_results_countup", 0)
|
this.stopSound("se_results_countup", 0)
|
||||||
}
|
}
|
||||||
if(this.state["countup1"]){
|
if (this.state["countup1"]) {
|
||||||
this.stopSound("se_results_countup", 1)
|
this.stopSound("se_results_countup", 1)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}else{
|
} else {
|
||||||
ctx.clearRect(0, 0, winW / ratio, winH / ratio)
|
ctx.clearRect(0, 0, winW / ratio, winH / ratio)
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
ctx.scale(ratio, ratio)
|
ctx.scale(ratio, ratio)
|
||||||
if(!this.canvasCache.canvas){
|
if (!this.canvasCache.canvas) {
|
||||||
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
|
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
|
||||||
}
|
}
|
||||||
if(!this.nameplateCache.canvas){
|
if (!this.nameplateCache.canvas) {
|
||||||
this.nameplateCache.resize(274, 67, ratio + 0.2)
|
this.nameplateCache.resize(274, 67, ratio + 0.2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,14 +272,14 @@ class Scoresheet{
|
|||||||
|
|
||||||
var bgOffset = 0
|
var bgOffset = 0
|
||||||
var elapsed = ms - this.state.screenMS
|
var elapsed = ms - this.state.screenMS
|
||||||
if(this.state.screen === "fadeIn" && elapsed < 1000){
|
if (this.state.screen === "fadeIn" && elapsed < 1000) {
|
||||||
bgOffset = Math.min(1, this.draw.easeIn(1 - elapsed / 1000)) * (winH / 2)
|
bgOffset = Math.min(1, this.draw.easeIn(1 - elapsed / 1000)) * (winH / 2)
|
||||||
}
|
}
|
||||||
if((this.state.screen !== "fadeIn" || elapsed >= 1000) && !this.scoreSaved){
|
if ((this.state.screen !== "fadeIn" || elapsed >= 1000) && !this.scoreSaved) {
|
||||||
this.saveScore()
|
this.saveScore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(bgOffset){
|
if (bgOffset) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(0, -bgOffset)
|
ctx.translate(0, -bgOffset)
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ class Scoresheet{
|
|||||||
ctx.fillRect(0, winH / 2 - 12, winW, 12)
|
ctx.fillRect(0, winH / 2 - 12, winW, 12)
|
||||||
ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
|
ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
|
||||||
ctx.fillRect(0, winH / 2, winW, 20)
|
ctx.fillRect(0, winH / 2, winW, 20)
|
||||||
if(bgOffset !== 0){
|
if (bgOffset !== 0) {
|
||||||
ctx.fillStyle = "#000"
|
ctx.fillStyle = "#000"
|
||||||
ctx.fillRect(0, winH / 2 - 2, winW, 2)
|
ctx.fillRect(0, winH / 2 - 2, winW, 2)
|
||||||
}
|
}
|
||||||
@@ -306,7 +306,7 @@ class Scoresheet{
|
|||||||
ctx.fillStyle = "#bf2900"
|
ctx.fillStyle = "#bf2900"
|
||||||
ctx.fillRect(0, frameTop + 64, winW, 8)
|
ctx.fillRect(0, frameTop + 64, winW, 8)
|
||||||
|
|
||||||
if(bgOffset){
|
if (bgOffset) {
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(0, bgOffset)
|
ctx.translate(0, bgOffset)
|
||||||
@@ -325,9 +325,9 @@ class Scoresheet{
|
|||||||
ctx.fillStyle = this.multiplayer ? "rgba(138, 245, 247, 0.5)" : "rgba(249, 163, 149, 0.5)"
|
ctx.fillStyle = this.multiplayer ? "rgba(138, 245, 247, 0.5)" : "rgba(249, 163, 149, 0.5)"
|
||||||
ctx.fillRect(0, winH / 2, winW, 12)
|
ctx.fillRect(0, winH / 2, winW, 12)
|
||||||
ctx.fillStyle = "#000"
|
ctx.fillStyle = "#000"
|
||||||
if(bgOffset === 0){
|
if (bgOffset === 0) {
|
||||||
ctx.fillRect(0, winH / 2 - 2, winW, 4)
|
ctx.fillRect(0, winH / 2 - 2, winW, 4)
|
||||||
}else{
|
} else {
|
||||||
ctx.fillRect(0, winH / 2, winW, 2)
|
ctx.fillRect(0, winH / 2, winW, 2)
|
||||||
}
|
}
|
||||||
ctx.fillStyle = this.multiplayer ? "#6bbec0" : "#fa4529"
|
ctx.fillStyle = this.multiplayer ? "#6bbec0" : "#fa4529"
|
||||||
@@ -337,46 +337,46 @@ class Scoresheet{
|
|||||||
ctx.fillStyle = this.multiplayer ? "#a8e0e0" : "#ff9b7a"
|
ctx.fillStyle = this.multiplayer ? "#a8e0e0" : "#ff9b7a"
|
||||||
ctx.fillRect(0, winH - frameTop - 66, winW, 2)
|
ctx.fillRect(0, winH - frameTop - 66, winW, 2)
|
||||||
|
|
||||||
if(bgOffset){
|
if (bgOffset) {
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.screen === "scoresShown" || this.state.screen === "fadeOut"){
|
if (this.state.screen === "scoresShown" || this.state.screen === "fadeOut") {
|
||||||
var elapsed = Infinity
|
var elapsed = Infinity
|
||||||
}else if(this.redrawing){
|
} else if (this.redrawing) {
|
||||||
var elapsed = ms - this.state.screenMS - this.state.startDelay
|
var elapsed = ms - this.state.screenMS - this.state.startDelay
|
||||||
}else{
|
} else {
|
||||||
var elapsed = 0
|
var elapsed = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var rules = this.controller.game.rules
|
var rules = this.controller.game.rules
|
||||||
var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000
|
var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000
|
||||||
if(players === 2 && failedOffset !== 0){
|
if (players === 2 && failedOffset !== 0) {
|
||||||
var p2results = this.results[this.player[1]]
|
var p2results = this.results[this.player[1]]
|
||||||
if(p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)){
|
if (p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)) {
|
||||||
failedOffset = 0
|
failedOffset = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(elapsed >= 3100 + failedOffset){
|
if (elapsed >= 3100 + failedOffset) {
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var clear = this.rules[p].clearReached(results.gauge)
|
var clear = this.rules[p].clearReached(results.gauge)
|
||||||
if(p === 1 || !this.multiplayer && clear){
|
if (p === 1 || !this.multiplayer && clear) {
|
||||||
ctx.translate(0, 290)
|
ctx.translate(0, 290)
|
||||||
}
|
}
|
||||||
if(clear){
|
if (clear) {
|
||||||
ctx.globalCompositeOperation = "lighter"
|
ctx.globalCompositeOperation = "lighter"
|
||||||
}
|
}
|
||||||
ctx.globalAlpha = Math.min(1, Math.max(0, (elapsed - (3100 + failedOffset)) / 500)) * 0.5
|
ctx.globalAlpha = Math.min(1, Math.max(0, (elapsed - (3100 + failedOffset)) / 500)) * 0.5
|
||||||
var grd = ctx.createLinearGradient(0, frameTop + 72, 0, frameTop + 368)
|
var grd = ctx.createLinearGradient(0, frameTop + 72, 0, frameTop + 368)
|
||||||
grd.addColorStop(0, "#000")
|
grd.addColorStop(0, "#000")
|
||||||
if(clear){
|
if (clear) {
|
||||||
grd.addColorStop(1, "#ffffba")
|
grd.addColorStop(1, "#ffffba")
|
||||||
}else{
|
} else {
|
||||||
grd.addColorStop(1, "transparent")
|
grd.addColorStop(1, "transparent")
|
||||||
}
|
}
|
||||||
ctx.fillStyle = grd
|
ctx.fillStyle = grd
|
||||||
@@ -385,10 +385,10 @@ class Scoresheet{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(elapsed >= 0){
|
if (elapsed >= 0) {
|
||||||
if(this.state.hasPointer === 0){
|
if (this.state.hasPointer === 0) {
|
||||||
this.state.hasPointer = 1
|
this.state.hasPointer = 1
|
||||||
if(!this.state.pointerLocked){
|
if (!this.state.pointerLocked) {
|
||||||
this.canvas.style.cursor = this.session ? "" : "pointer"
|
this.canvas.style.cursor = this.session ? "" : "pointer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,13 +416,13 @@ class Scoresheet{
|
|||||||
letterSpacing: strings.id === "en" ? 0 : 3,
|
letterSpacing: strings.id === "en" ? 0 : 3,
|
||||||
forceShadow: true
|
forceShadow: true
|
||||||
}, [
|
}, [
|
||||||
{x: -2, y: -2, outline: "#000", letterBorder: 22},
|
{ x: -2, y: -2, outline: "#000", letterBorder: 22 },
|
||||||
{},
|
{},
|
||||||
{x: 2, y: 2, shadow: [2, 2, 7]},
|
{ x: 2, y: 2, shadow: [2, 2, 7] },
|
||||||
{x: 2, y: 2, outline: "#ad1516", letterBorder: 10},
|
{ x: 2, y: 2, outline: "#ad1516", letterBorder: 10 },
|
||||||
{x: -2, y: -2, outline: "#ff797b"},
|
{ x: -2, y: -2, outline: "#ff797b" },
|
||||||
{outline: "#f70808"},
|
{ outline: "#f70808" },
|
||||||
{fill: "#fff", shadow: [-1, 1, 3, 1.5]}
|
{ fill: "#fff", shadow: [-1, 1, 3, 1.5] }
|
||||||
])
|
])
|
||||||
|
|
||||||
this.draw.layeredText({
|
this.draw.layeredText({
|
||||||
@@ -436,18 +436,18 @@ class Scoresheet{
|
|||||||
align: "right",
|
align: "right",
|
||||||
forceShadow: true
|
forceShadow: true
|
||||||
}, [
|
}, [
|
||||||
{outline: "#000", letterBorder: 10, shadow: [1, 1, 3]},
|
{ outline: "#000", letterBorder: 10, shadow: [1, 1, 3] },
|
||||||
{fill: "#fff"}
|
{ fill: "#fff" }
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if(p === 1){
|
if (p === 1) {
|
||||||
ctx.translate(0, p2Offset)
|
ctx.translate(0, p2Offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,9 +470,9 @@ class Scoresheet{
|
|||||||
ctx.miterLimit = 10
|
ctx.miterLimit = 10
|
||||||
|
|
||||||
var defaultName = p === 0 ? strings.defaultName : strings.default2PName
|
var defaultName = p === 0 ? strings.defaultName : strings.default2PName
|
||||||
if(p === this.player[0]){
|
if (p === this.player[0]) {
|
||||||
var name = account.loggedIn ? account.displayName : defaultName
|
var name = account.loggedIn ? account.displayName : defaultName
|
||||||
}else{
|
} else {
|
||||||
var name = results.name || defaultName
|
var name = results.name || defaultName
|
||||||
}
|
}
|
||||||
this.nameplateCache.get({
|
this.nameplateCache.get({
|
||||||
@@ -493,7 +493,7 @@ class Scoresheet{
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if(this.controller.autoPlayEnabled){
|
if (this.controller.autoPlayEnabled) {
|
||||||
ctx.drawImage(assets.image["badge_auto"],
|
ctx.drawImage(assets.image["badge_auto"],
|
||||||
431, 311, 34, 34
|
431, 311, 34, 34
|
||||||
)
|
)
|
||||||
@@ -549,8 +549,8 @@ class Scoresheet{
|
|||||||
align: "right",
|
align: "right",
|
||||||
width: 36
|
width: 36
|
||||||
}, [
|
}, [
|
||||||
{fill: "#fff"},
|
{ fill: "#fff" },
|
||||||
{outline: "#000", letterBorder: 0.5}
|
{ outline: "#000", letterBorder: 0.5 }
|
||||||
])
|
])
|
||||||
|
|
||||||
this.draw.score({
|
this.draw.score({
|
||||||
@@ -590,8 +590,8 @@ class Scoresheet{
|
|||||||
width: 154,
|
width: 154,
|
||||||
letterSpacing: strings.id === "ja" ? 1 : 0
|
letterSpacing: strings.id === "ja" ? 1 : 0
|
||||||
}, [
|
}, [
|
||||||
{outline: "#000", letterBorder: 8},
|
{ outline: "#000", letterBorder: 8 },
|
||||||
{fill: grd}
|
{ fill: grd }
|
||||||
])
|
])
|
||||||
this.draw.layeredText({
|
this.draw.layeredText({
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
@@ -604,8 +604,8 @@ class Scoresheet{
|
|||||||
width: 154,
|
width: 154,
|
||||||
letterSpacing: strings.id === "ja" ? 4 : 0
|
letterSpacing: strings.id === "ja" ? 4 : 0
|
||||||
}, [
|
}, [
|
||||||
{outline: "#000", letterBorder: 8},
|
{ outline: "#000", letterBorder: 8 },
|
||||||
{fill: "#ffc700"}
|
{ fill: "#ffc700" }
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
@@ -613,15 +613,15 @@ class Scoresheet{
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this.multiplayer){
|
if (!this.multiplayer) {
|
||||||
if(elapsed >= 400 && elapsed < 3100 + failedOffset){
|
if (elapsed >= 400 && elapsed < 3100 + failedOffset) {
|
||||||
if(this.tetsuoHanaClass !== "fadein"){
|
if (this.tetsuoHanaClass !== "fadein") {
|
||||||
this.tetsuoHana.classList.add("fadein")
|
this.tetsuoHana.classList.add("fadein")
|
||||||
this.tetsuoHanaClass = "fadein"
|
this.tetsuoHanaClass = "fadein"
|
||||||
}
|
}
|
||||||
}else if(elapsed >= 3100 + failedOffset){
|
} else if (elapsed >= 3100 + failedOffset) {
|
||||||
if(this.tetsuoHanaClass !== "dance" && this.tetsuoHanaClass !== "failed"){
|
if (this.tetsuoHanaClass !== "dance" && this.tetsuoHanaClass !== "failed") {
|
||||||
if(this.tetsuoHanaClass){
|
if (this.tetsuoHanaClass) {
|
||||||
this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
|
this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
|
||||||
}
|
}
|
||||||
this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed"
|
this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed"
|
||||||
@@ -630,19 +630,19 @@ class Scoresheet{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(elapsed >= 800){
|
if (elapsed >= 800) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||||
this.draw.alpha(Math.min(1, (elapsed - 800) / 500), ctx, ctx => {
|
this.draw.alpha(Math.min(1, (elapsed - 800) / 500), ctx, ctx => {
|
||||||
ctx.scale(ratio, ratio)
|
ctx.scale(ratio, ratio)
|
||||||
ctx.translate(frameLeft, frameTop)
|
ctx.translate(frameLeft, frameTop)
|
||||||
|
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if(p === 1){
|
if (p === 1) {
|
||||||
ctx.translate(0, p2Offset)
|
ctx.translate(0, p2Offset)
|
||||||
}
|
}
|
||||||
var w = 712
|
var w = 712
|
||||||
@@ -670,50 +670,50 @@ class Scoresheet{
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(elapsed >= 1200){
|
if (elapsed >= 1200) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||||
var noCrownResultWait = -2000;
|
var noCrownResultWait = -2000;
|
||||||
|
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var crownType = null
|
var crownType = null
|
||||||
if(this.rules[p].clearReached(results.gauge)){
|
if (this.rules[p].clearReached(results.gauge)) {
|
||||||
crownType = results.bad === "0" ? "gold" : "silver"
|
crownType = results.bad === "0" ? "gold" : "silver"
|
||||||
}
|
}
|
||||||
if(crownType !== null){
|
if (crownType !== null) {
|
||||||
noCrownResultWait = 0;
|
noCrownResultWait = 0;
|
||||||
var amount = Math.min(1, (elapsed - 1200) / 450)
|
var amount = Math.min(1, (elapsed - 1200) / 450)
|
||||||
this.draw.alpha(this.draw.easeIn(amount), ctx, ctx => {
|
this.draw.alpha(this.draw.easeIn(amount), ctx, ctx => {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.scale(ratio, ratio)
|
ctx.scale(ratio, ratio)
|
||||||
ctx.translate(frameLeft, frameTop)
|
ctx.translate(frameLeft, frameTop)
|
||||||
if(p === 1){
|
if (p === 1) {
|
||||||
ctx.translate(0, p2Offset)
|
ctx.translate(0, p2Offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
var crownScale = 1
|
var crownScale = 1
|
||||||
var shine = 0
|
var shine = 0
|
||||||
if(amount < 1){
|
if (amount < 1) {
|
||||||
crownScale = 2.8 * (1 - amount) + 0.9
|
crownScale = 2.8 * (1 - amount) + 0.9
|
||||||
}else if(elapsed < 1850){
|
} else if (elapsed < 1850) {
|
||||||
crownScale = 0.9 + (elapsed - 1650) / 2000
|
crownScale = 0.9 + (elapsed - 1650) / 2000
|
||||||
}else if(elapsed < 2200){
|
} else if (elapsed < 2200) {
|
||||||
shine = (elapsed - 1850) / 175
|
shine = (elapsed - 1850) / 175
|
||||||
if(shine > 1){
|
if (shine > 1) {
|
||||||
shine = 2 - shine
|
shine = 2 - shine
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(this.state.screen === "fadeIn" && elapsed >= 1200 && !this.state["fullcomboPlayed" + p]){
|
if (this.state.screen === "fadeIn" && elapsed >= 1200 && !this.state["fullcomboPlayed" + p]) {
|
||||||
this.state["fullcomboPlayed" + p] = true
|
this.state["fullcomboPlayed" + p] = true
|
||||||
if(crownType === "gold"){
|
if (crownType === "gold") {
|
||||||
this.playSound("v_results_fullcombo" + (p === 1 ? "2" : ""), p)
|
this.playSound("v_results_fullcombo" + (p === 1 ? "2" : ""), p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(this.state.screen === "fadeIn" && elapsed >= 1650 && !this.state["crownPlayed" + p]){
|
if (this.state.screen === "fadeIn" && elapsed >= 1650 && !this.state["crownPlayed" + p]) {
|
||||||
this.state["crownPlayed" + p] = true
|
this.state["crownPlayed" + p] = true
|
||||||
this.playSound("se_results_crown", p)
|
this.playSound("se_results_crown", p)
|
||||||
}
|
}
|
||||||
@@ -735,51 +735,51 @@ class Scoresheet{
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(elapsed >= 2400 + noCrownResultWait){
|
if (elapsed >= 2400 + noCrownResultWait) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(frameLeft, frameTop)
|
ctx.translate(frameLeft, frameTop)
|
||||||
|
|
||||||
var printNumbers = ["good", "ok", "bad", "maxCombo", "drumroll"]
|
var printNumbers = ["good", "ok", "bad", "maxCombo", "drumroll"]
|
||||||
if(!this.state["countupTime0"]){
|
if (!this.state["countupTime0"]) {
|
||||||
var times = {}
|
var times = {}
|
||||||
var lastTime = 0
|
var lastTime = 0
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame
|
var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame
|
||||||
if(currentTime > lastTime){
|
if (currentTime > lastTime) {
|
||||||
lastTime = currentTime
|
lastTime = currentTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(var i in printNumbers){
|
for (var i in printNumbers) {
|
||||||
var largestTime = 0
|
var largestTime = 0
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
times[printNumbers[i]] = lastTime + 500
|
times[printNumbers[i]] = lastTime + 500
|
||||||
var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
|
var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
|
||||||
if(currentTime > largestTime){
|
if (currentTime > largestTime) {
|
||||||
largestTime = currentTime
|
largestTime = currentTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastTime = largestTime
|
lastTime = largestTime
|
||||||
}
|
}
|
||||||
this.state.fadeInEnd = lastTime
|
this.state.fadeInEnd = lastTime
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
this.state["countupTime" + p] = times
|
this.state["countupTime" + p] = times
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if(p === 1){
|
if (p === 1) {
|
||||||
ctx.translate(0, p2Offset)
|
ctx.translate(0, p2Offset)
|
||||||
}
|
}
|
||||||
ctx.save()
|
ctx.save()
|
||||||
@@ -795,24 +795,24 @@ class Scoresheet{
|
|||||||
ctx.fillStyle = "#fff"
|
ctx.fillStyle = "#fff"
|
||||||
ctx.strokeStyle = "#fff"
|
ctx.strokeStyle = "#fff"
|
||||||
ctx.lineWidth = 0.5
|
ctx.lineWidth = 0.5
|
||||||
for(var i = 0; i < points.length; i++){
|
for (var i = 0; i < points.length; i++) {
|
||||||
ctx.translate(-23.3 * scale, 0)
|
ctx.translate(-23.3 * scale, 0)
|
||||||
ctx.fillText(points[points.length - i - 1], 0, 0)
|
ctx.fillText(points[points.length - i - 1], 0, 0)
|
||||||
ctx.strokeText(points[points.length - i - 1], 0, 0)
|
ctx.strokeText(points[points.length - i - 1], 0, 0)
|
||||||
}
|
}
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
|
|
||||||
if(!this.state["countupTime" + p]){
|
if (!this.state["countupTime" + p]) {
|
||||||
var times = {}
|
var times = {}
|
||||||
var lastTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame + 1000
|
var lastTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame + 1000
|
||||||
for(var i in printNumbers){
|
for (var i in printNumbers) {
|
||||||
times[printNumbers[i]] = lastTime + 500
|
times[printNumbers[i]] = lastTime + 500
|
||||||
lastTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
|
lastTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
|
||||||
}
|
}
|
||||||
this.state["countupTime" + p] = times
|
this.state["countupTime" + p] = times
|
||||||
}
|
}
|
||||||
|
|
||||||
for(var i in printNumbers){
|
for (var i in printNumbers) {
|
||||||
var start = this.state["countupTime" + p][printNumbers[i]]
|
var start = this.state["countupTime" + p][printNumbers[i]]
|
||||||
this.draw.layeredText({
|
this.draw.layeredText({
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
@@ -824,54 +824,77 @@ class Scoresheet{
|
|||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
align: "right"
|
align: "right"
|
||||||
}, [
|
}, [
|
||||||
{outline: "#000", letterBorder: 9},
|
{ outline: "#000", letterBorder: 9 },
|
||||||
{fill: "#fff"}
|
{ fill: "#fff" }
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.countupShown){
|
if (this.state.countupShown) {
|
||||||
if(!this.state["countup" + p]){
|
if (!this.state["countup" + p]) {
|
||||||
this.state["countup" + p] = true
|
this.state["countup" + p] = true
|
||||||
this.loopSound("se_results_countup", p, [0.1, false, 0, 0, 0.07])
|
this.loopSound("se_results_countup", p, [0.1, false, 0, 0, 0.07])
|
||||||
}
|
}
|
||||||
}else if(this.state["countup" + p]){
|
} else if (this.state["countup" + p]) {
|
||||||
this.state["countup" + p] = false
|
this.state["countup" + p] = false
|
||||||
this.stopSound("se_results_countup", p)
|
this.stopSound("se_results_countup", p)
|
||||||
if(this.state.screen === "fadeIn"){
|
if (this.state.screen === "fadeIn") {
|
||||||
this.playSound("neiro_1_don", p)
|
this.playSound("neiro_1_don", p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.screen === "fadeIn" && elapsed >= this.state.fadeInEnd){
|
if (this.state.screen === "fadeIn" && elapsed >= this.state.fadeInEnd) {
|
||||||
this.state.screen = "scoresShown"
|
this.state.screen = "scoresShown"
|
||||||
this.state.screenMS = this.getMS()
|
this.state.screenMS = this.getMS()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
if (this.leaderboardResult) {
|
||||||
|
ctx.save()
|
||||||
|
var text = strings.recordBroken.replace("%s", this.leaderboardResult.rank)
|
||||||
|
ctx.textAlign = "center"
|
||||||
|
var y = 130
|
||||||
|
if (this.multiplayer) {
|
||||||
|
y = 400
|
||||||
|
}
|
||||||
|
this.draw.layeredText({
|
||||||
|
ctx: ctx,
|
||||||
|
text: text,
|
||||||
|
x: 640,
|
||||||
|
y: y,
|
||||||
|
fontSize: 40,
|
||||||
|
fontFamily: strings.font,
|
||||||
|
align: "center"
|
||||||
|
}, [
|
||||||
|
{ outline: "#fff", letterBorder: 5 },
|
||||||
|
{ fill: "#ff0000" }
|
||||||
|
])
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
|
||||||
if(this.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000){
|
if (this.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000) {
|
||||||
this.state.scoreNext = true
|
this.state.scoreNext = true
|
||||||
if(p2.session){
|
if (p2.session) {
|
||||||
p2.send("songsel")
|
p2.send("songsel")
|
||||||
}else{
|
} else {
|
||||||
this.toSongsel(true)
|
this.toSongsel(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.screen === "fadeOut"){
|
if (this.state.screen === "fadeOut") {
|
||||||
if(this.state.hasPointer === 1){
|
if (this.state.hasPointer === 1) {
|
||||||
this.state.hasPointer = 2
|
this.state.hasPointer = 2
|
||||||
this.canvas.style.cursor = ""
|
this.canvas.style.cursor = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this.fadeScreenBlack){
|
if (!this.fadeScreenBlack) {
|
||||||
this.fadeScreenBlack = true
|
this.fadeScreenBlack = true
|
||||||
this.fadeScreen.style.backgroundColor = "#000"
|
this.fadeScreen.style.backgroundColor = "#000"
|
||||||
}
|
}
|
||||||
var elapsed = ms - this.state.screenMS
|
var elapsed = ms - this.state.screenMS
|
||||||
|
|
||||||
if(elapsed >= 1000){
|
if (elapsed >= 1000) {
|
||||||
this.clean()
|
this.clean()
|
||||||
this.controller.songSelection(true, this.showWarning)
|
this.controller.songSelection(true, this.showWarning)
|
||||||
}
|
}
|
||||||
@@ -880,47 +903,47 @@ class Scoresheet{
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
getNumber(score, start, elapsed){
|
getNumber(score, start, elapsed) {
|
||||||
var numberPos = Math.floor((elapsed - start) / this.frame)
|
var numberPos = Math.floor((elapsed - start) / this.frame)
|
||||||
if(numberPos < 0){
|
if (numberPos < 0) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var output = ""
|
var output = ""
|
||||||
for(var i = 0; i < score.length; i++){
|
for (var i = 0; i < score.length; i++) {
|
||||||
if(numberPos < 30 * (i + 1)){
|
if (numberPos < 30 * (i + 1)) {
|
||||||
this.state.countupShown = true
|
this.state.countupShown = true
|
||||||
return this.numbers[numberPos % 30] + output
|
return this.numbers[numberPos % 30] + output
|
||||||
}else{
|
} else {
|
||||||
output = score[score.length - i - 1] + output
|
output = score[score.length - i - 1] + output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
getSound(id, p){
|
getSound(id, p) {
|
||||||
return assets.sounds[id + (this.multiplayer ? "_p" + (p + 1) : "")]
|
return assets.sounds[id + (this.multiplayer ? "_p" + (p + 1) : "")]
|
||||||
}
|
}
|
||||||
playSound(id, p){
|
playSound(id, p) {
|
||||||
this.getSound(id, p).play()
|
this.getSound(id, p).play()
|
||||||
}
|
}
|
||||||
loopSound(id, p, args){
|
loopSound(id, p, args) {
|
||||||
this.getSound(id, p).playLoop(...args)
|
this.getSound(id, p).playLoop(...args)
|
||||||
}
|
}
|
||||||
stopSound(id, p){
|
stopSound(id, p) {
|
||||||
this.getSound(id, p).stop()
|
this.getSound(id, p).stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
mod(length, index){
|
mod(length, index) {
|
||||||
return ((index % length) + length) % length
|
return ((index % length) + length) % length
|
||||||
}
|
}
|
||||||
|
|
||||||
getMS(){
|
getMS() {
|
||||||
return Date.now()
|
return Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
saveScore(){
|
saveScore() {
|
||||||
if(this.controller.saveScore){
|
if (this.controller.saveScore) {
|
||||||
if(this.resultsObj.points < 0){
|
if (this.resultsObj.points < 0) {
|
||||||
this.resultsObj.points = 0
|
this.resultsObj.points = 0
|
||||||
}
|
}
|
||||||
var title = this.controller.selectedSong.originalTitle
|
var title = this.controller.selectedSong.originalTitle
|
||||||
@@ -929,11 +952,11 @@ class Scoresheet{
|
|||||||
var oldScore = scoreStorage.get(hash, difficulty, true)
|
var oldScore = scoreStorage.get(hash, difficulty, true)
|
||||||
var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge)
|
var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge)
|
||||||
var crown = ""
|
var crown = ""
|
||||||
if(clearReached){
|
if (clearReached) {
|
||||||
crown = this.resultsObj.bad === 0 ? "gold" : "silver"
|
crown = this.resultsObj.bad === 0 ? "gold" : "silver"
|
||||||
}
|
}
|
||||||
if(!oldScore || oldScore.points <= this.resultsObj.points){
|
if (!oldScore || oldScore.points <= this.resultsObj.points) {
|
||||||
if(oldScore && (oldScore.crown === "gold" || oldScore.crown === "silver" && !crown)){
|
if (oldScore && (oldScore.crown === "gold" || oldScore.crown === "silver" && !crown)) {
|
||||||
crown = oldScore.crown
|
crown = oldScore.crown
|
||||||
}
|
}
|
||||||
this.resultsObj.crown = crown
|
this.resultsObj.crown = crown
|
||||||
@@ -941,19 +964,66 @@ class Scoresheet{
|
|||||||
delete this.resultsObj.difficulty
|
delete this.resultsObj.difficulty
|
||||||
delete this.resultsObj.gauge
|
delete this.resultsObj.gauge
|
||||||
scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => {
|
scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => {
|
||||||
this.showWarning = {name: "scoreSaveFailed"}
|
this.showWarning = { name: "scoreSaveFailed" }
|
||||||
})
|
})
|
||||||
}else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){
|
} else if (oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)) {
|
||||||
oldScore.crown = crown
|
oldScore.crown = crown
|
||||||
scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => {
|
scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => {
|
||||||
this.showWarning = {name: "scoreSaveFailed"}
|
this.showWarning = { name: "scoreSaveFailed" }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!this.controller.autoPlayEnabled && account.loggedIn && !this.multiplayer) {
|
||||||
|
this.submitToLeaderboard()
|
||||||
|
}
|
||||||
this.scoreSaved = true
|
this.scoreSaved = true
|
||||||
}
|
}
|
||||||
|
|
||||||
clean(){
|
submitToLeaderboard() {
|
||||||
|
if (!this.resultsObj || !this.controller.selectedSong) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var song = this.controller.selectedSong
|
||||||
|
var body = {
|
||||||
|
song_id: song.id,
|
||||||
|
difficulty: this.resultsObj.difficulty,
|
||||||
|
score: this.resultsObj.points,
|
||||||
|
good: this.resultsObj.good,
|
||||||
|
ok: this.resultsObj.ok,
|
||||||
|
bad: this.resultsObj.bad,
|
||||||
|
maxCombo: this.resultsObj.maxCombo,
|
||||||
|
hash: song.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.getCsrfToken().then(token => {
|
||||||
|
var request = new XMLHttpRequest()
|
||||||
|
request.open("POST", gameConfig.basedir + "api/leaderboard/submit")
|
||||||
|
pageEvents.load(request).then(() => {
|
||||||
|
if (request.status === 200) {
|
||||||
|
try {
|
||||||
|
var data = JSON.parse(request.response)
|
||||||
|
if (data.status === "ok" && data.new_record && data.rank) {
|
||||||
|
this.leaderboardResult = {
|
||||||
|
rank: data.rank
|
||||||
|
}
|
||||||
|
assets.sounds["se_results_crown"].play()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Leaderboard response parse error:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
console.error("Leaderboard submit failed:", e)
|
||||||
|
})
|
||||||
|
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
request.setRequestHeader("X-CSRFToken", token)
|
||||||
|
request.send(JSON.stringify(body))
|
||||||
|
}).catch(e => {
|
||||||
|
console.error("Failed to get CSRF token:", e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clean() {
|
||||||
this.keyboard.clean()
|
this.keyboard.clean()
|
||||||
this.gamepad.clean()
|
this.gamepad.clean()
|
||||||
this.draw.clean()
|
this.draw.clean()
|
||||||
@@ -962,13 +1032,13 @@ class Scoresheet{
|
|||||||
snd.buffer.loadSettings()
|
snd.buffer.loadSettings()
|
||||||
this.redrawRunning = false
|
this.redrawRunning = false
|
||||||
pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
|
pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
|
||||||
if(this.touchEnabled){
|
if (this.touchEnabled) {
|
||||||
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
|
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
|
||||||
}
|
}
|
||||||
if(this.session){
|
if (this.session) {
|
||||||
pageEvents.remove(p2, "message")
|
pageEvents.remove(p2, "message")
|
||||||
}
|
}
|
||||||
if(!this.multiplayer){
|
if (!this.multiplayer) {
|
||||||
delete this.tetsuoHana
|
delete this.tetsuoHana
|
||||||
}
|
}
|
||||||
delete this.ctx
|
delete this.ctx
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -439,6 +439,77 @@ var translations = {
|
|||||||
ko: "연타 횟수"
|
ko: "연타 횟수"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
leaderboard: {
|
||||||
|
ja: "ランキング",
|
||||||
|
en: "Leaderboard",
|
||||||
|
cn: "排行榜",
|
||||||
|
tw: "排行榜",
|
||||||
|
ko: "순위표"
|
||||||
|
},
|
||||||
|
leaderboardTitle: {
|
||||||
|
ja: "%s の ランキング",
|
||||||
|
en: "%s Leaderboard",
|
||||||
|
cn: "%s 排行榜",
|
||||||
|
tw: "%s 排行榜",
|
||||||
|
ko: "%s 순위표"
|
||||||
|
},
|
||||||
|
rank: {
|
||||||
|
ja: "順位",
|
||||||
|
en: "Rank",
|
||||||
|
cn: "排名",
|
||||||
|
tw: "排名",
|
||||||
|
ko: "순위"
|
||||||
|
},
|
||||||
|
playerName: {
|
||||||
|
ja: "プレイヤー",
|
||||||
|
en: "Player",
|
||||||
|
cn: "玩家",
|
||||||
|
tw: "玩家",
|
||||||
|
ko: "플레이어"
|
||||||
|
},
|
||||||
|
score: {
|
||||||
|
ja: "スコア",
|
||||||
|
en: "Score",
|
||||||
|
cn: "分数",
|
||||||
|
tw: "分數",
|
||||||
|
ko: "점수"
|
||||||
|
},
|
||||||
|
recordBroken: {
|
||||||
|
ja: "最高記録を更新!第%s位",
|
||||||
|
en: "New Record! Rank #%s",
|
||||||
|
cn: "打破最佳纪录 第%s名",
|
||||||
|
tw: "打破最佳紀錄 第%s名",
|
||||||
|
ko: "최고 기록 경신! %s위"
|
||||||
|
},
|
||||||
|
yourRank: {
|
||||||
|
ja: "あなたの順位: %s位",
|
||||||
|
en: "Your Rank: #%s",
|
||||||
|
cn: "您的排名: 第%s名",
|
||||||
|
tw: "您的排名: 第%s名",
|
||||||
|
ko: "순위: %s위"
|
||||||
|
},
|
||||||
|
notRanked: {
|
||||||
|
ja: "ランク外",
|
||||||
|
en: "Not Ranked",
|
||||||
|
cn: "未上榜",
|
||||||
|
tw: "未上榜",
|
||||||
|
ko: "순위 없음"
|
||||||
|
},
|
||||||
|
loadingLeaderboard: {
|
||||||
|
ja: "ランキング読み込み中...",
|
||||||
|
en: "Loading Leaderboard...",
|
||||||
|
cn: "加载排行榜中...",
|
||||||
|
tw: "讀取排行榜中...",
|
||||||
|
ko: "순위표 로딩 중..."
|
||||||
|
},
|
||||||
|
noScores: {
|
||||||
|
ja: "記録なし",
|
||||||
|
en: "No Scores Yet",
|
||||||
|
cn: "暂无记录",
|
||||||
|
tw: "暫無紀錄",
|
||||||
|
ko: "기록 없음"
|
||||||
|
},
|
||||||
|
|
||||||
errorOccured: {
|
errorOccured: {
|
||||||
ja: "エラーが発生しました。再読み込みしてください。",
|
ja: "エラーが発生しました。再読み込みしてください。",
|
||||||
en: "An error occurred, please refresh",
|
en: "An error occurred, please refresh",
|
||||||
@@ -1472,26 +1543,26 @@ var translations = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var allStrings = {}
|
var allStrings = {}
|
||||||
function separateStrings(){
|
function separateStrings() {
|
||||||
for(var j in languageList){
|
for (var j in languageList) {
|
||||||
var lang = languageList[j]
|
var lang = languageList[j]
|
||||||
allStrings[lang] = {
|
allStrings[lang] = {
|
||||||
id: lang
|
id: lang
|
||||||
}
|
}
|
||||||
var str = allStrings[lang]
|
var str = allStrings[lang]
|
||||||
var translateObj = function(obj, name, str){
|
var translateObj = function (obj, name, str) {
|
||||||
if("en" in obj){
|
if ("en" in obj) {
|
||||||
for(var i in obj){
|
for (var i in obj) {
|
||||||
str[name] = obj[lang] || obj.en
|
str[name] = obj[lang] || obj.en
|
||||||
}
|
}
|
||||||
}else if(obj){
|
} else if (obj) {
|
||||||
str[name] = {}
|
str[name] = {}
|
||||||
for(var i in obj){
|
for (var i in obj) {
|
||||||
translateObj(obj[i], i, str[name])
|
translateObj(obj[i], i, str[name])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(var i in translations){
|
for (var i in translations) {
|
||||||
translateObj(translations[i], i, str)
|
translateObj(translations[i], i, str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,21 @@
|
|||||||
|
|
||||||
<label for="file_music">音楽ファイル:</label>
|
<label for="file_music">音楽ファイル:</label>
|
||||||
<input type="file" name="file_music" accept=".ogg,.mp3,.wav" required>
|
<input type="file" name="file_music" accept=".ogg,.mp3,.wav" required>
|
||||||
|
|
||||||
|
<label for="song_type">曲のタイプ:</label>
|
||||||
|
<select name="song_type" required>
|
||||||
|
<option value="01 Pop">01 Pop</option>
|
||||||
|
<option value="02 Anime">02 Anime</option>
|
||||||
|
<option value="03 Vocaloid">03 Vocaloid</option>
|
||||||
|
<option value="04 Children and Folk">04 Children and Folk</option>
|
||||||
|
<option value="05 Variety">05 Variety</option>
|
||||||
|
<option value="06 Classical">06 Classical</option>
|
||||||
|
<option value="07 Game Music">07 Game Music</option>
|
||||||
|
<option value="08 Live Festival Mode">08 Live Festival Mode</option>
|
||||||
|
<option value="09 Namco Original">09 Namco Original</option>
|
||||||
|
<option value="10 Taiko Towers">10 Taiko Towers</option>
|
||||||
|
<option value="11 Dan Dojo">11 Dan Dojo</option>
|
||||||
|
</select>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<button type="button" onclick="uploadFiles()">今すぐ投稿! (1分ほどかかる場合があります)</button>
|
<button type="button" onclick="uploadFiles()">今すぐ投稿! (1分ほどかかる場合があります)</button>
|
||||||
|
|||||||
26
schema.py
26
schema.py
@@ -80,3 +80,29 @@ scores_save = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leaderboard_submit = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'song_id': {'type': ['integer', 'string']},
|
||||||
|
'difficulty': {'type': 'string', 'enum': ['easy', 'normal', 'hard', 'oni', 'ura']},
|
||||||
|
'score': {'type': 'integer'},
|
||||||
|
'good': {'type': 'integer'},
|
||||||
|
'ok': {'type': 'integer'},
|
||||||
|
'bad': {'type': 'integer'},
|
||||||
|
'maxCombo': {'type': 'integer'},
|
||||||
|
'hash': {'type': 'string'}
|
||||||
|
},
|
||||||
|
'required': ['difficulty', 'score']
|
||||||
|
}
|
||||||
|
|
||||||
|
leaderboard_get = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'song_id': {'type': ['integer', 'string']},
|
||||||
|
'difficulty': {'type': 'string'}
|
||||||
|
},
|
||||||
|
'required': ['song_id', 'difficulty']
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,9 +29,9 @@
|
|||||||
<div id="screen" class="pattern-bg"></div>
|
<div id="screen" class="pattern-bg"></div>
|
||||||
<div data-nosnippet id="version">
|
<div data-nosnippet id="version">
|
||||||
{% if version.version and version.commit_short and version.commit %}
|
{% if version.version and version.commit_short and version.commit %}
|
||||||
<a href="{{version.url}}commit/{{version.commit}}" target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="taiko-web ver.{{version.version}} ({{version.commit_short}})">taiko-web ver.{{version.version}} ({{version.commit_short}})</a>
|
<a href="{{version.url}}commit/{{version.commit}}" target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.0.0">vLightNova 1.0.0</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="taiko-web vRAINBOW-BETA4">taiko-web vRAINBOW-BETA4</a>
|
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.0.0">vLightNova 1.0.0</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
|
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
|
||||||
|
|||||||
22
tjaf.py
22
tjaf.py
@@ -7,6 +7,8 @@ class Tja:
|
|||||||
self.text = text
|
self.text = text
|
||||||
self.title: Optional[str] = None
|
self.title: Optional[str] = None
|
||||||
self.subtitle: Optional[str] = None
|
self.subtitle: Optional[str] = None
|
||||||
|
self.title_ja: Optional[str] = None
|
||||||
|
self.subtitle_ja: Optional[str] = None
|
||||||
self.wave: Optional[str] = None
|
self.wave: Optional[str] = None
|
||||||
self.offset: Optional[float] = None
|
self.offset: Optional[float] = None
|
||||||
self.courses: Dict[str, Dict[str, Optional[int]]] = {}
|
self.courses: Dict[str, Dict[str, Optional[int]]] = {}
|
||||||
@@ -25,8 +27,12 @@ class Tja:
|
|||||||
val = v.strip()
|
val = v.strip()
|
||||||
if key == "TITLE":
|
if key == "TITLE":
|
||||||
self.title = val or None
|
self.title = val or None
|
||||||
|
elif key == "TITLEJA":
|
||||||
|
self.title_ja = val or None
|
||||||
elif key == "SUBTITLE":
|
elif key == "SUBTITLE":
|
||||||
self.subtitle = val or None
|
self.subtitle = val or None
|
||||||
|
elif key == "SUBTITLEJA":
|
||||||
|
self.subtitle_ja = val or None
|
||||||
elif key == "WAVE":
|
elif key == "WAVE":
|
||||||
self.wave = val or None
|
self.wave = val or None
|
||||||
elif key == "OFFSET":
|
elif key == "OFFSET":
|
||||||
@@ -73,8 +79,20 @@ class Tja:
|
|||||||
"type": "tja",
|
"type": "tja",
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"subtitle": self.subtitle,
|
"subtitle": self.subtitle,
|
||||||
"title_lang": {"ja": self.title, "en": None, "cn": None, "tw": None, "ko": None},
|
"title_lang": {
|
||||||
"subtitle_lang": {"ja": self.subtitle, "en": None, "cn": None, "tw": None, "ko": None},
|
"ja": self.title_ja or self.title,
|
||||||
|
"en": None,
|
||||||
|
"cn": self.title_ja or None,
|
||||||
|
"tw": None,
|
||||||
|
"ko": None,
|
||||||
|
},
|
||||||
|
"subtitle_lang": {
|
||||||
|
"ja": self.subtitle_ja or self.subtitle,
|
||||||
|
"en": None,
|
||||||
|
"cn": self.subtitle_ja or None,
|
||||||
|
"tw": None,
|
||||||
|
"ko": None,
|
||||||
|
},
|
||||||
"courses": courses_out,
|
"courses": courses_out,
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
"category_id": None,
|
"category_id": None,
|
||||||
|
|||||||
43
tools/reset_leaderboard.py
Normal file
43
tools/reset_leaderboard.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
try:
|
||||||
|
import config
|
||||||
|
except ImportError:
|
||||||
|
# Handle case where config might not be in path or environment variables are used
|
||||||
|
config = None
|
||||||
|
|
||||||
|
def reset_leaderboard():
|
||||||
|
"""Delete leaderboard entries not from the current month"""
|
||||||
|
mongo_host = os.environ.get("TAIKO_WEB_MONGO_HOST")
|
||||||
|
if not mongo_host and config:
|
||||||
|
mongo_host = config.MONGO['host']
|
||||||
|
|
||||||
|
db_name = "taiko"
|
||||||
|
if config:
|
||||||
|
db_name = config.MONGO['database']
|
||||||
|
|
||||||
|
if not mongo_host:
|
||||||
|
print("Error: content not found for MONGO_HOST")
|
||||||
|
return
|
||||||
|
|
||||||
|
client = MongoClient(host=mongo_host)
|
||||||
|
db = client[db_name]
|
||||||
|
|
||||||
|
current_month = datetime.now().strftime('%Y-%m')
|
||||||
|
|
||||||
|
# Delete old month data
|
||||||
|
result = db.leaderboard.delete_many({'month': {'$ne': current_month}})
|
||||||
|
|
||||||
|
print(f"Deleted {result.deleted_count} old leaderboard entries")
|
||||||
|
print(f"Current month: {current_month}")
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
reset_leaderboard()
|
||||||
40
update.sh
Normal file
40
update.sh
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
if [ "${EUID}" -ne 0 ]; then echo "需要 root 权限"; exit 1; fi
|
||||||
|
|
||||||
|
SRC_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
DEST_DIR=/srv/taiko-web
|
||||||
|
SONGS_DIR="$DEST_DIR/public/songs"
|
||||||
|
BACKUP_DIR="$DEST_DIR/.backup_songs_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
|
||||||
|
systemctl stop taiko-web || true
|
||||||
|
|
||||||
|
if [ -d "$SONGS_DIR" ]; then
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
rsync -a "$SONGS_DIR/" "$BACKUP_DIR/" || cp -a "$SONGS_DIR/." "$BACKUP_DIR/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$DEST_DIR"
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude '.git' \
|
||||||
|
--exclude '.venv' \
|
||||||
|
--exclude 'public/songs' \
|
||||||
|
"$SRC_DIR/" "$DEST_DIR/"
|
||||||
|
|
||||||
|
if [ -x "$DEST_DIR/.venv/bin/pip" ]; then
|
||||||
|
"$DEST_DIR/.venv/bin/pip" install -U pip
|
||||||
|
"$DEST_DIR/.venv/bin/pip" install -r "$DEST_DIR/requirements.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
chown -R www-data:www-data "$DEST_DIR"
|
||||||
|
|
||||||
|
if [ -d "$BACKUP_DIR" ]; then
|
||||||
|
mkdir -p "$SONGS_DIR"
|
||||||
|
rsync -a "$BACKUP_DIR/" "$SONGS_DIR/" || cp -a "$BACKUP_DIR/." "$SONGS_DIR/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
systemctl restart taiko-web || systemctl start taiko-web || true
|
||||||
|
|
||||||
|
systemctl is-active --quiet taiko-web
|
||||||
Reference in New Issue
Block a user