Merge branch 'add-lyrics'

This commit is contained in:
Bui 2020-04-02 20:05:35 +01:00
commit d910de6bc7
52 changed files with 5043 additions and 1800 deletions

3
.gitignore vendored
View File

@ -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
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

544
app.py
View File

@ -1,63 +1,128 @@
#!/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(): class HashException(Exception):
db = getattr(g, '_database', None) pass
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
def query_db(query, args=(), one=False): def api_error(message):
cur = get_db().execute(query, args) return jsonify({'status': 'error', 'message': message})
rv = cur.fetchall()
cur.close()
return (rv[0] if rv else None) if one else rv def generate_hash(id, form):
md5 = hashlib.md5()
if form['type'] == 'tja':
urls = ['%s%s/main.tja' % (config.SONGS_BASEURL, id)]
else:
urls = []
for diff in ['easy', 'normal', 'hard', 'oni', 'ura']:
if form['course_' + diff]:
urls.append('%s%s/%s.osu' % (config.SONGS_BASEURL, id, diff))
for url in urls:
if url.startswith("http://") or url.startswith("https://"):
resp = requests.get(url)
if resp.status_code != 200:
raise HashException('Invalid response from %s (status code %s)' % (resp.url, resp.status_code))
md5.update(resp.content)
else:
if url.startswith("/"):
url = url[1:]
with open(os.path.join("public", url), "rb") as file:
md5.update(file.read())
return base64.b64encode(md5.digest())[:-2].decode('utf-8')
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('username'):
return api_error('not_logged_in')
return f(*args, **kwargs)
return decorated_function
def admin_required(level):
def decorated_function(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not session.get('username'):
return abort(403)
user = db.users.find_one({'username': session.get('username')})
if user['user_level'] < level:
return abort(403)
return f(*args, **kwargs)
return wrapper
return decorated_function
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
return api_error('invalid_csrf')
@app.before_request
def before_request_func():
if session.get('session_id'):
if not db.users.find_one({'session_id': session.get('session_id')}):
session.clear()
def get_config(): def get_config():
if os.path.isfile('config.json'): config_out = {
try: 'songs_baseurl': config.SONGS_BASEURL,
config = json.load(open('config.json', 'r')) 'assets_baseurl': config.ASSETS_BASEURL,
except ValueError: 'email': config.EMAIL,
print('WARNING: Invalid config.json, using default values') 'accounts': config.ACCOUNTS,
config = {} 'custom_js': config.CUSTOM_JS
else: }
print('WARNING: No config.json found, using default values')
config = {}
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():
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():
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():
@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:
song['maker'] = db.makers.find_one({'id': song['maker_id']}, {'_id': False})
else:
song['maker'] = None
del song['maker_id']
category_out = categories[song['category']] if song['category'] in categories else '' if song['category_id']:
song_skin_out = song_skins[song['skin_id']] if song['skin_id'] in song_skins else None song['category'] = db.categories.find_one({'id': song['category_id']})['title']
maker = None else:
if song['maker_id'] == 0: song['category'] = None
maker = 0 del song['category_id']
elif song['maker_id'] and song['maker_id'] > 0:
maker = {'name': song['name'], 'url': song['url'], 'id': song['maker_id']}
songs_out.append({ if song['skin_id']:
'id': song_id, song['song_skin'] = db.song_skins.find_one({'id': song['skin_id']}, {'_id': False, 'id': False})
'title': song['title'], else:
'title_lang': song['title_lang'], song['song_skin'] = None
'subtitle': song['subtitle'], del song['skin_id']
'subtitle_lang': song['subtitle_lang'],
'stars': [
song['easy'], song['normal'], song['hard'], song['oni'], song['ura']
],
'preview': preview,
'category': category_out,
'type': song_type,
'offset': song['offset'],
'song_skin': song_skin_out,
'volume': song['volume'],
'maker': maker,
'hash': song['hash']
})
return jsonify(songs_out) return jsonify(songs)
@app.route('/api/config') @app.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

View File

@ -1,4 +0,0 @@
{
"songs_baseurl": "",
"assets_baseurl": ""
}

35
config.example.py Normal file
View File

@ -0,0 +1,35 @@
# The full URL base asset URL, with trailing slash.
ASSETS_BASEURL = ''
# The full URL base song URL, with trailing slash.
SONGS_BASEURL = ''
# The email address to display in the "About Simulator" menu.
EMAIL = 'taiko@example.com'
# Whether to use the user account system.
ACCOUNTS = True
# Custom JavaScript file to load with the simulator.
CUSTOM_JS = ''
# MongoDB server settings.
MONGO = {
'host': ['127.0.0.1:27017'],
'database': 'taiko'
}
# Redis server settings, used for sessions + cache.
REDIS = {
'CACHE_TYPE': 'redis',
'CACHE_REDIS_HOST': '127.0.0.1',
'CACHE_REDIS_PORT': 6379,
'CACHE_REDIS_PASSWORD': None,
'CACHE_REDIS_DB': None
}
# Secret key used for sessions.
SECRET_KEY = 'change-me'
# Git repository base URL.
URL = 'https://github.com/bui/taiko-web/'

156
public/src/css/admin.css Normal file
View File

@ -0,0 +1,156 @@
body {
margin: 0;
font-family: 'Noto Sans JP', sans-serif;
background: #FF7F00;
}
.nav {
margin: 0;
padding: 0;
width: 200px;
background-color: #A01300;
position: fixed;
height: 100%;
overflow: auto;
}
.nav a {
display: block;
color: #FFF;
padding: 16px;
text-decoration: none;
}
.nav a.active {
background-color: #4CAF50;
color: white;
}
.nav a:hover:not(.active) {
background-color: #555;
color: white;
}
main {
margin-left: 200px;
padding: 1px 16px;
height: 1000px;
}
@media screen and (max-width: 700px) {
.nav {
width: 100%;
height: auto;
position: relative;
}
.nav a {float: left;}
main {margin-left: 0;}
}
@media screen and (max-width: 400px) {
.sidebar a {
text-align: center;
float: none;
}
}
.container {
margin-bottom: 40px;
}
.song {
background: #F84828;
color: white;
padding: 10px;
font-size: 14pt;
margin: 10px 0;
}
.song p {
margin: 0;
}
.song-link {
text-decoration: none;
}
.song-form {
background: #ff5333;
color: #FFF;
padding: 20px;
}
.form-field {
background: #555555;
padding: 15px 20px 20px 20px;
margin-bottom: 20px;
}
.form-field p {
margin: 0;
font-size: 18pt;
}
.form-field > label {
display: block;
}
.form-field input {
margin: 5px 0;
}
.form-field input[type="text"] {
width: 300px;
}
.form-field input[type="number"] {
width: 50px;
}
h1 small {
color: #4a4a4a;
}
.form-field-indent {
margin-left: 20px;
}
.checkbox {
display: inline-block;
}
.checkbox input {
margin-right: 3px;
margin-left: 5px;
}
.message {
background: #2c862f;
padding: 15px;
margin-bottom: 10px;
color: white;
}
.message-error {
background: #b92222;
}
.save-song {
font-size: 22pt;
width: 120px;
}
.delete-song button {
float: right;
margin-top: -25px;
font-size: 12pt;
}
.side-button {
float: right;
background: green;
padding: 5px 20px;
color: white;
text-decoration: none;
margin-top: 25px;
}

View File

@ -123,6 +123,7 @@
} }
#debug .autoplay-label, #debug .autoplay-label,
#debug .branch-hide{ #debug .branch-hide,
#debug .lyrics-hide{
display: none; display: none;
} }

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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{
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{
.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;
}

View File

@ -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){

512
public/src/js/account.js Normal file
View File

@ -0,0 +1,512 @@
class Account{
constructor(touchEnabled){
this.touchEnabled = touchEnabled
cancelTouch = false
this.locked = false
if(account.loggedIn){
this.accountForm()
}else{
this.loginForm()
}
this.selected = this.items.length - 1
this.keyboard = new Keyboard({
confirm: ["enter", "space", "don_l", "don_r"],
previous: ["left", "up", "ka_l"],
next: ["right", "down", "ka_r"],
back: ["escape"]
}, this.keyPressed.bind(this))
this.gamepad = new Gamepad({
"confirm": ["b", "ls", "rs"],
"previous": ["u", "l", "lb", "lt", "lsu", "lsl"],
"next": ["d", "r", "rb", "rt", "lsd", "lsr"],
"back": ["start", "a"]
}, this.keyPressed.bind(this))
pageEvents.send("account", account.loggedIn)
}
accountForm(){
loader.changePage("account", true)
this.mode = "account"
this.setAltText(this.getElement("view-title"), account.username)
this.items = []
this.inputForms = []
this.shownDiv = ""
this.errorDiv = this.getElement("error-div")
this.getElement("displayname-hint").innerText = strings.account.displayName
this.displayname = this.getElement("displayname")
this.displayname.placeholder = strings.account.displayName
this.displayname.value = account.displayName
this.inputForms.push(this.displayname)
this.accountPassButton = this.getElement("accountpass-btn")
this.setAltText(this.accountPassButton, strings.account.changePassword)
pageEvents.add(this.accountPassButton, ["click", "touchstart"], event => {
this.showDiv(event, "pass")
})
this.accountPass = this.getElement("accountpass-form")
for(var i = 0; i < this.accountPass.length; i++){
this.accountPass[i].placeholder = strings.account.currentNewRepeat[i]
this.inputForms.push(this.accountPass[i])
}
this.accountPassDiv = this.getElement("accountpass-div")
this.accountDelButton = this.getElement("accountdel-btn")
this.setAltText(this.accountDelButton, strings.account.deleteAccount)
pageEvents.add(this.accountDelButton, ["click", "touchstart"], event => {
this.showDiv(event, "del")
})
this.accountDel = this.getElement("accountdel-form")
this.accountDel.password.placeholder = strings.account.verifyPassword
this.inputForms.push(this.accountDel.password)
this.accountDelDiv = this.getElement("accountdel-div")
this.logoutButton = this.getElement("logout-btn")
this.setAltText(this.logoutButton, strings.account.logout)
pageEvents.add(this.logoutButton, ["mousedown", "touchstart"], this.onLogout.bind(this))
this.items.push(this.logoutButton)
this.endButton = this.getElement("view-end-button")
this.setAltText(this.endButton, strings.account.cancel)
pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this))
this.items.push(this.endButton)
this.saveButton = this.getElement("save-btn")
this.setAltText(this.saveButton, strings.account.save)
pageEvents.add(this.saveButton, ["mousedown", "touchstart"], this.onSave.bind(this))
this.items.push(this.saveButton)
for(var i = 0; i < this.inputForms.length; i++){
pageEvents.add(this.inputForms[i], ["keydown", "keyup", "keypress"], this.onFormPress.bind(this))
}
}
showDiv(event, div){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
var otherDiv = this.shownDiv && this.shownDiv !== div
var display = this.shownDiv === div ? "" : "block"
this.shownDiv = display ? div : ""
switch(div){
case "pass":
if(otherDiv){
this.accountDelDiv.style.display = ""
}
this.accountPassDiv.style.display = display
break
case "del":
if(otherDiv){
this.accountPassDiv.style.display = ""
}
this.accountDelDiv.style.display = display
break
}
}
loginForm(register, fromSwitch){
loader.changePage("login", true)
this.mode = register ? "register" : "login"
this.setAltText(this.getElement("view-title"), strings.account[this.mode])
this.errorDiv = this.getElement("error-div")
this.items = []
this.form = this.getElement("login-form")
this.getElement("username-hint").innerText = strings.account.username
this.form.username.placeholder = strings.account.enterUsername
this.getElement("password-hint").innerText = strings.account.password
this.form.password.placeholder = strings.account.enterPassword
this.password2 = this.getElement("password2-div")
this.remember = this.getElement("remember-div")
this.getElement("remember-label").appendChild(document.createTextNode(strings.account.remember))
this.loginButton = this.getElement("login-btn")
this.registerButton = this.getElement("register-btn")
if(register){
var pass2 = document.createElement("input")
pass2.type = "password"
pass2.name = "password2"
pass2.required = true
pass2.placeholder = strings.account.repeatPassword
this.password2.appendChild(pass2)
this.password2.style.display = "block"
this.remember.style.display = "none"
this.setAltText(this.loginButton, strings.account.registerAccount)
this.setAltText(this.registerButton, strings.account.login)
}else{
this.setAltText(this.loginButton, strings.account.login)
this.setAltText(this.registerButton, strings.account.register)
}
pageEvents.add(this.form, "submit", this.onLogin.bind(this))
pageEvents.add(this.loginButton, ["mousedown", "touchstart"], this.onLogin.bind(this))
pageEvents.add(this.registerButton, ["mousedown", "touchstart"], this.onSwitchMode.bind(this))
this.items.push(this.registerButton)
if(!register){
this.items.push(this.loginButton)
}
for(var i = 0; i < this.form.length; i++){
pageEvents.add(this.form[i], ["keydown", "keyup", "keypress"], this.onFormPress.bind(this))
}
this.endButton = this.getElement("view-end-button")
this.setAltText(this.endButton, strings.account.back)
pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this))
this.items.push(this.endButton)
if(fromSwitch){
this.selected = 0
this.endButton.classList.remove("selected")
this.registerButton.classList.add("selected")
}
}
getElement(name){
return loader.screen.getElementsByClassName(name)[0]
}
setAltText(element, text){
element.innerText = text
element.setAttribute("alt", text)
}
keyPressed(pressed, name){
if(!pressed || this.locked){
return
}
var selected = this.items[this.selected]
if(name === "confirm"){
if(selected === this.endButton){
this.onEnd()
}else if(selected === this.registerButton){
this.onSwitchMode()
}else if(selected === this.loginButton){
this.onLogin()
}
}else if(name === "previous" || name === "next"){
selected.classList.remove("selected")
this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1))
this.items[this.selected].classList.add("selected")
assets.sounds["se_ka"].play()
}else if(name === "back"){
this.onEnd()
}
}
mod(length, index){
return ((index % length) + length) % length
}
onFormPress(event){
event.stopPropagation()
if(event.type === "keypress" && event.keyCode === 13){
if(this.mode === "account"){
this.onSave()
}else{
this.onLogin()
}
}
}
onSwitchMode(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
this.clean(true)
this.loginForm(this.mode === "login", true)
}
onLogin(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
var obj = {
username: this.form.username.value,
password: this.form.password.value
}
if(!obj.username || !obj.password){
this.error(strings.account.cannotBeEmpty.replace("%s", strings.account[!obj.username ? "username" : "password"]))
return
}
if(this.mode === "login"){
obj.remember = this.form.remember.checked
}else{
if(obj.password !== this.form.password2.value){
this.error(strings.account.passwordsDoNotMatch)
return
}
}
this.request(this.mode, obj).then(response => {
account.loggedIn = true
account.username = response.username
account.displayName = response.display_name
var loadScores = scores => {
scoreStorage.load(scores)
this.onEnd(false, true, true)
pageEvents.send("login", account.username)
}
if(this.mode === "login"){
this.request("scores/get", false, true).then(response => {
loadScores(response.scores)
}, () => {
loadScores({})
})
}else{
scoreStorage.save().catch(() => {}).finally(() => {
this.onEnd(false, true, true)
pageEvents.send("login", account.username)
})
}
}, response => {
if(response && response.status === "error" && response.message){
if(response.message in strings.serverError){
this.error(strings.serverError[response.message])
}else{
this.error(response.message)
}
}else{
this.error(strings.account.error)
}
})
}
onLogout(){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
account.loggedIn = false
delete account.username
delete account.displayName
var loadScores = () => {
scoreStorage.load()
this.onEnd(false, true)
pageEvents.send("logout")
}
this.request("logout").then(loadScores, loadScores)
}
onSave(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
this.clearError()
var promises = []
var noNameChange = false
if(this.shownDiv === "pass"){
var passwords = []
for(var i = 0; i < this.accountPass.length; i++){
passwords.push(this.accountPass[i].value)
}
if(passwords[1] === passwords[2]){
promises.push(this.request("account/password", {
current_password: passwords[0],
new_password: passwords[1]
}))
}else{
this.error(strings.account.newPasswordsDoNotMatch)
return
}
}
if(this.shownDiv === "del" && this.accountDel.password.value){
noNameChange = true
promises.push(this.request("account/remove", {
password: this.accountDel.password.value
}).then(() => {
account.loggedIn = false
delete account.username
delete account.displayName
scoreStorage.load()
pageEvents.send("logout")
return Promise.resolve
}))
}
var newName = this.displayname.value.trim()
if(!noNameChange && newName !== account.displayName){
promises.push(this.request("account/display_name", {
display_name: newName
}).then(response => {
account.displayName = response.display_name
}))
}
var error = false
var errorFunc = response => {
if(error){
return
}
if(response && response.message){
if(response.message in strings.serverError){
this.error(strings.serverError[response.message])
}else{
this.error(response.message)
}
}else{
this.error(strings.account.error)
}
}
Promise.all(promises).then(() => {
this.onEnd(false, true)
}, errorFunc).catch(errorFunc)
}
onEnd(event, noSound, noReset){
var touched = false
if(event){
if(event.type === "touchstart"){
event.preventDefault()
touched = true
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
this.clean(false, noReset)
assets.sounds["se_don"].play()
setTimeout(() => {
new SongSelect(false, false, touched)
}, 500)
}
request(url, obj, get){
this.lock(true)
var doRequest = token => {
return new Promise((resolve, reject) => {
var request = new XMLHttpRequest()
request.open(get ? "GET" : "POST", "api/" + url)
pageEvents.load(request).then(() => {
this.lock(false)
if(request.status !== 200){
reject()
return
}
try{
var json = JSON.parse(request.response)
}catch(e){
reject()
return
}
if(json.status === "ok"){
resolve(json)
}else{
reject(json)
}
}, () => {
this.lock(false)
reject()
})
if(!get){
request.setRequestHeader("X-CSRFToken", token)
}
if(obj){
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
request.send(JSON.stringify(obj))
}else{
request.send()
}
})
}
if(get){
return doRequest()
}else{
return loader.getCsrfToken().then(doRequest)
}
}
lock(isLocked){
this.locked = isLocked
if(this.mode === "login" || this.mode === "register"){
for(var i = 0; i < this.form.length; i++){
this.form[i].disabled = isLocked
}
}else if(this.mode === "account"){
for(var i = 0; i < this.inputForms.length; i++){
this.inputForms[i].disabled = isLocked
}
}
}
error(text){
this.errorDiv.innerText = text
this.errorDiv.style.display = "block"
}
clearError(){
this.errorDiv.innerText = ""
this.errorDiv.style.display = ""
}
clean(eventsOnly, noReset){
if(!eventsOnly){
cancelTouch = true
this.keyboard.clean()
this.gamepad.clean()
}
if(this.mode === "account"){
if(!noReset){
this.accountPass.reset()
this.accountDel.reset()
}
pageEvents.remove(this.accounPassButton, ["click", "touchstart"])
pageEvents.remove(this.accountDelButton, ["click", "touchstart"])
pageEvents.remove(this.logoutButton, ["mousedown", "touchstart"])
pageEvents.remove(this.saveButton, ["mousedown", "touchstart"])
for(var i = 0; i < this.inputForms.length; i++){
pageEvents.remove(this.inputForms[i], ["keydown", "keyup", "keypress"])
}
delete this.errorDiv
delete this.displayname
delete this.accountPassButton
delete this.accountPass
delete this.accountPassDiv
delete this.accountDelButton
delete this.accountDel
delete this.accountDelDiv
delete this.logoutButton
delete this.saveButton
delete this.inputForms
}else if(this.mode === "login" || this.mode === "register"){
if(!eventsOnly && !noReset){
this.form.reset()
}
pageEvents.remove(this.form, "submit")
pageEvents.remove(this.loginButton, ["mousedown", "touchstart"])
pageEvents.remove(this.registerButton, ["mousedown", "touchstart"])
for(var i = 0; i < this.form.length; i++){
pageEvents.remove(this.registerButton, ["keydown", "keyup", "keypress"])
}
delete this.errorDiv
delete this.form
delete this.password2
delete this.remember
delete this.loginButton
delete this.registerButton
}
pageEvents.remove(this.endButton, ["mousedown", "touchstart"])
delete this.endButton
delete this.items
}
}

View File

@ -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 = {
"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 = {
"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 = {
"about.html", "about.html",
"debug.html", "debug.html",
"session.html", "session.html",
"settings.html" "settings.html",
"account.html",
"login.html"
], ],
"songs": [], "songs": [],

View File

@ -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 @@
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 @@
} }
} }
} }
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 @@
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 @@
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)

View File

@ -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{
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{
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{
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{
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{
debugObj.debug.updateStatus() debugObj.debug.updateStatus()
} }
} }
if(this.lyrics){
this.lyrics.clean()
}
} }
} }

View File

@ -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{
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{
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{
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{
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{
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{
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{
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{
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

View File

@ -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{
} }
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{
} }
} }
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{
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{
} }
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{
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{
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

View File

@ -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 @@
} }
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 @@
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 @@
} }
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){ if(subtitleLangAdded){
songObj.title_lang = titleLangArray.join("\n") songObj.subtitle_lang = subtitleLang
}
var subtitleLangArray = []
for(var id in 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 @@
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 @@
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 @@
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
} }

View File

@ -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{
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{
} }
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{
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{
// 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{
}) })
} }
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{
if(!lang){ if(!lang){
lang = "en" lang = "en"
} }
var errorOccured = allStrings[lang].errorOccured loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang].errorOccured
}else{
var errorOccured = "An error occurred, please refresh"
} }
this.loaderPercentage.appendChild(document.createElement("br")) var loaderError = loader.screen.getElementsByClassName("loader-error-div")[0]
this.loaderPercentage.appendChild(document.createTextNode(errorOccured)) loaderError.style.display = "flex"
this.clean() var diagTxt = loader.screen.getElementsByClassName("diag-txt")[0]
var debugLink = loader.screen.getElementsByClassName("debug-link")[0]
if(navigator.userAgent.indexOf("Android") >= 0){
var iframe = document.createElement("iframe")
diagTxt.appendChild(iframe)
var body = iframe.contentWindow.document.body
body.setAttribute("style", `
font-family: monospace;
margin: 2px 0 0 2px;
white-space: pre-wrap;
word-break: break-all;
cursor: text;
`)
body.setAttribute("onblur", `
getSelection().removeAllRanges()
`)
this.errorTxt = {
element: body,
method: "innerText"
}
}else{
var textarea = document.createElement("textarea")
textarea.readOnly = true
diagTxt.appendChild(textarea)
if(!this.touchEnabled){
textarea.addEventListener("focus", () => {
textarea.select()
})
textarea.addEventListener("blur", () => {
getSelection().removeAllRanges()
})
}
this.errorTxt = {
element: textarea,
method: "value"
}
}
var show = () => {
diagTxt.style.display = "block"
debugLink.style.display = "none"
}
debugLink.addEventListener("click", show)
debugLink.addEventListener("touchstart", show)
this.clean(true)
} }
var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount))
this.errorTxt.element[this.errorTxt.method] = "```\n" + this.errorMessages.join("\n") + "\nPercentage: " + percentage + "%\n```"
} }
assetLoaded(){ assetLoaded(){
if(!this.error){ if(!this.error){
@ -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{
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")
} }
} }

View File

@ -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{
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{
} }
} }
} }
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{
}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)
}) })
} }
addPromise(promise, url){
this.promises.push(promise.catch(response => {
this.errorMsg(response, url)
return Promise.resolve()
}))
}
errorMsg(error, url){
if(!this.error){
if(url){
error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url
}
pageEvents.send("load-song-error", error)
errorMessage(new Error(error).stack)
var title = this.selectedSong.title
if(title !== this.selectedSong.originalTitle){
title += " (" + this.selectedSong.originalTitle + ")"
}
assets.sounds["v_start"].stop()
setTimeout(() => {
this.clean()
new SongSelect(false, false, this.touchEnabled, null, {
name: "loadSongError",
title: title,
id: this.selectedSong.folder,
error: error
})
}, 500)
}
this.error = true
}
loadSongBg(){ loadSongBg(){
return new Promise((resolve, reject) => { var filenames = []
var promises = [] if(this.selectedSong.songBg !== null){
var filenames = [] filenames.push("bg_song_" + this.selectedSong.songBg)
if(this.selectedSong.songBg !== null){ }
filenames.push("bg_song_" + this.selectedSong.songBg) if(this.selectedSong.donBg !== null){
filenames.push("bg_don_" + this.selectedSong.donBg)
if(this.multiplayer){
filenames.push("bg_don2_" + this.selectedSong.donBg)
} }
if(this.selectedSong.donBg !== null){ }
filenames.push("bg_don_" + this.selectedSong.donBg) if(this.selectedSong.songStage !== null){
if(this.multiplayer){ filenames.push("bg_stage_" + this.selectedSong.songStage)
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")
let force = filenameAb.startsWith("bg_song_") && this.touchEnabled
if(this.imgScale !== 1 || force){
img.crossOrigin = "Anonymous"
}
promises.push(pageEvents.load(img).then(() => {
return this.scaleImg(img, filenameAb, "", force)
}))
img.src = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
} }
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{
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{
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{
}) })
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{
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"])

231
public/src/js/lyrics.js Normal file
View File

@ -0,0 +1,231 @@
class Lyrics{
constructor(file, songOffset, div, parsed){
this.div = div
this.stroke = document.createElement("div")
this.stroke.classList.add("stroke")
div.appendChild(this.stroke)
this.fill = document.createElement("div")
this.fill.classList.add("fill")
div.appendChild(this.fill)
this.current = 0
this.shown = -1
this.songOffset = songOffset || 0
this.vttOffset = 0
this.rLinebreak = /\n|\r\n/
this.lines = parsed ? file : this.parseFile(file)
this.length = this.lines.length
}
parseFile(file){
var lines = []
var commands = file.split(/\n\n|\r\n\r\n/)
var arrow = " --> "
for(var i in commands){
var matches = commands[i].match(this.rLinebreak)
if(matches){
var cmd = commands[i].slice(0, matches.index)
var value = commands[i].slice(matches.index + 1)
}else{
var cmd = commands[i]
var value = ""
}
if(cmd.startsWith("WEBVTT")){
var nameValue = cmd.slice(7).split(";")
for(var j in nameValue){
var [name, value] = nameValue[j].split(":")
if(name.trim().toLowerCase() === "offset"){
this.vttOffset = (parseFloat(value.trim()) || 0) * 1000
}
}
}else{
var time = null
var index = cmd.indexOf(arrow)
if(index !== -1){
time = cmd
}else{
var matches = value.match(this.rLinebreak)
if(matches){
var value1 = value.slice(0, matches.index)
index = value1.indexOf(arrow)
if(index !== -1){
time = value1
value = value.slice(index)
}
}
}
if(time !== null){
var start = time.slice(0, index)
var end = time.slice(index + arrow.length)
var index = end.indexOf(" ")
if(index !== -1){
end = end.slice(0, index)
}
var text = value.trim()
var textLang = ""
var firstLang = -1
var index2 = -1
while(true){
var index1 = text.indexOf("<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
}
}

View File

@ -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"){

View File

@ -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{
} }
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{
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{
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{
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
} }
} }

View File

@ -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()

View File

@ -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{
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{
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{
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{
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,

View File

@ -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 @@
} }
}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 @@
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 @@
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 @@
} }
} }
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 @@
} }
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 @@
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
} }
} }

