Commit d910de6b authored by Bui's avatar Bui

Merge branch 'add-lyrics'

parents 47545e9d 967d882a
...@@ -36,6 +36,7 @@ $RECYCLE.BIN/ ...@@ -36,6 +36,7 @@ $RECYCLE.BIN/
.Trashes .Trashes
.vscode .vscode
*.pyc
# Directories potentially created on remote AFP share # Directories potentially created on remote AFP share
.AppleDB .AppleDB
...@@ -48,5 +49,5 @@ public/api ...@@ -48,5 +49,5 @@ public/api
taiko.db taiko.db
version.json version.json
public/index.html public/index.html
config.json config.py
public/assets/song_skins public/assets/song_skins
#!/usr/bin/env python2 #!/usr/bin/env python3
from __future__ import division
import base64
import bcrypt
import hashlib
import config
import json import json
import sqlite3
import re import re
import requests
import schema
import os 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_caching import Cache
from flask_session import Session
from flask_wtf.csrf import CSRFProtect, generate_csrf, CSRFError
from ffmpy import FFmpeg from ffmpy import FFmpeg
from pymongo import MongoClient
app = Flask(__name__) app = Flask(__name__)
try: client = MongoClient(host=config.MONGO['host'])
app.cache = Cache(app, config={'CACHE_TYPE': 'redis'})
except RuntimeError:
import tempfile
app.cache = Cache(app, config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': tempfile.gettempdir()})
DATABASE = 'taiko.db' app.secret_key = config.SECRET_KEY
DEFAULT_URL = 'https://github.com/bui/taiko-web/' 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 get_config():
if os.path.isfile('config.json'): def generate_hash(id, form):
try: md5 = hashlib.md5()
config = json.load(open('config.json', 'r')) if form['type'] == 'tja':
except ValueError: urls = ['%s%s/main.tja' % (config.SONGS_BASEURL, id)]
print('WARNING: Invalid config.json, using default values')
config = {}
else: else:
print('WARNING: No config.json found, using default values') urls = []
config = {} 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():
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'): if not config_out.get('songs_baseurl'):
config['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/' config_out['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/'
if not config.get('assets_baseurl'): if not config_out.get('assets_baseurl'):
config['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/' config_out['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/'
config['_version'] = get_version() config_out['_version'] = get_version()
return config return config_out
def get_version(): 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'): if os.path.isfile('version.json'):
try: try:
ver = json.load(open('version.json', 'r')) ver = json.load(open('version.json', 'r'))
...@@ -72,20 +137,158 @@ def get_version(): ...@@ -72,20 +137,158 @@ def get_version():
return 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.route('/')
@app.cache.cached(timeout=15)
def route_index(): def route_index():
version = get_version() version = get_version()
return render_template('index.html', version=version, config=get_config()) 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/<int:id>')
@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/<int:id>', 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/<int:id>/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.route('/api/preview')
@app.cache.cached(timeout=15, query_string=True) @app.cache.cached(timeout=15, query_string=True)
def route_api_preview(): def route_api_preview():
...@@ -93,12 +296,12 @@ def route_api_preview(): ...@@ -93,12 +296,12 @@ def route_api_preview():
if not song_id or not re.match('^[0-9]+$', song_id): if not song_id or not re.match('^[0-9]+$', song_id):
abort(400) abort(400)
song_row = query_db('select * from songs where id = ? and enabled = 1', (song_id,)) song = db.songs.find_one({'id': song_id})
if not song_row: if not song:
abort(400) abort(400)
song_type = song_row[0]['type'] song_type = song['type']
prev_path = make_preview(song_id, song_type, song_row[0]['preview']) prev_path = make_preview(song_id, song_type, song['preview'])
if not prev_path: if not prev_path:
return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id) return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id)
...@@ -108,52 +311,30 @@ def route_api_preview(): ...@@ -108,52 +311,30 @@ def route_api_preview():
@app.route('/api/songs') @app.route('/api/songs')
@app.cache.cached(timeout=15) @app.cache.cached(timeout=15)
def route_api_songs(): 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') songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False}))
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 = []
for song in songs: for song in songs:
song_id = song['id'] if song['maker_id']:
song_type = song['type'] if song['maker_id'] == 0:
preview = song['preview'] song['maker'] = 0
else:
category_out = categories[song['category']] if song['category'] in categories else '' song['maker'] = db.makers.find_one({'id': song['maker_id']}, {'_id': False})
song_skin_out = song_skins[song['skin_id']] if song['skin_id'] in song_skins else None else:
maker = None song['maker'] = None
if song['maker_id'] == 0: del song['maker_id']
maker = 0
elif song['maker_id'] and song['maker_id'] > 0: if song['category_id']:
maker = {'name': song['name'], 'url': song['url'], 'id': song['maker_id']} song['category'] = db.categories.find_one({'id': song['category_id']})['title']
else:
songs_out.append({ song['category'] = None
'id': song_id, del song['category_id']
'title': song['title'],
'title_lang': song['title_lang'], if song['skin_id']:
'subtitle': song['subtitle'], song['song_skin'] = db.song_skins.find_one({'id': song['skin_id']}, {'_id': False, 'id': False})
'subtitle_lang': song['subtitle_lang'], else:
'stars': [ song['song_skin'] = None
song['easy'], song['normal'], song['hard'], song['oni'], song['ura'] del song['skin_id']
],
'preview': preview, return jsonify(songs)
'category': category_out,
'type': song_type,
'offset': song['offset'],
'song_skin': song_skin_out,
'volume': song['volume'],
'maker': maker,
'hash': song['hash']
})
return jsonify(songs_out)
@app.route('/api/config') @app.route('/api/config')
...@@ -163,6 +344,183 @@ def route_api_config(): ...@@ -163,6 +344,183 @@ def route_api_config():
return jsonify(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): def make_preview(song_id, song_type, preview):
song_path = 'public/songs/%s/main.mp3' % song_id song_path = 'public/songs/%s/main.mp3' % song_id
prev_path = 'public/songs/%s/preview.mp3' % song_id prev_path = 'public/songs/%s/preview.mp3' % song_id
......
{
"songs_baseurl": "",
"assets_baseurl": ""
}
# 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/'
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;
}
...@@ -123,6 +123,7 @@ ...@@ -123,6 +123,7 @@
} }
#debug .autoplay-label, #debug .autoplay-label,
#debug .branch-hide{ #debug .branch-hide,
#debug .lyrics-hide{
display: none; display: none;
} }
...@@ -89,3 +89,39 @@ ...@@ -89,3 +89,39 @@
.fix-animations *{ .fix-animations *{
animation: none !important; 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;
}
...@@ -117,3 +117,20 @@ body{ ...@@ -117,3 +117,20 @@ body{
color: #777; color: #777;
text-shadow: 0.05em 0.05em #fff; 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;
}
...@@ -108,8 +108,8 @@ kbd{ ...@@ -108,8 +108,8 @@ kbd{
.left-buttons .taibtn{ .left-buttons .taibtn{
margin-right: 0.4em; margin-right: 0.4em;
} }
#diag-txt textarea, .diag-txt textarea,
#diag-txt iframe{ .diag-txt iframe{
width: 100%; width: 100%;
height: 5em; height: 5em;
font-size: inherit; font-size: inherit;
...@@ -119,6 +119,7 @@ kbd{ ...@@ -119,6 +119,7 @@ kbd{
background: #fff; background: #fff;
border: 1px solid #a9a9a9; border: 1px solid #a9a9a9;
user-select: all; user-select: all;
box-sizing: border-box;
} }
.text-warn{ .text-warn{
color: #d00; color: #d00;
...@@ -291,3 +292,88 @@ kbd{ ...@@ -291,3 +292,88 @@ kbd{
.left-buttons .taibtn{ .left-buttons .taibtn{
z-index: 1; 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;
}
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
cancelTouch = false cancelTouch = false
this.endButton = this.getElement("view-end-button") 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.version = document.getElementById("version-link").href
this.tutorialOuter = this.getElement("view-outer") this.tutorialOuter = this.getElement("view-outer")
if(touchEnabled){ if(touchEnabled){
......
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
}
}
...@@ -31,7 +31,9 @@ var assets = { ...@@ -31,7 +31,9 @@ var assets = {
"importsongs.js", "importsongs.js",
"logo.js", "logo.js",
"settings.js", "settings.js",
"scorestorage.js" "scorestorage.js",
"account.js",
"lyrics.js"
], ],
"css": [ "css": [
"main.css", "main.css",
...@@ -86,11 +88,7 @@ var assets = { ...@@ -86,11 +88,7 @@ var assets = {
"settings_gamepad.png" "settings_gamepad.png"
], ],
"audioSfx": [ "audioSfx": [
"se_cancel.wav",
"se_don.wav",
"se_ka.wav",
"se_pause.wav", "se_pause.wav",
"se_jump.wav",
"se_calibration.wav", "se_calibration.wav",
"v_results.wav", "v_results.wav",
...@@ -102,6 +100,10 @@ var assets = { ...@@ -102,6 +100,10 @@ var assets = {
"audioSfxLR": [ "audioSfxLR": [
"neiro_1_don.wav", "neiro_1_don.wav",
"neiro_1_ka.wav", "neiro_1_ka.wav",
"se_cancel.wav",
"se_don.wav",
"se_ka.wav",
"se_jump.wav",
"se_balloon.wav", "se_balloon.wav",
"se_gameclear.wav", "se_gameclear.wav",
...@@ -137,7 +139,9 @@ var assets = { ...@@ -137,7 +139,9 @@ var assets = {
"about.html", "about.html",
"debug.html", "debug.html",
"session.html", "session.html",
"settings.html" "settings.html",
"account.html",
"login.html"
], ],
"songs": [], "songs": [],
......
...@@ -706,12 +706,12 @@ ...@@ -706,12 +706,12 @@
}) })
}else if(r.smallHiragana.test(symbol)){ }else if(r.smallHiragana.test(symbol)){
// Small hiragana, small katakana // 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)){ }else if(r.hiragana.test(symbol)){
// Hiragana, katakana // 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{ }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 @@ ...@@ -720,6 +720,9 @@
if(config.letterSpacing){ if(config.letterSpacing){
symbol.w += config.letterSpacing symbol.w += config.letterSpacing
} }
if(config.kanaSpacing && symbol.kana){
symbol.w += config.kanaSpacing
}
drawnWidth += symbol.w * mul drawnWidth += symbol.w * mul
} }
...@@ -924,8 +927,22 @@ ...@@ -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" var skip = words[i].substitute || words[i] === "\n"
if(!skip){ if(!skip){
var currentWidth = ctx.measureText(line + words[i]).width var currentWidth = ctx.measureText(line + words[i]).width
...@@ -957,8 +974,22 @@ ...@@ -957,8 +974,22 @@
recenter() recenter()
x = 0 x = 0
y += lineHeight y += lineHeight
line = words[i] === "\n" ? "" : words[i] if(words[i] === "\n"){
lastWidth = ctx.measureText(line).width 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{ }else{
line += words[i] line += words[i]
...@@ -1549,6 +1580,99 @@ ...@@ -1549,6 +1580,99 @@
ctx.restore() 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){ alpha(amount, ctx, callback, winW, winH){
if(amount >= 1){ if(amount >= 1){
return callback(ctx) return callback(ctx)
......
...@@ -6,7 +6,11 @@ class Controller{ ...@@ -6,7 +6,11 @@ class Controller{
this.saveScore = !autoPlayEnabled this.saveScore = !autoPlayEnabled
this.multiplayer = multiplayer this.multiplayer = multiplayer
this.touchEnabled = touchEnabled 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.calibrationMode = selectedSong.folder === "calibration"
this.audioLatency = 0 this.audioLatency = 0
...@@ -53,6 +57,15 @@ class Controller{ ...@@ -53,6 +57,15 @@ class Controller{
if(song.id == this.selectedSong.folder){ if(song.id == this.selectedSong.folder){
this.mainAsset = song.sound this.mainAsset = song.sound
this.volume = song.volume || 1 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{ ...@@ -155,10 +168,16 @@ class Controller{
if(this.mainLoopRunning){ if(this.mainLoopRunning){
if(this.multiplayer !== 2){ if(this.multiplayer !== 2){
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.viewLoop() var player = this.multiplayer ? p2.player : 1
if(player === 1){
this.viewLoop()
}
if(this.multiplayer === 1){ if(this.multiplayer === 1){
this.syncWith.viewLoop() this.syncWith.viewLoop()
} }
if(player === 2){
this.viewLoop()
}
if(this.scoresheet){ if(this.scoresheet){
if(this.view.ctx){ if(this.view.ctx){
this.view.ctx.save() this.view.ctx.save()
...@@ -197,14 +216,14 @@ class Controller{ ...@@ -197,14 +216,14 @@ class Controller{
displayScore(score, notPlayed, bigNote){ displayScore(score, notPlayed, bigNote){
this.view.displayScore(score, notPlayed, bigNote) this.view.displayScore(score, notPlayed, bigNote)
} }
songSelection(fadeIn){ songSelection(fadeIn, showWarning){
if(!fadeIn){ if(!fadeIn){
this.clean() this.clean()
} }
if(this.calibrationMode){ if(this.calibrationMode){
new SettingsView(this.touchEnabled, false, null, "latency") new SettingsView(this.touchEnabled, false, null, "latency")
}else{ }else{
new SongSelect(false, fadeIn, this.touchEnabled) new SongSelect(false, fadeIn, this.touchEnabled, null, showWarning)
} }
} }
restartSong(){ restartSong(){
...@@ -217,20 +236,27 @@ class Controller{ ...@@ -217,20 +236,27 @@ class Controller{
resolve() resolve()
}else{ }else{
var songObj = assets.songs.find(song => song.id === this.selectedSong.folder) var songObj = assets.songs.find(song => song.id === this.selectedSong.folder)
var promises = []
if(songObj.chart && songObj.chart !== "blank"){ if(songObj.chart && songObj.chart !== "blank"){
var reader = new FileReader() 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") this.songData = event.target.result.replace(/\0/g, "").split("\n")
resolve() return Promise.resolve()
}) }))
if(this.selectedSong.type === "tja"){ if(this.selectedSong.type === "tja"){
reader.readAsText(songObj.chart, "sjis") reader.readAsText(songObj.chart, "sjis")
}else{ }else{
reader.readAsText(songObj.chart) 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(() => { }).then(() => {
var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled) var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled)
...@@ -306,5 +332,8 @@ class Controller{ ...@@ -306,5 +332,8 @@ class Controller{
debugObj.debug.updateStatus() debugObj.debug.updateStatus()
} }
} }
if(this.lyrics){
this.lyrics.clean()
}
} }
} }
...@@ -17,6 +17,8 @@ class Debug{ ...@@ -17,6 +17,8 @@ class Debug{
this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0] this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0]
this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0] this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0]
this.volumeDiv = this.byClass("music-volume") 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.restartLabel = this.byClass("change-restart-label")
this.restartCheckbox = this.byClass("change-restart") this.restartCheckbox = this.byClass("change-restart")
this.autoplayLabel = this.byClass("autoplay-label") this.autoplayLabel = this.byClass("autoplay-label")
...@@ -50,6 +52,9 @@ class Debug{ ...@@ -50,6 +52,9 @@ class Debug{
this.volumeSlider.onchange(this.volumeChange.bind(this)) this.volumeSlider.onchange(this.volumeChange.bind(this))
this.volumeSlider.set(1) 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.moveTo(100, 100)
this.restore() this.restore()
this.updateStatus() this.updateStatus()
...@@ -129,6 +134,9 @@ class Debug{ ...@@ -129,6 +134,9 @@ class Debug{
if(this.controller.parsedSongData.branches){ if(this.controller.parsedSongData.branches){
this.branchHideDiv.style.display = "block" this.branchHideDiv.style.display = "block"
} }
if(this.controller.lyrics){
this.lyricsHideDiv.style.display = "block"
}
var selectedSong = this.controller.selectedSong var selectedSong = this.controller.selectedSong
this.defaultOffset = selectedSong.offset || 0 this.defaultOffset = selectedSong.offset || 0
...@@ -136,19 +144,21 @@ class Debug{ ...@@ -136,19 +144,21 @@ class Debug{
this.offsetChange(this.offsetSlider.get(), true) this.offsetChange(this.offsetSlider.get(), true)
this.branchChange(null, true) this.branchChange(null, true)
this.volumeChange(this.volumeSlider.get(), true) this.volumeChange(this.volumeSlider.get(), true)
this.lyricsChange(this.lyricsSlider.get(), true)
}else{ }else{
this.songHash = selectedSong.hash this.songHash = selectedSong.hash
this.offsetSlider.set(this.defaultOffset) this.offsetSlider.set(this.defaultOffset)
this.branchReset(null, true) this.branchReset(null, true)
this.volumeSlider.set(this.controller.volume) 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) => { var measures = this.controller.parsedSongData.measures.filter((measure, i, array) => {
return i === 0 || Math.abs(measure.ms - array[i - 1].ms) > 0.01 return i === 0 || Math.abs(measure.ms - array[i - 1].ms) > 0.01
}) })
this.measureNumSlider.setMinMax(0, measures.length - 1) this.measureNumSlider.setMinMax(0, measures.length - 1)
if(this.measureNum && measures.length > this.measureNum){ if(this.measureNum > 0 && measures.length >= this.measureNum){
var measureMS = measures[this.measureNum].ms var measureMS = measures[this.measureNum - 1].ms
var game = this.controller.game var game = this.controller.game
game.started = true game.started = true
var timestamp = Date.now() var timestamp = Date.now()
...@@ -174,6 +184,7 @@ class Debug{ ...@@ -174,6 +184,7 @@ class Debug{
this.restartBtn.style.display = "" this.restartBtn.style.display = ""
this.autoplayLabel.style.display = "" this.autoplayLabel.style.display = ""
this.branchHideDiv.style.display = "" this.branchHideDiv.style.display = ""
this.lyricsHideDiv.style.display = ""
this.controller = null this.controller = null
} }
this.stopMove() this.stopMove()
...@@ -194,6 +205,9 @@ class Debug{ ...@@ -194,6 +205,9 @@ class Debug{
branch.ms = branch.originalMS + offset branch.ms = branch.originalMS + offset
}) })
} }
if(this.controller.lyrics){
this.controller.lyrics.offsetChange(value * 1000)
}
if(this.restartCheckbox.checked && !noRestart){ if(this.restartCheckbox.checked && !noRestart){
this.restartSong() this.restartSong()
} }
...@@ -213,6 +227,14 @@ class Debug{ ...@@ -213,6 +227,14 @@ class Debug{
this.restartSong() 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(){ restartSong(){
if(this.controller){ if(this.controller){
this.controller.restartSong() this.controller.restartSong()
...@@ -259,6 +281,7 @@ class Debug{ ...@@ -259,6 +281,7 @@ class Debug{
this.offsetSlider.clean() this.offsetSlider.clean()
this.measureNumSlider.clean() this.measureNumSlider.clean()
this.volumeSlider.clean() this.volumeSlider.clean()
this.lyricsSlider.clean()
pageEvents.remove(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.windowSymbol) pageEvents.remove(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.windowSymbol)
pageEvents.mouseRemove(this) pageEvents.mouseRemove(this)
...@@ -285,6 +308,8 @@ class Debug{ ...@@ -285,6 +308,8 @@ class Debug{
delete this.branchSelect delete this.branchSelect
delete this.branchResetBtn delete this.branchResetBtn
delete this.volumeDiv delete this.volumeDiv
delete this.lyricsHideDiv
delete this.lyricsOffsetDiv
delete this.restartCheckbox delete this.restartCheckbox
delete this.autoplayLabel delete this.autoplayLabel
delete this.autoplayCheckbox delete this.autoplayCheckbox
......
...@@ -5,6 +5,7 @@ class Game{ ...@@ -5,6 +5,7 @@ class Game{
this.songData = songData this.songData = songData
this.elapsedTime = 0 this.elapsedTime = 0
this.currentCircle = -1 this.currentCircle = -1
this.currentEvent = 0
this.updateCurrentCircle() this.updateCurrentCircle()
this.combo = 0 this.combo = 0
this.rules = new GameRules(this) this.rules = new GameRules(this)
...@@ -47,13 +48,7 @@ class Game{ ...@@ -47,13 +48,7 @@ class Game{
} }
initTiming(){ initTiming(){
// Date when the chrono is started (before the game begins) // Date when the chrono is started (before the game begins)
var firstCircle var firstCircle = this.songData.circles[0]
for(var i = 0; i < this.songData.circles.length; i++){
firstCircle = this.songData.circles[i]
if(firstCircle.type !== "event"){
break
}
}
if(this.controller.calibrationMode){ if(this.controller.calibrationMode){
var offsetTime = 0 var offsetTime = 0
}else{ }else{
...@@ -238,9 +233,6 @@ class Game{ ...@@ -238,9 +233,6 @@ class Game{
} }
} }
skipNote(circle){ skipNote(circle){
if(circle.type === "event"){
return
}
if(circle.section){ if(circle.section){
this.resetSection() this.resetSection()
} }
...@@ -258,9 +250,6 @@ class Game{ ...@@ -258,9 +250,6 @@ class Game{
checkPlays(){ checkPlays(){
var circles = this.songData.circles var circles = this.songData.circles
var circle = circles[this.currentCircle] var circle = circles[this.currentCircle]
if(circle && circle.type === "event"){
this.updateCurrentCircle()
}
if(this.controller.autoPlayEnabled){ if(this.controller.autoPlayEnabled){
while(circle && this.controller.autoPlay(circle)){ while(circle && this.controller.autoPlay(circle)){
...@@ -469,9 +458,7 @@ class Game{ ...@@ -469,9 +458,7 @@ class Game{
} }
getLastCircle(circles){ getLastCircle(circles){
for(var i = circles.length; i--;){ for(var i = circles.length; i--;){
if(circles[i].type !== "event"){ return circles[i]
return circles[i]
}
} }
} }
whenLastCirclePlayed(){ whenLastCirclePlayed(){
...@@ -505,7 +492,9 @@ class Game{ ...@@ -505,7 +492,9 @@ class Game{
var musicDuration = duration * 1000 - this.controller.offset var musicDuration = duration * 1000 - this.controller.offset
if(this.musicFadeOut === 0){ if(this.musicFadeOut === 0){
if(this.controller.multiplayer === 1){ 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++ this.musicFadeOut++
}else if(this.musicFadeOut === 1 && ms >= started + 1600){ }else if(this.musicFadeOut === 1 && ms >= started + 1600){
...@@ -621,7 +610,7 @@ class Game{ ...@@ -621,7 +610,7 @@ class Game{
var circles = this.songData.circles var circles = this.songData.circles
do{ do{
var circle = circles[++this.currentCircle] var circle = circles[++this.currentCircle]
}while(circle && (circle.branch && !circle.branch.active || circle.type === "event")) }while(circle && (circle.branch && !circle.branch.active))
} }
getCurrentCircle(){ getCurrentCircle(){
return this.currentCircle return this.currentCircle
......
...@@ -202,12 +202,16 @@ ...@@ -202,12 +202,16 @@
var tja = new ParseTja(data, "oni", 0, 0, true) var tja = new ParseTja(data, "oni", 0, 0, true)
var songObj = { var songObj = {
id: index + 1, id: index + 1,
order: index + 1,
type: "tja", type: "tja",
chart: file, chart: file,
stars: [], courses: {},
music: "muted" music: "muted"
} }
var coursesAdded = false
var titleLang = {} var titleLang = {}
var titleLangAdded = false
var subtitleLangAdded = false
var subtitleLang = {} var subtitleLang = {}
var dir = file.webkitRelativePath.toLowerCase() var dir = file.webkitRelativePath.toLowerCase()
dir = dir.slice(0, dir.lastIndexOf("/") + 1) dir = dir.slice(0, dir.lastIndexOf("/") + 1)
...@@ -221,7 +225,11 @@ ...@@ -221,7 +225,11 @@
} }
songObj.subtitle = subtitle songObj.subtitle = subtitle
songObj.preview = meta.demostart || 0 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){ if(meta.wave){
songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music
} }
...@@ -252,6 +260,15 @@ ...@@ -252,6 +260,15 @@
id: 1 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){ for(var id in allStrings){
var songTitle = songObj.title var songTitle = songObj.title
var ura = "" var ura = ""
...@@ -264,32 +281,27 @@ ...@@ -264,32 +281,27 @@
} }
if(meta["title" + id]){ if(meta["title" + id]){
titleLang[id] = meta["title" + id] titleLang[id] = meta["title" + id]
titleLangAdded = true
}else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){ }else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){
titleLang[id] = this.songTitle[songTitle][id] + ura titleLang[id] = this.songTitle[songTitle][id] + ura
titleLangAdded = true
} }
if(meta["subtitle" + id]){ if(meta["subtitle" + id]){
subtitleLang[id] = meta["subtitle" + id] subtitleLang[id] = meta["subtitle" + id]
subtitleLangAdded = true
} }
} }
} }
var titleLangArray = [] if(titleLangAdded){
for(var id in titleLang){ songObj.title_lang = titleLang
titleLangArray.push(id + " " + titleLang[id])
}
if(titleLangArray.length !== 0){
songObj.title_lang = titleLangArray.join("\n")
} }
var subtitleLangArray = [] if(subtitleLangAdded){
for(var id in subtitleLang){ songObj.subtitle_lang = subtitleLang
subtitleLangArray.push(id + " " + subtitleLang[id])
}
if(subtitleLangArray.length !== 0){
songObj.subtitle_lang = subtitleLangArray.join("\n")
} }
if(!songObj.category){ if(!songObj.category){
songObj.category = category || this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))]) 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 this.songs[index] = songObj
} }
var hash = md5.base64(event.target.result).slice(0, -2) var hash = md5.base64(event.target.result).slice(0, -2)
...@@ -316,12 +328,20 @@ ...@@ -316,12 +328,20 @@
dir = dir.slice(0, dir.lastIndexOf("/") + 1) dir = dir.slice(0, dir.lastIndexOf("/") + 1)
var songObj = { var songObj = {
id: index + 1, id: index + 1,
order: index + 1,
type: "osu", type: "osu",
chart: file, chart: file,
subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist, 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, 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" music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] || "muted"
} }
var filename = file.name.slice(0, file.name.lastIndexOf(".")) var filename = file.name.slice(0, file.name.lastIndexOf("."))
...@@ -333,7 +353,9 @@ ...@@ -333,7 +353,9 @@
suffix = " " + matches[0] suffix = " " + matches[0]
} }
songObj.title = title + suffix 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{ }else{
songObj.title = filename songObj.title = filename
} }
...@@ -417,7 +439,7 @@ ...@@ -417,7 +439,7 @@
for(var i = path.length - 2; i >= 0; i--){ for(var i = path.length - 2; i >= 0; i--){
var hasTitle = false var hasTitle = false
for(var j in exclude){ 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 hasTitle = true
break break
} }
......
...@@ -5,6 +5,7 @@ class Loader{ ...@@ -5,6 +5,7 @@ class Loader{
this.assetsDiv = document.getElementById("assets") this.assetsDiv = document.getElementById("assets")
this.screen = document.getElementById("screen") this.screen = document.getElementById("screen")
this.startTime = Date.now() this.startTime = Date.now()
this.errorMessages = []
var promises = [] var promises = []
...@@ -28,17 +29,24 @@ class Loader{ ...@@ -28,17 +29,24 @@ class Loader{
if(gameConfig.custom_js){ if(gameConfig.custom_js){
var script = document.createElement("script") var script = document.createElement("script")
this.addPromise(pageEvents.load(script)) var url = gameConfig.custom_js + queryString
script.src = gameConfig.custom_js + queryString this.addPromise(pageEvents.load(script), url)
script.src = url
document.head.appendChild(script) document.head.appendChild(script)
} }
assets.js.forEach(name => { assets.js.forEach(name => {
var script = document.createElement("script") var script = document.createElement("script")
this.addPromise(pageEvents.load(script)) var url = "/src/js/" + name + queryString
script.src = "/src/js/" + name + queryString this.addPromise(pageEvents.load(script), url)
script.src = url
document.head.appendChild(script) 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) => { this.addPromise(new Promise((resolve, reject) => {
if( if(
versionLink.href !== gameConfig._version.url && versionLink.href !== gameConfig._version.url &&
...@@ -69,48 +77,56 @@ class Loader{ ...@@ -69,48 +77,56 @@ class Loader{
} }
var interval = setInterval(checkStyles, 100) var interval = setInterval(checkStyles, 100)
checkStyles() checkStyles()
})) }), "Version on the page and config does not match\n(page: " + pageVersion + ",\nconfig: "+ gameConfig._version.commit + ")")
for(var name in assets.fonts){ 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) document.fonts.add(font)
})) }), url)
} }
assets.img.forEach(name => { assets.img.forEach(name => {
var id = this.getFilename(name) var id = this.getFilename(name)
var image = document.createElement("img") 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.id = name
image.src = gameConfig.assets_baseurl + "img/" + name image.src = url
this.assetsDiv.appendChild(image) this.assetsDiv.appendChild(image)
assets.image[id] = image assets.image[id] = image
}) })
assets.views.forEach(name => { assets.views.forEach(name => {
var id = this.getFilename(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 assets.pages[id] = page
})) }), url)
}) })
this.addPromise(this.ajax("/api/songs").then(songs => { this.addPromise(this.ajax("/api/songs").then(songs => {
assets.songsDefault = JSON.parse(songs) assets.songsDefault = JSON.parse(songs)
assets.songs = assets.songsDefault 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) vectors = JSON.parse(response)
})) }), url)
this.afterJSCount = this.afterJSCount =
["blurPerformance", "P2Connection"].length + ["blurPerformance"].length +
assets.audioSfx.length + assets.audioSfx.length +
assets.audioMusic.length + assets.audioMusic.length +
assets.audioSfxLR.length + assets.audioSfxLR.length +
assets.audioSfxLoud.length assets.audioSfxLoud.length +
(gameConfig.accounts ? 1 : 0)
Promise.all(this.promises).then(() => { Promise.all(this.promises).then(() => {
if(this.error){
return
}
snd.buffer = new SoundBuffer() snd.buffer = new SoundBuffer()
snd.musicGain = snd.buffer.createGain() snd.musicGain = snd.buffer.createGain()
...@@ -130,20 +146,20 @@ class Loader{ ...@@ -130,20 +146,20 @@ class Loader{
this.afterJSCount = 0 this.afterJSCount = 0
assets.audioSfx.forEach(name => { 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 => { 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 => { assets.audioSfxLR.forEach(name => {
this.addPromise(this.loadSound(name, snd.sfxGain).then(sound => { this.addPromise(this.loadSound(name, snd.sfxGain).then(sound => {
var id = this.getFilename(name) var id = this.getFilename(name)
assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL) assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL)
assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR) assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR)
})) }), this.soundUrl(name))
}) })
assets.audioSfxLoud.forEach(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() this.canvasTest = new CanvasTest()
...@@ -153,67 +169,92 @@ class Loader{ ...@@ -153,67 +169,92 @@ class Loader{
// Less than 50 fps with blur enabled // Less than 50 fps with blur enabled
disableBlur = true disableBlur = true
} }
})) }), "blurPerformance")
var readyEvent = "normal" if(gameConfig.accounts){
var songId this.addPromise(this.ajax("/api/scores/get").then(response => {
var hashLower = location.hash.toLowerCase() response = JSON.parse(response)
p2 = new P2Connection() if(response.status === "ok"){
if(hashLower.startsWith("#song=")){ account.loggedIn = true
var number = parseInt(location.hash.slice(6)) account.username = response.username
if(number > 0){ account.displayName = response.display_name
songId = number scoreStorage.load(response.scores)
readyEvent = "song-id" pageEvents.send("login", account.username)
} }
}else if(location.hash.length === 6){ }), "/api/scores/get")
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("")
} }
settings = new Settings() settings = new Settings()
pageEvents.setKbd() pageEvents.setKbd()
scoreStorage = new ScoreStorage() 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(() => { 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.allImg = result
perf.load = Date.now() - this.startTime perf.load = Date.now() - this.startTime
this.canvasTest.clean() this.canvasTest.clean()
...@@ -227,27 +268,36 @@ class Loader{ ...@@ -227,27 +268,36 @@ class Loader{
}) })
} }
addPromise(promise){ addPromise(promise, url){
this.promises.push(promise) 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){ loadSound(name, gain){
var id = this.getFilename(name) 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 assets.sounds[id] = sound
}) })
} }
getFilename(name){ getFilename(name){
return name.slice(0, name.lastIndexOf(".")) return name.slice(0, name.lastIndexOf("."))
} }
errorMsg(error){ errorMsg(error, url){
if(Array.isArray(error) && error[1] instanceof HTMLElement){ if(url || error){
error = error[0] + ": " + error[1].outerHTML 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){ if(!this.error){
this.error = true this.error = true
cancelTouch = false
this.loaderDiv.classList.add("loaderError") this.loaderDiv.classList.add("loaderError")
if(typeof allStrings === "object"){ if(typeof allStrings === "object"){
var lang = localStorage.lang var lang = localStorage.lang
...@@ -265,14 +315,57 @@ class Loader{ ...@@ -265,14 +315,57 @@ class Loader{
if(!lang){ if(!lang){
lang = "en" lang = "en"
} }
var errorOccured = allStrings[lang].errorOccured loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang].errorOccured
}
var loaderError = loader.screen.getElementsByClassName("loader-error-div")[0]
loaderError.style.display = "flex"
var diagTxt = loader.screen.getElementsByClassName("diag-txt")[0]
var debugLink = loader.screen.getElementsByClassName("debug-link")[0]
if(navigator.userAgent.indexOf("Android") >= 0){
var iframe = document.createElement("iframe")
diagTxt.appendChild(iframe)
var body = iframe.contentWindow.document.body
body.setAttribute("style", `
font-family: monospace;
margin: 2px 0 0 2px;
white-space: pre-wrap;
word-break: break-all;
cursor: text;
`)
body.setAttribute("onblur", `
getSelection().removeAllRanges()
`)
this.errorTxt = {
element: body,
method: "innerText"
}
}else{ }else{
var errorOccured = "An error occurred, please refresh" 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"
}
} }
this.loaderPercentage.appendChild(document.createElement("br")) var show = () => {
this.loaderPercentage.appendChild(document.createTextNode(errorOccured)) diagTxt.style.display = "block"
this.clean() 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(){ assetLoaded(){
if(!this.error){ if(!this.error){
...@@ -291,7 +384,11 @@ class Loader{ ...@@ -291,7 +384,11 @@ class Loader{
var request = new XMLHttpRequest() var request = new XMLHttpRequest()
request.open("GET", url) request.open("GET", url)
pageEvents.load(request).then(() => { pageEvents.load(request).then(() => {
resolve(request.response) if(request.status === 200){
resolve(request.response)
}else{
reject()
}
}, reject) }, reject)
if(customRequest){ if(customRequest){
customRequest(request) customRequest(request)
...@@ -299,14 +396,28 @@ class Loader{ ...@@ -299,14 +396,28 @@ class Loader{
request.send() 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") var fontDetectDiv = document.getElementById("fontdetectHelper")
if(fontDetectDiv){ if(fontDetectDiv){
fontDetectDiv.parentNode.removeChild(fontDetectDiv) fontDetectDiv.parentNode.removeChild(fontDetectDiv)
} }
delete this.loaderDiv
delete this.loaderPercentage delete this.loaderPercentage
delete this.loaderProgress delete this.loaderProgress
delete this.promises if(!error){
delete this.promises
delete this.errorText
}
pageEvents.remove(root, "touchstart") pageEvents.remove(root, "touchstart")
} }
} }
...@@ -34,7 +34,7 @@ class LoadSong{ ...@@ -34,7 +34,7 @@ class LoadSong{
run(){ run(){
var song = this.selectedSong var song = this.selectedSong
var id = song.folder var id = song.folder
var promises = [] this.promises = []
if(song.folder !== "calibration"){ if(song.folder !== "calibration"){
assets.sounds["v_start"].play() assets.sounds["v_start"].play()
var songObj = assets.songs.find(song => song.id === id) var songObj = assets.songs.find(song => song.id === id)
...@@ -92,9 +92,9 @@ class LoadSong{ ...@@ -92,9 +92,9 @@ class LoadSong{
img.crossOrigin = "Anonymous" img.crossOrigin = "Anonymous"
} }
let promise = pageEvents.load(img) let promise = pageEvents.load(img)
promises.push(promise.then(() => { this.addPromise(promise.then(() => {
return this.scaleImg(img, filename, prefix, force) return this.scaleImg(img, filename, prefix, force)
})) }), songObj.music ? filename + ".png" : skinBase + filename + ".png")
if(songObj.music){ if(songObj.music){
img.src = URL.createObjectURL(song.songSkin[filename + ".png"]) img.src = URL.createObjectURL(song.songSkin[filename + ".png"])
}else{ }else{
...@@ -102,14 +102,15 @@ class LoadSong{ ...@@ -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){ if(songObj.sound){
songObj.sound.gain = snd.musicGain songObj.sound.gain = snd.musicGain
resolve() resolve()
}else if(!songObj.music){ }else if(!songObj.music){
snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.mp3").then(sound => { snd.musicGain.load(url).then(sound => {
songObj.sound = sound songObj.sound = sound
resolve() resolve()
}, reject) }, reject)
...@@ -121,84 +122,120 @@ class LoadSong{ ...@@ -121,84 +122,120 @@ class LoadSong{
}else{ }else{
resolve() resolve()
} }
})) }), songObj.music ? songObj.music.webkitRelativePath : url)
if(songObj.chart){ if(songObj.chart){
if(songObj.chart === "blank"){ if(songObj.chart === "blank"){
this.songData = "" this.songData = ""
}else{ }else{
var reader = new FileReader() 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") this.songData = event.target.result.replace(/\0/g, "").split("\n")
})) }), songObj.chart.webkitRelativePath)
if(song.type === "tja"){ if(song.type === "tja"){
reader.readAsText(songObj.chart, "sjis") reader.readAsText(songObj.chart, "sjis")
}else{ }else{
reader.readAsText(songObj.chart) 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{ }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") 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"]){ if(this.touchEnabled && !assets.image["touch_drum"]){
let img = document.createElement("img") let img = document.createElement("img")
if(this.imgScale !== 1){ if(this.imgScale !== 1){
img.crossOrigin = "Anonymous" 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", "") return this.scaleImg(img, "touch_drum", "")
})) }), url)
img.src = gameConfig.assets_baseurl + "img/touch_drum.png" img.src = url
} }
Promise.all(promises).then(() => { Promise.all(this.promises).then(() => {
this.setupMultiplayer() if(!this.error){
}, error => { this.setupMultiplayer()
if(Array.isArray(error) && error[1] instanceof HTMLElement){
error = error[0] + ": " + error[1].outerHTML
} }
console.error(error)
pageEvents.send("load-song-error", error)
errorMessage(new Error(error).stack)
alert(strings.errorOccured)
}) })
} }
loadSongBg(){ addPromise(promise, url){
return new Promise((resolve, reject) => { this.promises.push(promise.catch(response => {
var promises = [] this.errorMsg(response, url)
var filenames = [] return Promise.resolve()
if(this.selectedSong.songBg !== null){ }))
filenames.push("bg_song_" + this.selectedSong.songBg) }
errorMsg(error, url){
if(!this.error){
if(url){
error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url
} }
if(this.selectedSong.donBg !== null){ pageEvents.send("load-song-error", error)
filenames.push("bg_don_" + this.selectedSong.donBg) errorMessage(new Error(error).stack)
if(this.multiplayer){ var title = this.selectedSong.title
filenames.push("bg_don2_" + this.selectedSong.donBg) if(title !== this.selectedSong.originalTitle){
} title += " (" + this.selectedSong.originalTitle + ")"
} }
if(this.selectedSong.songStage !== null){ assets.sounds["v_start"].stop()
filenames.push("bg_stage_" + this.selectedSong.songStage) setTimeout(() => {
this.clean()
new SongSelect(false, false, this.touchEnabled, null, {
name: "loadSongError",
title: title,
id: this.selectedSong.folder,
error: error
})
}, 500)
}
this.error = true
}
loadSongBg(){
var filenames = []
if(this.selectedSong.songBg !== null){
filenames.push("bg_song_" + this.selectedSong.songBg)
}
if(this.selectedSong.donBg !== null){
filenames.push("bg_don_" + this.selectedSong.donBg)
if(this.multiplayer){
filenames.push("bg_don2_" + this.selectedSong.donBg)
} }
for(var i = 0; i < filenames.length; i++){ }
var filename = filenames[i] if(this.selectedSong.songStage !== null){
var stage = filename.startsWith("bg_stage_") filenames.push("bg_stage_" + this.selectedSong.songStage)
for(var letter = 0; letter < (stage ? 1 : 2); letter++){ }
let filenameAb = filenames[i] + (stage ? "" : (letter === 0 ? "a" : "b")) for(var i = 0; i < filenames.length; i++){
if(!(filenameAb in assets.image)){ var filename = filenames[i]
let img = document.createElement("img") var stage = filename.startsWith("bg_stage_")
let force = filenameAb.startsWith("bg_song_") && this.touchEnabled for(var letter = 0; letter < (stage ? 1 : 2); letter++){
if(this.imgScale !== 1 || force){ let filenameAb = filenames[i] + (stage ? "" : (letter === 0 ? "a" : "b"))
img.crossOrigin = "Anonymous" if(!(filenameAb in assets.image)){
} let img = document.createElement("img")
promises.push(pageEvents.load(img).then(() => { let force = filenameAb.startsWith("bg_song_") && this.touchEnabled
return this.scaleImg(img, filenameAb, "", force) if(this.imgScale !== 1 || force){
})) img.crossOrigin = "Anonymous"
img.src = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
} }
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){ scaleImg(img, filename, prefix, force){
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
...@@ -238,8 +275,11 @@ class LoadSong{ ...@@ -238,8 +275,11 @@ class LoadSong{
randInt(min, max){ randInt(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min return Math.floor(Math.random() * (max - min + 1)) + min
} }
getSongDir(selectedSong){
return gameConfig.songs_baseurl + selectedSong.folder + "/"
}
getSongPath(selectedSong){ getSongPath(selectedSong){
var directory = gameConfig.songs_baseurl + selectedSong.folder + "/" var directory = this.getSongDir(selectedSong)
if(selectedSong.type === "tja"){ if(selectedSong.type === "tja"){
return directory + "main.tja" return directory + "main.tja"
}else{ }else{
...@@ -264,14 +304,14 @@ class LoadSong{ ...@@ -264,14 +304,14 @@ class LoadSong{
if(event.type === "gameload"){ if(event.type === "gameload"){
this.cancelButton.style.display = "" this.cancelButton.style.display = ""
if(event.value === song.difficulty){ if(event.value.diff === song.difficulty){
this.startMultiplayer() this.startMultiplayer()
}else{ }else{
this.selectedSong2 = {} this.selectedSong2 = {}
for(var i in this.selectedSong){ for(var i in this.selectedSong){
this.selectedSong2[i] = this.selectedSong[i] this.selectedSong2[i] = this.selectedSong[i]
} }
this.selectedSong2.difficulty = event.value this.selectedSong2.difficulty = event.value.diff
if(song.type === "tja"){ if(song.type === "tja"){
this.startMultiplayer() this.startMultiplayer()
}else{ }else{
...@@ -297,7 +337,8 @@ class LoadSong{ ...@@ -297,7 +337,8 @@ class LoadSong{
}) })
p2.send("join", { p2.send("join", {
id: song.folder, id: song.folder,
diff: song.difficulty diff: song.difficulty,
name: account.loggedIn ? account.displayName : null
}) })
}else{ }else{
this.clean() this.clean()
...@@ -332,6 +373,7 @@ class LoadSong{ ...@@ -332,6 +373,7 @@ class LoadSong{
pageEvents.send("load-song-cancel") pageEvents.send("load-song-cancel")
} }
clean(){ clean(){
delete this.promises
pageEvents.remove(p2, "message") pageEvents.remove(p2, "message")
if(this.cancelButton){ if(this.cancelButton){
pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"]) pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"])
......
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("<lang ", index2 + 1)
if(firstLang === -1){
firstLang = index1
}
if(index1 !== -1){
index2 = text.indexOf(">", index1 + 6)
if(index2 === -1){
break
}
var lang = text.slice(index1 + 6, index2).toLowerCase()
if(strings.id === lang){
var index3 = text.indexOf("<lang ", index2 + 1)
if(index3 !== -1){
textLang = text.slice(index2 + 1, index3)
}else{
textLang = text.slice(index2 + 1)
}
}
}else{
break
}
}
if(!textLang){
textLang = firstLang === -1 ? text : text.slice(0, firstLang)
}
lines.push({
start: this.convertTime(start),
end: this.convertTime(end),
text: textLang
})
}
}
}
return lines
}
convertTime(time){
if(time.startsWith("-")){
var mul = -1
time = time.slice(1)
}else{
var mul = 1
}
var array = time.split(":")
if(array.length === 2){
var h = 0
var m = array[0]
var s = array[1]
}else{
var h = parseInt(array[0])
var m = array[1]
var s = array[2]
}
var index = s.indexOf(",")
if(index !== -1){
s = s.slice(0, index) + "." + s.slice(index + 1)
}
return ((h * 60 + parseInt(m)) * 60 + parseFloat(s)) * 1000 * mul
}
update(ms){
if(this.current >= 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("<ruby>")
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("</ruby>")
if(index !== -1){
var ruby = text.slice(0, index)
text = text.slice(index + 7)
}else{
var ruby = text
text = ""
}
var index = ruby.indexOf("<rt>")
if(index !== -1){
var node1 = ruby.slice(0, index)
ruby = ruby.slice(index + 4)
var index = ruby.indexOf("</rt>")
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
}
}
...@@ -84,6 +84,7 @@ var strings ...@@ -84,6 +84,7 @@ var strings
var vectors var vectors
var settings var settings
var scoreStorage var scoreStorage
var account = {}
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){ if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){
......
...@@ -3,6 +3,8 @@ class P2Connection{ ...@@ -3,6 +3,8 @@ class P2Connection{
this.closed = true this.closed = true
this.lastMessages = {} this.lastMessages = {}
this.otherConnected = false this.otherConnected = false
this.name = null
this.player = 1
this.allEvents = new Map() this.allEvents = new Map()
this.addEventListener("message", this.message.bind(this)) this.addEventListener("message", this.message.bind(this))
this.currentHash = "" this.currentHash = ""
...@@ -102,6 +104,10 @@ class P2Connection{ ...@@ -102,6 +104,10 @@ class P2Connection{
} }
message(response){ message(response){
switch(response.type){ switch(response.type){
case "gameload":
if("player" in response.value){
this.player = response.value.player === 2 ? 2 : 1
}
case "gamestart": case "gamestart":
this.otherConnected = true this.otherConnected = true
this.notes = [] this.notes = []
...@@ -110,6 +116,7 @@ class P2Connection{ ...@@ -110,6 +116,7 @@ class P2Connection{
this.kaAmount = 0 this.kaAmount = 0
this.results = false this.results = false
this.branch = "normal" this.branch = "normal"
scoreStorage.clearP2()
break break
case "gameend": case "gameend":
this.otherConnected = false this.otherConnected = false
...@@ -123,11 +130,13 @@ class P2Connection{ ...@@ -123,11 +130,13 @@ class P2Connection{
this.hash("") this.hash("")
this.hashLock = false this.hashLock = false
} }
this.name = null
scoreStorage.clearP2()
break break
case "gameresults": case "gameresults":
this.results = {} this.results = {}
for(var i in response.value){ 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 break
case "note": case "note":
...@@ -150,6 +159,44 @@ class P2Connection{ ...@@ -150,6 +159,44 @@ class P2Connection{
this.clearMessage("users") this.clearMessage("users")
this.otherConnected = true this.otherConnected = true
this.session = 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 break
} }
} }
......
...@@ -86,6 +86,9 @@ class PageEvents{ ...@@ -86,6 +86,9 @@ class PageEvents{
}) })
} }
keyEvent(event){ 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){ if(this.kbd.indexOf(event.key.toLowerCase()) !== -1){
this.lastKeyEvent = Date.now() this.lastKeyEvent = Date.now()
event.preventDefault() event.preventDefault()
......
...@@ -48,6 +48,7 @@ class ParseOsu{ ...@@ -48,6 +48,7 @@ class ParseOsu{
lastBeatInterval: 0, lastBeatInterval: 0,
bpm: 0 bpm: 0
} }
this.events = []
this.generalInfo = this.parseGeneralInfo() this.generalInfo = this.parseGeneralInfo()
this.metadata = this.parseMetadata() this.metadata = this.parseMetadata()
this.editor = this.parseEditor() this.editor = this.parseEditor()
...@@ -244,6 +245,18 @@ class ParseOsu{ ...@@ -244,6 +245,18 @@ class ParseOsu{
var circles = [] var circles = []
var circleID = 0 var circleID = 0
var indexes = this.getStartEndIndexes("HitObjects") 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++){ for(var i = indexes.start; i <= indexes.end; i++){
circleID++ circleID++
var values = this.data[i].split(",") var values = this.data[i].split(",")
...@@ -277,7 +290,7 @@ class ParseOsu{ ...@@ -277,7 +290,7 @@ class ParseOsu{
var endTime = parseInt(values[this.osu.ENDTIME]) var endTime = parseInt(values[this.osu.ENDTIME])
var hitMultiplier = this.difficultyRange(this.difficulty.overallDifficulty, 3, 5, 7.5) * 1.65 var hitMultiplier = this.difficultyRange(this.difficulty.overallDifficulty, 3, 5, 7.5) * 1.65
var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier)) var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier))
circles.push(new Circle({ pushCircle(new Circle({
id: circleID, id: circleID,
start: start + this.offset, start: start + this.offset,
type: "balloon", type: "balloon",
...@@ -304,7 +317,7 @@ class ParseOsu{ ...@@ -304,7 +317,7 @@ class ParseOsu{
type = "drumroll" type = "drumroll"
txt = strings.note.drumroll txt = strings.note.drumroll
} }
circles.push(new Circle({ pushCircle(new Circle({
id: circleID, id: circleID,
start: start + this.offset, start: start + this.offset,
type: type, type: type,
...@@ -339,7 +352,7 @@ class ParseOsu{ ...@@ -339,7 +352,7 @@ class ParseOsu{
emptyValue = true emptyValue = true
} }
if(!emptyValue){ if(!emptyValue){
circles.push(new Circle({ pushCircle(new Circle({
id: circleID, id: circleID,
start: start + this.offset, start: start + this.offset,
type: type, type: type,
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
this.metadata = this.parseMetadata() this.metadata = this.parseMetadata()
this.measures = [] this.measures = []
this.beatInfo = {} this.beatInfo = {}
this.events = []
if(!metaOnly){ if(!metaOnly){
this.circles = this.parseCircles() this.circles = this.parseCircles()
} }
...@@ -83,6 +84,8 @@ ...@@ -83,6 +84,8 @@
} }
}else if(name.startsWith("branchstart") && inSong){ }else if(name.startsWith("branchstart") && inSong){
courses[courseName].branch = true courses[courseName].branch = true
}else if(name.startsWith("lyric") && inSong){
courses[courseName].inlineLyrics = true
} }
}else if(!inSong){ }else if(!inSong){
...@@ -157,6 +160,7 @@ ...@@ -157,6 +160,7 @@
var circleID = 0 var circleID = 0
var regexAZ = /[A-Z]/ var regexAZ = /[A-Z]/
var regexSpace = /\s/ var regexSpace = /\s/
var regexLinebreak = /\\n/g
var isAllDon = (note_chain, start_pos) => { var isAllDon = (note_chain, start_pos) => {
for (var i = start_pos; i < note_chain.length; ++i) { for (var i = start_pos; i < note_chain.length; ++i) {
var note = note_chain[i]; var note = note_chain[i];
...@@ -248,7 +252,12 @@ ...@@ -248,7 +252,12 @@
lastDrumroll = circleObj 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)) } 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))) { && !(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) { if (note_chain.length > 1 && currentMeasure.length >= 8) {
...@@ -266,9 +275,12 @@ ...@@ -266,9 +275,12 @@
} }
} }
var insertNote = circleObj => { var insertNote = circleObj => {
lastBpm = bpm
lastGogo = gogo
if(circleObj){ if(circleObj){
if(bpm !== lastBpm || gogo !== lastGogo){
circleObj.event = true
lastBpm = bpm
lastGogo = gogo
}
currentMeasure.push(circleObj) currentMeasure.push(circleObj)
} }
} }
...@@ -402,6 +414,18 @@ ...@@ -402,6 +414,18 @@
} }
branchObj[branchName] = currentBranch branchObj[branchName] = currentBranch
break 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{ }else{
...@@ -536,6 +560,10 @@ ...@@ -536,6 +560,10 @@
this.scoreinit = autoscore.ScoreInit; this.scoreinit = autoscore.ScoreInit;
this.scorediff = autoscore.ScoreDiff; 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 return circles
} }
} }
...@@ -2,9 +2,19 @@ class Scoresheet{ ...@@ -2,9 +2,19 @@ class Scoresheet{
constructor(controller, results, multiplayer, touchEnabled){ constructor(controller, results, multiplayer, touchEnabled){
this.controller = controller this.controller = controller
this.resultsObj = results 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){ 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.multiplayer = multiplayer
this.touchEnabled = touchEnabled this.touchEnabled = touchEnabled
...@@ -39,6 +49,7 @@ class Scoresheet{ ...@@ -39,6 +49,7 @@ class Scoresheet{
this.draw = new CanvasDraw(noSmoothing) this.draw = new CanvasDraw(noSmoothing)
this.canvasCache = new CanvasCache(noSmoothing) this.canvasCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing)
this.keyboard = new Keyboard({ this.keyboard = new Keyboard({
confirm: ["enter", "space", "esc", "don_l", "don_r"] confirm: ["enter", "space", "esc", "don_l", "don_r"]
...@@ -208,6 +219,7 @@ class Scoresheet{ ...@@ -208,6 +219,7 @@ class Scoresheet{
this.canvas.style.height = (winH / this.pixelRatio) + "px" this.canvas.style.height = (winH / this.pixelRatio) + "px"
this.canvasCache.resize(winW / ratio, 80 + 1, ratio) this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
this.nameplateCache.resize(274, 134, ratio + 0.2)
if(!this.multiplayer){ if(!this.multiplayer){
this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio) this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio)
...@@ -233,6 +245,9 @@ class Scoresheet{ ...@@ -233,6 +245,9 @@ class Scoresheet{
if(!this.canvasCache.canvas){ if(!this.canvasCache.canvas){
this.canvasCache.resize(winW / ratio, 80 + 1, ratio) this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
} }
if(!this.nameplateCache.canvas){
this.nameplateCache.resize(274, 67, ratio + 0.2)
}
} }
this.winW = winW this.winW = winW
this.winH = winH this.winH = winH
...@@ -243,7 +258,7 @@ class Scoresheet{ ...@@ -243,7 +258,7 @@ class Scoresheet{
var frameTop = winH / 2 - 720 / 2 var frameTop = winH / 2 - 720 / 2
var frameLeft = winW / 2 - 1280 / 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 p2Offset = 298
var bgOffset = 0 var bgOffset = 0
...@@ -326,28 +341,21 @@ class Scoresheet{ ...@@ -326,28 +341,21 @@ class Scoresheet{
} }
var rules = this.controller.game.rules var rules = this.controller.game.rules
var gaugePercent = rules.gaugePercent(this.results.gauge) var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000
var gaugeClear = [rules.gaugeClear] if(players === 2 && failedOffset !== 0){
if(players === 2){ var p2results = this.results[this.player[1]]
gaugeClear.push(this.controller.syncWith.game.rules.gaugeClear) if(p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)){
}
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]){
failedOffset = 0 failedOffset = 0
} }
} }
if(elapsed >= 3100 + failedOffset){ if(elapsed >= 3100 + failedOffset){
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
ctx.save() ctx.save()
var results = this.results var results = this.results[p]
if(p === 1){ if(!results){
results = p2.results continue
} }
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules var clear = this.rules[p].clearReached(results.gauge)
var resultGauge = playerRules.gaugePercent(results.gauge)
var clear = resultGauge >= gaugeClear[p]
if(p === 1 || !this.multiplayer && clear){ if(p === 1 || !this.multiplayer && clear){
ctx.translate(0, 290) ctx.translate(0, 290)
} }
...@@ -410,7 +418,7 @@ class Scoresheet{ ...@@ -410,7 +418,7 @@ class Scoresheet{
this.draw.layeredText({ this.draw.layeredText({
ctx: ctx, ctx: ctx,
text: this.results.title, text: this.results[this.player[0]].title,
fontSize: 40, fontSize: 40,
fontFamily: this.font, fontFamily: this.font,
x: 1257, x: 1257,
...@@ -426,9 +434,11 @@ class Scoresheet{ ...@@ -426,9 +434,11 @@ class Scoresheet{
ctx.save() ctx.save()
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(!results){
continue
}
if(p === 1){ if(p === 1){
results = p2.results
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
...@@ -450,6 +460,30 @@ class Scoresheet{ ...@@ -450,6 +460,30 @@ class Scoresheet{
ctx.fillText(text, 395, 308) ctx.fillText(text, 395, 308)
ctx.miterLimit = 10 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){ if(this.controller.autoPlayEnabled){
ctx.drawImage(assets.image["badge_auto"], ctx.drawImage(assets.image["badge_auto"],
431, 311, 34, 34 431, 311, 34, 34
...@@ -581,7 +615,7 @@ class Scoresheet{ ...@@ -581,7 +615,7 @@ class Scoresheet{
if(this.tetsuoHanaClass){ if(this.tetsuoHanaClass){
this.tetsuoHana.classList.remove(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) this.tetsuoHana.classList.add(this.tetsuoHanaClass)
} }
} }
...@@ -595,32 +629,32 @@ class Scoresheet{ ...@@ -595,32 +629,32 @@ class Scoresheet{
ctx.translate(frameLeft, frameTop) ctx.translate(frameLeft, frameTop)
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(!results){
continue
}
if(p === 1){ if(p === 1){
results = p2.results
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
var gaugePercent = rules.gaugePercent(results.gauge)
var w = 712 var w = 712
this.draw.gauge({ this.draw.gauge({
ctx: ctx, ctx: ctx,
x: 558 + w, x: 558 + w,
y: p === 1 ? 124 : 116, y: p === 1 ? 124 : 116,
clear: gaugeClear[p], clear: this.rules[p].gaugeClear,
percentage: gaugePercent, percentage: this.rules[p].gaugePercent(results.gauge),
font: this.font, font: this.font,
scale: w / 788, scale: w / 788,
scoresheet: true, scoresheet: true,
blue: p === 1, blue: p === 1,
multiplayer: p === 1 multiplayer: p === 1
}) })
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules
this.draw.soul({ this.draw.soul({
ctx: ctx, ctx: ctx,
x: 1215, x: 1215,
y: 144, y: 144,
scale: 36 / 42, scale: 36 / 42,
cleared: playerRules.clearReached(results.gauge) cleared: this.rules[p].clearReached(results.gauge)
}) })
} }
}) })
...@@ -633,13 +667,12 @@ class Scoresheet{ ...@@ -633,13 +667,12 @@ class Scoresheet{
var noCrownResultWait = -2000; var noCrownResultWait = -2000;
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(p === 1){ if(!results){
results = p2.results continue
} }
var crownType = null var crownType = null
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules if(this.rules[p].clearReached(results.gauge)){
if(playerRules.clearReached(results.gauge)){
crownType = results.bad === "0" ? "gold" : "silver" crownType = results.bad === "0" ? "gold" : "silver"
} }
if(crownType !== null){ if(crownType !== null){
...@@ -702,7 +735,10 @@ class Scoresheet{ ...@@ -702,7 +735,10 @@ class Scoresheet{
var times = {} var times = {}
var lastTime = 0 var lastTime = 0
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = p === 0 ? this.results : p2.results var results = this.results[p]
if(!results){
continue
}
var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame
if(currentTime > lastTime){ if(currentTime > lastTime){
lastTime = currentTime lastTime = currentTime
...@@ -711,7 +747,10 @@ class Scoresheet{ ...@@ -711,7 +747,10 @@ class Scoresheet{
for(var i in printNumbers){ for(var i in printNumbers){
var largestTime = 0 var largestTime = 0
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = p === 0 ? this.results : p2.results var results = this.results[p]
if(!results){
continue
}
times[printNumbers[i]] = lastTime + 500 times[printNumbers[i]] = lastTime + 500
var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
if(currentTime > largestTime){ if(currentTime > largestTime){
...@@ -727,9 +766,11 @@ class Scoresheet{ ...@@ -727,9 +766,11 @@ class Scoresheet{
} }
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(!results){
continue
}
if(p === 1){ if(p === 1){
results = p2.results
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
ctx.save() ctx.save()
...@@ -823,7 +864,7 @@ class Scoresheet{ ...@@ -823,7 +864,7 @@ class Scoresheet{
if(elapsed >= 1000){ if(elapsed >= 1000){
this.clean() this.clean()
this.controller.songSelection(true) this.controller.songSelection(true, this.showWarning)
} }
} }
...@@ -890,10 +931,14 @@ class Scoresheet{ ...@@ -890,10 +931,14 @@ class Scoresheet{
delete this.resultsObj.title delete this.resultsObj.title
delete this.resultsObj.difficulty delete this.resultsObj.difficulty
delete this.resultsObj.gauge 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)){ }else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){
oldScore.crown = 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 this.scoreSaved = true
...@@ -908,7 +953,7 @@ class Scoresheet{ ...@@ -908,7 +953,7 @@ class Scoresheet{
snd.buffer.loadSettings() snd.buffer.loadSettings()
this.redrawRunning = false this.redrawRunning = false
pageEvents.remove(this.canvas, ["mousedown", "touchstart"]) pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
if(this.multiplayer !== 2 && this.touchEnabled){ if(this.touchEnabled){
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend") pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
} }
if(this.session){ if(this.session){
...@@ -920,5 +965,7 @@ class Scoresheet{ ...@@ -920,5 +965,7 @@ class Scoresheet{
delete this.ctx delete this.ctx
delete this.canvas delete this.canvas
delete this.fadeScreen delete this.fadeScreen
delete this.results
delete this.rules
} }
} }
class ScoreStorage{ class ScoreStorage{
constructor(){ constructor(){
this.scores = {} this.scores = {}
this.scoresP2 = {}
this.requestP2 = new Set()
this.requestedP2 = new Set()
this.songTitles = {} this.songTitles = {}
this.difficulty = ["oni", "ura", "hard", "normal", "easy"] this.difficulty = ["oni", "ura", "hard", "normal", "easy"]
this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"] this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"]
this.crownValue = ["", "silver", "gold"] this.crownValue = ["", "silver", "gold"]
this.load()
} }
load(){ load(strings, loadFailed){
this.scores = {} var scores = {}
this.scoreStrings = {} var scoreStrings = {}
try{ if(loadFailed){
var localScores = localStorage.getItem("scoreStorage") try{
if(localScores){ var localScores = localStorage.getItem("saveFailed")
this.scoreStrings = JSON.parse(localScores) if(localScores){
} scoreStrings = JSON.parse(localScores)
}catch(e){} }
for(var hash in this.scoreStrings){ }catch(e){}
var scoreString = this.scoreStrings[hash] }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 var songAdded = false
if(typeof scoreString === "string" && scoreString){ if(typeof scoreString === "string" && scoreString){
var diffArray = scoreString.split(";") var diffArray = scoreString.split(";")
...@@ -37,25 +52,63 @@ class ScoreStorage{ ...@@ -37,25 +52,63 @@ class ScoreStorage{
score[name] = value score[name] = value
} }
if(!songAdded){ if(!songAdded){
this.scores[hash] = {title: null} scores[hash] = {title: null}
songAdded = true 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(){ save(){
for(var hash in this.scores){ for(var hash in this.scores){
this.writeString(hash) this.writeString(hash)
} }
this.write() this.write()
return this.sendToServer({
scores: this.prepareScores(this.scoreStrings),
is_import: true
})
} }
write(){ write(){
try{ if(!account.loggedIn){
localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings)) try{
}catch(e){} localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings))
}catch(e){}
}
} }
writeString(hash){ writeString(hash){
var score = this.scores[hash] var score = this.scores[hash]
...@@ -101,17 +154,82 @@ class ScoreStorage{ ...@@ -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) var hash = isHash ? song : this.titleHash(song)
if(!(hash in this.scores)){ if(!(hash in this.scores)){
this.scores[hash] = {} this.scores[hash] = {}
} }
if(setTitle){ if(difficulty){
this.scores[hash].title = setTitle 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.writeString(hash)
this.write() 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(){ template(){
var template = {crown: ""} var template = {crown: ""}
...@@ -146,6 +264,62 @@ class ScoreStorage{ ...@@ -146,6 +264,62 @@ class ScoreStorage{
delete this.scoreStrings[hash] delete this.scoreStrings[hash]
} }
this.write() 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()
}
} }
...@@ -34,7 +34,10 @@ class Session{ ...@@ -34,7 +34,10 @@ class Session{
pageEvents.send("session-start", "host") pageEvents.send("session-start", "host")
} }
}) })
p2.send("invite") p2.send("invite", {
id: null,
name: account.loggedIn ? account.displayName : null
})
pageEvents.send("session") pageEvents.send("session")
} }
getElement(name){ getElement(name){
......
...@@ -50,6 +50,10 @@ class Settings{ ...@@ -50,6 +50,10 @@ class Settings{
easierBigNotes: { easierBigNotes: {
type: "toggle", type: "toggle",
default: false default: false
},
showLyrics: {
type: "toggle",
default: true
} }
} }
......
class SongSelect{ class SongSelect{
constructor(fromTutorial, fadeIn, touchEnabled, songId){ constructor(fromTutorial, fadeIn, touchEnabled, songId, showWarning){
this.touchEnabled = touchEnabled this.touchEnabled = touchEnabled
loader.changePage("songselect", false) loader.changePage("songselect", false)
...@@ -116,7 +116,7 @@ class SongSelect{ ...@@ -116,7 +116,7 @@ class SongSelect{
originalTitle: song.title, originalTitle: song.title,
subtitle: subtitle, subtitle: subtitle,
skin: song.category in this.songSkin ? this.songSkin[song.category] : this.songSkin.default, skin: song.category in this.songSkin ? this.songSkin[song.category] : this.songSkin.default,
stars: song.stars, courses: song.courses,
category: song.category, category: song.category,
preview: song.preview || 0, preview: song.preview || 0,
type: song.type, type: song.type,
...@@ -126,14 +126,20 @@ class SongSelect{ ...@@ -126,14 +126,20 @@ class SongSelect{
volume: song.volume, volume: song.volume,
maker: song.maker, maker: song.maker,
canJump: true, canJump: true,
hash: song.hash || song.title hash: song.hash || song.title,
order: song.order,
lyrics: song.lyrics
}) })
} }
this.songs.sort((a, b) => { this.songs.sort((a, b) => {
var catA = a.category in this.songSkin ? this.songSkin[a.category] : this.songSkin.default 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 var catB = b.category in this.songSkin ? this.songSkin[b.category] : this.songSkin.default
if(catA.sort === catB.sort){ 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{ }else{
return catA.sort > catB.sort ? 1 : -1 return catA.sort > catB.sort ? 1 : -1
} }
...@@ -162,6 +168,10 @@ class SongSelect{ ...@@ -162,6 +168,10 @@ class SongSelect{
category: strings.random category: strings.random
}) })
} }
this.showWarning = showWarning
if(showWarning && showWarning.name === "scoreSaveFailed"){
scoreStorage.scoreSaveFailed = true
}
this.songs.push({ this.songs.push({
title: strings.aboutSimulator, title: strings.aboutSimulator,
skin: this.songSkin.about, skin: this.songSkin.about,
...@@ -192,7 +202,7 @@ class SongSelect{ ...@@ -192,7 +202,7 @@ class SongSelect{
}) })
this.songAsset = { this.songAsset = {
marginTop: 90, marginTop: 104,
marginLeft: 18, marginLeft: 18,
width: 82, width: 82,
selectedWidth: 382, selectedWidth: 382,
...@@ -226,6 +236,7 @@ class SongSelect{ ...@@ -226,6 +236,7 @@ class SongSelect{
this.difficultyCache = new CanvasCache(noSmoothing) this.difficultyCache = new CanvasCache(noSmoothing)
this.sessionCache = new CanvasCache(noSmoothing) this.sessionCache = new CanvasCache(noSmoothing)
this.currentSongCache = new CanvasCache(noSmoothing) this.currentSongCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing)
this.difficulty = [strings.easy, strings.normal, strings.hard, strings.oni] this.difficulty = [strings.easy, strings.normal, strings.hard, strings.oni]
this.difficultyId = ["easy", "normal", "hard", "oni", "ura"] this.difficultyId = ["easy", "normal", "hard", "oni", "ura"]
...@@ -237,6 +248,7 @@ class SongSelect{ ...@@ -237,6 +248,7 @@ class SongSelect{
this.selectedSong = 0 this.selectedSong = 0
this.selectedDiff = 0 this.selectedDiff = 0
this.lastCurrentSong = {}
assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506) assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506)
if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage) && !songId){ if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage) && !songId){
...@@ -267,7 +279,9 @@ class SongSelect{ ...@@ -267,7 +279,9 @@ class SongSelect{
}else if((!p2.session || fadeIn) && "selectedSong" in localStorage){ }else if((!p2.session || fadeIn) && "selectedSong" in localStorage){
this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1) 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() snd.musicGain.fadeOut()
this.playBgm(false) this.playBgm(false)
} }
...@@ -373,7 +387,13 @@ class SongSelect{ ...@@ -373,7 +387,13 @@ class SongSelect{
return return
} }
var shift = event ? event.shiftKey : this.pressedKeys["shift"] 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"){ if(name === "confirm"){
this.toSelectDifficulty() this.toSelectDifficulty()
}else if(name === "back"){ }else if(name === "back"){
...@@ -447,10 +467,20 @@ class SongSelect{ ...@@ -447,10 +467,20 @@ class SongSelect{
var ctrl = false var ctrl = false
var touch = true 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)){ 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) 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() this.toSession()
}else{ }else{
var moveBy = this.songSelMouse(mouse.x, mouse.y) var moveBy = this.songSelMouse(mouse.x, mouse.y)
...@@ -473,7 +503,7 @@ class SongSelect{ ...@@ -473,7 +503,7 @@ class SongSelect{
window.open(this.songs[this.selectedSong].maker.url) window.open(this.songs[this.selectedSong].maker.url)
}else if(moveBy === this.diffOptions.length + 4){ }else if(moveBy === this.diffOptions.length + 4){
this.state.ura = !this.state.ura 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){ if(this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura){
this.state.move = -1 this.state.move = -1
} }
...@@ -498,14 +528,22 @@ class SongSelect{ ...@@ -498,14 +528,22 @@ class SongSelect{
mouseMove(event){ mouseMove(event){
var mouse = this.mouseOffset(event.offsetX, event.offsetY) var mouse = this.mouseOffset(event.offsetX, event.offsetY)
var moveTo = null 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)){ 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" 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" moveTo = "session"
}else{ }else{
var moveTo = this.songSelMouse(mouse.x, mouse.y) 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 this.state.moveMS = this.getMS() - this.songSelecting.speed
} }
} }
...@@ -544,7 +582,7 @@ class SongSelect{ ...@@ -544,7 +582,7 @@ class SongSelect{
var dir = x > 0 ? 1 : -1 var dir = x > 0 ? 1 : -1
x = Math.abs(x) x = Math.abs(x)
var selectedWidth = this.songAsset.selectedWidth var selectedWidth = this.songAsset.selectedWidth
if(!this.songs[this.selectedSong].stars){ if(!this.songs[this.selectedSong].courses){
selectedWidth = this.songAsset.width selectedWidth = this.songAsset.width
} }
var moveBy = Math.ceil((x - selectedWidth / 2 - this.songAsset.marginLeft / 2) / (this.songAsset.width + this.songAsset.marginLeft)) * dir var moveBy = Math.ceil((x - selectedWidth / 2 - this.songAsset.marginLeft / 2) / (this.songAsset.width + this.songAsset.marginLeft)) * dir
...@@ -565,7 +603,13 @@ class SongSelect{ ...@@ -565,7 +603,13 @@ class SongSelect{
}else if(550 < x && x < 1050 && 95 < y && y < 524){ }else if(550 < x && x < 1050 && 95 < y && y < 524){
var moveBy = Math.floor((x - 550) / ((1050 - 550) / 5)) + this.diffOptions.length var moveBy = Math.floor((x - 550) / ((1050 - 550) / 5)) + this.diffOptions.length
var currentSong = this.songs[this.selectedSong] 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 return moveBy
} }
} }
...@@ -583,7 +627,7 @@ class SongSelect{ ...@@ -583,7 +627,7 @@ class SongSelect{
}) })
} }
}else if(this.state.locked !== 1 || fromP2){ }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 this.state.moveMS = ms
}else{ }else{
this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize
...@@ -601,9 +645,10 @@ class SongSelect{ ...@@ -601,9 +645,10 @@ class SongSelect{
var scroll = resize2 - resize - scrollDelay * 2 var scroll = resize2 - resize - scrollDelay * 2
var soundsDelay = Math.abs((scroll + resize) / moveBy) var soundsDelay = Math.abs((scroll + resize) / moveBy)
this.lastMoveBy = fromP2 ? fromP2.player : false
for(var i = 0; i < Math.abs(moveBy) - 1; i++){ 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) this.pointer(false)
} }
...@@ -625,7 +670,7 @@ class SongSelect{ ...@@ -625,7 +670,7 @@ class SongSelect{
this.state.locked = 1 this.state.locked = 1
this.endPreview() this.endPreview()
this.playSound("se_jump") this.playSound("se_jump", 0, fromP2 ? fromP2.player : false)
} }
} }
...@@ -634,7 +679,7 @@ class SongSelect{ ...@@ -634,7 +679,7 @@ class SongSelect{
this.state.move = moveBy this.state.move = moveBy
this.state.moveMS = this.getMS() - 500 this.state.moveMS = this.getMS() - 500
this.state.locked = 1 this.state.locked = 1
this.playSound("se_ka") this.playSound("se_ka", 0, p2.session ? p2.player : false)
} }
} }
...@@ -645,7 +690,7 @@ class SongSelect{ ...@@ -645,7 +690,7 @@ class SongSelect{
toSelectDifficulty(fromP2){ toSelectDifficulty(fromP2){
var currentSong = this.songs[this.selectedSong] var currentSong = this.songs[this.selectedSong]
if(p2.session && !fromP2 && currentSong.action !== "random"){ if(p2.session && !fromP2 && currentSong.action !== "random"){
if(this.songs[this.selectedSong].stars){ if(this.songs[this.selectedSong].courses){
if(!this.state.selLock){ if(!this.state.selLock){
this.state.selLock = true this.state.selLock = true
p2.send("songsel", { p2.send("songsel", {
...@@ -655,7 +700,7 @@ class SongSelect{ ...@@ -655,7 +700,7 @@ class SongSelect{
} }
} }
}else if(this.state.locked === 0 || fromP2){ }else if(this.state.locked === 0 || fromP2){
if(currentSong.stars){ if(currentSong.courses){
this.state.screen = "difficulty" this.state.screen = "difficulty"
this.state.screenMS = this.getMS() this.state.screenMS = this.getMS()
this.state.locked = true this.state.locked = true
...@@ -665,22 +710,24 @@ class SongSelect{ ...@@ -665,22 +710,24 @@ class SongSelect{
this.selectedDiff = this.diffOptions.length + 3 this.selectedDiff = this.diffOptions.length + 3
} }
this.playSound("se_don") this.playSound("se_don", 0, fromP2 ? fromP2.player : false)
assets.sounds["v_songsel"].stop() 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) pageEvents.send("song-select-difficulty", currentSong)
}else if(currentSong.action === "back"){ }else if(currentSong.action === "back"){
this.clean() this.clean()
this.toTitleScreen() this.toTitleScreen()
}else if(currentSong.action === "random"){ }else if(currentSong.action === "random"){
this.playSound("se_don") this.playSound("se_don", 0, fromP2 ? fromP2.player : false)
this.state.locked = true this.state.locked = true
do{ do{
var i = Math.floor(Math.random() * this.songs.length) var i = Math.floor(Math.random() * this.songs.length)
}while(!this.songs[i].stars) }while(!this.songs[i].courses)
var moveBy = i - this.selectedSong var moveBy = i - this.selectedSong
setTimeout(() => { setTimeout(() => {
this.moveToSong(moveBy) this.moveToSong(moveBy, fromP2)
}, 200) }, 200)
pageEvents.send("song-select-random") pageEvents.send("song-select-random")
}else if(currentSong.action === "tutorial"){ }else if(currentSong.action === "tutorial"){
...@@ -710,7 +757,7 @@ class SongSelect{ ...@@ -710,7 +757,7 @@ class SongSelect{
this.state.moveHover = null this.state.moveHover = null
assets.sounds["v_diffsel"].stop() assets.sounds["v_diffsel"].stop()
this.playSound("se_cancel") this.playSound("se_cancel", 0, fromP2 ? fromP2.player : false)
} }
this.clearHash() this.clearHash()
pageEvents.send("song-select-back") pageEvents.send("song-select-back")
...@@ -719,7 +766,7 @@ class SongSelect{ ...@@ -719,7 +766,7 @@ class SongSelect{
this.clean() this.clean()
var selectedSong = this.songs[this.selectedSong] var selectedSong = this.songs[this.selectedSong]
assets.sounds["v_diffsel"].stop() assets.sounds["v_diffsel"].stop()
this.playSound("se_don") this.playSound("se_don", 0, p2.session ? p2.player : false)
try{ try{
if(assets.customSongs){ if(assets.customSongs){
...@@ -744,23 +791,25 @@ class SongSelect{ ...@@ -744,23 +791,25 @@ class SongSelect{
}else if(p2.socket.readyState === 1 && !assets.customSongs){ }else if(p2.socket.readyState === 1 && !assets.customSongs){
multiplayer = ctrl multiplayer = ctrl
} }
var diff = this.difficultyId[difficulty]
new LoadSong({ new LoadSong({
"title": selectedSong.title, "title": selectedSong.title,
"originalTitle": selectedSong.originalTitle, "originalTitle": selectedSong.originalTitle,
"folder": selectedSong.id, "folder": selectedSong.id,
"difficulty": this.difficultyId[difficulty], "difficulty": diff,
"category": selectedSong.category, "category": selectedSong.category,
"type": selectedSong.type, "type": selectedSong.type,
"offset": selectedSong.offset, "offset": selectedSong.offset,
"songSkin": selectedSong.songSkin, "songSkin": selectedSong.songSkin,
"stars": selectedSong.stars[difficulty], "stars": selectedSong.courses[diff].stars,
"hash": selectedSong.hash "hash": selectedSong.hash,
"lyrics": selectedSong.lyrics
}, autoplay, multiplayer, touch) }, autoplay, multiplayer, touch)
} }
toOptions(moveBy){ toOptions(moveBy){
if(!p2.session){ if(!p2.session){
this.playSound("se_ka") this.playSound("se_ka", 0, p2.session ? p2.player : false)
this.selectedDiff = 1 this.selectedDiff = 1
do{ do{
this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy) this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy)
...@@ -797,12 +846,21 @@ class SongSelect{ ...@@ -797,12 +846,21 @@ class SongSelect{
new SettingsView(this.touchEnabled) new SettingsView(this.touchEnabled)
}, 500) }, 500)
} }
toAccount(){
this.playSound("se_don")
this.clean()
setTimeout(() => {
new Account(this.touchEnabled)
}, 500)
}
toSession(){ toSession(){
if(p2.socket.readyState !== 1 || assets.customSongs){ if(p2.socket.readyState !== 1 || assets.customSongs){
return return
} }
if(p2.session){ if(p2.session){
this.playSound("se_don")
p2.send("gameend") p2.send("gameend")
this.state.moveHover = null
}else{ }else{
localStorage["selectedSong"] = this.selectedSong localStorage["selectedSong"] = this.selectedSong
...@@ -893,6 +951,8 @@ class SongSelect{ ...@@ -893,6 +951,8 @@ class SongSelect{
var textW = strings.id === "en" ? 350 : 280 var textW = strings.id === "en" ? 350 : 280
this.selectTextCache.resize((textW + 53 + 60 + 1) * 2, this.songAsset.marginTop + 15, ratio + 0.5) 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 categories = 0
var lastCategory var lastCategory
this.songs.forEach(song => { this.songs.forEach(song => {
...@@ -921,7 +981,7 @@ class SongSelect{ ...@@ -921,7 +981,7 @@ class SongSelect{
fontFamily: this.font, fontFamily: this.font,
x: w / 2, x: w / 2,
y: 38 / 2, y: 38 / 2,
width: w - 30, width: id === "sessionend" ? 385 : w - 30,
align: "center", align: "center",
baseline: "middle" baseline: "middle"
}, [ }, [
...@@ -962,14 +1022,110 @@ class SongSelect{ ...@@ -962,14 +1022,110 @@ class SongSelect{
}else{ }else{
this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize + (ms - this.state.screenMS - 1000) 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" this.state.screen = "title"
screen = "title" screen = "title"
} }
} }
if((screen === "song" || screen === "difficulty") && (this.showWarning && !this.showWarning.shown || scoreStorage.scoreSaveFailed)){
if(!this.showWarning){
this.showWarning = {name: "scoreSaveFailed"}
}
if(this.bgmEnabled){
this.playBgm(false)
}
if(this.showWarning.name === "scoreSaveFailed"){
scoreStorage.scoreSaveFailed = false
}
this.showWarning.shown = true
this.state.showWarning = true
this.state.locked = true
this.playSound("se_pause")
}
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
var textW = strings.id === "en" ? 350 : 280
this.selectTextCache.get({
ctx: ctx,
x: frameLeft,
y: frameTop,
w: textW + 53 + 60,
h: this.songAsset.marginTop + 15,
id: "song"
}, ctx => {
this.draw.layeredText({
ctx: ctx,
text: strings.selectSong,
fontSize: 48,
fontFamily: this.font,
x: 53,
y: 30,
width: textW,
letterSpacing: strings.id === "en" ? 0 : 2,
forceShadow: true
}, [
{x: -2, y: -2, outline: "#000", letterBorder: 22},
{},
{x: 2, y: 2, shadow: [3, 3, 3]},
{x: 2, y: 2, outline: "#ad1516", letterBorder: 10},
{x: -2, y: -2, outline: "#ff797b"},
{outline: "#f70808"},
{fill: "#fff", shadow: [-1, 1, 3, 1.5]}
])
})
var category = this.songs[this.selectedSong].category
var selectedSong = this.songs[this.selectedSong]
this.draw.category({
ctx: ctx,
x: winW / 2 - 280 / 2 - 30,
y: frameTop + 60,
fill: selectedSong.skin.background,
highlight: this.state.moveHover === "categoryPrev"
})
this.draw.category({
ctx: ctx,
x: winW / 2 + 280 / 2 + 30,
y: frameTop + 60,
right: true,
fill: selectedSong.skin.background,
highlight: this.state.moveHover === "categoryNext"
})
this.categoryCache.get({
ctx: ctx,
x: winW / 2 - 280 / 2,
y: frameTop,
w: 280,
h: this.songAsset.marginTop,
id: category + selectedSong.skin.outline
}, ctx => {
if(category){
if(category in strings.categories){
var categoryName = strings.categories[category]
}else{
var categoryName = category
}
this.draw.layeredText({
ctx: ctx,
text: categoryName,
fontSize: 40,
fontFamily: this.font,
x: 280 / 2,
y: 38,
width: 255,
align: "center",
forceShadow: true
}, [
{outline: selectedSong.skin.outline, letterBorder: 12, shadow: [3, 3, 3]},
{fill: "#fff"}
])
}
})
}
if(screen === "song"){ if(screen === "song"){
if(this.songs[this.selectedSong].stars){ if(this.songs[this.selectedSong].courses){
selectedWidth = this.songAsset.selectedWidth selectedWidth = this.songAsset.selectedWidth
} }
...@@ -986,7 +1142,7 @@ class SongSelect{ ...@@ -986,7 +1142,7 @@ class SongSelect{
var previousSelectedSong = this.selectedSong var previousSelectedSong = this.selectedSong
if(!isJump){ if(!isJump){
this.playSound("se_ka") this.playSound("se_ka", 0, this.lastMoveBy)
this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move) this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move)
}else{ }else{
var currentCat = this.songs[this.selectedSong].category var currentCat = this.songs[this.selectedSong].category
...@@ -1054,7 +1210,7 @@ class SongSelect{ ...@@ -1054,7 +1210,7 @@ class SongSelect{
if(elapsed < resize){ if(elapsed < resize){
selectedWidth = this.songAsset.width + (((resize - elapsed) / resize) * (selectedWidth - this.songAsset.width)) selectedWidth = this.songAsset.width + (((resize - elapsed) / resize) * (selectedWidth - this.songAsset.width))
}else if(elapsed > resize2){ }else if(elapsed > resize2){
this.playBgm(!this.songs[this.selectedSong].stars) this.playBgm(!this.songs[this.selectedSong].courses)
this.state.locked = 1 this.state.locked = 1
selectedWidth = this.songAsset.width + ((elapsed - resize2) / resize * (selectedWidth - this.songAsset.width)) selectedWidth = this.songAsset.width + ((elapsed - resize2) / resize * (selectedWidth - this.songAsset.width))
}else{ }else{
...@@ -1062,7 +1218,7 @@ class SongSelect{ ...@@ -1062,7 +1218,7 @@ class SongSelect{
selectedWidth = this.songAsset.width selectedWidth = this.songAsset.width
} }
}else{ }else{
this.playBgm(!this.songs[this.selectedSong].stars) this.playBgm(!this.songs[this.selectedSong].courses)
this.state.locked = 0 this.state.locked = 0
} }
}else if(screen === "difficulty"){ }else if(screen === "difficulty"){
...@@ -1071,7 +1227,7 @@ class SongSelect{ ...@@ -1071,7 +1227,7 @@ class SongSelect{
this.state.locked = 0 this.state.locked = 0
} }
if(this.state.move){ if(this.state.move){
var hasUra = currentSong.stars[4] var hasUra = currentSong.courses.ura
var previousSelection = this.selectedDiff var previousSelection = this.selectedDiff
do{ do{
if(hasUra && this.state.move > 0){ if(hasUra && this.state.move > 0){
...@@ -1089,12 +1245,12 @@ class SongSelect{ ...@@ -1089,12 +1245,12 @@ class SongSelect{
this.selectedDiff = this.mod(this.diffOptions.length + 5, this.selectedDiff + this.state.move) this.selectedDiff = this.mod(this.diffOptions.length + 5, this.selectedDiff + this.state.move)
} }
}while( }while(
this.selectedDiff >= this.diffOptions.length && !currentSong.stars[this.selectedDiff - this.diffOptions.length] 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 + 3 && this.state.ura
|| this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura || this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura
) )
this.state.move = 0 this.state.move = 0
}else if(this.selectedDiff < 0 || this.selectedDiff >= this.diffOptions.length && !currentSong.stars[this.selectedDiff - this.diffOptions.length]){ }else if(this.selectedDiff < 0 || this.selectedDiff >= this.diffOptions.length && !currentSong.courses[this.difficultyId[this.selectedDiff - this.diffOptions.length]]){
this.selectedDiff = 0 this.selectedDiff = 0
} }
} }
...@@ -1164,7 +1320,7 @@ class SongSelect{ ...@@ -1164,7 +1320,7 @@ class SongSelect{
var currentSong = this.songs[this.selectedSong] var currentSong = this.songs[this.selectedSong]
var highlight = 0 var highlight = 0
if(!currentSong.stars){ if(!currentSong.courses){
highlight = 2 highlight = 2
} }
if(this.state.moveHover === 0){ if(this.state.moveHover === 0){
...@@ -1185,101 +1341,13 @@ class SongSelect{ ...@@ -1185,101 +1341,13 @@ class SongSelect{
highlight = 0 highlight = 0
} }
if(this.currentSongTitle !== currentSong.title){ if(this.lastCurrentSong.title !== currentSong.title || this.lastCurrentSong.subtitle !== currentSong.subtitle){
this.currentSongTitle = currentSong.title this.lastCurrentSong.title = currentSong.title
this.lastCurrentSong.subtitle = currentSong.subtitle
this.currentSongCache.clear() this.currentSongCache.clear()
} }
if(ms > this.state.screenMS + 2000 && selectedWidth === this.songAsset.width){ if(selectedWidth === this.songAsset.width){
this.drawSongCrown({
ctx: ctx,
song: currentSong,
x: winW / 2 - selectedWidth / 2 + xOffset,
y: songTop + this.songAsset.height - selectedHeight
})
}
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
var textW = strings.id === "en" ? 350 : 280
this.selectTextCache.get({
ctx: ctx,
x: frameLeft,
y: frameTop,
w: textW + 53 + 60,
h: this.songAsset.marginTop + 15,
id: "song"
}, ctx => {
this.draw.layeredText({
ctx: ctx,
text: strings.selectSong,
fontSize: 48,
fontFamily: this.font,
x: 53,
y: 30,
width: textW,
letterSpacing: strings.id === "en" ? 0 : 2,
forceShadow: true
}, [
{x: -2, y: -2, outline: "#000", letterBorder: 22},
{},
{x: 2, y: 2, shadow: [3, 3, 3]},
{x: 2, y: 2, outline: "#ad1516", letterBorder: 10},
{x: -2, y: -2, outline: "#ff797b"},
{outline: "#f70808"},
{fill: "#fff", shadow: [-1, 1, 3, 1.5]}
])
})
var category = this.songs[this.selectedSong].category
var selectedSong = this.songs[this.selectedSong]
this.draw.category({
ctx: ctx,
x: winW / 2 - 280 / 2 - 30,
y: frameTop + 60,
fill: selectedSong.skin.background,
highlight: this.state.moveHover === "categoryPrev"
})
this.draw.category({
ctx: ctx,
x: winW / 2 + 280 / 2 + 30,
y: frameTop + 60,
right: true,
fill: selectedSong.skin.background,
highlight: this.state.moveHover === "categoryNext"
})
this.categoryCache.get({
ctx: ctx,
x: winW / 2 - 280 / 2,
y: frameTop,
w: 280,
h: this.songAsset.marginTop,
id: category + selectedSong.skin.outline
}, ctx => {
if(category){
if(category in strings.categories){
var categoryName = strings.categories[category]
}else{
var categoryName = category
}
this.draw.layeredText({
ctx: ctx,
text: categoryName,
fontSize: 40,
fontFamily: this.font,
x: 280 / 2,
y: 38,
width: 255,
align: "center",
forceShadow: true
}, [
{outline: selectedSong.skin.outline, letterBorder: 12, shadow: [3, 3, 3]},
{fill: "#fff"}
])
}
})
}
if(ms <= this.state.screenMS + 2000 && selectedWidth === this.songAsset.width){
this.drawSongCrown({ this.drawSongCrown({
ctx: ctx, ctx: ctx,
song: currentSong, song: currentSong,
...@@ -1313,8 +1381,8 @@ class SongSelect{ ...@@ -1313,8 +1381,8 @@ class SongSelect{
var textW = strings.id === "en" ? 350 : 280 var textW = strings.id === "en" ? 350 : 280
this.selectTextCache.get({ this.selectTextCache.get({
ctx: ctx, ctx: ctx,
x: x - 144 - 53, x: frameLeft,
y: y - 24 - 30, y: frameTop,
w: textW + 53 + 60, w: textW + 53 + 60,
h: this.songAsset.marginTop + 15, h: this.songAsset.marginTop + 15,
id: "difficulty" id: "difficulty"
...@@ -1411,28 +1479,42 @@ class SongSelect{ ...@@ -1411,28 +1479,42 @@ class SongSelect{
ctx: ctx, ctx: ctx,
font: this.font, font: this.font,
x: _x, x: _x,
y: _y - 45 y: _y - 45,
two: p2.session && p2.player === 2
}) })
} }
} }
} }
} }
var drawDifficulty = (ctx, i, currentUra) => { var drawDifficulty = (ctx, i, currentUra) => {
if(currentSong.stars[i] || currentUra){ if(currentSong.courses[this.difficultyId[i]] || currentUra){
var score = scoreStorage.get(currentSong.hash, false, true)
var crownDiff = currentUra ? "ura" : this.difficultyId[i] var crownDiff = currentUra ? "ura" : this.difficultyId[i]
var crownType = "" var players = p2.session ? 2 : 1
if(score && score[crownDiff]){ var score = [scoreStorage.get(currentSong.hash, false, true)]
crownType = score[crownDiff].crown 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){ if(songSel){
var _x = x + 33 + i * 60 var _x = x + 33 + i * 60
var _y = y + 120 var _y = y + 120
...@@ -1502,9 +1584,9 @@ class SongSelect{ ...@@ -1502,9 +1584,9 @@ class SongSelect{
outlineSize: currentUra ? this.songAsset.letterBorder : 0 outlineSize: currentUra ? this.songAsset.letterBorder : 0
}) })
}) })
var songStarsArray = (currentUra ? currentSong.stars[4] : currentSong.stars[i]).toString().split(" ") var songStarsObj = (currentUra ? currentSong.courses.ura : currentSong.courses[this.difficultyId[i]])
var songStars = songStarsArray[0] var songStars = songStarsObj.stars
var songBranch = songStarsArray[1] === "B" var songBranch = songStarsObj.branch
var elapsedMS = this.state.screenMS > this.state.moveMS || !songSel ? this.state.screenMS : this.state.moveMS var elapsedMS = this.state.screenMS > this.state.moveMS || !songSel ? this.state.screenMS : this.state.moveMS
var fade = ((ms - elapsedMS) % 2000) / 2000 var fade = ((ms - elapsedMS) % 2000) / 2000
if(songBranch && fade > 0.25 && fade < 0.75){ if(songBranch && fade > 0.25 && fade < 0.75){
...@@ -1549,15 +1631,15 @@ class SongSelect{ ...@@ -1549,15 +1631,15 @@ class SongSelect{
if(this.selectedDiff === 4 + this.diffOptions.length){ if(this.selectedDiff === 4 + this.diffOptions.length){
currentDiff = 3 currentDiff = 3
} }
if(i === currentSong.p2Cursor && p2.socket.readyState === 1){ if(songSel && i === currentSong.p2Cursor && p2.socket.readyState === 1){
this.draw.diffCursor({ this.draw.diffCursor({
ctx: ctx, ctx: ctx,
font: this.font, font: this.font,
x: _x, x: _x,
y: _y - (songSel ? 45 : 65), y: _y - 45,
two: true, two: !p2.session || p2.player === 1,
side: songSel ? false : (currentSong.p2Cursor === currentDiff), side: false,
scale: songSel ? 0.7 : 1 scale: 0.7
}) })
} }
if(!songSel){ if(!songSel){
...@@ -1573,7 +1655,8 @@ class SongSelect{ ...@@ -1573,7 +1655,8 @@ class SongSelect{
font: this.font, font: this.font,
x: _x, x: _x,
y: _y - 65, 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){ if(highlight){
...@@ -1591,8 +1674,8 @@ class SongSelect{ ...@@ -1591,8 +1674,8 @@ class SongSelect{
} }
} }
} }
for(var i = 0; currentSong.stars && i < 4; i++){ for(var i = 0; currentSong.courses && i < 4; i++){
var currentUra = i === 3 && (this.state.ura && !songSel || currentSong.stars[4] && songSel) var currentUra = i === 3 && (this.state.ura && !songSel || currentSong.courses.ura && songSel)
if(songSel && currentUra){ if(songSel && currentUra){
drawDifficulty(ctx, i, false) drawDifficulty(ctx, i, false)
var elapsedMS = this.state.screenMS > this.state.moveMS ? this.state.screenMS : this.state.moveMS var elapsedMS = this.state.screenMS > this.state.moveMS ? this.state.screenMS : this.state.moveMS
...@@ -1614,6 +1697,22 @@ class SongSelect{ ...@@ -1614,6 +1697,22 @@ class SongSelect{
drawDifficulty(ctx, i, currentUra) 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 borders = (this.songAsset.border + this.songAsset.innerBorder) * 2
var textW = this.songAsset.width - borders var textW = this.songAsset.width - borders
...@@ -1646,46 +1745,64 @@ class SongSelect{ ...@@ -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) { if (songSel) {
var _x = x + 38 var _x = x + 38
var _y = y + 10 var _y = y + 10
ctx.strokeStyle = "#000"
ctx.lineWidth = 5 ctx.lineWidth = 5
var grd = ctx.createLinearGradient(_x, _y, _x, _y+50); if(hasMaker){
grd.addColorStop(0, '#fa251a'); var grd = ctx.createLinearGradient(_x, _y, _x, _y + 50)
grd.addColorStop(1, '#ffdc33'); grd.addColorStop(0, "#fa251a")
grd.addColorStop(1, "#ffdc33")
ctx.fillStyle = grd; ctx.fillStyle = grd
}else{
ctx.fillStyle = "#000"
}
this.draw.roundedRect({ this.draw.roundedRect({
ctx: ctx, ctx: ctx,
x: _x - 28, x: _x - 28,
y: _y, y: _y,
w: 130, w: 192,
h: 50, h: 50,
radius: 24 radius: 24
}) })
ctx.fill() ctx.fill()
ctx.stroke() ctx.stroke()
ctx.beginPath()
ctx.arc(_x, _y + 28, 20, 0, Math.PI * 2) if(hasMaker){
ctx.fill() this.draw.layeredText({
ctx: ctx,
this.draw.layeredText({ text: strings.creative.creative,
ctx: ctx, fontSize: strings.id === "en" ? 28 : 34,
text: strings.creative.creative, fontFamily: this.font,
fontSize: strings.id == "en" ? 30 : 34, align: "center",
fontFamily: this.font, baseline: "middle",
align: "center", x: _x + 68,
baseline: "middle", y: _y + (strings.id === "ja" || strings.id === "en" ? 25 : 28),
x: _x + 38, width: 172
y: _y + (["ja", "en"].indexOf(strings.id) >= 0 ? 25 : 28), }, [
width: 110 {outline: "#fff", letterBorder: 6},
}, [ {fill: "#000"}
{outline: "#fff", letterBorder: 8}, ])
{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){ } else if(currentSong.maker && currentSong.maker.id > 0 && currentSong.maker.name){
var _x = x + 62 var _x = x + 62
var _y = y + 380 var _y = y + 380
...@@ -1753,7 +1870,7 @@ class SongSelect{ ...@@ -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 fade = ((ms - this.state.screenMS) % 1200) / 1200
var _x = x + 402 + 4 * 100 + fade * 25 var _x = x + 402 + 4 * 100 + fade * 25
var _y = y + 258 var _y = y + 258
...@@ -1842,7 +1959,7 @@ class SongSelect{ ...@@ -1842,7 +1959,7 @@ class SongSelect{
ctx.fillRect(0, frameTop + 595, 1280 + frameLeft * 2, 125 + frameTop) ctx.fillRect(0, frameTop + 595, 1280 + frameLeft * 2, 125 + frameTop)
var x = 0 var x = 0
var y = frameTop + 603 var y = frameTop + 603
var w = frameLeft + 638 var w = p2.session ? frameLeft + 638 - 200 : frameLeft + 638
var h = 117 + frameTop var h = 117 + frameTop
this.draw.pattern({ this.draw.pattern({
ctx: ctx, ctx: ctx,
...@@ -1869,7 +1986,88 @@ class SongSelect{ ...@@ -1869,7 +1986,88 @@ class SongSelect{
ctx.lineTo(x + w - 4, y + h) ctx.lineTo(x + w - 4, y + h)
ctx.lineTo(x + w - 4, y + 4) ctx.lineTo(x + w - 4, y + 4)
ctx.fill() 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){ if(p2.session){
this.draw.pattern({ this.draw.pattern({
ctx: ctx, ctx: ctx,
...@@ -1925,7 +2123,7 @@ class SongSelect{ ...@@ -1925,7 +2123,7 @@ class SongSelect{
} }
this.sessionCache.get({ this.sessionCache.get({
ctx: ctx, ctx: ctx,
x: winW / 2, x: p2.session ? winW / 4 : winW / 2,
y: y + (h - 32) / 2, y: y + (h - 32) / 2,
w: winW / 2, w: winW / 2,
h: 38, h: 38,
...@@ -1933,7 +2131,7 @@ class SongSelect{ ...@@ -1933,7 +2131,7 @@ class SongSelect{
}) })
ctx.globalAlpha = 1 ctx.globalAlpha = 1
} }
if(this.state.moveHover === "session"){ if(!p2.session && this.state.moveHover === "session"){
this.draw.highlight({ this.draw.highlight({
ctx: ctx, ctx: ctx,
x: x, x: x,
...@@ -1944,6 +2142,146 @@ class SongSelect{ ...@@ -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"){ if(screen === "titleFadeIn"){
ctx.save() ctx.save()
...@@ -1955,6 +2293,11 @@ class SongSelect{ ...@@ -1955,6 +2293,11 @@ class SongSelect{
ctx.restore() ctx.restore()
} }
if(p2.session && (!this.lastScoreMS || ms > this.lastScoreMS + 1000)){
this.lastScoreMS = ms
scoreStorage.eventLoop()
}
} }
drawClosedSong(config){ drawClosedSong(config){
...@@ -1997,7 +2340,7 @@ class SongSelect{ ...@@ -1997,7 +2340,7 @@ class SongSelect{
}) })
} }
this.draw.songFrame(config) 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({ this.draw.diffCursor({
ctx: ctx, ctx: ctx,
font: this.font, font: this.font,
...@@ -2013,37 +2356,47 @@ class SongSelect{ ...@@ -2013,37 +2356,47 @@ class SongSelect{
drawSongCrown(config){ drawSongCrown(config){
if(!config.song.action && config.song.hash){ if(!config.song.action && config.song.hash){
var ctx = config.ctx 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--;){ for(var i = this.difficultyId.length; i--;){
var diff = this.difficultyId[i] var diff = this.difficultyId[i]
if(!score){ for(var p = players; p--;){
break if(!score[p] || scoreDrawn[p]){
} continue
if(config.song.stars[i] && score[diff] && score[diff].crown){ }
this.draw.crown({ if(config.song.courses[this.difficultyId[i]] && score[p][diff] && score[p][diff].crown){
ctx: ctx, this.draw.crown({
type: score[diff].crown, ctx: ctx,
x: config.x + this.songAsset.width / 2, type: score[p][diff].crown,
y: config.y - 13, x: (config.x + this.songAsset.width / 2) + (players === 2 ? p === 0 ? -13 : 13 : 0),
scale: 0.3, y: config.y - 13,
ratio: this.ratio / this.pixelRatio scale: 0.3,
}) ratio: this.ratio / this.pixelRatio
this.draw.diffIcon({ })
ctx: ctx, this.draw.diffIcon({
diff: i, ctx: ctx,
x: config.x + this.songAsset.width / 2 + 8, diff: i,
y: config.y - 8, x: (config.x + this.songAsset.width / 2 + 8) + (players === 2 ? p === 0 ? -13 : 13 : 0),
scale: diff === "hard" || diff === "normal" ? 0.45 : 0.5, y: config.y - 8,
border: 6.5, scale: diff === "hard" || diff === "normal" ? 0.45 : 0.5,
small: true border: 6.5,
}) small: true
break })
scoreDrawn[p] = true
}
} }
} }
} }
} }
startPreview(loadOnly){ startPreview(loadOnly){
if(!loadOnly && this.state && this.state.showWarning){
return
}
var currentSong = this.songs[this.selectedSong] var currentSong = this.songs[this.selectedSong]
var id = currentSong.id var id = currentSong.id
var prvTime = currentSong.preview var prvTime = currentSong.preview
...@@ -2119,6 +2472,9 @@ class SongSelect{ ...@@ -2119,6 +2472,9 @@ class SongSelect{
} }
} }
playBgm(enabled){ playBgm(enabled){
if(enabled && this.state && this.state.showWarning){
return
}
if(enabled && !this.bgmEnabled){ if(enabled && !this.bgmEnabled){
this.bgmEnabled = true this.bgmEnabled = true
snd.musicGain.fadeIn(0.4) snd.musicGain.fadeIn(0.4)
...@@ -2148,11 +2504,11 @@ class SongSelect{ ...@@ -2148,11 +2504,11 @@ class SongSelect{
}) })
if(currentSong){ if(currentSong){
currentSong.p2Cursor = diffId currentSong.p2Cursor = diffId
if(p2.session && currentSong.stars){ if(p2.session && currentSong.courses){
this.selectedSong = index this.selectedSong = index
this.state.move = 0 this.state.move = 0
if(this.state.screen !== "difficulty"){ if(this.state.screen !== "difficulty"){
this.toSelectDifficulty(true) this.toSelectDifficulty({player: response.value.player})
} }
} }
} }
...@@ -2173,7 +2529,7 @@ class SongSelect{ ...@@ -2173,7 +2529,7 @@ class SongSelect{
var moveBy = response.value.move var moveBy = response.value.move
if(moveBy === -1 || moveBy === 1){ if(moveBy === -1 || moveBy === 1){
this.selectedSong = song this.selectedSong = song
this.categoryJump(moveBy, true) this.categoryJump(moveBy, {player: response.value.player})
} }
}else if(!selected){ }else if(!selected){
this.state.locked = true this.state.locked = true
...@@ -2190,13 +2546,13 @@ class SongSelect{ ...@@ -2190,13 +2546,13 @@ class SongSelect{
if(Math.abs(altMoveBy) < Math.abs(moveBy)){ if(Math.abs(altMoveBy) < Math.abs(moveBy)){
moveBy = altMoveBy 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.selectedSong = song
this.state.move = 0 this.state.move = 0
if(this.state.screen !== "difficulty"){ if(this.state.screen !== "difficulty"){
this.toSelectDifficulty(true) this.toSelectDifficulty({player: response.value.player})
} }
} }
} }
...@@ -2238,16 +2594,11 @@ class SongSelect{ ...@@ -2238,16 +2594,11 @@ class SongSelect{
getLocalTitle(title, titleLang){ getLocalTitle(title, titleLang){
if(titleLang){ if(titleLang){
titleLang = titleLang.split("\n") for(var id in titleLang){
titleLang.forEach(line => { if(id === strings.id && titleLang[id]){
var space = line.indexOf(" ") return titleLang[id]
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
} }
}) }
} }
return title return title
} }
...@@ -2258,13 +2609,13 @@ class SongSelect{ ...@@ -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")){ if(!this.drumSounds && (id === "se_don" || id === "se_ka" || id === "se_cancel")){
return return
} }
var ms = Date.now() + (time || 0) * 1000 var ms = Date.now() + (time || 0) * 1000
if(!(id in this.playedSounds) || ms > this.playedSounds[id] + 30){ 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 this.playedSounds[id] = ms
} }
} }
......
function StringsJa(){ var languageList = ["ja", "en", "cn", "tw", "ko"]
this.id = "ja" var translations = {
this.name = "日本語" name: {
this.regex = /^ja$|^ja-/ ja: "日本語",
this.font = "TnT, Meiryo, sans-serif" 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 = "たいこウェブ" taikoWeb: {
this.titleProceed = "クリックするかEnterを押す!" ja: "たいこウェブ",
this.titleDisclaimer = "この非公式シミュレーターはバンダイナムコとは関係がありません。" en: "Taiko Web",
this.titleCopyright = "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc." cn: "太鼓网页",
this.categories = { tw: "太鼓網頁",
"J-POP": "J-POP", ko: "태고 웹"
"アニメ": "アニメ", },
"ボーカロイド™曲": "ボーカロイド™曲", titleProceed: {
"バラエティ": "バラエティ", ja: "クリックするかEnterを押す!",
"クラシック": "クラシック", en: "Click or Press Enter!",
"ゲームミュージック": "ゲームミュージック", cn: "点击或按回车!",
"ナムコオリジナル": "ナムコオリジナル" tw: "點擊或按確認!",
} ko: "클릭하거나 Enter를 누릅니다!"
this.selectSong = "曲をえらぶ" },
this.selectDifficulty = "むずかしさをえらぶ" titleDisclaimer: {
this.back = "もどる" ja: "この非公式シミュレーターはバンダイナムコとは関係がありません。",
this.random = "ランダム" en: "This unofficial simulator is unaffiliated with BANDAI NAMCO.",
this.randomSong = "ランダムに曲をえらぶ" cn: "这款非官方模拟器与BANDAI NAMCO无关。",
this.howToPlay = "あそびかた説明" tw: "這款非官方模擬器與BANDAI NAMCO無關。",
this.aboutSimulator = "このシミュレータについて" ko: "이 비공식 시뮬레이터는 반다이 남코와 관련이 없습니다."
this.gameSettings = "ゲーム設定" },
this.browse = "参照する…" titleCopyright: {
this.defaultSongList = "デフォルト曲リスト" en: "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc."
this.songOptions = "演奏オプション" },
this.none = "なし" categories: {
this.auto = "オート" "J-POP": {
this.netplay = "ネットプレイ" ja: "J-POP",
this.easy = "かんたん" en: "Pop",
this.normal = "ふつう" cn: "流行音乐",
this.hard = "むずかしい" tw: "流行音樂",
this.oni = "おに" ko: "POP"
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 = "エラーが発生しました。再読み込みしてください。"
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 = {
language: {
name: "言語"
}, },
resolution: { "アニメ": {
name: "ゲームの解像度", ja: "アニメ",
high: "", en: "Anime",
medium: "", cn: "卡通动画音乐",
low: "", tw: "卡通動畫音樂",
lowest: "最低" ko: "애니메이션"
}, },
touchAnimation: { "ボーカロイド™曲": {
name: "タッチアニメーション" ja: "ボーカロイド™曲",
en: "VOCALOID™ Music"
}, },
keyboardSettings: { "バラエティ": {
name: "キーボード設定", ja: "バラエティ",
ka_l: "ふち(左)", en: "Variety",
don_l: "面(左)", cn: "综合音乐",
don_r: "面(右)", tw: "綜合音樂",
ka_r: "ふち(右)" ko: "버라이어티"
}, },
gamepadLayout: { "クラシック": {
name: "そうさタイプ設定", ja: "クラシック",
a: "タイプA", en: "Classical",
b: "タイプB", cn: "古典音乐",
c: "タイプC" tw: "古典音樂",
ko: "클래식"
}, },
latency: { "ゲームミュージック": {
name: "Latency", ja: "ゲームミュージック",
value: "Audio: %s, Video: %s", en: "Game Music",
calibration: "Latency Calibration", cn: "游戏音乐",
audio: "Audio", tw: "遊戲音樂",
video: "Video", ko: "게임"
drumSounds: "Drum Sounds"
}, },
easierBigNotes: { "ナムコオリジナル": {
name: "簡単な大きな音符" 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: ""
}, },
on: "オン", ka: {
off: "オフ", ja: "カッ",
default: "既定値にリセット", en: "Ka",
ok: "OK" cn: "",
} tw: "",
this.calibration = { ko: ""
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!", daiDon: {
videoHelp: { ja: "ドン(大)",
title: "Video Latency Calibration", en: "DON",
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!" cn: "咚(大)",
tw: "咚(大)",
ko: "쿵(대)"
}, },
videoComplete: "Video Latency Calibration completed!", daiKa: {
results: { ja: "カッ(大)",
title: "Latency Calibration Results", en: "KA",
content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." cn: "咔(大)",
} tw: "咔(大)",
} ko: "딱(대)"
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: { drumroll: {
name: "Game Resolution", ja: "連打ーっ!!",
high: "High", en: "Drum rollー!!",
medium: "Medium", cn: "连打ー!!",
low: "Low", tw: "連打ー!!",
lowest: "Lowest" ko: "연타ー!!"
}, },
touchAnimation: { daiDrumroll: {
name: "Touch Animation" ja: "連打(大)ーっ!!",
en: "DRUM ROLLー!!",
cn: "连打(大)ー!!",
tw: "連打(大)ー!!",
ko: "연타(대)ー!!"
}, },
keyboardSettings: { balloon: {
name: "Keyboard Settings", ja: "ふうせん",
ka_l: "Left Rim", en: "Balloon",
don_l: "Left Surface", cn: "气球",
don_r: "Right Surface", tw: "氣球",
ka_r: "Right Rim" ko: "풍선"
}, },
gamepadLayout: { },
name: "Gamepad Layout", ex_note: {
a: "Type A", don: {
b: "Type B", ja: ["", ""],
c: "Type C" en: ["Do", "Do"],
cn: ["", ""],
tw: ["", ""],
ko: ["", ""]
}, },
latency: { ka: {
name: "Latency", ja: [""],
value: "Audio: %s, Video: %s", en: ["Ka"],
calibration: "Latency Calibration", cn: [""],
audio: "Audio", tw: [""],
video: "Video", ko: [""]
drumSounds: "Drum Sounds"
}, },
easierBigNotes: { daiDon: {
name: "Easier Big Notes" ja: ["ドン(大)", "ドン(大)"],
en: ["DON", "DON"],
cn: ["咚(大)", "咚(大)"],
tw: ["咚(大)", "咚(大)"],
ko: ["쿵(대)", "쿵(대)"]
}, },
on: "On", daiKa: {
off: "Off", ja: ["カッ(大)"],
default: "Reset to Defaults", en: ["KA"],
ok: "OK" cn: ["咔(大)"],
} tw: ["咔(大)"],
this.calibration = { ko: ["딱(대)"]
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: { combo: {
title: "Video Latency Calibration", ja: "コンボ",
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!" 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: "보통 악보"
}, },
videoComplete: "Video Latency Calibration completed!", advanced: {
results: { ja: "玄人譜面",
title: "Latency Calibration Results", en: "Professional",
content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." cn: "进阶谱面",
tw: "進階譜面",
ko: "현인 악보"
},
master: {
ja: "達人譜面",
en: "Master",
cn: "达人谱面",
tw: "達人譜面",
ko: "달인 악보"
} }
} },
this.browserSupport = { pauseOptions: {
browserWarning: "You are running an unsupported browser (%s)", ja: [
details: "Details...", "演奏をつづける",
failedTests: "The following tests have failed:", "はじめからやりなおす",
supportedBrowser: "Please use a supported browser such as %s" "「曲をえらぶ」にもどる"
} ],
this.creative = { en: [
creative: 'Creative', "Continue",
maker: 'Maker:' "Retry",
} "Back to Select Song"
}
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: [ cn: [
"" "继续演奏",
"从头开始",
"返回「选择乐曲」"
], ],
daiDon: [ tw: [
"咚(大)", "繼續演奏",
"咚(大)" "從頭開始",
"返回「選擇樂曲」"
], ],
daiKa: [ ko: [
"咔(大)" "연주 계속하기",
"처음부터 다시",
"「곡 선택」으로"
] ]
} },
this.combo = "连段" results: {
this.clear = "通关" ja: "成績発表",
this.good = "" en: "Results",
this.ok = "" cn: "发表成绩",
this.bad = "不可" tw: "發表成績",
this.branch = { ko: "성적 발표"
"normal": "一般谱面", },
"advanced": "进阶谱面", points: {
"master": "达人谱面" ja: "",
} en: "pts",
this.pauseOptions = [ cn: "",
"继续演奏", tw: "",
"从头开始", ko: ""
"返回「选择乐曲」" },
] maxCombo: {
this.results = "发表成绩" ja: "最大コンボ数",
this.points = "" en: "MAX Combo",
this.maxCombo = "最多连段数" cn: "最多连段数",
this.drumroll = "连打数" tw: "最多連段數",
ko: "최대 콤보 수"
},
drumroll: {
ja: "連打数",
en: "Drumroll",
cn: "连打数",
tw: "連打數",
ko: "연타 횟수"
},
this.errorOccured = "An error occurred, please refresh" errorOccured: {
this.tutorial = { ja: "エラーが発生しました。再読み込みしてください。",
basics: [ en: "An error occurred, please refresh"
"当流动的音符将与框框重叠时就用鼓棒敲打太鼓吧", },
"遇到红色音符要敲打鼓面(%s或%s)", tutorial: {
"遇到蓝色音符则敲打鼓边(%s或%s)", basics: {
"USB控制器也支持!" ja: [
], "流れてくる音符がワクに重なったらバチで太鼓をたたこう!",
otherControls: "其他控制", "赤い音符は面をたたこう(%sまたは%s)",
otherTutorial: [ "青い音符はフチをたたこう(%sまたは%s)",
"%s暂停游戏", "USBコントローラがサポートされています!"
'%s and %s while selecting song \u2014 navigate categories', ],
"选择难度时按住%s以启用自动模式", en: [
"选择难度时按住%s以启用网络对战模式" "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)...",
ok: "确定" "...and for blue notes, hit the rim! (%s or %s)",
} "USB controllers are also supported!"
this.about = { ],
bugReporting: [ cn: [
"This simulator is still in development.", "当流动的音符将与框框重叠时就用鼓棒敲打太鼓吧",
"Please report any bugs you find.", "遇到红色音符要敲打鼓面(%s或%s)",
"You can report bugs either via our Git repository or email." "遇到蓝色音符则敲打鼓边(%s或%s)",
], "USB控制器也支持!"
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.", tw: [
issues: "工单" "當流動的音符將與框框重疊時就用鼓棒敲打太鼓吧",
} "遇到紅色音符要敲打鼓面(%s或%s)",
this.session = { "遇到藍色音符則敲打鼓邊(%s或%s)",
multiplayerSession: "在线会话", "USB控制器也支持!"
linkTutorial: "复制下方地址,给你的朋友即可开始一起游戏!当他们与您联系之前,请不要离开此页面。", ],
cancel: "取消" ko: [
} "이동하는 음표가 테두리와 겹쳐졌을 때 북채로 태고를 두드리자!",
this.settings = { "빨간 음표는 면을 두드리자 (%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: { language: {
name: "语言" name: {
ja: "言語",
en: "Language",
cn: "语言",
tw: "語系",
ko: "언어"
}
}, },
resolution: { resolution: {
name: "游戏分辨率", name: {
high: "", ja: "ゲームの解像度",
medium: "", en: "Game Resolution",
low: "", cn: "游戏分辨率",
lowest: "最低" 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: { touchAnimation: {
name: "触摸动画" name: {
ja: "タッチアニメーション",
en: "Touch Animation",
cn: "触摸动画",
tw: "觸摸動畫",
ko: "터치 애니메이션"
}
}, },
keyboardSettings: { keyboardSettings: {
name: "键盘设置", name: {
ka_l: "边缘(左)", ja: "キーボード設定",
don_l: "表面(左)", en: "Keyboard Settings",
don_r: "表面(右)", cn: "键盘设置",
ka_r: "边缘(右)" 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: { gamepadLayout: {
name: "操作类型设定", name: {
a: "类型A", ja: "そうさタイプ設定",
b: "类型B", en: "Gamepad Layout",
c: "类型C" 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: { latency: {
name: "Latency", name: {
value: "Audio: %s, Video: %s", ja: null,
calibration: "Latency Calibration", en: "Latency",
audio: "Audio", },
video: "Video", value: {
drumSounds: "Drum Sounds" 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: { 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: "确定"
}
this.calibration = {
title: "Latency Calibration",
ms: "%sms",
back: "Back to Settings",
retryPrevious: "Retry Previous",
start: "Start",
finish: "Finish",
audioHelp: { audioHelp: {
title: "Audio Latency Calibration", title: {
content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!", ja: null,
contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!" 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: { videoHelp: {
title: "Video Latency Calibration", title: {
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!" 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: { results: {
title: "Latency Calibration Results", title: {
content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." 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.",
}
} }
} },
this.browserSupport = { account: {
browserWarning: "You are running an unsupported browser (%s)", username: {
details: "Details...", ja: "ユーザー名",
failedTests: "The following tests have failed:", en: "Username",
supportedBrowser: "Please use a supported browser such as %s" cn: "登录名",
} tw: "使用者名稱",
this.creative = { ko: "사용자 이름"
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: { enterUsername: {
name: "遊戲分辨率", ja: "ユーザー名を入力",
high: "", en: "Enter Username",
medium: "", cn: "输入用户名",
low: "", tw: "輸入用戶名",
lowest: "最低" ko: "사용자 이름을 입력하십시오"
}, },
touchAnimation: { password: {
name: "觸摸動畫" ja: "パスワード",
en: "Password",
cn: "密码",
tw: "密碼",
ko: "비밀번호"
}, },
keyboardSettings: { enterPassword: {
name: "鍵盤設置", ja: "パスワードを入力",
ka_l: "邊緣(左)", en: "Enter Password",
don_l: "表面(左)", cn: "输入密码",
don_r: "表面(右)", tw: "輸入密碼",
ka_r: "邊緣(右)" ko: "비밀번호 입력"
}, },
gamepadLayout: { repeatPassword: {
name: "操作類型設定", ja: "パスワードを再入力",
a: "類型A", en: "Repeat Password",
b: "類型B", cn: "重新输入密码",
c: "類型C" tw: "再次輸入密碼",
ko: "비밀번호 재입력"
}, },
latency: { remember: {
name: "Latency", ja: "ログイン状態を保持する",
value: "Audio: %s, Video: %s", en: "Remember me",
calibration: "Latency Calibration", cn: "记住登录",
audio: "Audio", tw: "記住登錄",
video: "Video", ko: "자동 로그인"
drumSounds: "Drum Sounds"
}, },
easierBigNotes: { login: {
name: "簡單的大音符" ja: "ログイン",
en: "Log In",
cn: "登录",
tw: "登入",
ko: "로그인"
}, },
on: "", register: {
off: "", ja: "登録",
default: "重置為默認值", en: "Register",
ok: "確定" cn: "注册",
} tw: "註冊",
this.calibration = { ko: "가입하기"
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!", registerAccount: {
videoHelp: { ja: "アカウントを登録",
title: "Video Latency Calibration", en: "Register account",
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!" cn: "注册帐号",
tw: "註冊帳號",
ko: "계정 등록"
}, },
videoComplete: "Video Latency Calibration completed!", passwordsDoNotMatch: {
results: { ja: "パスワードが一致しません",
title: "Latency Calibration Results", en: "Passwords do not match",
content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings." 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",
} }
} },
this.browserSupport = { serverError: {
browserWarning: "You are running an unsupported browser (%s)", not_logged_in: {
details: "Details...", ja: null,
failedTests: "The following tests have failed:", en: "Not logged in",
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: { invalid_username: {
name: "게임 해상도", ja: null,
high: "높은", en: "Invalid username, a username can only contain letters, numbers, and underscores, and must be between 3 and 20 characters long",
medium: "중간",
low: "",
lowest: "최저"
}, },
touchAnimation: { username_in_use: {
name: "터치 애니메이션" ja: null,
en: "A user already exists with that username",
}, },
keyboardSettings: { invalid_password: {
name: "키보드 설정", ja: null,
ka_l: "가장자리 (왼쪽)", en: "Cannot use this password, please check that your password is at least 6 characters long",
don_l: "표면 (왼쪽)",
don_r: "표면 (오른쪽)",
ka_r: "가장자리 (오른쪽)"
}, },
gamepadLayout: { invalid_username_password: {
name: "조작 타입 설정", ja: null,
a: "타입 A", en: "Invalid Username or Password",
b: "타입 B",
c: "타입 C"
}, },
latency: { invalid_display_name: {
name: "Latency", ja: null,
value: "Audio: %s, Video: %s", en: "Cannot use this name, please check that your new name is at most 25 characters long",
calibration: "Latency Calibration",
audio: "Audio",
video: "Video",
drumSounds: "Drum Sounds"
}, },
easierBigNotes: { current_password_invalid: {
name: "쉬운 큰 음표" ja: null,
en: "Current password does not match",
}, },
on: "", invalid_new_password: {
off: "오프", ja: null,
default: "기본값으로 재설정", en: "Cannot use this password, please check that your new password is at least 6 characters long",
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!", verify_password_invalid: {
videoHelp: { ja: null,
title: "Video Latency Calibration", en: "Verification password does not match",
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!", invalid_csrf: {
results: { ja: null,
title: "Latency Calibration Results", en: "Security token expired. Please refresh the page."
content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings."
} }
} },
this.browserSupport = { browserSupport: {
browserWarning: "You are running an unsupported browser (%s)", browserWarning: {
details: "Details...", ja: "サポートされていないブラウザを実行しています (%s)",
failedTests: "The following tests have failed:", en: "You are running an unsupported browser (%s)",
supportedBrowser: "Please use a supported browser such as %s" },
} details: {
this.creative = { ja: "詳しく",
creative: '창작', en: "Details...",
maker: '만드는 사람' },
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 = { var allStrings = {}
"ja": new StringsJa(), function separateStrings(){
"en": new StringsEn(), for(var j in languageList){
"cn": new StringsCn(), var lang = languageList[j]
"tw": new StringsTw(), allStrings[lang] = {
"ko": new StringsKo() id: lang
}
var str = allStrings[lang]
var translateObj = function(obj, name, str){
if("en" in obj){
for(var i in obj){
str[name] = obj[lang] || obj.en
}
}else if(obj){
str[name] = {}
for(var i in obj){
translateObj(obj[i], i, str[name])
}
}
}
for(var i in translations){
translateObj(translations[i], i, str)
}
}
} }
separateStrings()
...@@ -126,8 +126,14 @@ ...@@ -126,8 +126,14 @@
this.comboCache = new CanvasCache(noSmoothing) this.comboCache = new CanvasCache(noSmoothing)
this.pauseCache = new CanvasCache(noSmoothing) this.pauseCache = new CanvasCache(noSmoothing)
this.branchCache = new CanvasCache(noSmoothing) this.branchCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing)
this.multiplayer = this.controller.multiplayer 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.touchEnabled = this.controller.touchEnabled
this.touch = -Infinity this.touch = -Infinity
...@@ -223,24 +229,31 @@ ...@@ -223,24 +229,31 @@
this.winH = winH this.winH = winH
this.ratio = ratio this.ratio = ratio
if(this.multiplayer !== 2){ if(this.player !== 2){
this.canvas.width = winW this.canvas.width = winW
this.canvas.height = winH this.canvas.height = winH
ctx.scale(ratio, ratio) ctx.scale(ratio, ratio)
this.canvas.style.width = (winW / this.pixelRatio) + "px" this.canvas.style.width = (winW / this.pixelRatio) + "px"
this.canvas.style.height = (winH / this.pixelRatio) + "px" this.canvas.style.height = (winH / this.pixelRatio) + "px"
this.titleCache.resize(640, 90, ratio) this.titleCache.resize(640, 90, ratio)
} }
if(!this.multiplayer){ if(!this.multiplayer){
this.pauseCache.resize(81 * this.pauseOptions.length * 2, 464, ratio) 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.fillComboCache()
this.setDonBgHeight() this.setDonBgHeight()
if(this.controller.lyrics){
this.controller.lyrics.setScale(ratio / this.pixelRatio)
}
resized = true resized = true
}else if(this.controller.game.paused && !document.hasFocus()){ }else if(this.controller.game.paused && !document.hasFocus()){
return return
}else if(this.multiplayer !== 2){ }else if(this.player !== 2){
ctx.clearRect(0, 0, winW / ratio, winH / ratio) ctx.clearRect(0, 0, winW / ratio, winH / ratio)
} }
winW /= ratio winW /= ratio
...@@ -257,8 +270,8 @@ ...@@ -257,8 +270,8 @@
var frameTop = winH / 2 - 720 / 2 var frameTop = winH / 2 - 720 / 2
var frameLeft = winW / 2 - 1280 / 2 var frameLeft = winW / 2 - 1280 / 2
} }
if(this.multiplayer === 2){ if(this.player === 2){
frameTop += this.multiplayer === 2 ? 165 : 176 frameTop += 165
} }
if(touchMultiplayer){ if(touchMultiplayer){
if(!this.touchp2Class){ if(!this.touchp2Class){
...@@ -273,16 +286,20 @@ ...@@ -273,16 +286,20 @@
this.setDonBgHeight() this.setDonBgHeight()
} }
if(this.controller.lyrics){
this.controller.lyrics.update(ms)
}
ctx.save() ctx.save()
ctx.translate(0, frameTop) ctx.translate(0, frameTop)
this.drawGogoTime() this.drawGogoTime()
if(!touchMultiplayer || this.multiplayer === 1 && frameTop >= 0){ if(!touchMultiplayer || this.player === 1 && frameTop >= 0){
this.assets.drawAssets("background") this.assets.drawAssets("background")
} }
if(this.multiplayer !== 2){ if(this.player !== 2){
this.titleCache.get({ this.titleCache.get({
ctx: ctx, ctx: ctx,
x: winW - (touchMultiplayer && fullScreenSupported ? 750 : 650), x: winW - (touchMultiplayer && fullScreenSupported ? 750 : 650),
...@@ -350,7 +367,7 @@ ...@@ -350,7 +367,7 @@
var score = this.controller.getGlobalScore() var score = this.controller.getGlobalScore()
var gaugePercent = this.rules.gaugePercent(score.gauge) var gaugePercent = this.rules.gaugePercent(score.gauge)
if(this.multiplayer === 2){ if(this.player === 2){
var scoreImg = "bg_score_p2" var scoreImg = "bg_score_p2"
var scoreFill = "#6bbec0" var scoreFill = "#6bbec0"
}else{ }else{
...@@ -373,30 +390,55 @@ ...@@ -373,30 +390,55 @@
size: 100, size: 100,
paddingLeft: 0 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 = { var animPos = {
x1: this.slotPos.x + 13, 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, x2: winW - 38,
y2: frameTop + (this.multiplayer === 2 ? 484 : 293) y2: frameTop + (this.player === 2 ? 484 : 293)
} }
var taikoPos = { var taikoPos = {
x: 19, x: 19,
y: frameTop + (this.multiplayer === 2 ? 464 : 184), y: frameTop + (this.player === 2 ? 464 : 184),
w: 111, w: 111,
h: 130 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.fillStyle = "#000"
ctx.fillRect( ctx.fillRect(
0, 0,
this.multiplayer === 2 ? 306 : 288, this.player === 2 ? 306 : 288,
winW, winW,
this.multiplayer === 1 ? 184 : 183 this.player === 1 ? 184 : 183
) )
ctx.beginPath() ctx.beginPath()
if(this.multiplayer === 2){ if(this.player === 2){
ctx.moveTo(0, 467) ctx.moveTo(0, 467)
ctx.lineTo(384, 467) ctx.lineTo(384, 467)
ctx.lineTo(384, 512) ctx.lineTo(384, 512)
...@@ -415,7 +457,7 @@ ...@@ -415,7 +457,7 @@
ctx.fillStyle = scoreFill ctx.fillStyle = scoreFill
var leftSide = (ctx, mul) => { var leftSide = (ctx, mul) => {
ctx.beginPath() ctx.beginPath()
if(this.multiplayer === 2){ if(this.player === 2){
ctx.moveTo(0, 468 * mul) ctx.moveTo(0, 468 * mul)
ctx.lineTo(380 * mul, 468 * mul) ctx.lineTo(380 * mul, 468 * mul)
ctx.lineTo(380 * mul, 512 * mul) ctx.lineTo(380 * mul, 512 * mul)
...@@ -445,7 +487,7 @@ ...@@ -445,7 +487,7 @@
// Score background // Score background
ctx.fillStyle = "#000" ctx.fillStyle = "#000"
ctx.beginPath() ctx.beginPath()
if(this.multiplayer === 2){ if(this.player === 2){
this.draw.roundedCorner(ctx, 184, 512, 20, 0) this.draw.roundedCorner(ctx, 184, 512, 20, 0)
ctx.lineTo(384, 512) ctx.lineTo(384, 512)
this.draw.roundedCorner(ctx, 384, 560, 12, 2) this.draw.roundedCorner(ctx, 384, 560, 12, 2)
...@@ -463,16 +505,16 @@ ...@@ -463,16 +505,16 @@
ctx.drawImage(assets.image["difficulty"], ctx.drawImage(assets.image["difficulty"],
0, 144 * this.difficulty[this.controller.selectedSong.difficulty], 0, 144 * this.difficulty[this.controller.selectedSong.difficulty],
168, 143, 168, 143,
126, this.multiplayer === 2 ? 497 : 228, 126, this.player === 2 ? 497 : 228,
62, 53 62, 53
) )
} }
// Badges // Badges
if(this.controller.autoPlayEnabled && !this.controller.multiplayer){ if(this.controller.autoPlayEnabled && !this.multiplayer){
this.ctx.drawImage(assets.image["badge_auto"], this.ctx.drawImage(assets.image["badge_auto"],
183, 183,
this.multiplayer === 2 ? 490 : 265, this.player === 2 ? 490 : 265,
23, 23,
23 23
) )
...@@ -482,7 +524,7 @@ ...@@ -482,7 +524,7 @@
ctx.fillStyle = "#000" ctx.fillStyle = "#000"
ctx.beginPath() ctx.beginPath()
var gaugeX = winW - 788 * 0.7 - 32 var gaugeX = winW - 788 * 0.7 - 32
if(this.multiplayer === 2){ if(this.player === 2){
ctx.moveTo(gaugeX, 464) ctx.moveTo(gaugeX, 464)
ctx.lineTo(winW, 464) ctx.lineTo(winW, 464)
ctx.lineTo(winW, 489) ctx.lineTo(winW, 489)
...@@ -497,18 +539,18 @@ ...@@ -497,18 +539,18 @@
this.draw.gauge({ this.draw.gauge({
ctx: ctx, ctx: ctx,
x: winW, x: winW,
y: this.multiplayer === 2 ? 468 : 273, y: this.player === 2 ? 468 : 273,
clear: this.rules.gaugeClear, clear: this.rules.gaugeClear,
percentage: gaugePercent, percentage: gaugePercent,
font: this.font, font: this.font,
scale: 0.7, scale: 0.7,
multiplayer: this.multiplayer === 2, multiplayer: this.player === 2,
blue: this.multiplayer === 2 blue: this.player === 2
}) })
this.draw.soul({ this.draw.soul({
ctx: ctx, ctx: ctx,
x: winW - 40, x: winW - 40,
y: this.multiplayer === 2 ? 484 : 293, y: this.player === 2 ? 484 : 293,
scale: 0.75, scale: 0.75,
cleared: this.rules.clearReached(score.gauge) cleared: this.rules.clearReached(score.gauge)
}) })
...@@ -536,26 +578,50 @@ ...@@ -536,26 +578,50 @@
} }
this.scorePos = { this.scorePos = {
x: 155, x: 155,
y: frameTop + (this.multiplayer === 2 ? 318 : 193) y: frameTop + (this.player === 2 ? 318 : 193)
} }
var animPos = { var animPos = {
x1: this.slotPos.x + 14, 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, 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} 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.fillStyle = "#000"
ctx.fillRect( ctx.fillRect(
0, 0,
184, 184,
winW, winW,
this.multiplayer === 1 ? 177 : 176 this.multiplayer && this.player === 1 ? 177 : 176
) )
ctx.beginPath() ctx.beginPath()
if(this.multiplayer === 2){ if(this.player === 2){
ctx.moveTo(328, 351) ctx.moveTo(328, 351)
ctx.lineTo(winW, 351) ctx.lineTo(winW, 351)
ctx.lineTo(winW, 385) ctx.lineTo(winW, 385)
...@@ -572,17 +638,17 @@ ...@@ -572,17 +638,17 @@
this.draw.gauge({ this.draw.gauge({
ctx: ctx, ctx: ctx,
x: winW, x: winW,
y: this.multiplayer === 2 ? 357 : 135, y: this.player === 2 ? 357 : 135,
clear: this.rules.gaugeClear, clear: this.rules.gaugeClear,
percentage: gaugePercent, percentage: gaugePercent,
font: this.font, font: this.font,
multiplayer: this.multiplayer === 2, multiplayer: this.player === 2,
blue: this.multiplayer === 2 blue: this.player === 2
}) })
this.draw.soul({ this.draw.soul({
ctx: ctx, ctx: ctx,
x: winW - 57, x: winW - 57,
y: this.multiplayer === 2 ? 378 : 165, y: this.player === 2 ? 378 : 165,
cleared: this.rules.clearReached(score.gauge) cleared: this.rules.clearReached(score.gauge)
}) })
...@@ -614,7 +680,7 @@ ...@@ -614,7 +680,7 @@
ctx.drawImage(assets.image["difficulty"], ctx.drawImage(assets.image["difficulty"],
0, 144 * this.difficulty[this.controller.selectedSong.difficulty], 0, 144 * this.difficulty[this.controller.selectedSong.difficulty],
168, 143, 168, 143,
16, this.multiplayer === 2 ? 194 : 232, 16, this.player === 2 ? 194 : 232,
141, 120 141, 120
) )
var diff = this.controller.selectedSong.difficulty var diff = this.controller.selectedSong.difficulty
...@@ -626,13 +692,13 @@ ...@@ -626,13 +692,13 @@
ctx.fillStyle = "#fff" ctx.fillStyle = "#fff"
ctx.lineWidth = 7 ctx.lineWidth = 7
ctx.miterLimit = 1 ctx.miterLimit = 1
ctx.strokeText(text, 87, this.multiplayer === 2 ? 310 : 348) ctx.strokeText(text, 87, this.player === 2 ? 310 : 348)
ctx.fillText(text, 87, this.multiplayer === 2 ? 310 : 348) ctx.fillText(text, 87, this.player === 2 ? 310 : 348)
ctx.miterLimit = 10 ctx.miterLimit = 10
} }
// Badges // Badges
if(this.controller.autoPlayEnabled && !this.controller.multiplayer){ if(this.controller.autoPlayEnabled && !this.multiplayer){
this.ctx.drawImage(assets.image["badge_auto"], this.ctx.drawImage(assets.image["badge_auto"],
125, 235, 34, 34 125, 235, 34, 34
) )
...@@ -641,7 +707,7 @@ ...@@ -641,7 +707,7 @@
// Score background // Score background
ctx.fillStyle = "#000" ctx.fillStyle = "#000"
ctx.beginPath() ctx.beginPath()
if(this.multiplayer === 2){ if(this.player === 2){
ctx.moveTo(0, 312) ctx.moveTo(0, 312)
this.draw.roundedCorner(ctx, 176, 312, 20, 1) this.draw.roundedCorner(ctx, 176, 312, 20, 1)
ctx.lineTo(176, 353) ctx.lineTo(176, 353)
...@@ -666,11 +732,11 @@ ...@@ -666,11 +732,11 @@
}, { }, {
// 560, 10 // 560, 10
x: animPos.x1 + animPos.w / 6, 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 // 940, -150
x: animPos.x2 - animPos.w / 3, 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 // 1225, 165
x: animPos.x2, x: animPos.x2,
...@@ -1390,12 +1456,12 @@ ...@@ -1390,12 +1456,12 @@
var selectedSong = this.controller.selectedSong var selectedSong = this.controller.selectedSong
var songSkinName = selectedSong.songSkin.name var songSkinName = selectedSong.songSkin.name
var donLayers = [] 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 = "" var prefix = ""
this.donBg = document.createElement("div") this.donBg = document.createElement("div")
this.donBg.classList.add("donbg") this.donBg.classList.add("donbg")
if(this.multiplayer === 2){ if(this.player === 2){
this.donBg.classList.add("donbg-bottom") this.donBg.classList.add("donbg-bottom")
} }
for(var layer = 1; layer <= 3; layer++){ for(var layer = 1; layer <= 3; layer++){
...@@ -1525,17 +1591,21 @@ ...@@ -1525,17 +1591,21 @@
// Start animation to gauge // Start animation to gauge
circle.animate(ms) circle.animate(ms)
} }
if(ms - this.controller.audioLatency >= circle.ms && !circle.beatMSCopied && (!circle.branch || circle.branch.active)){ }
if(this.beatInterval !== circle.beatMS){ var game = this.controller.game
this.changeBeatInterval(circle.beatMS) 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(ms - this.controller.audioLatency >= event.ms && !event.gogoChecked && (!event.branch || event.branch.active)){
if(this.gogoTime != circle.gogoTime){ if(this.gogoTime != event.gogoTime){
this.toggleGogoTime(circle) this.toggleGogoTime(event)
} }
circle.gogoChecked = true event.gogoChecked = true
} }
} }
} }
......
...@@ -18,7 +18,7 @@ class ViewAssets{ ...@@ -18,7 +18,7 @@ class ViewAssets{
sw: imgw, sw: imgw,
sh: imgh - 1, sh: imgh - 1,
x: view.portrait ? -60 : 0, 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, w: w,
h: h - 1 h: h - 1
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="view"> <div class="view">
<div class="view-title stroke-sub"></div> <div class="view-title stroke-sub"></div>
<div class="view-content"></div> <div class="view-content"></div>
<div id="diag-txt"></div> <div class="diag-txt"></div>
<div class="left-buttons"> <div class="left-buttons">
<div id="link-issues" class="taibtn stroke-sub link-btn"> <div id="link-issues" class="taibtn stroke-sub link-btn">
<a target="_blank"></a> <a target="_blank"></a>
......
<div class="view-outer">
<div class="view account-view">
<div class="view-title stroke-sub"></div>
<div class="view-content">
<div class="error-div"></div>
<div class="displayname-div">
<div class="displayname-hint"></div>
<input type="text" class="displayname" maxlength="25">
</div>
<form class="accountpass-form">
<div>
<div class="accountpass-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="accountpass-div">
<input type="password" name="password"><input type="password" name="newpassword" autocomplete="new-password"><input type="password" name="newpassword2" autocomplete="new-password">
</div>
</form>
<form class="accountdel-form">
<div>
<div class="accountdel-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="accountdel-div">
<input type="password" name="password">
</div>
</form>
</div>
<div id="diag-txt"></div>
<div class="left-buttons">
<div class="logout-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="save-btn taibtn stroke-sub selected"></div>
<div class="view-end-button taibtn stroke-sub"></div>
</div>
</div>
...@@ -24,6 +24,12 @@ ...@@ -24,6 +24,12 @@
<div class="music-volume input-slider"> <div class="music-volume input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span> <span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div> </div>
<div class="lyrics-hide">
<div>Lyrics offset:</div>
<div class="lyrics-offset input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div>
</div>
<label class="change-restart-label"><input class="change-restart" type="checkbox">Restart on change</label> <label class="change-restart-label"><input class="change-restart" type="checkbox">Restart on change</label>
<label class="autoplay-label"><input class="autoplay" type="checkbox">Auto play</label> <label class="autoplay-label"><input class="autoplay" type="checkbox">Auto play</label>
<div class="bottom-btns"> <div class="bottom-btns">
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
<div id="touch-drum-img"></div> <div id="touch-drum-img"></div>
</div> </div>
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<div id="song-lyrics"></div>
<div id="touch-buttons"> <div id="touch-buttons">
<div id="touch-full-btn"></div><div id="touch-pause-btn"></div> <div id="touch-full-btn"></div><div id="touch-pause-btn"></div>
</div> </div>
......
...@@ -2,3 +2,10 @@ ...@@ -2,3 +2,10 @@
<div class="progress"></div> <div class="progress"></div>
<span class="percentage">0%</span> <span class="percentage">0%</span>
</div> </div>
<div class="view-outer loader-error-div">
<div class="view">
<div class="view-content">An error occurred, please refresh</div>
<div class="diag-txt"></div>
<span class="debug-link">Debug</span>
</div>
</div>
<div class="view-outer">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content">
<div class="error-div"></div>
<form class="login-form">
<div class="username-hint"></div>
<input type="text" name="username" maxlength="20" required>
<div class="password-hint"></div>
<input type="password" name="password" required>
<div class="password2-div"></div>
<div class="remember-div">
<label class="remember-label">
<input type="checkbox" checked="checked" name="remember">
</label>
</div>
<div class="login-btn taibtn stroke-sub link-btn"></div>
</form>
</div>
<div class="left-buttons">
<div class="register-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="view-end-button taibtn stroke-sub selected"></div>
</div>
</div>
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'}
}
}
}
}
...@@ -13,11 +13,11 @@ server_status = { ...@@ -13,11 +13,11 @@ server_status = {
} }
consonants = "bcdfghjklmnpqrstvwxyz" consonants = "bcdfghjklmnpqrstvwxyz"
def msgobj(type, value=None): def msgobj(msg_type, value=None):
if value == None: if value == None:
return json.dumps({"type": type}) return json.dumps({"type": msg_type})
else: else:
return json.dumps({"type": type, "value": value}) return json.dumps({"type": msg_type, "value": value})
def status_event(): def status_event():
value = [] value = []
...@@ -42,7 +42,8 @@ async def connection(ws, path): ...@@ -42,7 +42,8 @@ async def connection(ws, path):
user = { user = {
"ws": ws, "ws": ws,
"action": "ready", "action": "ready",
"session": False "session": False,
"name": None
} }
server_status["users"].append(user) server_status["users"].append(user)
try: try:
...@@ -69,16 +70,17 @@ async def connection(ws, path): ...@@ -69,16 +70,17 @@ async def connection(ws, path):
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
data = {} data = {}
action = user["action"] 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 value = data["value"] if "value" in data else None
if action == "ready": if action == "ready":
# Not playing or waiting # Not playing or waiting
if type == "join": if msg_type == "join":
if value == None: if value == None:
continue continue
waiting = server_status["waiting"] waiting = server_status["waiting"]
id = value["id"] if "id" in value else None id = value["id"] if "id" in value else None
diff = value["diff"] if "diff" 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: if not id or not diff:
continue continue
if id not in waiting: if id not in waiting:
...@@ -92,6 +94,7 @@ async def connection(ws, path): ...@@ -92,6 +94,7 @@ async def connection(ws, path):
await ws.send(msgobj("waiting")) await ws.send(msgobj("waiting"))
else: else:
# Join the other user and start game # Join the other user and start game
user["name"] = value["name"] if "name" in value else None
user["other_user"] = waiting[id]["user"] user["other_user"] = waiting[id]["user"]
waiting_diff = waiting[id]["diff"] waiting_diff = waiting[id]["diff"]
del waiting[id] del waiting[id]
...@@ -99,9 +102,13 @@ async def connection(ws, path): ...@@ -99,9 +102,13 @@ async def connection(ws, path):
user["action"] = "loading" user["action"] = "loading"
user["other_user"]["action"] = "loading" user["other_user"]["action"] = "loading"
user["other_user"]["other_user"] = user user["other_user"]["other_user"] = user
user["other_user"]["player"] = 1
user["player"] = 2
await asyncio.wait([ await asyncio.wait([
ws.send(msgobj("gameload", waiting_diff)), ws.send(msgobj("gameload", {"diff": waiting_diff, "player": 2})),
user["other_user"]["ws"].send(msgobj("gameload", diff)) 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: else:
# Wait for another user # Wait for another user
...@@ -115,28 +122,33 @@ async def connection(ws, path): ...@@ -115,28 +122,33 @@ async def connection(ws, path):
await ws.send(msgobj("waiting")) await ws.send(msgobj("waiting"))
# Update others on waiting players # Update others on waiting players
await notify_status() await notify_status()
elif type == "invite": elif msg_type == "invite":
if value == None: if value and "id" in value and value["id"] == None:
# Session invite link requested # Session invite link requested
invite = get_invite() invite = get_invite()
server_status["invites"][invite] = user server_status["invites"][invite] = user
user["action"] = "invite" user["action"] = "invite"
user["session"] = invite user["session"] = invite
user["name"] = value["name"] if "name" in value else None
await ws.send(msgobj("invite", invite)) 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 # Join a session with the other user
user["other_user"] = server_status["invites"][value] user["name"] = value["name"] if "name" in value else None
del server_status["invites"][value] user["other_user"] = server_status["invites"][value["id"]]
del server_status["invites"][value["id"]]
if "ws" in user["other_user"]: if "ws" in user["other_user"]:
user["other_user"]["other_user"] = user user["other_user"]["other_user"] = user
user["action"] = "invite" user["action"] = "invite"
user["session"] = value user["session"] = value["id"]
sent_msg = msgobj("session") user["other_user"]["player"] = 1
user["player"] = 2
await asyncio.wait([ await asyncio.wait([
ws.send(sent_msg), ws.send(msgobj("session", {"player": 2})),
user["other_user"]["ws"].send(sent_msg) 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: else:
del user["other_user"] del user["other_user"]
await ws.send(msgobj("gameend")) await ws.send(msgobj("gameend"))
...@@ -145,7 +157,7 @@ async def connection(ws, path): ...@@ -145,7 +157,7 @@ async def connection(ws, path):
await ws.send(msgobj("gameend")) await ws.send(msgobj("gameend"))
elif action == "waiting" or action == "loading" or action == "loaded": elif action == "waiting" or action == "loading" or action == "loaded":
# Waiting for another user # Waiting for another user
if type == "leave": if msg_type == "leave":
# Stop waiting # Stop waiting
if user["session"]: if user["session"]:
if "other_user" in user and "ws" in user["other_user"]: if "other_user" in user and "ws" in user["other_user"]:
...@@ -170,7 +182,7 @@ async def connection(ws, path): ...@@ -170,7 +182,7 @@ async def connection(ws, path):
notify_status() notify_status()
]) ])
if action == "loading": if action == "loading":
if type == "gamestart": if msg_type == "gamestart":
user["action"] = "loaded" user["action"] = "loaded"
if user["other_user"]["action"] == "loaded": if user["other_user"]["action"] == "loaded":
user["action"] = "playing" user["action"] = "playing"
...@@ -183,12 +195,12 @@ async def connection(ws, path): ...@@ -183,12 +195,12 @@ async def connection(ws, path):
elif action == "playing": elif action == "playing":
# Playing with another user # Playing with another user
if "other_user" in user and "ws" in user["other_user"]: if "other_user" in user and "ws" in user["other_user"]:
if type == "note"\ if msg_type == "note"\
or type == "drumroll"\ or msg_type == "drumroll"\
or type == "branch"\ or msg_type == "branch"\
or type == "gameresults": or msg_type == "gameresults":
await user["other_user"]["ws"].send(msgobj(type, value)) await user["other_user"]["ws"].send(msgobj(msg_type, value))
elif type == "songsel" and user["session"]: elif msg_type == "songsel" and user["session"]:
user["action"] = "songsel" user["action"] = "songsel"
user["other_user"]["action"] = "songsel" user["other_user"]["action"] = "songsel"
sent_msg1 = msgobj("songsel") sent_msg1 = msgobj("songsel")
...@@ -199,7 +211,7 @@ async def connection(ws, path): ...@@ -199,7 +211,7 @@ async def connection(ws, path):
user["other_user"]["ws"].send(sent_msg1), user["other_user"]["ws"].send(sent_msg1),
user["other_user"]["ws"].send(sent_msg2) user["other_user"]["ws"].send(sent_msg2)
]) ])
elif type == "gameend": elif msg_type == "gameend":
# User wants to disconnect # User wants to disconnect
user["action"] = "ready" user["action"] = "ready"
user["other_user"]["action"] = "ready" user["other_user"]["action"] = "ready"
...@@ -222,7 +234,7 @@ async def connection(ws, path): ...@@ -222,7 +234,7 @@ async def connection(ws, path):
ws.send(status_event()) ws.send(status_event())
]) ])
elif action == "invite": elif action == "invite":
if type == "leave": if msg_type == "leave":
# Cancel session invite # Cancel session invite
if user["session"] in server_status["invites"]: if user["session"] in server_status["invites"]:
del server_status["invites"][user["session"]] del server_status["invites"][user["session"]]
...@@ -243,11 +255,11 @@ async def connection(ws, path): ...@@ -243,11 +255,11 @@ async def connection(ws, path):
ws.send(msgobj("left")), ws.send(msgobj("left")),
ws.send(status_event()) 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"]: if "ws" in user["other_user"]:
user["action"] = "songsel" user["action"] = "songsel"
user["other_user"]["action"] = "songsel" user["other_user"]["action"] = "songsel"
sent_msg = msgobj(type) sent_msg = msgobj(msg_type)
await asyncio.wait([ await asyncio.wait([
ws.send(sent_msg), ws.send(sent_msg),
user["other_user"]["ws"].send(sent_msg) user["other_user"]["ws"].send(sent_msg)
...@@ -262,15 +274,22 @@ async def connection(ws, path): ...@@ -262,15 +274,22 @@ async def connection(ws, path):
elif action == "songsel": elif action == "songsel":
# Session song selection # Session song selection
if "other_user" in user and "ws" in user["other_user"]: 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 # Change song select position
if user["other_user"]["action"] == "songsel": if user["other_user"]["action"] == "songsel" and type(value) is dict:
sent_msg = msgobj(type, value) value["player"] = user["player"]
sent_msg = msgobj(msg_type, value)
await asyncio.wait([ await asyncio.wait([
ws.send(sent_msg), ws.send(sent_msg),
user["other_user"]["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 # Start game
if value == None: if value == None:
continue continue
...@@ -282,8 +301,8 @@ async def connection(ws, path): ...@@ -282,8 +301,8 @@ async def connection(ws, path):
user["action"] = "loading" user["action"] = "loading"
user["other_user"]["action"] = "loading" user["other_user"]["action"] = "loading"
await asyncio.wait([ await asyncio.wait([
ws.send(msgobj("gameload", user["other_user"]["gamediff"])), ws.send(msgobj("gameload", {"diff": user["other_user"]["gamediff"]})),
user["other_user"]["ws"].send(msgobj("gameload", diff)) user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff}))
]) ])
else: else:
user["action"] = "waiting" user["action"] = "waiting"
...@@ -292,7 +311,7 @@ async def connection(ws, path): ...@@ -292,7 +311,7 @@ async def connection(ws, path):
"id": id, "id": id,
"diff": diff "diff": diff
}])) }]))
elif type == "gameend": elif msg_type == "gameend":
# User wants to disconnect # User wants to disconnect
user["action"] = "ready" user["action"] = "ready"
user["session"] = False user["session"] = False
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Taiko Web Admin</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">
<link href="/src/css/admin.css" rel="stylesheet">
</head>
<body>
<header>
<div class="nav">
<a href="/admin/songs">Songs</a>
</div>
</header>
<main>
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
</body>
</html>
{% extends 'admin.html' %}
{% block content %}
<h1>{{ song.title }} <small>(ID: {{ song.id }})</small></h1>
{% for cat, message in get_flashed_messages(with_categories=true) %}
<div class="message{% if cat %} message-{{cat}}{% endif %}">{{ message }}</div>
{% endfor %}
<div class="song-form">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-field">
<span class="checkbox"><input type="checkbox" name="enabled" id="enabled"{% if song.enabled %} checked{% endif %}{% if admin.user_level < 100 %} disabled {% endif %}><label for="enabled"> Enabled</label></span>
</div>
<div class="form-field">
<p>Title</p>
<label for="title">Original</label>
<input type="text" id="title" value="{{song.title or ''}}" name="title" required>
<label for="title_ja">Japanese</label>
<input type="text" id="title_ja" value="{{song.title_lang.ja or ''}}" name="title_ja">
<label for="title_en">English</label>
<input type="text" id="title_en" value="{{song.title_lang.en or ''}}" name="title_en">
<label for="title_cn">Chinese (Simplified)</label>
<input type="text" id="title_cn" value="{{song.title_lang.cn or ''}}" name="title_cn">
<label for="title_tw">Chinese (Traditional)</label>
<input type="text" id="title_tw" value="{{song.title_lang.tw or ''}}" name="title_tw">
<label for="title_ko">Korean</label>
<input type="text" id="title_ko" value="{{song.title_lang.ko or ''}}" name="title_ko">
</div>
<div class="form-field">
<p>Subtitle</p>
<label for="subtitle">Original</label>
<input type="text" id="subtitle" value="{{song.subtitle or ''}}" name="subtitle">
<label for="subtitle_ja">Japanese</label>
<input type="text" id="subtitle_ja" value="{{song.subtitle_lang.ja or ''}}" name="subtitle_ja">
<label for="subtitle_en">English</label>
<input type="text" id="subtitle_en" value="{{song.subtitle_lang.en or ''}}" name="subtitle_en">
<label for="subtitle_cn">Chinese (Simplified)</label>
<input type="text" id="subtitle_cn" value="{{song.subtitle_lang.cn or ''}}" name="subtitle_cn">
<label for="subtitle_tw">Chinese (Traditional)</label>
<input type="text" id="subtitle_tw" value="{{song.subtitle_lang.tw or ''}}" name="subtitle_tw">
<label for="subtitle_ko">Korean</label>
<input type="text" id="subtitle_ko" value="{{song.subtitle_lang.ko or ''}}" name="subtitle_ko">
</div>
<div class="form-field">
<p>Courses</p>
<label for="course_easy">Easy</label>
<input type="number" id="course_easy" value="{{song.courses.easy.stars}}" name="course_easy" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_easy" id="branch_easy"{% if song.courses.easy.branch %} checked{% endif %}><label for="branch_easy"> Diverge Notes</label></span>
<label for="course_normal">Normal</label>
<input type="number" id="course_normal" value="{{song.courses.normal.stars}}" name="course_normal" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_normal" id="branch_normal"{% if song.courses.normal.branch %} checked{% endif %}><label for="branch_normal"> Diverge Notes</label></span>
<label for="course_hard">Hard</label>
<input type="number" id="course_hard" value="{{song.courses.hard.stars}}" name="course_hard" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_hard" id="branch_hard"{% if song.courses.hard.branch %} checked{% endif %}><label for="branch_hard"> Diverge Notes</label></span>
<label for="course_oni">Oni</label>
<input type="number" id="course_oni" value="{{song.courses.oni.stars}}" name="course_oni" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_oni" id="branch_oni"{% if song.courses.oni.branch %} checked{% endif %}><label for="branch_oni"> Diverge Notes</label></span>
<label for="course_ura">Ura</label>
<input type="number" id="course_ura" value="{{song.courses.ura.stars}}" name="course_ura" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_ura" id="branch_ura"{% if song.courses.ura.branch %} checked{% endif %}><label for="branch_ura"> Diverge Notes</label></span>
</div>
<div class="form-field">
<p><label for="category_id">Category</label></p>
<select name="category_id" id="category_id">
<option value="0">(none)</option>
{% for category in categories %}
<option value="{{ category.id }}"{% if song.category_id == category.id %} selected{% endif %}>{{ category.title }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="type">Type</label></p>
<select name="type" id="type">
<option value="tja"{% if song.type == 'tja' %} selected{% endif %}>TJA</option>
<option value="osu"{% if song.type == 'osu' %} selected{% endif %}>osu!taiko</option>
</select>
</div>
<div class="form-field">
<p><label for="offset">Offset</label></p>
<input type="text" id="offset" value="{{song.offset or '0'}}" name="offset" required>
</div>
<div class="form-field">
<p><label for="skin_id">Skin</label></p>
<select name="skin_id" id="skin_id">
<option value="0">(none)</option>
{% for skin in song_skins %}
<option value="{{ skin.id }}"{% if song.skin_id == skin.id %} selected{% endif %}>{{ skin.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="preview">Preview</label></p>
<input type="text" id="preview" value="{{song.preview or '0'}}" name="preview" required>
</div>
<div class="form-field">
<p><label for="volume">Volume</label></p>
<input type="text" id="volume" value="{{song.volume or '1.0'}}" name="volume" required>
</div>
<div class="form-field">
<p><label for="maker_id">Maker</label></p>
<select name="maker_id" id="maker_id">
<option value="0">(none)</option>
{% for maker in makers %}
<option value="{{ maker.id }}"{% if song.maker_id == maker.id %} selected{% endif %}>{{ maker.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="hash">Hash</label></p>
<input type="text" id="hash" value="{{song.hash}}" name="hash"> <span class="checkbox"><input type="checkbox" name="gen_hash" id="gen_hash"{><label for="gen_hash"> Generate</label></span>
</div>
<button type="submit" class="save-song">Save</button>
</form>
{% if admin.user_level >= 100 %}
<form class="delete-song" method="post" action="/admin/songs/{{song.id}}/delete" onsubmit="return confirm('Are you sure you wish to delete this song?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit">Delete song</button>
</form>
{% endif %}
</div>
{% endblock %}
{% extends 'admin.html' %}
{% block content %}
<h1>New song</h1>
{% for message in get_flashed_messages() %}
<div class="message">{{ message }}</div>
{% endfor %}
<div class="song-form">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-field">
<span class="checkbox"><input type="checkbox" name="enabled" id="enabled"><label for="enabled"> Enabled</label></span>
</div>
<div class="form-field">
<p>Title</p>
<label for="title">Original</label>
<input type="text" id="title" value="" name="title" required>
<label for="title_ja">Japanese</label>
<input type="text" id="title_ja" value="" name="title_ja">
<label for="title_en">English</label>
<input type="text" id="title_en" value="" name="title_en">
<label for="title_cn">Chinese (Simplified)</label>
<input type="text" id="title_cn" value="" name="title_cn">
<label for="title_tw">Chinese (Traditional)</label>
<input type="text" id="title_tw" value="" name="title_tw">
<label for="title_ko">Korean</label>
<input type="text" id="title_ko" value="" name="title_ko">
</div>
<div class="form-field">
<p>Subtitle</p>
<label for="subtitle">Original</label>
<input type="text" id="subtitle" value="" name="subtitle">
<label for="subtitle_ja">Japanese</label>
<input type="text" id="subtitle_ja" value="" name="subtitle_ja">
<label for="subtitle_en">English</label>
<input type="text" id="subtitle_en" value="" name="subtitle_en">
<label for="subtitle_cn">Chinese (Simplified)</label>
<input type="text" id="subtitle_cn" value="" name="subtitle_cn">
<label for="subtitle_tw">Chinese (Traditional)</label>
<input type="text" id="subtitle_tw" value="" name="subtitle_tw">
<label for="subtitle_ko">Korean</label>
<input type="text" id="subtitle_ko" value="" name="subtitle_ko">
</div>
<div class="form-field">
<p>Courses</p>
<label for="course_easy">Easy</label>
<input type="number" id="course_easy" value="" name="course_easy" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_easy" id="branch_easy"><label for="branch_easy"> Diverge Notes</label></span>
<label for="course_normal">Normal</label>
<input type="number" id="course_normal" value="" name="course_normal" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_normal" id="branch_normal"><label for="branch_normal"> Diverge Notes</label></span>
<label for="course_hard">Hard</label>
<input type="number" id="course_hard" value="" name="course_hard" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_hard" id="branch_hard"><label for="branch_hard"> Diverge Notes</label></span>
<label for="course_oni">Oni</label>
<input type="number" id="course_oni" value="" name="course_oni" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_oni" id="branch_oni"><label for="branch_oni"> Diverge Notes</label></span>
<label for="course_ura">Ura</label>
<input type="number" id="course_ura" value="" name="course_ura" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_ura" id="branch_ura"><label for="branch_ura"> Diverge Notes</label></span>
</div>
<div class="form-field">
<p><label for="category_id">Category</label></p>
<select name="category_id" id="category_id">
<option value="0">(none)</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.title }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="type">Type</label></p>
<select name="type" id="type">
<option value="tja">TJA</option>
<option value="osu">osu!taiko</option>
</select>
</div>
<div class="form-field">
<p><label for="offset">Offset</label></p>
<input type="text" id="offset" value="" name="offset" required>
</div>
<div class="form-field">
<p><label for="skin_id">Skin</label></p>
<select name="skin_id" id="skin_id">
<option value="0">(none)</option>
{% for skin in song_skins %}
<option value="{{ skin.id }}">{{ skin.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="preview">Preview</label></p>
<input type="text" id="preview" value="" name="preview" required>
</div>
<div class="form-field">
<p><label for="volume">Volume</label></p>
<input type="text" id="volume" value="" name="volume" required>
</div>
<div class="form-field">
<p><label for="maker_id">Maker</label></p>
<select name="maker_id" id="maker_id">
<option value="0">(none)</option>
{% for maker in makers %}
<option value="{{ maker.id }}">{{ maker.name }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="save-song">Save</button>
</form>
</div>
{% endblock %}
{% extends 'admin.html' %}
{% block content %}
{% if admin.user_level >= 100 %}
<a href="/admin/songs/new" class="side-button">New song</a>
{% endif %}
<h1>Songs</h1>
{% for message in get_flashed_messages() %}
<div class="message">{{ message }}</div>
{% endfor %}
{% for song in songs %}
<a href="/admin/songs/{{ song.id }}" class="song-link">
<div class="song">
{% if song.title_lang.en %}
<p>{{ song.title_lang.en }} <small>({{ song.title }})</small></p>
{% else %}
<p>{{ song.title }}</p>
{% endif %}
</div>
</a>
{% endfor %}
{% endblock %}
@echo off @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 ) > ../version.json
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"
#!/bin/bash
./tools/get_version.sh
#!/bin/bash
./tools/get_version.sh
#!/bin/bash
./tools/get_version.sh
#!/bin/bash
./tools/get_version.sh
#!/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()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment