mirror of
https://github.com/jiojciojsioe3/a3cjroijsiojiorj.git
synced 2024-12-22 17:26:13 +08:00
Merge branch 'add-lyrics'
This commit is contained in:
commit
d910de6bc7
3
.gitignore
vendored
3
.gitignore
vendored
@ -36,6 +36,7 @@ $RECYCLE.BIN/
|
||||
.Trashes
|
||||
|
||||
.vscode
|
||||
*.pyc
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
@ -48,5 +49,5 @@ public/api
|
||||
taiko.db
|
||||
version.json
|
||||
public/index.html
|
||||
config.json
|
||||
config.py
|
||||
public/assets/song_skins
|
||||
|
548
app.py
548
app.py
@ -1,63 +1,128 @@
|
||||
#!/usr/bin/env python2
|
||||
|
||||
from __future__ import division
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import base64
|
||||
import bcrypt
|
||||
import hashlib
|
||||
import config
|
||||
import json
|
||||
import sqlite3
|
||||
import re
|
||||
import requests
|
||||
import schema
|
||||
import os
|
||||
from flask import Flask, g, jsonify, render_template, request, abort, redirect
|
||||
|
||||
from functools import wraps
|
||||
from flask import Flask, g, jsonify, render_template, request, abort, redirect, session, flash
|
||||
from flask_caching import Cache
|
||||
from flask_session import Session
|
||||
from flask_wtf.csrf import CSRFProtect, generate_csrf, CSRFError
|
||||
from ffmpy import FFmpeg
|
||||
from pymongo import MongoClient
|
||||
|
||||
app = Flask(__name__)
|
||||
try:
|
||||
app.cache = Cache(app, config={'CACHE_TYPE': 'redis'})
|
||||
except RuntimeError:
|
||||
import tempfile
|
||||
app.cache = Cache(app, config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': tempfile.gettempdir()})
|
||||
client = MongoClient(host=config.MONGO['host'])
|
||||
|
||||
DATABASE = 'taiko.db'
|
||||
DEFAULT_URL = 'https://github.com/bui/taiko-web/'
|
||||
app.secret_key = config.SECRET_KEY
|
||||
app.config['SESSION_TYPE'] = 'redis'
|
||||
app.cache = Cache(app, config=config.REDIS)
|
||||
sess = Session()
|
||||
sess.init_app(app)
|
||||
csrf = CSRFProtect(app)
|
||||
|
||||
db = client[config.MONGO['database']]
|
||||
db.users.create_index('username', unique=True)
|
||||
db.songs.create_index('id', unique=True)
|
||||
|
||||
|
||||
def get_db():
|
||||
db = getattr(g, '_database', None)
|
||||
if db is None:
|
||||
db = g._database = sqlite3.connect(DATABASE)
|
||||
db.row_factory = sqlite3.Row
|
||||
return db
|
||||
class HashException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def query_db(query, args=(), one=False):
|
||||
cur = get_db().execute(query, args)
|
||||
rv = cur.fetchall()
|
||||
cur.close()
|
||||
return (rv[0] if rv else None) if one else rv
|
||||
def api_error(message):
|
||||
return jsonify({'status': 'error', 'message': message})
|
||||
|
||||
|
||||
def generate_hash(id, form):
|
||||
md5 = hashlib.md5()
|
||||
if form['type'] == 'tja':
|
||||
urls = ['%s%s/main.tja' % (config.SONGS_BASEURL, id)]
|
||||
else:
|
||||
urls = []
|
||||
for diff in ['easy', 'normal', 'hard', 'oni', 'ura']:
|
||||
if form['course_' + diff]:
|
||||
urls.append('%s%s/%s.osu' % (config.SONGS_BASEURL, id, diff))
|
||||
|
||||
for url in urls:
|
||||
if url.startswith("http://") or url.startswith("https://"):
|
||||
resp = requests.get(url)
|
||||
if resp.status_code != 200:
|
||||
raise HashException('Invalid response from %s (status code %s)' % (resp.url, resp.status_code))
|
||||
md5.update(resp.content)
|
||||
else:
|
||||
if url.startswith("/"):
|
||||
url = url[1:]
|
||||
with open(os.path.join("public", url), "rb") as file:
|
||||
md5.update(file.read())
|
||||
|
||||
return base64.b64encode(md5.digest())[:-2].decode('utf-8')
|
||||
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not session.get('username'):
|
||||
return api_error('not_logged_in')
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
def admin_required(level):
|
||||
def decorated_function(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not session.get('username'):
|
||||
return abort(403)
|
||||
|
||||
user = db.users.find_one({'username': session.get('username')})
|
||||
if user['user_level'] < level:
|
||||
return abort(403)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorated_function
|
||||
|
||||
|
||||
@app.errorhandler(CSRFError)
|
||||
def handle_csrf_error(e):
|
||||
return api_error('invalid_csrf')
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request_func():
|
||||
if session.get('session_id'):
|
||||
if not db.users.find_one({'session_id': session.get('session_id')}):
|
||||
session.clear()
|
||||
|
||||
|
||||
def get_config():
|
||||
if os.path.isfile('config.json'):
|
||||
try:
|
||||
config = json.load(open('config.json', 'r'))
|
||||
except ValueError:
|
||||
print('WARNING: Invalid config.json, using default values')
|
||||
config = {}
|
||||
else:
|
||||
print('WARNING: No config.json found, using default values')
|
||||
config = {}
|
||||
config_out = {
|
||||
'songs_baseurl': config.SONGS_BASEURL,
|
||||
'assets_baseurl': config.ASSETS_BASEURL,
|
||||
'email': config.EMAIL,
|
||||
'accounts': config.ACCOUNTS,
|
||||
'custom_js': config.CUSTOM_JS
|
||||
}
|
||||
|
||||
if not config.get('songs_baseurl'):
|
||||
config['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/'
|
||||
if not config.get('assets_baseurl'):
|
||||
config['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/'
|
||||
if not config_out.get('songs_baseurl'):
|
||||
config_out['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/'
|
||||
if not config_out.get('assets_baseurl'):
|
||||
config_out['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/'
|
||||
|
||||
config['_version'] = get_version()
|
||||
return config
|
||||
config_out['_version'] = get_version()
|
||||
return config_out
|
||||
|
||||
|
||||
def get_version():
|
||||
version = {'commit': None, 'commit_short': '', 'version': None, 'url': DEFAULT_URL}
|
||||
version = {'commit': None, 'commit_short': '', 'version': None, 'url': config.URL}
|
||||
if os.path.isfile('version.json'):
|
||||
try:
|
||||
ver = json.load(open('version.json', 'r'))
|
||||
@ -72,20 +137,158 @@ def get_version():
|
||||
return version
|
||||
|
||||
|
||||
@app.teardown_appcontext
|
||||
def close_connection(exception):
|
||||
db = getattr(g, '_database', None)
|
||||
if db is not None:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@app.cache.cached(timeout=15)
|
||||
def route_index():
|
||||
version = get_version()
|
||||
return render_template('index.html', version=version, config=get_config())
|
||||
|
||||
|
||||
@app.route('/api/csrftoken')
|
||||
def route_csrftoken():
|
||||
return jsonify({'status': 'ok', 'token': generate_csrf()})
|
||||
|
||||
|
||||
@app.route('/admin')
|
||||
@admin_required(level=50)
|
||||
def route_admin():
|
||||
return redirect('/admin/songs')
|
||||
|
||||
|
||||
@app.route('/admin/songs')
|
||||
@admin_required(level=50)
|
||||
def route_admin_songs():
|
||||
songs = db.songs.find({})
|
||||
user = db.users.find_one({'username': session['username']})
|
||||
return render_template('admin_songs.html', songs=list(songs), admin=user)
|
||||
|
||||
|
||||
@app.route('/admin/songs/<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.cache.cached(timeout=15, query_string=True)
|
||||
def route_api_preview():
|
||||
@ -93,12 +296,12 @@ def route_api_preview():
|
||||
if not song_id or not re.match('^[0-9]+$', song_id):
|
||||
abort(400)
|
||||
|
||||
song_row = query_db('select * from songs where id = ? and enabled = 1', (song_id,))
|
||||
if not song_row:
|
||||
song = db.songs.find_one({'id': song_id})
|
||||
if not song:
|
||||
abort(400)
|
||||
|
||||
song_type = song_row[0]['type']
|
||||
prev_path = make_preview(song_id, song_type, song_row[0]['preview'])
|
||||
song_type = song['type']
|
||||
prev_path = make_preview(song_id, song_type, song['preview'])
|
||||
if not prev_path:
|
||||
return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id)
|
||||
|
||||
@ -108,52 +311,30 @@ def route_api_preview():
|
||||
@app.route('/api/songs')
|
||||
@app.cache.cached(timeout=15)
|
||||
def route_api_songs():
|
||||
songs = query_db('select s.*, m.name, m.url from songs s left join makers m on s.maker_id = m.maker_id where enabled = 1')
|
||||
|
||||
raw_categories = query_db('select * from categories')
|
||||
categories = {}
|
||||
for cat in raw_categories:
|
||||
categories[cat['id']] = cat['title']
|
||||
|
||||
raw_song_skins = query_db('select * from song_skins')
|
||||
song_skins = {}
|
||||
for skin in raw_song_skins:
|
||||
song_skins[skin[0]] = {'name': skin['name'], 'song': skin['song'], 'stage': skin['stage'], 'don': skin['don']}
|
||||
|
||||
songs_out = []
|
||||
songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False}))
|
||||
for song in songs:
|
||||
song_id = song['id']
|
||||
song_type = song['type']
|
||||
preview = song['preview']
|
||||
|
||||
category_out = categories[song['category']] if song['category'] in categories else ''
|
||||
song_skin_out = song_skins[song['skin_id']] if song['skin_id'] in song_skins else None
|
||||
maker = None
|
||||
if song['maker_id'] == 0:
|
||||
maker = 0
|
||||
elif song['maker_id'] and song['maker_id'] > 0:
|
||||
maker = {'name': song['name'], 'url': song['url'], 'id': song['maker_id']}
|
||||
|
||||
songs_out.append({
|
||||
'id': song_id,
|
||||
'title': song['title'],
|
||||
'title_lang': song['title_lang'],
|
||||
'subtitle': song['subtitle'],
|
||||
'subtitle_lang': song['subtitle_lang'],
|
||||
'stars': [
|
||||
song['easy'], song['normal'], song['hard'], song['oni'], song['ura']
|
||||
],
|
||||
'preview': preview,
|
||||
'category': category_out,
|
||||
'type': song_type,
|
||||
'offset': song['offset'],
|
||||
'song_skin': song_skin_out,
|
||||
'volume': song['volume'],
|
||||
'maker': maker,
|
||||
'hash': song['hash']
|
||||
})
|
||||
if song['maker_id']:
|
||||
if song['maker_id'] == 0:
|
||||
song['maker'] = 0
|
||||
else:
|
||||
song['maker'] = db.makers.find_one({'id': song['maker_id']}, {'_id': False})
|
||||
else:
|
||||
song['maker'] = None
|
||||
del song['maker_id']
|
||||
|
||||
return jsonify(songs_out)
|
||||
if song['category_id']:
|
||||
song['category'] = db.categories.find_one({'id': song['category_id']})['title']
|
||||
else:
|
||||
song['category'] = None
|
||||
del song['category_id']
|
||||
|
||||
if song['skin_id']:
|
||||
song['song_skin'] = db.song_skins.find_one({'id': song['skin_id']}, {'_id': False, 'id': False})
|
||||
else:
|
||||
song['song_skin'] = None
|
||||
del song['skin_id']
|
||||
|
||||
return jsonify(songs)
|
||||
|
||||
|
||||
@app.route('/api/config')
|
||||
@ -163,6 +344,183 @@ def route_api_config():
|
||||
return jsonify(config)
|
||||
|
||||
|
||||
@app.route('/api/register', methods=['POST'])
|
||||
def route_api_register():
|
||||
data = request.get_json()
|
||||
if not schema.validate(data, schema.register):
|
||||
return abort(400)
|
||||
|
||||
if session.get('username'):
|
||||
session.clear()
|
||||
|
||||
username = data.get('username', '')
|
||||
if len(username) < 3 or len(username) > 20 or not re.match('^[a-zA-Z0-9_]{3,20}$', username):
|
||||
return api_error('invalid_username')
|
||||
|
||||
if db.users.find_one({'username_lower': username.lower()}):
|
||||
return api_error('username_in_use')
|
||||
|
||||
password = data.get('password', '').encode('utf-8')
|
||||
if not 6 <= len(password) <= 5000:
|
||||
return api_error('invalid_password')
|
||||
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(password, salt)
|
||||
|
||||
session_id = os.urandom(24).hex()
|
||||
db.users.insert_one({
|
||||
'username': username,
|
||||
'username_lower': username.lower(),
|
||||
'password': hashed,
|
||||
'display_name': username,
|
||||
'user_level': 1,
|
||||
'session_id': session_id
|
||||
})
|
||||
|
||||
session['session_id'] = session_id
|
||||
session['username'] = username
|
||||
session.permanent = True
|
||||
return jsonify({'status': 'ok', 'username': username, 'display_name': username})
|
||||
|
||||
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
def route_api_login():
|
||||
data = request.get_json()
|
||||
if not schema.validate(data, schema.login):
|
||||
return abort(400)
|
||||
|
||||
if session.get('username'):
|
||||
session.clear()
|
||||
|
||||
username = data.get('username', '')
|
||||
result = db.users.find_one({'username_lower': username.lower()})
|
||||
if not result:
|
||||
return api_error('invalid_username_password')
|
||||
|
||||
password = data.get('password', '').encode('utf-8')
|
||||
if not bcrypt.checkpw(password, result['password']):
|
||||
return api_error('invalid_username_password')
|
||||
|
||||
session['session_id'] = result['session_id']
|
||||
session['username'] = result['username']
|
||||
session.permanent = True if data.get('remember') else False
|
||||
|
||||
return jsonify({'status': 'ok', 'username': result['username'], 'display_name': result['display_name']})
|
||||
|
||||
|
||||
@app.route('/api/logout', methods=['POST'])
|
||||
@login_required
|
||||
def route_api_logout():
|
||||
session.clear()
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
|
||||
@app.route('/api/account/display_name', methods=['POST'])
|
||||
@login_required
|
||||
def route_api_account_display_name():
|
||||
data = request.get_json()
|
||||
if not schema.validate(data, schema.update_display_name):
|
||||
return abort(400)
|
||||
|
||||
display_name = data.get('display_name', '').strip()
|
||||
if not display_name:
|
||||
display_name = session.get('username')
|
||||
elif len(display_name) > 25:
|
||||
return api_error('invalid_display_name')
|
||||
|
||||
db.users.update_one({'username': session.get('username')}, {
|
||||
'$set': {'display_name': display_name}
|
||||
})
|
||||
|
||||
return jsonify({'status': 'ok', 'display_name': display_name})
|
||||
|
||||
|
||||
@app.route('/api/account/password', methods=['POST'])
|
||||
@login_required
|
||||
def route_api_account_password():
|
||||
data = request.get_json()
|
||||
if not schema.validate(data, schema.update_password):
|
||||
return abort(400)
|
||||
|
||||
user = db.users.find_one({'username': session.get('username')})
|
||||
current_password = data.get('current_password', '').encode('utf-8')
|
||||
if not bcrypt.checkpw(current_password, user['password']):
|
||||
return api_error('current_password_invalid')
|
||||
|
||||
new_password = data.get('new_password', '').encode('utf-8')
|
||||
if not 6 <= len(new_password) <= 5000:
|
||||
return api_error('invalid_new_password')
|
||||
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(new_password, salt)
|
||||
session_id = os.urandom(24).hex()
|
||||
|
||||
db.users.update_one({'username': session.get('username')}, {
|
||||
'$set': {'password': hashed, 'session_id': session_id}
|
||||
})
|
||||
|
||||
session['session_id'] = session_id
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
|
||||
@app.route('/api/account/remove', methods=['POST'])
|
||||
@login_required
|
||||
def route_api_account_remove():
|
||||
data = request.get_json()
|
||||
if not schema.validate(data, schema.delete_account):
|
||||
return abort(400)
|
||||
|
||||
user = db.users.find_one({'username': session.get('username')})
|
||||
password = data.get('password', '').encode('utf-8')
|
||||
if not bcrypt.checkpw(password, user['password']):
|
||||
return api_error('verify_password_invalid')
|
||||
|
||||
db.scores.delete_many({'username': session.get('username')})
|
||||
db.users.delete_one({'username': session.get('username')})
|
||||
|
||||
session.clear()
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
|
||||
@app.route('/api/scores/save', methods=['POST'])
|
||||
@login_required
|
||||
def route_api_scores_save():
|
||||
data = request.get_json()
|
||||
if not schema.validate(data, schema.scores_save):
|
||||
return abort(400)
|
||||
|
||||
username = session.get('username')
|
||||
if data.get('is_import'):
|
||||
db.scores.delete_many({'username': username})
|
||||
|
||||
scores = data.get('scores', [])
|
||||
for score in scores:
|
||||
db.scores.update_one({'username': username, 'hash': score['hash']},
|
||||
{'$set': {
|
||||
'username': username,
|
||||
'hash': score['hash'],
|
||||
'score': score['score']
|
||||
}}, upsert=True)
|
||||
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
|
||||
@app.route('/api/scores/get')
|
||||
@login_required
|
||||
def route_api_scores_get():
|
||||
username = session.get('username')
|
||||
|
||||
scores = []
|
||||
for score in db.scores.find({'username': username}):
|
||||
scores.append({
|
||||
'hash': score['hash'],
|
||||
'score': score['score']
|
||||
})
|
||||
|
||||
user = db.users.find_one({'username': username})
|
||||
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name']})
|
||||
|
||||
|
||||
def make_preview(song_id, song_type, preview):
|
||||
song_path = 'public/songs/%s/main.mp3' % song_id
|
||||
prev_path = 'public/songs/%s/preview.mp3' % song_id
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"songs_baseurl": "",
|
||||
"assets_baseurl": ""
|
||||
}
|
35
config.example.py
Normal file
35
config.example.py
Normal 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
156
public/src/css/admin.css
Normal 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;
|
||||
}
|
@ -123,6 +123,7 @@
|
||||
}
|
||||
|
||||
#debug .autoplay-label,
|
||||
#debug .branch-hide{
|
||||
#debug .branch-hide,
|
||||
#debug .lyrics-hide{
|
||||
display: none;
|
||||
}
|
||||
|
@ -89,3 +89,39 @@
|
||||
.fix-animations *{
|
||||
animation: none !important;
|
||||
}
|
||||
#song-lyrics{
|
||||
position: absolute;
|
||||
right: calc((100vw - 1280 / 720 * 100vh) / 2 + 100px * var(--scale));
|
||||
bottom: calc(44 / 720 * 100vh - 30px * var(--scale));
|
||||
left: calc((100vw - 1280 / 720 * 100vh) / 2 + 100px * var(--scale));
|
||||
text-align: center;
|
||||
font-family: Meiryo, sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: calc(45px * var(--scale));
|
||||
line-height: 1.2;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
#game.portrait #song-lyrics{
|
||||
right: calc(20px * var(--scale));
|
||||
left: calc(20px * var(--scale));
|
||||
}
|
||||
#song-lyrics .stroke,
|
||||
#song-lyrics .fill{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
#song-lyrics .stroke{
|
||||
-webkit-text-stroke: calc(7px * var(--scale)) #00a;
|
||||
}
|
||||
#song-lyrics .fill{
|
||||
color: #fff;
|
||||
}
|
||||
#song-lyrics ruby{
|
||||
display: inline-flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
#song-lyrics rt{
|
||||
line-height: 1;
|
||||
}
|
||||
|
@ -117,3 +117,20 @@ body{
|
||||
color: #777;
|
||||
text-shadow: 0.05em 0.05em #fff;
|
||||
}
|
||||
.view-outer.loader-error-div,
|
||||
.loader-error-div .diag-txt{
|
||||
display: none
|
||||
}
|
||||
.loader-error-div{
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.loader-error-div .debug-link{
|
||||
color: #00f;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
}
|
||||
.loader-error-div .diag-txt textarea,
|
||||
.loader-error-div .diag-txt iframe{
|
||||
height: 10em;
|
||||
}
|
||||
|
@ -108,8 +108,8 @@ kbd{
|
||||
.left-buttons .taibtn{
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
#diag-txt textarea,
|
||||
#diag-txt iframe{
|
||||
.diag-txt textarea,
|
||||
.diag-txt iframe{
|
||||
width: 100%;
|
||||
height: 5em;
|
||||
font-size: inherit;
|
||||
@ -119,6 +119,7 @@ kbd{
|
||||
background: #fff;
|
||||
border: 1px solid #a9a9a9;
|
||||
user-select: all;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.text-warn{
|
||||
color: #d00;
|
||||
@ -291,3 +292,88 @@ kbd{
|
||||
.left-buttons .taibtn{
|
||||
z-index: 1;
|
||||
}
|
||||
.accountpass-form,
|
||||
.accountdel-form,
|
||||
.login-form{
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
.accountpass-form .accountpass-div,
|
||||
.accountdel-form .accountdel-div,
|
||||
.login-form .password2-div{
|
||||
display: none;
|
||||
}
|
||||
.account-view .displayname,
|
||||
.accountpass-form input[type=password],
|
||||
.accountdel-form input[type=password],
|
||||
.login-form input[type=text],
|
||||
.login-form input[type=password]{
|
||||
width: 100%;
|
||||
font-size: 1.4em;
|
||||
margin: 0.1em 0;
|
||||
padding: 0.3em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.accountpass-form input[type=password]{
|
||||
width: calc(100% / 3);
|
||||
}
|
||||
.accountpass-form input[type=password]::placeholder{
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.login-form input[type=checkbox]{
|
||||
transform: scale(1.4);
|
||||
}
|
||||
.account-view .displayname-hint,
|
||||
.login-form .username-hint,
|
||||
.login-form .password-hint,
|
||||
.login-form .remember-label{
|
||||
display: block;
|
||||
font-size: 1.1em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.login-form .remember-label{
|
||||
padding: 0.85em;
|
||||
}
|
||||
.account-view .save-btn{
|
||||
float: right;
|
||||
padding: 0.4em 1.5em;
|
||||
font-weight: bold;
|
||||
border-color: #000;
|
||||
color: #000;
|
||||
z-index: 1;
|
||||
}
|
||||
.account-view .view-end-button{
|
||||
margin-right: 0.4em;
|
||||
font-weight: normal;
|
||||
border-color: #dacdb2;
|
||||
color: #555;
|
||||
}
|
||||
.account-view .save-btn:hover,
|
||||
.account-view .save-btn.selected,
|
||||
.account-view .view-end-button:hover,
|
||||
.account-view .view-end-button.selected{
|
||||
color: #fff;
|
||||
border-color: #fff;
|
||||
}
|
||||
.account-view .displayname-div{
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.accountpass-form .accountpass-btn,
|
||||
.accountdel-form .accountdel-btn,
|
||||
.login-form .login-btn{
|
||||
z-index: 1;
|
||||
}
|
||||
.accountpass-form,
|
||||
.accountdel-form{
|
||||
margin: 0.3em auto;
|
||||
}
|
||||
.view-content .error-div{
|
||||
display: none;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
padding: 0.5em;
|
||||
font-size: 1.1em;
|
||||
color: #d00;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
cancelTouch = false
|
||||
|
||||
this.endButton = this.getElement("view-end-button")
|
||||
this.diagTxt = document.getElementById("diag-txt")
|
||||
this.diagTxt = this.getElement("diag-txt")
|
||||
this.version = document.getElementById("version-link").href
|
||||
this.tutorialOuter = this.getElement("view-outer")
|
||||
if(touchEnabled){
|
||||
|
512
public/src/js/account.js
Normal file
512
public/src/js/account.js
Normal 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
|
||||
}
|
||||
}
|
@ -31,7 +31,9 @@ var assets = {
|
||||
"importsongs.js",
|
||||
"logo.js",
|
||||
"settings.js",
|
||||
"scorestorage.js"
|
||||
"scorestorage.js",
|
||||
"account.js",
|
||||
"lyrics.js"
|
||||
],
|
||||
"css": [
|
||||
"main.css",
|
||||
@ -86,11 +88,7 @@ var assets = {
|
||||
"settings_gamepad.png"
|
||||
],
|
||||
"audioSfx": [
|
||||
"se_cancel.wav",
|
||||
"se_don.wav",
|
||||
"se_ka.wav",
|
||||
"se_pause.wav",
|
||||
"se_jump.wav",
|
||||
"se_calibration.wav",
|
||||
|
||||
"v_results.wav",
|
||||
@ -102,6 +100,10 @@ var assets = {
|
||||
"audioSfxLR": [
|
||||
"neiro_1_don.wav",
|
||||
"neiro_1_ka.wav",
|
||||
"se_cancel.wav",
|
||||
"se_don.wav",
|
||||
"se_ka.wav",
|
||||
"se_jump.wav",
|
||||
|
||||
"se_balloon.wav",
|
||||
"se_gameclear.wav",
|
||||
@ -137,7 +139,9 @@ var assets = {
|
||||
"about.html",
|
||||
"debug.html",
|
||||
"session.html",
|
||||
"settings.html"
|
||||
"settings.html",
|
||||
"account.html",
|
||||
"login.html"
|
||||
],
|
||||
|
||||
"songs": [],
|
||||
|
@ -706,12 +706,12 @@
|
||||
})
|
||||
}else if(r.smallHiragana.test(symbol)){
|
||||
// Small hiragana, small katakana
|
||||
drawn.push({text: symbol, x: 0, y: 0, w: 30})
|
||||
drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 30})
|
||||
}else if(r.hiragana.test(symbol)){
|
||||
// Hiragana, katakana
|
||||
drawn.push({text: symbol, x: 0, y: 0, w: 35})
|
||||
drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 35})
|
||||
}else{
|
||||
drawn.push({text: symbol, x: 0, y: 0, w: 39})
|
||||
drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 39})
|
||||
}
|
||||
}
|
||||
|
||||
@ -720,6 +720,9 @@
|
||||
if(config.letterSpacing){
|
||||
symbol.w += config.letterSpacing
|
||||
}
|
||||
if(config.kanaSpacing && symbol.kana){
|
||||
symbol.w += config.kanaSpacing
|
||||
}
|
||||
drawnWidth += symbol.w * mul
|
||||
}
|
||||
|
||||
@ -924,8 +927,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
var search = () => {
|
||||
var end = line.length
|
||||
var dist = end
|
||||
while(dist){
|
||||
dist >>= 1
|
||||
line = words[i].slice(0, end)
|
||||
lastWidth = ctx.measureText(line).width
|
||||
end += lastWidth < config.width ? dist : -dist
|
||||
}
|
||||
if(line !== words[i]){
|
||||
words.splice(i + 1, 0, words[i].slice(line.length))
|
||||
words[i] = line
|
||||
}
|
||||
}
|
||||
|
||||
for(var i in words){
|
||||
for(var i = 0; i < words.length; i++){
|
||||
var skip = words[i].substitute || words[i] === "\n"
|
||||
if(!skip){
|
||||
var currentWidth = ctx.measureText(line + words[i]).width
|
||||
@ -957,8 +974,22 @@
|
||||
recenter()
|
||||
x = 0
|
||||
y += lineHeight
|
||||
line = words[i] === "\n" ? "" : words[i]
|
||||
lastWidth = ctx.measureText(line).width
|
||||
if(words[i] === "\n"){
|
||||
line = ""
|
||||
lastWidth = 0
|
||||
}else{
|
||||
line = words[i]
|
||||
lastWidth = ctx.measureText(line).width
|
||||
if(line.length !== 1 && lastWidth > config.width){
|
||||
search()
|
||||
}
|
||||
}
|
||||
}
|
||||
}else if(!line){
|
||||
line = words[i]
|
||||
lastWidth = ctx.measureText(line).width
|
||||
if(line.length !== 1 && lastWidth > config.width){
|
||||
search()
|
||||
}
|
||||
}else{
|
||||
line += words[i]
|
||||
@ -1549,6 +1580,99 @@
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
nameplate(config){
|
||||
var ctx = config.ctx
|
||||
var w = 264
|
||||
var h = 57
|
||||
var r = h / 2
|
||||
var pi = Math.PI
|
||||
|
||||
ctx.save()
|
||||
|
||||
ctx.translate(config.x, config.y)
|
||||
if(config.scale){
|
||||
ctx.scale(config.scale, config.scale)
|
||||
}
|
||||
|
||||
ctx.fillStyle="rgba(0, 0, 0, 0.25)"
|
||||
ctx.beginPath()
|
||||
ctx.arc(r + 4, r + 5, r, pi / 2, pi / -2)
|
||||
ctx.arc(w - r + 4, r + 5, r, pi / -2, pi / 2)
|
||||
ctx.fill()
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(r, 0)
|
||||
this.roundedCorner(ctx, w, 0, r, 1)
|
||||
ctx.lineTo(r, r)
|
||||
ctx.fillStyle = config.blue ? "#67cecb" : "#ff421d"
|
||||
ctx.fill()
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(r, r)
|
||||
this.roundedCorner(ctx, w, h, r, 2)
|
||||
ctx.lineTo(r, h)
|
||||
ctx.fillStyle = "rgba(255, 255, 255, 0.8)"
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = "#000"
|
||||
ctx.lineWidth = 4
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(r, 0)
|
||||
ctx.arc(w - r, r, r, pi / -2, pi / 2)
|
||||
ctx.lineTo(r, h)
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(r, r - 1)
|
||||
ctx.lineTo(w, r - 1)
|
||||
ctx.lineWidth = 2
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.arc(r, r, r, 0, pi * 2)
|
||||
ctx.fillStyle = config.blue ? "#67cecb" : "#ff421d"
|
||||
ctx.fill()
|
||||
ctx.lineWidth = 4
|
||||
ctx.stroke()
|
||||
ctx.font = this.bold(config.font) + "28px " + config.font
|
||||
ctx.textAlign = "center"
|
||||
ctx.textBaseline = "middle"
|
||||
ctx.lineWidth = 5
|
||||
ctx.miterLimit = 1
|
||||
ctx.strokeStyle = "#fff"
|
||||
ctx.fillStyle = "#000"
|
||||
var text = config.blue ? "2P" : "1P"
|
||||
ctx.strokeText(text, r + 2, r + 1)
|
||||
ctx.fillText(text, r + 2, r + 1)
|
||||
if(config.rank){
|
||||
this.layeredText({
|
||||
ctx: ctx,
|
||||
text: config.rank,
|
||||
fontSize: 20,
|
||||
fontFamily: config.font,
|
||||
x: w / 2 + r * 0.7,
|
||||
y: r * 0.5,
|
||||
width: 180,
|
||||
align: "center",
|
||||
baseline: "middle"
|
||||
}, [
|
||||
{fill: "#000"}
|
||||
])
|
||||
}
|
||||
this.layeredText({
|
||||
ctx: ctx,
|
||||
text: config.name || "",
|
||||
fontSize: 21,
|
||||
fontFamily: config.font,
|
||||
x: w / 2 + r * 0.7,
|
||||
y: r * 1.5 - 0.5,
|
||||
width: 180,
|
||||
kanaSpacing: 10,
|
||||
align: "center",
|
||||
baseline: "middle"
|
||||
}, [
|
||||
{outline: "#000", letterBorder: 6},
|
||||
{fill: "#fff"}
|
||||
])
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
alpha(amount, ctx, callback, winW, winH){
|
||||
if(amount >= 1){
|
||||
return callback(ctx)
|
||||
|
@ -6,7 +6,11 @@ class Controller{
|
||||
this.saveScore = !autoPlayEnabled
|
||||
this.multiplayer = multiplayer
|
||||
this.touchEnabled = touchEnabled
|
||||
this.snd = this.multiplayer ? "_p" + this.multiplayer : ""
|
||||
if(multiplayer === 2){
|
||||
this.snd = p2.player === 2 ? "_p1" : "_p2"
|
||||
}else{
|
||||
this.snd = multiplayer ? "_p" + p2.player : ""
|
||||
}
|
||||
|
||||
this.calibrationMode = selectedSong.folder === "calibration"
|
||||
this.audioLatency = 0
|
||||
@ -53,6 +57,15 @@ class Controller{
|
||||
if(song.id == this.selectedSong.folder){
|
||||
this.mainAsset = song.sound
|
||||
this.volume = song.volume || 1
|
||||
if(!multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){
|
||||
if(song.lyricsData){
|
||||
var lyricsDiv = document.getElementById("song-lyrics")
|
||||
this.lyrics = new Lyrics(song.lyricsData, selectedSong.offset, lyricsDiv)
|
||||
}else if(this.parsedSongData.lyrics){
|
||||
var lyricsDiv = document.getElementById("song-lyrics")
|
||||
this.lyrics = new Lyrics(this.parsedSongData.lyrics, selectedSong.offset, lyricsDiv, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -155,10 +168,16 @@ class Controller{
|
||||
if(this.mainLoopRunning){
|
||||
if(this.multiplayer !== 2){
|
||||
requestAnimationFrame(() => {
|
||||
this.viewLoop()
|
||||
var player = this.multiplayer ? p2.player : 1
|
||||
if(player === 1){
|
||||
this.viewLoop()
|
||||
}
|
||||
if(this.multiplayer === 1){
|
||||
this.syncWith.viewLoop()
|
||||
}
|
||||
if(player === 2){
|
||||
this.viewLoop()
|
||||
}
|
||||
if(this.scoresheet){
|
||||
if(this.view.ctx){
|
||||
this.view.ctx.save()
|
||||
@ -197,14 +216,14 @@ class Controller{
|
||||
displayScore(score, notPlayed, bigNote){
|
||||
this.view.displayScore(score, notPlayed, bigNote)
|
||||
}
|
||||
songSelection(fadeIn){
|
||||
songSelection(fadeIn, showWarning){
|
||||
if(!fadeIn){
|
||||
this.clean()
|
||||
}
|
||||
if(this.calibrationMode){
|
||||
new SettingsView(this.touchEnabled, false, null, "latency")
|
||||
}else{
|
||||
new SongSelect(false, fadeIn, this.touchEnabled)
|
||||
new SongSelect(false, fadeIn, this.touchEnabled, null, showWarning)
|
||||
}
|
||||
}
|
||||
restartSong(){
|
||||
@ -217,20 +236,27 @@ class Controller{
|
||||
resolve()
|
||||
}else{
|
||||
var songObj = assets.songs.find(song => song.id === this.selectedSong.folder)
|
||||
var promises = []
|
||||
if(songObj.chart && songObj.chart !== "blank"){
|
||||
var reader = new FileReader()
|
||||
var promise = pageEvents.load(reader).then(event => {
|
||||
promises.push(pageEvents.load(reader).then(event => {
|
||||
this.songData = event.target.result.replace(/\0/g, "").split("\n")
|
||||
resolve()
|
||||
})
|
||||
return Promise.resolve()
|
||||
}))
|
||||
if(this.selectedSong.type === "tja"){
|
||||
reader.readAsText(songObj.chart, "sjis")
|
||||
}else{
|
||||
reader.readAsText(songObj.chart)
|
||||
}
|
||||
}else{
|
||||
resolve()
|
||||
}
|
||||
if(songObj.lyricsFile){
|
||||
var reader = new FileReader()
|
||||
promises.push(pageEvents.load(reader).then(event => {
|
||||
songObj.lyricsData = event.target.result
|
||||
}, () => Promise.resolve()), songObj.lyricsFile.webkitRelativePath)
|
||||
reader.readAsText(songObj.lyricsFile)
|
||||
}
|
||||
Promise.all(promises).then(resolve)
|
||||
}
|
||||
}).then(() => {
|
||||
var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled)
|
||||
@ -306,5 +332,8 @@ class Controller{
|
||||
debugObj.debug.updateStatus()
|
||||
}
|
||||
}
|
||||
if(this.lyrics){
|
||||
this.lyrics.clean()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ class Debug{
|
||||
this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0]
|
||||
this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0]
|
||||
this.volumeDiv = this.byClass("music-volume")
|
||||
this.lyricsHideDiv = this.byClass("lyrics-hide")
|
||||
this.lyricsOffsetDiv = this.byClass("lyrics-offset")
|
||||
this.restartLabel = this.byClass("change-restart-label")
|
||||
this.restartCheckbox = this.byClass("change-restart")
|
||||
this.autoplayLabel = this.byClass("autoplay-label")
|
||||
@ -50,6 +52,9 @@ class Debug{
|
||||
this.volumeSlider.onchange(this.volumeChange.bind(this))
|
||||
this.volumeSlider.set(1)
|
||||
|
||||
this.lyricsSlider = new InputSlider(this.lyricsOffsetDiv, -60, 60, 3)
|
||||
this.lyricsSlider.onchange(this.lyricsChange.bind(this))
|
||||
|
||||
this.moveTo(100, 100)
|
||||
this.restore()
|
||||
this.updateStatus()
|
||||
@ -129,6 +134,9 @@ class Debug{
|
||||
if(this.controller.parsedSongData.branches){
|
||||
this.branchHideDiv.style.display = "block"
|
||||
}
|
||||
if(this.controller.lyrics){
|
||||
this.lyricsHideDiv.style.display = "block"
|
||||
}
|
||||
|
||||
var selectedSong = this.controller.selectedSong
|
||||
this.defaultOffset = selectedSong.offset || 0
|
||||
@ -136,19 +144,21 @@ class Debug{
|
||||
this.offsetChange(this.offsetSlider.get(), true)
|
||||
this.branchChange(null, true)
|
||||
this.volumeChange(this.volumeSlider.get(), true)
|
||||
this.lyricsChange(this.lyricsSlider.get(), true)
|
||||
}else{
|
||||
this.songHash = selectedSong.hash
|
||||
this.offsetSlider.set(this.defaultOffset)
|
||||
this.branchReset(null, true)
|
||||
this.volumeSlider.set(this.controller.volume)
|
||||
this.lyricsSlider.set(this.controller.lyrics ? this.controller.lyrics.vttOffset / 1000 : 0)
|
||||
}
|
||||
|
||||
var measures = this.controller.parsedSongData.measures.filter((measure, i, array) => {
|
||||
return i === 0 || Math.abs(measure.ms - array[i - 1].ms) > 0.01
|
||||
})
|
||||
this.measureNumSlider.setMinMax(0, measures.length - 1)
|
||||
if(this.measureNum && measures.length > this.measureNum){
|
||||
var measureMS = measures[this.measureNum].ms
|
||||
if(this.measureNum > 0 && measures.length >= this.measureNum){
|
||||
var measureMS = measures[this.measureNum - 1].ms
|
||||
var game = this.controller.game
|
||||
game.started = true
|
||||
var timestamp = Date.now()
|
||||
@ -174,6 +184,7 @@ class Debug{
|
||||
this.restartBtn.style.display = ""
|
||||
this.autoplayLabel.style.display = ""
|
||||
this.branchHideDiv.style.display = ""
|
||||
this.lyricsHideDiv.style.display = ""
|
||||
this.controller = null
|
||||
}
|
||||
this.stopMove()
|
||||
@ -194,6 +205,9 @@ class Debug{
|
||||
branch.ms = branch.originalMS + offset
|
||||
})
|
||||
}
|
||||
if(this.controller.lyrics){
|
||||
this.controller.lyrics.offsetChange(value * 1000)
|
||||
}
|
||||
if(this.restartCheckbox.checked && !noRestart){
|
||||
this.restartSong()
|
||||
}
|
||||
@ -213,6 +227,14 @@ class Debug{
|
||||
this.restartSong()
|
||||
}
|
||||
}
|
||||
lyricsChange(value, noRestart){
|
||||
if(this.controller && this.controller.lyrics){
|
||||
this.controller.lyrics.offsetChange(undefined, value * 1000)
|
||||
}
|
||||
if(this.restartCheckbox.checked && !noRestart){
|
||||
this.restartSong()
|
||||
}
|
||||
}
|
||||
restartSong(){
|
||||
if(this.controller){
|
||||
this.controller.restartSong()
|
||||
@ -259,6 +281,7 @@ class Debug{
|
||||
this.offsetSlider.clean()
|
||||
this.measureNumSlider.clean()
|
||||
this.volumeSlider.clean()
|
||||
this.lyricsSlider.clean()
|
||||
|
||||
pageEvents.remove(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.windowSymbol)
|
||||
pageEvents.mouseRemove(this)
|
||||
@ -285,6 +308,8 @@ class Debug{
|
||||
delete this.branchSelect
|
||||
delete this.branchResetBtn
|
||||
delete this.volumeDiv
|
||||
delete this.lyricsHideDiv
|
||||
delete this.lyricsOffsetDiv
|
||||
delete this.restartCheckbox
|
||||
delete this.autoplayLabel
|
||||
delete this.autoplayCheckbox
|
||||
|
@ -5,6 +5,7 @@ class Game{
|
||||
this.songData = songData
|
||||
this.elapsedTime = 0
|
||||
this.currentCircle = -1
|
||||
this.currentEvent = 0
|
||||
this.updateCurrentCircle()
|
||||
this.combo = 0
|
||||
this.rules = new GameRules(this)
|
||||
@ -47,13 +48,7 @@ class Game{
|
||||
}
|
||||
initTiming(){
|
||||
// Date when the chrono is started (before the game begins)
|
||||
var firstCircle
|
||||
for(var i = 0; i < this.songData.circles.length; i++){
|
||||
firstCircle = this.songData.circles[i]
|
||||
if(firstCircle.type !== "event"){
|
||||
break
|
||||
}
|
||||
}
|
||||
var firstCircle = this.songData.circles[0]
|
||||
if(this.controller.calibrationMode){
|
||||
var offsetTime = 0
|
||||
}else{
|
||||
@ -238,9 +233,6 @@ class Game{
|
||||
}
|
||||
}
|
||||
skipNote(circle){
|
||||
if(circle.type === "event"){
|
||||
return
|
||||
}
|
||||
if(circle.section){
|
||||
this.resetSection()
|
||||
}
|
||||
@ -258,9 +250,6 @@ class Game{
|
||||
checkPlays(){
|
||||
var circles = this.songData.circles
|
||||
var circle = circles[this.currentCircle]
|
||||
if(circle && circle.type === "event"){
|
||||
this.updateCurrentCircle()
|
||||
}
|
||||
|
||||
if(this.controller.autoPlayEnabled){
|
||||
while(circle && this.controller.autoPlay(circle)){
|
||||
@ -469,9 +458,7 @@ class Game{
|
||||
}
|
||||
getLastCircle(circles){
|
||||
for(var i = circles.length; i--;){
|
||||
if(circles[i].type !== "event"){
|
||||
return circles[i]
|
||||
}
|
||||
return circles[i]
|
||||
}
|
||||
}
|
||||
whenLastCirclePlayed(){
|
||||
@ -505,7 +492,9 @@ class Game{
|
||||
var musicDuration = duration * 1000 - this.controller.offset
|
||||
if(this.musicFadeOut === 0){
|
||||
if(this.controller.multiplayer === 1){
|
||||
p2.send("gameresults", this.getGlobalScore())
|
||||
var obj = this.getGlobalScore()
|
||||
obj.name = account.loggedIn ? account.displayName : null
|
||||
p2.send("gameresults", obj)
|
||||
}
|
||||
this.musicFadeOut++
|
||||
}else if(this.musicFadeOut === 1 && ms >= started + 1600){
|
||||
@ -621,7 +610,7 @@ class Game{
|
||||
var circles = this.songData.circles
|
||||
do{
|
||||
var circle = circles[++this.currentCircle]
|
||||
}while(circle && (circle.branch && !circle.branch.active || circle.type === "event"))
|
||||
}while(circle && (circle.branch && !circle.branch.active))
|
||||
}
|
||||
getCurrentCircle(){
|
||||
return this.currentCircle
|
||||
|
@ -202,12 +202,16 @@
|
||||
var tja = new ParseTja(data, "oni", 0, 0, true)
|
||||
var songObj = {
|
||||
id: index + 1,
|
||||
order: index + 1,
|
||||
type: "tja",
|
||||
chart: file,
|
||||
stars: [],
|
||||
courses: {},
|
||||
music: "muted"
|
||||
}
|
||||
var coursesAdded = false
|
||||
var titleLang = {}
|
||||
var titleLangAdded = false
|
||||
var subtitleLangAdded = false
|
||||
var subtitleLang = {}
|
||||
var dir = file.webkitRelativePath.toLowerCase()
|
||||
dir = dir.slice(0, dir.lastIndexOf("/") + 1)
|
||||
@ -221,7 +225,11 @@
|
||||
}
|
||||
songObj.subtitle = subtitle
|
||||
songObj.preview = meta.demostart || 0
|
||||
songObj.stars[this.courseTypes[diff]] = (meta.level || "0") + (meta.branch ? " B" : "")
|
||||
songObj.courses[diff] = {
|
||||
stars: meta.level || 0,
|
||||
branch: !!meta.branch
|
||||
}
|
||||
coursesAdded = true
|
||||
if(meta.wave){
|
||||
songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music
|
||||
}
|
||||
@ -252,6 +260,15 @@
|
||||
id: 1
|
||||
}
|
||||
}
|
||||
if(meta.lyrics){
|
||||
var lyricsFile = this.normPath(this.joinPath(dir, meta.lyrics))
|
||||
if(lyricsFile in this.otherFiles){
|
||||
songObj.lyrics = true
|
||||
songObj.lyricsFile = this.otherFiles[lyricsFile]
|
||||
}
|
||||
}else if(meta.inlineLyrics){
|
||||
songObj.lyrics = true
|
||||
}
|
||||
for(var id in allStrings){
|
||||
var songTitle = songObj.title
|
||||
var ura = ""
|
||||
@ -264,32 +281,27 @@
|
||||
}
|
||||
if(meta["title" + id]){
|
||||
titleLang[id] = meta["title" + id]
|
||||
titleLangAdded = true
|
||||
}else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){
|
||||
titleLang[id] = this.songTitle[songTitle][id] + ura
|
||||
titleLangAdded = true
|
||||
}
|
||||
if(meta["subtitle" + id]){
|
||||
subtitleLang[id] = meta["subtitle" + id]
|
||||
subtitleLangAdded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var titleLangArray = []
|
||||
for(var id in titleLang){
|
||||
titleLangArray.push(id + " " + titleLang[id])
|
||||
if(titleLangAdded){
|
||||
songObj.title_lang = titleLang
|
||||
}
|
||||
if(titleLangArray.length !== 0){
|
||||
songObj.title_lang = titleLangArray.join("\n")
|
||||
}
|
||||
var subtitleLangArray = []
|
||||
for(var id in subtitleLang){
|
||||
subtitleLangArray.push(id + " " + subtitleLang[id])
|
||||
}
|
||||
if(subtitleLangArray.length !== 0){
|
||||
songObj.subtitle_lang = subtitleLangArray.join("\n")
|
||||
if(subtitleLangAdded){
|
||||
songObj.subtitle_lang = subtitleLang
|
||||
}
|
||||
if(!songObj.category){
|
||||
songObj.category = category || this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))])
|
||||
}
|
||||
if(songObj.stars.length !== 0){
|
||||
if(coursesAdded){
|
||||
this.songs[index] = songObj
|
||||
}
|
||||
var hash = md5.base64(event.target.result).slice(0, -2)
|
||||
@ -316,12 +328,20 @@
|
||||
dir = dir.slice(0, dir.lastIndexOf("/") + 1)
|
||||
var songObj = {
|
||||
id: index + 1,
|
||||
order: index + 1,
|
||||
type: "osu",
|
||||
chart: file,
|
||||
subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist,
|
||||
subtitle_lang: osu.metadata.Artist || osu.metadata.ArtistUnicode,
|
||||
subtitle_lang: {
|
||||
en: osu.metadata.Artist || osu.metadata.ArtistUnicode
|
||||
},
|
||||
preview: osu.generalInfo.PreviewTime / 1000,
|
||||
stars: [null, null, null, parseInt(osu.difficulty.overallDifficulty) || 1],
|
||||
courses: {
|
||||
oni:{
|
||||
stars: parseInt(osu.difficulty.overallDifficulty) || 0,
|
||||
branch: false
|
||||
}
|
||||
},
|
||||
music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] || "muted"
|
||||
}
|
||||
var filename = file.name.slice(0, file.name.lastIndexOf("."))
|
||||
@ -333,7 +353,9 @@
|
||||
suffix = " " + matches[0]
|
||||
}
|
||||
songObj.title = title + suffix
|
||||
songObj.title_lang = (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix
|
||||
songObj.title_lang = {
|
||||
en: (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix
|
||||
}
|
||||
}else{
|
||||
songObj.title = filename
|
||||
}
|
||||
@ -417,7 +439,7 @@
|
||||
for(var i = path.length - 2; i >= 0; i--){
|
||||
var hasTitle = false
|
||||
for(var j in exclude){
|
||||
if(path[i].indexOf(exclude[j].toLowerCase()) !== -1){
|
||||
if(exclude[j] && path[i].indexOf(exclude[j].toLowerCase()) !== -1){
|
||||
hasTitle = true
|
||||
break
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ class Loader{
|
||||
this.assetsDiv = document.getElementById("assets")
|
||||
this.screen = document.getElementById("screen")
|
||||
this.startTime = Date.now()
|
||||
this.errorMessages = []
|
||||
|
||||
var promises = []
|
||||
|
||||
@ -28,17 +29,24 @@ class Loader{
|
||||
|
||||
if(gameConfig.custom_js){
|
||||
var script = document.createElement("script")
|
||||
this.addPromise(pageEvents.load(script))
|
||||
script.src = gameConfig.custom_js + queryString
|
||||
var url = gameConfig.custom_js + queryString
|
||||
this.addPromise(pageEvents.load(script), url)
|
||||
script.src = url
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
assets.js.forEach(name => {
|
||||
var script = document.createElement("script")
|
||||
this.addPromise(pageEvents.load(script))
|
||||
script.src = "/src/js/" + name + queryString
|
||||
var url = "/src/js/" + name + queryString
|
||||
this.addPromise(pageEvents.load(script), url)
|
||||
script.src = url
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
|
||||
var pageVersion = versionLink.href
|
||||
var index = pageVersion.lastIndexOf("/")
|
||||
if(index !== -1){
|
||||
pageVersion = pageVersion.slice(index + 1)
|
||||
}
|
||||
this.addPromise(new Promise((resolve, reject) => {
|
||||
if(
|
||||
versionLink.href !== gameConfig._version.url &&
|
||||
@ -69,48 +77,56 @@ class Loader{
|
||||
}
|
||||
var interval = setInterval(checkStyles, 100)
|
||||
checkStyles()
|
||||
}))
|
||||
}), "Version on the page and config does not match\n(page: " + pageVersion + ",\nconfig: "+ gameConfig._version.commit + ")")
|
||||
|
||||
for(var name in assets.fonts){
|
||||
this.addPromise(new FontFace(name, "url('" + gameConfig.assets_baseurl + "fonts/" + assets.fonts[name] + "')").load().then(font => {
|
||||
var url = gameConfig.assets_baseurl + "fonts/" + assets.fonts[name]
|
||||
this.addPromise(new FontFace(name, "url('" + url + "')").load().then(font => {
|
||||
document.fonts.add(font)
|
||||
}))
|
||||
}), url)
|
||||
}
|
||||
|
||||
assets.img.forEach(name => {
|
||||
var id = this.getFilename(name)
|
||||
var image = document.createElement("img")
|
||||
this.addPromise(pageEvents.load(image))
|
||||
var url = gameConfig.assets_baseurl + "img/" + name
|
||||
this.addPromise(pageEvents.load(image), url)
|
||||
image.id = name
|
||||
image.src = gameConfig.assets_baseurl + "img/" + name
|
||||
image.src = url
|
||||
this.assetsDiv.appendChild(image)
|
||||
assets.image[id] = image
|
||||
})
|
||||
|
||||
assets.views.forEach(name => {
|
||||
var id = this.getFilename(name)
|
||||
this.addPromise(this.ajax("/src/views/" + name + queryString).then(page => {
|
||||
var url = "/src/views/" + name + queryString
|
||||
this.addPromise(this.ajax(url).then(page => {
|
||||
assets.pages[id] = page
|
||||
}))
|
||||
}), url)
|
||||
})
|
||||
|
||||
this.addPromise(this.ajax("/api/songs").then(songs => {
|
||||
assets.songsDefault = JSON.parse(songs)
|
||||
assets.songs = assets.songsDefault
|
||||
}))
|
||||
}), "/api/songs")
|
||||
|
||||
this.addPromise(this.ajax(gameConfig.assets_baseurl + "img/vectors.json" + queryString).then(response => {
|
||||
var url = gameConfig.assets_baseurl + "img/vectors.json" + queryString
|
||||
this.addPromise(this.ajax(url).then(response => {
|
||||
vectors = JSON.parse(response)
|
||||
}))
|
||||
}), url)
|
||||
|
||||
this.afterJSCount =
|
||||
["blurPerformance", "P2Connection"].length +
|
||||
["blurPerformance"].length +
|
||||
assets.audioSfx.length +
|
||||
assets.audioMusic.length +
|
||||
assets.audioSfxLR.length +
|
||||
assets.audioSfxLoud.length
|
||||
assets.audioSfxLoud.length +
|
||||
(gameConfig.accounts ? 1 : 0)
|
||||
|
||||
Promise.all(this.promises).then(() => {
|
||||
if(this.error){
|
||||
return
|
||||
}
|
||||
|
||||
snd.buffer = new SoundBuffer()
|
||||
snd.musicGain = snd.buffer.createGain()
|
||||
@ -130,20 +146,20 @@ class Loader{
|
||||
this.afterJSCount = 0
|
||||
|
||||
assets.audioSfx.forEach(name => {
|
||||
this.addPromise(this.loadSound(name, snd.sfxGain))
|
||||
this.addPromise(this.loadSound(name, snd.sfxGain), this.soundUrl(name))
|
||||
})
|
||||
assets.audioMusic.forEach(name => {
|
||||
this.addPromise(this.loadSound(name, snd.musicGain))
|
||||
this.addPromise(this.loadSound(name, snd.musicGain), this.soundUrl(name))
|
||||
})
|
||||
assets.audioSfxLR.forEach(name => {
|
||||
this.addPromise(this.loadSound(name, snd.sfxGain).then(sound => {
|
||||
var id = this.getFilename(name)
|
||||
assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL)
|
||||
assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR)
|
||||
}))
|
||||
}), this.soundUrl(name))
|
||||
})
|
||||
assets.audioSfxLoud.forEach(name => {
|
||||
this.addPromise(this.loadSound(name, snd.sfxLoudGain))
|
||||
this.addPromise(this.loadSound(name, snd.sfxLoudGain), this.soundUrl(name))
|
||||
})
|
||||
|
||||
this.canvasTest = new CanvasTest()
|
||||
@ -153,67 +169,92 @@ class Loader{
|
||||
// Less than 50 fps with blur enabled
|
||||
disableBlur = true
|
||||
}
|
||||
}))
|
||||
}), "blurPerformance")
|
||||
|
||||
var readyEvent = "normal"
|
||||
var songId
|
||||
var hashLower = location.hash.toLowerCase()
|
||||
p2 = new P2Connection()
|
||||
if(hashLower.startsWith("#song=")){
|
||||
var number = parseInt(location.hash.slice(6))
|
||||
if(number > 0){
|
||||
songId = number
|
||||
readyEvent = "song-id"
|
||||
}
|
||||
}else if(location.hash.length === 6){
|
||||
p2.hashLock = true
|
||||
this.addPromise(new Promise(resolve => {
|
||||
p2.open()
|
||||
pageEvents.add(p2, "message", response => {
|
||||
if(response.type === "session"){
|
||||
pageEvents.send("session-start", "invited")
|
||||
readyEvent = "session-start"
|
||||
resolve()
|
||||
}else if(response.type === "gameend"){
|
||||
p2.hash("")
|
||||
p2.hashLock = false
|
||||
readyEvent = "session-expired"
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
p2.send("invite", location.hash.slice(1).toLowerCase())
|
||||
setTimeout(() => {
|
||||
if(p2.socket.readyState !== 1){
|
||||
p2.hash("")
|
||||
p2.hashLock = false
|
||||
resolve()
|
||||
}
|
||||
}, 10000)
|
||||
}).then(() => {
|
||||
pageEvents.remove(p2, "message")
|
||||
}))
|
||||
}else{
|
||||
p2.hash("")
|
||||
if(gameConfig.accounts){
|
||||
this.addPromise(this.ajax("/api/scores/get").then(response => {
|
||||
response = JSON.parse(response)
|
||||
if(response.status === "ok"){
|
||||
account.loggedIn = true
|
||||
account.username = response.username
|
||||
account.displayName = response.display_name
|
||||
scoreStorage.load(response.scores)
|
||||
pageEvents.send("login", account.username)
|
||||
}
|
||||
}), "/api/scores/get")
|
||||
}
|
||||
|
||||
settings = new Settings()
|
||||
pageEvents.setKbd()
|
||||
|
||||
scoreStorage = new ScoreStorage()
|
||||
for(var i in assets.songsDefault){
|
||||
var song = assets.songsDefault[i]
|
||||
if(!song.hash){
|
||||
song.hash = song.title
|
||||
}
|
||||
scoreStorage.songTitles[song.title] = song.hash
|
||||
var score = scoreStorage.get(song.hash, false, true)
|
||||
if(score){
|
||||
score.title = song.title
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(this.promises).then(() => {
|
||||
this.canvasTest.drawAllImages().then(result => {
|
||||
if(this.error){
|
||||
return
|
||||
}
|
||||
if(!account.loggedIn){
|
||||
scoreStorage.load()
|
||||
}
|
||||
for(var i in assets.songsDefault){
|
||||
var song = assets.songsDefault[i]
|
||||
if(!song.hash){
|
||||
song.hash = song.title
|
||||
}
|
||||
scoreStorage.songTitles[song.title] = song.hash
|
||||
var score = scoreStorage.get(song.hash, false, true)
|
||||
if(score){
|
||||
score.title = song.title
|
||||
}
|
||||
}
|
||||
var promises = []
|
||||
|
||||
var readyEvent = "normal"
|
||||
var songId
|
||||
var hashLower = location.hash.toLowerCase()
|
||||
p2 = new P2Connection()
|
||||
if(hashLower.startsWith("#song=")){
|
||||
var number = parseInt(location.hash.slice(6))
|
||||
if(number > 0){
|
||||
songId = number
|
||||
readyEvent = "song-id"
|
||||
}
|
||||
}else if(location.hash.length === 6){
|
||||
p2.hashLock = true
|
||||
promises.push(new Promise(resolve => {
|
||||
p2.open()
|
||||
pageEvents.add(p2, "message", response => {
|
||||
if(response.type === "session"){
|
||||
pageEvents.send("session-start", "invited")
|
||||
readyEvent = "session-start"
|
||||
resolve()
|
||||
}else if(response.type === "gameend"){
|
||||
p2.hash("")
|
||||
p2.hashLock = false
|
||||
readyEvent = "session-expired"
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
p2.send("invite", {
|
||||
id: location.hash.slice(1).toLowerCase(),
|
||||
name: account.loggedIn ? account.displayName : null
|
||||
})
|
||||
setTimeout(() => {
|
||||
if(p2.socket.readyState !== 1){
|
||||
p2.hash("")
|
||||
p2.hashLock = false
|
||||
resolve()
|
||||
}
|
||||
}, 10000)
|
||||
}).then(() => {
|
||||
pageEvents.remove(p2, "message")
|
||||
}))
|
||||
}else{
|
||||
p2.hash("")
|
||||
}
|
||||
|
||||
promises.push(this.canvasTest.drawAllImages())
|
||||
|
||||
Promise.all(promises).then(result => {
|
||||
perf.allImg = result
|
||||
perf.load = Date.now() - this.startTime
|
||||
this.canvasTest.clean()
|
||||
@ -227,27 +268,36 @@ class Loader{
|
||||
})
|
||||
|
||||
}
|
||||
addPromise(promise){
|
||||
addPromise(promise, url){
|
||||
this.promises.push(promise)
|
||||
promise.then(this.assetLoaded.bind(this), this.errorMsg.bind(this))
|
||||
promise.then(this.assetLoaded.bind(this), response => {
|
||||
this.errorMsg(response, url)
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
soundUrl(name){
|
||||
return gameConfig.assets_baseurl + "audio/" + name
|
||||
}
|
||||
loadSound(name, gain){
|
||||
var id = this.getFilename(name)
|
||||
return gain.load(gameConfig.assets_baseurl + "audio/" + name).then(sound => {
|
||||
return gain.load(this.soundUrl(name)).then(sound => {
|
||||
assets.sounds[id] = sound
|
||||
})
|
||||
}
|
||||
getFilename(name){
|
||||
return name.slice(0, name.lastIndexOf("."))
|
||||
}
|
||||
errorMsg(error){
|
||||
if(Array.isArray(error) && error[1] instanceof HTMLElement){
|
||||
error = error[0] + ": " + error[1].outerHTML
|
||||
errorMsg(error, url){
|
||||
if(url || error){
|
||||
if(url){
|
||||
error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url
|
||||
}
|
||||
this.errorMessages.push(error)
|
||||
pageEvents.send("loader-error", url || error)
|
||||
}
|
||||
console.error(error)
|
||||
pageEvents.send("loader-error", error)
|
||||
if(!this.error){
|
||||
this.error = true
|
||||
cancelTouch = false
|
||||
this.loaderDiv.classList.add("loaderError")
|
||||
if(typeof allStrings === "object"){
|
||||
var lang = localStorage.lang
|
||||
@ -265,14 +315,57 @@ class Loader{
|
||||
if(!lang){
|
||||
lang = "en"
|
||||
}
|
||||
var errorOccured = allStrings[lang].errorOccured
|
||||
}else{
|
||||
var errorOccured = "An error occurred, please refresh"
|
||||
loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang].errorOccured
|
||||
}
|
||||
this.loaderPercentage.appendChild(document.createElement("br"))
|
||||
this.loaderPercentage.appendChild(document.createTextNode(errorOccured))
|
||||
this.clean()
|
||||
var loaderError = loader.screen.getElementsByClassName("loader-error-div")[0]
|
||||
loaderError.style.display = "flex"
|
||||
var diagTxt = loader.screen.getElementsByClassName("diag-txt")[0]
|
||||
var debugLink = loader.screen.getElementsByClassName("debug-link")[0]
|
||||
if(navigator.userAgent.indexOf("Android") >= 0){
|
||||
var iframe = document.createElement("iframe")
|
||||
diagTxt.appendChild(iframe)
|
||||
var body = iframe.contentWindow.document.body
|
||||
body.setAttribute("style", `
|
||||
font-family: monospace;
|
||||
margin: 2px 0 0 2px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
cursor: text;
|
||||
`)
|
||||
body.setAttribute("onblur", `
|
||||
getSelection().removeAllRanges()
|
||||
`)
|
||||
this.errorTxt = {
|
||||
element: body,
|
||||
method: "innerText"
|
||||
}
|
||||
}else{
|
||||
var textarea = document.createElement("textarea")
|
||||
textarea.readOnly = true
|
||||
diagTxt.appendChild(textarea)
|
||||
if(!this.touchEnabled){
|
||||
textarea.addEventListener("focus", () => {
|
||||
textarea.select()
|
||||
})
|
||||
textarea.addEventListener("blur", () => {
|
||||
getSelection().removeAllRanges()
|
||||
})
|
||||
}
|
||||
this.errorTxt = {
|
||||
element: textarea,
|
||||
method: "value"
|
||||
}
|
||||
}
|
||||
var show = () => {
|
||||
diagTxt.style.display = "block"
|
||||
debugLink.style.display = "none"
|
||||
}
|
||||
debugLink.addEventListener("click", show)
|
||||
debugLink.addEventListener("touchstart", show)
|
||||
this.clean(true)
|
||||
}
|
||||
var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount))
|
||||
this.errorTxt.element[this.errorTxt.method] = "```\n" + this.errorMessages.join("\n") + "\nPercentage: " + percentage + "%\n```"
|
||||
}
|
||||
assetLoaded(){
|
||||
if(!this.error){
|
||||
@ -291,7 +384,11 @@ class Loader{
|
||||
var request = new XMLHttpRequest()
|
||||
request.open("GET", url)
|
||||
pageEvents.load(request).then(() => {
|
||||
resolve(request.response)
|
||||
if(request.status === 200){
|
||||
resolve(request.response)
|
||||
}else{
|
||||
reject()
|
||||
}
|
||||
}, reject)
|
||||
if(customRequest){
|
||||
customRequest(request)
|
||||
@ -299,14 +396,28 @@ class Loader{
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
clean(){
|
||||
getCsrfToken(){
|
||||
return this.ajax("api/csrftoken").then(response => {
|
||||
var json = JSON.parse(response)
|
||||
if(json.status === "ok"){
|
||||
return Promise.resolve(json.token)
|
||||
}else{
|
||||
return Promise.reject()
|
||||
}
|
||||
})
|
||||
}
|
||||
clean(error){
|
||||
var fontDetectDiv = document.getElementById("fontdetectHelper")
|
||||
if(fontDetectDiv){
|
||||
fontDetectDiv.parentNode.removeChild(fontDetectDiv)
|
||||
}
|
||||
delete this.loaderDiv
|
||||
delete this.loaderPercentage
|
||||
delete this.loaderProgress
|
||||
delete this.promises
|
||||
if(!error){
|
||||
delete this.promises
|
||||
delete this.errorText
|
||||
}
|
||||
pageEvents.remove(root, "touchstart")
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class LoadSong{
|
||||
run(){
|
||||
var song = this.selectedSong
|
||||
var id = song.folder
|
||||
var promises = []
|
||||
this.promises = []
|
||||
if(song.folder !== "calibration"){
|
||||
assets.sounds["v_start"].play()
|
||||
var songObj = assets.songs.find(song => song.id === id)
|
||||
@ -92,9 +92,9 @@ class LoadSong{
|
||||
img.crossOrigin = "Anonymous"
|
||||
}
|
||||
let promise = pageEvents.load(img)
|
||||
promises.push(promise.then(() => {
|
||||
this.addPromise(promise.then(() => {
|
||||
return this.scaleImg(img, filename, prefix, force)
|
||||
}))
|
||||
}), songObj.music ? filename + ".png" : skinBase + filename + ".png")
|
||||
if(songObj.music){
|
||||
img.src = URL.createObjectURL(song.songSkin[filename + ".png"])
|
||||
}else{
|
||||
@ -102,14 +102,15 @@ class LoadSong{
|
||||
}
|
||||
}
|
||||
}
|
||||
promises.push(this.loadSongBg(id))
|
||||
this.loadSongBg(id)
|
||||
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
var url = gameConfig.songs_baseurl + id + "/main.mp3"
|
||||
this.addPromise(new Promise((resolve, reject) => {
|
||||
if(songObj.sound){
|
||||
songObj.sound.gain = snd.musicGain
|
||||
resolve()
|
||||
}else if(!songObj.music){
|
||||
snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.mp3").then(sound => {
|
||||
snd.musicGain.load(url).then(sound => {
|
||||
songObj.sound = sound
|
||||
resolve()
|
||||
}, reject)
|
||||
@ -121,84 +122,120 @@ class LoadSong{
|
||||
}else{
|
||||
resolve()
|
||||
}
|
||||
}))
|
||||
}), songObj.music ? songObj.music.webkitRelativePath : url)
|
||||
if(songObj.chart){
|
||||
if(songObj.chart === "blank"){
|
||||
this.songData = ""
|
||||
}else{
|
||||
var reader = new FileReader()
|
||||
promises.push(pageEvents.load(reader).then(event => {
|
||||
this.addPromise(pageEvents.load(reader).then(event => {
|
||||
this.songData = event.target.result.replace(/\0/g, "").split("\n")
|
||||
}))
|
||||
}), songObj.chart.webkitRelativePath)
|
||||
if(song.type === "tja"){
|
||||
reader.readAsText(songObj.chart, "sjis")
|
||||
}else{
|
||||
reader.readAsText(songObj.chart)
|
||||
}
|
||||
}
|
||||
if(songObj.lyricsFile && settings.getItem("showLyrics")){
|
||||
var reader = new FileReader()
|
||||
this.addPromise(pageEvents.load(reader).then(event => {
|
||||
songObj.lyricsData = event.target.result
|
||||
}, () => Promise.resolve()), songObj.lyricsFile.webkitRelativePath)
|
||||
reader.readAsText(songObj.lyricsFile)
|
||||
}
|
||||
}else{
|
||||
promises.push(loader.ajax(this.getSongPath(song)).then(data => {
|
||||
var url = this.getSongPath(song)
|
||||
this.addPromise(loader.ajax(url).then(data => {
|
||||
this.songData = data.replace(/\0/g, "").split("\n")
|
||||
}))
|
||||
}), url)
|
||||
if(song.lyrics && !songObj.lyricsData && !this.multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){
|
||||
var url = this.getSongDir(song) + "main.vtt"
|
||||
this.addPromise(loader.ajax(url).then(data => {
|
||||
songObj.lyricsData = data
|
||||
}), url)
|
||||
}
|
||||
}
|
||||
if(this.touchEnabled && !assets.image["touch_drum"]){
|
||||
let img = document.createElement("img")
|
||||
if(this.imgScale !== 1){
|
||||
img.crossOrigin = "Anonymous"
|
||||
}
|
||||
promises.push(pageEvents.load(img).then(() => {
|
||||
var url = gameConfig.assets_baseurl + "img/touch_drum.png"
|
||||
this.addPromise(pageEvents.load(img).then(() => {
|
||||
return this.scaleImg(img, "touch_drum", "")
|
||||
}))
|
||||
img.src = gameConfig.assets_baseurl + "img/touch_drum.png"
|
||||
}), url)
|
||||
img.src = url
|
||||
}
|
||||
Promise.all(promises).then(() => {
|
||||
this.setupMultiplayer()
|
||||
}, error => {
|
||||
if(Array.isArray(error) && error[1] instanceof HTMLElement){
|
||||
error = error[0] + ": " + error[1].outerHTML
|
||||
Promise.all(this.promises).then(() => {
|
||||
if(!this.error){
|
||||
this.setupMultiplayer()
|
||||
}
|
||||
console.error(error)
|
||||
pageEvents.send("load-song-error", error)
|
||||
errorMessage(new Error(error).stack)
|
||||
alert(strings.errorOccured)
|
||||
})
|
||||
}
|
||||
addPromise(promise, url){
|
||||
this.promises.push(promise.catch(response => {
|
||||
this.errorMsg(response, url)
|
||||
return Promise.resolve()
|
||||
}))
|
||||
}
|
||||
errorMsg(error, url){
|
||||
if(!this.error){
|
||||
if(url){
|
||||
error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url
|
||||
}
|
||||
pageEvents.send("load-song-error", error)
|
||||
errorMessage(new Error(error).stack)
|
||||
var title = this.selectedSong.title
|
||||
if(title !== this.selectedSong.originalTitle){
|
||||
title += " (" + this.selectedSong.originalTitle + ")"
|
||||
}
|
||||
assets.sounds["v_start"].stop()
|
||||
setTimeout(() => {
|
||||
this.clean()
|
||||
new SongSelect(false, false, this.touchEnabled, null, {
|
||||
name: "loadSongError",
|
||||
title: title,
|
||||
id: this.selectedSong.folder,
|
||||
error: error
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
this.error = true
|
||||
}
|
||||
loadSongBg(){
|
||||
return new Promise((resolve, reject) => {
|
||||
var promises = []
|
||||
var filenames = []
|
||||
if(this.selectedSong.songBg !== null){
|
||||
filenames.push("bg_song_" + this.selectedSong.songBg)
|
||||
var filenames = []
|
||||
if(this.selectedSong.songBg !== null){
|
||||
filenames.push("bg_song_" + this.selectedSong.songBg)
|
||||
}
|
||||
if(this.selectedSong.donBg !== null){
|
||||
filenames.push("bg_don_" + this.selectedSong.donBg)
|
||||
if(this.multiplayer){
|
||||
filenames.push("bg_don2_" + this.selectedSong.donBg)
|
||||
}
|
||||
if(this.selectedSong.donBg !== null){
|
||||
filenames.push("bg_don_" + this.selectedSong.donBg)
|
||||
if(this.multiplayer){
|
||||
filenames.push("bg_don2_" + this.selectedSong.donBg)
|
||||
}
|
||||
}
|
||||
if(this.selectedSong.songStage !== null){
|
||||
filenames.push("bg_stage_" + this.selectedSong.songStage)
|
||||
}
|
||||
for(var i = 0; i < filenames.length; i++){
|
||||
var filename = filenames[i]
|
||||
var stage = filename.startsWith("bg_stage_")
|
||||
for(var letter = 0; letter < (stage ? 1 : 2); letter++){
|
||||
let filenameAb = filenames[i] + (stage ? "" : (letter === 0 ? "a" : "b"))
|
||||
if(!(filenameAb in assets.image)){
|
||||
let img = document.createElement("img")
|
||||
let force = filenameAb.startsWith("bg_song_") && this.touchEnabled
|
||||
if(this.imgScale !== 1 || force){
|
||||
img.crossOrigin = "Anonymous"
|
||||
}
|
||||
promises.push(pageEvents.load(img).then(() => {
|
||||
return this.scaleImg(img, filenameAb, "", force)
|
||||
}))
|
||||
img.src = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
|
||||
}
|
||||
if(this.selectedSong.songStage !== null){
|
||||
filenames.push("bg_stage_" + this.selectedSong.songStage)
|
||||
}
|
||||
for(var i = 0; i < filenames.length; i++){
|
||||
var filename = filenames[i]
|
||||
var stage = filename.startsWith("bg_stage_")
|
||||
for(var letter = 0; letter < (stage ? 1 : 2); letter++){
|
||||
let filenameAb = filenames[i] + (stage ? "" : (letter === 0 ? "a" : "b"))
|
||||
if(!(filenameAb in assets.image)){
|
||||
let img = document.createElement("img")
|
||||
let force = filenameAb.startsWith("bg_song_") && this.touchEnabled
|
||||
if(this.imgScale !== 1 || force){
|
||||
img.crossOrigin = "Anonymous"
|
||||
}
|
||||
var url = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
|
||||
this.addPromise(pageEvents.load(img).then(() => {
|
||||
return this.scaleImg(img, filenameAb, "", force)
|
||||
}), url)
|
||||
img.src = url
|
||||
}
|
||||
}
|
||||
Promise.all(promises).then(resolve, reject)
|
||||
})
|
||||
}
|
||||
}
|
||||
scaleImg(img, filename, prefix, force){
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -238,8 +275,11 @@ class LoadSong{
|
||||
randInt(min, max){
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
getSongDir(selectedSong){
|
||||
return gameConfig.songs_baseurl + selectedSong.folder + "/"
|
||||
}
|
||||
getSongPath(selectedSong){
|
||||
var directory = gameConfig.songs_baseurl + selectedSong.folder + "/"
|
||||
var directory = this.getSongDir(selectedSong)
|
||||
if(selectedSong.type === "tja"){
|
||||
return directory + "main.tja"
|
||||
}else{
|
||||
@ -264,14 +304,14 @@ class LoadSong{
|
||||
if(event.type === "gameload"){
|
||||
this.cancelButton.style.display = ""
|
||||
|
||||
if(event.value === song.difficulty){
|
||||
if(event.value.diff === song.difficulty){
|
||||
this.startMultiplayer()
|
||||
}else{
|
||||
this.selectedSong2 = {}
|
||||
for(var i in this.selectedSong){
|
||||
this.selectedSong2[i] = this.selectedSong[i]
|
||||
}
|
||||
this.selectedSong2.difficulty = event.value
|
||||
this.selectedSong2.difficulty = event.value.diff
|
||||
if(song.type === "tja"){
|
||||
this.startMultiplayer()
|
||||
}else{
|
||||
@ -297,7 +337,8 @@ class LoadSong{
|
||||
})
|
||||
p2.send("join", {
|
||||
id: song.folder,
|
||||
diff: song.difficulty
|
||||
diff: song.difficulty,
|
||||
name: account.loggedIn ? account.displayName : null
|
||||
})
|
||||
}else{
|
||||
this.clean()
|
||||
@ -332,6 +373,7 @@ class LoadSong{
|
||||
pageEvents.send("load-song-cancel")
|
||||
}
|
||||
clean(){
|
||||
delete this.promises
|
||||
pageEvents.remove(p2, "message")
|
||||
if(this.cancelButton){
|
||||
pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"])
|
||||
|
231
public/src/js/lyrics.js
Normal file
231
public/src/js/lyrics.js
Normal 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
|
||||
}
|
||||
}
|
@ -84,6 +84,7 @@ var strings
|
||||
var vectors
|
||||
var settings
|
||||
var scoreStorage
|
||||
var account = {}
|
||||
|
||||
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
|
||||
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){
|
||||
|
@ -3,6 +3,8 @@ class P2Connection{
|
||||
this.closed = true
|
||||
this.lastMessages = {}
|
||||
this.otherConnected = false
|
||||
this.name = null
|
||||
this.player = 1
|
||||
this.allEvents = new Map()
|
||||
this.addEventListener("message", this.message.bind(this))
|
||||
this.currentHash = ""
|
||||
@ -102,6 +104,10 @@ class P2Connection{
|
||||
}
|
||||
message(response){
|
||||
switch(response.type){
|
||||
case "gameload":
|
||||
if("player" in response.value){
|
||||
this.player = response.value.player === 2 ? 2 : 1
|
||||
}
|
||||
case "gamestart":
|
||||
this.otherConnected = true
|
||||
this.notes = []
|
||||
@ -110,6 +116,7 @@ class P2Connection{
|
||||
this.kaAmount = 0
|
||||
this.results = false
|
||||
this.branch = "normal"
|
||||
scoreStorage.clearP2()
|
||||
break
|
||||
case "gameend":
|
||||
this.otherConnected = false
|
||||
@ -123,11 +130,13 @@ class P2Connection{
|
||||
this.hash("")
|
||||
this.hashLock = false
|
||||
}
|
||||
this.name = null
|
||||
scoreStorage.clearP2()
|
||||
break
|
||||
case "gameresults":
|
||||
this.results = {}
|
||||
for(var i in response.value){
|
||||
this.results[i] = response.value[i].toString()
|
||||
this.results[i] = response.value[i] === null ? null : response.value[i].toString()
|
||||
}
|
||||
break
|
||||
case "note":
|
||||
@ -150,6 +159,44 @@ class P2Connection{
|
||||
this.clearMessage("users")
|
||||
this.otherConnected = true
|
||||
this.session = true
|
||||
scoreStorage.clearP2()
|
||||
if("player" in response.value){
|
||||
this.player = response.value.player === 2 ? 2 : 1
|
||||
}
|
||||
break
|
||||
case "name":
|
||||
this.name = response.value ? response.value.toString() : response.value
|
||||
break
|
||||
case "getcrowns":
|
||||
if(response.value){
|
||||
var output = {}
|
||||
for(var i in response.value){
|
||||
if(response.value[i]){
|
||||
var score = scoreStorage.get(response.value[i], false, true)
|
||||
if(score){
|
||||
var crowns = {}
|
||||
for(var diff in score){
|
||||
if(diff !== "title"){
|
||||
crowns[diff] = {
|
||||
crown: score[diff].crown
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
var crowns = null
|
||||
}
|
||||
output[response.value[i]] = crowns
|
||||
}
|
||||
}
|
||||
p2.send("crowns", output)
|
||||
}
|
||||
break
|
||||
case "crowns":
|
||||
if(response.value){
|
||||
for(var i in response.value){
|
||||
scoreStorage.addP2(i, false, response.value[i], true)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +86,9 @@ class PageEvents{
|
||||
})
|
||||
}
|
||||
keyEvent(event){
|
||||
if(!("key" in event) || event.ctrlKey && (event.key === "c" || event.key === "x" || event.key === "v")){
|
||||
return
|
||||
}
|
||||
if(this.kbd.indexOf(event.key.toLowerCase()) !== -1){
|
||||
this.lastKeyEvent = Date.now()
|
||||
event.preventDefault()
|
||||
|
@ -48,6 +48,7 @@ class ParseOsu{
|
||||
lastBeatInterval: 0,
|
||||
bpm: 0
|
||||
}
|
||||
this.events = []
|
||||
this.generalInfo = this.parseGeneralInfo()
|
||||
this.metadata = this.parseMetadata()
|
||||
this.editor = this.parseEditor()
|
||||
@ -244,6 +245,18 @@ class ParseOsu{
|
||||
var circles = []
|
||||
var circleID = 0
|
||||
var indexes = this.getStartEndIndexes("HitObjects")
|
||||
var lastBeatMS = this.beatInfo.beatInterval
|
||||
var lastGogo = false
|
||||
|
||||
var pushCircle = circle => {
|
||||
circles.push(circle)
|
||||
if(lastBeatMS !== circle.beatMS || lastGogo !== circle.gogoTime){
|
||||
lastBeatMS = circle.beatMS
|
||||
lastGogo = circle.gogoTime
|
||||
this.events.push(circle)
|
||||
}
|
||||
}
|
||||
|
||||
for(var i = indexes.start; i <= indexes.end; i++){
|
||||
circleID++
|
||||
var values = this.data[i].split(",")
|
||||
@ -277,7 +290,7 @@ class ParseOsu{
|
||||
var endTime = parseInt(values[this.osu.ENDTIME])
|
||||
var hitMultiplier = this.difficultyRange(this.difficulty.overallDifficulty, 3, 5, 7.5) * 1.65
|
||||
var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier))
|
||||
circles.push(new Circle({
|
||||
pushCircle(new Circle({
|
||||
id: circleID,
|
||||
start: start + this.offset,
|
||||
type: "balloon",
|
||||
@ -304,7 +317,7 @@ class ParseOsu{
|
||||
type = "drumroll"
|
||||
txt = strings.note.drumroll
|
||||
}
|
||||
circles.push(new Circle({
|
||||
pushCircle(new Circle({
|
||||
id: circleID,
|
||||
start: start + this.offset,
|
||||
type: type,
|
||||
@ -339,7 +352,7 @@ class ParseOsu{
|
||||
emptyValue = true
|
||||
}
|
||||
if(!emptyValue){
|
||||
circles.push(new Circle({
|
||||
pushCircle(new Circle({
|
||||
id: circleID,
|
||||
start: start + this.offset,
|
||||
type: type,
|
||||
|
@ -43,6 +43,7 @@
|
||||
this.metadata = this.parseMetadata()
|
||||
this.measures = []
|
||||
this.beatInfo = {}
|
||||
this.events = []
|
||||
if(!metaOnly){
|
||||
this.circles = this.parseCircles()
|
||||
}
|
||||
@ -83,6 +84,8 @@
|
||||
}
|
||||
}else if(name.startsWith("branchstart") && inSong){
|
||||
courses[courseName].branch = true
|
||||
}else if(name.startsWith("lyric") && inSong){
|
||||
courses[courseName].inlineLyrics = true
|
||||
}
|
||||
|
||||
}else if(!inSong){
|
||||
@ -157,6 +160,7 @@
|
||||
var circleID = 0
|
||||
var regexAZ = /[A-Z]/
|
||||
var regexSpace = /\s/
|
||||
var regexLinebreak = /\\n/g
|
||||
var isAllDon = (note_chain, start_pos) => {
|
||||
for (var i = start_pos; i < note_chain.length; ++i) {
|
||||
var note = note_chain[i];
|
||||
@ -248,7 +252,12 @@
|
||||
lastDrumroll = circleObj
|
||||
}
|
||||
|
||||
circles.push(circleObj)
|
||||
if(note.event){
|
||||
this.events.push(circleObj)
|
||||
}
|
||||
if(note.type !== "event"){
|
||||
circles.push(circleObj)
|
||||
}
|
||||
} else if (!(currentMeasure.length >= 24 && (!currentMeasure[i + 1] || currentMeasure[i + 1].type))
|
||||
&& !(currentMeasure.length >= 48 && (!currentMeasure[i + 2] || currentMeasure[i + 2].type || !currentMeasure[i + 3] || currentMeasure[i + 3].type))) {
|
||||
if (note_chain.length > 1 && currentMeasure.length >= 8) {
|
||||
@ -266,9 +275,12 @@
|
||||
}
|
||||
}
|
||||
var insertNote = circleObj => {
|
||||
lastBpm = bpm
|
||||
lastGogo = gogo
|
||||
if(circleObj){
|
||||
if(bpm !== lastBpm || gogo !== lastGogo){
|
||||
circleObj.event = true
|
||||
lastBpm = bpm
|
||||
lastGogo = gogo
|
||||
}
|
||||
currentMeasure.push(circleObj)
|
||||
}
|
||||
}
|
||||
@ -402,6 +414,18 @@
|
||||
}
|
||||
branchObj[branchName] = currentBranch
|
||||
break
|
||||
case "lyric":
|
||||
if(!this.lyrics){
|
||||
this.lyrics = []
|
||||
}
|
||||
if(this.lyrics.length !== 0){
|
||||
this.lyrics[this.lyrics.length - 1].end = ms
|
||||
}
|
||||
this.lyrics.push({
|
||||
start: ms,
|
||||
text: value.trim().replace(regexLinebreak, "\n")
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
}else{
|
||||
@ -536,6 +560,10 @@
|
||||
this.scoreinit = autoscore.ScoreInit;
|
||||
this.scorediff = autoscore.ScoreDiff;
|
||||
}
|
||||
if(this.lyrics){
|
||||
var line = this.lyrics[this.lyrics.length - 1]
|
||||
line.end = Math.max(ms, line.start) + 5000
|
||||
}
|
||||
return circles
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,19 @@ class Scoresheet{
|
||||
constructor(controller, results, multiplayer, touchEnabled){
|
||||
this.controller = controller
|
||||
this.resultsObj = results
|
||||
this.results = {}
|
||||
this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0]
|
||||
var player0 = this.player[0]
|
||||
this.results = []
|
||||
this.results[player0] = {}
|
||||
this.rules = []
|
||||
this.rules[player0] = this.controller.game.rules
|
||||
if(multiplayer){
|
||||
this.player.push(p2.player === 2 ? 0 : 1)
|
||||
this.results[this.player[1]] = p2.results
|
||||
this.rules[this.player[1]] = this.controller.syncWith.game.rules
|
||||
}
|
||||
for(var i in results){
|
||||
this.results[i] = results[i].toString()
|
||||
this.results[player0][i] = results[i] === null ? null : results[i].toString()
|
||||
}
|
||||
this.multiplayer = multiplayer
|
||||
this.touchEnabled = touchEnabled
|
||||
@ -39,6 +49,7 @@ class Scoresheet{
|
||||
|
||||
this.draw = new CanvasDraw(noSmoothing)
|
||||
this.canvasCache = new CanvasCache(noSmoothing)
|
||||
this.nameplateCache = new CanvasCache(noSmoothing)
|
||||
|
||||
this.keyboard = new Keyboard({
|
||||
confirm: ["enter", "space", "esc", "don_l", "don_r"]
|
||||
@ -208,6 +219,7 @@ class Scoresheet{
|
||||
this.canvas.style.height = (winH / this.pixelRatio) + "px"
|
||||
|
||||
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
|
||||
this.nameplateCache.resize(274, 134, ratio + 0.2)
|
||||
|
||||
if(!this.multiplayer){
|
||||
this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio)
|
||||
@ -233,6 +245,9 @@ class Scoresheet{
|
||||
if(!this.canvasCache.canvas){
|
||||
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
|
||||
}
|
||||
if(!this.nameplateCache.canvas){
|
||||
this.nameplateCache.resize(274, 67, ratio + 0.2)
|
||||
}
|
||||
}
|
||||
this.winW = winW
|
||||
this.winH = winH
|
||||
@ -243,7 +258,7 @@ class Scoresheet{
|
||||
var frameTop = winH / 2 - 720 / 2
|
||||
var frameLeft = winW / 2 - 1280 / 2
|
||||
|
||||
var players = this.multiplayer && p2.results ? 2 : 1
|
||||
var players = this.multiplayer ? 2 : 1
|
||||
var p2Offset = 298
|
||||
|
||||
var bgOffset = 0
|
||||
@ -326,28 +341,21 @@ class Scoresheet{
|
||||
}
|
||||
|
||||
var rules = this.controller.game.rules
|
||||
var gaugePercent = rules.gaugePercent(this.results.gauge)
|
||||
var gaugeClear = [rules.gaugeClear]
|
||||
if(players === 2){
|
||||
gaugeClear.push(this.controller.syncWith.game.rules.gaugeClear)
|
||||
}
|
||||
var failedOffset = gaugePercent >= gaugeClear[0] ? 0 : -2000
|
||||
if(players === 2){
|
||||
var gauge2 = this.controller.syncWith.game.rules.gaugePercent(p2.results.gauge)
|
||||
if(gauge2 > gaugePercent && failedOffset !== 0 && gauge2 >= gaugeClear[1]){
|
||||
var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000
|
||||
if(players === 2 && failedOffset !== 0){
|
||||
var p2results = this.results[this.player[1]]
|
||||
if(p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)){
|
||||
failedOffset = 0
|
||||
}
|
||||
}
|
||||
if(elapsed >= 3100 + failedOffset){
|
||||
for(var p = 0; p < players; p++){
|
||||
ctx.save()
|
||||
var results = this.results
|
||||
if(p === 1){
|
||||
results = p2.results
|
||||
var results = this.results[p]
|
||||
if(!results){
|
||||
continue
|
||||
}
|
||||
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules
|
||||
var resultGauge = playerRules.gaugePercent(results.gauge)
|
||||
var clear = resultGauge >= gaugeClear[p]
|
||||
var clear = this.rules[p].clearReached(results.gauge)
|
||||
if(p === 1 || !this.multiplayer && clear){
|
||||
ctx.translate(0, 290)
|
||||
}
|
||||
@ -410,7 +418,7 @@ class Scoresheet{
|
||||
|
||||
this.draw.layeredText({
|
||||
ctx: ctx,
|
||||
text: this.results.title,
|
||||
text: this.results[this.player[0]].title,
|
||||
fontSize: 40,
|
||||
fontFamily: this.font,
|
||||
x: 1257,
|
||||
@ -426,9 +434,11 @@ class Scoresheet{
|
||||
|
||||
ctx.save()
|
||||
for(var p = 0; p < players; p++){
|
||||
var results = this.results
|
||||
var results = this.results[p]
|
||||
if(!results){
|
||||
continue
|
||||
}
|
||||
if(p === 1){
|
||||
results = p2.results
|
||||
ctx.translate(0, p2Offset)
|
||||
}
|
||||
|
||||
@ -450,6 +460,30 @@ class Scoresheet{
|
||||
ctx.fillText(text, 395, 308)
|
||||
ctx.miterLimit = 10
|
||||
|
||||
var defaultName = p === 0 ? strings.defaultName : strings.default2PName
|
||||
if(p === this.player[0]){
|
||||
var name = account.loggedIn ? account.displayName : defaultName
|
||||
}else{
|
||||
var name = results.name || defaultName
|
||||
}
|
||||
this.nameplateCache.get({
|
||||
ctx: ctx,
|
||||
x: 259,
|
||||
y: 92,
|
||||
w: 273,
|
||||
h: 66,
|
||||
id: p.toString() + "p" + name,
|
||||
}, ctx => {
|
||||
this.draw.nameplate({
|
||||
ctx: ctx,
|
||||
x: 3,
|
||||
y: 3,
|
||||
name: name,
|
||||
font: this.font,
|
||||
blue: p === 1
|
||||
})
|
||||
})
|
||||
|
||||
if(this.controller.autoPlayEnabled){
|
||||
ctx.drawImage(assets.image["badge_auto"],
|
||||
431, 311, 34, 34
|
||||
@ -581,7 +615,7 @@ class Scoresheet{
|
||||
if(this.tetsuoHanaClass){
|
||||
this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
|
||||
}
|
||||
this.tetsuoHanaClass = rules.clearReached(this.results.gauge) ? "dance" : "failed"
|
||||
this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed"
|
||||
this.tetsuoHana.classList.add(this.tetsuoHanaClass)
|
||||
}
|
||||
}
|
||||
@ -595,32 +629,32 @@ class Scoresheet{
|
||||
ctx.translate(frameLeft, frameTop)
|
||||
|
||||
for(var p = 0; p < players; p++){
|
||||
var results = this.results
|
||||
var results = this.results[p]
|
||||
if(!results){
|
||||
continue
|
||||
}
|
||||
if(p === 1){
|
||||
results = p2.results
|
||||
ctx.translate(0, p2Offset)
|
||||
}
|
||||
var gaugePercent = rules.gaugePercent(results.gauge)
|
||||
var w = 712
|
||||
this.draw.gauge({
|
||||
ctx: ctx,
|
||||
x: 558 + w,
|
||||
y: p === 1 ? 124 : 116,
|
||||
clear: gaugeClear[p],
|
||||
percentage: gaugePercent,
|
||||
clear: this.rules[p].gaugeClear,
|
||||
percentage: this.rules[p].gaugePercent(results.gauge),
|
||||
font: this.font,
|
||||
scale: w / 788,
|
||||
scoresheet: true,
|
||||
blue: p === 1,
|
||||
multiplayer: p === 1
|
||||
})
|
||||
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules
|
||||
this.draw.soul({
|
||||
ctx: ctx,
|
||||
x: 1215,
|
||||
y: 144,
|
||||
scale: 36 / 42,
|
||||
cleared: playerRules.clearReached(results.gauge)
|
||||
cleared: this.rules[p].clearReached(results.gauge)
|
||||
})
|
||||
}
|
||||
})
|
||||
@ -633,13 +667,12 @@ class Scoresheet{
|
||||
var noCrownResultWait = -2000;
|
||||
|
||||
for(var p = 0; p < players; p++){
|
||||
var results = this.results
|
||||
if(p === 1){
|
||||
results = p2.results
|
||||
var results = this.results[p]
|
||||
if(!results){
|
||||
continue
|
||||
}
|
||||
var crownType = null
|
||||
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules
|
||||
if(playerRules.clearReached(results.gauge)){
|
||||
if(this.rules[p].clearReached(results.gauge)){
|
||||
crownType = results.bad === "0" ? "gold" : "silver"
|
||||
}
|
||||
if(crownType !== null){
|
||||
@ -702,7 +735,10 @@ class Scoresheet{
|
||||
var times = {}
|
||||
var lastTime = 0
|
||||
for(var p = 0; p < players; p++){
|
||||
var results = p === 0 ? this.results : p2.results
|
||||
var results = this.results[p]
|
||||
if(!results){
|
||||
continue
|
||||
}
|
||||
var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame
|
||||
if(currentTime > lastTime){
|
||||
lastTime = currentTime
|
||||
@ -711,7 +747,10 @@ class Scoresheet{
|
||||
for(var i in printNumbers){
|
||||
var largestTime = 0
|
||||
for(var p = 0; p < players; p++){
|
||||
var results = p === 0 ? this.results : p2.results
|
||||
var results = this.results[p]
|
||||
if(!results){
|
||||
continue
|
||||
}
|
||||
times[printNumbers[i]] = lastTime + 500
|
||||
var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
|
||||
if(currentTime > largestTime){
|
||||
@ -727,9 +766,11 @@ class Scoresheet{
|
||||
}
|
||||
|
||||
for(var p = 0; p < players; p++){
|
||||
var results = this.results
|
||||
var results = this.results[p]
|
||||
if(!results){
|
||||
continue
|
||||
}
|
||||
if(p === 1){
|
||||
results = p2.results
|
||||
ctx.translate(0, p2Offset)
|
||||
}
|
||||
ctx.save()
|
||||
@ -823,7 +864,7 @@ class Scoresheet{
|
||||
|
||||
if(elapsed >= 1000){
|
||||
this.clean()
|
||||
this.controller.songSelection(true)
|
||||
this.controller.songSelection(true, this.showWarning)
|
||||
}
|
||||
}
|
||||
|
||||
@ -890,10 +931,14 @@ class Scoresheet{
|
||||
delete this.resultsObj.title
|
||||
delete this.resultsObj.difficulty
|
||||
delete this.resultsObj.gauge
|
||||
scoreStorage.add(hash, difficulty, this.resultsObj, true, title)
|
||||
scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => {
|
||||
this.showWarning = {name: "scoreSaveFailed"}
|
||||
})
|
||||
}else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){
|
||||
oldScore.crown = crown
|
||||
scoreStorage.add(hash, difficulty, oldScore, true, title)
|
||||
scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => {
|
||||
this.showWarning = {name: "scoreSaveFailed"}
|
||||
})
|
||||
}
|
||||
}
|
||||
this.scoreSaved = true
|
||||
@ -908,7 +953,7 @@ class Scoresheet{
|
||||
snd.buffer.loadSettings()
|
||||
this.redrawRunning = false
|
||||
pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
|
||||
if(this.multiplayer !== 2 && this.touchEnabled){
|
||||
if(this.touchEnabled){
|
||||
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
|
||||
}
|
||||
if(this.session){
|
||||
@ -920,5 +965,7 @@ class Scoresheet{
|
||||
delete this.ctx
|
||||
delete this.canvas
|
||||
delete this.fadeScreen
|
||||
delete this.results
|
||||
delete this.rules
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,38 @@
|
||||
class ScoreStorage{
|
||||
constructor(){
|
||||
this.scores = {}
|
||||
this.scoresP2 = {}
|
||||
this.requestP2 = new Set()
|
||||
this.requestedP2 = new Set()
|
||||
this.songTitles = {}
|
||||
this.difficulty = ["oni", "ura", "hard", "normal", "easy"]
|
||||
this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"]
|
||||
this.crownValue = ["", "silver", "gold"]
|
||||
this.load()
|
||||
}
|
||||
load(){
|
||||
this.scores = {}
|
||||
this.scoreStrings = {}
|
||||
try{
|
||||
var localScores = localStorage.getItem("scoreStorage")
|
||||
if(localScores){
|
||||
this.scoreStrings = JSON.parse(localScores)
|
||||
}
|
||||
}catch(e){}
|
||||
for(var hash in this.scoreStrings){
|
||||
var scoreString = this.scoreStrings[hash]
|
||||
load(strings, loadFailed){
|
||||
var scores = {}
|
||||
var scoreStrings = {}
|
||||
if(loadFailed){
|
||||
try{
|
||||
var localScores = localStorage.getItem("saveFailed")
|
||||
if(localScores){
|
||||
scoreStrings = JSON.parse(localScores)
|
||||
}
|
||||
}catch(e){}
|
||||
}else if(strings){
|
||||
scoreStrings = this.prepareStrings(strings)
|
||||
}else if(account.loggedIn){
|
||||
return
|
||||
}else{
|
||||
try{
|
||||
var localScores = localStorage.getItem("scoreStorage")
|
||||
if(localScores){
|
||||
scoreStrings = JSON.parse(localScores)
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
for(var hash in scoreStrings){
|
||||
var scoreString = scoreStrings[hash]
|
||||
var songAdded = false
|
||||
if(typeof scoreString === "string" && scoreString){
|
||||
var diffArray = scoreString.split(";")
|
||||
@ -37,25 +52,63 @@ class ScoreStorage{
|
||||
score[name] = value
|
||||
}
|
||||
if(!songAdded){
|
||||
this.scores[hash] = {title: null}
|
||||
scores[hash] = {title: null}
|
||||
songAdded = true
|
||||
}
|
||||
this.scores[hash][this.difficulty[i]] = score
|
||||
scores[hash][this.difficulty[i]] = score
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(loadFailed){
|
||||
for(var hash in scores){
|
||||
for(var i in this.difficulty){
|
||||
var diff = this.difficulty[i]
|
||||
if(scores[hash][diff]){
|
||||
this.add(hash, diff, scores[hash][diff], true, this.songTitles[hash] || null).then(() => {
|
||||
localStorage.removeItem("saveFailed")
|
||||
}, () => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
this.scores = scores
|
||||
this.scoreStrings = scoreStrings
|
||||
}
|
||||
if(strings){
|
||||
this.load(false, true)
|
||||
}
|
||||
}
|
||||
prepareScores(scores){
|
||||
var output = []
|
||||
for (var k in scores) {
|
||||
output.push({'hash': k, 'score': scores[k]})
|
||||
}
|
||||
return output
|
||||
}
|
||||
prepareStrings(scores){
|
||||
var output = {}
|
||||
for(var k in scores){
|
||||
output[scores[k].hash] = scores[k].score
|
||||
}
|
||||
return output
|
||||
}
|
||||
save(){
|
||||
for(var hash in this.scores){
|
||||
this.writeString(hash)
|
||||
}
|
||||
this.write()
|
||||
return this.sendToServer({
|
||||
scores: this.prepareScores(this.scoreStrings),
|
||||
is_import: true
|
||||
})
|
||||
}
|
||||
write(){
|
||||
try{
|
||||
localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings))
|
||||
}catch(e){}
|
||||
if(!account.loggedIn){
|
||||
try{
|
||||
localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings))
|
||||
}catch(e){}
|
||||
}
|
||||
}
|
||||
writeString(hash){
|
||||
var score = this.scores[hash]
|
||||
@ -101,17 +154,82 @@ class ScoreStorage{
|
||||
}
|
||||
}
|
||||
}
|
||||
add(song, difficulty, scoreObject, isHash, setTitle){
|
||||
getP2(song, difficulty, isHash){
|
||||
if(!song){
|
||||
return this.scoresP2
|
||||
}else{
|
||||
var hash = isHash ? song : this.titleHash(song)
|
||||
if(!(hash in this.scoresP2) && !this.requestP2.has(hash) && !this.requestedP2.has(hash)){
|
||||
this.requestP2.add(hash)
|
||||
this.requestedP2.add(hash)
|
||||
}
|
||||
if(difficulty){
|
||||
if(hash in this.scoresP2){
|
||||
return this.scoresP2[hash][difficulty]
|
||||
}
|
||||
}else{
|
||||
return this.scoresP2[hash]
|
||||
}
|
||||
}
|
||||
}
|
||||
add(song, difficulty, scoreObject, isHash, setTitle, saveFailed){
|
||||
var hash = isHash ? song : this.titleHash(song)
|
||||
if(!(hash in this.scores)){
|
||||
this.scores[hash] = {}
|
||||
}
|
||||
if(setTitle){
|
||||
this.scores[hash].title = setTitle
|
||||
if(difficulty){
|
||||
if(setTitle){
|
||||
this.scores[hash].title = setTitle
|
||||
}
|
||||
this.scores[hash][difficulty] = scoreObject
|
||||
}else{
|
||||
this.scores[hash] = scoreObject
|
||||
if(setTitle){
|
||||
this.scores[hash].title = setTitle
|
||||
}
|
||||
}
|
||||
this.scores[hash][difficulty] = scoreObject
|
||||
this.writeString(hash)
|
||||
this.write()
|
||||
if(saveFailed){
|
||||
var failedScores = {}
|
||||
try{
|
||||
var localScores = localStorage.getItem("saveFailed")
|
||||
if(localScores){
|
||||
failedScores = JSON.parse(localScores)
|
||||
}
|
||||
}catch(e){}
|
||||
if(!(hash in failedScores)){
|
||||
failedScores[hash] = {}
|
||||
}
|
||||
failedScores[hash] = this.scoreStrings[hash]
|
||||
try{
|
||||
localStorage.setItem("saveFailed", JSON.stringify(failedScores))
|
||||
}catch(e){}
|
||||
return Promise.reject()
|
||||
}else{
|
||||
var obj = {}
|
||||
obj[hash] = this.scoreStrings[hash]
|
||||
return this.sendToServer({
|
||||
scores: this.prepareScores(obj)
|
||||
}).catch(() => this.add(song, difficulty, scoreObject, isHash, setTitle, true))
|
||||
}
|
||||
}
|
||||
addP2(song, difficulty, scoreObject, isHash, setTitle){
|
||||
var hash = isHash ? song : this.titleHash(song)
|
||||
if(!(hash in this.scores)){
|
||||
this.scoresP2[hash] = {}
|
||||
}
|
||||
if(difficulty){
|
||||
if(setTitle){
|
||||
this.scoresP2[hash].title = setTitle
|
||||
}
|
||||
this.scoresP2[hash][difficulty] = scoreObject
|
||||
}else{
|
||||
this.scoresP2[hash] = scoreObject
|
||||
if(setTitle){
|
||||
this.scoresP2[hash].title = setTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
template(){
|
||||
var template = {crown: ""}
|
||||
@ -146,6 +264,62 @@ class ScoreStorage{
|
||||
delete this.scoreStrings[hash]
|
||||
}
|
||||
this.write()
|
||||
this.sendToServer({
|
||||
scores: this.prepareScores(this.scoreStrings),
|
||||
is_import: true
|
||||
})
|
||||
}
|
||||
}
|
||||
sendToServer(obj, retry){
|
||||
if(account.loggedIn){
|
||||
return loader.getCsrfToken().then(token => {
|
||||
var request = new XMLHttpRequest()
|
||||
request.open("POST", "api/scores/save")
|
||||
var promise = pageEvents.load(request).then(response => {
|
||||
if(request.status !== 200){
|
||||
return Promise.reject()
|
||||
}
|
||||
}).catch(() => {
|
||||
if(retry){
|
||||
this.scoreSaveFailed = true
|
||||
account.loggedIn = false
|
||||
delete account.username
|
||||
delete account.displayName
|
||||
this.load()
|
||||
pageEvents.send("logout")
|
||||
return Promise.reject()
|
||||
}else{
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
}, 3000)
|
||||
}).then(() => this.sendToServer(obj, true))
|
||||
}
|
||||
})
|
||||
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
|
||||
request.setRequestHeader("X-CSRFToken", token)
|
||||
request.send(JSON.stringify(obj))
|
||||
return promise
|
||||
})
|
||||
}else{
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
eventLoop(){
|
||||
if(p2.session && this.requestP2.size){
|
||||
var req = []
|
||||
this.requestP2.forEach(hash => {
|
||||
req.push(hash)
|
||||
})
|
||||
this.requestP2.clear()
|
||||
if(req.length){
|
||||
p2.send("getcrowns", req)
|
||||
}
|
||||
}
|
||||
}
|
||||
clearP2(){
|
||||
this.scoresP2 = {}
|
||||
this.requestP2.clear()
|
||||
this.requestedP2.clear()
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,10 @@ class Session{
|
||||
pageEvents.send("session-start", "host")
|
||||
}
|
||||
})
|
||||
p2.send("invite")
|
||||
p2.send("invite", {
|
||||
id: null,
|
||||
name: account.loggedIn ? account.displayName : null
|
||||
})
|
||||
pageEvents.send("session")
|
||||
}
|
||||
getElement(name){
|
||||
|
@ -50,6 +50,10 @@ class Settings{
|
||||
easierBigNotes: {
|
||||
type: "toggle",
|
||||
default: false
|
||||
},
|
||||
showLyrics: {
|
||||
type: "toggle",
|
||||
default: true
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -126,8 +126,14 @@
|
||||
this.comboCache = new CanvasCache(noSmoothing)
|
||||
this.pauseCache = new CanvasCache(noSmoothing)
|
||||
this.branchCache = new CanvasCache(noSmoothing)
|
||||
this.nameplateCache = new CanvasCache(noSmoothing)
|
||||
|
||||
this.multiplayer = this.controller.multiplayer
|
||||
if(this.multiplayer === 2){
|
||||
this.player = p2.player === 2 ? 1 : 2
|
||||
}else{
|
||||
this.player = this.controller.multiplayer ? p2.player : 1
|
||||
}
|
||||
|
||||
this.touchEnabled = this.controller.touchEnabled
|
||||
this.touch = -Infinity
|
||||
@ -223,24 +229,31 @@
|
||||
this.winH = winH
|
||||
this.ratio = ratio
|
||||
|
||||
if(this.multiplayer !== 2){
|
||||
if(this.player !== 2){
|
||||
this.canvas.width = winW
|
||||
this.canvas.height = winH
|
||||
ctx.scale(ratio, ratio)
|
||||
this.canvas.style.width = (winW / this.pixelRatio) + "px"
|
||||
this.canvas.style.height = (winH / this.pixelRatio) + "px"
|
||||
|
||||
this.titleCache.resize(640, 90, ratio)
|
||||
}
|
||||
if(!this.multiplayer){
|
||||
this.pauseCache.resize(81 * this.pauseOptions.length * 2, 464, ratio)
|
||||
}
|
||||
if(this.portrait){
|
||||
this.nameplateCache.resize(220, 54, ratio + 0.2)
|
||||
}else{
|
||||
this.nameplateCache.resize(274, 67, ratio + 0.2)
|
||||
}
|
||||
this.fillComboCache()
|
||||
this.setDonBgHeight()
|
||||
if(this.controller.lyrics){
|
||||
this.controller.lyrics.setScale(ratio / this.pixelRatio)
|
||||
}
|
||||
resized = true
|
||||
}else if(this.controller.game.paused && !document.hasFocus()){
|
||||
return
|
||||
}else if(this.multiplayer !== 2){
|
||||
}else if(this.player !== 2){
|
||||
ctx.clearRect(0, 0, winW / ratio, winH / ratio)
|
||||
}
|
||||
winW /= ratio
|
||||
@ -257,8 +270,8 @@
|
||||
var frameTop = winH / 2 - 720 / 2
|
||||
var frameLeft = winW / 2 - 1280 / 2
|
||||
}
|
||||
if(this.multiplayer === 2){
|
||||
frameTop += this.multiplayer === 2 ? 165 : 176
|
||||
if(this.player === 2){
|
||||
frameTop += 165
|
||||
}
|
||||
if(touchMultiplayer){
|
||||
if(!this.touchp2Class){
|
||||
@ -273,16 +286,20 @@
|
||||
this.setDonBgHeight()
|
||||
}
|
||||
|
||||
if(this.controller.lyrics){
|
||||
this.controller.lyrics.update(ms)
|
||||
}
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(0, frameTop)
|
||||
|
||||
this.drawGogoTime()
|
||||
|
||||
if(!touchMultiplayer || this.multiplayer === 1 && frameTop >= 0){
|
||||
if(!touchMultiplayer || this.player === 1 && frameTop >= 0){
|
||||
this.assets.drawAssets("background")
|
||||
}
|
||||
|
||||
if(this.multiplayer !== 2){
|
||||
if(this.player !== 2){
|
||||
this.titleCache.get({
|
||||
ctx: ctx,
|
||||
x: winW - (touchMultiplayer && fullScreenSupported ? 750 : 650),
|
||||
@ -350,7 +367,7 @@
|
||||
var score = this.controller.getGlobalScore()
|
||||
var gaugePercent = this.rules.gaugePercent(score.gauge)
|
||||
|
||||
if(this.multiplayer === 2){
|
||||
if(this.player === 2){
|
||||
var scoreImg = "bg_score_p2"
|
||||
var scoreFill = "#6bbec0"
|
||||
}else{
|
||||
@ -373,30 +390,55 @@
|
||||
size: 100,
|
||||
paddingLeft: 0
|
||||
}
|
||||
this.scorePos = {x: 363, y: frameTop + (this.multiplayer === 2 ? 520 : 227)}
|
||||
this.scorePos = {x: 363, y: frameTop + (this.player === 2 ? 520 : 227)}
|
||||
|
||||
var animPos = {
|
||||
x1: this.slotPos.x + 13,
|
||||
y1: this.slotPos.y + (this.multiplayer === 2 ? 27 : -27),
|
||||
y1: this.slotPos.y + (this.player === 2 ? 27 : -27),
|
||||
x2: winW - 38,
|
||||
y2: frameTop + (this.multiplayer === 2 ? 484 : 293)
|
||||
y2: frameTop + (this.player === 2 ? 484 : 293)
|
||||
}
|
||||
var taikoPos = {
|
||||
x: 19,
|
||||
y: frameTop + (this.multiplayer === 2 ? 464 : 184),
|
||||
y: frameTop + (this.player === 2 ? 464 : 184),
|
||||
w: 111,
|
||||
h: 130
|
||||
}
|
||||
|
||||
this.nameplateCache.get({
|
||||
ctx: ctx,
|
||||
x: 167,
|
||||
y: this.player === 2 ? 565 : 160,
|
||||
w: 219,
|
||||
h: 53,
|
||||
id: "1p",
|
||||
}, ctx => {
|
||||
var defaultName = this.player === 1 ? strings.defaultName : strings.default2PName
|
||||
if(this.multiplayer === 2){
|
||||
var name = p2.name || defaultName
|
||||
}else{
|
||||
var name = account.loggedIn ? account.displayName : defaultName
|
||||
}
|
||||
this.draw.nameplate({
|
||||
ctx: ctx,
|
||||
x: 3,
|
||||
y: 3,
|
||||
scale: 0.8,
|
||||
name: name,
|
||||
font: this.font,
|
||||
blue: this.player === 2
|
||||
})
|
||||
})
|
||||
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.fillRect(
|
||||
0,
|
||||
this.multiplayer === 2 ? 306 : 288,
|
||||
this.player === 2 ? 306 : 288,
|
||||
winW,
|
||||
this.multiplayer === 1 ? 184 : 183
|
||||
this.player === 1 ? 184 : 183
|
||||
)
|
||||
ctx.beginPath()
|
||||
if(this.multiplayer === 2){
|
||||
if(this.player === 2){
|
||||
ctx.moveTo(0, 467)
|
||||
ctx.lineTo(384, 467)
|
||||
ctx.lineTo(384, 512)
|
||||
@ -415,7 +457,7 @@
|
||||
ctx.fillStyle = scoreFill
|
||||
var leftSide = (ctx, mul) => {
|
||||
ctx.beginPath()
|
||||
if(this.multiplayer === 2){
|
||||
if(this.player === 2){
|
||||
ctx.moveTo(0, 468 * mul)
|
||||
ctx.lineTo(380 * mul, 468 * mul)
|
||||
ctx.lineTo(380 * mul, 512 * mul)
|
||||
@ -445,7 +487,7 @@
|
||||
// Score background
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.beginPath()
|
||||
if(this.multiplayer === 2){
|
||||
if(this.player === 2){
|
||||
this.draw.roundedCorner(ctx, 184, 512, 20, 0)
|
||||
ctx.lineTo(384, 512)
|
||||
this.draw.roundedCorner(ctx, 384, 560, 12, 2)
|
||||
@ -463,16 +505,16 @@
|
||||
ctx.drawImage(assets.image["difficulty"],
|
||||
0, 144 * this.difficulty[this.controller.selectedSong.difficulty],
|
||||
168, 143,
|
||||
126, this.multiplayer === 2 ? 497 : 228,
|
||||
126, this.player === 2 ? 497 : 228,
|
||||
62, 53
|
||||
)
|
||||
}
|
||||
|
||||
// Badges
|
||||
if(this.controller.autoPlayEnabled && !this.controller.multiplayer){
|
||||
if(this.controller.autoPlayEnabled && !this.multiplayer){
|
||||
this.ctx.drawImage(assets.image["badge_auto"],
|
||||
183,
|
||||
this.multiplayer === 2 ? 490 : 265,
|
||||
this.player === 2 ? 490 : 265,
|
||||
23,
|
||||
23
|
||||
)
|
||||
@ -482,7 +524,7 @@
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.beginPath()
|
||||
var gaugeX = winW - 788 * 0.7 - 32
|
||||
if(this.multiplayer === 2){
|
||||
if(this.player === 2){
|
||||
ctx.moveTo(gaugeX, 464)
|
||||
ctx.lineTo(winW, 464)
|
||||
ctx.lineTo(winW, 489)
|
||||
@ -497,18 +539,18 @@
|
||||
this.draw.gauge({
|
||||
ctx: ctx,
|
||||
x: winW,
|
||||
y: this.multiplayer === 2 ? 468 : 273,
|
||||
y: this.player === 2 ? 468 : 273,
|
||||
clear: this.rules.gaugeClear,
|
||||
percentage: gaugePercent,
|
||||
font: this.font,
|
||||
scale: 0.7,
|
||||
multiplayer: this.multiplayer === 2,
|
||||
blue: this.multiplayer === 2
|
||||
multiplayer: this.player === 2,
|
||||
blue: this.player === 2
|
||||
})
|
||||
this.draw.soul({
|
||||
ctx: ctx,
|
||||
x: winW - 40,
|
||||
y: this.multiplayer === 2 ? 484 : 293,
|
||||
y: this.player === 2 ? 484 : 293,
|
||||
scale: 0.75,
|
||||
cleared: this.rules.clearReached(score.gauge)
|
||||
})
|
||||
@ -536,26 +578,50 @@
|
||||
}
|
||||
this.scorePos = {
|
||||
x: 155,
|
||||
y: frameTop + (this.multiplayer === 2 ? 318 : 193)
|
||||
y: frameTop + (this.player === 2 ? 318 : 193)
|
||||
}
|
||||
|
||||
var animPos = {
|
||||
x1: this.slotPos.x + 14,
|
||||
y1: this.slotPos.y + (this.multiplayer === 2 ? 29 : -29),
|
||||
y1: this.slotPos.y + (this.player === 2 ? 29 : -29),
|
||||
x2: winW - 55,
|
||||
y2: frameTop + (this.multiplayer === 2 ? 378 : 165)
|
||||
y2: frameTop + (this.player === 2 ? 378 : 165)
|
||||
}
|
||||
var taikoPos = {x: 179, y: frameTop + 190, w: 138, h: 162}
|
||||
|
||||
this.nameplateCache.get({
|
||||
ctx: ctx,
|
||||
x: 320,
|
||||
y: this.player === 2 ? 460 : 20,
|
||||
w: 273,
|
||||
h: 66,
|
||||
id: "1p",
|
||||
}, ctx => {
|
||||
var defaultName = this.player === 1 ? strings.defaultName : strings.default2PName
|
||||
if(this.multiplayer === 2){
|
||||
var name = p2.name || defaultName
|
||||
}else{
|
||||
var name = account.loggedIn ? account.displayName : defaultName
|
||||
}
|
||||
this.draw.nameplate({
|
||||
ctx: ctx,
|
||||
x: 3,
|
||||
y: 3,
|
||||
name: name,
|
||||
font: this.font,
|
||||
blue: this.player === 2
|
||||
})
|
||||
})
|
||||
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.fillRect(
|
||||
0,
|
||||
184,
|
||||
winW,
|
||||
this.multiplayer === 1 ? 177 : 176
|
||||
this.multiplayer && this.player === 1 ? 177 : 176
|
||||
)
|
||||
ctx.beginPath()
|
||||
if(this.multiplayer === 2){
|
||||
if(this.player === 2){
|
||||
ctx.moveTo(328, 351)
|
||||
ctx.lineTo(winW, 351)
|
||||
ctx.lineTo(winW, 385)
|
||||
@ -572,17 +638,17 @@
|
||||
this.draw.gauge({
|
||||
ctx: ctx,
|
||||
x: winW,
|
||||
y: this.multiplayer === 2 ? 357 : 135,
|
||||
y: this.player === 2 ? 357 : 135,
|
||||
clear: this.rules.gaugeClear,
|
||||
percentage: gaugePercent,
|
||||
font: this.font,
|
||||
multiplayer: this.multiplayer === 2,
|
||||
blue: this.multiplayer === 2
|
||||
multiplayer: this.player === 2,
|
||||
blue: this.player === 2
|
||||
})
|
||||
this.draw.soul({
|
||||
ctx: ctx,
|
||||
x: winW - 57,
|
||||
y: this.multiplayer === 2 ? 378 : 165,
|
||||
y: this.player === 2 ? 378 : 165,
|
||||
cleared: this.rules.clearReached(score.gauge)
|
||||
})
|
||||
|
||||
@ -614,7 +680,7 @@
|
||||
ctx.drawImage(assets.image["difficulty"],
|
||||
0, 144 * this.difficulty[this.controller.selectedSong.difficulty],
|
||||
168, 143,
|
||||
16, this.multiplayer === 2 ? 194 : 232,
|
||||
16, this.player === 2 ? 194 : 232,
|
||||
141, 120
|
||||
)
|
||||
var diff = this.controller.selectedSong.difficulty
|
||||
@ -626,13 +692,13 @@
|
||||
ctx.fillStyle = "#fff"
|
||||
ctx.lineWidth = 7
|
||||
ctx.miterLimit = 1
|
||||
ctx.strokeText(text, 87, this.multiplayer === 2 ? 310 : 348)
|
||||
ctx.fillText(text, 87, this.multiplayer === 2 ? 310 : 348)
|
||||
ctx.strokeText(text, 87, this.player === 2 ? 310 : 348)
|
||||
ctx.fillText(text, 87, this.player === 2 ? 310 : 348)
|
||||
ctx.miterLimit = 10
|
||||
}
|
||||
|
||||
// Badges
|
||||
if(this.controller.autoPlayEnabled && !this.controller.multiplayer){
|
||||
if(this.controller.autoPlayEnabled && !this.multiplayer){
|
||||
this.ctx.drawImage(assets.image["badge_auto"],
|
||||
125, 235, 34, 34
|
||||
)
|
||||
@ -641,7 +707,7 @@
|
||||
// Score background
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.beginPath()
|
||||
if(this.multiplayer === 2){
|
||||
if(this.player === 2){
|
||||
ctx.moveTo(0, 312)
|
||||
this.draw.roundedCorner(ctx, 176, 312, 20, 1)
|
||||
ctx.lineTo(176, 353)
|
||||
@ -666,11 +732,11 @@
|
||||
}, {
|
||||
// 560, 10
|
||||
x: animPos.x1 + animPos.w / 6,
|
||||
y: animPos.y1 - animPos.h * (this.multiplayer === 2 ? 2.5 : 3.5)
|
||||
y: animPos.y1 - animPos.h * (this.player === 2 ? 2.5 : 3.5)
|
||||
}, {
|
||||
// 940, -150
|
||||
x: animPos.x2 - animPos.w / 3,
|
||||
y: animPos.y2 - animPos.h * (this.multiplayer === 2 ? 3.5 : 5)
|
||||
y: animPos.y2 - animPos.h * (this.player === 2 ? 3.5 : 5)
|
||||
}, {
|
||||
// 1225, 165
|
||||
x: animPos.x2,
|
||||
@ -1390,12 +1456,12 @@
|
||||
var selectedSong = this.controller.selectedSong
|
||||
var songSkinName = selectedSong.songSkin.name
|
||||
var donLayers = []
|
||||
var filename = !selectedSong.songSkin.don && this.multiplayer === 2 ? "bg_don2_" : "bg_don_"
|
||||
var filename = !selectedSong.songSkin.don && this.player === 2 ? "bg_don2_" : "bg_don_"
|
||||
var prefix = ""
|
||||
|
||||
this.donBg = document.createElement("div")
|
||||
this.donBg.classList.add("donbg")
|
||||
if(this.multiplayer === 2){
|
||||
if(this.player === 2){
|
||||
this.donBg.classList.add("donbg-bottom")
|
||||
}
|
||||
for(var layer = 1; layer <= 3; layer++){
|
||||
@ -1525,17 +1591,21 @@
|
||||
// Start animation to gauge
|
||||
circle.animate(ms)
|
||||
}
|
||||
if(ms - this.controller.audioLatency >= circle.ms && !circle.beatMSCopied && (!circle.branch || circle.branch.active)){
|
||||
if(this.beatInterval !== circle.beatMS){
|
||||
this.changeBeatInterval(circle.beatMS)
|
||||
}
|
||||
var game = this.controller.game
|
||||
for(var i = 0; i < game.songData.events.length; i++){
|
||||
var event = game.songData.events[i]
|
||||
if(ms - this.controller.audioLatency >= event.ms && !event.beatMSCopied && (!event.branch || event.branch.active)){
|
||||
if(this.beatInterval !== event.beatMS){
|
||||
this.changeBeatInterval(event.beatMS)
|
||||
}
|
||||
circle.beatMSCopied = true
|
||||
event.beatMSCopied = true
|
||||
}
|
||||
if(ms - this.controller.audioLatency >= circle.ms && !circle.gogoChecked && (!circle.branch || circle.branch.active)){
|
||||
if(this.gogoTime != circle.gogoTime){
|
||||
this.toggleGogoTime(circle)
|
||||
if(ms - this.controller.audioLatency >= event.ms && !event.gogoChecked && (!event.branch || event.branch.active)){
|
||||
if(this.gogoTime != event.gogoTime){
|
||||
this.toggleGogoTime(event)
|
||||
}
|
||||
circle.gogoChecked = true
|
||||
event.gogoChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ class ViewAssets{
|
||||
sw: imgw,
|
||||
sh: imgh - 1,
|
||||
x: view.portrait ? -60 : 0,
|
||||
y: view.portrait ? (view.multiplayer === 2 ? 560 : 35) : (view.multiplayer === 2 ? 360 : 2),
|
||||
y: view.portrait ? (view.player === 2 ? 560 : 35) : (view.player === 2 ? 360 : 2),
|
||||
w: w,
|
||||
h: h - 1
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="view">
|
||||
<div class="view-title stroke-sub"></div>
|
||||
<div class="view-content"></div>
|
||||
<div id="diag-txt"></div>
|
||||
<div class="diag-txt"></div>
|
||||
<div class="left-buttons">
|
||||
<div id="link-issues" class="taibtn stroke-sub link-btn">
|
||||
<a target="_blank"></a>
|
||||
|
34
public/src/views/account.html
Normal file
34
public/src/views/account.html
Normal 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>
|
@ -24,6 +24,12 @@
|
||||
<div class="music-volume input-slider">
|
||||
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
|
||||
</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="autoplay-label"><input class="autoplay" type="checkbox">Auto play</label>
|
||||
<div class="bottom-btns">
|
||||
|
@ -8,6 +8,7 @@
|
||||
<div id="touch-drum-img"></div>
|
||||
</div>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="song-lyrics"></div>
|
||||
<div id="touch-buttons">
|
||||
<div id="touch-full-btn"></div><div id="touch-pause-btn"></div>
|
||||
</div>
|
||||
|
@ -2,3 +2,10 @@
|
||||
<div class="progress"></div>
|
||||
<span class="percentage">0%</span>
|
||||
</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>
|
||||
|
25
public/src/views/login.html
Normal file
25
public/src/views/login.html
Normal 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
73
schema.py
Normal 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'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
93
server.py
93
server.py
@ -13,11 +13,11 @@ server_status = {
|
||||
}
|
||||
consonants = "bcdfghjklmnpqrstvwxyz"
|
||||
|
||||
def msgobj(type, value=None):
|
||||
def msgobj(msg_type, value=None):
|
||||
if value == None:
|
||||
return json.dumps({"type": type})
|
||||
return json.dumps({"type": msg_type})
|
||||
else:
|
||||
return json.dumps({"type": type, "value": value})
|
||||
return json.dumps({"type": msg_type, "value": value})
|
||||
|
||||
def status_event():
|
||||
value = []
|
||||
@ -42,7 +42,8 @@ async def connection(ws, path):
|
||||
user = {
|
||||
"ws": ws,
|
||||
"action": "ready",
|
||||
"session": False
|
||||
"session": False,
|
||||
"name": None
|
||||
}
|
||||
server_status["users"].append(user)
|
||||
try:
|
||||
@ -69,16 +70,17 @@ async def connection(ws, path):
|
||||
except json.decoder.JSONDecodeError:
|
||||
data = {}
|
||||
action = user["action"]
|
||||
type = data["type"] if "type" in data else None
|
||||
msg_type = data["type"] if "type" in data else None
|
||||
value = data["value"] if "value" in data else None
|
||||
if action == "ready":
|
||||
# Not playing or waiting
|
||||
if type == "join":
|
||||
if msg_type == "join":
|
||||
if value == None:
|
||||
continue
|
||||
waiting = server_status["waiting"]
|
||||
id = value["id"] if "id" in value else None
|
||||
diff = value["diff"] if "diff" in value else None
|
||||
user["name"] = value["name"] if "name" in value else None
|
||||
if not id or not diff:
|
||||
continue
|
||||
if id not in waiting:
|
||||
@ -92,6 +94,7 @@ async def connection(ws, path):
|
||||
await ws.send(msgobj("waiting"))
|
||||
else:
|
||||
# Join the other user and start game
|
||||
user["name"] = value["name"] if "name" in value else None
|
||||
user["other_user"] = waiting[id]["user"]
|
||||
waiting_diff = waiting[id]["diff"]
|
||||
del waiting[id]
|
||||
@ -99,9 +102,13 @@ async def connection(ws, path):
|
||||
user["action"] = "loading"
|
||||
user["other_user"]["action"] = "loading"
|
||||
user["other_user"]["other_user"] = user
|
||||
user["other_user"]["player"] = 1
|
||||
user["player"] = 2
|
||||
await asyncio.wait([
|
||||
ws.send(msgobj("gameload", waiting_diff)),
|
||||
user["other_user"]["ws"].send(msgobj("gameload", diff))
|
||||
ws.send(msgobj("gameload", {"diff": waiting_diff, "player": 2})),
|
||||
user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff, "player": 1})),
|
||||
ws.send(msgobj("name", user["other_user"]["name"])),
|
||||
user["other_user"]["ws"].send(msgobj("name", user["name"]))
|
||||
])
|
||||
else:
|
||||
# Wait for another user
|
||||
@ -115,28 +122,33 @@ async def connection(ws, path):
|
||||
await ws.send(msgobj("waiting"))
|
||||
# Update others on waiting players
|
||||
await notify_status()
|
||||
elif type == "invite":
|
||||
if value == None:
|
||||
elif msg_type == "invite":
|
||||
if value and "id" in value and value["id"] == None:
|
||||
# Session invite link requested
|
||||
invite = get_invite()
|
||||
server_status["invites"][invite] = user
|
||||
user["action"] = "invite"
|
||||
user["session"] = invite
|
||||
user["name"] = value["name"] if "name" in value else None
|
||||
await ws.send(msgobj("invite", invite))
|
||||
elif value in server_status["invites"]:
|
||||
elif value and "id" in value and value["id"] in server_status["invites"]:
|
||||
# Join a session with the other user
|
||||
user["other_user"] = server_status["invites"][value]
|
||||
del server_status["invites"][value]
|
||||
user["name"] = value["name"] if "name" in value else None
|
||||
user["other_user"] = server_status["invites"][value["id"]]
|
||||
del server_status["invites"][value["id"]]
|
||||
if "ws" in user["other_user"]:
|
||||
user["other_user"]["other_user"] = user
|
||||
user["action"] = "invite"
|
||||
user["session"] = value
|
||||
sent_msg = msgobj("session")
|
||||
user["session"] = value["id"]
|
||||
user["other_user"]["player"] = 1
|
||||
user["player"] = 2
|
||||
await asyncio.wait([
|
||||
ws.send(sent_msg),
|
||||
user["other_user"]["ws"].send(sent_msg)
|
||||
ws.send(msgobj("session", {"player": 2})),
|
||||
user["other_user"]["ws"].send(msgobj("session", {"player": 1})),
|
||||
ws.send(msgobj("invite")),
|
||||
ws.send(msgobj("name", user["other_user"]["name"])),
|
||||
user["other_user"]["ws"].send(msgobj("name", user["name"]))
|
||||
])
|
||||
await ws.send(msgobj("invite"))
|
||||
else:
|
||||
del user["other_user"]
|
||||
await ws.send(msgobj("gameend"))
|
||||
@ -145,7 +157,7 @@ async def connection(ws, path):
|
||||
await ws.send(msgobj("gameend"))
|
||||
elif action == "waiting" or action == "loading" or action == "loaded":
|
||||
# Waiting for another user
|
||||
if type == "leave":
|
||||
if msg_type == "leave":
|
||||
# Stop waiting
|
||||
if user["session"]:
|
||||
if "other_user" in user and "ws" in user["other_user"]:
|
||||
@ -170,7 +182,7 @@ async def connection(ws, path):
|
||||
notify_status()
|
||||
])
|
||||
if action == "loading":
|
||||
if type == "gamestart":
|
||||
if msg_type == "gamestart":
|
||||
user["action"] = "loaded"
|
||||
if user["other_user"]["action"] == "loaded":
|
||||
user["action"] = "playing"
|
||||
@ -183,12 +195,12 @@ async def connection(ws, path):
|
||||
elif action == "playing":
|
||||
# Playing with another user
|
||||
if "other_user" in user and "ws" in user["other_user"]:
|
||||
if type == "note"\
|
||||
or type == "drumroll"\
|
||||
or type == "branch"\
|
||||
or type == "gameresults":
|
||||
await user["other_user"]["ws"].send(msgobj(type, value))
|
||||
elif type == "songsel" and user["session"]:
|
||||
if msg_type == "note"\
|
||||
or msg_type == "drumroll"\
|
||||
or msg_type == "branch"\
|
||||
or msg_type == "gameresults":
|
||||
await user["other_user"]["ws"].send(msgobj(msg_type, value))
|
||||
elif msg_type == "songsel" and user["session"]:
|
||||
user["action"] = "songsel"
|
||||
user["other_user"]["action"] = "songsel"
|
||||
sent_msg1 = msgobj("songsel")
|
||||
@ -199,7 +211,7 @@ async def connection(ws, path):
|
||||
user["other_user"]["ws"].send(sent_msg1),
|
||||
user["other_user"]["ws"].send(sent_msg2)
|
||||
])
|
||||
elif type == "gameend":
|
||||
elif msg_type == "gameend":
|
||||
# User wants to disconnect
|
||||
user["action"] = "ready"
|
||||
user["other_user"]["action"] = "ready"
|
||||
@ -222,7 +234,7 @@ async def connection(ws, path):
|
||||
ws.send(status_event())
|
||||
])
|
||||
elif action == "invite":
|
||||
if type == "leave":
|
||||
if msg_type == "leave":
|
||||
# Cancel session invite
|
||||
if user["session"] in server_status["invites"]:
|
||||
del server_status["invites"][user["session"]]
|
||||
@ -243,11 +255,11 @@ async def connection(ws, path):
|
||||
ws.send(msgobj("left")),
|
||||
ws.send(status_event())
|
||||
])
|
||||
elif type == "songsel" and "other_user" in user:
|
||||
elif msg_type == "songsel" and "other_user" in user:
|
||||
if "ws" in user["other_user"]:
|
||||
user["action"] = "songsel"
|
||||
user["other_user"]["action"] = "songsel"
|
||||
sent_msg = msgobj(type)
|
||||
sent_msg = msgobj(msg_type)
|
||||
await asyncio.wait([
|
||||
ws.send(sent_msg),
|
||||
user["other_user"]["ws"].send(sent_msg)
|
||||
@ -262,15 +274,22 @@ async def connection(ws, path):
|
||||
elif action == "songsel":
|
||||
# Session song selection
|
||||
if "other_user" in user and "ws" in user["other_user"]:
|
||||
if type == "songsel" or type == "catjump":
|
||||
if msg_type == "songsel" or msg_type == "catjump":
|
||||
# Change song select position
|
||||
if user["other_user"]["action"] == "songsel":
|
||||
sent_msg = msgobj(type, value)
|
||||
if user["other_user"]["action"] == "songsel" and type(value) is dict:
|
||||
value["player"] = user["player"]
|
||||
sent_msg = msgobj(msg_type, value)
|
||||
await asyncio.wait([
|
||||
ws.send(sent_msg),
|
||||
user["other_user"]["ws"].send(sent_msg)
|
||||
])
|
||||
elif type == "join":
|
||||
elif msg_type == "crowns" or msg_type == "getcrowns":
|
||||
if user["other_user"]["action"] == "songsel":
|
||||
sent_msg = msgobj(msg_type, value)
|
||||
await asyncio.wait([
|
||||
user["other_user"]["ws"].send(sent_msg)
|
||||
])
|
||||
elif msg_type == "join":
|
||||
# Start game
|
||||
if value == None:
|
||||
continue
|
||||
@ -282,8 +301,8 @@ async def connection(ws, path):
|
||||
user["action"] = "loading"
|
||||
user["other_user"]["action"] = "loading"
|
||||
await asyncio.wait([
|
||||
ws.send(msgobj("gameload", user["other_user"]["gamediff"])),
|
||||
user["other_user"]["ws"].send(msgobj("gameload", diff))
|
||||
ws.send(msgobj("gameload", {"diff": user["other_user"]["gamediff"]})),
|
||||
user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff}))
|
||||
])
|
||||
else:
|
||||
user["action"] = "waiting"
|
||||
@ -292,7 +311,7 @@ async def connection(ws, path):
|
||||
"id": id,
|
||||
"diff": diff
|
||||
}]))
|
||||
elif type == "gameend":
|
||||
elif msg_type == "gameend":
|
||||
# User wants to disconnect
|
||||
user["action"] = "ready"
|
||||
user["session"] = False
|
||||
|
23
templates/admin.html
Normal file
23
templates/admin.html
Normal 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>
|
134
templates/admin_song_detail.html
Normal file
134
templates/admin_song_detail.html
Normal 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 %}
|
||||
|
122
templates/admin_song_new.html
Normal file
122
templates/admin_song_new.html
Normal 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 %}
|
21
templates/admin_songs.html
Normal file
21
templates/admin_songs.html
Normal 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 %}
|
@ -1,4 +1,4 @@
|
||||
@echo off
|
||||
(
|
||||
git log -1 --pretty="format:{\"commit\": \"%%H\", \"commit_short\": \"%%h\", \"version\": \"%%ad\", \"url\": \"https://github.com/bui/taiko-web/\"}" --date="format:%%y.%%m.%%d"
|
||||
git log -1 --pretty="format:{\"commit\": \"%%H\", \"commit_short\": \"%%h\", \"version\": \"%%ad\"}" --date="format:%%y.%%m.%%d"
|
||||
) > ../version.json
|
||||
|
@ -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"
|
||||
|
2
tools/hooks/post-checkout
Normal file
2
tools/hooks/post-checkout
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
./tools/get_version.sh
|
2
tools/hooks/post-commit
Normal file
2
tools/hooks/post-commit
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
./tools/get_version.sh
|
2
tools/hooks/post-merge
Normal file
2
tools/hooks/post-merge
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
./tools/get_version.sh
|
2
tools/hooks/post-rewrite
Normal file
2
tools/hooks/post-rewrite
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
./tools/get_version.sh
|
114
tools/migrate_db.py
Normal file
114
tools/migrate_db.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user