View File

@ -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{
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{
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{
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{
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{
} }
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{
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{
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{
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{
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{
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{
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{
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{
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{
} }
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{
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{
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{
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{
delete this.ctx delete this.ctx
delete this.canvas delete this.canvas
delete this.fadeScreen delete this.fadeScreen
delete this.results
delete this.rules
} }
} }

View File

@ -1,23 +1,38 @@
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{
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{
} }
} }
} }
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{
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()
}
} }

View File

@ -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){

View File

@ -50,6 +50,10 @@ class Settings{
easierBigNotes: { easierBigNotes: {
type: "toggle", type: "toggle",
default: false default: false
},
showLyrics: {
type: "toggle",
default: true
} }
} }

View File

@ -1,5 +1,5 @@
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{
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{
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{
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{
}) })
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{
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{
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{
}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{
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{
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{
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{
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{
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{
}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{
}) })
} }
}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{
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{
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{
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{
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{
} }
} }
}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{
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{
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{
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{
}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{
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{
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{
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,241 +1022,26 @@ 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"){ if((screen === "song" || screen === "difficulty") && (this.showWarning && !this.showWarning.shown || scoreStorage.scoreSaveFailed)){
if(this.songs[this.selectedSong].stars){ if(!this.showWarning){
selectedWidth = this.songAsset.selectedWidth this.showWarning = {name: "scoreSaveFailed"}
} }
if(this.bgmEnabled){
var lastMoveMul = Math.pow(Math.abs(this.state.lastMove), 1 / 4) this.playBgm(false)
var changeSpeed = this.songSelecting.speed * lastMoveMul
var resize = changeSpeed * this.songSelecting.resize / lastMoveMul
var scrollDelay = changeSpeed * this.songSelecting.scrollDelay
var resize2 = changeSpeed - resize
var scroll = resize2 - resize - scrollDelay * 2
var elapsed = ms - this.state.moveMS
if(this.state.catJump || (this.state.move && ms > this.state.moveMS + resize2 - scrollDelay)){
var isJump = this.state.catJump
var previousSelectedSong = this.selectedSong
if(!isJump){
this.playSound("se_ka")
this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move)
}else{
var currentCat = this.songs[this.selectedSong].category
var currentIdx = this.mod(this.songs.length, this.selectedSong)
if(this.state.move > 0){
var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) > currentIdx && song.category !== currentCat && song.canJump)
if(!nextSong){
nextSong = this.songs[0]
}
}else{
var isFirstInCat = this.songs.findIndex(song => song.category === currentCat) == this.selectedSong
if(!isFirstInCat){
var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) < currentIdx && song.category === currentCat && song.canJump)
}else{
var idx = this.songs.length - 1
var nextSong
var lastCat
for(;idx>=0;idx--){
if(this.songs[idx].category !== lastCat && this.songs[idx].action !== "back"){
lastCat = this.songs[idx].category
if(nextSong){
break
}
}
if(lastCat !== currentCat && idx < currentIdx){
nextSong = idx
}
}
nextSong = this.songs[nextSong]
}
if(!nextSong){
var rev = [...this.songs].reverse()
nextSong = rev.find(song => song.canJump)
}
}
this.selectedSong = this.songs.indexOf(nextSong)
this.state.catJump = false
}
if(previousSelectedSong !== this.selectedSong){
pageEvents.send("song-select-move", this.songs[this.selectedSong])
}
this.state.move = 0
this.state.locked = 2
if(assets.customSongs){
assets.customSelected = this.selectedSong
}else if(!p2.session){
try{
localStorage["selectedSong"] = this.selectedSong
}catch(e){}
}
if(this.songs[this.selectedSong].action !== "back"){
var cat = this.songs[this.selectedSong].category
var sort = cat in this.songSkin ? this.songSkin[cat].sort : 7
this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_" + sort].src + "')"
}
} }
if(this.state.moveMS && ms < this.state.moveMS + changeSpeed){ if(this.showWarning.name === "scoreSaveFailed"){
xOffset = Math.min(scroll, Math.max(0, elapsed - resize - scrollDelay)) / scroll * (this.songAsset.width + this.songAsset.marginLeft) scoreStorage.scoreSaveFailed = false
xOffset *= -this.state.move
if(elapsed < resize){
selectedWidth = this.songAsset.width + (((resize - elapsed) / resize) * (selectedWidth - this.songAsset.width))
}else if(elapsed > resize2){
this.playBgm(!this.songs[this.selectedSong].stars)
this.state.locked = 1
selectedWidth = this.songAsset.width + ((elapsed - resize2) / resize * (selectedWidth - this.songAsset.width))
}else{
songSelMoving = true
selectedWidth = this.songAsset.width
}
}else{
this.playBgm(!this.songs[this.selectedSong].stars)
this.state.locked = 0
} }
}else if(screen === "difficulty"){ this.showWarning.shown = true
var currentSong = this.songs[this.selectedSong] this.state.showWarning = true
if(this.state.locked){ this.state.locked = true
this.state.locked = 0 this.playSound("se_pause")
}
if(this.state.move){
var hasUra = currentSong.stars[4]
var previousSelection = this.selectedDiff
do{
if(hasUra && this.state.move > 0){
this.selectedDiff += this.state.move
if(this.selectedDiff > this.diffOptions.length + 4){
this.state.ura = !this.state.ura
if(this.state.ura){
this.selectedDiff = previousSelection === this.diffOptions.length + 3 ? this.diffOptions.length + 4 : previousSelection
break
}else{
this.state.move = -1
}
}
}else{
this.selectedDiff = this.mod(this.diffOptions.length + 5, this.selectedDiff + this.state.move)
}
}while(
this.selectedDiff >= this.diffOptions.length && !currentSong.stars[this.selectedDiff - this.diffOptions.length]
|| this.selectedDiff === this.diffOptions.length + 3 && this.state.ura
|| this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura
)
this.state.move = 0
}else if(this.selectedDiff < 0 || this.selectedDiff >= this.diffOptions.length && !currentSong.stars[this.selectedDiff - this.diffOptions.length]){
this.selectedDiff = 0
}
}
if(songSelMoving){
if(this.previewing !== null){
this.endPreview()
}
}else if(screen !== "title" && screen !== "titleFadeIn" && ms > this.state.moveMS + 100){
if(this.previewing !== this.selectedSong && "id" in this.songs[this.selectedSong]){
this.startPreview()
}
}
this.songFrameCache = {
w: this.songAsset.width + this.songAsset.selectedWidth + this.songAsset.fullWidth + (15 + 1) * 3,
h: this.songAsset.fullHeight + 16,
ratio: ratio
}
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
for(var i = this.selectedSong - 1; ; i--){
var highlight = 0
if(i - this.selectedSong === this.state.moveHover){
highlight = 1
}
var index = this.mod(this.songs.length, i)
var _x = winW / 2 - (this.selectedSong - i) * (this.songAsset.width + this.songAsset.marginLeft) - selectedWidth / 2 + xOffset
if(_x + this.songAsset.width + this.songAsset.marginLeft < 0){
break
}
this.drawClosedSong({
ctx: ctx,
x: _x,
y: songTop,
song: this.songs[index],
highlight: highlight,
disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random"
})
}
var startFrom
for(var i = this.selectedSong + 1; ; i++){
var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset
if(_x > winW){
startFrom = i - 1
break
}
}
for(var i = startFrom; i > this.selectedSong ; i--){
var highlight = 0
if(i - this.selectedSong === this.state.moveHover){
highlight = 1
}
var index = this.mod(this.songs.length, i)
var currentSong = this.songs[index]
var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset
this.drawClosedSong({
ctx: ctx,
x: _x,
y: songTop,
song: this.songs[index],
highlight: highlight,
disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random"
})
}
}
var currentSong = this.songs[this.selectedSong]
var highlight = 0
if(!currentSong.stars){
highlight = 2
}
if(this.state.moveHover === 0){
highlight = 1
}
var selectedSkin = this.songSkin.selected
if(screen === "title" || screen === "titleFadeIn" || this.state.locked === 3){
selectedSkin = currentSong.skin
highlight = 2
}else if(songSelMoving){
selectedSkin = currentSong.skin
highlight = 0
}
var selectedHeight = this.songAsset.height
if(screen === "difficulty"){
selectedWidth = this.songAsset.fullWidth
selectedHeight = this.songAsset.fullHeight
highlight = 0
}
if(this.currentSongTitle !== currentSong.title){
this.currentSongTitle = currentSong.title
this.currentSongCache.clear()
}
if(ms > this.state.screenMS + 2000 && selectedWidth === this.songAsset.width){
this.drawSongCrown({
ctx: ctx,
song: currentSong,
x: winW / 2 - selectedWidth / 2 + xOffset,
y: songTop + this.songAsset.height - selectedHeight
})
} }
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){ if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
@ -1279,7 +1124,230 @@ class SongSelect{
}) })
} }
if(ms <= this.state.screenMS + 2000 && selectedWidth === this.songAsset.width){ if(screen === "song"){
if(this.songs[this.selectedSong].courses){
selectedWidth = this.songAsset.selectedWidth
}
var lastMoveMul = Math.pow(Math.abs(this.state.lastMove), 1 / 4)
var changeSpeed = this.songSelecting.speed * lastMoveMul
var resize = changeSpeed * this.songSelecting.resize / lastMoveMul
var scrollDelay = changeSpeed * this.songSelecting.scrollDelay
var resize2 = changeSpeed - resize
var scroll = resize2 - resize - scrollDelay * 2
var elapsed = ms - this.state.moveMS
if(this.state.catJump || (this.state.move && ms > this.state.moveMS + resize2 - scrollDelay)){
var isJump = this.state.catJump
var previousSelectedSong = this.selectedSong
if(!isJump){
this.playSound("se_ka", 0, this.lastMoveBy)
this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move)
}else{
var currentCat = this.songs[this.selectedSong].category
var currentIdx = this.mod(this.songs.length, this.selectedSong)
if(this.state.move > 0){
var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) > currentIdx && song.category !== currentCat && song.canJump)
if(!nextSong){
nextSong = this.songs[0]
}
}else{
var isFirstInCat = this.songs.findIndex(song => song.category === currentCat) == this.selectedSong
if(!isFirstInCat){
var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) < currentIdx && song.category === currentCat && song.canJump)
}else{
var idx = this.songs.length - 1
var nextSong
var lastCat
for(;idx>=0;idx--){
if(this.songs[idx].category !== lastCat && this.songs[idx].action !== "back"){
lastCat = this.songs[idx].category
if(nextSong){
break
}
}
if(lastCat !== currentCat && idx < currentIdx){
nextSong = idx
}
}
nextSong = this.songs[nextSong]
}
if(!nextSong){
var rev = [...this.songs].reverse()
nextSong = rev.find(song => song.canJump)
}
}
this.selectedSong = this.songs.indexOf(nextSong)
this.state.catJump = false
}
if(previousSelectedSong !== this.selectedSong){
pageEvents.send("song-select-move", this.songs[this.selectedSong])
}
this.state.move = 0
this.state.locked = 2
if(assets.customSongs){
assets.customSelected = this.selectedSong
}else if(!p2.session){
try{
localStorage["selectedSong"] = this.selectedSong
}catch(e){}
}
if(this.songs[this.selectedSong].action !== "back"){
var cat = this.songs[this.selectedSong].category
var sort = cat in this.songSkin ? this.songSkin[cat].sort : 7
this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_" + sort].src + "')"
}
}
if(this.state.moveMS && ms < this.state.moveMS + changeSpeed){
xOffset = Math.min(scroll, Math.max(0, elapsed - resize - scrollDelay)) / scroll * (this.songAsset.width + this.songAsset.marginLeft)
xOffset *= -this.state.move
if(elapsed < resize){
selectedWidth = this.songAsset.width + (((resize - elapsed) / resize) * (selectedWidth - this.songAsset.width))
}else if(elapsed > resize2){
this.playBgm(!this.songs[this.selectedSong].courses)
this.state.locked = 1
selectedWidth = this.songAsset.width + ((elapsed - resize2) / resize * (selectedWidth - this.songAsset.width))
}else{
songSelMoving = true
selectedWidth = this.songAsset.width
}
}else{
this.playBgm(!this.songs[this.selectedSong].courses)
this.state.locked = 0
}
}else if(screen === "difficulty"){
var currentSong = this.songs[this.selectedSong]
if(this.state.locked){
this.state.locked = 0
}
if(this.state.move){
var hasUra = currentSong.courses.ura
var previousSelection = this.selectedDiff
do{
if(hasUra && this.state.move > 0){
this.selectedDiff += this.state.move
if(this.selectedDiff > this.diffOptions.length + 4){
this.state.ura = !this.state.ura
if(this.state.ura){
this.selectedDiff = previousSelection === this.diffOptions.length + 3 ? this.diffOptions.length + 4 : previousSelection
break
}else{
this.state.move = -1
}
}
}else{
this.selectedDiff = this.mod(this.diffOptions.length + 5, this.selectedDiff + this.state.move)
}
}while(
this.selectedDiff >= this.diffOptions.length && !currentSong.courses[this.difficultyId[this.selectedDiff - this.diffOptions.length]]
|| this.selectedDiff === this.diffOptions.length + 3 && this.state.ura
|| this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura
)
this.state.move = 0
}else if(this.selectedDiff < 0 || this.selectedDiff >= this.diffOptions.length && !currentSong.courses[this.difficultyId[this.selectedDiff - this.diffOptions.length]]){
this.selectedDiff = 0
}
}
if(songSelMoving){
if(this.previewing !== null){
this.endPreview()
}
}else if(screen !== "title" && screen !== "titleFadeIn" && ms > this.state.moveMS + 100){
if(this.previewing !== this.selectedSong && "id" in this.songs[this.selectedSong]){
this.startPreview()
}
}
this.songFrameCache = {
w: this.songAsset.width + this.songAsset.selectedWidth + this.songAsset.fullWidth + (15 + 1) * 3,
h: this.songAsset.fullHeight + 16,
ratio: ratio
}
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
for(var i = this.selectedSong - 1; ; i--){
var highlight = 0
if(i - this.selectedSong === this.state.moveHover){
highlight = 1
}
var index = this.mod(this.songs.length, i)
var _x = winW / 2 - (this.selectedSong - i) * (this.songAsset.width + this.songAsset.marginLeft) - selectedWidth / 2 + xOffset
if(_x + this.songAsset.width + this.songAsset.marginLeft < 0){
break
}
this.drawClosedSong({
ctx: ctx,
x: _x,
y: songTop,
song: this.songs[index],
highlight: highlight,
disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random"
})
}
var startFrom
for(var i = this.selectedSong + 1; ; i++){
var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset
if(_x > winW){
startFrom = i - 1
break
}
}
for(var i = startFrom; i > this.selectedSong ; i--){
var highlight = 0
if(i - this.selectedSong === this.state.moveHover){
highlight = 1
}
var index = this.mod(this.songs.length, i)
var currentSong = this.songs[index]
var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset
this.drawClosedSong({
ctx: ctx,
x: _x,
y: songTop,
song: this.songs[index],
highlight: highlight,
disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random"
})
}
}
var currentSong = this.songs[this.selectedSong]
var highlight = 0
if(!currentSong.courses){
highlight = 2
}
if(this.state.moveHover === 0){
highlight = 1
}
var selectedSkin = this.songSkin.selected
if(screen === "title" || screen === "titleFadeIn" || this.state.locked === 3){
selectedSkin = currentSong.skin
highlight = 2
}else if(songSelMoving){
selectedSkin = currentSong.skin
highlight = 0
}
var selectedHeight = this.songAsset.height
if(screen === "difficulty"){
selectedWidth = this.songAsset.fullWidth
selectedHeight = this.songAsset.fullHeight
highlight = 0
}
if(this.lastCurrentSong.title !== currentSong.title || this.lastCurrentSong.subtitle !== currentSong.subtitle){
this.lastCurrentSong.title = currentSong.title
this.lastCurrentSong.subtitle = currentSong.subtitle
this.currentSongCache.clear()
}
if(selectedWidth === this.songAsset.width){
this.drawSongCrown({ this.drawSongCrown({
ctx: ctx, ctx: ctx,
song: currentSong, song: currentSong,
@ -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{
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{
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{
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{
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{
} }
} }
} }
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{
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
@ -1647,45 +1746,63 @@ 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)
ctx.fill()
this.draw.layeredText({ if(hasMaker){
ctx: ctx, this.draw.layeredText({
text: strings.creative.creative, ctx: ctx,
fontSize: strings.id == "en" ? 30 : 34, text: strings.creative.creative,
fontFamily: this.font, fontSize: strings.id === "en" ? 28 : 34,
align: "center", fontFamily: this.font,
baseline: "middle", align: "center",
x: _x + 38, baseline: "middle",
y: _y + (["ja", "en"].indexOf(strings.id) >= 0 ? 25 : 28), x: _x + 68,
width: 110 y: _y + (strings.id === "ja" || strings.id === "en" ? 25 : 28),
}, [ width: 172
{outline: "#fff", letterBorder: 8}, }, [
{fill: "#000"} {outline: "#fff", letterBorder: 6},
]) {fill: "#000"}
])
}else{
this.draw.layeredText({
ctx: ctx,
text: strings.withLyrics,
fontSize: strings.id === "en" ? 28 : 34,
fontFamily: this.font,
align: "center",
baseline: "middle",
x: _x + 68,
y: _y + (strings.id === "ja" || strings.id === "en" ? 25 : 28),
width: 172
}, [
{fill: currentSong.skin.border[0]}
])
}
} else if(currentSong.maker && currentSong.maker.id > 0 && currentSong.maker.name){ } 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{
} }
} }
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{
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{
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{
} }
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{
}) })
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{
}) })
} }
} }
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{
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{
}) })
} }
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{
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{
} }
} }
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{
}) })
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{
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{
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{
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{
} }
} }
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
} }
} }

