diff --git a/.gitignore b/.gitignore index 6d44399..e7ddeff 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ $RECYCLE.BIN/ .Trashes .vscode +*.pyc # Directories potentially created on remote AFP share .AppleDB @@ -48,5 +49,5 @@ public/api taiko.db version.json public/index.html -config.json +config.py public/assets/song_skins diff --git a/app.py b/app.py index d1ecbe3..d1713a3 100644 --- a/app.py +++ b/app.py @@ -1,63 +1,128 @@ -#!/usr/bin/env python2 - -from __future__ import division +#!/usr/bin/env python3 +import base64 +import bcrypt +import hashlib +import config import json -import sqlite3 import re +import requests +import schema import os -from flask import Flask, g, jsonify, render_template, request, abort, redirect + +from functools import wraps +from flask import Flask, g, jsonify, render_template, request, abort, redirect, session, flash 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 app = Flask(__name__) -try: - app.cache = Cache(app, config={'CACHE_TYPE': 'redis'}) -except RuntimeError: - import tempfile - app.cache = Cache(app, config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': tempfile.gettempdir()}) +client = MongoClient(host=config.MONGO['host']) -DATABASE = 'taiko.db' -DEFAULT_URL = 'https://github.com/bui/taiko-web/' +app.secret_key = config.SECRET_KEY +app.config['SESSION_TYPE'] = 'redis' +app.cache = Cache(app, config=config.REDIS) +sess = Session() +sess.init_app(app) +csrf = CSRFProtect(app) + +db = client[config.MONGO['database']] +db.users.create_index('username', unique=True) +db.songs.create_index('id', unique=True) -def get_db(): - db = getattr(g, '_database', None) - if db is None: - db = g._database = sqlite3.connect(DATABASE) - db.row_factory = sqlite3.Row - return db +class HashException(Exception): + pass -def query_db(query, args=(), one=False): - cur = get_db().execute(query, args) - rv = cur.fetchall() - cur.close() - return (rv[0] if rv else None) if one else rv +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' % (config.SONGS_BASEURL, id)] + else: + urls = [] + for diff in ['easy', 'normal', 'hard', 'oni', 'ura']: + if form['course_' + diff]: + urls.append('%s%s/%s.osu' % (config.SONGS_BASEURL, 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("/"): + url = url[1:] + with open(os.path.join("public", url), "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(): - if os.path.isfile('config.json'): - try: - config = json.load(open('config.json', 'r')) - except ValueError: - print('WARNING: Invalid config.json, using default values') - config = {} - else: - print('WARNING: No config.json found, using default values') - config = {} + config_out = { + 'songs_baseurl': config.SONGS_BASEURL, + 'assets_baseurl': config.ASSETS_BASEURL, + 'email': config.EMAIL, + 'accounts': config.ACCOUNTS, + 'custom_js': config.CUSTOM_JS + } - if not config.get('songs_baseurl'): - config['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/' - if not config.get('assets_baseurl'): - config['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/' + 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['_version'] = get_version() - return config + config_out['_version'] = get_version() + return config_out def get_version(): - version = {'commit': None, 'commit_short': '', 'version': None, 'url': DEFAULT_URL} + version = {'commit': None, 'commit_short': '', 'version': None, 'url': config.URL} if os.path.isfile('version.json'): try: ver = json.load(open('version.json', 'r')) @@ -72,20 +137,158 @@ def get_version(): return version -@app.teardown_appcontext -def close_connection(exception): - db = getattr(g, '_database', None) - if db is not None: - db.close() - - @app.route('/') -@app.cache.cached(timeout=15) def route_index(): version = get_version() return render_template('index.html', version=version, config=get_config()) +@app.route('/api/csrftoken') +def route_csrftoken(): + return jsonify({'status': 'ok', 'token': generate_csrf()}) + + +@app.route('/admin') +@admin_required(level=50) +def route_admin(): + return redirect('/admin/songs') + + +@app.route('/admin/songs') +@admin_required(level=50) +def route_admin_songs(): + songs = db.songs.find({}) + user = db.users.find_one({'username': session['username']}) + return render_template('admin_songs.html', songs=list(songs), admin=user) + + +@app.route('/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) + + +@app.route('/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({})) + + return render_template('admin_song_new.html', categories=categories, song_skins=song_skins, makers=makers) + + +@app.route('/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['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['hash'] = None + + seq = db.seq.find_one({'name': 'songs'}) + seq_new = seq['value'] + 1 if seq else 1 + output['id'] = seq_new + output['order'] = seq_new + + db.songs.insert_one(output) + flash('Song created.') + + db.seq.update_one({'name': 'songs'}, {'$set': {'value': seq_new}}, upsert=True) + + return redirect('/admin/songs/%s' % str(seq_new)) + + +@app.route('/admin/songs/', methods=['POST']) +@admin_required(level=100) +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['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['hash'] = request.form.get('hash') + + if request.form.get('gen_hash'): + try: + output['hash'] = generate_hash(id, request.form) + except HashException as e: + flash('An error occurred: %s' % str(e), 'error') + return redirect('/admin/songs/%s' % id) + + db.songs.update_one({'id': id}, {'$set': output}) + flash('Changes saved.') + + return redirect('/admin/songs/%s' % id) + + +@app.route('/admin/songs//delete', methods=['POST']) +@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('/admin/songs') + + @app.route('/api/preview') @app.cache.cached(timeout=15, query_string=True) def route_api_preview(): @@ -93,12 +296,12 @@ def route_api_preview(): if not song_id or not re.match('^[0-9]+$', song_id): abort(400) - song_row = query_db('select * from songs where id = ? and enabled = 1', (song_id,)) - if not song_row: + song = db.songs.find_one({'id': song_id}) + if not song: abort(400) - song_type = song_row[0]['type'] - prev_path = make_preview(song_id, song_type, song_row[0]['preview']) + song_type = song['type'] + prev_path = make_preview(song_id, song_type, song['preview']) if not prev_path: return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id) @@ -108,52 +311,30 @@ def route_api_preview(): @app.route('/api/songs') @app.cache.cached(timeout=15) def route_api_songs(): - songs = query_db('select s.*, m.name, m.url from songs s left join makers m on s.maker_id = m.maker_id where enabled = 1') - - raw_categories = query_db('select * from categories') - categories = {} - for cat in raw_categories: - categories[cat['id']] = cat['title'] - - raw_song_skins = query_db('select * from song_skins') - song_skins = {} - for skin in raw_song_skins: - song_skins[skin[0]] = {'name': skin['name'], 'song': skin['song'], 'stage': skin['stage'], 'don': skin['don']} - - songs_out = [] + songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False})) for song in songs: - song_id = song['id'] - song_type = song['type'] - preview = song['preview'] - - category_out = categories[song['category']] if song['category'] in categories else '' - song_skin_out = song_skins[song['skin_id']] if song['skin_id'] in song_skins else None - maker = None - if song['maker_id'] == 0: - maker = 0 - elif song['maker_id'] and song['maker_id'] > 0: - maker = {'name': song['name'], 'url': song['url'], 'id': song['maker_id']} - - songs_out.append({ - 'id': song_id, - 'title': song['title'], - 'title_lang': song['title_lang'], - 'subtitle': song['subtitle'], - 'subtitle_lang': song['subtitle_lang'], - 'stars': [ - song['easy'], song['normal'], song['hard'], song['oni'], song['ura'] - ], - 'preview': preview, - 'category': category_out, - 'type': song_type, - 'offset': song['offset'], - 'song_skin': song_skin_out, - 'volume': song['volume'], - 'maker': maker, - 'hash': song['hash'] - }) + 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'] - return jsonify(songs_out) + 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 jsonify(songs) @app.route('/api/config') @@ -163,6 +344,183 @@ def route_api_config(): return jsonify(config) +@app.route('/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) + + session_id = os.urandom(24).hex() + db.users.insert_one({ + 'username': username, + 'username_lower': username.lower(), + 'password': hashed, + 'display_name': username, + '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}) + + +@app.route('/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') + + 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']}) + + +@app.route('/api/logout', methods=['POST']) +@login_required +def route_api_logout(): + session.clear() + return jsonify({'status': 'ok'}) + + +@app.route('/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('/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('/api/account/remove', methods=['POST']) +@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('/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('/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}) + return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name']}) + + def make_preview(song_id, song_type, preview): song_path = 'public/songs/%s/main.mp3' % song_id prev_path = 'public/songs/%s/preview.mp3' % song_id diff --git a/config.example.json b/config.example.json deleted file mode 100644 index 9bfd207..0000000 --- a/config.example.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "songs_baseurl": "", - "assets_baseurl": "" -} diff --git a/config.example.py b/config.example.py new file mode 100644 index 0000000..478aa77 --- /dev/null +++ b/config.example.py @@ -0,0 +1,35 @@ +# The full URL base asset URL, with trailing slash. +ASSETS_BASEURL = '' + +# The full URL base song URL, with trailing slash. +SONGS_BASEURL = '' + +# The email address to display in the "About Simulator" menu. +EMAIL = 'taiko@example.com' + +# Whether to use the user account system. +ACCOUNTS = True + +# Custom JavaScript file to load with the simulator. +CUSTOM_JS = '' + +# 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/' diff --git a/public/src/css/admin.css b/public/src/css/admin.css new file mode 100644 index 0000000..033bb42 --- /dev/null +++ b/public/src/css/admin.css @@ -0,0 +1,156 @@ +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 small { + color: #4a4a4a; +} + +.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 index f9e1478..d87c209 100644 --- a/public/src/css/debug.css +++ b/public/src/css/debug.css @@ -123,6 +123,7 @@ } #debug .autoplay-label, -#debug .branch-hide{ +#debug .branch-hide, +#debug .lyrics-hide{ display: none; } diff --git a/public/src/css/game.css b/public/src/css/game.css index 69d1127..7918ce1 100644 --- a/public/src/css/game.css +++ b/public/src/css/game.css @@ -89,3 +89,39 @@ .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; +} +#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; +} diff --git a/public/src/css/loader.css b/public/src/css/loader.css index 9e97ec1..fcc7c23 100644 --- a/public/src/css/loader.css +++ b/public/src/css/loader.css @@ -117,3 +117,20 @@ body{ 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/view.css b/public/src/css/view.css index 76ed3f4..da0f13c 100644 --- a/public/src/css/view.css +++ b/public/src/css/view.css @@ -108,8 +108,8 @@ kbd{ .left-buttons .taibtn{ margin-right: 0.4em; } -#diag-txt textarea, -#diag-txt iframe{ +.diag-txt textarea, +.diag-txt iframe{ width: 100%; height: 5em; font-size: inherit; @@ -119,6 +119,7 @@ kbd{ background: #fff; border: 1px solid #a9a9a9; user-select: all; + box-sizing: border-box; } .text-warn{ color: #d00; @@ -291,3 +292,88 @@ kbd{ .left-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; +} diff --git a/public/src/js/about.js b/public/src/js/about.js index 8037a29..a2712c7 100644 --- a/public/src/js/about.js +++ b/public/src/js/about.js @@ -5,7 +5,7 @@ cancelTouch = false this.endButton = this.getElement("view-end-button") - this.diagTxt = document.getElementById("diag-txt") + this.diagTxt = this.getElement("diag-txt") this.version = document.getElementById("version-link").href this.tutorialOuter = this.getElement("view-outer") if(touchEnabled){ diff --git a/public/src/js/account.js b/public/src/js/account.js new file mode 100644 index 0000000..1df6723 --- /dev/null +++ b/public/src/js/account.js @@ -0,0 +1,512 @@ +class Account{ + constructor(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.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.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)) + } + } + 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) + 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(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 + } + onFormPress(event){ + event.stopPropagation() + if(event.type === "keypress" && event.keyCode === 13){ + 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 + 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) + } + }) + } + 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 + 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 + 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 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() + } + pageEvents.remove(this.accounPassButton, ["click", "touchstart"]) + pageEvents.remove(this.accountDelButton, ["click", "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.accountPassButton + delete this.accountPass + delete this.accountPassDiv + delete this.accountDelButton + delete this.accountDel + delete this.accountDelDiv + 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"]) + } + delete this.errorDiv + delete this.form + delete this.password2 + delete this.remember + delete this.loginButton + delete this.registerButton + } + 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 index 31e4d8d..7d769ee 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -31,7 +31,9 @@ var assets = { "importsongs.js", "logo.js", "settings.js", - "scorestorage.js" + "scorestorage.js", + "account.js", + "lyrics.js" ], "css": [ "main.css", @@ -86,11 +88,7 @@ var assets = { "settings_gamepad.png" ], "audioSfx": [ - "se_cancel.wav", - "se_don.wav", - "se_ka.wav", "se_pause.wav", - "se_jump.wav", "se_calibration.wav", "v_results.wav", @@ -102,6 +100,10 @@ var assets = { "audioSfxLR": [ "neiro_1_don.wav", "neiro_1_ka.wav", + "se_cancel.wav", + "se_don.wav", + "se_ka.wav", + "se_jump.wav", "se_balloon.wav", "se_gameclear.wav", @@ -137,7 +139,9 @@ var assets = { "about.html", "debug.html", "session.html", - "settings.html" + "settings.html", + "account.html", + "login.html" ], "songs": [], diff --git a/public/src/js/canvasdraw.js b/public/src/js/canvasdraw.js index ca984ad..bb38f1b 100644 --- a/public/src/js/canvasdraw.js +++ b/public/src/js/canvasdraw.js @@ -706,12 +706,12 @@ }) }else if(r.smallHiragana.test(symbol)){ // Small hiragana, small katakana - drawn.push({text: symbol, x: 0, y: 0, w: 30}) + drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 30}) }else if(r.hiragana.test(symbol)){ // Hiragana, katakana - drawn.push({text: symbol, x: 0, y: 0, w: 35}) + drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 35}) }else{ - drawn.push({text: symbol, x: 0, y: 0, w: 39}) + drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 39}) } } @@ -720,6 +720,9 @@ if(config.letterSpacing){ symbol.w += config.letterSpacing } + if(config.kanaSpacing && symbol.kana){ + symbol.w += config.kanaSpacing + } drawnWidth += symbol.w * mul } @@ -924,8 +927,22 @@ } } } + 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 in words){ + 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 @@ -957,8 +974,22 @@ recenter() x = 0 y += lineHeight - line = words[i] === "\n" ? "" : words[i] - lastWidth = ctx.measureText(line).width + 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] @@ -1549,6 +1580,99 @@ 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) diff --git a/public/src/js/controller.js b/public/src/js/controller.js index 583ffd3..58d9315 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -6,7 +6,11 @@ class Controller{ this.saveScore = !autoPlayEnabled this.multiplayer = multiplayer this.touchEnabled = touchEnabled - this.snd = this.multiplayer ? "_p" + this.multiplayer : "" + if(multiplayer === 2){ + this.snd = p2.player === 2 ? "_p1" : "_p2" + }else{ + this.snd = multiplayer ? "_p" + p2.player : "" + } this.calibrationMode = selectedSong.folder === "calibration" this.audioLatency = 0 @@ -53,6 +57,15 @@ class Controller{ 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) + } + } } }) } @@ -155,10 +168,16 @@ class Controller{ if(this.mainLoopRunning){ if(this.multiplayer !== 2){ requestAnimationFrame(() => { - this.viewLoop() + 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() @@ -197,14 +216,14 @@ class Controller{ displayScore(score, notPlayed, bigNote){ this.view.displayScore(score, notPlayed, bigNote) } - songSelection(fadeIn){ + songSelection(fadeIn, showWarning){ if(!fadeIn){ this.clean() } if(this.calibrationMode){ new SettingsView(this.touchEnabled, false, null, "latency") }else{ - new SongSelect(false, fadeIn, this.touchEnabled) + new SongSelect(false, fadeIn, this.touchEnabled, null, showWarning) } } restartSong(){ @@ -217,20 +236,27 @@ class Controller{ resolve() }else{ var songObj = assets.songs.find(song => song.id === this.selectedSong.folder) + var promises = [] if(songObj.chart && songObj.chart !== "blank"){ var reader = new FileReader() - var promise = pageEvents.load(reader).then(event => { + promises.push(pageEvents.load(reader).then(event => { this.songData = event.target.result.replace(/\0/g, "").split("\n") - resolve() - }) + return Promise.resolve() + })) if(this.selectedSong.type === "tja"){ reader.readAsText(songObj.chart, "sjis") }else{ reader.readAsText(songObj.chart) } - }else{ - resolve() } + if(songObj.lyricsFile){ + var reader = new FileReader() + promises.push(pageEvents.load(reader).then(event => { + songObj.lyricsData = event.target.result + }, () => Promise.resolve()), songObj.lyricsFile.webkitRelativePath) + reader.readAsText(songObj.lyricsFile) + } + Promise.all(promises).then(resolve) } }).then(() => { var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled) @@ -306,5 +332,8 @@ class Controller{ debugObj.debug.updateStatus() } } + if(this.lyrics){ + this.lyrics.clean() + } } } diff --git a/public/src/js/debug.js b/public/src/js/debug.js index 271d66a..447f24f 100644 --- a/public/src/js/debug.js +++ b/public/src/js/debug.js @@ -17,6 +17,8 @@ class Debug{ 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") @@ -50,6 +52,9 @@ class Debug{ 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() @@ -129,6 +134,9 @@ class Debug{ 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 @@ -136,19 +144,21 @@ class Debug{ 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 && measures.length > this.measureNum){ - var measureMS = measures[this.measureNum].ms + 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() @@ -174,6 +184,7 @@ class Debug{ this.restartBtn.style.display = "" this.autoplayLabel.style.display = "" this.branchHideDiv.style.display = "" + this.lyricsHideDiv.style.display = "" this.controller = null } this.stopMove() @@ -194,6 +205,9 @@ class Debug{ branch.ms = branch.originalMS + offset }) } + if(this.controller.lyrics){ + this.controller.lyrics.offsetChange(value * 1000) + } if(this.restartCheckbox.checked && !noRestart){ this.restartSong() } @@ -213,6 +227,14 @@ class Debug{ 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() @@ -259,6 +281,7 @@ class Debug{ 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) @@ -285,6 +308,8 @@ class Debug{ delete this.branchSelect delete this.branchResetBtn delete this.volumeDiv + delete this.lyricsHideDiv + delete this.lyricsOffsetDiv delete this.restartCheckbox delete this.autoplayLabel delete this.autoplayCheckbox diff --git a/public/src/js/game.js b/public/src/js/game.js index 8fcdd99..58664b4 100644 --- a/public/src/js/game.js +++ b/public/src/js/game.js @@ -5,6 +5,7 @@ class Game{ this.songData = songData this.elapsedTime = 0 this.currentCircle = -1 + this.currentEvent = 0 this.updateCurrentCircle() this.combo = 0 this.rules = new GameRules(this) @@ -47,13 +48,7 @@ class Game{ } initTiming(){ // Date when the chrono is started (before the game begins) - var firstCircle - for(var i = 0; i < this.songData.circles.length; i++){ - firstCircle = this.songData.circles[i] - if(firstCircle.type !== "event"){ - break - } - } + var firstCircle = this.songData.circles[0] if(this.controller.calibrationMode){ var offsetTime = 0 }else{ @@ -238,9 +233,6 @@ class Game{ } } skipNote(circle){ - if(circle.type === "event"){ - return - } if(circle.section){ this.resetSection() } @@ -258,9 +250,6 @@ class Game{ checkPlays(){ var circles = this.songData.circles var circle = circles[this.currentCircle] - if(circle && circle.type === "event"){ - this.updateCurrentCircle() - } if(this.controller.autoPlayEnabled){ while(circle && this.controller.autoPlay(circle)){ @@ -469,9 +458,7 @@ class Game{ } getLastCircle(circles){ for(var i = circles.length; i--;){ - if(circles[i].type !== "event"){ - return circles[i] - } + return circles[i] } } whenLastCirclePlayed(){ @@ -505,7 +492,9 @@ class Game{ var musicDuration = duration * 1000 - this.controller.offset if(this.musicFadeOut === 0){ if(this.controller.multiplayer === 1){ - p2.send("gameresults", this.getGlobalScore()) + 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){ @@ -621,7 +610,7 @@ class Game{ var circles = this.songData.circles do{ var circle = circles[++this.currentCircle] - }while(circle && (circle.branch && !circle.branch.active || circle.type === "event")) + }while(circle && (circle.branch && !circle.branch.active)) } getCurrentCircle(){ return this.currentCircle diff --git a/public/src/js/importsongs.js b/public/src/js/importsongs.js index 20a431a..87d63df 100644 --- a/public/src/js/importsongs.js +++ b/public/src/js/importsongs.js @@ -202,12 +202,16 @@ var tja = new ParseTja(data, "oni", 0, 0, true) var songObj = { id: index + 1, + order: index + 1, type: "tja", chart: file, - stars: [], + courses: {}, music: "muted" } + var coursesAdded = false var titleLang = {} + var titleLangAdded = false + var subtitleLangAdded = false var subtitleLang = {} var dir = file.webkitRelativePath.toLowerCase() dir = dir.slice(0, dir.lastIndexOf("/") + 1) @@ -221,7 +225,11 @@ } songObj.subtitle = subtitle songObj.preview = meta.demostart || 0 - songObj.stars[this.courseTypes[diff]] = (meta.level || "0") + (meta.branch ? " B" : "") + 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 } @@ -252,6 +260,15 @@ 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 = "" @@ -264,32 +281,27 @@ } 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 } } } - var titleLangArray = [] - for(var id in titleLang){ - titleLangArray.push(id + " " + titleLang[id]) + if(titleLangAdded){ + songObj.title_lang = titleLang } - if(titleLangArray.length !== 0){ - songObj.title_lang = titleLangArray.join("\n") - } - var subtitleLangArray = [] - for(var id in subtitleLang){ - subtitleLangArray.push(id + " " + subtitleLang[id]) - } - if(subtitleLangArray.length !== 0){ - songObj.subtitle_lang = subtitleLangArray.join("\n") + if(subtitleLangAdded){ + songObj.subtitle_lang = subtitleLang } if(!songObj.category){ songObj.category = category || this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))]) } - if(songObj.stars.length !== 0){ + if(coursesAdded){ this.songs[index] = songObj } var hash = md5.base64(event.target.result).slice(0, -2) @@ -316,12 +328,20 @@ 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: osu.metadata.Artist || osu.metadata.ArtistUnicode, + subtitle_lang: { + en: osu.metadata.Artist || osu.metadata.ArtistUnicode + }, preview: osu.generalInfo.PreviewTime / 1000, - stars: [null, null, null, parseInt(osu.difficulty.overallDifficulty) || 1], + courses: { + oni:{ + stars: parseInt(osu.difficulty.overallDifficulty) || 0, + branch: false + } + }, music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] || "muted" } var filename = file.name.slice(0, file.name.lastIndexOf(".")) @@ -333,7 +353,9 @@ suffix = " " + matches[0] } songObj.title = title + suffix - songObj.title_lang = (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix + songObj.title_lang = { + en: (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix + } }else{ songObj.title = filename } @@ -417,7 +439,7 @@ for(var i = path.length - 2; i >= 0; i--){ var hasTitle = false for(var j in exclude){ - if(path[i].indexOf(exclude[j].toLowerCase()) !== -1){ + if(exclude[j] && path[i].indexOf(exclude[j].toLowerCase()) !== -1){ hasTitle = true break } diff --git a/public/src/js/loader.js b/public/src/js/loader.js index a2d5da9..fd98cfc 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -5,6 +5,7 @@ class Loader{ this.assetsDiv = document.getElementById("assets") this.screen = document.getElementById("screen") this.startTime = Date.now() + this.errorMessages = [] var promises = [] @@ -28,17 +29,24 @@ class Loader{ if(gameConfig.custom_js){ var script = document.createElement("script") - this.addPromise(pageEvents.load(script)) - script.src = gameConfig.custom_js + queryString + var url = gameConfig.custom_js + queryString + this.addPromise(pageEvents.load(script), url) + script.src = url document.head.appendChild(script) } assets.js.forEach(name => { var script = document.createElement("script") - this.addPromise(pageEvents.load(script)) - script.src = "/src/js/" + name + queryString + var url = "/src/js/" + name + queryString + this.addPromise(pageEvents.load(script), url) + script.src = url document.head.appendChild(script) }) + 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 && @@ -69,48 +77,56 @@ class Loader{ } var interval = setInterval(checkStyles, 100) checkStyles() - })) + }), "Version on the page and config does not match\n(page: " + pageVersion + ",\nconfig: "+ gameConfig._version.commit + ")") for(var name in assets.fonts){ - this.addPromise(new FontFace(name, "url('" + gameConfig.assets_baseurl + "fonts/" + assets.fonts[name] + "')").load().then(font => { + 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") - this.addPromise(pageEvents.load(image)) + var url = gameConfig.assets_baseurl + "img/" + name + this.addPromise(pageEvents.load(image), url) image.id = name - image.src = gameConfig.assets_baseurl + "img/" + name + image.src = url this.assetsDiv.appendChild(image) assets.image[id] = image }) assets.views.forEach(name => { var id = this.getFilename(name) - this.addPromise(this.ajax("/src/views/" + name + queryString).then(page => { + var url = "/src/views/" + name + queryString + this.addPromise(this.ajax(url).then(page => { assets.pages[id] = page - })) + }), url) }) this.addPromise(this.ajax("/api/songs").then(songs => { assets.songsDefault = JSON.parse(songs) assets.songs = assets.songsDefault - })) + }), "/api/songs") - this.addPromise(this.ajax(gameConfig.assets_baseurl + "img/vectors.json" + queryString).then(response => { + var url = gameConfig.assets_baseurl + "img/vectors.json" + queryString + this.addPromise(this.ajax(url).then(response => { vectors = JSON.parse(response) - })) + }), url) this.afterJSCount = - ["blurPerformance", "P2Connection"].length + + ["blurPerformance"].length + assets.audioSfx.length + assets.audioMusic.length + assets.audioSfxLR.length + - assets.audioSfxLoud.length + assets.audioSfxLoud.length + + (gameConfig.accounts ? 1 : 0) Promise.all(this.promises).then(() => { + if(this.error){ + return + } snd.buffer = new SoundBuffer() snd.musicGain = snd.buffer.createGain() @@ -130,20 +146,20 @@ class Loader{ this.afterJSCount = 0 assets.audioSfx.forEach(name => { - this.addPromise(this.loadSound(name, snd.sfxGain)) + this.addPromise(this.loadSound(name, snd.sfxGain), this.soundUrl(name)) }) assets.audioMusic.forEach(name => { - this.addPromise(this.loadSound(name, snd.musicGain)) + 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.addPromise(this.loadSound(name, snd.sfxLoudGain), this.soundUrl(name)) }) this.canvasTest = new CanvasTest() @@ -153,67 +169,92 @@ class Loader{ // Less than 50 fps with blur enabled disableBlur = true } - })) + }), "blurPerformance") - 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 - this.addPromise(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", location.hash.slice(1).toLowerCase()) - setTimeout(() => { - if(p2.socket.readyState !== 1){ - p2.hash("") - p2.hashLock = false - resolve() - } - }, 10000) - }).then(() => { - pageEvents.remove(p2, "message") - })) - }else{ - p2.hash("") + 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 + scoreStorage.load(response.scores) + pageEvents.send("login", account.username) + } + }), "/api/scores/get") } settings = new Settings() pageEvents.setKbd() - scoreStorage = new ScoreStorage() - 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 - } - } Promise.all(this.promises).then(() => { - this.canvasTest.drawAllImages().then(result => { + 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 + }) + 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()) + + Promise.all(promises).then(result => { perf.allImg = result perf.load = Date.now() - this.startTime this.canvasTest.clean() @@ -227,27 +268,36 @@ class Loader{ }) } - addPromise(promise){ + addPromise(promise, url){ this.promises.push(promise) - promise.then(this.assetLoaded.bind(this), this.errorMsg.bind(this)) + promise.then(this.assetLoaded.bind(this), response => { + this.errorMsg(response, url) + return Promise.resolve() + }) + } + soundUrl(name){ + return gameConfig.assets_baseurl + "audio/" + name } loadSound(name, gain){ var id = this.getFilename(name) - return gain.load(gameConfig.assets_baseurl + "audio/" + name).then(sound => { + return gain.load(this.soundUrl(name)).then(sound => { assets.sounds[id] = sound }) } getFilename(name){ return name.slice(0, name.lastIndexOf(".")) } - errorMsg(error){ - if(Array.isArray(error) && error[1] instanceof HTMLElement){ - error = error[0] + ": " + error[1].outerHTML + errorMsg(error, url){ + if(url || error){ + if(url){ + error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url + } + this.errorMessages.push(error) + pageEvents.send("loader-error", url || error) } - console.error(error) - pageEvents.send("loader-error", error) if(!this.error){ this.error = true + cancelTouch = false this.loaderDiv.classList.add("loaderError") if(typeof allStrings === "object"){ var lang = localStorage.lang @@ -265,14 +315,57 @@ class Loader{ if(!lang){ lang = "en" } - var errorOccured = allStrings[lang].errorOccured - }else{ - var errorOccured = "An error occurred, please refresh" + loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang].errorOccured } - this.loaderPercentage.appendChild(document.createElement("br")) - this.loaderPercentage.appendChild(document.createTextNode(errorOccured)) - this.clean() + 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```" } assetLoaded(){ if(!this.error){ @@ -291,7 +384,11 @@ class Loader{ var request = new XMLHttpRequest() request.open("GET", url) pageEvents.load(request).then(() => { - resolve(request.response) + if(request.status === 200){ + resolve(request.response) + }else{ + reject() + } }, reject) if(customRequest){ customRequest(request) @@ -299,14 +396,28 @@ class Loader{ request.send() }) } - clean(){ + 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){ var fontDetectDiv = document.getElementById("fontdetectHelper") if(fontDetectDiv){ fontDetectDiv.parentNode.removeChild(fontDetectDiv) } + delete this.loaderDiv delete this.loaderPercentage delete this.loaderProgress - delete this.promises + 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 index b40461b..b2e3f98 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -34,7 +34,7 @@ class LoadSong{ run(){ var song = this.selectedSong var id = song.folder - var promises = [] + this.promises = [] if(song.folder !== "calibration"){ assets.sounds["v_start"].play() var songObj = assets.songs.find(song => song.id === id) @@ -92,9 +92,9 @@ class LoadSong{ img.crossOrigin = "Anonymous" } let promise = pageEvents.load(img) - promises.push(promise.then(() => { + this.addPromise(promise.then(() => { return this.scaleImg(img, filename, prefix, force) - })) + }), songObj.music ? filename + ".png" : skinBase + filename + ".png") if(songObj.music){ img.src = URL.createObjectURL(song.songSkin[filename + ".png"]) }else{ @@ -102,14 +102,15 @@ class LoadSong{ } } } - promises.push(this.loadSongBg(id)) + this.loadSongBg(id) - promises.push(new Promise((resolve, reject) => { + var url = gameConfig.songs_baseurl + id + "/main.mp3" + this.addPromise(new Promise((resolve, reject) => { if(songObj.sound){ songObj.sound.gain = snd.musicGain resolve() }else if(!songObj.music){ - snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.mp3").then(sound => { + snd.musicGain.load(url).then(sound => { songObj.sound = sound resolve() }, reject) @@ -121,84 +122,120 @@ class LoadSong{ }else{ resolve() } - })) + }), songObj.music ? songObj.music.webkitRelativePath : url) if(songObj.chart){ if(songObj.chart === "blank"){ this.songData = "" }else{ var reader = new FileReader() - promises.push(pageEvents.load(reader).then(event => { + this.addPromise(pageEvents.load(reader).then(event => { this.songData = event.target.result.replace(/\0/g, "").split("\n") - })) + }), songObj.chart.webkitRelativePath) if(song.type === "tja"){ reader.readAsText(songObj.chart, "sjis") }else{ reader.readAsText(songObj.chart) } } + if(songObj.lyricsFile && settings.getItem("showLyrics")){ + var reader = new FileReader() + this.addPromise(pageEvents.load(reader).then(event => { + songObj.lyricsData = event.target.result + }, () => Promise.resolve()), songObj.lyricsFile.webkitRelativePath) + reader.readAsText(songObj.lyricsFile) + } }else{ - promises.push(loader.ajax(this.getSongPath(song)).then(data => { + var url = this.getSongPath(song) + this.addPromise(loader.ajax(url).then(data => { this.songData = data.replace(/\0/g, "").split("\n") - })) + }), url) + if(song.lyrics && !songObj.lyricsData && !this.multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){ + var url = this.getSongDir(song) + "main.vtt" + this.addPromise(loader.ajax(url).then(data => { + songObj.lyricsData = data + }), url) + } } if(this.touchEnabled && !assets.image["touch_drum"]){ let img = document.createElement("img") if(this.imgScale !== 1){ img.crossOrigin = "Anonymous" } - promises.push(pageEvents.load(img).then(() => { + var url = gameConfig.assets_baseurl + "img/touch_drum.png" + this.addPromise(pageEvents.load(img).then(() => { return this.scaleImg(img, "touch_drum", "") - })) - img.src = gameConfig.assets_baseurl + "img/touch_drum.png" + }), url) + img.src = url } - Promise.all(promises).then(() => { - this.setupMultiplayer() - }, error => { - if(Array.isArray(error) && error[1] instanceof HTMLElement){ - error = error[0] + ": " + error[1].outerHTML + Promise.all(this.promises).then(() => { + if(!this.error){ + this.setupMultiplayer() } - console.error(error) - pageEvents.send("load-song-error", error) - errorMessage(new Error(error).stack) - alert(strings.errorOccured) }) } + 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(){ - return new Promise((resolve, reject) => { - var promises = [] - var filenames = [] - if(this.selectedSong.songBg !== null){ - filenames.push("bg_song_" + this.selectedSong.songBg) + 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.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 - if(this.imgScale !== 1 || force){ - img.crossOrigin = "Anonymous" - } - promises.push(pageEvents.load(img).then(() => { - return this.scaleImg(img, filenameAb, "", force) - })) - img.src = gameConfig.assets_baseurl + "img/" + filenameAb + ".png" + } + 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 + if(this.imgScale !== 1 || force){ + 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 } } - Promise.all(promises).then(resolve, reject) - }) + } } scaleImg(img, filename, prefix, force){ return new Promise((resolve, reject) => { @@ -238,8 +275,11 @@ class LoadSong{ randInt(min, max){ return Math.floor(Math.random() * (max - min + 1)) + min } + getSongDir(selectedSong){ + return gameConfig.songs_baseurl + selectedSong.folder + "/" + } getSongPath(selectedSong){ - var directory = gameConfig.songs_baseurl + selectedSong.folder + "/" + var directory = this.getSongDir(selectedSong) if(selectedSong.type === "tja"){ return directory + "main.tja" }else{ @@ -264,14 +304,14 @@ class LoadSong{ if(event.type === "gameload"){ this.cancelButton.style.display = "" - if(event.value === song.difficulty){ + 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 + this.selectedSong2.difficulty = event.value.diff if(song.type === "tja"){ this.startMultiplayer() }else{ @@ -297,7 +337,8 @@ class LoadSong{ }) p2.send("join", { id: song.folder, - diff: song.difficulty + diff: song.difficulty, + name: account.loggedIn ? account.displayName : null }) }else{ this.clean() @@ -332,6 +373,7 @@ class LoadSong{ pageEvents.send("load-song-cancel") } clean(){ + delete this.promises pageEvents.remove(p2, "message") if(this.cancelButton){ pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"]) diff --git a/public/src/js/lyrics.js b/public/src/js/lyrics.js new file mode 100644 index 0000000..d4974b3 --- /dev/null +++ b/public/src/js/lyrics.js @@ -0,0 +1,231 @@ +class Lyrics{ + constructor(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.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 = this.lines[++this.current] + } + if(this.shown !== this.current){ + if(currentLine && ms >= currentLine.start){ + this.setText(this.lines[this.current].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 index 3a393a4..c01209c 100644 --- a/public/src/js/main.js +++ b/public/src/js/main.js @@ -84,6 +84,7 @@ var strings var vectors var settings var scoreStorage +var account = {} pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){ diff --git a/public/src/js/p2.js b/public/src/js/p2.js index a0c266d..a8a1d34 100644 --- a/public/src/js/p2.js +++ b/public/src/js/p2.js @@ -3,6 +3,8 @@ class P2Connection{ 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 = "" @@ -102,6 +104,10 @@ class P2Connection{ } 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 = [] @@ -110,6 +116,7 @@ class P2Connection{ this.kaAmount = 0 this.results = false this.branch = "normal" + scoreStorage.clearP2() break case "gameend": this.otherConnected = false @@ -123,11 +130,13 @@ class P2Connection{ this.hash("") this.hashLock = false } + this.name = null + scoreStorage.clearP2() break case "gameresults": this.results = {} for(var i in response.value){ - this.results[i] = response.value[i].toString() + this.results[i] = response.value[i] === null ? null : response.value[i].toString() } break case "note": @@ -150,6 +159,44 @@ class P2Connection{ 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.toString() : response.value + 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 } } diff --git a/public/src/js/pageevents.js b/public/src/js/pageevents.js index 56c0d1a..7779a24 100644 --- a/public/src/js/pageevents.js +++ b/public/src/js/pageevents.js @@ -86,6 +86,9 @@ class PageEvents{ }) } 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() event.preventDefault() diff --git a/public/src/js/parseosu.js b/public/src/js/parseosu.js index 0ef70eb..f836cdf 100644 --- a/public/src/js/parseosu.js +++ b/public/src/js/parseosu.js @@ -48,6 +48,7 @@ class ParseOsu{ lastBeatInterval: 0, bpm: 0 } + this.events = [] this.generalInfo = this.parseGeneralInfo() this.metadata = this.parseMetadata() this.editor = this.parseEditor() @@ -244,6 +245,18 @@ class ParseOsu{ 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(",") @@ -277,7 +290,7 @@ class ParseOsu{ 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)) - circles.push(new Circle({ + pushCircle(new Circle({ id: circleID, start: start + this.offset, type: "balloon", @@ -304,7 +317,7 @@ class ParseOsu{ type = "drumroll" txt = strings.note.drumroll } - circles.push(new Circle({ + pushCircle(new Circle({ id: circleID, start: start + this.offset, type: type, @@ -339,7 +352,7 @@ class ParseOsu{ emptyValue = true } if(!emptyValue){ - circles.push(new Circle({ + pushCircle(new Circle({ id: circleID, start: start + this.offset, type: type, diff --git a/public/src/js/parsetja.js b/public/src/js/parsetja.js index 515266d..97b51fa 100644 --- a/public/src/js/parsetja.js +++ b/public/src/js/parsetja.js @@ -43,6 +43,7 @@ this.metadata = this.parseMetadata() this.measures = [] this.beatInfo = {} + this.events = [] if(!metaOnly){ this.circles = this.parseCircles() } @@ -83,6 +84,8 @@ } }else if(name.startsWith("branchstart") && inSong){ courses[courseName].branch = true + }else if(name.startsWith("lyric") && inSong){ + courses[courseName].inlineLyrics = true } }else if(!inSong){ @@ -157,6 +160,7 @@ var circleID = 0 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]; @@ -248,7 +252,12 @@ lastDrumroll = circleObj } - circles.push(circleObj) + if(note.event){ + this.events.push(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) { @@ -266,9 +275,12 @@ } } var insertNote = circleObj => { - lastBpm = bpm - lastGogo = gogo if(circleObj){ + if(bpm !== lastBpm || gogo !== lastGogo){ + circleObj.event = true + lastBpm = bpm + lastGogo = gogo + } currentMeasure.push(circleObj) } } @@ -402,6 +414,18 @@ } branchObj[branchName] = currentBranch break + case "lyric": + if(!this.lyrics){ + this.lyrics = [] + } + if(this.lyrics.length !== 0){ + this.lyrics[this.lyrics.length - 1].end = ms + } + this.lyrics.push({ + start: ms, + text: value.trim().replace(regexLinebreak, "\n") + }) + break } }else{ @@ -536,6 +560,10 @@ this.scoreinit = autoscore.ScoreInit; this.scorediff = autoscore.ScoreDiff; } + if(this.lyrics){ + var line = this.lyrics[this.lyrics.length - 1] + line.end = Math.max(ms, line.start) + 5000 + } return circles } } diff --git a/public/src/js/scoresheet.js b/public/src/js/scoresheet.js index 4e5cca6..2a2dd81 100644 --- a/public/src/js/scoresheet.js +++ b/public/src/js/scoresheet.js @@ -2,9 +2,19 @@ class Scoresheet{ constructor(controller, results, multiplayer, touchEnabled){ this.controller = controller this.resultsObj = results - this.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[i] = results[i].toString() + this.results[player0][i] = results[i] === null ? null : results[i].toString() } this.multiplayer = multiplayer this.touchEnabled = touchEnabled @@ -39,6 +49,7 @@ class Scoresheet{ 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"] @@ -208,6 +219,7 @@ class Scoresheet{ 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) @@ -233,6 +245,9 @@ class Scoresheet{ 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 @@ -243,7 +258,7 @@ class Scoresheet{ var frameTop = winH / 2 - 720 / 2 var frameLeft = winW / 2 - 1280 / 2 - var players = this.multiplayer && p2.results ? 2 : 1 + var players = this.multiplayer ? 2 : 1 var p2Offset = 298 var bgOffset = 0 @@ -326,28 +341,21 @@ class Scoresheet{ } var rules = this.controller.game.rules - var gaugePercent = rules.gaugePercent(this.results.gauge) - var gaugeClear = [rules.gaugeClear] - if(players === 2){ - gaugeClear.push(this.controller.syncWith.game.rules.gaugeClear) - } - var failedOffset = gaugePercent >= gaugeClear[0] ? 0 : -2000 - if(players === 2){ - var gauge2 = this.controller.syncWith.game.rules.gaugePercent(p2.results.gauge) - if(gauge2 > gaugePercent && failedOffset !== 0 && gauge2 >= gaugeClear[1]){ + 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 - if(p === 1){ - results = p2.results + var results = this.results[p] + if(!results){ + continue } - var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules - var resultGauge = playerRules.gaugePercent(results.gauge) - var clear = resultGauge >= gaugeClear[p] + var clear = this.rules[p].clearReached(results.gauge) if(p === 1 || !this.multiplayer && clear){ ctx.translate(0, 290) } @@ -410,7 +418,7 @@ class Scoresheet{ this.draw.layeredText({ ctx: ctx, - text: this.results.title, + text: this.results[this.player[0]].title, fontSize: 40, fontFamily: this.font, x: 1257, @@ -426,9 +434,11 @@ class Scoresheet{ ctx.save() for(var p = 0; p < players; p++){ - var results = this.results + var results = this.results[p] + if(!results){ + continue + } if(p === 1){ - results = p2.results ctx.translate(0, p2Offset) } @@ -450,6 +460,30 @@ class Scoresheet{ 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 @@ -581,7 +615,7 @@ class Scoresheet{ if(this.tetsuoHanaClass){ this.tetsuoHana.classList.remove(this.tetsuoHanaClass) } - this.tetsuoHanaClass = rules.clearReached(this.results.gauge) ? "dance" : "failed" + this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed" this.tetsuoHana.classList.add(this.tetsuoHanaClass) } } @@ -595,32 +629,32 @@ class Scoresheet{ ctx.translate(frameLeft, frameTop) for(var p = 0; p < players; p++){ - var results = this.results + var results = this.results[p] + if(!results){ + continue + } if(p === 1){ - results = p2.results ctx.translate(0, p2Offset) } - var gaugePercent = rules.gaugePercent(results.gauge) var w = 712 this.draw.gauge({ ctx: ctx, x: 558 + w, y: p === 1 ? 124 : 116, - clear: gaugeClear[p], - percentage: gaugePercent, + 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 }) - var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules this.draw.soul({ ctx: ctx, x: 1215, y: 144, scale: 36 / 42, - cleared: playerRules.clearReached(results.gauge) + cleared: this.rules[p].clearReached(results.gauge) }) } }) @@ -633,13 +667,12 @@ class Scoresheet{ var noCrownResultWait = -2000; for(var p = 0; p < players; p++){ - var results = this.results - if(p === 1){ - results = p2.results + var results = this.results[p] + if(!results){ + continue } var crownType = null - var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules - if(playerRules.clearReached(results.gauge)){ + if(this.rules[p].clearReached(results.gauge)){ crownType = results.bad === "0" ? "gold" : "silver" } if(crownType !== null){ @@ -702,7 +735,10 @@ class Scoresheet{ var times = {} var lastTime = 0 for(var p = 0; p < players; p++){ - var results = p === 0 ? this.results : p2.results + var results = this.results[p] + if(!results){ + continue + } var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame if(currentTime > lastTime){ lastTime = currentTime @@ -711,7 +747,10 @@ class Scoresheet{ for(var i in printNumbers){ var largestTime = 0 for(var p = 0; p < players; p++){ - var results = p === 0 ? this.results : p2.results + 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){ @@ -727,9 +766,11 @@ class Scoresheet{ } for(var p = 0; p < players; p++){ - var results = this.results + var results = this.results[p] + if(!results){ + continue + } if(p === 1){ - results = p2.results ctx.translate(0, p2Offset) } ctx.save() @@ -823,7 +864,7 @@ class Scoresheet{ if(elapsed >= 1000){ this.clean() - this.controller.songSelection(true) + this.controller.songSelection(true, this.showWarning) } } @@ -890,10 +931,14 @@ class Scoresheet{ delete this.resultsObj.title delete this.resultsObj.difficulty delete this.resultsObj.gauge - scoreStorage.add(hash, difficulty, this.resultsObj, true, title) + 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) + scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => { + this.showWarning = {name: "scoreSaveFailed"} + }) } } this.scoreSaved = true @@ -908,7 +953,7 @@ class Scoresheet{ snd.buffer.loadSettings() this.redrawRunning = false pageEvents.remove(this.canvas, ["mousedown", "touchstart"]) - if(this.multiplayer !== 2 && this.touchEnabled){ + if(this.touchEnabled){ pageEvents.remove(document.getElementById("touch-full-btn"), "touchend") } if(this.session){ @@ -920,5 +965,7 @@ class Scoresheet{ 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 index 87820f5..82f6d11 100644 --- a/public/src/js/scorestorage.js +++ b/public/src/js/scorestorage.js @@ -1,23 +1,38 @@ class ScoreStorage{ constructor(){ 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"] - this.load() } - load(){ - this.scores = {} - this.scoreStrings = {} - try{ - var localScores = localStorage.getItem("scoreStorage") - if(localScores){ - this.scoreStrings = JSON.parse(localScores) - } - }catch(e){} - for(var hash in this.scoreStrings){ - var scoreString = this.scoreStrings[hash] + 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(";") @@ -37,25 +52,63 @@ class ScoreStorage{ score[name] = value } if(!songAdded){ - this.scores[hash] = {title: null} + scores[hash] = {title: null} songAdded = true } - this.scores[hash][this.difficulty[i]] = score + 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(){ - try{ - localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings)) - }catch(e){} + if(!account.loggedIn){ + try{ + localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings)) + }catch(e){} + } } writeString(hash){ var score = this.scores[hash] @@ -101,17 +154,82 @@ class ScoreStorage{ } } } - add(song, difficulty, scoreObject, isHash, setTitle){ + 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(setTitle){ - this.scores[hash].title = setTitle + 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.scores[hash][difficulty] = scoreObject 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: ""} @@ -146,6 +264,62 @@ class ScoreStorage{ 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 + 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/session.js b/public/src/js/session.js index 0f08c67..f9d267e 100644 --- a/public/src/js/session.js +++ b/public/src/js/session.js @@ -34,7 +34,10 @@ class Session{ pageEvents.send("session-start", "host") } }) - p2.send("invite") + p2.send("invite", { + id: null, + name: account.loggedIn ? account.displayName : null + }) pageEvents.send("session") } getElement(name){ diff --git a/public/src/js/settings.js b/public/src/js/settings.js index e2c2f4d..1f7a9bb 100644 --- a/public/src/js/settings.js +++ b/public/src/js/settings.js @@ -50,6 +50,10 @@ class Settings{ easierBigNotes: { type: "toggle", default: false + }, + showLyrics: { + type: "toggle", + default: true } } diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 85df8db..0ef845b 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -1,5 +1,5 @@ class SongSelect{ - constructor(fromTutorial, fadeIn, touchEnabled, songId){ + constructor(fromTutorial, fadeIn, touchEnabled, songId, showWarning){ this.touchEnabled = touchEnabled loader.changePage("songselect", false) @@ -116,7 +116,7 @@ class SongSelect{ originalTitle: song.title, subtitle: subtitle, skin: song.category in this.songSkin ? this.songSkin[song.category] : this.songSkin.default, - stars: song.stars, + courses: song.courses, category: song.category, preview: song.preview || 0, type: song.type, @@ -126,14 +126,20 @@ class SongSelect{ volume: song.volume, maker: song.maker, canJump: true, - hash: song.hash || song.title + hash: song.hash || song.title, + order: song.order, + lyrics: song.lyrics }) } this.songs.sort((a, b) => { var catA = a.category in this.songSkin ? this.songSkin[a.category] : this.songSkin.default var catB = b.category in this.songSkin ? this.songSkin[b.category] : this.songSkin.default if(catA.sort === catB.sort){ - return a.id > b.id ? 1 : -1 + if(a.order === b.order){ + return a.id > b.id ? 1 : -1 + }else{ + return a.order > b.order ? 1 : -1 + } }else{ return catA.sort > catB.sort ? 1 : -1 } @@ -162,6 +168,10 @@ class SongSelect{ category: strings.random }) } + this.showWarning = showWarning + if(showWarning && showWarning.name === "scoreSaveFailed"){ + scoreStorage.scoreSaveFailed = true + } this.songs.push({ title: strings.aboutSimulator, skin: this.songSkin.about, @@ -192,7 +202,7 @@ class SongSelect{ }) this.songAsset = { - marginTop: 90, + marginTop: 104, marginLeft: 18, width: 82, selectedWidth: 382, @@ -226,6 +236,7 @@ class SongSelect{ 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"] @@ -237,6 +248,7 @@ class SongSelect{ this.selectedSong = 0 this.selectedDiff = 0 + this.lastCurrentSong = {} assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506) if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage) && !songId){ @@ -267,7 +279,9 @@ class SongSelect{ }else if((!p2.session || fadeIn) && "selectedSong" in localStorage){ this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1) } - this.playSound(songIdIndex !== -1 ? "v_diffsel" : "v_songsel") + if(!this.showWarning){ + this.playSound(songIdIndex !== -1 ? "v_diffsel" : "v_songsel") + } snd.musicGain.fadeOut() this.playBgm(false) } @@ -373,7 +387,13 @@ class SongSelect{ return } var shift = event ? event.shiftKey : this.pressedKeys["shift"] - if(this.state.screen === "song"){ + if(this.state.showWarning){ + if(name === "confirm"){ + this.playSound("se_don") + this.state.showWarning = false + this.showWarning = false + } + }else if(this.state.screen === "song"){ if(name === "confirm"){ this.toSelectDifficulty() }else if(name === "back"){ @@ -447,10 +467,20 @@ class SongSelect{ var ctrl = false var touch = true } - if(this.state.screen === "song"){ + 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(mouse.x > 641 && mouse.y > 603){ + }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.readyState === 1 && !assets.customSongs){ this.toSession() }else{ var moveBy = this.songSelMouse(mouse.x, mouse.y) @@ -473,7 +503,7 @@ class SongSelect{ 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") + this.playSound("se_ka", 0, p2.session ? p2.player : false) if(this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura){ this.state.move = -1 } @@ -498,14 +528,22 @@ class SongSelect{ mouseMove(event){ var mouse = this.mouseOffset(event.offsetX, event.offsetY) var moveTo = null - if(this.state.screen === "song"){ + 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"){ 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(mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){ + }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.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].stars){ + if(moveTo === null && this.state.moveHover === 0 && !this.songs[this.selectedSong].courses){ this.state.moveMS = this.getMS() - this.songSelecting.speed } } @@ -544,7 +582,7 @@ class SongSelect{ var dir = x > 0 ? 1 : -1 x = Math.abs(x) var selectedWidth = this.songAsset.selectedWidth - if(!this.songs[this.selectedSong].stars){ + 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 @@ -565,7 +603,13 @@ class SongSelect{ }else if(550 < x && x < 1050 && 95 < y && y < 524){ 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.stars[moveBy - this.diffOptions.length]){ + if( + this.state.ura + && moveBy === this.diffOptions.length + 3 + || currentSong.courses[ + this.difficultyId[moveBy - this.diffOptions.length] + ] + ){ return moveBy } } @@ -583,7 +627,7 @@ class SongSelect{ }) } }else if(this.state.locked !== 1 || fromP2){ - if(this.songs[this.selectedSong].stars && (this.state.locked === 0 || fromP2)){ + if(this.songs[this.selectedSong].courses && (this.state.locked === 0 || fromP2)){ this.state.moveMS = ms }else{ this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize @@ -601,9 +645,10 @@ class SongSelect{ 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) + this.playSound("se_ka", (resize + i * soundsDelay) / 1000, fromP2 ? fromP2.player : false) } this.pointer(false) } @@ -625,7 +670,7 @@ class SongSelect{ this.state.locked = 1 this.endPreview() - this.playSound("se_jump") + this.playSound("se_jump", 0, fromP2 ? fromP2.player : false) } } @@ -634,7 +679,7 @@ class SongSelect{ this.state.move = moveBy this.state.moveMS = this.getMS() - 500 this.state.locked = 1 - this.playSound("se_ka") + this.playSound("se_ka", 0, p2.session ? p2.player : false) } } @@ -645,7 +690,7 @@ class SongSelect{ toSelectDifficulty(fromP2){ var currentSong = this.songs[this.selectedSong] if(p2.session && !fromP2 && currentSong.action !== "random"){ - if(this.songs[this.selectedSong].stars){ + if(this.songs[this.selectedSong].courses){ if(!this.state.selLock){ this.state.selLock = true p2.send("songsel", { @@ -655,7 +700,7 @@ class SongSelect{ } } }else if(this.state.locked === 0 || fromP2){ - if(currentSong.stars){ + if(currentSong.courses){ this.state.screen = "difficulty" this.state.screenMS = this.getMS() this.state.locked = true @@ -665,22 +710,24 @@ class SongSelect{ this.selectedDiff = this.diffOptions.length + 3 } - this.playSound("se_don") + this.playSound("se_don", 0, fromP2 ? fromP2.player : false) assets.sounds["v_songsel"].stop() - this.playSound("v_diffsel", 0.3) + if(!this.showWarning){ + this.playSound("v_diffsel", 0.3) + } pageEvents.send("song-select-difficulty", currentSong) }else if(currentSong.action === "back"){ this.clean() this.toTitleScreen() }else if(currentSong.action === "random"){ - this.playSound("se_don") + this.playSound("se_don", 0, fromP2 ? fromP2.player : false) this.state.locked = true do{ var i = Math.floor(Math.random() * this.songs.length) - }while(!this.songs[i].stars) + }while(!this.songs[i].courses) var moveBy = i - this.selectedSong setTimeout(() => { - this.moveToSong(moveBy) + this.moveToSong(moveBy, fromP2) }, 200) pageEvents.send("song-select-random") }else if(currentSong.action === "tutorial"){ @@ -710,7 +757,7 @@ class SongSelect{ this.state.moveHover = null assets.sounds["v_diffsel"].stop() - this.playSound("se_cancel") + this.playSound("se_cancel", 0, fromP2 ? fromP2.player : false) } this.clearHash() pageEvents.send("song-select-back") @@ -719,7 +766,7 @@ class SongSelect{ this.clean() var selectedSong = this.songs[this.selectedSong] assets.sounds["v_diffsel"].stop() - this.playSound("se_don") + this.playSound("se_don", 0, p2.session ? p2.player : false) try{ if(assets.customSongs){ @@ -744,23 +791,25 @@ class SongSelect{ }else if(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": this.difficultyId[difficulty], + "difficulty": diff, "category": selectedSong.category, "type": selectedSong.type, "offset": selectedSong.offset, "songSkin": selectedSong.songSkin, - "stars": selectedSong.stars[difficulty], - "hash": selectedSong.hash + "stars": selectedSong.courses[diff].stars, + "hash": selectedSong.hash, + "lyrics": selectedSong.lyrics }, autoplay, multiplayer, touch) } toOptions(moveBy){ if(!p2.session){ - this.playSound("se_ka") + 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) @@ -797,12 +846,21 @@ class SongSelect{ 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 @@ -893,6 +951,8 @@ class SongSelect{ 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 categories = 0 var lastCategory this.songs.forEach(song => { @@ -921,7 +981,7 @@ class SongSelect{ fontFamily: this.font, x: w / 2, y: 38 / 2, - width: w - 30, + width: id === "sessionend" ? 385 : w - 30, align: "center", baseline: "middle" }, [ @@ -962,241 +1022,26 @@ class SongSelect{ }else{ this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize + (ms - this.state.screenMS - 1000) } - if(ms > this.state.screenMS + 500){ + if(screen === "titleFadeIn" && ms > this.state.screenMS + 500){ this.state.screen = "title" screen = "title" } } - if(screen === "song"){ - if(this.songs[this.selectedSong].stars){ - selectedWidth = this.songAsset.selectedWidth + if((screen === "song" || screen === "difficulty") && (this.showWarning && !this.showWarning.shown || scoreStorage.scoreSaveFailed)){ + if(!this.showWarning){ + this.showWarning = {name: "scoreSaveFailed"} } - - var lastMoveMul = Math.pow(Math.abs(this.state.lastMove), 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 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") - this.selectedSong = 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.selectedSong = 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 - }else if(!p2.session){ - try{ - localStorage["selectedSong"] = this.selectedSong - }catch(e){} - } - - if(this.songs[this.selectedSong].action !== "back"){ - var cat = this.songs[this.selectedSong].category - var sort = cat in this.songSkin ? this.songSkin[cat].sort : 7 - this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_" + sort].src + "')" - } + if(this.bgmEnabled){ + this.playBgm(false) } - 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].stars) - this.state.locked = 1 - selectedWidth = this.songAsset.width + ((elapsed - resize2) / resize * (selectedWidth - this.songAsset.width)) - }else{ - songSelMoving = true - selectedWidth = this.songAsset.width - } - }else{ - this.playBgm(!this.songs[this.selectedSong].stars) - this.state.locked = 0 + if(this.showWarning.name === "scoreSaveFailed"){ + scoreStorage.scoreSaveFailed = false } - }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.stars[4] - 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.stars[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.stars[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 !== 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].action !== "random" - }) - } - 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].action !== "random" - }) - } - } - - var currentSong = this.songs[this.selectedSong] - var highlight = 0 - if(!currentSong.stars){ - highlight = 2 - } - if(this.state.moveHover === 0){ - highlight = 1 - } - var selectedSkin = this.songSkin.selected - if(screen === "title" || screen === "titleFadeIn" || this.state.locked === 3){ - 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.currentSongTitle !== currentSong.title){ - this.currentSongTitle = currentSong.title - this.currentSongCache.clear() - } - - if(ms > this.state.screenMS + 2000 && selectedWidth === this.songAsset.width){ - this.drawSongCrown({ - ctx: ctx, - song: currentSong, - x: winW / 2 - selectedWidth / 2 + xOffset, - y: songTop + this.songAsset.height - selectedHeight - }) + this.showWarning.shown = true + this.state.showWarning = true + this.state.locked = true + this.playSound("se_pause") } if(screen === "title" || screen === "titleFadeIn" || screen === "song"){ @@ -1279,7 +1124,230 @@ class SongSelect{ }) } - if(ms <= this.state.screenMS + 2000 && selectedWidth === this.songAsset.width){ + if(screen === "song"){ + if(this.songs[this.selectedSong].courses){ + selectedWidth = this.songAsset.selectedWidth + } + + var lastMoveMul = Math.pow(Math.abs(this.state.lastMove), 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 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.selectedSong = 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.selectedSong = 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 + }else if(!p2.session){ + try{ + localStorage["selectedSong"] = this.selectedSong + }catch(e){} + } + + if(this.songs[this.selectedSong].action !== "back"){ + var cat = this.songs[this.selectedSong].category + var sort = cat in this.songSkin ? this.songSkin[cat].sort : 7 + this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_" + sort].src + "')" + } + } + 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{ + 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 !== 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].action !== "random" + }) + } + 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].action !== "random" + }) + } + } + + 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){ + 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, @@ -1313,8 +1381,8 @@ class SongSelect{ var textW = strings.id === "en" ? 350 : 280 this.selectTextCache.get({ ctx: ctx, - x: x - 144 - 53, - y: y - 24 - 30, + x: frameLeft, + y: frameTop, w: textW + 53 + 60, h: this.songAsset.marginTop + 15, id: "difficulty" @@ -1411,28 +1479,42 @@ class SongSelect{ ctx: ctx, font: this.font, x: _x, - y: _y - 45 + y: _y - 45, + two: p2.session && p2.player === 2 }) } } } } var drawDifficulty = (ctx, i, currentUra) => { - if(currentSong.stars[i] || currentUra){ - var score = scoreStorage.get(currentSong.hash, false, true) + if(currentSong.courses[this.difficultyId[i]] || currentUra){ var crownDiff = currentUra ? "ura" : this.difficultyId[i] - var crownType = "" - if(score && score[crownDiff]){ - crownType = score[crownDiff].crown + 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 + }) + } } - this.draw.crown({ - ctx: ctx, - type: crownType, - x: songSel ? x + 33 + i * 60 : x + 402 + i * 100, - y: songSel ? y + 75 : y + 30, - scale: 0.25, - ratio: this.ratio / this.pixelRatio - }) if(songSel){ var _x = x + 33 + i * 60 var _y = y + 120 @@ -1502,9 +1584,9 @@ class SongSelect{ outlineSize: currentUra ? this.songAsset.letterBorder : 0 }) }) - var songStarsArray = (currentUra ? currentSong.stars[4] : currentSong.stars[i]).toString().split(" ") - var songStars = songStarsArray[0] - var songBranch = songStarsArray[1] === "B" + var songStarsObj = (currentUra ? currentSong.courses.ura : currentSong.courses[this.difficultyId[i]]) + var songStars = songStarsObj.stars + var songBranch = songStarsObj.branch var elapsedMS = this.state.screenMS > this.state.moveMS || !songSel ? this.state.screenMS : this.state.moveMS var fade = ((ms - elapsedMS) % 2000) / 2000 if(songBranch && fade > 0.25 && fade < 0.75){ @@ -1549,15 +1631,15 @@ class SongSelect{ if(this.selectedDiff === 4 + this.diffOptions.length){ currentDiff = 3 } - if(i === currentSong.p2Cursor && p2.socket.readyState === 1){ + if(songSel && i === currentSong.p2Cursor && p2.socket.readyState === 1){ this.draw.diffCursor({ ctx: ctx, font: this.font, x: _x, - y: _y - (songSel ? 45 : 65), - two: true, - side: songSel ? false : (currentSong.p2Cursor === currentDiff), - scale: songSel ? 0.7 : 1 + y: _y - 45, + two: !p2.session || p2.player === 1, + side: false, + scale: 0.7 }) } if(!songSel){ @@ -1573,7 +1655,8 @@ class SongSelect{ font: this.font, x: _x, y: _y - 65, - side: currentSong.p2Cursor === currentDiff && p2.socket.readyState === 1 + side: currentSong.p2Cursor === currentDiff && p2.socket.readyState === 1, + two: p2.session && p2.player === 2 }) } if(highlight){ @@ -1591,8 +1674,8 @@ class SongSelect{ } } } - for(var i = 0; currentSong.stars && i < 4; i++){ - var currentUra = i === 3 && (this.state.ura && !songSel || currentSong.stars[4] && songSel) + 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 = this.state.screenMS > this.state.moveMS ? this.state.screenMS : this.state.moveMS @@ -1614,6 +1697,22 @@ class SongSelect{ 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 + 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 @@ -1646,46 +1745,64 @@ class SongSelect{ }) }) } - - if(currentSong.maker || currentSong.maker === 0){ + + var hasMaker = currentSong.maker || currentSong.maker === 0 + if(hasMaker || currentSong.lyrics){ if (songSel) { var _x = x + 38 var _y = y + 10 + ctx.strokeStyle = "#000" ctx.lineWidth = 5 - - var grd = ctx.createLinearGradient(_x, _y, _x, _y+50); - grd.addColorStop(0, '#fa251a'); - grd.addColorStop(1, '#ffdc33'); - - ctx.fillStyle = grd; + + 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: 130, + w: 192, h: 50, radius: 24 }) 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.creative, - fontSize: strings.id == "en" ? 30 : 34, - fontFamily: this.font, - align: "center", - baseline: "middle", - x: _x + 38, - y: _y + (["ja", "en"].indexOf(strings.id) >= 0 ? 25 : 28), - width: 110 - }, [ - {outline: "#fff", letterBorder: 8}, - {fill: "#000"} - ]) + + 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 @@ -1753,7 +1870,7 @@ class SongSelect{ } } - if(!songSel && currentSong.stars[4]){ + 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 @@ -1842,7 +1959,7 @@ class SongSelect{ ctx.fillRect(0, frameTop + 595, 1280 + frameLeft * 2, 125 + frameTop) var x = 0 var y = frameTop + 603 - var w = frameLeft + 638 + var w = p2.session ? frameLeft + 638 - 200 : frameLeft + 638 var h = 117 + frameTop this.draw.pattern({ ctx: ctx, @@ -1869,7 +1986,88 @@ class SongSelect{ ctx.lineTo(x + w - 4, y + h) ctx.lineTo(x + w - 4, y + 4) ctx.fill() - x = frameLeft + 642 + + 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, @@ -1925,7 +2123,7 @@ class SongSelect{ } this.sessionCache.get({ ctx: ctx, - x: winW / 2, + x: p2.session ? winW / 4 : winW / 2, y: y + (h - 32) / 2, w: winW / 2, h: 38, @@ -1933,7 +2131,7 @@ class SongSelect{ }) ctx.globalAlpha = 1 } - if(this.state.moveHover === "session"){ + if(!p2.session && this.state.moveHover === "session"){ this.draw.highlight({ ctx: ctx, x: x, @@ -1944,6 +2142,146 @@ class SongSelect{ }) } } + 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.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: this.state.moveMS, + opacity: highlight === 2 ? 0.8 : 1, + radius: 30 + }) + } + + ctx.restore() + } if(screen === "titleFadeIn"){ ctx.save() @@ -1955,6 +2293,11 @@ class SongSelect{ ctx.restore() } + + if(p2.session && (!this.lastScoreMS || ms > this.lastScoreMS + 1000)){ + this.lastScoreMS = ms + scoreStorage.eventLoop() + } } drawClosedSong(config){ @@ -1997,7 +2340,7 @@ class SongSelect{ }) } this.draw.songFrame(config) - if(config.song.p2Cursor && p2.socket.readyState === 1){ + if("p2Cursor" in config.song && config.song.p2Cursor !== null && p2.socket.readyState === 1){ this.draw.diffCursor({ ctx: ctx, font: this.font, @@ -2013,37 +2356,47 @@ class SongSelect{ drawSongCrown(config){ if(!config.song.action && config.song.hash){ var ctx = config.ctx - var score = scoreStorage.get(config.song.hash, false, true) + 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] - if(!score){ - break - } - if(config.song.stars[i] && score[diff] && score[diff].crown){ - this.draw.crown({ - ctx: ctx, - type: score[diff].crown, - x: config.x + this.songAsset.width / 2, - 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, - y: config.y - 8, - scale: diff === "hard" || diff === "normal" ? 0.45 : 0.5, - border: 6.5, - small: true - }) - break + 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){ + return + } var currentSong = this.songs[this.selectedSong] var id = currentSong.id var prvTime = currentSong.preview @@ -2119,6 +2472,9 @@ class SongSelect{ } } playBgm(enabled){ + if(enabled && this.state && this.state.showWarning){ + return + } if(enabled && !this.bgmEnabled){ this.bgmEnabled = true snd.musicGain.fadeIn(0.4) @@ -2148,11 +2504,11 @@ class SongSelect{ }) if(currentSong){ currentSong.p2Cursor = diffId - if(p2.session && currentSong.stars){ + if(p2.session && currentSong.courses){ this.selectedSong = index this.state.move = 0 if(this.state.screen !== "difficulty"){ - this.toSelectDifficulty(true) + this.toSelectDifficulty({player: response.value.player}) } } } @@ -2173,7 +2529,7 @@ class SongSelect{ var moveBy = response.value.move if(moveBy === -1 || moveBy === 1){ this.selectedSong = song - this.categoryJump(moveBy, true) + this.categoryJump(moveBy, {player: response.value.player}) } }else if(!selected){ this.state.locked = true @@ -2190,13 +2546,13 @@ class SongSelect{ if(Math.abs(altMoveBy) < Math.abs(moveBy)){ moveBy = altMoveBy } - this.moveToSong(moveBy, true) + this.moveToSong(moveBy, {player: response.value.player}) } - }else if(this.songs[song].stars){ + }else if(this.songs[song].courses){ this.selectedSong = song this.state.move = 0 if(this.state.screen !== "difficulty"){ - this.toSelectDifficulty(true) + this.toSelectDifficulty({player: response.value.player}) } } } @@ -2238,16 +2594,11 @@ class SongSelect{ getLocalTitle(title, titleLang){ if(titleLang){ - titleLang = titleLang.split("\n") - titleLang.forEach(line => { - var space = line.indexOf(" ") - var id = line.slice(0, space) - if(id === strings.id){ - title = line.slice(space + 1) - }else if(titleLang.length === 1 && strings.id === "en" && !(id in allStrings)){ - title = line + for(var id in titleLang){ + if(id === strings.id && titleLang[id]){ + return titleLang[id] } - }) + } } return title } @@ -2258,13 +2609,13 @@ class SongSelect{ } } - playSound(id, time){ + 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].play(time) + assets.sounds[id + (snd ? "_p" + snd : "")].play(time) this.playedSounds[id] = ms } } diff --git a/public/src/js/strings.js b/public/src/js/strings.js index 17986fc..5dd8ddd 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -1,992 +1,1130 @@ -function StringsJa(){ - this.id = "ja" - this.name = "日本語" - this.regex = /^ja$|^ja-/ - this.font = "TnT, Meiryo, sans-serif" +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" + }, - this.taikoWeb = "たいこウェブ" - this.titleProceed = "クリックするかEnterを押す!" - this.titleDisclaimer = "この非公式シミュレーターはバンダイナムコとは関係がありません。" - this.titleCopyright = "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc." - this.categories = { - "J-POP": "J-POP", - "アニメ": "アニメ", - "ボーカロイド™曲": "ボーカロイド™曲", - "バラエティ": "バラエティ", - "クラシック": "クラシック", - "ゲームミュージック": "ゲームミュージック", - "ナムコオリジナル": "ナムコオリジナル" - } - this.selectSong = "曲をえらぶ" - this.selectDifficulty = "むずかしさをえらぶ" - this.back = "もどる" - this.random = "ランダム" - this.randomSong = "ランダムに曲をえらぶ" - this.howToPlay = "あそびかた説明" - this.aboutSimulator = "このシミュレータについて" - this.gameSettings = "ゲーム設定" - this.browse = "参照する…" - this.defaultSongList = "デフォルト曲リスト" - this.songOptions = "演奏オプション" - this.none = "なし" - this.auto = "オート" - this.netplay = "ネットプレイ" - this.easy = "かんたん" - this.normal = "ふつう" - this.hard = "むずかしい" - this.oni = "おに" - this.songBranch = "譜面分岐あり" - this.sessionStart = "オンラインセッションを開始する!" - this.sessionEnd = "オンラインセッションを終了する" - this.loading = "ロード中..." - this.waitingForP2 = "他のプレイヤーを待っている..." - this.cancel = "キャンセル" - this.note = { - don: "ドン", - ka: "カッ", - daiDon: "ドン(大)", - daiKa: "カッ(大)", - drumroll: "連打ーっ!!", - daiDrumroll: "連打(大)ーっ!!", - balloon: "ふうせん" - } - this.ex_note = { - don: [ - "ド", - "コ" + 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." + }, + categories: { + "J-POP": { + ja: "J-POP", + en: "Pop", + cn: "流行音乐", + tw: "流行音樂", + ko: "POP" + }, + "アニメ": { + ja: "アニメ", + en: "Anime", + cn: "卡通动画音乐", + tw: "卡通動畫音樂", + ko: "애니메이션" + }, + "ボーカロイド™曲": { + ja: "ボーカロイド™曲", + en: "VOCALOID™ Music" + }, + "バラエティ": { + ja: "バラエティ", + en: "Variety", + cn: "综合音乐", + tw: "綜合音樂", + ko: "버라이어티" + }, + "クラシック": { + ja: "クラシック", + en: "Classical", + cn: "古典音乐", + tw: "古典音樂", + ko: "클래식" + }, + "ゲームミュージック": { + ja: "ゲームミュージック", + en: "Game Music", + cn: "游戏音乐", + tw: "遊戲音樂", + ko: "게임" + }, + "ナムコオリジナル": { + ja: "ナムコオリジナル", + en: "NAMCO Original", + cn: "NAMCO原创音乐", + tw: "NAMCO原創音樂", + ko: "남코 오리지널" + } + }, + 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: "게임 설정" + }, + browse: { + ja: "参照する…", + en: "Browse…", + cn: "浏览…", + tw: "開啟檔案…", + ko: "찾아보기…" + }, + defaultSongList: { + ja: "デフォルト曲リスト", + en: "Default Song List", + 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: null, + 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." + }, + loadSongError: { + ja: null, + en: "Could not load song %s with id %s.\n\n%s" + }, + loading: { + ja: "ロード中...", + en: "Loading...", + cn: "加载中...", + tw: "讀取中...", + ko: "로딩 중..." + }, + waitingForP2: { + ja: "他のプレイヤーを待っている...", + en: "Waiting for Another Player...", + cn: "正在等待对方玩家...", + tw: "正在等待對方玩家...", + ko: "Waiting for Another Player..." + }, + 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: [ + "演奏をつづける", + "はじめからやりなおす", + "「曲をえらぶ」にもどる" ], - ka: [ - "カ" + en: [ + "Continue", + "Retry", + "Back to Select Song" ], - daiDon: [ - "ドン(大)", - "ドン(大)" + cn: [ + "继续演奏", + "从头开始", + "返回「选择乐曲」" ], - daiKa: [ - "カッ(大)" + tw: [ + "繼續演奏", + "從頭開始", + "返回「選擇樂曲」" + ], + ko: [ + "연주 계속하기", + "처음부터 다시", + "「곡 선택」으로" ] - } - this.combo = "コンボ" - this.clear = "クリア" - this.good = "良" - this.ok = "可" - this.bad = "不可" - this.branch = { - "normal": "普通譜面", - "advanced": "玄人譜面", - "master": "達人譜面" - } - this.pauseOptions = [ - "演奏をつづける", - "はじめからやりなおす", - "「曲をえらぶ」にもどる" - ] - this.results = "成績発表" - this.points = "点" - this.maxCombo = "最大コンボ数" - this.drumroll = "連打数" + }, + 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: "연타 횟수" + }, - this.errorOccured = "エラーが発生しました。再読み込みしてください。" - this.tutorial = { - basics: [ - "流れてくる音符がワクに重なったらバチで太鼓をたたこう!", - "赤い音符は面をたたこう(%sまたは%s)", - "青い音符はフチをたたこう(%sまたは%s)", - "USBコントローラがサポートされています!" - ], - otherControls: "他のコントロール", - otherTutorial: [ - "%sはゲームを一時停止します", - "曲をえらぶしながら%sか%sキーを押してジャンルをスキップします", - "むずかしさをえらぶしながら%sキーを押しながらオートモードを有効", - "むずかしさをえらぶしながら%sキーを押しながらネットプレイモードを有効" - ], - ok: "OK" - } - this.about = { - bugReporting: [ - "このシミュレータは現在開発中です。", - "バグが発生した場合は、報告してください。", - "Gitリポジトリかメールでバグを報告してください。" - ], - diagnosticWarning: "以下の端末診断情報も併せて報告してください!", - issueTemplate: "###### 下記の問題を説明してください。 スクリーンショットと診断情報を含めてください。", - issues: "課題" - } - this.session = { - multiplayerSession: "オンラインセッション", - linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.", - cancel: "キャンセル" - } - this.settings = { + errorOccured: { + ja: "エラーが発生しました。再読み込みしてください。", + en: "An error occurred, please refresh" + }, + 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 and %s while selecting song \u2014 navigate categories', + "选择难度时按住%s以启用自动模式", + "选择难度时按住%s以启用网络对战模式" + ], + tw: [ + "%s暫停遊戲", + '%s and %s while selecting song \u2014 navigate categories', + "選擇難度時按住%s以啟用自動模式", + "選擇難度時按住%s以啟用網上對打模式" + ], + ko: [ + "%s \u2014 게임을 일시 중지합니다", + '%s and %s while selecting song \u2014 navigate categories', + "난이도 선택 동안 %s 홀드 \u2014 오토 모드 활성화", + "난이도 선택 동안 %s 홀드 \u2014 넷 플레이 모드 활성화" + ], + }, + ok: { + ja: "OK", + en: "OK", + 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." + ], + }, + diagnosticWarning: { + ja: "以下の端末診断情報も併せて報告してください!", + en: "Be sure to include the following diagnostic data!", + }, + issueTemplate: { + ja: "###### 下記の問題を説明してください。 スクリーンショットと診断情報を含めてください。", + en: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.", + }, + issues: { + ja: "課題", + en: "Issues", + cn: "工单", + tw: "問題", + ko: "이슈" + } + }, + session: { + multiplayerSession: { + ja: "オンラインセッション", + en: "Multiplayer Session", + cn: "在线会话", + tw: "多人模式", + ko: null + }, + linkTutorial: { + ja: null, + en: "Share this link with your friend to start playing together! Do not leave this screen while they join.", + cn: "复制下方地址,给你的朋友即可开始一起游戏!当他们与您联系之前,请不要离开此页面。", + tw: "複製下方地址,給你的朋友即可開始一起遊戲!當他們與您聯繫之前,請不要離開此頁面。", + ko: null + }, + cancel: { + ja: "キャンセル", + en: "Cancel", + cn: "取消", + tw: "取消", + ko: "취소" + } + }, + settings: { language: { - name: "言語" + name: { + ja: "言語", + en: "Language", + cn: "语言", + tw: "語系", + ko: "언어" + } }, resolution: { - name: "ゲームの解像度", - high: "高", - medium: "中", - low: "低", - lowest: "最低" + 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: "タッチアニメーション" + name: { + ja: "タッチアニメーション", + en: "Touch Animation", + cn: "触摸动画", + tw: "觸摸動畫", + ko: "터치 애니메이션" + } }, keyboardSettings: { - name: "キーボード設定", - ka_l: "ふち(左)", - don_l: "面(左)", - don_r: "面(右)", - ka_r: "ふち(右)" + 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: "そうさタイプ設定", - a: "タイプA", - b: "タイプB", - c: "タイプC" + 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: "Latency", - value: "Audio: %s, Video: %s", - calibration: "Latency Calibration", - audio: "Audio", - video: "Video", - drumSounds: "Drum Sounds" + name: { + ja: null, + en: "Latency", + }, + value: { + ja: null, + en: "Audio: %s, Video: %s", + }, + calibration: { + ja: null, + en: "Latency Calibration", + }, + audio: { + ja: null, + en: "Audio", + }, + video: { + ja: null, + en: "Video", + }, + drumSounds: { + ja: null, + en: "Drum Sounds", + } }, easierBigNotes: { - name: "簡単な大きな音符" + name: { + ja: "簡単な大きな音符", + en: "Easier Big Notes", + cn: "简单的大音符", + tw: "簡單的大音符", + ko: "쉬운 큰 음표" + } + }, + showLyrics: { + name: { + ja: "歌詞の表示", + en: "Show Lyrics" + } + }, + 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: null, + en: "Latency Calibration", + }, + ms: { + ja: null, + en: "%sms", + }, + back: { + ja: null, + en: "Back to Settings", + }, + retryPrevious: { + ja: null, + en: "Retry Previous", + }, + start: { + ja: null, + en: "Start", + }, + finish: { + ja: null, + en: "Finish", }, - on: "オン", - off: "オフ", - default: "既定値にリセット", - ok: "OK" - } - this.calibration = { - title: "Latency Calibration", - ms: "%sms", - back: "Back to Settings", - retryPrevious: "Retry Previous", - start: "Start", - finish: "Finish", audioHelp: { - title: "Audio Latency Calibration", - content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", - contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" + title: { + ja: null, + en: "Audio Latency Calibration", + }, + content: { + ja: null, + en: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", + }, + contentAlt: { + ja: null, + en: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!", + } + }, + audioComplete: { + ja: null, + en: "Audio Latency Calibration completed!", }, - audioComplete: "Audio Latency Calibration completed!", videoHelp: { - title: "Video Latency Calibration", - content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!" + title: { + ja: null, + en: "Video Latency Calibration", + }, + content: { + ja: null, + 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!", + } + }, + videoComplete: { + ja: null, + en: "Video Latency Calibration completed!", }, - videoComplete: "Video Latency Calibration completed!", results: { - title: "Latency Calibration Results", - content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." + title: { + ja: null, + en: "Latency Calibration Results", + }, + content: { + ja: null, + en: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings.", + } + } + }, + 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: "가입하기" + }, + registerAccount: { + ja: "アカウントを登録", + en: "Register account", + cn: "注册帐号", + tw: "註冊帳號", + ko: "계정 등록" + }, + passwordsDoNotMatch: { + ja: "パスワードが一致しません", + en: "Passwords do not match", + cn: "密码不匹配", + tw: "密碼不匹配", + ko: "비밀번호가 일치하지 않습니다" + }, + newPasswordsDoNotMatch: { + ja: null, + en: "New passwords do not match", + }, + 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: null, + en: "Cancel", + }, + save: { + ja: null, + en: "Save", + }, + displayName: { + en: "Displayed Name", + }, + changePassword: { + ja: null, + en: "Change Password", + }, + currentNewRepeat: { + ja: null, + en: [ + "Current Password", + "New Password", + "Repeat New Password" + ], + }, + deleteAccount: { + ja: null, + en: "Delete Account", + }, + verifyPassword: { + ja: null, + en: "Verify password to delete this account", + } + }, + serverError: { + not_logged_in: { + ja: null, + en: "Not logged in", + }, + invalid_username: { + ja: null, + en: "Invalid username, a username can only contain letters, numbers, and underscores, and must be between 3 and 20 characters long", + }, + username_in_use: { + ja: null, + en: "A user already exists with that username", + }, + invalid_password: { + ja: null, + en: "Cannot use this password, please check that your password is at least 6 characters long", + }, + invalid_username_password: { + ja: null, + en: "Invalid Username or Password", + }, + invalid_display_name: { + ja: null, + en: "Cannot use this name, please check that your new name is at most 25 characters long", + }, + current_password_invalid: { + ja: null, + en: "Current password does not match", + }, + invalid_new_password: { + ja: null, + en: "Cannot use this password, please check that your new password is at least 6 characters long", + }, + verify_password_invalid: { + ja: null, + en: "Verification password does not match", + }, + invalid_csrf: { + ja: null, + en: "Security token expired. Please refresh the page." + } + }, + browserSupport: { + browserWarning: { + ja: "サポートされていないブラウザを実行しています (%s)", + en: "You are running an unsupported browser (%s)", + }, + details: { + ja: "詳しく", + en: "Details...", + }, + failedTests: { + ja: "このテストは失敗しました:", + en: "The following tests have failed:", + }, + supportedBrowser: { + ja: "%sなどのサポートされているブラウザを使用してください", + en: "Please use a supported browser such as %s", + } + }, + creative: { + creative: { + ja: "創作", + en: "Creative", + cn: "创作", + tw: "創作", + ko: "창작" + }, + maker: { + ja: "メーカー", + en: "Maker:", + cn: "制作者", + tw: "製作者", + ko: "만드는 사람" + } + }, + withLyrics: { + ja: "歌詞あり", + en: "With lyrics", + cn: "带歌词", + tw: "帶歌詞", + ko: "가사가있는" + } +} +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) } } - this.browserSupport = { - browserWarning: "サポートされていないブラウザを実行しています (%s)", - details: "詳しく", - failedTests: "このテストは失敗しました:", - supportedBrowser: "%sなどのサポートされているブラウザを使用してください" - } - this.creative = { - creative: '創作', - maker: 'メーカー' - } -} -function StringsEn(){ - this.id = "en" - this.name = "English" - this.regex = /^en$|^en-/ - this.font = "TnT, Meiryo, sans-serif" - - this.taikoWeb = "Taiko Web" - this.titleProceed = "Click or Press Enter!" - this.titleDisclaimer = "This unofficial simulator is unaffiliated with BANDAI NAMCO." - this.titleCopyright = "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc." - this.categories = { - "J-POP": "Pop", - "アニメ": "Anime", - "ボーカロイド™曲": "VOCALOID™ Music", - "バラエティ": "Variety", - "クラシック": "Classical", - "ゲームミュージック": "Game Music", - "ナムコオリジナル": "NAMCO Original" - } - this.selectSong = "Select Song" - this.selectDifficulty = "Select Difficulty" - this.back = "Back" - this.random = "Random" - this.randomSong = "Random Song" - this.howToPlay = "How to Play" - this.aboutSimulator = "About Simulator" - this.gameSettings = "Game Settings" - this.browse = "Browse…" - this.defaultSongList = "Default Song List" - this.songOptions = "Song Options" - this.none = "None" - this.auto = "Auto" - this.netplay = "Netplay" - this.easy = "Easy" - this.normal = "Normal" - this.hard = "Hard" - this.oni = "Extreme" - this.songBranch = "Diverge Notes" - this.sessionStart = "Begin an Online Session!" - this.sessionEnd = "End Online Session" - this.loading = "Loading..." - this.waitingForP2 = "Waiting for Another Player..." - this.cancel = "Cancel" - this.note = { - don: "Don", - ka: "Ka", - daiDon: "DON", - daiKa: "KA", - drumroll: "Drum rollー!!", - daiDrumroll: "DRUM ROLLー!!", - balloon: "Balloon" - } - this.ex_note = { - don: [ - "Do", - "Do" - ], - ka: [ - "Ka" - ], - daiDon: [ - "DON", - "DON" - ], - daiKa: [ - "KA" - ] - } - this.combo = "Combo" - this.clear = "Clear" - this.good = "GOOD" - this.ok = "OK" - this.bad = "BAD" - this.branch = { - "normal": "Normal", - "advanced": "Professional", - "master": "Master" - } - this.pauseOptions = [ - "Continue", - "Retry", - "Back to Select Song" - ] - this.results = "Results" - this.points = "pts" - this.maxCombo = "MAX Combo" - this.drumroll = "Drumroll" - - this.errorOccured = "An error occurred, please refresh" - this.tutorial = { - basics: [ - "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!" - ], - otherControls: "Other controls", - otherTutorial: [ - "%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" - ], - ok: "OK" - } - this.about = { - bugReporting: [ - "This simulator is still in development.", - "Please report any bugs you find.", - "You can report bugs either via our Git repository or email." - ], - diagnosticWarning: "Be sure to include the following diagnostic data!", - issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.", - issues: "Issues" - } - this.session = { - multiplayerSession: "Multiplayer Session", - linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.", - cancel: "Cancel" - } - this.settings = { - language: { - name: "Language" - }, - resolution: { - name: "Game Resolution", - high: "High", - medium: "Medium", - low: "Low", - lowest: "Lowest" - }, - touchAnimation: { - name: "Touch Animation" - }, - keyboardSettings: { - name: "Keyboard Settings", - ka_l: "Left Rim", - don_l: "Left Surface", - don_r: "Right Surface", - ka_r: "Right Rim" - }, - gamepadLayout: { - name: "Gamepad Layout", - a: "Type A", - b: "Type B", - c: "Type C" - }, - latency: { - name: "Latency", - value: "Audio: %s, Video: %s", - calibration: "Latency Calibration", - audio: "Audio", - video: "Video", - drumSounds: "Drum Sounds" - }, - easierBigNotes: { - name: "Easier Big Notes" - }, - on: "On", - off: "Off", - default: "Reset to Defaults", - ok: "OK" - } - this.calibration = { - title: "Latency Calibration", - ms: "%sms", - back: "Back to Settings", - retryPrevious: "Retry Previous", - start: "Start", - finish: "Finish", - audioHelp: { - title: "Audio Latency Calibration", - content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", - contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" - }, - audioComplete: "Audio Latency Calibration completed!", - videoHelp: { - title: "Video Latency Calibration", - content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!" - }, - videoComplete: "Video Latency Calibration completed!", - results: { - title: "Latency Calibration Results", - content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." - } - } - this.browserSupport = { - browserWarning: "You are running an unsupported browser (%s)", - details: "Details...", - failedTests: "The following tests have failed:", - supportedBrowser: "Please use a supported browser such as %s" - } - this.creative = { - creative: 'Creative', - maker: 'Maker:' - } -} -function StringsCn(){ - this.id = "cn" - this.name = "简体中文" - this.regex = /^zh$|^zh-CN$|^zh-SG$/ - this.font = "Microsoft YaHei, sans-serif" - - this.taikoWeb = "太鼓网页" - this.titleProceed = "点击或按回车!" - this.titleDisclaimer = "这款非官方模拟器与BANDAI NAMCO无关。" - this.titleCopyright = "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc." - this.categories = { - "J-POP": "流行音乐", - "アニメ": "卡通动画音乐", - "ボーカロイド™曲": "VOCALOID™ Music", - "バラエティ": "综合音乐", - "クラシック": "古典音乐", - "ゲームミュージック": "游戏音乐", - "ナムコオリジナル": "NAMCO原创音乐" - } - this.selectSong = "选择乐曲" - this.selectDifficulty = "选择难度" - this.back = "返回" - this.random = "随机" - this.randomSong = "随机选曲" - this.howToPlay = "操作说明" - this.aboutSimulator = "关于模拟器" - this.gameSettings = "游戏设定" - this.browse = "浏览…" - this.defaultSongList = "默认歌曲列表" - this.songOptions = "选项" - this.none = "无" - this.auto = "自动" - this.netplay = "网络对战" - this.easy = "简单" - this.normal = "普通" - this.hard = "困难" - this.oni = "魔王" - this.songBranch = "有谱面分歧" - this.sessionStart = "开始在线会话!" - this.sessionEnd = "结束在线会话" - this.loading = "加载中..." - this.waitingForP2 = "正在等待对方玩家..." - this.cancel = "取消" - this.note = { - don: "咚", - ka: "咔", - daiDon: "咚(大)", - daiKa: "咔(大)", - drumroll: "连打ー!!", - daiDrumroll: "连打(大)ー!!", - balloon: "气球" - } - this.ex_note = { - don: [ - "咚", - "咚" - ], - ka: [ - "咔" - ], - daiDon: [ - "咚(大)", - "咚(大)" - ], - daiKa: [ - "咔(大)" - ] - } - this.combo = "连段" - this.clear = "通关" - this.good = "良" - this.ok = "可" - this.bad = "不可" - this.branch = { - "normal": "一般谱面", - "advanced": "进阶谱面", - "master": "达人谱面" - } - this.pauseOptions = [ - "继续演奏", - "从头开始", - "返回「选择乐曲」" - ] - this.results = "发表成绩" - this.points = "点" - this.maxCombo = "最多连段数" - this.drumroll = "连打数" - - this.errorOccured = "An error occurred, please refresh" - this.tutorial = { - basics: [ - "当流动的音符将与框框重叠时就用鼓棒敲打太鼓吧", - "遇到红色音符要敲打鼓面(%s或%s)", - "遇到蓝色音符则敲打鼓边(%s或%s)", - "USB控制器也支持!" - ], - otherControls: "其他控制", - otherTutorial: [ - "%s暂停游戏", - '%s and %s while selecting song \u2014 navigate categories', - "选择难度时按住%s以启用自动模式", - "选择难度时按住%s以启用网络对战模式" - ], - ok: "确定" - } - this.about = { - bugReporting: [ - "This simulator is still in development.", - "Please report any bugs you find.", - "You can report bugs either via our Git repository or email." - ], - diagnosticWarning: "Be sure to include the following diagnostic data!", - issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.", - issues: "工单" - } - this.session = { - multiplayerSession: "在线会话", - linkTutorial: "复制下方地址,给你的朋友即可开始一起游戏!当他们与您联系之前,请不要离开此页面。", - cancel: "取消" - } - this.settings = { - language: { - name: "语言" - }, - resolution: { - name: "游戏分辨率", - high: "高", - medium: "中", - low: "低", - lowest: "最低" - }, - touchAnimation: { - name: "触摸动画" - }, - keyboardSettings: { - name: "键盘设置", - ka_l: "边缘(左)", - don_l: "表面(左)", - don_r: "表面(右)", - ka_r: "边缘(右)" - }, - gamepadLayout: { - name: "操作类型设定", - a: "类型A", - b: "类型B", - c: "类型C" - }, - latency: { - name: "Latency", - value: "Audio: %s, Video: %s", - calibration: "Latency Calibration", - audio: "Audio", - video: "Video", - drumSounds: "Drum Sounds" - }, - easierBigNotes: { - name: "简单的大音符" - }, - on: "开", - off: "关", - default: "重置为默认值", - ok: "确定" - } - this.calibration = { - title: "Latency Calibration", - ms: "%sms", - back: "Back to Settings", - retryPrevious: "Retry Previous", - start: "Start", - finish: "Finish", - audioHelp: { - title: "Audio Latency Calibration", - content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", - contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" - }, - audioComplete: "Audio Latency Calibration completed!", - videoHelp: { - title: "Video Latency Calibration", - content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!" - }, - videoComplete: "Video Latency Calibration completed!", - results: { - title: "Latency Calibration Results", - content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." - } - } - this.browserSupport = { - browserWarning: "You are running an unsupported browser (%s)", - details: "Details...", - failedTests: "The following tests have failed:", - supportedBrowser: "Please use a supported browser such as %s" - } - this.creative = { - creative: '创作', - maker: '制作者' - } -} -function StringsTw(){ - this.id = "tw" - this.name = "正體中文" - this.regex = /^zh-HK$|^zh-TW$/ - this.font = "Microsoft YaHei, sans-serif" - - this.taikoWeb = "太鼓網頁" - this.titleProceed = "點擊或按確認!" - this.titleDisclaimer = "這款非官方模擬器與BANDAI NAMCO無關。" - this.titleCopyright = "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc." - this.categories = { - "J-POP": "流行音樂", - "アニメ": "卡通動畫音樂", - "ボーカロイド™曲": "VOCALOID™ Music", - "バラエティ": "綜合音樂", - "クラシック": "古典音樂", - "ゲームミュージック": "遊戲音樂", - "ナムコオリジナル": "NAMCO原創音樂" - } - this.selectSong = "選擇樂曲" - this.selectDifficulty = "選擇難度" - this.back = "返回" - this.random = "隨機" - this.randomSong = "隨機選曲" - this.howToPlay = "操作說明" - this.aboutSimulator = "關於模擬器" - this.gameSettings = "遊戲設定" - this.browse = "開啟檔案…" - this.defaultSongList = "默認歌曲列表" - this.songOptions = "選項" - this.none = "無" - this.auto = "自動" - this.netplay = "網上對打" - this.easy = "簡單" - this.normal = "普通" - this.hard = "困難" - this.oni = "魔王" - this.songBranch = "有譜面分歧" - this.sessionStart = "開始多人模式!" - this.sessionEnd = "結束多人模式" - this.loading = "讀取中..." - this.waitingForP2 = "正在等待對方玩家..." - this.cancel = "取消" - this.note = { - don: "咚", - ka: "咔", - daiDon: "咚(大)", - daiKa: "咔(大)", - drumroll: "連打ー!!", - daiDrumroll: "連打(大)ー!!", - balloon: "氣球" - } - this.ex_note = { - don: [ - "咚", - "咚" - ], - ka: [ - "咔" - ], - daiDon: [ - "咚(大)", - "咚(大)" - ], - daiKa: [ - "咔(大)" - ] - } - this.combo = "連段" - this.clear = "通關" - this.good = "良" - this.ok = "可" - this.bad = "不可" - this.branch = { - "normal": "一般譜面", - "advanced": "進階譜面", - "master": "達人譜面" - } - this.pauseOptions = [ - "繼續演奏", - "從頭開始", - "返回「選擇樂曲」" - ] - this.results = "發表成績" - this.points = "分" - this.maxCombo = "最多連段數" - this.drumroll = "連打數" - - this.errorOccured = "An error occurred, please refresh" - this.tutorial = { - basics: [ - "當流動的音符將與框框重疊時就用鼓棒敲打太鼓吧", - "遇到紅色音符要敲打鼓面(%s或%s)", - "遇到藍色音符則敲打鼓邊(%s或%s)", - "USB控制器也支持!" - ], - otherControls: "其他控制", - otherTutorial: [ - "%s暫停遊戲", - '%s and %s while selecting song \u2014 navigate categories', - "選擇難度時按住%s以啟用自動模式", - "選擇難度時按住%s以啟用網上對打模式" - ], - ok: "確定" - } - this.about = { - bugReporting: [ - "This simulator is still in development.", - "Please report any bugs you find.", - "You can report bugs either via our Git repository or email." - ], - diagnosticWarning: "Be sure to include the following diagnostic data!", - issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.", - issues: "問題" - } - this.session = { - multiplayerSession: "多人模式", - linkTutorial: "複製下方地址,給你的朋友即可開始一起遊戲!當他們與您聯繫之前,請不要離開此頁面。", - cancel: "取消" - } - this.settings = { - language: { - name: "語系" - }, - resolution: { - name: "遊戲分辨率", - high: "高", - medium: "中", - low: "低", - lowest: "最低" - }, - touchAnimation: { - name: "觸摸動畫" - }, - keyboardSettings: { - name: "鍵盤設置", - ka_l: "邊緣(左)", - don_l: "表面(左)", - don_r: "表面(右)", - ka_r: "邊緣(右)" - }, - gamepadLayout: { - name: "操作類型設定", - a: "類型A", - b: "類型B", - c: "類型C" - }, - latency: { - name: "Latency", - value: "Audio: %s, Video: %s", - calibration: "Latency Calibration", - audio: "Audio", - video: "Video", - drumSounds: "Drum Sounds" - }, - easierBigNotes: { - name: "簡單的大音符" - }, - on: "開", - off: "關", - default: "重置為默認值", - ok: "確定" - } - this.calibration = { - title: "Latency Calibration", - ms: "%sms", - back: "Back to Settings", - retryPrevious: "Retry Previous", - start: "Start", - finish: "Finish", - audioHelp: { - title: "Audio Latency Calibration", - content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", - contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" - }, - audioComplete: "Audio Latency Calibration completed!", - videoHelp: { - title: "Video Latency Calibration", - content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!" - }, - videoComplete: "Video Latency Calibration completed!", - results: { - title: "Latency Calibration Results", - content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." - } - } - this.browserSupport = { - browserWarning: "You are running an unsupported browser (%s)", - details: "Details...", - failedTests: "The following tests have failed:", - supportedBrowser: "Please use a supported browser such as %s" - } - this.creative = { - creative: '創作', - maker: '製作者' - } -} -function StringsKo(){ - this.id = "ko" - this.name = "한국어" - this.regex = /^ko$|^ko-/ - this.font = "Microsoft YaHei, sans-serif" - - this.taikoWeb = "태고 웹" - this.titleProceed = "클릭하거나 Enter를 누릅니다!" - this.titleDisclaimer = "이 비공식 시뮬레이터는 반다이 남코와 관련이 없습니다." - this.titleCopyright = "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc." - this.categories = { - "J-POP": "POP", - "アニメ": "애니메이션", - "ボーカロイド™曲": "VOCALOID™ Music", - "バラエティ": "버라이어티", - "クラシック": "클래식", - "ゲームミュージック": "게임", - "ナムコオリジナル": "남코 오리지널" - } - this.selectSong = "곡 선택" - this.selectDifficulty = "난이도 선택" - this.back = "돌아간다" - this.random = "랜덤" - this.randomSong = "랜덤" - this.howToPlay = "지도 시간" - this.aboutSimulator = "게임 정보" - this.gameSettings = "게임 설정" - this.browse = "찾아보기…" - this.defaultSongList = "기본 노래 목록" - this.songOptions = "옵션" - this.none = "없음" - this.auto = "오토" - this.netplay = "넷 플레이" - this.easy = "쉬움" - this.normal = "보통" - this.hard = "어려움" - this.oni = "귀신" - this.songBranch = "악보 분기 있습니다" - this.sessionStart = "온라인 세션 시작!" - this.sessionEnd = "온라인 세션 끝내기" - this.loading = "로딩 중..." - this.waitingForP2 = "Waiting for Another Player..." - this.cancel = "취소" - this.note = { - don: "쿵", - ka: "딱", - daiDon: "쿵(대)", - daiKa: "딱(대)", - drumroll: "연타ー!!", - daiDrumroll: "연타(대)ー!!", - balloon: "풍선" - } - this.ex_note = { - don: [ - "쿠", - "쿠" - ], - ka: [ - "딱" - ], - daiDon: [ - "쿵(대)", - "쿵(대)" - ], - daiKa: [ - "딱(대)" - ] - } - this.combo = "콤보" - this.clear = "클리어" - this.good = "얼쑤" - this.ok = "좋다" - this.bad = "에구" - this.branch = { - "normal": "보통 악보", - "advanced": "현인 악보", - "master": "달인 악보" - } - this.pauseOptions = [ - "연주 계속하기", - "처음부터 다시", - "「곡 선택」으로" - ] - this.results = "성적 발표" - this.points = "점" - this.maxCombo = "최대 콤보 수" - this.drumroll = "연타 횟수" - - this.errorOccured = "An error occurred, please refresh" - this.tutorial = { - basics: [ - "이동하는 음표가 테두리와 겹쳐졌을 때 북채로 태고를 두드리자!", - "빨간 음표는 면을 두드리자 (%s 또는 %s)", - "파란 음표는 테를 두드리자 (%s 또는 %s)", - "USB 컨트롤러도 지원됩니다!" - ], - otherControls: "기타 컨트롤", - otherTutorial: [ - "%s \u2014 게임을 일시 중지합니다", - '%s and %s while selecting song \u2014 navigate categories', - "난이도 선택 동안 %s 홀드 \u2014 오토 모드 활성화", - "난이도 선택 동안 %s 홀드 \u2014 넷 플레이 모드 활성화" - ], - ok: "확인" - } - this.about = { - bugReporting: [ - "This simulator is still in development.", - "Please report any bugs you find.", - "You can report bugs either via our Git repository or email." - ], - diagnosticWarning: "Be sure to include the following diagnostic data!", - issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.", - issues: "이슈" - } - this.session = { - multiplayerSession: "Multiplayer Session", - linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.", - cancel: "취소" - } - this.settings = { - language: { - name: "언어" - }, - resolution: { - name: "게임 해상도", - high: "높은", - medium: "중간", - low: "저", - lowest: "최저" - }, - touchAnimation: { - name: "터치 애니메이션" - }, - keyboardSettings: { - name: "키보드 설정", - ka_l: "가장자리 (왼쪽)", - don_l: "표면 (왼쪽)", - don_r: "표면 (오른쪽)", - ka_r: "가장자리 (오른쪽)" - }, - gamepadLayout: { - name: "조작 타입 설정", - a: "타입 A", - b: "타입 B", - c: "타입 C" - }, - latency: { - name: "Latency", - value: "Audio: %s, Video: %s", - calibration: "Latency Calibration", - audio: "Audio", - video: "Video", - drumSounds: "Drum Sounds" - }, - easierBigNotes: { - name: "쉬운 큰 음표" - }, - on: "온", - off: "오프", - default: "기본값으로 재설정", - ok: "확인" - } - this.calibration = { - title: "Latency Calibration", - ms: "%sms", - back: "Back to Settings", - retryPrevious: "Retry Previous", - start: "Start", - finish: "Finish", - audioHelp: { - title: "Audio Latency Calibration", - content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", - contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" - }, - audioComplete: "Audio Latency Calibration completed!", - videoHelp: { - title: "Video Latency Calibration", - content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!" - }, - videoComplete: "Video Latency Calibration completed!", - results: { - title: "Latency Calibration Results", - content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." - } - } - this.browserSupport = { - browserWarning: "You are running an unsupported browser (%s)", - details: "Details...", - failedTests: "The following tests have failed:", - supportedBrowser: "Please use a supported browser such as %s" - } - this.creative = { - creative: '창작', - maker: '만드는 사람' - } -} -var allStrings = { - "ja": new StringsJa(), - "en": new StringsEn(), - "cn": new StringsCn(), - "tw": new StringsTw(), - "ko": new StringsKo() } +separateStrings() diff --git a/public/src/js/view.js b/public/src/js/view.js index 9964c83..4326379 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -126,8 +126,14 @@ this.comboCache = new CanvasCache(noSmoothing) this.pauseCache = new CanvasCache(noSmoothing) this.branchCache = new CanvasCache(noSmoothing) + this.nameplateCache = new CanvasCache(noSmoothing) this.multiplayer = this.controller.multiplayer + 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 @@ -223,24 +229,31 @@ this.winH = winH this.ratio = ratio - if(this.multiplayer !== 2){ + if(this.player !== 2){ this.canvas.width = winW this.canvas.height = 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.multiplayer !== 2){ + }else if(this.player !== 2){ ctx.clearRect(0, 0, winW / ratio, winH / ratio) } winW /= ratio @@ -257,8 +270,8 @@ var frameTop = winH / 2 - 720 / 2 var frameLeft = winW / 2 - 1280 / 2 } - if(this.multiplayer === 2){ - frameTop += this.multiplayer === 2 ? 165 : 176 + if(this.player === 2){ + frameTop += 165 } if(touchMultiplayer){ if(!this.touchp2Class){ @@ -273,16 +286,20 @@ this.setDonBgHeight() } + if(this.controller.lyrics){ + this.controller.lyrics.update(ms) + } + ctx.save() ctx.translate(0, frameTop) this.drawGogoTime() - if(!touchMultiplayer || this.multiplayer === 1 && frameTop >= 0){ + if(!touchMultiplayer || this.player === 1 && frameTop >= 0){ this.assets.drawAssets("background") } - if(this.multiplayer !== 2){ + if(this.player !== 2){ this.titleCache.get({ ctx: ctx, x: winW - (touchMultiplayer && fullScreenSupported ? 750 : 650), @@ -350,7 +367,7 @@ var score = this.controller.getGlobalScore() var gaugePercent = this.rules.gaugePercent(score.gauge) - if(this.multiplayer === 2){ + if(this.player === 2){ var scoreImg = "bg_score_p2" var scoreFill = "#6bbec0" }else{ @@ -373,30 +390,55 @@ size: 100, paddingLeft: 0 } - this.scorePos = {x: 363, y: frameTop + (this.multiplayer === 2 ? 520 : 227)} + this.scorePos = {x: 363, y: frameTop + (this.player === 2 ? 520 : 227)} var animPos = { x1: this.slotPos.x + 13, - y1: this.slotPos.y + (this.multiplayer === 2 ? 27 : -27), + y1: this.slotPos.y + (this.player === 2 ? 27 : -27), x2: winW - 38, - y2: frameTop + (this.multiplayer === 2 ? 484 : 293) + y2: frameTop + (this.player === 2 ? 484 : 293) } var taikoPos = { x: 19, - y: frameTop + (this.multiplayer === 2 ? 464 : 184), + 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.multiplayer === 2 ? 306 : 288, + this.player === 2 ? 306 : 288, winW, - this.multiplayer === 1 ? 184 : 183 + this.player === 1 ? 184 : 183 ) ctx.beginPath() - if(this.multiplayer === 2){ + if(this.player === 2){ ctx.moveTo(0, 467) ctx.lineTo(384, 467) ctx.lineTo(384, 512) @@ -415,7 +457,7 @@ ctx.fillStyle = scoreFill var leftSide = (ctx, mul) => { ctx.beginPath() - if(this.multiplayer === 2){ + if(this.player === 2){ ctx.moveTo(0, 468 * mul) ctx.lineTo(380 * mul, 468 * mul) ctx.lineTo(380 * mul, 512 * mul) @@ -445,7 +487,7 @@ // Score background ctx.fillStyle = "#000" ctx.beginPath() - if(this.multiplayer === 2){ + if(this.player === 2){ this.draw.roundedCorner(ctx, 184, 512, 20, 0) ctx.lineTo(384, 512) this.draw.roundedCorner(ctx, 384, 560, 12, 2) @@ -463,16 +505,16 @@ ctx.drawImage(assets.image["difficulty"], 0, 144 * this.difficulty[this.controller.selectedSong.difficulty], 168, 143, - 126, this.multiplayer === 2 ? 497 : 228, + 126, this.player === 2 ? 497 : 228, 62, 53 ) } // Badges - if(this.controller.autoPlayEnabled && !this.controller.multiplayer){ + if(this.controller.autoPlayEnabled && !this.multiplayer){ this.ctx.drawImage(assets.image["badge_auto"], 183, - this.multiplayer === 2 ? 490 : 265, + this.player === 2 ? 490 : 265, 23, 23 ) @@ -482,7 +524,7 @@ ctx.fillStyle = "#000" ctx.beginPath() var gaugeX = winW - 788 * 0.7 - 32 - if(this.multiplayer === 2){ + if(this.player === 2){ ctx.moveTo(gaugeX, 464) ctx.lineTo(winW, 464) ctx.lineTo(winW, 489) @@ -497,18 +539,18 @@ this.draw.gauge({ ctx: ctx, x: winW, - y: this.multiplayer === 2 ? 468 : 273, + y: this.player === 2 ? 468 : 273, clear: this.rules.gaugeClear, percentage: gaugePercent, font: this.font, scale: 0.7, - multiplayer: this.multiplayer === 2, - blue: this.multiplayer === 2 + multiplayer: this.player === 2, + blue: this.player === 2 }) this.draw.soul({ ctx: ctx, x: winW - 40, - y: this.multiplayer === 2 ? 484 : 293, + y: this.player === 2 ? 484 : 293, scale: 0.75, cleared: this.rules.clearReached(score.gauge) }) @@ -536,26 +578,50 @@ } this.scorePos = { x: 155, - y: frameTop + (this.multiplayer === 2 ? 318 : 193) + y: frameTop + (this.player === 2 ? 318 : 193) } var animPos = { x1: this.slotPos.x + 14, - y1: this.slotPos.y + (this.multiplayer === 2 ? 29 : -29), + y1: this.slotPos.y + (this.player === 2 ? 29 : -29), x2: winW - 55, - y2: frameTop + (this.multiplayer === 2 ? 378 : 165) + y2: frameTop + (this.player === 2 ? 378 : 165) } var taikoPos = {x: 179, y: frameTop + 190, w: 138, h: 162} + this.nameplateCache.get({ + ctx: ctx, + x: 320, + y: 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 === 1 ? 177 : 176 + this.multiplayer && this.player === 1 ? 177 : 176 ) ctx.beginPath() - if(this.multiplayer === 2){ + if(this.player === 2){ ctx.moveTo(328, 351) ctx.lineTo(winW, 351) ctx.lineTo(winW, 385) @@ -572,17 +638,17 @@ this.draw.gauge({ ctx: ctx, x: winW, - y: this.multiplayer === 2 ? 357 : 135, + y: this.player === 2 ? 357 : 135, clear: this.rules.gaugeClear, percentage: gaugePercent, font: this.font, - multiplayer: this.multiplayer === 2, - blue: this.multiplayer === 2 + multiplayer: this.player === 2, + blue: this.player === 2 }) this.draw.soul({ ctx: ctx, x: winW - 57, - y: this.multiplayer === 2 ? 378 : 165, + y: this.player === 2 ? 378 : 165, cleared: this.rules.clearReached(score.gauge) }) @@ -614,7 +680,7 @@ ctx.drawImage(assets.image["difficulty"], 0, 144 * this.difficulty[this.controller.selectedSong.difficulty], 168, 143, - 16, this.multiplayer === 2 ? 194 : 232, + 16, this.player === 2 ? 194 : 232, 141, 120 ) var diff = this.controller.selectedSong.difficulty @@ -626,13 +692,13 @@ ctx.fillStyle = "#fff" ctx.lineWidth = 7 ctx.miterLimit = 1 - ctx.strokeText(text, 87, this.multiplayer === 2 ? 310 : 348) - ctx.fillText(text, 87, this.multiplayer === 2 ? 310 : 348) + 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.controller.multiplayer){ + if(this.controller.autoPlayEnabled && !this.multiplayer){ this.ctx.drawImage(assets.image["badge_auto"], 125, 235, 34, 34 ) @@ -641,7 +707,7 @@ // Score background ctx.fillStyle = "#000" ctx.beginPath() - if(this.multiplayer === 2){ + if(this.player === 2){ ctx.moveTo(0, 312) this.draw.roundedCorner(ctx, 176, 312, 20, 1) ctx.lineTo(176, 353) @@ -666,11 +732,11 @@ }, { // 560, 10 x: animPos.x1 + animPos.w / 6, - y: animPos.y1 - animPos.h * (this.multiplayer === 2 ? 2.5 : 3.5) + 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.multiplayer === 2 ? 3.5 : 5) + y: animPos.y2 - animPos.h * (this.player === 2 ? 3.5 : 5) }, { // 1225, 165 x: animPos.x2, @@ -1390,12 +1456,12 @@ var selectedSong = this.controller.selectedSong var songSkinName = selectedSong.songSkin.name var donLayers = [] - var filename = !selectedSong.songSkin.don && this.multiplayer === 2 ? "bg_don2_" : "bg_don_" + 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.multiplayer === 2){ + if(this.player === 2){ this.donBg.classList.add("donbg-bottom") } for(var layer = 1; layer <= 3; layer++){ @@ -1525,17 +1591,21 @@ // Start animation to gauge circle.animate(ms) } - if(ms - this.controller.audioLatency >= circle.ms && !circle.beatMSCopied && (!circle.branch || circle.branch.active)){ - if(this.beatInterval !== circle.beatMS){ - this.changeBeatInterval(circle.beatMS) + } + 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) } - circle.beatMSCopied = true + event.beatMSCopied = true } - if(ms - this.controller.audioLatency >= circle.ms && !circle.gogoChecked && (!circle.branch || circle.branch.active)){ - if(this.gogoTime != circle.gogoTime){ - this.toggleGogoTime(circle) + if(ms - this.controller.audioLatency >= event.ms && !event.gogoChecked && (!event.branch || event.branch.active)){ + if(this.gogoTime != event.gogoTime){ + this.toggleGogoTime(event) } - circle.gogoChecked = true + event.gogoChecked = true } } } diff --git a/public/src/js/viewassets.js b/public/src/js/viewassets.js index dae06c6..9affaf7 100644 --- a/public/src/js/viewassets.js +++ b/public/src/js/viewassets.js @@ -18,7 +18,7 @@ class ViewAssets{ sw: imgw, sh: imgh - 1, x: view.portrait ? -60 : 0, - y: view.portrait ? (view.multiplayer === 2 ? 560 : 35) : (view.multiplayer === 2 ? 360 : 2), + y: view.portrait ? (view.player === 2 ? 560 : 35) : (view.player === 2 ? 360 : 2), w: w, h: h - 1 } diff --git a/public/src/views/about.html b/public/src/views/about.html index ae18ed6..0e168ee 100644 --- a/public/src/views/about.html +++ b/public/src/views/about.html @@ -2,7 +2,7 @@
-
+
+
+
+
An error occurred, please refresh
+
+ Debug +
+
diff --git a/public/src/views/login.html b/public/src/views/login.html new file mode 100644 index 0000000..d8a76b6 --- /dev/null +++ b/public/src/views/login.html @@ -0,0 +1,25 @@ +
+
+
+
+
+ +
+
+ +
+
+
+
diff --git a/schema.py b/schema.py new file mode 100644 index 0000000..1ba0c81 --- /dev/null +++ b/schema.py @@ -0,0 +1,73 @@ +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_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 index 04e802d..b7057fc 100644 --- a/server.py +++ b/server.py @@ -13,11 +13,11 @@ server_status = { } consonants = "bcdfghjklmnpqrstvwxyz" -def msgobj(type, value=None): +def msgobj(msg_type, value=None): if value == None: - return json.dumps({"type": type}) + return json.dumps({"type": msg_type}) else: - return json.dumps({"type": type, "value": value}) + return json.dumps({"type": msg_type, "value": value}) def status_event(): value = [] @@ -42,7 +42,8 @@ async def connection(ws, path): user = { "ws": ws, "action": "ready", - "session": False + "session": False, + "name": None } server_status["users"].append(user) try: @@ -69,16 +70,17 @@ async def connection(ws, path): except json.decoder.JSONDecodeError: data = {} action = user["action"] - type = data["type"] if "type" in data else None + 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 type == "join": + 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 if not id or not diff: continue if id not in waiting: @@ -92,6 +94,7 @@ async def connection(ws, path): await ws.send(msgobj("waiting")) else: # Join the other user and start game + user["name"] = value["name"] if "name" in value else None user["other_user"] = waiting[id]["user"] waiting_diff = waiting[id]["diff"] del waiting[id] @@ -99,9 +102,13 @@ async def connection(ws, path): 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", waiting_diff)), - user["other_user"]["ws"].send(msgobj("gameload", diff)) + ws.send(msgobj("gameload", {"diff": waiting_diff, "player": 2})), + user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff, "player": 1})), + ws.send(msgobj("name", user["other_user"]["name"])), + user["other_user"]["ws"].send(msgobj("name", user["name"])) ]) else: # Wait for another user @@ -115,28 +122,33 @@ async def connection(ws, path): await ws.send(msgobj("waiting")) # Update others on waiting players await notify_status() - elif type == "invite": - if value == None: + 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 await ws.send(msgobj("invite", invite)) - elif value in server_status["invites"]: + elif value and "id" in value and value["id"] in server_status["invites"]: # Join a session with the other user - user["other_user"] = server_status["invites"][value] - del server_status["invites"][value] + user["name"] = value["name"] if "name" 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 - sent_msg = msgobj("session") + user["session"] = value["id"] + user["other_user"]["player"] = 1 + user["player"] = 2 await asyncio.wait([ - ws.send(sent_msg), - user["other_user"]["ws"].send(sent_msg) + ws.send(msgobj("session", {"player": 2})), + user["other_user"]["ws"].send(msgobj("session", {"player": 1})), + ws.send(msgobj("invite")), + ws.send(msgobj("name", user["other_user"]["name"])), + user["other_user"]["ws"].send(msgobj("name", user["name"])) ]) - await ws.send(msgobj("invite")) else: del user["other_user"] await ws.send(msgobj("gameend")) @@ -145,7 +157,7 @@ async def connection(ws, path): await ws.send(msgobj("gameend")) elif action == "waiting" or action == "loading" or action == "loaded": # Waiting for another user - if type == "leave": + if msg_type == "leave": # Stop waiting if user["session"]: if "other_user" in user and "ws" in user["other_user"]: @@ -170,7 +182,7 @@ async def connection(ws, path): notify_status() ]) if action == "loading": - if type == "gamestart": + if msg_type == "gamestart": user["action"] = "loaded" if user["other_user"]["action"] == "loaded": user["action"] = "playing" @@ -183,12 +195,12 @@ async def connection(ws, path): elif action == "playing": # Playing with another user if "other_user" in user and "ws" in user["other_user"]: - if type == "note"\ - or type == "drumroll"\ - or type == "branch"\ - or type == "gameresults": - await user["other_user"]["ws"].send(msgobj(type, value)) - elif type == "songsel" and user["session"]: + 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") @@ -199,7 +211,7 @@ async def connection(ws, path): user["other_user"]["ws"].send(sent_msg1), user["other_user"]["ws"].send(sent_msg2) ]) - elif type == "gameend": + elif msg_type == "gameend": # User wants to disconnect user["action"] = "ready" user["other_user"]["action"] = "ready" @@ -222,7 +234,7 @@ async def connection(ws, path): ws.send(status_event()) ]) elif action == "invite": - if type == "leave": + if msg_type == "leave": # Cancel session invite if user["session"] in server_status["invites"]: del server_status["invites"][user["session"]] @@ -243,11 +255,11 @@ async def connection(ws, path): ws.send(msgobj("left")), ws.send(status_event()) ]) - elif type == "songsel" and "other_user" in user: + 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(type) + sent_msg = msgobj(msg_type) await asyncio.wait([ ws.send(sent_msg), user["other_user"]["ws"].send(sent_msg) @@ -262,15 +274,22 @@ async def connection(ws, path): elif action == "songsel": # Session song selection if "other_user" in user and "ws" in user["other_user"]: - if type == "songsel" or type == "catjump": + if msg_type == "songsel" or msg_type == "catjump": # Change song select position - if user["other_user"]["action"] == "songsel": - sent_msg = msgobj(type, value) + 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 type == "join": + 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 @@ -282,8 +301,8 @@ async def connection(ws, path): user["action"] = "loading" user["other_user"]["action"] = "loading" await asyncio.wait([ - ws.send(msgobj("gameload", user["other_user"]["gamediff"])), - user["other_user"]["ws"].send(msgobj("gameload", diff)) + ws.send(msgobj("gameload", {"diff": user["other_user"]["gamediff"]})), + user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff})) ]) else: user["action"] = "waiting" @@ -292,7 +311,7 @@ async def connection(ws, path): "id": id, "diff": diff }])) - elif type == "gameend": + elif msg_type == "gameend": # User wants to disconnect user["action"] = "ready" user["session"] = False diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..6080e06 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,23 @@ + + + + + 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..ff221c8 --- /dev/null +++ b/templates/admin_song_detail.html @@ -0,0 +1,134 @@ +{% extends 'admin.html' %} +{% block content %} +

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

