commit 66d8ed5c6f65fb88b7e5b28f4ee6e4466aca830d Author: AnthonyDuan Date: Sat Nov 22 21:29:13 2025 +0800 refactor: remove tjaf dependency; add local TJA parser diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..a30e3af --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,3 @@ +{ + "postCreateCommand": "" +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f111784 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +/.git +/__pycache__ + +/public/songs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bdb0cab --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..0b1ad1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,3 @@ + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7395278 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +.DS_Store + +# build/version artifacts +version.json + +# large assets not meant for VCS +public/songs/ +public/songs/** +public/preview/ +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..182724b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.13.2 +COPY . /app +WORKDIR /app +RUN pip install -r requirements.txt +ENV PYTHONUNBUFFERED 1 +CMD ["gunicorn", "app:app", "--access-logfile", "-", "--bind", "0.0.0.0"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..797ee1f --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# 太鼓ウェブ + +この太鼓ウェブは改良版です + +## デバッグの開始 + +依存関係をインストールします + +```bash +pip install -r requirements.txt +``` + +データベースを起動します + +```bash +docker run --detach \ + --name taiko-web-mongo-debug \ + --volume taiko-web-mongo-debug:/data/db \ + --publish 27017:27017 \ + mongo +``` + +```bash +docker run --detach \ + --name taiko-web-redis-debug \ + --volume taiko-web-redis-debug:/data \ + --publish 6379:6379 \ + redis +``` + +サーバーを起動してください + +```bash +flask run +``` + +## デプロイ + +今すぐデプロイ! + +- https://taikoapp.uk/ diff --git a/app.py b/app.py new file mode 100644 index 0000000..c689b72 --- /dev/null +++ b/app.py @@ -0,0 +1,895 @@ +#!/usr/bin/env python3 + +import base64 +import bcrypt +import hashlib +try: + import config +except ModuleNotFoundError: + raise FileNotFoundError('No such file or directory: \'config.py\'. Copy the example config file config.example.py to config.py') +import json +import re +import requests +import schema +import os +import time + +# -- カスタム -- +import traceback +import pprint +import pathlib +import shutil +from flask_limiter import Limiter + +import flask +import tjaf + +# ---- + +from functools import wraps +from flask import Flask, g, jsonify, render_template, request, abort, redirect, session, flash, make_response, send_from_directory +from flask_caching import Cache +from flask_session import Session +from flask_wtf.csrf import CSRFProtect, generate_csrf, CSRFError +from ffmpy import FFmpeg +from pymongo import MongoClient +from redis import Redis + +def take_config(name, required=False): + if hasattr(config, name): + return getattr(config, name) + elif required: + raise ValueError('Required option is not defined in the config.py file: {}'.format(name)) + else: + return None + +app = Flask(__name__) + +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" + +limiter = Limiter( + get_remote_address, + app=app, + # default_limits=[], + # storage_uri="memory://", + # Redis + storage_uri=os.environ.get("REDIS_URI", "redis://127.0.0.1:6379/"), + # Redis cluster + # storage_uri="redis+cluster://localhost:7000,localhost:7001,localhost:70002", + # Memcached + # storage_uri="memcached://localhost:11211", + # Memcached Cluster + # storage_uri="memcached://localhost:11211,localhost:11212,localhost:11213", + # MongoDB + # storage_uri="mongodb://localhost:27017", + # Etcd + # storage_uri="etcd://localhost:2379", + strategy="fixed-window", # or "moving-window" +) + +client = MongoClient(host=os.environ.get("TAIKO_WEB_MONGO_HOST") or take_config('MONGO', required=True)['host']) +basedir = take_config('BASEDIR') or '/' + +app.secret_key = take_config('SECRET_KEY') or 'change-me' +app.config['SESSION_TYPE'] = 'redis' +redis_config = take_config('REDIS', required=True) +redis_config['CACHE_REDIS_HOST'] = os.environ.get("TAIKO_WEB_REDIS_HOST") or redis_config['CACHE_REDIS_HOST'] +app.config['SESSION_REDIS'] = Redis( + host=redis_config['CACHE_REDIS_HOST'], + port=redis_config['CACHE_REDIS_PORT'], + password=redis_config['CACHE_REDIS_PASSWORD'], + db=redis_config['CACHE_REDIS_DB'] +) +app.cache = Cache(app, config=redis_config) +sess = Session() +sess.init_app(app) +#csrf = CSRFProtect(app) + +db = client[take_config('MONGO', required=True)['database']] +db.users.create_index('username', unique=True) +db.songs.create_index('id', unique=True) +db.scores.create_index('username') + + +class HashException(Exception): + pass + + +def api_error(message): + return jsonify({'status': 'error', 'message': message}) + + +def generate_hash(id, form): + md5 = hashlib.md5() + if form['type'] == 'tja': + urls = ['%s%s/main.tja' % (take_config('SONGS_BASEURL', required=True), id)] + else: + urls = [] + for diff in ['easy', 'normal', 'hard', 'oni', 'ura']: + if form['course_' + diff]: + urls.append('%s%s/%s.osu' % (take_config('SONGS_BASEURL', required=True), id, diff)) + + for url in urls: + if url.startswith("http://") or url.startswith("https://"): + resp = requests.get(url) + if resp.status_code != 200: + raise HashException('Invalid response from %s (status code %s)' % (resp.url, resp.status_code)) + md5.update(resp.content) + else: + if url.startswith(basedir): + url = url[len(basedir):] + path = os.path.normpath(os.path.join("public", url)) + if not os.path.isfile(path): + raise HashException("File not found: %s" % (os.path.abspath(path))) + with open(path, "rb") as file: + md5.update(file.read()) + + return base64.b64encode(md5.digest())[:-2].decode('utf-8') + + +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not session.get('username'): + return api_error('not_logged_in') + return f(*args, **kwargs) + return decorated_function + + +def admin_required(level): + def decorated_function(f): + @wraps(f) + def wrapper(*args, **kwargs): + if not session.get('username'): + return abort(403) + + user = db.users.find_one({'username': session.get('username')}) + if user['user_level'] < level: + return abort(403) + + return f(*args, **kwargs) + return wrapper + return decorated_function + + +@app.errorhandler(CSRFError) +def handle_csrf_error(e): + return api_error('invalid_csrf') + + +@app.before_request +def before_request_func(): + if session.get('session_id'): + if not db.users.find_one({'session_id': session.get('session_id')}): + session.clear() + + +def get_config(credentials=False): + config_out = { + 'basedir': basedir, + 'songs_baseurl': take_config('SONGS_BASEURL', required=True), + 'assets_baseurl': take_config('ASSETS_BASEURL', required=True), + 'email': take_config('EMAIL'), + 'accounts': take_config('ACCOUNTS'), + 'custom_js': take_config('CUSTOM_JS'), + 'plugins': take_config('PLUGINS') and [x for x in take_config('PLUGINS') if x['url']], + 'preview_type': take_config('PREVIEW_TYPE') or 'mp3', + 'multiplayer_url': take_config('MULTIPLAYER_URL') + } + relative_urls = ['songs_baseurl', 'assets_baseurl'] + for name in relative_urls: + if not config_out[name].startswith("/") and not config_out[name].startswith("http://") and not config_out[name].startswith("https://"): + config_out[name] = basedir + config_out[name] + if credentials: + google_credentials = take_config('GOOGLE_CREDENTIALS') + min_level = google_credentials['min_level'] or 0 + if not session.get('username'): + user_level = 0 + else: + user = db.users.find_one({'username': session.get('username')}) + user_level = user['user_level'] + if user_level >= min_level: + config_out['google_credentials'] = google_credentials + else: + config_out['google_credentials'] = { + 'gdrive_enabled': False + } + + if not config_out.get('songs_baseurl'): + config_out['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/' + if not config_out.get('assets_baseurl'): + config_out['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/' + + config_out['_version'] = get_version() + return config_out + +def get_version(): + version = {'commit': None, 'commit_short': '', 'version': None, 'url': take_config('URL')} + if os.path.isfile('version.json'): + try: + ver = json.load(open('version.json', 'r')) + except ValueError: + print('Invalid version.json file') + return version + + for key in version.keys(): + if ver.get(key): + version[key] = ver.get(key) + + return version + +def get_db_don(user): + don_body_fill = user['don_body_fill'] if 'don_body_fill' in user else get_default_don('body_fill') + don_face_fill = user['don_face_fill'] if 'don_face_fill' in user else get_default_don('face_fill') + return {'body_fill': don_body_fill, 'face_fill': don_face_fill} + +def get_default_don(part=None): + if part == None: + return { + 'body_fill': get_default_don('body_fill'), + 'face_fill': get_default_don('face_fill') + } + elif part == 'body_fill': + return '#5fb7c1' + elif part == 'face_fill': + return '#ff5724' + +def is_hex(input): + try: + int(input, 16) + return True + except ValueError: + return False + + +@app.route(basedir) +def route_index(): + version = get_version() + return render_template('index.html', version=version, config=get_config()) + + +@app.route(basedir + 'api/csrftoken') +def route_csrftoken(): + return jsonify({'status': 'ok', 'token': generate_csrf()}) + + +@app.route(basedir + 'admin') +@admin_required(level=50) +def route_admin(): + return redirect(basedir + 'admin/songs') + + +@app.route(basedir + 'admin/songs') +@admin_required(level=50) +def route_admin_songs(): + songs = sorted(list(db.songs.find({})), key=lambda x: x['id']) + categories = db.categories.find({}) + user = db.users.find_one({'username': session['username']}) + return render_template('admin_songs.html', songs=songs, admin=user, categories=list(categories), config=get_config()) + + +@app.route(basedir + 'admin/songs/') +@admin_required(level=50) +def route_admin_songs_id(id): + song = db.songs.find_one({'id': id}) + if not song: + return abort(404) + + categories = list(db.categories.find({})) + song_skins = list(db.song_skins.find({})) + makers = list(db.makers.find({})) + user = db.users.find_one({'username': session['username']}) + + return render_template('admin_song_detail.html', + song=song, categories=categories, song_skins=song_skins, makers=makers, admin=user, config=get_config()) + + +@app.route(basedir + 'admin/songs/new') +@admin_required(level=100) +def route_admin_songs_new(): + categories = list(db.categories.find({})) + song_skins = list(db.song_skins.find({})) + makers = list(db.makers.find({})) + seq = db.seq.find_one({'name': 'songs'}) + seq_new = seq['value'] + 1 if seq else 1 + + return render_template('admin_song_new.html', categories=categories, song_skins=song_skins, makers=makers, config=get_config(), id=seq_new) + + +@app.route(basedir + 'admin/songs/new', methods=['POST']) +@admin_required(level=100) +def route_admin_songs_new_post(): + output = {'title_lang': {}, 'subtitle_lang': {}, 'courses': {}} + output['enabled'] = True if request.form.get('enabled') else False + output['title'] = request.form.get('title') or None + output['subtitle'] = request.form.get('subtitle') or None + for lang in ['ja', 'en', 'cn', 'tw', 'ko']: + output['title_lang'][lang] = request.form.get('title_%s' % lang) or None + output['subtitle_lang'][lang] = request.form.get('subtitle_%s' % lang) or None + + for course in ['easy', 'normal', 'hard', 'oni', 'ura']: + if request.form.get('course_%s' % course): + output['courses'][course] = {'stars': int(request.form.get('course_%s' % course)), + 'branch': True if request.form.get('branch_%s' % course) else False} + else: + output['courses'][course] = None + + output['category_id'] = int(request.form.get('category_id')) or None + output['type'] = request.form.get('type') + output['music_type'] = request.form.get('music_type') + output['offset'] = float(request.form.get('offset')) or None + output['skin_id'] = int(request.form.get('skin_id')) or None + output['preview'] = float(request.form.get('preview')) or None + output['volume'] = float(request.form.get('volume')) or None + output['maker_id'] = int(request.form.get('maker_id')) or None + output['lyrics'] = True if request.form.get('lyrics') else False + output['hash'] = request.form.get('hash') + + seq = db.seq.find_one({'name': 'songs'}) + seq_new = seq['value'] + 1 if seq else 1 + + hash_error = False + if request.form.get('gen_hash'): + try: + output['hash'] = generate_hash(seq_new, request.form) + except HashException as e: + hash_error = True + flash('An error occurred: %s' % str(e), 'error') + + output['id'] = seq_new + output['order'] = seq_new + + db.songs.insert_one(output) + if not hash_error: + flash('Song created.') + + db.seq.update_one({'name': 'songs'}, {'$set': {'value': seq_new}}, upsert=True) + + return redirect(basedir + 'admin/songs/%s' % str(seq_new)) + + +@app.route(basedir + 'admin/songs/', methods=['POST']) +@admin_required(level=50) +def route_admin_songs_id_post(id): + song = db.songs.find_one({'id': id}) + if not song: + return abort(404) + + user = db.users.find_one({'username': session['username']}) + user_level = user['user_level'] + + output = {'title_lang': {}, 'subtitle_lang': {}, 'courses': {}} + if user_level >= 100: + output['enabled'] = True if request.form.get('enabled') else False + + output['title'] = request.form.get('title') or None + output['subtitle'] = request.form.get('subtitle') or None + for lang in ['ja', 'en', 'cn', 'tw', 'ko']: + output['title_lang'][lang] = request.form.get('title_%s' % lang) or None + output['subtitle_lang'][lang] = request.form.get('subtitle_%s' % lang) or None + + for course in ['easy', 'normal', 'hard', 'oni', 'ura']: + if request.form.get('course_%s' % course): + output['courses'][course] = {'stars': int(request.form.get('course_%s' % course)), + 'branch': True if request.form.get('branch_%s' % course) else False} + else: + output['courses'][course] = None + + output['category_id'] = int(request.form.get('category_id')) or None + output['type'] = request.form.get('type') + output['music_type'] = request.form.get('music_type') + output['offset'] = float(request.form.get('offset')) or None + output['skin_id'] = int(request.form.get('skin_id')) or None + output['preview'] = float(request.form.get('preview')) or None + output['volume'] = float(request.form.get('volume')) or None + output['maker_id'] = int(request.form.get('maker_id')) or None + output['lyrics'] = True if request.form.get('lyrics') else False + output['hash'] = request.form.get('hash') + + hash_error = False + if request.form.get('gen_hash'): + try: + output['hash'] = generate_hash(id, request.form) + except HashException as e: + hash_error = True + flash('An error occurred: %s' % str(e), 'error') + + db.songs.update_one({'id': id}, {'$set': output}) + if not hash_error: + flash('Changes saved.') + + return redirect(basedir + 'admin/songs/%s' % id) + + +@app.route(basedir + 'admin/songs//delete', methods=['POST']) +@limiter.limit("1 per day") +@admin_required(level=100) +def route_admin_songs_id_delete(id): + song = db.songs.find_one({'id': id}) + if not song: + return abort(404) + + db.songs.delete_one({'id': id}) + flash('Song deleted.') + return redirect(basedir + 'admin/songs') + + +@app.route(basedir + 'admin/users') +@admin_required(level=50) +def route_admin_users(): + user = db.users.find_one({'username': session.get('username')}) + max_level = user['user_level'] - 1 + return render_template('admin_users.html', config=get_config(), max_level=max_level, username='', level='') + + +@app.route(basedir + 'admin/users', methods=['POST']) +@admin_required(level=50) +def route_admin_users_post(): + admin_name = session.get('username') + admin = db.users.find_one({'username': admin_name}) + max_level = admin['user_level'] - 1 + + username = request.form.get('username') + try: + level = int(request.form.get('level')) or 0 + except ValueError: + level = 0 + + user = db.users.find_one({'username_lower': username.lower()}) + if not user: + flash('Error: User was not found.') + elif admin['username'] == user['username']: + flash('Error: You cannot modify your own level.') + else: + user_level = user['user_level'] + if level < 0 or level > max_level: + flash('Error: Invalid level.') + elif user_level > max_level: + flash('Error: This user has higher level than you.') + else: + output = {'user_level': level} + db.users.update_one({'username': user['username']}, {'$set': output}) + flash('User updated.') + + return render_template('admin_users.html', config=get_config(), max_level=max_level, username=username, level=level) + + +@app.route(basedir + 'api/preview') +@app.cache.cached(timeout=15, query_string=True) +def route_api_preview(): + song_id = request.args.get('id', None) + if not song_id or not re.match('^[0-9]{1,9}$', song_id): + abort(400) + + song_id = int(song_id) + song = db.songs.find_one({'id': song_id}) + if not song: + abort(400) + + song_type = song['type'] + song_ext = song['music_type'] if song['music_type'] else "mp3" + prev_path = make_preview(song_id, song_type, song_ext, song['preview']) + if not prev_path: + return redirect(get_config()['songs_baseurl'] + '%s/main.%s' % (song_id, song_ext)) + + return redirect(get_config()['songs_baseurl'] + '%s/preview.mp3' % song_id) + + +@app.route(basedir + 'api/songs') +@app.cache.cached(timeout=15) +def route_api_songs(): + songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False})) + for song in songs: + if song['maker_id']: + if song['maker_id'] == 0: + song['maker'] = 0 + else: + song['maker'] = db.makers.find_one({'id': song['maker_id']}, {'_id': False}) + else: + song['maker'] = None + del song['maker_id'] + + if song['category_id']: + song['category'] = db.categories.find_one({'id': song['category_id']})['title'] + else: + song['category'] = None + #del song['category_id'] + + if song['skin_id']: + song['song_skin'] = db.song_skins.find_one({'id': song['skin_id']}, {'_id': False, 'id': False}) + else: + song['song_skin'] = None + del song['skin_id'] + + return cache_wrap(flask.jsonify(songs), 60) + +@app.route(basedir + 'api/categories') +@app.cache.cached(timeout=15) +def route_api_categories(): + categories = list(db.categories.find({},{'_id': False})) + return jsonify(categories) + +@app.route(basedir + 'api/config') +@app.cache.cached(timeout=15) +def route_api_config(): + config = get_config(credentials=True) + return jsonify(config) + + +@app.route(basedir + 'api/register', methods=['POST']) +def route_api_register(): + data = request.get_json() + if not schema.validate(data, schema.register): + return abort(400) + + if session.get('username'): + session.clear() + + username = data.get('username', '') + if len(username) < 3 or len(username) > 20 or not re.match('^[a-zA-Z0-9_]{3,20}$', username): + return api_error('invalid_username') + + if db.users.find_one({'username_lower': username.lower()}): + return api_error('username_in_use') + + password = data.get('password', '').encode('utf-8') + if not 6 <= len(password) <= 5000: + return api_error('invalid_password') + + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(password, salt) + don = get_default_don() + + session_id = os.urandom(24).hex() + db.users.insert_one({ + 'username': username, + 'username_lower': username.lower(), + 'password': hashed, + 'display_name': username, + 'don': don, + 'user_level': 1, + 'session_id': session_id + }) + + session['session_id'] = session_id + session['username'] = username + session.permanent = True + return jsonify({'status': 'ok', 'username': username, 'display_name': username, 'don': don}) + + +@app.route(basedir + 'api/login', methods=['POST']) +def route_api_login(): + data = request.get_json() + if not schema.validate(data, schema.login): + return abort(400) + + if session.get('username'): + session.clear() + + username = data.get('username', '') + result = db.users.find_one({'username_lower': username.lower()}) + if not result: + return api_error('invalid_username_password') + + password = data.get('password', '').encode('utf-8') + if not bcrypt.checkpw(password, result['password']): + return api_error('invalid_username_password') + + don = get_db_don(result) + + session['session_id'] = result['session_id'] + session['username'] = result['username'] + session.permanent = True if data.get('remember') else False + + return jsonify({'status': 'ok', 'username': result['username'], 'display_name': result['display_name'], 'don': don}) + + +@app.route(basedir + 'api/logout', methods=['POST']) +@login_required +def route_api_logout(): + session.clear() + return jsonify({'status': 'ok'}) + + +@app.route(basedir + 'api/account/display_name', methods=['POST']) +@login_required +def route_api_account_display_name(): + data = request.get_json() + if not schema.validate(data, schema.update_display_name): + return abort(400) + + display_name = data.get('display_name', '').strip() + if not display_name: + display_name = session.get('username') + elif len(display_name) > 25: + return api_error('invalid_display_name') + + db.users.update_one({'username': session.get('username')}, { + '$set': {'display_name': display_name} + }) + + return jsonify({'status': 'ok', 'display_name': display_name}) + + +@app.route(basedir + 'api/account/don', methods=['POST']) +@login_required +def route_api_account_don(): + data = request.get_json() + if not schema.validate(data, schema.update_don): + return abort(400) + + don_body_fill = data.get('body_fill', '').strip() + don_face_fill = data.get('face_fill', '').strip() + if len(don_body_fill) != 7 or\ + not don_body_fill.startswith("#")\ + or not is_hex(don_body_fill[1:])\ + or len(don_face_fill) != 7\ + or not don_face_fill.startswith("#")\ + or not is_hex(don_face_fill[1:]): + return api_error('invalid_don') + + db.users.update_one({'username': session.get('username')}, {'$set': { + 'don_body_fill': don_body_fill, + 'don_face_fill': don_face_fill, + }}) + + return jsonify({'status': 'ok', 'don': {'body_fill': don_body_fill, 'face_fill': don_face_fill}}) + + +@app.route(basedir + 'api/account/password', methods=['POST']) +@login_required +def route_api_account_password(): + data = request.get_json() + if not schema.validate(data, schema.update_password): + return abort(400) + + user = db.users.find_one({'username': session.get('username')}) + current_password = data.get('current_password', '').encode('utf-8') + if not bcrypt.checkpw(current_password, user['password']): + return api_error('current_password_invalid') + + new_password = data.get('new_password', '').encode('utf-8') + if not 6 <= len(new_password) <= 5000: + return api_error('invalid_new_password') + + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(new_password, salt) + session_id = os.urandom(24).hex() + + db.users.update_one({'username': session.get('username')}, { + '$set': {'password': hashed, 'session_id': session_id} + }) + + session['session_id'] = session_id + return jsonify({'status': 'ok'}) + + +@app.route(basedir + 'api/account/remove', methods=['POST']) +@limiter.limit("1 per day") +@login_required +def route_api_account_remove(): + data = request.get_json() + if not schema.validate(data, schema.delete_account): + return abort(400) + + user = db.users.find_one({'username': session.get('username')}) + password = data.get('password', '').encode('utf-8') + if not bcrypt.checkpw(password, user['password']): + return api_error('verify_password_invalid') + + db.scores.delete_many({'username': session.get('username')}) + db.users.delete_one({'username': session.get('username')}) + + session.clear() + return jsonify({'status': 'ok'}) + + +@app.route(basedir + 'api/scores/save', methods=['POST']) +@login_required +def route_api_scores_save(): + data = request.get_json() + if not schema.validate(data, schema.scores_save): + return abort(400) + + username = session.get('username') + if data.get('is_import'): + db.scores.delete_many({'username': username}) + + scores = data.get('scores', []) + for score in scores: + db.scores.update_one({'username': username, 'hash': score['hash']}, + {'$set': { + 'username': username, + 'hash': score['hash'], + 'score': score['score'] + }}, upsert=True) + + return jsonify({'status': 'ok'}) + + +@app.route(basedir + 'api/scores/get') +@login_required +def route_api_scores_get(): + username = session.get('username') + + scores = [] + for score in db.scores.find({'username': username}): + scores.append({ + 'hash': score['hash'], + 'score': score['score'] + }) + + user = db.users.find_one({'username': username}) + don = get_db_don(user) + return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don}) + + +@app.route(basedir + 'privacy') +def route_api_privacy(): + last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt'))) + integration = take_config('GOOGLE_CREDENTIALS')['gdrive_enabled'] if take_config('GOOGLE_CREDENTIALS') else False + + response = make_response(render_template('privacy.txt', last_modified=last_modified, config=get_config(), integration=integration)) + response.headers['Content-type'] = 'text/plain; charset=utf-8' + return response + + +def make_preview(song_id, song_type, song_ext, preview): + song_path = 'public/songs/%s/main.%s' % (song_id, song_ext) + prev_path = 'public/songs/%s/preview.mp3' % song_id + + if os.path.isfile(song_path) and not os.path.isfile(prev_path): + if not preview or preview <= 0: + print('Skipping #%s due to no preview' % song_id) + return False + + print('Making preview.mp3 for song #%s' % song_id) + ff = FFmpeg(inputs={song_path: '-ss %s' % preview}, + outputs={prev_path: '-codec:a libmp3lame -ar 32000 -b:a 92k -y -loglevel panic'}) + ff.run() + + return prev_path + +error_pages = take_config('ERROR_PAGES') or {} + +def create_error_page(code, url): + if url.startswith("http://") or url.startswith("https://"): + resp = requests.get(url) + if resp.status_code == 200: + app.register_error_handler(code, lambda e: (resp.content, code)) + else: + if url.startswith(basedir): + url = url[len(basedir):] + path = os.path.normpath(os.path.join("public", url)) + if os.path.isfile(path): + app.register_error_handler(code, lambda e: (send_from_directory(".", path), code)) + +for code in error_pages: + if error_pages[code]: + create_error_page(code, error_pages[code]) + +def cache_wrap(res_from, secs): + res = flask.make_response(res_from) + res.headers["Cache-Control"] = f"public, max-age={secs}, s-maxage={secs}" + res.headers["CDN-Cache-Control"] = f"max-age={secs}" + return res + +@app.route(basedir + "src/") +def send_src(ref): + return cache_wrap(flask.send_from_directory("public/src", ref), 3600) + +@app.route(basedir + "assets/") +def send_assets(ref): + return cache_wrap(flask.send_from_directory("public/assets", ref), 3600) + +@app.route(basedir + "songs/") +def send_songs(ref): + return cache_wrap(flask.send_from_directory("public/songs", ref), 604800) + +@app.route(basedir + "manifest.json") +def send_manifest(): + return cache_wrap(flask.send_from_directory("public", "manifest.json"), 3600) + +@app.route("/upload/", defaults={"ref": "index.html"}) +@app.route("/upload/") +def send_upload(ref): + return cache_wrap(flask.send_from_directory("public/upload", ref), 3600) + +@app.route("/api/upload", methods=["POST"]) +def upload_file(): + try: + # POSTリクエストにファイルの部分がない場合 + if 'file_tja' not in flask.request.files or 'file_music' not in flask.request.files: + return flask.jsonify({'error': 'リクエストにファイルの部分がありません'}) + + file_tja = flask.request.files['file_tja'] + file_music = flask.request.files['file_music'] + + # ファイルが選択されておらず空のファイルを受け取った場合 + if file_tja.filename == '' or file_music.filename == '': + return flask.jsonify({'error': 'ファイルが選択されていません'}) + + # TJAファイルをテキストUTF-8/LFに変換 + tja_data = file_tja.read() + # 尝试检测编码并转换为UTF-8 + try: + # 首先尝试UTF-8解码 + tja_text = tja_data.decode("utf-8") + except UnicodeDecodeError: + # 如果UTF-8失败,尝试其他常见编码 + for encoding in ['shift_jis', 'euc-jp', 'iso-2022-jp', 'cp932']: + try: + tja_text = tja_data.decode(encoding) + break + except UnicodeDecodeError: + continue + else: + # 如果所有编码都失败,使用错误处理 + tja_text = tja_data.decode("utf-8", errors="replace") + + # 删除回车符(CR),只保留换行符(LF) + tja_text = tja_text.replace('\r', '') + print("TJAのサイズ:",len(tja_text)) + # TJAファイルの内容を解析 + tja = tjaf.Tja(tja_text) + # TJAファイルのハッシュ値を生成 + msg = hashlib.sha256() + msg.update(tja_data) + tja_hash = msg.hexdigest() + print("TJA:",tja_hash) + # 音楽ファイルのハッシュ値を生成 + music_data = file_music.read() + msg2 = hashlib.sha256() + msg2.update(music_data) + music_hash = msg2.hexdigest() + print("音楽:",music_hash) + # IDを生成 + generated_id = f"{tja_hash}-{music_hash}" + # MongoDBのデータも作成 + db_entry = tja.to_mongo(generated_id, time.time_ns()) + pprint.pprint(db_entry) + + # mongoDBにデータをぶち込む + client['taiko']["songs"].insert_one(db_entry) + + # ディレクトリを作成 + target_dir = pathlib.Path(os.getenv("TAIKO_WEB_SONGS_DIR", "public/songs")) / generated_id + target_dir.mkdir(parents=True,exist_ok=True) + + # TJAを保存 + (target_dir / "main.tja").write_bytes(tja_data) + # 曲ファイルも保存 + (target_dir / f"main.{db_entry['music_type']}").write_bytes(music_data) + except Exception as e: + error_str = ''.join(traceback.TracebackException.from_exception(e).format()) + return flask.jsonify({'error': error_str}) + + return flask.jsonify({'success': True}) + +@app.route("/api/delete", methods=["POST"]) +@limiter.limit("1 per day") +def delete(): + id = flask.request.get_json().get('id') + 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__': + import argparse + + parser = argparse.ArgumentParser(description='Run the taiko-web development server.') + parser.add_argument('port', type=int, metavar='PORT', nargs='?', default=34801, help='Port to listen on.') + parser.add_argument('-b', '--bind-address', default='localhost', help='Bind server to address.') + parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode.') + args = parser.parse_args() + + app.run(host=args.bind_address, port=args.port, debug=args.debug) + diff --git a/config.example.py b/config.example.py new file mode 100644 index 0000000..68347c5 --- /dev/null +++ b/config.example.py @@ -0,0 +1,65 @@ +# The base URL for Taiko Web, with trailing slash. +BASEDIR = '/' + +# The full URL base asset URL, with trailing slash. +ASSETS_BASEURL = '/assets/' + +# The full URL base song URL, with trailing slash. +SONGS_BASEURL = '/songs/' + +# Multiplayer websocket URL. Defaults to /p2 if blank. +MULTIPLAYER_URL = '' + +# Send static files for custom error pages +ERROR_PAGES = { + 404: '' +} + +# The email address to display in the "About Simulator" menu. +EMAIL = None + +# Whether to use the user account system. +ACCOUNTS = True + +# Custom JavaScript file to load with the simulator. +CUSTOM_JS = '' + +# Default plugins to load with the simulator. +PLUGINS = [{ + 'url': '', + 'start': False, + 'hide': False +}] + +# Filetype to use for song previews. (mp3/ogg) +PREVIEW_TYPE = 'mp3' + +# MongoDB server settings. +MONGO = { + 'host': ['127.0.0.1:27017'], + 'database': 'taiko' +} + +# Redis server settings, used for sessions + cache. +REDIS = { + 'CACHE_TYPE': 'redis', + 'CACHE_REDIS_HOST': '127.0.0.1', + 'CACHE_REDIS_PORT': 6379, + 'CACHE_REDIS_PASSWORD': None, + 'CACHE_REDIS_DB': None +} + +# Secret key used for sessions. +SECRET_KEY = 'change-me' + +# Git repository base URL. +URL = 'https://github.com/bui/taiko-web/' + +# Google Drive API. +GOOGLE_CREDENTIALS = { + 'gdrive_enabled': False, + 'api_key': '', + 'oauth_client_id': '', + 'project_number': '', + 'min_level': None +} diff --git a/config.py b/config.py new file mode 100644 index 0000000..68347c5 --- /dev/null +++ b/config.py @@ -0,0 +1,65 @@ +# The base URL for Taiko Web, with trailing slash. +BASEDIR = '/' + +# The full URL base asset URL, with trailing slash. +ASSETS_BASEURL = '/assets/' + +# The full URL base song URL, with trailing slash. +SONGS_BASEURL = '/songs/' + +# Multiplayer websocket URL. Defaults to /p2 if blank. +MULTIPLAYER_URL = '' + +# Send static files for custom error pages +ERROR_PAGES = { + 404: '' +} + +# The email address to display in the "About Simulator" menu. +EMAIL = None + +# Whether to use the user account system. +ACCOUNTS = True + +# Custom JavaScript file to load with the simulator. +CUSTOM_JS = '' + +# Default plugins to load with the simulator. +PLUGINS = [{ + 'url': '', + 'start': False, + 'hide': False +}] + +# Filetype to use for song previews. (mp3/ogg) +PREVIEW_TYPE = 'mp3' + +# MongoDB server settings. +MONGO = { + 'host': ['127.0.0.1:27017'], + 'database': 'taiko' +} + +# Redis server settings, used for sessions + cache. +REDIS = { + 'CACHE_TYPE': 'redis', + 'CACHE_REDIS_HOST': '127.0.0.1', + 'CACHE_REDIS_PORT': 6379, + 'CACHE_REDIS_PASSWORD': None, + 'CACHE_REDIS_DB': None +} + +# Secret key used for sessions. +SECRET_KEY = 'change-me' + +# Git repository base URL. +URL = 'https://github.com/bui/taiko-web/' + +# Google Drive API. +GOOGLE_CREDENTIALS = { + 'gdrive_enabled': False, + 'api_key': '', + 'oauth_client_id': '', + 'project_number': '', + 'min_level': None +} diff --git a/public/assets/audio/bgm_result.mp3 b/public/assets/audio/bgm_result.mp3 new file mode 100644 index 0000000..c83548c Binary files /dev/null and b/public/assets/audio/bgm_result.mp3 differ diff --git a/public/assets/audio/bgm_setsume.mp3 b/public/assets/audio/bgm_setsume.mp3 new file mode 100644 index 0000000..f7e681d Binary files /dev/null and b/public/assets/audio/bgm_setsume.mp3 differ diff --git a/public/assets/audio/bgm_settings.mp3 b/public/assets/audio/bgm_settings.mp3 new file mode 100644 index 0000000..0b24a0f Binary files /dev/null and b/public/assets/audio/bgm_settings.mp3 differ diff --git a/public/assets/audio/bgm_songsel.mp3 b/public/assets/audio/bgm_songsel.mp3 new file mode 100644 index 0000000..a185ae8 Binary files /dev/null and b/public/assets/audio/bgm_songsel.mp3 differ diff --git a/public/assets/audio/neiro_1_don.ogg b/public/assets/audio/neiro_1_don.ogg new file mode 100644 index 0000000..695ffd3 Binary files /dev/null and b/public/assets/audio/neiro_1_don.ogg differ diff --git a/public/assets/audio/neiro_1_ka.ogg b/public/assets/audio/neiro_1_ka.ogg new file mode 100644 index 0000000..a78131d Binary files /dev/null and b/public/assets/audio/neiro_1_ka.ogg differ diff --git a/public/assets/audio/se_balloon.ogg b/public/assets/audio/se_balloon.ogg new file mode 100644 index 0000000..389669e Binary files /dev/null and b/public/assets/audio/se_balloon.ogg differ diff --git a/public/assets/audio/se_calibration.ogg b/public/assets/audio/se_calibration.ogg new file mode 100644 index 0000000..a253123 Binary files /dev/null and b/public/assets/audio/se_calibration.ogg differ diff --git a/public/assets/audio/se_cancel.ogg b/public/assets/audio/se_cancel.ogg new file mode 100644 index 0000000..d8956aa Binary files /dev/null and b/public/assets/audio/se_cancel.ogg differ diff --git a/public/assets/audio/se_don.ogg b/public/assets/audio/se_don.ogg new file mode 100644 index 0000000..b2b494c Binary files /dev/null and b/public/assets/audio/se_don.ogg differ diff --git a/public/assets/audio/se_gameclear.ogg b/public/assets/audio/se_gameclear.ogg new file mode 100644 index 0000000..abd7327 Binary files /dev/null and b/public/assets/audio/se_gameclear.ogg differ diff --git a/public/assets/audio/se_gamefail.ogg b/public/assets/audio/se_gamefail.ogg new file mode 100644 index 0000000..fbad0b6 Binary files /dev/null and b/public/assets/audio/se_gamefail.ogg differ diff --git a/public/assets/audio/se_gamefullcombo.ogg b/public/assets/audio/se_gamefullcombo.ogg new file mode 100644 index 0000000..3657f43 Binary files /dev/null and b/public/assets/audio/se_gamefullcombo.ogg differ diff --git a/public/assets/audio/se_jump.ogg b/public/assets/audio/se_jump.ogg new file mode 100644 index 0000000..cdd5f94 Binary files /dev/null and b/public/assets/audio/se_jump.ogg differ diff --git a/public/assets/audio/se_ka.ogg b/public/assets/audio/se_ka.ogg new file mode 100644 index 0000000..3de2ff9 Binary files /dev/null and b/public/assets/audio/se_ka.ogg differ diff --git a/public/assets/audio/se_pause.ogg b/public/assets/audio/se_pause.ogg new file mode 100644 index 0000000..64ca5a4 Binary files /dev/null and b/public/assets/audio/se_pause.ogg differ diff --git a/public/assets/audio/se_results_countup.ogg b/public/assets/audio/se_results_countup.ogg new file mode 100644 index 0000000..4321187 Binary files /dev/null and b/public/assets/audio/se_results_countup.ogg differ diff --git a/public/assets/audio/se_results_crown.ogg b/public/assets/audio/se_results_crown.ogg new file mode 100644 index 0000000..7f938cd Binary files /dev/null and b/public/assets/audio/se_results_crown.ogg differ diff --git a/public/assets/audio/v_combo_100.ogg b/public/assets/audio/v_combo_100.ogg new file mode 100644 index 0000000..b3b1063 Binary files /dev/null and b/public/assets/audio/v_combo_100.ogg differ diff --git a/public/assets/audio/v_combo_1000.ogg b/public/assets/audio/v_combo_1000.ogg new file mode 100644 index 0000000..4a60156 Binary files /dev/null and b/public/assets/audio/v_combo_1000.ogg differ diff --git a/public/assets/audio/v_combo_1100.ogg b/public/assets/audio/v_combo_1100.ogg new file mode 100644 index 0000000..a6b6647 Binary files /dev/null and b/public/assets/audio/v_combo_1100.ogg differ diff --git a/public/assets/audio/v_combo_1200.ogg b/public/assets/audio/v_combo_1200.ogg new file mode 100644 index 0000000..2175617 Binary files /dev/null and b/public/assets/audio/v_combo_1200.ogg differ diff --git a/public/assets/audio/v_combo_1300.ogg b/public/assets/audio/v_combo_1300.ogg new file mode 100644 index 0000000..62808c7 Binary files /dev/null and b/public/assets/audio/v_combo_1300.ogg differ diff --git a/public/assets/audio/v_combo_1400.ogg b/public/assets/audio/v_combo_1400.ogg new file mode 100644 index 0000000..3f575de Binary files /dev/null and b/public/assets/audio/v_combo_1400.ogg differ diff --git a/public/assets/audio/v_combo_1500.ogg b/public/assets/audio/v_combo_1500.ogg new file mode 100644 index 0000000..e0e6641 Binary files /dev/null and b/public/assets/audio/v_combo_1500.ogg differ diff --git a/public/assets/audio/v_combo_1600.ogg b/public/assets/audio/v_combo_1600.ogg new file mode 100644 index 0000000..f3657f2 Binary files /dev/null and b/public/assets/audio/v_combo_1600.ogg differ diff --git a/public/assets/audio/v_combo_1700.ogg b/public/assets/audio/v_combo_1700.ogg new file mode 100644 index 0000000..1545b23 Binary files /dev/null and b/public/assets/audio/v_combo_1700.ogg differ diff --git a/public/assets/audio/v_combo_1800.ogg b/public/assets/audio/v_combo_1800.ogg new file mode 100644 index 0000000..1fdc880 Binary files /dev/null and b/public/assets/audio/v_combo_1800.ogg differ diff --git a/public/assets/audio/v_combo_1900.ogg b/public/assets/audio/v_combo_1900.ogg new file mode 100644 index 0000000..5009f0a Binary files /dev/null and b/public/assets/audio/v_combo_1900.ogg differ diff --git a/public/assets/audio/v_combo_200.ogg b/public/assets/audio/v_combo_200.ogg new file mode 100644 index 0000000..a97a8f5 Binary files /dev/null and b/public/assets/audio/v_combo_200.ogg differ diff --git a/public/assets/audio/v_combo_2000.ogg b/public/assets/audio/v_combo_2000.ogg new file mode 100644 index 0000000..e768c06 Binary files /dev/null and b/public/assets/audio/v_combo_2000.ogg differ diff --git a/public/assets/audio/v_combo_2100.ogg b/public/assets/audio/v_combo_2100.ogg new file mode 100644 index 0000000..ee773d0 Binary files /dev/null and b/public/assets/audio/v_combo_2100.ogg differ diff --git a/public/assets/audio/v_combo_2200.ogg b/public/assets/audio/v_combo_2200.ogg new file mode 100644 index 0000000..5817792 Binary files /dev/null and b/public/assets/audio/v_combo_2200.ogg differ diff --git a/public/assets/audio/v_combo_2300.ogg b/public/assets/audio/v_combo_2300.ogg new file mode 100644 index 0000000..48ea577 Binary files /dev/null and b/public/assets/audio/v_combo_2300.ogg differ diff --git a/public/assets/audio/v_combo_2400.ogg b/public/assets/audio/v_combo_2400.ogg new file mode 100644 index 0000000..f979823 Binary files /dev/null and b/public/assets/audio/v_combo_2400.ogg differ diff --git a/public/assets/audio/v_combo_2500.ogg b/public/assets/audio/v_combo_2500.ogg new file mode 100644 index 0000000..33450f9 Binary files /dev/null and b/public/assets/audio/v_combo_2500.ogg differ diff --git a/public/assets/audio/v_combo_2600.ogg b/public/assets/audio/v_combo_2600.ogg new file mode 100644 index 0000000..743cea9 Binary files /dev/null and b/public/assets/audio/v_combo_2600.ogg differ diff --git a/public/assets/audio/v_combo_2700.ogg b/public/assets/audio/v_combo_2700.ogg new file mode 100644 index 0000000..e02ab02 Binary files /dev/null and b/public/assets/audio/v_combo_2700.ogg differ diff --git a/public/assets/audio/v_combo_2800.ogg b/public/assets/audio/v_combo_2800.ogg new file mode 100644 index 0000000..d54a9e3 Binary files /dev/null and b/public/assets/audio/v_combo_2800.ogg differ diff --git a/public/assets/audio/v_combo_2900.ogg b/public/assets/audio/v_combo_2900.ogg new file mode 100644 index 0000000..7d7286c Binary files /dev/null and b/public/assets/audio/v_combo_2900.ogg differ diff --git a/public/assets/audio/v_combo_300.ogg b/public/assets/audio/v_combo_300.ogg new file mode 100644 index 0000000..2f1d09c Binary files /dev/null and b/public/assets/audio/v_combo_300.ogg differ diff --git a/public/assets/audio/v_combo_3000.ogg b/public/assets/audio/v_combo_3000.ogg new file mode 100644 index 0000000..23afa1f Binary files /dev/null and b/public/assets/audio/v_combo_3000.ogg differ diff --git a/public/assets/audio/v_combo_3100.ogg b/public/assets/audio/v_combo_3100.ogg new file mode 100644 index 0000000..2c888c6 Binary files /dev/null and b/public/assets/audio/v_combo_3100.ogg differ diff --git a/public/assets/audio/v_combo_3200.ogg b/public/assets/audio/v_combo_3200.ogg new file mode 100644 index 0000000..128b287 Binary files /dev/null and b/public/assets/audio/v_combo_3200.ogg differ diff --git a/public/assets/audio/v_combo_3300.ogg b/public/assets/audio/v_combo_3300.ogg new file mode 100644 index 0000000..312c496 Binary files /dev/null and b/public/assets/audio/v_combo_3300.ogg differ diff --git a/public/assets/audio/v_combo_3400.ogg b/public/assets/audio/v_combo_3400.ogg new file mode 100644 index 0000000..42039d8 Binary files /dev/null and b/public/assets/audio/v_combo_3400.ogg differ diff --git a/public/assets/audio/v_combo_3500.ogg b/public/assets/audio/v_combo_3500.ogg new file mode 100644 index 0000000..47fe5e0 Binary files /dev/null and b/public/assets/audio/v_combo_3500.ogg differ diff --git a/public/assets/audio/v_combo_3600.ogg b/public/assets/audio/v_combo_3600.ogg new file mode 100644 index 0000000..fe1e18d Binary files /dev/null and b/public/assets/audio/v_combo_3600.ogg differ diff --git a/public/assets/audio/v_combo_3700.ogg b/public/assets/audio/v_combo_3700.ogg new file mode 100644 index 0000000..2875e76 Binary files /dev/null and b/public/assets/audio/v_combo_3700.ogg differ diff --git a/public/assets/audio/v_combo_3800.ogg b/public/assets/audio/v_combo_3800.ogg new file mode 100644 index 0000000..93a35da Binary files /dev/null and b/public/assets/audio/v_combo_3800.ogg differ diff --git a/public/assets/audio/v_combo_3900.ogg b/public/assets/audio/v_combo_3900.ogg new file mode 100644 index 0000000..adb954f Binary files /dev/null and b/public/assets/audio/v_combo_3900.ogg differ diff --git a/public/assets/audio/v_combo_400.ogg b/public/assets/audio/v_combo_400.ogg new file mode 100644 index 0000000..b3116eb Binary files /dev/null and b/public/assets/audio/v_combo_400.ogg differ diff --git a/public/assets/audio/v_combo_4000.ogg b/public/assets/audio/v_combo_4000.ogg new file mode 100644 index 0000000..28b0bd6 Binary files /dev/null and b/public/assets/audio/v_combo_4000.ogg differ diff --git a/public/assets/audio/v_combo_4100.ogg b/public/assets/audio/v_combo_4100.ogg new file mode 100644 index 0000000..b11f7b5 Binary files /dev/null and b/public/assets/audio/v_combo_4100.ogg differ diff --git a/public/assets/audio/v_combo_4200.ogg b/public/assets/audio/v_combo_4200.ogg new file mode 100644 index 0000000..206766a Binary files /dev/null and b/public/assets/audio/v_combo_4200.ogg differ diff --git a/public/assets/audio/v_combo_4300.ogg b/public/assets/audio/v_combo_4300.ogg new file mode 100644 index 0000000..7cffb6f Binary files /dev/null and b/public/assets/audio/v_combo_4300.ogg differ diff --git a/public/assets/audio/v_combo_4400.ogg b/public/assets/audio/v_combo_4400.ogg new file mode 100644 index 0000000..6315a6a Binary files /dev/null and b/public/assets/audio/v_combo_4400.ogg differ diff --git a/public/assets/audio/v_combo_4500.ogg b/public/assets/audio/v_combo_4500.ogg new file mode 100644 index 0000000..41064a9 Binary files /dev/null and b/public/assets/audio/v_combo_4500.ogg differ diff --git a/public/assets/audio/v_combo_4600.ogg b/public/assets/audio/v_combo_4600.ogg new file mode 100644 index 0000000..55ce69f Binary files /dev/null and b/public/assets/audio/v_combo_4600.ogg differ diff --git a/public/assets/audio/v_combo_4700.ogg b/public/assets/audio/v_combo_4700.ogg new file mode 100644 index 0000000..b05c270 Binary files /dev/null and b/public/assets/audio/v_combo_4700.ogg differ diff --git a/public/assets/audio/v_combo_4800.ogg b/public/assets/audio/v_combo_4800.ogg new file mode 100644 index 0000000..95f26f6 Binary files /dev/null and b/public/assets/audio/v_combo_4800.ogg differ diff --git a/public/assets/audio/v_combo_4900.ogg b/public/assets/audio/v_combo_4900.ogg new file mode 100644 index 0000000..7de2f55 Binary files /dev/null and b/public/assets/audio/v_combo_4900.ogg differ diff --git a/public/assets/audio/v_combo_50.ogg b/public/assets/audio/v_combo_50.ogg new file mode 100644 index 0000000..b960fd5 Binary files /dev/null and b/public/assets/audio/v_combo_50.ogg differ diff --git a/public/assets/audio/v_combo_500.ogg b/public/assets/audio/v_combo_500.ogg new file mode 100644 index 0000000..2fc66ee Binary files /dev/null and b/public/assets/audio/v_combo_500.ogg differ diff --git a/public/assets/audio/v_combo_5000.ogg b/public/assets/audio/v_combo_5000.ogg new file mode 100644 index 0000000..038296f Binary files /dev/null and b/public/assets/audio/v_combo_5000.ogg differ diff --git a/public/assets/audio/v_combo_600.ogg b/public/assets/audio/v_combo_600.ogg new file mode 100644 index 0000000..40b6980 Binary files /dev/null and b/public/assets/audio/v_combo_600.ogg differ diff --git a/public/assets/audio/v_combo_700.ogg b/public/assets/audio/v_combo_700.ogg new file mode 100644 index 0000000..2be6254 Binary files /dev/null and b/public/assets/audio/v_combo_700.ogg differ diff --git a/public/assets/audio/v_combo_800.ogg b/public/assets/audio/v_combo_800.ogg new file mode 100644 index 0000000..212b50c Binary files /dev/null and b/public/assets/audio/v_combo_800.ogg differ diff --git a/public/assets/audio/v_combo_900.ogg b/public/assets/audio/v_combo_900.ogg new file mode 100644 index 0000000..0b9bd70 Binary files /dev/null and b/public/assets/audio/v_combo_900.ogg differ diff --git a/public/assets/audio/v_diffsel.ogg b/public/assets/audio/v_diffsel.ogg new file mode 100644 index 0000000..1de6b35 Binary files /dev/null and b/public/assets/audio/v_diffsel.ogg differ diff --git a/public/assets/audio/v_fullcombo.ogg b/public/assets/audio/v_fullcombo.ogg new file mode 100644 index 0000000..5883eb2 Binary files /dev/null and b/public/assets/audio/v_fullcombo.ogg differ diff --git a/public/assets/audio/v_renda.ogg b/public/assets/audio/v_renda.ogg new file mode 100644 index 0000000..42f789e Binary files /dev/null and b/public/assets/audio/v_renda.ogg differ diff --git a/public/assets/audio/v_results.ogg b/public/assets/audio/v_results.ogg new file mode 100644 index 0000000..679f74e Binary files /dev/null and b/public/assets/audio/v_results.ogg differ diff --git a/public/assets/audio/v_results_fullcombo.ogg b/public/assets/audio/v_results_fullcombo.ogg new file mode 100644 index 0000000..162b692 Binary files /dev/null and b/public/assets/audio/v_results_fullcombo.ogg differ diff --git a/public/assets/audio/v_results_fullcombo2.ogg b/public/assets/audio/v_results_fullcombo2.ogg new file mode 100644 index 0000000..739248b Binary files /dev/null and b/public/assets/audio/v_results_fullcombo2.ogg differ diff --git a/public/assets/audio/v_sanka.ogg b/public/assets/audio/v_sanka.ogg new file mode 100644 index 0000000..90ef29b Binary files /dev/null and b/public/assets/audio/v_sanka.ogg differ diff --git a/public/assets/audio/v_songsel.ogg b/public/assets/audio/v_songsel.ogg new file mode 100644 index 0000000..4bcb90a Binary files /dev/null and b/public/assets/audio/v_songsel.ogg differ diff --git a/public/assets/audio/v_start.ogg b/public/assets/audio/v_start.ogg new file mode 100644 index 0000000..0bf2206 Binary files /dev/null and b/public/assets/audio/v_start.ogg differ diff --git a/public/assets/audio/v_title.ogg b/public/assets/audio/v_title.ogg new file mode 100644 index 0000000..50877ed Binary files /dev/null and b/public/assets/audio/v_title.ogg differ diff --git a/public/assets/fonts/Kozuka.otf b/public/assets/fonts/Kozuka.otf new file mode 100644 index 0000000..b405dc7 Binary files /dev/null and b/public/assets/fonts/Kozuka.otf differ diff --git a/public/assets/fonts/TnT.ttf b/public/assets/fonts/TnT.ttf new file mode 100644 index 0000000..d06d749 Binary files /dev/null and b/public/assets/fonts/TnT.ttf differ diff --git a/public/assets/img/badge_auto.png b/public/assets/img/badge_auto.png new file mode 100644 index 0000000..cbaa4b6 Binary files /dev/null and b/public/assets/img/badge_auto.png differ diff --git a/public/assets/img/balloon.png b/public/assets/img/balloon.png new file mode 100644 index 0000000..e2a7061 Binary files /dev/null and b/public/assets/img/balloon.png differ diff --git a/public/assets/img/bg-pattern-1.png b/public/assets/img/bg-pattern-1.png new file mode 100644 index 0000000..6cacd6c Binary files /dev/null and b/public/assets/img/bg-pattern-1.png differ diff --git a/public/assets/img/bg_don2_1a.png b/public/assets/img/bg_don2_1a.png new file mode 100644 index 0000000..ed5ab2b Binary files /dev/null and b/public/assets/img/bg_don2_1a.png differ diff --git a/public/assets/img/bg_don2_1b.png b/public/assets/img/bg_don2_1b.png new file mode 100644 index 0000000..1403d1f Binary files /dev/null and b/public/assets/img/bg_don2_1b.png differ diff --git a/public/assets/img/bg_don2_2a.png b/public/assets/img/bg_don2_2a.png new file mode 100644 index 0000000..87e83e7 Binary files /dev/null and b/public/assets/img/bg_don2_2a.png differ diff --git a/public/assets/img/bg_don2_2b.png b/public/assets/img/bg_don2_2b.png new file mode 100644 index 0000000..fe99167 Binary files /dev/null and b/public/assets/img/bg_don2_2b.png differ diff --git a/public/assets/img/bg_don2_3a.png b/public/assets/img/bg_don2_3a.png new file mode 100644 index 0000000..e1813ed Binary files /dev/null and b/public/assets/img/bg_don2_3a.png differ diff --git a/public/assets/img/bg_don2_3b.png b/public/assets/img/bg_don2_3b.png new file mode 100644 index 0000000..ea6ca4d Binary files /dev/null and b/public/assets/img/bg_don2_3b.png differ diff --git a/public/assets/img/bg_don2_4a.png b/public/assets/img/bg_don2_4a.png new file mode 100644 index 0000000..331257c Binary files /dev/null and b/public/assets/img/bg_don2_4a.png differ diff --git a/public/assets/img/bg_don2_4b.png b/public/assets/img/bg_don2_4b.png new file mode 100644 index 0000000..d79d600 Binary files /dev/null and b/public/assets/img/bg_don2_4b.png differ diff --git a/public/assets/img/bg_don2_5a.png b/public/assets/img/bg_don2_5a.png new file mode 100644 index 0000000..87352bd Binary files /dev/null and b/public/assets/img/bg_don2_5a.png differ diff --git a/public/assets/img/bg_don2_5b.png b/public/assets/img/bg_don2_5b.png new file mode 100644 index 0000000..4aef2ec Binary files /dev/null and b/public/assets/img/bg_don2_5b.png differ diff --git a/public/assets/img/bg_don2_6a.png b/public/assets/img/bg_don2_6a.png new file mode 100644 index 0000000..d7bbb99 Binary files /dev/null and b/public/assets/img/bg_don2_6a.png differ diff --git a/public/assets/img/bg_don2_6b.png b/public/assets/img/bg_don2_6b.png new file mode 100644 index 0000000..8336925 Binary files /dev/null and b/public/assets/img/bg_don2_6b.png differ diff --git a/public/assets/img/bg_don_1a.png b/public/assets/img/bg_don_1a.png new file mode 100644 index 0000000..ac9e7b9 Binary files /dev/null and b/public/assets/img/bg_don_1a.png differ diff --git a/public/assets/img/bg_don_1b.png b/public/assets/img/bg_don_1b.png new file mode 100644 index 0000000..461e82a Binary files /dev/null and b/public/assets/img/bg_don_1b.png differ diff --git a/public/assets/img/bg_don_2a.png b/public/assets/img/bg_don_2a.png new file mode 100644 index 0000000..5fe7fab Binary files /dev/null and b/public/assets/img/bg_don_2a.png differ diff --git a/public/assets/img/bg_don_2b.png b/public/assets/img/bg_don_2b.png new file mode 100644 index 0000000..4043bde Binary files /dev/null and b/public/assets/img/bg_don_2b.png differ diff --git a/public/assets/img/bg_don_3a.png b/public/assets/img/bg_don_3a.png new file mode 100644 index 0000000..8c74f41 Binary files /dev/null and b/public/assets/img/bg_don_3a.png differ diff --git a/public/assets/img/bg_don_3b.png b/public/assets/img/bg_don_3b.png new file mode 100644 index 0000000..f3019c3 Binary files /dev/null and b/public/assets/img/bg_don_3b.png differ diff --git a/public/assets/img/bg_don_4a.png b/public/assets/img/bg_don_4a.png new file mode 100644 index 0000000..daf785c Binary files /dev/null and b/public/assets/img/bg_don_4a.png differ diff --git a/public/assets/img/bg_don_4b.png b/public/assets/img/bg_don_4b.png new file mode 100644 index 0000000..51b6e76 Binary files /dev/null and b/public/assets/img/bg_don_4b.png differ diff --git a/public/assets/img/bg_don_5a.png b/public/assets/img/bg_don_5a.png new file mode 100644 index 0000000..bb6be3a Binary files /dev/null and b/public/assets/img/bg_don_5a.png differ diff --git a/public/assets/img/bg_don_5b.png b/public/assets/img/bg_don_5b.png new file mode 100644 index 0000000..7272d05 Binary files /dev/null and b/public/assets/img/bg_don_5b.png differ diff --git a/public/assets/img/bg_don_6a.png b/public/assets/img/bg_don_6a.png new file mode 100644 index 0000000..3d7e0e1 Binary files /dev/null and b/public/assets/img/bg_don_6a.png differ diff --git a/public/assets/img/bg_don_6b.png b/public/assets/img/bg_don_6b.png new file mode 100644 index 0000000..4b27e62 Binary files /dev/null and b/public/assets/img/bg_don_6b.png differ diff --git a/public/assets/img/bg_genre_0.png b/public/assets/img/bg_genre_0.png new file mode 100644 index 0000000..ed6dca8 Binary files /dev/null and b/public/assets/img/bg_genre_0.png differ diff --git a/public/assets/img/bg_genre_1.png b/public/assets/img/bg_genre_1.png new file mode 100644 index 0000000..a162130 Binary files /dev/null and b/public/assets/img/bg_genre_1.png differ diff --git a/public/assets/img/bg_genre_2.png b/public/assets/img/bg_genre_2.png new file mode 100644 index 0000000..9d171bc Binary files /dev/null and b/public/assets/img/bg_genre_2.png differ diff --git a/public/assets/img/bg_genre_3.png b/public/assets/img/bg_genre_3.png new file mode 100644 index 0000000..45fdd5e Binary files /dev/null and b/public/assets/img/bg_genre_3.png differ diff --git a/public/assets/img/bg_genre_4.png b/public/assets/img/bg_genre_4.png new file mode 100644 index 0000000..2e6672d Binary files /dev/null and b/public/assets/img/bg_genre_4.png differ diff --git a/public/assets/img/bg_genre_5.png b/public/assets/img/bg_genre_5.png new file mode 100644 index 0000000..c63b5d4 Binary files /dev/null and b/public/assets/img/bg_genre_5.png differ diff --git a/public/assets/img/bg_genre_6.png b/public/assets/img/bg_genre_6.png new file mode 100644 index 0000000..ba778f0 Binary files /dev/null and b/public/assets/img/bg_genre_6.png differ diff --git a/public/assets/img/bg_genre_def.png b/public/assets/img/bg_genre_def.png new file mode 100644 index 0000000..01b3175 Binary files /dev/null and b/public/assets/img/bg_genre_def.png differ diff --git a/public/assets/img/bg_pause.png b/public/assets/img/bg_pause.png new file mode 100644 index 0000000..2952338 Binary files /dev/null and b/public/assets/img/bg_pause.png differ diff --git a/public/assets/img/bg_score_p1.png b/public/assets/img/bg_score_p1.png new file mode 100644 index 0000000..1f0e00d Binary files /dev/null and b/public/assets/img/bg_score_p1.png differ diff --git a/public/assets/img/bg_score_p2.png b/public/assets/img/bg_score_p2.png new file mode 100644 index 0000000..0d5e1c7 Binary files /dev/null and b/public/assets/img/bg_score_p2.png differ diff --git a/public/assets/img/bg_search.png b/public/assets/img/bg_search.png new file mode 100644 index 0000000..5878cb8 Binary files /dev/null and b/public/assets/img/bg_search.png differ diff --git a/public/assets/img/bg_settings.png b/public/assets/img/bg_settings.png new file mode 100644 index 0000000..3ff3b09 Binary files /dev/null and b/public/assets/img/bg_settings.png differ diff --git a/public/assets/img/bg_song_1a.png b/public/assets/img/bg_song_1a.png new file mode 100644 index 0000000..ea6b10b Binary files /dev/null and b/public/assets/img/bg_song_1a.png differ diff --git a/public/assets/img/bg_song_1b.png b/public/assets/img/bg_song_1b.png new file mode 100644 index 0000000..b78f0dd Binary files /dev/null and b/public/assets/img/bg_song_1b.png differ diff --git a/public/assets/img/bg_song_2a.png b/public/assets/img/bg_song_2a.png new file mode 100644 index 0000000..e953758 Binary files /dev/null and b/public/assets/img/bg_song_2a.png differ diff --git a/public/assets/img/bg_song_2b.png b/public/assets/img/bg_song_2b.png new file mode 100644 index 0000000..719843a Binary files /dev/null and b/public/assets/img/bg_song_2b.png differ diff --git a/public/assets/img/bg_song_3a.png b/public/assets/img/bg_song_3a.png new file mode 100644 index 0000000..152fa91 Binary files /dev/null and b/public/assets/img/bg_song_3a.png differ diff --git a/public/assets/img/bg_song_3b.png b/public/assets/img/bg_song_3b.png new file mode 100644 index 0000000..cdd2d87 Binary files /dev/null and b/public/assets/img/bg_song_3b.png differ diff --git a/public/assets/img/bg_song_4a.png b/public/assets/img/bg_song_4a.png new file mode 100644 index 0000000..235ba8c Binary files /dev/null and b/public/assets/img/bg_song_4a.png differ diff --git a/public/assets/img/bg_song_4b.png b/public/assets/img/bg_song_4b.png new file mode 100644 index 0000000..a05961c Binary files /dev/null and b/public/assets/img/bg_song_4b.png differ diff --git a/public/assets/img/bg_song_5a.png b/public/assets/img/bg_song_5a.png new file mode 100644 index 0000000..982ea2d Binary files /dev/null and b/public/assets/img/bg_song_5a.png differ diff --git a/public/assets/img/bg_song_5b.png b/public/assets/img/bg_song_5b.png new file mode 100644 index 0000000..b1bef6e Binary files /dev/null and b/public/assets/img/bg_song_5b.png differ diff --git a/public/assets/img/bg_stage_1.png b/public/assets/img/bg_stage_1.png new file mode 100644 index 0000000..2df97d1 Binary files /dev/null and b/public/assets/img/bg_stage_1.png differ diff --git a/public/assets/img/bg_stage_2.png b/public/assets/img/bg_stage_2.png new file mode 100644 index 0000000..e8a5be8 Binary files /dev/null and b/public/assets/img/bg_stage_2.png differ diff --git a/public/assets/img/bg_stage_3.png b/public/assets/img/bg_stage_3.png new file mode 100644 index 0000000..9ee5266 Binary files /dev/null and b/public/assets/img/bg_stage_3.png differ diff --git a/public/assets/img/crown.png b/public/assets/img/crown.png new file mode 100644 index 0000000..10d8904 Binary files /dev/null and b/public/assets/img/crown.png differ diff --git a/public/assets/img/dancing-don.gif b/public/assets/img/dancing-don.gif new file mode 100644 index 0000000..833ae36 Binary files /dev/null and b/public/assets/img/dancing-don.gif differ diff --git a/public/assets/img/difficulty.png b/public/assets/img/difficulty.png new file mode 100644 index 0000000..cce0d4f Binary files /dev/null and b/public/assets/img/difficulty.png differ diff --git a/public/assets/img/don_anim_10combo_a.png b/public/assets/img/don_anim_10combo_a.png new file mode 100644 index 0000000..26d5d46 Binary files /dev/null and b/public/assets/img/don_anim_10combo_a.png differ diff --git a/public/assets/img/don_anim_10combo_b1.png b/public/assets/img/don_anim_10combo_b1.png new file mode 100644 index 0000000..d661c0f Binary files /dev/null and b/public/assets/img/don_anim_10combo_b1.png differ diff --git a/public/assets/img/don_anim_10combo_b2.png b/public/assets/img/don_anim_10combo_b2.png new file mode 100644 index 0000000..0f6dc94 Binary files /dev/null and b/public/assets/img/don_anim_10combo_b2.png differ diff --git a/public/assets/img/don_anim_clear_a.png b/public/assets/img/don_anim_clear_a.png new file mode 100644 index 0000000..41fc58e Binary files /dev/null and b/public/assets/img/don_anim_clear_a.png differ diff --git a/public/assets/img/don_anim_clear_b1.png b/public/assets/img/don_anim_clear_b1.png new file mode 100644 index 0000000..4898266 Binary files /dev/null and b/public/assets/img/don_anim_clear_b1.png differ diff --git a/public/assets/img/don_anim_clear_b2.png b/public/assets/img/don_anim_clear_b2.png new file mode 100644 index 0000000..d26b594 Binary files /dev/null and b/public/assets/img/don_anim_clear_b2.png differ diff --git a/public/assets/img/don_anim_gogo_a.png b/public/assets/img/don_anim_gogo_a.png new file mode 100644 index 0000000..8e7cb22 Binary files /dev/null and b/public/assets/img/don_anim_gogo_a.png differ diff --git a/public/assets/img/don_anim_gogo_b1.png b/public/assets/img/don_anim_gogo_b1.png new file mode 100644 index 0000000..2e4b2a1 Binary files /dev/null and b/public/assets/img/don_anim_gogo_b1.png differ diff --git a/public/assets/img/don_anim_gogo_b2.png b/public/assets/img/don_anim_gogo_b2.png new file mode 100644 index 0000000..2141e07 Binary files /dev/null and b/public/assets/img/don_anim_gogo_b2.png differ diff --git a/public/assets/img/don_anim_gogostart_a.png b/public/assets/img/don_anim_gogostart_a.png new file mode 100644 index 0000000..c87aa52 Binary files /dev/null and b/public/assets/img/don_anim_gogostart_a.png differ diff --git a/public/assets/img/don_anim_gogostart_b1.png b/public/assets/img/don_anim_gogostart_b1.png new file mode 100644 index 0000000..4075b7d Binary files /dev/null and b/public/assets/img/don_anim_gogostart_b1.png differ diff --git a/public/assets/img/don_anim_gogostart_b2.png b/public/assets/img/don_anim_gogostart_b2.png new file mode 100644 index 0000000..2453f8b Binary files /dev/null and b/public/assets/img/don_anim_gogostart_b2.png differ diff --git a/public/assets/img/don_anim_normal_a.png b/public/assets/img/don_anim_normal_a.png new file mode 100644 index 0000000..494e0ca Binary files /dev/null and b/public/assets/img/don_anim_normal_a.png differ diff --git a/public/assets/img/don_anim_normal_b1.png b/public/assets/img/don_anim_normal_b1.png new file mode 100644 index 0000000..37c283f Binary files /dev/null and b/public/assets/img/don_anim_normal_b1.png differ diff --git a/public/assets/img/don_anim_normal_b2.png b/public/assets/img/don_anim_normal_b2.png new file mode 100644 index 0000000..4f8927e Binary files /dev/null and b/public/assets/img/don_anim_normal_b2.png differ diff --git a/public/assets/img/favicon.png b/public/assets/img/favicon.png new file mode 100644 index 0000000..9c06c33 Binary files /dev/null and b/public/assets/img/favicon.png differ diff --git a/public/assets/img/fire_anim.png b/public/assets/img/fire_anim.png new file mode 100644 index 0000000..4760699 Binary files /dev/null and b/public/assets/img/fire_anim.png differ diff --git a/public/assets/img/fireworks_anim.png b/public/assets/img/fireworks_anim.png new file mode 100644 index 0000000..ffa78c3 Binary files /dev/null and b/public/assets/img/fireworks_anim.png differ diff --git a/public/assets/img/mimizu.png b/public/assets/img/mimizu.png new file mode 100644 index 0000000..c149707 Binary files /dev/null and b/public/assets/img/mimizu.png differ diff --git a/public/assets/img/miss.png b/public/assets/img/miss.png new file mode 100644 index 0000000..f6d0ec3 Binary files /dev/null and b/public/assets/img/miss.png differ diff --git a/public/assets/img/notes.png b/public/assets/img/notes.png new file mode 100644 index 0000000..8b57212 Binary files /dev/null and b/public/assets/img/notes.png differ diff --git a/public/assets/img/notes_drumroll.png b/public/assets/img/notes_drumroll.png new file mode 100644 index 0000000..0466f1c Binary files /dev/null and b/public/assets/img/notes_drumroll.png differ diff --git a/public/assets/img/notes_explosion.png b/public/assets/img/notes_explosion.png new file mode 100644 index 0000000..421df21 Binary files /dev/null and b/public/assets/img/notes_explosion.png differ diff --git a/public/assets/img/notes_hit.png b/public/assets/img/notes_hit.png new file mode 100644 index 0000000..3d4c16a Binary files /dev/null and b/public/assets/img/notes_hit.png differ diff --git a/public/assets/img/results_flowers.png b/public/assets/img/results_flowers.png new file mode 100644 index 0000000..785a293 Binary files /dev/null and b/public/assets/img/results_flowers.png differ diff --git a/public/assets/img/results_mikoshi.png b/public/assets/img/results_mikoshi.png new file mode 100644 index 0000000..03ef9a8 Binary files /dev/null and b/public/assets/img/results_mikoshi.png differ diff --git a/public/assets/img/results_tetsuohana.png b/public/assets/img/results_tetsuohana.png new file mode 100644 index 0000000..2be2786 Binary files /dev/null and b/public/assets/img/results_tetsuohana.png differ diff --git a/public/assets/img/results_tetsuohana2.png b/public/assets/img/results_tetsuohana2.png new file mode 100644 index 0000000..16a5304 Binary files /dev/null and b/public/assets/img/results_tetsuohana2.png differ diff --git a/public/assets/img/settings_gamepad.png b/public/assets/img/settings_gamepad.png new file mode 100644 index 0000000..dc0d438 Binary files /dev/null and b/public/assets/img/settings_gamepad.png differ diff --git a/public/assets/img/taiko.png b/public/assets/img/taiko.png new file mode 100644 index 0000000..2f8195b Binary files /dev/null and b/public/assets/img/taiko.png differ diff --git a/public/assets/img/title-screen.png b/public/assets/img/title-screen.png new file mode 100644 index 0000000..72354f6 Binary files /dev/null and b/public/assets/img/title-screen.png differ diff --git a/public/assets/img/touch_drum.png b/public/assets/img/touch_drum.png new file mode 100644 index 0000000..3622f85 Binary files /dev/null and b/public/assets/img/touch_drum.png differ diff --git a/public/assets/img/touch_fullscreen.png b/public/assets/img/touch_fullscreen.png new file mode 100644 index 0000000..917999f Binary files /dev/null and b/public/assets/img/touch_fullscreen.png differ diff --git a/public/assets/img/touch_pause.png b/public/assets/img/touch_pause.png new file mode 100644 index 0000000..ac4b80c Binary files /dev/null and b/public/assets/img/touch_pause.png differ diff --git a/public/assets/img/vectors.json b/public/assets/img/vectors.json new file mode 100644 index 0000000..4f4ef50 --- /dev/null +++ b/public/assets/img/vectors.json @@ -0,0 +1,29 @@ +{ + "diffStar": "M3 17 5 11 0 6h6l3-6 3 6h6l-5 5 2 6-6-3", + "longVowelMark": "m1 5c2 3 1 17 .5 25 0 5 6 5 6.5 0C9 22 9 6 7 3 4-2-1 2 1 5", + "diffEasy1": "m27 10c9-9 21 9 5 11 10 9-6 18-12 7C14 39-2 30 8 21-8 19 4 1 13 10 6-4 34-3 27 10Z", + "diffEasy2": "m12 15c5 1 7 0 8-4 1 4 3 5 8 4-4 3-4 5-2 8-4-4-8-4-12 0 2.2-3 2-5-2-8", + "diffNormal": "m24 0c-3 0-4 3-5 6-2 6-2 11 0 17 0 0 1 4 5 8 4-4 5-8 5-8C31 17 31 12 29 6 28 3 27 0 24 0M37 2c4 3 7 6 8 8 2 4 3 6 2 13C43 21 39 18 39 18 35 15 32 12 30 8 27 0 32-2 37 2M11 2C7 5 4 8 3 10 1 14 0 16 1 23 5 21 9 18 9 18 13 15 16 12 18 8 21 0 16-2 11 2", + "diffHard1": "m26 34v-2c-10 1-12 0-12-7 4-3 8-5 14-5 6 0 10 2 14 5 0 7-2 8-12 7V34Z", + "diffHard2": "m18 19v9h8v-9m4 9h8v-9h-8", + "diffHard3": "M8 26C3 26-3 21 2 11 6 5 11 4 18 10c0-6 4-10 10-10 6 0 10 4 10 10 7-6 12-5 16 1 5 10-1 15-6 15-5 0-10-7-20-7-10 0-15 7-20 7", + "diffOni1": "m18 9c1 3 4 4 7 3 0 4 1 11 4 16H0c3-5 4-12 4-16 3 1 6 0 7-3z", + "diffOni2": "m6 0.5-2 11c4 1.5 6-0.5 6.5-3zm17 0-4.5 8C19 11 21 13 25 11.5ZM5.5 17.5C4.5 23.5 9 25 11 22Zm18 0L18 22c2 3 6.5 1.5 5.5-4.5z", + "good": "m12 17c4 3 9 7 10 9 0 0 1 3-1 3C19 29 9 18 9 18m6 2c3 0 3-3 3-3 2-1 5 1 4 3-1 1-2 2-5 3m-1 0C13 26 4 29 1 29 0 29 0 26 0 26 0 24 2 24 2 24V13l5-1v4l8-1c1 0 1-3 1-3 0 0-9 1-14 1V8L7 7v4.5L15 11C16 11 16 8 16 8 16 7 2 9 2 9-1 10 0 5 1 5h10l6-1c3 0 4 2 4 6 0 3-1 7-1 7L7 19v4.5c4 0 7-2.5 7-2.5M9 6C8 4 8 1 8 1c0 0 4-1 6 0 0 0 0 3 1 5", + "ok": "m4 10c0 0 3-1 7-1 4 0 3 8 2 11-1 2-3 1-3 1-1-1 1-7 0-8-1-1-4-1-6 0m8 6c-1 1.2-7 1-7 1v-3c0 0 6 0 7-1M2 10c1-2 3 0 3 0 0 0 0 4 1 9-2 3-4 2-4 0zM21 5v19c0 1-2 3-3 3-1 0-5-4-5-4 0-1 4-1 4-1V5M1 2C12 2 17.9 0 20 0 23 0 25 3 21 5 11.7 6 9 6 5 6 0 7-1 2 1 2Z", + "bad": "m13 7c8 0 10 9 10 9 1 4-6 3-8 0 0-1 4 0 2.5-3 0 0-2.5-4-4.5 0M16 6 3 18c-2 2-4 1-4 0 0-1 8-8 9-12m6 0c1 8 0 18 0 18-0.1 1-2 3-3 3-1 0-5-4-5-4 0-1 4-1 4-1 0 0-1-8 0-16M2 7C1 7 1 2 2 2 10 2 21 0 22 1 22 1 24 2 24 4 24 7 2 7 2 7Z", + "crown": "m82 21c0-4 3-6 5.5-6 2.5 0 5.5 2 5.5 6 0 4-3 6-5.5 6C85 27 82 25 82 21ZM41.5 6c0-4 3-6 5.5-6 2.5 0 5.5 2 5.5 6 0 4-3 6-5.5 6-2.5 0-5.5-2-5.5-6zM1 21C1 17 4 15 6.5 15 9 15 12 17 12 21 12 25 9 27 6.5 27 4 27 1 25 1 21Zm12 46h68l2 11H11ZM13 62 5 18 29 34 47 6 65 34 89 18 81 62Z", + "soul": "m37 29c1-4 7 2 7 4 0 1-2 2-4 2-1 0-1-4-3-6zm-4-7c3 1 5 3 4 5-1 2-3 3-3 4 3-2 5-2 5-2 0 0-1 3-5 5-4 3-5-1-3-4 2-3 3-6 0-7zm-3 8c1 3-5 10 8 7 6-1 2-4 2-4 5 1 7 3 7 5 0 3-8 4-12 4-4 0-9-2-9-5 0 0 0-6-1-8 0-3 3-3 5 1zM20 23h8C27 27 20 44 9 42 18 36 23 28 21 26 19 24 20 23 20 23Zm0-6c4-2 9-4 14-2v2c-5 0-9 1-14 2zm8-7v12h-4c2-1 1-9-1-10zm-6 12c3 0 10-2 10-2 0 0 1-10 0-10-5 1-8 3-12 3 0 0 2 5 2 9zm0-12c0 0 6-1 9-3 3-2 9 3 8 6-1 3-2 6-4 9-2 2-15 2-15 2-1 0-3-7-2-11zM21 0c8 0 10 3 8 5 0 0-6 7-15 10C22 8 23 3 23 3 23 1 21 0 21 0ZM5 9c-1 3 2 6 4 6 5-1 13-6 4-7-3 0-5 5-8 1zm7 17c2-1 4-1 6 1 2 2 1 6-2 6-1 0-2-2-2-2-1-2-3-4-2-5zm2 4c-3 2-4 5-5 6-1 1-4 2-6-2-1-2 0-3 2-5l4-4c0-1-1 0-4 1-2 1-5-2-4-4 2-5 1 0 3-1 6-4 9-5 11-3 2 2 0 3-2 5-1 1-3 4-4 6 0 1 3-1 4-2", + "options": "M4 0V8H9V0h1l3 5v4l-4 5v10l4 5v4l-3 5H9V30H4v8H3L0 33V29L4 24V14L0 9V5L3 0Z", + "optionsShadow": "M4-1V8H9V0l1-1 3 6v4l-4 5v10l4 5v4l-2 4-2 1V30H4L5 37 3 38 0 33V29L4 24V14L0 9V5L3 0Z", + "logo1": "M0 83C11 65 5 65 16 43 6 45 9 43 6 39 3 37 1 37 1 34c1-5 2-4 2-7 4 2 6 3 9 2 9-3 10 1 13-9 1-4 0 0 4-11 2-4 1-9 3-9 3 1 6 5 8 8 1 2 4 4 5 6 2 4-2 2-3 10 10 0 9-4 12-4 2 0 4 2 7 4 4 3 8 4 9 6 1 3-3 8-6 9-4 1-1-3-17 0 0 1 0 1 1 3 5 12 10 20 14 30 2 6 2 4 6 6 2 1 3 1 2 3-2 3 1 3-1 5-2 1-3 2-5 4-6 2-9 0-12-4-3-8-4-8-5-12-3-5-4-9-8-17-4-5-2-8-7-14-1-2-3 4-4 6 0 3-2 6-4 9-1 2 2 1 4 3 1 1 3 2 4 4l5 5c2 2 2 2 2 4 0 2-2 4-5 3 0 3 2 8 1 8-5 0-10-7-15-18-3 3-1 4-2 6-2 5-5 11-4 13 2 3-1 6-4 6C4 91 7 89 0 83Z", + "logo2": "m51 48c1-4-5-3-6-6-1-2 1-8 3-6 1 1 2 1 12-1 15-2 5-6 17-4 3 3 7 5 7 6 0 2-2 3-3 5-2 6-3 10-10 16 3 3 6 8 10 13 5 6 5 5 5 7 0 2 3 2 0 4-1 1-2 1-3 2-1 1-2 1-6-1-3-1-9-8-11-11-1-1-2 0-3 1-1 1-3 1-4 2-1 1 0-2-7 1 11 2 5 6-2 7-3-1-7 0-13 0-9 1-14 4-21 5-2 2-4 2-8 0-1-6-6-7-5-9 3 2 8 1 12-1-2-3-4-6-4-9 0-3 0-2 4 0 4 1 4 4 7 3 2 0 1 2 2 5 6 0 7-1 7-4 0-3 1-4 0-11-1-3 4-1 6 2 2 2 4 4 5 7 1 1 2-1 8-4 4-1 8-3 7-6-6-7-7-9-6-13zm11 4c1 1 1 0 2-2 1-2 3-6 1-5-4 1-5 1-10 2 3 2 5 3 7 5zM43 15c4 0 9 1 17-1 9-1-1-9 5-13 3 0 4 4 6 5 5 3 1 7 6 8 1 0 3 2 3 3-2 3-3 3-4 4-1 3-3 1-4 4-1 3-2 4-4 5-1 1-2 0-3 1-5 1-1-5-5-5-3 0-4 1-7 0-3-2-5-2-6-3-1 0-3-2-4-3 0-1-3-1 0-5zM24 56c2 0 6 1 4-2-1-1-1-1-2-4-1-2-1-3-2-3-2 0-3 1-2 4 1 5 0 5 2 5zM13 44c0-2 0-3-1-4 4 1 6 4 9 2 1-1 3-1 4-2 2-2 2 0 4-2 3 1 6 3 8 5 2 1-1 2 0 5 0 2-2 7-3 8 0 3-3 3-8 7-4 2-4 5-7 4-1 0-4-2-6-3-5-2-4-5-3-6 1-1 1-1 5 0 4 0 0-1-1-5-1-2-2-5-1-9zM18 21C12 23 13 24 9 21 7 19 7 16 6 14 4 11 4 12 5 10 6 8 5 6 7 8c1 1 4 3 5 3 2 0 9-1 9-8 0-2 0-3 1-3 2 0 2 1 3 2 2 2 2 2 3 4 2 4 5 0 9 2 2 2 3 2 4 4 1 1 1 3-1 4-2 2-4 3-6 2-2-1-2-1-3 2 0 2-1 3-2 8 4 0 6 1 7-2 2 1 3 1 4 1 1 1 3 2 5 3 1 2 1 3 0 5-2 1-4 1-8 1-6 1-23 2-28 5-2 1-3 1-4 1 0-1 1-2 0-3-3-2-5-6-5-8 2 1 3 2 5 2 3-1 11-2 16-3-1-2-1-6-3-9z", + "logo2Shadow": "m51 48c1-4-5-3-6-6-1-2 1-8 3-6 1 1 2 1 12-1 15-2 5-6 17-4 3 3 7 5 7 6 0 2-2 3-3 5-2 6-3 10-10 16 3 3 6 8 10 13 5 6 5 5 5 7 0 2 3 2 0 4-1 1-2 1-3 2-1 1-2 1-6-1-3-1-9-8-11-11-1-1-2 0-3 1-1 1-3 1-4 2-1 1 0-2-7 1 11 2 5 6-2 7-3-1-7 0-13 0-9 1-14 4-21 5-2 2-4 2-8 0-1-6-6-7-5-9 3 2 8 1 12-1-2-3-4-6-4-9 0-3 0-2 4 0 4 1 4 4 7 3 2 0 1 2 2 5 6 0 7-1 7-4 0-3 1-4 0-11-1-3 4-1 6 2 2 2 4 4 5 7 1 1 2-1 8-4 4-1 8-3 7-6-6-7-7-9-6-13zm11 4c1 1 1 0 2-2 1-2 3-6 1-5-4 1-5 1-10 2 3 2 5 3 7 5zM43 15c4 0 9 1 17-1 9-1-1-9 5-13 3 0 4 4 6 5 5 3 1 7 6 8l3 3-17 14c-2-1 0-5-3-5-3 0-4 1-7 0-3-2-5-2-6-3-1 0-3-2-4-3 0-1-3-1 0-5zM24 56c2 0 6 1 4-2-1-1-1-1-2-4-1-2-1-3-2-3-2 0-3 1-2 4 1 5 0 5 2 5zM13 44c0-2 0-3-1-4 4 1 6 4 9 2 1-1 3-1 4-2 2-2 2 0 4-2 3 1 5 2 7 4L35 53 18 66 13 64c-5-2-4-5-3-6 1-1 1-1 5 0 4 0 0-1-1-5-1-2-2-5-1-9zM18 21C12 23 13 24 9 21 7 19 7 16 6 14 4 11 4 12 5 10 6 8 5 6 7 8c1 1 4 3 5 3 2 0 9-1 9-8 0-2 0-3 1-3 2 0 2 1 3 2 2 2 2 2 3 4 2 4 5 0 9 2l4 3-10 9c0 2-1 3-2 8 4 0 6 1 7-2 2 1 3 1 4 1 1 1 3 2 5 3L42 34 26 36 9 38 5 42C5 41 6 40 5 39 2 37 0 33 0 31c2 1 3 2 5 2 3-1 11-2 16-3-1-2-1-6-3-9z", + "logo3": "m31 0c6 0 10 1 13 3 3 2 5 6 4 10 19-9 28 2 32 7 6 7 7 18-1 35-8 19-23 35-38 35-6 0-11-2-16-5-3-2-8-4-8-7 0-4 5 1 19-6 11-5 27-26 32-37 6-11 1-15-6-10-7 4-14 11-25 15-7 3-14 1-17 0-1 5 0 7 0 13 3 7 1 12-6 12C3 65 3 50 3 46 3 35 8 31 5 30 0 28-1 25 1 22c2-3 5-3 9-1 5 2 6 3 8 7 4-2 9-4 11-7V11C29 8 24 10 24 6 24 0 30 0 31 0Z", + "logo4": "m53 5c2 2 1 10-6 8-7-4-12 0-17 2 1 2 2 7 2 11v9c8-3 20 1 24 7 2 3 2 6-1 9-5 3-6 1-10-2C41 46 36 45 29 48 23 50 16 54 10 54 6 54 0 50 0 46 0 37 10 33 19 32 18 27 19 24 18 19 10 19 9 19 5 17-1 14 1 9 5 10 7 11 8 10 11 9L23 4C33-1 45-2 53 5ZM18 41C15 40 7 40 6 43c-1 5 6 2 12-2z", + "logo5": "m52 6c11 0 18 4 24 13 7 10 4 23-2 32C61 71 52 83 46 87 40 91 32 91 23 87 21 86 17 83 13 81 9 78 13 76 17 76 29 75 37 68 43 60 53 48 59 39 64 28 67 21 62 16 56 20 44 27 35 37 22 40 13 42 0 36 0 28 0 26 1 25 3 24 6 23 8 27 12 25 32 15 38 6 52 6Zm43 5c2 7-5 9-8 4-1-2-2-5-3-7-1-3-3-3-4-1 2 4 4 10 0 12-5 2-7-4-8-7-1-3-2-5-4-6-2 0-2-3 0-4 2-1 6 0 8 1 9-7 17 1 19 8z", + "globe": "M19 4V34M19 4C13 4 9 12 9 19 9 26 13 34 19 34 25 34 29 26 29 19 29 12 25 4 19 4ZM6 11H32M4 19H34M6 27h26", + "category": "M17 0V22L0 11z", + "categoryHighlight": "M-2.7 11 18.5-2.7V6L32-2.8V24.8L18.5 16v8.8z", + "categoryShadow": "m0 11h5l12 7.5v5" +} \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..32411c3 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,3 @@ +{ + "display": "standalone" +} diff --git a/public/src/css/admin.css b/public/src/css/admin.css new file mode 100644 index 0000000..e3140e1 --- /dev/null +++ b/public/src/css/admin.css @@ -0,0 +1,160 @@ +body { + margin: 0; + font-family: 'Noto Sans JP', sans-serif; + background: #FF7F00; + } + + .nav { + margin: 0; + padding: 0; + width: 200px; + background-color: #A01300; + position: fixed; + height: 100%; + overflow: auto; + } + + .nav a { + display: block; + color: #FFF; + padding: 16px; + text-decoration: none; + } + + .nav a.active { + background-color: #4CAF50; + color: white; + } + + .nav a:hover:not(.active) { + background-color: #555; + color: white; + } + +main { + margin-left: 200px; + padding: 1px 16px; + height: 1000px; + } + + @media screen and (max-width: 700px) { + .nav { + width: 100%; + height: auto; + position: relative; + } + .nav a {float: left;} + main {margin-left: 0;} + } + + @media screen and (max-width: 400px) { + .sidebar a { + text-align: center; + float: none; + } + } + + .container { + margin-bottom: 40px; + } + +.song { + background: #F84828; + color: white; + padding: 10px; + font-size: 14pt; + margin: 10px 0; +} + +.song p { + margin: 0; +} + +.song-link { + text-decoration: none; +} + +.song-form { + background: #ff5333; + color: #FFF; + padding: 20px; +} + +.form-field { + background: #555555; + padding: 15px 20px 20px 20px; + margin-bottom: 20px; +} + +.form-field p { + margin: 0; + font-size: 18pt; +} + + .form-field > label { + display: block; +} + +.form-field input { + margin: 5px 0; +} + +.form-field input[type="text"] { + width: 300px; +} + +.form-field input[type="number"] { + width: 50px; +} + +h1 .song-id { + color: #4a4a4a; +} + +.song .song-id { + color: #a01300; +} + +.form-field-indent { + margin-left: 20px; +} + +.checkbox { + display: inline-block; +} + +.checkbox input { + margin-right: 3px; + margin-left: 5px; +} + +.message { + background: #2c862f; + padding: 15px; + margin-bottom: 10px; + color: white; +} + +.message-error { + background: #b92222; +} + +.save-song { + font-size: 22pt; + width: 120px; +} + +.delete-song button { + float: right; + margin-top: -25px; + font-size: 12pt; +} + +.side-button { + float: right; + background: green; + padding: 5px 20px; + color: white; + text-decoration: none; + margin-top: 25px; +} diff --git a/public/src/css/debug.css b/public/src/css/debug.css new file mode 100644 index 0000000..d87c209 --- /dev/null +++ b/public/src/css/debug.css @@ -0,0 +1,129 @@ +#debug{ + position: absolute; + top: 0; + left: 0; + width: 260px; + background: #fff; + border: 1px solid #333; + color: #000; + z-index: 50; + font-size: 14px; + font-family: TnT, Meiryo, sans-serif; +} + +#debug .title{ + position: relative; + height: 25px; + padding: 5px 0 0 5px; + box-sizing: border-box; + background: #bbb; + color: #fff; + cursor: default; + z-index: 1 +} + +#debug .title::before{ + left: auto; + -webkit-text-stroke: 0.25em #555; +} + +#debug .minimise{ + position: absolute; + top: 3px; + right: 3px; + width: 19px; + height: 19px; + background: #d77; + z-index: 1; +} + +#debug .content{ + height: calc(100% - 25px); + overflow-y: auto; + padding: 8px; + box-sizing: border-box; +} + +#debug .input-slider, +#debug .select{ + display: flex; + width: 100%; + height: 30px; + margin: 5px 0 15px 0; +} +#debug .input-slider>input{ + width: 70%; + height: 100%; + box-sizing: border-box; + font-size: 18px; + font-family: monospace; + padding: 2px 4px; + text-align: center; +} +#debug .input-slider>span, +#debug .select>span{ + display: block; + width: 10%; + height: 100%; + opacity: 0.8; + background: #666; + color: #fff; + text-align: center; + line-height: 2em; + cursor: pointer; +} +#debug .input-slider>span:hover, +#debug .select>span:hover{ + opacity: 1; + background: #333; +} +#debug .select select{ + width: 90%; + height: 100%; + box-sizing: border-box; + font-size: 18px; + font-family: sans-serif; + padding: 2px 4px; +} + +#debug label{ + display: block; + margin: 15px 0; +} + +#debug input[type="checkbox"]{ + margin-right: 1em; +} + +#debug .bottom-btns{ + display: flex; + width: 100%; + justify-content: flex-end; +} +#debug .bottom-btns div{ + width: calc(50% - 3px); + height: 30px; + opacity: 0.8; + background: #666; + color: #fff; + text-align: center; + line-height: 2em; + cursor: pointer; +} +#debug .bottom-btns div:hover{ + opacity: 1; + background: #333; +} +#debug .restart-btn{ + display: none; + margin-right: 3px; +} +#debug .exit-btn{ + margin-left: 3px; +} + +#debug .autoplay-label, +#debug .branch-hide, +#debug .lyrics-hide{ + display: none; +} diff --git a/public/src/css/game.css b/public/src/css/game.css new file mode 100644 index 0000000..82a4e5b --- /dev/null +++ b/public/src/css/game.css @@ -0,0 +1,143 @@ +#game{ + width: 100%; + height: 100%; + position: absolute; + overflow: hidden; + background-size: calc(100vh / 720 * 512); + background-position: center; +} +#screen.view{ + background-image: none; + background-color: #000; +} +#canvas{ + position: relative; + z-index: 1; + width: 100%; + height: 100%; + touch-action: none; +} +#touch-drum{ + display: none; + position: absolute; + right: 0; + bottom: 0; + left: 0; + width: 50%; + height: 50%; + text-align: center; + margin: auto; + overflow: hidden; +} +#touch-drum-img{ + position: absolute; + z-index: 1; + width: 100%; + height: 100%; + background-position: top; + background-size: cover; + background-repeat: no-repeat; +} +#touch-buttons{ + display: none; + position: absolute; + top: 8vh; + right: 2vh; + opacity: 0.5; + z-index: 5; +} +#touch-buttons div{ + display: inline-block; + width: 12.5vmin; + height: 12.5vmin; + background-size: contain; + background-repeat: no-repeat; +} +.portrait #touch-buttons{ + top: 11vh; +} +.touchp2 #touch-buttons{ + top: -1.9vh; +} +.touch-visible #touch-drum, +.touch-visible #touch-buttons{ + display: block; +} +.touch-visible .window{ + width: 80vmin; + height: 53vmin; +} +.touch-visible #pause-menu .window button{ + font-size: 5vmin; +} +.touch-visible #pause-menu .window button.selected{ + color: #000; + background: #fff; + border-color: #ae7a26; +} +.touch-results #touch-pause-btn{ + display: none; +} +#fade-screen{ + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: transparent; + pointer-events: none; + z-index: 2; + transition: 1s background-color linear; +} +.fix-animations *{ + animation: none !important; +} +#song-lyrics{ + position: absolute; + right: calc((100vw - 1280 / 720 * 100vh) / 2 + 100px * var(--scale)); + bottom: calc(44 / 720 * 100vh - 30px * var(--scale)); + left: calc((100vw - 1280 / 720 * 100vh) / 2 + 100px * var(--scale)); + text-align: center; + font-family: Meiryo, sans-serif; + font-weight: bold; + font-size: calc(45px * var(--scale)); + line-height: 1.2; + white-space: pre-wrap; + overflow-wrap: break-word; +} +#game.portrait #song-lyrics{ + right: calc(20px * var(--scale)); + left: calc(20px * var(--scale)); +} +#song-lyrics .stroke, +#song-lyrics .fill{ + position: absolute; + right: 0; + bottom: 0; + left: 0; +} +#song-lyrics .stroke{ + -webkit-text-stroke: calc(7px * var(--scale)) #00a; +} +#song-lyrics .fill{ + color: #fff; +} +#song-lyrics ruby{ + display: inline-flex; + flex-direction: column-reverse; +} +#song-lyrics rt{ + line-height: 1; +} +.pixelated #canvas, +.pixelated .donbg>div, +.pixelated #songbg>div, +.pixelated #song-stage, +.pixelated #touch-drum-img, +.pixelated #flowers1-in, +.pixelated #flowers2-in, +.pixelated #mikoshi-in, +.pixelated #tetsuo-in, +.pixelated #hana-in{ + image-rendering: pixelated; +} diff --git a/public/src/css/loader.css b/public/src/css/loader.css new file mode 100644 index 0000000..fcc7c23 --- /dev/null +++ b/public/src/css/loader.css @@ -0,0 +1,136 @@ +html, +body{ + margin: 0; + width: 100%; + height: 100%; + background: #fe7839; + position: absolute; + user-select: none; + touch-action: none; + overflow: hidden; +} +#screen{ + width: 100%; + height: 100%; + margin: 0; + padding: 0; + background-color: #000; + background-position: center; + background-size: 30vh; +} +#screen.pattern-bg{ + background-color: #fe7839; +} +#assets, +#browse{ + display: none; +} +#loader{ + width:90%; + height:10%; + border:1px solid black; + position: absolute; + top:45%; + left:5%; + background: rgba(0,0,0,0.65); +} + +#loader .progress{ + width:0%; + height: 100%; + background: #b52a2a; + opacity: 0.90; +} + +#loader .percentage{ + position:absolute; + top:0; + right:0; + bottom:0; + left:0; + display:flex; + justify-content:center; + align-items:center; + text-align:center; + font-family: sans-serif; + font-size: 5vmin; + color: white; +} + +#unsupportedBrowser{ + position: absolute; + top: 0; + right: 0; + left: 0; + max-height: 100%; + overflow: hidden auto; + padding: 0.5em; + background: #aef; + font-family: sans-serif; + font-size: 20px; + cursor: default; + z-index: 10; +} +#unsupportedWarn{ + display: inline-block; + width: 1.5em; + height: 1.5em; + margin-right: 0.5em; + background: #39a; + color: #fff; + text-align: center; + line-height: 1.5em; +} +#unsupportedBrowser.hidden{ + width: 1.5em; +} +#unsupportedBrowser.hidden *:not(#unsupportedWarn){ + display: none !important; +} +#unsupportedBrowser a{ + color: #02e; + cursor: pointer; + text-decoration: none; +} +#unsupportedBrowser a:hover{ + text-decoration: underline; +} +#unsupportedBrowser ul{ + margin: 0.25em; +} +#unsupportedDetails{ + display: none; + margin: 0.5em 2.5em 0 2.5em; + border: 0.15em solid #39a; + padding: 0.25em; + cursor: auto; + user-select: text; +} +#unsupportedHide{ + position: absolute; + right: 0; + top: 0; + width: 2.5em; + height: 2.5em; + text-align: center; + line-height: 2.25em; + color: #777; + text-shadow: 0.05em 0.05em #fff; +} +.view-outer.loader-error-div, +.loader-error-div .diag-txt{ + display: none +} +.loader-error-div{ + font-family: sans-serif; +} +.loader-error-div .debug-link{ + color: #00f; + text-decoration: underline; + cursor: pointer; + float: right; +} +.loader-error-div .diag-txt textarea, +.loader-error-div .diag-txt iframe{ + height: 10em; +} diff --git a/public/src/css/loadsong.css b/public/src/css/loadsong.css new file mode 100644 index 0000000..f4e5489 --- /dev/null +++ b/public/src/css/loadsong.css @@ -0,0 +1,40 @@ +#load-song{ + width: 100%; + height: 100%; +} +#loading-song{ + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 20vmax; + height: 15vmax; + background: rgba(0, 0, 0, 0.75); + border-radius: 5px; + border: 3px solid white; + color: #fff; + z-index: 1; +} +#loading-don{ + width: 10vmax; + height: calc(10vmax / 120 * 115); + background-size: contain; + background-repeat: no-repeat; +} +.loading-text{ + position: relative; + font-size: 1.5vmax; + text-align: center; + z-index: 1; +} +#p2-cancel-button{ + display: none; + position: absolute; + bottom: -55px; +} diff --git a/public/src/css/main.css b/public/src/css/main.css new file mode 100644 index 0000000..47badc0 --- /dev/null +++ b/public/src/css/main.css @@ -0,0 +1,128 @@ +.window{ + width: 60vmin; + height: 23vmin; + padding: 3vmin; + color: black; + background: rgba(255, 220, 47, 0.95); + border: .5vmin outset #f4ae00; + box-shadow: 2px 2px 10px black; + margin: auto; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + flex-direction: column; + justify-content: space-between; +} +.stroke-sub::before{ + content: attr(alt); + position: absolute; + -webkit-text-stroke: 0.25em #000; + left: 0; + z-index: -1; +} +#session-invite{ + width: 100%; + height: 1.9em; + font-family: sans-serif; + font-size: 2em; + background: #fff; + border: 1px solid #a9a9a9; + padding: 0.3em; + margin: 0.3em 0; + box-sizing: border-box; + text-align: center; + user-select: all; + cursor: text; + overflow: hidden; +} +@keyframes bgscroll{ + from{ + background-position: 50% top; + } + to{ + background-position: calc(50% - 100vh / 720 * 512) top; + } +} +#song-select{ + width: 100%; + height: 100%; + background-size: calc(100vh / 720 * 512); + background-repeat: repeat no-repeat; + animation: bgscroll 16s infinite linear; + white-space: nowrap; + transition: background-image 0.5s; +} +#song-select.unfocused{ + animation-play-state: paused; +} +#song-sel-canvas{ + position: absolute; + right: 0; + left: 0; + margin: auto; +} +#song-select #touch-full-btn{ + display: none; + position: absolute; + top: 0; + right: 0; + width: 12.5vmin; + height: 12.5vmin; + opacity: 0.5; + background-size: contain; + background-repeat: no-repeat; +} +#song-sel-selectable{ + position: absolute; + opacity: 1; + text-align: center; + word-break: break-all; + white-space: pre-wrap; + user-select: all; + cursor: text; + color: transparent; +} +#song-sel-selectable:focus{ + background: #ffdb2c; + color: #000; +} +#song-sel-selectable .stroke-sub{ + position: absolute; + z-index: 1; +} +#song-sel-selectable .stroke-sub::before{ + -webkit-text-stroke: 0; +} +#song-sel-selectable:focus .stroke-sub::before{ + -webkit-text-stroke: 0.25em #fff; +} + +#version { + position: fixed; + z-index: 1000; + font-size: 2vh; + position: absolute; + bottom: 1vh; + right: 1vh; + opacity: 0.7; + font-family: TnT, Meiryo, sans-serif; + pointer-events: none; +} +#version:hover{ + opacity: 1; +} + +#version-link{ + color: #FFFFFF; + text-decoration: none; + pointer-events: none; + white-space: pre-line; + word-break: break-word; +} + +.version-hide{ + pointer-events: none; +} diff --git a/public/src/css/search.css b/public/src/css/search.css new file mode 100644 index 0000000..eb859dd --- /dev/null +++ b/public/src/css/search.css @@ -0,0 +1,287 @@ +#song-search-container { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0,0,0,0.5); + z-index: 2; + display: flex; + justify-content: center; + align-items: center; + font-size: 21px; +} + +#song-search { + position: relative; + display: flex; + flex-direction: column; + width: min(100%, 60em); + height: 80%; + border-radius: 0.8em; + border: 0.35em solid #8C0C42; + color: #fff; + padding: 1em 1em 0 1em; + z-index: 1; + box-sizing: border-box; + background-size: auto, 3.12em; + background-position: 0%, -2%; +} + +#song-search-container.touch-enabled{ + font-size: calc(3 * var(--vmin, 1vmin)); +} +@media (max-width: 950px){ + #song-search-container:not(.touch-enabled){ + font-size: calc(3 * var(--vmin, 1vmin)); + } +} +@media (max-height: 650px){ + #song-search-container:not(.touch-enabled){ + font-size: calc(2 * var(--vmin, 1vmin)); + } +} + +#song-search-input { + width: 100%; + font-size: 1.8em; + padding: 0.5em 0.7em; + border-radius: 0.2em; + border: 0.13em black solid; + font-family: inherit; + box-sizing: border-box; + -webkit-box-sizing:border-box; + -moz-box-sizing: border-box; + outline: none; +} + +#song-search-input:focus { + border-color: #fff923; +} + +#song-search-results { + margin-top: 0.5em; + overflow-y: scroll; + -ms-overflow-style: none; + scrollbar-width: none; + scroll-behavior: smooth; +} + +#song-search-results::-webkit-scrollbar { + display: none; +} + +.song-search-result { + display: flex; + height: 3.2em; + margin: 0.2em; + padding: 0.7em; + flex-direction: row; + text-align: center; + align-items: center; + justify-content: center; + border: 0.3em black solid; + position: relative; + --course-width: min(3em, calc(7 * var(--vmin, 1vmin))); + content-visibility: auto; + contain-intrinsic-size: 1px 3.2em; +} + +.song-search-result::before { + display: block; + position: absolute; + content: ''; + height: 100%; + width: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + border: 0.4em solid; +} + +.song-search-result:last-of-type { + margin-bottom: 1em; +} + +.song-search-result-info { + font-size: 1.2em; + padding: 0.3em 0.3em 0.3em 0.5em; + text-align: left; + z-index: 0; + position: relative; + white-space: nowrap; + overflow-x: hidden; + width: calc(100% - (var(--course-width) + 0.4em) * 5 - 0.6em); +} + +.song-search-result-info .highlighted-text { + color: #faff00; +} + +.song-search-result-title, +.song-search-result-subtitle { + display: inline-block; + transform-origin: left; +} + +.song-search-result-subtitle { + font-size: 0.8em; + margin-top: 0.5em; +} + +.song-search-result-title::before, +.song-search-result-subtitle::before { + content: attr(alt); + position: absolute; + z-index: -1; + -webkit-text-stroke-width: 0.4em; +} + +.song-search-result-course { + width: var(--course-width); + height: 100%; + margin: 0.2em; + font-size: 1.2em; + border-radius: 0.3em; + position: relative; + z-index: 1; +} + +.song-search-result-hidden { + visibility: hidden; +} + +.song-search-result:hover { + border-color: #fff923; + cursor: pointer; +} + +.song-search-result-active { + border-color: #fff923; +} + +.song-search-result-course::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.5; + z-index: -1; + background-size: 4.8em; + border-radius: 0.3em; +} + +.song-search-result-stars { + bottom: 0; + background: rgb(0 0 0 / 47%); + position: absolute; + width: 100%; + padding: 0.1em 0; + border-radius: 0 0 0.3em 0.3em; + overflow: hidden; +} + +.song-search-result-easy { + background-color: #D13215; +} + +.song-search-result-easy::before { + background-position-x: center; + background-position-y: -0.6em; +} + +.song-search-result-normal { + background-color: #799C22; +} + +.song-search-result-normal::before { + background-position-x: center; + background-position-y: -5.1em; +} + +.song-search-result-hard { + background-color: #31799B; +} + +.song-search-result-hard::before { + background-position-x: center; + background-position-y: -9.1em; +} + +.song-search-result-oni { + background-color: #AF2C7F; +} + +.song-search-result-oni::before { + background-position-x: center; + background-position-y: -13.1em; +} + +.song-search-result-ura { + background-color: #604AD5; +} + +.song-search-result-ura::before { + background-position-x: center; + background-position-y: -17.2em; +} + +.song-search-result-crown { + background-size: 1.4em; + background-position-x: center; + background-repeat: repeat-y; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + width: 1.4em; + height: 1.3em; + margin-bottom: 1.2em; +} + +.song-search-result-gold { + background-position-y: 59%; +} + +.song-search-result-silver { + background-position-y: 29%; +} + +.song-search-result-noclear { + background-position-y: -1%; +} + +#song-search-tip { + font-size: 1em; + margin-top: 1em; + text-align: center; + background-repeat: no-repeat; + background-position: top; + background-size: 10em; + background-color: #00000087; + border-radius: 0.5em; + padding: 1em; +} + +#song-search-close { + position: absolute; + right: -0.5em; + top: -0.8em; + font-size: 2em; + font-family: TnT; + cursor: pointer; +} + +#song-search-close:hover::before{ + -webkit-text-stroke: 0.25em #fff923; +} + +.song-search-tip-error { + height: 8em; +} diff --git a/public/src/css/songbg.css b/public/src/css/songbg.css new file mode 100644 index 0000000..5f6f2ef --- /dev/null +++ b/public/src/css/songbg.css @@ -0,0 +1,364 @@ +#songbg, +#songbg>*, +.donbg, +.donbg *, +#song-stage{ + position: absolute; + right: 0; + left: 0; + width: 100%; + max-width: calc(100vh / 9 * 32); +} +#songbg{ + height: 50.1%; +} +#songbg>*{ + top: 0; +} +#songbg, +#songbg>*{ + background-size: cover; + background-position: center; + background-repeat: no-repeat; + bottom: 0; +} +#song-stage{ + height: calc(44 / 720 * 100vh); + background-position: center bottom; + background-repeat-y: no-repeat; + background-size: auto 100%; + bottom: 0; +} +.portrait #songbg{ + height: 63.4%; + max-height: calc(50% + 24vw); +} +.touchp2 #songbg{ + height: calc(50% - 5.9vw); + min-height: 39.5%; +} +.multiplayer.portrait #songbg{ + height: calc(50% - 37vw); + min-height: calc(29% + 1px); +} +.multiplayer:not(.touchp2):not(.portrait) #songbg, +.multiplayer:not(.touchp2):not(.portrait) #song-stage{ + display: none; +} +.game-paused *{ + animation-play-state: paused !important; +} +@keyframes songbg-strobe{ + 0%{opacity: 1} + 25%{opacity: 0} + 50%{opacity: 0.66} + 75%{opacity: 0} +} +@keyframes songbg-pulse{ + 0%{opacity: 1} + 50%{opacity: 0} +} +.songbg-1 #layer2, +.songbg-2 #layer2, +.songbg-3 #layer2{ + animation: 0.4s linear songbg-strobe infinite; + mix-blend-mode: difference; +} +.songbg-4 #layer2{ + animation: 0.4s linear songbg-pulse infinite; + mix-blend-mode: difference; +} +.songbg-5 #layer2{ + animation: 1s linear songbg-pulse infinite; + mix-blend-mode: color-dodge; +} +.songbg-strobe #layer2{ + animation: 0.4s linear songbg-strobe infinite; +} +.songbg-pulse #layer2{ + animation: 0.4s linear songbg-pulse infinite; +} +.songbg-slowfade #layer2{ + animation: 2s cubic-bezier(0.68, -0.55, 0.27, 1.55) songbg-pulse infinite; +} +.touch-visible #layer2{ + display: none; + background-image: none; + animation: none; +} +.donbg{ + top: 0; + height: calc(50% - 13.7vw); + min-height: 25.6%; +} +.multiplayer .donbg{ + min-height: 27.2%; +} +.portrait .donbg{ + height: calc(50% - 48.9vw); + min-height: 22.5%; +} +.donbg *{ + top: 0; + bottom: 0; + background-repeat-y: no-repeat; +} +.donbg.donbg-bottom{ + top: auto; + bottom: 0; +} +.portrait .donbg.donbg-bottom { + top: calc(50% + -1vw); + bottom: auto; +} +@keyframes donbg-scroll{ + from{background-position-x: 0} + to{background-position-x: calc(var(--h) / var(--sh1) * var(--sw) * -1)} +} +@keyframes donbg-scroll1{ + from{background-position-x: 0} + to{background-position-x: calc(var(--h) / var(--sh1) * var(--sw1) * -1)} +} +@keyframes donbg-scroll2{ + from{background-position-x: 0} + to{background-position-x: calc(var(--h) / var(--sh1) * var(--sw2) * -1)} +} +@keyframes donbg-raise{ + from{background-position-y: 0} + to{background-position-y: var(--raised)} +} +@keyframes donbg-anim3{ + 0%{background-position-y: 0} + 13%{background-position-y: var(--raised)} + 15%{background-position-y: var(--raised)} + 45%{background-position-y: 0} + 50%{background-position-y: 0} + 65%{background-position-y: calc(var(--raised) / 2)} + 80%{background-position-y: 0} +} +@keyframes donbg-anim5{ + 0%{background-position-y: 0} + 13%{background-position-y: var(--raised)} + 17%{background-position-y: var(--raised)} + 30%{background-position-y: 0} +} +.donlayer1{ + animation: 5s linear donbg-scroll infinite; + background-size: auto 100%; +} +.donlayer2{ + background-size: auto calc(var(--sh2) / var(--sh1) * 100%); + --raised: calc((var(--sh2) - var(--sh1)) / var(--sh2) * var(--h) * -1); +} +.donlayer3{ + background-color: #000; + opacity: 0; + transition: 0.15s opacity linear; +} +.donbg-dark .donlayer3{ + opacity: 0.5; +} +.donbg-1 .donlayer2, +.donbg-2 .donlayer2, +.donbg-4 .donlayer2, +.donbg-6 .donlayer2, +.donbg-raise .donlayer2{ + animation: 5s linear donbg-scroll infinite, 1s linear donbg-raise infinite alternate; +} +.donbg-3 .donlayer2, +.donbg-anim3 .donlayer2{ + animation: 3.4s linear donbg-scroll infinite, 1.8s linear donbg-anim3 infinite; +} +.donbg-5 .donlayer2, +.donbg-anim5 .donlayer2{ + animation: 2.7s linear donbg-scroll infinite, 2.2s linear donbg-anim5 infinite; +} +.donbg-equalscroll .donlayer1{ + animation: 5.3s linear donbg-scroll1 infinite; +} +.donbg-equalscroll .donlayer2{ + animation: 5.3s linear donbg-scroll2 infinite; +} +.donbg-fastscroll .donlayer1{ + animation: 2s linear donbg-scroll1 infinite; +} +.donbg-fastscroll .donlayer2{ + animation: 1s linear donbg-scroll2 infinite; +} + +#tetsuohana{ + position: absolute; + right: calc(-12px * var(--scale)); + left: calc(-12px * var(--scale)); + margin: auto; + z-index: 1; + overflow: hidden; + pointer-events: none; + top: calc(50% - 15px * var(--scale)); + width: calc(1304px * var(--scale)); + height: calc(375px * var(--scale)); + --frame: 0; + --low: calc(36px * var(--scale)); +} +#tetsuo, +#hana{ + position: absolute; + top: 0; + width: calc(292px * var(--scale)); + height: calc(425px * var(--scale)); + transform: translateY(calc(360px * var(--scale))); +} +#tetsuo-in, +#hana-in{ + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-size: calc(292px * var(--scale) * 2); + background-position-y: calc(-425px * var(--frame) * var(--scale)); +} +#tetsuo{ + left: calc(173px * var(--scale)); +} +#hana{ + right: calc(178px * var(--scale)); +} +#hana-in{ + background-position-x: calc(-292px * var(--scale)); +} +#mikoshi{ + position: absolute; + top: 0; + left: calc(390px * var(--scale)); + width: calc(553px * var(--scale)); + height: calc(416px * var(--scale)); + transform: translateY(calc(461px * var(--scale))); +} +#mikoshi-in{ + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-size: contain; +} +#flowers1, +#flowers2{ + position: absolute; + top: calc(218px * var(--scale)); + width: calc(483px * var(--scale)); + height: calc(159px * var(--scale)); + transform: translateY(calc(243px * var(--scale))) scaleX(var(--flip)); +} +#flowers1-in, +#flowers2-in{ + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-size: calc(483px * var(--scale)); + background-position-y: calc(-159px * var(--frame) * var(--scale)); +} +#flowers1{ + left: 0; + --flip: 1; +} +#flowers2{ + right: calc(4px * var(--scale)); + --flip: -1; +} +#tetsuohana.fadein, +#tetsuohana.dance, +#tetsuohana.dance2, +#tetsuohana.failed{ + height: calc(461px * var(--scale)); +} +#tetsuohana.fadein #tetsuo, +#tetsuohana.fadein #hana{ + transition: 0.5s transform cubic-bezier(0.2, 0.6, 0.4, 1.2); + transform: translateY(var(--low)); +} +@keyframes tetsuohana-dance{ + 0%{transform: translateY(var(--low))} + 50%{transform: translateY(0)} + 100%{transform: translateY(0)} +} +@keyframes tetsuohana-failed1{ + 0%{transform: translateY(calc(10px * var(--scale)))} + 50%{transform: translateY(0)} + 100%{transform: translateY(0)} +} +@keyframes tetsuohana-failed2{ + 0%{transform: translateY(0)} + 49%{transform: translateY(0)} + 50%{transform: translateY(calc(5px * var(--scale)))} + 100%{transform: translateY(calc(15px * var(--scale)))} +} +@keyframes tetsuohana-flowers{ + 0%{background-position-y: 0} + 50%{background-position-y: calc(-159px * var(--scale))} + 100%{background-position-y: calc(-318px * var(--scale))} +} +@keyframes tetsuohana-mikoshi{ + 0%{transform: translateY(calc(425px * var(--scale)))} + 100%{transform: translateY(0)} +} +#tetsuohana.dance #tetsuo, +#tetsuohana.dance #hana, +#tetsuohana.dance2 #tetsuo, +#tetsuohana.dance2 #hana{ + --frame: 1; + transform: translateY(var(--low)); + animation: 0.5s ease-out tetsuohana-dance infinite forwards; +} +#tetsuohana.dance #tetsuo-in, +#tetsuohana.dance #hana-in, +#tetsuohana.dance2 #tetsuo-in, +#tetsuohana.dance2 #hana-in{ + transform: translateY(0); + animation: 0.5s ease-out tetsuohana-dance infinite forwards reverse; +} +#tetsuohana.dance #flowers1, +#tetsuohana.dance #flowers2{ + transform: translateY(0) scaleX(var(--flip)); + transition: 0.34s transform ease-out; +} +#tetsuohana.dance2 #flowers1, +#tetsuohana.dance2 #flowers2{ + transform: translateY(0) scaleX(var(--flip)); +} +#tetsuohana.dance #flowers1-in, +#tetsuohana.dance #flowers2-in{ + animation: 0.25s 0.4s step-end tetsuohana-flowers both; +} +#tetsuohana.dance2 #flowers1-in, +#tetsuohana.dance2 #flowers2-in{ + background-position-y: calc(-318px * var(--scale)); +} +#tetsuohana.dance #mikoshi-out{ + animation: 0.4s 0.4s ease-out tetsuohana-mikoshi both; +} +#tetsuohana.dance #mikoshi{ + transform: translateY(var(--low)); + animation: 0.5s 0.8s ease-out tetsuohana-dance infinite forwards; +} +#tetsuohana.dance #mikoshi-in{ + transform: translateY(0); + animation: 0.5s 0.8s ease-out tetsuohana-dance infinite forwards reverse; +} +#tetsuohana.dance2 #mikoshi{ + transform: translateY(var(--low)); + animation: 0.5s -0.2s ease-out tetsuohana-dance infinite forwards; +} +#tetsuohana.dance2 #mikoshi-in{ + transform: translateY(0); + animation: 0.5s -0.2s ease-out tetsuohana-dance infinite forwards reverse; +} +#tetsuohana.failed #tetsuo, +#tetsuohana.failed #hana{ + --frame: 2; + top: calc(26px * var(--scale)); + transform: translateY(calc(46px * var(--scale))); + animation: 1.25s ease-out tetsuohana-failed1 forwards infinite; +} +#tetsuohana.failed #tetsuo-in, +#tetsuohana.failed #hana-in{ + transform: translateY(0); + animation: 1.25s ease-in tetsuohana-failed2 forwards infinite; +} diff --git a/public/src/css/titlescreen.css b/public/src/css/titlescreen.css new file mode 100644 index 0000000..3ca80fc --- /dev/null +++ b/public/src/css/titlescreen.css @@ -0,0 +1,57 @@ +@keyframes toggleFade{ + 40%{ + opacity: 1; + } + 70%{ + opacity: 0.2; + } +} +#title-screen{ + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: #1389f0; + background-repeat: no-repeat; + background-position: center; + background-size: cover; + cursor: pointer; +} +#logo{ + width: 100vmin; + height: calc(100vmin / 1170 * 390); +} +.click-to-continue{ + position: absolute; + bottom: 15%; + color: #fff; + font-size: 8vmin; + text-align: center; + z-index: 1; + animation: toggleFade 2s infinite ease-in-out; +} +.click-to-continue::before{ + -webkit-text-stroke: 0.25em #f00; + filter: blur(0.3vmin); +} +#title-disclaimer { + text-align: center; + position: absolute; + bottom: 5%; + color: #fff; + z-index: 1; +} +#title-disclaimer span { + color: #fff; + font-size: 2vmin; + text-align: center; + display: block; +} +#title-disclaimer span:before { + left: initial; + filter: blur(0.1vmin); +} diff --git a/public/src/css/view.css b/public/src/css/view.css new file mode 100644 index 0000000..66a90fa --- /dev/null +++ b/public/src/css/view.css @@ -0,0 +1,472 @@ +.view-outer{ + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + position: absolute; + width: 100%; + height: 100%; + background-position: center; +} +.view{ + background: rgb(246, 234, 212); + color: black; + border: 0.25em black solid; + border-radius: 0.5em; + width: 800px; + max-width: 40em; + padding: 1em; + margin: 1em; + font-size: 21px; + position: relative; +} +@media (max-width: 950px){ + .view-outer:not(.touch-enabled) .view{ + font-size: 3vmin; + } +} +@media (max-height: 650px){ + .view-outer:not(.touch-enabled) .view{ + font-size: 3vmin; + } +} +.touch-enabled .view{ + font-size: 3vmin; +} +.view-title{ + z-index: 1; + position: absolute; + color: white; + top: -0.7em; + font-size: 1.65em; +} +.view-content{ + margin: 0.7em 0; + overflow-y: auto; + max-height: calc(100vh - 14em); +} +kbd{ + font-family: inherit; + padding: 0.1em 0.6em; + border: 1px solid #ccc; + font-size: 0.6em; + background-color: #f7f7f7; + color: #333; + box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; + border-radius: 3px; + display: inline-block; + text-shadow: 0 1px 0 #fff; + line-height: 1.4; + white-space: nowrap; +} +.key-join{ + font-size: 0.6em; +} +.taibtn{ + display: inline-block; + background: #f6ead4; + padding: 0.4em 0.4em; + border-radius: 0.5em; + border: 0.1em rgba(218, 205, 178, 1) solid; + cursor: pointer; + font-size: 1.4em; + box-sizing: border-box; + color: #555; + text-align: center; +} +.view-end-button{ + float: right; + padding: 0.4em 1.5em; + font-weight: bold; + border-color: #000; + color: #000; + z-index: 1; +} +.taibtn:hover, +.taibtn.selected, +.view-end-button:hover, +.view-end-button.selected{ + position: relative; + color: #fff; + background: #ffb547; + border-color: #fff; +} +.taibtn::before, +.view-end-button::before{ + display: none; +} +.taibtn:hover::before, +.taibtn.selected::before, +.view-end-button:hover::before, +.view-end-button.selected::before{ + display: block +} +.taibtn::before{ + padding-left: inherit; +} +.left-buttons{ + float: left; + display: flex; +} +.left-buttons .taibtn{ + margin-right: 0.4em; +} +.center-buttons{ + margin: 1.5em 0; +} +.account-view .center-buttons{ + margin: 0.3em 0; +} +.center-buttons>div{ + text-align: center; + margin: 0.2em 0; +} +.center-buttons .taibtn{ + margin: 0 0.2em; +} +.diag-txt textarea, +.diag-txt iframe{ + width: 100%; + height: 5em; + font-size: inherit; + resize: none; + word-break: break-all; + margin-bottom: 1em; + background: #fff; + border: 1px solid #a9a9a9; + user-select: all; + box-sizing: border-box; +} +.text-warn{ + color: #d00; +} +.link-btn a{ + color: inherit; + text-decoration: none; + pointer-events: none; +} +.nowrap{ + white-space: nowrap; +} +@keyframes border-pulse{ + 0%{border-color: #ff0} + 50%{border-color: rgba(255, 255, 0, 0)} + 100%{border-color: #ff0} +} +@keyframes border-pulse2{ + 0%{border-color: #e29e06} + 50%{border-color: rgba(226, 158, 6, 0)} + 100%{border-color: #e29e06} +} +.settings-outer{ + background-size: 50vh; +} +.setting-box{ + display: flex; + height: 2em; + margin-top: 1.2em; + border: 0.25em solid #000; + border-radius: 0.5em; + padding: 0.3em; + outline: none; + color: #000; + cursor: pointer; +} +.setting-box:first-child{ + margin-top: 0; +} +.view-content:not(:hover) .setting-box.selected, +.setting-box:hover{ + background: #ffb547; + animation: 2s linear border-pulse infinite; +} +.bold-fonts .setting-box{ + line-height: 1em; +} +.setting-name{ + position: relative; + width: 50%; + padding: 0.3em; + font-size: 1.3em; + box-sizing: border-box; + white-space: nowrap; + overflow: hidden; +} +.view-content:not(:hover) .setting-box.selected .setting-name, +.setting-box:hover .setting-name, +.setting-box:hover #gamepad-value{ + color: #fff; + z-index: 0; +} +.setting-name::before{ + padding-left: 0.3em; +} +.setting-name::after{ + content: ""; + display: block; + position: absolute; + top: 0; + right: 0; + width: 40px; + height: 100%; + background-image: linear-gradient(90deg, rgba(246, 234, 212, 0), #f6ead4 90%); +} +.view-content:not(:hover) .setting-box.selected .setting-name::after, +.setting-box:hover .setting-name::after{ + background-image: linear-gradient(90deg, rgba(255, 181, 71, 0), #ffb547 90%); +} +.setting-value{ + display: flex; + background: #fff; + width: 50%; + border-radius: 0.2em; + padding: 0.5em; + box-sizing: border-box; + overflow: hidden; + white-space: nowrap; +} +.setting-value.selected{ + width: calc(50% + 0.2em); + margin: -0.1em; + border: 0.2em solid #e29e06; + padding: 0.4em; + animation: 2s linear border-pulse2 infinite; +} +.setting-value>div{ + padding: 0 0.4em; + overflow: hidden; + text-overflow: ellipsis; +} +.shadow-outer{ + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1; +} +#settings-gamepad, +#settings-latency, +#customsongs-error{ + display: none; +} +#settings-gamepad .view{ + width: 29.9em; + max-width: 100vw; +} +#settings-gamepad .setting-box{ + height: auto; + overflow: hidden; +} +#gamepad-bg, +#gamepad-buttons{ + background-size: 20.53em; +} +#gamepad-bg{ + position: relative; + width: 20.53em; + height: 11.83em; + max-height: none; + background-repeat: no-repeat; + text-align: center; + font-size: 1.4em; + cursor: pointer; +} +#gamepad-buttons{ + position: absolute; + left: 5.26em; + top: 4.48em; + width: 10.52em; + height: 4.89em; + background-position: 0 -11.87em; + background-repeat: no-repeat; + pointer-events: none; +} +#gamepad-value{ + position: relative; + margin-top: 1em; +} +#gamepad-value::before{ + left: auto; +} +#settings-latency .view{ + width: 30em; +} +.setting-value{ + position: relative; +} +.setting-value:not(.selected) .latency-buttons{ + display: none; +} +.setting-value .latency-buttons{ + position: absolute; + top: 0; + right: 0; + bottom: 0; + padding: 0; +} +.latency-buttons span{ + display: inline-block; + width: 2em; + height: 100%; + text-align: center; + background-color: #c3862a; + color: #fff; + line-height: 2em; + outline: none; +} +.latency-buttons span:hover, +.latency-buttons span:active{ + background-color: #946013; +} +.left-buttons .taibtn, +.center-buttons .taibtn{ + z-index: 1; +} +.accountpass-form, +.accountdel-form, +.login-form{ + text-align: center; + width: 80%; + margin: auto; +} +.accountpass-form .accountpass-div, +.accountdel-form .accountdel-div, +.login-form .password2-div{ + display: none; +} +.account-view .displayname, +.accountpass-form input[type=password], +.accountdel-form input[type=password], +.login-form input[type=text], +.login-form input[type=password]{ + width: 100%; + font-size: 1.4em; + margin: 0.1em 0; + padding: 0.3em; + box-sizing: border-box; +} +.accountpass-form input[type=password]{ + width: calc(100% / 3); +} +.accountpass-form input[type=password]::placeholder{ + font-size: 0.8em; +} +.login-form input[type=checkbox]{ + transform: scale(1.4); +} +.account-view .displayname-hint, +.login-form .username-hint, +.login-form .password-hint, +.login-form .remember-label{ + display: block; + font-size: 1.1em; + padding: 0.5em; +} +.login-form .remember-label{ + padding: 0.85em; +} +.account-view .save-btn{ + float: right; + padding: 0.4em 1.5em; + font-weight: bold; + border-color: #000; + color: #000; + z-index: 1; +} +.account-view .view-end-button{ + margin-right: 0.4em; + font-weight: normal; + border-color: #dacdb2; + color: #555; +} +.account-view .save-btn:hover, +.account-view .save-btn.selected, +.account-view .view-end-button:hover, +.account-view .view-end-button.selected{ + color: #fff; + border-color: #fff; +} +.account-view .displayname-div{ + width: 80%; + margin: 0 auto; +} +.accountpass-form .accountpass-btn, +.accountdel-form .accountdel-btn, +.login-form .login-btn{ + z-index: 1; +} +.accountpass-form, +.accountdel-form{ + margin: 0.3em auto; +} +.view-content .error-div{ + display: none; + width: 80%; + margin: 0 auto; + padding: 0.5em; + font-size: 1.1em; + color: #d00; +} +.customdon-div{ + display: flex; + justify-content: center; + align-items: center; + text-align: right; +} +.customdon-canvas{ + width: 13em; + cursor: pointer; +} +.customdon-div label{ + display: block; + padding: 0.3em; +} +.customdon-div input[type="color"]{ + font-size: inherit; + width: 2.6em; + height: 1.6em; + padding: 0 0.1em; + vertical-align: middle; +} +.customdon-reset{ + width: 100%; + font-family: inherit; + font-size: 1em; + padding: 0.2em; +} +#customsongs-error .view, +#dropzone .view{ + width: 600px; +} +#dropzone{ + pointer-events: none; + opacity: 0; + transition: opacity 0.5s; +} +#dropzone .view-content{ + font-size: 2em; + text-align: center; +} +#dropzone.dragover{ + opacity: 1; +} +.plugin-browse-button{ + position: relative; + overflow: hidden; +} +#plugin-browse{ + position: absolute; + font-size: inherit; + top: -0.1em; + left: -0.1em; + right: -0.1em; + bottom: -0.1em; + border-radius: 0.5em; + opacity: 0; + cursor: pointer; +} +#plugin-browse::-webkit-file-upload-button{ + cursor: pointer; +} diff --git a/public/src/js/about.js b/public/src/js/about.js new file mode 100644 index 0000000..7734e9b --- /dev/null +++ b/public/src/js/about.js @@ -0,0 +1,253 @@ +class About{ + constructor(...args){ + this.init(...args) + } + init(touchEnabled){ + this.touchEnabled = touchEnabled + loader.changePage("about", true) + cancelTouch = false + + this.endButton = this.getElement("view-end-button") + this.diagTxt = this.getElement("diag-txt") + this.version = document.getElementById("version-link").href + this.tutorialOuter = this.getElement("view-outer") + if(touchEnabled){ + this.tutorialOuter.classList.add("touch-enabled") + } + this.linkIssues = document.getElementById("link-issues") + this.linkEmail = document.getElementById("link-email") + + var tutorialTitle = this.getElement("view-title") + tutorialTitle.innerText = strings.aboutSimulator + tutorialTitle.setAttribute("alt", strings.aboutSimulator) + var tutorialContent = this.getElement("view-content") + strings.about.bugReporting.forEach(string => { + tutorialContent.appendChild(document.createTextNode(string)) + tutorialContent.appendChild(document.createElement("br")) + }) + var span = document.createElement("span") + span.classList.add("text-warn") + span.innerText = strings.about.diagnosticWarning + tutorialContent.appendChild(span) + this.endButton.innerText = strings.tutorial.ok + this.endButton.setAttribute("alt", strings.tutorial.ok) + + this.items = [] + + this.getLink(this.linkIssues).innerText = strings.about.issues + this.linkIssues.setAttribute("alt", strings.about.issues) + var versionUrl = gameConfig._version.url + this.getLink(this.linkIssues).href = versionUrl + "issues" + this.items.push(this.linkIssues) + + var contactEmail = gameConfig.email + this.hasEmail = typeof contactEmail === "string" + if(this.hasEmail){ + this.linkEmail.setAttribute("alt", contactEmail) + this.getLink(this.linkEmail).href = "mailto:" + contactEmail + this.getLink(this.linkEmail).innerText = contactEmail + this.items.push(this.linkEmail) + }else{ + this.linkEmail.parentNode.removeChild(this.linkEmail) + } + + pageEvents.add(this.linkIssues, ["click", "touchend"], this.linkButton.bind(this)) + if(this.hasEmail){ + pageEvents.add(this.linkEmail, ["click", "touchend"], this.linkButton.bind(this)) + } + pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this)) + this.items.push(this.endButton) + this.selected = this.items.length - 1 + + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"], + previous: ["left", "up", "ka_l"], + next: ["right", "down", "ka_r"], + back: ["escape"] + }, this.keyPressed.bind(this)) + this.gamepad = new Gamepad({ + "confirm": ["b", "ls", "rs"], + "previous": ["u", "l", "lb", "lt", "lsu", "lsl"], + "next": ["d", "r", "rb", "rt", "lsd", "lsr"], + "back": ["start", "a"] + }, this.keyPressed.bind(this)) + + pageEvents.send("about", this.addDiag()) + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + keyPressed(pressed, name){ + if(!pressed){ + return + } + var selected = this.items[this.selected] + if(name === "confirm"){ + if(selected === this.endButton){ + this.onEnd() + }else{ + this.getLink(selected).click() + pageEvents.send("about-link", selected) + assets.sounds["se_don"].play() + } + }else if(name === "previous" || name === "next"){ + selected.classList.remove("selected") + this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1)) + this.items[this.selected].classList.add("selected") + assets.sounds["se_ka"].play() + }else if(name === "back"){ + this.onEnd() + } + } + mod(length, index){ + return ((index % length) + length) % length + } + onEnd(event){ + var touched = false + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + touched = true + }else if(event.which !== 1){ + return + } + } + this.clean() + assets.sounds["se_don"].play() + localStorage.setItem("tutorial", "true") + setTimeout(() => { + new SongSelect("about", false, touched) + }, 500) + } + addDiag(){ + var diag = [] + + diag.push("```") + diag.push("Taiko-Web version: " + this.version) + diag.push("URL: " + location.href) + diag.push("User agent: " + navigator.userAgent) + diag.push("Screen size: " + innerWidth + "x" + innerHeight + ", outer: " + outerWidth + "x" + outerHeight + ", ratio: " + (window.devicePixelRatio || 1).toFixed(2)) + if(this.touchEnabled){ + diag.push("Touch enabled: true") + } + if(!fullScreenSupported){ + diag.push("Full screen supported: false") + } + diag.push("Blur performance: " + perf.blur + "ms, all images: " + perf.allImg + "ms") + diag.push("Page load: " + (perf.load / 1000).toFixed(1) + "s") + if("getGamepads" in navigator){ + var gamepads = navigator.getGamepads() + for(var i = 0; i < gamepads.length; i++){ + if(gamepads[i]){ + var gamepadDiag = [] + gamepadDiag.push(gamepads[i].id) + gamepadDiag.push("buttons: " + gamepads[i].buttons.length) + gamepadDiag.push("axes: " + gamepads[i].axes.length) + diag.push("Gamepad #" + (i + 1) + ": " + gamepadDiag.join(", ")) + } + } + } + var userLangStr = " (none)" + if("languages" in navigator){ + var userLang = navigator.languages.slice() + if(userLang[0] !== navigator.language){ + userLang.unshift(navigator.language) + } + if(userLang.length !== 0){ + userLangStr = " (" + userLang.join(", ") + ")" + } + } + diag.push("Language: " + strings.id + userLangStr) + var latency = settings.getItem("latency") + diag.push("Audio Latency: " + (latency.audio > 0 ? "+" : "") + latency.audio.toString() + "ms, Video Latency: " + (latency.video > 0 ? "+" : "") + latency.video.toString() + "ms") + var pluginList = plugins.allPlugins.map(pluginLoader => { + return (pluginLoader.plugin.module && pluginLoader.plugin.module.name || pluginLoader.name) + (pluginLoader.plugin.started ? " (started)" : "") + }) + diag.push("Plugins: " + pluginList.join(", ")) + var errorObj = {} + if(localStorage["lastError"]){ + try{ + errorObj = JSON.parse(localStorage["lastError"]) + }catch(e){} + } + if(errorObj.timestamp && errorObj.stack){ + if(errorObj.timestamp + 1000 * 60 * 60 * 24 > Date.now()){ + diag.push("Last error: " + errorObj.stack) + diag.push("Error date: " + new Date(errorObj.timestamp).toGMTString()) + }else{ + localStorage.removeItem("lastError") + } + } + diag.push("```") + var diag = diag.join("\n") + + if(navigator.userAgent.indexOf("Android") >= 0){ + var iframe = document.createElement("iframe") + this.diagTxt.appendChild(iframe) + var body = iframe.contentWindow.document.body + body.innerText = diag + + body.setAttribute("style", ` + font-family: monospace; + margin: 2px 0 0 2px; + white-space: pre-wrap; + word-break: break-all; + cursor: text; + `) + body.setAttribute("onblur", ` + getSelection().removeAllRanges() + `) + }else{ + this.textarea = document.createElement("textarea") + this.textarea.readOnly = true + this.textarea.value = diag + this.diagTxt.appendChild(this.textarea) + if(!this.touchEnabled){ + pageEvents.add(this.textarea, "focus", () => { + this.textarea.select() + }) + pageEvents.add(this.textarea, "blur", () => { + getSelection().removeAllRanges() + }) + } + } + + var issueBody = strings.about.issueTemplate + "\n\n\n\n" + diag + if(this.hasEmail){ + this.getLink(this.linkEmail).href += "?body=" + encodeURIComponent(issueBody.replace(/\n/g, "
\r\n")) + } + + return diag + } + getLink(target){ + return target.getElementsByTagName("a")[0] + } + linkButton(event){ + if(event.target === event.currentTarget){ + this.getLink(event.currentTarget).click() + pageEvents.send("about-link", event.currentTarget) + assets.sounds["se_don"].play() + } + } + clean(){ + cancelTouch = true + this.keyboard.clean() + this.gamepad.clean() + pageEvents.remove(this.linkIssues, ["click", "touchend"]) + if(this.hasEmail){ + pageEvents.remove(this.linkEmail, ["click", "touchend"]) + } + pageEvents.remove(this.endButton, ["mousedown", "touchstart"]) + if(this.textarea){ + pageEvents.remove(this.textarea, ["focus", "blur"]) + } + pageEvents.keyRemove(this, "all") + delete this.endButton + delete this.diagTxt + delete this.version + delete this.tutorialOuter + delete this.linkIssues + delete this.linkEmail + delete this.textarea + } +} diff --git a/public/src/js/abstractfile.js b/public/src/js/abstractfile.js new file mode 100644 index 0000000..62694f5 --- /dev/null +++ b/public/src/js/abstractfile.js @@ -0,0 +1,151 @@ +function readFile(file, arrayBuffer, encoding){ + var reader = new FileReader() + var promise = pageEvents.load(reader).then(event => event.target.result) + reader[arrayBuffer ? "readAsArrayBuffer" : "readAsText"](file, encoding) + return promise +} +function filePermission(file){ + return file.queryPermission().then(response => { + if(response === "granted"){ + return file + }else{ + return file.requestPermission().then(response => { + if(response === "granted"){ + return file + }else{ + return Promise.reject(strings.accessNotGrantedError) + } + }) + } + }) +} +class RemoteFile{ + constructor(...args){ + this.init(...args) + } + init(url){ + this.url = url + try{ + this.path = new URL(url).pathname + }catch(e){ + this.path = url + } + if(this.path.startsWith("/")){ + this.path = this.path.slice(1) + } + if(this.url.startsWith("data:")){ + this.name = "datauri" + if(this.url.startsWith("data:audio/ogg")){ + this.name += ".ogg" + } + }else{ + this.name = this.path + var index = this.name.lastIndexOf("/") + if(index !== -1){ + this.name = this.name.slice(index + 1) + } + } + } + arrayBuffer(){ + return loader.ajax(this.url, request => { + request.responseType = "arraybuffer" + }) + } + read(encoding){ + if(encoding){ + return this.blob().then(blob => readFile(blob, false, encoding)) + }else{ + return loader.ajax(this.url) + } + } + blob(){ + return loader.ajax(this.url, request => { + request.responseType = "blob" + }) + } +} +class LocalFile{ + constructor(...args){ + this.init(...args) + } + init(file, path){ + this.file = file + this.path = path || file.webkitRelativePath + this.url = this.path + this.name = file.name + } + arrayBuffer(){ + return readFile(this.file, true) + } + read(encoding){ + return readFile(this.file, false, encoding) + } + blob(){ + return Promise.resolve(this.file) + } +} +class FilesystemFile{ + constructor(...args){ + this.init(...args) + } + init(file, path){ + this.file = file + this.path = path + this.url = this.path + this.name = file.name + } + arrayBuffer(){ + return this.blob().then(blob => blob.arrayBuffer()) + } + read(encoding){ + return this.blob().then(blob => readFile(blob, false, encoding)) + } + blob(){ + return filePermission(this.file).then(file => file.getFile()) + } +} +class GdriveFile{ + constructor(...args){ + this.init(...args) + } + init(fileObj){ + this.path = fileObj.path + this.name = fileObj.name + this.id = fileObj.id + this.url = gpicker.filesUrl + this.id + "?alt=media" + } + arrayBuffer(){ + return gpicker.downloadFile(this.id, "arraybuffer") + } + read(encoding){ + if(encoding){ + return this.blob().then(blob => readFile(blob, false, encoding)) + }else{ + return gpicker.downloadFile(this.id) + } + } + blob(){ + return gpicker.downloadFile(this.id, "blob") + } +} +class CachedFile{ + constructor(...args){ + this.init(...args) + } + init(contents, oldFile){ + this.contents = contents + this.oldFile = oldFile + this.path = oldFile.path + this.name = oldFile.name + this.url = oldFile.url + } + arrayBuffer(){ + return Promise.resolve(this.contents) + } + read(encoding){ + return this.arrayBuffer() + } + blob(){ + return this.arrayBuffer() + } +} diff --git a/public/src/js/account.js b/public/src/js/account.js new file mode 100644 index 0000000..b2584ef --- /dev/null +++ b/public/src/js/account.js @@ -0,0 +1,682 @@ +class Account{ + constructor(...args){ + this.init(...args) + } + init(touchEnabled){ + this.touchEnabled = touchEnabled + cancelTouch = false + this.locked = false + + if(account.loggedIn){ + this.accountForm() + }else{ + this.loginForm() + } + this.selected = this.items.length - 1 + + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"], + previous: ["left", "up", "ka_l"], + next: ["right", "down", "ka_r"], + back: ["escape"] + }, this.keyPressed.bind(this)) + this.gamepad = new Gamepad({ + "confirm": ["b", "ls", "rs"], + "previous": ["u", "l", "lb", "lt", "lsu", "lsl"], + "next": ["d", "r", "rb", "rt", "lsd", "lsr"], + "back": ["start", "a"] + }, this.keyPressed.bind(this)) + + pageEvents.send("account", account.loggedIn) + } + accountForm(){ + loader.changePage("account", true) + this.mode = "account" + + this.setAltText(this.getElement("view-title"), account.username) + this.items = [] + this.inputForms = [] + this.shownDiv = "" + + this.errorDiv = this.getElement("error-div") + this.getElement("displayname-hint").innerText = strings.account.displayName + this.displayname = this.getElement("displayname") + this.displayname.placeholder = strings.account.displayName + this.displayname.value = account.displayName + this.inputForms.push(this.displayname) + + this.redrawRunning = true + this.redrawPaused = matchMedia("(prefers-reduced-motion: reduce)").matches + this.redrawForce = true + this.customdonRedrawBind = this.customdonRedraw.bind(this) + this.start = new Date().getTime() + this.frames = [ + 0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 , + 0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 , + 0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,7 ,8 ,9 ,10, + 11,11,11,11,10,9 ,8 ,7 ,13,12,12,13,14,15,16,17 + ] + this.customdonCache = new CanvasCache() + this.customdonCache.resize(723 * 2, 1858, 1) + this.customdonCanvas = this.getElement("customdon-canvas") + pageEvents.add(this.customdonCanvas, "click", this.customdonPause.bind(this)) + this.customdonCtx = this.customdonCanvas.getContext("2d") + this.customdonBodyFill = this.getElement("customdon-bodyfill") + this.customdonBodyFill.value = account.don.body_fill + var parent = this.customdonBodyFill.parentNode + parent.insertBefore(document.createTextNode(strings.account.customdon.bodyFill), parent.firstChild) + pageEvents.add(this.customdonBodyFill, ["change", "input"], this.customdonChange.bind(this)) + this.customdonFaceFill = this.getElement("customdon-facefill") + this.customdonFaceFill.value = account.don.face_fill + var parent = this.customdonFaceFill.parentNode + parent.insertBefore(document.createTextNode(strings.account.customdon.faceFill), parent.firstChild) + pageEvents.add(this.customdonFaceFill, ["change", "input"], this.customdonChange.bind(this)) + this.customdonResetBtn = this.getElement("customdon-reset") + this.customdonResetBtn.value = strings.account.customdon.reset + pageEvents.add(this.customdonResetBtn, ["click", "touchstart"], this.customdonReset.bind(this)) + this.customdonChange() + this.customdonRedraw() + + this.accountPassButton = this.getElement("accountpass-btn") + this.setAltText(this.accountPassButton, strings.account.changePassword) + pageEvents.add(this.accountPassButton, ["click", "touchstart"], event => { + this.showDiv(event, "pass") + }) + this.accountPass = this.getElement("accountpass-form") + for(var i = 0; i < this.accountPass.length; i++){ + this.accountPass[i].placeholder = strings.account.currentNewRepeat[i] + this.inputForms.push(this.accountPass[i]) + } + this.accountPassDiv = this.getElement("accountpass-div") + + this.accountDelButton = this.getElement("accountdel-btn") + this.setAltText(this.accountDelButton, strings.account.deleteAccount) + pageEvents.add(this.accountDelButton, ["click", "touchstart"], event => { + this.showDiv(event, "del") + }) + this.accountDel = this.getElement("accountdel-form") + this.accountDel.password.placeholder = strings.account.verifyPassword + this.inputForms.push(this.accountDel.password) + this.accountDelDiv = this.getElement("accountdel-div") + + this.linkPrivacy = this.getElement("privacy-btn") + this.setAltText(this.linkPrivacy, strings.account.privacy) + pageEvents.add(this.linkPrivacy, ["mousedown", "touchstart"], this.openPrivacy.bind(this)) + this.items.push(this.linkPrivacy) + + this.logoutButton = this.getElement("logout-btn") + this.setAltText(this.logoutButton, strings.account.logout) + pageEvents.add(this.logoutButton, ["mousedown", "touchstart"], this.onLogout.bind(this)) + this.items.push(this.logoutButton) + + this.endButton = this.getElement("view-end-button") + this.setAltText(this.endButton, strings.account.cancel) + pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this)) + this.items.push(this.endButton) + + this.saveButton = this.getElement("save-btn") + this.setAltText(this.saveButton, strings.account.save) + pageEvents.add(this.saveButton, ["mousedown", "touchstart"], this.onSave.bind(this)) + this.items.push(this.saveButton) + + for(var i = 0; i < this.inputForms.length; i++){ + pageEvents.add(this.inputForms[i], ["keydown", "keyup", "keypress"], this.onFormPress.bind(this)) + } + } + customdonPause(){ + this.redrawPaused = !this.redrawPaused + this.redrawForce = true + this.start = new Date().getTime() + } + customdonChange(){ + var ctx = this.customdonCtx + this.customdonCache.clear() + var w = 722 + var h = 1858 + this.customdonCache.set({ + w: w, h: h, id: "bodyFill" + }, ctx => { + ctx.drawImage(assets.image["don_anim_normal_b1"], 0, 0) + ctx.globalCompositeOperation = "source-atop" + ctx.fillStyle = this.customdonBodyFill.value + ctx.fillRect(0, 0, w, h) + }) + this.customdonCache.set({ + w: w, h: h, id: "faceFill" + }, ctx => { + ctx.drawImage(assets.image["don_anim_normal_b2"], 0, 0) + ctx.globalCompositeOperation = "source-atop" + ctx.fillStyle = this.customdonFaceFill.value + ctx.fillRect(0, 0, w, h) + + ctx.globalCompositeOperation = "source-over" + this.customdonCache.get({ + ctx: ctx, + x: 0, y: 0, w: w, h: h, + id: "bodyFill" + }) + }) + this.redrawForce = true + } + customdonReset(event){ + if(event.type === "touchstart"){ + event.preventDefault() + } + this.customdonBodyFill.value = defaultDon.body_fill + this.customdonFaceFill.value = defaultDon.face_fill + this.customdonChange() + } + customdonRedraw(){ + if(!this.redrawRunning){ + return + } + requestAnimationFrame(this.customdonRedrawBind) + if(!document.hasFocus() || this.redrawPaused && !this.redrawForce){ + return + } + var ms = new Date().getTime() + var ctx = this.customdonCtx + if(this.redrawPaused){ + var frame = 0 + }else{ + var frame = this.frames[Math.floor((ms - this.start) / 30) % this.frames.length] + } + var w = 360 + var h = 184 + var sx = Math.floor(frame / 10) * (w + 2) + var sy = (frame % 10) * (h + 2) + ctx.clearRect(0, 0, w, h) + this.customdonCache.get({ + ctx: ctx, + sx: sx, sy: sy, sw: w, sh: h, + x: -26, y: 0, w: w, h: h, + id: "faceFill" + }) + ctx.drawImage(assets.image["don_anim_normal_a"], + sx, sy, w, h, + -26, 0, w, h + ) + this.redrawForce = false + } + showDiv(event, div){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + var otherDiv = this.shownDiv && this.shownDiv !== div + var display = this.shownDiv === div ? "" : "block" + this.shownDiv = display ? div : "" + switch(div){ + case "pass": + if(otherDiv){ + this.accountDelDiv.style.display = "" + } + this.accountPassDiv.style.display = display + break + case "del": + if(otherDiv){ + this.accountPassDiv.style.display = "" + } + this.accountDelDiv.style.display = display + break + } + } + loginForm(register, fromSwitch){ + loader.changePage("login", true) + this.mode = register ? "register" : "login" + + this.setAltText(this.getElement("view-title"), strings.account[this.mode]) + + this.errorDiv = this.getElement("error-div") + this.items = [] + this.form = this.getElement("login-form") + this.getElement("username-hint").innerText = strings.account.username + this.form.username.placeholder = strings.account.enterUsername + this.getElement("password-hint").innerText = strings.account.password + this.form.password.placeholder = strings.account.enterPassword + this.password2 = this.getElement("password2-div") + this.remember = this.getElement("remember-div") + this.getElement("remember-label").appendChild(document.createTextNode(strings.account.remember)) + this.loginButton = this.getElement("login-btn") + this.registerButton = this.getElement("register-btn") + + if(register){ + var pass2 = document.createElement("input") + pass2.type = "password" + pass2.name = "password2" + pass2.required = true + pass2.placeholder = strings.account.repeatPassword + this.password2.appendChild(pass2) + this.password2.style.display = "block" + this.remember.style.display = "none" + this.setAltText(this.loginButton, strings.account.registerAccount) + this.setAltText(this.registerButton, strings.account.login) + }else{ + this.setAltText(this.loginButton, strings.account.login) + this.setAltText(this.registerButton, strings.account.register) + } + + pageEvents.add(this.form, "submit", this.onLogin.bind(this)) + pageEvents.add(this.loginButton, ["mousedown", "touchstart"], this.onLogin.bind(this)) + + pageEvents.add(this.registerButton, ["mousedown", "touchstart"], this.onSwitchMode.bind(this)) + this.items.push(this.registerButton) + + this.linkPrivacy = this.getElement("privacy-btn") + this.setAltText(this.linkPrivacy, strings.account.privacy) + pageEvents.add(this.linkPrivacy, ["mousedown", "touchstart"], this.openPrivacy.bind(this)) + this.items.push(this.linkPrivacy) + + if(!register){ + this.items.push(this.loginButton) + } + + for(var i = 0; i < this.form.length; i++){ + pageEvents.add(this.form[i], ["keydown", "keyup", "keypress"], this.onFormPress.bind(this)) + } + + this.endButton = this.getElement("view-end-button") + this.setAltText(this.endButton, strings.account.back) + pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this)) + this.items.push(this.endButton) + if(fromSwitch){ + this.selected = 0 + this.endButton.classList.remove("selected") + this.registerButton.classList.add("selected") + } + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + setAltText(element, text){ + element.innerText = text + element.setAttribute("alt", text) + } + keyPressed(pressed, name){ + if(!pressed || this.locked){ + return + } + var selected = this.items[this.selected] + if(name === "confirm"){ + if(selected === this.endButton){ + this.onEnd() + }else if(selected === this.registerButton){ + this.onSwitchMode() + }else if(selected === this.loginButton){ + this.onLogin() + }else if(selected === this.linkPrivacy){ + assets.sounds["se_don"].play() + this.openPrivacy() + } + }else if(name === "previous" || name === "next"){ + selected.classList.remove("selected") + this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1)) + this.items[this.selected].classList.add("selected") + if(this.items[this.selected] === this.linkPrivacy){ + this.items[this.selected].scrollIntoView() + } + assets.sounds["se_ka"].play() + }else if(name === "back"){ + this.onEnd() + } + } + mod(length, index){ + return ((index % length) + length) % length + } + onFormPress(event){ + event.stopPropagation() + if(event.type === "keypress" && event.keyCode === 13){ + event.preventDefault() + if(this.mode === "account"){ + this.onSave() + }else{ + this.onLogin() + } + } + } + onSwitchMode(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + this.clean(true) + this.loginForm(this.mode === "login", true) + } + onLogin(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + var obj = { + username: this.form.username.value, + password: this.form.password.value + } + if(!obj.username || !obj.password){ + this.error(strings.account.cannotBeEmpty.replace("%s", strings.account[!obj.username ? "username" : "password"])) + return + } + if(this.mode === "login"){ + obj.remember = this.form.remember.checked + }else{ + if(obj.password !== this.form.password2.value){ + this.error(strings.account.passwordsDoNotMatch) + return + } + } + this.request(this.mode, obj).then(response => { + account.loggedIn = true + account.username = response.username + account.displayName = response.display_name + account.don = response.don + var loadScores = scores => { + scoreStorage.load(scores) + this.onEnd(false, true, true) + pageEvents.send("login", account.username) + } + if(this.mode === "login"){ + this.request("scores/get", false, true).then(response => { + loadScores(response.scores) + }, () => { + loadScores({}) + }) + }else{ + scoreStorage.save().catch(() => {}).finally(() => { + this.onEnd(false, true, true) + pageEvents.send("login", account.username) + }) + } + }, response => { + if(response && response.status === "error" && response.message){ + if(response.message in strings.serverError){ + this.error(strings.serverError[response.message]) + }else{ + this.error(response.message) + } + }else{ + this.error(strings.account.error) + } + }) + } + openPrivacy(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + open("privacy") + } + onLogout(){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + account.loggedIn = false + delete account.username + delete account.displayName + delete account.don + var loadScores = () => { + scoreStorage.load() + this.onEnd(false, true) + pageEvents.send("logout") + } + this.request("logout").then(loadScores, loadScores) + } + onSave(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + this.clearError() + var promises = [] + var noNameChange = false + if(this.shownDiv === "pass"){ + var passwords = [] + for(var i = 0; i < this.accountPass.length; i++){ + passwords.push(this.accountPass[i].value) + } + if(passwords[1] === passwords[2]){ + promises.push(this.request("account/password", { + current_password: passwords[0], + new_password: passwords[1] + })) + }else{ + this.error(strings.account.newPasswordsDoNotMatch) + return + } + } + if(this.shownDiv === "del" && this.accountDel.password.value){ + noNameChange = true + promises.push(this.request("account/remove", { + password: this.accountDel.password.value + }).then(() => { + account.loggedIn = false + delete account.username + delete account.displayName + delete account.don + scoreStorage.load() + pageEvents.send("logout") + return Promise.resolve + })) + } + var newName = this.displayname.value.trim() + if(!noNameChange && newName !== account.displayName){ + promises.push(this.request("account/display_name", { + display_name: newName + }).then(response => { + account.displayName = response.display_name + })) + } + var bodyFill = this.customdonBodyFill.value + var faceFill = this.customdonFaceFill.value + if(!noNameChange && (bodyFill !== account.body_fill || this.customdonFaceFill.value !== account.face_fill)){ + promises.push(this.request("account/don", { + body_fill: bodyFill, + face_fill: faceFill + }).then(response => { + account.don = response.don + })) + } + var error = false + var errorFunc = response => { + if(error){ + return + } + if(response && response.message){ + if(response.message in strings.serverError){ + this.error(strings.serverError[response.message]) + }else{ + this.error(response.message) + } + }else{ + this.error(strings.account.error) + } + } + Promise.all(promises).then(() => { + this.onEnd(false, true) + }, errorFunc).catch(errorFunc) + } + onEnd(event, noSound, noReset){ + var touched = false + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + touched = true + }else if(event.which !== 1){ + return + } + } + if(this.locked){ + return + } + this.clean(false, noReset) + assets.sounds["se_don"].play() + setTimeout(() => { + new SongSelect(false, false, touched) + }, 500) + } + request(url, obj, get){ + this.lock(true) + var doRequest = token => { + return new Promise((resolve, reject) => { + var request = new XMLHttpRequest() + request.open(get ? "GET" : "POST", "api/" + url) + pageEvents.load(request).then(() => { + this.lock(false) + if(request.status !== 200){ + reject() + return + } + try{ + var json = JSON.parse(request.response) + }catch(e){ + reject() + return + } + if(json.status === "ok"){ + resolve(json) + }else{ + reject(json) + } + }, () => { + this.lock(false) + reject() + }) + if(!get){ + request.setRequestHeader("X-CSRFToken", token) + } + if(obj){ + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8") + request.send(JSON.stringify(obj)) + }else{ + request.send() + } + }) + } + if(get){ + return doRequest() + }else{ + return loader.getCsrfToken().then(doRequest) + } + } + lock(isLocked){ + this.locked = isLocked + if(this.mode === "login" || this.mode === "register"){ + for(var i = 0; i < this.form.length; i++){ + this.form[i].disabled = isLocked + } + }else if(this.mode === "account"){ + for(var i = 0; i < this.inputForms.length; i++){ + this.inputForms[i].disabled = isLocked + } + } + } + error(text){ + this.errorDiv.innerText = text + this.errorDiv.style.display = "block" + } + clearError(){ + this.errorDiv.innerText = "" + this.errorDiv.style.display = "" + } + clean(eventsOnly, noReset){ + if(!eventsOnly){ + cancelTouch = true + this.keyboard.clean() + this.gamepad.clean() + } + if(this.mode === "account"){ + if(!noReset){ + this.accountPass.reset() + this.accountDel.reset() + } + this.redrawRunning = false + this.customdonCache.clean() + pageEvents.remove(this.customdonCanvas, "click") + pageEvents.remove(this.customdonBodyFill, ["change", "input"]) + pageEvents.remove(this.customdonFaceFill, ["change", "input"]) + pageEvents.remove(this.customdonResetBtn, ["click", "touchstart"]) + pageEvents.remove(this.accounPassButton, ["click", "touchstart"]) + pageEvents.remove(this.accountDelButton, ["click", "touchstart"]) + pageEvents.remove(this.linkPrivacy, ["mousedown", "touchstart"]) + pageEvents.remove(this.logoutButton, ["mousedown", "touchstart"]) + pageEvents.remove(this.saveButton, ["mousedown", "touchstart"]) + for(var i = 0; i < this.inputForms.length; i++){ + pageEvents.remove(this.inputForms[i], ["keydown", "keyup", "keypress"]) + } + delete this.errorDiv + delete this.displayname + delete this.frames + delete this.customdonCanvas + delete this.customdonCtx + delete this.customdonBodyFill + delete this.customdonFaceFill + delete this.customdonResetBtn + delete this.accountPassButton + delete this.accountPass + delete this.accountPassDiv + delete this.accountDelButton + delete this.accountDel + delete this.accountDelDiv + delete this.linkPrivacy + delete this.logoutButton + delete this.saveButton + delete this.inputForms + }else if(this.mode === "login" || this.mode === "register"){ + if(!eventsOnly && !noReset){ + this.form.reset() + } + pageEvents.remove(this.form, "submit") + pageEvents.remove(this.loginButton, ["mousedown", "touchstart"]) + pageEvents.remove(this.registerButton, ["mousedown", "touchstart"]) + for(var i = 0; i < this.form.length; i++){ + pageEvents.remove(this.registerButton, ["keydown", "keyup", "keypress"]) + } + pageEvents.remove(this.linkPrivacy, ["mousedown", "touchstart"]) + delete this.errorDiv + delete this.form + delete this.password2 + delete this.remember + delete this.loginButton + delete this.registerButton + delete this.linkPrivacy + } + pageEvents.remove(this.endButton, ["mousedown", "touchstart"]) + delete this.endButton + delete this.items + } +} diff --git a/public/src/js/assets.js b/public/src/js/assets.js new file mode 100644 index 0000000..24d4232 --- /dev/null +++ b/public/src/js/assets.js @@ -0,0 +1,164 @@ +var assets = { + "js": [ + "lib/md5.min.js", + "lib/fuzzysort.js", + "loadsong.js", + "parseosu.js", + "titlescreen.js", + "scoresheet.js", + "songselect.js", + "keyboard.js", + "gameinput.js", + "game.js", + "controller.js", + "circle.js", + "view.js", + "mekadon.js", + "gamepad.js", + "tutorial.js", + "soundbuffer.js", + "p2.js", + "canvasasset.js", + "viewassets.js", + "gamerules.js", + "canvasdraw.js", + "canvastest.js", + "canvascache.js", + "parsetja.js", + "autoscore.js", + "about.js", + "debug.js", + "session.js", + "importsongs.js", + "logo.js", + "settings.js", + "scorestorage.js", + "account.js", + "lyrics.js", + "customsongs.js", + "abstractfile.js", + "idb.js", + "plugins.js", + "search.js" + ], + "css": [ + "main.css", + "titlescreen.css", + "loadsong.css", + "game.css", + "debug.css", + "songbg.css", + "view.css", + "search.css" + ], + "img": [ + "notes.png", + "notes_drumroll.png", + "notes_hit.png", + "notes_explosion.png", + "balloon.png", + "taiko.png", + "don_anim_normal_a.png", + "don_anim_normal_b1.png", + "don_anim_normal_b2.png", + "don_anim_10combo_a.png", + "don_anim_10combo_b1.png", + "don_anim_10combo_b2.png", + "don_anim_gogo_a.png", + "don_anim_gogo_b1.png", + "don_anim_gogo_b2.png", + "don_anim_gogostart_a.png", + "don_anim_gogostart_b1.png", + "don_anim_gogostart_b2.png", + "don_anim_clear_a.png", + "don_anim_clear_b1.png", + "don_anim_clear_b2.png", + "fire_anim.png", + "fireworks_anim.png", + "bg_score_p1.png", + "bg_score_p2.png", + "bg_pause.png", + "badge_auto.png", + "mimizu.png" + ], + "cssBackground": { + "#title-screen": "title-screen.png", + "#loading-don": "dancing-don.gif", + ".pattern-bg": "bg-pattern-1.png", + ".song-search-result-course::before": "difficulty.png", + "#song-select": "bg_genre_def.png", + ".settings-outer": "bg_settings.png", + "#touch-pause-btn": "touch_pause.png", + "#touch-full-btn": "touch_fullscreen.png", + "#gamepad-bg, #gamepad-buttons": "settings_gamepad.png", + ".song-search-result-crown": "crown.png", + ".song-search-tip-error": "miss.png", + "#song-search": "bg_search.png" + }, + "audioSfx": [ + "se_pause.ogg", + "se_calibration.ogg", + + "v_results.ogg", + "v_sanka.ogg", + "v_songsel.ogg", + "v_start.ogg", + "v_title.ogg" + ], + "audioSfxLR": [ + "neiro_1_don.ogg", + "neiro_1_ka.ogg", + "se_cancel.ogg", + "se_don.ogg", + "se_ka.ogg", + "se_jump.ogg", + + "se_balloon.ogg", + "se_gameclear.ogg", + "se_gamefail.ogg", + "se_gamefullcombo.ogg", + "se_results_countup.ogg", + "se_results_crown.ogg", + + "v_fullcombo.ogg", + "v_renda.ogg", + "v_results_fullcombo.ogg", + "v_results_fullcombo2.ogg" + ], + "audioSfxLoud": [ + "v_diffsel.ogg" + ], + "audioMusic": [ + "bgm_songsel.mp3", + "bgm_result.mp3", + "bgm_setsume.mp3", + "bgm_settings.mp3" + ], + "fonts": { + "Kozuka": "Kozuka.otf", + "TnT": "TnT.ttf" + }, + "views": [ + "game.html", + "loadsong.html", + "songselect.html", + "titlescreen.html", + "tutorial.html", + "about.html", + "debug.html", + "session.html", + "settings.html", + "account.html", + "login.html", + "customsongs.html", + "search.html" + ], + + "songs": [], + "sounds": {}, + "image": {}, + "pages": {}, + "categories": [] +} + +var gameConfig = {} diff --git a/public/src/js/autoscore.js b/public/src/js/autoscore.js new file mode 100644 index 0000000..c1b6b54 --- /dev/null +++ b/public/src/js/autoscore.js @@ -0,0 +1,212 @@ +class AutoScore { + constructor(...args){ + this.init(...args) + } + init(difficulty, level, scoremode, circles) { + this.scoremode = scoremode; + this.circles = circles; + this.basic_max_score_list = { + oni: [ + 1200000, + 700000, + 750000, + 800000, + 850000, + 900000, + 950000, + 1000000, + 1050000, + 1100000, + 1200000 + ], + ura: [ + 1200000, + 700000, + 750000, + 800000, + 850000, + 900000, + 950000, + 1000000, + 1050000, + 1100000, + 1200000 + ], + hard: [ + 900000, + 550000, + 600000, + 650000, + 700000, + 750000, + 800000, + 850000, + 900000, + ], + normal: [ + 700000, + 400000, + 450000, + 500000, + 550000, + 600000, + 650000, + 700000, + ], + easy: [ + 380000, + 300000, + 320000, + 340000, + 360000, + 380000, + ] + } + if (this.GetMaxCombo() === 0) { + this.ScoreDiff = 100; + this.ScoreInit = 450; + return; + } + const target = this.GetTargetScore(difficulty, level); + this.Score = 0; + this.ScoreDiff = 0; + this.ScoreInit = 0; + var max_init = this.GetMaxPossibleInit(target); + var min_init = 0; + while (true) { + this.ScoreInit = (max_init + min_init) / 2; + this.ScoreDiff = Math.round(this.ScoreInit / 4); + this.Score = this.TryScore(this.ScoreInit, this.ScoreDiff); + //console.log(min_init, max_init, this.ScoreInit, this.ScoreDiff, this.Score); + if (this.ScoreInit === target) { + this.ScoreInit = Math.floor(this.ScoreInit / 10) * 10; + this.ScoreDiff = Math.round(this.ScoreInit / 4); + this.Score = this.TryScore(this.ScoreInit, this.ScoreDiff); + break; + } else if (this.Score >= target) { + max_init = this.ScoreInit; + } else { + min_init = this.ScoreInit; + } + if (max_init - min_init <= 10) { + this.ScoreInit = Math.floor(this.ScoreInit / 10) * 10; + this.ScoreDiff = Math.round(this.ScoreInit / 4); + this.Score = this.TryScore(this.ScoreInit, this.ScoreDiff); + break; + } + } + while (this.Score < target) { + this.ScoreInit += 10; + this.ScoreDiff = Math.round(this.ScoreInit / 4); + this.Score = this.TryScore(this.ScoreInit, this.ScoreDiff); + //console.log(this.ScoreInit, this.ScoreDiff, this.Score); + } + //console.log(this.ScoreInit, this.ScoreDiff, this.Score); + } + IsCommonCircle(circle) { + const ty = circle.type; + return ty === "don" || ty === "ka" || ty === "daiDon" || ty === "daiKa"; + } + TryScore(init, diff) { + var score = 0; + var combo = 0; + for (var circle of this.circles) { + if (circle.branch && circle.branch.name !== "master") { + continue; + } + if (this.IsCommonCircle(circle)) { + combo++; + if (combo % 100 === 0 && this.scoremode !== 1) { + score += 10000; + } + } + var diff_mul = 0; + var multiplier = circle.gogoTime ? 1.2 : 1; + if (this.scoremode === 1) { + diff_mul = Math.max(0, Math.floor((Math.min(combo, 100) - 1) / 10)); + } else { + if (combo >= 100) { + diff_mul = 8; + } else if (combo >= 50) { + diff_mul = 4; + } else if (combo >= 30) { + diff_mul = 2; + } else if (combo >= 10) { + diff_mul = 1; + } + } + switch (circle.type) { + case "don": + case "ka": { + score += Math.floor((init + diff * diff_mul) * multiplier / 10) * 10; + break; + } + case "daiDon": + case "daiKa": { + score += Math.floor((init + diff * diff_mul) * multiplier / 5) * 10; + break; + } + case "balloon": { + score += (5000 + 300 * circle.requiredHits) * multiplier; + break; + } + default: { + break; + } + } + } + return score; + } + GetTargetScore(difficulty, level) { + //console.log(difficulty, level) + var ret = this.basic_max_score_list[difficulty][level]; + if (!ret) { + ret = this.basic_max_score_list[difficulty][0]; + } + return ret; + } + GetMaxCombo() { + var combo = 0; + for (var circle of this.circles) { + if (this.IsCommonCircle(circle) && (!circle.branch || circle.branch.name === "master")) { + combo++; + } + } + return combo; + } + GetMaxPossibleInit(target) { + var basic_score = 0; + if (this.scoremode !== 1) { + const max_combo = this.GetMaxCombo(); + basic_score += Math.floor(max_combo / 100); + } + var combo = 0; + for (var circle of this.circles) { + if (circle.branch && circle.branch.name !== "master") { + continue; + } + var multiplier = circle.gogoTime ? 1.2 : 1; + switch (circle.type) { + case "don": + case "ka": { + combo += (1 * multiplier); + break; + } + case "daiDon": + case "daiKa": { + combo += (2 * multiplier); + break; + } + case "balloon": { + basic_score += (5000 + 300 * circle.requiredHits) * multiplier; + break; + } + default: { + break; + } + } + } + combo = Math.floor(combo); + return Math.ceil((target - basic_score) / combo / 10) * 10; + } +} diff --git a/public/src/js/browsersupport.js b/public/src/js/browsersupport.js new file mode 100644 index 0000000..12af3c0 --- /dev/null +++ b/public/src/js/browsersupport.js @@ -0,0 +1,203 @@ +function browserSupport(){ + var tests = { + "Arrow function": function(){ + eval("()=>{}") + return true + }, + "AudioContext": function(){ + if("AudioContext" in window || "webkitAudioContext" in window){ + return typeof (window.AudioContext || window.webkitAudioContext) === "function" + } + return false + }, + "Class": function(){ + eval("class a{}") + return true + }, + "Class field declarations": function(){ + eval("class a{a=1}") + return true + }, + "Array.find": function(){ + return "find" in Array.prototype && "findIndex" in Array.prototype + }, + "Path2D SVG": function(){ + var canvas = document.createElement("canvas") + canvas.width = 1 + canvas.height = 1 + var ctx = canvas.getContext("2d") + var path = new Path2D("M0 0H1V1H0") + ctx.fill(path) + return ctx.getImageData(0, 0, 1, 1).data[3] !== 0 + }, + "Promise": function(){ + if("Promise" in window && "resolve" in window.Promise && "reject" in window.Promise && "all" in window.Promise && "race" in window.Promise){ + var resolve + new window.Promise(function(r){resolve = r}) + return typeof resolve === "function" + } + return false + }, + "CSS calc": function(){ + var el = document.createElement("a") + el.style.width = "calc(1px)" + return el.style.length !== 0 + }, + "let statement": function(){ + eval("let a") + return true + }, + "CSS custom property": function(){ + var el = document.createElement("a") + el.style.setProperty("--a", 1) + return el.style.length !== 0 + }, + "Font Loading API": function(){ + return typeof FontFace === "function" + }, + "OGG or WebAssembly": function(){ + return new Audio().canPlayType("audio/ogg;codecs=vorbis") || "WebAssembly" in window + }, + "KeyboardEvent.key": function(){ + return "key" in KeyboardEvent.prototype + }, + "Module import": function(){ + eval("import('data:text/javascript,')") + return true + } + } + failedTests = [] + for(var name in tests){ + var result = false + try{ + result = tests[name]() + }catch(e){} + if(result === false){ + failedTests.push(name) + } + } + if(failedTests.length !== 0){ + showUnsupported() + } +} +function showUnsupported(strings){ + if(!strings){ + var lang + try{ + if("localStorage" in window && window.localStorage.lang && window.localStorage.lang in allStrings){ + lang = window.localStorage.lang + } + if(!lang && "languages" in navigator){ + var userLang = navigator.languages.slice() + userLang.unshift(navigator.language) + for(var i in userLang){ + for(var j in allStrings){ + if(allStrings[j].regex.test(userLang[i])){ + lang = j + } + } + } + } + }catch(e){} + if(!lang){ + lang = "en" + } + strings = allStrings[lang] + } + + var div = document.getElementById("unsupportedBrowser") + if(div){ + div.parentNode.removeChild(div) + } + div = document.createElement("div") + div.id = "unsupportedBrowser" + + var warn = document.createElement("div") + warn.id = "unsupportedWarn" + warn.innerText = "!" + warn.textContent = "!" + div.appendChild(warn) + var hide = document.createElement("div") + hide.id = "unsupportedHide" + hide.innerText = "x" + hide.textContent = "x" + div.appendChild(hide) + + var span = document.createElement("span") + var browserWarning = strings.browserSupport.browserWarning.split("%s") + for(var i = 0; i < browserWarning.length; i++){ + if(i !== 0){ + var link = document.createElement("a") + link.innerText = strings.browserSupport.details + link.textContent = strings.browserSupport.details + span.appendChild(link) + } + span.appendChild(document.createTextNode(browserWarning[i])) + } + div.appendChild(span) + + var details = document.createElement("div") + details.id = "unsupportedDetails" + details.appendChild(document.createTextNode(strings.browserSupport.failedTests)) + + var ul = document.createElement("ul") + for(var i = 0; i < failedTests.length; i++){ + var li = document.createElement("li") + li.innerText = failedTests[i] + li.textContent = failedTests[i] + ul.appendChild(li) + } + details.appendChild(ul) + + var supportedBrowser = strings.browserSupport.supportedBrowser.split("%s") + for(var i = 0; i < supportedBrowser.length; i++){ + if(i !== 0){ + var chrome = document.createElement("a") + chrome.href = "https://www.google.com/chrome/" + chrome.innerText = "Google Chrome" + chrome.textContent = "Google Chrome" + details.appendChild(chrome) + } + details.appendChild(document.createTextNode(supportedBrowser[i])) + } + + div.appendChild(details) + + document.body.appendChild(div) + var divClick = function(event){ + if(event.type === "touchstart"){ + event.preventDefault() + getSelection().removeAllRanges() + } + div.classList.remove("hidden") + } + div.addEventListener("click", divClick) + div.addEventListener("touchstart", divClick) + var toggleDetails = function(event){ + if(event.type === "touchstart"){ + event.preventDefault() + } + if(details.style.display === "block"){ + details.style.display = "" + }else{ + details.style.display = "block" + } + } + link.addEventListener("click", toggleDetails) + link.addEventListener("touchstart", toggleDetails) + var hideClick = function(event){ + if(event.type === "touchstart"){ + event.preventDefault() + } + event.stopPropagation() + div.classList.add("hidden") + } + hide.addEventListener("click", hideClick) + hide.addEventListener("touchstart", hideClick) + chrome.addEventListener("touchend", function(event){ + event.preventDefault() + chrome.click() + }) +} +var failedTests +browserSupport() diff --git a/public/src/js/canvasasset.js b/public/src/js/canvasasset.js new file mode 100644 index 0000000..03f6ec3 --- /dev/null +++ b/public/src/js/canvasasset.js @@ -0,0 +1,151 @@ +class CanvasAsset{ + constructor(...args){ + this.init(...args) + } + init(view, layer, position){ + this.ctx = view.ctx + this.view = view + this.position = position + this.animationFrames = {} + this.speed = 1000 / 60 + this.animationStart = 0 + this.layer = layer + this.beatInterval = 468.75 + } + draw(){ + if(this.animation){ + var u = (a, b) => typeof a === "undefined" ? b : a + var frame = 0 + var ms = this.view.getMS() + var beatInterval = this.frameSpeed ? 1000 / 60 : this.beatInterval + + if(this.animationEnd){ + if(ms > this.animationStart + this.animationEnd.frameCount * this.speed * beatInterval){ + this.animationEnd.callback() + this.animationEnd = false + return this.draw() + } + } + var index = Math.floor((ms - this.animationStart) / (this.speed * beatInterval)) + if(Array.isArray(this.animation)){ + frame = this.animation[this.mod(this.animation.length, index)] + }else{ + frame = this.mod(this.animation, index) + } + this.ctx.save() + var pos = this.position(frame) + if(this.image){ + this.ctx.drawImage(this.image, + u(pos.sx, pos.x), u(pos.sy, pos.y), + u(pos.sw, pos.w), u(pos.sh, pos.h), + pos.x, pos.y, pos.w, pos.h + ) + } + this.ctx.restore() + } + } + mod(length, index){ + return ((index % length) + length) % length + } + addFrames(name, frames, image, don){ + var framesObj = { + frames: frames, + don: don + } + if(don){ + var img = assets.image[image + "_a"] + var cache1 = new CanvasCache() + var cache2 = new CanvasCache() + var w = img.width + var h = img.height + cache1.resize(w, h, 1) + cache2.resize(w, h, 1) + cache1.set({ + w: w, h: h, id: "1" + }, ctx => { + ctx.drawImage(assets.image[image + "_b1"], 0, 0) + ctx.globalCompositeOperation = "source-atop" + ctx.fillStyle = don.body_fill + ctx.fillRect(0, 0, w, h) + }) + cache2.set({ + w: w, h: h, id: "2" + }, ctx => { + ctx.drawImage(assets.image[image + "_b2"], 0, 0) + ctx.globalCompositeOperation = "source-atop" + ctx.fillStyle = don.face_fill + ctx.fillRect(0, 0, w, h) + + ctx.globalCompositeOperation = "source-over" + cache1.get({ + ctx: ctx, + x: 0, y: 0, w: w, h: h, + id: "1" + }) + ctx.drawImage(img, 0, 0) + }) + cache1.clean() + framesObj.cache = cache2 + framesObj.image = cache2.canvas + }else if(image){ + framesObj.image = assets.image[image] + } + this.animationFrames[name] = framesObj + } + setAnimation(name){ + var framesObj = this.animationFrames[name] + this.animationName = name + if(framesObj){ + this.animation = framesObj.frames + if(framesObj.image){ + this.image = framesObj.image + } + this.don = framesObj.don + }else{ + this.animation = false + } + } + getAnimation(){ + return this.animationName + } + getAnimationLength(name){ + var frames = this.animationFrames[name].frames + if(Array.isArray(frames)){ + return frames.length + }else{ + return frames + } + } + setUpdateSpeed(speed, frameSpeed){ + this.speed = speed + this.frameSpeed = frameSpeed + } + setAnimationStart(ms){ + this.animationStart = ms + } + setAnimationEnd(frameCount, callback){ + if(typeof frameCount === "undefined"){ + this.animationEnd = false + }else{ + this.animationEnd = { + frameCount: frameCount, + callback: callback + } + } + } + changeBeatInterval(beatMS, initial){ + if(!initial && !this.frameSpeed){ + var ms = this.view.getMS() + this.animationStart = ms - (ms - this.animationStart) / this.beatInterval * beatMS + } + this.beatInterval = beatMS + } + clean(){ + for(var i in this.animationFrames){ + var frame = this.animationFrames[i] + if(frame.cache){ + frame.cache.clean() + } + } + } +} diff --git a/public/src/js/canvascache.js b/public/src/js/canvascache.js new file mode 100644 index 0000000..afa2cfb --- /dev/null +++ b/public/src/js/canvascache.js @@ -0,0 +1,129 @@ +class CanvasCache{ + constructor(...args){ + this.init(...args) + } + init(noSmoothing, w, h, scale){ + this.noSmoothing = noSmoothing + if(w){ + this.resize(w, h, scale) + } + this.index = Number.MIN_SAFE_INTEGER + } + resize(w, h, scale){ + if(this.canvas){ + this.map.clear() + }else{ + this.map = new Map() + this.canvas = document.createElement("canvas") + this.ctx = this.canvas.getContext("2d") + if(this.noSmoothing){ + this.ctx.imageSmoothingEnabled = false + } + } + this.scale = scale + this.x = 0 + this.y = 0 + this.w = w + this.h = h + this.lastW = 0 + this.lastH = 0 + this.canvas.width = Math.max(1, this.w * this.scale) + this.canvas.height = Math.max(1, this.h * this.scale) + this.ctx.scale(this.scale, this.scale) + } + get(config, callback, setOnly){ + var img = this.map.get(config.id) + if(img && setOnly || !img && !callback){ + return + } + var saved = false + var index = this.index++ + if(!img){ + var w = config.w + var h = config.h + this.x += this.lastW + (this.lastW ? 1 : 0) + if(this.x + w > this.w){ + this.x = 0 + this.y += this.lastH + 1 + } + if(this.y + h > this.h){ + var clear = true + var oldest = {index: index} + this.map.forEach((oldImg, id) => { + if(oldImg.index < oldest.index){ + oldest.id = id + oldest.index = oldImg.index + } + }) + var oldImg = this.map.get(oldest.id) + this.map.delete(oldest.id) + img = { + x: oldImg.x, + y: oldImg.y, + w: w, + h: h + } + }else{ + var clear = false + this.lastW = w + this.lastH = Math.max(this.lastH, h) + img = { + x: this.x, + y: this.y, + w: w, + h: h + } + } + + saved = true + this.ctx.save() + this.ctx.translate(img.x |0, img.y |0) + if(clear){ + this.ctx.clearRect(0, 0, (img.w |0) + 1, (img.h |0) + 1) + } + this.ctx.beginPath() + this.ctx.rect(0, 0, img.w |0, img.h |0) + this.ctx.clip() + + this.map.set(config.id, img) + callback(this.ctx) + } + img.index = index + if(setOnly){ + this.ctx.restore() + return + } + var z = this.scale + var sx = (img.x + (config.sx || 0)) * z |0 + var sy = (img.y + (config.sy || 0)) * z |0 + var sw = (config.sw || img.w) * z |0 + var sh = (config.sh || img.h) * z |0 + config.ctx.drawImage(this.canvas, + sx, sy, sw, sh, + config.x |0, config.y |0, config.w |0, config.h |0 + ) + if(saved){ + this.ctx.restore() + } + } + set(config, callback){ + return this.get(config, callback, true) + } + clear(){ + this.x = 0 + this.y = 0 + this.lastW = 0 + this.lastH = 0 + this.map.clear() + this.ctx.clearRect(0, 0, this.w, this.h) + } + clean(){ + if(!this.canvas){ + return + } + this.resize(1, 1, 1) + delete this.map + delete this.ctx + delete this.canvas + } +} diff --git a/public/src/js/canvasdraw.js b/public/src/js/canvasdraw.js new file mode 100644 index 0000000..6d822cb --- /dev/null +++ b/public/src/js/canvasdraw.js @@ -0,0 +1,1733 @@ +class CanvasDraw{ + constructor(...args){ + this.init(...args) + } + init(noSmoothing){ + this.diffStarPath = new Path2D(vectors.diffStar) + this.longVowelMark = new Path2D(vectors.longVowelMark) + + this.diffIconPath = [[{w: 40, h: 33}, { + fill: "#ff2803", + d: new Path2D(vectors.diffEasy1) + }, { + fill: "#ffb910", + noStroke: true, + d: new Path2D(vectors.diffEasy2) + }], [{w: 48, h: 31}, { + fill: "#8daf51", + d: new Path2D(vectors.diffNormal) + }], [{w: 56, h: 37}, { + fill: "#784439", + d: new Path2D(vectors.diffHard1) + }, { + fill: "#000", + noStroke: true, + d: new Path2D(vectors.diffHard2) + }, { + fill: "#414b2b", + d: new Path2D(vectors.diffHard3) + }], [{w: 29, h: 27}, { + fill: "#db1885", + d: new Path2D(vectors.diffOni1) + }, { + fill: "#fff", + d: new Path2D(vectors.diffOni2) + }]] + + this.diffPath = { + good: new Path2D(vectors.good), + ok: new Path2D(vectors.ok), + bad: new Path2D(vectors.bad) + } + + this.crownPath = new Path2D(vectors.crown) + this.soulPath = new Path2D(vectors.soul) + + this.optionsPath = { + main: new Path2D(vectors.options), + shadow: new Path2D(vectors.optionsShadow) + } + + this.categoryPath = { + main: new Path2D(vectors.category), + shadow: new Path2D(vectors.categoryShadow), + highlight: new Path2D(vectors.categoryHighlight) + } + + this.regex = { + comma: /[,.]/, + ideographicComma: /[、。]/, + apostrophe: /[''’]/, + degree: /[゚°]/, + brackets: /[\((\))\[\]「」『』【】::;;≠]/, + tilde: /[\--~~〜_]/, + tall: /[bbddffgghhj-lj-ltt♪]/, + i: /[ii]/, + uppercase: /[A-ZA-Z]/, + lowercase: /[a-za-z・]/, + latin: /[A-ZA-Za-za-z・]/, + numbers: /[0-90-9]/, + exclamation: /[!!\?? ]/, + question: /[\??]/, + smallHiragana: /[ぁぃぅぇぉっゃゅょァィゥェォッャュョ]/, + hiragana: /[\u3040-\u30ff]/, + todo: /[トド]/, + en: /[ceghknsuxyzceghknsuxyzçèéêëùúûü]/, + em: /[mwmw]/, + emCap: /[MWMW]/, + rWidth: /[abdfIjo-rtvabdfIjo-rtvàáâäòóôö]/, + lWidth: /[ililìíîï]/, + ura: /\s*[\((]裏[\))]$/, + cjk: /[\u3040-ゞ゠-ヾ一-\u9ffe]/ + } + + var numbersFull = "0123456789" + var numbersHalf = "0123456789" + this.numbersFullToHalf = {} + for(var i = 0; i < 10; i++){ + this.numbersFullToHalf[numbersFull[i]] = numbersHalf[i] + this.numbersFullToHalf[numbersHalf[i]] = numbersHalf[i] + } + this.wrapOn = [" ", "\n", "%s"] + this.stickySymbols = "!,.:;?~‐–‼、。々〜ぁぃぅぇぉっゃゅょァィゥェォッャュョ・ーヽヾ!:;?" + + this.songFrameCache = new CanvasCache(noSmoothing) + this.diffStarCache = new CanvasCache(noSmoothing) + this.crownCache = new CanvasCache(noSmoothing) + + this.tmpCanvas = document.createElement("canvas") + this.tmpCtx = this.tmpCanvas.getContext("2d") + } + + roundedRect(config){ + var ctx = config.ctx + var x = config.x + var y = config.y + var w = config.w + var h = config.h + var r = config.radius + ctx.beginPath() + this.roundedCorner(ctx, x, y, r, 0) + this.roundedCorner(ctx, x + w, y, r, 1) + this.roundedCorner(ctx, x + w, y + h, r, 2) + this.roundedCorner(ctx, x, y + h, r, 3) + ctx.closePath() + } + + roundedCorner(ctx, x, y, r, rotation){ + var pi = Math.PI + switch(rotation){ + case 0: + return ctx.arc(x + r, y + r, r, pi, pi / -2) + case 1: + return ctx.arc(x - r, y + r, r, pi / -2, 0) + case 2: + return ctx.arc(x - r, y - r, r, 0, pi / 2) + case 3: + return ctx.arc(x + r, y - r, r, pi / 2, pi) + } + } + + songFrame(config){ + var ctx = config.ctx + var x = config.x + var y = config.y + var w = config.width + var h = config.height + var border = config.border + var innerBorder = config.innerBorder + var allBorders = border + innerBorder + var innerX = x + allBorders + var innerY = y + allBorders + var innerW = w - allBorders * 2 + var innerH = h - allBorders * 2 + + ctx.save() + + var shadowBg = (ctx, noForce) => { + this.shadow({ + ctx: ctx, + fill: "rgba(0, 0, 0, 0.5)", + blur: 10, + x: 5, + y: 5, + force: !noForce + }) + ctx.fillStyle = "rgba(0,0,0,.2)" + ctx.fillRect(0, 0, w, h) + } + if(config.cached){ + if(this.songFrameCache.w !== config.frameCache.w || this.songFrameCache.scale !== config.frameCache.ratio){ + this.songFrameCache.resize(config.frameCache.w, config.frameCache.h, config.frameCache.ratio) + } + this.songFrameCache.get({ + ctx: ctx, + x: x, + y: y, + w: w + 15, + h: h + 15, + id: "shadow" + config.cached + }, shadowBg) + }else{ + ctx.translate(x, y) + shadowBg(ctx, true) + } + + ctx.restore() + ctx.save() + + { + let _x = x + border + let _y = y + border + let _w = w - border * 2 + let _h = h - border * 2 + ctx.fillStyle = config.borderStyle[1] + ctx.fillRect(_x, _y, _w, _h) + ctx.fillStyle = config.borderStyle[0] + ctx.beginPath() + ctx.moveTo(_x, _y) + ctx.lineTo(_x + _w, _y) + ctx.lineTo(_x + _w - innerBorder, _y + innerBorder) + ctx.lineTo(_x + innerBorder, _y + _h - innerBorder) + ctx.lineTo(_x, _y + _h) + ctx.fill() + } + ctx.fillStyle = config.background + ctx.fillRect(innerX, innerY, innerW, innerH) + + ctx.save() + + ctx.strokeStyle = "rgba(255, 255, 255, 0.3)" + ctx.lineWidth = 3 + ctx.strokeRect(innerX, innerY, innerW, innerH) + if(!config.noCrop){ + ctx.beginPath() + ctx.rect(innerX, innerY, innerW, innerH) + ctx.clip() + } + + config.innerContent(innerX, innerY, innerW, innerH) + + ctx.restore() + + if(config.disabled){ + ctx.fillStyle = "rgba(0, 0, 0, 0.5)" + ctx.fillRect(x, y, w, h) + } + + if(config.highlight){ + this.highlight({ + ctx: ctx, + x: x, + y: y, + w: w, + h: h, + animate: config.highlight === 2, + animateMS: config.animateMS, + opacity: config.highlight === 1 ? 0.8 : 1 + }) + } + + ctx.restore() + } + + highlight(config){ + var ctx = config.ctx + ctx.save() + if(config.shape){ + ctx.translate(config.x, config.y) + }else{ + var _x = config.x + 3.5 + var _y = config.y + 3.5 + var _w = config.w - 7 + var _h = config.h - 7 + } + if(config.animate){ + ctx.globalAlpha = this.fade((this.getMS() - config.animateMS) % 2000 / 2000) + }else if(config.opacity){ + ctx.globalAlpha = config.opacity + } + if(config.radius){ + this.roundedRect({ + ctx: ctx, + x: _x, + y: _y, + w: _w, + h: _h, + radius: config.radius + }) + }else if(!config.shape){ + ctx.beginPath() + ctx.rect(_x, _y, _w, _h) + } + if(config.shape){ + var stroke = () => ctx.stroke(config.shape) + }else{ + var stroke = () => ctx.stroke() + } + var size = config.size || 14 + ctx.strokeStyle = "rgba(255, 249, 1, 0.45)" + ctx.lineWidth = size + stroke() + ctx.strokeStyle = "rgba(255, 249, 1, .8)" + ctx.lineWidth = 8 / 14 * size + stroke() + ctx.strokeStyle = "#fff" + ctx.lineWidth = 6 / 14 * size + stroke() + + ctx.restore() + } + fade(pos){ + if(pos < 0.5){ + pos = 1 - pos + } + return (1 - Math.cos(Math.PI * pos * 2)) / 2 + } + easeIn(pos){ + return 1 - Math.cos(Math.PI / 2 * pos) + } + easeOut(pos){ + return Math.sin(Math.PI / 2 * pos) + } + easeOutBack(pos){ + return Math.sin(Math.PI / 1.74 * pos) * 1.03 + } + easeInOut(pos){ + return (Math.cos(Math.PI * pos) - 1) / -2 + } + + verticalText(config){ + var ctx = config.ctx + var inputText = "" + config.text + var mul = config.fontSize / 40 + var ura = false + var r = this.regex + + var matches = inputText.match(r.ura) + if(matches){ + inputText = inputText.slice(0, matches.index) + ura = matches[0] + } + var bold = this.bold(config.fontFamily) + + var string = inputText.split("") + var drawn = [] + var quoteOpened = false + + for(var i = 0; i < string.length; i++){ + let symbol = string[i] + if(symbol === " "){ + // Space + drawn.push({text: symbol, x: 0, y: 0, h: 18}) + }else if(symbol === "ー"){ + // Long-vowel mark + if(bold){ + drawn.push({text: symbol, x: -1, y: -1, h: 33, rotate: true}) + }else{ + drawn.push({realText: symbol, svg: this.longVowelMark, x: -4, y: 5, h: 33, scale: [mul, mul]}) + } + }else if(symbol === "∀"){ + drawn.push({text: symbol, x: 0, y: 0, h: 39, rotate: true}) + }else if(symbol === "↓"){ + drawn.push({text: symbol, x: 0, y: 12, h: 45}) + }else if(symbol === "."){ + if(bold){ + drawn.push({realText: symbol, text: ".", x: 13, y: -15, h: 15}) + }else{ + drawn.push({realText: symbol, text: ".", x: 13, y: -7, h: 15, scale: [1.2, 0.7]}) + } + }else if(symbol === "…"){ + drawn.push({text: symbol, x: bold ? 9 : 0, y: 5, h: 25, rotate: true}) + }else if(symbol === '"'){ + if(quoteOpened){ + drawn.push({realText: symbol, text: "“", x: -25, y: 10, h: 20}) + }else{ + drawn.push({realText: symbol, text: "”", x: 12, y: 15, h: 20}) + } + quoteOpened = !quoteOpened + }else if(r.comma.test(symbol)){ + // Comma, full stop + if(bold){ + drawn.push({text: symbol, x: 13, y: -15, h: 15}) + }else{ + drawn.push({text: symbol, x: 13, y: -7, h: 15, scale: [1.2, 0.7]}) + } + }else if(r.ideographicComma.test(symbol)){ + // Ideographic comma, full stop + drawn.push({text: symbol, x: 16, y: -16, h: 18}) + }else if(r.apostrophe.test(symbol)){ + // Apostrophe + if(bold){ + drawn.push({text: symbol, x: 20, y: -25, h: 0}) + }else{ + drawn.push({realText: symbol, text: ",", x: 20, y: -39, h: 0, scale: [1.2, 0.7]}) + } + }else if(r.degree.test(symbol)){ + // Degree + if(bold){ + drawn.push({text: symbol, x: 16, y: 9, h: 25}) + }else{ + drawn.push({text: symbol, x: 16, y: 3, h: 18}) + } + }else if(r.brackets.test(symbol)){ + // Rotated brackets + if(bold){ + drawn.push({text: symbol, x: 0, y: 0, h: 35, rotate: true}) + }else{ + drawn.push({text: symbol, x: 0, y: -5, h: 25, rotate: true}) + } + }else if(r.tilde.test(symbol)){ + // Rotated hyphen, tilde + drawn.push({realText: symbol, text: symbol === "~" ? "~" : symbol, x: 0, y: 2, h: 35, rotate: true}) + }else if(r.tall.test(symbol)){ + // Tall latin script lowercase + drawn.push({text: symbol, x: 0, y: 4, h: 34}) + }else if(r.i.test(symbol)){ + // Lowercase i + drawn.push({text: symbol, x: 0, y: 7, h: 34}) + }else if(r.uppercase.test(symbol)){ + // Latin script upper case + drawn.push({text: symbol, x: 0, y: 8, h: 37}) + }else if(r.lowercase.test(symbol)){ + // Latin script lower case + drawn.push({text: symbol, x: 0, y: -1, h: 28}) + }else if(r.numbers.test(symbol)){ + // Numbers + var number = this.numbersFullToHalf[symbol] + drawn.push({realText: symbol, text: number, x: 0, y: 4, h: 34}) + }else if(r.exclamation.test(symbol)){ + // Exclamation mark + var toDraw = [symbol] + for(var repeat = 1; repeat - 1 < i; repeat++){ + if(!r.exclamation.test(string[i - repeat])){ + break + } + toDraw.push(string[i - repeat]) + } + if(repeat > 1){ + drawn.splice(i - repeat + 1, repeat) + var allExclamations = !toDraw.find(a => a !== "!") + + for(var j = 1; j < repeat + 1; j++){ + var text = string[i - repeat + j] + if(allExclamations){ + var y = 18 + var h = 61 + }else{ + var y = 8 + var h = 37 + } + if(i === repeat - 1){ + h -= y - 4 + y = 4 + } + + var addX = bold && (text === "!" || text === "?") ? 10 : 0 + drawn.push({ + text: text, + x: ((j - 1) - (repeat - 1) / 2) * 15 + addX, + y: y - (j === 1 ? 0 : h), + h: j === 1 ? h : 0 + }) + } + }else{ + var addX = bold && (symbol === "!" || symbol === "?") ? 10 : 0 + drawn.push({text: symbol, x: addX, y: 8, h: 37}) + } + }else if(r.smallHiragana.test(symbol)){ + // Small hiragana, small katakana + drawn.push({text: symbol, x: 0, y: -8, h: 25, right: true}) + }else if(r.hiragana.test(symbol)){ + // Hiragana, katakana + drawn.push({text: symbol, x: 0, y: 5, h: 38, right: r.todo.test(symbol)}) + }else{ + // Kanji, other + drawn.push({text: symbol, x: 0, y: 3, h: 39}) + } + } + + var drawnHeight = 0 + for(let symbol of drawn){ + if(config.letterSpacing){ + symbol.h += config.letterSpacing + } + drawnHeight += symbol.h * mul + } + + ctx.save() + ctx.translate(config.x, config.y) + + if(config.selectable){ + config.selectable.innerHTML = "" + var scale = config.selectableScale + var style = config.selectable.style + style.left = ((config.x - config.width / 2) * scale + (config.selectableX || 0)) + "px" + style.top = (config.y * scale + (config.selectableY || 0)) + "px" + style.width = config.width * scale + "px" + style.height = (drawnHeight+15) * scale + "px" + style.fontSize = 40 * mul * scale + "px" + style.transform = "" + } + + var scaling = 1 + var strokeScaling = 1 + var height = config.height - (ura ? 52 * mul : 0) + if(height && drawnHeight > height){ + scaling = height / drawnHeight + if(config.align === "bottom"){ + strokeScaling = Math.max(0.6, height / drawnHeight) + ctx.translate(40 * mul, 0) + ctx.scale(strokeScaling, scaling) + ctx.translate(-40 * mul, 0) + }else{ + strokeScaling = scaling + ctx.scale(1, scaling) + } + if(config.selectable){ + style.transform = "scale(1, " + scaling + ")" + style.top = (config.y + (height - drawnHeight) / 2 - 15 / 2 * scaling) * scale + "px" + } + } + + if(ura){ + // Circled ura + drawn.push({realText: ura, text: "裏", x: 0, y: 25, h: 52, ura: true, scale: [1, 1 / scaling]}) + } + + if(config.align === "bottom"){ + drawn.reverse() + } + + var actions = [] + if(config.outline){ + actions.push("stroke") + } + if(config.fill){ + actions.push("fill") + } + if(config.selectable){ + actions.push("selectable") + } + for(let action of actions){ + ctx.font = bold + config.fontSize + "px " + config.fontFamily + ctx.textBaseline = "top" + if(action === "stroke"){ + ctx.strokeStyle = config.outline + ctx.lineWidth = config.outlineSize * mul + if(config.align === "bottom"){ + ctx.lineWidth /= strokeScaling + } + ctx.lineJoin = "round" + ctx.miterLimit = 1 + }else if(action === "fill"){ + ctx.fillStyle = config.fill + } + if(config.align === "bottom"){ + var offsetY = drawnHeight > config.height ? drawnHeight : config.height + }else{ + var offsetY = 0 + } + + for(let symbol of drawn){ + var saved = false + var currentX = symbol.x + if(symbol.right){ + currentX += 20 * mul + } + var currentY = offsetY + symbol.y * mul + if(config.align === "bottom"){ + currentY -= symbol.h * mul + } + offsetY = offsetY + symbol.h * mul * (config.align === "bottom" ? -1 : 1) + if(action === "selectable"){ + let div = document.createElement("div") + div.classList.add("stroke-sub") + let text = symbol.realText || symbol.text + let textWidth = ctx.measureText(text).width + let transform = [] + if(symbol.scale){ + transform.push("scale(" + symbol.scale[0] + "," + symbol.scale[1] + ")") + } + if(symbol.rotate || symbol.realText === "ー"){ + transform.push("rotate(90deg)") + } + if(transform.length){ + div.style.transform = transform.join(" ") + } + if(symbol.right){ + currentX = currentX + config.width / 2 - textWidth + }else{ + currentX = currentX + config.width / 2 - textWidth / 2 + } + if(symbol.ura){ + div.style.font = (30 / (40 * mul)) + "em Meiryo, sans-serif" + } + div.style.left = currentX * scale + "px" + div.style.top = currentY * scale + "px" + div.appendChild(document.createTextNode(text)) + div.setAttribute("alt", text) + config.selectable.appendChild(div) + continue + } + if(symbol.rotate || symbol.scale || symbol.svg || symbol.ura){ + saved = true + ctx.save() + + if(symbol.rotate){ + ctx.translate(currentX + 20 * mul, currentY + 20 * mul) + ctx.rotate(Math.PI / 2) + }else{ + ctx.translate(currentX, currentY) + } + if(symbol.scale){ + ctx.scale(symbol.scale[0], symbol.scale[1]) + ctx.lineWidth = ctx.lineWidth / symbol.scale[0] + } + currentX = 0 + currentY = 0 + } + if(symbol.svg){ + ctx[action](symbol.svg) + }else{ + if(symbol.right){ + ctx.textAlign = "right" + }else{ + ctx.textAlign = "center" + } + if(symbol.ura){ + ctx.font = (30 * mul) + "px Meiryo, sans-serif" + ctx.textBaseline = "middle" + ctx.beginPath() + ctx.arc(currentX, currentY + (17 * mul), (18 * mul), 0, Math.PI * 2) + if(action === "stroke"){ + ctx.fillStyle = config.outline + ctx.fill() + }else if(action === "fill"){ + ctx.strokeStyle = config.fill + ctx.lineWidth = 2.5 * mul + ctx.fillText(symbol.text, currentX, currentY + (17 * mul)) + } + ctx.stroke() + }else{ + ctx[action + "Text"](symbol.text, currentX, currentY) + } + } + if(saved){ + ctx.restore() + } + } + } + ctx.restore() + } + + layeredText(config, layers){ + var ctx = config.ctx + var inputText = "" + config.text + var mul = config.fontSize / 40 + var ura = false + var r = this.regex + + var matches = inputText.match(r.ura) + if(matches){ + inputText = inputText.slice(0, matches.index) + ura = matches[0] + } + var bold = this.bold(config.fontFamily) + + var string = inputText.split("") + var drawn = [] + + for(var i = 0; i < string.length; i++){ + let symbol = string[i] + + if(symbol === "-"){ + drawn.push({text: symbol, x: -2, y: 0, w: 28}) + }else if(symbol === "™"){ + drawn.push({text: symbol, x: -2, y: 0, w: 20, scale: [0.6, 0.5]}) + }else if(symbol === " "){ + drawn.push({text: symbol, x: 0, y: 0, w: 10}) + }else if(symbol === '"'){ + drawn.push({text: symbol, x: 2, y: 0, w: 10}) + }else if(symbol === "∀"){ + if(bold){ + drawn.push({text: symbol, x: 0, y: 0, w: 40}) + }else{ + drawn.push({text: symbol, x: -3, y: 0, w: 55}) + } + }else if(symbol === "."){ + drawn.push({text: symbol, x: -9, y: 0, w: 37}) + }else if(r.apostrophe.test(symbol)){ + drawn.push({text: ",", x: 0, y: -15, w: 7, scale: [1, 0.7]}) + }else if(r.comma.test(symbol)){ + // Comma, full stop + if(bold){ + drawn.push({text: symbol, x: -3, y: 0, w: 13}) + }else{ + drawn.push({text: symbol, x: -3, y: 13, w: 13, scale: [1.2, 0.7]}) + } + }else if(r.tilde.test(symbol)){ + // Hyphen, tilde + drawn.push({text: symbol === "~" ? "~" : symbol, x: 0, y: 0, w: 39}) + }else if(r.en.test(symbol)){ + // n-width + drawn.push({text: symbol, x: 0, y: 0, w: 28}) + }else if(r.em.test(symbol)){ + // m-width + drawn.push({text: symbol, x: 0, y: 0, w: 38}) + }else if(r.rWidth.test(symbol)){ + // r-width + drawn.push({text: symbol, x: 0, y: 0, w: 24}) + }else if(r.lWidth.test(symbol)){ + // l-width + drawn.push({text: symbol, x: 0, y: 0, w: 12}) + }else if(r.emCap.test(symbol)){ + // m-width uppercase + drawn.push({text: symbol, x: 0, y: 0, w: 38}) + }else if(r.numbers.test(symbol)){ + // Numbers + var number = this.numbersFullToHalf[symbol] + drawn.push({text: number, x: 0, y: 0, w: 32}) + }else if(r.degree.test(symbol)){ + // Degree + if(bold){ + drawn.push({text: symbol, x: 0, y: 0, w: 20}) + }else{ + drawn.push({text: symbol, x: 5, y: 0, w: 0}) + } + }else if(r.uppercase.test(symbol)){ + // Latin script uppercase + drawn.push({text: symbol, x: 0, y: 0, w: 32}) + }else if(r.exclamation.test(symbol)){ + // Exclamation mark + var nextExclamation = string[i + 1] ? r.exclamation.test(string[i + 1]) : false + drawn.push({ + text: symbol, + x: nextExclamation ? 4 : -1, + y: 0, + w: nextExclamation ? 16 : 28 + }) + }else if(r.smallHiragana.test(symbol)){ + // Small hiragana, small katakana + drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 30}) + }else if(r.hiragana.test(symbol)){ + // Hiragana, katakana + drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 35}) + }else{ + drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 39}) + } + } + + var drawnWidth = 0 + for(let symbol of drawn){ + if(config.letterSpacing){ + symbol.w += config.letterSpacing + } + if(config.kanaSpacing && symbol.kana){ + symbol.w += config.kanaSpacing + } + drawnWidth += symbol.w * mul + } + + ctx.save() + ctx.translate(config.x, config.y) + + if(config.scale){ + ctx.scale(config.scale[0], config.scale[1]) + } + var scaling = 1 + var width = config.width - (ura ? 55 * mul : 0) + if(width && drawnWidth > width){ + scaling = width / drawnWidth + ctx.scale(scaling, 1) + } + + if(ura){ + // Circled ura + drawn.push({text: "裏", x: 0, y: 3, w: 55, ura: true, scale: [1 / scaling, 1]}) + } + + if(config.align === "right"){ + drawn.reverse() + } + + ctx.font = bold + config.fontSize + "px " + config.fontFamily + ctx.textBaseline = config.baseline || "top" + ctx.textAlign = "center" + + for(let layer of layers){ + var savedLayer = false + var action = "strokeText" + if(layer.scale){ + savedLayer = true + ctx.save() + ctx.scale(layer.scale[0], layer.scale[1]) + } + if(layer.outline){ + ctx.strokeStyle = layer.outline + ctx.lineJoin = "round" + ctx.miterLimit = 1 + } + if(layer.letterBorder){ + ctx.lineWidth = layer.letterBorder + } + if(layer.fill){ + ctx.fillStyle = layer.fill + action = "fillText" + } + if(layer.shadow){ + if(!savedLayer){ + savedLayer = true + ctx.save() + } + this.shadow({ + ctx: ctx, + fill: "rgba(0, 0, 0, " + (1 / (layer.shadow[3] || 2)) + ")", + blur: layer.shadow[2], + x: layer.shadow[0], + y: layer.shadow[1], + force: config.forceShadow + }) + } + var offsetX = 0 + for(let symbol of drawn){ + var saved = false + var currentX = offsetX + symbol.x * mul + (layer.x || 0) + symbol.w * mul / 2 + var currentY = symbol.y + (layer.y || 0) + + if(config.align === "center"){ + currentX -= drawnWidth / 2 + }else if(config.align === "right"){ + currentX = -offsetX + symbol.x + (layer.x || 0) - symbol.w / 2 + } + if(symbol.scale || symbol.ura){ + saved = true + ctx.save() + ctx.translate(currentX, currentY) + if(symbol.scale){ + if(config.baseline === "middle"){ + ctx.translate(0, -ctx.lineWidth * (2 / symbol.scale[1])) + } + ctx.scale(symbol.scale[0], symbol.scale[1]) + ctx.lineWidth /= symbol.scale[0] + } + currentX = 0 + currentY = 0 + } + if(symbol.ura){ + ctx.font = (30 * mul) + "px Meiryo, sans-serif" + ctx.textBaseline = "middle" + ctx.beginPath() + ctx.arc(currentX, currentY + (17 * mul), (18 * mul), 0, Math.PI * 2) + if(action === "strokeText"){ + ctx.fillStyle = layer.outline + ctx.fill() + }else if(action === "fillText"){ + ctx.strokeStyle = layer.fill + ctx.lineWidth = 2.5 * mul + ctx.fillText(symbol.text, currentX, currentY + (17 * mul)) + } + ctx.stroke() + }else{ + ctx[action](symbol.text, currentX, currentY) + } + if(saved){ + ctx.restore() + } + offsetX += symbol.w * mul + } + if(savedLayer){ + ctx.restore() + } + } + ctx.restore() + } + + wrappingText(config){ + var ctx = config.ctx + var inputText = config.text.toString() + var words = [] + var start = 0 + var substituteIndex = 0 + while(start < inputText.length){ + var character = inputText.slice(start, start + 1) + if(words.length !== 0){ + var previous = words[words.length - 1] + if(!previous.substitute && previous !== "\n" && this.stickySymbols.indexOf(character) !== -1){ + words[words.length - 1] += character + start++ + continue + } + } + var index = Infinity + var currentIndex = inputText.slice(start).search(this.regex.cjk) + if(currentIndex !== -1){ + index = start + currentIndex + var on = inputText.charAt(index) + } + for(var i = 0; i < this.wrapOn.length; i++){ + var currentIndex = inputText.indexOf(this.wrapOn[i], start) + if(currentIndex !== -1 && currentIndex < index){ + var on = this.wrapOn[i] + index = currentIndex + } + } + if(index === Infinity){ + if(start !== inputText.length){ + words.push(inputText.slice(start, inputText.length)) + } + break + } + var end = index + (on === " " ? 1 : 0) + if(start !== end){ + words.push(inputText.slice(start, end)) + } + if(on === "%s" && config.substitute){ + words.push({ + substitute: true, + index: substituteIndex, + width: config.substitute(config, substituteIndex, true) || 0 + }) + substituteIndex++ + }else if(on !== " "){ + words.push(on) + } + start = index + on.length + } + + ctx.save() + + var bold = this.bold(config.fontFamily) + ctx.font = bold + config.fontSize + "px " + config.fontFamily + ctx.textBaseline = config.baseline || "top" + ctx.textAlign = "left" + ctx.fillStyle = config.fill + var lineHeight = config.lineHeight || config.fontSize + + var x = 0 + var y = 0 + var totalW = 0 + var totalH = 0 + var line = "" + var toDraw = [] + var lastWidth = 0 + + var addToDraw = obj => { + toDraw.push(obj) + if(x + lastWidth > totalW){ + totalW = x + lastWidth + } + if(y + lineHeight > totalH){ + totalH = y + lineHeight + } + } + var recenter = () => { + if(config.textAlign === "center"){ + for(var j in toDraw){ + if(toDraw[j].y === y){ + toDraw[j].x += (config.width - x - lastWidth) / 2 + } + } + } + } + var search = () => { + var end = line.length + var dist = end + while(dist){ + dist >>= 1 + line = words[i].slice(0, end) + lastWidth = ctx.measureText(line).width + end += lastWidth < config.width ? dist : -dist + } + if(line !== words[i]){ + words.splice(i + 1, 0, words[i].slice(line.length)) + words[i] = line + } + } + + for(var i = 0; i < words.length; i++){ + var skip = words[i].substitute || words[i] === "\n" + if(!skip){ + var currentWidth = ctx.measureText(line + words[i]).width + } + if(skip || (x !== 0 || line) && x + currentWidth > config.width){ + if(line){ + addToDraw({ + text: line, + x: x, y: y + }) + } + if(words[i].substitute){ + line = "" + var currentWidth = words[i].width + if(x + lastWidth + currentWidth > config.width){ + recenter() + x = 0 + y += lineHeight + lastWidth = 0 + } + addToDraw({ + substitute: true, + index: words[i].index, + x: x + lastWidth, y: y + }) + x += lastWidth + currentWidth + lastWidth = currentWidth + }else{ + recenter() + x = 0 + y += lineHeight + if(words[i] === "\n"){ + line = "" + lastWidth = 0 + }else{ + line = words[i] + lastWidth = ctx.measureText(line).width + if(line.length !== 1 && lastWidth > config.width){ + search() + } + } + } + }else if(!line){ + line = words[i] + lastWidth = ctx.measureText(line).width + if(line.length !== 1 && lastWidth > config.width){ + search() + } + }else{ + line += words[i] + lastWidth = currentWidth + } + } + if(line){ + addToDraw({ + text: line, + x: x, y: y + }) + recenter() + } + + var addX = 0 + var addY = 0 + if(config.verticalAlign === "middle"){ + addY = ((config.height || 0) - totalH) / 2 + } + for(var i in toDraw){ + var x = config.x + toDraw[i].x + addX + var y = config.y + toDraw[i].y + addY + if(toDraw[i].text){ + ctx.fillText(toDraw[i].text, x, y) + }else if(toDraw[i].substitute){ + ctx.save() + ctx.translate(x, y) + config.substitute(config, toDraw[i].index) + ctx.restore() + } + } + + ctx.restore() + } + + diffIcon(config){ + var ctx = config.ctx + var scale = config.scale + ctx.save() + ctx.lineWidth = config.border + ctx.strokeStyle = "#000" + var icon = this.diffIconPath[config.diff === 4 ? 3 : config.diff] + ctx.translate(config.x - icon[0].w * scale / 2, config.y - icon[0].h * scale / 2) + ctx.scale(scale, scale) + for(var i = 1; i < icon.length; i++){ + if(!icon[i].noStroke){ + ctx.stroke(icon[i].d) + } + } + if(!config.noFill){ + for(var i = 1; i < icon.length; i++){ + if(config.diff === 4 && icon[i].fill === "#db1885"){ + ctx.fillStyle = "#7135db" + }else{ + ctx.fillStyle = icon[i].fill + } + ctx.fill(icon[i].d) + } + } + ctx.restore() + } + + diffOptionsIcon(config){ + var ctx = config.ctx + ctx.save() + + if(config.iconName === "download" ||config.iconName === "back" ||config.iconName === "trash"){ + ctx.translate(config.x - 21, config.y - 21) + if(config.iconName === "download"){ + ctx.rotate(Math.PI) + ctx.translate(-42, -42) + } else if(config.iconName === "trash") { + ctx.rotate(Math.PI / 2 * 3) + ctx.translate(-42, 0) + } + + var drawLine = y => { + ctx.beginPath() + ctx.moveTo(12, y) + ctx.arc(20.5, 24, 8.5, Math.PI, Math.PI * 2, true) + ctx.lineTo(29, 18) + ctx.stroke() + } + var drawTriangle = noFill => { + ctx.beginPath() + ctx.moveTo(29, 5) + ctx.lineTo(21, 19) + ctx.lineTo(37, 19) + ctx.closePath() + if(!noFill){ + ctx.fill() + } + } + ctx.strokeStyle = "#000" + ctx.lineWidth = 13 + drawLine(8) + ctx.lineWidth = 6 + drawTriangle(true) + ctx.stroke() + ctx.lineWidth = 7 + ctx.fillStyle = "#fff" + ctx.strokeStyle = "#fff" + drawLine(11) + drawTriangle() + ctx.translate(-1.5, -0.5) + ctx.fillStyle = config.iconName === "download" ? "#a08eea" : config.iconName == "trash" ? "#ff0000" : "#23a6e1" + ctx.strokeStyle = config.iconName === "download" ? "#a08eea" : config.iconName == "trash" ? "#ff0000" : "#23a6e1" + ctx.globalCompositeOperation = "darken" + drawLine(11) + drawTriangle() + }else if(config.iconName === "options"){ + ctx.translate(config.x, config.y) + ctx.rotate(-55 * Math.PI / 180) + ctx.translate(-6, -20) + ctx.strokeStyle = "#000" + ctx.lineWidth = 6 + ctx.stroke(this.optionsPath.main) + ctx.translate(-2, 2) + ctx.stroke(this.optionsPath.main) + ctx.fillStyle = "#7e7c76" + ctx.fill(this.optionsPath.shadow) + ctx.translate(2, -2) + ctx.fillStyle = "gold" + ctx.fill(this.optionsPath.main) + } + + ctx.restore() + } + + diffCursor(config){ + var ctx = config.ctx + ctx.save() + if(config.scale){ + ctx.translate(config.x, config.y) + ctx.scale(config.scale, config.scale) + ctx.translate(-48, -64) + }else{ + ctx.translate(config.x - 48, config.y - 64) + } + + ctx.fillStyle = config.two ? "#65cdcd" : "#ff411c" + ctx.strokeStyle = "#000" + ctx.lineWidth = 6 + ctx.beginPath() + if(!config.side){ + var textX = config.two ? 22 : 20 + ctx.moveTo(48, 120) + ctx.arc(48, 48.5, 45, Math.PI * 0.58, Math.PI * 0.42) + }else if(config.two){ + var textX = 72 + ctx.moveTo(56, 115) + ctx.arc(98, 48.5, 45, Math.PI * 0.75, Math.PI * 0.59) + }else{ + var textX = -30 + ctx.moveTo(39, 115) + ctx.arc(-2, 48.5, 45, Math.PI * 0.41, Math.PI * 0.25) + } + ctx.closePath() + ctx.fill() + ctx.stroke() + this.layeredText({ + ctx: ctx, + text: config.two ? "2P" : "1P", + fontSize: 43, + fontFamily: config.font, + x: textX, + y: 26, + width: 54, + letterSpacing: -4 + }, [ + {outline: "#fff", letterBorder: 11}, + {fill: "#000"} + ]) + + ctx.restore() + } + + diffStar(config){ + var ctx = config.ctx + ctx.save() + if(config.songSel || config.ura){ + if(this.diffStarCache.scale !== config.ratio){ + this.diffStarCache.resize(62, 31, config.ratio) + } + var offset = 30 / 2 - 18 / 2 + var big = config.ura && !config.songSel + this.diffStarCache.get({ + ctx: ctx, + x: config.x - 9 - offset, + y: config.y - 9 - offset, + w: 30, + h: 30, + id: big ? "big" : "small" + }, ctx => { + ctx.fillStyle = "#fff" + this.shadow({ + ctx: ctx, + fill: "#fff", + blur: 10, + force: true + }) + if(big){ + ctx.translate(30 / 2 - 21 / 2, 30 / 2 - 19 / 2) + ctx.scale(1.1, 1.1) + }else{ + ctx.translate(offset, offset) + } + ctx.fill(this.diffStarPath) + }) + }else{ + ctx.fillStyle = "#f72568" + ctx.translate(config.x - 10.5, config.y - 9.5) + ctx.scale(1.1, 1.1) + ctx.fill(this.diffStarPath) + } + ctx.restore() + } + + pattern(config){ + var ctx = config.ctx + ctx.save() + var mul = config.scale || 1 + + if(mul !== 1){ + ctx.scale(1 / mul, 1 / mul) + } + ctx.fillStyle = ctx.createPattern(config.img, "repeat") + if(config.shape){ + config.shape(ctx, mul) + }else{ + ctx.beginPath() + ctx.rect(config.x * mul, config.y * mul, config.w * mul, config.h * mul) + } + ctx.translate(config.dx * mul, config.dy * mul) + ctx.fill() + + ctx.restore() + } + + score(config){ + var ctx = config.ctx + ctx.save() + + ctx.translate(config.x, config.y) + if(config.scale){ + ctx.scale(config.scale, config.scale) + } + ctx.strokeStyle = "#000" + ctx.lineWidth = 7 + if(strings.good === "良"){ + if(config.align === "center"){ + ctx.translate(config.score === "bad" ? -49 / 2 : -23 / 2, 0) + } + if(config.score === "good"){ + var grd = ctx.createLinearGradient(0, 0, 0, 29) + grd.addColorStop(0.3, "#f7fb00") + grd.addColorStop(0.9, "#ff4900") + ctx.fillStyle = grd + ctx.stroke(this.diffPath.good) + ctx.fill(this.diffPath.good) + }else if(config.score === "ok"){ + ctx.fillStyle = "#fff" + ctx.stroke(this.diffPath.ok) + ctx.fill(this.diffPath.ok) + }else if(config.score === "bad"){ + var grd = ctx.createLinearGradient(0, 0, 0, 27) + grd.addColorStop(0.1, "#6B5DFF") + grd.addColorStop(0.7, "#00AEDE") + ctx.fillStyle = grd + ctx.stroke(this.diffPath.bad) + ctx.fill(this.diffPath.bad) + ctx.translate(26, 0) + ctx.stroke(this.diffPath.ok) + ctx.fill(this.diffPath.ok) + } + }else{ + ctx.font = this.bold(strings.font) + "26px " + strings.font + if(config.results){ + ctx.textAlign = "left" + }else{ + ctx.textAlign = "center" + } + ctx.textBaseline = "top" + ctx.miterLimit = 1 + if(config.score === "good"){ + if(config.results && strings.id === "en"){ + ctx.scale(0.75, 1) + } + var grd = ctx.createLinearGradient(0, 0, 0, 29) + grd.addColorStop(0.3, "#f7fb00") + grd.addColorStop(0.9, "#ff4900") + ctx.fillStyle = grd + ctx.strokeText(strings.good, 0, 4) + ctx.fillText(strings.good, 0, 4) + }else if(config.score === "ok"){ + ctx.fillStyle = "#fff" + ctx.strokeText(strings.ok, 0, 4) + ctx.fillText(strings.ok, 0, 4) + }else if(config.score === "bad"){ + var grd = ctx.createLinearGradient(0, 0, 0, 27) + grd.addColorStop(0.1, "#6B5DFF") + grd.addColorStop(0.7, "#00AEDE") + ctx.fillStyle = grd + ctx.strokeText(strings.bad, 0, 4) + ctx.fillText(strings.bad, 0, 4) + } + } + ctx.restore() + } + + crown(config){ + var ctx = config.ctx + ctx.save() + + ctx.translate(config.x, config.y) + if(config.scale){ + ctx.scale(config.scale, config.scale) + } + ctx.translate(-47, -39) + ctx.miterLimit = 1.7 + + if(config.whiteOutline){ + if(!this.crownCache.w){ + this.crownCache.resize(140, 140, config.ratio) + } + var offset = 140 / 2 - 94 / 2 + this.crownCache.get({ + ctx: ctx, + x: -offset, + y: -offset, + w: 140, + h: 140, + id: "crown" + }, ctx => { + ctx.save() + ctx.translate(offset, offset) + ctx.strokeStyle = "#fff" + ctx.lineWidth = 35 + ctx.miterLimit = 1.7 + ctx.filter = "blur(1.5px)" + ctx.stroke(this.crownPath) + ctx.restore() + }) + } + + if(config.shine){ + ctx.strokeStyle = "#fff" + ctx.lineWidth = 18 + ctx.stroke(this.crownPath) + ctx.globalAlpha = 1 - config.shine + } + + ctx.strokeStyle = config.type ? "#000" : "#ffc616" + ctx.lineWidth = 18 + ctx.stroke(this.crownPath) + + if(config.shine){ + ctx.globalAlpha = 1 + ctx.fillStyle = "#fff" + ctx.fill(this.crownPath) + ctx.globalAlpha = 1 - config.shine + } + + if(config.type){ + var grd = ctx.createLinearGradient(0, 0, 94, 0) + if(config.type === "gold"){ + grd.addColorStop(0, "#ffffc5") + grd.addColorStop(0.23, "#ffff44") + grd.addColorStop(0.53, "#efbd12") + grd.addColorStop(0.83, "#ffff44") + grd.addColorStop(1, "#efbd12") + }else if(config.type === "silver"){ + grd.addColorStop(0, "#d6efef") + grd.addColorStop(0.23, "#bddfde") + grd.addColorStop(0.53, "#97c1c0") + grd.addColorStop(0.83, "#bddfde") + grd.addColorStop(1, "#97c1c0") + } + ctx.fillStyle = grd + }else{ + ctx.fillStyle = "#ffdb2c" + } + ctx.fill(this.crownPath) + + ctx.restore() + } + + gauge(config){ + var ctx = config.ctx + ctx.save() + + ctx.translate(config.x, config.y) + if(config.scale){ + ctx.scale(config.scale, config.scale) + } + ctx.translate(-788, 0) + + var firstTop = config.multiplayer ? 0 : 30 + var secondTop = config.multiplayer ? 0 : 8 + + config.percentage = Math.max(0, Math.min(1, config.percentage)) + var cleared = config.percentage >= config.clear + + var gaugeW = 14 * 50 + var gaugeClear = gaugeW * (config.clear - 1 / 50) + var gaugeFilled = gaugeW * config.percentage + + ctx.fillStyle = "#000" + ctx.beginPath() + if(config.scoresheet){ + if(config.multiplayer){ + ctx.moveTo(-4, -4) + ctx.lineTo(760, -4) + this.roundedCorner(ctx, 760, 48, 13, 2) + this.roundedCorner(ctx, gaugeClear - 4, 48, 13, 3) + ctx.lineTo(gaugeClear - 4, 26) + ctx.lineTo(-4, 26) + }else{ + ctx.moveTo(-4, 26) + ctx.lineTo(gaugeClear - 4, 26) + this.roundedCorner(ctx, gaugeClear - 4, 4, 13, 0) + this.roundedCorner(ctx, 760, 4, 13, 1) + ctx.lineTo(760, 56) + ctx.lineTo(-4, 56) + } + }else if(config.multiplayer){ + ctx.moveTo(gaugeClear - 7, 27) + ctx.lineTo(788, 27) + ctx.lineTo(788, 52) + this.roundedCorner(ctx, gaugeClear - 7, 52, 18, 3) + }else{ + ctx.moveTo(gaugeClear - 7, 24) + this.roundedCorner(ctx, gaugeClear - 7, 0, 18, 0) + ctx.lineTo(788, 0) + ctx.lineTo(788, 24) + } + ctx.fill() + + if(!cleared){ + ctx.fillStyle = config.blue ? "#184d55" : "#680000" + var x = Math.max(0, gaugeFilled - 5) + ctx.fillRect(x, firstTop, gaugeClear - x + 2 + (gaugeClear < gaugeW ? 0 : -7), 22) + } + if(gaugeFilled > 0){ + var w = Math.min(gaugeW - 5, gaugeClear + 1, gaugeFilled - 4) + ctx.fillStyle = config.blue ? "#00edff" : "#ff3408" + ctx.fillRect(0, firstTop + 2, w, 20) + ctx.fillStyle = config.blue ? "#9cffff" : "#ffa191" + ctx.fillRect(0, firstTop, w, 3) + } + if(gaugeClear < gaugeW){ + if(gaugeFilled < gaugeW - 4){ + ctx.fillStyle = "#684900" + var x = Math.max(gaugeClear + 9, gaugeFilled - gaugeClear + 9) + ctx.fillRect(x, secondTop, gaugeW - 4 - x, 44) + } + if(gaugeFilled > gaugeClear + 14){ + var w = Math.min(gaugeW - 4, gaugeFilled - gaugeClear - 14) + ctx.fillStyle = "#ff0" + ctx.fillRect(gaugeClear + 9, secondTop + 2, w, 42) + ctx.fillStyle = "#fff" + ctx.fillRect(gaugeClear + 9, secondTop, w, 3) + } + ctx.fillStyle = cleared ? "#ff0" : "#684900" + ctx.beginPath() + if(config.multiplayer){ + this.roundedCorner(ctx, gaugeClear, secondTop + 44, 10, 3) + ctx.lineTo(gaugeClear, secondTop) + ctx.lineTo(gaugeClear + 10, secondTop) + }else{ + ctx.moveTo(gaugeClear, secondTop + 44) + this.roundedCorner(ctx, gaugeClear, secondTop, 10, 0) + ctx.lineTo(gaugeClear + 10, secondTop + 44) + } + ctx.fill() + } + if(cleared){ + ctx.save() + ctx.clip() + ctx.fillStyle = "#fff" + ctx.fillRect(gaugeClear, secondTop, 10, 3) + ctx.restore() + } + + ctx.strokeStyle = "rgba(0, 0, 0, 0.16)" + ctx.beginPath() + ctx.lineWidth = 5 + for(var i = 0; i < 49; i++){ + var x = 14 + i * 14 - ctx.lineWidth / 2 + if(i === config.clear * 50 - 1){ + ctx.stroke() + ctx.beginPath() + ctx.lineWidth = 4 + } + ctx.moveTo(x, x < gaugeClear ? firstTop : secondTop) + ctx.lineTo(x, x < gaugeClear ? firstTop + 22 : secondTop + 44) + } + ctx.stroke() + if(config.clear < 47 / 50){ + this.layeredText({ + ctx: ctx, + text: strings.clear, + fontSize: 18, + fontFamily: config.font, + x: gaugeClear + 3, + y: config.multiplayer ? 22 : 11, + letterSpacing: -2 + }, [ + {scale: [1.1, 1.01], outline: "#000", letterBorder: 6}, + {scale: [1.11, 1], fill: cleared ? "#fff" : "#737373"} + ]) + } + + ctx.restore() + } + + soul(config){ + var ctx = config.ctx + ctx.save() + + ctx.translate(config.x, config.y) + if(config.scale){ + ctx.scale(config.scale, config.scale) + } + ctx.translate(-23, -21) + + ctx.fillStyle = config.cleared ? "#fff" : "#737373" + ctx.fill(this.soulPath) + + ctx.restore() + } + + slot(ctx, x, y, size){ + var mul = size / 106 + + ctx.save() + ctx.globalAlpha = 0.7 + ctx.globalCompositeOperation = "screen" + ctx.fillStyle = "#444544" + ctx.beginPath() + ctx.arc(x, y, 26 * mul, 0, Math.PI * 2) + ctx.fill() + ctx.lineWidth = 3 + ctx.strokeStyle = "#9c9e9c" + ctx.beginPath() + ctx.arc(x, y, 33.5 * mul, 0, Math.PI * 2) + ctx.stroke() + ctx.lineWidth = 3.5 + ctx.strokeStyle = "#5d5e5d" + ctx.beginPath() + ctx.arc(x, y, 51.5 * mul, 0, Math.PI * 2) + ctx.stroke() + ctx.restore() + } + + category(config){ + var ctx = config.ctx + ctx.save() + + ctx.translate(config.x, config.y) + if(config.scale || config.right){ + ctx.scale((config.right ? -1 : 1) * (config.scale || 1), config.scale || 1) + } + ctx.translate(-15.5 + 14, -11) + for(var i = 0; i < 4; i++){ + if(i < 2){ + ctx.lineWidth = 6 + ctx.strokeStyle = "#000" + ctx.stroke(this.categoryPath.main) + }else{ + ctx.fillStyle = config.fill + ctx.fill(this.categoryPath.main) + ctx.fillStyle = "rgba(255, 255, 255, 0.25)" + ctx.fill(this.categoryPath.main) + ctx.fillStyle = "rgba(0, 0, 0, 0.25)" + ctx.fill(this.categoryPath.shadow) + } + if(i % 2 === 0){ + ctx.translate(-14, 0) + }else if(i === 1){ + ctx.translate(14, 0) + } + } + if(config.highlight){ + this.highlight({ + ctx: ctx, + x: 0, + y: 0, + opacity: 0.8, + shape: this.categoryPath.highlight, + size: 8 + }) + } + + ctx.restore() + } + + nameplate(config){ + var ctx = config.ctx + var w = 264 + var h = 57 + var r = h / 2 + var pi = Math.PI + + ctx.save() + + ctx.translate(config.x, config.y) + if(config.scale){ + ctx.scale(config.scale, config.scale) + } + + ctx.fillStyle="rgba(0, 0, 0, 0.25)" + ctx.beginPath() + ctx.arc(r + 4, r + 5, r, pi / 2, pi / -2) + ctx.arc(w - r + 4, r + 5, r, pi / -2, pi / 2) + ctx.fill() + ctx.beginPath() + ctx.moveTo(r, 0) + this.roundedCorner(ctx, w, 0, r, 1) + ctx.lineTo(r, r) + ctx.fillStyle = config.blue ? "#67cecb" : "#ff421d" + ctx.fill() + ctx.beginPath() + ctx.moveTo(r, r) + this.roundedCorner(ctx, w, h, r, 2) + ctx.lineTo(r, h) + ctx.fillStyle = "rgba(255, 255, 255, 0.8)" + ctx.fill() + ctx.strokeStyle = "#000" + ctx.lineWidth = 4 + ctx.beginPath() + ctx.moveTo(r, 0) + ctx.arc(w - r, r, r, pi / -2, pi / 2) + ctx.lineTo(r, h) + ctx.stroke() + ctx.beginPath() + ctx.moveTo(r, r - 1) + ctx.lineTo(w, r - 1) + ctx.lineWidth = 2 + ctx.stroke() + ctx.beginPath() + ctx.arc(r, r, r, 0, pi * 2) + ctx.fillStyle = config.blue ? "#67cecb" : "#ff421d" + ctx.fill() + ctx.lineWidth = 4 + ctx.stroke() + ctx.font = this.bold(config.font) + "28px " + config.font + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.lineWidth = 5 + ctx.miterLimit = 1 + ctx.strokeStyle = "#fff" + ctx.fillStyle = "#000" + var text = config.blue ? "2P" : "1P" + ctx.strokeText(text, r + 2, r + 1) + ctx.fillText(text, r + 2, r + 1) + if(config.rank){ + this.layeredText({ + ctx: ctx, + text: config.rank, + fontSize: 20, + fontFamily: config.font, + x: w / 2 + r * 0.7, + y: r * 0.5, + width: 180, + align: "center", + baseline: "middle" + }, [ + {fill: "#000"} + ]) + } + this.layeredText({ + ctx: ctx, + text: config.name || "", + fontSize: 21, + fontFamily: config.font, + x: w / 2 + r * 0.7, + y: r * 1.5 - 0.5, + width: 180, + kanaSpacing: 10, + align: "center", + baseline: "middle" + }, [ + {outline: "#000", letterBorder: 6}, + {fill: "#fff"} + ]) + + ctx.restore() + } + + alpha(amount, ctx, callback, winW, winH){ + if(amount >= 1){ + return callback(ctx) + }else if(amount >= 0){ + this.tmpCanvas.width = Math.max(1, winW || ctx.canvas.width) + this.tmpCanvas.height = Math.max(1, winH || ctx.canvas.height) + callback(this.tmpCtx) + ctx.save() + ctx.globalAlpha = amount + ctx.drawImage(this.tmpCanvas, 0, 0) + ctx.restore() + } + } + + shadow(config){ + if(!disableBlur || config.force){ + var ctx = config.ctx + if(config.fill){ + ctx.shadowColor = config.fill + } + if(config.blur){ + ctx.shadowBlur = config.blur + } + if(config.x){ + ctx.shadowOffsetX = config.x + } + if(config.y){ + ctx.shadowOffsetY = config.y + } + } + } + + bold(font){ + return font === "Microsoft YaHei, sans-serif" ? "bold " : "" + } + + getMS(){ + return Date.now() + } + + clean(){ + this.songFrameCache.clean() + this.diffStarCache.clean() + this.crownCache.clean() + delete this.tmpCtx + delete this.tmpCanvas + } +} diff --git a/public/src/js/canvastest.js b/public/src/js/canvastest.js new file mode 100644 index 0000000..882c1b5 --- /dev/null +++ b/public/src/js/canvastest.js @@ -0,0 +1,151 @@ +class CanvasTest{ + constructor(...args){ + this.init(...args) + } + init(){ + this.canvas = document.createElement("canvas") + var pixelRatio = window.devicePixelRatio || 1 + var width = innerWidth * pixelRatio + var height = innerHeight * pixelRatio + this.canvas.width = Math.max(1, width) + this.canvas.height = Math.max(1, height) + this.ctx = this.canvas.getContext("2d") + this.ctx.scale(pixelRatio, pixelRatio) + this.ratio = pixelRatio + this.draw = new CanvasDraw() + this.font = "serif" + + this.songAsset = { + marginLeft: 18, + marginTop: 90, + width: 82, + height: 452, + border: 6, + innerBorder: 8 + } + } + blurPerformance(){ + return new Promise(resolve => { + requestAnimationFrame(() => { + var ctx = this.ctx + ctx.save() + var lastIteration = this.blurIteration() + var frameTime = [] + + for(var i = 0; i < 10; i++){ + lastIteration = lastIteration.then(ms => { + frameTime.push(ms) + return this.blurIteration() + }) + } + + lastIteration.then(() => { + ctx.restore() + resolve(frameTime.reduce((a, b) => a + b) / frameTime.length) + }) + }) + }) + } + blurIteration(){ + return new Promise(resolve => { + requestAnimationFrame(() => { + var startTime = Date.now() + var ctx = this.ctx + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) + + for(var x = 0; x < this.canvas.width; x += this.songAsset.width + this.songAsset.marginLeft){ + this.draw.songFrame({ + ctx: ctx, + x: x, + y: this.songAsset.marginTop, + width: this.songAsset.width, + height: this.songAsset.height, + border: this.songAsset.border, + innerBorder: this.songAsset.innerBorder, + background: "#efb058", + borderStyle: ["#ffe7bd", "#c68229"], + ratio: this.ratio, + innerContent: () => {} + }) + } + + for(var i = 0; i < 2; i++){ + this.draw.layeredText({ + ctx: ctx, + text: "I am a text", + fontSize: 48, + fontFamily: this.font, + x: 23 + 300 * i, + y: 15 + }, [ + {x: -2, y: -2, outline: "#000", letterBorder: 22}, + {}, + {x: 2, y: 2, shadow: [2, 2, 7]}, + {x: 2, y: 2, outline: "#ad1516", letterBorder: 10}, + {x: -2, y: -2, outline: "#ff797b"}, + {outline: "#f70808"}, + {fill: "#fff", shadow: [-1, 1, 3, 1.5]} + ]) + } + resolve(Date.now() - startTime) + }) + }) + } + drawAllImages(){ + return new Promise(resolve => { + requestAnimationFrame(() => { + var startTime = Date.now() + var ctx = this.ctx + ctx.save() + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) + + for(var name in assets.image){ + ctx.drawImage(assets.image[name], 0, 0) + } + + var comboCount = 765 + var comboX = 100 + var comboY = 100 + var fontSize = 120 + this.ctx.font = "normal " + fontSize + "px TnT, Meiryo, sans-serif" + this.ctx.textAlign = "center" + this.ctx.strokeStyle = "#000" + this.ctx.lineWidth = fontSize / 10 + var glyph = this.ctx.measureText("0").width + var comboText = comboCount.toString().split("") + for(var i in comboText){ + var textX = comboX + glyph * (i - (comboText.length - 1) / 2) + if(comboCount >= 100){ + var grd = this.ctx.createLinearGradient( + textX - glyph * 0.2, + comboY - fontSize * 0.8, + textX + glyph * 0.2, + comboY - fontSize * 0.2 + ) + grd.addColorStop(0, "#f00") + grd.addColorStop(1, "#fe0") + this.ctx.fillStyle = grd + }else{ + this.ctx.fillStyle = "#fff" + } + this.strokeFillText(comboText[i], + textX, + comboY + ) + } + + ctx.restore() + resolve(Date.now() - startTime) + }) + }) + } + strokeFillText(text, x, y){ + this.ctx.strokeText(text, x, y) + this.ctx.fillText(text, x, y) + } + clean(){ + this.draw.clean() + delete this.ctx + delete this.canvas + } +} diff --git a/public/src/js/circle.js b/public/src/js/circle.js new file mode 100644 index 0000000..3492f10 --- /dev/null +++ b/public/src/js/circle.js @@ -0,0 +1,45 @@ +class Circle{ + constructor(...args){ + this.init(...args) + } + init(config){ + this.id = config.id + this.ms = config.start + this.originalMS = this.ms + this.type = config.type + this.text = config.txt + this.speed = config.speed + this.endTime = config.endTime || this.ms + this.originalEndTime = this.endTime + this.isPlayed = 0 + this.animating = false + this.animT = 0 + this.score = 0 + this.lastFrame = this.ms + 100 + this.animationEnded = false + this.timesHit = 0 + this.timesKa = 0 + this.requiredHits = config.requiredHits || 0 + this.rendaPlayed = false + this.gogoTime = config.gogoTime || false + this.gogoChecked = false + this.beatMS = config.beatMS + this.fixedPos = config.fixedPos + this.branch = config.branch + this.section = config.section + } + animate(ms){ + this.animating = true + this.animT = ms + } + played(score, big){ + this.score = score + this.isPlayed = score <= 0 ? score - 1 : (big ? 2 : 1) + } + hit(keysKa){ + this.timesHit++ + if(keysKa){ + this.timesKa++ + } + } +} \ No newline at end of file diff --git a/public/src/js/controller.js b/public/src/js/controller.js new file mode 100644 index 0000000..c0b13ee --- /dev/null +++ b/public/src/js/controller.js @@ -0,0 +1,414 @@ +class Controller{ + constructor(...args){ + this.init(...args) + } + init(selectedSong, songData, autoPlayEnabled, multiplayer, touchEnabled){ + this.selectedSong = selectedSong + this.songData = songData + this.autoPlayEnabled = autoPlayEnabled + this.saveScore = !autoPlayEnabled + this.multiplayer = multiplayer + this.touchEnabled = touchEnabled + if(multiplayer === 2){ + this.snd = p2.player === 2 ? "_p1" : "_p2" + this.don = p2.don || defaultDon + }else{ + this.snd = multiplayer ? "_p" + p2.player : "" + this.don = account.loggedIn ? account.don : defaultDon + } + if(this.snd === "_p2" && this.objEqual(defaultDon, this.don)){ + this.don = { + body_fill: defaultDon.face_fill, + face_fill: defaultDon.body_fill + } + } + + this.calibrationMode = selectedSong.folder === "calibration" + this.audioLatency = 0 + this.videoLatency = 0 + if(!this.calibrationMode){ + var latency = settings.getItem("latency") + if(!autoPlayEnabled || this.multiplayer){ + this.audioLatency = Math.round(latency.audio) || 0 + } + this.videoLatency = Math.round(latency.video) || 0 + this.audioLatency + } + if(this.multiplayer !== 2){ + loader.changePage("game", false) + } + + if(selectedSong.type === "tja"){ + this.parsedSongData = new ParseTja(songData, selectedSong.difficulty, selectedSong.stars, selectedSong.offset) + }else{ + this.parsedSongData = new ParseOsu(songData, selectedSong.difficulty, selectedSong.stars, selectedSong.offset) + } + this.offset = this.parsedSongData.soundOffset + + var maxCombo = this.parsedSongData.circles.filter(circle => ["don", "ka", "daiDon", "daiKa"].indexOf(circle.type) > -1 && (!circle.branch || circle.branch.name == "master")).length + if (maxCombo >= 50) { + var comboVoices = ["v_combo_50"].concat(Array.from(Array(Math.min(50, Math.floor(maxCombo / 100))), (d, i) => "v_combo_" + ((i + 1) * 100))) + var promises = [] + + comboVoices.forEach(name => { + if (!assets.sounds[name + "_p1"]) { + promises.push(loader.loadSound(name + ".ogg", snd.sfxGain).then(sound => { + assets.sounds[name + "_p1"] = assets.sounds[name].copy(snd.sfxGainL) + assets.sounds[name + "_p2"] = assets.sounds[name].copy(snd.sfxGainR) + })) + } + }) + + Promise.all(promises) + } + + if(this.calibrationMode){ + this.volume = 1 + }else{ + assets.songs.forEach(song => { + if(song.id == this.selectedSong.folder){ + this.mainAsset = song.sound + this.volume = song.volume || 1 + if(!multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){ + if(song.lyricsData){ + var lyricsDiv = document.getElementById("song-lyrics") + this.lyrics = new Lyrics(song.lyricsData, selectedSong.offset, lyricsDiv) + }else if(this.parsedSongData.lyrics){ + var lyricsDiv = document.getElementById("song-lyrics") + this.lyrics = new Lyrics(this.parsedSongData.lyrics, selectedSong.offset, lyricsDiv, true) + } + } + } + }) + } + + this.game = new Game(this, this.selectedSong, this.parsedSongData) + this.view = new View(this) + if (parseFloat(localStorage.getItem("baisoku") ?? "1", 10) !== 1) { + this.saveScore = false; + } + this.mekadon = new Mekadon(this, this.game) + this.keyboard = new GameInput(this) + if(!autoPlayEnabled && this.multiplayer !== 2){ + this.easierBigNotes = settings.getItem("easierBigNotes") || this.keyboard.keyboard.TaikoForceLv5 + }else{ + this.easierBigNotes = false + } + + this.drumSounds = settings.getItem("latency").drumSounds + this.playedSounds = {} + } + run(syncWith){ + if(syncWith){ + this.syncWith = syncWith + } + if(this.multiplayer !== 2){ + snd.musicGain.setVolumeMul(this.volume) + } + this.game.run() + this.view.run() + if(this.multiplayer === 1){ + syncWith.run(this) + syncWith.game.elapsedTime = this.game.elapsedTime + syncWith.game.startDate = this.game.startDate + } + requestAnimationFrame(() => { + this.startMainLoop() + if(!this.multiplayer){ + debugObj.controller = this + if(debugObj.debug){ + debugObj.debug.updateStatus() + } + } + }) + } + startMainLoop(){ + this.mainLoopRunning = true + window.gamestatus = 'start' + this.gameLoop() + this.viewLoop() + if(this.multiplayer !== 2){ + this.gameInterval = setInterval(this.gameLoop.bind(this), 1000 / 60) + pageEvents.send("game-start", { + selectedSong: this.selectedSong, + autoPlayEnabled: this.autoPlayEnabled, + multiplayer: this.multiplayer, + touchEnabled: this.touchEnabled + }) + } + } + stopMainLoop(){ + this.mainLoopRunning = false + window.gamestatus = 'stop' + + if (window.videoElement) { + // 停止视频播放 + window.videoElement.pause(); + + // 移除视频元素 + document.body.removeChild(window.videoElement); + + // 解除引用,允许垃圾回收 + window.videoElement = null; + } + + + if(this.game.mainAsset){ + this.game.mainAsset.stop() + } + if(this.multiplayer !== 2){ + clearInterval(this.gameInterval) + } + } + gameLoop(){ + if(this.mainLoopRunning){ + if(this.multiplayer === 1){ + this.syncWith.game.elapsedTime = this.game.elapsedTime + this.syncWith.game.startDate = this.game.startDate + } + var ms = this.game.elapsedTime + + if(this.game.musicFadeOut < 3){ + this.keyboard.checkMenuKeys() + } + if(this.calibrationMode){ + this.game.calibration() + } + if(!this.game.isPaused()){ + this.keyboard.checkGameKeys() + + if(ms < 0){ + this.game.updateTime() + }else{ + if(!this.calibrationMode){ + this.game.update() + } + if(!this.mainLoopRunning){ + return + } + this.game.playMainMusic() + } + } + if(this.multiplayer === 1){ + this.syncWith.gameLoop() + } + } + } + viewLoop(){ + if(this.mainLoopRunning){ + if(this.multiplayer !== 2){ + requestAnimationFrame(() => { + var player = this.multiplayer ? p2.player : 1 + if(player === 1){ + this.viewLoop() + } + if(this.multiplayer === 1){ + this.syncWith.viewLoop() + } + if(player === 2){ + this.viewLoop() + } + if(this.scoresheet){ + if(this.view.ctx){ + this.view.ctx.save() + this.view.ctx.setTransform(1, 0, 0, 1, 0, 0) + } + this.scoresheet.redraw() + if(this.view.ctx){ + this.view.ctx.restore() + } + } + }) + } + this.view.refresh() + } + } + gameEnded(){ + var score = this.getGlobalScore() + var vp + if(this.game.rules.clearReached(score.gauge)){ + if(score.bad === 0){ + vp = "fullcombo" + this.playSound("v_fullcombo", 1.350) + }else{ + vp = "clear" + } + }else{ + vp = "fail" + } + this.playSound("se_game" + vp) + } + displayResults(){ + if(this.multiplayer !== 2){ + if(this.view.cursorHidden){ + this.view.canvas.style.cursor = "" + } + this.scoresheet = new Scoresheet(this, this.getGlobalScore(), this.multiplayer, this.touchEnabled) + } + } + displayScore(score, notPlayed, bigNote){ + this.view.displayScore(score, notPlayed, bigNote) + } + songSelection(fadeIn, showWarning){ + if(!fadeIn){ + if(this.cleaned){ + return + } + this.clean() + } + if(this.calibrationMode){ + new SettingsView(this.touchEnabled, false, null, "latency") + }else{ + new SongSelect(false, fadeIn, this.touchEnabled, null, showWarning) + } + } + restartSong(){ + if(this.cleaned){ + return + } + this.clean() + if(this.multiplayer){ + new LoadSong(this.selectedSong, false, true, this.touchEnabled) + }else{ + new Promise(resolve => { + if(this.calibrationMode){ + resolve() + }else{ + var songObj = assets.songs.find(song => song.id === this.selectedSong.folder) + var promises = [] + if(songObj.chart && songObj.chart !== "blank"){ + var chart = songObj.chart + if(chart.separateDiff){ + var chartDiff = this.selectedSong.difficulty + chart = chart[chartDiff] + } + this.addPromise(promises, chart.read(this.selectedSong.type === "tja" ? "utf-8" : undefined).then(data => { + this.songData = data.replace(/\0/g, "").split("\n") + return Promise.resolve() + }), chart.url) + } + if(songObj.lyricsFile){ + this.addPromise(promises, songObj.lyricsFile.read().then(result => { + songObj.lyricsData = result + }, () => Promise.resolve()), songObj.lyricsFile.url) + } + if(songObj && songObj.category_id === 9){ + LoadSong.insertBackgroundVideo(songObj.id) + } + Promise.all(promises).then(resolve) + } + }).then(() => { + var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled) + taikoGame.run() + }) + } + } + addPromise(promises, promise, url){ + promises.push(promise.catch(error => { + if(this.restartSongError){ + return + } + this.restartSongError = true + if(url){ + error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url + } + pageEvents.send("load-song-error", error) + errorMessage(new Error(error).stack) + var title = this.selectedSong.title + if(title !== this.selectedSong.originalTitle){ + title += " (" + this.selectedSong.originalTitle + ")" + } + setTimeout(() => { + new SongSelect(false, false, this.touchEnabled, null, { + name: "loadSongError", + title: title, + id: this.selectedSong.folder, + error: error + }) + }, 500) + return Promise.reject(error) + })) + } + playSound(id, time, noSnd){ + if(!this.drumSounds && (id === "neiro_1_don" || id === "neiro_1_ka" || id === "se_don" || id === "se_ka")){ + return + } + var ms = Date.now() + (time || 0) * 1000 + if(!(id in this.playedSounds) || ms > this.playedSounds[id] + 30){ + assets.sounds[id + (noSnd ? "" : this.snd)].play(time) + this.playedSounds[id] = ms + } + } + togglePause(forcePause, pauseMove, noSound){ + if(this.multiplayer === 1){ + this.syncWith.game.togglePause(forcePause, pauseMove, noSound) + } + this.game.togglePause(forcePause, pauseMove, noSound) + + + } + getKeys(){ + return this.keyboard.getKeys() + } + setKey(pressed, name, ms){ + return this.keyboard.setKey(pressed, name, ms) + } + getElapsedTime(){ + return this.game.elapsedTime + } + getCircles(){ + return this.game.getCircles() + } + getCurrentCircle(){ + return this.game.getCurrentCircle() + } + isWaiting(key, type){ + return this.keyboard.isWaiting(key, type) + } + waitForKeyup(key, type){ + this.keyboard.waitForKeyup(key, type) + } + getKeyTime(){ + return this.keyboard.getKeyTime() + } + getCombo(){ + return this.game.getCombo() + } + getGlobalScore(){ + return this.game.getGlobalScore() + } + autoPlay(circle){ + if(this.multiplayer){ + p2.play(circle, this.mekadon) + }else{ + return this.mekadon.play(circle) + } + } + objEqual(a, b){ + for(var i in a){ + if(a[i] !== b[i]){ + return false + } + } + return true + } + clean(){ + this.cleaned = true + if(this.multiplayer === 1){ + this.syncWith.clean() + } + this.stopMainLoop() + this.keyboard.clean() + this.view.clean() + snd.buffer.loadSettings() + + if(!this.multiplayer){ + debugObj.controller = null + if(debugObj.debug){ + debugObj.debug.updateStatus() + } + } + if(this.lyrics){ + this.lyrics.clean() + } + } +} diff --git a/public/src/js/customsongs.js b/public/src/js/customsongs.js new file mode 100644 index 0000000..7ded3ce --- /dev/null +++ b/public/src/js/customsongs.js @@ -0,0 +1,551 @@ +class CustomSongs{ + constructor(...args){ + this.init(...args) + } + init(touchEnabled, noPage, noLoading){ + this.loaderDiv = document.createElement("div") + this.loaderDiv.innerHTML = assets.pages["loadsong"] + var loadingText = this.loaderDiv.querySelector("#loading-text") + this.setAltText(loadingText, strings.loading) + + this.locked = false + this.mode = "main" + + if(noPage){ + this.noPage = true + this.noLoading = noLoading + return + } + + this.touchEnabled = touchEnabled + loader.changePage("customsongs", true) + if(touchEnabled){ + this.getElement("view-outer").classList.add("touch-enabled") + } + + var tutorialTitle = this.getElement("view-title") + this.setAltText(tutorialTitle, strings.customSongs.title) + + var tutorialContent = this.getElement("view-content") + strings.customSongs.description.forEach(string => { + tutorialContent.appendChild(document.createTextNode(string)) + tutorialContent.appendChild(document.createElement("br")) + }) + + this.items = [] + this.linkLocalFolder = document.getElementById("link-localfolder") + this.hasLocal = (typeof showDirectoryPicker === "function" || "webkitdirectory" in HTMLInputElement.prototype) && !(/Android|iPhone|iPad/.test(navigator.userAgent)) + this.selected = -1 + + if(this.hasLocal){ + this.browse = document.getElementById("browse") + pageEvents.add(this.browse, "change", this.browseChange.bind(this)) + this.setAltText(this.linkLocalFolder, strings.customSongs.localFolder) + pageEvents.add(this.linkLocalFolder, ["mousedown", "touchstart"], this.localFolder.bind(this)) + this.items.push(this.linkLocalFolder) + if(this.selected === -1){ + this.linkLocalFolder.classList.add("selected") + this.selected = this.items.length - 1 + } + }else{ + this.linkLocalFolder.parentNode.removeChild(this.linkLocalFolder) + } + + var groupGdrive = document.getElementById("group-gdrive") + this.linkGdriveFolder = document.getElementById("link-gdrivefolder") + this.linkGdriveAccount = document.getElementById("link-gdriveaccount") + this.linkPrivacy = document.getElementById("link-privacy") + if(gameConfig.google_credentials.gdrive_enabled){ + this.setAltText(this.linkGdriveFolder, strings.customSongs.gdriveFolder) + pageEvents.add(this.linkGdriveFolder, ["mousedown", "touchstart"], this.gdriveFolder.bind(this)) + this.items.push(this.linkGdriveFolder) + if(this.selected === -1){ + this.linkGdriveFolder.classList.add("selected") + this.selected = this.items.length - 1 + } + this.setAltText(this.linkGdriveAccount, strings.customSongs.gdriveAccount) + pageEvents.add(this.linkGdriveAccount, ["mousedown", "touchstart"], this.gdriveAccount.bind(this)) + this.items.push(this.linkGdriveAccount) + this.setAltText(this.linkPrivacy, strings.account.privacy) + pageEvents.add(this.linkPrivacy, ["mousedown", "touchstart"], this.openPrivacy.bind(this)) + this.items.push(this.linkPrivacy) + }else{ + groupGdrive.style.display = "none" + this.linkPrivacy.parentNode.removeChild(this.linkPrivacy) + } + + this.endButton = this.getElement("view-end-button") + this.setAltText(this.endButton, strings.session.cancel) + pageEvents.add(this.endButton, ["mousedown", "touchstart"], event => this.onEnd(event, true)) + this.items.push(this.endButton) + if(this.selected === -1){ + this.endButton.classList.add("selected") + this.selected = this.items.length - 1 + } + + this.fileSystem = location.protocol === "https:" && DataTransferItem.prototype.getAsFileSystemHandle + if(this.fileSystem || DataTransferItem.prototype.webkitGetAsEntry){ + this.dropzone = document.getElementById("dropzone") + var dropContent = this.dropzone.getElementsByClassName("view-content")[0] + dropContent.innerText = strings.customSongs.dropzone + this.dragging = false + this.dragTarget = null + pageEvents.add(document, "dragenter", event => { + event.preventDefault() + this.dragTarget = event.target + }) + pageEvents.add(document, "dragover", event => { + event.preventDefault() + if(!this.locked){ + event.dataTransfer.dropEffect = "copy" + this.dropzone.classList.add("dragover") + this.dragging = true + }else{ + event.dataTransfer.dropEffect = "none" + } + }) + pageEvents.add(document, "dragleave", () => { + if(this.dragTarget === event.target){ + event.preventDefault() + this.dropzone.classList.remove("dragover") + this.dragging = false + } + }) + pageEvents.add(document, "drop", this.filesDropped.bind(this)) + } + + this.errorDiv = document.getElementById("customsongs-error") + pageEvents.add(this.errorDiv, ["mousedown", "touchstart"], event => { + if(event.target === event.currentTarget){ + this.hideError() + } + }) + var errorTitle = this.errorDiv.getElementsByClassName("view-title")[0] + this.setAltText(errorTitle, strings.customSongs.importError) + this.errorContent = this.errorDiv.getElementsByClassName("view-content")[0] + this.errorEnd = this.errorDiv.getElementsByClassName("view-end-button")[0] + this.setAltText(this.errorEnd, strings.tutorial.ok) + pageEvents.add(this.errorEnd, ["mousedown", "touchstart"], () => this.hideError(true)) + + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"], + previous: ["left", "up", "ka_l"], + next: ["right", "down", "ka_r"], + backEsc: ["escape"] + }, this.keyPressed.bind(this)) + this.gamepad = new Gamepad({ + confirmPad: ["b", "ls", "rs"], + previous: ["u", "l", "lb", "lt", "lsu", "lsl"], + next: ["d", "r", "rb", "rt", "lsd", "lsr"], + back: ["start", "a"] + }, this.keyPressed.bind(this)) + + pageEvents.send("custom-songs") + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + setAltText(element, text){ + element.innerText = text + element.setAttribute("alt", text) + } + localFolder(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked || this.mode !== "main"){ + return + } + this.changeSelected(this.linkLocalFolder) + if(typeof showDirectoryPicker === "function" && !(/\bOPR\/|\bOPRGX\//.test(navigator.userAgent))){ + return showDirectoryPicker().then(file => { + this.walkFilesystem(file).then(files => this.importLocal(files)).then(input => { + if(input){ + db.setItem("customFolder", [file]) + } + }).catch(e => { + if(e !== "cancel"){ + return Promise.reject(e) + } + }) + }, () => {}) + }else{ + this.browse.click() + } + } + browseChange(event){ + var files = [] + for(var i = 0; i < event.target.files.length; i++){ + files.push(new LocalFile(event.target.files[i])) + } + this.importLocal(files) + } + walkFilesystem(file, path="", output=[]){ + if(file.kind === "directory"){ + return filePermission(file).then(file => { + var values = file.values() + var walkValues = () => values.next().then(generator => { + if(generator.done){ + return output + } + return this.walkFilesystem(generator.value, path + file.name + "/", output).then(() => walkValues()) + }) + return walkValues() + }, () => Promise.resolve()) + }else{ + output.push(new FilesystemFile(file, path + file.name)) + return Promise.resolve(output) + } + } + filesDropped(event){ + event.preventDefault() + this.dropzone.classList.remove("dragover") + this.dragging = false + if(this.locked){ + return + } + var allFiles = [] + var dropPromises = [] + var dbItems = [] + for(var i = 0; i < event.dataTransfer.items.length; i++){ + var item = event.dataTransfer.items[i] + let promise + if(this.fileSystem){ + promise = item.getAsFileSystemHandle().then(file => { + dbItems.push(file) + return this.walkFilesystem(file) + }) + }else{ + var entry = item.webkitGetAsEntry() + if(entry){ + promise = this.walkEntry(entry) + } + } + if(promise){ + dropPromises.push(promise.then(files => { + allFiles = allFiles.concat(files) + })) + } + } + Promise.all(dropPromises).then(() => this.importLocal(allFiles)).then(input => { + if(input && dbItems.length){ + db.setItem("customFolder", dbItems) + } + }) + } + walkEntry(entry, path="", output=[]){ + return new Promise(resolve => { + if(entry.isFile){ + entry.file(file => { + output.push(new LocalFile(file, path + file.name)) + return resolve() + }, resolve) + }else if(entry.isDirectory){ + var dirReader = entry.createReader() + dirReader.readEntries(entries => { + var dirPromises = [] + for(var i = 0; i < entries.length; i++){ + dirPromises.push(this.walkEntry(entries[i], path + entry.name + "/", output)) + } + return Promise.all(dirPromises).then(resolve) + }, resolve) + }else{ + return resolve() + } + }).then(() => output) + } + importLocal(files){ + if(!files.length){ + if(this.noPage){ + return Promise.reject("cancel") + }else{ + return Promise.resolve("cancel") + } + } + this.locked = true + this.loading(true) + + var importSongs = new ImportSongs() + return importSongs.load(files).then(this.songsLoaded.bind(this), e => { + if(!this.noPage){ + this.browse.form.reset() + } + this.locked = false + this.loading(false) + if(e === "nosongs"){ + this.showError(strings.customSongs.noSongs, "nosongs") + }else if(e !== "cancel"){ + return Promise.reject(e) + } + return false + }) + } + gdriveFolder(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked || this.mode !== "main"){ + return + } + this.changeSelected(this.linkGdriveFolder) + this.locked = true + this.loading(true) + var importSongs = new ImportSongs(true) + if(!gpicker){ + var gpickerPromise = loader.loadScript("src/js/gpicker.js").then(() => { + gpicker = new Gpicker() + }) + }else{ + var gpickerPromise = Promise.resolve() + } + gpickerPromise.then(() => { + return gpicker.browse(locked => { + this.locked = locked + this.loading(locked) + }, error => { + this.showError(error) + }) + }).then(files => importSongs.load(files)) + .then(this.songsLoaded.bind(this)) + .catch(e => { + this.locked = false + this.loading(false) + if(e === "nosongs"){ + this.showError(strings.customSongs.noSongs, "nosongs") + }else if(e !== "cancel"){ + return Promise.reject(e) + } + }).finally(() => { + if(this.linkGdriveAccount){ + var addRemove = !gpicker || !gpicker.oauthToken ? "add" : "remove" + this.linkGdriveAccount.classList[addRemove]("hiddenbtn") + } + }) + } + gdriveAccount(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked || this.mode !== "main"){ + return + } + this.changeSelected(this.linkGdriveAccount) + this.locked = true + this.loading(true) + if(!gpicker){ + var gpickerPromise = loader.loadScript("/src/js/gpicker.js").then(() => { + gpicker = new Gpicker() + }) + }else{ + var gpickerPromise = Promise.resolve() + } + gpickerPromise.then(() => { + return gpicker.switchAccounts(locked => { + this.locked = locked + this.loading(locked) + }, error => { + this.showError(error) + }) + }).then(() => { + this.locked = false + this.loading(false) + }).catch(error => { + if(error !== "cancel"){ + this.showError(error) + } + }) + } + openPrivacy(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked || this.mode !== "main"){ + return + } + this.changeSelected(this.linkPrivacy) + open("privacy") + } + loading(show){ + if(this.noLoading){ + return + } + if(show){ + loader.screen.appendChild(this.loaderDiv) + }else if(this.loaderDiv.parentNode){ + this.loaderDiv.parentNode.removeChild(this.loaderDiv) + } + } + songsLoaded(songs){ + if(songs){ + var length = songs.length + assets.songs = songs + assets.customSongs = true + assets.customSelected = this.noPage ? +localStorage.getItem("customSelected") : 0 + } + if(this.noPage){ + pageEvents.send("import-songs", length) + }else{ + assets.sounds["se_don"].play() + setTimeout(() => { + new SongSelect("customSongs", false, this.touchEnabled) + pageEvents.send("import-songs", length) + }, 500) + } + this.clean() + return songs && songs.length + } + keyPressed(pressed, name){ + if(!pressed || this.locked){ + return + } + var selected = this.items[this.selected] + if(this.mode === "main"){ + if(name === "confirm" || name === "confirmPad"){ + if(selected === this.endButton){ + this.onEnd(null, true) + }else if(name !== "confirmPad"){ + if(selected === this.linkLocalFolder){ + assets.sounds["se_don"].play() + this.localFolder() + }else if(selected === this.linkGdriveFolder){ + assets.sounds["se_don"].play() + this.gdriveFolder() + }else if(selected === this.linkGdriveAccount){ + assets.sounds["se_don"].play() + this.gdriveAccount() + }else if(selected === this.linkPrivacy){ + assets.sounds["se_don"].play() + this.openPrivacy() + } + } + }else if(name === "previous" || name === "next"){ + selected.classList.remove("selected") + do{ + this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1)) + }while(this.items[this.selected] === this.linkPrivacy && name !== "previous") + this.items[this.selected].classList.add("selected") + assets.sounds["se_ka"].play() + }else if(name === "back" || name === "backEsc"){ + if(!this.dragging || name !== "backEsc"){ + this.onEnd() + } + } + }else if(this.mode === "error"){ + if(name === "confirm" || name === "confirmPad" || name === "back" || name === "backEsc"){ + this.hideError(name === "confirm" || name === "confirmPad") + } + } + } + changeSelected(button){ + var selected = this.items[this.selected] + if(selected !== button){ + selected.classList.remove("selected") + this.selected = this.items.findIndex(item => item === button) + this.items[this.selected].classList.add("selected") + } + } + mod(length, index){ + return ((index % length) + length) % length + } + onEnd(event, confirm){ + if(this.locked || this.mode !== "main"){ + return + } + var touched = false + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + touched = true + }else if(event.which !== 1){ + return + } + }else{ + touched = this.touchEnabled + } + this.clean() + if(!this.noPage){ + assets.sounds[confirm ? "se_don" : "se_cancel"].play() + } + return new Promise(resolve => setTimeout(() => { + new SongSelect("customSongs", false, touched) + resolve() + }, 500)) + } + showError(text, errorName){ + this.locked = false + this.loading(false) + if(this.noPage){ + var error = new Error(text) + error.name = errorName + throw error + }else if(this.mode === "error"){ + return + } + this.mode = "error" + this.errorContent.innerText = text + this.errorDiv.style.display = "flex" + assets.sounds["se_pause"].play() + } + hideError(confirm){ + if(this.mode !== "error"){ + return + } + this.mode = "main" + this.errorDiv.style.display = "" + assets.sounds[confirm ? "se_don" : "se_cancel"].play() + } + clean(){ + delete this.loaderDiv + if(this.noPage){ + return + } + this.keyboard.clean() + this.gamepad.clean() + pageEvents.remove(this.browse, "change") + if(this.hasLocal){ + pageEvents.remove(this.linkLocalFolder, ["mousedown", "touchstart"]) + } + if(gameConfig.google_credentials.gdrive_enabled){ + pageEvents.remove(this.linkGdriveFolder, ["mousedown", "touchstart"]) + pageEvents.remove(this.linkGdriveAccount, ["mousedown", "touchstart"]) + pageEvents.remove(this.linkPrivacy, ["mousedown", "touchstart"]) + } + pageEvents.remove(this.endButton, ["mousedown", "touchstart"]) + pageEvents.remove(this.errorDiv, ["mousedown", "touchstart"]) + pageEvents.remove(this.errorEnd, ["mousedown", "touchstart"]) + if(DataTransferItem.prototype.webkitGetAsEntry){ + pageEvents.remove(document, ["dragenter", "dragover", "dragleave", "drop"]) + delete this.dropzone + delete this.dragTarget + } + if(gpicker){ + gpicker.tokenResolve = null + } + delete this.browse + delete this.linkLocalFolder + delete this.linkGdriveFolder + delete this.linkGdriveAccount + delete this.linkPrivacy + delete this.endButton + delete this.items + delete this.errorDiv + delete this.errorContent + delete this.errorEnd + } +} diff --git a/public/src/js/debug.js b/public/src/js/debug.js new file mode 100644 index 0000000..4c9bf16 --- /dev/null +++ b/public/src/js/debug.js @@ -0,0 +1,491 @@ +class Debug{ + constructor(...args){ + this.init(...args) + } + init(){ + if(!assets.pages["debug"]){ + return + } + this.debugDiv = document.createElement("div") + this.debugDiv.id = "debug" + this.debugDiv.innerHTML = assets.pages["debug"] + document.body.appendChild(this.debugDiv) + + this.titleDiv = this.byClass("title") + this.minimiseDiv = this.byClass("minimise") + this.offsetDiv = this.byClass("offset") + this.measureNumDiv = this.byClass("measure-num") + this.branchHideDiv = this.byClass("branch-hide") + this.branchSelectDiv = this.byClass("branch-select") + this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0] + this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0] + this.volumeDiv = this.byClass("music-volume") + this.lyricsHideDiv = this.byClass("lyrics-hide") + this.lyricsOffsetDiv = this.byClass("lyrics-offset") + this.restartLabel = this.byClass("change-restart-label") + this.restartCheckbox = this.byClass("change-restart") + this.autoplayLabel = this.byClass("autoplay-label") + this.autoplayCheckbox = this.byClass("autoplay") + this.restartBtn = this.byClass("restart-btn") + this.exitBtn = this.byClass("exit-btn") + + this.moving = false + this.windowSymbol = Symbol() + pageEvents.add(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.stopMove.bind(this), this.windowSymbol) + pageEvents.mouseAdd(this, this.onMove.bind(this)) + pageEvents.add(window, "touchmove", this.onMove.bind(this)) + pageEvents.add(this.titleDiv, ["mousedown", "touchstart"], this.startMove.bind(this)) + pageEvents.add(this.minimiseDiv, ["click", "touchstart"], this.minimise.bind(this)) + pageEvents.add(this.restartBtn, ["click", "touchstart"], this.restartSong.bind(this)) + pageEvents.add(this.exitBtn, ["click", "touchstart"], this.clean.bind(this)) + pageEvents.add(this.restartLabel, "touchstart", this.touchBox.bind(this)) + pageEvents.add(this.autoplayLabel, "touchstart", this.touchBox.bind(this)) + pageEvents.add(this.autoplayCheckbox, "change", this.toggleAutoplay.bind(this)) + pageEvents.add(this.branchSelect, "change", this.branchChange.bind(this)) + pageEvents.add(this.branchResetBtn, ["click", "touchstart"], this.branchReset.bind(this)) + + this.offsetSlider = new InputSlider(this.offsetDiv, -60, 60, 3) + this.offsetSlider.onchange(this.offsetChange.bind(this)) + + this.measureNumSlider = new InputSlider(this.measureNumDiv, 0, 1000, 0) + this.measureNumSlider.onchange(this.measureNumChange.bind(this)) + this.measureNumSlider.set(0) + + this.volumeSlider = new InputSlider(this.volumeDiv, 0, 3, 2) + this.volumeSlider.onchange(this.volumeChange.bind(this)) + this.volumeSlider.set(1) + + this.lyricsSlider = new InputSlider(this.lyricsOffsetDiv, -60, 60, 3) + this.lyricsSlider.onchange(this.lyricsChange.bind(this)) + + this.moveTo(100, 100) + this.restore() + this.updateStatus() + pageEvents.send("debug") + } + byClass(name){ + return this.debugDiv.getElementsByClassName(name)[0] + } + startMove(event){ + if(event.which === 1 || event.type === "touchstart"){ + event.stopPropagation() + var divPos = this.debugDiv.getBoundingClientRect() + var click = event.type === "touchstart" ? event.changedTouches[0] : event + var x = click.pageX - divPos.left + var y = click.pageY - divPos.top + this.moving = {x: x, y: y} + } + } + onMove(event){ + if(this.moving){ + var click = event.type === "touchmove" ? event.changedTouches[0] : event + var x = click.clientX - this.moving.x + var y = click.clientY - this.moving.y + this.moveTo(x, y) + } + } + stopMove(event){ + if(this.debugDiv.style.display === "none"){ + return + } + if(!event || event.type === "resize"){ + var divPos = this.debugDiv.getBoundingClientRect() + var x = divPos.left + var y = divPos.top + }else{ + var click = event.type === "touchstart" || event.type === "touchend" ? event.changedTouches[0] : event + if(event.type == "blur"){ + var x = this.moving.x + var y = this.moving.y + }else{ + var x = click.clientX - this.moving.x + var y = click.clientY - this.moving.y + } + } + var w = this.debugDiv.offsetWidth + var h = this.debugDiv.offsetHeight + if(x + w > innerWidth){ + x = innerWidth - w + } + if(y + h > lastHeight){ + y = lastHeight - h + } + if(x < 0){ + x = 0 + } + if(y < 0){ + y = 0 + } + this.moveTo(x, y) + this.moving = false + } + moveTo(x, y){ + this.debugDiv.style.transform = "translate(" + x + "px, " + y + "px)" + } + restore(){ + debugObj.state = "open" + this.debugDiv.style.display = "" + this.stopMove() + } + minimise(){ + debugObj.state = "minimised" + this.debugDiv.style.display = "none" + } + updateStatus(){ + if(debugObj.controller && !this.controller){ + this.controller = debugObj.controller + + this.restartBtn.style.display = "block" + this.autoplayLabel.style.display = "block" + if(this.controller.parsedSongData.branches){ + this.branchHideDiv.style.display = "block" + } + if(this.controller.lyrics){ + this.lyricsHideDiv.style.display = "block" + } + + var selectedSong = this.controller.selectedSong + this.defaultOffset = selectedSong.offset || 0 + if(this.songHash === selectedSong.hash){ + this.offsetChange(this.offsetSlider.get(), true) + this.branchChange(null, true) + this.volumeChange(this.volumeSlider.get(), true) + this.lyricsChange(this.lyricsSlider.get(), true) + }else{ + this.songHash = selectedSong.hash + this.offsetSlider.set(this.defaultOffset) + this.branchReset(null, true) + this.volumeSlider.set(this.controller.volume) + this.lyricsSlider.set(this.controller.lyrics ? this.controller.lyrics.vttOffset / 1000 : 0) + } + + var measures = this.controller.parsedSongData.measures.filter((measure, i, array) => { + return i === 0 || Math.abs(measure.ms - array[i - 1].ms) > 0.01 + }) + this.measureNumSlider.setMinMax(0, measures.length - 1) + if(this.measureNum > 0 && measures.length >= this.measureNum){ + var measureMS = measures[this.measureNum - 1].ms + var game = this.controller.game + game.started = true + var timestamp = Date.now() + var currentDate = timestamp - measureMS + game.startDate = currentDate + game.sndTime = timestamp - snd.buffer.getTime() * 1000 + var circles = game.songData.circles + for(var i in circles){ + game.currentCircle = i + if(circles[i].endTime >= measureMS){ + break + } + game.skipNote(circles[i]) + } + if(game.mainMusicPlaying){ + game.mainMusicPlaying = false + game.mainAsset.stop() + } + } + this.autoplayCheckbox.checked = this.controller.autoPlayEnabled + } + if(this.controller && !debugObj.controller){ + this.restartBtn.style.display = "" + this.autoplayLabel.style.display = "" + this.branchHideDiv.style.display = "" + this.lyricsHideDiv.style.display = "" + this.controller = null + } + this.stopMove() + } + offsetChange(value, noRestart){ + if(this.controller){ + var offset = (this.defaultOffset - value) * 1000 + var songData = this.controller.parsedSongData + songData.circles.forEach(circle => { + circle.ms = circle.originalMS + offset + circle.endTime = circle.originalEndTime + offset + }) + songData.measures.forEach(measure => { + measure.ms = measure.originalMS + offset + }) + if(songData.branches){ + songData.branches.forEach(branch => { + branch.ms = branch.originalMS + offset + }) + } + if(this.controller.lyrics){ + this.controller.lyrics.offsetChange(value * 1000) + } + if(this.restartCheckbox.checked && !noRestart){ + this.restartSong() + } + } + } + measureNumChange(value){ + this.measureNum = value + if(this.restartCheckbox.checked){ + this.restartSong() + } + } + volumeChange(value, noRestart){ + if(this.controller){ + snd.musicGain.setVolumeMul(value) + } + if(this.restartCheckbox.checked && !noRestart){ + this.restartSong() + } + } + lyricsChange(value, noRestart){ + if(this.controller && this.controller.lyrics){ + this.controller.lyrics.offsetChange(undefined, value * 1000) + } + if(this.restartCheckbox.checked && !noRestart){ + this.restartSong() + } + } + restartSong(){ + if(this.controller){ + this.controller.restartSong() + } + } + toggleAutoplay(event){ + if(this.controller){ + this.controller.autoPlayEnabled = this.autoplayCheckbox.checked + if(this.controller.autoPlayEnabled){ + this.controller.saveScore = false + }else{ + var keyboard = debugObj.controller.keyboard + keyboard.setKey(false, "don_l") + keyboard.setKey(false, "don_r") + keyboard.setKey(false, "ka_l") + keyboard.setKey(false, "ka_r") + } + } + } + branchChange(event, noRestart){ + if(this.controller){ + var game = this.controller.game + var name = this.branchSelect.value + game.branch = name === "auto" ? false : name + game.branchSet = name === "auto" + if(noRestart){ + game.branchStatic = true + } + var selectedOption = this.branchSelect.selectedOptions[0] + this.branchSelect.style.background = selectedOption.style.background + if(this.restartCheckbox.checked && !noRestart){ + this.restartSong() + } + } + } + branchReset(event, noRestart){ + this.branchSelect.value = "auto" + this.branchChange(null, noRestart) + } + touchBox(event){ + event.currentTarget.click() + } + clean(){ + this.offsetSlider.clean() + this.measureNumSlider.clean() + this.volumeSlider.clean() + this.lyricsSlider.clean() + + pageEvents.remove(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.windowSymbol) + pageEvents.mouseRemove(this) + pageEvents.remove(window, "touchmove") + pageEvents.remove(this.titleDiv, ["mousedown", "touchstart"]) + pageEvents.remove(this.minimiseDiv, ["click", "touchstart"]) + pageEvents.remove(this.restartBtn, ["click", "touchstart"]) + pageEvents.remove(this.exitBtn, ["click", "touchstart"]) + pageEvents.remove(this.restartLabel, "touchstart") + pageEvents.remove(this.autoplayLabel, "touchstart") + pageEvents.remove(this.autoplayCheckbox, "change") + pageEvents.remove(this.branchSelect, "change") + pageEvents.remove(this.branchResetBtn, ["click", "touchstart"]) + + delete this.offsetSlider + delete this.measureNumSlider + delete this.volumeSlider + delete this.titleDiv + delete this.minimiseDiv + delete this.offsetDiv + delete this.measureNumDiv + delete this.branchHideDiv + delete this.branchSelectDiv + delete this.branchSelect + delete this.branchResetBtn + delete this.volumeDiv + delete this.lyricsHideDiv + delete this.lyricsOffsetDiv + delete this.restartCheckbox + delete this.autoplayLabel + delete this.autoplayCheckbox + delete this.restartBtn + delete this.exitBtn + delete this.controller + delete this.windowSymbol + + debugObj.state = "closed" + debugObj.debug = null + document.body.removeChild(this.debugDiv) + + delete this.debugDiv + } +} +class InputSlider{ + constructor(...args){ + this.init(...args) + } + init(sliderDiv, min, max, fixedPoint){ + this.fixedPoint = fixedPoint + this.mul = Math.pow(10, fixedPoint) + this.min = min * this.mul + this.max = max * this.mul + + this.input = sliderDiv.getElementsByTagName("input")[0] + this.reset = sliderDiv.getElementsByClassName("reset")[0] + this.plus = sliderDiv.getElementsByClassName("plus")[0] + this.minus = sliderDiv.getElementsByClassName("minus")[0] + this.value = null + this.defaultValue = null + this.callbacks = [] + this.touchEnd = [] + this.windowSymbol = Symbol() + pageEvents.add(this.input, ["touchstart", "touchend"], event => { + event.stopPropagation() + }) + pageEvents.add(window, ["mouseup", "touchstart", "touchend", "blur"], event => { + if(event.type !== "touchstart"){ + this.touchEnd.forEach(func => func(event)) + }else if(event.target !== this.input){ + this.input.blur() + } + }, this.windowSymbol) + + this.addTouchRepeat(this.plus, this.add.bind(this)) + this.addTouchRepeat(this.minus, this.subtract.bind(this)) + this.addTouch(this.reset, this.resetValue.bind(this)) + pageEvents.add(this.input, "change", this.manualSet.bind(this)) + pageEvents.add(this.input, "keydown", this.captureKeys.bind(this)) + } + update(noCallback, force){ + var oldValue = this.input.value + if(this.value === null){ + this.input.value = "" + this.input.readOnly = true + }else{ + if(this.value > this.max){ + this.value = this.max + } + if(this.value < this.min){ + this.value = this.min + } + this.input.value = this.get().toFixed(this.fixedPoint) + this.input.readOnly = false + } + if(force || !noCallback && oldValue !== this.input.value){ + this.callbacks.forEach(callback => { + callback(this.get()) + }) + } + } + set(number){ + this.value = Math.floor(number * this.mul) + this.defaultValue = this.value + this.update(true) + } + setMinMax(min, max){ + this.min = min + this.max = max + this.update() + } + get(){ + if(this.value === null){ + return null + }else{ + return Math.floor(this.value) / this.mul + } + } + add(event){ + if(this.value !== null){ + var newValue = this.value + this.eventNumber(event) + if(newValue <= this.max){ + this.value = newValue + this.update() + } + } + } + subtract(event){ + if(this.value !== null){ + var newValue = this.value - this.eventNumber(event) + if(newValue >= this.min){ + this.value = newValue + this.update() + } + } + } + eventNumber(event){ + return (event.ctrlKey ? 10 : 1) * (event.shiftKey ? 10 : 1) * (event.altKey ? 10 : 1) * 1 + } + resetValue(){ + this.value = this.defaultValue + this.update() + } + onchange(callback){ + this.callbacks.push(callback) + } + manualSet(){ + var number = parseFloat(this.input.value) * this.mul + if(Number.isFinite(number) && this.min <= number && number <= this.max){ + this.value = number + } + this.update(false, true) + } + captureKeys(event){ + event.stopPropagation() + } + addTouch(element, callback){ + pageEvents.add(element, ["mousedown", "touchstart"], event => { + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + callback(event) + }) + } + addTouchRepeat(element, callback){ + this.addTouch(element, event => { + var active = true + var func = () => { + active = false + this.touchEnd.splice(this.touchEnd.indexOf(func), 1) + } + this.touchEnd.push(func) + var repeat = delay => { + if(active && this.touchEnd){ + callback(event) + setTimeout(() => repeat(50), delay) + } + } + repeat(400) + }) + } + removeTouch(element){ + pageEvents.remove(element, ["mousedown", "touchstart"]) + } + clean(){ + this.removeTouch(this.plus) + this.removeTouch(this.minus) + this.removeTouch(this.reset) + pageEvents.remove(this.input, ["touchstart", "touchend"]) + pageEvents.remove(window, ["mouseup", "touchstart", "touchend", "blur"], this.windowSymbol) + pageEvents.remove(this.input, ["touchstart", "change", "keydown"]) + + delete this.input + delete this.reset + delete this.plus + delete this.minus + delete this.windowSymbol + delete this.touchEnd + } +} diff --git a/public/src/js/game.js b/public/src/js/game.js new file mode 100644 index 0000000..dc64545 --- /dev/null +++ b/public/src/js/game.js @@ -0,0 +1,862 @@ +class Game{ + constructor(...args){ + this.init(...args) + } + init(controller, selectedSong, songData){ + this.controller = controller + this.selectedSong = selectedSong + this.songData = songData + this.elapsedTime = 0 + this.currentCircle = -1 + this.currentEvent = 0 + this.updateCurrentCircle() + this.combo = 0 + this.rules = new GameRules(this) + this.globalScore = { + points: 0, + good: 0, + ok: 0, + bad: 0, + maxCombo: 0, + drumroll: 0, + gauge: 0, + title: selectedSong.title, + difficulty: this.rules.difficulty + } + var combo = this.songData.circles.filter(circle => { + var type = circle.type + return (type === "don" || type === "ka" || type === "daiDon" || type === "daiKa") && (!circle.branch || circle.branch.active) + }).length + this.soulPoints = this.rules.soulPoints(combo) + this.paused = false + this.started = false + this.mainMusicPlaying = false + this.musicFadeOut = 0 + this.fadeOutStarted = false + this.currentTimingPoint = 0 + this.branchNames = ["normal", "advanced", "master"] + this.resetSection() + this.gameLagSync = !this.controller.touchEnabled && !(/Firefox/.test(navigator.userAgent)) + + assets.songs.forEach(song => { + if(song.id == selectedSong.folder){ + this.mainAsset = song.sound + } + }) + } + run(){ + this.timeForDistanceCircle = 2500 + this.initTiming() + this.view = this.controller.view + } + initTiming(){ + // Date when the chrono is started (before the game begins) + var firstCircle = this.songData.circles[0] + if(this.controller.calibrationMode){ + var offsetTime = 0 + }else{ + var offsetTime = Math.max(0, this.timeForDistanceCircle - (firstCircle ? firstCircle.ms : 0)) |0 + } + if(this.controller.multiplayer){ + var syncWith = this.controller.syncWith + var syncCircles = syncWith.game.songData.circles + var syncOffsetTime = Math.max(0, this.timeForDistanceCircle - syncCircles[0].ms) |0 + offsetTime = Math.max(offsetTime, syncOffsetTime) + } + this.elapsedTime = -offsetTime + // The real start for the game will start when chrono will reach 0 + this.startDate = Date.now() + offsetTime + } + update(){ + this.updateTime() + // Main operations + this.updateCirclesStatus() + this.checkPlays() + // Event operations + this.whenFadeoutMusic() + if(this.controller.multiplayer !== 2){ + this.whenLastCirclePlayed() + } + } + getCircles(){ + return this.songData.circles + } + updateCirclesStatus(){ + var nextSet = false + var ms = this.elapsedTime + var circles = this.songData.circles + var startIndex = this.currentCircle === 0 ? 0 : this.currentCircle - 1 + var index = 0 + + for(var i = startIndex; i < circles.length; i++){ + var circle = circles[i] + if(circle && (!circle.branch || circle.branch.active) && !circle.isPlayed){ + var type = circle.type + var drumrollNotes = type === "balloon" || type === "drumroll" || type === "daiDrumroll" + var endTime = circle.endTime + (drumrollNotes ? 0 : this.rules.bad) + this.controller.audioLatency + + if(ms >= circle.ms + this.controller.audioLatency){ + if(drumrollNotes && !circle.rendaPlayed && ms < endTime + this.controller.audioLatency){ + circle.rendaPlayed = true + if(this.rules.difficulty === "easy"){ + assets.sounds["v_renda" + this.controller.snd].stop() + this.controller.playSound("v_renda") + } + } + } + if(circle.daiFailed && (ms >= circle.daiFailed.ms + this.rules.daiLeniency || ms > endTime)){ + this.checkScore(circle, circle.daiFailed.check) + }else if(ms > endTime){ + if(!this.controller.autoPlayEnabled){ + if(drumrollNotes){ + if(circle.section && circle.timesHit === 0){ + this.resetSection() + } + circle.played(-1, false) + this.updateCurrentCircle() + if(this.controller.multiplayer === 1){ + var value = { + pace: (ms - circle.ms - this.controller.audioLatency) / circle.timesHit + } + if(type === "drumroll" || type === "daiDrumroll"){ + value.kaAmount = circle.timesKa / circle.timesHit + } + p2.send("drumroll", value) + } + }else{ + this.skipNote(circle) + this.updateCurrentCircle() + } + } + }else if(!this.controller.autoPlayEnabled && !nextSet){ + nextSet = true + this.currentCircle = i + } + if(index++ > 1){ + break + } + } + } + + var branches = this.songData.branches + if(branches){ + var force = this.controller.multiplayer === 2 ? p2 : this + var measures = this.songData.measures + if(this.controller.multiplayer === 2 || force.branch){ + if(!force.branchSet){ + force.branchSet = true + if(branches.length){ + this.setBranch(branches[0], force.branch) + } + var view = this.controller.view + var currentMeasure = view.branch + for(var i = measures.length; i--;){ + var measure = measures[i] + if(measure.nextBranch && measure.ms <= ms){ + currentMeasure = measure.nextBranch.active + } + } + if(view.branch !== currentMeasure){ + if(!this.branchStatic){ + view.branchAnimate = { + ms: ms, + fromBranch: view.branch + } + } + this.branchStatic = false + view.branch = currentMeasure + } + } + } + for(var i = 0; i < measures.length; i++){ + var measure = measures[i] + if(measure.ms > ms){ + break + }else{ + if(measure.nextBranch && !measure.gameChecked){ + measure.gameChecked = true + var branch = measure.nextBranch + if(branch.type){ + var accuracy = 0 + if(branch.type === "drumroll"){ + if(force.branch){ + var accuracy = Math.max(0, branch.requirement[force.branch]) + }else{ + var accuracy = this.sectionDrumroll + } + }else if(this.sectionNotes.length !== 0){ + if(force.branch){ + var accuracy = Math.max(0, Math.min(100, branch.requirement[force.branch])) + }else{ + var accuracy = this.sectionNotes.reduce((a, b) => a + b) / this.sectionNotes.length * 100 + } + } + if(accuracy >= branch.requirement.master){ + this.setBranch(branch, "master") + }else if(accuracy >= branch.requirement.advanced){ + this.setBranch(branch, "advanced") + }else{ + this.setBranch(branch, "normal") + } + }else if(this.controller.multiplayer === 1){ + p2.send("branch", "normal") + } + } + if(this.controller.lyrics){ + if(!measure.branch){ + this.controller.lyrics.branch = null + }else if(measure.branch.active){ + this.controller.lyrics.branch = measure.branch.name + } + } + } + } + } + + if(this.controller.lyrics){ + this.controller.lyrics.update(ms) + } + } + fixNoteStream(keysDon){ + var circleIsNote = circle => { + var type = circle.type + return type === "don" || type === "ka" || type === "daiDon" || type === "daiKa" + } + var correctNote = circle => { + var type = circle.type + return keysDon ? (type === "don" || type === "daiDon") : (type === "ka" || type === "daiKa") + } + var ms = this.elapsedTime + var circles = this.songData.circles + + for(var i = this.currentCircle + 1; i < circles.length; i++){ + var circle = circles[i] + var relative = ms - circle.ms - this.controller.audioLatency + if(!circle.branch || circle.branch.active){ + if((!circleIsNote(circle) || relative < -this.rules.bad)){ + break + }else if(Math.abs(relative) < this.rules.ok && correctNote(circle)){ + for(var j = this.currentCircle; j < i; j++){ + var circle = circles[j] + if(circle && !circle.branch || circle.branch.active){ + this.skipNote(circles[j]) + } + } + this.currentCircle = i + return circles[i] + } + } + } + } + skipNote(circle){ + if(circle.section){ + this.resetSection() + } + circle.played(-1, circle.type === "daiDon" || circle.type === "daiKa") + this.sectionNotes.push(0) + this.controller.displayScore(0, true) + this.updateCombo(0) + this.updateGlobalScore(0, 1) + if(this.controller.multiplayer === 1){ + p2.send("note", { + score: -1 + }) + } + } + checkPlays(){ + var circles = this.songData.circles + var circle = circles[this.currentCircle] + + if(this.controller.autoPlayEnabled){ + while(circle && this.controller.autoPlay(circle)){ + circle = circles[this.currentCircle] + } + return + } + var keys = this.controller.getKeys() + + var don_l = keys["don_l"] && !this.controller.isWaiting("don_l", "score") + var don_r = keys["don_r"] && !this.controller.isWaiting("don_r", "score") + var ka_l = keys["ka_l"] && !this.controller.isWaiting("ka_l", "score") + var ka_r = keys["ka_r"] && !this.controller.isWaiting("ka_r", "score") + + var checkDon = () => { + if(don_l && don_r){ + this.checkKey(["don_l", "don_r"], circle, "daiDon") + }else if(don_l){ + this.checkKey(["don_l"], circle, "don") + }else if(don_r){ + this.checkKey(["don_r"], circle, "don") + } + } + var checkKa = () => { + if(ka_l && ka_r){ + this.checkKey(["ka_l", "ka_r"], circle, "daiKa") + }else if(ka_l){ + this.checkKey(["ka_l"], circle, "ka") + }else if(ka_r){ + this.checkKey(["ka_r"], circle, "ka") + } + } + var keyTime = this.controller.getKeyTime() + if(keyTime["don"] >= keyTime["ka"]){ + checkDon() + checkKa() + }else{ + checkKa() + checkDon() + } + } + checkKey(keyCodes, circle, check){ + if(circle && !circle.isPlayed){ + if(!this.checkScore(circle, check)){ + return + } + } + keyCodes.forEach(keyCode => { + this.controller.waitForKeyup(keyCode, "score") + }) + } + checkScore(circle, check){ + var ms = this.elapsedTime + var type = circle.type + + var keysDon = check === "don" || check === "daiDon" + var keysKa = check === "ka" || check === "daiKa" + var keyDai = check === "daiDon" || check === "daiKa" + var typeDon = type === "don" || type === "daiDon" + var typeKa = type === "ka" || type === "daiKa" + var typeDai = type === "daiDon" || type === "daiKa" + + var keyTime = this.controller.getKeyTime() + var currentTime = circle.daiFailed ? circle.daiFailed.ms : keysDon ? keyTime["don"] : keyTime["ka"] + var relative = currentTime - circle.ms - this.controller.audioLatency + + if(relative >= this.rules.ok){ + var fixedNote = this.fixNoteStream(keysDon) + if(fixedNote){ + return this.checkScore(fixedNote, check) + } + } + + if(typeDon || typeKa){ + if(-this.rules.bad >= relative || relative >= this.rules.bad){ + return true + } + var score = 0 + if(keysDon && typeDon || keysKa && typeKa){ + var circleStatus = -1 + relative = Math.abs(relative) + if(relative < this.rules.good){ + circleStatus = 450 + }else if(relative < this.rules.ok){ + circleStatus = 230 + }else if(relative < this.rules.bad){ + circleStatus = 0 + } + if(typeDai && !keyDai){ + if(this.controller.easierBigNotes){ + // Taiko Force Lv5 can't hit both Dons at the same time, so dai offered + keyDai = true + }else if(!circle.daiFailed){ + circle.daiFailed = { + ms: ms, + status: circleStatus, + check: check + } + return false + }else if(ms < circle.daiFailed.ms + this.rules.daiLeniency){ + return false + }else{ + circleStatus = circle.daiFailed.status + } + } + if(circleStatus === 230 || circleStatus === 450){ + score = circleStatus + } + circle.played(score, score === 0 ? typeDai : keyDai) + this.controller.displayScore(score, false, typeDai && keyDai) + }else{ + var keyTime = this.controller.getKeyTime() + var keyTimeRelative = Math.abs(keyTime.don - keyTime.ka) + if(Math.abs(relative) >= (keyTimeRelative <= 25 ? this.rules.bad : this.rules.good)){ + return true + } + circle.played(-1, typeDai) + this.controller.displayScore(score, true, false) + } + this.updateCombo(score) + this.updateGlobalScore(score, typeDai && keyDai ? 2 : 1, circle.gogoTime) + this.updateCurrentCircle() + if(circle.section){ + this.resetSection() + } + this.sectionNotes.push(score === 450 ? 1 : (score === 230 ? 0.5 : 0)) + if(this.controller.multiplayer === 1){ + var value = { + score: score, + ms: circle.ms - currentTime - this.controller.audioLatency, + dai: typeDai ? (keyDai ? 2 : 1) : 0 + } + if((!keysDon || !typeDon) && (!keysKa || !typeKa)){ + value.reverse = true + } + p2.send("note", value) + } + }else{ + if(circle.ms + this.controller.audioLatency > currentTime || currentTime > circle.endTime + this.controller.audioLatency){ + return true + } + if(keysDon && type === "balloon"){ + this.checkBalloon(circle) + if(check === "daiDon" && !circle.isPlayed){ + this.checkBalloon(circle) + } + }else if((keysDon || keysKa) && (type === "drumroll" || type === "daiDrumroll")){ + this.checkDrumroll(circle, keysKa) + if(keyDai){ + this.checkDrumroll(circle, keysKa) + } + } + } + return true + } + checkBalloon(circle){ + if(circle.timesHit >= circle.requiredHits - 1){ + var score = 5000 + this.updateCurrentCircle() + circle.hit() + circle.played(score) + if(this.controller.multiplayer == 1){ + p2.send("drumroll", { + pace: (this.elapsedTime - circle.ms + this.controller.audioLatency) / circle.timesHit + }) + } + }else{ + var score = 300 + circle.hit() + } + this.globalScore.drumroll++ + this.sectionDrumroll++ + this.globalScore.points += score + this.view.setDarkBg(false) + } + checkDrumroll(circle, keysKa){ + var ms = this.elapsedTime + var dai = circle.type === "daiDrumroll" + var score = 100 + if(circle.section && circle.timesHit === 0){ + this.resetSection() + } + circle.hit(keysKa) + var keyTime = this.controller.getKeyTime() + if(circle.type === "drumroll"){ + var sound = keyTime["don"] > keyTime["ka"] ? "don" : "ka" + }else{ + var sound = keyTime["don"] > keyTime["ka"] ? "daiDon" : "daiKa" + } + var circleAnim = new Circle({ + id: 0, + start: ms, + type: sound, + txt: "", + speed: circle.speed, + gogoTime: circle.gogoTime, + fixedPos: document.hasFocus() + }) + circleAnim.played(score, dai) + circleAnim.animate(ms) + this.view.drumroll.push(circleAnim) + this.globalScore.drumroll++ + this.sectionDrumroll++ + this.globalScore.points += score * (dai ? 2 : 1) + this.view.setDarkBg(false) + } + getLastCircle(circles){ + for(var i = circles.length; i--;){ + return circles[i] + } + } + whenLastCirclePlayed(){ + var ms = this.elapsedTime + if(!this.lastCircle){ + var circles = this.songData.circles + var circle = this.getLastCircle(circles) + this.lastCircle = circle ? circle.endTime : 0 + if(this.controller.multiplayer){ + var syncWith = this.controller.syncWith + var syncCircles = syncWith.game.songData.circles + circle = this.getLastCircle(syncCircles) + var syncLastCircle = circle ? circle.endTime : 0 + if(syncLastCircle > this.lastCircle){ + this.lastCircle = syncLastCircle + } + } + } + if(!this.fadeOutStarted && ms >= this.lastCircle + 2000 + this.controller.audioLatency){ + this.fadeOutStarted = ms + if(this.controller.multiplayer){ + this.controller.syncWith.game.fadeOutStarted = ms + } + } + } + whenFadeoutMusic(){ + var started = this.fadeOutStarted + if(started){ + var ms = this.elapsedTime + var duration = this.mainAsset ? this.mainAsset.duration : 0 + var musicDuration = duration * 1000 - this.controller.offset + if(this.musicFadeOut === 0){ + if(this.controller.multiplayer === 1){ + var obj = this.getGlobalScore() + obj.name = account.loggedIn ? account.displayName : null + p2.send("gameresults", obj) + } + this.musicFadeOut++ + }else if(this.musicFadeOut === 1 && ms >= started + 1600){ + this.controller.gameEnded() + if(!p2.session && this.controller.multiplayer === 1){ + p2.send("gameend") + } + this.musicFadeOut++ + }else if(this.musicFadeOut === 2 && (ms >= Math.max(started + 8600, Math.min(started + 8600 + 5000, musicDuration + 250)))){ + this.controller.displayResults() + this.musicFadeOut++ + }else if(this.musicFadeOut === 3 && (ms >= Math.max(started + 9600, Math.min(started + 9600 + 5000, musicDuration + 1250)))){ + this.controller.clean() + if(this.controller.scoresheet){ + this.controller.scoresheet.startRedraw() + } + } + } + } + playMainMusic(){ + var ms = this.elapsedTime + this.controller.offset + if(!this.mainMusicPlaying && (!this.fadeOutStarted || ms < this.fadeOutStarted + 1600)){ + if(this.calibrationState === "audio"){ + var beatInterval = this.controller.view.beatInterval + var startAt = ms % beatInterval + var duration = this.mainAsset.duration * 1000 + if(startAt < duration){ + this.mainAsset.playLoop(0, false, startAt / 1000, 0, beatInterval / 1000) + }else{ + this.mainAsset.playLoop((startAt - duration) / 1000, false, 0, 0, beatInterval / 1000) + } + }else if(this.controller.multiplayer !== 2 && this.mainAsset){ + this.mainAsset.play((ms < 0 ? -ms : 0) / 1000, false, Math.max(0, ms / 1000)) + } + this.mainMusicPlaying = true + } + } + togglePause(forcePause, pauseMove, noSound){ + if(!this.paused){ + if(forcePause === false){ + return + } + if(!noSound){ + this.controller.playSound("se_pause", 0, true) + } + this.paused = true + this.latestDate = Date.now() + if(this.mainAsset){ + this.mainAsset.stop() + } + this.mainMusicPlaying = false + this.view.pauseMove(pauseMove || 0, true) + this.view.gameDiv.classList.add("game-paused") + this.view.lastMousemove = this.view.getMS() + this.view.cursorHidden = false + pageEvents.send("pause") + }else if(!forcePause){ + if(forcePause !== false && this.calibrationState && ["audioHelp", "audioComplete", "videoHelp", "videoComplete", "results"].indexOf(this.calibrationState) !== -1){ + return + } + if(this.calibrationState === "audioHelp" || this.calibrationState === "videoHelp"){ + this.calibrationState = this.calibrationState === "audioHelp" ? "audio" : "video" + this.controller.view.pauseOptions = strings.pauseOptions + this.controller.playSound("se_don", 0, true) + }else if(!noSound){ + this.controller.playSound("se_cancel", 0, true) + } + this.paused = false + var currentDate = Date.now() + this.startDate += currentDate - this.latestDate + this.sndTime = currentDate - snd.buffer.getTime() * 1000 + this.view.gameDiv.classList.remove("game-paused") + this.view.pointer() + pageEvents.send("unpause", currentDate - this.latestDate) + } + } + isPaused(){ + return this.paused + } + updateTime(){ + // Refreshed date + var ms = this.elapsedTime + if(ms >= 0 && !this.started){ + this.startDate = Date.now() + this.elapsedTime = this.getAccurateTime() + this.started = true + this.sndTime = this.startDate - snd.buffer.getTime() * 1000 + }else if(ms < 0 || ms >= 0 && this.started){ + var currentDate = Date.now() + if(this.gameLagSync){ + var sndTime = currentDate - snd.buffer.getTime() * 1000 + var lag = sndTime - this.sndTime + if(Math.abs(lag) >= 50){ + this.startDate += lag + this.sndTime = sndTime + pageEvents.send("game-lag", lag) + } + } + this.elapsedTime = currentDate - this.startDate + } + } + getAccurateTime(){ + if(this.isPaused()){ + return this.elapsedTime + }else{ + return Date.now() - this.startDate + } + } + getCircles(){ + return this.songData.circles + } + updateCurrentCircle(){ + var circles = this.songData.circles + do{ + var circle = circles[++this.currentCircle] + }while(circle && (circle.branch && !circle.branch.active)) + } + getCurrentCircle(){ + return this.currentCircle + } + updateCombo(score){ + if(score !== 0){ + this.combo++ + }else{ + this.combo = 0 + } + if(this.combo > this.globalScore.maxCombo){ + this.globalScore.maxCombo = this.combo + } + if(this.combo === 50 || this.combo > 0 && this.combo % 100 === 0 && this.combo <= 5000){ + this.controller.playSound("v_combo_" + this.combo) + } + if (this.songData.scoremode == 2 && this.combo > 0 && this.combo % 100 == 0) { + this.globalScore.points += 10000; + } + this.view.updateCombo(this.combo) + } + getCombo(){ + return this.combo + } + getGlobalScore(){ + return this.globalScore + } + updateGlobalScore(score, multiplier, gogoTime){ + // Circle score + switch(score){ + case 450: + this.globalScore.good++ + this.globalScore.gauge += this.soulPoints.good + break + case 230: + this.globalScore.ok++ + this.globalScore.gauge += this.soulPoints.ok + break + case 0: + this.globalScore.bad++ + this.globalScore.gauge += this.soulPoints.bad + break + } + if (this.songData.scoremode) { + switch (score) { + case 450: + score = this.songData.scoreinit; + break; + case 230: + score = Math.floor(this.songData.scoreinit / 2); + break; + } + } + // Gauge update + if(this.globalScore.gauge < 0){ + this.globalScore.gauge = 0 + }else if(this.globalScore.gauge > 10000){ + this.globalScore.gauge = 10000 + } + // Points update + if (this.songData.scoremode == 2) { + var diff_mul = 0; + if (this.combo >= 100) { + diff_mul = 8; + } else if (this.combo >= 50) { + diff_mul = 4; + } else if (this.combo >= 30) { + diff_mul = 2; + } else if (this.combo >= 10) { + diff_mul = 1; + } + score += this.songData.scorediff * diff_mul; + } else { + score += Math.max(0, Math.floor((Math.min(this.combo, 100) - 1) / 10) * (this.songData.scoremode ? this.songData.scorediff : 100)); + } + + if(gogoTime){ + multiplier *= 1.2 + } + this.globalScore.points += Math.floor(score * multiplier / 10) * 10 + } + setBranch(currentBranch, activeName){ + var pastActive = currentBranch.active + var ms = currentBranch.ms + for(var i = 0; i < this.songData.branches.length; i++){ + var branch = this.songData.branches[i] + if(branch.ms >= ms){ + var relevantName = activeName + var req = branch.requirement + var noNormal = req.advanced <= 0 + var noAdvanced = req.master <= 0 || req.advanced >= req.master || branch.type === "accuracy" && req.advanced > 100 + var noMaster = branch.type === "accuracy" && req.master > 100 + if(relevantName === "normal" && noNormal){ + relevantName = noAdvanced ? "master" : "advanced" + } + if(relevantName === "advanced" && noAdvanced){ + relevantName = noMaster ? "normal" : "master" + } + if(relevantName === "master" && noMaster){ + relevantName = noAdvanced ? "normal" : "advanced" + } + for(var j in this.branchNames){ + var name = this.branchNames[j] + if(name in branch){ + branch[name].active = name === relevantName + } + } + if(branch === currentBranch){ + activeName = relevantName + } + branch.active = relevantName + } + } + var circles = this.songData.circles + var circle = circles[this.currentCircle] + if(!circle || circle.branch === currentBranch[pastActive]){ + var ms = this.elapsedTime + var closestCircle = circles.findIndex(circle => { + return (!circle.branch || circle.branch.active) && circle.endTime + this.controller.audioLatency >= ms + }) + if(closestCircle !== -1){ + this.currentCircle = closestCircle + } + } + if(this.controller.multiplayer === 1){ + p2.send("branch", activeName) + } + } + resetSection(){ + this.sectionNotes = [] + this.sectionDrumroll = 0 + } + clearKeyTime(){ + var keyboard = this.controller.keyboard + for(var key in keyboard.keyTime){ + keyboard.keys[key] = null + keyboard.keyTime[key] = -Infinity + } + } + calibration(){ + var view = this.controller.view + if(!this.calibrationState){ + this.controller.parsedSongData.measures = [] + this.calibrationProgress = { + audio: 0, + video: 0, + requirement: 40 + } + this.calibrationReset("audio", true) + } + var progress = this.calibrationProgress + var state = this.calibrationState + switch(state){ + case "audio": + case "video": + if(state === "audio" && !this.mainAsset){ + this.mainAsset = assets.sounds["se_calibration"] + this.mainMusicPlaying = false + } + if(progress.hit >= progress.requirement){ + var reduced = 0 + for(var i = 2; i < progress.offsets.length; i++){ + reduced += progress.offsets[i] + } + progress[state] = Math.max(0, Math.round(reduced / progress.offsets.length - 2)) + this.calibrationState += "Complete" + view.pauseOptions = [] + this.clearKeyTime() + this.togglePause(true, 1) + this.mainAsset = null + } + break + case "audioComplete": + case "videoComplete": + if(Date.now() - this.latestDate > 3000){ + var audioComplete = this.calibrationState === "audioComplete" + this.controller.playSound("se_pause", 0, true) + if(audioComplete){ + this.calibrationReset("video") + }else{ + view.pauseOptions = [ + strings.calibration.retryPrevious, + strings.calibration.finish + ] + } + this.calibrationState = audioComplete ? "videoHelp" : "results" + } + break + } + } + calibrationHit(ms){ + var progress = this.calibrationProgress + var beatInterval = this.controller.view.beatInterval + var current = Math.floor((ms + 100) / beatInterval) + if(current !== progress.last){ + var offset = ((ms + 100) % beatInterval) - 100 + var offsets = progress.offsets + if(offsets.length >= progress.requirement){ + offsets.shift() + } + offsets.push(offset) + progress.hit++ + progress.last = current + this.globalScore.gauge = 10000 / (progress.requirement / progress.hit) + } + } + calibrationReset(to, togglePause){ + var view = this.controller.view + this.songData.circles = [] + view.pauseOptions = [ + to === "audio" ? strings.calibration.back : strings.calibration.retryPrevious, + strings.calibration.start + ] + this.calibrationState = to + "Help" + var progress = this.calibrationProgress + progress.offsets = [] + progress.hit = 0 + progress.last = null + this.globalScore.gauge = 0 + if(to === "video"){ + this.clearKeyTime() + this.initTiming() + this.latestDate = this.startDate + this.elapsedTime = 0 + view.ms = 0 + } + if(togglePause){ + this.togglePause(true, 1, true) + }else{ + view.pauseMove(1, true) + } + } +} diff --git a/public/src/js/gameinput.js b/public/src/js/gameinput.js new file mode 100644 index 0000000..e31502c --- /dev/null +++ b/public/src/js/gameinput.js @@ -0,0 +1,253 @@ +class GameInput{ + constructor(...args){ + this.init(...args) + } + init(controller){ + this.controller = controller + this.game = this.controller.game + + this.keyboard = new Keyboard({ + ka_l: ["ka_l"], + don_l: ["don_l"], + don_r: ["don_r"], + ka_r: ["ka_r"], + pause: ["q", "esc"], + back: ["backspace"], + previous: ["left", "up"], + next: ["right", "down"], + confirm: ["enter", "space"] + }, this.keyPress.bind(this)) + this.keys = {} + this.waitKeyupScore = {} + this.waitKeyupSound = {} + this.waitKeyupMenu = {} + this.keyTime = { + "don": -Infinity, + "ka": -Infinity + } + this.keyboardEvents = 0 + + var layout = settings.getItem("gamepadLayout") + if(layout === "b"){ + var gameBtn = { + don_l: ["d", "r", "ls"], + don_r: ["a", "x", "rs"], + ka_l: ["u", "l", "lb", "lt"], + ka_r: ["b", "y", "rb", "rt"] + } + }else if(layout === "c"){ + var gameBtn = { + don_l: ["d", "l", "ls"], + don_r: ["a", "b", "rs"], + ka_l: ["u", "r", "lb", "lt"], + ka_r: ["x", "y", "rb", "rt"] + } + }else{ + var gameBtn = { + don_l: ["u", "d", "l", "r", "ls"], + don_r: ["a", "b", "x", "y", "rs"], + ka_l: ["lb", "lt"], + ka_r: ["rb", "rt"] + } + } + this.gamepad = new Gamepad(gameBtn) + this.gamepadInterval = setInterval(this.gamepadKeys.bind(this), 1000 / 60 / 2) + + this.gamepadMenu = new Gamepad({ + cancel: ["a"], + confirm: ["b", "ls", "rs"], + previous: ["u", "l", "lb", "lt", "lsu", "lsl"], + next: ["d", "r", "rb", "rt", "lsd", "lsr"], + pause: ["start"] + }) + + if(controller.multiplayer === 1){ + pageEvents.add(window, "beforeunload", event => { + if(p2.otherConnected){ + pageEvents.send("p2-abandoned", event) + } + }) + } + } + keyPress(pressed, name){ + if(!this.controller.autoPlayEnabled || this.game.isPaused() || name !== "don_l" && name !== "don_r" && name !== "ka_l" && name !== "ka_r"){ + this.setKey(pressed, name, this.game.getAccurateTime()) + } + } + checkGameKeys(){ + if(this.controller.autoPlayEnabled){ + this.checkKeySound("don_l", "don") + this.checkKeySound("don_r", "don") + this.checkKeySound("ka_l", "ka") + this.checkKeySound("ka_r", "ka") + } + } + gamepadKeys(){ + if(!this.game.isPaused() && !this.controller.autoPlayEnabled){ + this.gamepad.play((pressed, name) => { + if(pressed){ + if(this.keys[name]){ + this.setKey(false, name) + } + this.setKey(true, name, this.game.getAccurateTime()) + }else{ + this.setKey(false, name) + } + }) + } + } + checkMenuKeys(){ + if(!this.controller.multiplayer && !this.locked && this.controller.view.pauseOptions.length !== 0){ + var moveMenu = 0 + var ms = this.game.getAccurateTime() + this.gamepadMenu.play((pressed, name) => { + if(pressed){ + if(this.game.isPaused()){ + if(name === "cancel"){ + this.locked = true + return setTimeout(() => { + this.controller.togglePause() + this.locked = false + }, 200) + } + } + if(this.keys[name]){ + this.setKey(false, name) + } + this.setKey(true, name, ms) + }else{ + this.setKey(false, name) + } + }) + this.checkKey("pause", "menu", () => { + this.controller.togglePause() + for(var key in this.keyTime){ + this.keys[key] = null + this.keyTime[key] = -Infinity + } + }) + var moveMenuMinus = () => { + moveMenu = -1 + } + var moveMenuPlus = () => { + moveMenu = 1 + } + var moveMenuConfirm = () => { + if(this.game.isPaused()){ + this.locked = true + setTimeout(() => { + this.controller.view.pauseConfirm() + this.locked = false + }, 200) + } + } + this.checkKey("previous", "menu", moveMenuMinus) + this.checkKey("ka_l", "menu", moveMenuMinus) + this.checkKey("next", "menu", moveMenuPlus) + this.checkKey("ka_r", "menu", moveMenuPlus) + this.checkKey("confirm", "menu", moveMenuConfirm) + this.checkKey("don_l", "menu", moveMenuConfirm) + this.checkKey("don_r", "menu", moveMenuConfirm) + if(moveMenu && this.game.isPaused()){ + this.controller.playSound("se_ka", 0, true) + this.controller.view.pauseMove(moveMenu) + } + } + if(this.controller.multiplayer !== 2){ + this.checkKey("back", "menu", () => { + if(this.controller.multiplayer === 1 && p2.otherConnected){ + p2.send("gameend") + pageEvents.send("p2-abandoned") + } + this.controller.togglePause() + this.controller.songSelection() + }) + } + } + checkKey(name, type, callback){ + if(this.keys[name] && !this.isWaiting(name, type)){ + this.waitForKeyup(name, type) + callback() + } + } + checkKeySound(name, sound){ + this.checkKey(name, "sound", () => { + var circles = this.controller.getCircles() + var circle = circles[this.controller.getCurrentCircle()] + var currentTime = this.keyTime[name] + this.keyTime[sound] = currentTime + if(circle && !circle.isPlayed){ + if(circle.type === "balloon"){ + if(sound === "don" && circle.requiredHits - circle.timesHit <= 1){ + this.controller.playSound("se_balloon") + return + } + } + } + this.controller.playSound("neiro_1_" + sound) + }) + } + getKeys(){ + return this.keys + } + setKey(pressed, name, ms){ + if(pressed){ + this.keys[name] = true + this.waitKeyupScore[name] = false + this.waitKeyupSound[name] = false + this.waitKeyupMenu[name] = false + if(this.game.isPaused()){ + return + } + this.keyTime[name] = ms + var calibrationState = this.game.calibrationState + var calibration = calibrationState && !this.game.paused + if(name == "don_l" || name == "don_r"){ + if(calibration){ + this.game.calibrationHit(ms) + }else{ + this.checkKeySound(name, "don") + } + this.keyboardEvents++ + }else if(name == "ka_l" || name == "ka_r"){ + if(!calibration){ + this.checkKeySound(name, "ka") + } + this.keyboardEvents++ + } + } + } + isWaiting(name, type){ + if(type === "score"){ + return this.waitKeyupScore[name] + }else if(type === "sound"){ + return this.waitKeyupSound[name] + }else if(type === "menu"){ + return this.waitKeyupMenu[name] + } + } + waitForKeyup(name, type){ + if(!this.keys[name]){ + return + } + if(type === "score"){ + this.waitKeyupScore[name] = true + }else if(type === "sound"){ + this.waitKeyupSound[name] = true + }else if(type === "menu"){ + this.waitKeyupMenu[name] = true + } + } + getKeyTime(){ + return this.keyTime + } + clean(){ + this.keyboard.clean() + this.gamepad.clean() + this.gamepadMenu.clean() + clearInterval(this.gamepadInterval) + if(this.controller.multiplayer === 1){ + pageEvents.remove(window, "beforeunload") + } + } +} diff --git a/public/src/js/gamepad.js b/public/src/js/gamepad.js new file mode 100644 index 0000000..2bcd3f2 --- /dev/null +++ b/public/src/js/gamepad.js @@ -0,0 +1,153 @@ +class Gamepad{ + constructor(...args){ + this.init(...args) + } + init(bindings, callback){ + this.bindings = bindings + this.callback = !!callback + this.b = { + "a": 0, + "b": 1, + "x": 2, + "y": 3, + "lb": 4, + "rb": 5, + "lt": 6, + "rt": 7, + "back": 8, + "start": 9, + "ls": 10, + "rs": 11, + "u": 12, + "d": 13, + "l": 14, + "r": 15, + "guide": 16, + "lsu": "lsu", + "lsr": "lsr", + "lsd": "lsd", + "lsl": "lsl" + } + this.btn = {} + this.gamepadEvents = 0 + if(callback){ + this.interval = setInterval(() => { + this.play(callback) + }, 1000 / 60) + } + } + play(callback){ + if("getGamepads" in navigator){ + var gamepads = navigator.getGamepads() + if(gamepads.length === 0){ + return + } + }else{ + return + } + if(pageEvents.lastKeyEvent + 5000 > Date.now()){ + return + } + + var bindings = this.bindings + var force = { + lsu: false, + lsr: false, + lsd: false, + lsl: false + } + + for(var i = 0; i < gamepads.length; i++){ + if(gamepads[i]){ + var axes = gamepads[i].axes + if(axes.length >= 2){ + force.lsl = force.lsl || axes[0] <= -0.5 + force.lsr = force.lsr || axes[0] >= 0.5 + force.lsu = force.lsu || axes[1] <= -0.5 + force.lsd = force.lsd || axes[1] >= 0.5 + } + if(axes.length >= 10){ + // TaTaCon left D-Pad, DualSense D-Pad + for(var pov = 0; pov < 8; pov++){ + if(Math.abs(axes[9] - (pov / 3.5 - 1)) <= 0.01){ + force.u = force.u || pov === 7 || pov === 0 || pov === 1 + force.r = force.r || pov === 1 || pov === 2 || pov === 3 + force.d = force.d || pov === 3 || pov === 4 || pov === 5 + force.l = force.l || pov === 5 || pov === 6 || pov === 7 + break + } + } + } + } + } + + for(var i = 0; i < gamepads.length; i++){ + if(gamepads[i]){ + this.toRelease = {} + for(var bind in bindings){ + this.toRelease[bind] = bindings[bind].length + } + for(var bind in bindings){ + for(var name in bindings[bind]){ + var bindName = bindings[bind][name] + this.checkButton(gamepads, this.b[bindName], bind, callback, force[bindName]) + if(!this.b){ + return + } + } + } + break + } + } + } + checkButton(gamepads, btnName, keyCode, callback, force){ + var button = false + + if(typeof force === "undefined"){ + for(var i = 0; i < gamepads.length; i++){ + if(gamepads[i]){ + var btn = gamepads[i].buttons[btnName] + if(btn){ + var btnPressed = btn.pressed || btn.value >= 0.5 + if(btnPressed){ + button = btnPressed + } + } + } + } + + var pressed = !this.btn[btnName] && button + var released = this.btn[btnName] && !button + }else{ + var pressed = !this.btn[btnName] && force + var released = this.btn[btnName] && !force + } + + if(pressed){ + this.btn[btnName] = true + }else if(released){ + this.btn[btnName] = false + } + + if(pressed){ + callback(true, keyCode) + this.gamepadEvents++ + }else if(!button){ + if(released){ + this.toRelease[keyCode + "released"] = true + } + this.toRelease[keyCode]-- + if(this.toRelease[keyCode] === 0 && this.toRelease[keyCode + "released"]){ + callback(false, keyCode) + } + } + } + clean(){ + if(this.callback){ + clearInterval(this.interval) + } + delete this.bindings + delete this.b + delete this.btn + } +} diff --git a/public/src/js/gamerules.js b/public/src/js/gamerules.js new file mode 100644 index 0000000..79654f0 --- /dev/null +++ b/public/src/js/gamerules.js @@ -0,0 +1,77 @@ +class GameRules{ + constructor(...args){ + this.init(...args) + } + init(game){ + this.difficulty = game.controller.selectedSong.difficulty + var frame = 1000 / 60 + + switch(this.difficulty){ + case "easy": + case "normal": + this.good = 5 / 2 * frame + this.ok = 13 / 2 * frame + this.bad = 15 / 2 * frame + break + case "hard": + case "oni": + case "ura": + default: + this.good = 3 / 2 * frame + this.ok = 9 / 2 * frame + this.bad = 13 / 2 * frame + break + } + switch(this.difficulty){ + case "easy": + this.gaugeClear = 30 / 50 + break + case "normal": + case "hard": + this.gaugeClear = 35 / 50 + break + case "oni": + case "ura": + this.gaugeClear = 40 / 50 + break + default: + this.gaugeClear = 51 / 50 + break + } + + this.daiLeniency = 2 * frame + } + soulPoints(combo){ + var good, ok, bad + switch(this.difficulty){ + case "easy": + good = Math.floor(10000 / combo * 1.575) + ok = Math.floor(good * 0.75) + bad = Math.ceil(good / -2) + break + case "normal": + good = Math.floor(10000 / combo / 0.7) + ok = Math.floor(good * 0.75) + bad = Math.ceil(good / -0.75) + break + case "hard": + good = Math.floor(10000 / combo * 1.5) + ok = Math.floor(good * 0.75) + bad = Math.ceil(good / -0.8) + break + case "oni": + case "ura": + good = Math.floor(10000 / combo / 0.7) + ok = Math.floor(good * 0.5) + bad = Math.ceil(good * -1.6) + break + } + return {good: good, ok: ok, bad: bad} + } + gaugePercent(gauge){ + return Math.floor(gauge / 200) / 50 + } + clearReached(gauge){ + return this.gaugePercent(gauge) >= this.gaugeClear + } +} diff --git a/public/src/js/gpicker.js b/public/src/js/gpicker.js new file mode 100644 index 0000000..869ef31 --- /dev/null +++ b/public/src/js/gpicker.js @@ -0,0 +1,285 @@ +class Gpicker{ + constructor(...args){ + this.init(...args) + } + init(){ + this.apiKey = gameConfig.google_credentials.api_key + this.oauthClientId = gameConfig.google_credentials.oauth_client_id + this.projectNumber = gameConfig.google_credentials.project_number + this.scope = "https://www.googleapis.com/auth/drive.readonly" + this.folder = "application/vnd.google-apps.folder" + this.filesUrl = "https://www.googleapis.com/drive/v3/files/" + this.resolveQueue = [] + this.queueActive = false + this.clientCallbackBind = this.clientCallback.bind(this) + } + browse(lockedCallback, errorCallback){ + return this.loadApi(lockedCallback, errorCallback) + .then(() => this.getToken(lockedCallback, errorCallback)) + .then(() => new Promise((resolve, reject) => { + this.displayPicker(data => { + if(data.action === "picked"){ + var file = data.docs[0] + var folders = [] + var rateLimit = -1 + var lastBatch = 0 + var walk = (files, output=[]) => { + for(var i = 0; i < files.length; i++){ + var path = files[i].path ? files[i].path + "/" : "" + var list = files[i].list + if(!list){ + continue + } + for(var j = 0; j < list.length; j++){ + var file = list[j] + if(file.mimeType === this.folder){ + folders.push({ + path: path + file.name, + id: file.id + }) + }else{ + output.push(new GdriveFile({ + path: path + file.name, + name: file.name, + id: file.id + })) + } + } + } + var batchList = [] + for(var i = 0; i < folders.length && batchList.length < 100; i++){ + if(!folders[i].listed){ + folders[i].pos = i + folders[i].listed = true + batchList.push(folders[i]) + } + } + if(batchList.length){ + var batch = gapi.client.newBatch() + batchList.forEach(folder => { + var req = { + q: "'" + folder.id + "' in parents and trashed = false", + orderBy: "name_natural" + } + if(folder.pageToken){ + req.pageToken = folder.pageToken + } + batch.add(gapi.client.drive.files.list(req), {id: folder.pos}) + }) + if(lastBatch + batchList.length > 100){ + var waitPromise = this.sleep(1000) + }else{ + var waitPromise = Promise.resolve() + } + return waitPromise.then(() => this.queue()).then(() => batch.then(responses => { + var files = [] + var rateLimited = false + for(var i in responses.result){ + var result = responses.result[i].result + if(result.error){ + if(result.error.errors[0].domain !== "usageLimits"){ + console.warn(result) + }else if(!rateLimited){ + rateLimited = true + rateLimit++ + folders.push({ + path: folders[i].path, + id: folders[i].id, + pageToken: folders[i].pageToken + }) + } + }else{ + if(result.nextPageToken){ + folders.push({ + path: folders[i].path, + id: folders[i].id, + pageToken: result.nextPageToken + }) + } + files.push({path: folders[i].path, list: result.files}) + } + } + if(rateLimited){ + return this.sleep(Math.pow(2, rateLimit) * 1000).then(() => walk(files, output)) + }else{ + return walk(files, output) + } + })) + }else{ + return output + } + } + if(file.mimeType === this.folder){ + return walk([{list: [file]}]).then(resolve, reject) + }else{ + return reject("cancel") + } + }else if(data.action === "cancel"){ + return reject("cancel") + } + }) + })) + } + loadApi(lockedCallback=()=>{}, errorCallback=()=>{}){ + if(window.gapi && gapi.client && gapi.client.drive){ + return Promise.resolve() + } + var promises = [ + loader.loadScript("https://apis.google.com/js/api.js"), + loader.loadScript("https://accounts.google.com/gsi/client") + ] + var apiLoaded = false + return Promise.all(promises).then(() => new Promise((resolve, reject) => + gapi.load("picker:client", { + callback: resolve, + onerror: reject + }) + )) + .then(() => new Promise((resolve, reject) => { + setTimeout(() => { + if(!apiLoaded){ + lockedCallback(false) + } + }, 3000) + return gapi.client.load("drive", "v3").then(resolve, reject) + })).then(() => { + apiLoaded = true + lockedCallback(true) + }).catch(e => { + errorCallback(Array.isArray(e) ? e[0] : e) + return Promise.reject("cancel") + }) + } + getClient(errorCallback=()=>{}, force){ + var obj = { + client_id: this.oauthClientId, + scope: this.scope, + callback: this.clientCallbackBind + } + if(force){ + if(!this.clientForce){ + obj.select_account = true + this.clientForce = google.accounts.oauth2.initTokenClient(obj) + } + return this.clientForce + }else{ + if(!this.client){ + this.client = google.accounts.oauth2.initTokenClient(obj) + } + return this.client + } + } + clientCallback(tokenResponse){ + this.tokenResponse = tokenResponse + this.oauthToken = tokenResponse && tokenResponse.access_token + if(this.oauthToken && this.tokenResolve){ + this.tokenResolve() + } + } + getToken(lockedCallback=()=>{}, errorCallback=()=>{}, force){ + if(this.oauthToken && !force){ + return Promise.resolve() + } + var client = this.getClient(errorCallback, force) + var promise = new Promise(resolve => { + this.tokenResolve = resolve + }) + lockedCallback(false) + client.requestAccessToken() + return promise.then(() => { + this.tokenResolve = null + if(this.checkScope()){ + lockedCallback(true) + }else{ + return Promise.reject("cancel") + } + }) + } + checkScope(){ + return google.accounts.oauth2.hasGrantedAnyScope(this.tokenResponse, this.scope) + } + switchAccounts(lockedCallback, errorCallback){ + return this.loadApi().then(() => this.getToken(lockedCallback, errorCallback, true)) + } + displayPicker(callback){ + var picker = gapi.picker.api + new picker.PickerBuilder() + .setDeveloperKey(this.apiKey) + .setAppId(this.projectNumber) + .setOAuthToken(this.oauthToken) + .setLocale(strings.gpicker.locale) + .hideTitleBar() + .addView(new picker.DocsView("folders") + .setLabel(strings.gpicker.myDrive) + .setParent("root") + .setSelectFolderEnabled(true) + .setMode("grid") + ) + .addView(new picker.DocsView("folders") + .setLabel(strings.gpicker.starred) + .setStarred(true) + .setSelectFolderEnabled(true) + .setMode("grid") + ) + .addView(new picker.DocsView("folders") + .setLabel(strings.gpicker.sharedWithMe) + .setOwnedByMe(false) + .setSelectFolderEnabled(true) + .setMode("list") + ) + .setCallback(callback) + .setSize(Infinity, Infinity) + .build() + .setVisible(true) + } + downloadFile(id, responseType, retry){ + var url = this.filesUrl + id + "?alt=media" + return this.queue().then(this.getToken.bind(this)).then(() => + loader.ajax(url, request => { + if(responseType){ + request.responseType = responseType + } + request.setRequestHeader("Authorization", "Bearer " + this.oauthToken) + }, true).then(event => { + var request = event.target + var reject = () => Promise.reject(`${url} (${request.status})`) + if(request.status === 200){ + return request.response + }else if(request.status === 401 && !retry){ + return new Response(request.response).json().then(response => { + var e = response.error + if(e && e.errors[0].reason === "authError"){ + delete this.oauthToken + return this.downloadFile(id, responseType, true) + }else{ + return reject() + } + }, reject) + } + return reject() + }) + ) + } + sleep(time){ + return new Promise(resolve => setTimeout(resolve, time)) + } + queue(){ + return new Promise(resolve => { + this.resolveQueue.push(resolve) + if(!this.queueActive){ + this.queueActive = true + this.queueTimer = setInterval(this.parseQueue.bind(this), 100) + this.parseQueue() + } + }) + } + parseQueue(){ + if(this.resolveQueue.length){ + var resolve = this.resolveQueue.shift() + resolve() + }else{ + this.queueActive = false + clearInterval(this.queueTimer) + } + } +} diff --git a/public/src/js/idb.js b/public/src/js/idb.js new file mode 100644 index 0000000..a8b50db --- /dev/null +++ b/public/src/js/idb.js @@ -0,0 +1,54 @@ +class IDB{ + constructor(...args){ + this.init(...args) + } + init(name, store){ + this.name = name + this.store = store + } + start(){ + if(this.db){ + return Promise.resolve(this.db) + } + var request = indexedDB.open(this.name) + request.onupgradeneeded = event => { + var db = event.target.result + db.createObjectStore(this.store) + } + return this.promise(request).then(result => { + this.db = result + return this.db + }, target => + console.warn("DB error", target) + ) + } + promise(request){ + return new Promise((resolve, reject) => { + return pageEvents.race(request, "success", "error").then(response => { + if(response.type === "success"){ + return resolve(event.target.result) + }else{ + return reject(event.target) + } + }) + }) + } + transaction(method, ...args){ + return this.start().then(db => + db.transaction(this.store, "readwrite").objectStore(this.store)[method](...args) + ).then(this.promise.bind(this)) + } + getItem(name){ + return this.transaction("get", name) + } + setItem(name, value){ + return this.transaction("put", value, name) + } + removeItem(name){ + return this.transaction("delete", name) + } + removeDB(){ + delete this.db + return indexedDB.deleteDatabase(this.name) + } +} diff --git a/public/src/js/importsongs.js b/public/src/js/importsongs.js new file mode 100644 index 0000000..7ebede4 --- /dev/null +++ b/public/src/js/importsongs.js @@ -0,0 +1,658 @@ +class ImportSongs{ + constructor(...args){ + this.init(...args) + } + init(limited, otherFiles, noPlugins, pluginAmount){ + this.limited = limited + this.tjaFiles = [] + this.osuFiles = [] + this.assetFiles = {} + this.pluginFiles = [] + this.otherFiles = otherFiles || {} + this.noPlugins = noPlugins + this.pluginAmount = pluginAmount + this.songs = [] + this.stylesheet = [] + this.plugins = [] + this.songTitle = this.otherFiles.songTitle || {} + this.uraRegex = /\s*[\((]裏[\))]$/ + this.courseTypes = { + "easy": 0, + "normal": 1, + "hard": 2, + "oni": 3, + "ura": 4 + } + this.categoryAliases = {} + assets.categories.forEach(cat => { + this.categoryAliases[cat.title.toLowerCase()] = cat.id + if(cat.aliases){ + cat.aliases.forEach(alias => { + this.categoryAliases[alias.toLowerCase()] = cat.id + }) + } + if(cat.title_lang){ + for(var i in cat.title_lang){ + this.categoryAliases[cat.title_lang[i].toLowerCase()] = cat.id + } + } + }) + this.assetSelectors = {} + for(var selector in assets.cssBackground){ + var filename = assets.cssBackground[selector] + var index = filename.lastIndexOf(".") + if(index !== -1){ + filename = filename.slice(0, index) + } + this.assetSelectors[filename] = selector + } + this.comboVoices = ["v_combo_50"].concat(Array.from(Array(50), (d, i) => "v_combo_" + ((i + 1) * 100))) + } + load(files){ + var extensionRegex = /\.[^\/]+$/ + files.sort((a, b) => { + var path1 = a.path.replace(extensionRegex, "") + var path2 = b.path.replace(extensionRegex, "") + return path1 > path2 ? 1 : -1 + }) + + var metaFiles = [] + for(var i = 0; i < files.length; i++){ + var file = files[i] + var name = file.name.toLowerCase() + var path = file.path.toLowerCase() + if(name.endsWith(".tja") || name.endsWith(".tjf")){ + this.tjaFiles.push({ + file: file, + index: i + }) + }else if(name.endsWith(".osu")){ + this.osuFiles.push({ + file: file, + index: i + }) + }else if(!this.limited && (name === "genre.ini" || name === "box.def") || name === "songtitle.txt"){ + var level = (file.path.match(/\//g) || []).length + metaFiles.push({ + file: file, + level: (level * 2) + (name === "genre.ini" ? 1 : 0) + }) + }else if(!this.limited && (path.indexOf("/taiko-web assets/") !== -1 || path.indexOf("taiko-web assets/") === 0)){ + if(!(name in this.assetFiles)){ + this.assetFiles[name] = file + } + }else if(name.endsWith(".taikoweb.js")){ + this.pluginFiles.push({ + file: file, + index: i + }) + }else{ + this.otherFiles[path] = file + } + } + + if(!this.noPlugins && this.pluginFiles.length){ + var pluginPromises = [] + this.pluginFiles.forEach(fileObj => { + pluginPromises.push(this.addPlugin(fileObj).catch(e => console.warn(e))) + }) + return Promise.all(pluginPromises).then(() => { + var startPromises = [] + var pluginAmount = 0 + if(this.plugins.length && confirm(strings.plugins.warning.replace("%s", + strings.plugins.plugin[strings.plural.select(this.plugins.length)].replace("%s", + this.plugins.length.toString() + ) + ))){ + this.plugins.forEach(obj => { + var plugin = plugins.add(obj.data, { + name: obj.name, + raw: true + }) + if(plugin){ + pluginAmount++ + plugin.imported = true + startPromises.push(plugin.start()) + } + }) + } + return Promise.all(startPromises).then(() => { + var importSongs = new ImportSongs(this.limited, this.otherFiles, true, pluginAmount) + return importSongs.load(files) + }) + }) + } + + var metaPromises = [] + metaFiles.forEach(fileObj => { + metaPromises.push(this.addMeta(fileObj)) + }) + + return Promise.all(metaPromises).then(() => { + var songPromises = [] + + this.tjaFiles.forEach(fileObj => { + songPromises.push(this.addTja(fileObj).catch(e => console.warn(e))) + }) + this.osuFiles.forEach(fileObj => { + songPromises.push(this.addOsu(fileObj).catch(e => console.warn(e))) + }) + songPromises.push(this.addAssets()) + return Promise.all(songPromises) + }).then(this.loaded.bind(this)) + } + + addMeta(fileObj){ + var file = fileObj.file + var level = fileObj.level + var name = file.name.toLowerCase() + return file.read(name === "songtitle.txt" ? undefined : "utf-8").then(data => { + var data = data.replace(/\0/g, "").split("\n") + var category + if(name === "genre.ini"){ + var key + for(var i = 0; i < data.length; i++){ + var line = data[i].trim().toLowerCase() + if(line.startsWith("[") && line.endsWith("]")){ + key = line.slice(1, -1) + }else if(key === "genre"){ + var equalsPos = line.indexOf("=") + if(equalsPos !== -1 && line.slice(0, equalsPos).trim() === "genrename"){ + var value = line.slice(equalsPos + 1).trim() + if(value.toLowerCase() in this.categoryAliases){ + category = value + }else{ + category = data[i].trim().slice(equalsPos + 1).trim() + } + break + } + } + } + }else if(name === "box.def"){ + for(var i = 0; i < data.length; i++){ + var line = data[i].trim().toLowerCase() + if(line.startsWith("#title:")){ + var value = line.slice(7).trim() + if(value.toLowerCase() in this.categoryAliases){ + category = value + } + }else if(line.startsWith("#genre:")){ + var value = line.slice(7).trim() + if(value.toLowerCase() in this.categoryAliases){ + category = value + }else{ + category = data[i].trim().slice(7).trim() + } + break + } + } + }else if(name === "songtitle.txt"){ + var lastTitle + for(var i = 0; i < data.length; i++){ + var line = data[i].trim() + if(line){ + var lang = line.slice(0, 2) + if(line.charAt(2) !== " " || !(lang in allStrings)){ + this.songTitle[line] = {} + lastTitle = line + }else if(lastTitle){ + this.songTitle[lastTitle][lang] = line.slice(3).trim() + } + } + } + } + if(category){ + var metaPath = file.path.toLowerCase().slice(0, file.name.length * -1) + var filesLoop = fileObj => { + var tjaPath = fileObj.file.path.toLowerCase().slice(0, fileObj.file.name.length * -1) + if(tjaPath.startsWith(metaPath) && (!("categoryLevel" in fileObj) || fileObj.categoryLevel < level)){ + if(category.toLowerCase() in this.categoryAliases){ + fileObj.category_id = this.categoryAliases[category.toLowerCase()] + }else{ + fileObj.category = category + } + fileObj.categoryLevel = level + } + } + this.tjaFiles.forEach(filesLoop) + this.osuFiles.forEach(filesLoop) + } + }).catch(e => { + console.warn(e) + }) + } + + addTja(fileObj){ + var file = fileObj.file + var index = fileObj.index + var category = fileObj.category + var category_id = fileObj.category_id + if(!this.limited){ + var filePromise = file.read(prompt("太鼓さん次郎のファイルは\"sjis\"、TJAPlayer3のファイルは\"utf-8\"と入力してください。")) + }else{ + var filePromise = Promise.resolve() + } + return filePromise.then(dataRaw => { + var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : [] + var tja = new ParseTja(data, "oni", 0, 0, true) + var songObj = { + id: index + 1, + order: index + 1, + title: file.name.slice(0, file.name.lastIndexOf(".")), + type: "tja", + chart: file, + courses: {}, + music: "muted", + custom: true + } + if(this.limited){ + songObj.unloaded = true + } + var coursesAdded = false + var titleLang = {} + var titleLangAdded = false + var subtitleLangAdded = false + var subtitleLang = {} + var dir = file.path.toLowerCase() + dir = dir.slice(0, dir.lastIndexOf("/") + 1) + for(var diff in tja.metadata){ + var meta = tja.metadata[diff] + songObj.title = meta.title || file.name.slice(0, file.name.lastIndexOf(".")) + var subtitle = meta.subtitle || "" + if(subtitle.startsWith("--") || subtitle.startsWith("++")){ + subtitle = subtitle.slice(2).trim() + } + songObj.subtitle = subtitle + songObj.preview = meta.demostart || 0 + songObj.courses[diff] = { + stars: meta.level || 0, + branch: !!meta.branch + } + coursesAdded = true + if(meta.wave){ + songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music + } + if(meta.genre){ + if(meta.genre.toLowerCase() in this.categoryAliases){ + songObj.category_id = this.categoryAliases[meta.genre.toLowerCase()] + }else{ + songObj.category = meta.genre + } + } + if(meta.taikowebskin){ + songObj.song_skin = this.getSkin(dir, meta.taikowebskin) + } + if(meta.maker){ + var maker = meta.maker + var url = null + var gt = maker.lastIndexOf(">") + if(gt === maker.length - 1){ + var lt = maker.lastIndexOf("<") + if(lt !== -1 && lt !== gt - 2){ + url = maker.slice(lt + 1, gt).trim() + if(url.startsWith("http://") || url.startsWith("https://")){ + maker = maker.slice(0, lt) + }else{ + url = null + } + } + } + songObj.maker = { + name: maker, + url: url, + id: 1 + } + } + if(meta.lyrics){ + var lyricsFile = this.normPath(this.joinPath(dir, meta.lyrics)) + if(lyricsFile in this.otherFiles){ + songObj.lyrics = true + songObj.lyricsFile = this.otherFiles[lyricsFile] + } + }else if(meta.inlineLyrics){ + songObj.lyrics = true + } + for(var id in allStrings){ + var songTitle = songObj.title + var ura = "" + if(songTitle){ + var uraPos = songTitle.search(this.uraRegex) + if(uraPos !== -1){ + ura = songTitle.slice(uraPos) + songTitle = songTitle.slice(0, uraPos) + } + } + if(meta["title" + id]){ + titleLang[id] = meta["title" + id] + titleLangAdded = true + }else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){ + titleLang[id] = this.songTitle[songTitle][id] + ura + titleLangAdded = true + } + if(meta["subtitle" + id]){ + subtitleLang[id] = meta["subtitle" + id] + subtitleLangAdded = true + } + } + } + if(titleLangAdded){ + songObj.title_lang = titleLang + } + if(subtitleLangAdded){ + songObj.subtitle_lang = subtitleLang + } + if(!songObj.category_id && !songObj.category){ + if(!category && category_id === undefined){ + songObj.category_id = this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))]) + }else if(category){ + songObj.category = category + songObj.orginalCategory = category + }else{ + songObj.category_id = category_id + } + } + if(coursesAdded || songObj.unloaded){ + this.songs[index] = songObj + } + if(!this.limited){ + var hash = md5.base64(dataRaw).slice(0, -2) + songObj.hash = hash + scoreStorage.songTitles[songObj.title] = hash + var score = scoreStorage.get(hash, false, true) + if(score){ + score.title = songObj.title + } + } + }) + } + + addOsu(fileObj){ + var file = fileObj.file + var index = fileObj.index + var category = fileObj.category + var category_id = fileObj.category_id + if(!this.limited){ + var filePromise = file.read() + }else{ + var filePromise = Promise.resolve() + } + return filePromise.then(dataRaw => { + var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : [] + var osu = new ParseOsu(data, "oni", 0, 0, true) + var dir = file.path.toLowerCase() + dir = dir.slice(0, dir.lastIndexOf("/") + 1) + var songObj = { + id: index + 1, + order: index + 1, + type: "osu", + chart: file, + subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist, + subtitle_lang: { + en: osu.metadata.Artist || osu.metadata.ArtistUnicode + }, + preview: osu.generalInfo.PreviewTime ? osu.generalInfo.PreviewTime / 1000 : 0, + courses: {}, + music: (osu.generalInfo.AudioFilename ? this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] : "") || "muted" + } + if(!this.limited){ + songObj.courses.oni = { + stars: parseInt(osu.difficulty.overallDifficulty) || 0, + branch: false + } + }else{ + songObj.unloaded = true + } + var filename = file.name.slice(0, file.name.lastIndexOf(".")) + var title = osu.metadata.TitleUnicode || osu.metadata.Title || file.name.slice(0, file.name.lastIndexOf(".")) + if(title){ + var suffix = "" + var matches = filename.match(/\[.+?\]$/) + if(matches){ + suffix = " " + matches[0] + } + songObj.title = title + suffix + songObj.title_lang = { + en: (osu.metadata.Title || osu.metadata.TitleUnicode || title) + suffix + } + }else{ + songObj.title = filename + } + this.songs[index] = songObj + if(!category && category_id === undefined){ + songObj.category_id = this.getCategory(file, [osu.metadata.TitleUnicode, osu.metadata.Title, file.name.slice(0, file.name.lastIndexOf("."))]) + }else if(category){ + songObj.category = category + songObj.orginalCategory = category + }else{ + songObj.category_id = category_id + } + if(!this.limited){ + var hash = md5.base64(dataRaw).slice(0, -2) + songObj.hash = hash + scoreStorage.songTitles[songObj.title] = hash + var score = scoreStorage.get(hash, false, true) + if(score){ + score.title = songObj.title + } + } + }) + } + + addAssets(){ + var promises = [] + for(let name in this.assetFiles){ + let id = this.getFilename(name) + var file = this.assetFiles[name] + var index = name.lastIndexOf(".") + if(name === "vectors.json"){ + promises.push(file.read().then(response => { + vectors = JSON.parse(response) + })) + } + if(name.endsWith(".png") || name.endsWith(".gif")){ + let image = document.createElement("img") + promises.push(pageEvents.load(image).then(() => { + if(id in this.assetSelectors){ + var selector = this.assetSelectors[id] + var gradient = "" + if(selector === "#song-search"){ + gradient = loader.songSearchGradient + } + this.stylesheet.push(loader.cssRuleset({ + [selector]: { + "background-image": gradient + "url(\"" + image.src + "\")" + } + })) + } + })) + image.id = name + promises.push(file.blob().then(blob => { + image.src = URL.createObjectURL(blob) + })) + loader.assetsDiv.appendChild(image) + var oldImage = assets.image[id] + if(oldImage && oldImage.parentNode){ + URL.revokeObjectURL(oldImage.src) + oldImage.parentNode.removeChild(oldImage) + } + assets.image[id] = image + } + if(assets.audioSfx.indexOf(name) !== -1){ + assets.sounds[id].clean() + promises.push(this.loadSound(file, name, snd.sfxGain)) + } + if(assets.audioMusic.indexOf(name) !== -1){ + assets.sounds[id].clean() + promises.push(this.loadSound(file, name, snd.musicGain)) + } + if(assets.audioSfxLR.indexOf(name) !== -1){ + assets.sounds[id + "_p1"].clean() + assets.sounds[id + "_p2"].clean() + promises.push(this.loadSound(file, name, snd.sfxGain).then(sound => { + assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL) + assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR) + })) + } + if(assets.audioSfxLoud.indexOf(name) !== -1){ + assets.sounds[id].clean() + promises.push(this.loadSound(file, name, snd.sfxLoudGain)) + } + if(this.comboVoices.indexOf(id) !== -1){ + promises.push(snd.sfxGain.load(file).then(sound => { + assets.sounds[id] = sound + assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL) + assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR) + })) + } + } + return Promise.all(promises) + } + loadSound(file, name, gain){ + var id = this.getFilename(name) + return gain.load(file).then(sound => { + assets.sounds[id] = sound + }) + } + getFilename(name){ + return name.slice(0, name.lastIndexOf(".")) + } + + addPlugin(fileObj){ + var file = fileObj.file + var filePromise = file.read() + return filePromise.then(dataRaw => { + var name = file.name.slice(0, file.name.lastIndexOf(".taikoweb.js")) + this.plugins.push({ + name: name, + data: dataRaw + }) + }) + } + + getCategory(file, exclude){ + var path = file.path.toLowerCase().split("/") + for(var i = path.length - 2; i >= 0; i--){ + var hasTitle = false + for(var j in exclude){ + if(exclude[j] && path[i].indexOf(exclude[j].toLowerCase()) !== -1){ + hasTitle = true + break + } + } + if(!hasTitle){ + for(var cat in this.categoryAliases){ + if(path[i].indexOf(cat) !== -1){ + return this.categoryAliases[cat] + } + } + } + } + } + + getSkin(dir, config){ + var configArray = config.toLowerCase().split(",") + var configObj = {} + for(var i in configArray){ + var string = configArray[i].trim() + var space = string.indexOf(" ") + if(space !== -1){ + configObj[string.slice(0, space).trim()] = string.slice(space + 1).trim() + } + } + if(!configObj.dir){ + configObj.dir = "" + } + configObj.prefix = "custom " + var skinnable = ["song", "stage", "don"] + for(var i in skinnable){ + var skinName = skinnable[i] + var skinValue = configObj[skinName] + if(skinValue && skinValue !== "none"){ + var fileName = "bg_" + skinName + "_" + configObj.name + var skinPath = this.joinPath(dir, configObj.dir, fileName) + for(var j = 0; j < 2; j++){ + if(skinValue !== "static"){ + var suffix = (j === 0 ? "_a" : "_b") + ".png" + }else{ + var suffix = ".png" + } + var skinFull = this.normPath(skinPath + suffix) + if(skinFull in this.otherFiles){ + configObj[fileName + suffix] = this.otherFiles[skinFull] + }else{ + configObj[skinName] = null + } + if(skinValue === "static"){ + break + } + } + } + } + return configObj + } + + loaded(){ + this.songs = this.songs.filter(song => typeof song !== "undefined") + if(this.stylesheet.length){ + var style = document.createElement("style") + style.appendChild(document.createTextNode(this.stylesheet.join("\n"))) + document.head.appendChild(style) + } + if(this.songs.length){ + if(this.limited){ + assets.otherFiles = this.otherFiles + assets.otherFiles.songTitle = this.songTitle + } + return Promise.resolve(this.songs) + }else{ + if(this.noPlugins && this.pluginAmount || Object.keys(this.assetFiles).length){ + return Promise.resolve() + }else{ + return Promise.reject("nosongs") + } + } + this.clean() + } + + joinPath(){ + var resultPath = arguments[0] + for(var i = 1; i < arguments.length; i++){ + var pPath = arguments[i] + if(pPath && (pPath[0] === "/" || pPath[0] === "\\")){ + resultPath = pPath + }else{ + var lastChar = resultPath.slice(-1) + if(resultPath && (lastChar !== "/" || lastChar !== "\\")){ + resultPath = resultPath + "/" + } + resultPath = resultPath + pPath + } + } + return resultPath + } + normPath(path){ + path = path.replace(/\\/g, "/").toLowerCase() + while(path[0] === "/"){ + path = path.slice(1) + } + var comps = path.split("/") + for(var i = 0; i < comps.length; i++){ + if(comps[i] === "." || comps[i] === ""){ + comps.splice(i, 1) + i-- + }else if(i !== 0 && comps[i] === ".." && comps[i - 1] !== ".."){ + comps.splice(i - 1, 2) + i -= 2 + } + } + return comps.join("/") + } + + clean(){ + delete this.songs + delete this.tjaFiles + delete this.osuFiles + delete this.assetFiles + delete this.otherFiles + } +} diff --git a/public/src/js/keyboard.js b/public/src/js/keyboard.js new file mode 100644 index 0000000..d72641c --- /dev/null +++ b/public/src/js/keyboard.js @@ -0,0 +1,124 @@ +class Keyboard{ + constructor(...args){ + this.init(...args) + } + init(bindings, callback){ + this.bindings = bindings + this.callback = callback + this.wildcard = false + this.substitute = { + "up": "arrowup", + "right": "arrowright", + "down": "arrowdown", + "left": "arrowleft", + "space": " ", + "esc": "escape", + "ctrl": "control", + "altgr": "altgraph" + } + this.btn = {} + this.TaikoForceLv5 = false; + this.update() + pageEvents.keyAdd(this, "all", "both", this.keyEvent.bind(this)) + pageEvents.blurAdd(this, this.blurEvent.bind(this)) + } + isTaikoForceLv5(kbdSettings){ + // the key of Taiko Force Lv5's PC module looks like this + return (kbdSettings.ka_l[0] === "f") && (kbdSettings.ka_r[0] === "e") && (kbdSettings.don_l[0] === "i") && (kbdSettings.don_r[0] === "j"); + } + update(){ + var kbdSettings = settings.getItem("keyboardSettings") + this.TaikoForceLv5 = this.isTaikoForceLv5(kbdSettings) + var drumKeys = {} + for(var name in kbdSettings){ + var keys = kbdSettings[name] + for(var i in keys){ + drumKeys[keys[i]] = name + } + } + this.kbd = {} + for(var name in this.bindings){ + var keys = this.bindings[name] + for(var i in keys){ + var key = keys[i] + if(key in drumKeys){ + continue + } + if(key in kbdSettings){ + var keyArray = kbdSettings[key] + for(var j in keyArray){ + key = keyArray[j] + if(!(key in this.kbd)){ + this.kbd[key] = name + } + } + }else{ + if(key in this.substitute){ + key = this.substitute[key] + } + if(!(key in this.kbd)){ + if(key === "wildcard"){ + this.wildcard = true + } + this.kbd[key] = name + } + } + } + } + } + keyEvent(event){ + var key = event.key.toLowerCase() + if(event.target.tagName === "INPUT"){ + if(key === "escape"){ + event.preventDefault() + } + }else if( + key === "escape" || + key === "backspace" || + key === "tab" || + key === "contextmenu" || + key === "alt" + ){ + event.preventDefault() + } + if(!event.repeat){ + var pressed = event.type === "keydown" + if(pressed){ + this.btn[key] = true + }else{ + delete this.btn[key] + if(key in this.kbd){ + for(var i in this.btn){ + if(this.kbd[i] === this.kbd[key]){ + return + } + } + } + } + if(key in this.kbd){ + this.callback(pressed, this.kbd[key], event) + }else if(this.wildcard){ + this.callback(pressed, this.kbd["wildcard"], event) + } + } + } + blurEvent(){ + for(var key in this.btn){ + if(this.btn[key]){ + delete this.btn[key] + var name = this.kbd[key] || (this.wildcard ? "wildcard" : false) + if(name){ + this.callback(false, name) + } + } + } + } + clean(){ + pageEvents.keyRemove(this, "all") + pageEvents.blurRemove(this) + delete this.bindings + delete this.callback + delete this.kbd + delete this.btn + } +} diff --git a/public/src/js/lib/fuzzysort.js b/public/src/js/lib/fuzzysort.js new file mode 100644 index 0000000..457aa28 --- /dev/null +++ b/public/src/js/lib/fuzzysort.js @@ -0,0 +1,636 @@ +/* + fuzzysort.js https://github.com/farzher/fuzzysort + SublimeText-like Fuzzy Search + + fuzzysort.single('fs', 'Fuzzy Search') // {score: -16} + fuzzysort.single('test', 'test') // {score: 0} + fuzzysort.single('doesnt exist', 'target') // null + + fuzzysort.go('mr', [{file:'Monitor.cpp'}, {file:'MeshRenderer.cpp'}], {key:'file'}) + // [{score:-18, obj:{file:'MeshRenderer.cpp'}}, {score:-6009, obj:{file:'Monitor.cpp'}}] + + fuzzysort.go('mr', ['Monitor.cpp', 'MeshRenderer.cpp']) + // [{score: -18, target: "MeshRenderer.cpp"}, {score: -6009, target: "Monitor.cpp"}] + + fuzzysort.highlight(fuzzysort.single('fs', 'Fuzzy Search'), '', '') + // Fuzzy Search +*/ + +// UMD (Universal Module Definition) for fuzzysort +;(function(root, UMD) { + if(typeof define === 'function' && define.amd) define([], UMD) + else if(typeof module === 'object' && module.exports) module.exports = UMD() + else root.fuzzysort = UMD() +})(this, function UMD() { function fuzzysortNew(instanceOptions) { + + var fuzzysort = { + + single: function(search, target, options) { ;if(search=='farzher')return{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6]} + if(!search) return null + if(!isObj(search)) search = fuzzysort.getPreparedSearch(search) + + if(!target) return null + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo + : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo + : true + var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo + return algorithm(search, target, search[0]) + }, + + go: function(search, targets, options) { ;if(search=='farzher')return[{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6],obj:targets?targets[0]:null}] + if(!search) return noResults + search = fuzzysort.prepareSearch(search) + var searchLowerCode = search[0] + + var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991 + var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991 + var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo + : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo + : true + var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo + var resultsLen = 0; var limitedCount = 0 + var targetsLen = targets.length + + // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys] + + // options.keys + if(options && options.keys) { + var scoreFn = options.scoreFn || defaultScoreFn + var keys = options.keys + var keysLen = keys.length + for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i] + var objResults = new Array(keysLen) + for (var keyI = keysLen - 1; keyI >= 0; --keyI) { + var key = keys[keyI] + var target = getValue(obj, key) + if(!target) { objResults[keyI] = null; continue } + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + objResults[keyI] = algorithm(search, target, searchLowerCode) + } + objResults.obj = obj // before scoreFn so scoreFn can use it + var score = scoreFn(objResults) + if(score === null) continue + if(score < threshold) continue + objResults.score = score + if(resultsLen < limit) { q.add(objResults); ++resultsLen } + else { + ++limitedCount + if(score > q.peek().score) q.replaceTop(objResults) + } + } + + // options.key + } else if(options && options.key) { + var key = options.key + for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i] + var target = getValue(obj, key) + if(!target) continue + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + var result = algorithm(search, target, searchLowerCode) + if(result === null) continue + if(result.score < threshold) continue + + // have to clone result so duplicate targets from different obj can each reference the correct obj + result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden + + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result.score > q.peek().score) q.replaceTop(result) + } + } + + // no keys + } else { + for(var i = targetsLen - 1; i >= 0; --i) { var target = targets[i] + if(!target) continue + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + var result = algorithm(search, target, searchLowerCode) + if(result === null) continue + if(result.score < threshold) continue + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result.score > q.peek().score) q.replaceTop(result) + } + } + } + + if(resultsLen === 0) return noResults + var results = new Array(resultsLen) + for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() + results.total = resultsLen + limitedCount + return results + }, + + goAsync: function(search, targets, options) { + var canceled = false + var p = new Promise(function(resolve, reject) { ;if(search=='farzher')return resolve([{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6],obj:targets?targets[0]:null}]) + if(!search) return resolve(noResults) + search = fuzzysort.prepareSearch(search) + var searchLowerCode = search[0] + + var q = fastpriorityqueue() + var iCurrent = targets.length - 1 + var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991 + var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991 + var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo + : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo + : true + var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo + var resultsLen = 0; var limitedCount = 0 + function step() { + if(canceled) return reject('canceled') + + var startMs = Date.now() + + // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys] + + // options.keys + if(options && options.keys) { + var scoreFn = options.scoreFn || defaultScoreFn + var keys = options.keys + var keysLen = keys.length + for(; iCurrent >= 0; --iCurrent) { + if(iCurrent%1000/*itemsPerCheck*/ === 0) { + if(Date.now() - startMs >= 10/*asyncInterval*/) { + isNode?setImmediate(step):setTimeout(step) + return + } + } + + var obj = targets[iCurrent] + var objResults = new Array(keysLen) + for (var keyI = keysLen - 1; keyI >= 0; --keyI) { + var key = keys[keyI] + var target = getValue(obj, key) + if(!target) { objResults[keyI] = null; continue } + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + objResults[keyI] = algorithm(search, target, searchLowerCode) + } + objResults.obj = obj // before scoreFn so scoreFn can use it + var score = scoreFn(objResults) + if(score === null) continue + if(score < threshold) continue + objResults.score = score + if(resultsLen < limit) { q.add(objResults); ++resultsLen } + else { + ++limitedCount + if(score > q.peek().score) q.replaceTop(objResults) + } + } + + // options.key + } else if(options && options.key) { + var key = options.key + for(; iCurrent >= 0; --iCurrent) { + if(iCurrent%1000/*itemsPerCheck*/ === 0) { + if(Date.now() - startMs >= 10/*asyncInterval*/) { + isNode?setImmediate(step):setTimeout(step) + return + } + } + + var obj = targets[iCurrent] + var target = getValue(obj, key) + if(!target) continue + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + var result = algorithm(search, target, searchLowerCode) + if(result === null) continue + if(result.score < threshold) continue + + // have to clone result so duplicate targets from different obj can each reference the correct obj + result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden + + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result.score > q.peek().score) q.replaceTop(result) + } + } + + // no keys + } else { + for(; iCurrent >= 0; --iCurrent) { + if(iCurrent%1000/*itemsPerCheck*/ === 0) { + if(Date.now() - startMs >= 10/*asyncInterval*/) { + isNode?setImmediate(step):setTimeout(step) + return + } + } + + var target = targets[iCurrent] + if(!target) continue + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + var result = algorithm(search, target, searchLowerCode) + if(result === null) continue + if(result.score < threshold) continue + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result.score > q.peek().score) q.replaceTop(result) + } + } + } + + if(resultsLen === 0) return resolve(noResults) + var results = new Array(resultsLen) + for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() + results.total = resultsLen + limitedCount + resolve(results) + } + + isNode?setImmediate(step):step() //setTimeout here is too slow + }) + p.cancel = function() { canceled = true } + return p + }, + + highlight: function(result, hOpen, hClose) { + if(typeof hOpen == 'function') return fuzzysort.highlightCallback(result, hOpen) + if(result === null) return null + if(hOpen === undefined) hOpen = '' + if(hClose === undefined) hClose = '' + var highlighted = '' + var matchesIndex = 0 + var opened = false + var target = result.target + var targetLen = target.length + var matchesBest = result.indexes + for(var i = 0; i < targetLen; ++i) { var char = target[i] + if(matchesBest[matchesIndex] === i) { + ++matchesIndex + if(!opened) { opened = true + highlighted += hOpen + } + + if(matchesIndex === matchesBest.length) { + highlighted += char + hClose + target.substr(i+1) + break + } + } else { + if(opened) { opened = false + highlighted += hClose + } + } + highlighted += char + } + + return highlighted + }, + highlightCallback: function(result, cb) { + if(result === null) return null + var target = result.target + var targetLen = target.length + var indexes = result.indexes + var highlighted = '' + var matchI = 0 + var indexesI = 0 + var opened = false + var result = [] + for(var i = 0; i < targetLen; ++i) { var char = target[i] + if(indexes[indexesI] === i) { + ++indexesI + if(!opened) { opened = true + result.push(highlighted); highlighted = '' + } + + if(indexesI === indexes.length) { + highlighted += char + result.push(cb(highlighted, matchI++)); highlighted = '' + result.push(target.substr(i+1)) + break + } + } else { + if(opened) { opened = false + result.push(cb(highlighted, matchI++)); highlighted = '' + } + } + highlighted += char + } + return result + }, + + prepare: function(target) { + if(!target) return {target: '', _targetLowerCodes: [0/*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], _nextBeginningIndexes: null, score: null, indexes: null, obj: null} // hidden + return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:null, score:null, indexes:null, obj:null} // hidden + }, + prepareSlow: function(target) { + if(!target) return {target: '', _targetLowerCodes: [0/*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], _nextBeginningIndexes: null, score: null, indexes: null, obj: null} // hidden + return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:fuzzysort.prepareNextBeginningIndexes(target), score:null, indexes:null, obj:null} // hidden + }, + prepareSearch: function(search) { + if(!search) search = '' + return fuzzysort.prepareLowerCodes(search) + }, + + + + // Below this point is only internal code + // Below this point is only internal code + // Below this point is only internal code + // Below this point is only internal code + + + + getPrepared: function(target) { + if(target.length > 999) return fuzzysort.prepare(target) // don't cache huge targets + var targetPrepared = preparedCache.get(target) + if(targetPrepared !== undefined) return targetPrepared + targetPrepared = fuzzysort.prepare(target) + preparedCache.set(target, targetPrepared) + return targetPrepared + }, + getPreparedSearch: function(search) { + if(search.length > 999) return fuzzysort.prepareSearch(search) // don't cache huge searches + var searchPrepared = preparedSearchCache.get(search) + if(searchPrepared !== undefined) return searchPrepared + searchPrepared = fuzzysort.prepareSearch(search) + preparedSearchCache.set(search, searchPrepared) + return searchPrepared + }, + + algorithm: function(searchLowerCodes, prepared, searchLowerCode) { + var targetLowerCodes = prepared._targetLowerCodes + var searchLen = searchLowerCodes.length + var targetLen = targetLowerCodes.length + var searchI = 0 // where we at + var targetI = 0 // where you at + var typoSimpleI = 0 + var matchesSimpleLen = 0 + + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for(;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI] + if(isMatch) { + matchesSimple[matchesSimpleLen++] = targetI + ++searchI; if(searchI === searchLen) break + searchLowerCode = searchLowerCodes[typoSimpleI===0?searchI : (typoSimpleI===searchI?searchI+1 : (typoSimpleI===searchI-1?searchI-1 : searchI))] + } + + ++targetI; if(targetI >= targetLen) { // Failed to find searchI + // Check for typo or exit + // we go as far as possible before trying to transpose + // then we transpose backwards until we reach the beginning + for(;;) { + if(searchI <= 1) return null // not allowed to transpose first char + if(typoSimpleI === 0) { // we haven't tried to transpose yet + --searchI + var searchLowerCodeNew = searchLowerCodes[searchI] + if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char + typoSimpleI = searchI + } else { + if(typoSimpleI === 1) return null // reached the end of the line for transposing + --typoSimpleI + searchI = typoSimpleI + searchLowerCode = searchLowerCodes[searchI + 1] + var searchLowerCodeNew = searchLowerCodes[searchI] + if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char + } + matchesSimpleLen = searchI + targetI = matchesSimple[matchesSimpleLen - 1] + 1 + break + } + } + } + + var searchI = 0 + var typoStrictI = 0 + var successStrict = false + var matchesStrictLen = 0 + + var nextBeginningIndexes = prepared._nextBeginningIndexes + if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target) + var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] + + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + if(targetI !== targetLen) for(;;) { + if(targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if(searchI <= 0) { // We failed to push chars forward for a better match + // transpose, starting from the beginning + ++typoStrictI; if(typoStrictI > searchLen-2) break + if(searchLowerCodes[typoStrictI] === searchLowerCodes[typoStrictI+1]) continue // doesn't make sense to transpose a repeat char + targetI = firstPossibleI + continue + } + + --searchI + var lastMatch = matchesStrict[--matchesStrictLen] + targetI = nextBeginningIndexes[lastMatch] + + } else { + var isMatch = searchLowerCodes[typoStrictI===0?searchI : (typoStrictI===searchI?searchI+1 : (typoStrictI===searchI-1?searchI-1 : searchI))] === targetLowerCodes[targetI] + if(isMatch) { + matchesStrict[matchesStrictLen++] = targetI + ++searchI; if(searchI === searchLen) { successStrict = true; break } + ++targetI + } else { + targetI = nextBeginningIndexes[targetI] + } + } + } + + { // tally up the score & keep track of matches for highlighting later + if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen } + else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen } + var score = 0 + var lastTargetI = -1 + for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i] + // score only goes down if they're not consecutive + if(lastTargetI !== targetI - 1) score -= targetI + lastTargetI = targetI + } + if(!successStrict) { + score *= 1000 + if(typoSimpleI !== 0) score += -20/*typoPenalty*/ + } else { + if(typoStrictI !== 0) score += -20/*typoPenalty*/ + } + score -= targetLen - searchLen + prepared.score = score + prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i] + + return prepared + } + }, + + algorithmNoTypo: function(searchLowerCodes, prepared, searchLowerCode) { + var targetLowerCodes = prepared._targetLowerCodes + var searchLen = searchLowerCodes.length + var targetLen = targetLowerCodes.length + var searchI = 0 // where we at + var targetI = 0 // where you at + var matchesSimpleLen = 0 + + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for(;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI] + if(isMatch) { + matchesSimple[matchesSimpleLen++] = targetI + ++searchI; if(searchI === searchLen) break + searchLowerCode = searchLowerCodes[searchI] + } + ++targetI; if(targetI >= targetLen) return null // Failed to find searchI + } + + var searchI = 0 + var successStrict = false + var matchesStrictLen = 0 + + var nextBeginningIndexes = prepared._nextBeginningIndexes + if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target) + var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] + + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + if(targetI !== targetLen) for(;;) { + if(targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if(searchI <= 0) break // We failed to push chars forward for a better match + + --searchI + var lastMatch = matchesStrict[--matchesStrictLen] + targetI = nextBeginningIndexes[lastMatch] + + } else { + var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI] + if(isMatch) { + matchesStrict[matchesStrictLen++] = targetI + ++searchI; if(searchI === searchLen) { successStrict = true; break } + ++targetI + } else { + targetI = nextBeginningIndexes[targetI] + } + } + } + + { // tally up the score & keep track of matches for highlighting later + if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen } + else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen } + var score = 0 + var lastTargetI = -1 + for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i] + // score only goes down if they're not consecutive + if(lastTargetI !== targetI - 1) score -= targetI + lastTargetI = targetI + } + if(!successStrict) score *= 1000 + score -= targetLen - searchLen + prepared.score = score + prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i] + + return prepared + } + }, + + prepareLowerCodes: function(str) { + var strLen = str.length + var lowerCodes = [] // new Array(strLen) sparse array is too slow + var lower = str.toLowerCase() + for(var i = 0; i < strLen; ++i) lowerCodes[i] = lower.charCodeAt(i) + return lowerCodes + }, + prepareBeginningIndexes: function(target) { + var targetLen = target.length + var beginningIndexes = []; var beginningIndexesLen = 0 + var wasUpper = false + var wasAlphanum = false + for(var i = 0; i < targetLen; ++i) { + var targetCode = target.charCodeAt(i) + var isUpper = targetCode>=65&&targetCode<=90 + var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57 + var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum + wasUpper = isUpper + wasAlphanum = isAlphanum + if(isBeginning) beginningIndexes[beginningIndexesLen++] = i + } + return beginningIndexes + }, + prepareNextBeginningIndexes: function(target) { + var targetLen = target.length + var beginningIndexes = fuzzysort.prepareBeginningIndexes(target) + var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow + var lastIsBeginning = beginningIndexes[0] + var lastIsBeginningI = 0 + for(var i = 0; i < targetLen; ++i) { + if(lastIsBeginning > i) { + nextBeginningIndexes[i] = lastIsBeginning + } else { + lastIsBeginning = beginningIndexes[++lastIsBeginningI] + nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning + } + } + return nextBeginningIndexes + }, + + cleanup: cleanup, + new: fuzzysortNew, + } + return fuzzysort +} // fuzzysortNew + +// This stuff is outside fuzzysortNew, because it's shared with instances of fuzzysort.new() +var isNode = typeof require !== 'undefined' && typeof window === 'undefined' +var MyMap = typeof Map === 'function' ? Map : function(){var s=Object.create(null);this.get=function(k){return s[k]};this.set=function(k,val){s[k]=val;return this};this.clear=function(){s=Object.create(null)}} +var preparedCache = new MyMap() +var preparedSearchCache = new MyMap() +var noResults = []; noResults.total = 0 +var matchesSimple = []; var matchesStrict = [] +function cleanup() { preparedCache.clear(); preparedSearchCache.clear(); matchesSimple = []; matchesStrict = [] } +function defaultScoreFn(a) { + var max = -9007199254740991 + for (var i = a.length - 1; i >= 0; --i) { + var result = a[i]; if(result === null) continue + var score = result.score + if(score > max) max = score + } + if(max === -9007199254740991) return null + return max +} + +// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop] +// prop = 'key1.key2' 10ms +// prop = ['key1', 'key2'] 27ms +function getValue(obj, prop) { + var tmp = obj[prop]; if(tmp !== undefined) return tmp + var segs = prop + if(!Array.isArray(prop)) segs = prop.split('.') + var len = segs.length + var i = -1 + while (obj && (++i < len)) obj = obj[segs[i]] + return obj +} + +function isObj(x) { return typeof x === 'object' } // faster as a function + +// Hacked version of https://github.com/lemire/FastPriorityQueue.js +var fastpriorityqueue=function(){var r=[],o=0,e={};function n(){for(var e=0,n=r[e],c=1;c>1]=r[e],c=1+(e<<1)}for(var a=e-1>>1;e>0&&n.score>1)r[e]=r[a];r[e]=n}return e.add=function(e){var n=o;r[o++]=e;for(var c=n-1>>1;n>0&&e.score>1)r[n]=r[c];r[n]=e},e.poll=function(){if(0!==o){var e=r[0];return r[0]=r[--o],n(),e}},e.peek=function(e){if(0!==o)return r[0]},e.replaceTop=function(o){r[0]=o,n()},e}; +var q = fastpriorityqueue() // reuse this, except for async, it needs to make its own + +return fuzzysortNew() +}) // UMD + +// TODO: (performance) wasm version!? +// TODO: (performance) threads? +// TODO: (performance) avoid cache misses +// TODO: (performance) preparedCache is a memory leak +// TODO: (like sublime) backslash === forwardslash +// TODO: (like sublime) spaces: "a b" should do 2 searches 1 for a and 1 for b +// TODO: (scoring) garbage in targets that allows most searches to strict match need a penality +// TODO: (performance) idk if allowTypo is optimized diff --git a/public/src/js/lib/jszip.js b/public/src/js/lib/jszip.js new file mode 100644 index 0000000..4aed620 --- /dev/null +++ b/public/src/js/lib/jszip.js @@ -0,0 +1,199 @@ + +/*! + +JSZip v3.7.1 - A JavaScript class for generating and reading zip files + + +(c) 2009-2016 Stuart Knightley +Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/master/LICENSE.markdown. + +JSZip uses the library pako released under the MIT license : +https://github.com/nodeca/pako/blob/master/LICENSE +*/ + +function JSZip(){return function s(a,o,h){function u(r,t){if(!o[r]){if(!a[r]){var e="function"==typeof require&&require;if(!t&&e)return e(r,!0);if(l)return l(r,!0);var i=new Error("Cannot find module '"+r+"'");throw i.code="MODULE_NOT_FOUND",i;}var n=o[r]={exports:{}};a[r][0].call(n.exports,function(t){var e=a[r][1][t];return u(e||t)},n,n.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,t=0;t>2,s=(3&e)<<4|r>>4,a=1>6:64,o=2>4,r=(15&n)<<4|(s=p.indexOf(t.charAt(o++)))>>2,i=(3&s)<<6|(a=p.indexOf(t.charAt(o++))), +l[h++]=e,64!==s&&(l[h++]=r),64!==a&&(l[h++]=i);return l}},{"./support":30,"./utils":32}],2:[function(t,e,r){var i=t("./external"),n=t("./stream/DataWorker"),s=t("./stream/Crc32Probe"),a=t("./stream/DataLengthProbe");function o(t,e,r,i,n){this.compressedSize=t,this.uncompressedSize=e,this.crc32=r,this.compression=i,this.compressedContent=n}o.prototype={getContentWorker:function(){var t=(new n(i.Promise.resolve(this.compressedContent))).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")), +e=this;return t.on("end",function(){if(this.streamInfo.data_length!==e.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch");}),t},getCompressedWorker:function(){return(new n(i.Promise.resolve(this.compressedContent))).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(t,e,r){return t.pipe(new s).pipe(new a("uncompressedSize")).pipe(e.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression", +e)},e.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,e,r){var i=t("./stream/GenericWorker");r.STORE={magic:"\x00\x00",compressWorker:function(t){return new i("STORE compression")},uncompressWorker:function(){return new i("STORE decompression")}},r.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,e,r){var i=t("./utils");var o=function(){for(var t,e=[],r=0;r<256;r++){t=r;for(var i=0;i<8;i++)t=1& +t?3988292384^t>>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e){return void 0!==t&&t.length?"string"!==i.getTypeOf(t)?function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e[a])];return-1^t}(0|e,t,t.length,0):function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e.charCodeAt(a))];return-1^t}(0|e,t,t.length,0):0}},{"./utils":32}],5:[function(t,e,r){r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null, +r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(t,e,r){var i=null;i="undefined"!=typeof Promise?Promise:t("lie"),e.exports={Promise:i}},{lie:37}],7:[function(t,e,r){var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,n=t("pako"),s=t("./utils"),a=t("./stream/GenericWorker"),o=i?"uint8array":"array";function h(t,e){a.call(this,"FlateWorker/"+t),this._pako=null,this._pakoAction=t,this._pakoOptions=e,this.meta={}}r.magic= +"\b\x00",s.inherits(h,a),h.prototype.processChunk=function(t){this.meta=t.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,t.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new n[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var e=this;this._pako.onData= +function(t){e.push({data:t,meta:e.meta})}},r.compressWorker=function(t){return new h("Deflate",t)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,e,r){function A(t,e){var r,i="";for(r=0;r>>=8;return i}function i(t,e,r,i,n,s){var a,o,h=t.file,u=t.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),d=I.transformTo("string",O.utf8encode(h.name)),c=h.comment,p=I.transformTo("string", +s(c)),m=I.transformTo("string",O.utf8encode(c)),_=d.length!==h.name.length,g=m.length!==c.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};e&&!r||(x.crc32=t.crc32,x.compressedSize=t.compressedSize,x.uncompressedSize=t.uncompressedSize);var S=0;e&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===n?(C=798,z|=function(t,e){var r=t;return t||(r=e?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(t){return 63&(t||0)}(h.dosPermissions)), +a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+d,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\x00",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+ +A(p.length,2)+"\x00\x00\x00\x00"+A(z,4)+A(i,4)+f+b+p}}var I=t("../utils"),n=t("../stream/GenericWorker"),O=t("../utf8"),B=t("../crc32"),R=t("../signature");function s(t,e,r,i){n.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=e,this.zipPlatform=r,this.encodeFileName=i,this.streamFiles=t,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,n),s.prototype.push=function(t){var e=t.meta.percent|| +0,r=this.entriesCount,i=this._sources.length;this.accumulate?this.contentBuffer.push(t):(this.bytesWritten+=t.data.length,n.prototype.push.call(this,{data:t.data,meta:{currentFile:this.currentFile,percent:r?(e+100*(r-i-1))/r:100}}))},s.prototype.openedSource=function(t){this.currentSourceOffset=this.bytesWritten,this.currentFile=t.file.name;var e=this.streamFiles&&!t.file.dir;if(e){var r=i(t,e,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate= +!0},s.prototype.closedSource=function(t){this.accumulate=!1;var e=this.streamFiles&&!t.file.dir,r=i(t,e,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),e)this.push({data:function(t){return R.DATA_DESCRIPTOR+A(t.crc32,4)+A(t.compressedSize,4)+A(t.uncompressedSize,4)}(t),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush= +function(){for(var t=this.bytesWritten,e=0;e=this.index;e--)r=(r<<8)+this.byteAt(e);return this.index+=t,r},readString:function(t){return i.transformTo("string",this.readData(t))},readData:function(t){},lastIndexOfSignature:function(t){}, +readAndCheckSignature:function(t){},readDate:function(){var t=this.readInt(4);return new Date(Date.UTC(1980+(t>>25&127),(t>>21&15)-1,t>>16&31,t>>11&31,t>>5&63,(31&t)<<1))}},e.exports=n},{"../utils":32}],19:[function(t,e,r){var i=t("./Uint8ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./Uint8ArrayReader":21}], +20:[function(t,e,r){var i=t("./DataReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.byteAt=function(t){return this.data.charCodeAt(this.zero+t)},n.prototype.lastIndexOfSignature=function(t){return this.data.lastIndexOf(t)-this.zero},n.prototype.readAndCheckSignature=function(t){return t===this.readData(4)},n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32, +"./DataReader":18}],21:[function(t,e,r){var i=t("./ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){if(this.checkOffset(t),0===t)return new Uint8Array(0);var e=this.data.subarray(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./ArrayReader":17}],22:[function(t,e,r){var i=t("../utils"),n=t("../support"),s=t("./ArrayReader"),a=t("./StringReader"),o=t("./NodeBufferReader"),h=t("./Uint8ArrayReader"); +e.exports=function(t){var e=i.getTypeOf(t);return i.checkSupport(e),"string"!==e||n.uint8array?"nodebuffer"===e?new o(t):n.uint8array?new h(i.transformTo("uint8array",t)):new s(i.transformTo("array",t)):new a(t)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,e,r){r.LOCAL_FILE_HEADER="PK\u0003\u0004",r.CENTRAL_FILE_HEADER="PK\u0001\u0002",r.CENTRAL_DIRECTORY_END="PK\u0005\u0006",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR= +"PK\u0006\u0007",r.ZIP64_CENTRAL_DIRECTORY_END="PK\u0006\u0006",r.DATA_DESCRIPTOR="PK\u0007\b"},{}],24:[function(t,e,r){var i=t("./GenericWorker"),n=t("../utils");function s(t){i.call(this,"ConvertWorker to "+t),this.destType=t}n.inherits(s,i),s.prototype.processChunk=function(t){this.push({data:n.transformTo(this.destType,t.data),meta:t.meta})},e.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(t,e,r){var i=t("./GenericWorker"),n=t("../crc32");function s(){i.call(this,"Crc32Probe"), +this.withStreamInfo("crc32",0)}t("../utils").inherits(s,i),s.prototype.processChunk=function(t){this.streamInfo.crc32=n(t.data,this.streamInfo.crc32||0),this.push(t)},e.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,e,r){var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataLengthProbe for "+t),this.propName=t,this.withStreamInfo(t,0)}i.inherits(s,n),s.prototype.processChunk=function(t){if(t){var e=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]= +e+t.data.length}n.prototype.processChunk.call(this,t)},e.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(t,e,r){var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataWorker");var e=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,t.then(function(t){e.dataIsReady=!0,e.data=t,e.max=t&&t.length||0,e.type=i.getTypeOf(t),e.isPaused||e._tickAndRepeat()},function(t){e.error(t)})}i.inherits(s,n),s.prototype.cleanUp=function(){n.prototype.cleanUp.call(this), +this.data=null},s.prototype.resume=function(){return!!n.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,i.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(i.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var t=null,e=Math.min(this.max,this.index+16384); +if(this.index>=this.max)return this.end();switch(this.type){case "string":t=this.data.substring(this.index,e);break;case "uint8array":t=this.data.subarray(this.index,e);break;case "array":case "nodebuffer":t=this.data.slice(this.index,e)}return this.index=e,this.push({data:t,meta:{percent:this.max?this.index/this.max*100:0}})},e.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(t,e,r){function i(t){this.name=t||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo= +{},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}i.prototype={push:function(t){this.emit("data",t)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(t$3){this.emit("error",t$3)}return!0},error:function(t){return!this.isFinished&&(this.isPaused?this.generatedError=t:(this.isFinished=!0,this.emit("error",t),this.previous&&this.previous.error(t),this.cleanUp()),!0)}, +on:function(t,e){return this._listeners[t].push(e),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(t,e){if(this._listeners[t])for(var r=0;r "+t:t}},e.exports=i},{}],29:[function(t, +e,r){var h=t("../utils"),n=t("./ConvertWorker"),s=t("./GenericWorker"),u=t("../base64"),i=t("../support"),a=t("../external"),o=null;if(i.nodestream)try{o=t("../nodejs/NodejsStreamOutputAdapter")}catch(t$4){}function l(t,o){return new a.Promise(function(e,r){var i=[],n=t._internalType,s=t._outputType,a=t._mimeType;t.on("data",function(t,e){i.push(t),o&&o(e)}).on("error",function(t){i=[],r(t)}).on("end",function(){try{var t=function(t,e,r){switch(t){case "blob":return h.newBlob(h.transformTo("arraybuffer", +e),r);case "base64":return u.encode(e);default:return h.transformTo(t,e)}}(s,function(t,e){var r,i=0,n=null,s=0;for(r=0;r>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63), +e[s++]=128|63&r);return e}(t)},s.utf8decode=function(t){return h.nodebuffer?o.transformTo("nodebuffer",t).toString("utf-8"):function(t){var e,r,i,n,s=t.length,a=new Array(2*s);for(e=r=0;e>10&1023,a[r++]=56320|1023&i)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(t=o.transformTo(h.uint8array? +"uint8array":"array",t))},o.inherits(a,i),a.prototype.processChunk=function(t){var e=o.transformTo(h.uint8array?"uint8array":"array",t.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=e;(e=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),e.set(r,this.leftOver.length)}else e=this.leftOver.concat(e);this.leftOver=null}var i=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:r+u[t[r]]>e?r:e}(e),n= +e;i!==e.length&&(h.uint8array?(n=e.subarray(0,i),this.leftOver=e.subarray(i,e.length)):(n=e.slice(0,i),this.leftOver=e.slice(i,e.length))),this.push({data:s.utf8decode(n),meta:t.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,i),l.prototype.processChunk=function(t){this.push({data:s.utf8encode(t.data),meta:t.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28, +"./support":30,"./utils":32}],32:[function(t,e,a){var o=t("./support"),h=t("./base64"),r=t("./nodejsUtils"),i=t("set-immediate-shim"),u=t("./external");function n(t){return t}function l(t,e){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==t&&(this.dosPermissions=63&this.externalFileAttributes),3==t&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(t){if(this.extraFields[1]){var e=i(this.extraFields[1].value);this.uncompressedSize=== +s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(t){var e,r,i,n=t.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});t.index+4>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e},r.buf2binstring=function(t){return l(t,t.length)},r.binstring2buf=function(t){for(var e=new h.Buf8(t.length), +r=0,i=e.length;r>10&1023,o[i++]=56320|1023&n)}return l(o,i)},r.utf8border=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e: +r+u[t[r]]>e?r:e}},{"./common":41}],43:[function(t,e,r){e.exports=function(t,e,r,i){for(var n=65535&t|0,s=t>>>16&65535|0,a=0;0!==r;){for(r-=a=2E3>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e[a])];return-1^t}},{}],46:[function(t,e,r){var h,d=t("../utils/common"),u=t("./trees"),c=t("./adler32"),p=t("./crc32"),i=t("./messages"),l=0,f=4,m=0,_=-2,g= +-1,b=4,n=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(t,e){return t.msg=i[e],e}function T(t){return(t<<1)-(4t.avail_out&&(r=t.avail_out),0!==r&&(d.arraySet(t.output,e.pending_buf,e.pending_out,r,t.next_out),t.next_out+=r,e.pending_out+=r,t.total_out+=r,t.avail_out-=r,e.pending-=r,0===e.pending&&(e.pending_out=0))}function N(t,e){u._tr_flush_block(t,0<= +t.block_start?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,F(t.strm)}function U(t,e){t.pending_buf[t.pending++]=e}function P(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function L(t,e){var r,i,n=t.max_chain_length,s=t.strstart,a=t.prev_length,o=t.nice_match,h=t.strstart>t.w_size-z?t.strstart-(t.w_size-z):0,u=t.window,l=t.w_mask,f=t.prev,d=t.strstart+S,c=u[s+a-1],p=u[s+a];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do if(u[(r= +e)+a]===p&&u[r+a-1]===c&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do;while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--n);return a<=t.lookahead?a:t.lookahead}function j(t){var e,r,i,n,s,a,o,h,u,l,f=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=f+(f-z)){for(d.arraySet(t.window,t.window, +f,f,0),t.match_start-=f,t.strstart-=f,t.block_start-=f,e=r=t.hash_size;i=t.head[--e],t.head[e]=f<=i?i-f:0,--r;);for(e=r=f;i=t.prev[--e],t.prev[e]=f<=i?i-f:0,--r;);n+=f}if(0===t.strm.avail_in)break;if(a=t.strm,o=t.window,h=t.strstart+t.lookahead,u=n,l=void 0,l=a.avail_in,u=x)for(s= +t.strstart-t.insert,t.ins_h=t.window[s],t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x)if(i=u._tr_tally(t,t.strstart-t.match_start,t.match_length-x),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=x){for(t.match_length--;t.strstart++,t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x&&t.match_length<=t.prev_length){for(n=t.strstart+t.lookahead- +x,i=u._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-x),t.lookahead-=t.prev_length-1,t.prev_length-=2;++t.strstart<=n&&(t.ins_h=(t.ins_h<t.pending_buf_size-5&&(r=t.pending_buf_size-5);;){if(t.lookahead<=1){if(j(t),0===t.lookahead&&e===l)return A;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead= +0;var i=t.block_start+r;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,N(t,!1),0===t.strm.avail_out))return A;if(t.strstart-t.block_start>=t.w_size-z&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):(t.strstart>t.block_start&&(N(t,!1),t.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32, +258,258,4096,W)],r.deflateInit=function(t,e){return Y(t,e,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?_:(t.state.gzhead=e,m):_},r.deflate=function(t,e){var r,i,n,s;if(!t||!t.state||5>8&255),U(i,i.gzhead.time>>16&255),U(i,i.gzhead.time>>24&255),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(U(i,255&i.gzhead.extra.length),U(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(t.adler=p(t.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69):(U(i,0),U(i,0), +U(i,0),U(i,0),U(i,0),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,3),i.status=E);else{var a=v+(i.w_bits-8<<4)<<8;a|=(2<=i.strategy||i.level<2?0:i.level<6?1:6===i.level?2:3)<<6,0!==i.strstart&&(a|=32),a+=31-a%31,i.status=E,P(i,a),0!==i.strstart&&(P(i,t.adler>>>16),P(i,65535&t.adler)),t.adler=1}if(69===i.status)if(i.gzhead.extra){for(n=i.pending;i.gzindex<(65535&i.gzhead.extra.length)&&(i.pending!==i.pending_buf_size||(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n, +n)),F(t),n=i.pending,i.pending!==i.pending_buf_size));)U(i,255&i.gzhead.extra[i.gzindex]),i.gzindex++;i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),i.gzindex===i.gzhead.extra.length&&(i.gzindex=0,i.status=73)}else i.status=73;if(73===i.status)if(i.gzhead.name){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindexn&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.gzindex=0,i.status=91)}else i.status=91;if(91===i.status)if(i.gzhead.comment){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindexn&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.status=103)}else i.status=103;if(103===i.status&&(i.gzhead.hcrc?(i.pending+2>i.pending_buf_size&&F(t),i.pending+2<=i.pending_buf_size&&(U(i,255&t.adler),U(i,t.adler>>8&255),t.adler=0,i.status=E)):i.status=E),0!==i.pending){if(F(t),0===t.avail_out)return i.last_flush=-1,m}else if(0===t.avail_in&&T(e)<=T(r)&&e!==f)return R(t,-5);if(666===i.status&&0!==t.avail_in)return R(t,-5);if(0!==t.avail_in||0!== +i.lookahead||e!==l&&666!==i.status){var o=2===i.strategy?function(t,e){for(var r;;){if(0===t.lookahead&&(j(t),0===t.lookahead)){if(e===l)return A;break}if(t.match_length=0,r=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):3===i.strategy?function(t,e){for(var r,i,n,s,a=t.window;;){if(t.lookahead<=S){if(j(t),t.lookahead<=S&&e===l)return A; +if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=x&&0t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=x?(r=u._tr_tally(t,1,t.match_length-x),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(r=u._tr_tally(t,0,t.window[t.strstart]), +t.lookahead--,t.strstart++),r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):h[i.level].func(i,e);if(o!==O&&o!==B||(i.status=666),o===A||o===O)return 0===t.avail_out&&(i.last_flush=-1),m;if(o===I&&(1===e?u._tr_align(i):5!==e&&(u._tr_stored_block(i,0,0,!1),3===e&&(D(i.head),0===i.lookahead&&(i.strstart=0,i.block_start=0,i.insert=0))),F(t),0===t.avail_out))return i.last_flush=-1,m}return e!==f? +m:i.wrap<=0?1:(2===i.wrap?(U(i,255&t.adler),U(i,t.adler>>8&255),U(i,t.adler>>16&255),U(i,t.adler>>24&255),U(i,255&t.total_in),U(i,t.total_in>>8&255),U(i,t.total_in>>16&255),U(i,t.total_in>>24&255)):(P(i,t.adler>>>16),P(i,65535&t.adler)),F(t),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new d.Buf8(r.w_size),d.arraySet(u,e,l-r.w_size,r.w_size,0),e=u,l=r.w_size),a=t.avail_in,o=t.next_in,h=t.input,t.avail_in=l,t.next_in=0,t.input=e,j(r);r.lookahead>=x;){for(i=r.strstart,n=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>= +y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(c&(1<>>=y,p-=y),p<15&&(c+=z[i++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(c&(1<>>=y,p-=y,(y=s-a)>3,c&=(1<<(p-=w<<3))-1,t.next_in=i,t.next_out=s,t.avail_in=i>> +24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288), +this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=P,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new I.Buf32(i),e.distcode=e.distdyn=new I.Buf32(n),e.sane=1,e.back=-1,N):U}function o(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,a(t)):U}function h(t,e){var r,i;return t&&t.state?(i=t.state,e<0?(r= +0,e=-e):(r=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15=s.wsize?(I.arraySet(s.window,e,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(i<(n=s.wsize-s.wnext)&&(n=i),I.arraySet(s.window,e,r-i,n,s.wnext),(i-=n)?(I.arraySet(s.window,e,r-i,i,0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){t.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){t.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){t.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check, +E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(c=r.length)&&(c=o),c&&(r.head&&(k=r.head.extra_len- +r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,i,s,c,k)),512&r.flags&&(r.check=B(r.check,i,c,s)),o-=c,s+=c,r.length-=c),r.length))break t;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break t;for(c=0;k=i[s+c++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&c>9&1,r.head.done=!0),t.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break t;o--,u+=i[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==e)break;u>>>=2,l-=2;break t;case 2:r.mode=17;break;case 3:t.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7& +l,l-=7&l;l<32;){if(0===o)break t;o--,u+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===e)break t;case 15:r.mode=16;case 16:if(c=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>> +16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){t.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],c=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)), +u>>>=7,l-=7}if(r.have+c>r.nlen+r.ndist){t.msg="invalid bit length repeat",r.mode=30;break}for(;c--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){t.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){t.msg= +"invalid distances set",r.mode=30;break}if(r.mode=20,6===e)break t;case 20:r.mode=21;case 21:if(6<=o&&258<=h){t.next_out=a,t.avail_out=h,t.next_in=s,t.avail_in=o,r.hold=u,r.bits=l,R(t,d),a=t.next_out,n=t.output,h=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<> +v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){t.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){t.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+= +r.extra}if(r.offset>r.dmax){t.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break t;if(c=d-h,r.offset>c){if((c=r.offset-c)>r.whave&&r.sane){t.msg="invalid distance too far back",r.mode=30;break}p=c>r.wnext?(c-=r.wnext,r.wsize-c):r.wnext-c,c>r.length&&(c=r.length),m=r.window}else m=n,p=a-r.offset,c=r.length;for(hc?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=e[r+a[v]]}if(k>>7)]}function U(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function P(t,e,r){t.bi_valid>c-r?(t.bi_buf|=e<>c-t.bi_valid,t.bi_valid+=r-c):(t.bi_buf|=e<>>=1,r<<=1,0<--e;);return r>>>1}function Z(t,e,r){var i,n,s=new Array(g+1), +a=0;for(i=1;i<=g;i++)s[i]=a=a+r[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=j(s[o]++,o))}}function W(t){var e;for(e=0;e>1;1<=r;r--)G(t,s,r);for(n=h;r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],G(t,s,1),i=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=i,s[2*n]=s[2*r]+s[2*i],t.depth[n]=(t.depth[r]>=t.depth[i]?t.depth[r]: +t.depth[i])+1,s[2*r+1]=s[2*i+1]=n,t.heap[1]=n++,G(t,s,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1],function(t,e){var r,i,n,s,a,o,h=e.dyn_tree,u=e.max_code,l=e.stat_desc.static_tree,f=e.stat_desc.has_stree,d=e.stat_desc.extra_bits,c=e.stat_desc.extra_base,p=e.stat_desc.max_length,m=0;for(s=0;s<=g;s++)t.bl_count[s]=0;for(h[2*t.heap[t.heap_max]+1]=0,r=t.heap_max+1;r<_;r++)p<(s=h[2*h[2*(i=t.heap[r])+1]+1]+1)&&(s=p,m++),h[2*i+1]=s,u>=7;i>>=1)if(1&r&&0!==t.dyn_ltree[2*e])return o;if(0!== +t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return h;for(e=32;e>>3,(s=t.static_len+3+7>>>3)<=n&&(n=s)):n=s=r+5,r+4<=n&&-1!==e?J(t,e,r,i):4===t.strategy||s===n?(P(t,2+(i?1:0),3),K(t,z,C)):(P(t,4+(i?1:0), +3),function(t,e,r,i){var n;for(P(t,e-257,5),P(t,r-1,5),P(t,i-4,4),n=0;n>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&r,t.last_lit++,0===e?t.dyn_ltree[2*r]++:(t.matches++,e--,t.dyn_ltree[2*(A[r]+u+1)]++,t.dyn_dtree[2* +N(e)]++),t.last_lit===t.lit_bufsize-1},r._tr_align=function(t){P(t,2,3),L(t,m,z),function(t){16===t.bi_valid?(U(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):8<=t.bi_valid&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}(t)}},{"../utils/common":41}],53:[function(t,e,r){e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t, +e,r){e.exports="function"==typeof setImmediate?setImmediate:function(){var t=[].slice.apply(arguments);t.splice(1,0,0),setTimeout.apply(null,t)}},{}]},{},[10])(10)}; diff --git a/public/src/js/lib/md5.min.js b/public/src/js/lib/md5.min.js new file mode 100644 index 0000000..7418ff8 --- /dev/null +++ b/public/src/js/lib/md5.min.js @@ -0,0 +1,10 @@ +/** + * [js-md5]{@link https://github.com/emn178/js-md5} + * + * @namespace md5 + * @version 0.7.3 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2017 + * @license MIT + */ +!function(){"use strict";function t(t){if(t)d[0]=d[16]=d[1]=d[2]=d[3]=d[4]=d[5]=d[6]=d[7]=d[8]=d[9]=d[10]=d[11]=d[12]=d[13]=d[14]=d[15]=0,this.blocks=d,this.buffer8=l;else if(a){var r=new ArrayBuffer(68);this.buffer8=new Uint8Array(r),this.blocks=new Uint32Array(r)}else this.blocks=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];this.h0=this.h1=this.h2=this.h3=this.start=this.bytes=this.hBytes=0,this.finalized=this.hashed=!1,this.first=!0}var r="input is invalid type",e="object"==typeof window,i=e?window:{};i.JS_MD5_NO_WINDOW&&(e=!1);var s=!e&&"object"==typeof self,h=!i.JS_MD5_NO_NODE_JS&&"object"==typeof process&&process.versions&&process.versions.node;h?i=global:s&&(i=self);var f=!i.JS_MD5_NO_COMMON_JS&&"object"==typeof module&&module.exports,o="function"==typeof define&&define.amd,a=!i.JS_MD5_NO_ARRAY_BUFFER&&"undefined"!=typeof ArrayBuffer,n="0123456789abcdef".split(""),u=[128,32768,8388608,-2147483648],y=[0,8,16,24],c=["hex","array","digest","buffer","arrayBuffer","base64"],p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split(""),d=[],l;if(a){var A=new ArrayBuffer(68);l=new Uint8Array(A),d=new Uint32Array(A)}!i.JS_MD5_NO_NODE_JS&&Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),!a||!i.JS_MD5_NO_ARRAY_BUFFER_IS_VIEW&&ArrayBuffer.isView||(ArrayBuffer.isView=function(t){return"object"==typeof t&&t.buffer&&t.buffer.constructor===ArrayBuffer});var b=function(r){return function(e){return new t(!0).update(e)[r]()}},v=function(){var r=b("hex");h&&(r=w(r)),r.create=function(){return new t},r.update=function(t){return r.create().update(t)};for(var e=0;e>2]|=t[f]<>6,u[h++]=128|63&s):s<55296||s>=57344?(u[h++]=224|s>>12,u[h++]=128|s>>6&63,u[h++]=128|63&s):(s=65536+((1023&s)<<10|1023&t.charCodeAt(++f)),u[h++]=240|s>>18,u[h++]=128|s>>12&63,u[h++]=128|s>>6&63,u[h++]=128|63&s);else for(h=this.start;f>2]|=s<>2]|=(192|s>>6)<>2]|=(128|63&s)<=57344?(n[h>>2]|=(224|s>>12)<>2]|=(128|s>>6&63)<>2]|=(128|63&s)<>2]|=(240|s>>18)<>2]|=(128|s>>12&63)<>2]|=(128|s>>6&63)<>2]|=(128|63&s)<=64?(this.start=h-64,this.hash(),this.hashed=!0):this.start=h}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296<<0,this.bytes=this.bytes%4294967296),this}},t.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var t=this.blocks,r=this.lastByteIndex;t[r>>2]|=u[3&r],r>=56&&(this.hashed||this.hash(),t[0]=t[16],t[16]=t[1]=t[2]=t[3]=t[4]=t[5]=t[6]=t[7]=t[8]=t[9]=t[10]=t[11]=t[12]=t[13]=t[14]=t[15]=0),t[14]=this.bytes<<3,t[15]=this.hBytes<<3|this.bytes>>>29,this.hash()}},t.prototype.hash=function(){var t,r,e,i,s,h,f=this.blocks;this.first?r=((r=((t=((t=f[0]-680876937)<<7|t>>>25)-271733879<<0)^(e=((e=(-271733879^(i=((i=(-1732584194^2004318071&t)+f[1]-117830708)<<12|i>>>20)+t<<0)&(-271733879^t))+f[2]-1126478375)<<17|e>>>15)+i<<0)&(i^t))+f[3]-1316259209)<<22|r>>>10)+e<<0:(t=this.h0,r=this.h1,e=this.h2,r=((r+=((t=((t+=((i=this.h3)^r&(e^i))+f[0]-680876936)<<7|t>>>25)+r<<0)^(e=((e+=(r^(i=((i+=(e^t&(r^e))+f[1]-389564586)<<12|i>>>20)+t<<0)&(t^r))+f[2]+606105819)<<17|e>>>15)+i<<0)&(i^t))+f[3]-1044525330)<<22|r>>>10)+e<<0),r=((r+=((t=((t+=(i^r&(e^i))+f[4]-176418897)<<7|t>>>25)+r<<0)^(e=((e+=(r^(i=((i+=(e^t&(r^e))+f[5]+1200080426)<<12|i>>>20)+t<<0)&(t^r))+f[6]-1473231341)<<17|e>>>15)+i<<0)&(i^t))+f[7]-45705983)<<22|r>>>10)+e<<0,r=((r+=((t=((t+=(i^r&(e^i))+f[8]+1770035416)<<7|t>>>25)+r<<0)^(e=((e+=(r^(i=((i+=(e^t&(r^e))+f[9]-1958414417)<<12|i>>>20)+t<<0)&(t^r))+f[10]-42063)<<17|e>>>15)+i<<0)&(i^t))+f[11]-1990404162)<<22|r>>>10)+e<<0,r=((r+=((t=((t+=(i^r&(e^i))+f[12]+1804603682)<<7|t>>>25)+r<<0)^(e=((e+=(r^(i=((i+=(e^t&(r^e))+f[13]-40341101)<<12|i>>>20)+t<<0)&(t^r))+f[14]-1502002290)<<17|e>>>15)+i<<0)&(i^t))+f[15]+1236535329)<<22|r>>>10)+e<<0,r=((r+=((i=((i+=(r^e&((t=((t+=(e^i&(r^e))+f[1]-165796510)<<5|t>>>27)+r<<0)^r))+f[6]-1069501632)<<9|i>>>23)+t<<0)^t&((e=((e+=(t^r&(i^t))+f[11]+643717713)<<14|e>>>18)+i<<0)^i))+f[0]-373897302)<<20|r>>>12)+e<<0,r=((r+=((i=((i+=(r^e&((t=((t+=(e^i&(r^e))+f[5]-701558691)<<5|t>>>27)+r<<0)^r))+f[10]+38016083)<<9|i>>>23)+t<<0)^t&((e=((e+=(t^r&(i^t))+f[15]-660478335)<<14|e>>>18)+i<<0)^i))+f[4]-405537848)<<20|r>>>12)+e<<0,r=((r+=((i=((i+=(r^e&((t=((t+=(e^i&(r^e))+f[9]+568446438)<<5|t>>>27)+r<<0)^r))+f[14]-1019803690)<<9|i>>>23)+t<<0)^t&((e=((e+=(t^r&(i^t))+f[3]-187363961)<<14|e>>>18)+i<<0)^i))+f[8]+1163531501)<<20|r>>>12)+e<<0,r=((r+=((i=((i+=(r^e&((t=((t+=(e^i&(r^e))+f[13]-1444681467)<<5|t>>>27)+r<<0)^r))+f[2]-51403784)<<9|i>>>23)+t<<0)^t&((e=((e+=(t^r&(i^t))+f[7]+1735328473)<<14|e>>>18)+i<<0)^i))+f[12]-1926607734)<<20|r>>>12)+e<<0,r=((r+=((h=(i=((i+=((s=r^e)^(t=((t+=(s^i)+f[5]-378558)<<4|t>>>28)+r<<0))+f[8]-2022574463)<<11|i>>>21)+t<<0)^t)^(e=((e+=(h^r)+f[11]+1839030562)<<16|e>>>16)+i<<0))+f[14]-35309556)<<23|r>>>9)+e<<0,r=((r+=((h=(i=((i+=((s=r^e)^(t=((t+=(s^i)+f[1]-1530992060)<<4|t>>>28)+r<<0))+f[4]+1272893353)<<11|i>>>21)+t<<0)^t)^(e=((e+=(h^r)+f[7]-155497632)<<16|e>>>16)+i<<0))+f[10]-1094730640)<<23|r>>>9)+e<<0,r=((r+=((h=(i=((i+=((s=r^e)^(t=((t+=(s^i)+f[13]+681279174)<<4|t>>>28)+r<<0))+f[0]-358537222)<<11|i>>>21)+t<<0)^t)^(e=((e+=(h^r)+f[3]-722521979)<<16|e>>>16)+i<<0))+f[6]+76029189)<<23|r>>>9)+e<<0,r=((r+=((h=(i=((i+=((s=r^e)^(t=((t+=(s^i)+f[9]-640364487)<<4|t>>>28)+r<<0))+f[12]-421815835)<<11|i>>>21)+t<<0)^t)^(e=((e+=(h^r)+f[15]+530742520)<<16|e>>>16)+i<<0))+f[2]-995338651)<<23|r>>>9)+e<<0,r=((r+=((i=((i+=(r^((t=((t+=(e^(r|~i))+f[0]-198630844)<<6|t>>>26)+r<<0)|~e))+f[7]+1126891415)<<10|i>>>22)+t<<0)^((e=((e+=(t^(i|~r))+f[14]-1416354905)<<15|e>>>17)+i<<0)|~t))+f[5]-57434055)<<21|r>>>11)+e<<0,r=((r+=((i=((i+=(r^((t=((t+=(e^(r|~i))+f[12]+1700485571)<<6|t>>>26)+r<<0)|~e))+f[3]-1894986606)<<10|i>>>22)+t<<0)^((e=((e+=(t^(i|~r))+f[10]-1051523)<<15|e>>>17)+i<<0)|~t))+f[1]-2054922799)<<21|r>>>11)+e<<0,r=((r+=((i=((i+=(r^((t=((t+=(e^(r|~i))+f[8]+1873313359)<<6|t>>>26)+r<<0)|~e))+f[15]-30611744)<<10|i>>>22)+t<<0)^((e=((e+=(t^(i|~r))+f[6]-1560198380)<<15|e>>>17)+i<<0)|~t))+f[13]+1309151649)<<21|r>>>11)+e<<0,r=((r+=((i=((i+=(r^((t=((t+=(e^(r|~i))+f[4]-145523070)<<6|t>>>26)+r<<0)|~e))+f[11]-1120210379)<<10|i>>>22)+t<<0)^((e=((e+=(t^(i|~r))+f[2]+718787259)<<15|e>>>17)+i<<0)|~t))+f[9]-343485551)<<21|r>>>11)+e<<0,this.first?(this.h0=t+1732584193<<0,this.h1=r-271733879<<0,this.h2=e-1732584194<<0,this.h3=i+271733878<<0,this.first=!1):(this.h0=this.h0+t<<0,this.h1=this.h1+r<<0,this.h2=this.h2+e<<0,this.h3=this.h3+i<<0)},t.prototype.hex=function(){this.finalize();var t=this.h0,r=this.h1,e=this.h2,i=this.h3;return n[t>>4&15]+n[15&t]+n[t>>12&15]+n[t>>8&15]+n[t>>20&15]+n[t>>16&15]+n[t>>28&15]+n[t>>24&15]+n[r>>4&15]+n[15&r]+n[r>>12&15]+n[r>>8&15]+n[r>>20&15]+n[r>>16&15]+n[r>>28&15]+n[r>>24&15]+n[e>>4&15]+n[15&e]+n[e>>12&15]+n[e>>8&15]+n[e>>20&15]+n[e>>16&15]+n[e>>28&15]+n[e>>24&15]+n[i>>4&15]+n[15&i]+n[i>>12&15]+n[i>>8&15]+n[i>>20&15]+n[i>>16&15]+n[i>>28&15]+n[i>>24&15]},t.prototype.toString=t.prototype.hex,t.prototype.digest=function(){this.finalize();var t=this.h0,r=this.h1,e=this.h2,i=this.h3;return[255&t,t>>8&255,t>>16&255,t>>24&255,255&r,r>>8&255,r>>16&255,r>>24&255,255&e,e>>8&255,e>>16&255,e>>24&255,255&i,i>>8&255,i>>16&255,i>>24&255]},t.prototype.array=t.prototype.digest,t.prototype.arrayBuffer=function(){this.finalize();var t=new ArrayBuffer(16),r=new Uint32Array(t);return r[0]=this.h0,r[1]=this.h1,r[2]=this.h2,r[3]=this.h3,t},t.prototype.buffer=t.prototype.arrayBuffer,t.prototype.base64=function(){for(var t,r,e,i="",s=this.array(),h=0;h<15;)t=s[h++],r=s[h++],e=s[h++],i+=p[t>>>2]+p[63&(t<<4|r>>>4)]+p[63&(r<<2|e>>>6)]+p[63&e];return t=s[h],i+=p[t>>>2]+p[t<<4&63]+"=="};var _=v();f?module.exports=_:(i.md5=_,o&&define(function(){return _}))}(); \ No newline at end of file diff --git a/public/src/js/lib/oggmented-wasm.js b/public/src/js/lib/oggmented-wasm.js new file mode 100644 index 0000000..f10dc7b --- /dev/null +++ b/public/src/js/lib/oggmented-wasm.js @@ -0,0 +1,46 @@ + +var Oggmented = (function() { + var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; + + return ( +function(Oggmented) { + Oggmented = Oggmented || {}; + +(function(b,c){function m(h){delete k[h]}function l(h){if(e)setTimeout(l,0,h);else{var t=k[h];if(t){e=!0;try{var p=t.A,y=t.v;switch(y.length){case 0:p();break;case 1:p(y[0]);break;case 2:p(y[0],y[1]);break;case 3:p(y[0],y[1],y[2]);break;default:p.apply(c,y)}}finally{m(h),e=!1}}}}function q(){r=function(h){process.C(function(){l(h)})}}function u(){if(b.postMessage&&!b.importScripts){var h=!0,t=b.onmessage;b.onmessage=function(){h=!1};b.postMessage("","*");b.onmessage=t;return h}}function w(){function h(p){p.source=== +b&&"string"===typeof p.data&&0===p.data.indexOf(t)&&l(+p.data.slice(t.length))}var t="setImmediate$"+Math.random()+"$";b.addEventListener?b.addEventListener("message",h,!1):b.attachEvent("onmessage",h);r=function(p){b.postMessage(t+p,"*")}}function v(){var h=new MessageChannel;h.port1.onmessage=function(t){l(t.data)};r=function(t){h.port2.postMessage(t)}}function A(){var h=f.documentElement;r=function(t){var p=f.createElement("script");p.onreadystatechange=function(){l(t);p.onreadystatechange=null; +h.removeChild(p);p=null};h.appendChild(p)}}function d(){r=function(h){setTimeout(l,0,h)}}if(!b.setImmediate){var g=1,k={},e=!1,f=b.document,r,n=Object.getPrototypeOf&&Object.getPrototypeOf(b);n=n&&n.setTimeout?n:b;"[object process]"==={}.toString.call(b.process)?q():u()?w():b.MessageChannel?v():f&&"onreadystatechange"in f.createElement("script")?A():d();n.setImmediate=function(h){"function"!==typeof h&&(h=new Function(""+h));for(var t=Array(arguments.length-1),p=0;p{try{var {channels:l,length:q,rate:u}=(g=>{const k=g.byteLength,e=B(k);g=new Int8Array(g);C.set(g,e);D("open_buffer","number",["number","number"],[e,k]);return{channels:ba(),length:ca(),rate:da()}})(b),w=aa.createBuffer(l,q,u),v=B(Uint32Array.BYTES_PER_ELEMENT),A=0}catch(g){return m&&m(g)}const d=()=>{try{const g=Date.now();let k;for(;k=ea(v);){const e=fa(v,"*"),f=new Uint32Array(ha.buffer,e,l);for(let r=0;r>0];case "i8":return C[b>>0];case "i16":return ka[b>>1];case "i32":return M[b>>2];case "i64":return M[b>>2];case "float":return E[b>>2];case "double":return la[b>>3];default:L("invalid type for getValue: "+c)}return null}var N,O=!1;function ma(b){var c=a["_"+b];c||L("Assertion failed: Cannot call unknown function "+(b+", make sure it is exported"));return c} +function D(b,c,m,l){var q={string:function(d){var g=0;if(null!==d&&void 0!==d&&0!==d){var k=(d.length<<2)+1;g=P(k);var e=g,f=Q;if(0=n){var h=d.charCodeAt(++r);n=65536+((n&1023)<<10)|h&1023}if(127>=n){if(e>=k)break;f[e++]=n}else{if(2047>=n){if(e+1>=k)break;f[e++]=192|n>>6}else{if(65535>=n){if(e+2>=k)break;f[e++]=224|n>>12}else{if(e+3>=k)break;f[e++]=240|n>>18;f[e++]=128|n>>12&63}f[e++]=128|n>>6&63}f[e++]=128|n&63}}f[e]= +0}}return g},array:function(d){var g=P(d.length);C.set(d,g);return g}},u=ma(b),w=[];b=0;if(l)for(var v=0;v=k);)++e;if(16f?k+=String.fromCharCode(f):(f-=65536,k+=String.fromCharCode(55296|f>>10,56320|f&1023))}}else k+=String.fromCharCode(f)}d=k}}else d="";else d="boolean"===c?!!d:d;return d}(m);0!==b&&pa(b);return m}var oa="undefined"!==typeof TextDecoder?new TextDecoder("utf8"):void 0,R,C,Q,ka,M,ha,E,la; +function qa(b){R=b;a.HEAP8=C=new Int8Array(b);a.HEAP16=ka=new Int16Array(b);a.HEAP32=M=new Int32Array(b);a.HEAPU8=Q=new Uint8Array(b);a.HEAPU16=new Uint16Array(b);a.HEAPU32=ha=new Uint32Array(b);a.HEAPF32=E=new Float32Array(b);a.HEAPF64=la=new Float64Array(b)}var ra=a.INITIAL_MEMORY||16777216;a.wasmMemory?N=a.wasmMemory:N=new WebAssembly.Memory({initial:ra/65536,maximum:32768});N&&(R=N.buffer);ra=R.byteLength;qa(R);var S,sa=[],ta=[],ua=[],va=[];function wa(){var b=a.preRun.shift();sa.unshift(b)} +var T=0,U=null,V=null;a.preloadedImages={};a.preloadedAudios={};function L(b){if(a.onAbort)a.onAbort(b);J(b);O=!0;b=new WebAssembly.RuntimeError("abort("+b+"). Build with -s ASSERTIONS=1 for more info.");z(b);throw b;}function xa(){var b=W;return String.prototype.startsWith?b.startsWith("data:application/octet-stream;base64,"):0===b.indexOf("data:application/octet-stream;base64,")}var W="oggmented-wasm.wasm";if(!xa()){var ya=W;W=a.locateFile?a.locateFile(ya,I):I+ya} +function za(){try{if(K)return new Uint8Array(K);throw"both async and sync fetching of the wasm failed";}catch(b){L(b)}}function Aa(){return K||"function"!==typeof fetch?Promise.resolve().then(za):fetch(W,{credentials:"same-origin"}).then(function(b){if(!b.ok)throw"failed to load wasm binary file at '"+W+"'";return b.arrayBuffer()}).catch(function(){return za()})} +function X(b){for(;0>>=0;var c=Q.length;if(2147483648=m;m*=2){var l=c*(1+.2/m);l=Math.min(l,b+100663296);l=Math.max(16777216,b,l);0>>16);qa(N.buffer);var q=1;break a}catch(u){}q=void 0}if(q)return!0}return!1},d:function(b){if(!noExitRuntime){if(a.onExit)a.onExit(b);O=!0}ja(b,new Ca(b))},a:N}; +(function(){function b(q){a.asm=q.exports;S=a.asm.e;T--;a.monitorRunDependencies&&a.monitorRunDependencies(T);0==T&&(null!==U&&(clearInterval(U),U=null),V&&(q=V,V=null,q()))}function c(q){b(q.instance)}function m(q){return Aa().then(function(u){return WebAssembly.instantiate(u,l)}).then(q,function(u){J("failed to asynchronously prepare wasm: "+u);L(u)})}var l={a:Da};T++;a.monitorRunDependencies&&a.monitorRunDependencies(T);if(a.instantiateWasm)try{return a.instantiateWasm(l,b)}catch(q){return J("Module.instantiateWasm callback failed with error: "+ +q),!1}(function(){return K||"function"!==typeof WebAssembly.instantiateStreaming||xa()||"function"!==typeof fetch?m(c):fetch(W,{credentials:"same-origin"}).then(function(q){return WebAssembly.instantiateStreaming(q,l).then(c,function(u){J("wasm streaming compile failed: "+u);J("falling back to ArrayBuffer instantiation");return m(c)})})})().catch(z);return{}})();var Ba=a.___wasm_call_ctors=function(){return(Ba=a.___wasm_call_ctors=a.asm.f).apply(null,arguments)}; +a._open_buffer=function(){return(a._open_buffer=a.asm.g).apply(null,arguments)};var ia=a._close_buffer=function(){return(ia=a._close_buffer=a.asm.h).apply(null,arguments)},ca=a._get_length=function(){return(ca=a._get_length=a.asm.i).apply(null,arguments)},ba=a._get_channels=function(){return(ba=a._get_channels=a.asm.j).apply(null,arguments)},da=a._get_rate=function(){return(da=a._get_rate=a.asm.k).apply(null,arguments)};a._get_time=function(){return(a._get_time=a.asm.l).apply(null,arguments)}; +a._get_streams=function(){return(a._get_streams=a.asm.m).apply(null,arguments)}; +var ea=a._read_float=function(){return(ea=a._read_float=a.asm.n).apply(null,arguments)},F=a._free=function(){return(F=a._free=a.asm.o).apply(null,arguments)},B=a._malloc=function(){return(B=a._malloc=a.asm.p).apply(null,arguments)},na=a.stackSave=function(){return(na=a.stackSave=a.asm.q).apply(null,arguments)},pa=a.stackRestore=function(){return(pa=a.stackRestore=a.asm.r).apply(null,arguments)},P=a.stackAlloc=function(){return(P=a.stackAlloc=a.asm.s).apply(null,arguments)};a.ccall=D;a.getValue=fa; +var Y;function Ca(b){this.name="ExitStatus";this.message="Program terminated with exit("+b+")";this.status=b}V=function Ea(){Y||Z();Y||(V=Ea)}; +function Z(){function b(){if(!Y&&(Y=!0,a.calledRun=!0,!O)){X(ta);X(ua);x(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;){var c=a.postRun.shift();va.unshift(c)}X(va)}}if(!(0 { + this.screen.innerHTML = page + })) + + promises.push(this.ajax("api/config").then(conf => { + gameConfig = JSON.parse(conf) + })) + + Promise.all(promises).then(this.run.bind(this)) + } + run(){ + this.promises = [] + this.loaderDiv = document.querySelector("#loader") + this.loaderPercentage = document.querySelector("#loader .percentage") + this.loaderProgress = document.querySelector("#loader .progress") + + this.queryString = gameConfig._version.commit_short ? "?" + gameConfig._version.commit_short : "" + + if(gameConfig.custom_js){ + this.addPromise(this.loadScript(gameConfig.custom_js), gameConfig.custom_js) + } + var oggSupport = new Audio().canPlayType("audio/ogg;codecs=vorbis") + if(!oggSupport){ + assets.js.push("lib/oggmented-wasm.js") + } + assets.js.forEach(name => { + this.addPromise(this.loadScript("src/js/" + name), "src/js/" + name) + }) + + var pageVersion = versionLink.href + var index = pageVersion.lastIndexOf("/") + if(index !== -1){ + pageVersion = pageVersion.slice(index + 1) + } + this.addPromise(new Promise((resolve, reject) => { + if( + versionLink.href !== gameConfig._version.url && + gameConfig._version.commit && + versionLink.href.indexOf(gameConfig._version.commit) === -1 + ){ + reject("Version on the page and config does not match\n(page: " + pageVersion + ",\nconfig: "+ gameConfig._version.commit + ")") + } + var cssCount = document.styleSheets.length + assets.css.length + assets.css.forEach(name => { + var stylesheet = document.createElement("link") + stylesheet.rel = "stylesheet" + stylesheet.href = "src/css/" + name + this.queryString + document.head.appendChild(stylesheet) + }) + var checkStyles = () => { + if(document.styleSheets.length >= cssCount){ + resolve() + clearInterval(interval) + } + } + var interval = setInterval(checkStyles, 100) + checkStyles() + })) + + for(var name in assets.fonts){ + var url = gameConfig.assets_baseurl + "fonts/" + assets.fonts[name] + this.addPromise(new FontFace(name, "url('" + url + "')").load().then(font => { + document.fonts.add(font) + }), url) + } + + assets.img.forEach(name => { + var id = this.getFilename(name) + var image = document.createElement("img") + image.crossOrigin = "anonymous" + var url = gameConfig.assets_baseurl + "img/" + name + this.addPromise(pageEvents.load(image), url) + image.id = name + image.src = url + this.assetsDiv.appendChild(image) + assets.image[id] = image + }) + + var css = [] + for(let selector in assets.cssBackground){ + let name = assets.cssBackground[selector] + var url = gameConfig.assets_baseurl + "img/" + name + this.addPromise(loader.ajax(url, request => { + request.responseType = "blob" + }).then(blob => { + var id = this.getFilename(name) + var image = document.createElement("img") + let blobUrl = URL.createObjectURL(blob) + var promise = pageEvents.load(image).then(() => { + var gradient = "" + if(selector === ".pattern-bg"){ + loader.screen.style.backgroundImage = "url(\"" + blobUrl + "\")" + }else if(selector === "#song-search"){ + gradient = this.songSearchGradient + } + css.push(this.cssRuleset({ + [selector]: { + "background-image": gradient + "url(\"" + blobUrl + "\")" + } + })) + }) + image.id = name + image.src = blobUrl + this.assetsDiv.appendChild(image) + assets.image[id] = image + return promise + }), url) + } + + assets.views.forEach(name => { + var id = this.getFilename(name) + var url = "src/views/" + name + this.queryString + this.addPromise(this.ajax(url).then(page => { + assets.pages[id] = page + }), url) + }) + + this.addPromise(this.ajax("api/categories").then(cats => { + assets.categories = JSON.parse(cats) + assets.categories.forEach(cat => { + if(cat.song_skin){ + cat.songSkin = cat.song_skin //rename the song_skin property and add category title to categories array + delete cat.song_skin + cat.songSkin.infoFill = cat.songSkin.info_fill + delete cat.songSkin.info_fill + } + }) + + assets.categories.push({ + title: "default", + songSkin: { + background: "#ececec", + border: ["#fbfbfb", "#8b8b8b"], + outline: "#656565", + infoFill: "#656565" + } + }) + }), "api/categories") + + var url = gameConfig.assets_baseurl + "img/vectors.json" + this.queryString + this.addPromise(this.ajax(url).then(response => { + vectors = JSON.parse(response) + }), url) + + this.afterJSCount = + [ + "api/songs", + "blurPerformance", + "categories" + ].length + + assets.audioSfx.length + + assets.audioMusic.length + + assets.audioSfxLR.length + + assets.audioSfxLoud.length + + (gameConfig.accounts ? 1 : 0) + + Promise.all(this.promises).then(() => { + if(this.error){ + return + } + + var style = document.createElement("style") + style.appendChild(document.createTextNode(css.join("\n"))) + document.head.appendChild(style) + + this.addPromise(this.ajax("api/songs").then(songs => { + songs = JSON.parse(songs) + songs.forEach(song => { + var directory = gameConfig.songs_baseurl + song.id + "/" + var songExt = song.music_type ? song.music_type : "mp3" + song.music = new RemoteFile(directory + "main." + songExt) + if(song.type === "tja"){ + song.chart = new RemoteFile(directory + "main.tja") + }else{ + song.chart = {separateDiff: true} + for(var diff in song.courses){ + if(song.courses[diff]){ + song.chart[diff] = new RemoteFile(directory + diff + ".osu") + } + } + } + if(song.lyrics){ + song.lyricsFile = new RemoteFile(directory + "main.vtt") + } + if(song.preview > 0){ + song.previewMusic = new RemoteFile(directory + "preview." + gameConfig.preview_type) + } + }) + assets.songsDefault = songs + assets.songs = assets.songsDefault + }), "api/songs") + + var categoryPromises = [] + assets.categories //load category backgrounds to DOM + .filter(cat => cat.songSkin && cat.songSkin.bg_img) + .forEach(cat => { + let name = cat.songSkin.bg_img + var url = gameConfig.assets_baseurl + "img/" + name + categoryPromises.push(loader.ajax(url, request => { + request.responseType = "blob" + }).then(blob => { + var id = this.getFilename(name) + var image = document.createElement("img") + let blobUrl = URL.createObjectURL(blob) + var promise = pageEvents.load(image) + image.id = name + image.src = blobUrl + this.assetsDiv.appendChild(image) + assets.image[id] = image + return promise + }).catch(response => { + return this.errorMsg(response, url) + })) + }) + this.addPromise(Promise.all(categoryPromises)) + + snd.buffer = new SoundBuffer() + if(!oggSupport){ + snd.buffer.oggDecoder = snd.buffer.fallbackDecoder + } + snd.musicGain = snd.buffer.createGain() + snd.sfxGain = snd.buffer.createGain() + snd.previewGain = snd.buffer.createGain() + snd.sfxGainL = snd.buffer.createGain("left") + snd.sfxGainR = snd.buffer.createGain("right") + snd.sfxLoudGain = snd.buffer.createGain() + snd.buffer.setCrossfade( + [snd.musicGain, snd.previewGain], + [snd.sfxGain, snd.sfxGainL, snd.sfxGainR], + 0.5 + ) + snd.sfxLoudGain.setVolume(1.2) + snd.buffer.saveSettings() + + this.afterJSCount = 0 + + assets.audioSfx.forEach(name => { + this.addPromise(this.loadSound(name, snd.sfxGain), this.soundUrl(name)) + }) + assets.audioMusic.forEach(name => { + this.addPromise(this.loadSound(name, snd.musicGain), this.soundUrl(name)) + }) + assets.audioSfxLR.forEach(name => { + this.addPromise(this.loadSound(name, snd.sfxGain).then(sound => { + var id = this.getFilename(name) + assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL) + assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR) + }), this.soundUrl(name)) + }) + assets.audioSfxLoud.forEach(name => { + this.addPromise(this.loadSound(name, snd.sfxLoudGain), this.soundUrl(name)) + }) + + this.canvasTest = new CanvasTest() + this.addPromise(this.canvasTest.blurPerformance().then(result => { + perf.blur = result + if(result > 1000 / 50){ + // Less than 50 fps with blur enabled + disableBlur = true + } + }), "blurPerformance") + + if(gameConfig.accounts){ + this.addPromise(this.ajax("api/scores/get").then(response => { + response = JSON.parse(response) + if(response.status === "ok"){ + account.loggedIn = true + account.username = response.username + account.displayName = response.display_name + account.don = response.don + scoreStorage.load(response.scores) + pageEvents.send("login", account.username) + } + }), "api/scores/get") + } + + settings = new Settings() + pageEvents.setKbd() + scoreStorage = new ScoreStorage() + db = new IDB("taiko", "store") + plugins = new Plugins() + + if(localStorage.getItem("lastSearchQuery")){ + localStorage.removeItem("lastSearchQuery") + } + + Promise.all(this.promises).then(() => { + if(this.error){ + return + } + if(!account.loggedIn){ + scoreStorage.load() + } + for(var i in assets.songsDefault){ + var song = assets.songsDefault[i] + if(!song.hash){ + song.hash = song.title + } + scoreStorage.songTitles[song.title] = song.hash + var score = scoreStorage.get(song.hash, false, true) + if(score){ + score.title = song.title + } + } + var promises = [] + + var readyEvent = "normal" + var songId + var hashLower = location.hash.toLowerCase() + p2 = new P2Connection() + if(hashLower.startsWith("#song=")){ + var number = parseInt(location.hash.slice(6)) + if(number > 0){ + songId = number + readyEvent = "song-id" + } + }else if(location.hash.length === 6){ + p2.hashLock = true + promises.push(new Promise(resolve => { + p2.open() + pageEvents.add(p2, "message", response => { + if(response.type === "session"){ + pageEvents.send("session-start", "invited") + readyEvent = "session-start" + resolve() + }else if(response.type === "gameend"){ + p2.hash("") + p2.hashLock = false + readyEvent = "session-expired" + resolve() + } + }) + p2.send("invite", { + id: location.hash.slice(1).toLowerCase(), + name: account.loggedIn ? account.displayName : null, + don: account.loggedIn ? account.don : null + }) + setTimeout(() => { + if(p2.socket.readyState !== 1){ + p2.hash("") + p2.hashLock = false + resolve() + } + }, 10000) + }).then(() => { + pageEvents.remove(p2, "message") + })) + }else{ + p2.hash("") + } + + promises.push(this.canvasTest.drawAllImages().then(result => { + perf.allImg = result + })) + + if(gameConfig.plugins){ + gameConfig.plugins.forEach(obj => { + if(obj.url){ + var plugin = plugins.add(obj.url, { + hide: obj.hide + }) + if(plugin){ + plugin.loadErrors = true + promises.push(plugin.load(true).then(() => { + if(obj.start){ + return plugin.start(false, true) + } + }).catch(response => { + return this.errorMsg(response, obj.url) + })) + } + } + }) + } + + Promise.all(promises).then(() => { + perf.load = Date.now() - this.startTime + this.canvasTest.clean() + this.clean() + this.callback(songId) + this.ready = true + pageEvents.send("ready", readyEvent) + }, e => this.errorMsg(e)) + }, e => this.errorMsg(e)) + }) + } + addPromise(promise, url){ + this.promises.push(promise) + promise.then(this.assetLoaded.bind(this), response => { + return this.errorMsg(response, url) + }) + } + soundUrl(name){ + return gameConfig.assets_baseurl + "audio/" + name + } + loadSound(name, gain){ + var id = this.getFilename(name) + return gain.load(new RemoteFile(this.soundUrl(name))).then(sound => { + assets.sounds[id] = sound + }) + } + getFilename(name){ + return name.slice(0, name.lastIndexOf(".")) + } + errorMsg(error, url){ + var rethrow + if(url || error){ + if(typeof error === "object" && error.constructor === Error){ + rethrow = error + error = error.stack || "" + var index = error.indexOf("\n ") + if(index !== -1){ + error = error.slice(0, index) + } + }else if(Array.isArray(error)){ + error = error[0] + } + if(url){ + error = (error ? error + ": " : "") + url + } + this.errorMessages.push(error) + pageEvents.send("loader-error", url || error) + } + if(!this.error){ + this.error = true + cancelTouch = false + this.loaderDiv.classList.add("loaderError") + if(typeof allStrings === "object"){ + var lang = localStorage.lang + if(!lang){ + var userLang = navigator.languages.slice() + userLang.unshift(navigator.language) + for(var i in userLang){ + for(var j in allStrings){ + if(allStrings[j].regex.test(userLang[i])){ + lang = j + } + } + } + } + if(!lang){ + lang = "en" + } + loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang] && allStrings[lang].errorOccured || allStrings.en.errorOccured + } + var loaderError = loader.screen.getElementsByClassName("loader-error-div")[0] + loaderError.style.display = "flex" + var diagTxt = loader.screen.getElementsByClassName("diag-txt")[0] + var debugLink = loader.screen.getElementsByClassName("debug-link")[0] + if(navigator.userAgent.indexOf("Android") >= 0){ + var iframe = document.createElement("iframe") + diagTxt.appendChild(iframe) + var body = iframe.contentWindow.document.body + body.setAttribute("style", ` + font-family: monospace; + margin: 2px 0 0 2px; + white-space: pre-wrap; + word-break: break-all; + cursor: text; + `) + body.setAttribute("onblur", ` + getSelection().removeAllRanges() + `) + this.errorTxt = { + element: body, + method: "innerText" + } + }else{ + var textarea = document.createElement("textarea") + textarea.readOnly = true + diagTxt.appendChild(textarea) + if(!this.touchEnabled){ + textarea.addEventListener("focus", () => { + textarea.select() + }) + textarea.addEventListener("blur", () => { + getSelection().removeAllRanges() + }) + } + this.errorTxt = { + element: textarea, + method: "value" + } + } + var show = () => { + diagTxt.style.display = "block" + debugLink.style.display = "none" + } + debugLink.addEventListener("click", show) + debugLink.addEventListener("touchstart", show) + this.clean(true) + } + var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount)) + this.errorTxt.element[this.errorTxt.method] = "```\n" + this.errorMessages.join("\n") + "\nPercentage: " + percentage + "%\n```" + if(rethrow || error){ + console.error(rethrow || error) + } + return Promise.reject() + } + assetLoaded(){ + if(!this.error){ + this.loadedAssets++ + var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount)) + this.loaderProgress.style.width = percentage + "%" + this.loaderPercentage.firstChild.data = percentage + "%" + } + } + changePage(name, patternBg){ + this.screen.innerHTML = assets.pages[name] + this.screen.classList[patternBg ? "add" : "remove"]("pattern-bg") + } + cssRuleset(rulesets){ + var css = [] + for(var selector in rulesets){ + var declarationsObj = rulesets[selector] + var declarations = [] + for(var property in declarationsObj){ + var value = declarationsObj[property] + declarations.push("\t" + property + ": " + value + ";") + } + css.push(selector + "{\n" + declarations.join("\n") + "\n}") + } + return css.join("\n") + } + ajax(url, customRequest, customResponse){ + var request = new XMLHttpRequest() + request.open("GET", url) + var promise = pageEvents.load(request) + if(!customResponse){ + promise = promise.then(() => { + if(request.status === 200){ + return request.response + }else{ + return Promise.reject(`${url} (${request.status})`) + } + }) + } + if(customRequest){ + customRequest(request) + } + request.send() + return promise + } + loadScript(url){ + var script = document.createElement("script") + var url = url + this.queryString + var promise = pageEvents.load(script) + script.src = url + document.head.appendChild(script) + return promise + } + getCsrfToken(){ + return this.ajax("api/csrftoken").then(response => { + var json = JSON.parse(response) + if(json.status === "ok"){ + return Promise.resolve(json.token) + }else{ + return Promise.reject() + } + }) + } + clean(error){ + delete this.loaderDiv + delete this.loaderPercentage + delete this.loaderProgress + if(!error){ + delete this.promises + delete this.errorText + } + pageEvents.remove(root, "touchstart") + } +} diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js new file mode 100644 index 0000000..6c8c0e8 --- /dev/null +++ b/public/src/js/loadsong.js @@ -0,0 +1,397 @@ +class LoadSong{ + constructor(...args){ + this.init(...args) + } + init(selectedSong, autoPlayEnabled, multiplayer, touchEnabled){ + this.selectedSong = selectedSong + this.autoPlayEnabled = autoPlayEnabled + this.multiplayer = multiplayer + this.touchEnabled = touchEnabled + var resolution = settings.getItem("resolution") + this.imgScale = 1 + if(resolution === "medium"){ + this.imgScale = 0.75 + }else if(resolution === "low"){ + this.imgScale = 0.5 + }else if(resolution === "lowest"){ + this.imgScale = 0.25 + } + + loader.changePage("loadsong", true) + var loadingText = document.getElementById("loading-text") + loadingText.appendChild(document.createTextNode(strings.loading)) + loadingText.setAttribute("alt", strings.loading) + if(multiplayer){ + var cancel = document.getElementById("p2-cancel-button") + cancel.appendChild(document.createTextNode(strings.cancel)) + cancel.setAttribute("alt", strings.cancel) + } + this.run() + pageEvents.send("load-song", { + selectedSong: selectedSong, + autoPlayEnabled: autoPlayEnabled, + multiplayer: multiplayer, + touchEnabled: touchEnabled + }) + } + run(){ + var song = this.selectedSong + var id = song.folder + var songObj + this.promises = [] + if(id !== "calibration"){ + assets.sounds["v_start"].play() + assets.songs.forEach(song => { + if(song.id === id){ + songObj = song + }else{ + if(song.sound){ + song.sound.clean() + delete song.sound + } + delete song.lyricsData + } + }) + }else{ + songObj = { + music: "muted", + custom: true + } + } + this.songObj = songObj + song.songBg = this.randInt(1, 5) + song.songStage = this.randInt(1, 3) + song.donBg = this.randInt(1, 6) + if(this.songObj && this.songObj.category_id === 9){ + LoadSong.insertBackgroundVideo(this.songObj.id) + } + if(song.songSkin && song.songSkin.name){ + var imgLoad = [] + for(var type in song.songSkin){ + var value = song.songSkin[type] + if(["song", "stage", "don"].indexOf(type) !== -1 && value && value !== "none"){ + var filename = "bg_" + type + "_" + song.songSkin.name + if(value === "static"){ + imgLoad.push({ + filename: filename, + type: type + }) + }else{ + imgLoad.push({ + filename: filename + "_a", + type: type + }) + imgLoad.push({ + filename: filename + "_b", + type: type + }) + } + if(type === "don"){ + song.donBg = null + }else if(type === "song"){ + song.songBg = null + }else if(type === "stage"){ + song.songStage = null + } + } + } + var skinBase = gameConfig.assets_baseurl + "song_skins/" + for(var i = 0; i < imgLoad.length; i++){ + let filename = imgLoad[i].filename + let prefix = song.songSkin.prefix || "" + if((prefix + filename) in assets.image){ + continue + } + let img = document.createElement("img") + let force = imgLoad[i].type === "song" && this.touchEnabled + if(!songObj.custom){ + img.crossOrigin = "anonymous" + } + let promise = pageEvents.load(img) + this.addPromise(promise.then(() => { + return this.scaleImg(img, filename, prefix, force) + }), songObj.custom ? filename + ".png" : skinBase + filename + ".png") + if(songObj.custom){ + this.addPromise(song.songSkin[filename + ".png"].blob().then(blob => { + img.src = URL.createObjectURL(blob) + }), song.songSkin[filename + ".png"].url) + }else{ + img.src = skinBase + filename + ".png" + } + } + } + this.loadSongBg(id) + + if(songObj.sound && songObj.sound.buffer){ + songObj.sound.gain = snd.musicGain + }else if(songObj.music !== "muted"){ + this.addPromise(snd.musicGain.load(songObj.music).then(sound => { + songObj.sound = sound + }), songObj.music.url) + } + var chart = songObj.chart + if(chart && chart.separateDiff){ + var chartDiff = this.selectedSong.difficulty + chart = chart[chartDiff] + } + if(chart){ + this.addPromise(chart.read(song.type === "tja" ? "utf-8" : "").then(data => { + this.songData = data.replace(/\0/g, "").split("\n") + }), chart.url) + }else{ + this.songData = "" + } + if(songObj.lyricsFile && !songObj.lyricsData && !this.multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){ + this.addPromise(songObj.lyricsFile.read().then(data => { + songObj.lyricsData = data + }, () => {}), songObj.lyricsFile.url) + } + if(this.touchEnabled && !assets.image["touch_drum"]){ + let img = document.createElement("img") + img.crossOrigin = "anonymous" + var url = gameConfig.assets_baseurl + "img/touch_drum.png" + this.addPromise(pageEvents.load(img).then(() => { + return this.scaleImg(img, "touch_drum", "") + }), url) + img.src = url + } + var resultsImg = [ + "results_flowers", + "results_mikoshi", + "results_tetsuohana", + "results_tetsuohana2" + ] + resultsImg.forEach(id => { + if(!assets.image[id]){ + var img = document.createElement("img") + img.crossOrigin = "anonymous" + var url = gameConfig.assets_baseurl + "img/" + id + ".png" + this.addPromise(pageEvents.load(img).then(() => { + return this.scaleImg(img, id, "") + }), url) + img.src = url + } + }) + if(songObj.volume && songObj.volume !== 1){ + this.promises.push(new Promise(resolve => setTimeout(resolve, 500))) + } + Promise.all(this.promises).then(() => { + if(!this.error){ + this.setupMultiplayer() + } + }) + } + addPromise(promise, url){ + this.promises.push(promise.catch(response => { + this.errorMsg(response, url) + return Promise.resolve() + })) + } + errorMsg(error, url){ + if(!this.error){ + if(url){ + error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url + } + pageEvents.send("load-song-error", error) + errorMessage(new Error(error).stack) + var title = this.selectedSong.title + if(title !== this.selectedSong.originalTitle){ + title += " (" + this.selectedSong.originalTitle + ")" + } + assets.sounds["v_start"].stop() + setTimeout(() => { + this.clean() + new SongSelect(false, false, this.touchEnabled, null, { + name: "loadSongError", + title: title, + id: this.selectedSong.folder, + error: error + }) + }, 500) + } + this.error = true + } + loadSongBg(){ + var filenames = [] + if(this.selectedSong.songBg !== null){ + filenames.push("bg_song_" + this.selectedSong.songBg) + } + if(this.selectedSong.donBg !== null){ + filenames.push("bg_don_" + this.selectedSong.donBg) + if(this.multiplayer){ + filenames.push("bg_don2_" + this.selectedSong.donBg) + } + } + if(this.selectedSong.songStage !== null){ + filenames.push("bg_stage_" + this.selectedSong.songStage) + } + for(var i = 0; i < filenames.length; i++){ + var filename = filenames[i] + var stage = filename.startsWith("bg_stage_") + for(var letter = 0; letter < (stage ? 1 : 2); letter++){ + let filenameAb = filenames[i] + (stage ? "" : (letter === 0 ? "a" : "b")) + if(!(filenameAb in assets.image)){ + let img = document.createElement("img") + let force = filenameAb.startsWith("bg_song_") && this.touchEnabled + img.crossOrigin = "anonymous" + var url = gameConfig.assets_baseurl + "img/" + filenameAb + ".png" + this.addPromise(pageEvents.load(img).then(() => { + return this.scaleImg(img, filenameAb, "", force) + }), url) + img.src = url + } + } + } + } + scaleImg(img, filename, prefix, force){ + return new Promise((resolve, reject) => { + var scale = this.imgScale + if(force && scale > 0.5){ + scale = 0.5 + } + var canvas = document.createElement("canvas") + var w = Math.floor(img.width * scale) + var h = Math.floor(img.height * scale) + canvas.width = Math.max(1, w) + canvas.height = Math.max(1, h) + var ctx = canvas.getContext("2d") + ctx.drawImage(img, 0, 0, w, h) + var saveScaled = url => { + let img2 = document.createElement("img") + pageEvents.load(img2).then(() => { + assets.image[prefix + filename] = img2 + loader.assetsDiv.appendChild(img2) + resolve() + }, reject) + img2.id = prefix + filename + img2.src = url + } + if("toBlob" in canvas){ + canvas.toBlob(blob => { + saveScaled(URL.createObjectURL(blob)) + }) + }else{ + saveScaled(canvas.toDataURL()) + } + }) + } + randInt(min, max){ + return Math.floor(Math.random() * (max - min + 1)) + min + } + setupMultiplayer(){ + var song = this.selectedSong + + if(this.multiplayer){ + var loadingText = document.getElementsByClassName("loading-text")[0] + loadingText.firstChild.data = strings.waitingForP2 + loadingText.setAttribute("alt", strings.waitingForP2) + + this.cancelButton = document.getElementById("p2-cancel-button") + this.cancelButton.style.display = "inline-block" + pageEvents.add(this.cancelButton, ["mousedown", "touchstart"], this.cancelLoad.bind(this)) + + this.song2Data = this.songData + this.selectedSong2 = song + pageEvents.add(p2, "message", event => { + if(event.type === "gameload"){ + this.cancelButton.style.display = "" + + if(event.value.diff === song.difficulty){ + this.startMultiplayer() + }else{ + this.selectedSong2 = {} + for(var i in this.selectedSong){ + this.selectedSong2[i] = this.selectedSong[i] + } + this.selectedSong2.difficulty = event.value.diff + var chart = this.songObj.chart + var chartDiff = this.selectedSong2.difficulty + if(song.type === "tja" || !chart || !chart.separateDiff || !chart[chartDiff]){ + this.startMultiplayer() + }else{ + chart[chartDiff].read(song.type === "tja" ? "utf-8" : "").then(data => { + this.song2Data = data.replace(/\0/g, "").split("\n") + }, () => {}).then(() => { + this.startMultiplayer() + }) + } + } + }else if(event.type === "gamestart"){ + this.clean() + p2.clearMessage("songsel") + var taikoGame1 = new Controller(song, this.songData, false, 1, this.touchEnabled) + var taikoGame2 = new Controller(this.selectedSong2, this.song2Data, true, 2, this.touchEnabled) + taikoGame1.run(taikoGame2) + pageEvents.send("load-song-player2", this.selectedSong2) + }else if(event.type === "left" || event.type === "gameend"){ + this.clean() + new SongSelect(false, false, this.touchEnabled) + } + }) + p2.send("join", { + id: song.folder, + diff: song.difficulty, + name: account.loggedIn ? account.displayName : null, + don: account.loggedIn ? account.don : null + }) + }else{ + this.clean() + var taikoGame = new Controller(song, this.songData, this.autoPlayEnabled, false, this.touchEnabled) + taikoGame.run() + } + } + startMultiplayer(repeat){ + if(document.hasFocus()){ + p2.send("gamestart") + }else{ + if(!repeat){ + assets.sounds["v_sanka"].play() + pageEvents.send("load-song-unfocused") + } + setTimeout(() => { + this.startMultiplayer(true) + }, 100) + } + } + cancelLoad(event){ + if(event.type === "mousedown"){ + if(event.which !== 1){ + return + } + }else{ + event.preventDefault() + } + p2.send("leave") + assets.sounds["se_don"].play() + this.cancelButton.style.pointerEvents = "none" + pageEvents.send("load-song-cancel") + } + clean(){ + delete this.promises + delete this.songObj + delete this.videoElement + pageEvents.remove(p2, "message") + if(this.cancelButton){ + pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"]) + delete this.cancelButton + } + } + + static insertBackgroundVideo(songId) { + const video = document.createElement("video"); + video.src = `songs/${songId}/main.mp4`; + video.autoplay = true; + video.muted = true; // 可选:静音 + video.style.objectFit = 'cover'; + video.style.position = 'fixed'; + video.style.top = "0"; + video.style.left = "0"; + video.style.zIndex = "0"; // 背景视频 + video.style.width = "100vw"; + video.style.height = "100vh"; + document.body.appendChild(video); + window.videoElement = video; + } + + +} diff --git a/public/src/js/logo.js b/public/src/js/logo.js new file mode 100644 index 0000000..2979625 --- /dev/null +++ b/public/src/js/logo.js @@ -0,0 +1,193 @@ +class Logo{ + constructor(...args){ + this.init(...args) + } + init(){ + this.canvas = document.getElementById("logo") + this.ctx = this.canvas.getContext("2d") + this.pathSvg = failedTests.indexOf("Path2D SVG") === -1 && vectors.logo1 + this.symbolFont = "TnT, Meiryo, sans-serif" + this.symbols = [{ + x: 315, y: 18, xAlt: 15, scale: true, text: "ブ", + path: new Path2D(vectors.logo5) + }, { + x: 267, y: 50, yAlt: -34, scale: true, text: "ェ", + path: new Path2D(vectors.logo4) + }, { + x: 197, y: 7, xAlt: 15, scale: true, text: "ウ", + path: new Path2D(vectors.logo3) + }, { + x: 87, y: 7, xAlt: 15, text: "鼓", + path: new Path2D(vectors.logo2), + shadow: new Path2D(vectors.logo2Shadow) + }, { + x: 22, y: 16, xAlt: 10, scaleAlt: true, text: "太", + path: new Path2D(vectors.logo1) + }] + pageEvents.add(window, "resize", this.update.bind(this)) + } + updateSubtitle(){ + this.subtitleGradient = ["#df600d", "#d8446f", "#b2147b", "#428ac2", "#1f9099"] + this.subtitle = [] + this.subtitleW = 0 + var index = 0 + var latinLowercase = /[a-z]/ + for(var i = 0; i < strings.taikoWeb.length; i++){ + var letter = strings.taikoWeb[i] + var width = 57 + if(letter === "ェ"){ + width = 40 + }else if(letter === " "){ + width = 20 + }else if(letter === "i"){ + width = 22 + }else if(letter === "T"){ + width = 30 + }else if(latinLowercase.test(letter)){ + width = 38 + } + this.subtitle.push({ + letter: letter, + x: this.subtitleW + width / 2, + index: letter === " " ? index : index++ + }) + this.subtitleW += width + } + this.update() + } + update(){ + var ctx = this.ctx + ctx.save() + + this.width = 1170 + this.height = 390 + var pixelRatio = window.devicePixelRatio || 1 + var winW = this.canvas.offsetWidth * pixelRatio + var winH = this.canvas.offsetHeight * pixelRatio + this.canvas.width = Math.max(1, winW) + this.canvas.height = Math.max(1, winH) + ctx.scale(winW / this.width, winH / this.height) + + ctx.lineJoin = "round" + ctx.miterLimit = 1 + ctx.textBaseline = "top" + ctx.textAlign = "center" + if(!this.pathSvg){ + ctx.font = "100px " + this.symbolFont + } + + for(var i = 0; i < this.symbols.length; i++){ + ctx.strokeStyle = "#3f0406" + ctx.lineWidth = 13.5 + this.drawSymbol(this.symbols[i], "stroke", 4) + } + ctx.font = this.bold(strings.font) + "55px " + strings.font + this.subtitleIterate((letter, x) => { + ctx.lineWidth = strings.id === "en" ? 19 : 18.5 + ctx.strokeStyle = "#3f0406" + ctx.strokeText(letter, x, 315) + }) + if(this.pathSvg){ + ctx.fillStyle = "#3f0406" + ctx.fillRect(400, 180, 30, 50) + }else{ + ctx.font = "100px " + this.symbolFont + } + for(var i = 0; i < this.symbols.length; i++){ + var symbol = this.symbols[i] + ctx.strokeStyle = "#7c361e" + ctx.lineWidth = 13.5 + this.drawSymbol(symbol, "stroke") + ctx.strokeStyle = "#fff" + ctx.lineWidth = 7.5 + this.drawSymbol(symbol, "stroke") + if(this.pathSvg){ + var grd = ctx.createLinearGradient(0, 55 - symbol.y, 0, 95 - symbol.y) + grd.addColorStop(0, "#a41f1e") + grd.addColorStop(1, "#a86a29") + ctx.fillStyle = grd + this.drawSymbol(symbol, "fill") + ctx.save() + ctx.scale(symbol.scale ? 2.8 : 3.2, 3.2) + ctx.translate(symbol.x, symbol.y) + ctx.clip(symbol.path) + } + grd = ctx.createLinearGradient(0, 55 - symbol.y, 0, 95 - symbol.y) + grd.addColorStop(0, "#d80e11") + grd.addColorStop(1, "#e08f19") + ctx.fillStyle = grd + if(this.pathSvg){ + ctx.translate(3, 2) + ctx.fill(symbol.shadow || symbol.path) + ctx.restore() + }else{ + this.drawSymbol(symbol, "fill") + } + } + if(this.pathSvg){ + ctx.fillStyle = "#fff" + ctx.fillRect(382, 85, 30, 15) + ctx.fillRect(402, 145, 15, 15) + }else{ + ctx.font = this.bold(strings.font) + "55px " + strings.font + } + + this.subtitleIterate((letter, x) => { + ctx.lineWidth = strings.id === "en" ? 19 : 18.5 + ctx.strokeStyle = "#7c361e" + ctx.strokeText(letter, x, 305) + }) + this.subtitleIterate((letter, x, i) => { + ctx.lineWidth = strings.id === "en" ? 11 : 9.5 + ctx.strokeStyle = this.getSubtitleGradient(i) + ctx.fillStyle = "#fff" + ctx.strokeText(letter, x, 305) + ctx.fillText(letter, x, 305) + }) + + ctx.restore() + } + drawSymbol(symbol, action, y){ + var ctx = this.ctx + ctx.save() + ctx.scale((symbol.scale || !this.pathSvg && symbol.scaleAlt) ? 2.8 : 3.2, 3.2) + ctx.translate(symbol.x, symbol.y + (y || 0)) + if(this.pathSvg){ + ctx[action](symbol.path) + }else{ + ctx[action + "Text"](symbol.text, 30 + (symbol.xAlt || 0), -4 + (symbol.yAlt || 0)) + } + ctx.restore() + } + subtitleIterate(func){ + for(var i = this.subtitle.length; i--;){ + var subtitleObj = this.subtitle[i] + var x = (this.width - this.subtitleW) / 2 + subtitleObj.x + func(subtitleObj.letter, x, subtitleObj.index) + } + } + getSubtitleGradient(index){ + var sign = 1 + var position = 0 + var length = this.subtitleGradient.length - 1 + while(index >= 0){ + if(sign === 1){ + position = index % length + }else{ + position = length - (index % length) + } + sign *= -1 + index -= length + } + return this.subtitleGradient[position] + } + bold(font){ + return font === "Microsoft YaHei, sans-serif" ? "bold " : "" + } + clean(){ + pageEvents.remove(window, "resize") + delete this.symbols + delete this.ctx + delete this.canvas + } +} diff --git a/public/src/js/lyrics.js b/public/src/js/lyrics.js new file mode 100644 index 0000000..2e0bbdc --- /dev/null +++ b/public/src/js/lyrics.js @@ -0,0 +1,236 @@ +class Lyrics{ + constructor(...args){ + this.init(...args) + } + init(file, songOffset, div, parsed){ + this.div = div + this.stroke = document.createElement("div") + this.stroke.classList.add("stroke") + div.appendChild(this.stroke) + this.fill = document.createElement("div") + this.fill.classList.add("fill") + div.appendChild(this.fill) + this.current = 0 + this.shown = -1 + this.songOffset = songOffset || 0 + this.vttOffset = 0 + this.rLinebreak = /\n|\r\n/ + this.lines = parsed ? file : this.parseFile(file) + this.length = this.lines.length + } + parseFile(file){ + var lines = [] + var commands = file.split(/\n\n|\r\n\r\n/) + var arrow = " --> " + for(var i in commands){ + var matches = commands[i].match(this.rLinebreak) + if(matches){ + var cmd = commands[i].slice(0, matches.index) + var value = commands[i].slice(matches.index + 1) + }else{ + var cmd = commands[i] + var value = "" + } + if(cmd.startsWith("WEBVTT")){ + var nameValue = cmd.slice(7).split(";") + for(var j in nameValue){ + var [name, value] = nameValue[j].split(":") + if(name.trim().toLowerCase() === "offset"){ + this.vttOffset = (parseFloat(value.trim()) || 0) * 1000 + } + } + }else{ + var time = null + var index = cmd.indexOf(arrow) + if(index !== -1){ + time = cmd + }else{ + var matches = value.match(this.rLinebreak) + if(matches){ + var value1 = value.slice(0, matches.index) + index = value1.indexOf(arrow) + if(index !== -1){ + time = value1 + value = value.slice(index) + } + } + } + if(time !== null){ + var start = time.slice(0, index) + var end = time.slice(index + arrow.length) + var index = end.indexOf(" ") + if(index !== -1){ + end = end.slice(0, index) + } + var text = value.trim() + var textLang = "" + var firstLang = -1 + var index2 = -1 + while(true){ + var index1 = text.indexOf("", index1 + 6) + if(index2 === -1){ + break + } + var lang = text.slice(index1 + 6, index2).toLowerCase() + if(strings.preferEn && lang === "en" || strings.id === lang){ + var index3 = text.indexOf("= this.length){ + return + } + ms += this.songOffset + this.vttOffset + var currentLine = this.lines[this.current] + while(currentLine && (ms > currentLine.end || currentLine.branch && this.branch && currentLine.branch !== this.branch)){ + currentLine = this.lines[++this.current] + } + if(this.shown !== this.current){ + if(currentLine && ms >= currentLine.start){ + if(!currentLine.copy){ + this.setText(currentLine.text) + } + this.shown = this.current + }else if(this.shown !== -1){ + this.setText("") + this.shown = -1 + } + } + } + setText(text){ + this.stroke.innerHTML = this.fill.innerHTML = "" + var hasRuby = false + while(text){ + var matches = text.match(this.rLinebreak) + var index1 = matches ? matches.index : -1 + var index2 = text.indexOf("") + if(index1 !== -1 && (index2 === -1 || index2 > index1)){ + this.textNode(text.slice(0, index1)) + this.linebreakNode() + text = text.slice(index1 + matches[0].length) + }else if(index2 !== -1){ + hasRuby = true + this.textNode(text.slice(0, index2)) + text = text.slice(index2 + 6) + var index = text.indexOf("") + if(index !== -1){ + var ruby = text.slice(0, index) + text = text.slice(index + 7) + }else{ + var ruby = text + text = "" + } + var index = ruby.indexOf("") + if(index !== -1){ + var node1 = ruby.slice(0, index) + ruby = ruby.slice(index + 4) + var index = ruby.indexOf("") + if(index !== -1){ + var node2 = ruby.slice(0, index) + }else{ + var node2 = ruby + } + }else{ + var node1 = ruby + var node2 = "" + } + this.rubyNode(node1, node2) + }else{ + this.textNode(text) + break + } + } + } + insertNode(func){ + this.stroke.appendChild(func()) + this.fill.appendChild(func()) + } + textNode(text){ + this.insertNode(() => document.createTextNode(text)) + } + linebreakNode(){ + this.insertNode(() => document.createElement("br")) + } + rubyNode(node1, node2){ + this.insertNode(() => { + var ruby = document.createElement("ruby") + var rt = document.createElement("rt") + ruby.appendChild(document.createTextNode(node1)) + rt.appendChild(document.createTextNode(node2)) + ruby.appendChild(rt) + return ruby + }) + } + setScale(ratio){ + this.div.style.setProperty("--scale", ratio) + } + offsetChange(songOffset, vttOffset){ + if(typeof songOffset !== "undefined"){ + this.songOffset = songOffset + } + if(typeof vttOffset !== "undefined"){ + this.vttOffset = vttOffset + } + this.setText("") + this.current = 0 + this.shown = -1 + } + clean(){ + if(this.shown !== -1){ + this.setText("") + } + delete this.div + delete this.stroke + delete this.fill + delete this.lines + } +} diff --git a/public/src/js/main.js b/public/src/js/main.js new file mode 100644 index 0000000..e5602fc --- /dev/null +++ b/public/src/js/main.js @@ -0,0 +1,144 @@ +addEventListener("error", function(err){ + var stack + if("error" in err && err.error){ + stack = err.error.stack + }else{ + stack = err.message + "\n at " + err.filename + ":" + err.lineno + ":" + err.colno + } + errorMessage(stack) +}) + +function errorMessage(stack){ + localStorage["lastError"] = JSON.stringify({ + timestamp: Date.now(), + stack: stack + }) +} + +function toggleFullscreen(){ + if("requestFullscreen" in root){ + if(document.fullscreenElement){ + document.exitFullscreen() + }else{ + root.requestFullscreen() + } + }else if("webkitRequestFullscreen" in root){ + if(document.webkitFullscreenElement){ + document.webkitExitFullscreen() + }else{ + root.webkitRequestFullscreen() + } + }else if("mozRequestFullScreen" in root){ + if(document.mozFullScreenElement){ + document.mozCancelFullScreen() + }else{ + root.mozRequestFullScreen() + } + } +} + +function resizeRoot(){ + if((noResizeRoot ? lastWidth !== innerWidth : true) && lastHeight !== innerHeight){ + lastWidth = innerWidth + lastHeight = innerHeight + root.style.height = innerHeight + "px" + } +} + +function debug(){ + if(debugObj.state === "open"){ + debugObj.debug.clean() + return "Debug closed" + }else if(debugObj.state === "minimised"){ + debugObj.debug.restore() + return "Debug restored" + }else{ + debugObj.debug = new Debug() + return "Debug opened" + } +} + +var root = document.documentElement + +if(/iPhone|iPad/.test(navigator.userAgent)){ + var fullScreenSupported = false +}else{ + var fullScreenSupported = "requestFullscreen" in root || "webkitRequestFullscreen" in root || "mozRequestFullScreen" in root +} + +var pageEvents = new PageEvents() +var snd = {} +var p2 +var disableBlur = false +var cancelTouch = true +var lastWidth +var lastHeight +var debugObj = { + state: "closed", + debug: null +} +var perf = { + blur: 0, + allImg: 0, + load: 0 +} +var defaultDon = { + body_fill: "#5fb7c1", + face_fill: "#ff5724" +} +var strings +var vectors +var settings +var scoreStorage +var account = {} +var gpicker +var db +var plugins +var noResizeRoot = false +var kanaPairs = [["っきゃ","ッキャ"],["っきゅ","ッキュ"],["っきょ","ッキョ"],["っしゃ","ッシャ"],["っしゅ","ッシュ"],["っしょ","ッショ"],["っちゃ","ッチャ"],["っちゅ","ッチュ"],["っちょ","ッチョ"],["っひゃ","ッヒャ"],["っひゅ","ッヒュ"],["っひょ","ッヒョ"],["っみゃ","ッミャ"],["っみゅ","ッミュ"],["っみょ","ッミョ"],["っりゃ","ッリャ"],["っりゅ","ッリュ"],["っりょ","ッリョ"],["っぎゃ","ッギャ"],["っぎゅ","ッギュ"],["っぎょ","ッギョ"],["っじゃ","ッジャ"],["っじゅ","ッジュ"],["っじょ","ッジョ"],["っびゃ","ッビャ"],["っびゅ","ッビュ"],["っびょ","ッビョ"],["っぴゃ","ッピャ"],["っぴゅ","ッピュ"],["っぴょ","ッピョ"],["っいぇ","ッイェ"],["っわぃ","ッウィ"],["っわぇ","ッウェ"], +["っわぉ","ッウォ"],["っゔぁ","ッヴァ"],["っゔぃ","ッヴィ"],["っゔぇ","ッヴェ"],["っゔぉ","ッヴォ"],["っすぃ","ッスィ"],["っずぃ","ッズィ"],["っしぇ","ッシェ"],["っじぇ","ッジェ"],["っとぃ","ッティ"],["っとぅ","ットゥ"],["っでぅ","ッディ"],["っどぅ","ッドゥ"],["っつぁ","ッツァ"],["っつぃ","ッツィ"],["っつぇ","ッツェ"],["っつぉ","ッツォ"],["っふぁ","ッファ"],["っふぃ","ッフィ"],["っふぇ","ッフェ"],["っふぉ","ッフォ"],["っふゅ","ッフュ"],["っひぇ","ッヒェ"],["きゃ","キャ"],["きゅ","キュ"],["きょ","キョ"],["しゃ","シャ"],["しゅ","シュ"],["しょ","ショ"],["ちゃ","チャ"],["ちゅ","チュ"],["ちょ","チョ"],["にゃ","ニャ"],["にゅ","ニュ"],["にょ","ニョ"],["ひゃ","ヒャ"], +["ひゅ","ヒュ"],["ひょ","ヒョ"],["みゃ","ミャ"],["みゅ","ミュ"],["みょ","ミョ"],["りゃ","リャ"],["りゅ","リュ"],["りょ","リョ"],["ぎゃ","ギャ"],["ぎゅ","ギュ"],["ぎょ","ギョ"],["じゃ","ジャ"],["じゅ","ジュ"],["じょ","ジョ"],["びゃ","ビャ"],["びゅ","ビュ"],["びょ","ビョ"],["ぴゃ","ピャ"],["ぴゅ","ピュ"],["ぴょ","ピョ"],["いぇ","イェ"],["わぃ","ウィ"],["わぇ","ウェ"],["わぉ","ウォ"],["ゔぁ","ヴァ"],["ゔぃ","ヴィ"],["ゔぇ","ヴェ"],["ゔぉ","ヴォ"],["すぃ","スィ"],["ずぃ","ズィ"],["しぇ","シェ"],["じぇ","ジェ"],["とぃ","ティ"],["とぅ","トゥ"],["でぅ","ディ"],["どぅ","ドゥ"],["つぁ","ツァ"],["つぃ","ツィ"],["つぇ","ツェ"],["つぉ","ツォ"],["ふぁ","ファ"], +["ふぃ","フィ"],["ふぇ","フェ"],["ふぉ","フォ"],["ふゅ","フュ"],["ひぇ","ヒェ"],["っか","ッカ"],["っき","ッキ"],["っく","ック"],["っけ","ッケ"],["っこ","ッコ"],["っさ","ッサ"],["っし","ッシ"],["っす","ッス"],["っせ","ッセ"],["っそ","ッソ"],["った","ッタ"],["っち","ッチ"],["っつ","ッツ"],["って","ッテ"],["っと","ット"],["っは","ッハ"],["っひ","ッヒ"],["っふ","ッフ"],["っへ","ッヘ"],["っほ","ッホ"],["っま","ッマ"],["っみ","ッミ"],["っむ","ッム"],["っめ","ッメ"],["っも","ッモ"],["っや","ッヤ"],["っゆ","ッユ"],["っよ","ッヨ"],["っら","ッラ"],["っり","ッリ"],["っる","ッル"],["っれ","ッレ"],["っろ","ッロ"],["っわ","ッワ"],["っゐ","ッヰ"], +["っゑ","ッヱ"],["っを","ッヲ"],["っが","ッガ"],["っぎ","ッギ"],["っぐ","ッグ"],["っげ","ッゲ"],["っご","ッゴ"],["っざ","ッザ"],["っじ","ッジ"],["っず","ッズ"],["っぜ","ッゼ"],["っぞ","ッゾ"],["っだ","ッダ"],["っぢ","ッヂ"],["っづ","ッヅ"],["っで","ッデ"],["っど","ッド"],["っば","ッバ"],["っび","ッビ"],["っぶ","ッブ"],["っべ","ッベ"],["っぼ","ッボ"],["っぱ","ッパ"],["っぴ","ッパ"],["っぷ","ップ"],["っぺ","ッペ"],["っぽ","ッポ"],["っゔ","ッヴ"],["あ","ア"],["い","イ"],["う","ウ"],["え","エ"],["お","オ"],["か","カ"],["き","キ"],["く","ク"],["け","ケ"],["こ","コ"],["さ","サ"],["し","シ"],["す","ス"],["せ","セ"],["そ","ソ"],["た","タ"], +["ち","チ"],["つ","ツ"],["て","テ"],["と","ト"],["な","ナ"],["に","ニ"],["ぬ","ヌ"],["ね","ネ"],["の","ノ"],["は","ハ"],["ひ","ヒ"],["ふ","フ"],["へ","ヘ"],["ほ","ホ"],["ま","マ"],["み","ミ"],["む","ム"],["め","メ"],["も","モ"],["や","ヤ"],["ゆ","ユ"],["よ","ヨ"],["ら","ラ"],["り","リ"],["る","ル"],["れ","レ"],["ろ","ロ"],["わ","ワ"],["ゐ","ヰ"],["ゑ","ヱ"],["を","ヲ"],["ん","ン"],["が","ガ"],["ぎ","ギ"],["ぐ","グ"],["げ","ゲ"],["ご","ゴ"],["ざ","ザ"],["じ","ジ"],["ず","ズ"],["ぜ","ゼ"],["ぞ","ゾ"],["だ","ダ"],["ぢ","ヂ"],["づ","ヅ"],["で","デ"],["ど","ド"], +["ば","バ"],["び","ビ"],["ぶ","ブ"],["べ","ベ"],["ぼ","ボ"],["ぱ","パ"],["ぴ","パ"],["ぷ","プ"],["ぺ","ペ"],["ぽ","ポ"],["ゔ","ヴ"]] + +pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { + if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT" && (event.target.tagName !== "INPUT" || event.target.type !== "file")){ + event.preventDefault() + } +}) +var versionDiv = document.getElementById("version") +var versionLink = document.getElementById("version-link") +versionLink.tabIndex = -1 +pageEvents.add(versionDiv, ["click", "touchend"], event => { + if(event.target === versionDiv){ + versionLink.click() + pageEvents.send("version-link") + } +}) +resizeRoot() +setInterval(resizeRoot, 100) +pageEvents.keyAdd(debugObj, "all", "down", event => { + if((event.keyCode === 186 || event.keyCode === 59) && event.ctrlKey && (event.shiftKey || event.altKey)){ + // Semicolon + if(debugObj.state === "open"){ + debugObj.debug.minimise() + }else if(debugObj.state === "minimised"){ + debugObj.debug.restore() + }else{ + try{ + debugObj.debug = new Debug() + }catch(e){} + } + } + if(event.keyCode === 82 && debugObj.debug && debugObj.controller){ + // R + debugObj.controller.restartSong() + } +}) + +var loader = new Loader(songId => { + new Titlescreen(songId) +}) + diff --git a/public/src/js/mekadon.js b/public/src/js/mekadon.js new file mode 100644 index 0000000..e76abdc --- /dev/null +++ b/public/src/js/mekadon.js @@ -0,0 +1,119 @@ +class Mekadon{ + constructor(...args){ + this.init(...args) + } + init(controller, game){ + this.controller = controller + this.game = game + this.lr = false + this.lastHit = -Infinity + this.delay = controller.audioLatency + } + play(circle){ + var type = circle.type + if((type === "balloon" || type === "drumroll" || type === "daiDrumroll") && this.getMS() > circle.endTime){ + if(circle.section && circle.timesHit === 0){ + this.game.resetSection() + } + circle.played(-1, false) + this.game.updateCurrentCircle() + } + type = circle.type + if(type === "balloon"){ + return this.playDrumrollAt(circle, 0, 30) + }else if(type === "drumroll" || type === "daiDrumroll"){ + return this.playDrumrollAt(circle, 0, 60) + }else{ + return this.playAt(circle, 0, 450) + } + } + playAt(circle, ms, score, dai, reverse){ + var currentMs = circle.ms - this.getMS() + this.delay + if(ms > currentMs - 10){ + return this.playNow(circle, score, dai, reverse) + } + } + playDrumrollAt(circle, ms, pace, kaAmount){ + if(pace && this.getMS() >= this.lastHit + pace){ + var score = 1 + if(kaAmount > 0){ + score = Math.random() > kaAmount ? 1 : 2 + } + return this.playAt(circle, ms, score) + } + } + miss(circle){ + var currentMs = circle.ms - this.getMS() + if(0 >= currentMs - 10){ + this.controller.displayScore(0, true) + this.game.updateCurrentCircle() + this.game.updateCombo(0) + this.game.updateGlobalScore(0, 1, circle.gogoTime) + this.game.sectionNotes.push(0) + return true + } + } + playNow(circle, score, dai, reverse){ + var type = circle.type + var keyDai = false + var playDai = !dai || dai === 2 + var drumrollNotes = type === "balloon" || type === "drumroll" || type === "daiDrumroll" + + if(drumrollNotes){ + var ms = this.getMS() + }else{ + var ms = circle.ms + } + + if(reverse){ + if(type === "don" || type === "daiDon"){ + type = "ka" + }else if(type === "ka" || type === "daiKa"){ + type = "don" + } + } + if(type === "daiDon" && playDai){ + this.setKey("don_l", ms) + this.setKey("don_r", ms) + this.lr = false + keyDai = true + }else if(type === "don" || type === "daiDon" || drumrollNotes && score !== 2){ + this.setKey(this.lr ? "don_l" : "don_r", ms) + this.lr = !this.lr + }else if(type === "daiKa" && playDai){ + this.setKey("ka_l", ms) + this.setKey("ka_r", ms) + this.lr = false + keyDai = true + }else if(type === "ka" || type === "daiKa" || drumrollNotes){ + this.setKey(this.lr ? "ka_l" : "ka_r", ms) + this.lr = !this.lr + } + if(type === "balloon"){ + if(circle.requiredHits === 1){ + assets.sounds["se_balloon"].play() + } + this.game.checkBalloon(circle) + }else if(type === "drumroll" || type === "daiDrumroll"){ + this.game.checkDrumroll(circle, score === 2) + }else{ + this.controller.displayScore(score, false, keyDai) + this.game.updateCombo(score) + this.game.updateGlobalScore(score, keyDai ? 2 : 1, circle.gogoTime) + this.game.updateCurrentCircle() + circle.played(score, keyDai) + if(circle.section){ + this.game.resetSection() + } + this.game.sectionNotes.push(score === 450 ? 1 : (score === 230 ? 0.5 : 0)) + } + this.lastHit = ms + return true + } + getMS(){ + return this.controller.getElapsedTime() + } + setKey(name, ms){ + this.controller.setKey(true, name, ms) + } +} diff --git a/public/src/js/p2.js b/public/src/js/p2.js new file mode 100644 index 0000000..02b7ebe --- /dev/null +++ b/public/src/js/p2.js @@ -0,0 +1,268 @@ +class P2Connection{ + constructor(...args){ + this.init(...args) + } + init(){ + this.closed = true + this.lastMessages = {} + this.otherConnected = false + this.name = null + this.player = 1 + this.allEvents = new Map() + this.addEventListener("message", this.message.bind(this)) + this.currentHash = "" + this.disabled = 0 + pageEvents.add(window, "hashchange", this.onhashchange.bind(this)) + } + addEventListener(type, callback){ + var addedType = this.allEvents.get(type) + if(!addedType){ + addedType = new Set() + this.allEvents.set(type, addedType) + } + return addedType.add(callback) + } + removeEventListener(type, callback){ + var addedType = this.allEvents.get(type) + if(addedType){ + return addedType.delete(callback) + } + } + open(){ + if(this.closed && !this.disabled){ + this.closed = false + var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:" + this.socket = new WebSocket(gameConfig.multiplayer_url ? gameConfig.multiplayer_url : wsProtocol + "//" + location.host + location.pathname + "p2") + pageEvents.race(this.socket, "open", "close").then(response => { + if(response.type === "open"){ + return this.openEvent() + } + return this.closeEvent() + }) + pageEvents.add(this.socket, "message", this.messageEvent.bind(this)) + } + } + openEvent(){ + var addedType = this.allEvents.get("open") + if(addedType){ + addedType.forEach(callback => callback()) + } + } + close(){ + if(!this.closed){ + this.closed = true + if(this.socket){ + this.socket.close() + } + } + } + closeEvent(){ + this.removeEventListener(onmessage) + this.otherConnected = false + this.session = false + if(this.hashLock){ + this.hash("") + this.hashLock = false + } + if(!this.closed){ + setTimeout(() => { + if(this.socket.readyState !== this.socket.OPEN){ + this.open() + } + }, 500) + pageEvents.send("p2-disconnected") + } + var addedType = this.allEvents.get("close") + if(addedType){ + addedType.forEach(callback => callback()) + } + } + send(type, value){ + if(this.socket.readyState === this.socket.OPEN){ + if(typeof value === "undefined"){ + this.socket.send(JSON.stringify({type: type})) + }else{ + this.socket.send(JSON.stringify({type: type, value: value})) + } + }else{ + pageEvents.once(this, "open").then(() => { + this.send(type, value) + }) + } + } + messageEvent(event){ + try{ + var response = JSON.parse(event.data) + }catch(e){ + var response = {} + } + this.lastMessages[response.type] = response + var addedType = this.allEvents.get("message") + if(addedType){ + addedType.forEach(callback => callback(response)) + } + } + getMessage(type){ + if(type in this.lastMessages){ + return this.lastMessages[type] + } + } + clearMessage(type){ + if(type in this.lastMessages){ + this.lastMessages[type] = null + } + } + message(response){ + switch(response.type){ + case "gameload": + if("player" in response.value){ + this.player = response.value.player === 2 ? 2 : 1 + } + case "gamestart": + this.otherConnected = true + this.notes = [] + this.drumrollPace = 45 + this.dai = 2 + this.kaAmount = 0 + this.results = false + this.branch = "normal" + scoreStorage.clearP2() + break + case "gameend": + this.otherConnected = false + if(this.session){ + pageEvents.send("session-end") + }else if(!this.results){ + pageEvents.send("p2-game-end") + } + this.session = false + if(this.hashLock){ + this.hash("") + this.hashLock = false + } + this.name = null + this.don = null + scoreStorage.clearP2() + break + case "gameresults": + this.results = {} + for(var i in response.value){ + this.results[i] = response.value[i] === null ? null : response.value[i].toString() + } + break + case "note": + this.notes.push(response.value) + if(response.value.dai){ + this.dai = response.value.dai + } + break + case "drumroll": + this.drumrollPace = response.value.pace + if("kaAmount" in response.value){ + this.kaAmount = response.value.kaAmount + } + break + case "branch": + this.branch = response.value + this.branchSet = false + break + case "session": + this.clearMessage("users") + this.otherConnected = true + this.session = true + scoreStorage.clearP2() + if("player" in response.value){ + this.player = response.value.player === 2 ? 2 : 1 + } + break + case "name": + this.name = response.value ? (response.value.name || "").toString() : "" + this.don = response.value ? (response.value.don) : null + break + case "getcrowns": + if(response.value){ + var output = {} + for(var i in response.value){ + if(response.value[i]){ + var score = scoreStorage.get(response.value[i], false, true) + if(score){ + var crowns = {} + for(var diff in score){ + if(diff !== "title"){ + crowns[diff] = { + crown: score[diff].crown + } + } + } + }else{ + var crowns = null + } + output[response.value[i]] = crowns + } + } + p2.send("crowns", output) + } + break + case "crowns": + if(response.value){ + for(var i in response.value){ + scoreStorage.addP2(i, false, response.value[i], true) + } + } + break + } + } + onhashchange(){ + if(this.hashLock){ + this.hash(this.currentHash) + }else{ + location.reload() + } + } + hash(string){ + this.currentHash = string + history.replaceState("", "", location.pathname + (string ? "#" + string : "")) + } + play(circle, mekadon){ + if(this.otherConnected || this.notes.length > 0){ + var type = circle.type + var drumrollNotes = type === "balloon" || type === "drumroll" || type === "daiDrumroll" + + if(drumrollNotes && mekadon.getMS() > circle.endTime + mekadon.delay){ + circle.played(-1, false) + mekadon.game.updateCurrentCircle() + } + + if(drumrollNotes){ + mekadon.playDrumrollAt(circle, 0, this.drumrollPace, type === "drumroll" || type === "daiDrumroll" ? this.kaAmount : 0) + }else if(this.notes.length === 0){ + mekadon.play(circle) + }else{ + var note = this.notes[0] + if(note.score >= 0){ + var dai = 1 + if(circle.type === "daiDon" || circle.type === "daiKa"){ + dai = this.dai + } + if(mekadon.playAt(circle, note.ms, note.score, dai, note.reverse)){ + this.notes.shift() + } + }else{ + if(mekadon.miss(circle)){ + this.notes.shift() + } + } + } + }else if(mekadon.miss(circle)){ + this.notes.shift() + } + } + enable(){ + this.disabled = Math.max(0, this.disabled - 1) + setTimeout(this.open.bind(this), 100) + } + disable(){ + this.disabled++ + this.close() + } +} diff --git a/public/src/js/pageevents.js b/public/src/js/pageevents.js new file mode 100644 index 0000000..de94814 --- /dev/null +++ b/public/src/js/pageevents.js @@ -0,0 +1,182 @@ +class PageEvents{ + constructor(...args){ + this.init(...args) + } + init(){ + this.allEvents = new Map() + this.keyListeners = new Map() + this.mouseListeners = new Map() + this.blurListeners = new Map() + this.lastKeyEvent = -Infinity + this.add(window, "keydown", this.keyEvent.bind(this)) + this.add(window, "keyup", this.keyEvent.bind(this)) + this.add(window, "mousemove", this.mouseEvent.bind(this)) + this.add(window, "blur", this.blurEvent.bind(this)) + this.kbd = [] + } + add(target, type, callback, symbol){ + if(Array.isArray(type)){ + type.forEach(type => this.add(target, type, callback, symbol)) + return + } + this.remove(target, type) + var addedEvent = this.allEvents.get(symbol || target) + if(!addedEvent){ + addedEvent = new Map() + this.allEvents.set(symbol || target, addedEvent) + } + addedEvent.set(type, callback) + return target.addEventListener(type, callback) + } + remove(target, type, symbol){ + if(Array.isArray(type)){ + type.forEach(type => this.remove(target, type, symbol)) + return + } + var addedEvent = this.allEvents.get(symbol || target) + if(addedEvent){ + var callback = addedEvent.get(type) + if(callback){ + target.removeEventListener(type, callback) + addedEvent.delete(type) + if(addedEvent.size == 0){ + return this.allEvents.delete(symbol || target) + } + } + } + } + once(target, type, symbol){ + return new Promise(resolve => { + this.add(target, type, event => { + this.remove(target, type) + return resolve(event) + }, symbol) + }) + } + race(){ + var symbols = [] + var target = arguments[0] + return new Promise(resolve => { + for(var i = 1;i < arguments.length; i++){ + symbols[i] = Symbol() + let type = arguments[i] + this.add(target, type, event => { + resolve({ + type: type, + event: event + }) + }, symbols[i]) + } + }).then(response => { + for(var i = 1;i < arguments.length; i++){ + this.remove(target, arguments[i], symbols[i]) + } + return response + }) + } + load(target){ + return new Promise((resolve, reject) => { + this.race(target, "load", "error", "abort").then(response => { + switch(response.type){ + case "load": + return resolve(response.event) + case "error": + return reject(["Loading error", target]) + case "abort": + return reject("Loading aborted") + } + }) + }) + } + keyEvent(event){ + if(!("key" in event) || event.ctrlKey && (event.key === "c" || event.key === "x" || event.key === "v")){ + return + } + if(this.kbd.indexOf(event.key.toLowerCase()) !== -1){ + this.lastKeyEvent = Date.now() + if(event.target.tagName !== "INPUT"){ + event.preventDefault() + } + } + this.keyListeners.forEach(addedKeyCode => { + this.checkListener(addedKeyCode.get("all"), event) + this.checkListener(addedKeyCode.get(event.keyCode), event) + }) + } + checkListener(keyObj, event){ + if(keyObj && ( + keyObj.type === "both" + || keyObj.type === "down" && event.type === "keydown" + || keyObj.type === "up" && event.type === "up" + )){ + keyObj.callback(event) + } + } + keyAdd(target, keyCode, type, callback){ + // keyCode="all", type="both" + var addedKeyCode = this.keyListeners.get(target) + if(!addedKeyCode){ + addedKeyCode = new Map() + this.keyListeners.set(target, addedKeyCode) + } + addedKeyCode.set(keyCode, { + type: type, + callback: callback + }) + } + keyRemove(target, keyCode){ + var addedKeyCode = this.keyListeners.get(target) + if(addedKeyCode){ + var keyObj = addedKeyCode.get(keyCode) + if(keyObj){ + addedKeyCode.delete(keyCode) + if(addedKeyCode.size == 0){ + return this.keyListeners.delete(target) + } + } + } + } + keyOnce(target, keyCode, type){ + return new Promise(resolve => { + this.keyAdd(target, keyCode, type, event => { + this.keyRemove(target, keyCode) + return resolve(event) + }) + }) + } + mouseEvent(event){ + this.lastMouse = event + this.mouseListeners.forEach(callback => callback(event)) + } + mouseAdd(target, callback){ + this.mouseListeners.set(target, callback) + } + mouseRemove(target){ + this.mouseListeners.delete(target) + } + blurEvent(event){ + this.blurListeners.forEach(callback => callback(event)) + } + blurAdd(target, callback){ + this.blurListeners.set(target, callback) + } + blurRemove(target){ + this.blurListeners.delete(target) + } + getMouse(){ + return this.lastMouse + } + send(name, detail){ + dispatchEvent(new CustomEvent(name, {detail: detail})) + } + setKbd(){ + this.kbd = [] + var kbdSettings = settings.getItem("keyboardSettings") + for(var name in kbdSettings){ + var keys = kbdSettings[name] + for(var i in keys){ + this.kbd.push(keys[i]) + } + } + } +} diff --git a/public/src/js/parseosu.js b/public/src/js/parseosu.js new file mode 100644 index 0000000..e95c714 --- /dev/null +++ b/public/src/js/parseosu.js @@ -0,0 +1,381 @@ +class ParseOsu{ + constructor(...args){ + this.init(...args) + } + init(fileContent, difficulty, stars, offset, metaOnly){ + this.osu = { + OFFSET: 0, + MSPERBEAT: 1, + METER: 2, + SAMPLESET: 3, + SAMPLEINDEX: 4, + VOLUME: 5, + INHERITED: 6, + KIAIMODE: 7, + + X: 0, + Y: 1, + TIME: 2, + TYPE: 3, + HITSOUND: 4, + EXTRAS: 5, + ENDTIME: 5, + + CIRCLE: 1, + SLIDER: 2, + NEWCOMBO: 4, + SPINNER: 8, + + NORMAL: 1, + WHISTLE: 2, + FINISH: 4, + CLAP: 8, + + CURVEPOINTS: 0, + REPEAT: 1, + PIXELLENGTH: 2, + EDGEHITSOUNDS: 3, + EDGEADDITIONS: 4 + } + this.data = [] + for(let line of fileContent){ + line = line.replace(/\/\/.*/, "").trim() + if(line !== ""){ + this.data.push(line) + } + } + this.offset = (offset || 0) * -1000 + this.soundOffset = 0 + this.beatInfo = { + beatInterval: 0, + lastBeatInterval: 0, + bpm: 0 + } + this.events = [] + this.generalInfo = this.parseGeneralInfo() + this.metadata = this.parseMetadata() + this.editor = this.parseEditor() + this.difficulty = this.parseDifficulty() + this._difficulty = difficulty; + this.stars = stars + if(!metaOnly){ + this.timingPoints = this.parseTiming() + this.circles = this.parseCircles() + this.measures = this.parseMeasures() + } + } + getStartEndIndexes(type){ + var indexes = { + start: 0, + end: 0 + } + while(indexes.start < this.data.length){ + if(this.data[indexes.start] === "[" + type + "]"){ + break + } + indexes.start++ + } + indexes.start++ + indexes.end = indexes.start + while(indexes.end < this.data.length){ + if(this.data[indexes.end].match(/^\[\w+\]$/)){ + break + } + indexes.end++ + } + indexes.end-- + return indexes + } + parseDifficulty(){ + var difficulty = { + sliderMultiplier: 0, + sliderTickRate: 0, + approachRate: 0 + } + var indexes = this.getStartEndIndexes("Difficulty") + for(var i = indexes.start; i <= indexes.end; i++){ + var [item, key] = this.data[i].split(":") + switch(item){ + case "SliderMultiplier": + difficulty.sliderMultiplier = key + difficulty.originalMultiplier = key + break + case "SliderTickRate": + difficulty.sliderTickRate = key + break + case "ApproachRate": + difficulty.approachRate = key + break + case "OverallDifficulty": + difficulty.overallDifficulty = key + break + } + } + return difficulty + } + parseTiming(){ + var timingPoints = [] + var indexes = this.getStartEndIndexes("TimingPoints") + var lastBeatInterval = parseInt(this.data[indexes.start].split(",")[1]) + for(var i = indexes.start; i <= indexes.end; i++){ + var values = this.data[i].split(",") + var start = parseInt(values[this.osu.OFFSET]) + var msOrPercent = parseFloat(values[this.osu.MSPERBEAT]) + if(i == indexes.start){ + this.beatInfo.beatInterval = msOrPercent + this.beatInfo.bpm = Math.floor(1000 / this.beatInfo.beatInterval * 60) + } + var beatReset = false + if(msOrPercent < 0){ + var sliderMultiplier = this.difficulty.lastMultiplier / Math.abs(msOrPercent / 100) + }else{ + var sliderMultiplier = 1000 / msOrPercent + if(i == 0){ + this.difficulty.originalMultiplier = sliderMultiplier + } + this.difficulty.lastMultiplier = sliderMultiplier + beatReset = true + } + timingPoints.push({ + start: start + this.offset, + sliderMultiplier: sliderMultiplier, + measure: parseInt(values[this.osu.METER]), + gogoTime: parseInt(values[this.osu.KIAIMODE]), + beatMS: 1000 / this.difficulty.lastMultiplier, + beatReset: beatReset + }) + } + return timingPoints + } + parseMeasures(){ + var measures = [] + + for(var i = 0; i < this.timingPoints.length; i++){ + var currentTiming = this.timingPoints[i] + var firstTiming = i === 0 + + var limit = this.circles[this.circles.length - 1].endTime + currentTiming.beatMS + + for(var j = i + 1; j < this.timingPoints.length; j++){ + var nextTiming = this.timingPoints[j] + var newLimit = nextTiming.start + if(nextTiming.measure !== currentTiming.measure || nextTiming.beatReset){ + limit = newLimit - currentTiming.beatMS + break + } + i = j + } + + var start = currentTiming.start + var interval = currentTiming.beatMS * currentTiming.measure + if(firstTiming){ + while(start >= interval){ + start -= interval + } + } + for(var ms = start; ms <= limit; ms += interval){ + + var speed = currentTiming.sliderMultiplier + for(var j = 0; j < this.timingPoints.length; j++){ + var timingPoint = this.timingPoints[j] + if(j !== 0 && timingPoint.start - this.offset > ms){ + break + } + speed = timingPoint.sliderMultiplier + } + + measures.push({ + ms: ms, + originalMS: ms, + speed: speed, + visible: true + }) + } + } + return measures + } + parseGeneralInfo(){ + var generalInfo = {} + var indexes = this.getStartEndIndexes("General") + for(var i = indexes.start; i<= indexes.end; i++){ + var [item, key] = this.data[i].split(":") + generalInfo[item] = key.trim() + } + return generalInfo + } + parseMetadata(){ + var metadata = {} + var indexes = this.getStartEndIndexes("Metadata") + for(var i = indexes.start; i <= indexes.end; i++){ + var [item, key] = this.data[i].split(":") + metadata[item] = key.trim() + } + return metadata + } + parseEditor(){ + var editor = { + distanceSpacing: 0, + beatDivisor: 0, + gridSize: 0 + } + var indexes = this.getStartEndIndexes("Editor") + for(var i = indexes.start; i <= indexes.end; i++){ + var [item, key] = this.data[i].split(":") + switch(item){ + case "DistanceSpacing": + editor.distanceSpacing = parseFloat(key) + break + case "BeatDivisor": + editor.beatDivisor = parseInt(key) + break + case "GridSize": + editor.gridSize = parseInt(key) + break + } + } + return editor + } + difficultyRange(difficulty, min, mid, max){ + if(difficulty > 5){ + return mid + (max - mid) * (difficulty - 5) / 5 + } + if(difficulty < 5){ + return mid - (mid - min) * (5 - difficulty) / 5 + } + return mid + } + parseCircles(){ + var circles = [] + var circleID = 0 + var indexes = this.getStartEndIndexes("HitObjects") + var lastBeatMS = this.beatInfo.beatInterval + var lastGogo = false + + var pushCircle = circle => { + circles.push(circle) + if(lastBeatMS !== circle.beatMS || lastGogo !== circle.gogoTime){ + lastBeatMS = circle.beatMS + lastGogo = circle.gogoTime + this.events.push(circle) + } + } + + for(var i = indexes.start; i <= indexes.end; i++){ + circleID++ + var values = this.data[i].split(",") + var emptyValue = false + var start = parseInt(values[this.osu.TIME]) + var speed = this.difficulty.originalMultiplier + var gogoTime = false + var osuType = parseInt(values[this.osu.TYPE]) + var hitSound = parseInt(values[this.osu.HITSOUND]) + var beatLength = speed + var lastMultiplier = this.difficulty.lastMultiplier + var beatMS = this.beatInfo.beatInterval + if(circleID === 1 && start + this.offset < 0){ + var offset = start + this.offset + this.soundOffset = offset + this.offset -= offset + } + + for(var j = 0; j < this.timingPoints.length; j++){ + var timingPoint = this.timingPoints[j] + if(j !== 0 && timingPoint.start - this.offset > start){ + break + } + speed = timingPoint.sliderMultiplier + gogoTime = timingPoint.gogoTime + beatMS = timingPoint.beatMS + } + + if(osuType & this.osu.SPINNER){ + + var endTime = parseInt(values[this.osu.ENDTIME]) + var hitMultiplier = this.difficultyRange(this.difficulty.overallDifficulty, 3, 5, 7.5) * 1.65 + var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier)) + pushCircle(new Circle({ + id: circleID, + start: start + this.offset, + type: "balloon", + txt: strings.note.balloon, + speed: speed, + endTime: endTime + this.offset, + requiredHits: requiredHits, + gogoTime: gogoTime, + beatMS: beatMS + })) + + }else if(osuType & this.osu.SLIDER){ + + var extras = values.slice(this.osu.EXTRAS) + + var distance = parseFloat(extras[this.osu.PIXELLENGTH]) * parseFloat(extras[this.osu.REPEAT]) + var velocity = this.difficulty.sliderMultiplier * speed / 10 + var endTime = start + distance / velocity + + if(hitSound & this.osu.FINISH){ + type = "daiDrumroll" + txt = strings.note.daiDrumroll + }else{ + type = "drumroll" + txt = strings.note.drumroll + } + pushCircle(new Circle({ + id: circleID, + start: start + this.offset, + type: type, + txt: txt, + speed: speed, + endTime: endTime + this.offset, + gogoTime: gogoTime, + beatMS: beatMS + })) + + }else if(osuType & this.osu.CIRCLE){ + var type + var txt + + if(hitSound & this.osu.FINISH){ + if(hitSound & this.osu.WHISTLE || hitSound & this.osu.CLAP){ + type = "daiKa" + txt = strings.note.daiKa + }else if(hitSound & this.osu.NORMAL || hitSound === this.osu.FINISH){ + type = "daiDon" + txt = strings.note.daiDon + }else{ + emptyValue = true + } + }else if(hitSound & this.osu.WHISTLE || hitSound & this.osu.CLAP){ + type = "ka" + txt = strings.note.ka + }else if(hitSound & this.osu.NORMAL || hitSound === 0){ + type = "don" + txt = strings.note.don + }else{ + emptyValue = true + } + if(!emptyValue){ + pushCircle(new Circle({ + id: circleID, + start: start + this.offset, + type: type, + txt: txt, + speed: speed, + gogoTime: gogoTime, + beatMS: beatMS + })) + } + }else{ + emptyValue = true + } + if(emptyValue){ + console.warn("Unknown note type found on line " + (i + 1) + ": " + this.data[i]) + } + } + this.scoremode = 2; + var autoscore = new AutoScore(this._difficulty, this.stars, 2, circles); + this.scoreinit = autoscore.ScoreInit; + this.scorediff = autoscore.ScoreDiff; + return circles + } +} diff --git a/public/src/js/parsetja.js b/public/src/js/parsetja.js new file mode 100644 index 0000000..744781f --- /dev/null +++ b/public/src/js/parsetja.js @@ -0,0 +1,702 @@ +class ParseTja{ + constructor(...args){ + this.init(...args) + } + init(file, difficulty, stars, offset, metaOnly){ + this.data = [] + for(let line of file){ + var indexComment = line.indexOf("//") + if(indexComment !== -1 && !line.trim().toLowerCase().startsWith("maker:")){ + line = line.slice(0, indexComment).trim() + }else{ + line = line.trim() + } + if(line !== ""){ + this.data.push(line) + } + } + this.difficulty = difficulty + this.stars = stars + this.offset = (offset || 0) * -1000 + this.soundOffset = 0 + this.noteTypes = { + "0": {name: false, txt: false}, + "1": {name: "don", txt: strings.note.don}, + "2": {name: "ka", txt: strings.note.ka}, + "3": {name: "daiDon", txt: strings.note.daiDon}, + "4": {name: "daiKa", txt: strings.note.daiKa}, + "5": {name: "drumroll", txt: strings.note.drumroll}, + "6": {name: "daiDrumroll", txt: strings.note.daiDrumroll}, + "7": {name: "balloon", txt: strings.note.balloon}, + "8": {name: false, txt: false}, + "9": {name: "balloon", txt: strings.note.balloon}, + "A": {name: "daiDon", txt: strings.note.daiDon}, + "B": {name: "daiKa", txt: strings.note.daiKa} + } + this.noteTypes_ex = strings.ex_note; + this.courseTypes = { + "0": "easy", + "1": "normal", + "2": "hard", + "3": "oni", + "4": "ura", + "edit": "ura" + } + + this.metadata = this.parseMetadata() + this.measures = [] + this.beatInfo = {} + this.events = [] + if(!metaOnly){ + this.circles = this.parseCircles(difficulty) + } + } + parseMetadata(){ + var metaNumbers = ["bpm", "offset", "demostart", "level", "scoremode", "scorediff"] + var inSong = false + var hasSong = false + var courses = {} + var currentCourse = {} + var courseName = "oni" + for(var lineNum = 0; lineNum < this.data.length; lineNum++){ + var line = this.data[lineNum] + + if(line.slice(0, 1) === "#"){ + + var name = line.slice(1).toLowerCase() + if((name === "start" || name === "start p1") && !inSong){ + + inSong = true + if(!hasSong || name === "start" && courses[courseName] && courses[courseName].startName !== "start"){ + hasSong = false + if(!(courseName in courses)){ + courses[courseName] = {} + } + courses[courseName].startName = name + for(var opt in currentCourse){ + if(opt !== "branch"){ + courses[courseName][opt] = currentCourse[opt] + } + } + courses[courseName].start = lineNum + 1 + courses[courseName].end = this.data.length + } + }else if(name === "end" && inSong){ + inSong = false + if(!hasSong){ + hasSong = true + courses[courseName].end = lineNum + } + }else if(name.startsWith("branchstart") && inSong){ + courses[courseName].branch = true + }else if(name.startsWith("lyric") && inSong){ + courses[courseName].inlineLyrics = true + } + + }else if(!inSong){ + + if(line.indexOf(":") > 0){ + + var [name, value] = this.split(line, ":") + name = name.toLowerCase().trim() + value = value.trim() + + if(name === "course"){ + value = value.toLowerCase() + if(value in this.courseTypes){ + courseName = this.courseTypes[value] + }else{ + courseName = value + } + hasSong = false + }else if(name === "balloon"){ + value = value ? value.split(",").map(digit => parseInt(digit)) : [] + }else if(this.inArray(name, metaNumbers)){ + value = parseFloat(value) + } + else if (name === "scoreinit") { + value = value ? parseFloat(value.split(",")[0]) : 0; + } + + currentCourse[name] = value + } + + } + } + return courses + } + inArray(string, array){ + return array.indexOf(string) >= 0 + } + split(string, delimiter){ + var index = string.indexOf(delimiter) + if(index < 0){ + return [string, ""] + } + return [string.slice(0, index), string.slice(index + delimiter.length)] + } + parseCircles(difficulty, lyricsOnly){ + var meta = this.metadata[difficulty] || {} + var ms = (meta.offset || 0) * -1000 + this.offset + var bpm = Math.abs(meta.bpm) || 120 + var scroll = 1 + var measure = 4 + if(!lyricsOnly){ + this.beatInfo.beatInterval = 60000 / bpm + } + var gogo = false + var barLine = true + + var balloonID = 0 + var balloons = meta.balloon || [] + + var lastDrumroll = false + + var branches + var branch = false + var branchObj = {} + var currentBranch = false + var branchSettings = {} + var branchFirstMeasure = false + var sectionBegin = true + var lastBpm = bpm + var lastGogo = gogo + var lyrics + var lyricsIndex = null + var lyricsLine = null + var lyricsCopy = false + + var measures = [] + var currentMeasure = [] + var firstNote = true + var circles = [] + var circleID = 0 + var events = [] + var regexAZ = /[A-Z]/ + var regexSpace = /\s/ + var regexLinebreak = /\\n/g + var isAllDon = (note_chain, start_pos) => { + for (var i = start_pos; i < note_chain.length; ++i) { + var note = note_chain[i]; + if (note && note.type !== "don" && note.type !== "daiDon") { + return false; + } + } + return true; + } + var checkChain = (note_chain, measure_length, is_last) => { + //console.log(note_chain, measure_length, is_last); + /*if (measure_length >= 24) { + for (var note of note_chain) { + note.text = this.noteTypes_ex[note.type][0]; + } + } else { */ + var alldon_pos = null; + for (var i = 0; i < note_chain.length - (is_last ? 1 : 0); ++i) { + var note = note_chain[i]; + if (alldon_pos === null && is_last && isAllDon(note_chain, i)) { + alldon_pos = i; + } + note.text = this.noteTypes_ex[note.type][alldon_pos != null ? (i - alldon_pos) % 2 : 0]; + } + //} + } + var pushMeasure = () => { + var note = currentMeasure[0] + if(note){ + var speed = note.bpm * note.scroll / 60 + }else{ + var speed = bpm * scroll / 60 + } + if(!lyricsOnly){ + measures.push({ + ms: ms, + originalMS: ms, + speed: speed, + visible: barLine, + branch: currentBranch, + branchFirst: branchFirstMeasure + }) + } + branchFirstMeasure = false + if(currentMeasure.length){ + for(var i = 0; i < currentMeasure.length; i++){ + var note = currentMeasure[i] + if(firstNote && note.type && note.type !== "event"){ + firstNote = false + if(ms < 0){ + this.soundOffset = ms + ms = 0 + } + } + note.start = ms + if(note.endDrumroll){ + note.endDrumroll.endTime = ms + note.endDrumroll.originalEndTime = ms + } + var msPerMeasure = 60000 * measure / note.bpm + ms += msPerMeasure / currentMeasure.length + } + var note_chain = []; + for (var i = 0; i < currentMeasure.length; i++){ + var note = currentMeasure[i] + if(!lyricsOnly){ + circleID++ + var circleObj = new Circle({ + id: circleID, + start: note.start, + type: note.type, + txt: note.txt, + speed: note.bpm * note.scroll / 60, + gogoTime: note.gogo, + endTime: note.endTime, + requiredHits: note.requiredHits, + beatMS: 60000 / note.bpm, + branch: currentBranch, + section: note.section + }) + if(note.type){ + if(note.type === "don" || note.type === "ka" || note.type === "daiDon" || note.type === "daiKa"){ + note_chain.push(circleObj) + }else{ + if(note_chain.length > 1 && currentMeasure.length >= 8){ + checkChain(note_chain, currentMeasure.length, false) + } + note_chain = [] + } + if (lastDrumroll === note) { + lastDrumroll = circleObj + } + + if(note.type !== "event"){ + circles.push(circleObj) + } + }else if( + (currentMeasure.length < 24 || + currentMeasure[i + 1] + && !currentMeasure[i + 1].type + ) && (currentMeasure.length < 48 || + currentMeasure[i + 2] + && !currentMeasure[i + 2].type + && currentMeasure[i + 3] + && !currentMeasure[i + 3].type + ) + ){ + if(note_chain.length > 1 && currentMeasure.length >= 8){ + checkChain(note_chain, currentMeasure.length, true) + } + note_chain = [] + } + if(note.event){ + events.push(circleObj) + } + } + var lyricsObj = null + if("lyricsLine" in note){ + lyricsObj = { + start: note.start, + text: note.lyricsLine + } + }else if(note.lyricsCopy){ + lyricsObj = { + start: note.start, + copy: true + } + } + if(lyricsObj){ + if(currentBranch){ + lyricsObj.branch = currentBranch.name + } + insertLyrics(lyricsObj) + } + } + if(!lyricsOnly && note_chain.length > 1 && currentMeasure.length >= 8){ + checkChain(note_chain, currentMeasure.length, false) + } + }else{ + var msPerMeasure = 60000 * measure / bpm + ms += msPerMeasure + } + } + var insertNote = circleObj => { + if(circleObj){ + if(bpm !== lastBpm || gogo !== lastGogo){ + circleObj.event = true + lastBpm = bpm + lastGogo = gogo + } + if(lyricsLine !== null){ + circleObj.lyricsLine = lyricsLine + lyricsLine = null + }else if(lyricsCopy){ + circleObj.lyricsCopy = true + } + lyricsCopy = false + currentMeasure.push(circleObj) + } + } + var insertBlankNote = circleObj => { + if(bpm !== lastBpm || gogo !== lastGogo){ + insertNote({ + type: "event", + bpm: bpm, + scroll: scroll, + gogo: gogo + }) + }else if(!circleObj){ + var circleObj2 = { + bpm: bpm, + scroll: scroll + } + if(lyricsLine !== null){ + circleObj2.lyricsLine = lyricsLine + lyricsLine = null + }else if(lyricsCopy){ + circleObj2.lyricsCopy = true + } + lyricsCopy = false + currentMeasure.push(circleObj2) + } + if(circleObj){ + if(lyricsLine !== null){ + circleObj.lyricsLine = lyricsLine + lyricsLine = null + }else if(lyricsCopy){ + circleObj.lyricsCopy = true + } + lyricsCopy = false + currentMeasure.push(circleObj) + } + } + var insertLyrics = obj => { + if(!lyrics){ + lyrics = [] + }else if(lyricsIndex !== null){ + lyrics[lyricsIndex].end = obj.start + } + lyricsIndex = lyrics.length + lyrics.push(obj) + } + + for(var lineNum = meta.start; lineNum < meta.end; lineNum++){ + var line = this.data[lineNum] + if(line.slice(0, 1) === "#"){ + + var line = line.slice(1) + var [name, value] = this.split(line, " ") + name = name.toLowerCase() + + switch(name){ + case "gogostart": + gogo = true + break + case "gogoend": + gogo = false + break + case "bpmchange": + bpm = parseFloat(value) || bpm + break + case "scroll": + scroll = Math.abs(parseFloat(value)) || scroll + break + case "measure": + var [numerator, denominator] = value.split("/") + measure = numerator / denominator * 4 || measure + break + case "delay": + ms += (parseFloat(value) || 0) * 1000 + break + case "barlineon": + barLine = true + break + case "barlineoff": + barLine = false + break + case "branchstart": + branch = true + currentBranch = false + branchFirstMeasure = true + branchSettings = { + ms: ms, + gogo: gogo, + bpm: bpm, + scroll: scroll, + sectionBegin: sectionBegin, + lyricsCopy: !!lyrics + } + if(lyrics && lyricsIndex !== null){ + var line = lyrics[lyricsIndex] + line.end = ms + } + lyricsIndex = null + + value = value.split(",") + if(!branches){ + branches = [] + } + var req = { + advanced: parseFloat(value[1]) || 0, + master: parseFloat(value[2]) || 0 + } + if(req.advanced > 0){ + var active = req.master > 0 ? "normal" : "master" + }else{ + var active = req.master > 0 ? "advanced" : "master" + } + branchObj = { + ms: ms, + originalMS: ms, + active: active, + type: value[0].trim().toLowerCase() === "r" ? "drumroll" : "accuracy", + requirement: req + } + branches.push(branchObj) + if(measures.length === 1 && branchObj.type === "drumroll"){ + for(var i = circles.length; i--;){ + var circle = circles[i] + if(circle.endTime && (circle.type === "drumroll" || circle.type === "daiDrumroll" || circle.type === "balloon")){ + measures.push({ + ms: circle.endTime, + originalMS: circle.endTime, + speed: circle.bpm * circle.scroll / 60, + visible: false, + branch: circle.branch + }) + break + } + } + } + if(measures.length !== 0){ + measures[measures.length - 1].nextBranch = branchObj + } + break + case "branchend": + branch = false + currentBranch = false + lyricsCopy = lyricsCopy || !!lyrics + break + case "section": + sectionBegin = true + if(branch && !currentBranch){ + branchSettings.sectionBegin = true + } + break + case "n": case "e": case "m": + if(!branch){ + break + } + if(lyrics){ + if(lyricsIndex !== null){ + var line = lyrics[lyricsIndex] + line.end = ms + } + lyricsIndex = null + } + ms = branchSettings.ms + gogo = branchSettings.gogo + bpm = branchSettings.bpm + scroll = branchSettings.scroll + sectionBegin = branchSettings.sectionBegin + lyricsCopy = branchSettings.lyricsCopy + branchFirstMeasure = true + var branchName = name === "m" ? "master" : (name === "e" ? "advanced" : "normal") + currentBranch = { + name: branchName, + active: branchName === branchObj.active + } + branchObj[branchName] = currentBranch + break + case "lyric": + lyricsLine = value.replace(regexLinebreak, "\n").trim() + break + } + + }else{ + + var string = line.toUpperCase().split("") + + const abekobe = localStorage.getItem("abekobe") ?? "false"; + const detarame = parseFloat(localStorage.getItem("detarame") ?? "0", 10); + + for(let symbol of string){ + + if (abekobe === "true") { + if (symbol === "1") { + symbol = "2"; + } else if (symbol === "2") { + symbol = "1"; + } else if (symbol === "3") { + symbol = "4"; + } else if (symbol === "4") { + symbol = "3"; + } else if (symbol === "A") { + symbol = "B"; + } else if (symbol === "B") { + symbol - "A"; + } + } + + if (detarame > 0) { + const randomValue = Math.random() * 100; + if (randomValue < detarame) { + + const first = ["1", "2"]; + const second = ["3", "4", "A", "B"]; + if (first.includes(symbol)) { + const firstIndex = Math.floor(Math.random() * first.length); + symbol = first[firstIndex]; + } else if (second.includes(symbol)) { + const secondIndex = Math.floor(Math.random() * second.length); + symbol = second[secondIndex]; + } + } + } + + var error = false + switch(symbol){ + + case "0": + insertBlankNote() + break + case "1": case "2": case "3": case "4": case "A": case "B": + var type = this.noteTypes[symbol] + var circleObj = { + type: type.name, + txt: type.txt, + gogo: gogo, + bpm: bpm, + scroll: scroll, + section: sectionBegin + } + sectionBegin = false + if(lastDrumroll){ + circleObj.endDrumroll = lastDrumroll + lastDrumroll = false + } + insertNote(circleObj) + break + case "5": case "6": case "7": case "9": + var type = this.noteTypes[symbol] + var circleObj = { + type: type.name, + txt: type.txt, + gogo: gogo, + bpm: bpm, + scroll: scroll, + section: sectionBegin + } + sectionBegin = false + if(lastDrumroll){ + if(symbol === "9"){ + insertNote({ + endDrumroll: lastDrumroll, + gogo: gogo, + bpm: bpm, + scroll: scroll, + section: sectionBegin + }) + sectionBegin = false + lastDrumroll = false + }else{ + insertBlankNote() + } + break + } + if(symbol === "7" || symbol === "9"){ + var hits = balloons[balloonID] + if(!hits || hits < 1){ + hits = 1 + } + circleObj.requiredHits = hits + balloonID++ + } + lastDrumroll = circleObj + insertNote(circleObj) + break + case "8": + if(lastDrumroll){ + insertNote({ + endDrumroll: lastDrumroll, + gogo: gogo, + bpm: bpm, + scroll: scroll, + section: sectionBegin + }) + sectionBegin = false + lastDrumroll = false + }else{ + insertBlankNote() + } + break + case ",": + if(currentMeasure.length === 0 && (bpm !== lastBpm || gogo !== lastGogo || lyricsLine !== null)){ + insertBlankNote() + } + pushMeasure() + currentMeasure = [] + break + default: + if(regexAZ.test(symbol)){ + insertBlankNote() + }else if(!regexSpace.test(symbol)){ + error = true + } + break + + } + if(error){ + break + } + } + + } + } + if(lastDrumroll){ + lastDrumroll.endTime = ms + lastDrumroll.originalEndTime = ms + } + if(lyricsLine !== null){ + insertLyrics({ + start: ms, + text: lyricsLine + }) + } + pushMeasure() + + if(!lyricsOnly){ + if(branches){ + circles.sort((a, b) => a.ms > b.ms ? 1 : -1) + measures.sort((a, b) => a.ms > b.ms ? 1 : -1) + circles.forEach((circle, i) => circle.id = i + 1) + } + this.measures = measures + this.events = events + this.branches = branches + this.scoreinit = meta.scoreinit + this.scorediff = meta.scorediff + if(this.scoreinit && this.scorediff){ + this.scoremode = meta.scoremode || 1 + }else{ + this.scoremode = meta.scoremode || 2 + var autoscore = new AutoScore(difficulty, this.stars, this.scoremode, circles) + this.scoreinit = autoscore.ScoreInit + this.scorediff = autoscore.ScoreDiff + } + } + if(lyrics && lyricsIndex !== null){ + var line = lyrics[lyricsIndex] + line.end = Math.max(ms, line.start) + 5000 + } + if(lyrics){ + this.lyrics = lyrics + }else if(!lyricsOnly){ + for(var courseName in this.metadata){ + if(this.metadata[courseName].inlineLyrics){ + this.parseCircles(courseName, true) + break + } + } + } + return circles + } +} diff --git a/public/src/js/plugins.js b/public/src/js/plugins.js new file mode 100644 index 0000000..46fd35a --- /dev/null +++ b/public/src/js/plugins.js @@ -0,0 +1,677 @@ +class Plugins{ + constructor(...args){ + this.init(...args) + } + init(){ + this.allPlugins = [] + this.pluginMap = {} + this.hashes = [] + this.startOrder = [] + } + add(script, options){ + options = options || {} + var hash = md5.base64(script.toString()) + var isUrl = typeof script === "string" && !options.raw + if(isUrl){ + hash = "url " + hash + }else if(typeof script !== "string"){ + hash = "class " + hash + } + var name = options.name + if(!name && isUrl){ + name = script + var index = name.lastIndexOf("?") + if(index !== -1){ + name = name.slice(0, index) + } + var index = name.lastIndexOf("/") + if(index !== -1){ + name = name.slice(index + 1) + } + if(name.endsWith(".taikoweb.js")){ + name = name.slice(0, -".taikoweb.js".length) + }else if(name.endsWith(".js")){ + name = name.slice(0, -".js".length) + } + } + name = name || "plugin" + if(this.hashes.indexOf(hash) !== -1){ + console.warn("Skip adding an already addded plugin: " + name) + return + } + var baseName = name + for(var i = 2; name in this.pluginMap; i++){ + name = baseName + i.toString() + } + var plugin = new PluginLoader(script, name, hash, options.raw) + plugin.hide = !!options.hide + this.allPlugins.push({ + name: name, + plugin: plugin + }) + this.pluginMap[name] = plugin + this.hashes.push(hash) + return plugin + } + remove(name){ + if(name in this.pluginMap){ + var hash = this.pluginMap[name].hash + if(hash){ + var index = this.hashes.indexOf(hash) + if(index !== -1){ + this.hashes.splice(index, 1) + } + } + this.unload(name) + } + var index = this.allPlugins.findIndex(obj => obj.name === name) + if(index !== -1){ + this.allPlugins.splice(index, 1) + } + var index = this.startOrder.indexOf(name) + if(index !== -1){ + this.startOrder.splice(index, 1) + } + delete this.pluginMap[name] + } + load(name){ + return this.pluginMap[name].load() + } + loadAll(){ + for(var i = 0; i < this.allPlugins.length; i++){ + this.allPlugins[i].plugin.load() + } + } + start(name){ + return this.pluginMap[name].start() + } + startAll(){ + for(var i = 0; i < this.allPlugins.length; i++){ + this.allPlugins[i].plugin.start() + } + } + stop(name){ + return this.pluginMap[name].stop() + } + stopAll(){ + for(var i = this.startOrder.length; i--;){ + this.pluginMap[this.startOrder[i]].stop() + } + } + unload(name){ + return this.pluginMap[name].unload() + } + unloadAll(){ + for(var i = this.startOrder.length; i--;){ + this.pluginMap[this.startOrder[i]].unload() + } + for(var i = this.allPlugins.length; i--;){ + this.allPlugins[i].plugin.unload() + } + } + unloadImported(){ + for(var i = this.startOrder.length; i--;){ + var plugin = this.pluginMap[this.startOrder[i]] + if(plugin.imported){ + plugin.unload() + } + } + for(var i = this.allPlugins.length; i--;){ + var obj = this.allPlugins[i] + if(obj.plugin.imported){ + obj.plugin.unload() + } + } + } + + strFromFunc(func){ + var output = func.toString() + return output.slice(output.indexOf("{") + 1, output.lastIndexOf("}")) + } + argsFromFunc(func){ + var output = func.toString() + output = output.slice(0, output.indexOf("{")) + output = output.slice(output.indexOf("(") + 1, output.lastIndexOf(")")) + return output.split(",").map(str => str.trim()).filter(Boolean) + } + insertBefore(input, insertedText, searchString){ + var index = input.indexOf(searchString) + if(index === -1){ + throw new Error("searchString not found: " + searchString) + } + return input.slice(0, index) + insertedText + input.slice(index) + } + insertAfter(input, searchString, insertedText){ + var index = input.indexOf(searchString) + if(index === -1){ + throw new Error("searchString not found: " + searchString) + } + var length = searchString.length + return input.slice(0, index + length) + insertedText + input.slice(index + length) + } + strReplace(input, searchString, insertedText, repeat=1){ + var position = 0 + for(var i = 0; i < repeat; i++){ + var index = input.indexOf(searchString, position) + if(index === -1){ + if(repeat === Infinity){ + break + }else{ + throw new Error("searchString not found: " + searchString) + } + } + input = input.slice(0, index) + insertedText + input.slice(index + searchString.length) + position = index + insertedText.length + } + return input + } + isObject(input){ + return input && typeof input === "object" && input.constructor === Object + } + deepMerge(target, ...sources){ + sources.forEach(source => { + if(this.isObject(target) && this.isObject(source)){ + for(var i in source){ + if(this.isObject(source[i])){ + if(!target[i]){ + target[i] = {} + } + this.deepMerge(target[i], source[i]) + }else if(source[i]){ + target[i] = source[i] + } + } + } + }) + return target + } + arrayDel(array, item){ + var index = array.indexOf(item) + if(index !== -1){ + array.splice(index, 1) + return true + } + return false + } + + hasSettings(){ + for(var i = 0; i < this.allPlugins.length; i++){ + var plugin = this.allPlugins[i].plugin + if(plugin.loaded && (!plugin.hide || plugin.settings())){ + return true + } + } + return false + } + getSettings(){ + var items = [] + for(var i = 0; i < this.allPlugins.length; i++){ + var obj = this.allPlugins[i] + let plugin = obj.plugin + if(!plugin.loaded){ + continue + } + if(!plugin.hide){ + let description + let description_lang + var module = plugin.module + if(module){ + description = [ + module.description, + module.author ? strings.plugins.author.replace("%s", module.author) : null, + module.version ? strings.plugins.version.replace("%s", module.version) : null + ].filter(Boolean).join("\n") + description_lang = {} + languageList.forEach(lang => { + description_lang[lang] = [ + this.getLocalTitle(module.description, module.description_lang, lang), + module.author ? allStrings[lang].plugins.author.replace("%s", module.author) : null, + module.version ? allStrings[lang].plugins.version.replace("%s", module.version) : null + ].filter(Boolean).join("\n") + }) + } + var name = module && module.name || obj.name + var name_lang = module && module.name_lang + items.push({ + name: name, + name_lang: name_lang, + description: description, + description_lang: description_lang, + type: "toggle", + default: true, + getItem: () => plugin.started, + setItem: value => { + if(plugin.name in this.pluginMap){ + if(plugin.started && !value){ + this.stop(plugin.name) + }else if(!plugin.started && value){ + this.start(plugin.name) + } + } + } + }) + } + var settings = plugin.settings() + if(settings){ + settings.forEach(setting => { + if(!setting.name){ + setting.name = name + if(!setting.name_lang){ + setting.name_lang = name_lang + } + } + if(typeof setting.getItem !== "function"){ + setting.getItem = () => {} + } + if(typeof setting.setItem !== "function"){ + setting.setItem = () => {} + } + if(!("indent" in setting) && !plugin.hide){ + setting.indent = 1 + } + items.push(setting) + }) + } + } + return items + } + getLocalTitle(title, titleLang, lang){ + if(titleLang){ + for(var id in titleLang){ + if(id === (lang || strings.id) && titleLang[id]){ + return titleLang[id] + } + } + } + return title + } +} + +class PluginLoader{ + constructor(...args){ + this.init(...args) + } + init(script, name, hash, raw){ + this.name = name + this.hash = hash + if(typeof script === "string"){ + if(raw){ + this.url = URL.createObjectURL(new Blob([script], { + type: "application/javascript" + })) + }else{ + this.url = script + } + }else{ + this.class = script + } + } + load(loadErrors){ + if(this.loaded){ + return Promise.resolve() + }else if(!this.url && !this.class){ + if(loadErrors){ + return Promise.reject() + }else{ + return Promise.resolve() + } + }else{ + return (this.url ? import(this.url) : Promise.resolve({ + default: this.class + })).then(module => { + if(this.url){ + URL.revokeObjectURL(this.url) + delete this.url + }else{ + delete this.class + } + this.loaded = true + try{ + this.module = new module.default() + }catch(e){ + this.error() + var error = new Error() + error.stack = "Error initializing plugin: " + this.name + "\n" + e.stack + if(loadErrors){ + return Promise.reject(error) + }else{ + console.error(error) + return Promise.resolve() + } + } + var output + try{ + if(this.module.beforeLoad){ + this.module.beforeLoad(this) + } + if(this.module.load){ + output = this.module.load(this) + } + }catch(e){ + this.error() + var error = new Error() + error.stack = "Error in plugin load: " + this.name + "\n" + e.stack + if(loadErrors){ + return Promise.reject(error) + }else{ + console.error(error) + return Promise.resolve() + } + } + if(typeof output === "object" && output.constructor === Promise){ + return output.catch(e => { + this.error() + var error = new Error() + error.stack = "Error in plugin load promise: " + this.name + (e ? "\n" + e.stack : "") + if(loadErrors){ + return Promise.reject(error) + }else{ + console.error(error) + return Promise.resolve() + } + }) + } + }, e => { + this.error() + plugins.remove(this.name) + if(e.name === "SyntaxError"){ + var error = new SyntaxError() + error.stack = "Error in plugin syntax: " + this.name + "\n" + e.stack + }else{ + var error = e + } + if(loadErrors){ + return Promise.reject(error) + }else{ + console.error(error) + return Promise.resolve() + } + }) + } + } + start(orderChange, startErrors){ + if(!orderChange){ + plugins.startOrder.push(this.name) + } + return this.load().then(() => { + if(!this.started && this.module){ + this.started = true + try{ + if(this.module.beforeStart){ + this.module.beforeStart() + } + if(this.module.start){ + this.module.start() + } + }catch(e){ + this.error() + var error = new Error() + error.stack = "Error in plugin start: " + this.name + "\n" + e.stack + if(startErrors){ + return Promise.reject(error) + }else{ + console.error(error) + return Promise.resolve() + } + } + } + }) + } + stop(orderChange, noError){ + if(this.loaded && this.started){ + if(!orderChange){ + var stopIndex = plugins.startOrder.indexOf(this.name) + if(stopIndex !== -1){ + plugins.startOrder.splice(stopIndex, 1) + for(var i = plugins.startOrder.length; i-- > stopIndex;){ + plugins.pluginMap[plugins.startOrder[i]].stop(true) + } + } + } + + this.started = false + try{ + if(this.module.beforeStop){ + this.module.beforeStop() + } + if(this.module.stop){ + this.module.stop() + } + }catch(e){ + var error = new Error() + error.stack = "Error in plugin stop: " + this.name + "\n" + e.stack + console.error(error) + if(!noError){ + this.error() + } + } + + if(!orderChange && stopIndex !== -1){ + for(var i = stopIndex; i < plugins.startOrder.length; i++){ + plugins.pluginMap[plugins.startOrder[i]].start(true) + } + } + } + } + unload(error){ + if(this.loaded){ + if(this.started){ + this.stop(false, error) + } + this.loaded = false + plugins.remove(this.name) + if(this.module){ + try{ + if(this.module.beforeUnload){ + this.module.beforeUnload() + } + if(this.module.unload){ + this.module.unload() + } + }catch(e){ + var error = new Error() + error.stack = "Error in plugin unload: " + this.name + "\n" + e.stack + console.error(error) + } + delete this.module + } + } + } + error(){ + if(this.module && this.module.error){ + try{ + this.module.error() + }catch(e){ + var error = new Error() + error.stack = "Error in plugin error: " + this.name + "\n" + e.stack + console.error(error) + } + } + this.unload(true) + } + settings(){ + if(this.module && this.module.settings){ + try{ + var settings = this.module.settings() + }catch(e){ + console.error(e) + this.error() + return + } + if(Array.isArray(settings)){ + return settings + } + } + } +} + +class EditValue{ + constructor(...args){ + this.init(...args) + } + init(parent, name){ + if(name){ + if(!parent){ + throw new Error("Parent is not defined") + } + this.name = [parent, name] + this.delete = !(name in parent) + }else{ + this.original = parent + } + } + load(callback){ + this.loadCallback = callback + return this + } + start(){ + if(this.name){ + this.original = this.name[0][this.name[1]] + } + try{ + var output = this.loadCallback(this.original) + }catch(e){ + console.error(this.loadCallback) + var error = new Error() + error.stack = "Error editing the value of " + this.getName() + "\n" + e.stack + throw error + } + if(typeof output === "undefined"){ + console.error(this.loadCallback) + throw new Error("Error editing the value of " + this.getName() + ": A value is expected to be returned") + } + if(this.name){ + this.name[0][this.name[1]] = output + } + return output + } + stop(){ + if(this.name){ + if(this.delete){ + delete this.name[0][this.name[1]] + }else{ + this.name[0][this.name[1]] = this.original + } + } + return this.original + } + getName(){ + var name = "unknown" + try{ + if(this.name){ + var name = ( + typeof this.name[0] === "function" && this.name[0].name + || ( + typeof this.name[0] === "object" && typeof this.name[0].constructor === "function" && ( + this.name[0] instanceof this.name[0].constructor ? (() => { + var consName = this.name[0].constructor.name || "" + return consName.slice(0, 1).toLowerCase() + consName.slice(1) + })() : this.name[0].constructor.name + ".prototype" + ) + ) || name + ) + (this.name[1] ? "." + this.name[1] : "") + } + }catch(e){ + name = "error" + } + return name + } + unload(){ + delete this.name + delete this.original + delete this.loadCallback + } +} + +class EditFunction extends EditValue{ + start(){ + if(this.name){ + this.original = this.name[0][this.name[1]] + } + if(typeof this.original !== "function"){ + console.error(this.loadCallback) + var error = new Error() + error.stack = "Error editing the function value of " + this.getName() + ": Original value is not a function" + throw error + } + var args = plugins.argsFromFunc(this.original) + try{ + var output = this.loadCallback(plugins.strFromFunc(this.original), args) + }catch(e){ + console.error(this.loadCallback) + var error = new Error() + error.stack = "Error editing the function value of " + this.getName() + "\n" + e.stack + throw error + } + if(typeof output === "undefined"){ + console.error(this.loadCallback) + throw new Error("Error editing the function value of " + this.getName() + ": A value is expected to be returned") + } + try{ + var output = Function(...args, output) + }catch(e){ + console.error(this.loadCallback) + var error = new SyntaxError() + var blob = new Blob([output], { + type: "application/javascript" + }) + var url = URL.createObjectURL(blob) + error.stack = "Error editing the function value of " + this.getName() + ": Could not evaluate string, check the full string for errors: " + url + "\n" + e.stack + setTimeout(() => { + URL.revokeObjectURL(url) + }, 5 * 60 * 1000) + throw error + } + if(this.name){ + this.name[0][this.name[1]] = output + } + return output + } +} + +class Patch{ + constructor(...args){ + this.init(...args) + } + init(){ + this.edits = [] + this.addedLanguages = [] + } + addEdits(...args){ + args.forEach(arg => this.edits.push(arg)) + } + addLanguage(lang, forceSet, fallback="en"){ + if(fallback){ + lang = plugins.deepMerge({}, allStrings[fallback], lang) + } + this.addedLanguages.push({ + lang: lang, + forceSet: forceSet + }) + } + beforeStart(){ + this.edits.forEach(edit => edit.start()) + this.addedLanguages.forEach(obj => { + settings.addLang(obj.lang, obj.forceSet) + }) + } + beforeStop(){ + for(var i = this.edits.length; i--;){ + this.edits[i].stop() + } + for(var i = this.addedLanguages.length; i--;){ + settings.removeLang(this.addedLanguages[i].lang) + } + } + beforeUnload(){ + this.edits.forEach(edit => edit.unload()) + } + log(...args){ + var name = this.name || "Plugin" + console.log( + "%c[" + name + "]", + "font-weight: bold;", + ...args + ) + } +} diff --git a/public/src/js/scoresheet.js b/public/src/js/scoresheet.js new file mode 100644 index 0000000..a220be9 --- /dev/null +++ b/public/src/js/scoresheet.js @@ -0,0 +1,980 @@ +class Scoresheet{ + constructor(...args){ + this.init(...args) + } + init(controller, results, multiplayer, touchEnabled){ + this.controller = controller + this.resultsObj = results + this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0] + var player0 = this.player[0] + this.results = [] + this.results[player0] = {} + this.rules = [] + this.rules[player0] = this.controller.game.rules + if(multiplayer){ + this.player.push(p2.player === 2 ? 0 : 1) + this.results[this.player[1]] = p2.results + this.rules[this.player[1]] = this.controller.syncWith.game.rules + } + for(var i in results){ + this.results[player0][i] = results[i] === null ? null : results[i].toString() + } + this.multiplayer = multiplayer + this.touchEnabled = touchEnabled + + this.canvas = document.getElementById("canvas") + this.ctx = this.canvas.getContext("2d") + var resolution = settings.getItem("resolution") + var noSmoothing = resolution === "low" || resolution === "lowest" + if(noSmoothing){ + this.ctx.imageSmoothingEnabled = false + } + if(resolution === "lowest"){ + this.canvas.style.imageRendering = "pixelated" + } + this.game = document.getElementById("game") + + this.fadeScreen = document.createElement("div") + this.fadeScreen.id = "fade-screen" + this.game.appendChild(this.fadeScreen) + + this.font = strings.font + this.numbersFont = "TnT, Meiryo, sans-serif" + this.state = { + screen: "fadeIn", + screenMS: this.getMS(), + startDelay: 3300, + hasPointer: 0, + scoreNext: false + } + this.frame = 1000 / 60 + this.numbers = "001122334455667788900112233445".split("") + + this.draw = new CanvasDraw(noSmoothing) + this.canvasCache = new CanvasCache(noSmoothing) + this.nameplateCache = new CanvasCache(noSmoothing) + + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "esc", "don_l", "don_r"] + }, this.keyDown.bind(this)) + this.gamepad = new Gamepad({ + confirm: ["a", "b", "start", "ls", "rs"] + }, this.keyDown.bind(this)) + + this.difficulty = { + "easy": 0, + "normal": 1, + "hard": 2, + "oni": 3, + "ura": 4 + } + + this.scoreSaved = false + this.redrawRunning = true + this.redrawBind = this.redraw.bind(this) + this.redraw() + + assets.sounds["v_results"].play() + assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689) + + this.session = p2.session + if(this.session){ + if(p2.getMessage("songsel")){ + this.toSongsel(true) + } + pageEvents.add(p2, "message", response => { + if(response.type === "songsel"){ + this.toSongsel(true) + } + }) + } + pageEvents.send("scoresheet", { + selectedSong: controller.selectedSong, + autoPlayEnabled: controller.autoPlayEnabled, + multiplayer: multiplayer, + touchEnabled: touchEnabled, + results: this.results, + p2results: multiplayer ? p2.results : null, + keyboardEvents: controller.keyboard.keyboardEvents, + gamepadEvents: controller.keyboard.gamepad.gamepadEvents, + touchEvents: controller.view.touchEvents + }) + } + keyDown(pressed){ + if(pressed && this.redrawing){ + this.toNext() + } + } + mouseDown(event){ + if(event.type === "touchstart"){ + event.preventDefault() + this.canvas.style.cursor = "" + this.state.pointerLocked = true + }else{ + this.state.pointerLocked = false + if(event.which !== 1){ + return + } + } + this.toNext() + } + toNext(){ + var elapsed = this.getMS() - this.state.screenMS + if(this.state.screen === "fadeIn" && elapsed >= this.state.startDelay){ + this.toScoresShown() + }else if(this.state.screen === "scoresShown" && elapsed >= 1000){ + this.toSongsel() + } + } + toScoresShown(){ + if(!p2.session){ + this.state.screen = "scoresShown" + this.state.screenMS = this.getMS() + this.controller.playSound("neiro_1_don", 0, true) + } + } + toSongsel(fromP2){ + if(!p2.session || fromP2){ + snd.musicGain.fadeOut(0.5) + this.state.screen = "fadeOut" + this.state.screenMS = this.getMS() + if(!fromP2){ + this.controller.playSound("neiro_1_don", 0, true) + } + } + } + + startRedraw(){ + this.redrawing = true + requestAnimationFrame(this.redrawBind) + this.winW = null + this.winH = null + + pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.mouseDown.bind(this)) + + if(!this.multiplayer){ + this.tetsuoHana = document.createElement("div") + this.tetsuoHana.id = "tetsuohana" + var flowersBg = "url('" + assets.image["results_flowers"].src + "')" + var mikoshiBg = "url('" + assets.image["results_mikoshi"].src + "')" + var tetsuoHanaBg = "url('" + assets.image["results_tetsuohana" + (debugObj.state === "closed" ? "" : "2")].src + "')" + var id = ["flowers1", "flowers2", "mikoshi", "tetsuo", "hana"] + var bg = [flowersBg, flowersBg, mikoshiBg, tetsuoHanaBg, tetsuoHanaBg] + for(var i = 0; i < id.length; i++){ + if(id[i] === "mikoshi"){ + var divOut = document.createElement("div") + divOut.id = id[i] + "-out" + this.tetsuoHana.appendChild(divOut) + }else{ + var divOut = this.tetsuoHana + } + var div = document.createElement("div") + div.id = id[i] + var divIn = document.createElement("div") + divIn.id = id[i] + "-in" + divIn.style.backgroundImage = bg[i] + div.appendChild(divIn) + divOut.appendChild(div) + } + this.game.appendChild(this.tetsuoHana) + } + } + + redraw(){ + if(!this.redrawRunning){ + return + } + if(this.redrawing){ + requestAnimationFrame(this.redrawBind) + } + var ms = this.getMS() + + if(!this.redrawRunning){ + return + } + + var ctx = this.ctx + ctx.save() + + var winW = innerWidth + var winH = lastHeight + this.pixelRatio = window.devicePixelRatio || 1 + var resolution = settings.getItem("resolution") + if(resolution === "medium"){ + this.pixelRatio *= 0.75 + }else if(resolution === "low"){ + this.pixelRatio *= 0.5 + }else if(resolution === "lowest"){ + this.pixelRatio *= 0.25 + } + winW *= this.pixelRatio + winH *= this.pixelRatio + var ratioX = winW / 1280 + var ratioY = winH / 720 + var ratio = (ratioX < ratioY ? ratioX : ratioY) + + if(this.redrawing){ + if(this.winW !== winW || this.winH !== winH){ + this.canvas.width = Math.max(1, winW) + this.canvas.height = Math.max(1, winH) + ctx.scale(ratio, ratio) + this.canvas.style.width = (winW / this.pixelRatio) + "px" + this.canvas.style.height = (winH / this.pixelRatio) + "px" + + this.canvasCache.resize(winW / ratio, 80 + 1, ratio) + this.nameplateCache.resize(274, 134, ratio + 0.2) + + if(!this.multiplayer){ + this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio) + if(this.tetsuoHanaClass === "dance"){ + this.tetsuoHana.classList.remove("dance", "dance2") + setTimeout(()=>{ + this.tetsuoHana.classList.add("dance2") + },50) + }else if(this.tetsuoHanaClass === "failed"){ + this.tetsuoHana.classList.remove("failed") + setTimeout(()=>{ + this.tetsuoHana.classList.add("failed") + },50) + } + } + }else if(!document.hasFocus() && this.state.screen === "scoresShown"){ + if(this.state["countup0"]){ + this.stopSound("se_results_countup", 0) + } + if(this.state["countup1"]){ + this.stopSound("se_results_countup", 1) + } + return + }else{ + ctx.clearRect(0, 0, winW / ratio, winH / ratio) + } + }else{ + ctx.scale(ratio, ratio) + if(!this.canvasCache.canvas){ + this.canvasCache.resize(winW / ratio, 80 + 1, ratio) + } + if(!this.nameplateCache.canvas){ + this.nameplateCache.resize(274, 67, ratio + 0.2) + } + } + this.winW = winW + this.winH = winH + this.ratio = ratio + winW /= ratio + winH /= ratio + + var frameTop = winH / 2 - 720 / 2 + var frameLeft = winW / 2 - 1280 / 2 + + var players = this.multiplayer ? 2 : 1 + var p2Offset = 298 + + var bgOffset = 0 + var elapsed = ms - this.state.screenMS + if(this.state.screen === "fadeIn" && elapsed < 1000){ + bgOffset = Math.min(1, this.draw.easeIn(1 - elapsed / 1000)) * (winH / 2) + } + if((this.state.screen !== "fadeIn" || elapsed >= 1000) && !this.scoreSaved){ + this.saveScore() + } + + if(bgOffset){ + ctx.save() + ctx.translate(0, -bgOffset) + } + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_score_p1"], + x: 0, + y: 0, + w: winW, + h: winH / 2, + dx: frameLeft - 35, + dy: frameTop + 17 + }) + ctx.fillStyle = "rgba(127, 28, 12, 0.5)" + ctx.fillRect(0, winH / 2 - 12, winW, 12) + ctx.fillStyle = "rgba(0, 0, 0, 0.25)" + ctx.fillRect(0, winH / 2, winW, 20) + if(bgOffset !== 0){ + ctx.fillStyle = "#000" + ctx.fillRect(0, winH / 2 - 2, winW, 2) + } + ctx.fillStyle = "#fa4529" + ctx.fillRect(0, 0, winW, frameTop + 64) + ctx.fillStyle = "#bf2900" + ctx.fillRect(0, frameTop + 64, winW, 8) + + if(bgOffset){ + ctx.restore() + ctx.save() + ctx.translate(0, bgOffset) + } + + this.draw.pattern({ + ctx: ctx, + img: assets.image[this.multiplayer ? "bg_score_p2" : "bg_score_p1"], + x: 0, + y: winH / 2, + w: winW, + h: winH / 2, + dx: frameLeft - 35, + dy: frameTop - 17 + }) + ctx.fillStyle = this.multiplayer ? "rgba(138, 245, 247, 0.5)" : "rgba(249, 163, 149, 0.5)" + ctx.fillRect(0, winH / 2, winW, 12) + ctx.fillStyle = "#000" + if(bgOffset === 0){ + ctx.fillRect(0, winH / 2 - 2, winW, 4) + }else{ + ctx.fillRect(0, winH / 2, winW, 2) + } + ctx.fillStyle = this.multiplayer ? "#6bbec0" : "#fa4529" + ctx.fillRect(0, winH - frameTop - 64, winW, frameTop + 64) + ctx.fillStyle = this.multiplayer ? "rgba(160, 228, 229, 0.8)" : "rgba(255, 144, 116, 0.8)" + ctx.fillRect(0, winH - frameTop - 72, winW, 7) + ctx.fillStyle = this.multiplayer ? "#a8e0e0" : "#ff9b7a" + ctx.fillRect(0, winH - frameTop - 66, winW, 2) + + if(bgOffset){ + ctx.restore() + } + + if(this.state.screen === "scoresShown" || this.state.screen === "fadeOut"){ + var elapsed = Infinity + }else if(this.redrawing){ + var elapsed = ms - this.state.screenMS - this.state.startDelay + }else{ + var elapsed = 0 + } + + var rules = this.controller.game.rules + var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000 + if(players === 2 && failedOffset !== 0){ + var p2results = this.results[this.player[1]] + if(p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)){ + failedOffset = 0 + } + } + if(elapsed >= 3100 + failedOffset){ + for(var p = 0; p < players; p++){ + ctx.save() + var results = this.results[p] + if(!results){ + continue + } + var clear = this.rules[p].clearReached(results.gauge) + if(p === 1 || !this.multiplayer && clear){ + ctx.translate(0, 290) + } + if(clear){ + ctx.globalCompositeOperation = "lighter" + } + ctx.globalAlpha = Math.min(1, Math.max(0, (elapsed - (3100 + failedOffset)) / 500)) * 0.5 + var grd = ctx.createLinearGradient(0, frameTop + 72, 0, frameTop + 368) + grd.addColorStop(0, "#000") + if(clear){ + grd.addColorStop(1, "#ffffba") + }else{ + grd.addColorStop(1, "transparent") + } + ctx.fillStyle = grd + ctx.fillRect(0, frameTop + 72, winW, 286) + ctx.restore() + } + } + + if(elapsed >= 0){ + if(this.state.hasPointer === 0){ + this.state.hasPointer = 1 + if(!this.state.pointerLocked){ + this.canvas.style.cursor = this.session ? "" : "pointer" + } + } + ctx.save() + ctx.setTransform(1, 0, 0, 1, 0, 0) + this.draw.alpha(Math.min(1, elapsed / 400), ctx, ctx => { + ctx.scale(ratio, ratio) + ctx.translate(frameLeft, frameTop) + + this.canvasCache.get({ + ctx: ctx, + x: 0, + y: 0, + w: winW, + h: 80, + id: "results" + }, ctx => { + this.draw.layeredText({ + ctx: ctx, + text: strings.results, + fontSize: 48, + fontFamily: this.font, + x: 23, + y: 15, + letterSpacing: strings.id === "en" ? 0 : 3, + forceShadow: true + }, [ + {x: -2, y: -2, outline: "#000", letterBorder: 22}, + {}, + {x: 2, y: 2, shadow: [2, 2, 7]}, + {x: 2, y: 2, outline: "#ad1516", letterBorder: 10}, + {x: -2, y: -2, outline: "#ff797b"}, + {outline: "#f70808"}, + {fill: "#fff", shadow: [-1, 1, 3, 1.5]} + ]) + + this.draw.layeredText({ + ctx: ctx, + text: this.results[this.player[0]].title, + fontSize: 40, + fontFamily: this.font, + x: 1257, + y: 20, + width: 600, + align: "right", + forceShadow: true + }, [ + {outline: "#000", letterBorder: 10, shadow: [1, 1, 3]}, + {fill: "#fff"} + ]) + }) + + ctx.save() + for(var p = 0; p < players; p++){ + var results = this.results[p] + if(!results){ + continue + } + if(p === 1){ + ctx.translate(0, p2Offset) + } + + ctx.drawImage(assets.image["difficulty"], + 0, 144 * this.difficulty[results.difficulty], + 168, 143, + 300, 150, 189, 162 + ) + var diff = results.difficulty + var text = strings[diff === "ura" ? "oni" : diff] + ctx.font = this.draw.bold(this.font) + "28px " + this.font + ctx.textAlign = "center" + ctx.textBaseline = "bottom" + ctx.strokeStyle = "#000" + ctx.fillStyle = "#fff" + ctx.lineWidth = 9 + ctx.miterLimit = 1 + ctx.strokeText(text, 395, 308) + ctx.fillText(text, 395, 308) + ctx.miterLimit = 10 + + var defaultName = p === 0 ? strings.defaultName : strings.default2PName + if(p === this.player[0]){ + var name = account.loggedIn ? account.displayName : defaultName + }else{ + var name = results.name || defaultName + } + this.nameplateCache.get({ + ctx: ctx, + x: 259, + y: 92, + w: 273, + h: 66, + id: p.toString() + "p" + name, + }, ctx => { + this.draw.nameplate({ + ctx: ctx, + x: 3, + y: 3, + name: name, + font: this.font, + blue: p === 1 + }) + }) + + if(this.controller.autoPlayEnabled){ + ctx.drawImage(assets.image["badge_auto"], + 431, 311, 34, 34 + ) + } + + this.draw.roundedRect({ + ctx: ctx, + x: 532, + y: 98, + w: 728, + h: 232, + radius: 30, + }) + ctx.fillStyle = p === 1 ? "rgba(195, 228, 229, 0.8)" : "rgba(255, 224, 216, 0.8)" + ctx.fill() + this.draw.roundedRect({ + ctx: ctx, + x: 556, + y: 237, + w: 254, + h: 70, + radius: 15, + }) + ctx.fillStyle = "#000" + ctx.fill() + this.draw.roundedRect({ + ctx: ctx, + x: 559, + y: 240, + w: 248, + h: 64, + radius: 14, + }) + ctx.fillStyle = "#eec954" + ctx.fill() + this.draw.roundedRect({ + ctx: ctx, + x: 567, + y: 248, + w: 232, + h: 48, + radius: 6, + }) + ctx.fillStyle = "#000" + ctx.fill() + this.draw.layeredText({ + ctx: ctx, + text: strings.points, + x: 792, + y: strings.id === "ko" ? 260 : 253, + fontSize: 36, + fontFamily: this.font, + align: "right", + width: 36 + }, [ + {fill: "#fff"}, + {outline: "#000", letterBorder: 0.5} + ]) + + this.draw.score({ + ctx: ctx, + score: "good", + x: 823, + y: 192, + results: true + }) + this.draw.score({ + ctx: ctx, + score: "ok", + x: 823, + y: 233, + results: true + }) + this.draw.score({ + ctx: ctx, + score: "bad", + x: 823, + y: 273, + results: true + }) + + ctx.textAlign = "right" + var grd = ctx.createLinearGradient(0, 0, 0, 30) + grd.addColorStop(0.2, "#ff4900") + grd.addColorStop(0.9, "#f7fb00") + this.draw.layeredText({ + ctx: ctx, + text: strings.maxCombo, + x: 1149, + y: 193, + fontSize: 29, + fontFamily: this.font, + align: "right", + width: 154, + letterSpacing: strings.id === "ja" ? 1 : 0 + }, [ + {outline: "#000", letterBorder: 8}, + {fill: grd} + ]) + this.draw.layeredText({ + ctx: ctx, + text: strings.drumroll, + x: 1150, + y: 233, + fontSize: 29, + fontFamily: this.font, + align: "right", + width: 154, + letterSpacing: strings.id === "ja" ? 4 : 0 + }, [ + {outline: "#000", letterBorder: 8}, + {fill: "#ffc700"} + ]) + } + ctx.restore() + }) + ctx.restore() + } + + if(!this.multiplayer){ + if(elapsed >= 400 && elapsed < 3100 + failedOffset){ + if(this.tetsuoHanaClass !== "fadein"){ + this.tetsuoHana.classList.add("fadein") + this.tetsuoHanaClass = "fadein" + } + }else if(elapsed >= 3100 + failedOffset){ + if(this.tetsuoHanaClass !== "dance" && this.tetsuoHanaClass !== "failed"){ + if(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.tetsuoHana.classList.add(this.tetsuoHanaClass) + } + } + } + + if(elapsed >= 800){ + ctx.save() + ctx.setTransform(1, 0, 0, 1, 0, 0) + this.draw.alpha(Math.min(1, (elapsed - 800) / 500), ctx, ctx => { + ctx.scale(ratio, ratio) + ctx.translate(frameLeft, frameTop) + + for(var p = 0; p < players; p++){ + var results = this.results[p] + if(!results){ + continue + } + if(p === 1){ + ctx.translate(0, p2Offset) + } + var w = 712 + this.draw.gauge({ + ctx: ctx, + x: 558 + w, + y: p === 1 ? 124 : 116, + clear: this.rules[p].gaugeClear, + percentage: this.rules[p].gaugePercent(results.gauge), + font: this.font, + scale: w / 788, + scoresheet: true, + blue: p === 1, + multiplayer: p === 1 + }) + this.draw.soul({ + ctx: ctx, + x: 1215, + y: 144, + scale: 36 / 42, + cleared: this.rules[p].clearReached(results.gauge) + }) + } + }) + ctx.restore() + } + + if(elapsed >= 1200){ + ctx.save() + ctx.setTransform(1, 0, 0, 1, 0, 0) + var noCrownResultWait = -2000; + + for(var p = 0; p < players; p++){ + var results = this.results[p] + if(!results){ + continue + } + var crownType = null + if(this.rules[p].clearReached(results.gauge)){ + crownType = results.bad === "0" ? "gold" : "silver" + } + if(crownType !== null){ + noCrownResultWait = 0; + var amount = Math.min(1, (elapsed - 1200) / 450) + this.draw.alpha(this.draw.easeIn(amount), ctx, ctx => { + ctx.save() + ctx.scale(ratio, ratio) + ctx.translate(frameLeft, frameTop) + if(p === 1){ + ctx.translate(0, p2Offset) + } + + var crownScale = 1 + var shine = 0 + if(amount < 1){ + crownScale = 2.8 * (1 - amount) + 0.9 + }else if(elapsed < 1850){ + crownScale = 0.9 + (elapsed - 1650) / 2000 + }else if(elapsed < 2200){ + shine = (elapsed - 1850) / 175 + if(shine > 1){ + shine = 2 - shine + } + } + if(this.state.screen === "fadeIn" && elapsed >= 1200 && !this.state["fullcomboPlayed" + p]){ + this.state["fullcomboPlayed" + p] = true + if(crownType === "gold"){ + this.playSound("v_results_fullcombo" + (p === 1 ? "2" : ""), p) + } + } + if(this.state.screen === "fadeIn" && elapsed >= 1650 && !this.state["crownPlayed" + p]){ + this.state["crownPlayed" + p] = true + this.playSound("se_results_crown", p) + } + this.draw.crown({ + ctx: ctx, + type: crownType, + x: 395, + y: 218, + scale: crownScale, + shine: shine, + whiteOutline: true, + ratio: ratio + }) + + ctx.restore() + }) + } + } + ctx.restore() + } + + if(elapsed >= 2400 + noCrownResultWait){ + ctx.save() + ctx.translate(frameLeft, frameTop) + + var printNumbers = ["good", "ok", "bad", "maxCombo", "drumroll"] + if(!this.state["countupTime0"]){ + var times = {} + var lastTime = 0 + for(var p = 0; p < players; p++){ + var results = this.results[p] + if(!results){ + continue + } + var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame + if(currentTime > lastTime){ + lastTime = currentTime + } + } + for(var i in printNumbers){ + var largestTime = 0 + for(var p = 0; p < players; p++){ + var results = this.results[p] + if(!results){ + continue + } + times[printNumbers[i]] = lastTime + 500 + var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame + if(currentTime > largestTime){ + largestTime = currentTime + } + } + lastTime = largestTime + } + this.state.fadeInEnd = lastTime + for(var p = 0; p < players; p++){ + this.state["countupTime" + p] = times + } + } + + for(var p = 0; p < players; p++){ + var results = this.results[p] + if(!results){ + continue + } + if(p === 1){ + ctx.translate(0, p2Offset) + } + ctx.save() + + this.state.countupShown = false + + var points = this.getNumber(results.points, 3100 + noCrownResultWait, elapsed) + var scale = 1.3 + ctx.font = "35px " + this.numbersFont + ctx.translate(760, 286) + ctx.scale(1 / scale, 1 * 1.1) + ctx.textAlign = "center" + ctx.fillStyle = "#fff" + ctx.strokeStyle = "#fff" + ctx.lineWidth = 0.5 + for(var i = 0; i < points.length; i++){ + ctx.translate(-23.3 * scale, 0) + ctx.fillText(points[points.length - i - 1], 0, 0) + ctx.strokeText(points[points.length - i - 1], 0, 0) + } + ctx.restore() + + if(!this.state["countupTime" + p]){ + var times = {} + var lastTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame + 1000 + for(var i in printNumbers){ + times[printNumbers[i]] = lastTime + 500 + lastTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame + } + this.state["countupTime" + p] = times + } + + for(var i in printNumbers){ + var start = this.state["countupTime" + p][printNumbers[i]] + this.draw.layeredText({ + ctx: ctx, + text: this.getNumber(results[printNumbers[i]], start, elapsed), + x: 971 + 270 * Math.floor(i / 3), + y: 196 + (40 * (i % 3)), + fontSize: 26, + fontFamily: this.numbersFont, + letterSpacing: 1, + align: "right" + }, [ + {outline: "#000", letterBorder: 9}, + {fill: "#fff"} + ]) + } + + if(this.state.countupShown){ + if(!this.state["countup" + p]){ + this.state["countup" + p] = true + this.loopSound("se_results_countup", p, [0.1, false, 0, 0, 0.07]) + } + }else if(this.state["countup" + p]){ + this.state["countup" + p] = false + this.stopSound("se_results_countup", p) + if(this.state.screen === "fadeIn"){ + this.playSound("neiro_1_don", p) + } + } + + if(this.state.screen === "fadeIn" && elapsed >= this.state.fadeInEnd){ + this.state.screen = "scoresShown" + this.state.screenMS = this.getMS() + } + } + ctx.restore() + } + + if(this.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000){ + this.state.scoreNext = true + if(p2.session){ + p2.send("songsel") + }else{ + this.toSongsel(true) + } + } + + if(this.state.screen === "fadeOut"){ + if(this.state.hasPointer === 1){ + this.state.hasPointer = 2 + this.canvas.style.cursor = "" + } + + if(!this.fadeScreenBlack){ + this.fadeScreenBlack = true + this.fadeScreen.style.backgroundColor = "#000" + } + var elapsed = ms - this.state.screenMS + + if(elapsed >= 1000){ + this.clean() + this.controller.songSelection(true, this.showWarning) + } + } + + ctx.restore() + } + + getNumber(score, start, elapsed){ + var numberPos = Math.floor((elapsed - start) / this.frame) + if(numberPos < 0){ + return "" + } + var output = "" + for(var i = 0; i < score.length; i++){ + if(numberPos < 30 * (i + 1)){ + this.state.countupShown = true + return this.numbers[numberPos % 30] + output + }else{ + output = score[score.length - i - 1] + output + } + } + return output + } + + getSound(id, p){ + return assets.sounds[id + (this.multiplayer ? "_p" + (p + 1) : "")] + } + playSound(id, p){ + this.getSound(id, p).play() + } + loopSound(id, p, args){ + this.getSound(id, p).playLoop(...args) + } + stopSound(id, p){ + this.getSound(id, p).stop() + } + + mod(length, index){ + return ((index % length) + length) % length + } + + getMS(){ + return Date.now() + } + + saveScore(){ + if(this.controller.saveScore){ + if(this.resultsObj.points < 0){ + this.resultsObj.points = 0 + } + var title = this.controller.selectedSong.originalTitle + var hash = this.controller.selectedSong.hash + var difficulty = this.resultsObj.difficulty + var oldScore = scoreStorage.get(hash, difficulty, true) + var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge) + var crown = "" + if(clearReached){ + crown = this.resultsObj.bad === 0 ? "gold" : "silver" + } + if(!oldScore || oldScore.points <= this.resultsObj.points){ + if(oldScore && (oldScore.crown === "gold" || oldScore.crown === "silver" && !crown)){ + crown = oldScore.crown + } + this.resultsObj.crown = crown + delete this.resultsObj.title + delete this.resultsObj.difficulty + delete this.resultsObj.gauge + scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => { + this.showWarning = {name: "scoreSaveFailed"} + }) + }else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){ + oldScore.crown = crown + scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => { + this.showWarning = {name: "scoreSaveFailed"} + }) + } + } + this.scoreSaved = true + } + + clean(){ + this.keyboard.clean() + this.gamepad.clean() + this.draw.clean() + this.canvasCache.clean() + assets.sounds["bgm_result"].stop() + snd.buffer.loadSettings() + this.redrawRunning = false + pageEvents.remove(this.canvas, ["mousedown", "touchstart"]) + if(this.touchEnabled){ + pageEvents.remove(document.getElementById("touch-full-btn"), "touchend") + } + if(this.session){ + pageEvents.remove(p2, "message") + } + if(!this.multiplayer){ + delete this.tetsuoHana + } + delete this.ctx + delete this.canvas + delete this.fadeScreen + delete this.results + delete this.rules + } +} diff --git a/public/src/js/scorestorage.js b/public/src/js/scorestorage.js new file mode 100644 index 0000000..6fac5c0 --- /dev/null +++ b/public/src/js/scorestorage.js @@ -0,0 +1,329 @@ +class ScoreStorage{ + constructor(...args){ + this.init(...args) + } + init(){ + this.scores = {} + this.scoresP2 = {} + this.requestP2 = new Set() + this.requestedP2 = new Set() + this.songTitles = {} + this.difficulty = ["oni", "ura", "hard", "normal", "easy"] + this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"] + this.crownValue = ["", "silver", "gold"] + } + load(strings, loadFailed){ + var scores = {} + var scoreStrings = {} + if(loadFailed){ + try{ + var localScores = localStorage.getItem("saveFailed") + if(localScores){ + scoreStrings = JSON.parse(localScores) + } + }catch(e){} + }else if(strings){ + scoreStrings = this.prepareStrings(strings) + }else if(account.loggedIn){ + return + }else{ + try{ + var localScores = localStorage.getItem("scoreStorage") + if(localScores){ + scoreStrings = JSON.parse(localScores) + } + }catch(e){} + } + for(var hash in scoreStrings){ + var scoreString = scoreStrings[hash] + var songAdded = false + if(typeof scoreString === "string" && scoreString){ + var diffArray = scoreString.split(";") + for(var i in this.difficulty){ + if(diffArray[i]){ + var crown = parseInt(diffArray[i].slice(0, 1)) || 0 + var score = { + crown: this.crownValue[crown] || "" + } + var scoreArray = diffArray[i].slice(1).split(",") + for(var j in this.scoreKeys){ + var name = this.scoreKeys[j] + var value = parseInt(scoreArray[j] || 0, 36) || 0 + if(value < 0){ + value = 0 + } + score[name] = value + } + if(!songAdded){ + scores[hash] = {title: null} + songAdded = true + } + scores[hash][this.difficulty[i]] = score + } + } + } + } + if(loadFailed){ + for(var hash in scores){ + for(var i in this.difficulty){ + var diff = this.difficulty[i] + if(scores[hash][diff]){ + this.add(hash, diff, scores[hash][diff], true, this.songTitles[hash] || null).then(() => { + localStorage.removeItem("saveFailed") + }, () => {}) + } + } + } + }else{ + this.scores = scores + this.scoreStrings = scoreStrings + } + if(strings){ + this.load(false, true) + } + } + prepareScores(scores){ + var output = [] + for (var k in scores) { + output.push({'hash': k, 'score': scores[k]}) + } + return output + } + prepareStrings(scores){ + var output = {} + for(var k in scores){ + output[scores[k].hash] = scores[k].score + } + return output + } + save(){ + for(var hash in this.scores){ + this.writeString(hash) + } + this.write() + return this.sendToServer({ + scores: this.prepareScores(this.scoreStrings), + is_import: true + }) + } + write(){ + if(!account.loggedIn){ + try{ + localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings)) + }catch(e){} + } + } + writeString(hash){ + var score = this.scores[hash] + var diffArray = [] + var notEmpty = false + for(var i = this.difficulty.length; i--;){ + var diff = this.difficulty[i] + if(score[diff]){ + var scoreArray = [] + var crown = this.crownValue.indexOf(score[diff].crown).toString() + for(var j in this.scoreKeys){ + var name = this.scoreKeys[j] + var value = score[diff][name] + value = Math.floor(value).toString(36) + scoreArray.push(value) + } + diffArray.unshift(crown + scoreArray.join(",")) + notEmpty = true + }else if(notEmpty){ + diffArray.unshift("") + } + } + this.scoreStrings[hash] = diffArray.join(";") + } + titleHash(song){ + if(song in this.songTitles){ + return this.songTitles[song] + }else{ + return song + } + } + get(song, difficulty, isHash){ + if(!song){ + return this.scores + }else{ + var hash = isHash ? song : this.titleHash(song) + if(difficulty){ + if(hash in this.scores){ + return this.scores[hash][difficulty] + } + }else{ + return this.scores[hash] + } + } + } + getP2(song, difficulty, isHash){ + if(!song){ + return this.scoresP2 + }else{ + var hash = isHash ? song : this.titleHash(song) + if(!(hash in this.scoresP2) && !this.requestP2.has(hash) && !this.requestedP2.has(hash)){ + this.requestP2.add(hash) + this.requestedP2.add(hash) + } + if(difficulty){ + if(hash in this.scoresP2){ + return this.scoresP2[hash][difficulty] + } + }else{ + return this.scoresP2[hash] + } + } + } + add(song, difficulty, scoreObject, isHash, setTitle, saveFailed){ + var hash = isHash ? song : this.titleHash(song) + if(!(hash in this.scores)){ + this.scores[hash] = {} + } + if(difficulty){ + if(setTitle){ + this.scores[hash].title = setTitle + } + this.scores[hash][difficulty] = scoreObject + }else{ + this.scores[hash] = scoreObject + if(setTitle){ + this.scores[hash].title = setTitle + } + } + this.writeString(hash) + this.write() + if(saveFailed){ + var failedScores = {} + try{ + var localScores = localStorage.getItem("saveFailed") + if(localScores){ + failedScores = JSON.parse(localScores) + } + }catch(e){} + if(!(hash in failedScores)){ + failedScores[hash] = {} + } + failedScores[hash] = this.scoreStrings[hash] + try{ + localStorage.setItem("saveFailed", JSON.stringify(failedScores)) + }catch(e){} + return Promise.reject() + }else{ + var obj = {} + obj[hash] = this.scoreStrings[hash] + return this.sendToServer({ + scores: this.prepareScores(obj) + }).catch(() => this.add(song, difficulty, scoreObject, isHash, setTitle, true)) + } + } + addP2(song, difficulty, scoreObject, isHash, setTitle){ + var hash = isHash ? song : this.titleHash(song) + if(!(hash in this.scores)){ + this.scoresP2[hash] = {} + } + if(difficulty){ + if(setTitle){ + this.scoresP2[hash].title = setTitle + } + this.scoresP2[hash][difficulty] = scoreObject + }else{ + this.scoresP2[hash] = scoreObject + if(setTitle){ + this.scoresP2[hash].title = setTitle + } + } + } + template(){ + var template = {crown: ""} + for(var i in this.scoreKeys){ + var name = this.scoreKeys[i] + template[name] = 0 + } + return template + } + remove(song, difficulty, isHash){ + var hash = isHash ? song : this.titleHash(song) + if(hash in this.scores){ + if(difficulty){ + if(difficulty in this.scores[hash]){ + delete this.scores[hash][difficulty] + var noDiff = true + for(var i in this.difficulty){ + if(this.scores[hash][this.difficulty[i]]){ + noDiff = false + break + } + } + if(noDiff){ + delete this.scores[hash] + delete this.scoreStrings[hash] + }else{ + this.writeString(hash) + } + } + }else{ + delete this.scores[hash] + delete this.scoreStrings[hash] + } + this.write() + this.sendToServer({ + scores: this.prepareScores(this.scoreStrings), + is_import: true + }) + } + } + sendToServer(obj, retry){ + if(account.loggedIn){ + return loader.getCsrfToken().then(token => { + var request = new XMLHttpRequest() + request.open("POST", "api/scores/save") + var promise = pageEvents.load(request).then(response => { + if(request.status !== 200){ + return Promise.reject() + } + }).catch(() => { + if(retry){ + this.scoreSaveFailed = true + account.loggedIn = false + delete account.username + delete account.displayName + delete account.don + this.load() + pageEvents.send("logout") + return Promise.reject() + }else{ + return new Promise(resolve => { + setTimeout(() => { + resolve() + }, 3000) + }).then(() => this.sendToServer(obj, true)) + } + }) + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8") + request.setRequestHeader("X-CSRFToken", token) + request.send(JSON.stringify(obj)) + return promise + }) + }else{ + return Promise.resolve() + } + } + eventLoop(){ + if(p2.session && this.requestP2.size){ + var req = [] + this.requestP2.forEach(hash => { + req.push(hash) + }) + this.requestP2.clear() + if(req.length){ + p2.send("getcrowns", req) + } + } + } + clearP2(){ + this.scoresP2 = {} + this.requestP2.clear() + this.requestedP2.clear() + } +} diff --git a/public/src/js/search.js b/public/src/js/search.js new file mode 100644 index 0000000..939b9e9 --- /dev/null +++ b/public/src/js/search.js @@ -0,0 +1,678 @@ +class Search{ + constructor(...args){ + this.init(...args) + } + init(songSelect){ + this.songSelect = songSelect + this.opened = false + this.enabled = true + + this.style = document.createElement("style") + var css = [] + for(var i in this.songSelect.songSkin){ + var skin = this.songSelect.songSkin[i] + if("id" in skin || i === "default"){ + var id = "id" in skin ? ("cat" + skin.id) : i + + css.push(loader.cssRuleset({ + [".song-search-" + id]: { + "background-color": skin.background + }, + [".song-search-" + id + "::before"]: { + "border-color": skin.border[0], + "border-bottom-color": skin.border[1], + "border-right-color": skin.border[1] + }, + [".song-search-" + id + " .song-search-result-title::before, .song-search-" + id + " .song-search-result-subtitle::before"]: { + "-webkit-text-stroke-color": skin.outline + } + })) + } + } + this.style.appendChild(document.createTextNode(css.join("\n"))) + loader.screen.appendChild(this.style) + } + + normalizeString(string){ + string = string + .replace('’', '\'').replace('“', '"').replace('”', '"') + .replace('。', '.').replace(',', ',').replace('、', ',') + + kanaPairs.forEach(pair => { + string = string.replace(pair[1], pair[0]) + }) + + return string.normalize("NFKD").replace(/[\u0300-\u036f]/g, "") + } + + perform(query){ + var results = [] + var filters = {} + + var querySplit = query.split(" ").filter(word => { + if(word.length > 0){ + var parts = word.toLowerCase().split(":") + if(parts.length > 1){ + switch(parts[0]){ + case "easy": + case "normal": + case "hard": + case "oni": + case "ura": + var range = this.parseRange(parts[1]) + if(range){ + filters[parts[0]] = range + } + break + case "extreme": + var range = this.parseRange(parts[1]) + if(range){ + filters.oni = this.parseRange(parts[1]) + } + break + case "clear": + case "silver": + case "gold": + case "genre": + case "lyrics": + case "creative": + case "played": + case "maker": + case "diverge": + case "random": + case "all": + filters[parts[0]] = parts[1] + break + default: + return true + } + return false + } + } + return true + }) + + query = this.normalizeString(querySplit.join(" ").trim()) + + var totalFilters = Object.keys(filters).length + var random = false + var allResults = false + for(var i = 0; i < assets.songs.length; i++){ + var song = assets.songs[i] + var passedFilters = 0 + + Object.keys(filters).forEach(filter => { + var value = filters[filter] + switch(filter){ + case "easy": + case "normal": + case "hard": + case "oni": + case "ura": + if(song.courses[filter] && song.courses[filter].stars >= value.min && song.courses[filter].stars <= value.max){ + passedFilters++ + } + break + case "clear": + case "silver": + case "gold": + if(value === "any"){ + var score = scoreStorage.scores[song.hash] + scoreStorage.difficulty.forEach(difficulty => { + if(score && score[difficulty] && score[difficulty].crown && (filter === "clear" || score[difficulty].crown === filter)){ + passedFilters++ + } + }) + } else { + var score = scoreStorage.scores[song.hash] + if(score && score[value] && score[value].crown && (filter === "clear" || score[value].crown === filter)){ + passedFilters++ + } + } + break + case "played": + var score = scoreStorage.scores[song.hash] + if((value === "yes" && score) || (value === "no" && !score)){ + passedFilters++ + } + break + case "lyrics": + if((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)){ + passedFilters++ + } + break + case "creative": + if((value === "yes" && song.maker) || (value === "no" && !song.maker)){ + passedFilters++ + } + break + case "maker": + if(song.maker && song.maker.name.toLowerCase().includes(value.toLowerCase())){ + passedFilters++ + } + break + case "genre": + var cat = assets.categories.find(cat => cat.id === song.category_id) + var aliases = cat.aliases ? cat.aliases.concat([cat.title]) : [cat.title] + + if(aliases.find(alias => alias.toLowerCase() === value.toLowerCase())){ + passedFilters++ + } + break + case "diverge": + var branch = Object.values(song.courses).find(course => course && course.branch) + if((value === "yes" && branch) || (value === "no" && !branch)){ + passedFilters++ + } + break + case "random": + if(value === "yes" || value === "no"){ + random = value === "yes" + passedFilters++ + } + break + case "all": + if(value === "yes" || value === "no"){ + allResults = value === "yes" + passedFilters++ + } + break + } + }) + + if(passedFilters === totalFilters){ + results.push(song) + } + } + + var maxResults = allResults ? Infinity : (totalFilters > 0 && !query ? 100 : 50) + + if(query){ + results = fuzzysort.go(query, results, { + keys: ["titlePrepared", "subtitlePrepared"], + allowTypo: true, + limit: maxResults, + scoreFn: a => { + if(a[0]){ + var score0 = a[0].score + a[0].ranges = this.indexesToRanges(a[0].indexes) + if(a[0].indexes.length > 1){ + var rangeAmount = a[0].ranges.length + var lastIdx = -3 + a[0].ranges.forEach(range => { + if(range[0] - lastIdx <= 2){ + rangeAmount-- + score0 -= 1000 + } + lastIdx = range[1] + }) + var index = a[0].target.toLowerCase().indexOf(query) + if(index !== -1){ + a[0].ranges = [[index, index + query.length - 1]] + }else if(rangeAmount > a[0].indexes.length / 2){ + score0 = -Infinity + a[0].ranges = null + }else if(rangeAmount !== 1){ + score0 -= 9000 + } + } + } + if(a[1]){ + var score1 = a[1].score - 1000 + a[1].ranges = this.indexesToRanges(a[1].indexes) + if(a[1].indexes.length > 1){ + var rangeAmount = a[1].ranges.length + var lastIdx = -3 + a[1].ranges.forEach(range => { + if(range[0] - lastIdx <= 2){ + rangeAmount-- + score1 -= 1000 + } + lastIdx = range[1] + }) + var index = a[1].target.indexOf(query) + if(index !== -1){ + a[1].ranges = [[index, index + query.length - 1]] + }else if(rangeAmount > a[1].indexes.length / 2){ + score1 = -Infinity + a[1].ranges = null + }else if(rangeAmount !== 1){ + score1 -= 9000 + } + } + } + if(random){ + var rand = Math.random() * -9000 + if(score0 !== -Infinity){ + score0 = rand + } + if(score1 !== -Infinity){ + score1 = rand + } + } + if(a[0]){ + return a[1] ? Math.max(score0, score1) : score0 + }else{ + return a[1] ? score1 : -Infinity + } + } + }) + }else{ + if(random){ + for(var i = results.length - 1; i > 0; i--){ + var j = Math.floor(Math.random() * (i + 1)) + var temp = results[i] + results[i] = results[j] + results[j] = temp + } + } + results = results.slice(0, maxResults).map(result => { + return {obj: result} + }) + } + + return results + } + + createResult(result, resultWidth, fontSize){ + var song = result.obj + var title = this.songSelect.getLocalTitle(song.title, song.title_lang) + var subtitle = this.songSelect.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang) + + var id = "default" + if(song.category_id){ + var cat = assets.categories.find(cat => cat.id === song.category_id) + if(cat && "id" in cat){ + id = "cat" + cat.id + } + } + + var resultDiv = document.createElement("div") + resultDiv.classList.add("song-search-result", "song-search-" + id) + resultDiv.dataset.songId = song.id + + var resultInfoDiv = document.createElement("div") + resultInfoDiv.classList.add("song-search-result-info") + var resultInfoTitle = document.createElement("span") + resultInfoTitle.classList.add("song-search-result-title") + + resultInfoTitle.appendChild(this.highlightResult(title, result[0])) + resultInfoTitle.setAttribute("alt", title) + + resultInfoDiv.appendChild(resultInfoTitle) + + if(subtitle){ + resultInfoDiv.appendChild(document.createElement("br")) + var resultInfoSubtitle = document.createElement("span") + resultInfoSubtitle.classList.add("song-search-result-subtitle") + + resultInfoSubtitle.appendChild(this.highlightResult(subtitle, result[1])) + resultInfoSubtitle.setAttribute("alt", subtitle) + + resultInfoDiv.appendChild(resultInfoSubtitle) + } + + resultDiv.appendChild(resultInfoDiv) + + var courses = ["easy", "normal", "hard", "oni", "ura"] + courses.forEach(course => { + var courseDiv = document.createElement("div") + courseDiv.classList.add("song-search-result-course", "song-search-result-" + course) + if (song.courses[course]) { + var crown = "noclear" + if (scoreStorage.scores[song.hash]) { + if (scoreStorage.scores[song.hash][course]) { + crown = scoreStorage.scores[song.hash][course].crown || "noclear" + } + } + var courseCrown = document.createElement("div") + courseCrown.classList.add("song-search-result-crown", "song-search-result-" + crown) + var courseStars = document.createElement("div") + courseStars.classList.add("song-search-result-stars") + courseStars.innerText = song.courses[course].stars + "\u2605" + + courseDiv.appendChild(courseCrown) + courseDiv.appendChild(courseStars) + } else { + courseDiv.classList.add("song-search-result-hidden") + } + + resultDiv.appendChild(courseDiv) + }) + + this.songSelect.ctx.font = (1.2 * fontSize) + "px " + strings.font + var titleWidth = this.songSelect.ctx.measureText(title).width + var titleRatio = resultWidth / titleWidth + if(titleRatio < 1){ + resultInfoTitle.style.transform = "scale(" + titleRatio + ", 1)" + } + if(subtitle){ + this.songSelect.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font + var subtitleWidth = this.songSelect.ctx.measureText(subtitle).width + var subtitleRatio = resultWidth / subtitleWidth + if(subtitleRatio < 1){ + resultInfoSubtitle.style.transform = "scale(" + subtitleRatio + ", 1)" + } + } + + return resultDiv + } + + highlightResult(text, result){ + var fragment = document.createDocumentFragment() + var ranges = (result ? result.ranges : null) || [] + var lastIdx = 0 + ranges.forEach(range => { + if(lastIdx !== range[0]){ + fragment.appendChild(document.createTextNode(text.slice(lastIdx, range[0]))) + } + var span = document.createElement("span") + span.classList.add("highlighted-text") + span.innerText = text.slice(range[0], range[1] + 1) + fragment.appendChild(span) + lastIdx = range[1] + 1 + }) + if(text.length !== lastIdx){ + fragment.appendChild(document.createTextNode(text.slice(lastIdx))) + } + return fragment + } + + setActive(idx){ + this.songSelect.playSound("se_ka") + var active = this.div.querySelector(":scope .song-search-result-active") + if(active){ + active.classList.remove("song-search-result-active") + } + + if(idx === null){ + this.active = null + return + } + + var el = this.results[idx] + this.input.blur() + el.classList.add("song-search-result-active") + this.scrollTo(el) + + this.active = idx + } + + display(fromButton=false){ + if(!this.enabled){ + return + } + if(this.opened){ + return this.remove(true) + } + + this.opened = true + this.results = [] + this.div = document.createElement("div") + this.div.innerHTML = assets.pages["search"] + + this.container = this.div.querySelector(":scope #song-search-container") + if(this.touchEnabled){ + this.container.classList.add("touch-enabled") + } + pageEvents.add(this.container, ["mousedown", "touchstart"], this.onClick.bind(this)) + + this.input = this.div.querySelector(":scope #song-search-input") + this.input.setAttribute("placeholder", strings.search.searchInput) + pageEvents.add(this.input, ["input"], () => this.onInput()) + + this.songSelect.playSound("se_pause") + loader.screen.appendChild(this.div) + this.setTip() + cancelTouch = false + noResizeRoot = true + if(this.songSelect.songs[this.songSelect.selectedSong].courses){ + snd.previewGain.setVolumeMul(0.5) + }else if(this.songSelect.bgmEnabled){ + snd.musicGain.setVolumeMul(0.5) + } + + setTimeout(() => { + this.input.focus() + this.input.setSelectionRange(0, this.input.value.length) + }, 10) + + var lastQuery = localStorage.getItem("lastSearchQuery") + if(lastQuery){ + this.input.value = lastQuery + this.input.dispatchEvent(new Event("input", { + value: lastQuery + })) + } + } + + remove(byUser=false){ + if(this.opened){ + this.opened = false + if(byUser){ + this.songSelect.playSound("se_cancel") + } + + pageEvents.remove(this.div.querySelector(":scope #song-search-container"), + ["mousedown", "touchstart"]) + pageEvents.remove(this.input, ["input"]) + + this.div.remove() + delete this.results + delete this.div + delete this.input + delete this.tip + delete this.active + cancelTouch = true + noResizeRoot = false + if(this.songSelect.songs[this.songSelect.selectedSong].courses){ + snd.previewGain.setVolumeMul(1) + }else if(this.songSelect.bgmEnabled){ + snd.musicGain.setVolumeMul(1) + } + } + } + + setTip(tip, error=false){ + if(this.tip){ + this.tip.remove() + delete this.tip + } + + if(!tip){ + tip = strings.search.tip + " " + strings.search.tips[Math.floor(Math.random() * strings.search.tips.length)] + } + + var resultsDiv = this.div.querySelector(":scope #song-search-results") + resultsDiv.innerHTML = "" + this.results = [] + + this.tip = document.createElement("div") + this.tip.id = "song-search-tip" + this.tip.innerText = tip + this.div.querySelector(":scope #song-search").appendChild(this.tip) + + if(error){ + this.tip.classList.add("song-search-tip-error") + } + } + + proceed(songId){ + if (/^-?\d+$/.test(songId)) { + songId = parseInt(songId) + } + + var song = this.songSelect.songs.find(song => song.id === songId) + this.remove() + this.songSelect.playBgm(false) + if(this.songSelect.previewing === "muted"){ + this.songSelect.previewing = null + } + + var songIndex = this.songSelect.songs.findIndex(song => song.id === songId) + this.songSelect.setSelectedSong(songIndex) + this.songSelect.toSelectDifficulty() + } + + scrollTo(element){ + var parentNode = element.parentNode + var selected = element.getBoundingClientRect() + var parent = parentNode.getBoundingClientRect() + var scrollY = parentNode.scrollTop + var selectedPosTop = selected.top - selected.height / 2 + if(Math.floor(selectedPosTop) < Math.floor(parent.top)){ + parentNode.scrollTop += selectedPosTop - parent.top + }else{ + var selectedPosBottom = selected.top + selected.height * 1.5 - parent.top + if(Math.floor(selectedPosBottom) > Math.floor(parent.height)){ + parentNode.scrollTop += selectedPosBottom - parent.height + } + } + } + + parseRange(string){ + var range = string.split("-") + if(range.length == 1){ + var min = parseInt(range[0]) || 0 + return min > 0 ? {min: min, max: min} : false + } else if(range.length == 2){ + var min = parseInt(range[0]) || 0 + var max = parseInt(range[1]) || 0 + return min > 0 && max > 0 ? {min: min, max: max} : false + } + } + + indexesToRanges(indexes){ + var ranges = [] + var range + indexes.forEach(idx => { + if(range && range[1] === idx - 1){ + range[1] = idx + }else{ + range = [idx, idx] + ranges.push(range) + } + }) + return ranges + } + + onInput(resize){ + var text = this.input.value + localStorage.setItem("lastSearchQuery", text) + text = text.toLowerCase() + + if(text.length === 0){ + if(!resize){ + this.setTip() + } + return + } + + var new_results = this.perform(text) + + if(new_results.length === 0){ + this.setTip(strings.search.noResults, true) + return + }else if(this.tip){ + this.tip.remove() + delete this.tip + } + + var resultsDiv = this.div.querySelector(":scope #song-search-results") + resultsDiv.innerHTML = "" + this.results = [] + + var fontSize = parseFloat(getComputedStyle(this.div.querySelector(":scope #song-search")).fontSize.slice(0, -2)) + var resultsWidth = parseFloat(getComputedStyle(resultsDiv).width.slice(0, -2)) + var vmin = Math.min(innerWidth, lastHeight) / 100 + var courseWidth = Math.min(3 * fontSize * 1.2, 7 * vmin) + var resultWidth = resultsWidth - 1.8 * fontSize - 0.8 * fontSize - (courseWidth + 0.4 * fontSize * 1.2) * 5 - 0.6 * fontSize + + this.songSelect.ctx.save() + + var fragment = document.createDocumentFragment() + new_results.forEach(result => { + var result = this.createResult(result, resultWidth, fontSize) + fragment.appendChild(result) + this.results.push(result) + }) + resultsDiv.appendChild(fragment) + + this.songSelect.ctx.restore() + } + + onClick(e){ + if((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1){ + this.remove(true) + }else if(e.which === 1){ + var songEl = e.target.closest(".song-search-result") + if(songEl){ + var songId = songEl.dataset.songId + this.proceed(songId) + } + } + } + + keyPress(pressed, name, event, repeat, ctrl){ + if(name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) { + this.remove(true) + if(event){ + event.preventDefault() + } + }else if(name === "down" && this.results.length){ + if(this.input == document.activeElement && this.results){ + this.setActive(0) + }else if(this.active === this.results.length - 1){ + this.setActive(null) + this.input.focus() + }else if(Number.isInteger(this.active)){ + this.setActive(this.active + 1) + }else{ + this.setActive(0) + } + }else if(name === "up" && this.results.length){ + if(this.input == document.activeElement && this.results){ + this.setActive(this.results.length - 1) + }else if(this.active === 0){ + this.setActive(null) + this.input.focus() + setTimeout(() => { + this.input.setSelectionRange(this.input.value.length, this.input.value.length) + }, 0) + }else if(Number.isInteger(this.active)){ + this.setActive(this.active - 1) + }else{ + this.setActive(this.results.length - 1) + } + }else if(name === "confirm"){ + if(Number.isInteger(this.active)){ + this.proceed(this.results[this.active].dataset.songId) + }else{ + this.onInput() + if(event.keyCode === 13 && this.songSelect.touchEnabled){ + this.input.blur() + } + } + } + } + + redraw(){ + if(this.opened && this.container){ + var vmin = Math.min(innerWidth, lastHeight) / 100 + if(this.vmin !== vmin){ + this.container.style.setProperty("--vmin", vmin + "px") + this.vmin = vmin + } + }else{ + this.vmin = null + } + } + + clean(){ + loader.screen.removeChild(this.style) + fuzzysort.cleanup() + delete this.container + delete this.style + delete this.songSelect + } +} \ No newline at end of file diff --git a/public/src/js/session.js b/public/src/js/session.js new file mode 100644 index 0000000..ddd511e --- /dev/null +++ b/public/src/js/session.js @@ -0,0 +1,92 @@ +class Session{ + constructor(...args){ + this.init(...args) + } + init(touchEnabled){ + this.touchEnabled = touchEnabled + loader.changePage("session", true) + this.endButton = this.getElement("view-end-button") + if(touchEnabled){ + this.getElement("view-outer").classList.add("touch-enabled") + } + this.sessionInvite = document.getElementById("session-invite") + + var tutorialTitle = this.getElement("view-title") + tutorialTitle.innerText = strings.session.multiplayerSession + tutorialTitle.setAttribute("alt", strings.session.multiplayerSession) + this.sessionInvite.parentNode.insertBefore(document.createTextNode(strings.session.linkTutorial), this.sessionInvite) + this.endButton.innerText = strings.session.cancel + this.endButton.setAttribute("alt", strings.session.cancel) + + pageEvents.add(window, ["mousedown", "touchstart"], this.mouseDown.bind(this)) + this.keyboard = new Keyboard({ + confirm: ["esc"] + }, this.keyPress.bind(this)) + this.gamepad = new Gamepad({ + confirm: ["start", "b", "ls", "rs"] + }, this.keyPress.bind(this)) + + p2.hashLock = true + pageEvents.add(p2, "message", response => { + if(response.type === "invite"){ + this.sessionInvite.innerText = location.origin + location.pathname + "#" + response.value + p2.hash(response.value) + }else if(response.type === "songsel"){ + p2.clearMessage("users") + this.onEnd(true) + pageEvents.send("session-start", "host") + } + }) + p2.send("invite", { + id: null, + name: account.loggedIn ? account.displayName : null, + don: account.loggedIn ? account.don : null + }) + pageEvents.send("session") + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + mouseDown(event){ + if(event.type === "mousedown" && event.which !== 1){ + return + } + if(event.target === this.sessionInvite){ + this.sessionInvite.focus() + }else{ + getSelection().removeAllRanges() + this.sessionInvite.blur() + } + if(event.target === this.endButton){ + this.onEnd() + } + } + keyPress(pressed){ + if(pressed){ + this.onEnd() + } + } + onEnd(fromP2){ + if(!p2.session){ + p2.send("leave") + p2.hash("") + p2.hashLock = false + pageEvents.send("session-cancel") + }else if(!fromP2){ + return p2.send("songsel") + } + this.clean() + assets.sounds["se_don"].play() + setTimeout(() => { + new SongSelect(false, false, this.touchEnabled) + }, 500) + } + clean(){ + this.keyboard.clean() + this.gamepad.clean() + pageEvents.remove(window, ["mousedown", "touchstart"]) + pageEvents.remove(p2, "message") + delete this.endButton + delete this.sessionInvite + } +} diff --git a/public/src/js/settings.js b/public/src/js/settings.js new file mode 100644 index 0000000..46d4fee --- /dev/null +++ b/public/src/js/settings.js @@ -0,0 +1,1313 @@ +class Settings{ + constructor(...args){ + this.init(...args) + } + init(){ + var ios = /iPhone|iPad/.test(navigator.userAgent) + var phone = /Android|iPhone|iPad/.test(navigator.userAgent) + this.allLanguages = [] + for(var i in allStrings){ + this.allLanguages.push(i) + } + + this.items = { + language: { + type: "language", + options: this.allLanguages, + default: this.getLang() + }, + resolution: { + type: "select", + options: ["high", "medium", "low", "lowest"], + default: phone ? "medium" : "high" + }, + touchAnimation: { + type: "toggle", + default: !ios, + touch: true + }, + keyboardSettings: { + type: "keyboard", + default: { + ka_l: ["d"], + don_l: ["f"], + don_r: ["j"], + ka_r: ["k"] + }, + touch: false + }, + gamepadLayout: { + type: "gamepad", + options: ["a", "b", "c"], + default: "a", + gamepad: true + }, + latency: { + type: "latency", + default: { + "audio": 0, + "video": 0, + "drumSounds": true + } + }, + easierBigNotes: { + type: "toggle", + default: false + }, + showLyrics: { + type: "toggle", + default: true + } + } + + this.storage = {} + try{ + var storage = JSON.parse(localStorage.getItem("settings") || "{}") + for(var i in this.items){ + var current = this.items[i] + if(current.type === "language"){ + this.storage[i] = localStorage.getItem("lang") + if(current.options.indexOf(this.storage[i]) === -1){ + this.storage[i] = null + } + }else if(i in storage){ + if((current.type === "select" || current.type === "gamepad") && current.options.indexOf(storage[i]) === -1){ + this.storage[i] = null + }else if(current.type === "keyboard"){ + var obj = {} + for(var j in current.default){ + if(storage[i] && storage[i][j] && storage[i][j][0]){ + obj[j] = storage[i][j] + }else{ + obj = null + break + } + } + this.storage[i] = obj + }else if(current.type === "latency"){ + var obj = {} + for(var j in current.default){ + if(storage[i] && j in storage[i]){ + if(j === "drumSounds"){ + obj[j] = !!storage[i][j] + continue + }else if(!isNaN(storage[i][j])){ + obj[j] = Math.round(parseFloat(storage[i][j]) || 0) + continue + } + } + obj = null + break + } + this.storage[i] = obj + }else{ + this.storage[i] = storage[i] + } + }else{ + this.storage[i] = null + } + } + }catch(e){ + for(var i in this.items){ + this.storage[i] = null + } + } + } + getItem(name){ + var value = this.storage[name] + return value === null ? this.items[name].default : value + } + setItem(name, value){ + this.storage[name] = value + try{ + if(name === "language"){ + if(value){ + localStorage.setItem("lang", value) + }else{ + localStorage.removeItem("lang") + } + }else{ + var language = this.storage.language + delete this.storage.language + localStorage.setItem("settings", JSON.stringify(this.storage)) + this.storage.language = language + } + }catch(e){} + } + getLang(){ + if("languages" in navigator){ + var userLang = navigator.languages.slice() + userLang.unshift(navigator.language) + for(var i in userLang){ + for(var j in allStrings){ + if(allStrings[j].regex.test(userLang[i])){ + return j + } + } + } + } + return this.allLanguages[0] + } + setLang(lang, noEvent){ + strings = lang + var boldFonts = strings.font === "Microsoft YaHei, sans-serif" + loader.screen.style.fontFamily = strings.font + loader.screen.style.fontWeight = boldFonts ? "bold" : "" + loader.screen.classList[boldFonts ? "add" : "remove"]("bold-fonts") + strings.plural = new Intl.PluralRules(lang.intl) + if(!noEvent){ + pageEvents.send("language-change", lang.id) + } + } + addLang(lang, forceSet){ + allStrings[lang.id] = lang + if(lang.categories){ + assets.categories.forEach(category => { + if("title_lang" in category && lang.categories[category.title_lang.en]){ + category.title_lang[lang.id] = lang.categories[category.title_lang.en] + } + }) + } + languageList.push(lang.id) + this.allLanguages.push(lang.id) + this.items.language.default = this.getLang() + if(forceSet){ + this.storage.language = lang.id + }else{ + try{ + this.storage.language = localStorage.getItem("lang") + }catch(e){} + if(this.items.language.options.indexOf(this.storage.language) === -1){ + this.storage.language = null + } + } + if(settings.getItem("language") === lang.id){ + settings.setLang(lang) + } + } + removeLang(lang){ + delete allStrings[lang.id] + assets.categories.forEach(category => { + if("title_lang" in category){ + delete category.title_lang[lang.id] + } + }) + var index = languageList.indexOf(lang.id) + if(index !== -1){ + languageList.splice(index, 1) + } + var index = this.allLanguages.indexOf(lang.id) + if(index !== -1){ + this.allLanguages.splice(index, 1) + } + this.items.language.default = this.getLang() + try{ + this.storage.language = localStorage.getItem("lang") + }catch(e){} + if(this.items.language.options.indexOf(this.storage.language) === -1){ + this.storage.language = null + } + if(lang.id === strings.id){ + settings.setLang(allStrings[this.getItem("language")]) + } + } +} + +class SettingsView{ + constructor(...args){ + this.init(...args) + } + init(touchEnabled, tutorial, songId, toSetting, settingsItems, noSoundStart){ + this.touchEnabled = touchEnabled + this.tutorial = tutorial + this.songId = songId + this.customSettings = !!settingsItems + this.settingsItems = settingsItems || settings.items + this.locked = false + + loader.changePage("settings", tutorial) + if(!noSoundStart){ + assets.sounds["bgm_settings"].playLoop(0.1, false, 0, 1.392, 26.992) + } + this.defaultButton = document.getElementById("settings-default") + this.viewOuter = this.getElement("view-outer") + if(touchEnabled){ + this.viewOuter.classList.add("touch-enabled") + } + this.touchEnd = [] + this.windowSymbol = Symbol() + this.touchMove = { + active: false, + x: 0, + y: 0 + } + pageEvents.add(window, ["mouseup", "touchstart", "touchmove", "touchend", "blur"], event => { + var move = this.touchMove + if(event.type === "touchstart"){ + var cursor = event.changedTouches[0] + move.active = false + move.x = cursor.pageX + move.y = cursor.pageY + }else if(event.type === "touchmove"){ + var cursor = event.changedTouches[0] + if (Math.abs(move.x - cursor.pageX) > 10 || Math.abs(move.y - cursor.pageY) > 10){ + move.active = true + } + }else{ + this.touchEnd.forEach(func => func(event)) + move.active = false + } + }, this.windowSymbol) + + if(this.customSettings){ + pageEvents.add(window, "language-change", event => this.setLang(), this.windowSymbol) + } + + var gamepadEnabled = false + if("getGamepads" in navigator){ + var gamepads = navigator.getGamepads() + for(var i = 0; i < gamepads.length; i++){ + if(gamepads[i]){ + gamepadEnabled = true + break + } + } + } + this.mode = "settings" + + this.pressedKeys = {} + this.keyboard = new Keyboard({ + "confirm": ["enter", "space", "don_l", "don_r"], + "up": ["up"], + "right": ["right", "ka_r"], + "down": ["down"], + "left": ["left", "ka_l"], + "back": ["esc"], + "other": ["wildcard"] + }, this.keyPressed.bind(this)) + this.gamepad = new Gamepad({ + "confirm": ["b", "ls", "rs"], + "up": ["u", "lsu"], + "right": ["r", "rb", "rt", "lsr"], + "down": ["d", "lsd"], + "left": ["l", "lb", "lt", "lsl"], + "back": ["start", "a"] + }, this.keyPressed.bind(this)) + + this.viewTitle = this.getElement("view-title") + this.endButton = this.getElement("view-end-button") + this.resolution = settings.getItem("resolution") + + var content = this.getElement("view-content") + this.items = [] + this.selected = 0 + for(let i in this.settingsItems){ + var current = this.settingsItems[i] + if( + !touchEnabled && current.touch === true || + touchEnabled && current.touch === false || + !gamepadEnabled && current.gamepad === true + ){ + continue + } + var settingBox = document.createElement("div") + settingBox.classList.add("setting-box") + if(current.indent){ + settingBox.style.marginLeft = (2 * current.indent || 0).toString() + "em" + } + var nameDiv = document.createElement("div") + nameDiv.classList.add("setting-name", "stroke-sub") + if(current.name || current.name_lang){ + var name = this.getLocalTitle(current.name, current.name_lang) + }else{ + var name = strings.settings[i].name + } + this.setAltText(nameDiv, name) + if(current.description || current.description_lang){ + settingBox.title = this.getLocalTitle(current.description, current.description_lang) || "" + } + settingBox.appendChild(nameDiv) + var valueDiv = document.createElement("div") + valueDiv.classList.add("setting-value") + let outputObject = { + id: i, + settingBox: settingBox, + nameDiv: nameDiv, + valueDiv: valueDiv, + name: current.name, + name_lang: current.name_lang, + description: current.description, + description_lang: current.description_lang + } + if(current.type === "number"){ + ["min", "max", "fixedPoint", "step", "sign", "format", "format_lang"].forEach(opt => { + if(opt in current){ + outputObject[opt] = current[opt] + } + }) + outputObject.valueText = document.createTextNode("") + valueDiv.appendChild(outputObject.valueText) + var buttons = document.createElement("div") + buttons.classList.add("latency-buttons") + buttons.title = "" + var buttonMinus = document.createElement("span") + buttonMinus.innerText = "-" + buttons.appendChild(buttonMinus) + this.addTouchRepeat(buttonMinus, event => { + this.numberAdjust(outputObject, -1) + }) + var buttonPlus = document.createElement("span") + buttonPlus.innerText = "+" + buttons.appendChild(buttonPlus) + this.addTouchRepeat(buttonPlus, event => { + this.numberAdjust(outputObject, 1) + }) + valueDiv.appendChild(buttons) + this.addTouch(settingBox, event => { + if(event.target.tagName !== "SPAN"){ + this.setValue(i) + } + }, true) + }else{ + this.addTouchEnd(settingBox, event => this.setValue(i)) + } + settingBox.appendChild(valueDiv) + content.appendChild(settingBox) + if(!toSetting && this.items.length === this.selected || toSetting === i){ + this.selected = this.items.length + settingBox.classList.add("selected") + } + this.items.push(outputObject) + this.getValue(i, valueDiv) + } + var selectBack = this.items.length === 0 + if(this.customSettings){ + var form = document.createElement("form") + this.browse = document.createElement("input") + this.browse.id = "plugin-browse" + this.browse.type = "file" + this.browse.multiple = true + this.browse.accept = ".taikoweb.js" + pageEvents.add(this.browse, "change", this.browseChange.bind(this)) + form.appendChild(this.browse) + this.browseButton = document.createElement("div") + this.browseButton.classList.add("taibtn", "stroke-sub", "plugin-browse-button") + this.browseText = document.createTextNode("") + this.browseButton.appendChild(this.browseText) + this.browseButton.appendChild(form) + this.defaultButton.parentNode.insertBefore(this.browseButton, this.defaultButton) + this.items.push({ + id: "browse", + settingBox: this.browseButton + }) + } + this.showDefault = !this.customSettings || plugins.allPlugins.filter(obj => obj.plugin.imported).length + if(this.showDefault){ + this.items.push({ + id: "default", + settingBox: this.defaultButton + }) + this.addTouch(this.defaultButton, this.defaultSettings.bind(this)) + }else{ + this.defaultButton.parentNode.removeChild(this.defaultButton) + } + this.items.push({ + id: "back", + settingBox: this.endButton + }) + this.addTouch(this.endButton, this.onEnd.bind(this)) + if(selectBack){ + this.selected = this.items.length - 1 + this.endButton.classList.add("selected") + } + + if(!this.customSettings){ + this.gamepadSettings = document.getElementById("settings-gamepad") + this.addTouch(this.gamepadSettings, event => { + if(event.target === event.currentTarget){ + this.gamepadBack() + } + }) + this.gamepadTitle = this.gamepadSettings.getElementsByClassName("view-title")[0] + this.gamepadEndButton = this.gamepadSettings.getElementsByClassName("view-end-button")[0] + this.addTouch(this.gamepadEndButton, event => this.gamepadBack(true)) + this.gamepadBox = this.gamepadSettings.getElementsByClassName("setting-box")[0] + this.addTouch(this.gamepadBox, event => this.gamepadSet(1)) + this.gamepadButtons = document.getElementById("gamepad-buttons") + this.gamepadValue = document.getElementById("gamepad-value") + + this.latencySettings = document.getElementById("settings-latency") + this.addTouch(this.latencySettings, event => { + if(event.target === event.currentTarget){ + this.latencyBack() + } + }) + this.latencyTitle = this.latencySettings.getElementsByClassName("view-title")[0] + this.latencyItems = [] + this.latencySelected = 0 + var latencyContent = this.latencySettings.getElementsByClassName("view-content")[0] + var latencyWindow = ["calibration", "audio", "video", "drumSounds"] + for(let i in latencyWindow){ + let current = latencyWindow[i] + var settingBox = document.createElement("div") + settingBox.classList.add("setting-box") + var nameDiv = document.createElement("div") + nameDiv.classList.add("setting-name", "stroke-sub") + var name = strings.settings.latency[current] + this.setAltText(nameDiv, name) + settingBox.appendChild(nameDiv) + let outputObject = { + id: current, + settingBox: settingBox, + nameDiv: nameDiv + } + if(current === "calibration"){ + nameDiv.style.width = "100%" + }else{ + var valueDiv = document.createElement("div") + valueDiv.classList.add("setting-value") + settingBox.appendChild(valueDiv) + var valueText = document.createTextNode("") + valueDiv.appendChild(valueText) + this.latencyGetValue(current, valueText) + if(current !== "drumSounds"){ + var buttons = document.createElement("div") + buttons.classList.add("latency-buttons") + var buttonMinus = document.createElement("span") + buttonMinus.innerText = "-" + buttons.appendChild(buttonMinus) + this.addTouchRepeat(buttonMinus, event => { + this.latencySetAdjust(outputObject, -1) + }) + var buttonPlus = document.createElement("span") + buttonPlus.innerText = "+" + buttons.appendChild(buttonPlus) + this.addTouchRepeat(buttonPlus, event => { + this.latencySetAdjust(outputObject, 1) + }) + valueDiv.appendChild(buttons) + } + } + latencyContent.appendChild(settingBox) + if(this.latencyItems.length === this.latencySelected){ + settingBox.classList.add("selected") + } + this.addTouch(settingBox, event => { + if(event.target.tagName !== "SPAN"){ + this.latencySetValue(current, event.type === "touchend") + } + }, true) + if(current !== "calibration"){ + outputObject.valueDiv = valueDiv + outputObject.valueText = valueText + outputObject.buttonMinus = buttonMinus + outputObject.buttonPlus = buttonPlus + } + this.latencyItems.push(outputObject) + } + this.latencyDefaultButton = document.getElementById("latency-default") + this.latencyItems.push({ + id: "default", + settingBox: this.latencyDefaultButton + }) + this.addTouch(this.latencyDefaultButton, event => this.latencyDefault()) + this.latencyEndButton = this.latencySettings.getElementsByClassName("view-end-button")[0] + this.latencyItems.push({ + id: "back", + settingBox: this.latencyEndButton + }) + this.addTouch(this.latencyEndButton, event => this.latencyBack(true)) + } + + this.setStrings() + + this.drumSounds = settings.getItem("latency").drumSounds + this.playedSounds = {} + this.redrawRunning = true + this.redrawBind = this.redraw.bind(this) + this.redraw() + if(toSetting === "latency"){ + this.mode = "latency" + this.latencySet() + } + if(this.customSettings){ + pageEvents.send("plugins") + }else{ + pageEvents.send("settings") + } + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + addTouch(element, callback, end){ + var touchEvent = end ? "touchend" : "touchstart" + pageEvents.add(element, ["mousedown", touchEvent], event => { + if(event.type === touchEvent){ + if(event.cancelable){ + event.preventDefault() + } + this.touched = true + }else if(event.which !== 1){ + return + }else{ + this.touched = false + } + if(event.type !== "touchend" || !this.touchMove.active){ + callback(event) + } + }) + } + addTouchEnd(element, callback){ + this.addTouch(element, callback, true) + } + addTouchRepeat(element, callback){ + this.addTouch(element, event => { + var active = true + var func = () => { + active = false + this.touchEnd.splice(this.touchEnd.indexOf(func), 1) + } + this.touchEnd.push(func) + var repeat = delay => { + if(active){ + callback() + setTimeout(() => repeat(50), delay) + } + } + repeat(400) + }) + } + removeTouch(element){ + pageEvents.remove(element, ["mousedown", "touchstart"]) + } + removeTouchEnd(element){ + pageEvents.remove(element, ["mousedown", "touchend"]) + } + getValue(name, valueDiv){ + if(!this.items){ + return + } + var current = this.settingsItems[name] + if(current.getItem){ + var value = current.getItem() + }else{ + var value = settings.getItem(name) + } + if(current.type === "language"){ + value = allStrings[value].name + " (" + value + ")" + }else if(current.type === "select" || current.type === "gamepad"){ + if(current.options_lang && current.options_lang[value]){ + value = this.getLocalTitle(value, current.options_lang[value]) + }else if(!current.getItem){ + value = strings.settings[name][value] + } + }else if(current.type === "toggle"){ + value = value ? strings.settings.on : strings.settings.off + }else if(current.type === "keyboard"){ + valueDiv.innerHTML = "" + for(var i in value){ + var keyDiv = document.createElement("div") + keyDiv.style.color = i === "ka_l" || i === "ka_r" ? "#009aa5" : "#ef2c10" + var key = value[i][0] + for(var j in this.keyboard.substitute){ + if(this.keyboard.substitute[j] === key){ + key = j + break + } + } + keyDiv.innerText = key.toUpperCase() + valueDiv.appendChild(keyDiv) + } + return + }else if(current.type === "latency"){ + var audioVideo = [Math.round(value.audio), Math.round(value.video)] + var latencyValue = strings.settings[name].value.split("%s") + var latencyIndex = 0 + value = "" + latencyValue.forEach((string, i) => { + if(i !== 0){ + value += this.addMs(audioVideo[latencyIndex++]) + } + value += string + }) + }else if(current.type === "number"){ + var mul = Math.pow(10, current.fixedPoint || 0) + this.items[name].value = value * mul + value = Intl.NumberFormat(strings.intl, current.sign ? { + signDisplay: "always" + } : undefined).format(value) + if(current.format || current.format_lang){ + value = this.getLocalTitle(current.format, current.format_lang).replace("%s", value) + } + this.items[name].valueText.data = value + return + } + valueDiv.innerText = value + } + setValue(name){ + if(this.locked){ + return + } + var promise + var current = this.settingsItems[name] + if(current.getItem){ + var value = current.getItem() + }else{ + var value = settings.getItem(name) + } + var selectedIndex = this.items.findIndex(item => item.id === name) + var selected = this.items[selectedIndex] + if(this.mode !== "settings"){ + if(this.mode === "number"){ + return this.numberBack(this.items[this.selected]) + } + if(this.selected === selectedIndex){ + this.keyboardBack(selected) + this.playSound("se_don") + } + return + } + if(this.selected !== selectedIndex){ + this.items[this.selected].settingBox.classList.remove("selected") + this.selected = selectedIndex + selected.settingBox.classList.add("selected") + } + if(current.type === "language" || current.type === "select"){ + value = current.options[this.mod(current.options.length, current.options.indexOf(value) + 1)] + }else if(current.type === "toggle"){ + value = !value + }else if(current.type === "keyboard"){ + this.mode = "keyboard" + selected.settingBox.style.animation = "none" + selected.valueDiv.classList.add("selected") + this.keyboardKeys = {} + this.keyboardSet() + this.playSound("se_don") + return + }else if(current.type === "gamepad"){ + this.mode = "gamepad" + this.gamepadSelected = current.options.indexOf(value) + this.gamepadSet() + this.playSound("se_don") + return + }else if(current.type === "latency"){ + this.mode = "latency" + this.latencySet() + this.playSound("se_don") + return + }else if(current.type === "number"){ + this.mode = "number" + selected.settingBox.style.animation = "none" + selected.valueDiv.classList.add("selected") + this.playSound("se_don") + return + } + if(current.setItem){ + promise = current.setItem(value) + }else{ + settings.setItem(name, value) + } + (promise || Promise.resolve()).then(() => { + this.getValue(name, this.items[this.selected].valueDiv) + this.playSound("se_ka") + if(current.type === "language"){ + this.setLang(allStrings[value]) + } + }) + } + keyPressed(pressed, name, event, repeat){ + if(this.locked){ + return + } + if(pressed){ + if(!this.pressedKeys[name]){ + this.pressedKeys[name] = this.getMS() + 300 + } + }else{ + this.pressedKeys[name] = 0 + return + } + if(repeat && name !== "up" && name !== "right" && name !== "down" && name !== "left"){ + return + } + this.touched = false + var selected = this.items[this.selected] + if(this.mode === "settings"){ + if(name === "confirm"){ + if(selected.id === "back"){ + this.onEnd() + }else if(selected.id === "default"){ + this.defaultSettings() + }else if(selected.id === "browse"){ + if(event){ + this.playSound("se_don") + this.browse.click() + } + }else{ + this.setValue(selected.id) + } + }else if(name === "up" || name === "right" || name === "down" || name === "left"){ + selected.settingBox.classList.remove("selected") + do{ + this.selected = this.mod(this.items.length, this.selected + ((name === "right" || name === "down") ? 1 : -1)) + }while((this.items[this.selected].id === "default" || this.items[this.selected].id === "browse") && name !== "left") + selected = this.items[this.selected] + selected.settingBox.classList.add("selected") + this.scrollTo(selected.settingBox) + this.playSound("se_ka") + }else if(name === "back"){ + this.onEnd() + } + }else if(this.mode === "gamepad"){ + if(name === "confirm"){ + this.gamepadBack(true) + }else if(name === "up" || name === "right" || name === "down" || name === "left"){ + this.gamepadSet((name === "right" || name === "down") ? 1 : -1) + }else if(name === "back"){ + this.gamepadBack() + } + }else if(this.mode === "keyboard"){ + if(name === "back"){ + this.keyboardBack(selected) + this.playSound("se_cancel") + }else if(event){ + event.preventDefault() + var currentKey = event.key.toLowerCase() + for(var i in this.keyboardKeys){ + if(this.keyboardKeys[i][0] === currentKey || !currentKey){ + return + } + } + var current = this.keyboardCurrent + this.playSound(current === "ka_l" || current === "ka_r" ? "se_ka" : "se_don") + this.keyboardKeys[current] = [currentKey] + this.keyboardSet() + } + }else if(this.mode === "latency"){ + var latencySelected = this.latencyItems[this.latencySelected] + if(name === "confirm"){ + if(latencySelected.id === "back"){ + this.latencyBack(true) + }else if(latencySelected.id === "default"){ + this.latencyDefault() + }else{ + this.latencySetValue(latencySelected.id) + } + }else if(name === "up" || name === "right" || name === "down" || name === "left"){ + latencySelected.settingBox.classList.remove("selected") + do{ + this.latencySelected = this.mod(this.latencyItems.length, this.latencySelected + ((name === "right" || name === "down") ? 1 : -1)) + }while(this.latencyItems[this.latencySelected].id === "default" && name !== "left") + latencySelected = this.latencyItems[this.latencySelected] + latencySelected.settingBox.classList.add("selected") + latencySelected.settingBox.scrollIntoView() + this.playSound("se_ka") + }else if(name === "back"){ + this.latencyBack() + } + }else if(this.mode === "latencySet"){ + var latencySelected = this.latencyItems[this.latencySelected] + if(name === "confirm" || name === "back"){ + this.latencySetBack(latencySelected) + this.playSound(name === "confirm" ? "se_don" : "se_cancel") + }else if(name === "up" || name === "right" || name === "down" || name === "left"){ + this.latencySetAdjust(latencySelected, (name === "up" || name === "right") ? 1 : -1) + if(event){ + event.preventDefault() + } + } + }else if(this.mode === "number"){ + if(name === "confirm" || name === "back"){ + this.numberBack(selected) + this.playSound(name === "confirm" ? "se_don" : "se_cancel") + }else if(name === "up" || name === "right" || name === "down" || name === "left"){ + this.numberAdjust(selected, (name === "up" || name === "right") ? 1 : -1) + if(event){ + event.preventDefault() + } + } + } + } + scrollTo(element){ + var parentNode = element.parentNode + var selected = element.getBoundingClientRect() + var parent = parentNode.getBoundingClientRect() + var scrollY = parentNode.scrollTop + var selectedPosTop = selected.top - selected.height / 2 + if(Math.floor(selectedPosTop) < Math.floor(parent.top)){ + parentNode.scrollTop += selectedPosTop - parent.top + }else{ + var selectedPosBottom = selected.top + selected.height * 1.5 - parent.top + if(Math.floor(selectedPosBottom) > Math.floor(parent.height)){ + parentNode.scrollTop += selectedPosBottom - parent.height + } + } + } + keyboardSet(){ + var selected = this.items[this.selected] + var current = this.settingsItems[selected.id] + selected.valueDiv.innerHTML = "" + for(var i in current.default){ + var keyDiv = document.createElement("div") + keyDiv.style.color = i === "ka_l" || i === "ka_r" ? "#009aa5" : "#ef2c10" + if(this.keyboardKeys[i]){ + var key = this.keyboardKeys[i][0] + for(var j in this.keyboard.substitute){ + if(this.keyboard.substitute[j] === key){ + key = j + break + } + } + keyDiv.innerText = key.toUpperCase() + selected.valueDiv.appendChild(keyDiv) + }else{ + keyDiv.innerText = "[" + strings.settings[selected.id][i] + "]" + selected.valueDiv.appendChild(keyDiv) + this.keyboardCurrent = i + return + } + } + settings.setItem(selected.id, this.keyboardKeys) + this.keyboardBack(selected) + this.keyboard.update() + pageEvents.setKbd() + } + keyboardBack(selected){ + this.mode = "settings" + selected.settingBox.style.animation = "" + selected.valueDiv.classList.remove("selected") + this.getValue(selected.id, selected.valueDiv) + } + gamepadSet(diff){ + if(this.mode !== "gamepad"){ + return + } + var selected = this.items[this.selected] + var current = this.settingsItems[selected.id] + if(diff){ + this.gamepadSelected = this.mod(current.options.length, this.gamepadSelected + diff) + this.playSound("se_ka") + } + var opt = current.options[this.gamepadSelected] + var value = strings.settings[selected.id][opt] + this.setAltText(this.gamepadValue, value) + this.gamepadButtons.style.backgroundPosition = "0 " + (-11.87 - 4.93 * this.gamepadSelected) + "em" + this.gamepadSettings.style.display = "flex" + } + gamepadBack(confirm){ + if(this.mode !== "gamepad"){ + return + } + var selected = this.items[this.selected] + var current = this.settingsItems[selected.id] + settings.setItem(selected.id, current.options[this.gamepadSelected]) + this.getValue(selected.id, selected.valueDiv) + this.playSound(confirm ? "se_don" : "se_cancel") + this.gamepadSettings.style.display = "" + this.mode = "settings" + } + latencySet(){ + if(this.mode !== "latency"){ + return + } + var selected = this.items[this.selected] + var current = this.settingsItems[selected.id] + this.latencySettings.style.display = "flex" + } + latencyGetValue(name, valueText){ + var currentLatency = settings.getItem("latency") + if(name === "drumSounds"){ + valueText.data = currentLatency[name] ? strings.settings.on : strings.settings.off + }else{ + valueText.data = this.addMs(currentLatency[name] || 0) + } + } + latencySetValue(name, touched){ + var selectedIndex = this.latencyItems.findIndex(item => item.id === name) + var selected = this.latencyItems[selectedIndex] + if(this.mode === "latencySet"){ + this.latencySetBack(this.latencyItems[this.latencySelected]) + if(this.latencySelected === selectedIndex){ + this.playSound("se_don") + return + } + }else if(this.mode !== "latency"){ + return + } + if(name === "calibration"){ + this.playSound("se_don") + this.clean() + new LoadSong({ + "title": strings.calibration.title, + "folder": "calibration", + "type": "tja", + "songSkin": {} + }, false, false, touched) + }else if(name === "drumSounds"){ + this.drumSounds = !settings.getItem("latency")[name] + this.latencySave(name, this.drumSounds) + this.latencyGetValue(name, selected.valueText) + this.playSound("se_don") + }else{ + var value = Math.round(settings.getItem("latency")[name] || 0) + if(this.latencySelected !== selectedIndex){ + this.latencyItems[this.latencySelected].settingBox.classList.remove("selected") + this.latencySelected = selectedIndex + selected.settingBox.classList.add("selected") + } + this.mode = "latencySet" + selected.settingBox.style.animation = "none" + selected.valueDiv.classList.add("selected") + selected.value = value + this.playSound("se_don") + } + } + latencySetAdjust(selected, add){ + selected.value += add + if(selected.value > 500){ + selected.value = 500 + }else if(selected.value < -200){ + selected.value = -200 + }else{ + this.playSound("se_ka") + } + selected.valueText.data = this.addMs(selected.value) + } + latencySetBack(selected){ + this.mode = "latency" + selected.settingBox.style.animation = "" + selected.valueDiv.classList.remove("selected") + this.latencySave(selected.id, selected.value) + this.latencyGetValue(selected.id, selected.valueText) + } + latencySave(id, value){ + var input = settings.getItem("latency") + var output = {} + for(var i in input){ + if(i === id){ + output[i] = value + }else{ + output[i] = input[i] + } + } + settings.setItem("latency", output) + } + latencyDefault(){ + if(this.mode === "latencySet"){ + this.latencySetBack(this.latencyItems[this.latencySelected]) + }else if(this.mode !== "latency"){ + return + } + settings.setItem("latency", null) + this.latencyItems.forEach(item => { + if(item.id === "audio" || item.id === "video" || item.id === "drumSounds"){ + this.latencyGetValue(item.id, item.valueText) + } + }) + this.drumSounds = settings.getItem("latency").drumSounds + this.playSound("se_don") + } + latencyBack(confirm){ + if(this.mode === "latencySet"){ + this.latencySetBack(this.latencyItems[this.latencySelected]) + if(!confirm){ + this.playSound("se_don") + return + } + } + if(this.mode !== "latency"){ + return + } + var selected = this.items[this.selected] + var current = this.settingsItems[selected.id] + this.getValue(selected.id, selected.valueDiv) + this.playSound(confirm ? "se_don" : "se_cancel") + this.latencySettings.style.display = "" + this.mode = "settings" + } + numberAdjust(selected, add){ + var selectedItem = this.items[this.selected] + var mul = Math.pow(10, selected.fixedPoint || 0) + selectedItem.value += add * ("step" in selected ? selected.step : 1) + if("max" in selected && selectedItem.value > selected.max * mul){ + selectedItem.value = selected.max * mul + }else if("min" in selected && selectedItem.value < selected.min * mul){ + selectedItem.value = selected.min * mul + }else{ + this.playSound("se_ka") + } + var valueText = Intl.NumberFormat(strings.intl, selected.sign ? { + signDisplay: "always" + } : undefined).format(selectedItem.value / mul) + if(selected.format || selected.format_lang){ + valueText = this.getLocalTitle(selected.format, selected.format_lang).replace("%s", valueText) + } + selectedItem.valueText.data = valueText + } + numberBack(selected){ + this.mode = "settings" + selected.settingBox.style.animation = "" + selected.valueDiv.classList.remove("selected") + var current = this.settingsItems[selected.id] + var promise + var mul = Math.pow(10, selected.fixedPoint || 0) + var value = selected.value / mul + if(current.setItem){ + promise = current.setItem(value) + }else{ + settings.setItem(selected.id, value) + } + (promise || Promise.resolve()).then(() => { + this.getValue(selected.id, selected.valueText) + }) + } + addMs(input){ + var split = strings.calibration.ms.split("%s") + var index = 0 + var output = "" + var inputStrings = [(input > 0 ? "+" : "") + input.toString()] + split.forEach((string, i) => { + if(i !== 0){ + output += inputStrings[index++] + } + output += string + }) + return output + } + defaultSettings(){ + if(this.customSettings){ + plugins.unloadImported() + this.clean(true) + this.playSound("se_don") + return setTimeout(() => this.restart(), 500) + } + if(this.mode === "keyboard"){ + this.keyboardBack(this.items[this.selected]) + } + for(var i in this.settingsItems){ + settings.setItem(i, null) + } + this.setLang(allStrings[settings.getItem("language")]) + this.keyboard.update() + pageEvents.setKbd() + this.latencyItems.forEach(item => { + if(item.id === "audio" || item.id === "video" || item.id === "drumSounds"){ + this.latencyGetValue(item.id, item.valueText) + } + }) + this.drumSounds = settings.getItem("latency").drumSounds + this.playSound("se_don") + } + browseChange(event){ + this.locked = true + var files = [] + for(var i = 0; i < event.target.files.length; i++){ + files.push(new LocalFile(event.target.files[i])) + } + var customSongs = new CustomSongs(this.touchEnabled, true) + customSongs.importLocal(files).then(() => { + this.clean(true) + return this.restart() + }).catch(e => { + if(e){ + var message = e.message + if(e.name === "nosongs"){ + message = strings.plugins.noPlugins + } + if(message){ + alert(message) + } + } + this.locked = false + this.browse.form.reset() + return Promise.resolve() + }) + } + onEnd(){ + if(this.mode === "number"){ + this.numberBack(this.items[this.selected]) + } + this.clean() + this.playSound("se_don") + setTimeout(() => { + if(this.tutorial && !this.touched){ + new Tutorial(false, this.songId) + }else{ + try{ + localStorage.setItem("tutorial", "true") + }catch(e){} + new SongSelect(this.tutorial ? false : this.customSettings ? "plugins" : "settings", false, this.touched, this.songId) + } + }, 500) + } + restart(){ + if(this.mode === "number"){ + this.numberBack(this.items[this.selected]) + } + return new SettingsView(this.touchEnabled, this.tutorial, this.songId, undefined, this.customSettings ? plugins.getSettings() : undefined, true) + } + getLocalTitle(title, titleLang){ + if(titleLang){ + for(var id in titleLang){ + if(id === strings.id && titleLang[id]){ + return titleLang[id] + } + } + } + return title + } + setLang(lang){ + if(lang){ + settings.setLang(lang) + } + if(failedTests.length !== 0){ + showUnsupported(strings) + } + for(var i in this.items){ + var item = this.items[i] + if(item.valueDiv){ + if(item.name || item.name_lang){ + var name = this.getLocalTitle(item.name, item.name_lang) + }else{ + var name = strings.settings[item.id].name + } + this.setAltText(item.nameDiv, name) + if(item.description || item.description_lang){ + item.settingBox.title = this.getLocalTitle(item.description, item.description_lang) || "" + } + this.getValue(item.id, item.valueDiv) + } + } + for(var i in this.latencyItems){ + var current = this.latencyItems[i] + if(current.nameDiv){ + this.setAltText(current.nameDiv, strings.settings.latency[current.id]) + } + if(current.valueText){ + this.latencyGetValue(current.id, current.valueText) + } + } + this.setStrings() + } + setStrings(){ + this.setAltText(this.viewTitle, this.customSettings ? strings.plugins.title : strings.gameSettings) + this.setAltText(this.endButton, strings.settings.ok) + if(this.customSettings){ + this.browseText.data = strings.plugins.browse + this.browseButton.setAttribute("alt", strings.plugins.browse) + }else{ + this.setAltText(this.gamepadTitle, strings.settings.gamepadLayout.name) + this.setAltText(this.gamepadEndButton, strings.settings.ok) + this.setAltText(this.latencyTitle, strings.settings.latency.name) + this.setAltText(this.latencyDefaultButton, strings.settings.default) + this.setAltText(this.latencyEndButton, strings.settings.ok) + } + if(this.showDefault){ + this.setAltText(this.defaultButton, this.customSettings ? strings.plugins.unloadAll : strings.settings.default) + } + } + setAltText(element, text){ + element.innerText = text + element.setAttribute("alt", text) + } + mod(length, index){ + return ((index % length) + length) % length + } + playSound(id, time){ + if(!this.drumSounds && (id === "se_don" || id === "se_ka" || id === "se_cancel")){ + return + } + var ms = Date.now() + (time || 0) * 1000 + if(!(id in this.playedSounds) || ms > this.playedSounds[id] + 30){ + assets.sounds[id].play(time) + this.playedSounds[id] = ms + } + } + redraw(){ + if(!this.redrawRunning){ + return + } + requestAnimationFrame(this.redrawBind) + var ms = this.getMS() + + for(var key in this.pressedKeys){ + if(this.pressedKeys[key]){ + if(ms >= this.pressedKeys[key] + 50){ + this.keyPressed(true, key, null, true) + this.pressedKeys[key] = ms + } + } + } + } + getMS(){ + return Date.now() + } + clean(noSoundStop){ + this.redrawRunning = false + this.keyboard.clean() + this.gamepad.clean() + if(!noSoundStop){ + assets.sounds["bgm_settings"].stop() + } + pageEvents.remove(window, ["mouseup", "touchstart", "touchmove", "touchend", "blur"], this.windowSymbol) + if(this.customSettings){ + pageEvents.remove(window, "language-change", this.windowSymbol) + } + for(var i in this.items){ + this.removeTouchEnd(this.items[i].settingBox) + } + for(var i in this.latencyItems){ + this.removeTouch(this.latencyItems[i].settingBox) + if(this.latencyItems[i].buttonMinus){ + this.removeTouch(this.latencyItems[i].buttonMinus) + this.removeTouch(this.latencyItems[i].buttonPlus) + } + } + if(this.defaultButton){ + delete this.defaultButton + } + if(this.customSettings){ + pageEvents.remove(this.browse, "change") + delete this.browse + delete this.browseButton + delete this.browseText + }else{ + this.removeTouch(this.gamepadSettings) + this.removeTouch(this.gamepadEndButton) + this.removeTouch(this.gamepadBox) + this.removeTouch(this.latencySettings) + this.removeTouch(this.latencyDefaultButton) + this.removeTouch(this.latencyEndButton) + } + delete this.windowSymbol + delete this.touchMove + delete this.viewOuter + delete this.touchEnd + delete this.tutorialTitle + delete this.endButton + delete this.items + delete this.gamepadSettings + delete this.gamepadTitle + delete this.gamepadEndButton + delete this.gamepadBox + delete this.gamepadButtons + delete this.gamepadValue + delete this.latencyItems + delete this.latencySettings + delete this.latencyTitle + delete this.latencyDefaultButton + delete this.latencyEndButton + if(this.resolution !== settings.getItem("resolution")){ + for(var i in assets.image){ + if(i === "touch_drum" || i.startsWith("bg_song_") || i.startsWith("bg_stage_") || i.startsWith("bg_don_") || i.startsWith("results_")){ + var img = assets.image[i] + URL.revokeObjectURL(img.src) + if(img.parentNode){ + img.parentNode.removeChild(img) + } + delete assets.image[i] + } + } + } + } +} diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js new file mode 100644 index 0000000..58ba125 --- /dev/null +++ b/public/src/js/songselect.js @@ -0,0 +1,3277 @@ +class SongSelect{ + constructor(...args){ + this.init(...args) + } + init(fromTutorial, fadeIn, touchEnabled, songId, showWarning){ + this.touchEnabled = touchEnabled + + loader.changePage("songselect", false) + this.canvas = document.getElementById("song-sel-canvas") + this.ctx = this.canvas.getContext("2d") + var resolution = settings.getItem("resolution") + var noSmoothing = resolution === "low" || resolution === "lowest" + if(noSmoothing){ + this.ctx.imageSmoothingEnabled = false + } + if(resolution === "lowest"){ + this.canvas.style.imageRendering = "pixelated" + } + + let rand = () => { + let color = Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0"); + return `#${color}`; + } + + this.songSkin = { + "selected": { + background: "#ffdb2c", + border: ["#fff4b5", "#ffa600"], + outline: "#000" + }, + "back": { + background: "#efb058", + border: ["#ffe7bd", "#c68229"], + outline: "#ad7723" + }, + "random": { + sort: 0, + background: "#fa91ff", + border: ["#ffdfff", "#b068b2"], + outline: "#b221bb" + }, + "search": { + sort: 0, + background: "#FF5266", + border: ["#FF9FB7", "#BE1432"], + outline: "#A50B15" + }, + "tutorial": { + sort: 0, + background: "#29e8aa", + border: ["#86ffbd", "#009a8c"], + outline: "#08a28c" + }, + "about": { + sort: 0, + background: "#a2d0e7", + border: ["#c6dfff", "#4485d9"], + outline: "#2390d9" + }, + "settings": { + sort: 0, + background: "#ce93fa", + border: ["#dec4fd", "#a543ef"], + outline: "#a741ef" + }, + "customSongs": { + sort: 0, + background: "#fab5d3", + border: ["#ffe7ef", "#d36aa2"], + outline: "#d36aa2" + }, + "plugins": { + sort: 0, + background: "#f6bba1", + border: ["#fde9df", "#ce7553"], + outline: "#ce7553" + }, + // カスタム曲スキン + "upload": { + sort: 0, + background: "#ffe57f", + border: ["#ffd54f", "#ff9800"], + outline: "#ffab40", + }, + "keijiban": { + sort: 0, + background: "#1c1c1c", + border: ["#000000", "#333333"], + outline: "#222222", + }, + "customSettings": { + sort: 0, + background: "#a5d6a7", // 緑色の背景 + border: ["#81c784", "#66bb6a"], // 緑色の境界線 + outline: "#388e3c" // 緑色のアウトライン + }, + "default": { + sort: null, + background: `${rand()}`, + border: [`${rand()}`, `${rand()}`], + outline: `#333333`, + infoFill: `${rand()}` + } + } + + var songSkinLength = Object.keys(this.songSkin).length + for(var i in assets.categories){ + var category = assets.categories[i] + if(!this.songSkin[category.title] && category.songSkin){ + if(category.songSkin.sort === null){ + category.songSkin.sort = songSkinLength + 1 + } + category.songSkin.id = category.id + this.songSkin[category.title] = category.songSkin + } + } + this.songSkin["default"].sort = songSkinLength + 1 + + this.font = strings.font + + this.search = new Search(this) + + this.songs = [] + for(let song of assets.songs){ + var title = this.getLocalTitle(song.title, song.title_lang) + song.titlePrepared = title ? fuzzysort.prepare(this.search.normalizeString(title)) : null + var subtitle = this.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang) + song.subtitlePrepared = subtitle ? fuzzysort.prepare(this.search.normalizeString(subtitle)) : null + this.songs.push(this.addSong(song)) + } + this.songs.sort((a, b) => { + var catA = a.originalCategory in this.songSkin ? this.songSkin[a.originalCategory] : this.songSkin.default + var catB = b.originalCategory in this.songSkin ? this.songSkin[b.originalCategory] : this.songSkin.default + if(catA.sort !== catB.sort){ + return catA.sort > catB.sort ? 1 : -1 + }else if(a.originalCategory !== b.originalCategory){ + return a.originalCategory > b.originalCategory ? 1 : -1 + }else if(a.order !== b.order){ + return a.order > b.order ? 1 : -1 + }else{ + return a.id > b.id ? 1 : -1 + } + }) + const titlesort = localStorage.getItem("titlesort") ?? "false"; + if (titlesort === "true") { + this.songs.sort((a, b) => a.title.localeCompare(b.title)); + } + if(assets.songs.length){ + this.songs.push({ + title: strings.back, + skin: this.songSkin.back, + action: "back" + }) + this.songs.push({ + title: strings.randomSong, + skin: this.songSkin.random, + action: "random", + category: strings.random, + canJump: true, + p2Enabled: true + }) + this.songs.push({ + title: strings.search.search, + skin: this.songSkin.search, + action: "search", + category: strings.random, + p2Enabled: true + }) + } + if(touchEnabled){ + if(fromTutorial === "tutorial"){ + fromTutorial = false + } + }else{ + this.songs.push({ + title: strings.howToPlay, + skin: this.songSkin.tutorial, + action: "tutorial", + category: strings.random + }) + } + this.showWarning = showWarning + if(showWarning && showWarning.name === "scoreSaveFailed"){ + scoreStorage.scoreSaveFailed = true + } + this.songs.push({ + title: strings.aboutSimulator, + skin: this.songSkin.about, + action: "about", + category: strings.random + }) + this.songs.push({ + title: strings.gameSettings, + skin: this.songSkin.settings, + action: "settings", + category: strings.random + }) + + var showCustom = false + if(gameConfig.google_credentials.gdrive_enabled){ + showCustom = true + }else if("webkitdirectory" in HTMLInputElement.prototype && !(/Android|iPhone|iPad/.test(navigator.userAgent))){ + showCustom = true + } + if(showCustom){ + this.songs.push({ + title: assets.customSongs ? strings.customSongs.default : strings.customSongs.title, + skin: this.songSkin.customSongs, + action: "customSongs", + category: strings.random + }) + } + this.songs.push({ + title: strings.plugins.title, + skin: this.songSkin.plugins, + action: "plugins", + category: strings.random + }) + + // カスタムメニュー + // this.songs.push({ + // title: "ソースコード", + // skin: this.songSkin.sourceCode, + // action: "sourceCode", + // }); + // for (let i = 0; i < 10; i++) { + this.songs.push({ + title: "曲を投稿!", + skin: this.songSkin.upload, + action: "upload", + }); + // } + this.songs.push({ + title: "掲示板", + skin: this.songSkin.keijiban, + action: "keijiban", + }); + + this.songs.push({ + title: "曲選択速度", + skin: this.songSkin.customSettings, + action: "songSelectingSpeed", + }); + + this.songs.push({ + title: "ばいそく", + skin: this.songSkin.customSettings, + action: "baisoku", + }); + + this.songs.push({ + title: "ドロン", + skin: this.songSkin.customSettings, + action: "doron", + }); + + this.songs.push({ + title: "あべこべ", + skin: this.songSkin.customSettings, + action: "abekobe", + }); + + this.songs.push({ + title: "でたらめ", + skin: this.songSkin.customSettings, + action: "detarame", + }); + + + this.songs.push({ + title: "タイトル順で並べ替え", + skin: this.songSkin.customSettings, + action: "titlesort", + }); + + this.songs.push({ + title: strings.back, + skin: this.songSkin.back, + action: "back" + }) + + this.songAsset = { + marginTop: 104, + marginLeft: 18, + width: 82, + selectedWidth: 382, + fullWidth: 912, + height: 452, + fullHeight: 502, + border: 6, + innerBorder: 8, + letterBorder: 12 + } + + this.diffOptions = [{ + text: strings.back, + fill: "#efb058", + iconName: "back", + iconFill: "#f7d39c", + letterSpacing: 4 + }, { + text: strings.songOptions, + fill: "#b2e442", + iconName: "options", + iconFill: "#d9f19f", + letterSpacing: 0 + }, { + text: "ダウンロード", + fill: "#e7a9da", + iconName: "download", + iconFill: "#e7cbe1", + letterSpacing: 4 + }, { + text: "削除", + fill: "silver", + iconName: "trash", + iconFill: "#111111", + letterSpacing: 4 + }] + this.optionsList = [strings.none, strings.auto, strings.netplay] + + this.draw = new CanvasDraw(noSmoothing) + this.songTitleCache = new CanvasCache(noSmoothing) + this.selectTextCache = new CanvasCache(noSmoothing) + this.categoryCache = new CanvasCache(noSmoothing) + this.difficultyCache = new CanvasCache(noSmoothing) + this.sessionCache = new CanvasCache(noSmoothing) + this.currentSongCache = new CanvasCache(noSmoothing) + this.nameplateCache = new CanvasCache(noSmoothing) + + + this.difficulty = [strings.easy, strings.normal, strings.hard, strings.oni] + this.difficultyId = ["easy", "normal", "hard", "oni", "ura"] + + this.sessionText = { + "sessionstart": strings.sessionStart, + "sessionend": strings.sessionEnd + } + + this.selectedSong = 0 + this.selectedDiff = 0 + this.lastCurrentSong = {} + this.lastRandom = false + assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506) + + if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage) && !songId){ + fromTutorial = touchEnabled ? "about" : "tutorial" + } + if(p2.session || assets.customSongs && "customSelected" in localStorage){ + fromTutorial = false + } + + this.drumSounds = settings.getItem("latency").drumSounds + this.playedSounds = {} + + var songIdIndex = -1 + var newSelected = -1 + if(fromTutorial){ + newSelected = this.songs.findIndex(song => song.action === fromTutorial) + } + if(newSelected !== -1){ + this.setSelectedSong(newSelected, false) + this.playBgm(true) + }else{ + if(songId){ + songIdIndex = this.songs.findIndex(song => song.id === songId) + if(songIdIndex === -1){ + this.clearHash() + } + } + if(songIdIndex !== -1){ + this.setSelectedSong(songIdIndex, false) + }else if(assets.customSongs){ + this.setSelectedSong(Math.min(Math.max(0, assets.customSelected), this.songs.length - 1), false) + }else if((!p2.session || fadeIn) && "selectedSong" in localStorage){ + this.setSelectedSong(Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1), false) + } + if(!this.showWarning){ + this.playSound(songIdIndex !== -1 ? "v_diffsel" : "v_songsel") + } + snd.musicGain.fadeOut() + this.playBgm(false) + } + if("selectedDiff" in localStorage){ + this.selectedDiff = Math.min(Math.max(0, localStorage["selectedDiff"] |0), this.diffOptions.length + 3) + } + + this.songSelect = document.getElementById("song-select") + var cat = this.songs[this.selectedSong].originalCategory + this.drawBackground(cat) + + this.previewId = 0 + this.previewList = Array(5) + + var skipStart = fromTutorial || p2.session + this.state = { + screen: songIdIndex !== -1 ? "difficulty" : (fadeIn ? "titleFadeIn" : (skipStart ? "song" : "title")), + screenMS: this.getMS(), + move: 0, + moveMS: 0, + mouseMoveMS: 0, + ura: 0, + moveHover: null, + locked: true, + hasPointer: false, + options: 0, + selLock: false, + catJump: false, + focused: true, + waitPreview: 0 + } + this.songSelecting = { + speed: parseFloat(localStorage.getItem("sss") ?? "400", 10), + resize: 0.3, + scrollDelay: 0.1 + } + this.wheelScrolls = 0 + this.wheelTimer = 0 + + this.startPreview(true) + + this.pressedKeys = {} + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"], + back: ["escape"], + left: ["left", "ka_l"], + right: ["right", "ka_r"], + up: ["up"], + down: ["down"], + session: ["backspace"], + ctrl: ["ctrl"], + shift: ["shift"], + mute: ["q"], + search: ["f"] + }, this.keyPress.bind(this)) + this.gamepad = new Gamepad({ + confirm: ["b", "start", "ls", "rs"], + back: ["a"], + left: ["l", "lsl", "lt"], + right: ["r", "lsr", "rt"], + up: ["u", "lsu"], + down: ["d", "lsd"], + session: ["back"], + ctrlGamepad: ["y"], + shift: ["x"], + jump_left: ["lb"], + jump_right: ["rb"] + }, this.keyPress.bind(this)) + + if(!assets.customSongs){ + this.startP2() + } + + pageEvents.add(loader.screen, "mousemove", this.mouseMove.bind(this)) + pageEvents.add(loader.screen, "mouseleave", () => { + this.state.moveHover = null + }) + pageEvents.add(loader.screen, ["mousedown", "touchstart"], this.mouseDown.bind(this)) + pageEvents.add(this.canvas, "touchend", this.touchEnd.bind(this)) + if(touchEnabled && fullScreenSupported){ + this.touchFullBtn = document.getElementById("touch-full-btn") + this.touchFullBtn.style.display = "block" + pageEvents.add(this.touchFullBtn, "touchend", toggleFullscreen) + } + + pageEvents.add(this.canvas, "wheel", this.mouseWheel.bind(this)) + + this.selectable = document.getElementById("song-sel-selectable") + this.selectableText = "" + + this.redrawRunning = true + this.redrawBind = this.redraw.bind(this) + this.redraw() + pageEvents.send("song-select") + pageEvents.send("song-select-move", this.songs[this.selectedSong]) + if(songIdIndex !== -1){ + pageEvents.send("song-select-difficulty", this.songs[this.selectedSong]) + } + } + + setAltText(element, text){ + element.innerText = text + element.setAttribute("alt", text) + } + + setSelectedSong(songIdx, drawBg=true){ + if (songIdx < 0) { + return; + } + + if(drawBg){ + var cat = this.songs[songIdx].originalCategory + if(cat){ + this.drawBackground(cat) + }else{ + this.drawBackground(false) + } + } + + this.selectedSong = songIdx + } + + keyPress(pressed, name, event, repeat){ + if(pressed){ + if(!this.pressedKeys[name]){ + this.pressedKeys[name] = this.getMS() + (name === "left" || name === "right" ? 150 : 300) + } + }else{ + this.pressedKeys[name] = 0 + return + } + if(name === "ctrl" || name === "shift" || !this.redrawRunning){ + return + } + var ctrl = event ? event.ctrlKey : (this.pressedKeys["ctrl"] || this.pressedKeys["ctrlGamepad"]) + var shift = event ? event.shiftKey : this.pressedKeys["shift"] + if(this.state.showWarning){ + if(name === "confirm"){ + this.playSound("se_don") + this.state.showWarning = false + this.showWarning = false + } + }else if(this.search.opened){ + this.search.keyPress(pressed, name, event, repeat, ctrl) + }else if(this.state.screen === "song"){ + if(event && event.keyCode && event.keyCode === 70 && ctrl){ + this.search.display() + if(event){ + event.preventDefault() + } + }else if(name === "confirm"){ + this.toSelectDifficulty() + }else if(name === "back"){ + this.toTitleScreen() + }else if(name === "session"){ + this.toSession() + }else if(name === "left"){ + if(shift){ + if(!repeat){ + this.categoryJump(-1) + } + }else{ + this.moveToSong(-1) + } + }else if(name === "right"){ + if(shift){ + if(!repeat){ + this.categoryJump(1) + } + }else{ + this.moveToSong(1) + } + }else if(name === "jump_left" && !repeat){ + this.categoryJump(-1) + }else if(name === "jump_right" && !repeat){ + this.categoryJump(1) + }else if(name === "mute" || name === "ctrlGamepad"){ + this.endPreview(true) + this.playBgm(false) + } + }else if(this.state.screen === "difficulty"){ + if(event && event.keyCode && event.keyCode === 70 && ctrl){ + this.search.display() + if(event){ + event.preventDefault() + } + }else if(name === "confirm"){ + if(this.selectedDiff === 0){ + this.toSongSelect() + }else if(this.selectedDiff === 2){ + this.toDownload() + }else if(this.selectedDiff === 3){ + this.toDelete() + }else if(this.selectedDiff === 1){ + this.toOptions(1) + }else{ + this.toLoadSong(this.selectedDiff - this.diffOptions.length, shift, ctrl) + } + }else if(name === "back" || name === "session"){ + this.toSongSelect() + }else if(name === "left"){ + this.moveToDiff(-1) + }else if(name === "right"){ + this.moveToDiff(1) + }else if(this.selectedDiff === 1 && (name === "up" || name === "down")){ + this.toOptions(name === "up" ? -1 : 1) + }else if(name === "mute" || name === "ctrlGamepad"){ + this.endPreview(true) + this.playBgm(false) + } + }else if(this.state.screen === "title" || this.state.screen === "titleFadeIn"){ + if(event && event.keyCode && event.keyCode === 70 && ctrl){ + this.search.display() + if(event){ + event.preventDefault() + } + } + } + } + + mouseDown(event){ + if(event.target === this.selectable || event.target.parentNode === this.selectable){ + this.selectable.focus() + }else if(event.target.tagName !== "INPUT"){ + getSelection().removeAllRanges() + this.selectable.blur() + } + if(event.target !== this.canvas || !this.redrawRunning){ + return + } + if(event.type === "mousedown"){ + if(event.which !== 1){ + return + } + var mouse = this.mouseOffset(event.offsetX, event.offsetY) + var shift = event.shiftKey + var ctrl = event.ctrlKey + var touch = false + }else{ + event.preventDefault() + var x = event.touches[0].pageX - this.canvas.offsetLeft + var y = event.touches[0].pageY - this.canvas.offsetTop + var mouse = this.mouseOffset(x, y) + var shift = false + var ctrl = false + var touch = true + } + if(this.state.showWarning){ + if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){ + this.playSound("se_don") + this.state.showWarning = false + this.showWarning = false + } + }else if(this.state.screen === "song"){ + if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){ + this.categoryJump(mouse.x < 640 ? -1 : 1) + }else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig.accounts){ + this.toAccount() + }else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){ + this.toSession() + }else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket && p2.socket.readyState === 1 && !assets.customSongs){ + this.toSession() + }else{ + var moveBy = this.songSelMouse(mouse.x, mouse.y) + if(moveBy === 0){ + this.toSelectDifficulty() + }else if(moveBy !== null){ + this.moveToSong(moveBy) + } + } + }else if(this.state.screen === "difficulty"){ + var moveBy = this.diffSelMouse(mouse.x, mouse.y) + if(mouse.x < 183 || mouse.x > 1095 || mouse.y < 54 || mouse.y > 554){ + this.toSongSelect() + }else if(moveBy === 0){ + this.selectedDiff = 0 + this.toSongSelect() + }else if(moveBy === 2){ + this.toDownload() + }else if(moveBy === 3){ + this.toDelete() + }else if(moveBy === 1){ + this.toOptions(1) + }else if(moveBy === "maker"){ + window.open(this.songs[this.selectedSong].maker.url) + }else if(moveBy === this.diffOptions.length + 4){ + this.state.ura = !this.state.ura + this.playSound("se_ka", 0, p2.session ? p2.player : false) + if(this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura){ + this.state.move = -1 + } + }else if(moveBy !== null){ + this.toLoadSong(moveBy - this.diffOptions.length, shift, ctrl, touch) + } + } + } + touchEnd(event){ + event.preventDefault() + } + mouseWheel(event){ + if(this.state.screen === "song" && this.state.focused){ + this.wheelTimer = this.getMS() + + if(event.deltaY < 0) { + this.wheelScrolls-- + }else if(event.deltaY > 0){ + this.wheelScrolls++ + } + } + } + mouseMove(event){ + var mouse = this.mouseOffset(event.offsetX, event.offsetY) + var moveTo = null + if(this.state.showWarning){ + if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){ + moveTo = "showWarning" + } + }else if(this.state.screen === "song" && !this.search.opened){ + if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){ + moveTo = mouse.x < 640 ? "categoryPrev" : "categoryNext" + }else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig.accounts){ + moveTo = "account" + }else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){ + moveTo = "session" + }else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket && p2.socket.readyState === 1 && !assets.customSongs){ + moveTo = "session" + }else{ + var moveTo = this.songSelMouse(mouse.x, mouse.y) + if(moveTo === null && this.state.moveHover === 0 && !this.songs[this.selectedSong].courses){ + this.state.mouseMoveMS = this.getMS() - this.songSelecting.speed + } + } + this.state.moveHover = moveTo + }else if(this.state.screen === "difficulty"){ + var moveTo = this.diffSelMouse(mouse.x, mouse.y) + if(moveTo === null && this.state.moveHover === this.selectedDiff){ + this.state.mouseMoveMS = this.getMS() - 1000 + } + this.state.moveHover = moveTo + } + this.pointer(moveTo !== null) + } + mouseOffset(offsetX, offsetY){ + return { + x: (offsetX * this.pixelRatio - this.winW / 2) / this.ratio + 1280 / 2, + y: (offsetY * this.pixelRatio - this.winH / 2) / this.ratio + 720 / 2 + } + } + pointer(enabled){ + if(!this.canvas){ + return + } + if(enabled && this.state.hasPointer === false){ + this.canvas.style.cursor = "pointer" + this.state.hasPointer = true + }else if(!enabled && this.state.hasPointer === true){ + this.canvas.style.cursor = "" + this.state.hasPointer = false + } + } + + songSelMouse(x, y){ + if(this.state.locked === 0 && this.songAsset.marginTop <= y && y <= this.songAsset.marginTop + this.songAsset.height){ + x -= 1280 / 2 + var dir = x > 0 ? 1 : -1 + x = Math.abs(x) + var selectedWidth = this.songAsset.selectedWidth + if(!this.songs[this.selectedSong].courses){ + selectedWidth = this.songAsset.width + } + var moveBy = Math.ceil((x - selectedWidth / 2 - this.songAsset.marginLeft / 2) / (this.songAsset.width + this.songAsset.marginLeft)) * dir + if(moveBy / dir > 0){ + return moveBy + }else{ + return 0 + } + } + return null + } + diffSelMouse(x, y){ + if(this.state.locked === 0){ + if(223 < x && x < 223 + 72 * this.diffOptions.length && 132 < y && y < 436){ + return Math.floor((x - 223) / 72) + }else if(this.songs[this.selectedSong].maker && this.songs[this.selectedSong].maker.id > 0 && this.songs[this.selectedSong].maker.url && x > 230 && x < 485 && y > 446 && y < 533) { + return "maker" + }else if(550 < x && x < 1050 && 109 < y && y < 538){ + var moveBy = Math.floor((x - 550) / ((1050 - 550) / 5)) + this.diffOptions.length + var currentSong = this.songs[this.selectedSong] + if( + this.state.ura + && moveBy === this.diffOptions.length + 3 + || currentSong.courses[ + this.difficultyId[moveBy - this.diffOptions.length] + ] + ){ + return moveBy + } + } + } + return null + } + + moveToSong(moveBy, fromP2){ + var ms = this.getMS() + if(p2.session && !fromP2){ + if(!this.state.selLock && ms > this.state.moveMS + 800){ + this.state.selLock = true + p2.send("songsel", { + song: this.mod(this.songs.length, this.selectedSong + moveBy) + }) + } + }else if(this.state.locked !== 1 || fromP2){ + if(this.songs[this.selectedSong].courses && !this.songs[this.selectedSong].unloaded && (this.state.locked === 0 || fromP2)){ + this.state.moveMS = ms + }else{ + this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize + } + this.state.move = moveBy + this.state.lastMove = moveBy + this.state.locked = 1 + this.state.moveHover = null + + var lastMoveMul = Math.pow(Math.abs(moveBy), 1 / 4) + var changeSpeed = this.songSelecting.speed * lastMoveMul + var resize = changeSpeed * this.songSelecting.resize / lastMoveMul + var scrollDelay = changeSpeed * this.songSelecting.scrollDelay + var resize2 = changeSpeed - resize + var scroll = resize2 - resize - scrollDelay * 2 + + var soundsDelay = Math.abs((scroll + resize) / moveBy) + this.lastMoveBy = fromP2 ? fromP2.player : false + + for(var i = 0; i < Math.abs(moveBy) - 1; i++){ + this.playSound("se_ka", (resize + i * soundsDelay) / 1000, fromP2 ? fromP2.player : false) + } + this.pointer(false) + } + } + + categoryJump(moveBy, fromP2){ + if(p2.session && !fromP2){ + var ms = this.getMS() + if(!this.state.selLock && ms > this.state.moveMS + 800){ + this.state.selLock = true + p2.send("catjump", { + song: this.selectedSong, + move: moveBy + }) + } + }else if(this.state.locked !== 1 || fromP2){ + this.state.catJump = true + this.state.move = moveBy; + this.state.locked = 1 + + this.endPreview() + this.playSound("se_jump", 0, fromP2 ? fromP2.player : false) + } + } + + moveToDiff(moveBy){ + if(this.state.locked !== 1){ + this.state.move = moveBy + this.state.moveMS = this.getMS() - 500 + this.state.locked = 1 + this.playSound("se_ka", 0, p2.session ? p2.player : false) + } + } + + toSelectDifficulty(fromP2, playVoice=true){ + var currentSong = this.songs[this.selectedSong] + if(p2.session && !fromP2 && (!currentSong.action || !currentSong.p2Enabled)){ + if(this.songs[this.selectedSong].courses){ + if(!this.state.selLock){ + this.state.selLock = true + p2.send("songsel", { + song: this.selectedSong, + selected: true, + fromRandom: this.lastRandom + }) + } + } + }else if(this.state.locked === 0 || fromP2){ + this.search.remove() + if(currentSong.courses){ + if(currentSong.unloaded){ + return + } + + var prevScreen = this.state.screen + this.state.screen = "difficulty" + this.state.screenMS = this.getMS() + this.state.locked = true + this.state.moveHover = null + this.state.ura = 0 + if(this.selectedDiff === this.diffOptions.length + 4){ + this.selectedDiff = this.diffOptions.length + 3 + } + + this.playSound("se_don", 0, fromP2 ? fromP2.player : false) + assets.sounds["v_songsel"].stop() + if(!this.showWarning && prevScreen !== "difficulty" && playVoice){ + this.playSound("v_diffsel", 0.3) + } + pageEvents.send("song-select-difficulty", currentSong) + }else if(currentSong.action === "back"){ + this.toTitleScreen() + }else if(currentSong.action === "random"){ + do{ + var i = Math.floor(Math.random() * this.songs.length) + }while(!this.songs[i].courses) + this.setSelectedSong(i) + this.lastRandom = true + this.playBgm(false) + this.toSelectDifficulty(false, playVoice=false) + pageEvents.send("song-select-random") + }else if(currentSong.action === "search"){ + this.search.display(true) + }else if(currentSong.action === "tutorial"){ + this.toTutorial() + }else if(currentSong.action === "about"){ + this.toAbout() + }else if(currentSong.action === "settings"){ + this.toSettings() + }else if(currentSong.action === "customSongs"){ + this.toCustomSongs() + }else if(currentSong.action === "plugins"){ + this.toPlugins() + } + // カスタムメニューの実行処理 + else if (currentSong.action === "sourceCode") { + this.playSound("se_don"); + setTimeout(() => { + open("https://github.com/yuukialpha/taiko-web","_blank"); + }, 500); + } else if (currentSong.action === "upload") { + this.playSound("se_don"); + setTimeout(() => { + window.location.href = "/upload/"; + }, 100); + } else if (currentSong.action === "keijiban") { + this.playSound("se_don"); + setTimeout(() => { + window.location.href = "https://litey.trade/"; + }, 100); + } else if (currentSong.action === "songSelectingSpeed") { + this.playSound("se_don"); + setTimeout(() => { + let songSelectingSpeed = localStorage.getItem("sss") ?? "400"; + const pro = prompt("曲選択速度を入力してね!", songSelectingSpeed); + if (pro === null) { + // キャンセル + } else if (pro === "") { + songSelectingSpeed = "400"; + } else { + songSelectingSpeed = pro; + } + const preValue = localStorage.getItem("sss") ?? "400"; + localStorage.setItem("sss", songSelectingSpeed.toString()); + if (preValue !== songSelectingSpeed) { + location.reload(); + } + }, 100); + } else if (currentSong.action === "baisoku") { + this.playSound("se_don"); + setTimeout(() => { + let baisoku = localStorage.getItem("baisoku") ?? "1"; + const input = prompt("ばいそくの倍率を入力してね!", baisoku); + if (input === null) { + // キャンセル + } else if (input === "") { + baisoku = "1"; + } else { + baisoku = input; + } + localStorage.setItem("baisoku", baisoku.toString()); + }, 100); + } else if (currentSong.action === "doron") { + this.playSound("se_don"); + setTimeout(() => { + let doron = localStorage.getItem("doron") ?? "false"; + const input = prompt("ドロンを有効にするには\"true\"を入力してね!", doron); + if (input === null) { + // キャンセル + } else if (input === "") { + doron = "false"; + } else { + doron = input; + } + localStorage.setItem("doron", doron); + }, 100); + } else if (currentSong.action === "abekobe") { + this.playSound("se_don"); + setTimeout(() => { + let abekobe = localStorage.getItem("abekobe") ?? "false"; + const input = prompt("あべこべを有効にするには\"true\"を入力してね!", abekobe); + if (input === null) { + // キャンセル + } else if (input === "") { + abekobe = "false"; + } else { + abekobe = input; + } + localStorage.setItem("abekobe", abekobe); + }, 100); + } else if (currentSong.action === "detarame") { + this.playSound("se_don"); + setTimeout(() => { + let detarame = localStorage.getItem("detarame") ?? "0"; + const input = prompt("でたらめになる確率をパーセントで入力してね!", detarame); + if (input === null) { + // キャンセル + } else if (input === "") { + detarame = "0"; + } else { + detarame = input; + } + localStorage.setItem("detarame", detarame); + }, 100); + } else if (currentSong.action === "titlesort") { + this.playSound("se_don"); + setTimeout(() => { + let titlesort = localStorage.getItem("titlesort") ?? "false"; + const input = prompt("タイトル順で並べ替えするには\"true\"を入力してね!", titlesort); + if (input === null) { + // キャンセル + } else if (input === "") { + titlesort = "false"; + } else { + titlesort = input; + } + const preValue = localStorage.getItem("titlesort") ?? "false"; + localStorage.setItem("titlesort", titlesort); + if (preValue !== titlesort) { + location.reload(); + } + }, 100); + } + } + this.pointer(false) + } + toSongSelect(fromP2){ + if(p2.session && !fromP2){ + if(!this.state.selLock){ + this.state.selLock = true + p2.send("songsel", { + song: this.lastRandom ? this.songs.findIndex(song => song.action === "random") : this.selectedSong + }) + } + + }else if(fromP2 || this.state.locked !== 1){ + this.state.screen = "song" + this.state.screenMS = this.getMS() + this.state.locked = true + this.state.moveHover = null + + if(this.lastRandom){ + this.endPreview(false) + this.setSelectedSong(this.songs.findIndex(song => song.action === "random")) + this.lastRandom = false + } + + assets.sounds["v_diffsel"].stop() + this.playSound("se_cancel", 0, fromP2 ? fromP2.player : false) + } + this.clearHash() + pageEvents.send("song-select-back") + } + toLoadSong(difficulty, shift, ctrl, touch){ + this.clean() + var selectedSong = this.songs[this.selectedSong] + assets.sounds["v_diffsel"].stop() + this.playSound("se_don", 0, p2.session ? p2.player : false) + + try{ + if(assets.customSongs){ + assets.customSelected = this.selectedSong + localStorage["customSelected"] = this.selectedSong + }else{ + localStorage["selectedSong"] = this.selectedSong + } + localStorage["selectedDiff"] = difficulty + this.diffOptions.length + }catch(e){} + + if(difficulty === 3 && this.state.ura){ + difficulty = 4 + } + var autoplay = false + var multiplayer = false + if(p2.session || this.state.options === 2){ + multiplayer = true + }else if(this.state.options === 1){ + autoplay = true + }else if(shift){ + autoplay = shift + }else if(p2.socket && p2.socket.readyState === 1 && !assets.customSongs){ + multiplayer = ctrl + } + var diff = this.difficultyId[difficulty] + + new LoadSong({ + "title": selectedSong.title, + "originalTitle": selectedSong.originalTitle, + "folder": selectedSong.id, + "difficulty": diff, + "category": selectedSong.category, + "category_id":selectedSong.category_id, + "type": selectedSong.type, + "offset": selectedSong.offset, + "songSkin": selectedSong.songSkin, + "stars": selectedSong.courses[diff].stars, + "hash": selectedSong.hash, + "lyrics": selectedSong.lyrics, + "video": selectedSong.video, + }, autoplay, multiplayer, touch) + } + toOptions(moveBy){ + if(!p2.session){ + this.playSound("se_ka", 0, p2.session ? p2.player : false) + this.selectedDiff = 1 + do{ + this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy) + }while((!p2.socket || p2.socket.readyState !== 1 || assets.customSongs) && this.state.options === 2) + } + } + toTitleScreen(){ + if(!p2.session){ + this.playSound("se_cancel") + this.clean() + setTimeout(() => { + new Titlescreen() + }, 500) + } + } + toTutorial(){ + this.playSound("se_don") + this.clean() + setTimeout(() => { + new Tutorial(true) + }, 500) + } + toAbout(){ + this.playSound("se_don") + this.clean() + setTimeout(() => { + new About(this.touchEnabled) + }, 500) + } + toSettings(){ + this.playSound("se_don") + this.clean() + setTimeout(() => { + new SettingsView(this.touchEnabled) + }, 500) + } + toAccount(){ + this.playSound("se_don") + this.clean() + setTimeout(() => { + new Account(this.touchEnabled) + }, 500) + } + toSession(){ + if(p2.socket.readyState !== 1 || assets.customSongs){ + return + } + if(p2.session){ + this.playSound("se_don") + p2.send("gameend") + this.state.moveHover = null + }else{ + localStorage["selectedSong"] = this.selectedSong + + this.playSound("se_don") + this.clean() + setTimeout(() => { + new Session(this.touchEnabled) + }, 500) + } + } + toCustomSongs(){ + if(assets.customSongs){ + assets.customSongs = false + assets.songs = assets.songsDefault + delete assets.otherFiles + this.playSound("se_don") + this.clean() + setTimeout(() => { + new SongSelect("customSongs", false, this.touchEnabled) + }, 500) + localStorage.removeItem("customSelected") + db.removeItem("customFolder") + pageEvents.send("import-songs-default") + }else{ + localStorage["selectedSong"] = this.selectedSong + + this.playSound("se_don") + this.clean() + setTimeout(() => { + new CustomSongs(this.touchEnabled) + }, 500) + } + } + toPlugins(){ + this.playSound("se_don") + this.clean() + setTimeout(() => { + new SettingsView(this.touchEnabled, false, undefined, undefined, plugins.getSettings()) + }, 500) + } + + redraw(){ + if(!this.redrawRunning){ + return + } + requestAnimationFrame(this.redrawBind) + var ms = this.getMS() + + for(var key in this.pressedKeys){ + if(this.pressedKeys[key]){ + if(ms >= this.pressedKeys[key] + (this.state.screen === "song" && (key === "right" || key === "left") ? 20 : 50)){ + this.keyPress(true, key, null, true) + this.pressedKeys[key] = ms + } + } + } + + if(!this.redrawRunning){ + return + } + + var ctx = this.ctx + var winW = innerWidth + var winH = lastHeight + if(winW / 32 > winH / 9){ + winW = winH / 9 * 32 + } + this.pixelRatio = window.devicePixelRatio || 1 + var resolution = settings.getItem("resolution") + if(resolution === "medium"){ + this.pixelRatio *= 0.75 + }else if(resolution === "low"){ + this.pixelRatio *= 0.5 + }else if(resolution === "lowest"){ + this.pixelRatio *= 0.25 + } + winW *= this.pixelRatio + winH *= this.pixelRatio + var ratioX = winW / 1280 + var ratioY = winH / 720 + var ratio = (ratioX < ratioY ? ratioX : ratioY) + if(this.winW !== winW || this.winH !== winH){ + this.canvas.width = Math.max(1, winW) + this.canvas.height = Math.max(1, winH) + ctx.scale(ratio, ratio) + this.canvas.style.width = (winW / this.pixelRatio) + "px" + this.canvas.style.height = (winH / this.pixelRatio) + "px" + + var borders = (this.songAsset.border + this.songAsset.innerBorder) * 2 + var songsLength = Math.ceil(winW / ratio / (this.songAsset.width + this.songAsset.marginLeft)) + 1 + + this.songTitleCache.resize( + (this.songAsset.width - borders + 1) * songsLength, + this.songAsset.height - borders + 1, + ratio + 0.2 + ) + + this.currentSongCache.resize( + (this.songAsset.width - borders + 1) * 2, + this.songAsset.height - borders + 1, + ratio + 0.2 + ) + + var textW = strings.id === "en" ? 350 : 280 + this.selectTextCache.resize((textW + 53 + 60 + 1) * 2, this.songAsset.marginTop + 15, ratio + 0.5) + + this.nameplateCache.resize(274, 134, ratio + 0.2) + + var lastCategory + this.songs.forEach(song => { + var cat = (song.category || "") + song.skin.outline + if(lastCategory !== cat){ + lastCategory = cat + } + }) + this.categoryCache.resize(280, this.songAsset.marginTop + 1 , ratio + 0.5) + + this.difficultyCache.resize((44 + 56 + 2) * 5, 135 + 10, ratio + 0.5) + + var w = winW / ratio / 2 + this.sessionCache.resize(w, 39 * 2, ratio + 0.5) + for(var id in this.sessionText){ + this.sessionCache.set({ + w: w, + h: 38, + id: id + }, ctx => { + this.draw.layeredText({ + ctx: ctx, + text: this.sessionText[id], + fontSize: 28, + fontFamily: this.font, + x: w / 2, + y: 38 / 2, + width: id === "sessionend" ? 385 : w - 30, + align: "center", + baseline: "middle" + }, [ + {outline: "#000", letterBorder: 8}, + {fill: "#fff"} + ]) + }) + } + + this.selectableText = "" + + if(this.search.opened && this.search.container){ + this.search.onInput(true) + } + }else if(!document.hasFocus() && !p2.session){ + if(this.state.focused){ + this.state.focused = false + this.songSelect.classList.add("unfocused") + this.pressedKeys = {} + } + return + }else{ + ctx.clearRect(0, 0, winW / ratio, winH / ratio) + } + if(!this.state.focused){ + this.state.focused = true + this.songSelect.classList.remove("unfocused") + } + this.winW = winW + this.winH = winH + this.ratio = ratio + winW /= ratio + winH /= ratio + + var frameTop = winH / 2 - 720 / 2 + var frameLeft = winW / 2 - 1280 / 2 + var songTop = frameTop + this.songAsset.marginTop + var xOffset = 0 + var songSelMoving = false + var screen = this.state.screen + var selectedWidth = this.songAsset.width + + this.search.redraw() + + if(this.wheelScrolls !== 0 && !this.state.locked && ms >= this.wheelTimer + 20) { + if(p2.session){ + this.moveToSong(this.wheelScrolls) + }else{ + this.state.move = this.wheelScrolls + this.state.waitPreview = ms + 400 + this.endPreview() + } + this.wheelScrolls = 0 + } + + if(screen === "title" || screen === "titleFadeIn"){ + if(ms > this.state.screenMS + 1000){ + this.state.screen = "song" + this.state.screenMS = ms + (ms - this.state.screenMS - 1000) + this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize + (ms - this.state.screenMS) + this.state.locked = 3 + this.state.lastMove = 1 + }else{ + this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize + (ms - this.state.screenMS - 1000) + } + if(screen === "titleFadeIn" && ms > this.state.screenMS + 500){ + this.state.screen = "title" + screen = "title" + } + } + + if((screen === "song" || screen === "difficulty") && (this.showWarning && !this.showWarning.shown || scoreStorage.scoreSaveFailed)){ + if(!this.showWarning){ + this.showWarning = {name: "scoreSaveFailed"} + } + if(this.bgmEnabled){ + this.playBgm(false) + } + if(this.showWarning.name === "scoreSaveFailed"){ + scoreStorage.scoreSaveFailed = false + } + this.showWarning.shown = true + this.state.showWarning = true + this.state.locked = true + this.playSound("se_pause") + } + + if(screen === "title" || screen === "titleFadeIn" || screen === "song"){ + var textW = strings.id === "en" ? 350 : 280 + this.selectTextCache.get({ + ctx: ctx, + x: frameLeft, + y: frameTop, + w: textW + 53 + 60, + h: this.songAsset.marginTop + 15, + id: "song" + }, ctx => { + this.draw.layeredText({ + ctx: ctx, + text: strings.selectSong, + fontSize: 48, + fontFamily: this.font, + x: 53, + y: 30, + width: textW, + letterSpacing: strings.id === "en" ? 0 : 2, + forceShadow: true + }, [ + {x: -2, y: -2, outline: "#000", letterBorder: 22}, + {}, + {x: 2, y: 2, shadow: [3, 3, 3]}, + {x: 2, y: 2, outline: "#ad1516", letterBorder: 10}, + {x: -2, y: -2, outline: "#ff797b"}, + {outline: "#f70808"}, + {fill: "#fff", shadow: [-1, 1, 3, 1.5]} + ]) + }) + + var selectedSong = this.songs[this.selectedSong] + var category = selectedSong.category + this.draw.category({ + ctx: ctx, + x: winW / 2 - 280 / 2 - 30, + y: frameTop + 60, + fill: selectedSong.skin.background, + highlight: this.state.moveHover === "categoryPrev" + }) + this.draw.category({ + ctx: ctx, + x: winW / 2 + 280 / 2 + 30, + y: frameTop + 60, + right: true, + fill: selectedSong.skin.background, + highlight: this.state.moveHover === "categoryNext" + }) + this.categoryCache.get({ + ctx: ctx, + x: winW / 2 - 280 / 2, + y: frameTop, + w: 280, + h: this.songAsset.marginTop, + id: category + selectedSong.skin.outline + }, ctx => { + if(category){ + let cat = assets.categories.find(cat=>cat.title === category) + if(cat){ + var categoryName = this.getLocalTitle(cat.title, cat.title_lang) + }else{ + var categoryName = category + } + this.draw.layeredText({ + ctx: ctx, + text: categoryName, + fontSize: 40, + fontFamily: this.font, + x: 280 / 2, + y: 38, + width: 255, + align: "center", + forceShadow: true + }, [ + {outline: selectedSong.skin.outline, letterBorder: 12, shadow: [3, 3, 3]}, + {fill: "#fff"} + ]) + } + }) + } + + if(screen === "song"){ + if(this.songs[this.selectedSong].courses && !this.songs[this.selectedSong].unloaded){ + selectedWidth = this.songAsset.selectedWidth + } + + var lastMoveMul = Math.pow(Math.abs(this.state.lastMove || 0), 1 / 4) + var changeSpeed = this.songSelecting.speed * lastMoveMul + var resize = changeSpeed * (lastMoveMul === 0 ? 0 : this.songSelecting.resize / lastMoveMul) + var scrollDelay = changeSpeed * this.songSelecting.scrollDelay + var resize2 = changeSpeed - resize + var scroll = resize2 - resize - scrollDelay * 2 + var elapsed = ms - this.state.moveMS + + if(this.state.catJump || (this.state.move && ms > this.state.moveMS + resize2 - scrollDelay)){ + var isJump = this.state.catJump + var previousSelectedSong = this.selectedSong + + if(!isJump){ + this.playSound("se_ka", 0, this.lastMoveBy) + this.setSelectedSong(this.mod(this.songs.length, this.selectedSong + this.state.move)) + }else{ + var currentCat = this.songs[this.selectedSong].category + var currentIdx = this.mod(this.songs.length, this.selectedSong) + + if(this.state.move > 0){ + var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) > currentIdx && song.category !== currentCat && song.canJump) + if(!nextSong){ + nextSong = this.songs[0] + } + }else{ + var isFirstInCat = this.songs.findIndex(song => song.category === currentCat) == this.selectedSong + if(!isFirstInCat){ + var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) < currentIdx && song.category === currentCat && song.canJump) + }else{ + var idx = this.songs.length - 1 + var nextSong + var lastCat + for(;idx>=0;idx--){ + if(this.songs[idx].category !== lastCat && this.songs[idx].action !== "back"){ + lastCat = this.songs[idx].category + if(nextSong){ + break + } + } + if(lastCat !== currentCat && idx < currentIdx){ + nextSong = idx + } + } + nextSong = this.songs[nextSong] + } + + if(!nextSong){ + var rev = [...this.songs].reverse() + nextSong = rev.find(song => song.canJump) + } + } + + this.setSelectedSong(this.songs.indexOf(nextSong)) + this.state.catJump = false + } + + if(previousSelectedSong !== this.selectedSong){ + pageEvents.send("song-select-move", this.songs[this.selectedSong]) + } + this.state.move = 0 + this.state.locked = 2 + if(assets.customSongs){ + assets.customSelected = this.selectedSong + localStorage["customSelected"] = this.selectedSong + }else if(!p2.session){ + try{ + localStorage["selectedSong"] = this.selectedSong + }catch(e){} + } + } + if(this.state.moveMS && ms < this.state.moveMS + changeSpeed){ + xOffset = Math.min(scroll, Math.max(0, elapsed - resize - scrollDelay)) / scroll * (this.songAsset.width + this.songAsset.marginLeft) + xOffset *= -this.state.move + if(elapsed < resize){ + selectedWidth = this.songAsset.width + (((resize - elapsed) / resize) * (selectedWidth - this.songAsset.width)) + }else if(elapsed > resize2){ + this.playBgm(!this.songs[this.selectedSong].courses) + this.state.locked = 1 + selectedWidth = this.songAsset.width + ((elapsed - resize2) / resize * (selectedWidth - this.songAsset.width)) + }else{ + songSelMoving = true + selectedWidth = this.songAsset.width + } + }else{ + if(this.previewing !== "muted"){ + this.playBgm(!this.songs[this.selectedSong].courses) + } + this.state.locked = 0 + } + }else if(screen === "difficulty"){ + var currentSong = this.songs[this.selectedSong] + if(this.state.locked){ + this.state.locked = 0 + } + if(this.state.move){ + var hasUra = currentSong.courses.ura + var previousSelection = this.selectedDiff + do{ + if(hasUra && this.state.move > 0){ + this.selectedDiff += this.state.move + if(this.selectedDiff > this.diffOptions.length + 4){ + this.state.ura = !this.state.ura + if(this.state.ura){ + this.selectedDiff = previousSelection === this.diffOptions.length + 3 ? this.diffOptions.length + 4 : previousSelection + break + }else{ + this.state.move = -1 + } + } + }else{ + this.selectedDiff = this.mod(this.diffOptions.length + 5, this.selectedDiff + this.state.move) + } + }while( + this.selectedDiff >= this.diffOptions.length && !currentSong.courses[this.difficultyId[this.selectedDiff - this.diffOptions.length]] + || this.selectedDiff === this.diffOptions.length + 3 && this.state.ura + || this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura + ) + this.state.move = 0 + }else if(this.selectedDiff < 0 || this.selectedDiff >= this.diffOptions.length && !currentSong.courses[this.difficultyId[this.selectedDiff - this.diffOptions.length]]){ + this.selectedDiff = 0 + } + } + + if(songSelMoving){ + if(this.previewing !== null){ + this.endPreview() + } + }else if(screen !== "title" && screen !== "titleFadeIn" && ms > this.state.moveMS + 100){ + if(this.previewing !== "muted" && this.previewing !== this.selectedSong && "id" in this.songs[this.selectedSong]){ + this.startPreview() + } + } + + this.songFrameCache = { + w: this.songAsset.width + this.songAsset.selectedWidth + this.songAsset.fullWidth + (15 + 1) * 3, + h: this.songAsset.fullHeight + 16, + ratio: ratio + } + + if(screen === "title" || screen === "titleFadeIn" || screen === "song"){ + for(var i = this.selectedSong - 1; ; i--){ + var highlight = 0 + if(i - this.selectedSong === this.state.moveHover){ + highlight = 1 + } + var index = this.mod(this.songs.length, i) + var _x = winW / 2 - (this.selectedSong - i) * (this.songAsset.width + this.songAsset.marginLeft) - selectedWidth / 2 + xOffset + if(_x + this.songAsset.width + this.songAsset.marginLeft < 0){ + break + } + this.drawClosedSong({ + ctx: ctx, + x: _x, + y: songTop, + song: this.songs[index], + highlight: highlight, + disabled: p2.session && this.songs[index].action && !this.songs[index].p2Enabled + }) + } + var startFrom + for(var i = this.selectedSong + 1; ; i++){ + var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset + if(_x > winW){ + startFrom = i - 1 + break + } + } + for(var i = startFrom; i > this.selectedSong ; i--){ + var highlight = 0 + if(i - this.selectedSong === this.state.moveHover){ + highlight = 1 + } + var index = this.mod(this.songs.length, i) + var currentSong = this.songs[index] + var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset + this.drawClosedSong({ + ctx: ctx, + x: _x, + y: songTop, + song: this.songs[index], + highlight: highlight, + disabled: p2.session && this.songs[index].action && !this.songs[index].p2Enabled + }) + } + } + + var currentSong = this.songs[this.selectedSong] + var highlight = 0 + if(!currentSong.courses){ + highlight = 2 + } + if(this.state.moveHover === 0){ + highlight = 1 + } + var selectedSkin = this.songSkin.selected + if(screen === "title" || screen === "titleFadeIn" || this.state.locked === 3 || currentSong.unloaded){ + selectedSkin = currentSong.skin + highlight = 2 + }else if(songSelMoving){ + selectedSkin = currentSong.skin + highlight = 0 + } + var selectedHeight = this.songAsset.height + if(screen === "difficulty"){ + selectedWidth = this.songAsset.fullWidth + selectedHeight = this.songAsset.fullHeight + highlight = 0 + } + + if(this.lastCurrentSong.title !== currentSong.title || this.lastCurrentSong.subtitle !== currentSong.subtitle){ + this.lastCurrentSong.title = currentSong.title + this.lastCurrentSong.subtitle = currentSong.subtitle + this.currentSongCache.clear() + } + + if(selectedWidth === this.songAsset.width){ + this.drawSongCrown({ + ctx: ctx, + song: currentSong, + x: winW / 2 - selectedWidth / 2 + xOffset, + y: songTop + this.songAsset.height - selectedHeight + }) + } + + this.draw.songFrame({ + ctx: ctx, + x: winW / 2 - selectedWidth / 2 + xOffset, + y: songTop + this.songAsset.height - selectedHeight, + width: selectedWidth, + height: selectedHeight, + border: this.songAsset.border, + innerBorder: this.songAsset.innerBorder, + background: selectedSkin.background, + borderStyle: selectedSkin.border, + highlight: highlight, + noCrop: screen === "difficulty", + animateMS: Math.max(this.state.moveMS, this.state.mouseMoveMS), + cached: selectedWidth === this.songAsset.fullWidth ? 3 : (selectedWidth === this.songAsset.selectedWidth ? 2 : (selectedWidth === this.songAsset.width ? 1 : 0)), + frameCache: this.songFrameCache, + disabled: p2.session && currentSong.action && !currentSong.p2Enabled, + innerContent: (x, y, w, h) => { + ctx.strokeStyle = "#000" + if(screen === "title" || screen === "titleFadeIn" || screen === "song"){ + var opened = ((selectedWidth - this.songAsset.width) / (this.songAsset.selectedWidth - this.songAsset.width)) + var songSel = true + }else{ + var textW = strings.id === "en" ? 350 : 280 + this.selectTextCache.get({ + ctx: ctx, + x: frameLeft, + y: frameTop, + w: textW + 53 + 60, + h: this.songAsset.marginTop + 15, + id: "difficulty" + }, ctx => { + this.draw.layeredText({ + ctx: ctx, + text: strings.selectDifficulty, + fontSize: 46, + fontFamily: this.font, + x: 53, + y: 30, + width: textW, + forceShadow: true + }, [ + {x: -2, y: -2, outline: "#000", letterBorder: 23}, + {}, + {x: 2, y: 2, shadow: [3, 3, 3]}, + {x: 2, y: 2, outline: "#ad1516", letterBorder: 10}, + {x: -2, y: -2, outline: "#ff797b"}, + {outline: "#f70808"}, + {fill: "#fff", shadow: [-1, 1, 3, 1.5]} + ]) + }) + var opened = 1 + var songSel = false + + for(var i = 0; i < this.diffOptions.length; i++){ + var _x = x + 62 + i * 72 + var _y = y + 67 + ctx.fillStyle = this.diffOptions[i].fill + ctx.lineWidth = 5 + this.draw.roundedRect({ + ctx: ctx, + x: _x - 28, + y: _y, + w: 56, + h: 298, + radius: 24 + }) + ctx.fill() + ctx.stroke() + ctx.fillStyle = this.diffOptions[i].iconFill + ctx.beginPath() + ctx.arc(_x, _y + 28, 20, 0, Math.PI * 2) + ctx.fill() + this.draw.diffOptionsIcon({ + ctx: ctx, + x: _x, + y: _y + 28, + iconName: this.diffOptions[i].iconName + }) + + var text = this.diffOptions[i].text + if(this.diffOptions[i].iconName === "options" && (this.selectedDiff === i || this.state.options !== 0)){ + text = this.optionsList[this.state.options] + } + + this.draw.verticalText({ + ctx: ctx, + text: text, + x: _x, + y: _y + 57, + width: 56, + height: 220, + fill: "#fff", + outline: "#000", + outlineSize: this.songAsset.letterBorder, + letterBorder: 4, + fontSize: 28, + fontFamily: this.font, + letterSpacing: this.diffOptions[i].letterSpacing + }) + + var highlight = 0 + if(this.state.moveHover === i){ + highlight = 2 + }else if(this.selectedDiff === i){ + highlight = 1 + } + if(highlight){ + this.draw.highlight({ + ctx: ctx, + x: _x - 32, + y: _y - 3, + w: 64, + h: 304, + animate: highlight === 1, + animateMS: Math.max(this.state.moveMS, this.state.mouseMoveMS), + opacity: highlight === 2 ? 0.8 : 1, + radius: 24 + }) + if(this.selectedDiff === i && !this.touchEnabled){ + this.draw.diffCursor({ + ctx: ctx, + font: this.font, + x: _x, + y: _y - 45, + two: p2.session && p2.player === 2 + }) + } + } + } + } + var drawDifficulty = (ctx, i, currentUra) => { + if(currentSong.courses[this.difficultyId[i]] || currentUra){ + var crownDiff = currentUra ? "ura" : this.difficultyId[i] + var players = p2.session ? 2 : 1 + var score = [scoreStorage.get(currentSong.hash, false, true)] + if(p2.session){ + score[p2.player === 1 ? "push" : "unshift"](scoreStorage.getP2(currentSong.hash, false, true)) + } + var reversed = false + for(var a = players; a--;){ + var crownType = "" + var p = reversed ? -(a - 1) : a + if(score[p] && score[p][crownDiff]){ + crownType = score[p][crownDiff].crown + } + if(!reversed && players === 2 && p === 1 && crownType){ + reversed = true + a++ + }else{ + this.draw.crown({ + ctx: ctx, + type: crownType, + x: (songSel ? x + 33 + i * 60 : x + 402 + i * 100) + (players === 2 ? p === 0 ? -13 : 13 : 0), + y: songSel ? y + 75 : y + 30, + scale: 0.25, + ratio: this.ratio / this.pixelRatio + }) + } + } + if(songSel && !this.state.move){ + var _x = x + 33 + i * 60 + var _y = y + 120 + ctx.fillStyle = currentUra ? "#006279" : "#ff9f18" + ctx.beginPath() + ctx.arc(_x, _y + 22, 22, -Math.PI, 0) + ctx.arc(_x, _y + 266, 22, 0, Math.PI) + ctx.fill() + this.draw.diffIcon({ + ctx: ctx, + diff: currentUra ? 4 : i, + x: _x, + y: _y - 8, + scale: 1, + border: 6 + }) + }else{ + var _x = x + 402 + i * 100 + var _y = y + 87 + this.draw.diffIcon({ + ctx: ctx, + diff: i, + x: _x, + y: _y - 12, + scale: 1.4, + border: 6.5, + noFill: true + }) + ctx.fillStyle = "#aa7023" + ctx.lineWidth = 4.5 + ctx.fillRect(_x - 35.5, _y + 2, 71, 380) + ctx.strokeRect(_x - 35.5, _y + 2, 71, 380) + ctx.fillStyle = currentUra ? "#006279" : "#fff" + ctx.lineWidth = 2.5 + ctx.fillRect(_x - 28, _y + 19, 56, 351) + ctx.strokeRect(_x - 28, _y + 19, 56, 351) + this.draw.diffIcon({ + ctx: ctx, + diff: currentUra ? 4 : i, + x: _x, + y: _y - 12, + scale: 1.4, + border: 4.5 + }) + } + var offset = (songSel ? 44 : 56) / 2 + this.difficultyCache.get({ + ctx: ctx, + x: _x - offset, + y: songSel ? _y + 10 : _y + 23, + w: songSel ? 44 : 56, + h: (songSel ? 88 : 135) + 10, + id: this.difficulty[currentUra ? 4 : i] + (songSel ? "1" : "0") + }, ctx => { + var ja = strings.id === "ja" + this.draw.verticalText({ + ctx: ctx, + text: this.difficulty[i], + x: offset, + y: 0, + width: songSel ? 44 : 56, + height: songSel ? (i === 1 && ja ? 66 : 88) : (ja ? 130 : (i === 1 && ja ? 110 : 135)), + fill: currentUra ? "#fff" : "#000", + fontSize: songSel ? 25 : (i === 2 && ja ? 45 : 40), + fontFamily: this.font, + outline: currentUra ? "#003C52" : false, + outlineSize: currentUra ? this.songAsset.letterBorder : 0 + }) + }) + var songStarsObj = (currentUra ? currentSong.courses.ura : currentSong.courses[this.difficultyId[i]]) + var songStars = songStarsObj.stars + var songBranch = songStarsObj.branch + var moveMS = Math.max(this.state.moveMS, this.state.mouseMoveMS) + var elapsedMS = this.state.screenMS > moveMS || !songSel ? this.state.screenMS : moveMS + var fade = ((ms - elapsedMS) % 2000) / 2000 + if(songBranch && fade > 0.25 && fade < 0.75){ + this.draw.verticalText({ + ctx: ctx, + text: strings.songBranch, + x: _x, + y: _y + (songSel ? 110 : 185), + width: songSel ? 44 : 56, + height: songSel ? 160 : 170, + fill: songSel && !currentUra ? "#c85200" : "#fff", + fontSize: songSel ? 25 : 27, + fontFamily: songSel ? "Meiryo, Microsoft YaHei, sans-serif" : this.font, + outline: songSel ? false : "#f22666", + outlineSize: songSel ? 0 : this.songAsset.letterBorder + }) + }else{ + for(var j = 0; j < 10; j++){ + if(songSel){ + var yPos = _y + 113 + j * 17 + }else{ + var yPos = _y + 178 + j * 19.5 + } + if(10 - j > songStars){ + ctx.fillStyle = currentUra ? "#187085" : (songSel ? "#e97526" : "#e7e7e7") + ctx.beginPath() + ctx.arc(_x, yPos, songSel ? 4.5 : 5, 0, Math.PI * 2) + ctx.fill() + }else{ + this.draw.diffStar({ + ctx: ctx, + songSel: songSel, + ura: currentUra, + x: _x, + y: yPos, + ratio: ratio + }) + } + } + } + var currentDiff = this.selectedDiff - this.diffOptions.length + if(this.selectedDiff === 4 + this.diffOptions.length){ + currentDiff = 3 + } + if(!songSel){ + var highlight = 0 + if(this.state.moveHover - this.diffOptions.length === i){ + highlight = 2 + }else if(currentDiff === i){ + highlight = 1 + } + if(currentDiff === i && !this.touchEnabled){ + this.draw.diffCursor({ + ctx: ctx, + font: this.font, + x: _x, + y: _y - 65, + side: currentSong.p2Cursor === currentDiff && p2.socket.readyState === 1, + two: p2.session && p2.player === 2 + }) + } + if(highlight){ + this.draw.highlight({ + ctx: ctx, + x: _x - 32, + y: _y + 14, + w: 64, + h: 362, + animate: highlight === 1, + animateMS: Math.max(this.state.moveMS, this.state.mouseMoveMS), + opacity: highlight === 2 ? 0.8 : 1 + }) + } + } + } + } + for(var i = 0; currentSong.courses && i < 4; i++){ + var currentUra = i === 3 && (this.state.ura && !songSel || currentSong.courses.ura && songSel) + if(songSel && currentUra){ + drawDifficulty(ctx, i, false) + var elapsedMS = Math.max(this.state.screenMS, this.state.moveMS, this.state.mouseMoveMS) + var fade = ((ms - elapsedMS) % 4000) / 4000 + var alphaFade = 0 + if(fade > 0.95){ + alphaFade = this.draw.easeOut(1 - (fade - 0.95) * 20) + }else if(fade > 0.5){ + alphaFade = 1 + }else if(fade > 0.45){ + alphaFade = this.draw.easeIn((fade - 0.45) * 20) + } + this.draw.alpha(alphaFade, ctx, ctx => { + ctx.fillStyle = this.songSkin.selected.background + ctx.fillRect(x + 7 + i * 60, y + 60, 52, 352) + drawDifficulty(ctx, i, true) + }, winW, winH) + }else{ + drawDifficulty(ctx, i, currentUra) + } + } + for(var i = 0; currentSong.courses && i < 4; i++){ + if(!songSel && i === currentSong.p2Cursor && p2.socket.readyState === 1){ + var _x = x + 402 + i * 100 + var _y = y + 87 + var currentDiff = this.selectedDiff - this.diffOptions.length + if(this.selectedDiff === 4 + this.diffOptions.length){ + currentDiff = 3 + } + this.draw.diffCursor({ + ctx: ctx, + font: this.font, + x: _x, + y: _y - 65, + two: !p2.session || p2.player === 1, + side: currentSong.p2Cursor === currentDiff, + scale: 1 + }) + } + } + + var borders = (this.songAsset.border + this.songAsset.innerBorder) * 2 + var textW = this.songAsset.width - borders + var textH = this.songAsset.height - borders + var textX = Math.max(w - 37 - textW / 2, w / 2 - textW / 2) + var textY = opened * 12 + (1 - opened) * 7 + + if(currentSong.subtitle){ + this.currentSongCache.get({ + ctx: ctx, + x: x + textX - textW, + y: y + textY, + w: textW, + h: textH, + id: "subtitle", + }, ctx => { + this.draw.verticalText({ + ctx: ctx, + text: currentSong.subtitle, + x: textW / 2, + y: 7, + width: textW, + height: textH - 35, + fill: "#fff", + outline: "#000", + outlineSize: 14, + fontSize: 28, + fontFamily: this.font, + align: "bottom" + }) + }) + } + + var hasMaker = currentSong.maker || currentSong.maker === 0 + var hasVideo = currentSong.video || currentSong.video === 0 + if(hasMaker || currentSong.lyrics){ + if (songSel) { + var _x = x + 38 + var _y = y + 10 + ctx.strokeStyle = "#000" + ctx.lineWidth = 5 + + if(hasMaker){ + var grd = ctx.createLinearGradient(_x, _y, _x, _y + 50) + grd.addColorStop(0, "#fa251a") + grd.addColorStop(1, "#ffdc33") + ctx.fillStyle = grd + }else{ + ctx.fillStyle = "#000" + } + this.draw.roundedRect({ + ctx: ctx, + x: _x - 28, + y: _y, + w: 192, + h: 50, + radius: 24 + }) + ctx.fill() + ctx.stroke() + + if(hasMaker){ + this.draw.layeredText({ + ctx: ctx, + text: strings.creative.creative, + fontSize: strings.id === "en" ? 28 : 34, + fontFamily: this.font, + align: "center", + baseline: "middle", + x: _x + 68, + y: _y + (strings.id === "ja" || strings.id === "en" ? 25 : 28), + width: 172 + }, [ + {outline: "#fff", letterBorder: 6}, + {fill: "#000"} + ]) + }else{ + this.draw.layeredText({ + ctx: ctx, + text: strings.withLyrics, + fontSize: strings.id === "en" ? 28 : 34, + fontFamily: this.font, + align: "center", + baseline: "middle", + x: _x + 68, + y: _y + (strings.id === "ja" || strings.id === "en" ? 25 : 28), + width: 172 + }, [ + {fill: currentSong.skin.border[0]} + ]) + } + } else if(currentSong.maker && currentSong.maker.id > 0 && currentSong.maker.name){ + var _x = x + 62 + var _y = y + 380 + ctx.lineWidth = 5 + + var grd = ctx.createLinearGradient(_x, _y, _x, _y+50); + grd.addColorStop(0, '#fa251a'); + grd.addColorStop(1, '#ffdc33'); + + ctx.fillStyle = '#75E2EE'; + this.draw.roundedRect({ + ctx: ctx, + x: _x - 28, + y: _y, + w: 250, + h: 80, + radius: 15 + }) + ctx.fill() + ctx.stroke() + ctx.beginPath() + ctx.arc(_x, _y + 28, 20, 0, Math.PI * 2) + ctx.fill() + + this.draw.layeredText({ + ctx: ctx, + text: strings.creative.maker, + fontSize: 24, + fontFamily: this.font, + align: "left", + baseline: "middle", + x: _x - 15, + y: _y + 23 + }, [ + {outline: "#000", letterBorder: 8}, + {fill: "#fff"} + ]) + + this.draw.layeredText({ + ctx: ctx, + text: currentSong.maker.name, + fontSize: 28, + fontFamily: this.font, + align: "center", + baseline: "middle", + x: _x + 100, + y: _y + 56, + width: 210 + }, [ + {outline: "#fff", letterBorder: 8}, + {fill: "#000"} + ]) + + if(this.state.moveHover === "maker"){ + this.draw.highlight({ + ctx: ctx, + x: _x - 32, + y: _y - 3, + w: 250 + 7, + h: 80 + 7, + opacity: 0.8, + radius: 15 + }) + } + } + } + + for(var i = 0; currentSong.courses && i < 4; i++){ + if(currentSong.courses[this.difficultyId[i]] || currentUra){ + if(songSel && i === currentSong.p2Cursor && p2.socket.readyState === 1){ + var _x = x + 33 + i * 60 + var _y = y + 120 + this.draw.diffCursor({ + ctx: ctx, + font: this.font, + x: _x, + y: _y - 45, + two: !p2.session || p2.player === 1, + side: false, + scale: 0.7 + }) + } + } + } + + if(!songSel && currentSong.courses.ura){ + var fade = ((ms - this.state.screenMS) % 1200) / 1200 + var _x = x + 402 + 4 * 100 + fade * 25 + var _y = y + 258 + ctx.fillStyle = "rgba(0, 0, 0, " + 0.2 * this.draw.easeInOut(1 - fade) + ")" + ctx.beginPath() + ctx.moveTo(_x - 35, _y - 25) + ctx.lineTo(_x - 10, _y) + ctx.lineTo(_x - 35, _y + 25) + ctx.fill() + } + if (songSel && hasVideo) { + var _x = x + 230; + var _y = y - 13; + + // Icon dimensions and positions + const rectX = _x - 10; + const rectY = _y + 23; + const rectWidth = 30; // Adjust width + const rectHeight = 30; // Adjust height + const rectRadius = 5; // Rounded corners + + // Draw the outer rectangle (film strip body) + ctx.beginPath(); + ctx.moveTo(rectX + rectRadius, rectY); + ctx.arcTo(rectX + rectWidth, rectY, rectX + rectWidth, rectY + rectHeight, rectRadius); + ctx.arcTo(rectX + rectWidth, rectY + rectHeight, rectX, rectY + rectHeight, rectRadius); + ctx.arcTo(rectX, rectY + rectHeight, rectX, rectY, rectRadius); + ctx.arcTo(rectX, rectY, rectX + rectWidth, rectY, rectRadius); + ctx.closePath(); + ctx.fillStyle = "#000"; // Black fill + ctx.fill(); + ctx.strokeStyle = "#fff"; // White border + ctx.lineWidth = 2; + ctx.stroke(); + + // Draw the "holes" for the film strip + ctx.fillStyle = "#000"; // White for the holes + const holeSize = 3; + const holeSpacing = 5; + for (let i = 0; i < 4; i++) { + // Left side holes + ctx.fillRect(rectX - holeSize - 2, rectY + 5 + i * holeSpacing, holeSize, holeSize); + // Right side holes + ctx.fillRect(rectX + rectWidth + 2, rectY + 5 + i * holeSpacing, holeSize, holeSize); + } + + // Draw the play button (triangle) + const centerX = rectX + rectWidth / 2; + const centerY = rectY + rectHeight / 2; + const triangleSize = 12; + ctx.beginPath(); + ctx.moveTo(centerX - triangleSize / 2, centerY - triangleSize); + ctx.lineTo(centerX + triangleSize, centerY); + ctx.lineTo(centerX - triangleSize / 2, centerY + triangleSize); + ctx.closePath(); + ctx.fillStyle = "#fff"; // White triangle + ctx.fill(); + } + ctx.globalAlpha = 1 - Math.max(0, opened - 0.5) * 2 + ctx.fillStyle = selectedSkin.background + ctx.fillRect(x, y, w, h) + ctx.globalAlpha = 1 + var verticalTitle = ctx => { + this.draw.verticalText({ + ctx: ctx, + text: currentSong.title, + x: textW / 2, + y: 7, + width: textW, + height: textH - 35, + fill: "#fff", + outline: selectedSkin.outline, + outlineSize: this.songAsset.letterBorder, + fontSize: 40, + fontFamily: this.font + }) + } + if(selectedSkin.outline === "#000"){ + this.currentSongCache.get({ + ctx: ctx, + x: x + textX, + y: y + textY - 7, + w: textW, + h: textH, + id: "title", + }, verticalTitle) + }else{ + this.songTitleCache.get({ + ctx: ctx, + x: x + textX, + y: y + textY - 7, + w: textW, + h: textH, + id: currentSong.title + selectedSkin.outline, + }, verticalTitle) + } + if(!songSel && this.selectableText !== currentSong.title){ + this.draw.verticalText({ + ctx: ctx, + text: currentSong.title, + x: x + textX + textW / 2, + y: y + textY, + width: textW, + height: textH - 35, + fontSize: 40, + fontFamily: this.font, + selectable: this.selectable, + selectableScale: this.ratio / this.pixelRatio, + selectableX: Math.max(0, innerWidth / 2 - lastHeight * 16 / 9) + }) + this.selectable.style.display = "" + this.selectableText = currentSong.title + } + } + }) + + if(screen !== "difficulty" && this.selectableText){ + this.selectableText = "" + this.selectable.style.display = "none" + } + + if(songSelMoving){ + this.draw.highlight({ + ctx: ctx, + x: winW / 2 - selectedWidth / 2, + y: songTop, + w: selectedWidth, + h: selectedHeight, + opacity: 0.8 + }) + } + + ctx.fillStyle = "#000" + ctx.fillRect(0, frameTop + 595, 1280 + frameLeft * 2, 125 + frameTop) + var x = 0 + var y = frameTop + 603 + var w = p2.session ? frameLeft + 638 - 200 : frameLeft + 638 + var h = 117 + frameTop + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_score_p1"], + x: x, + y: y, + w: w, + h: h, + dx: frameLeft + 10, + dy: frameTop + 15, + scale: 1.55 + }) + ctx.fillStyle = "rgba(249, 163, 149, 0.5)" + ctx.beginPath() + ctx.moveTo(x, y) + ctx.lineTo(x + w, y) + ctx.lineTo(x + w - 4, y + 4) + ctx.lineTo(x, y + 4) + ctx.fill() + ctx.fillStyle = "rgba(0, 0, 0, 0.25)" + ctx.beginPath() + ctx.moveTo(x + w, y) + ctx.lineTo(x + w, y + h) + ctx.lineTo(x + w - 4, y + h) + ctx.lineTo(x + w - 4, y + 4) + ctx.fill() + + if(!p2.session || p2.player === 1){ + var name = account.loggedIn ? account.displayName : strings.defaultName + var rank = account.loggedIn || !gameConfig.accounts || p2.session ? false : strings.notLoggedIn + }else{ + var name = p2.name || strings.defaultName + var rank = false + } + this.nameplateCache.get({ + ctx: ctx, + x: frameLeft + 60, + y: frameTop + 640, + w: 273, + h: 66, + id: "1p" + name + "\n" + rank, + }, ctx => { + this.draw.nameplate({ + ctx: ctx, + x: 3, + y: 3, + name: name, + rank: rank, + font: this.font + }) + }) + if(this.state.moveHover === "account"){ + this.draw.highlight({ + ctx: ctx, + x: frameLeft + 59.5, + y: frameTop + 639.5, + w: 271, + h: 64, + radius: 28.5, + opacity: 0.8, + size: 10 + }) + } + + if(p2.session){ + x = x + w + 4 + w = 396 + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_settings"], + x: x, + y: y, + w: w, + h: h, + dx: frameLeft + 11, + dy: frameTop + 45, + scale: 3.1 + }) + ctx.fillStyle = "rgba(255, 255, 255, 0.5)" + ctx.beginPath() + ctx.moveTo(x, y + h) + ctx.lineTo(x, y) + ctx.lineTo(x + w, y) + ctx.lineTo(x + w, y + 4) + ctx.lineTo(x + 4, y + 4) + ctx.lineTo(x + 4, y + h) + ctx.fill() + ctx.fillStyle = "rgba(0, 0, 0, 0.25)" + ctx.beginPath() + ctx.moveTo(x + w, y) + ctx.lineTo(x + w, y + h) + ctx.lineTo(x + w - 4, y + h) + ctx.lineTo(x + w - 4, y + 4) + ctx.fill() + if(this.state.moveHover === "session"){ + this.draw.highlight({ + ctx: ctx, + x: x, + y: y, + w: w, + h: h, + opacity: 0.8 + }) + } + } + + x = p2.session ? frameLeft + 642 + 200 : frameLeft + 642 + w = p2.session ? frameLeft + 638 - 200 : frameLeft + 638 + if(p2.session){ + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_score_p2"], + x: x, + y: y, + w: w, + h: h, + dx: frameLeft + 15, + dy: frameTop - 20, + scale: 1.55 + }) + ctx.fillStyle = "rgba(138, 245, 247, 0.5)" + }else{ + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_settings"], + x: x, + y: y, + w: w, + h: h, + dx: frameLeft + 11, + dy: frameTop + 45, + scale: 3.1 + }) + ctx.fillStyle = "rgba(255, 255, 255, 0.5)" + } + ctx.beginPath() + ctx.moveTo(x, y + h) + ctx.lineTo(x, y) + ctx.lineTo(x + w, y) + ctx.lineTo(x + w, y + 4) + ctx.lineTo(x + 4, y + 4) + ctx.lineTo(x + 4, y + h) + ctx.fill() + if(screen !== "difficulty" && p2.socket && p2.socket.readyState === 1 && !assets.customSongs){ + var elapsed = (ms - this.state.screenMS) % 3100 + var fade = 1 + if(!p2.session && screen === "song"){ + if(elapsed > 2800){ + fade = (elapsed - 2800) / 300 + }else if(2000 < elapsed){ + if(elapsed < 2300){ + fade = 1 - (elapsed - 2000) / 300 + }else{ + fade = 0 + } + } + } + if(fade > 0){ + if(fade < 1){ + ctx.globalAlpha = this.draw.easeIn(fade) + } + this.sessionCache.get({ + ctx: ctx, + x: p2.session ? winW / 4 : winW / 2, + y: y + (h - 32) / 2, + w: winW / 2, + h: 38, + id: p2.session ? "sessionend" : "sessionstart" + }) + ctx.globalAlpha = 1 + } + if(!p2.session && this.state.moveHover === "session"){ + this.draw.highlight({ + ctx: ctx, + x: x, + y: y, + w: w, + h: h, + opacity: 0.8 + }) + } + } + if(p2.session){ + if(p2.player === 1){ + var name = p2.name || strings.default2PName + }else{ + var name = account.loggedIn ? account.displayName : strings.default2PName + } + this.nameplateCache.get({ + ctx: ctx, + x: frameLeft + 949, + y: frameTop + 640, + w: 273, + h: 66, + id: "2p" + name, + }, ctx => { + this.draw.nameplate({ + ctx: ctx, + x: 3, + y: 3, + name: name, + font: this.font, + blue: true + }) + }) + } + + if(this.state.showWarning){ + if(this.preview){ + this.endPreview() + } + ctx.fillStyle = "rgba(0, 0, 0, 0.5)" + ctx.fillRect(0, 0, winW, winH) + + ctx.save() + ctx.translate(frameLeft, frameTop) + + var pauseRect = (ctx, mul) => { + this.draw.roundedRect({ + ctx: ctx, + x: 269 * mul, + y: 93 * mul, + w: 742 * mul, + h: 494 * mul, + radius: 17 * mul + }) + } + pauseRect(ctx, 1) + ctx.strokeStyle = "#fff" + ctx.lineWidth = 24 + ctx.stroke() + ctx.strokeStyle = "#000" + ctx.lineWidth = 12 + ctx.stroke() + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_pause"], + shape: pauseRect, + dx: 68, + dy: 11 + }) + if(this.showWarning.name === "scoreSaveFailed"){ + var text = strings.scoreSaveFailed + }else if(this.showWarning.name === "loadSongError"){ + var text = [] + var textIndex = 0 + var subText = [this.showWarning.title, this.showWarning.id, this.showWarning.error] + var textParts = strings.loadSongError.split("%s") + textParts.forEach((textPart, i) => { + if(i !== 0){ + text.push(subText[textIndex++]) + } + text.push(textPart) + }) + text = text.join("") + } + this.draw.wrappingText({ + ctx: ctx, + text: text, + fontSize: 30, + fontFamily: this.font, + x: 300, + y: 130, + width: 680, + height: 300, + lineHeight: 35, + fill: "#000", + verticalAlign: "middle", + textAlign: "center" + }) + + var _x = 640 + var _y = 470 + var _w = 464 + var _h = 80 + ctx.fillStyle = "#ffb447" + this.draw.roundedRect({ + ctx: ctx, + x: _x - _w / 2, + y: _y, + w: _w, + h: _h, + radius: 30 + }) + ctx.fill() + var layers = [ + {outline: "#000", letterBorder: 10}, + {fill: "#fff"} + ] + this.draw.layeredText({ + ctx: ctx, + text: strings.tutorial.ok, + x: _x, + y: _y + 18, + width: _w, + height: _h - 54, + fontSize: 40, + fontFamily: this.font, + letterSpacing: -1, + align: "center" + }, layers) + + var highlight = 1 + if(this.state.moveHover === "showWarning"){ + highlight = 2 + } + if(highlight){ + this.draw.highlight({ + ctx: ctx, + x: _x - _w / 2 - 3.5, + y: _y - 3.5, + w: _w + 7, + h: _h + 7, + animate: highlight === 1, + animateMS: Math.max(this.state.moveMS, this.state.mouseMoveMS), + opacity: highlight === 2 ? 0.8 : 1, + radius: 30 + }) + } + + ctx.restore() + } + + if(screen === "titleFadeIn"){ + ctx.save() + + var elapsed = ms - this.state.screenMS + ctx.globalAlpha = Math.max(0, 1 - elapsed / 500) + ctx.fillStyle = "#000" + ctx.fillRect(0, 0, winW, winH) + + ctx.restore() + } + + if(p2.session && (!this.lastScoreMS || ms > this.lastScoreMS + 1000)){ + this.lastScoreMS = ms + scoreStorage.eventLoop() + } + } + + drawBackground(cat){ + if(this.songSkin[cat] && this.songSkin[cat].bg_img){ + let filename = this.songSkin[cat].bg_img.slice(0, this.songSkin[cat].bg_img.lastIndexOf(".")) + this.songSelect.style.backgroundImage = "url('" + assets.image[filename].src + "')" + }else{ + this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_def"].src + "')" + } + } + + drawClosedSong(config){ + var ctx = config.ctx + + this.drawSongCrown(config) + config.width = this.songAsset.width + config.height = this.songAsset.height + config.border = this.songAsset.border + config.innerBorder = this.songAsset.innerBorder + config.background = config.song.skin.background + config.borderStyle = config.song.skin.border + config.outline = config.song.skin.outline + config.text = config.song.title + config.animateMS = Math.max(this.state.moveMS, this.state.mouseMoveMS) + config.cached = 1 + config.frameCache = this.songFrameCache + config.innerContent = (x, y, w, h) => { + this.songTitleCache.get({ + ctx: ctx, + x: x, + y: y, + w: w, + h: h, + id: config.text + config.outline, + }, ctx => { + this.draw.verticalText({ + ctx: ctx, + text: config.text, + x: w / 2, + y: 7, + width: w, + height: h - 35, + fill: "#fff", + outline: config.outline, + outlineSize: this.songAsset.letterBorder, + fontSize: 40, + fontFamily: this.font + }) + }) + } + this.draw.songFrame(config) + if("p2Cursor" in config.song && config.song.p2Cursor !== null && p2.socket.readyState === 1){ + this.draw.diffCursor({ + ctx: ctx, + font: this.font, + x: config.x + 48, + y: config.y - 27, + two: true, + scale: 1, + side: true + }) + } + } + + drawSongCrown(config){ + if(!config.song.action && config.song.hash){ + var ctx = config.ctx + var players = p2.session ? 2 : 1 + var score = [scoreStorage.get(config.song.hash, false, true)] + var scoreDrawn = [] + if(p2.session){ + score[p2.player === 1 ? "push" : "unshift"](scoreStorage.getP2(config.song.hash, false, true)) + } + for(var i = this.difficultyId.length; i--;){ + var diff = this.difficultyId[i] + for(var p = players; p--;){ + if(!score[p] || scoreDrawn[p]){ + continue + } + if(config.song.courses[this.difficultyId[i]] && score[p][diff] && score[p][diff].crown){ + this.draw.crown({ + ctx: ctx, + type: score[p][diff].crown, + x: (config.x + this.songAsset.width / 2) + (players === 2 ? p === 0 ? -13 : 13 : 0), + y: config.y - 13, + scale: 0.3, + ratio: this.ratio / this.pixelRatio + }) + this.draw.diffIcon({ + ctx: ctx, + diff: i, + x: (config.x + this.songAsset.width / 2 + 8) + (players === 2 ? p === 0 ? -13 : 13 : 0), + y: config.y - 8, + scale: diff === "hard" || diff === "normal" ? 0.45 : 0.5, + border: 6.5, + small: true + }) + scoreDrawn[p] = true + } + } + } + } + } + + startPreview(loadOnly){ + if(!loadOnly && this.state && this.state.showWarning || this.state.waitPreview > this.getMS()){ + return + } + var currentSong = this.songs[this.selectedSong] + var id = currentSong.id + var prvTime = currentSong.preview + this.endPreview() + + if("id" in currentSong){ + var startLoad = this.getMS() + if(loadOnly){ + var currentId = null + }else{ + var currentId = this.previewId + this.previewing = this.selectedSong + } + var songObj = this.previewList.find(song => song && song.id === id) + + if(songObj){ + if(!loadOnly){ + this.preview = songObj.preview_sound + this.preview.gain = snd.previewGain + this.previewLoaded(startLoad, songObj.preview_time, currentSong.volume) + } + }else{ + songObj = {id: id} + if(currentSong.previewMusic){ + songObj.preview_time = 0 + var promise = snd.previewGain.load(currentSong.previewMusic).catch(() => { + songObj.preview_time = prvTime + return snd.previewGain.load(currentSong.music) + }) + }else if(currentSong.unloaded){ + var promise = this.getUnloaded(this.selectedSong, songObj, currentId) + }else if(currentSong.sound){ + songObj.preview_time = prvTime + currentSong.sound.gain = snd.previewGain + var promise = Promise.resolve(currentSong.sound) + }else if(currentSong.music !== "muted"){ + songObj.preview_time = prvTime + var promise = snd.previewGain.load(currentSong.music) + }else{ + return + } + promise.then(sound => { + if(currentId === this.previewId || loadOnly){ + songObj.preview_sound = sound + if(!loadOnly){ + this.preview = sound + this.previewLoaded(startLoad, songObj.preview_time, currentSong.volume) + } + var oldPreview = this.previewList.shift() + if(oldPreview){ + oldPreview.preview_sound.clean() + } + this.previewList.push(songObj) + }else{ + sound.clean() + } + }).catch(e => { + if(e !== "cancel"){ + return Promise.reject(e) + } + }) + } + } + } + previewLoaded(startLoad, prvTime, volume){ + var endLoad = this.getMS() + var difference = endLoad - startLoad + var minDelay = 300 + var delay = minDelay - Math.min(minDelay, difference) + snd.previewGain.setVolumeMul(volume || 1) + this.preview.playLoop(delay / 1000, false, prvTime) + } + endPreview(force){ + this.previewId++ + this.previewing = force ? "muted" : null + if(this.preview){ + this.preview.stop() + } + } + playBgm(enabled){ + if(enabled && this.state && this.state.showWarning){ + return + } + if(enabled && !this.bgmEnabled){ + this.bgmEnabled = true + snd.musicGain.fadeIn(0.4) + }else if(!enabled && this.bgmEnabled){ + this.bgmEnabled = false + snd.musicGain.fadeOut(0.4) + } + } + getUnloaded(selectedSong, songObj, currentId){ + var currentSong = this.songs[selectedSong] + var file = currentSong.chart + var importSongs = new ImportSongs(false, assets.otherFiles) + return file.read(currentSong.type === "tja" ? "utf-8" : "").then(data => { + currentSong.chart = new CachedFile(data, file) + return importSongs[currentSong.type === "tja" ? "addTja" : "addOsu"]({ + file: currentSong.chart, + index: currentSong.id + }) + }).then(() => { + var imported = importSongs.songs[currentSong.id] + importSongs.clean() + songObj.preview_time = imported.preview + var index = assets.songs.findIndex(song => song.id === currentSong.id) + if(index !== -1){ + assets.songs[index] = imported + } + this.songs[selectedSong] = this.addSong(imported) + this.state.moveMS = this.getMS() - this.songSelecting.speed * this.songSelecting.resize + if(imported.music && currentId === this.previewId){ + return snd.previewGain.load(imported.music).then(sound => { + imported.sound = sound + this.songs[selectedSong].sound = sound + return sound.copy() + }) + }else{ + return Promise.reject("cancel") + } + }) + } + addSong(song){ + var title = this.getLocalTitle(song.title, song.title_lang) + var subtitle = this.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang) + var skin = null + var categoryName = "" + var originalCategory = "" + if(song.category_id !== null && song.category_id !== undefined){ + var category = assets.categories.find(cat => cat.id === song.category_id) + var categoryName = this.getLocalTitle(category.title, category.title_lang) + var originalCategory = category.title + var skin = this.songSkin[category.title] + }else if(song.category){ + var categoryName = song.category + var originalCategory = song.category + } + var addedSong = { + title: title, + originalTitle: song.title, + subtitle: subtitle, + skin: skin || this.songSkin.default, + originalCategory: originalCategory, + category: categoryName, + preview: song.preview || 0, + songSkin: song.song_skin || {}, + canJump: true, + hash: song.hash || song.title + } + for(var i in song){ + if(!(i in addedSong)){ + addedSong[i] = song[i] + } + } + return addedSong + } + + onusers(response){ + var p2InSong = false + this.songs.forEach(song => { + song.p2Cursor = null + }) + if(response && response.value){ + response.value.forEach(idDiff => { + var id = idDiff.id |0 + var diff = idDiff.diff + var diffId = this.difficultyId.indexOf(diff) + if(diffId > 3){ + diffId = 3 + } + if(diffId >= 0){ + var index = 0 + var currentSong = this.songs.find((song, i) => { + index = i + return song.id === id + }) + if(currentSong){ + currentSong.p2Cursor = diffId + if(p2.session && currentSong.courses){ + this.setSelectedSong(index) + this.state.move = 0 + if(this.state.screen !== "difficulty"){ + this.toSelectDifficulty({player: response.value.player}) + } + this.search.enabled = false + p2InSong = true + this.search.remove() + } + } + } + }) + } + + if(!this.search.enabled && !p2InSong){ + this.search.enabled = true + } + } + onsongsel(response){ + if(response && response.value){ + var selected = false + if(response.type === "songsel" && "selected" in response.value){ + selected = response.value.selected + } + if("fromRandom" in response.value && response.value.fromRandom === true){ + this.lastRandom = true + } + if("song" in response.value){ + var song = +response.value.song + if(song >= 0 && song < this.songs.length){ + if(response.type === "catjump"){ + var moveBy = response.value.move + if(moveBy === -1 || moveBy === 1){ + this.setSelectedSong(song) + this.categoryJump(moveBy, {player: response.value.player}) + } + }else if(!selected){ + this.state.locked = true + if(this.state.screen === "difficulty"){ + this.toSongSelect(true) + } + var moveBy = song - this.selectedSong + if(moveBy){ + if(this.selectedSong < song){ + var altMoveBy = -this.mod(this.songs.length, this.selectedSong - song) + }else{ + var altMoveBy = this.mod(this.songs.length, moveBy) + } + if(Math.abs(altMoveBy) < Math.abs(moveBy)){ + moveBy = altMoveBy + } + this.moveToSong(moveBy, {player: response.value.player}) + } + }else if(this.songs[song].courses){ + this.setSelectedSong(song) + this.state.move = 0 + if(this.state.screen !== "difficulty"){ + this.playBgm(false) + this.toSelectDifficulty({player: response.value.player}) + } + } + } + } + } + } + oncatjump(response){ + if(response && response.value){ + if("song" in response.value){ + var song = +response.value.song + if(song >= 0 && song < this.songs.length){ + this.state.locked = true + } + } + } + } + startP2(){ + this.onusers(p2.getMessage("users")) + if(p2.session){ + this.onsongsel(p2.getMessage("songsel")) + } + pageEvents.add(p2, "message", response => { + if(response.type == "users"){ + this.onusers(response) + } + if(p2.session && (response.type == "songsel" || response.type == "catjump")){ + this.onsongsel(response) + this.state.selLock = false + } + }) + if(p2.closed){ + p2.open() + } + } + + mod(length, index){ + return ((index % length) + length) % length + } + + getLocalTitle(title, titleLang){ + if(titleLang){ + for(var id in titleLang){ + if(id === "en" && strings.preferEn && !(strings.id in titleLang) && titleLang.en || id === strings.id && titleLang[id]){ + return titleLang[id] + } + } + } + return title + } + + clearHash(){ + if(location.hash.toLowerCase().startsWith("#song=")){ + p2.hash("") + } + } + + playSound(id, time, snd){ + if(!this.drumSounds && (id === "se_don" || id === "se_ka" || id === "se_cancel")){ + return + } + var ms = Date.now() + (time || 0) * 1000 + if(!(id in this.playedSounds) || ms > this.playedSounds[id] + 30){ + assets.sounds[id + (snd ? "_p" + snd : "")].play(time) + this.playedSounds[id] = ms + } + } + + getMS(){ + return Date.now() + } + + clean(){ + this.keyboard.clean() + this.gamepad.clean() + this.clearHash() + this.draw.clean() + this.songTitleCache.clean() + this.selectTextCache.clean() + this.categoryCache.clean() + this.difficultyCache.clean() + this.sessionCache.clean() + this.currentSongCache.clean() + this.nameplateCache.clean() + this.search.clean() + assets.sounds["bgm_songsel"].stop() + if(!this.bgmEnabled){ + snd.musicGain.fadeIn() + setTimeout(() => { + snd.buffer.loadSettings() + }, 500) + } + this.redrawRunning = false + this.endPreview() + this.previewList.forEach(song => { + if(song){ + song.preview_sound.clean() + } + }) + pageEvents.remove(loader.screen, ["mousemove", "mouseleave", "mousedown", "touchstart"]) + pageEvents.remove(this.canvas, ["touchend", "wheel"]) + pageEvents.remove(p2, "message") + if(this.touchEnabled && fullScreenSupported){ + pageEvents.remove(this.touchFullBtn, "click") + delete this.touchFullBtn + } + delete this.selectable + delete this.ctx + delete this.canvas + } + + toDownload(){ + var jsZip = new JSZip() + var zip = new jsZip() + var song = this.songs[this.selectedSong] + var promises = [] + var chartParsed = false + var musicFilename + var chartBlob + var musicBlob + var lyricsBlob + var blobs = [] + if(song.chart){ + var charts = [] + if(song.chart.separateDiff){ + for(var i in song.chart){ + if(song.chart[i] && i !== "separateDiff"){ + charts.push(song.chart[i]) + } + } + }else{ + charts.push(song.chart) + } + charts.forEach(chart => { + promises.push(chart.blob().then(blob => { + var promise + if(!chartParsed){ + chartParsed = true + if(song.type === "tja"){ + promise = readFile(blob, false, "utf-8").then(dataRaw => { + var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : [] + var tja = new ParseTja(data, "oni", 0, 0, true) + for(var diff in tja.metadata){ + var meta = tja.metadata[diff] + if(meta.wave){ + musicFilename = meta.wave + } + } + }) + }else if(song.type === "osu"){ + promise = readFile(blob).then(dataRaw => { + var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : [] + var osu = new ParseOsu(data, "oni", 0, 0, true) + if(osu.generalInfo.AudioFilename){ + musicFilename = osu.generalInfo.AudioFilename + } + }) + } + } + var outputBlob = { + name: chart.name, + data: blob + } + if(song.type === "tja" && !song.chart.separateDiff){ + chartBlob = outputBlob + } + blobs.push(outputBlob) + return promise + })) + }) + } + if(song.music){ + promises.push(song.music.blob().then(blob => { + musicBlob = { + name: song.music.name, + data: blob + } + blobs.push(musicBlob) + })) + } + // if(song.lyricsFile){ + // promises.push(song.lyricsFile.blob().then(blob => { + // lyricsBlob = { + // name: song.lyricsFile.name, + // data: blob + // } + // blobs.push(lyricsBlob) + // })) + // } + Promise.all(promises).then(() => { + if(musicFilename){ + if(musicBlob){ + musicBlob.name = musicFilename + } + var filename = musicFilename + var index = filename.lastIndexOf(".") + if(index !== -1){ + filename = filename.slice(0, index) + } + if(chartBlob){ + chartBlob.name = filename + ".tja" + } + if(lyricsBlob){ + lyricsBlob.name = filename + ".vtt" + } + } + blobs.forEach(blob => zip.file(blob.name, blob.data)) + }).then(() => zip.generateAsync({type: "blob"})).then(zip => { + var url = URL.createObjectURL(zip) + var link = document.createElement("a") + link.href = url + if("download" in HTMLAnchorElement.prototype){ + link.download = song.title + ".zip" + }else{ + link.target = "_blank" + } + link.innerText = "." + link.style.opacity = "0" + document.body.appendChild(link) + setTimeout(() => { + link.click() + document.body.removeChild(link) + setTimeout(() => { + URL.revokeObjectURL(url) + }, 5000) + }) + }) + } + + toDelete() { + // ここに削除処理を書く + if (!confirm("本当に削除しますか?\nこの曲に問題がある場合や\n公序良俗に反する場合にのみ実行したほうがいいと思います\n本当に曲が削除されます\n成功しても反映まで1分ほどかかる場合があります")) { + return; + } + fetch("/api/delete", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + id: this.songs[this.selectedSong].id, + }) + }) + .then((res) => res.text()) + .then((text) => { + alert(text); + }); + } +} diff --git a/public/src/js/soundbuffer.js b/public/src/js/soundbuffer.js new file mode 100644 index 0000000..fee3d23 --- /dev/null +++ b/public/src/js/soundbuffer.js @@ -0,0 +1,246 @@ +class SoundBuffer{ + constructor(...args){ + this.init(...args) + } + init(){ + var AudioContext = window.AudioContext || window.webkitAudioContext + this.context = new AudioContext() + this.audioDecoder = this.context.decodeAudioData.bind(this.context) + this.oggDecoder = this.audioDecoder + pageEvents.add(window, ["click", "touchend", "keypress"], this.pageClicked.bind(this)) + this.gainList = [] + } + load(file, gain){ + var decoder = file.name.endsWith(".ogg") ? this.oggDecoder : this.audioDecoder + return file.arrayBuffer().then(response => { + return new Promise((resolve, reject) => { + return decoder(response, resolve, reject) + }).catch(error => Promise.reject([error, file.url])) + }).then(buffer => { + return new Sound(gain || {soundBuffer: this}, buffer) + }) + } + createGain(channel){ + var gain = new SoundGain(this, channel) + this.gainList.push(gain) + return gain + } + setCrossfade(gain1, gain2, median){ + if(!Array.isArray(gain1)){ + gain1 = [gain1] + } + if(!Array.isArray(gain2)){ + gain2 = [gain2] + } + gain1.forEach(gain => gain.setCrossfade(1 - median)) + gain2.forEach(gain => gain.setCrossfade(median)) + } + getTime(){ + return this.context.currentTime + } + convertTime(time, absolute){ + time = (time || 0) + if(time < 0){ + time = 0 + } + return time + (absolute ? 0 : this.getTime()) + } + createSource(sound){ + var source = this.context.createBufferSource() + source.buffer = sound.buffer + source.connect(sound.gain.gainNode || this.context.destination) + return source + } + pageClicked(){ + if(this.context.state === "suspended"){ + this.context.resume() + } + } + saveSettings(){ + for(var i = 0; i < this.gainList.length; i++){ + var gain = this.gainList[i] + gain.defaultVol = gain.volume + } + } + loadSettings(){ + for(var i = 0; i < this.gainList.length; i++){ + var gain = this.gainList[i] + gain.setVolume(gain.defaultVol) + } + } + fallbackDecoder(buffer, resolve, reject){ + Oggmented().then(oggmented => oggmented.decodeOggData(buffer, resolve, reject), reject) + } +} +class SoundGain{ + constructor(...args){ + this.init(...args) + } + init(soundBuffer, channel){ + this.soundBuffer = soundBuffer + this.gainNode = soundBuffer.context.createGain() + if(channel){ + var index = channel === "left" ? 0 : 1 + this.merger = soundBuffer.context.createChannelMerger(2) + this.merger.connect(soundBuffer.context.destination) + this.gainNode.connect(this.merger, 0, index) + }else{ + this.gainNode.connect(soundBuffer.context.destination) + } + this.setVolume(1) + } + load(url){ + return this.soundBuffer.load(url, this) + } + convertTime(time, absolute){ + return this.soundBuffer.convertTime(time, absolute) + } + setVolume(amount){ + this.gainNode.gain.value = amount * amount + this.volume = amount + } + setVolumeMul(amount){ + this.setVolume(amount * this.defaultVol) + } + setCrossfade(amount){ + this.setVolume(Math.sqrt(Math.sin(Math.PI / 2 * amount))) + } + fadeIn(duration, time, absolute){ + this.fadeVolume(0, this.volume * this.volume, duration, time, absolute) + } + fadeOut(duration, time, absolute){ + this.fadeVolume(this.volume * this.volume, 0, duration, time, absolute) + } + fadeVolume(vol1, vol2, duration, time, absolute){ + time = this.convertTime(time, absolute) + this.gainNode.gain.linearRampToValueAtTime(vol1, time) + this.gainNode.gain.linearRampToValueAtTime(vol2, time + (duration || 0)) + } + mute(){ + this.gainNode.gain.value = 0 + } + unmute(){ + this.setVolume(this.volume) + } +} +class Sound{ + constructor(...args){ + this.init(...args) + } + init(gain, buffer){ + this.gain = gain + this.buffer = buffer + this.soundBuffer = gain.soundBuffer + this.duration = buffer.duration + this.timeouts = new Set() + this.sources = new Set() + } + copy(gain){ + return new Sound(gain || this.gain, this.buffer) + } + getTime(){ + return this.soundBuffer.getTime() + } + convertTime(time, absolute){ + return this.soundBuffer.convertTime(time, absolute) + } + setTimeouts(time){ + return new Promise(resolve => { + var relTime = time - this.getTime() + if(relTime > 0){ + var timeout = setTimeout(() => { + this.timeouts.delete(timeout) + resolve() + }, relTime * 1000) + this.timeouts.add(timeout) + }else{ + resolve() + } + }) + } + clearTimeouts(){ + this.timeouts.forEach(timeout => { + clearTimeout(timeout) + this.timeouts.delete(timeout) + }) + } + playLoop(time, absolute, seek1, seek2, until){ + time = this.convertTime(time, absolute) + seek1 = seek1 || 0 + if(typeof seek2 === "undefined"){ + seek2 = seek1 + } + until = until || this.duration + if(seek1 >= until || seek2 >= until){ + return + } + this.loop = { + started: time + until - seek1, + seek: seek2, + until: until + } + this.play(time, true, seek1, until) + this.addLoop() + this.loop.interval = setInterval(() => { + this.addLoop() + }, 100) + } + addLoop(){ + while(this.getTime() > this.loop.started - 1){ + this.play(this.loop.started, true, this.loop.seek, this.loop.until) + this.loop.started += this.loop.until - this.loop.seek + } + } + play(time, absolute, seek, until){ + time = this.convertTime(time, absolute) + var source = this.soundBuffer.createSource(this) + seek = seek || 0 + until = until || this.duration + this.setTimeouts(time).then(() => { + this.cfg = { + started: time, + seek: seek, + until: until + } + }) + source.start(time, Math.max(0, seek || 0), Math.max(0, until - seek)) + source.startTime = time + this.sources.add(source) + source.onended = () => { + this.sources.delete(source) + } + } + stop(time, absolute){ + time = this.convertTime(time, absolute) + this.sources.forEach(source => { + try{ + source.stop(Math.max(source.startTime, time)) + }catch(e){} + }) + this.setTimeouts(time).then(() => { + if(this.loop){ + clearInterval(this.loop.interval) + } + this.clearTimeouts() + }) + } + pause(time, absolute){ + if(this.cfg){ + time = this.convertTime(time, absolute) + this.stop(time, true) + this.cfg.pauseSeek = time - this.cfg.started + this.cfg.seek + } + } + resume(time, absolute){ + if(this.cfg){ + if(this.loop){ + this.playLoop(time, absolute, this.cfg.pauseSeek, this.loop.seek, this.loop.until) + }else{ + this.play(time, absolute, this.cfg.pauseSeek, this.cfg.until) + } + } + } + clean(){ + delete this.buffer + } +} diff --git a/public/src/js/strings.js b/public/src/js/strings.js new file mode 100644 index 0000000..6f0729e --- /dev/null +++ b/public/src/js/strings.js @@ -0,0 +1,1499 @@ +var languageList = ["ja", "en", "cn", "tw", "ko"] +var translations = { + name: { + ja: "日本語", + en: "English", + cn: "简体中文", + tw: "正體中文", + ko: "한국어" + }, + regex: { + ja: /^ja$|^ja-/, + en: /^en$|^en-/, + cn: /^zh$|^zh-CN$|^zh-SG$/, + tw: /^zh-HK$|^zh-TW$/, + ko: /^ko$|^ko-/ + }, + font: { + ja: "TnT, Meiryo, sans-serif", + en: "TnT, Meiryo, sans-serif", + cn: "Microsoft YaHei, sans-serif", + tw: "Microsoft YaHei, sans-serif", + ko: "Microsoft YaHei, sans-serif" + }, + intl: { + ja: "ja", + en: "en-GB", + cn: "zh-Hans", + tw: "zh-Hant", + ko: "ko" + }, + + taikoWeb: { + ja: "たいこウェブ", + en: "Taiko Web", + cn: "太鼓网页", + tw: "太鼓網頁", + ko: "태고 웹" + }, + titleProceed: { + ja: "クリックするかEnterを押す!", + en: "Click or Press Enter!", + cn: "点击或按回车!", + tw: "點擊或按確認!", + ko: "클릭하거나 Enter를 누르세요!" + }, + titleDisclaimer: { + ja: "この非公式シミュレーターはバンダイナムコとは関係がありません。", + en: "This unofficial simulator is unaffiliated with BANDAI NAMCO.", + cn: "这款非官方模拟器与BANDAI NAMCO无关。", + tw: "這款非官方模擬器與 BANDAI NAMCO 無關。", + ko: "이 비공식 시뮬레이터는 반다이 남코와 관련이 없습니다." + }, + titleCopyright: { + en: "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc." + }, + selectSong: { + ja: "曲をえらぶ", + en: "Select Song", + cn: "选择乐曲", + tw: "選擇樂曲", + ko: "곡 선택" + }, + selectDifficulty: { + ja: "むずかしさをえらぶ", + en: "Select Difficulty", + cn: "选择难度", + tw: "選擇難度", + ko: "난이도 선택" + }, + back: { + ja: "もどる", + en: "Back", + cn: "返回", + tw: "返回", + ko: "돌아가기" + }, + random: { + ja: "ランダム", + en: "Random", + cn: "随机", + tw: "隨機", + ko: "랜덤" + }, + randomSong: { + ja: "ランダムに曲をえらぶ", + en: "Random Song", + cn: "随机选曲", + tw: "隨機選曲", + ko: "랜덤" + }, + howToPlay: { + ja: "あそびかた説明", + en: "How to Play", + cn: "操作说明", + tw: "操作說明", + ko: "플레이 방법" + }, + aboutSimulator: { + ja: "このシミュレータについて", + en: "About Simulator", + cn: "关于模拟器", + tw: "關於模擬器", + ko: "시뮬레이터 정보" + }, + gameSettings: { + ja: "ゲーム設定", + en: "Game Settings", + cn: "游戏设定", + tw: "遊戲設定", + ko: "게임 설정" + }, + songOptions: { + ja: "演奏オプション", + en: "Song Options", + cn: "选项", + tw: "選項", + ko: "옵션" + }, + none: { + ja: "なし", + en: "None", + cn: "无", + tw: "無", + ko: "없음" + }, + auto: { + ja: "オート", + en: "Auto", + cn: "自动", + tw: "自動", + ko: "자동" + }, + netplay: { + ja: "ネットプレイ", + en: "Netplay", + cn: "网络对战", + tw: "網上對打", + ko: "온라인 대전" + }, + easy: { + ja: "かんたん", + en: "Easy", + cn: "简单", + tw: "簡單", + ko: "쉬움" + }, + normal: { + ja: "ふつう", + en: "Normal", + cn: "普通", + tw: "普通", + ko: "보통" + }, + hard: { + ja: "むずかしい", + en: "Hard", + cn: "困难", + tw: "困難", + ko: "어려움" + }, + oni: { + ja: "おに", + en: "Extreme", + cn: "魔王", + tw: "魔王", + ko: "귀신" + }, + songBranch: { + ja: "譜面分岐あり", + en: "Diverge Notes", + cn: "有谱面分歧", + tw: "有譜面分歧", + ko: "악보 분기 있음" + }, + defaultName: { + ja: "どんちゃん", + en: "Don-chan", + cn: "小咚", + tw: "小咚", + ko: "동이" + }, + default2PName: { + ja: "かっちゃん", + en: "Katsu-chan", + cn: "小咔", + tw: "小咔", + ko: "딱이" + }, + notLoggedIn: { + ja: "ログインしていない", + en: "Not logged in", + cn: "未登录", + tw: "未登錄", + ko: "로그인하지 않았습니다" + }, + sessionStart: { + ja: "オンラインセッションを開始する!", + en: "Begin an Online Session!", + cn: "开始在线会话!", + tw: "開始多人模式!", + ko: "온라인 세션 시작!" + }, + sessionEnd: { + ja: "オンラインセッションを終了する", + en: "End Online Session", + cn: "结束在线会话", + tw: "結束多人模式", + ko: "온라인 세션 끝내기" + }, + scoreSaveFailed: { + ja: "サーバーに接続できませんでした。スコアは保存されていません。\n\nログインするか、ページを更新して、再度スコアの保存をお試しください。", + en: "Could not connect to the server, your score has not been saved.\n\nPlease log in or refresh the page to try saving the score again.", + tw: "無法連接至伺服器,你的成績未能儲存。若要儲存成績,請登入或重新載入頁面。", + ko: "서버에 연결되지 않아 점수가 저장되지 않았습니다.\n\n로그인하거나 페이지를 새로고침하여 점수를 다시 저장할 수 있습니다." + }, + loadSongError: { + ja: "曲「%s」を読み込むことができませんでした。(ID:%s)\n\n%s", + en: "Could not load song %s with ID %s.\n\n%s", + ko: "곡 %s (ID:%s)를 로드할 수 없습니다.\n\n%s" + }, + accessNotGrantedError: { + ja: "ファイルへのアクセス権が拒否されました", + en: "Permission to access the file was not granted", + ko: "파일에 접근할 수 있는 권한이 부여되지 않았습니다." + }, + loading: { + ja: "ロード中...", + en: "Loading...", + cn: "加载中...", + tw: "讀取中...", + ko: "로딩 중..." + }, + waitingForP2: { + ja: "他のプレイヤーを待っている...", + en: "Waiting for Another Player...", + cn: "正在等待对方玩家...", + tw: "正在等待對方玩家...", + ko: "다른 플레이어 대기 중..." + }, + cancel: { + ja: "キャンセル", + en: "Cancel", + cn: "取消", + tw: "取消", + ko: "취소" + }, + note: { + don: { + ja: "ドン", + en: "Don", + cn: "咚", + tw: "咚", + ko: "쿵" + }, + ka: { + ja: "カッ", + en: "Ka", + cn: "咔", + tw: "咔", + ko: "딱" + }, + daiDon: { + ja: "ドン(大)", + en: "DON", + cn: "咚(大)", + tw: "咚(大)", + ko: "쿵(대)" + }, + daiKa: { + ja: "カッ(大)", + en: "KA", + cn: "咔(大)", + tw: "咔(大)", + ko: "딱(대)" + }, + drumroll: { + ja: "連打ーっ!!", + en: "Drum rollー!!", + cn: "连打ー!!", + tw: "連打ー!!", + ko: "연타ー!!" + }, + daiDrumroll: { + ja: "連打(大)ーっ!!", + en: "DRUM ROLLー!!", + cn: "连打(大)ー!!", + tw: "連打(大)ー!!", + ko: "연타(대)ー!!" + }, + balloon: { + ja: "ふうせん", + en: "Balloon", + cn: "气球", + tw: "氣球", + ko: "풍선" + }, + }, + ex_note: { + don: { + ja: ["ド", "コ"], + en: ["Do", "Do"], + cn: ["咚", "咚"], + tw: ["咚", "咚"], + ko: ["쿠", "쿠"] + }, + ka: { + ja: ["カ"], + en: ["Ka"], + cn: ["咔"], + tw: ["咔"], + ko: ["딱"] + }, + daiDon: { + ja: ["ドン(大)", "ドン(大)"], + en: ["DON", "DON"], + cn: ["咚(大)", "咚(大)"], + tw: ["咚(大)", "咚(大)"], + ko: ["쿵(대)", "쿵(대)"] + }, + daiKa: { + ja: ["カッ(大)"], + en: ["KA"], + cn: ["咔(大)"], + tw: ["咔(大)"], + ko: ["딱(대)"] + }, + }, + combo: { + ja: "コンボ", + en: "Combo", + cn: "连段", + tw: "連段", + ko: "콤보" + }, + clear: { + ja: "クリア", + en: "Clear", + cn: "通关", + tw: "通關", + ko: "클리어" + }, + good: { + ja: "良", + en: "GOOD", + cn: "良", + tw: "良", + ko: "얼쑤" + }, + ok: { + ja: "可", + en: "OK", + cn: "可", + tw: "可", + ko: "좋다" + }, + bad: { + ja: "不可", + en: "BAD", + cn: "不可", + tw: "不可", + ko: "에구" + }, + branch: { + normal: { + ja: "普通譜面", + en: "Normal", + cn: "一般谱面", + tw: "一般譜面", + ko: "보통 악보" + }, + advanced: { + ja: "玄人譜面", + en: "Professional", + cn: "进阶谱面", + tw: "進階譜面", + ko: "현인 악보" + }, + master: { + ja: "達人譜面", + en: "Master", + cn: "达人谱面", + tw: "達人譜面", + ko: "달인 악보" + } + }, + pauseOptions: { + ja: [ + "演奏をつづける", + "はじめからやりなおす", + "「曲をえらぶ」にもどる" + ], + en: [ + "Continue", + "Retry", + "Back to Select Song" + ], + cn: [ + "继续演奏", + "从头开始", + "返回「选择乐曲」" + ], + tw: [ + "繼續演奏", + "從頭開始", + "返回「選擇樂曲」" + ], + ko: [ + "연주 계속하기", + "처음부터 다시", + "「곡 선택」으로" + ] + }, + results: { + ja: "成績発表", + en: "Results", + cn: "发表成绩", + tw: "發表成績", + ko: "성적 발표" + }, + points: { + ja: "点", + en: "pts", + cn: "点", + tw: "分", + ko: "점" + }, + maxCombo: { + ja: "最大コンボ数", + en: "MAX Combo", + cn: "最多连段数", + tw: "最多連段數", + ko: "최대 콤보 수" + }, + drumroll: { + ja: "連打数", + en: "Drumroll", + cn: "连打数", + tw: "連打數", + ko: "연타 횟수" + }, + + errorOccured: { + ja: "エラーが発生しました。再読み込みしてください。", + en: "An error occurred, please refresh", + tw: "發生錯誤,請重新載入頁面。", + ko: "오류가 발생했습니다. 페이지를 새로 고침하시기 바랍니다." + }, + tutorial: { + basics: { + ja: [ + "流れてくる音符がワクに重なったらバチで太鼓をたたこう!", + "赤い音符は面をたたこう(%sまたは%s)", + "青い音符はフチをたたこう(%sまたは%s)", + "USBコントローラがサポートされています!" + ], + en: [ + "When a note overlaps the frame, that is your cue to hit the drum!", + "For red notes, hit the surface of the drum (%s or %s)...", + "...and for blue notes, hit the rim! (%s or %s)", + "USB controllers are also supported!" + ], + cn: [ + "当流动的音符将与框框重叠时就用鼓棒敲打太鼓吧", + "遇到红色音符要敲打鼓面(%s或%s)", + "遇到蓝色音符则敲打鼓边(%s或%s)", + "USB控制器也支持!" + ], + tw: [ + "當流動的音符將與框框重疊時就用鼓棒敲打太鼓吧", + "遇到紅色音符要敲打鼓面(%s或%s)", + "遇到藍色音符則敲打鼓邊(%s或%s)", + "USB控制器也支持!" + ], + ko: [ + "이동하는 음표가 테두리와 겹쳐졌을 때 북채로 태고를 두드리자!", + "빨간 음표는 면을 두드리자 (%s 또는 %s)", + "파란 음표는 테를 두드리자 (%s 또는 %s)", + "USB 컨트롤러도 지원됩니다!" + ], + }, + otherControls: { + ja: "他のコントロール", + en: "Other controls", + cn: "其他控制", + tw: "其他控制", + ko: "기타 컨트롤", + }, + otherTutorial: { + ja: [ + "%sはゲームを一時停止します", + "曲をえらびながら%sか%sキーを押してジャンルをスキップします", + "むずかしさをえらびながら%sキーを押してオートモードを有効", + "むずかしさをえらびながら%sキーを押してネットプレイモードを有効" + ], + en: [ + "%s \u2014 pause game", + '%s and %s while selecting song \u2014 navigate categories', + "%s while selecting difficulty \u2014 enable autoplay mode", + "%s while selecting difficulty \u2014 enable 2P mode" + ], + cn: [ + "%s暂停游戏", + '%s 和 %s 选择歌曲时快速切换类别', + "选择难度时按住%s以启用自动模式", + "选择难度时按住%s以启用网络对战模式" + ], + tw: [ + "%s暫停遊戲", + '選擇歌曲時,按下 %s 或 %s 以快速切換類別', + "選擇難度時,按住 %s 以啟用自動模式", + "選擇難度時,按住 %s 以啟用網上對打模式" + ], + ko: [ + "%s \u2014 게임을 일시 중지합니다", + '곡 선택 중 %s 또는 %s \u2014 카테고리 이동', + "난이도 선택 동안 %s 홀드 \u2014 자동 모드 활성화", + "난이도 선택 동안 %s 홀드 \u2014 온라인 대전 모드 활성화" + ], + }, + ok: { + ja: "OK", + en: "OK", + cn: "确定", + tw: "確定", + ko: "확인" + }, + key: { + ctrl: { + en: "CTRL" + }, + shift: { + en: "⇧ SHIFT" + }, + leftArrow: { + en: "\u2190" + }, + rightArrow: { + en: "\u2192" + }, + esc: { + en: "ESC" + }, + join: { + en: "+" + }, + or: { + ja: "または", + en: " or ", + cn: "或", + tw: "或", + ko: " 또는 " + } + } + }, + about: { + bugReporting: { + ja: [ + "このシミュレータは現在開発中です。", + "バグが発生した場合は、報告してください。", + "Gitリポジトリかメールでバグを報告してください。" + ], + en: [ + "This simulator is still in development.", + "Please report any bugs you find.", + "You can report bugs either via our Git repository or email." + ], + cn: [ + "这款模拟器仍处于开发中,", + "您可以向我们报告在游戏中出现的任何bug,", + "可以通过我们的Github仓库或发送电子邮件来报告错误。" + ], + tw: [ + "此模擬器仍處於開發階段,", + "請回報任何你遇到的 bug。", + "你可以透過 Github 或電子郵件回報。" + ], + ko: [ + "이 시뮬레이터는 아직 개발 중입니다.", + "버그를 찾으시면 신고해주시기 바랍니다.", + "Github 레포지터리나 이메일을 통해 버그를 신고하실 수 있습니다." + ] + }, + diagnosticWarning: { + ja: "以下の端末診断情報も併せて報告してください!", + en: "Be sure to include the following diagnostic data!", + cn: "请确保您的报告包括以下诊断数据!", + tw: "記得附上下方的診斷資料!", + ko: "신고하실 때 반드시 다음 진단 정보를 포함해주시기 바랍니다!" + }, + issueTemplate: { + ja: "###### 下記の問題を説明してください。 スクリーンショットと診断情報を含めてください。", + en: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.", + tw: "###### 在下方說明您遇到的問題。請一併傳送截圖及診斷資料。", + ko: "###### 겪고 있는 문제를 아래에 적어주시기 바랍니다. 스크린샷과 진단 정보가 포함되어야 합니다." + }, + issues: { + ja: "課題", + en: "Issues", + cn: "工单", + tw: "問題", + ko: "이슈" + } + }, + session: { + multiplayerSession: { + ja: "オンラインセッション", + en: "Multiplayer Session", + cn: "在线会话", + tw: "多人模式", + ko: "멀티플레이어 세션" + }, + linkTutorial: { + ja: "このリンクをお友達とシェアして、一緒にプレイを始めて。相手が参加するまで、この画面を離れないでください。", + en: "Share this link with your friend to start playing together! Do not leave this screen while they join.", + cn: "复制下方地址,给你的朋友即可开始一起游戏!当他们与您联系之前,请不要离开此页面。", + tw: "分享下方網址給你的朋友即可開始一起遊戲!在他們加入時,請不要離開此頁面。", + ko: "링크를 공유하여 친구와 플레이하세요! 친구가 입장하기 전에 페이지를 나가지 말아주세요." + }, + cancel: { + ja: "キャンセル", + en: "Cancel", + cn: "取消", + tw: "取消", + ko: "취소" + } + }, + settings: { + language: { + name: { + ja: "言語", + en: "Language", + cn: "语言", + tw: "語系", + ko: "언어" + } + }, + resolution: { + name: { + ja: "ゲームの解像度", + en: "Game Resolution", + cn: "游戏分辨率", + tw: "遊戲解析度", + ko: "게임 해상도" + }, + high: { + ja: "高", + en: "High", + cn: "高", + tw: "高", + ko: "높음" + }, + medium: { + ja: "中", + en: "Medium", + cn: "中", + tw: "中", + ko: "중간" + }, + low: { + ja: "低", + en: "Low", + cn: "低", + tw: "低", + ko: "낮음" + }, + lowest: { + ja: "最低", + en: "Lowest", + cn: "最低", + tw: "最低", + ko: "매우 낮음" + } + }, + touchAnimation: { + name: { + ja: "タッチアニメーション", + en: "Touch Animation", + cn: "触摸动画", + tw: "觸摸動畫", + ko: "터치 애니메이션" + } + }, + keyboardSettings: { + name: { + ja: "キーボード設定", + en: "Keyboard Settings", + cn: "键盘设置", + tw: "鍵盤設置", + ko: "키보드 설정" + }, + ka_l: { + ja: "ふち(左)", + en: "Left Rim", + cn: "边缘(左)", + tw: "邊緣(左)", + ko: "가장자리 (왼쪽)" + }, + don_l: { + ja: "面(左)", + en: "Left Surface", + cn: "表面(左)", + tw: "鼓面(左)", + ko: "북 면 (왼쪽)" + }, + don_r: { + ja: "面(右)", + en: "Right Surface", + cn: "表面(右)", + tw: "鼓面(右)", + ko: "북 면 (오른쪽)" + }, + ka_r: { + ja: "ふち(右)", + en: "Right Rim", + cn: "边缘(右)", + tw: "邊緣(右)", + ko: "가장자리 (오른쪽)" + } + }, + gamepadLayout: { + name: { + ja: "そうさタイプ設定", + en: "Gamepad Layout", + cn: "操作类型设定", + tw: "操作類型設定", + ko: "조작 타입 설정" + }, + a: { + ja: "タイプA", + en: "Type A", + cn: "类型A", + tw: "類型 A", + ko: "타입 A" + }, + b: { + ja: "タイプB", + en: "Type B", + cn: "类型B", + tw: "類型 B", + ko: "타입 B" + }, + c: { + ja: "タイプC", + en: "Type C", + cn: "类型C", + tw: "類型 C", + ko: "타입 C" + } + }, + latency: { + name: { + ja: "レイテンシー", + en: "Latency", + cn: "延迟校准", + tw: "延遲校準", + ko: "레이턴시" + }, + value: { + ja: "オーディオ: %s, ビデオ: %s", + en: "Audio: %s, Video: %s", + cn: "音频: %s, 视频: %s", + tw: "聲音: %s, 畫面: %s", + ko: "오디오: %s, 비디오: %s" + }, + calibration: { + ja: "レイテンシー較正", + en: "Latency Calibration", + cn: "自动辅助校准", + tw: "自動延遲校正", + ko: "레이턴시 조절" + }, + audio: { + ja: "オーディオ", + en: "Audio", + cn: "音频", + tw: "聲音", + ko: "오디오" + }, + video: { + ja: "ビデオ", + en: "Video", + cn: "视频", + tw: "畫面", + ko: "비디오" + }, + drumSounds: { + ja: "太鼓の音", + en: "Drum Sounds", + cn: "鼓声", + tw: "鼓聲", + ko: "북 소리" + } + }, + easierBigNotes: { + name: { + ja: "簡単な大きな音符", + en: "Easier Big Notes", + cn: "简单的大音符", + tw: "簡單的大音符", + ko: "쉬운 큰 음표" + } + }, + showLyrics: { + name: { + ja: "歌詞の表示", + en: "Show Lyrics", + cn: "显示歌词", + tw: "顯示歌詞", + ko: "가사 표시하기" + } + }, + on: { + ja: "オン", + en: "On", + cn: "开", + tw: "開", + ko: "켜짐" + }, + off: { + ja: "オフ", + en: "Off", + cn: "关", + tw: "關", + ko: "꺼짐" + }, + default: { + ja: "既定値にリセット", + en: "Reset to Defaults", + cn: "重置为默认值", + tw: "重置為預設值", + ko: "기본값으로 재설정" + }, + ok: { + ja: "OK", + en: "OK", + cn: "确定", + tw: "確定", + ko: "확인" + } + }, + calibration: { + title: { + ja: "レイテンシー・キャリブレーション", + en: "Latency Calibration", + tw: "延遲校正", + ko: "레이턴시 조절" + }, + ms: { + ja: "%sms", + en: "%sms", + }, + back: { + ja: "「ゲーム設定」に戻る", + en: "Back to Settings", + tw: "返回設定", + ko: "설정으로 돌아가기" + }, + retryPrevious: { + ja: "前回のリトライ", + en: "Retry Previous", + tw: "重試", + ko: "재시도" + }, + start: { + ja: "スタート", + en: "Start", + tw: "開始", + ko: "시작" + }, + finish: { + ja: "終了する", + en: "Finish", + tw: "完成", + ko: "완료" + }, + audioHelp: { + title: { + ja: "オーディオ・レイテンシー・キャリブレーション", + en: "Audio Latency Calibration", + tw: "聲音延遲校正", + ko: "오디오 레이턴시 조절" + + }, + content: { + ja: "背景で鳴っている音を聴いてみましょう。\n\n音が聞こえたら、太鼓の面(%sまたは%s)をたたこう!", + en: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", + tw: "仔細聆聽背景播放的音效。\n\n聽到音效就敲打鼓面 (%s 或 %s)!", + ko: "배경에서 들리는 소리에 귀를 기울여주세요.\n\n소리가 들리면 북(%s 나 %s)을 쳐주세요!" + }, + contentAlt: { + ja: "背景で鳴っている音を聴いてみましょう。\n\n音が聞こえたら、太鼓の面をたたこう!", + en: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!", + tw: "仔細聆聽背景播放的音效。\n\n聽到音效就敲打鼓面!", + ko: "배경에서 들리는 소리에 귀를 기울여주세요.\n\n소리가 들리면 북을 쳐주세요!" + } + }, + audioComplete: { + ja: "オーディオ・レイテンシー・キャリブレーション完了!", + en: "Audio Latency Calibration completed!", + tw: "聲音延遲校正完成!", + ko: "오디오 레이턴시 조절 완료!" + }, + videoHelp: { + title: { + ja: "ビデオ・レイテンシー・キャリブレーション", + en: "Video Latency Calibration", + tw: "畫面延遲校正", + ko: "비디오 레이턴시 조절" + }, + content: { + ja: "今回は音が出ません。\n\n代わりに、丸い枠の中で音符が点滅しているのを見て、音符が現れたら太鼓をたたこう!", + en: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!", + tw: "這次不會有音效。\n\n請注意正在閃爍音符的圓形框框,當音符出現時就敲打鼓面!", + ko: "이번에는 소리가 나오지 않습니다.\n\n대신 원 안에서 깜빡이는 음표에 맞춰 북을 쳐주세요!" + } + }, + videoComplete: { + ja: "ビデオ・レイテンシー・キャリブレーション完了!", + en: "Video Latency Calibration completed!", + tw: "畫面延遲校正完成!", + ko: "비디오 레이턴시 조절 완료!" + }, + results: { + title: { + ja: "レイテンシー・キャリブレーション結果", + en: "Latency Calibration Results", + tw: "延遲校正結果", + ko: "레이턴시 조절 결과" + }, + content: { + ja: "オーディオ・レイテンシー: %s\nビデオ・レイテンシ: %s\n\nこれらのレイテンシーの値は、設定で設定することができます。", + en: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings.", + tw: "聲音延遲 :%s\n畫面延遲: %s\n\n您可以在設定中調整這些數值。", + ko: "오디오 레이턴시: %s\n비디오 레이턴시: %s\n\n설정에서 레이턴시 값들을 조절할 수 있습니다." + } + } + }, + account: { + username: { + ja: "ユーザー名", + en: "Username", + cn: "登录名", + tw: "使用者名稱", + ko: "사용자 이름" + }, + enterUsername: { + ja: "ユーザー名を入力", + en: "Enter Username", + cn: "输入用户名", + tw: "輸入用戶名", + ko: "사용자 이름을 입력하십시오" + }, + password: { + ja: "パスワード", + en: "Password", + cn: "密码", + tw: "密碼", + ko: "비밀번호" + }, + enterPassword: { + ja: "パスワードを入力", + en: "Enter Password", + cn: "输入密码", + tw: "輸入密碼", + ko: "비밀번호 입력" + }, + repeatPassword: { + ja: "パスワードを再入力", + en: "Repeat Password", + cn: "重新输入密码", + tw: "再次輸入密碼", + ko: "비밀번호 재입력" + }, + remember: { + ja: "ログイン状態を保持する", + en: "Remember me", + cn: "记住登录", + tw: "記住我", + ko: "자동 로그인" + }, + login: { + ja: "ログイン", + en: "Log In", + cn: "登录", + tw: "登入", + ko: "로그인" + }, + register: { + ja: "登録", + en: "Register", + cn: "注册", + tw: "註冊", + ko: "가입하기" + }, + privacy: { + ja: "プライバシー", + en: "Privacy", + cn: "隐私权", + tw: "隱私權", + ko: "개인정보처리방침" + }, + registerAccount: { + ja: "アカウントを登録", + en: "Register account", + cn: "注册帐号", + tw: "註冊帳號", + ko: "계정 등록" + }, + passwordsDoNotMatch: { + ja: "パスワードが一致しません", + en: "Passwords do not match", + cn: "密码不匹配", + tw: "密碼不符合", + ko: "비밀번호가 일치하지 않습니다" + }, + newPasswordsDoNotMatch: { + ja: "新しいパスワードが一致しない", + en: "New passwords do not match", + tw: "新密碼不符合", + ko: "새 비밀번호가 일치하지 않습니다" + }, + cannotBeEmpty: { + ja: "%sは空にできません", + en: "%s cannot be empty", + cn: "%s不能为空", + tw: "%s不能為空", + ko: "%s는 비어 있을 수 없습니다" + }, + error: { + ja: "リクエストの処理中にエラーが発生しました", + en: "An error occurred while processing your request", + cn: "处理您的请求时发生错误", + tw: "處理您的請求時發生錯誤", + ko: "요청을 처리하는 동안 오류가 발생했습니다" + }, + logout: { + ja: "ログアウト", + en: "Log Out", + cn: "登出", + tw: "登出", + ko: "로그아웃" + }, + back: { + ja: "もどる", + en: "Back", + cn: "返回", + tw: "返回", + ko: "돌아가기" + }, + cancel: { + ja: "キャンセル", + en: "Cancel", + tw: "取消", + ko: "취소" + }, + save: { + ja: "保存する", + en: "Save", + tw: "儲存", + ko: "저장" + }, + displayName: { + ja: "表示名", + en: "Displayed Name", + ko: "닉네임" + }, + customdon: { + bodyFill: { + ja: "どう", + en: "Body", + tw: "身體", + ko: "몸" + }, + faceFill: { + ja: "かお", + en: "Face", + tw: "臉", + ko: "얼굴" + }, + reset: { + ja: "リセット", + en: "Reset", + tw: "重設", + ko: "초기화" + } + }, + changePassword: { + ja: "パスワードの変更", + en: "Change Password", + tw: "更改密碼", + ko: "비밀번호 변경" + }, + currentNewRepeat: { + ja: [ + "現在のパスワード", + "新しいパスワード", + "新しいパスワードの繰り返し" + ], + en: [ + "Current Password", + "New Password", + "Repeat New Password" + ], + tw: [ + "目前密碼", + "新密碼", + "重複新密碼" + ], + ko: [ + "현재 비밀번호", + "새 비밀번호", + "새 비밀번호 재입력" + ], + }, + deleteAccount: { + ja: "アカウント削除", + en: "Delete Account", + tw: "刪除帳號", + ko: "계정 삭제" + }, + verifyPassword: { + ja: "削除するパスワードの確認", + en: "Verify password to delete this account", + tw: "請確認您的密碼以刪除帳號", + ko: "계정을 삭제하기 위해 비밀번호를 인증해주세요" + } + }, + serverError: { + not_logged_in: { + ja: "ログインしていない", + en: "Not logged in", + tw: "未登入", + ko: "로그인되지 않음" + }, + invalid_username: { + ja: "ユーザー名が無効です。ユーザー名には、文字、数字、アンダースコアのみを使用でき、3文字以上20文字以下である必要があります。", + en: "Invalid username, a username can only contain letters, numbers, and underscores, and must be between 3 and 20 characters long", + tw: "使用者名稱無效,使用者名稱只能由字母、數字、及底線組成,且長度必須介於 3 到 20 個字之間", + ko: "유효하지 않은 아이디. 아이디에는 글자, 숫자, 밑줄만 들어갈 수 있으며, 길이는 3자와 20자 사이여야 합니다." + }, + username_in_use: { + ja: "そのユーザ名で既にユーザが存在する", + en: "A user already exists with that username", + tw: "已有一位相同名稱的使用者", + ko: "해당 아이디는 이미 다른 계정에서 사용하고 있습니다." + }, + invalid_password: { + ja: "このパスワードは使用できません。パスワードの長さが6文字以上であることを確認してください", + en: "Cannot use this password, please check that your password is at least 6 characters long", + tw: "無法使用此密碼,密碼長度至少要 6 個字。", + ko: "해당 비밀번호를 사용할 수 없습니다. 비밀번호가 6자 이상인지 확인하시기 바랍니다." + }, + invalid_username_password: { + ja: "ユーザー名またはパスワードが無効", + en: "Invalid Username or Password", + tw: "無效的使用者名稱或密碼", + ko: "잘못된 아이디 혹은 비밀번호" + }, + invalid_display_name: { + ja: "この名前は使用できません。新しい名前が25文字以下であることを確認してください", + en: "Cannot use this name, please check that your new name is at most 25 characters long", + tw: "無法使用此名稱。名稱最多不能超過 25 個字。", + ko: "해당 닉네임을 사용할 수 없습니다. 닉네임의 길이가 25자 미만인지 확인하시기 바랍니다." + }, + invalid_don: { + ja: "マイどんを保存できませんでした", + en: "Could not save your custom Don", + tw: "無法儲存小咚", + ko: "커스텀 동이를 저장할 수 없습니다" + }, + current_password_invalid: { + ja: "現在のパスワードが一致しません", + en: "Current password does not match", + tw: "目前密碼不符合", + ko: "기존 비밀번호가 틀립니다." + }, + invalid_new_password: { + ja: "このパスワードは使用できません。新しいパスワードが6文字以上であることを確認してください", + en: "Cannot use this password, please check that your new password is at least 6 characters long", + tw: "無法使用此密碼,您的新密碼長度至少要 6 個字", + ko: "해당 비밀번호를 사용할 수 없습니다. 비밀번호가 6자 이상인지 확인해주시기 바랍니다." + }, + verify_password_invalid: { + ja: "確認用パスワードが一致しない", + en: "Verification password does not match", + tw: "確認密碼不符合", + ko: "비밀번호가 일치하지 않습니다." + }, + invalid_csrf: { + ja: "セキュリティトークンの期限が切れました。ページを更新してください。", + en: "Security token expired. Please refresh the page.", + tw: "安全權杖過期。請重新載入頁面。", + ko: "보안 토큰이 만료되었습니다. 페이지를 새로고침해주세요." + } + }, + browserSupport: { + browserWarning: { + ja: "サポートされていないブラウザを実行しています (%s)", + en: "You are running an unsupported browser (%s)", + tw: "您正在使用不支援的瀏覽器 (%s)", + ko: "호환되지 않는 브라우저입니다. (%s)" + }, + details: { + ja: "詳しく", + en: "Details...", + tw: "詳細資料", + ko: "세부 사항" + }, + failedTests: { + ja: "このテストは失敗しました:", + en: "The following tests have failed:", + tw: "以下的測試未通過:", + ko: "다음 테스트에서 실패했습니다:" + }, + supportedBrowser: { + ja: "%sなどのサポートされているブラウザを使用してください", + en: "Please use a supported browser such as %s", + tw: "請使用支援的瀏覽器,例如:%s", + ko: "%s 등의 호환되는 브라우저를 사용해주시기 바랍니다" + } + }, + creative: { + creative: { + ja: "創作", + en: "Creative", + cn: "创作", + tw: "創作", + ko: "창작" + }, + maker: { + ja: "メーカー", + en: "Maker:", + cn: "制作者", + tw: "製作者", + ko: "제작자:" + } + }, + withLyrics: { + ja: "歌詞あり", + en: "With lyrics", + cn: "带歌词", + tw: "帶歌詞", + ko: "가사 포함됨" + }, + customSongs: { + title: { + ja: "カスタム曲リスト", + en: "Custom Song List", + cn: "自定义歌曲列表", + tw: "自定義歌曲列表", + ko: "커스텀 노래 목록" + }, + default: { + ja: "デフォルト曲リスト", + en: "Default Song List", + cn: "默认歌曲列表", + tw: "預設歌曲列表", + ko: "기본 노래 목록" + }, + description: { + ja: [ + "TJA形式の太鼓譜面ファイルが入ったフォルダを選んで、カスタム曲リストで演奏しよう!" + ], + en: [ + "Pick a folder with Taiko chart files in TJA format to play on a custom song list!" + ], + cn: [ + "请选择一个含有太鼓谱面文件(TJA格式)的文件夹,以用于在自定义歌单中游玩。" + ], + tw: [ + "請選擇包含太鼓譜面檔案 (TJA格式) 的資料夾,即可在自訂歌單中遊玩。" + ], + ko: [ + "TJA 확장자로 되어 있는 채보가 포함되어 있는 폴더를 골라 커스텀 곡을 플레이하세요!" + ] + }, + localFolder: { + ja: "ローカルフォルダ...", + en: "Local Folder...", + cn: "本地文件夹...", + tw: "本機資料夾...", + ko: "로컬 폴더..." + }, + gdriveFolder: { + ja: "Google ドライブ...", + en: "Google Drive...", + cn: "Google云端硬盘...", + tw: "Google 雲端硬碟...", + ko: "구글 드라이브..." + }, + gdriveAccount: { + ja: "アカウントの切り替え", + en: "Switch Accounts", + cn: "切换帐户", + tw: "切換帳戶", + ko: "계정 전환" + }, + dropzone: { + ja: "ここにファイルをドロップ", + en: "Drop files here", + cn: "将文件拖至此处", + tw: "將文件拖至此處", + ko: "파일을 여기에 드롭하세요" + }, + importError: { + ja: "インポートエラー", + en: "Import Error", + tw: "匯入失敗", + ko: "불러오기 오류" + }, + noSongs: { + ja: "提供されたフォルダーに太鼓譜面ファイルは見つかりませんでした。", + en: "No Taiko chart files have been found in the provided folder.", + tw: "在你選擇的資料夾中找不到譜面檔案。", + ko: "해당 폴더에서 채보 파일을 찾을 수 없습니다." + } + }, + gpicker: { + locale: { + ja: "ja", + en: "en-GB", + cn: "zh-CN", + tw: "zh-TW", + ko: "ko" + }, + myDrive: { + ja: "マイドライブ", + en: "My Drive", + cn: "我的云端硬盘", + tw: "我的雲端硬碟", + ko: "내 드라이브" + }, + starred: { + ja: "スター付き", + en: "Starred", + cn: "已加星标", + tw: "已加星號", + ko: "중요 문서함" + }, + sharedWithMe: { + ja: "共有アイテム", + en: "Shared with me", + cn: "与我共享", + tw: "與我共用", + ko: "공유 문서함" + }, + authError: { + ja: "認証エラー: %s", + en: "Auth error: %s", + tw: "驗證錯誤:%s", + ko: "인증 오류: %s" + }, + cookieError: { + ja: "この機能には、サードパーティのクッキーが必要です。", + en: "This function requires third party cookies.", + tw: "此功能需要第三方 cookies。", + ko: "이 기능은 제3자 쿠키가 허용되어야 합니다." + } + }, + plugins: { + title: { + ja: "プラグイン", + en: "Plugins", + ko: "플러그인" + }, + unloadAll: { + ja: "すべて無効にする", + en: "Unload All", + ko: "모두 해제" + }, + warning: { + ja: "%sを読み込もうとしています。プラグインは信頼できる場合のみ読み込むようにしてください。続行しますか?", + en: "You are about to load %s. Plugins should only be loaded if you trust them. Continue?", + ko: "%s을 로드하려고 합니다. 신뢰할 수 있는 플러그인만 로드하시기 바랍니다. 계속할까요?" + }, + plugin: { + ja: { + one: "%sつのプラグイン", + other: "%sつのプラグイン" + }, + en: { + one: "%s plugin", + other: "%s plugins" + }, + ko: { + one: "%s 플러그인", + other: "%s 플러그인들" + } + }, + author: { + ja: "作成者:%s", + en: "By %s", + ko: "제작자:%s" + }, + version: { + ja: "Ver. %s", + en: "Version %s", + ko: "버전 %s" + }, + browse: { + ja: "参照する…", + en: "Browse...", + cn: "浏览…", + tw: "開啟檔案…", + ko: "찾아보기…" + }, + noPlugins: { + ja: null, + en: "No .taikoweb.js plugin files have been found in the provided file list.", + ko: "주어진 파일 리스트에서 .taikoweb.js 플러그인 파일들을 발견할 수 없습니다." + } + }, + search: { + search: { + ja: "曲を検索", + en: "Search Songs", + ko: "노래 검색" + }, + searchInput: { + ja: "曲を検索...", + en: "Search for songs...", + ko: "곡 검색..." + }, + noResults: { + ja: "結果は見つかりませんでした。", + en: "No results found.", + ko: "결과 없음" + }, + tip: { + ja: "ヒント:", + en: "Tip:", + ko: "팁:" + }, + tips: { + ja: [ + "CTRL+Fで検索窓を開く!", + "検索フィルタの組み合わせは自由自在です!", + "キーワードでジャンルを絞り込めます!(例: \"genre:variety\", \"genre:namco\")", + "「oni:10」などのフィルターを使用して、特定の難易度の曲を検索して!", + "Difficulty filters support ranges, too! Try \"ura:1-5\"!", + "Want to see your full combos? Try \"gold:any\", \"gold:oni\", etc.!", + "Only want to see creative songs? Use the \"creative:yes\" filter!", + "Find songs with lyrics enabled with the \"lyrics:yes\" filter!", + "Feel like trying something new? Use the \"played:no\" filter to only see songs you haven't played yet!", + "Looking for creative courses from a specific creator? Use the \"maker:\" filter!", + ], + en: [ + "Open the search window by pressing CTRL+F!", + "Mix and match as many search filters as you want!", + "Filter by genre by using the \"genre:\" keyword! (e.g. \"genre:variety\", \"genre:namco\")", + "Use filters like \"oni:10\" to search for songs with a particular difficulty!", + "Difficulty filters support ranges, too! Try \"ura:1-5\"!", + "Want to see your full combos? Try \"gold:any\", \"gold:oni\", etc.!", + "Only want to see creative songs? Use the \"creative:yes\" filter!", + "Find songs with lyrics enabled with the \"lyrics:yes\" filter!", + "Feel like trying something new? Use the \"played:no\" filter to only see songs you haven't played yet!", + "Looking for creative courses from a specific creator? Use the \"maker:\" filter!" + ], + ko: [ + "CTRL+F를 눌러 검색 창을 여세요!", + "자유롭게 필터를 조합해 검색하세요!", + "\"genre:\" 키워드로 원하는 장르의 곡만 찾아보세요! (예시: \"genre:variety\", \"genre:namco\")", + "\"oni:10\" 같은 키워드로 원하는 난이도를 가진 곡을 찾아보세요!", + "\"ura:1-5\" 같은 키워드로 여러 난이도를 선택할 수 있어요!", + "풀 콤보한 곡을 찾아보고 싶나요? \"gold:any\", \"gold:oni\" 등의 키워드로 검색할 수 있습니다!", + "창작 채보 곡들을 검색하고 싶나요? \"creative:yes\" 키워드를 사용하세요!", + "\"lyrics:yes\" 키워드로 가사가 있는 곡들을 선택할 수 있어요!", + "새로운 곡들을 플레이해보고 싶나요? \"played:no\" 키워드로 아직 플레이하지 않은 곡들만을 볼 수 있어요!", + "특정 창작자가 만든 채보를 검색하고 싶으신가요? \"maker:<창작자 이름>\" 키워드를 사용하세요!" + ] + } + } +} +var allStrings = {} +function separateStrings(){ + for(var j in languageList){ + var lang = languageList[j] + allStrings[lang] = { + id: lang + } + var str = allStrings[lang] + var translateObj = function(obj, name, str){ + if("en" in obj){ + for(var i in obj){ + str[name] = obj[lang] || obj.en + } + }else if(obj){ + str[name] = {} + for(var i in obj){ + translateObj(obj[i], i, str[name]) + } + } + } + for(var i in translations){ + translateObj(translations[i], i, str) + } + } +} +separateStrings() diff --git a/public/src/js/titlescreen.js b/public/src/js/titlescreen.js new file mode 100644 index 0000000..459121e --- /dev/null +++ b/public/src/js/titlescreen.js @@ -0,0 +1,146 @@ +class Titlescreen{ + constructor(...args){ + this.init(...args) + } + init(songId){ + this.songId = songId + db.getItem("customFolder").then(folder => this.customFolder = folder) + + if(!songId){ + loader.changePage("titlescreen", false) + loader.screen.style.backgroundImage = "" + + this.titleScreen = document.getElementById("title-screen") + this.proceed = document.getElementById("title-proceed") + this.disclaimerText = document.getElementById("title-disclaimer-text") + this.disclaimerCopyright = document.getElementById("title-disclaimer-copyright") + this.logo = new Logo() + } + this.setLang(allStrings[settings.getItem("language")]) + + if(songId){ + if(localStorage.getItem("tutorial") === "true"){ + new SongSelect(false, false, this.touched, this.songId) + }else{ + new SettingsView(false, true, this.songId) + } + }else{ + pageEvents.add(this.titleScreen, ["mousedown", "touchstart"], event => { + if(event.type === "touchstart"){ + event.preventDefault() + this.touched = true + }else if(event.type === "mousedown" && event.which !== 1){ + return + } + this.onPressed(true) + }) + + assets.sounds["v_title"].play() + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"] + }, this.onPressed.bind(this)) + this.gamepad = new Gamepad({ + gamepadConfirm: ["a", "b", "x", "y", "start", "ls", "rs"] + }, this.onPressed.bind(this)) + if(p2.session){ + pageEvents.add(p2, "message", response => { + if(response.type === "songsel"){ + this.goNext(true) + } + }) + } + pageEvents.send("title-screen") + } + } + + onPressed(pressed, name){ + if(pressed){ + if(name === "gamepadConfirm" && (snd.buffer.context.state === "suspended" || this.customFolder)){ + return + } + this.titleScreen.style.cursor = "auto" + this.clean() + if(!this.customFolder || assets.customSongs){ + assets.sounds["se_don"].play() + } + this.goNext() + } + } + goNext(fromP2){ + if(p2.session && !fromP2){ + p2.send("songsel") + }else{ + if(fromP2 || this.customFolder || localStorage.getItem("tutorial") === "true"){ + if(this.touched){ + localStorage.setItem("tutorial", "true") + } + pageEvents.remove(p2, "message") + if(this.customFolder && !fromP2 && !assets.customSongs){ + var customSongs = new CustomSongs(this.touched, true, true) + var soundPlayed = false + var noError = true + var promises = [] + var allFiles = [] + this.customFolder.forEach(file => { + promises.push(customSongs.walkFilesystem(file, undefined, allFiles)) + }) + Promise.all(promises).then(() => { + assets.sounds["se_don"].play() + soundPlayed = true + return customSongs.importLocal(allFiles) + }).catch(() => { + localStorage.removeItem("customSelected") + db.removeItem("customFolder") + if(!soundPlayed){ + assets.sounds["se_don"].play() + } + setTimeout(() => { + new SongSelect(false, false, this.touched, this.songId) + }, 500) + noError = false + }).then(() => { + if(noError){ + setTimeout(() => { + new SongSelect("customSongs", false, this.touched) + }, 500) + } + }) + }else{ + setTimeout(() => { + new SongSelect(false, false, this.touched, this.songId) + }, 500) + } + }else{ + setTimeout(() => { + new SettingsView(this.touched, true, this.songId) + }, 500) + } + } + } + setLang(lang, noEvent){ + settings.setLang(lang, true) + if(this.songId){ + return + } + this.proceed.innerText = strings.titleProceed + this.proceed.setAttribute("alt", strings.titleProceed) + + this.disclaimerText.innerText = strings.titleDisclaimer + this.disclaimerText.setAttribute("alt", strings.titleDisclaimer) + this.disclaimerCopyright.innerText = strings.titleCopyright + this.disclaimerCopyright.setAttribute("alt", strings.titleCopyright) + + this.logo.updateSubtitle() + } + clean(){ + this.keyboard.clean() + this.gamepad.clean() + this.logo.clean() + assets.sounds["v_title"].stop() + pageEvents.remove(this.titleScreen, ["mousedown", "touchstart"]) + delete this.titleScreen + delete this.proceed + delete this.titleDisclaimer + delete this.titleCopyright + } +} diff --git a/public/src/js/tutorial.js b/public/src/js/tutorial.js new file mode 100644 index 0000000..73c5d5e --- /dev/null +++ b/public/src/js/tutorial.js @@ -0,0 +1,184 @@ +class Tutorial{ + constructor(...args){ + this.init(...args) + } + init(fromSongSel, songId){ + this.fromSongSel = fromSongSel + this.songId = songId + loader.changePage("tutorial", true) + assets.sounds["bgm_setsume"].playLoop(0.1, false, 0, 1.054, 16.054) + this.endButton = this.getElement("view-end-button") + + this.tutorialTitle = this.getElement("view-title") + this.tutorialDiv = document.createElement("div") + this.getElement("view-content").appendChild(this.tutorialDiv) + + this.items = [] + this.items.push(this.endButton) + this.selected = this.items.length - 1 + + this.setStrings() + + pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this)) + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"], + previous: ["left", "up", "ka_l"], + next: ["right", "down", "ka_r"], + back: ["escape"] + }, this.keyPressed.bind(this)) + this.gamepad = new Gamepad({ + "confirm": ["b", "ls", "rs"], + "previous": ["u", "l", "lb", "lt", "lsu", "lsl"], + "next": ["d", "r", "rb", "rt", "lsd", "lsr"], + "back": ["start", "a"] + }, this.keyPressed.bind(this)) + + pageEvents.send("tutorial") + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + keyPressed(pressed, name){ + if(!pressed){ + return + } + var selected = this.items[this.selected] + if(name === "confirm"){ + if(selected === this.endButton){ + this.onEnd() + }else{ + this.getLink(selected).click() + assets.sounds["se_don"].play() + } + }else if(name === "previous" || name === "next"){ + if(this.items.length >= 2){ + selected.classList.remove("selected") + this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1)) + this.items[this.selected].classList.add("selected") + assets.sounds["se_ka"].play() + } + }else if(name === "back"){ + this.onEnd() + } + } + mod(length, index){ + return ((index % length) + length) % length + } + onEnd(event){ + var touched = false + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + touched = true + }else if(event.which !== 1){ + return + } + } + this.clean() + assets.sounds["se_don"].play() + try{ + localStorage.setItem("tutorial", "true") + }catch(e){} + setTimeout(() => { + new SongSelect(this.fromSongSel ? "tutorial" : false, false, touched, this.songId) + }, 500) + } + getLink(target){ + return target.getElementsByTagName("a")[0] + } + linkButton(event){ + if(event.target === event.currentTarget && (event.type === "touchstart" || event.which === 1)){ + this.getLink(event.currentTarget).click() + assets.sounds["se_don"].play() + } + } + insertText(text, parent){ + parent.appendChild(document.createTextNode(text)) + } + insertKey(key, parent){ + if(!Array.isArray(key)){ + key = [key] + } + var join = true + for(var i = 0; i < key.length; i++){ + if(key[i] === false){ + join = false + continue + } + if(i !== 0){ + if(join){ + var span = document.createElement("span") + span.classList.add("key-join") + span.innerText = strings.tutorial.key.join + parent.appendChild(span) + }else{ + parent.appendChild(document.createTextNode(strings.tutorial.key.or)) + } + } + var kbd = document.createElement("kbd") + kbd.innerText = key[i] + parent.appendChild(kbd) + } + } + setStrings(){ + this.tutorialTitle.innerText = strings.howToPlay + this.tutorialTitle.setAttribute("alt", strings.howToPlay) + this.endButton.innerText = strings.tutorial.ok + this.endButton.setAttribute("alt", strings.tutorial.ok) + this.tutorialDiv.innerHTML = "" + var kbdSettings = settings.getItem("keyboardSettings") + var pauseKey = [strings.tutorial.key.esc] + if(pageEvents.kbd.indexOf("q") === -1){ + pauseKey.push(false) + pauseKey.push("Q") + } + var keys = [ + kbdSettings.don_l[0].toUpperCase(), + kbdSettings.don_r[0].toUpperCase(), + kbdSettings.ka_l[0].toUpperCase(), + kbdSettings.ka_r[0].toUpperCase(), + pauseKey, + [strings.tutorial.key.shift, strings.tutorial.key.leftArrow], + [strings.tutorial.key.shift, strings.tutorial.key.rightArrow], + strings.tutorial.key.shift, + strings.tutorial.key.ctrl + ] + var keyIndex = 0 + strings.tutorial.basics.forEach(string => { + var par = document.createElement("p") + var stringKeys = string.split("%s") + stringKeys.forEach((stringKey, i) => { + if(i !== 0){ + this.insertKey(keys[keyIndex++], par) + } + this.insertText(stringKey, par) + }) + this.tutorialDiv.appendChild(par) + }) + var par = document.createElement("p") + var span = document.createElement("span") + span.style.fontWeight = "bold" + span.innerText = strings.tutorial.otherControls + par.appendChild(span) + strings.tutorial.otherTutorial.forEach(string => { + par.appendChild(document.createElement("br")) + var stringKeys = string.split("%s") + stringKeys.forEach((stringKey, i) => { + if(i !== 0){ + this.insertKey(keys[keyIndex++], par) + } + this.insertText(stringKey, par) + }) + }) + this.tutorialDiv.appendChild(par) + } + clean(){ + this.keyboard.clean() + this.gamepad.clean() + pageEvents.remove(this.endButton, ["mousedown", "touchstart"]) + assets.sounds["bgm_setsume"].stop() + delete this.tutorialTitle + delete this.endButton + delete this.tutorialDiv + } +} diff --git a/public/src/js/view.js b/public/src/js/view.js new file mode 100644 index 0000000..717720a --- /dev/null +++ b/public/src/js/view.js @@ -0,0 +1,2269 @@ +class View{ + constructor(...args){ + this.init(...args) + } + init(controller){ + this.controller = controller + + this.canvas = document.getElementById("canvas") + this.ctx = this.canvas.getContext("2d") + var resolution = settings.getItem("resolution") + var noSmoothing = resolution === "low" || resolution === "lowest" + if(noSmoothing){ + this.ctx.imageSmoothingEnabled = false + } + this.multiplayer = this.controller.multiplayer + if(this.multiplayer !== 2 && resolution === "lowest"){ + document.getElementById("game").classList.add("pixelated") + } + + this.gameDiv = document.getElementById("game") + this.songBg = document.getElementById("songbg") + this.songStage = document.getElementById("song-stage") + + this.rules = this.controller.game.rules + this.portraitClass = false + this.touchp2Class = false + this.darkDonBg = false + + this.pauseOptions = strings.pauseOptions + this.difficulty = { + "easy": 0, + "normal": 1, + "hard": 2, + "oni": 3, + "ura": 4 + } + + this.currentScore = { + ms: -Infinity, + type: 0 + } + this.noteFace = { + small: 0, + big: 3 + } + this.state = { + pausePos: 0, + moveMS: 0, + moveHover: null, + hasPointer: false + } + this.nextBeat = 0 + this.gogoTime = 0 + this.gogoTimeStarted = -Infinity + this.drumroll = [] + this.touchEvents = 0 + if(this.controller.parsedSongData.branches){ + this.branch = "normal" + this.branchAnimate = { + ms: -Infinity, + fromBranch: "normal" + } + this.branchMap = { + "normal": { + "bg": "rgba(0, 0, 0, 0)", + "text": "#d3d3d3", + "stroke": "#393939", + "shadow": "#000" + }, + "advanced": { + "bg": "rgba(29, 129, 189, 0.4)", + "text": "#94d7e7", + "stroke": "#315973", + "shadow": "#082031" + }, + "master": { + "bg": "rgba(230, 29, 189, 0.4)", + "text": "#f796ef", + "stroke": "#7e2e6e", + "shadow": "#3e0836" + } + } + } + + if(this.controller.calibrationMode){ + this.beatInterval = 512 + }else{ + this.beatInterval = this.controller.parsedSongData.beatInfo.beatInterval + } + this.font = strings.font + + this.draw = new CanvasDraw(noSmoothing) + this.assets = new ViewAssets(this) + + this.titleCache = new CanvasCache(noSmoothing) + this.comboCache = new CanvasCache(noSmoothing) + this.pauseCache = new CanvasCache(noSmoothing) + this.branchCache = new CanvasCache(noSmoothing) + this.nameplateCache = new CanvasCache(noSmoothing) + + if(this.multiplayer === 2){ + this.player = p2.player === 2 ? 1 : 2 + }else{ + this.player = this.controller.multiplayer ? p2.player : 1 + } + + this.touchEnabled = this.controller.touchEnabled + this.touch = -Infinity + this.touchAnimation = settings.getItem("touchAnimation") + + versionDiv.classList.add("version-hide") + loader.screen.parentNode.insertBefore(versionDiv, loader.screen) + + if(this.multiplayer !== 2){ + + if(this.controller.touchEnabled){ + this.touchDrumDiv = document.getElementById("touch-drum") + this.touchDrumImg = document.getElementById("touch-drum-img") + + this.setBgImage(this.touchDrumImg, assets.image["touch_drum"].src) + + if(this.controller.autoPlayEnabled){ + this.touchDrumDiv.style.display = "none" + } + pageEvents.add(this.canvas, "touchstart", this.ontouch.bind(this)) + + this.gameDiv.classList.add("touch-visible") + + this.touchFullBtn = document.getElementById("touch-full-btn") + pageEvents.add(this.touchFullBtn, "touchend", toggleFullscreen) + if(!fullScreenSupported){ + this.touchFullBtn.style.display = "none" + } + + this.touchPauseBtn = document.getElementById("touch-pause-btn") + pageEvents.add(this.touchPauseBtn, "touchend", () => { + this.controller.togglePause() + }) + if(this.multiplayer){ + this.touchPauseBtn.style.display = "none" + } + } + } + if(this.multiplayer){ + this.gameDiv.classList.add("multiplayer") + }else{ + pageEvents.add(this.canvas, "mousedown", this.onmousedown.bind(this)) + } + } + run(){ + if(this.multiplayer !== 2){ + this.setBackground() + } + this.setDonBg() + + this.startTime = this.controller.game.getAccurateTime() + this.lastMousemove = this.startTime + pageEvents.mouseAdd(this, this.onmousemove.bind(this)) + + this.refresh() + } + refresh(){ + var ctx = this.ctx + + var winW = innerWidth + var winH = lastHeight + + if(winW / 32 > winH / 9){ + winW = winH / 9 * 32 + } + + this.portrait = winW < winH + var touchMultiplayer = this.touchEnabled && this.multiplayer && !this.portrait + + this.pixelRatio = window.devicePixelRatio || 1 + var resolution = settings.getItem("resolution") + if(resolution === "medium"){ + this.pixelRatio *= 0.75 + }else if(resolution === "low"){ + this.pixelRatio *= 0.5 + }else if(resolution === "lowest"){ + this.pixelRatio *= 0.25 + } + winW *= this.pixelRatio + winH *= this.pixelRatio + if(this.portrait){ + var ratioX = winW / 720 + var ratioY = winH / 1280 + }else{ + var ratioX = winW / 1280 + var ratioY = winH / 720 + } + var ratio = (ratioX < ratioY ? ratioX : ratioY) + + var resized = false + if(this.winW !== winW || this.winH !== winH){ + this.winW = winW + this.winH = winH + this.ratio = ratio + + if(this.player !== 2){ + this.canvas.width = Math.max(1, winW) + this.canvas.height = Math.max(1, winH) + ctx.scale(ratio, ratio) + this.canvas.style.width = (winW / this.pixelRatio) + "px" + this.canvas.style.height = (winH / this.pixelRatio) + "px" + this.titleCache.resize(640, 90, ratio) + } + if(!this.multiplayer){ + this.pauseCache.resize(81 * this.pauseOptions.length * 2, 464, ratio) + } + if(this.portrait){ + this.nameplateCache.resize(220, 54, ratio + 0.2) + }else{ + this.nameplateCache.resize(274, 67, ratio + 0.2) + } + this.fillComboCache() + this.setDonBgHeight() + if(this.controller.lyrics){ + this.controller.lyrics.setScale(ratio / this.pixelRatio) + } + resized = true + }else if(this.controller.game.paused && !document.hasFocus()){ + return + }else if(this.player !== 2){ + ctx.clearRect(0, 0, winW / ratio, winH / ratio) + } + winW /= ratio + winH /= ratio + if(!this.controller.game.paused){ + this.ms = this.controller.game.getAccurateTime() + } + var ms = this.ms + + if(this.portrait){ + var frameTop = winH / 2 - 1280 / 2 + var frameLeft = winW / 2 - 720 / 2 + }else{ + var frameTop = winH / 2 - 720 / 2 + var frameLeft = winW / 2 - 1280 / 2 + } + if(this.player === 2){ + frameTop += 165 + } + if(touchMultiplayer){ + if(!this.touchp2Class){ + this.touchp2Class = true + this.gameDiv.classList.add("touchp2") + this.setDonBgHeight() + } + frameTop -= 90 + }else if(this.touchp2Class){ + this.touchp2Class = false + this.gameDiv.classList.remove("touchp2") + this.setDonBgHeight() + } + + ctx.save() + ctx.translate(0, frameTop) + + this.drawGogoTime() + + if(!touchMultiplayer || this.player === 1 && frameTop >= 0){ + this.assets.drawAssets("background") + } + + if(this.player !== 2){ + this.titleCache.get({ + ctx: ctx, + x: winW - (touchMultiplayer && fullScreenSupported ? 750 : 650), + y: touchMultiplayer ? 75 : 10, + w: 640, + h: 90, + id: "title" + }, ctx => { + var selectedSong = this.controller.selectedSong + + this.draw.layeredText({ + ctx: ctx, + text: selectedSong.title, + fontSize: 40, + fontFamily: this.font, + x: 620, + y: 20, + width: 600, + align: "right" + }, [ + {outline: "#000", letterBorder: 10}, + {fill: "#fff"} + ]) + + if(selectedSong.category){ + var _w = 142 + var _h = 22 + var _x = 628 - _w + var _y = 88 - _h + + let category = assets.categories.find(cat=>cat.id == selectedSong.category_id) + if(category != null && category.songSkin != null && category.songSkin.infoFill != null){ + ctx.fillStyle = category.songSkin.infoFill + }else{ + ctx.fillStyle = assets.categories.find(cat=>cat.title == 'default').songSkin.infoFill + } + this.draw.roundedRect({ + ctx: ctx, + x: _x, y: _y, + w: _w, h: _h, + radius: 11 + }) + ctx.fill() + + this.draw.layeredText({ + ctx: ctx, + text: selectedSong.category, + fontSize: 15, + fontFamily: this.font, + align: "center", + baseline: "middle", + x: _x + _w / 2, + y: _y + _h / 2, + width: 122 + }, [ + {fill: "#fff"} + ]) + } + }) + } + + var score = this.controller.getGlobalScore() + var gaugePercent = this.rules.gaugePercent(score.gauge) + + if(this.player === 2){ + var scoreImg = "bg_score_p2" + var scoreFill = "#6bbec0" + }else{ + var scoreImg = "bg_score_p1" + var scoreFill = "#fa4529" + } + + if(this.portrait){ + // Portrait + + if(!this.portraitClass){ + this.portraitClass = true + this.gameDiv.classList.add("portrait") + this.setDonBgHeight() + } + + this.slotPos = { + x: 66, + y: frameTop + 375, + size: 100, + paddingLeft: 0 + } + this.scorePos = {x: 363, y: frameTop + (this.player === 2 ? 520 : 227)} + + var animPos = { + x1: this.slotPos.x + 13, + y1: this.slotPos.y + (this.player === 2 ? 27 : -27), + x2: winW - 38, + y2: frameTop + (this.player === 2 ? 484 : 293) + } + var taikoPos = { + x: 19, + y: frameTop + (this.player === 2 ? 464 : 184), + w: 111, + h: 130 + } + + this.nameplateCache.get({ + ctx: ctx, + x: 167, + y: this.player === 2 ? 565 : 160, + w: 219, + h: 53, + id: "1p", + }, ctx => { + var defaultName = this.player === 1 ? strings.defaultName : strings.default2PName + if(this.multiplayer === 2){ + var name = p2.name || defaultName + }else{ + var name = account.loggedIn ? account.displayName : defaultName + } + this.draw.nameplate({ + ctx: ctx, + x: 3, + y: 3, + scale: 0.8, + name: name, + font: this.font, + blue: this.player === 2 + }) + }) + + ctx.fillStyle = "#000" + ctx.fillRect( + 0, + this.player === 2 ? 306 : 288, + winW, + this.player === 1 ? 184 : 183 + ) + ctx.beginPath() + if(this.player === 2){ + ctx.moveTo(0, 467) + ctx.lineTo(384, 467) + ctx.lineTo(384, 512) + ctx.lineTo(184, 560) + ctx.lineTo(0, 560) + }else{ + ctx.moveTo(0, 217) + ctx.lineTo(184, 217) + ctx.lineTo(384, 265) + ctx.lineTo(384, 309) + ctx.lineTo(0, 309) + } + ctx.fill() + + // Left side + ctx.fillStyle = scoreFill + var leftSide = (ctx, mul) => { + ctx.beginPath() + if(this.player === 2){ + ctx.moveTo(0, 468 * mul) + ctx.lineTo(380 * mul, 468 * mul) + ctx.lineTo(380 * mul, 512 * mul) + ctx.lineTo(184 * mul, 556 * mul) + ctx.lineTo(0, 556 * mul) + }else{ + ctx.moveTo(0, 221 * mul) + ctx.lineTo(184 * mul, 221 * mul) + ctx.lineTo(380 * mul, 265 * mul) + ctx.lineTo(380 * mul, 309 * mul) + ctx.lineTo(0, 309 * mul) + } + } + leftSide(ctx, 1) + ctx.fill() + ctx.globalAlpha = 0.5 + this.draw.pattern({ + ctx: ctx, + img: assets.image[scoreImg], + shape: leftSide, + dx: 0, + dy: 45, + scale: 1.55 + }) + ctx.globalAlpha = 1 + + // Score background + ctx.fillStyle = "#000" + ctx.beginPath() + if(this.player === 2){ + this.draw.roundedCorner(ctx, 184, 512, 20, 0) + ctx.lineTo(384, 512) + this.draw.roundedCorner(ctx, 384, 560, 12, 2) + ctx.lineTo(184, 560) + }else{ + ctx.moveTo(184, 217) + this.draw.roundedCorner(ctx, 384, 217, 12, 1) + ctx.lineTo(384, 265) + this.draw.roundedCorner(ctx, 184, 265, 20, 3) + } + ctx.fill() + + // Difficulty + if(this.controller.selectedSong.difficulty){ + ctx.drawImage(assets.image["difficulty"], + 0, 144 * this.difficulty[this.controller.selectedSong.difficulty], + 168, 143, + 126, this.player === 2 ? 497 : 228, + 62, 53 + ) + } + + // Badges + if(this.controller.autoPlayEnabled && !this.multiplayer){ + this.ctx.drawImage(assets.image["badge_auto"], + 183, + this.player === 2 ? 490 : 265, + 23, + 23 + ) + } + + // Gauge + ctx.fillStyle = "#000" + ctx.beginPath() + var gaugeX = winW - 788 * 0.7 - 32 + if(this.player === 2){ + ctx.moveTo(gaugeX, 464) + ctx.lineTo(winW, 464) + ctx.lineTo(winW, 489) + this.draw.roundedCorner(ctx, gaugeX, 489, 12, 3) + }else{ + this.draw.roundedCorner(ctx, gaugeX, 288, 12, 0) + ctx.lineTo(winW, 288) + ctx.lineTo(winW, 314) + ctx.lineTo(gaugeX, 314) + } + ctx.fill() + this.draw.gauge({ + ctx: ctx, + x: winW, + y: this.player === 2 ? 468 : 273, + clear: this.rules.gaugeClear, + percentage: gaugePercent, + font: this.font, + scale: 0.7, + multiplayer: this.player === 2, + blue: this.player === 2 + }) + this.draw.soul({ + ctx: ctx, + x: winW - 40, + y: this.player === 2 ? 484 : 293, + scale: 0.75, + cleared: this.rules.clearReached(score.gauge) + }) + + // Note bar + ctx.fillStyle = "#2c2a2c" + ctx.fillRect(0, 314, winW, 122) + ctx.fillStyle = "#847f84" + ctx.fillRect(0, 440, winW, 24) + + }else{ + // Landscape + + if(this.portraitClass){ + this.portraitClass = false + this.gameDiv.classList.remove("portrait") + this.setDonBgHeight() + } + + this.slotPos = { + x: 413, + y: frameTop + 257, + size: 106, + paddingLeft: 332 + } + this.scorePos = { + x: 155, + y: frameTop + (this.player === 2 ? 318 : 193) + } + + var animPos = { + x1: this.slotPos.x + 14, + y1: this.slotPos.y + (this.player === 2 ? 29 : -29), + x2: winW - 55, + y2: frameTop + (this.player === 2 ? 378 : 165) + } + var taikoPos = {x: 179, y: frameTop + 190, w: 138, h: 162} + + this.nameplateCache.get({ + ctx: ctx, + x: touchMultiplayer ? 47 : 320, + y: touchMultiplayer ? (this.player === 2 ? 361 : 119) : (this.player === 2 ? 460 : 20), + w: 273, + h: 66, + id: "1p", + }, ctx => { + var defaultName = this.player === 1 ? strings.defaultName : strings.default2PName + if(this.multiplayer === 2){ + var name = p2.name || defaultName + }else{ + var name = account.loggedIn ? account.displayName : defaultName + } + this.draw.nameplate({ + ctx: ctx, + x: 3, + y: 3, + name: name, + font: this.font, + blue: this.player === 2 + }) + }) + + ctx.fillStyle = "#000" + ctx.fillRect( + 0, + 184, + winW, + this.multiplayer && this.player === 1 ? 177 : 176 + ) + ctx.beginPath() + if(this.player === 2){ + ctx.moveTo(328, 351) + ctx.lineTo(winW, 351) + ctx.lineTo(winW, 385) + this.draw.roundedCorner(ctx, 328, 385, 10, 3) + }else{ + ctx.moveTo(328, 192) + this.draw.roundedCorner(ctx, 328, 158, 10, 0) + ctx.lineTo(winW, 158) + ctx.lineTo(winW, 192) + } + ctx.fill() + + // Gauge + this.draw.gauge({ + ctx: ctx, + x: winW, + y: this.player === 2 ? 357 : 135, + clear: this.rules.gaugeClear, + percentage: gaugePercent, + font: this.font, + multiplayer: this.player === 2, + blue: this.player === 2 + }) + this.draw.soul({ + ctx: ctx, + x: winW - 57, + y: this.player === 2 ? 378 : 165, + cleared: this.rules.clearReached(score.gauge) + }) + + // Note bar + ctx.fillStyle = "#2c2a2c" + ctx.fillRect(332, 192, winW - 332, 130) + ctx.fillStyle = "#847f84" + ctx.fillRect(332, 326, winW - 332, 26) + + // Left side + ctx.fillStyle = scoreFill + ctx.fillRect(0, 192, 328, 160) + ctx.globalAlpha = 0.5 + this.draw.pattern({ + ctx: ctx, + img: assets.image[scoreImg], + x: 0, + y: 192, + w: 328, + h: 160, + dx: 0, + dy: 45, + scale: 1.55 + }) + ctx.globalAlpha = 1 + + // Difficulty + if(this.controller.selectedSong.difficulty){ + ctx.drawImage(assets.image["difficulty"], + 0, 144 * this.difficulty[this.controller.selectedSong.difficulty], + 168, 143, + 16, this.player === 2 ? 194 : 232, + 141, 120 + ) + var diff = this.controller.selectedSong.difficulty + var text = strings[diff === "ura" ? "oni" : diff] + ctx.font = this.draw.bold(this.font) + "20px " + this.font + ctx.textAlign = "center" + ctx.textBaseline = "bottom" + ctx.strokeStyle = "#000" + ctx.fillStyle = "#fff" + ctx.lineWidth = 7 + ctx.miterLimit = 1 + ctx.strokeText(text, 87, this.player === 2 ? 310 : 348) + ctx.fillText(text, 87, this.player === 2 ? 310 : 348) + ctx.miterLimit = 10 + } + + // Badges + if(this.controller.autoPlayEnabled && !this.multiplayer){ + this.ctx.drawImage(assets.image["badge_auto"], + 125, 235, 34, 34 + ) + } + + // Score background + ctx.fillStyle = "#000" + ctx.beginPath() + if(this.player === 2){ + ctx.moveTo(0, 312) + this.draw.roundedCorner(ctx, 176, 312, 20, 1) + ctx.lineTo(176, 353) + ctx.lineTo(0, 353) + }else{ + ctx.moveTo(0, 191) + ctx.lineTo(176, 191) + this.draw.roundedCorner(ctx, 176, 232, 20, 2) + ctx.lineTo(0, 232) + } + ctx.fill() + } + + ctx.restore() + + animPos.w = animPos.x2 - animPos.x1 + animPos.h = animPos.y1 - animPos.y2 + this.animateBezier = [{ + // 427, 228 + x: animPos.x1, + y: animPos.y1 + }, { + // 560, 10 + x: animPos.x1 + animPos.w / 6, + y: animPos.y1 - animPos.h * (this.player === 2 ? 2.5 : 3.5) + }, { + // 940, -150 + x: animPos.x2 - animPos.w / 3, + y: animPos.y2 - animPos.h * (this.player === 2 ? 3.5 : 5) + }, { + // 1225, 165 + x: animPos.x2, + y: animPos.y2 + }] + + var touchTop = frameTop + (touchMultiplayer ? 135 : 0) + (this.player === 2 ? -165 : 0) + this.touchDrum = (() => { + var sw = 842 + var sh = 340 + var x = 0 + var y = this.portrait ? touchTop + 477 : touchTop + 365 + var paddingTop = 13 + var w = winW + var maxH = winH - y + var h = maxH - paddingTop + if(w / h >= sw / sh){ + w = h / sh * sw + x = (winW - w) / 2 + y += paddingTop + }else{ + h = w / sw * sh + y = y + (maxH - h) + } + return { + x: x, y: y, w: w, h: h + } + })() + this.touchCircle = { + x: winW / 2, + y: winH + this.touchDrum.h * 0.1, + rx: this.touchDrum.w / 2 - this.touchDrum.h * 0.03, + ry: this.touchDrum.h * 1.07 + } + + if(this.multiplayer !== 2){ + this.mouseIdle() + this.drawTouch() + } + + // Score + ctx.save() + ctx.font = "30px TnT, Meiryo, sans-serif" + ctx.fillStyle = "#fff" + ctx.strokeStyle = "#fff" + ctx.lineWidth = 0.3 + ctx.textAlign = "center" + ctx.textBaseline = "top" + var glyph = 29 + var pointsText = score.points.toString().split("") + ctx.translate(this.scorePos.x, this.scorePos.y) + ctx.scale(0.7, 1) + for(var i in pointsText){ + var x = glyph * (i - pointsText.length + 1) + ctx.strokeText(pointsText[i], x, 0) + ctx.fillText(pointsText[i], x, 0) + } + ctx.restore() + + // Branch background + var keyTime = this.controller.getKeyTime() + var sound = keyTime["don"] > keyTime["ka"] ? "don" : "ka" + var padding = this.slotPos.paddingLeft + var mul = this.slotPos.size / 106 + var barY = this.slotPos.y - 65 * mul + var barH = 130 * mul + + if(this.branchAnimate && ms <= this.branchAnimate.ms + 300){ + var alpha = Math.max(0, (ms - this.branchAnimate.ms) / 300) + ctx.globalAlpha = 1 - alpha + ctx.fillStyle = this.branchMap[this.branchAnimate.fromBranch].bg + ctx.fillRect(padding, barY, winW - padding, barH) + ctx.globalAlpha = alpha + } + if(this.branch){ + ctx.fillStyle = this.branchMap[this.branch].bg + ctx.fillRect(padding, barY, winW - padding, barH) + ctx.globalAlpha = 1 + } + + // Current branch text + if(this.branch){ + if(resized){ + this.fillBranchCache() + } + var textW = Math.floor(260 * mul) + var textH = Math.floor(barH) + var textX = winW - textW + var oldOffset = 0 + var newOffset = 0 + var elapsed = ms - this.startTime + if(elapsed < 250){ + textX = winW + }else if(elapsed < 500){ + textX += (1 - this.draw.easeOutBack((elapsed - 250) / 250)) * textW + } + if(this.branchAnimate && ms - this.branchAnimate.ms < 310 && ms >= this.branchAnimate.ms){ + var fromBranch = this.branchAnimate.fromBranch + var elapsed = ms - this.branchAnimate.ms + var reverse = fromBranch === "master" || fromBranch === "advanced" && this.branch === "normal" ? -1 : 1 + if(elapsed < 65){ + oldOffset = elapsed / 65 * 12 * mul * reverse + ctx.globalAlpha = 1 + var newAlpha = 0 + }else if(elapsed < 215){ + var animPoint = (elapsed - 65) / 150 + oldOffset = (12 - animPoint * 48) * mul * reverse + newOffset = (36 - animPoint * 48) * mul * reverse + ctx.globalAlpha = this.draw.easeIn(1 - animPoint) + var newAlpha = this.draw.easeIn(animPoint) + }else{ + newOffset = (1 - (elapsed - 215) / 95) * -12 * mul * reverse + ctx.globalAlpha = 0 + var newAlpha = 1 + } + this.branchCache.get({ + ctx: ctx, + x: textX, y: barY + oldOffset, + w: textW, h: textH, + id: fromBranch + }) + ctx.globalAlpha = newAlpha + } + this.branchCache.get({ + ctx: ctx, + x: textX, y: barY + newOffset, + w: textW, h: textH, + id: this.branch + }) + ctx.globalAlpha = 1 + } + + // Go go time background + if(this.gogoTime || ms <= this.gogoTimeStarted + 100){ + var grd = ctx.createLinearGradient(padding, 0, winW, 0) + grd.addColorStop(0, "rgba(255, 0, 0, 0.16)") + grd.addColorStop(0.45, "rgba(255, 0, 0, 0.28)") + grd.addColorStop(0.77, "rgba(255, 83, 157, 0.4)") + grd.addColorStop(1, "rgba(255, 83, 157, 0)") + ctx.fillStyle = grd + if(!this.touchEnabled){ + var alpha = Math.min(100, ms - this.gogoTimeStarted) / 100 + if(!this.gogoTime){ + alpha = 1 - alpha + } + ctx.globalAlpha = alpha + } + ctx.fillRect(padding, barY, winW - padding, barH) + } + + // Bar pressed keys + if(keyTime[sound] > ms - 130){ + var gradients = { + "don": "255, 0, 0", + "ka": "0, 170, 255" + } + var yellow = "255, 231, 0" + var currentGradient = gradients[sound] + ctx.globalCompositeOperation = "lighter" + do{ + var grd = ctx.createLinearGradient(padding, 0, winW, 0) + grd.addColorStop(0, "rgb(" + currentGradient + ")") + grd.addColorStop(1, "rgba(" + currentGradient + ", 0)") + ctx.fillStyle = grd + ctx.globalAlpha = (1 - (ms - keyTime[sound]) / 130) / 5 + ctx.fillRect(padding, barY, winW - padding, barH) + }while(this.currentScore.ms > ms - 130 && currentGradient !== yellow && (currentGradient = yellow)) + ctx.globalCompositeOperation = "source-over" + } + ctx.globalAlpha = 1 + + // Taiko + ctx.drawImage(assets.image["taiko"], + 0, 0, 138, 162, + taikoPos.x, taikoPos.y, taikoPos.w, taikoPos.h + ) + + // Taiko pressed keys + var keys = ["ka_l", "ka_r", "don_l", "don_r"] + + for(var i = 0; i < keys.length; i++){ + var keyMS = ms - keyTime[keys[i]] + if(keyMS < 130){ + if(keyMS > 70 && !this.touchEnabled){ + ctx.globalAlpha = this.draw.easeOut(1 - (keyMS - 70) / 60) + } + ctx.drawImage(assets.image["taiko"], + 0, 162 * (i + 1), 138, 162, + taikoPos.x, taikoPos.y, taikoPos.w, taikoPos.h + ) + } + } + ctx.globalAlpha = 1 + + // Combo + var scoreMS = ms - this.currentScore.ms + + var comboCount = this.controller.getCombo() + if(comboCount >= 10){ + var comboText = comboCount.toString().split("") + var mul = this.portrait ? 0.8 : 1 + var comboX = taikoPos.x + taikoPos.w / 2 + var comboY = taikoPos.y + taikoPos.h * 0.09 + var comboScale = 0 + if(this.currentScore !== 0 && scoreMS < 100){ + comboScale = this.draw.fade(scoreMS / 100) + } + var glyphW = 51 + var glyphH = 65 + var letterSpacing = (comboText.length >= 4 ? 38 : 42) * mul + var orange = comboCount >= 100 ? "1" : "0" + + var w = glyphW * mul + var h = glyphH * mul * (1 + comboScale / 8) + + for(var i in comboText){ + var textX = comboX + letterSpacing * (i - (comboText.length - 1) / 2) + this.comboCache.get({ + ctx: ctx, + x: textX - w / 2, + y: comboY + glyphH * mul - h, + w: w, + h: h, + id: orange + "combo" + comboText[i] + }) + } + + var fontSize = 24 * mul + var comboTextY = taikoPos.y + taikoPos.h * 0.63 + if(orange === "1"){ + var grd = ctx.createLinearGradient( + 0, + comboTextY - fontSize * 0.6, + 0, + comboTextY + fontSize * 0.1 + ) + grd.addColorStop(0, "#ff2000") + grd.addColorStop(0.5, "#ffc321") + grd.addColorStop(1, "#ffedb7") + ctx.fillStyle = grd + }else{ + ctx.fillStyle = "#fff" + } + ctx.font = this.draw.bold(this.font) + fontSize + "px " + this.font + ctx.lineWidth = 7 * mul + ctx.textAlign = "center" + ctx.miterLimit = 1 + ctx.strokeStyle = "#000" + ctx.strokeText(strings.combo, comboX, comboTextY) + ctx.miterLimit = 10 + ctx.fillText(strings.combo, comboX, comboTextY) + } + + // Slot + this.draw.slot(ctx, this.slotPos.x, this.slotPos.y, this.slotPos.size) + + // Measures + ctx.save() + ctx.beginPath() + ctx.rect(this.slotPos.paddingLeft, 0, winW - this.slotPos.paddingLeft, winH) + ctx.clip() + this.drawMeasures() + ctx.restore() + + // Go go time fire + this.assets.drawAssets("bar") + + // Hit notes shadow + if(scoreMS < 300 && this.currentScore.type){ + var fadeOut = scoreMS > 120 && !this.touchEnabled + if(fadeOut){ + ctx.globalAlpha = 1 - (scoreMS - 120) / 180 + } + var scoreId = this.currentScore.type === 230 ? 0 : 1 + if(this.currentScore.bigNote){ + scoreId += 2 + } + ctx.drawImage(assets.image["notes_hit"], + 0, 128 * scoreId, 128, 128, + this.slotPos.x - 64, this.slotPos.y - 64, + 128, 128 + ) + if(fadeOut){ + ctx.globalAlpha = 1 + } + } + + // Future notes + this.updateNoteFaces() + ctx.save() + ctx.beginPath() + ctx.rect(this.slotPos.paddingLeft, 0, winW - this.slotPos.paddingLeft, winH) + ctx.clip() + + this.drawCircles(this.controller.getCircles()) + if(this.controller.game.calibrationState === "video"){ + if(ms % this.beatInterval < 1000 / 60 * 5){ + this.drawCircle({ + ms: ms, + type: "don", + endTime: ms + 100, + speed: 0 + }, { + x: this.slotPos.x, + y: this.slotPos.y + }) + } + } + + ctx.restore() + + // Hit notes explosion + this.assets.drawAssets("notes") + + // Good, OK, Bad + if(scoreMS < 300){ + var mul = this.slotPos.size / 106 + var scores = { + "0": "bad", + "230": "ok", + "450": "good" + } + var yOffset = scoreMS < 70 ? scoreMS * (13 / 70) : 0 + var fadeOut = scoreMS > 250 && !this.touchEnabled + if(fadeOut){ + ctx.globalAlpha = 1 - (scoreMS - 250) / 50 + } + this.draw.score({ + ctx: ctx, + score: scores[this.currentScore.type], + x: this.slotPos.x, + y: this.slotPos.y - 98 * mul - yOffset, + scale: 1.35 * mul, + align: "center" + }) + if(fadeOut){ + ctx.globalAlpha = 1 + } + } + + // Animating notes + this.drawAnimatedCircles(this.controller.getCircles()) + this.drawAnimatedCircles(this.drumroll) + + // Go-go time fireworks + if(!this.touchEnabled && !this.portrait && !this.multiplayer){ + this.assets.drawAssets("foreground") + } + + // Pause screen + if(!this.multiplayer && this.controller.game.paused){ + ctx.fillStyle = "rgba(0, 0, 0, 0.5)" + ctx.fillRect(0, 0, winW, winH) + + ctx.save() + if(this.portrait){ + ctx.translate(frameLeft - 242, frameTop + 308) + var pauseScale = 720 / 766 + ctx.scale(pauseScale, pauseScale) + }else{ + ctx.translate(frameLeft, frameTop) + } + + var state = this.controller.game.calibrationState + if(state && state in strings.calibration){ + var boldTitle = strings.calibration[state].title + } + if(boldTitle){ + this.draw.layeredText({ + ctx: ctx, + text: boldTitle, + fontSize: 35, + fontFamily: this.font, + x: 300, + y: 70 + }, [ + {outline: "#fff", letterBorder: 22} + ]) + } + var pauseRect = (ctx, mul) => { + this.draw.roundedRect({ + ctx: ctx, + x: 269 * mul, + y: 93 * mul, + w: 742 * mul, + h: 494 * mul, + radius: 17 * mul + }) + } + pauseRect(ctx, 1) + ctx.strokeStyle = "#fff" + ctx.lineWidth = 24 + ctx.stroke() + ctx.strokeStyle = "#000" + ctx.lineWidth = 12 + ctx.stroke() + this.draw.pattern({ + ctx: ctx, + img: assets.image["bg_pause"], + shape: pauseRect, + dx: 68, + dy: 11 + }) + if(boldTitle){ + this.draw.layeredText({ + ctx: ctx, + text: boldTitle, + fontSize: 35, + fontFamily: this.font, + x: 300, + y: 70 + }, [ + {outline: "#000", letterBorder: 10}, + {fill: "#fff"} + ]) + } + + switch(state){ + case "audioHelp": + case "videoHelp": + case "results": + var content = state === "audioHelp" && this.touchEnabled ? "contentAlt" : "content" + if(state === "audioHelp"){ + var kbdSettings = settings.getItem("keyboardSettings") + var keys = [ + kbdSettings.don_l[0].toUpperCase(), + kbdSettings.don_r[0].toUpperCase() + ] + var substitute = (config, index, width) => { + var ctx = config.ctx + var bold = this.draw.bold(config.fontFamily) + ctx.font = bold + (config.fontSize * 0.66) + "px " + config.fontFamily + var w = config.fontSize * 0.6 + ctx.measureText(keys[index]).width + if(width){ + return w + }else{ + var h = 30 + ctx.lineWidth = 3 + ctx.strokeStyle = "rgba(0, 0, 0, 0.2)" + this.draw.roundedRect({ + ctx: ctx, + x: 0, y: 1, w: w, h: h, + radius: 3 + }) + ctx.stroke() + ctx.strokeStyle = "#ccc" + ctx.fillStyle = "#fff" + this.draw.roundedRect({ + ctx: ctx, + x: 0, y: 0, w: w, h: h, + radius: 3 + }) + ctx.stroke() + ctx.fill() + ctx.fillStyle = "#f7f7f7" + ctx.fillRect(2, 2, w - 4, h - 4) + + ctx.fillStyle = "#333" + ctx.textBaseline = "middle" + ctx.textAlign = "center" + ctx.fillText(keys[index], w / 2, h / 2) + } + } + }else if(state === "results"){ + var progress = this.controller.game.calibrationProgress + var latency = [ + progress.audio, + progress.video + ] + var substitute = (config, index, width) => { + var ctx = config.ctx + var bold = this.draw.bold(config.fontFamily) + ctx.font = bold + (config.fontSize * 1.1) + "px " + config.fontFamily + var text = this.addMs(latency[index]) + if(width){ + return ctx.measureText(text).width + }else{ + ctx.fillText(text, 0, 0) + } + } + }else{ + var substitute = null + } + this.draw.wrappingText({ + ctx: ctx, + text: strings.calibration[state][content], + fontSize: 30, + fontFamily: this.font, + x: 300, + y: 130, + width: 680, + height: 240, + lineHeight: 35, + fill: "#000", + verticalAlign: "middle", + substitute: substitute + }) + + var _x = 640 + var _w = 464 + var _h = 80 + for(var i = 0; i < this.pauseOptions.length; i++){ + var text = this.pauseOptions[i] + var _y = 470 - 90 * (this.pauseOptions.length - i - 1) + if(this.state.moveHover !== null){ + var selected = i === this.state.moveHover + }else{ + var selected = i === this.state.pausePos + } + if(selected){ + ctx.fillStyle = "#ffb447" + this.draw.roundedRect({ + ctx: ctx, + x: _x - _w / 2, + y: _y, + w: _w, + h: _h, + radius: 30 + }) + ctx.fill() + } + if(selected){ + var layers = [ + {outline: "#000", letterBorder: 10}, + {fill: "#fff"} + ] + }else{ + var layers = [ + {fill: "#000"} + ] + } + this.draw.layeredText({ + ctx: ctx, + text: text, + x: _x, + y: _y + 18, + width: _w, + height: _h - 54, + fontSize: 40, + fontFamily: this.font, + letterSpacing: -1, + align: "center" + }, layers) + + var highlight = 0 + if(this.state.moveHover === i){ + highlight = 2 + }else if(selected){ + highlight = 1 + } + if(highlight){ + this.draw.highlight({ + ctx: ctx, + x: _x - _w / 2 - 3.5, + y: _y - 3.5, + w: _w + 7, + h: _h + 7, + animate: highlight === 1, + animateMS: this.state.moveMS, + opacity: highlight === 2 ? 0.8 : 1, + radius: 30 + }) + } + } + break + case "audioComplete": + case "videoComplete": + this.draw.wrappingText({ + ctx: ctx, + text: strings.calibration[state], + fontSize: 40, + fontFamily: this.font, + x: 300, + y: 130, + width: 680, + height: 420, + lineHeight: 47, + fill: "#000", + verticalAlign: "middle", + textAlign: "center", + }) + break + default: + ctx.drawImage(assets.image["mimizu"], + 313, 247, 136, 315 + ) + + var _y = 108 + var _w = 80 + var _h = 464 + for(var i = 0; i < this.pauseOptions.length; i++){ + var text = this.pauseOptions[i] + if(this.controller.calibrationMode && i === this.pauseOptions.length - 1){ + text = strings.calibration.back + } + var _x = 520 + 110 * i + if(this.state.moveHover !== null){ + var selected = i === this.state.moveHover + }else{ + var selected = i === this.state.pausePos + } + if(selected){ + ctx.fillStyle = "#ffb447" + this.draw.roundedRect({ + ctx: ctx, + x: _x - _w / 2, + y: _y, + w: _w, + h: _h, + radius: 30 + }) + ctx.fill() + } + this.pauseCache.get({ + ctx: ctx, + x: _x - _w / 2, + y: _y, + w: _w, + h: _h, + id: text + (selected ? "1" : "0") + }, ctx => { + var textConfig = { + ctx: ctx, + text: text, + x: _w / 2, + y: 18, + width: _w, + height: _h - 54, + fontSize: 40, + fontFamily: this.font, + letterSpacing: -1 + } + if(selected){ + textConfig.fill = "#fff" + textConfig.outline = "#000" + textConfig.outlineSize = 10 + }else{ + textConfig.fill = "#000" + } + this.draw.verticalText(textConfig) + }) + + var highlight = 0 + if(this.state.moveHover === i){ + highlight = 2 + }else if(selected){ + highlight = 1 + } + if(highlight){ + this.draw.highlight({ + ctx: ctx, + x: _x - _w / 2 - 3.5, + y: _y - 3.5, + w: _w + 7, + h: _h + 7, + animate: highlight === 1, + animateMS: this.state.moveMS, + opacity: highlight === 2 ? 0.8 : 1, + radius: 30 + }) + } + } + break + } + + ctx.restore() + } + } + addMs(input){ + var split = strings.calibration.ms.split("%s") + var index = 0 + var output = "" + var inputStrings = [(input > 0 ? "+" : "") + input.toString()] + split.forEach((string, i) => { + if(i !== 0){ + output += inputStrings[index++] + } + output += string + }) + return output + } + setBackground(){ + var selectedSong = this.controller.selectedSong + var songSkinName = selectedSong.songSkin.name + var supportsBlend = "mixBlendMode" in this.songBg.style + var songLayers = [document.getElementById("layer1"), document.getElementById("layer2")] + var prefix = "" + + if(!selectedSong.songSkin.song){ + var id = selectedSong.songBg + this.songBg.classList.add("songbg-" + id) + this.setLayers(songLayers, "bg_song_" + id + (supportsBlend ? "" : "a"), supportsBlend) + }else if(selectedSong.songSkin.song !== "none"){ + var prefix = selectedSong.songSkin.prefix || "" + var notStatic = selectedSong.songSkin.song !== "static" + if(notStatic){ + this.songBg.classList.add("songbg-" + selectedSong.songSkin.song) + } + this.setLayers(songLayers, prefix + "bg_song_" + songSkinName + (notStatic ? "_" : ""), notStatic) + } + + if(!selectedSong.songSkin.stage){ + this.songStage.classList.add("song-stage-" + selectedSong.songStage) + this.setBgImage(this.songStage, assets.image["bg_stage_" + selectedSong.songStage].src) + }else if(selectedSong.songSkin.stage !== "none"){ + var prefix = selectedSong.songSkin.prefix || "" + this.setBgImage(this.songStage, assets.image[prefix + "bg_stage_" + songSkinName].src) + } + } + setDonBg(){ + var selectedSong = this.controller.selectedSong + var songSkinName = selectedSong.songSkin.name + var donLayers = [] + var filename = !selectedSong.songSkin.don && this.player === 2 ? "bg_don2_" : "bg_don_" + var prefix = "" + + this.donBg = document.createElement("div") + this.donBg.classList.add("donbg") + if(this.player === 2){ + this.donBg.classList.add("donbg-bottom") + } + for(var layer = 1; layer <= 3; layer++){ + var donLayer = document.createElement("div") + donLayer.classList.add("donlayer" + layer) + this.donBg.appendChild(donLayer) + if(layer !== 3){ + donLayers.push(donLayer) + } + } + this.songBg.parentNode.insertBefore(this.donBg, this.songBg) + var asset1, asset2 + if(!selectedSong.songSkin.don){ + this.donBg.classList.add("donbg-" + selectedSong.donBg) + this.setLayers(donLayers, filename + selectedSong.donBg, true) + asset1 = filename + selectedSong.donBg + "a" + asset2 = filename + selectedSong.donBg + "b" + }else if(selectedSong.songSkin.don !== "none"){ + var prefix = selectedSong.songSkin.prefix || "" + var notStatic = selectedSong.songSkin.don !== "static" + if(notStatic){ + this.donBg.classList.add("donbg-" + selectedSong.songSkin.don) + asset1 = filename + songSkinName + "_a" + asset2 = filename + songSkinName + "_b" + }else{ + asset1 = filename + songSkinName + asset2 = filename + songSkinName + } + this.setLayers(donLayers, prefix + filename + songSkinName + (notStatic ? "_" : ""), notStatic) + }else{ + return + } + var w1 = assets.image[prefix + asset1].width + var w2 = assets.image[prefix + asset2].width + this.donBg.style.setProperty("--sw", w1 > w2 ? w1 : w2) + this.donBg.style.setProperty("--sw1", w1) + this.donBg.style.setProperty("--sw2", w2) + this.donBg.style.setProperty("--sh1", assets.image[prefix + asset1].height) + this.donBg.style.setProperty("--sh2", assets.image[prefix + asset2].height) + } + setDonBgHeight(){ + this.donBg.style.setProperty("--h", getComputedStyle(this.donBg).height) + var gameDiv = this.gameDiv + gameDiv.classList.add("fix-animations") + setTimeout(()=>{ + gameDiv.classList.remove("fix-animations") + }, 50) + } + setLayers(elements, file, ab){ + if(ab){ + this.setBgImage(elements[0], assets.image[file + "a"].src) + this.setBgImage(elements[1], assets.image[file + "b"].src) + }else{ + this.setBgImage(elements[0], assets.image[file].src) + } + } + setBgImage(element, url){ + element.style.backgroundImage = "url('" + url + "')" + } + + drawMeasures(){ + var measures = this.controller.parsedSongData.measures + var ms = this.getMS() + var mul = this.slotPos.size / 106 + var distanceForCircle = this.winW / this.ratio - this.slotPos.x + var measureY = this.slotPos.y - 65 * mul + var measureH = 130 * mul + + measures.forEach(measure => { + var timeForDistance = this.posToMs(distanceForCircle, measure.speed * parseFloat(localStorage.getItem("baisoku") ?? "1", 10)) + var startingTime = measure.ms - timeForDistance + this.controller.videoLatency + var finishTime = measure.ms + this.posToMs(this.slotPos.x - this.slotPos.paddingLeft + 3, measure.speed * parseFloat(localStorage.getItem("baisoku") ?? "1", 10)) + this.controller.videoLatency + if(measure.visible && (!measure.branch || measure.branch.active) && ms >= startingTime && ms <= finishTime){ + var measureX = this.slotPos.x + this.msToPos(measure.ms - ms + this.controller.videoLatency, measure.speed * parseFloat(localStorage.getItem("baisoku") ?? "1", 10)) + this.ctx.strokeStyle = measure.branchFirst ? "#ff0" : "#bdbdbd" + this.ctx.lineWidth = 3 + this.ctx.beginPath() + this.ctx.moveTo(measureX, measureY) + this.ctx.lineTo(measureX, measureY + measureH) + this.ctx.stroke() + } + if(this.multiplayer !== 2 && ms >= measure.ms && measure.nextBranch && !measure.viewChecked && measure.gameChecked){ + measure.viewChecked = true + if(measure.nextBranch.active !== this.branch){ + this.branchAnimate.ms = ms + this.branchAnimate.fromBranch = this.branch + } + this.branch = measure.nextBranch.active + } + }) + } + updateNoteFaces(){ + var ms = this.getMS() + var lastNextBeat = this.nextBeat + while(ms >= this.nextBeat){ + this.nextBeat += this.beatInterval + if(this.controller.getCombo() >= 50){ + var face = Math.floor(ms / this.beatInterval) % 2 + this.noteFace = { + small: face, + big: face + 2 + } + }else{ + this.noteFace = { + small: 0, + big: 3 + } + } + if(this.nextBeat <= lastNextBeat){ + break + } + } + } + drawCircles(circles){ + var distanceForCircle = this.winW / this.ratio - this.slotPos.x + var ms = this.getMS() + + for(var i = circles.length; i--;){ + var circle = circles[i] + var speed = circle.speed * parseFloat(localStorage.getItem("baisoku") ?? "1", 10) + + var timeForDistance = this.posToMs(distanceForCircle + this.slotPos.size / 2, speed) + var startingTime = circle.ms - timeForDistance + this.controller.videoLatency + var finishTime = circle.endTime + this.posToMs(this.slotPos.x - this.slotPos.paddingLeft + this.slotPos.size * 2, speed) + this.controller.videoLatency + + if(circle.isPlayed <= 0 || circle.score === 0){ + if((!circle.branch || circle.branch.active) && ms >= startingTime && ms <= finishTime && circle.isPlayed !== -1){ + this.drawCircle(circle) + } + }else if(!circle.animating){ + // Start animation to gauge + circle.animate(ms) + } + } + var game = this.controller.game + for(var i = 0; i < game.songData.events.length; i++){ + var event = game.songData.events[i] + if(ms - this.controller.audioLatency >= event.ms && !event.beatMSCopied && (!event.branch || event.branch.active)){ + if(this.beatInterval !== event.beatMS){ + this.changeBeatInterval(event.beatMS) + } + event.beatMSCopied = true + } + if(ms - this.controller.audioLatency >= event.ms && !event.gogoChecked && (!event.branch || event.branch.active)){ + if(this.gogoTime != event.gogoTime){ + this.toggleGogoTime(event) + } + event.gogoChecked = true + } + } + } + drawAnimatedCircles(circles){ + var ms = this.getMS() + + for(var i = 0; i < circles.length; i++){ + var circle = circles[i] + + if(circle.animating){ + + var animT = circle.animT + if(ms < animT + 490){ + + if(circle.fixedPos){ + circle.fixedPos = false + circle.animT = ms + animT = ms + } + var animPoint = (ms - animT) / 490 + var bezierPoint = this.calcBezierPoint(this.draw.easeOut(animPoint), this.animateBezier) + this.drawCircle(circle, {x: bezierPoint.x, y: bezierPoint.y}) + + }else if(ms < animT + 810){ + var pos = this.animateBezier[3] + this.drawCircle(circle, pos, (ms - animT - 490) / 160) + }else{ + circle.animationEnded = true + } + } + } + } + calcBezierPoint(t, data){ + var at = 1 - t + data = data.slice() + + for(var i = 1; i < data.length; i++){ + for(var k = 0; k < data.length - i; k++){ + data[k] = { + x: data[k].x * at + data[k + 1].x * t, + y: data[k].y * at + data[k + 1].y * t + } + } + } + return data[0] + } + drawCircle(circle, circlePos, fade){ + var ctx = this.ctx + var mul = this.slotPos.size / 106 + + var bigCircleSize = 106 * mul / 2 + var circleSize = 70 * mul / 2 + var lyricsSize = 20 * mul + + var fill, size, faceID + var type = circle.type + var ms = this.getMS() + var circleMs = circle.ms + var endTime = circle.endTime + var animated = circle.animating + var speed = circle.speed * parseFloat(localStorage.getItem("baisoku") ?? "1", 10) + var played = circle.isPlayed + var drumroll = 0 + var endX = 0 + + const doron = localStorage.getItem("doron") ?? "false"; + + if(!circlePos){ + circlePos = { + x: this.slotPos.x + this.msToPos(circleMs - ms + this.controller.videoLatency, speed), + y: this.slotPos.y + } + } + if(animated){ + var noteFace = { + small: 0, + big: 3 + } + }else{ + var noteFace = this.noteFace + } + if(type === "don" || type === "daiDon" && played === 1){ + fill = "#f34728" + size = circleSize + faceID = noteFace.small + }else if(type === "ka" || type === "daiKa" && played === 1){ + fill = "#65bdbb" + size = circleSize + faceID = noteFace.small + }else if(type === "daiDon"){ + fill = "#f34728" + size = bigCircleSize + faceID = noteFace.big + }else if(type === "daiKa"){ + fill = "#65bdbb" + size = bigCircleSize + faceID = noteFace.big + }else if(type === "balloon"){ + if(animated){ + fill = "#f34728" + size = bigCircleSize * 0.8 + faceID = noteFace.big + }else{ + fill = "#f87700" + size = circleSize + faceID = noteFace.small + var h = size * 1.8 + if(circleMs + this.controller.audioLatency < ms && ms <= endTime + this.controller.audioLatency){ + circlePos.x = this.slotPos.x + }else if(ms > endTime + this.controller.audioLatency){ + circlePos.x = this.slotPos.x + this.msToPos(endTime - ms + this.controller.audioLatency, speed) + } + if (doron !== "true") { + ctx.drawImage(assets.image["balloon"], + circlePos.x + size - 4, + circlePos.y - h / 2 + 2, + h / 61 * 115, + h + ) + } + } + }else if(type === "drumroll" || type === "daiDrumroll"){ + fill = "#f3b500" + if(type == "drumroll"){ + size = circleSize + faceID = noteFace.small + }else{ + size = bigCircleSize + faceID = noteFace.big + } + endX = this.msToPos(endTime - circleMs, speed) + drumroll = endX > 50 ? 2 : 1 + + if (doron !== "true") { + ctx.fillStyle = fill + ctx.strokeStyle = "#000" + ctx.lineWidth = 3 + ctx.beginPath() + ctx.moveTo(circlePos.x, circlePos.y - size + 1.5) + ctx.arc(circlePos.x + endX, circlePos.y, size - 1.5, Math.PI / -2, Math.PI / 2) + ctx.lineTo(circlePos.x, circlePos.y + size - 1.5) + ctx.fill() + ctx.stroke() + } + } + + if((!fade || fade < 1) && doron !== "true"){ + // Main circle + ctx.fillStyle = fill + ctx.beginPath() + ctx.arc(circlePos.x, circlePos.y, size - 1, 0, Math.PI * 2) + ctx.fill() + // Face on circle + var drawSize = size + if(faceID < 2){ + drawSize *= bigCircleSize / circleSize + } + ctx.drawImage(assets.image[drumroll ? "notes_drumroll" : "notes"], + 0, 172 * faceID, + 172, 172, + circlePos.x - drawSize - 4, + circlePos.y - drawSize - 4, + drawSize * 2 + 8, + drawSize * 2 + 8 + ) + } + if(fade && !this.touchEnabled){ + ctx.globalAlpha = this.draw.easeOut(fade < 1 ? fade : 2 - fade) + ctx.fillStyle = "#fff" + ctx.beginPath() + ctx.arc(circlePos.x, circlePos.y, size - 1, 0, Math.PI * 2) + ctx.fill() + ctx.globalAlpha = 1 + } + if(!circle.animating && circle.text){ + // Text + var text = circle.text + var textX = circlePos.x + var textY = circlePos.y + 83 * mul + ctx.font = lyricsSize + "px Kozuka, Microsoft YaHei, sans-serif" + ctx.textBaseline = "middle" + ctx.textAlign = "center" + + if(drumroll === 2){ + var longText = text.split("ー") + text = longText[0] + var text0Width = ctx.measureText(longText[0]).width + var text1Width = ctx.measureText(longText[1]).width + } + + ctx.fillStyle = "#fff" + ctx.strokeStyle = "#000" + ctx.lineWidth = 5 + ctx.strokeText(text, textX, textY) + + if(drumroll === 2){ + ctx.strokeText(longText[1], textX + endX, textY) + + ctx.lineWidth = 4 + var x1 = textX + text0Width / 2 + var x2 = textX + endX - text1Width / 2 + ctx.beginPath() + ctx.moveTo(x1, textY - 2) + ctx.lineTo(x2, textY - 2) + ctx.lineTo(x2, textY + 1) + ctx.lineTo(x1, textY + 1) + ctx.closePath() + ctx.stroke() + ctx.fill() + } + + ctx.strokeStyle = "#fff" + ctx.lineWidth = 0.5 + + ctx.strokeText(text, textX, textY) + ctx.fillText(text, textX, textY) + + if(drumroll === 2){ + ctx.strokeText(longText[1], textX + endX, textY) + ctx.fillText(longText[1], textX + endX, textY) + } + } + } + fillComboCache(){ + var fontSize = 58 + var letterSpacing = fontSize * 0.67 + var glyphW = 51 + var glyphH = 65 + var textX = 5 + var textY = 5 + var letterBorder = fontSize * 0.15 + + this.comboCache.resize((glyphW + 1) * 20, glyphH + 1, this.ratio) + for(var orange = 0; orange < 2; orange++){ + for(var i = 0; i < 10; i++){ + this.comboCache.set({ + w: glyphW, + h: glyphH, + id: orange + "combo" + i + }, ctx => { + ctx.scale(0.9, 1) + if(orange){ + var grd = ctx.createLinearGradient( + (glyphW - glyphH) / 2, + 0, + (glyphW + glyphH) / 2, + glyphH + ) + grd.addColorStop(0.3, "#ff2000") + grd.addColorStop(0.5, "#ffc321") + grd.addColorStop(0.6, "#ffedb7") + grd.addColorStop(0.8, "#ffffce") + var fill = grd + }else{ + var fill = "#fff" + } + this.draw.layeredText({ + ctx: ctx, + text: i.toString(), + fontSize: fontSize, + fontFamily: "TnT, Meiryo, sans-serif", + x: textX, + y: textY + }, [ + {x: -2, y: -1, outline: "#000", letterBorder: letterBorder}, + {x: 3.5, y: 1.5}, + {x: 3, y: 1}, + {}, + {x: -2, y: -1, fill: "#fff"}, + {x: 3.5, y: 1.5, fill: fill}, + {x: 3, y: 1, fill: "rgba(0, 0, 0, 0.5)"}, + {fill: fill} + ]) + }) + } + } + this.globalAlpha = 0 + this.comboCache.get({ + ctx: this.ctx, + x: 0, + y: 0, + w: 54, + h: 77, + id: "combo0" + }) + this.globalAlpha = 1 + } + fillBranchCache(){ + var mul = this.slotPos.size / 106 + var textW = Math.floor(260 * mul) + var barH = Math.floor(130 * mul) + var branchNames = this.controller.game.branchNames + var textX = textW - 33 * mul + var textY = 63 * mul + var fontSize = (strings.id === "en" ? 33 : (strings.id === "ko" ? 38 : 43)) * mul + this.branchCache.resize((textW + 1), (barH + 1) * 3, this.ratio) + for(var i in branchNames){ + this.branchCache.set({ + w: textW, + h: barH, + id: branchNames[i] + }, ctx => { + var currentMap = this.branchMap[branchNames[i]] + ctx.font = this.draw.bold(this.font) + fontSize + "px " + this.font + ctx.lineJoin = "round" + ctx.miterLimit = 1 + ctx.textAlign = "right" + ctx.textBaseline = "middle" + ctx.lineWidth = 8 * mul + ctx.strokeStyle = currentMap.shadow + ctx.strokeText(strings.branch[branchNames[i]], textX, textY + 4 * mul) + ctx.strokeStyle = currentMap.stroke + ctx.strokeText(strings.branch[branchNames[i]], textX, textY) + ctx.fillStyle = currentMap.text + ctx.fillText(strings.branch[branchNames[i]], textX, textY) + }) + } + } + toggleGogoTime(circle) { + var startMS = circle.ms + this.controller.audioLatency; + this.gogoTime = circle.gogoTime; + if (circle.gogoTime || this.gogoTimeStarted !== -Infinity) { + this.gogoTimeStarted = startMS; + } + + if (this.gogoTime) { + this.assets.fireworks.forEach(fireworksAsset => { + fireworksAsset.zIndex = 10; + fireworksAsset.setAnimation("normal"); + fireworksAsset.setAnimationStart(startMS); + var length = fireworksAsset.getAnimationLength("normal"); + fireworksAsset.setAnimationEnd(length, () => { + fireworksAsset.setAnimation(false); + }); + }); + + this.assets.fireworks.sort((a, b) => a.zIndex - b.zIndex); + + this.assets.fire.setAnimation("normal"); + var don = this.assets.don; + don.setAnimation("gogostart"); + var length = don.getAnimationLength("gogo"); + don.setUpdateSpeed(4 / length); + var start = startMS - (startMS % this.beatInterval); + don.setAnimationStart(start); + var length = don.getAnimationLength("gogostart"); + don.setAnimationEnd(length, don.normalAnimation); + } +} + drawGogoTime(){ + var ms = this.getMS() + + if(this.gogoTime){ + var circles = this.controller.parsedSongData.circles + var lastCircle = circles[circles.length - 1] + var endTime = lastCircle.endTime + 3000 + if(ms >= endTime){ + this.toggleGogoTime({ + gogoTime: 0, + ms: endTime + }) + } + }else{ + var animation = this.assets.don.getAnimation() + var score = this.controller.getGlobalScore() + var cleared = this.rules.clearReached(score.gauge) + if(animation === "gogo" || cleared && animation === "normal" || !cleared && animation === "clear"){ + this.assets.don.normalAnimation() + } + if(ms >= this.gogoTimeStarted + 100){ + this.assets.fire.setAnimation(false) + } + } + } + updateCombo(combo){ + var don = this.assets.don + var animation = don.getAnimation() + if( + combo > 0 + && combo % 10 === 0 + && animation !== "10combo" + && animation !== "gogostart" + && animation !== "gogo" + ){ + don.setAnimation("10combo") + var ms = this.getMS() + don.setAnimationStart(ms) + var length = don.getAnimationLength("normal") + don.setUpdateSpeed(4 / length) + var length = don.getAnimationLength("10combo") + don.setAnimationEnd(length, don.normalAnimation) + } + } + displayScore(score, notPlayed, bigNote){ + if(!notPlayed){ + this.currentScore.ms = this.getMS() + this.currentScore.type = score + this.currentScore.bigNote = bigNote + + if(score > 0){ + var explosion = this.assets.explosion + explosion.type = (bigNote ? 0 : 2) + (score === 450 ? 0 : 1) + explosion.setAnimation("normal") + explosion.setAnimationStart(this.getMS()) + explosion.setAnimationEnd(bigNote ? 14 : 7, () => { + explosion.setAnimation(false) + }) + } + this.setDarkBg(score === 0) + }else{ + this.setDarkBg(true) + } + } + setDarkBg(miss){ + if(!miss && this.darkDonBg){ + this.darkDonBg = false + this.donBg.classList.remove("donbg-dark") + }else if(miss && !this.darkDonBg){ + this.darkDonBg = true + this.donBg.classList.add("donbg-dark") + } + } + posToMs(pos, speed){ + var circleSize = 70 * this.slotPos.size / 106 / 2 + return 140 / circleSize * pos / speed + } + msToPos(ms, speed){ + var circleSize = 70 * this.slotPos.size / 106 / 2 + return speed / (140 / circleSize) * ms + } + drawTouch(){ + if(this.touchEnabled){ + var ms = this.getMS() + var mul = this.ratio / this.pixelRatio + + var drumWidth = this.touchDrum.w * mul + var drumHeight = this.touchDrum.h * mul + if(drumHeight !== this.touchDrumHeight || drumWidth !== this.touchDrumWidth){ + this.touchDrumWidth = drumWidth + this.touchDrumHeight = drumHeight + this.touchDrumDiv.style.width = drumWidth + "px" + this.touchDrumDiv.style.height = drumHeight + "px" + } + if(this.touchAnimation){ + if(this.touch > ms - 100){ + if(!this.drumPadding){ + this.drumPadding = true + this.touchDrumImg.style.backgroundPositionY = "7px" + } + }else if(this.drumPadding){ + this.drumPadding = false + this.touchDrumImg.style.backgroundPositionY = "" + } + } + } + } + ontouch(event){ + if(!("changedTouches" in event)){ + event.changedTouches = [event] + } + for(var i = 0; i < event.changedTouches.length; i++){ + var touch = event.changedTouches[i] + event.preventDefault() + if(this.controller.game.paused){ + var mouse = this.mouseOffset(touch.pageX, touch.pageY) + var moveTo = this.pauseMouse(mouse.x, mouse.y) + if(moveTo !== null){ + this.pauseConfirm(moveTo) + } + }else if(!this.controller.autoPlayEnabled){ + var pageX = touch.pageX * this.pixelRatio + var pageY = touch.pageY * this.pixelRatio + + var c = this.touchCircle + var pi = Math.PI + var inPath = () => this.ctx.isPointInPath(pageX, pageY) + + this.ctx.beginPath() + this.ctx.ellipse(c.x, c.y, c.rx, c.ry, 0, pi, 0) + + if(inPath()){ + if(pageX < this.winW / 2){ + this.touchNote("don_l") + }else{ + this.touchNote("don_r") + } + }else{ + if(pageX < this.winW / 2){ + this.touchNote("ka_l") + }else{ + this.touchNote("ka_r") + } + } + this.touchEvents++ + } + } + } + touchNote(note){ + var keyboard = this.controller.keyboard + var ms = this.controller.game.getAccurateTime() + this.touch = ms + keyboard.setKey(false, note) + keyboard.setKey(true, note, ms) + } + mod(length, index){ + return ((index % length) + length) % length + } + pauseMove(pos, absolute){ + if(absolute){ + this.state.pausePos = pos + }else{ + this.state.pausePos = this.mod(this.pauseOptions.length, this.state.pausePos + pos) + } + this.state.moveMS = Date.now() - (absolute ? 0 : 500) + this.state.moveHover = null + } + pauseConfirm(pos){ + if(typeof pos === "undefined"){ + pos = this.state.pausePos + } + var game = this.controller.game + var state = game.calibrationState + switch(state){ + case "audioHelp": + pos = pos === 0 ? 2 : 0 + break + case "videoHelp": + if(pos === 0){ + assets.sounds["se_don"].play() + game.calibrationReset("audio") + return + }else{ + pos = 0 + } + break + case "results": + if(pos === 0){ + assets.sounds["se_don"].play() + game.calibrationReset("video") + return + }else{ + var input = settings.getItem("latency") + var output = {} + var progress = game.calibrationProgress + for(var i in input){ + if(i === "audio" || i === "video"){ + output[i] = progress[i] + }else{ + output[i] = input[i] + } + } + settings.setItem("latency", output) + pos = 2 + } + break + } + switch(pos){ + case 1: + this.controller.playSound("se_don", 0, true) + if(state === "video"){ + game.calibrationReset(state) + }else{ + this.controller.restartSong() + } + pageEvents.send("pause-restart") + break + case 2: + this.controller.playSound("se_don", 0, true) + this.controller.songSelection() + pageEvents.send("pause-song-select") + break + default: + this.controller.togglePause(false) + break + } + return true + } + onmousedown(event){ + if(this.controller.game.paused){ + if(event.which !== 1){ + return + } + var mouse = this.mouseOffset(event.offsetX, event.offsetY) + var moveTo = this.pauseMouse(mouse.x, mouse.y) + if(moveTo !== null){ + this.pauseConfirm(moveTo) + } + } + } + onmousemove(event){ + this.lastMousemove = this.getMS() + this.cursorHidden = false + + if(!this.multiplayer && this.controller.game.paused){ + var mouse = this.mouseOffset(event.offsetX, event.offsetY) + var moveTo = this.pauseMouse(mouse.x, mouse.y) + if(moveTo === null && this.state.moveHover === this.state.pausePos){ + this.state.moveMS = Date.now() - 500 + } + this.state.moveHover = moveTo + this.pointer(moveTo !== null) + } + } + mouseOffset(offsetX, offsetY){ + return { + x: (offsetX * this.pixelRatio - this.winW / 2) / this.ratio + (this.portrait ? 720 : 1280) / 2, + y: (offsetY * this.pixelRatio - this.winH / 2) / this.ratio + (this.portrait ? 1280 : 720) / 2 + } + } + pointer(enabled){ + if(!this.canvas){ + return + } + if(enabled && this.state.hasPointer === false){ + this.canvas.style.cursor = "pointer" + this.state.hasPointer = true + }else if(!enabled && this.state.hasPointer === true){ + this.canvas.style.cursor = "" + this.state.hasPointer = false + } + } + pauseMouse(x, y){ + if(this.portrait){ + var pauseScale = 766 / 720 + x = x * pauseScale + 257 + y = y * pauseScale - 328 + } + switch(this.controller.game.calibrationState){ + case "audioHelp": + case "videoHelp": + case "results": + if(554 - 90 * this.pauseOptions.length <= y && y <= 554 && 404 <= x && x <= 876){ + return Math.floor((y - 554 + 90 * this.pauseOptions.length) / 90) + } + break + default: + if(104 <= y && y <= 575 && 465 <= x && x <= 465 + 110 * this.pauseOptions.length){ + return Math.floor((x - 465) / 110) + } + break + } + return null + } + mouseIdle(){ + var lastMouse = pageEvents.getMouse() + if(lastMouse && !this.cursorHidden && !this.state.hasPointer){ + if(this.getMS() >= this.lastMousemove + 2000){ + this.canvas.style.cursor = "none" + this.cursorHidden = true + }else{ + this.canvas.style.cursor = "" + } + } + } + changeBeatInterval(beatMS){ + this.beatInterval = beatMS + this.assets.changeBeatInterval(beatMS) + } + getMS(){ + return this.ms + } + clean(){ + this.draw.clean() + this.assets.clean() + this.titleCache.clean() + this.comboCache.clean() + this.pauseCache.clean() + this.branchCache.clean() + this.nameplateCache.clean() + + versionDiv.classList.remove("version-hide") + loader.screen.parentNode.appendChild(versionDiv) + if(this.multiplayer !== 2){ + if(this.touchEnabled){ + pageEvents.remove(this.canvas, "touchstart") + pageEvents.remove(this.touchPauseBtn, "touchend") + this.gameDiv.classList.add("touch-results") + this.touchDrumDiv.parentNode.removeChild(this.touchDrumDiv) + delete this.touchDrumDiv + delete this.touchDrumImg + delete this.touchFullBtn + delete this.touchPauseBtn + } + } + if(!this.multiplayer){ + pageEvents.remove(this.canvas, "mousedown") + this.songBg.parentNode.removeChild(this.songBg) + this.songStage.parentNode.removeChild(this.songStage) + this.donBg.parentNode.removeChild(this.donBg) + delete this.donBg + delete this.songBg + delete this.songStage + } + pageEvents.mouseRemove(this) + + delete this.pauseMenu + delete this.gameDiv + delete this.canvas + delete this.ctx + } +} diff --git a/public/src/js/viewassets.js b/public/src/js/viewassets.js new file mode 100644 index 0000000..58dd7f8 --- /dev/null +++ b/public/src/js/viewassets.js @@ -0,0 +1,190 @@ +class ViewAssets{ + constructor(...args){ + this.init(...args) + } + init(view){ + this.view = view + this.controller = this.view.controller + this.allAssets = [] + this.ctx = this.view.ctx + + // Background + this.don = this.createAsset("background", frame => { + var imgw = 360 + var imgh = 184 + var w = imgw + var h = imgh + return { + sx: Math.floor(frame / 10) * (imgw + 2), + sy: (frame % 10) * (imgh + 2), + sw: imgw, + sh: imgh, + x: view.portrait ? -60 : 0, + y: view.portrait ? (view.player === 2 ? 560 : 35) : (view.player === 2 ? 360 : 0), + w: w / h * (h + 0.5), + h: h + 0.5 + } + }) + this.don.addFrames("normal", [ + 0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 , + 0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 , + 0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,7 ,8 ,9 ,10, + 11,11,11,11,10,9 ,8 ,7 ,13,12,12,13,14,15,16,17 + ], "don_anim_normal", this.controller.don) + this.don.addFrames("10combo", 22, "don_anim_10combo", this.controller.don) + this.don.addFrames("gogo", [ + 42,43,43,44,45,46,47,48,49,50,51,52,53,54, + 55,0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,11,12,13, + 14,14,15,16,17,18,19,20,21,22,23,24,25,26, + 27,28,29,30,31,32,33,34,35,36,37,38,39,40,41 + ], "don_anim_gogo", this.controller.don) + this.don.addFrames("gogostart", 27, "don_anim_gogostart", this.controller.don) + this.don.normalAnimation = () => { + if(this.view.gogoTime){ + var length = this.don.getAnimationLength("gogo") + this.don.setUpdateSpeed(4 / length) + this.don.setAnimation("gogo") + }else{ + var score = this.controller.getGlobalScore() + var cleared = this.controller.game.rules.clearReached(score.gauge) + if(cleared){ + this.don.setAnimationStart(0) + var length = this.don.getAnimationLength("clear") + this.don.setUpdateSpeed(2 / length) + this.don.setAnimation("clear") + }else{ + this.don.setAnimationStart(0) + var length = this.don.getAnimationLength("normal") + this.don.setUpdateSpeed(4 / length) + this.don.setAnimation("normal") + } + } + } + this.don.addFrames("clear", 30, "don_anim_clear", this.controller.don) + this.don.normalAnimation() + + // Bar + this.fire = this.createAsset("bar", frame => { + var imgw = 360 + var imgh = 370 + var scale = 130 + var ms = this.view.getMS() + var elapsed = ms - this.view.gogoTimeStarted + + var mul = this.view.slotPos.size / 106 + var barH = 130 * mul + + if(this.view.gogoTime){ + var grow = 3 - Math.min(200, elapsed) / 100 + this.ctx.globalAlpha = Math.min(200, elapsed) / 200 + }else{ + var grow = 1 - Math.min(100, elapsed) / 100 + } + var w = (barH * imgw) / scale * grow + var h = (barH * imgh) / scale * grow + this.ctx.globalCompositeOperation = "lighter" + return { + sx: frame * imgw, + sy: 0, + sw: imgw, + sh: imgh, + x: this.view.slotPos.x - w / 2, + y: this.view.slotPos.y - h / 2, + w: w, + h: h + } + }) + this.fire.addFrames("normal", 7, "fire_anim") + this.fire.setUpdateSpeed(1 / 8) + + // Notes + this.explosion = this.createAsset("notes", frame => { + var w = 222 + var h = 222 + var mul = this.view.slotPos.size / 106 + this.ctx.globalCompositeOperation = "screen" + var alpha = 1 + if(this.explosion.type < 2){ + if(frame < 2){ + mul *= 1 - (frame + 1) * 0.2 + }else if(frame > 9){ + alpha = Math.max(0, 1 - (frame - 10) / 4) + } + }else if(frame > 5){ + alpha = 0.5 + } + if(alpha < 1 && !this.controller.touchEnabled){ + this.ctx.globalAlpha = alpha + } + return { + sx: this.explosion.type * w, + sy: Math.min(3, Math.floor(frame / 2)) * h, + sw: w, + sh: h, + x: this.view.slotPos.x - w * mul / 2, + y: this.view.slotPos.y - h * mul / 2, + w: w * mul, + h: h * mul + } + }) + this.explosion.type = null + this.explosion.addFrames("normal", 14, "notes_explosion") + this.explosion.setUpdateSpeed(1, true) + + // Foreground + this.fireworks = [] + for(let i = 0; i < 5 ; i++){ + var fireworksAsset = this.createAsset("foreground", frame => { + var imgw = 230 + var imgh = 460 + var scale = 165 + var w = imgw + var h = imgh + var winW = this.view.winW / this.view.ratio + var winH = this.view.winH / this.view.ratio + return { + sx: Math.floor(frame / 4) * imgw, + sy: (frame % 4) * imgh, + sw: imgw, + sh: imgh, + x: winW / 4 * i - w / 2 * (i / 2), + y: winH - h, + w: w, + h: h + } + }) + fireworksAsset.addFrames("normal", 30, "fireworks_anim") + fireworksAsset.setUpdateSpeed(1 / 16) + this.fireworks.push(fireworksAsset) + } + + this.changeBeatInterval(this.view.beatInterval, true) + } + createAsset(layer, position){ + var asset = new CanvasAsset(this.view, layer, position) + this.allAssets.push(asset) + return asset + } + drawAssets(layer){ + this.allAssets.forEach(asset => { + if(layer === asset.layer){ + asset.draw() + } + }) + } + changeBeatInterval(beatMS, initial){ + this.allAssets.forEach(asset => { + asset.changeBeatInterval(beatMS, initial) + }) + } + clean(){ + if(this.don){ + this.don.clean() + } + delete this.ctx + delete this.don + delete this.fire + delete this.fireworks + delete this.allAssets + } +} diff --git a/public/src/views/about.html b/public/src/views/about.html new file mode 100644 index 0000000..0e168ee --- /dev/null +++ b/public/src/views/about.html @@ -0,0 +1,16 @@ +
+
+
+
+
+
+ + +
+
+
+
diff --git a/public/src/views/account.html b/public/src/views/account.html new file mode 100644 index 0000000..03f30ba --- /dev/null +++ b/public/src/views/account.html @@ -0,0 +1,53 @@ +
+ +
diff --git a/public/src/views/customsongs.html b/public/src/views/customsongs.html new file mode 100644 index 0000000..dec9b80 --- /dev/null +++ b/public/src/views/customsongs.html @@ -0,0 +1,32 @@ +
+
+
+
+
+
+ +
+
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/src/views/debug.html b/public/src/views/debug.html new file mode 100644 index 0000000..14d6062 --- /dev/null +++ b/public/src/views/debug.html @@ -0,0 +1,39 @@ +
Debug
+
+
+
Song offset:
+
+ x-+ +
+
Starting measure:
+
+ x-+ +
+
+
Branch:
+
+ x +
+
+
Music volume:
+
+ x-+ +
+
+
Lyrics offset:
+
+ x-+ +
+
+ + +
+
Restart song
+
Exit debug
+
+
diff --git a/public/src/views/game.html b/public/src/views/game.html new file mode 100644 index 0000000..60c7908 --- /dev/null +++ b/public/src/views/game.html @@ -0,0 +1,15 @@ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
diff --git a/public/src/views/loader.html b/public/src/views/loader.html new file mode 100644 index 0000000..e1bb5f0 --- /dev/null +++ b/public/src/views/loader.html @@ -0,0 +1,11 @@ +
+
+ 0% +
+
+
+
An error occurred, please refresh
+
+ Debug +
+
diff --git a/public/src/views/loadsong.html b/public/src/views/loadsong.html new file mode 100644 index 0000000..4412c05 --- /dev/null +++ b/public/src/views/loadsong.html @@ -0,0 +1,7 @@ +
+
+
+
+
+
+
diff --git a/public/src/views/login.html b/public/src/views/login.html new file mode 100644 index 0000000..8bcaea2 --- /dev/null +++ b/public/src/views/login.html @@ -0,0 +1,26 @@ +
+
+
+
+
+ +
+
+ + +
+
+
+
diff --git a/public/src/views/search.html b/public/src/views/search.html new file mode 100644 index 0000000..fb84198 --- /dev/null +++ b/public/src/views/search.html @@ -0,0 +1,9 @@ +
+ +
diff --git a/public/src/views/session.html b/public/src/views/session.html new file mode 100644 index 0000000..f47ce54 --- /dev/null +++ b/public/src/views/session.html @@ -0,0 +1,9 @@ +
+
+
+
+
+
+
+
+
diff --git a/public/src/views/settings.html b/public/src/views/settings.html new file mode 100644 index 0000000..c1f1a97 --- /dev/null +++ b/public/src/views/settings.html @@ -0,0 +1,34 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/src/views/songselect.html b/public/src/views/songselect.html new file mode 100644 index 0000000..0c47814 --- /dev/null +++ b/public/src/views/songselect.html @@ -0,0 +1,5 @@ +
+ +
+
+
diff --git a/public/src/views/titlescreen.html b/public/src/views/titlescreen.html new file mode 100644 index 0000000..449427f --- /dev/null +++ b/public/src/views/titlescreen.html @@ -0,0 +1,8 @@ +
+ +
+
+ + +
+
diff --git a/public/src/views/tutorial.html b/public/src/views/tutorial.html new file mode 100644 index 0000000..079f48b --- /dev/null +++ b/public/src/views/tutorial.html @@ -0,0 +1,7 @@ +
+
+
+
+
+
+
diff --git a/public/upload/index.html b/public/upload/index.html new file mode 100644 index 0000000..6c43997 --- /dev/null +++ b/public/upload/index.html @@ -0,0 +1,25 @@ + + + + + + 太鼓ウェブあっぷろーだー + + + + +

太鼓ウェブあっぷろーだー

+ +
+ + + + + +
+ + + +
+ + diff --git a/public/upload/roboto/Roboto-Black.ttf b/public/upload/roboto/Roboto-Black.ttf new file mode 100644 index 0000000..0112e7d Binary files /dev/null and b/public/upload/roboto/Roboto-Black.ttf differ diff --git a/public/upload/roboto/Roboto-BlackItalic.ttf b/public/upload/roboto/Roboto-BlackItalic.ttf new file mode 100644 index 0000000..b2c6aca Binary files /dev/null and b/public/upload/roboto/Roboto-BlackItalic.ttf differ diff --git a/public/upload/roboto/Roboto-Bold.ttf b/public/upload/roboto/Roboto-Bold.ttf new file mode 100644 index 0000000..43da14d Binary files /dev/null and b/public/upload/roboto/Roboto-Bold.ttf differ diff --git a/public/upload/roboto/Roboto-BoldItalic.ttf b/public/upload/roboto/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..bcfdab4 Binary files /dev/null and b/public/upload/roboto/Roboto-BoldItalic.ttf differ diff --git a/public/upload/roboto/Roboto-Italic.ttf b/public/upload/roboto/Roboto-Italic.ttf new file mode 100644 index 0000000..1b5eaa3 Binary files /dev/null and b/public/upload/roboto/Roboto-Italic.ttf differ diff --git a/public/upload/roboto/Roboto-Light.ttf b/public/upload/roboto/Roboto-Light.ttf new file mode 100644 index 0000000..e7307e7 Binary files /dev/null and b/public/upload/roboto/Roboto-Light.ttf differ diff --git a/public/upload/roboto/Roboto-LightItalic.ttf b/public/upload/roboto/Roboto-LightItalic.ttf new file mode 100644 index 0000000..2d277af Binary files /dev/null and b/public/upload/roboto/Roboto-LightItalic.ttf differ diff --git a/public/upload/roboto/Roboto-Medium.ttf b/public/upload/roboto/Roboto-Medium.ttf new file mode 100644 index 0000000..ac0f908 Binary files /dev/null and b/public/upload/roboto/Roboto-Medium.ttf differ diff --git a/public/upload/roboto/Roboto-MediumItalic.ttf b/public/upload/roboto/Roboto-MediumItalic.ttf new file mode 100644 index 0000000..fc36a47 Binary files /dev/null and b/public/upload/roboto/Roboto-MediumItalic.ttf differ diff --git a/public/upload/roboto/Roboto-Regular.ttf b/public/upload/roboto/Roboto-Regular.ttf new file mode 100644 index 0000000..ddf4bfa Binary files /dev/null and b/public/upload/roboto/Roboto-Regular.ttf differ diff --git a/public/upload/roboto/Roboto-Thin.ttf b/public/upload/roboto/Roboto-Thin.ttf new file mode 100644 index 0000000..2e0dee6 Binary files /dev/null and b/public/upload/roboto/Roboto-Thin.ttf differ diff --git a/public/upload/roboto/Roboto-ThinItalic.ttf b/public/upload/roboto/Roboto-ThinItalic.ttf new file mode 100644 index 0000000..084f9c0 Binary files /dev/null and b/public/upload/roboto/Roboto-ThinItalic.ttf differ diff --git a/public/upload/style.css b/public/upload/style.css new file mode 100644 index 0000000..4702cee --- /dev/null +++ b/public/upload/style.css @@ -0,0 +1,23 @@ +@font-face { + font-family: "Roboto"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(roboto/Roboto-Regular.ttf) format("truetype"); +} + +body * { + margin: 0; + font-family: "Roboto", sans-serif; + white-space: pre-line; + word-break: break-word; +} + +body { + margin: 0; + padding: 2rem; +} + +body > :not(:last-child) { + margin-bottom: 2rem; +} diff --git a/public/upload/upload.js b/public/upload/upload.js new file mode 100644 index 0000000..8b808d9 --- /dev/null +++ b/public/upload/upload.js @@ -0,0 +1,27 @@ +function uploadFiles() { + const form = document.querySelector("#upload-form"); + const formData = new FormData(form); + + fetch("/api/upload", { + method: "POST", + body: formData, + }) + .then((res) => { + if (res.ok) { + return res.json(); + } else { + throw new Error(res.url + " で " + res.status.toString() + " が発生しました。"); + } + }) + .then((data) => { + if (data.success) { + alert("おめでとう!ファイルの投稿に成功しました!"); + } else { + throw new Error(data.error); + } + }) + .catch((error) => { + console.error("エラー:", error); + document.querySelector("#error-view").textContent = error; + }); +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..baeccab --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +bcrypt==4.2.1 +ffmpy==0.5.0 +Flask==3.1.0 +Flask-Caching==2.3.0 +Flask-Session==0.8.0 +Flask-WTF==1.2.2 +gunicorn==23.0.0 +jsonschema==4.23.0 +pymongo==4.11 +redis==5.2.1 +requests==2.32.3 +websockets==14.2 +Flask-Limiter==3.10.1 +msgspec==0.19.0 diff --git a/schema.py b/schema.py new file mode 100644 index 0000000..a012b02 --- /dev/null +++ b/schema.py @@ -0,0 +1,82 @@ +import jsonschema + +def validate(data, schema): + try: + jsonschema.validate(data, schema) + return True + except jsonschema.exceptions.ValidationError: + return False + +register = { + '$schema': 'http://json-schema.org/schema#', + 'type': 'object', + 'properties': { + 'username': {'type': 'string'}, + 'password': {'type': 'string'} + } +} + +login = { + '$schema': 'http://json-schema.org/schema#', + 'type': 'object', + 'properties': { + 'username': {'type': 'string'}, + 'password': {'type': 'string'}, + 'remember': {'type': 'boolean'} + } +} + +update_display_name = { + '$schema': 'http://json-schema.org/schema#', + 'type': 'object', + 'properties': { + 'display_name': {'type': 'string'} + } +} + +update_don = { + '$schema': 'http://json-schema.org/schema#', + 'type': 'object', + 'properties': { + 'body_fill': {'type': 'string'}, + 'face_fill': {'type': 'string'} + } +} + +update_password = { + '$schema': 'http://json-schema.org/schema#', + 'type': 'object', + 'properties': { + 'current_password': {'type': 'string'}, + 'new_password': {'type': 'string'} + } +} + +delete_account = { + '$schema': 'http://json-schema.org/schema#', + 'type': 'object', + 'properties': { + 'password': {'type': 'string'} + } +} + +scores_save = { + '$schema': 'http://json-schema.org/schema#', + 'type': 'object', + 'properties': { + 'scores': { + 'type': 'array', + 'items': {'$ref': '#/definitions/score'} + }, + 'is_import': {'type': 'boolean'} + }, + 'definitions': { + 'score': { + 'type': 'object', + 'properties': { + 'hash': {'type': 'string'}, + 'score': {'type': 'string'} + } + } + } +} diff --git a/server.py b/server.py new file mode 100644 index 0000000..65d0fdf --- /dev/null +++ b/server.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python3 + +import argparse +import asyncio +import websockets +import json +import random +import sys + +parser = argparse.ArgumentParser(description='Run the taiko-web multiplayer server.') +parser.add_argument('port', type=int, metavar='PORT', nargs='?', default=34802, help='Port to listen on.') +parser.add_argument('-b', '--bind-address', default='localhost', help='Bind server to address.') +parser.add_argument('-o', '--allow-origin', action='append', help='Limit incoming connections to the specified origin. Can be specified multiple times.') +args = parser.parse_args() + +server_status = { + "waiting": {}, + "users": [], + "invites": {} +} +consonants = "bcdfghjklmnpqrstvwxyz" + +def msgobj(msg_type, value=None): + if value == None: + return json.dumps({"type": msg_type}) + else: + return json.dumps({"type": msg_type, "value": value}) + +def status_event(): + value = [] + for id, userDiff in server_status["waiting"].items(): + value.append({ + "id": id, + "diff": userDiff["diff"] + }) + return msgobj("users", value) + +def get_invite(): + return "".join([random.choice(consonants) for x in range(5)]) + +async def notify_status(): + ready_users = [user for user in server_status["users"] if "ws" in user and user["action"] == "ready"] + if ready_users: + sent_msg = status_event() + await asyncio.wait([user["ws"].send(sent_msg) for user in ready_users]) + +async def connection(ws, path): + # User connected + user = { + "ws": ws, + "action": "ready", + "session": False, + "name": None, + "don": None + } + server_status["users"].append(user) + try: + # Notify user about other users + await ws.send(status_event()) + while True: + try: + message = await asyncio.wait_for(ws.recv(), timeout=10) + except asyncio.TimeoutError: + # Keep user connected + pong_waiter = await ws.ping() + try: + await asyncio.wait_for(pong_waiter, timeout=10) + except asyncio.TimeoutError: + # Disconnect + break + except websockets.exceptions.ConnectionClosed: + # Connection closed + break + else: + # Message received + try: + data = json.loads(message) + except json.decoder.JSONDecodeError: + data = {} + action = user["action"] + msg_type = data["type"] if "type" in data else None + value = data["value"] if "value" in data else None + if action == "ready": + # Not playing or waiting + if msg_type == "join": + if value == None: + continue + waiting = server_status["waiting"] + id = value["id"] if "id" in value else None + diff = value["diff"] if "diff" in value else None + user["name"] = value["name"] if "name" in value else None + user["don"] = value["don"] if "don" in value else None + if not id or not diff: + continue + if id not in waiting: + # Wait for another user + user["action"] = "waiting" + user["gameid"] = id + waiting[id] = { + "user": user, + "diff": diff + } + await ws.send(msgobj("waiting")) + else: + # Join the other user and start game + user["name"] = value["name"] if "name" in value else None + user["don"] = value["don"] if "don" in value else None + user["other_user"] = waiting[id]["user"] + waiting_diff = waiting[id]["diff"] + del waiting[id] + if "ws" in user["other_user"]: + user["action"] = "loading" + user["other_user"]["action"] = "loading" + user["other_user"]["other_user"] = user + user["other_user"]["player"] = 1 + user["player"] = 2 + await asyncio.wait([ + ws.send(msgobj("gameload", {"diff": waiting_diff, "player": 2})), + user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff, "player": 1})), + ws.send(msgobj("name", { + "name": user["other_user"]["name"], + "don": user["other_user"]["don"] + })), + user["other_user"]["ws"].send(msgobj("name", { + "name": user["name"], + "don": user["don"] + })) + ]) + else: + # Wait for another user + del user["other_user"] + user["action"] = "waiting" + user["gameid"] = id + waiting[id] = { + "user": user, + "diff": diff + } + await ws.send(msgobj("waiting")) + # Update others on waiting players + await notify_status() + elif msg_type == "invite": + if value and "id" in value and value["id"] == None: + # Session invite link requested + invite = get_invite() + server_status["invites"][invite] = user + user["action"] = "invite" + user["session"] = invite + user["name"] = value["name"] if "name" in value else None + user["don"] = value["don"] if "don" in value else None + await ws.send(msgobj("invite", invite)) + elif value and "id" in value and value["id"] in server_status["invites"]: + # Join a session with the other user + user["name"] = value["name"] if "name" in value else None + user["don"] = value["don"] if "don" in value else None + user["other_user"] = server_status["invites"][value["id"]] + del server_status["invites"][value["id"]] + if "ws" in user["other_user"]: + user["other_user"]["other_user"] = user + user["action"] = "invite" + user["session"] = value["id"] + user["other_user"]["player"] = 1 + user["player"] = 2 + await asyncio.wait([ + ws.send(msgobj("session", {"player": 2})), + user["other_user"]["ws"].send(msgobj("session", {"player": 1})), + ws.send(msgobj("invite")), + ws.send(msgobj("name", { + "name": user["other_user"]["name"], + "don": user["other_user"]["don"] + })), + user["other_user"]["ws"].send(msgobj("name", { + "name": user["name"], + "don": user["don"] + })) + ]) + else: + del user["other_user"] + await ws.send(msgobj("gameend")) + else: + # Session code is invalid + await ws.send(msgobj("gameend")) + elif action == "waiting" or action == "loading" or action == "loaded": + # Waiting for another user + if msg_type == "leave": + # Stop waiting + if user["session"]: + if "other_user" in user and "ws" in user["other_user"]: + user["action"] = "songsel" + await asyncio.wait([ + ws.send(msgobj("left")), + user["other_user"]["ws"].send(msgobj("users", [])) + ]) + else: + user["action"] = "ready" + user["session"] = False + await asyncio.wait([ + ws.send(msgobj("gameend")), + ws.send(status_event()) + ]) + else: + del server_status["waiting"][user["gameid"]] + del user["gameid"] + user["action"] = "ready" + await asyncio.wait([ + ws.send(msgobj("left")), + notify_status() + ]) + if action == "loading": + if msg_type == "gamestart": + user["action"] = "loaded" + if user["other_user"]["action"] == "loaded": + user["action"] = "playing" + user["other_user"]["action"] = "playing" + sent_msg = msgobj("gamestart") + await asyncio.wait([ + ws.send(sent_msg), + user["other_user"]["ws"].send(sent_msg) + ]) + elif action == "playing": + # Playing with another user + if "other_user" in user and "ws" in user["other_user"]: + if msg_type == "note"\ + or msg_type == "drumroll"\ + or msg_type == "branch"\ + or msg_type == "gameresults": + await user["other_user"]["ws"].send(msgobj(msg_type, value)) + elif msg_type == "songsel" and user["session"]: + user["action"] = "songsel" + user["other_user"]["action"] = "songsel" + sent_msg1 = msgobj("songsel") + sent_msg2 = msgobj("users", []) + await asyncio.wait([ + ws.send(sent_msg1), + ws.send(sent_msg2), + user["other_user"]["ws"].send(sent_msg1), + user["other_user"]["ws"].send(sent_msg2) + ]) + elif msg_type == "gameend": + # User wants to disconnect + user["action"] = "ready" + user["other_user"]["action"] = "ready" + sent_msg1 = msgobj("gameend") + sent_msg2 = status_event() + await asyncio.wait([ + ws.send(sent_msg1), + ws.send(sent_msg2), + user["other_user"]["ws"].send(sent_msg1), + user["other_user"]["ws"].send(sent_msg2) + ]) + del user["other_user"]["other_user"] + del user["other_user"] + else: + # Other user disconnected + user["action"] = "ready" + user["session"] = False + await asyncio.wait([ + ws.send(msgobj("gameend")), + ws.send(status_event()) + ]) + elif action == "invite": + if msg_type == "leave": + # Cancel session invite + if user["session"] in server_status["invites"]: + del server_status["invites"][user["session"]] + user["action"] = "ready" + user["session"] = False + if "other_user" in user and "ws" in user["other_user"]: + user["other_user"]["action"] = "ready" + user["other_user"]["session"] = False + sent_msg = status_event() + await asyncio.wait([ + ws.send(msgobj("left")), + ws.send(sent_msg), + user["other_user"]["ws"].send(msgobj("gameend")), + user["other_user"]["ws"].send(sent_msg) + ]) + else: + await asyncio.wait([ + ws.send(msgobj("left")), + ws.send(status_event()) + ]) + elif msg_type == "songsel" and "other_user" in user: + if "ws" in user["other_user"]: + user["action"] = "songsel" + user["other_user"]["action"] = "songsel" + sent_msg = msgobj(msg_type) + await asyncio.wait([ + ws.send(sent_msg), + user["other_user"]["ws"].send(sent_msg) + ]) + else: + user["action"] = "ready" + user["session"] = False + await asyncio.wait([ + ws.send(msgobj("gameend")), + ws.send(status_event()) + ]) + elif action == "songsel": + # Session song selection + if "other_user" in user and "ws" in user["other_user"]: + if msg_type == "songsel" or msg_type == "catjump": + # Change song select position + if user["other_user"]["action"] == "songsel" and type(value) is dict: + value["player"] = user["player"] + sent_msg = msgobj(msg_type, value) + await asyncio.wait([ + ws.send(sent_msg), + user["other_user"]["ws"].send(sent_msg) + ]) + elif msg_type == "crowns" or msg_type == "getcrowns": + if user["other_user"]["action"] == "songsel": + sent_msg = msgobj(msg_type, value) + await asyncio.wait([ + user["other_user"]["ws"].send(sent_msg) + ]) + elif msg_type == "join": + # Start game + if value == None: + continue + id = value["id"] if "id" in value else None + diff = value["diff"] if "diff" in value else None + if not id or not diff: + continue + if user["other_user"]["action"] == "waiting": + user["action"] = "loading" + user["other_user"]["action"] = "loading" + await asyncio.wait([ + ws.send(msgobj("gameload", {"diff": user["other_user"]["gamediff"]})), + user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff})) + ]) + else: + user["action"] = "waiting" + user["gamediff"] = diff + await user["other_user"]["ws"].send(msgobj("users", [{ + "id": id, + "diff": diff + }])) + elif msg_type == "gameend": + # User wants to disconnect + user["action"] = "ready" + user["session"] = False + user["other_user"]["action"] = "ready" + user["other_user"]["session"] = False + sent_msg1 = msgobj("gameend") + sent_msg2 = status_event() + await asyncio.wait([ + ws.send(sent_msg1), + user["other_user"]["ws"].send(sent_msg1) + ]) + await asyncio.wait([ + ws.send(sent_msg2), + user["other_user"]["ws"].send(sent_msg2) + ]) + del user["other_user"]["other_user"] + del user["other_user"] + else: + # Other user disconnected + user["action"] = "ready" + user["session"] = False + await asyncio.wait([ + ws.send(msgobj("gameend")), + ws.send(status_event()) + ]) + finally: + # User disconnected + del user["ws"] + del server_status["users"][server_status["users"].index(user)] + if "other_user" in user and "ws" in user["other_user"]: + user["other_user"]["action"] = "ready" + user["other_user"]["session"] = False + await asyncio.wait([ + user["other_user"]["ws"].send(msgobj("gameend")), + user["other_user"]["ws"].send(status_event()) + ]) + del user["other_user"]["other_user"] + if user["action"] == "waiting": + del server_status["waiting"][user["gameid"]] + await notify_status() + elif user["action"] == "invite" and user["session"] in server_status["invites"]: + del server_status["invites"][user["session"]] + +port = args.port +print('Starting server on port %d' % port) +loop = asyncio.get_event_loop() +tasks = asyncio.gather( + websockets.serve(connection, args.bind_address, port, origins=args.allow_origin) +) +try: + loop.run_until_complete(tasks) + loop.run_forever() +except KeyboardInterrupt: + print("Stopping server") + def shutdown_exception_handler(loop, context): + if "exception" not in context or not isinstance(context["exception"], asyncio.CancelledError): + loop.default_exception_handler(context) + loop.set_exception_handler(shutdown_exception_handler) + tasks = asyncio.gather(*asyncio.all_tasks(loop=loop), loop=loop, return_exceptions=True) + tasks.add_done_callback(lambda t: loop.stop()) + tasks.cancel() + while not tasks.done() and not loop.is_closed(): + loop.run_forever() +finally: + if hasattr(loop, "shutdown_asyncgens"): + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.close() + diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..3c8fc6b --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,26 @@ + + + + + Taiko Web Admin + + + + + + + +
+ +
+ +
+
+ {% block content %}{% endblock %} +
+
+ + diff --git a/templates/admin_song_detail.html b/templates/admin_song_detail.html new file mode 100644 index 0000000..17932ee --- /dev/null +++ b/templates/admin_song_detail.html @@ -0,0 +1,151 @@ +{% extends 'admin.html' %} +{% block content %} +{% if song.title_lang.en and song.title_lang.en != song.title %} +

{{ song.title_lang.en }} ({{ song.title }}) (ID: {{ song.id }})

+{% else %} +

{{ song.title }} (ID: {{ song.id }})

+{% endif %} +{% for cat, message in get_flashed_messages(with_categories=true) %} +
{{ message }}
+{% endfor %} +
+
+ + +
+ +
+ +
+

Title

+ + + + + + + + + + + + +
+ +
+

Subtitle

+ + + + + + + + + + + + +
+ +
+

Courses

+ + + + + + + + + + + + + + + +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ + +
+ {% if admin.user_level >= 100 %} +
+ + +
+ {% endif %} +
+{% endblock %} + diff --git a/templates/admin_song_new.html b/templates/admin_song_new.html new file mode 100644 index 0000000..09b1991 --- /dev/null +++ b/templates/admin_song_new.html @@ -0,0 +1,140 @@ +{% extends 'admin.html' %} +{% block content %} +

New song (ID: {{ id }})

+{% for message in get_flashed_messages() %} +
{{ message }}
+{% endfor %} +
+
+ + +
+ +
+ +
+

Title

+ + + + + + + + + + + + +
+ +
+

Subtitle

+ + + + + + + + + + + + +
+ +
+

Courses

+ + + + + + + + + + + + + + + +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ + +
+
+{% endblock %} diff --git a/templates/admin_songs.html b/templates/admin_songs.html new file mode 100644 index 0000000..8a89a37 --- /dev/null +++ b/templates/admin_songs.html @@ -0,0 +1,25 @@ +{% extends 'admin.html' %} +{% block content %} +{% if admin.user_level >= 100 %} +New song +{% endif %} +

Songs

+{% for message in get_flashed_messages() %} +
{{ message }}
+{% endfor %} +{% for song in songs %} + +
+ {% if song.title_lang.en and song.title_lang.en != song.title %} +

{{ song.id }}. + {% if not song.enabled %}(Not enabled){% endif %} + {{ song.title_lang.en }} ({{ song.title }})

+ {% else %} +

{{ song.id }}. + {% if not song.enabled %}(Not enabled){% endif %} + {{ song.title }}

+ {% endif %} +
+
+{% endfor %} +{% endblock %} diff --git a/templates/admin_users.html b/templates/admin_users.html new file mode 100644 index 0000000..0183940 --- /dev/null +++ b/templates/admin_users.html @@ -0,0 +1,21 @@ +{% extends 'admin.html' %} +{% block content %} +

Users

+{% for message in get_flashed_messages() %} +
{{ message }}
+{% endfor %} +
+
+ + +
+ + + + +
+ + +
+
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..26873f9 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,46 @@ + + + + + 太鼓ウェブ - Taiko Web | (゚∀゚) + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ {% if version.version and version.commit_short and version.commit %} + taiko-web ver.{{version.version}} ({{version.commit_short}}) + {% else %} + taiko-web vRAINBOW-BETA4 + {% endif %} +
+ + + + + diff --git a/templates/privacy.txt b/templates/privacy.txt new file mode 100644 index 0000000..9816989 --- /dev/null +++ b/templates/privacy.txt @@ -0,0 +1,35 @@ +Taiko Web is committed to safeguarding and preserving the privacy of our website visitors and players. This Privacy Policy explains what happens to any personal data that you provide to us or that we collect from you while you play our game or visit our site. + +1. The Types and Sources of Data We Collect + +1.1 Basic Account Data +When setting up an account, we will collect your user name and password. + +1.2 Game High Score Data +When setting a high score in-game, we will collect and store your score. This information may become available to other users when you start a multiplayer game with them. + +1.3 Cookies +We use "Cookies", which are text files placed on your computer, and similar technologies to improve the services we are offering and to improve website functionality. + +2. Who Has Access to Data + +Taiko Web does not sell Personal Data. However, we may share or provide access to each of the categories of Personal Data we collect as necessary for the following business purposes. + +2.1 Taiko Web includes multiplayer features, where users can play the game with each other. When playing with other users, please be aware that the information is being made available to them; therefore, you are doing so at your own risk. +{% if integration %} +3. Google Drive Integration + +You can use the Google Drive integration to let Taiko Web make your Taiko chart files and other related files available in-game. + +Applications that integrate with a Google account must declare their intent by requesting permissions. These permissions to your account must be granted in order for Taiko Web to integrate with Google accounts. Below is a list of these permissions and why they are required. At no time will Taiko Web request or have access to your Google account password. + +3.1 "See and download all your Google Drive files" Permission +When selecting a folder with the Google Drive file picker, Taiko Web instructs your Browser to recursively download all the files of that folder directly into your computer's memory. Limitation of Google Drive's permission model requires us to request access to all your Google Drive files, however, Taiko Web will only access the selected folder and its children, and only when requested. File parsing is handled locally; none of your Google Drive files is ever sent to our servers or third parties. +{% endif %}{% if config.email %} +{% if integration %}4{% else %}3{% endif %}. Contact Info + +You can contact the Taiko Web operator at the email address below. + +{{config.email}} +{% endif %} +Revision Date: {{last_modified}} diff --git a/tjaf.py b/tjaf.py new file mode 100644 index 0000000..67162cd --- /dev/null +++ b/tjaf.py @@ -0,0 +1,90 @@ +import os +import re +from typing import Dict, Optional + +class Tja: + def __init__(self, text: str): + self.text = text + self.title: Optional[str] = None + self.subtitle: Optional[str] = None + self.wave: Optional[str] = None + self.offset: Optional[float] = None + self.courses: Dict[str, Dict[str, Optional[int]]] = {} + self._parse() + + def _parse(self) -> None: + lines = self.text.split("\n") + current_course: Optional[str] = None + for raw in lines: + line = raw.strip() + if not line: + continue + if ":" in line: + k, v = line.split(":", 1) + key = k.strip().upper() + val = v.strip() + if key == "TITLE": + self.title = val or None + elif key == "SUBTITLE": + self.subtitle = val or None + elif key == "WAVE": + self.wave = val or None + elif key == "OFFSET": + try: + self.offset = float(val) + except ValueError: + self.offset = None + elif key == "COURSE": + course_map = { + "EASY": "easy", + "NORMAL": "normal", + "HARD": "hard", + "ONI": "oni", + "EDIT": "ura", + "URA": "ura", + } + current_course = course_map.get(val.strip().upper()) + if current_course and current_course not in self.courses: + self.courses[current_course] = {"stars": None, "branch": False} + elif key == "LEVEL" and current_course: + try: + stars = int(re.split(r"\s+", val)[0]) + except ValueError: + stars = None + self.courses[current_course]["stars"] = stars + else: + if current_course and (line.startswith("BRANCHSTART") or line.startswith("#BRANCHSTART")): + self.courses[current_course]["branch"] = True + + def to_mongo(self, song_id: str, created_ns: int) -> Dict: + ext = None + if self.wave: + base = os.path.basename(self.wave) + _, e = os.path.splitext(base) + if e: + ext = e.lstrip(".").lower() + if not ext: + ext = "mp3" + courses_out: Dict[str, Optional[Dict[str, Optional[int]]]] = {} + for name in ["easy", "normal", "hard", "oni", "ura"]: + courses_out[name] = self.courses.get(name) or None + return { + "id": song_id, + "type": "tja", + "title": self.title, + "subtitle": self.subtitle, + "title_lang": {"ja": self.title, "en": None, "cn": None, "tw": None, "ko": None}, + "subtitle_lang": {"ja": self.subtitle, "en": None, "cn": None, "tw": None, "ko": None}, + "courses": courses_out, + "enabled": False, + "category_id": None, + "music_type": ext, + "offset": self.offset or 0, + "skin_id": None, + "preview": 0, + "volume": 1.0, + "maker_id": None, + "hash": None, + "order": song_id, + "created_ns": created_ns, + } \ No newline at end of file diff --git a/tools/categories.json b/tools/categories.json new file mode 100644 index 0000000..72d5c47 --- /dev/null +++ b/tools/categories.json @@ -0,0 +1,157 @@ +[{ + "id": 1, + "title": "Pop", + "title_lang": { + "ja": "J-POP", + "en": "Pop", + "cn": "流行音乐", + "tw": "流行音樂", + "ko": "POP" + }, + "song_skin": { + "sort": 1, + "background": "#219fbb", + "border": ["#7ec3d3", "#0b6773"], + "outline": "#005058", + "info_fill": "#004d68", + "bg_img": "bg_genre_0.png" + }, + "aliases": [ + "jpop", + "j-pop", + "pops" + ] +},{ + "id": 2, + "title": "Anime", + "title_lang": { + "ja": "アニメ", + "en": "Anime", + "cn": "卡通动画音乐", + "tw": "卡通動畫音樂", + "ko": "애니메이션" + }, + "song_skin": { + "sort": 2, + "background": "#ff9700", + "border": ["#ffdb8c", "#e75500"], + "outline": "#9c4100", + "info_fill": "#9c4002", + "bg_img": "bg_genre_1.png" + }, + "aliases": null +},{ + "id": 3, + "title": "VOCALOID™ Music", + "title_lang": { + "ja": "ボーカロイド™曲", + "en": "VOCALOID™ Music" + }, + "song_skin": { + "sort": 3, + "background": "#def2ef", + "border": ["#f7fbff", "#79919f"], + "outline": "#5a6584", + "info_fill": "#546184", + "bg_img": "bg_genre_2.png" + }, + "aliases": [ + "ボーカロイド曲", + "ボーカロイド", + "vocaloid music", + "vocaloidmusic", + "vocaloid" + ] +},{ + "id": 4, + "title": "Variety", + "title_lang": { + "ja": "バラエティ", + "en": "Variety", + "cn": "综合音乐", + "tw": "綜合音樂", + "ko": "버라이어티" + }, + "song_skin": { + "sort": 4, + "background": "#8fd321", + "border": ["#f7fbff", "#587d0b"], + "outline": "#374c00", + "info_fill": "#3c6800", + "bg_img": "bg_genre_3.png" + }, + "aliases": [ + "バラエティー", + "どうよう", + "童謡・民謡", + "children", + "children/folk", + "children-folk" + ] +},{ + "id": 5, + "title": "Classical", + "title_lang": { + "ja": "クラシック", + "en": "Classical", + "cn": "古典音乐", + "tw": "古典音樂", + "ko": "클래식" + }, + "song_skin": { + "sort": 5, + "background": "#d1a016", + "border": ["#e7cf6b", "#9a6b00"], + "outline": "#734d00", + "info_fill": "#865800", + "bg_img": "bg_genre_4.png" + }, + "aliases": [ + "クラッシック", + "classic" + ] +},{ + "id": 6, + "title": "Game Music", + "title_lang": { + "ja": "ゲームミュージック", + "en": "Game Music", + "cn": "游戏音乐", + "tw": "遊戲音樂", + "ko": "게임" + }, + "song_skin": { + "sort": 6, + "background": "#9c72c0", + "border": ["#bda2ce", "#63407e"], + "outline": "#4b1c74", + "info_fill": "#4f2886", + "bg_img": "bg_genre_5.png" + }, + "aliases": [ + "game", + "gamemusic" + ] +},{ + "id": 7, + "title": "NAMCO Original", + "title_lang": { + "ja": "ナムコオリジナル", + "en": "NAMCO Original", + "cn": "NAMCO原创音乐", + "tw": "NAMCO原創音樂", + "ko": "남코 오리지널" + }, + "song_skin": { + "sort": 7, + "background": "#ff5716", + "border": ["#ffa66b", "#b53000"], + "outline": "#941c00", + "info_fill": "#961e00", + "bg_img": "bg_genre_6.png" + }, + "aliases": [ + "namco", + "namcooriginal" + ] +}] diff --git a/tools/generate_previews.py b/tools/generate_previews.py new file mode 100644 index 0000000..6847410 --- /dev/null +++ b/tools/generate_previews.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# .ogg preview generator for use when app and songs are on two different machines + +import argparse +import requests +import os.path +from ffmpy import FFmpeg + + +parser = argparse.ArgumentParser(description='Generate song previews.') +parser.add_argument('site', help='Instance URL, eg. https://taiko.bui.pm') +parser.add_argument('song_dir', help='Path to songs directory, eg. /srv/taiko/public/taiko/songs') +parser.add_argument('--overwrite', action='store_true', help='Overwrite existing previews') +args = parser.parse_args() + + +if __name__ == '__main__': + songs = requests.get('{}/api/songs'.format(args.site)).json() + for i, song in enumerate(songs): + print('{}/{} {} (id: {})'.format(i + 1, len(songs), song['title'], song['id'])) + + song_path = '{}/{}/main.{}'.format(args.song_dir, song['id'], song['music_type'] if 'music_type' in song else 'mp3') + prev_path = '{}/{}/preview.ogg'.format(args.song_dir, song['id']) + + if os.path.isfile(song_path): + if not os.path.isfile(prev_path) or args.overwrite: + if not song['preview'] or song['preview'] <= 0: + print('Skipping due to no preview') + continue + + print('Making preview.ogg') + ff = FFmpeg(inputs={song_path: '-ss %s' % song['preview']}, + outputs={prev_path: '-codec:a libvorbis -b:a 64k -ar 32000 -y -loglevel panic'}) + ff.run() + else: + print('Preview already exists') + else: + print('song file not found') diff --git a/tools/get_version.bat b/tools/get_version.bat new file mode 100644 index 0000000..72f2562 --- /dev/null +++ b/tools/get_version.bat @@ -0,0 +1,4 @@ +@echo off +( +git log -1 --pretty="format:{\"commit\": \"%%H\", \"commit_short\": \"%%h\", \"version\": \"%%ad\"}" --date="format:%%y.%%m.%%d" +) > ../version.json diff --git a/tools/get_version.sh b/tools/get_version.sh new file mode 100644 index 0000000..13ee916 --- /dev/null +++ b/tools/get_version.sh @@ -0,0 +1,3 @@ +#!/bin/bash +toplevel=$( git rev-parse --show-toplevel ) +git log -1 --pretty="format:{\"commit\": \"%H\", \"commit_short\": \"%h\", \"version\": \"%ad\"}" --date="format:%y.%m.%d" > "$toplevel/version.json" diff --git a/tools/hooks/post-checkout b/tools/hooks/post-checkout new file mode 100644 index 0000000..12285a5 --- /dev/null +++ b/tools/hooks/post-checkout @@ -0,0 +1,2 @@ +#!/bin/bash +./tools/get_version.sh diff --git a/tools/hooks/post-commit b/tools/hooks/post-commit new file mode 100644 index 0000000..12285a5 --- /dev/null +++ b/tools/hooks/post-commit @@ -0,0 +1,2 @@ +#!/bin/bash +./tools/get_version.sh diff --git a/tools/hooks/post-merge b/tools/hooks/post-merge new file mode 100644 index 0000000..12285a5 --- /dev/null +++ b/tools/hooks/post-merge @@ -0,0 +1,2 @@ +#!/bin/bash +./tools/get_version.sh diff --git a/tools/hooks/post-rewrite b/tools/hooks/post-rewrite new file mode 100644 index 0000000..12285a5 --- /dev/null +++ b/tools/hooks/post-rewrite @@ -0,0 +1,2 @@ +#!/bin/bash +./tools/get_version.sh diff --git a/tools/merge_image.htm b/tools/merge_image.htm new file mode 100644 index 0000000..fd4726f --- /dev/null +++ b/tools/merge_image.htm @@ -0,0 +1,196 @@ + + +Merge Image + + + +
+ + + +
+
Drag and drop your images here...
+ + + + diff --git a/tools/migrate_db.py b/tools/migrate_db.py new file mode 100644 index 0000000..e04ea9f --- /dev/null +++ b/tools/migrate_db.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# Migrate old SQLite taiko.db to MongoDB + +import sqlite3 +from pymongo import MongoClient + +import os,sys,inspect +current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +parent_dir = os.path.dirname(current_dir) +sys.path.insert(0, parent_dir) +import config + +client = MongoClient(config.MONGO['host']) +client.drop_database(config.MONGO['database']) +db = client[config.MONGO['database']] +sqdb = sqlite3.connect('taiko.db') +sqdb.row_factory = sqlite3.Row +curs = sqdb.cursor() + +def migrate_songs(): + curs.execute('select * from songs order by id') + rows = curs.fetchall() + + for row in rows: + song = { + 'id': row['id'], + 'title': row['title'], + 'title_lang': {'ja': row['title'], 'en': None, 'cn': None, 'tw': None, 'ko': None}, + 'subtitle': row['subtitle'], + 'subtitle_lang': {'ja': row['subtitle'], 'en': None, 'cn': None, 'tw': None, 'ko': None}, + 'courses': {'easy': None, 'normal': None, 'hard': None, 'oni': None, 'ura': None}, + 'enabled': True if row['enabled'] else False, + 'category_id': row['category'], + 'type': row['type'], + 'offset': row['offset'] or 0, + 'skin_id': row['skin_id'], + 'preview': row['preview'] or 0, + 'volume': row['volume'] or 1.0, + 'maker_id': row['maker_id'], + 'hash': row['hash'], + 'order': row['id'] + } + + for diff in ['easy', 'normal', 'hard', 'oni', 'ura']: + if row[diff]: + spl = row[diff].split(' ') + branch = False + if len(spl) > 1 and spl[1] == 'B': + branch = True + + song['courses'][diff] = {'stars': int(spl[0]), 'branch': branch} + + if row['title_lang']: + langs = row['title_lang'].splitlines() + for lang in langs: + spl = lang.split(' ', 1) + if spl[0] in ['ja', 'en', 'cn', 'tw', 'ko']: + song['title_lang'][spl[0]] = spl[1] + else: + song['title_lang']['en'] = lang + + if row['subtitle_lang']: + langs = row['subtitle_lang'].splitlines() + for lang in langs: + spl = lang.split(' ', 1) + if spl[0] in ['ja', 'en', 'cn', 'tw', 'ko']: + song['subtitle_lang'][spl[0]] = spl[1] + else: + song['subtitle_lang']['en'] = lang + + db.songs.insert_one(song) + last_song = song['id'] + + db.seq.insert_one({'name': 'songs', 'value': last_song}) + +def migrate_makers(): + curs.execute('select * from makers') + rows = curs.fetchall() + + for row in rows: + db.makers.insert_one({ + 'id': row['maker_id'], + 'name': row['name'], + 'url': row['url'] + }) + +def migrate_categories(): + curs.execute('select * from categories') + rows = curs.fetchall() + + for row in rows: + db.categories.insert_one({ + 'id': row['id'], + 'title': row['title'] + }) + +def migrate_song_skins(): + curs.execute('select * from song_skins') + rows = curs.fetchall() + + for row in rows: + db.song_skins.insert_one({ + 'id': row['id'], + 'name': row['name'], + 'song': row['song'], + 'stage': row['stage'], + 'don': row['don'] + }) + +if __name__ == '__main__': + migrate_songs() + migrate_makers() + migrate_categories() + migrate_song_skins() diff --git a/tools/nginx.conf b/tools/nginx.conf new file mode 100644 index 0000000..c51e33f --- /dev/null +++ b/tools/nginx.conf @@ -0,0 +1,25 @@ +server { + listen 80; + #server_name taiko.example.com; + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $server_name; + proxy_pass http://127.0.0.1:34801; + } + + location ~ ^/(assets/|songs/|src/|manifest.json$) { + root /srv/taiko-web/public; + location ~ ^/songs/(\d+)/preview\.mp3$ { + try_files $uri /api/preview?id=$1; + } + } + + location /p2 { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_pass http://127.0.0.1:34802; + } +} diff --git a/tools/nginx_subdir.conf b/tools/nginx_subdir.conf new file mode 100644 index 0000000..7177b46 --- /dev/null +++ b/tools/nginx_subdir.conf @@ -0,0 +1,28 @@ +server { + listen 80; + #server_name taiko.example.com; + + location /taiko-web/ { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $server_name; + proxy_pass http://127.0.0.1:34801; + } + + location ~ ^/taiko-web/(assets/|songs/|src/|manifest.json$) { + rewrite ^/taiko-web/(.*) /$1 break; + root /srv/taiko-web/public; + location ~ ^/taiko-web/songs/([0-9]+)/preview\.mp3$ { + set $id $1; + rewrite ^/taiko-web/(.*) /$1 break; + try_files $uri /taiko-web/api/preview?id=$id; + } + } + + location /taiko-web/p2 { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_pass http://127.0.0.1:34802; + } +} diff --git a/tools/set_previews.py b/tools/set_previews.py new file mode 100644 index 0000000..835884e --- /dev/null +++ b/tools/set_previews.py @@ -0,0 +1,86 @@ +from __future__ import division +import os +import sqlite3 +import re +DATABASE = 'taiko.db' + +conn = sqlite3.connect(DATABASE) +curs = conn.cursor() + +def parse_osu(osu): + osu_lines = open(osu, 'r').read().replace('\x00', '').split('\n') + sections = {} + current_section = (None, []) + + for line in osu_lines: + line = line.strip() + secm = re.match('^\[(\w+)\]$', line) + if secm: + if current_section: + sections[current_section[0]] = current_section[1] + current_section = (secm.group(1), []) + else: + if current_section: + current_section[1].append(line) + else: + current_section = ('Default', [line]) + + if current_section: + sections[current_section[0]] = current_section[1] + + return sections + + +def get_osu_key(osu, section, key, default=None): + sec = osu[section] + for line in sec: + ok = line.split(':', 1)[0].strip() + ov = line.split(':', 1)[1].strip() + + if ok.lower() == key.lower(): + return ov + + return default + + +def get_preview(song_id, song_type): + preview = 0 + + if song_type == "tja": + if os.path.isfile('public/songs/%s/main.tja' % song_id): + preview = get_tja_preview('public/songs/%s/main.tja' % song_id) + else: + osus = [osu for osu in os.listdir('public/songs/%s' % song_id) if osu in ['easy.osu', 'normal.osu', 'hard.osu', 'oni.osu']] + if osus: + osud = parse_osu('public/songs/%s/%s' % (song_id, osus[0])) + preview = int(get_osu_key(osud, 'General', 'PreviewTime', 0)) + + return preview + + +def get_tja_preview(tja): + tja_lines = open(tja, 'r').read().replace('\x00', '').split('\n') + + for line in tja_lines: + line = line.strip() + if ':' in line: + name, value = line.split(':', 1) + if name.lower() == 'demostart': + value = value.strip() + try: + value = float(value) + except ValueError: + pass + else: + return int(value * 1000) + elif line.lower() == '#start': + break + return 0 + + +if __name__ == '__main__': + songs = curs.execute('select id, type from songs').fetchall() + for song in songs: + preview = get_preview(song[0], song[1]) / 1000 + curs.execute('update songs set preview = ? where id = ?', (preview, song[0])) + conn.commit() diff --git a/tools/setup.sh b/tools/setup.sh new file mode 100644 index 0000000..55cf8d3 --- /dev/null +++ b/tools/setup.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -euo pipefail + +sudo apt update +sudo apt install -y git python3-pip python3-virtualenv python3-venv nginx ffmpeg redis supervisor + +if [[ -r /etc/os-release ]]; then + . /etc/os-release + if [[ $ID = ubuntu ]]; then + # MongoDB supports only LTS and has not released package for Ubuntu 22.04 LTS yet + case $VERSION_CODENAME in + impish|kinetic|jammy) + VERSION_CODENAME=focal ;; + esac + REPO="https://repo.mongodb.org/apt/ubuntu $VERSION_CODENAME/mongodb-org/5.0 multiverse" + elif [[ $ID = debian ]]; then + # MongoDB does not provide packages for Debian 11 yet + case $VERSION_CODENAME in + bullseye|bookworm|sid) + VERSION_CODENAME=buster ;; + esac + REPO="https://repo.mongodb.org/apt/debian $VERSION_CODENAME/mongodb-org/5.0 main" + else + echo "Unsupported distribution $ID" + exit 1 + fi +else + echo "Not running a distribution with /etc/os-release available" + exit 1 +fi + +wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo tee /etc/apt/trusted.gpg.d/mongodb-server-5.0.asc +echo "deb [ arch=amd64,arm64 ] $REPO" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list + +sudo apt update +sudo apt install -y mongodb-org + +sudo mkdir -p /srv/taiko-web +sudo chown $USER /srv/taiko-web +git clone https://github.com/bui/taiko-web.git /srv/taiko-web + +cd /srv/taiko-web +tools/get_version.sh +cp tools/hooks/* .git/hooks/ +cp config.example.py config.py +sudo cp tools/nginx.conf /etc/nginx/conf.d/taiko-web.conf + +sudo sed -i 's/^\(\s\{0,\}\)\(include \/etc\/nginx\/sites-enabled\/\*;\)$/\1#\2/g' /etc/nginx/nginx.conf +sudo sed -i 's/}/ application\/wasm wasm;\n}/g' /etc/nginx/mime.types +sudo service nginx restart + +python3 -m venv .venv +.venv/bin/pip install --upgrade pip wheel setuptools +.venv/bin/pip install -r requirements.txt + +sudo mkdir -p /var/log/taiko-web +sudo cp tools/supervisor.conf /etc/supervisor/conf.d/taiko-web.conf +sudo service supervisor restart + +sudo systemctl enable mongod.service +sudo service mongod start + +IP=$(dig +short txt ch whoami.cloudflare @1.0.0.1 | tr -d '"') +echo +echo "Setup complete! You should be able to access your taiko-web instance at http://$IP" +echo diff --git a/tools/supervisor.conf b/tools/supervisor.conf new file mode 100644 index 0000000..f405b31 --- /dev/null +++ b/tools/supervisor.conf @@ -0,0 +1,15 @@ +[program:taiko_app] +directory=/srv/taiko-web +command=/srv/taiko-web/.venv/bin/gunicorn -b 127.0.0.1:34801 app:app +autostart=true +autorestart=true +stdout_logfile=/var/log/taiko-web/app.out.log +stderr_logfile=/var/log/taiko-web/app.err.log + +[program:taiko_server] +directory=/srv/taiko-web +command=/srv/taiko-web/.venv/bin/python server.py 34802 +autostart=true +autorestart=true +stdout_logfile=/var/log/taiko-web/server.out.log +stderr_logfile=/var/log/taiko-web/server.err.log diff --git a/tools/taikodb_hash.py b/tools/taikodb_hash.py new file mode 100644 index 0000000..7c6955f --- /dev/null +++ b/tools/taikodb_hash.py @@ -0,0 +1,49 @@ +import os +import sys +import hashlib +import base64 +import sqlite3 + +def md5(md5hash, filename): + with open(filename, "rb") as file: + for chunk in iter(lambda: file.read(64 * 1024), b""): + md5hash.update(chunk) + +def get_hashes(root): + hashes = {} + diffs = ["easy", "normal", "hard", "oni", "ura"] + dirs = os.listdir(root) + for dir in dirs: + dir_path = os.path.join(root, dir) + if dir.isdigit() and os.path.isdir(dir_path): + files = os.listdir(dir_path) + md5hash = hashlib.md5() + if "main.tja" in files: + md5(md5hash, os.path.join(dir_path, "main.tja")) + else: + for diff in diffs: + if diff + ".osu" in files: + md5(md5hash, os.path.join(dir_path, diff + ".osu")) + hashes[dir] = base64.b64encode(md5hash.digest())[:-2] + return hashes + +def write_db(database, songs): + db = sqlite3.connect(database) + hashes = get_hashes(songs) + added = 0 + for id in hashes: + added += 1 + cur = db.cursor() + cur.execute("update songs set hash = ? where id = ?", (hashes[id].decode(), int(id))) + cur.close() + db.commit() + db.close() + if added: + print("{0} hashes have been added to the database.".format(added)) + else: + print("Error: No songs were found in the given directory.") + +if len(sys.argv) >= 3: + write_db(sys.argv[1], sys.argv[2]) +else: + print("Usage: taikodb_hash.py ../taiko.db ../public/songs")