View File

@ -1,992 +1,1130 @@
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 = "オンラインセッションを終了する" ja: "アニメ",
this.loading = "ロード中..." en: "Anime",
this.waitingForP2 = "他のプレイヤーを待っている..." cn: "卡通动画音乐",
this.cancel = "キャンセル" tw: "卡通動畫音樂",
this.note = { ko: "애니메이션"
don: "ドン", },
ka: "カッ", "ボーカロイド™曲": {
daiDon: "ドン(大)", ja: "ボーカロイド™曲",
daiKa: "カッ(大)", en: "VOCALOID™ Music"
drumroll: "連打ーっ!!", },
daiDrumroll: "連打(大)ーっ!!", "バラエティ": {
balloon: "ふうせん" ja: "バラエティ",
} en: "Variety",
this.ex_note = { cn: "综合音乐",
don: [ tw: "綜合音樂",
"ド", ko: "버라이어티"
"コ" },
"クラシック": {
ja: "クラシック",
en: "Classical",
cn: "古典音乐",
tw: "古典音樂",
ko: "클래식"
},
"ゲームミュージック": {
ja: "ゲームミュージック",
en: "Game Music",
cn: "游戏音乐",
tw: "遊戲音樂",
ko: "게임"
},
"ナムコオリジナル": {
ja: "ナムコオリジナル",
en: "NAMCO Original",
cn: "NAMCO原创音乐",
tw: "NAMCO原創音樂",
ko: "남코 오리지널"
}
},
selectSong: {
ja: "曲をえらぶ",
en: "Select Song",
cn: "选择乐曲",
tw: "選擇樂曲",
ko: "곡 선택"
},
selectDifficulty: {
ja: "むずかしさをえらぶ",
en: "Select Difficulty",
cn: "选择难度",
tw: "選擇難度",
ko: "난이도 선택"
},
back: {
ja: "もどる",
en: "Back",
cn: "返回",
tw: "返回",
ko: "돌아간다"
},
random: {
ja: "ランダム",
en: "Random",
cn: "随机",
tw: "隨機",
ko: "랜덤"
},
randomSong: {
ja: "ランダムに曲をえらぶ",
en: "Random Song",
cn: "随机选曲",
tw: "隨機選曲",
ko: "랜덤"
},
howToPlay: {
ja: "あそびかた説明",
en: "How to Play",
cn: "操作说明",
tw: "操作說明",
ko: "지도 시간"
},
aboutSimulator: {
ja: "このシミュレータについて",
en: "About Simulator",
cn: "关于模拟器",
tw: "關於模擬器",
ko: "게임 정보"
},
gameSettings: {
ja: "ゲーム設定",
en: "Game Settings",
cn: "游戏设定",
tw: "遊戲設定",
ko: "게임 설정"
},
browse: {
ja: "参照する…",
en: "Browse…",
cn: "浏览…",
tw: "開啟檔案…",
ko: "찾아보기…"
},
defaultSongList: {
ja: "デフォルト曲リスト",
en: "Default Song List",
cn: "默认歌曲列表",
tw: "默認歌曲列表",
ko: "기본 노래 목록"
},
songOptions: {
ja: "演奏オプション",
en: "Song Options",
cn: "选项",
tw: "選項",
ko: "옵션"
},
none: {
ja: "なし",
en: "None",
cn: "无",
tw: "無",
ko: "없음"
},
auto: {
ja: "オート",
en: "Auto",
cn: "自动",
tw: "自動",
ko: "오토"
},
netplay: {
ja: "ネットプレイ",
en: "Netplay",
cn: "网络对战",
tw: "網上對打",
ko: "넷 플레이"
},
easy: {
ja: "かんたん",
en: "Easy",
cn: "简单",
tw: "簡單",
ko: "쉬움"
},
normal: {
ja: "ふつう",
en: "Normal",
cn: "普通",
tw: "普通",
ko: "보통"
},
hard: {
ja: "むずかしい",
en: "Hard",
cn: "困难",
tw: "困難",
ko: "어려움"
},
oni: {
ja: "おに",
en: "Extreme",
cn: "魔王",
tw: "魔王",
ko: "귀신"
},
songBranch: {
ja: "譜面分岐あり",
en: "Diverge Notes",
cn: "有谱面分歧",
tw: "有譜面分歧",
ko: "악보 분기 있습니다"
},
defaultName: {
ja: "どんちゃん",
en: "Don-chan",
cn: "小咚",
tw: "小咚",
ko: "동이"
},
default2PName: {
ja: "かっちゃん",
en: "Katsu-chan",
cn: "小咔",
tw: "小咔",
ko: "딱이"
},
notLoggedIn: {
ja: "ログインしていない",
en: "Not logged in",
cn: "未登录",
tw: "未登錄",
ko: "로그인하지 않았습니다"
},
sessionStart: {
ja: "オンラインセッションを開始する!",
en: "Begin an Online Session!",
cn: "开始在线会话!",
tw: "開始多人模式!",
ko: "온라인 세션 시작!"
},
sessionEnd: {
ja: "オンラインセッションを終了する",
en: "End Online Session",
cn: "结束在线会话",
tw: "結束多人模式",
ko: "온라인 세션 끝내기"
},
scoreSaveFailed: {
ja: null,
en: "Could not connect to the server, your score has not been saved.\n\nPlease log in or refresh the page to try saving the score again."
},
loadSongError: {
ja: null,
en: "Could not load song %s with id %s.\n\n%s"
},
loading: {
ja: "ロード中...",
en: "Loading...",
cn: "加载中...",
tw: "讀取中...",
ko: "로딩 중..."
},
waitingForP2: {
ja: "他のプレイヤーを待っている...",
en: "Waiting for Another Player...",
cn: "正在等待对方玩家...",
tw: "正在等待對方玩家...",
ko: "Waiting for Another Player..."
},
cancel: {
ja: "キャンセル",
en: "Cancel",
cn: "取消",
tw: "取消",
ko: "취소"
},
note: {
don: {
ja: "ドン",
en: "Don",
cn: "咚",
tw: "咚",
ko: "쿵"
},
ka: {
ja: "カッ",
en: "Ka",
cn: "咔",
tw: "咔",
ko: "딱"
},
daiDon: {
ja: "ドン(大)",
en: "DON",
cn: "咚(大)",
tw: "咚(大)",
ko: "쿵(대)"
},
daiKa: {
ja: "カッ(大)",
en: "KA",
cn: "咔(大)",
tw: "咔(大)",
ko: "딱(대)"
},
drumroll: {
ja: "連打ーっ!!",
en: "Drum rollー!!",
cn: "连打ー!!",
tw: "連打ー!!",
ko: "연타ー!!"
},
daiDrumroll: {
ja: "連打(大)ーっ!!",
en: "DRUM ROLLー!!",
cn: "连打(大)ー!!",
tw: "連打(大)ー!!",
ko: "연타(대)ー!!"
},
balloon: {
ja: "ふうせん",
en: "Balloon",
cn: "气球",
tw: "氣球",
ko: "풍선"
},
},
ex_note: {
don: {
ja: ["ド", "コ"],
en: ["Do", "Do"],
cn: ["咚", "咚"],
tw: ["咚", "咚"],
ko: ["쿠", "쿠"]
},
ka: {
ja: ["カ"],
en: ["Ka"],
cn: ["咔"],
tw: ["咔"],
ko: ["딱"]
},
daiDon: {
ja: ["ドン(大)", "ドン(大)"],
en: ["DON", "DON"],
cn: ["咚(大)", "咚(大)"],
tw: ["咚(大)", "咚(大)"],
ko: ["쿵(대)", "쿵(대)"]
},
daiKa: {
ja: ["カッ(大)"],
en: ["KA"],
cn: ["咔(大)"],
tw: ["咔(大)"],
ko: ["딱(대)"]
},
},
combo: {
ja: "コンボ",
en: "Combo",
cn: "连段",
tw: "連段",
ko: "콤보"
},
clear: {
ja: "クリア",
en: "Clear",
cn: "通关",
tw: "通關",
ko: "클리어"
},
good: {
ja: "良",
en: "GOOD",
cn: "良",
tw: "良",
ko: "얼쑤"
},
ok: {
ja: "可",
en: "OK",
cn: "可",
tw: "可",
ko: "좋다"
},
bad: {
ja: "不可",
en: "BAD",
cn: "不可",
tw: "不可",
ko: "에구"
},
branch: {
normal: {
ja: "普通譜面",
en: "Normal",
cn: "一般谱面",
tw: "一般譜面",
ko: "보통 악보"
},
advanced: {
ja: "玄人譜面",
en: "Professional",
cn: "进阶谱面",
tw: "進階譜面",
ko: "현인 악보"
},
master: {
ja: "達人譜面",
en: "Master",
cn: "达人谱面",
tw: "達人譜面",
ko: "달인 악보"
}
},
pauseOptions: {
ja: [
"演奏をつづける",
"はじめからやりなおす",
"「曲をえらぶ」にもどる"
], ],
ka: [ en: [
"カ" "Continue",
"Retry",
"Back to Select Song"
], ],
daiDon: [ cn: [
"ドン(大)", "继续演奏",
"ドン(大)" "从头开始",
"返回「选择乐曲」"
], ],
daiKa: [ tw: [
"カッ(大)" "繼續演奏",
"從頭開始",
"返回「選擇樂曲」"
],
ko: [
"연주 계속하기",
"처음부터 다시",
"「곡 선택」으로"
] ]
} },
this.combo = "コンボ" 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 = "エラーが発生しました。再読み込みしてください。" 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か%sキーを押してジャンルをスキップします", ],
"むずかしさをえらぶしながら%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: "OK" "...and for blue notes, hit the rim! (%s or %s)",
} "USB controllers are also supported!"
this.about = { ],
bugReporting: [ cn: [
"このシミュレータは現在開発中です。", "当流动的音符将与框框重叠时就用鼓棒敲打太鼓吧",
"バグが発生した場合は、報告してください。", "遇到红色音符要敲打鼓面(%s或%s",
"Gitリポジトリかメールでバグを報告してください。" "遇到蓝色音符则敲打鼓边(%s或%s",
], "USB控制器也支持"
diagnosticWarning: "以下の端末診断情報も併せて報告してください!", ],
issueTemplate: "###### 下記の問題を説明してください。 スクリーンショットと診断情報を含めてください。", tw: [
issues: "課題" "當流動的音符將與框框重疊時就用鼓棒敲打太鼓吧",
} "遇到紅色音符要敲打鼓面(%s或%s",
this.session = { "遇到藍色音符則敲打鼓邊(%s或%s",
multiplayerSession: "オンラインセッション", "USB控制器也支持"
linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.", ],
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: "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.",
}
}
},
account: {
username: {
ja: "ユーザー名",
en: "Username",
cn: "登录名",
tw: "使用者名稱",
ko: "사용자 이름"
},
enterUsername: {
ja: "ユーザー名を入力",
en: "Enter Username",
cn: "输入用户名",
tw: "輸入用戶名",
ko: "사용자 이름을 입력하십시오"
},
password: {
ja: "パスワード",
en: "Password",
cn: "密码",
tw: "密碼",
ko: "비밀번호"
},
enterPassword: {
ja: "パスワードを入力",
en: "Enter Password",
cn: "输入密码",
tw: "輸入密碼",
ko: "비밀번호 입력"
},
repeatPassword: {
ja: "パスワードを再入力",
en: "Repeat Password",
cn: "重新输入密码",
tw: "再次輸入密碼",
ko: "비밀번호 재입력"
},
remember: {
ja: "ログイン状態を保持する",
en: "Remember me",
cn: "记住登录",
tw: "記住登錄",
ko: "자동 로그인"
},
login: {
ja: "ログイン",
en: "Log In",
cn: "登录",
tw: "登入",
ko: "로그인"
},
register: {
ja: "登録",
en: "Register",
cn: "注册",
tw: "註冊",
ko: "가입하기"
},
registerAccount: {
ja: "アカウントを登録",
en: "Register account",
cn: "注册帐号",
tw: "註冊帳號",
ko: "계정 등록"
},
passwordsDoNotMatch: {
ja: "パスワードが一致しません",
en: "Passwords do not match",
cn: "密码不匹配",
tw: "密碼不匹配",
ko: "비밀번호가 일치하지 않습니다"
},
newPasswordsDoNotMatch: {
ja: null,
en: "New passwords do not match",
},
cannotBeEmpty: {
ja: "%sは空にできません",
en: "%s cannot be empty",
cn: "%s不能为空",
tw: "%s不能為空",
ko: "%s 비어 있을 수 없습니다"
},
error: {
ja: "リクエストの処理中にエラーが発生しました",
en: "An error occurred while processing your request",
cn: "处理您的请求时发生错误",
tw: "處理您的請求時發生錯誤",
ko: "요청을 처리하는 동안 오류가 발생했습니다"
},
logout: {
ja: "ログアウト",
en: "Log Out",
cn: "登出",
tw: "登出",
ko: "로그 아웃"
},
back: {
ja: "もどる",
en: "Back",
cn: "返回",
tw: "返回",
ko: "돌아간다"
},
cancel: {
ja: null,
en: "Cancel",
},
save: {
ja: null,
en: "Save",
},
displayName: {
en: "Displayed Name",
},
changePassword: {
ja: null,
en: "Change Password",
},
currentNewRepeat: {
ja: null,
en: [
"Current Password",
"New Password",
"Repeat New Password"
],
},
deleteAccount: {
ja: null,
en: "Delete Account",
},
verifyPassword: {
ja: null,
en: "Verify password to delete this account",
}
},
serverError: {
not_logged_in: {
ja: null,
en: "Not logged in",
},
invalid_username: {
ja: null,
en: "Invalid username, a username can only contain letters, numbers, and underscores, and must be between 3 and 20 characters long",
},
username_in_use: {
ja: null,
en: "A user already exists with that username",
},
invalid_password: {
ja: null,
en: "Cannot use this password, please check that your password is at least 6 characters long",
},
invalid_username_password: {
ja: null,
en: "Invalid Username or Password",
},
invalid_display_name: {
ja: null,
en: "Cannot use this name, please check that your new name is at most 25 characters long",
},
current_password_invalid: {
ja: null,
en: "Current password does not match",
},
invalid_new_password: {
ja: null,
en: "Cannot use this password, please check that your new password is at least 6 characters long",
},
verify_password_invalid: {
ja: null,
en: "Verification password does not match",
},
invalid_csrf: {
ja: null,
en: "Security token expired. Please refresh the page."
}
},
browserSupport: {
browserWarning: {
ja: "サポートされていないブラウザを実行しています (%s)",
en: "You are running an unsupported browser (%s)",
},
details: {
ja: "詳しく",
en: "Details...",
},
failedTests: {
ja: "このテストは失敗しました:",
en: "The following tests have failed:",
},
supportedBrowser: {
ja: "%sなどのサポートされているブラウザを使用してください",
en: "Please use a supported browser such as %s",
}
},
creative: {
creative: {
ja: "創作",
en: "Creative",
cn: "创作",
tw: "創作",
ko: "창작"
},
maker: {
ja: "メーカー",
en: "Maker:",
cn: "制作者",
tw: "製作者",
ko: "만드는 사람"
}
},
withLyrics: {
ja: "歌詞あり",
en: "With lyrics",
cn: "带歌词",
tw: "帶歌詞",
ko: "가사가있는"
}
}
var allStrings = {}
function separateStrings(){
for(var j in languageList){
var lang = languageList[j]
allStrings[lang] = {
id: lang
}
var str = allStrings[lang]
var translateObj = function(obj, name, str){
if("en" in obj){
for(var i in obj){
str[name] = obj[lang] || obj.en
}
}else if(obj){
str[name] = {}
for(var i in obj){
translateObj(obj[i], i, str[name])
}
}
}
for(var i in translations){
translateObj(translations[i], i, str)
} }
} }
this.browserSupport = {
browserWarning: "サポートされていないブラウザを実行しています (%s)",
details: "詳しく",
failedTests: "このテストは失敗しました:",
supportedBrowser: "%sなどのサポートされているブラウザを使用してください"
}
this.creative = {
creative: '創作',
maker: 'メーカー'
}
}
function StringsEn(){
this.id = "en"
this.name = "English"
this.regex = /^en$|^en-/
this.font = "TnT, Meiryo, sans-serif"
this.taikoWeb = "Taiko Web"
this.titleProceed = "Click or Press Enter!"
this.titleDisclaimer = "This unofficial simulator is unaffiliated with BANDAI NAMCO."
this.titleCopyright = "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc."
this.categories = {
"J-POP": "Pop",
"アニメ": "Anime",
"ボーカロイド™曲": "VOCALOID™ Music",
"バラエティ": "Variety",
"クラシック": "Classical",
"ゲームミュージック": "Game Music",
"ナムコオリジナル": "NAMCO Original"
}
this.selectSong = "Select Song"
this.selectDifficulty = "Select Difficulty"
this.back = "Back"
this.random = "Random"
this.randomSong = "Random Song"
this.howToPlay = "How to Play"
this.aboutSimulator = "About Simulator"
this.gameSettings = "Game Settings"
this.browse = "Browse…"
this.defaultSongList = "Default Song List"
this.songOptions = "Song Options"
this.none = "None"
this.auto = "Auto"
this.netplay = "Netplay"
this.easy = "Easy"
this.normal = "Normal"
this.hard = "Hard"
this.oni = "Extreme"
this.songBranch = "Diverge Notes"
this.sessionStart = "Begin an Online Session!"
this.sessionEnd = "End Online Session"
this.loading = "Loading..."
this.waitingForP2 = "Waiting for Another Player..."
this.cancel = "Cancel"
this.note = {
don: "Don",
ka: "Ka",
daiDon: "DON",
daiKa: "KA",
drumroll: "Drum rollー!!",
daiDrumroll: "DRUM ROLLー!!",
balloon: "Balloon"
}
this.ex_note = {
don: [
"Do",
"Do"
],
ka: [
"Ka"
],
daiDon: [
"DON",
"DON"
],
daiKa: [
"KA"
]
}
this.combo = "Combo"
this.clear = "Clear"
this.good = "GOOD"
this.ok = "OK"
this.bad = "BAD"
this.branch = {
"normal": "Normal",
"advanced": "Professional",
"master": "Master"
}
this.pauseOptions = [
"Continue",
"Retry",
"Back to Select Song"
]
this.results = "Results"
this.points = "pts"
this.maxCombo = "MAX Combo"
this.drumroll = "Drumroll"
this.errorOccured = "An error occurred, please refresh"
this.tutorial = {
basics: [
"When a note overlaps the frame, that is your cue to hit the drum!",
"For red notes, hit the surface of the drum (%s or %s)...",
"...and for blue notes, hit the rim! (%s or %s)",
"USB controllers are also supported!"
],
otherControls: "Other controls",
otherTutorial: [
"%s \u2014 pause game",
'%s and %s while selecting song \u2014 navigate categories',
"%s while selecting difficulty \u2014 enable autoplay mode",
"%s while selecting difficulty \u2014 enable 2P mode"
],
ok: "OK"
}
this.about = {
bugReporting: [
"This simulator is still in development.",
"Please report any bugs you find.",
"You can report bugs either via our Git repository or email."
],
diagnosticWarning: "Be sure to include the following diagnostic data!",
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.",
issues: "Issues"
}
this.session = {
multiplayerSession: "Multiplayer Session",
linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.",
cancel: "Cancel"
}
this.settings = {
language: {
name: "Language"
},
resolution: {
name: "Game Resolution",
high: "High",
medium: "Medium",
low: "Low",
lowest: "Lowest"
},
touchAnimation: {
name: "Touch Animation"
},
keyboardSettings: {
name: "Keyboard Settings",
ka_l: "Left Rim",
don_l: "Left Surface",
don_r: "Right Surface",
ka_r: "Right Rim"
},
gamepadLayout: {
name: "Gamepad Layout",
a: "Type A",
b: "Type B",
c: "Type C"
},
latency: {
name: "Latency",
value: "Audio: %s, Video: %s",
calibration: "Latency Calibration",
audio: "Audio",
video: "Video",
drumSounds: "Drum Sounds"
},
easierBigNotes: {
name: "Easier Big Notes"
},
on: "On",
off: "Off",
default: "Reset to Defaults",
ok: "OK"
}
this.calibration = {
title: "Latency Calibration",
ms: "%sms",
back: "Back to Settings",
retryPrevious: "Retry Previous",
start: "Start",
finish: "Finish",
audioHelp: {
title: "Audio Latency Calibration",
content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!",
contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!"
},
audioComplete: "Audio Latency Calibration completed!",
videoHelp: {
title: "Video Latency Calibration",
content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!"
},
videoComplete: "Video Latency Calibration completed!",
results: {
title: "Latency Calibration Results",
content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings."
}
}
this.browserSupport = {
browserWarning: "You are running an unsupported browser (%s)",
details: "Details...",
failedTests: "The following tests have failed:",
supportedBrowser: "Please use a supported browser such as %s"
}
this.creative = {
creative: 'Creative',
maker: 'Maker:'
}
}
function StringsCn(){
this.id = "cn"
this.name = "简体中文"
this.regex = /^zh$|^zh-CN$|^zh-SG$/
this.font = "Microsoft YaHei, sans-serif"
this.taikoWeb = "太鼓网页"
this.titleProceed = "点击或按回车!"
this.titleDisclaimer = "这款非官方模拟器与BANDAI NAMCO无关。"
this.titleCopyright = "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc."
this.categories = {
"J-POP": "流行音乐",
"アニメ": "卡通动画音乐",
"ボーカロイド™曲": "VOCALOID™ Music",
"バラエティ": "综合音乐",
"クラシック": "古典音乐",
"ゲームミュージック": "游戏音乐",
"ナムコオリジナル": "NAMCO原创音乐"
}
this.selectSong = "选择乐曲"
this.selectDifficulty = "选择难度"
this.back = "返回"
this.random = "随机"
this.randomSong = "随机选曲"
this.howToPlay = "操作说明"
this.aboutSimulator = "关于模拟器"
this.gameSettings = "游戏设定"
this.browse = "浏览…"
this.defaultSongList = "默认歌曲列表"
this.songOptions = "选项"
this.none = "无"
this.auto = "自动"
this.netplay = "网络对战"
this.easy = "简单"
this.normal = "普通"
this.hard = "困难"
this.oni = "魔王"
this.songBranch = "有谱面分歧"
this.sessionStart = "开始在线会话!"
this.sessionEnd = "结束在线会话"
this.loading = "加载中..."
this.waitingForP2 = "正在等待对方玩家..."
this.cancel = "取消"
this.note = {
don: "咚",
ka: "咔",
daiDon: "咚(大)",
daiKa: "咔(大)",
drumroll: "连打ー!!",
daiDrumroll: "连打(大)ー!!",
balloon: "气球"
}
this.ex_note = {
don: [
"咚",
"咚"
],
ka: [
"咔"
],
daiDon: [
"咚(大)",
"咚(大)"
],
daiKa: [
"咔(大)"
]
}
this.combo = "连段"
this.clear = "通关"
this.good = "良"
this.ok = "可"
this.bad = "不可"
this.branch = {
"normal": "一般谱面",
"advanced": "进阶谱面",
"master": "达人谱面"
}
this.pauseOptions = [
"继续演奏",
"从头开始",
"返回「选择乐曲」"
]
this.results = "发表成绩"
this.points = "点"
this.maxCombo = "最多连段数"
this.drumroll = "连打数"
this.errorOccured = "An error occurred, please refresh"
this.tutorial = {
basics: [
"当流动的音符将与框框重叠时就用鼓棒敲打太鼓吧",
"遇到红色音符要敲打鼓面(%s或%s",
"遇到蓝色音符则敲打鼓边(%s或%s",
"USB控制器也支持"
],
otherControls: "其他控制",
otherTutorial: [
"%s暂停游戏",
'%s and %s while selecting song \u2014 navigate categories',
"选择难度时按住%s以启用自动模式",
"选择难度时按住%s以启用网络对战模式"
],
ok: "确定"
}
this.about = {
bugReporting: [
"This simulator is still in development.",
"Please report any bugs you find.",
"You can report bugs either via our Git repository or email."
],
diagnosticWarning: "Be sure to include the following diagnostic data!",
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.",
issues: "工单"
}
this.session = {
multiplayerSession: "在线会话",
linkTutorial: "复制下方地址,给你的朋友即可开始一起游戏!当他们与您联系之前,请不要离开此页面。",
cancel: "取消"
}
this.settings = {
language: {
name: "语言"
},
resolution: {
name: "游戏分辨率",
high: "高",
medium: "中",
low: "低",
lowest: "最低"
},
touchAnimation: {
name: "触摸动画"
},
keyboardSettings: {
name: "键盘设置",
ka_l: "边缘(左)",
don_l: "表面(左)",
don_r: "表面(右)",
ka_r: "边缘(右)"
},
gamepadLayout: {
name: "操作类型设定",
a: "类型A",
b: "类型B",
c: "类型C"
},
latency: {
name: "Latency",
value: "Audio: %s, Video: %s",
calibration: "Latency Calibration",
audio: "Audio",
video: "Video",
drumSounds: "Drum Sounds"
},
easierBigNotes: {
name: "简单的大音符"
},
on: "开",
off: "关",
default: "重置为默认值",
ok: "确定"
}
this.calibration = {
title: "Latency Calibration",
ms: "%sms",
back: "Back to Settings",
retryPrevious: "Retry Previous",
start: "Start",
finish: "Finish",
audioHelp: {
title: "Audio Latency Calibration",
content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!",
contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!"
},
audioComplete: "Audio Latency Calibration completed!",
videoHelp: {
title: "Video Latency Calibration",
content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!"
},
videoComplete: "Video Latency Calibration completed!",
results: {
title: "Latency Calibration Results",
content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings."
}
}
this.browserSupport = {
browserWarning: "You are running an unsupported browser (%s)",
details: "Details...",
failedTests: "The following tests have failed:",
supportedBrowser: "Please use a supported browser such as %s"
}
this.creative = {
creative: '创作',
maker: '制作者'
}
}
function StringsTw(){
this.id = "tw"
this.name = "正體中文"
this.regex = /^zh-HK$|^zh-TW$/
this.font = "Microsoft YaHei, sans-serif"
this.taikoWeb = "太鼓網頁"
this.titleProceed = "點擊或按確認!"
this.titleDisclaimer = "這款非官方模擬器與BANDAI NAMCO無關。"
this.titleCopyright = "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc."
this.categories = {
"J-POP": "流行音樂",
"アニメ": "卡通動畫音樂",
"ボーカロイド™曲": "VOCALOID™ Music",
"バラエティ": "綜合音樂",
"クラシック": "古典音樂",
"ゲームミュージック": "遊戲音樂",
"ナムコオリジナル": "NAMCO原創音樂"
}
this.selectSong = "選擇樂曲"
this.selectDifficulty = "選擇難度"
this.back = "返回"
this.random = "隨機"
this.randomSong = "隨機選曲"
this.howToPlay = "操作說明"
this.aboutSimulator = "關於模擬器"
this.gameSettings = "遊戲設定"
this.browse = "開啟檔案…"
this.defaultSongList = "默認歌曲列表"
this.songOptions = "選項"
this.none = "無"
this.auto = "自動"
this.netplay = "網上對打"
this.easy = "簡單"
this.normal = "普通"
this.hard = "困難"
this.oni = "魔王"
this.songBranch = "有譜面分歧"
this.sessionStart = "開始多人模式!"
this.sessionEnd = "結束多人模式"
this.loading = "讀取中..."
this.waitingForP2 = "正在等待對方玩家..."
this.cancel = "取消"
this.note = {
don: "咚",
ka: "咔",
daiDon: "咚(大)",
daiKa: "咔(大)",
drumroll: "連打ー!!",
daiDrumroll: "連打(大)ー!!",
balloon: "氣球"
}
this.ex_note = {
don: [
"咚",
"咚"
],
ka: [
"咔"
],
daiDon: [
"咚(大)",
"咚(大)"
],
daiKa: [
"咔(大)"
]
}
this.combo = "連段"
this.clear = "通關"
this.good = "良"
this.ok = "可"
this.bad = "不可"
this.branch = {
"normal": "一般譜面",
"advanced": "進階譜面",
"master": "達人譜面"
}
this.pauseOptions = [
"繼續演奏",
"從頭開始",
"返回「選擇樂曲」"
]
this.results = "發表成績"
this.points = "分"
this.maxCombo = "最多連段數"
this.drumroll = "連打數"
this.errorOccured = "An error occurred, please refresh"
this.tutorial = {
basics: [
"當流動的音符將與框框重疊時就用鼓棒敲打太鼓吧",
"遇到紅色音符要敲打鼓面(%s或%s",
"遇到藍色音符則敲打鼓邊(%s或%s",
"USB控制器也支持"
],
otherControls: "其他控制",
otherTutorial: [
"%s暫停遊戲",
'%s and %s while selecting song \u2014 navigate categories',
"選擇難度時按住%s以啟用自動模式",
"選擇難度時按住%s以啟用網上對打模式"
],
ok: "確定"
}
this.about = {
bugReporting: [
"This simulator is still in development.",
"Please report any bugs you find.",
"You can report bugs either via our Git repository or email."
],
diagnosticWarning: "Be sure to include the following diagnostic data!",
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.",
issues: "問題"
}
this.session = {
multiplayerSession: "多人模式",
linkTutorial: "複製下方地址,給你的朋友即可開始一起遊戲!當他們與您聯繫之前,請不要離開此頁面。",
cancel: "取消"
}
this.settings = {
language: {
name: "語系"
},
resolution: {
name: "遊戲分辨率",
high: "高",
medium: "中",
low: "低",
lowest: "最低"
},
touchAnimation: {
name: "觸摸動畫"
},
keyboardSettings: {
name: "鍵盤設置",
ka_l: "邊緣(左)",
don_l: "表面(左)",
don_r: "表面(右)",
ka_r: "邊緣(右)"
},
gamepadLayout: {
name: "操作類型設定",
a: "類型A",
b: "類型B",
c: "類型C"
},
latency: {
name: "Latency",
value: "Audio: %s, Video: %s",
calibration: "Latency Calibration",
audio: "Audio",
video: "Video",
drumSounds: "Drum Sounds"
},
easierBigNotes: {
name: "簡單的大音符"
},
on: "開",
off: "關",
default: "重置為默認值",
ok: "確定"
}
this.calibration = {
title: "Latency Calibration",
ms: "%sms",
back: "Back to Settings",
retryPrevious: "Retry Previous",
start: "Start",
finish: "Finish",
audioHelp: {
title: "Audio Latency Calibration",
content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!",
contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!"
},
audioComplete: "Audio Latency Calibration completed!",
videoHelp: {
title: "Video Latency Calibration",
content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!"
},
videoComplete: "Video Latency Calibration completed!",
results: {
title: "Latency Calibration Results",
content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings."
}
}
this.browserSupport = {
browserWarning: "You are running an unsupported browser (%s)",
details: "Details...",
failedTests: "The following tests have failed:",
supportedBrowser: "Please use a supported browser such as %s"
}
this.creative = {
creative: '創作',
maker: '製作者'
}
}
function StringsKo(){
this.id = "ko"
this.name = "한국어"
this.regex = /^ko$|^ko-/
this.font = "Microsoft YaHei, sans-serif"
this.taikoWeb = "태고 웹"
this.titleProceed = "클릭하거나 Enter를 누릅니다!"
this.titleDisclaimer = "이 비공식 시뮬레이터는 반다이 남코와 관련이 없습니다."
this.titleCopyright = "Taiko no Tatsujin ©&™ 2011 BANDAI NAMCO Entertainment Inc."
this.categories = {
"J-POP": "POP",
"アニメ": "애니메이션",
"ボーカロイド™曲": "VOCALOID™ Music",
"バラエティ": "버라이어티",
"クラシック": "클래식",
"ゲームミュージック": "게임",
"ナムコオリジナル": "남코 오리지널"
}
this.selectSong = "곡 선택"
this.selectDifficulty = "난이도 선택"
this.back = "돌아간다"
this.random = "랜덤"
this.randomSong = "랜덤"
this.howToPlay = "지도 시간"
this.aboutSimulator = "게임 정보"
this.gameSettings = "게임 설정"
this.browse = "찾아보기…"
this.defaultSongList = "기본 노래 목록"
this.songOptions = "옵션"
this.none = "없음"
this.auto = "오토"
this.netplay = "넷 플레이"
this.easy = "쉬움"
this.normal = "보통"
this.hard = "어려움"
this.oni = "귀신"
this.songBranch = "악보 분기 있습니다"
this.sessionStart = "온라인 세션 시작!"
this.sessionEnd = "온라인 세션 끝내기"
this.loading = "로딩 중..."
this.waitingForP2 = "Waiting for Another Player..."
this.cancel = "취소"
this.note = {
don: "쿵",
ka: "딱",
daiDon: "쿵(대)",
daiKa: "딱(대)",
drumroll: "연타ー!!",
daiDrumroll: "연타(대)ー!!",
balloon: "풍선"
}
this.ex_note = {
don: [
"쿠",
"쿠"
],
ka: [
"딱"
],
daiDon: [
"쿵(대)",
"쿵(대)"
],
daiKa: [
"딱(대)"
]
}
this.combo = "콤보"
this.clear = "클리어"
this.good = "얼쑤"
this.ok = "좋다"
this.bad = "에구"
this.branch = {
"normal": "보통 악보",
"advanced": "현인 악보",
"master": "달인 악보"
}
this.pauseOptions = [
"연주 계속하기",
"처음부터 다시",
"「곡 선택」으로"
]
this.results = "성적 발표"
this.points = "점"
this.maxCombo = "최대 콤보 수"
this.drumroll = "연타 횟수"
this.errorOccured = "An error occurred, please refresh"
this.tutorial = {
basics: [
"이동하는 음표가 테두리와 겹쳐졌을 때 북채로 태고를 두드리자!",
"빨간 음표는 면을 두드리자 (%s 또는 %s)",
"파란 음표는 테를 두드리자 (%s 또는 %s)",
"USB 컨트롤러도 지원됩니다!"
],
otherControls: "기타 컨트롤",
otherTutorial: [
"%s \u2014 게임을 일시 중지합니다",
'%s and %s while selecting song \u2014 navigate categories',
"난이도 선택 동안 %s 홀드 \u2014 오토 모드 활성화",
"난이도 선택 동안 %s 홀드 \u2014 넷 플레이 모드 활성화"
],
ok: "확인"
}
this.about = {
bugReporting: [
"This simulator is still in development.",
"Please report any bugs you find.",
"You can report bugs either via our Git repository or email."
],
diagnosticWarning: "Be sure to include the following diagnostic data!",
issueTemplate: "###### Describe the problem you are having below. Please include a screenshot and the diagnostic information.",
issues: "이슈"
}
this.session = {
multiplayerSession: "Multiplayer Session",
linkTutorial: "Share this link with your friend to start playing together! Do not leave this screen while they join.",
cancel: "취소"
}
this.settings = {
language: {
name: "언어"
},
resolution: {
name: "게임 해상도",
high: "높은",
medium: "중간",
low: "저",
lowest: "최저"
},
touchAnimation: {
name: "터치 애니메이션"
},
keyboardSettings: {
name: "키보드 설정",
ka_l: "가장자리 (왼쪽)",
don_l: "표면 (왼쪽)",
don_r: "표면 (오른쪽)",
ka_r: "가장자리 (오른쪽)"
},
gamepadLayout: {
name: "조작 타입 설정",
a: "타입 A",
b: "타입 B",
c: "타입 C"
},
latency: {
name: "Latency",
value: "Audio: %s, Video: %s",
calibration: "Latency Calibration",
audio: "Audio",
video: "Video",
drumSounds: "Drum Sounds"
},
easierBigNotes: {
name: "쉬운 큰 음표"
},
on: "온",
off: "오프",
default: "기본값으로 재설정",
ok: "확인"
}
this.calibration = {
title: "Latency Calibration",
ms: "%sms",
back: "Back to Settings",
retryPrevious: "Retry Previous",
start: "Start",
finish: "Finish",
audioHelp: {
title: "Audio Latency Calibration",
content: "Listen to a sound playing in the background.\n\nHit the surface of the drum (%s or %s) as you hear it!",
contentAlt: "Listen to a sound playing in the background.\n\nHit the surface of the drum as you hear it!"
},
audioComplete: "Audio Latency Calibration completed!",
videoHelp: {
title: "Video Latency Calibration",
content: "This time there will be no sounds.\n\nInstead, watch for notes blinking on the circle-shaped frame, hit the drum as they appear!"
},
videoComplete: "Video Latency Calibration completed!",
results: {
title: "Latency Calibration Results",
content: "Audio latency: %s\nVideo latency: %s\n\nYou can configure these latency values in the settings."
}
}
this.browserSupport = {
browserWarning: "You are running an unsupported browser (%s)",
details: "Details...",
failedTests: "The following tests have failed:",
supportedBrowser: "Please use a supported browser such as %s"
}
this.creative = {
creative: '창작',
maker: '만드는 사람'
}
}
var allStrings = {
"ja": new StringsJa(),
"en": new StringsEn(),
"cn": new StringsCn(),
"tw": new StringsTw(),
"ko": new StringsKo()
} }
separateStrings()