+{% 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..35557b0 --- /dev/null +++ b/templates/admin_song_new.html @@ -0,0 +1,122 @@ +{% extends 'admin.html' %} +{% block content %} +

New song

+{% 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..2ef70ee --- /dev/null +++ b/templates/admin_songs.html @@ -0,0 +1,21 @@ +{% 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 %} +

{{ song.title_lang.en }} ({{ song.title }})

+ {% else %} +

{{ song.title }}

+ {% endif %} +
+
+{% endfor %} +{% endblock %} diff --git a/tools/get_version.bat b/tools/get_version.bat index 3d2737d..72f2562 100644 --- a/tools/get_version.bat +++ b/tools/get_version.bat @@ -1,4 +1,4 @@ @echo off ( -git log -1 --pretty="format:{\"commit\": \"%%H\", \"commit_short\": \"%%h\", \"version\": \"%%ad\", \"url\": \"https://github.com/bui/taiko-web/\"}" --date="format:%%y.%%m.%%d" +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 index 8d46c64..13ee916 100755 --- a/tools/get_version.sh +++ b/tools/get_version.sh @@ -1 +1,3 @@ -git log -1 --pretty="format:{\"commit\": \"%H\", \"commit_short\": \"%h\", \"version\": \"%ad\", \"url\": \"https://github.com/bui/taiko-web/\"}" --date="format:%y.%m.%d" > ../version.json +#!/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/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()