View File

@ -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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
} }
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 @@
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 @@
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 @@
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 @@
// 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 @@
}, { }, {
// 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 @@
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 @@
// 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
} }
} }
} }

View File

@ -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
} }

View File

@ -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>

View File

@ -0,0 +1,34 @@
<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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,25 @@
<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>

73
schema.py Normal file
View File

@ -0,0 +1,73 @@
import jsonschema
def validate(data, schema):
try:
jsonschema.validate(data, schema)
return True
except jsonschema.exceptions.ValidationError:
return False
register = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'username': {'type': 'string'},
'password': {'type': 'string'}
}
}
login = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'username': {'type': 'string'},
'password': {'type': 'string'},
'remember': {'type': 'boolean'}
}
}
update_display_name = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'display_name': {'type': 'string'}
}
}
update_password = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'current_password': {'type': 'string'},
'new_password': {'type': 'string'}
}
}
delete_account = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'password': {'type': 'string'}
}
}
scores_save = {
'$schema': 'http://json-schema.org/schema#',
'type': 'object',
'properties': {
'scores': {
'type': 'array',
'items': {'$ref': '#/definitions/score'}
},
'is_import': {'type': 'boolean'}
},
'definitions': {
'score': {
'type': 'object',
'properties': {
'hash': {'type': 'string'},
'score': {'type': 'string'}
}
}
}
}

View File

@ -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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
"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

23
templates/admin.html Normal file
View File

@ -0,0 +1,23 @@
<!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>

View File

@ -0,0 +1,134 @@
{% 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 %}

View File

@ -0,0 +1,122 @@
{% 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 %}

View File

@ -0,0 +1,21 @@
{% 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 %}

View File

@ -1,4 +1,4 @@
@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

View File

@ -1 +1,3 @@
git log -1 --pretty="format:{\"commit\": \"%H\", \"commit_short\": \"%h\", \"version\": \"%ad\", \"url\": \"https://github.com/bui/taiko-web/\"}" --date="format:%y.%m.%d" > ../version.json #!/bin/bash
toplevel=$( git rev-parse --show-toplevel )
git log -1 --pretty="format:{\"commit\": \"%H\", \"commit_short\": \"%h\", \"version\": \"%ad\"}" --date="format:%y.%m.%d" > "$toplevel/version.json"

View File

@ -0,0 +1,2 @@
#!/bin/bash
./tools/get_version.sh

2
tools/hooks/post-commit Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
./tools/get_version.sh

2
tools/hooks/post-merge Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
./tools/get_version.sh

2
tools/hooks/post-rewrite Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
./tools/get_version.sh

114
tools/migrate_db.py Normal file
View File

@ -0,0 +1,114 @@
#!/usr/bin/env python3
# Migrate old SQLite taiko.db to MongoDB
import sqlite3
from pymongo import MongoClient
import os,sys,inspect
current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
parent_dir = os.path.dirname(current_dir)
sys.path.insert(0, parent_dir)
import config
client = MongoClient(config.MONGO['host'])
client.drop_database(config.MONGO['database'])
db = client[config.MONGO['database']]
sqdb = sqlite3.connect('taiko.db')
sqdb.row_factory = sqlite3.Row
curs = sqdb.cursor()
def migrate_songs():
curs.execute('select * from songs order by id')
rows = curs.fetchall()
for row in rows:
song = {
'id': row['id'],
'title': row['title'],
'title_lang': {'ja': row['title'], 'en': None, 'cn': None, 'tw': None, 'ko': None},
'subtitle': row['subtitle'],
'subtitle_lang': {'ja': row['subtitle'], 'en': None, 'cn': None, 'tw': None, 'ko': None},
'courses': {'easy': None, 'normal': None, 'hard': None, 'oni': None, 'ura': None},
'enabled': True if row['enabled'] else False,
'category_id': row['category'],
'type': row['type'],
'offset': row['offset'] or 0,
'skin_id': row['skin_id'],
'preview': row['preview'] or 0,
'volume': row['volume'] or 1.0,
'maker_id': row['maker_id'],
'hash': row['hash'],
'order': row['id']
}
for diff in ['easy', 'normal', 'hard', 'oni', 'ura']:
if row[diff]:
spl = row[diff].split(' ')
branch = False
if len(spl) > 1 and spl[1] == 'B':
branch = True
song['courses'][diff] = {'stars': int(spl[0]), 'branch': branch}
if row['title_lang']:
langs = row['title_lang'].splitlines()
for lang in langs:
spl = lang.split(' ', 1)
if spl[0] in ['ja', 'en', 'cn', 'tw', 'ko']:
song['title_lang'][spl[0]] = spl[1]
else:
song['title_lang']['en'] = lang
if row['subtitle_lang']:
langs = row['subtitle_lang'].splitlines()
for lang in langs:
spl = lang.split(' ', 1)
if spl[0] in ['ja', 'en', 'cn', 'tw', 'ko']:
song['subtitle_lang'][spl[0]] = spl[1]
else:
song['subtitle_lang']['en'] = lang
db.songs.insert_one(song)
last_song = song['id']
db.seq.insert_one({'name': 'songs', 'value': last_song})
def migrate_makers():
curs.execute('select * from makers')
rows = curs.fetchall()
for row in rows:
db.makers.insert_one({
'id': row['maker_id'],
'name': row['name'],
'url': row['url']
})
def migrate_categories():
curs.execute('select * from categories')
rows = curs.fetchall()
for row in rows:
db.categories.insert_one({
'id': row['id'],
'title': row['title']
})
def migrate_song_skins():
curs.execute('select * from song_skins')
rows = curs.fetchall()
for row in rows:
db.song_skins.insert_one({
'id': row['id'],
'name': row['name'],
'song': row['song'],
'stage': row['stage'],
'don': row['don']
})
if __name__ == '__main__':
migrate_songs()
migrate_makers()
migrate_categories()
migrate_song_skins()