Merge branch 'add-lyrics'

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

3
.gitignore vendored
View File

@ -36,6 +36,7 @@ $RECYCLE.BIN/
.Trashes .Trashes
.vscode .vscode
*.pyc
# Directories potentially created on remote AFP share # Directories potentially created on remote AFP share
.AppleDB .AppleDB
@ -48,5 +49,5 @@ public/api
taiko.db taiko.db
version.json version.json
public/index.html public/index.html
config.json config.py
public/assets/song_skins public/assets/song_skins

544
app.py
View File

@ -1,63 +1,128 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
from __future__ import division
import base64
import bcrypt
import hashlib
import config
import json import json
import sqlite3
import re import re
import requests
import schema
import os import os
from flask import Flask, g, jsonify, render_template, request, abort, redirect
from functools import wraps
from flask import Flask, g, jsonify, render_template, request, abort, redirect, session, flash
from flask_caching import Cache from flask_caching import Cache
from flask_session import Session
from flask_wtf.csrf import CSRFProtect, generate_csrf, CSRFError
from ffmpy import FFmpeg from ffmpy import FFmpeg
from pymongo import MongoClient
app = Flask(__name__) app = Flask(__name__)
try: client = MongoClient(host=config.MONGO['host'])
app.cache = Cache(app, config={'CACHE_TYPE': 'redis'})
except RuntimeError:
import tempfile
app.cache = Cache(app, config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': tempfile.gettempdir()})
DATABASE = 'taiko.db' app.secret_key = config.SECRET_KEY
DEFAULT_URL = 'https://github.com/bui/taiko-web/' app.config['SESSION_TYPE'] = 'redis'
app.cache = Cache(app, config=config.REDIS)
sess = Session()
sess.init_app(app)
csrf = CSRFProtect(app)
db = client[config.MONGO['database']]
db.users.create_index('username', unique=True)
db.songs.create_index('id', unique=True)
def get_db(): class HashException(Exception):
db = getattr(g, '_database', None) pass
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
def query_db(query, args=(), one=False): def api_error(message):
cur = get_db().execute(query, args) return jsonify({'status': 'error', 'message': message})
rv = cur.fetchall()
cur.close()
return (rv[0] if rv else None) if one else rv def generate_hash(id, form):
md5 = hashlib.md5()
if form['type'] == 'tja':
urls = ['%s%s/main.tja' % (config.SONGS_BASEURL, id)]
else:
urls = []
for diff in ['easy', 'normal', 'hard', 'oni', 'ura']:
if form['course_' + diff]:
urls.append('%s%s/%s.osu' % (config.SONGS_BASEURL, id, diff))
for url in urls:
if url.startswith("http://") or url.startswith("https://"):
resp = requests.get(url)
if resp.status_code != 200:
raise HashException('Invalid response from %s (status code %s)' % (resp.url, resp.status_code))
md5.update(resp.content)
else:
if url.startswith("/"):
url = url[1:]
with open(os.path.join("public", url), "rb") as file:
md5.update(file.read())
return base64.b64encode(md5.digest())[:-2].decode('utf-8')
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('username'):
return api_error('not_logged_in')
return f(*args, **kwargs)
return decorated_function
def admin_required(level):
def decorated_function(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not session.get('username'):
return abort(403)
user = db.users.find_one({'username': session.get('username')})
if user['user_level'] < level:
return abort(403)
return f(*args, **kwargs)
return wrapper
return decorated_function
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
return api_error('invalid_csrf')
@app.before_request
def before_request_func():
if session.get('session_id'):
if not db.users.find_one({'session_id': session.get('session_id')}):
session.clear()
def get_config(): def get_config():
if os.path.isfile('config.json'): config_out = {
try: 'songs_baseurl': config.SONGS_BASEURL,
config = json.load(open('config.json', 'r')) 'assets_baseurl': config.ASSETS_BASEURL,
except ValueError: 'email': config.EMAIL,
print('WARNING: Invalid config.json, using default values') 'accounts': config.ACCOUNTS,
config = {} 'custom_js': config.CUSTOM_JS
else: }
print('WARNING: No config.json found, using default values')
config = {}
if not config.get('songs_baseurl'): if not config_out.get('songs_baseurl'):
config['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/' config_out['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/'
if not config.get('assets_baseurl'): if not config_out.get('assets_baseurl'):
config['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/' config_out['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/'
config['_version'] = get_version() config_out['_version'] = get_version()
return config return config_out
def get_version(): def get_version():
version = {'commit': None, 'commit_short': '', 'version': None, 'url': DEFAULT_URL} version = {'commit': None, 'commit_short': '', 'version': None, 'url': config.URL}
if os.path.isfile('version.json'): if os.path.isfile('version.json'):
try: try:
ver = json.load(open('version.json', 'r')) ver = json.load(open('version.json', 'r'))
@ -72,20 +137,158 @@ def get_version():
return version return version
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
@app.route('/') @app.route('/')
@app.cache.cached(timeout=15)
def route_index(): def route_index():
version = get_version() version = get_version()
return render_template('index.html', version=version, config=get_config()) return render_template('index.html', version=version, config=get_config())
@app.route('/api/csrftoken')
def route_csrftoken():
return jsonify({'status': 'ok', 'token': generate_csrf()})
@app.route('/admin')
@admin_required(level=50)
def route_admin():
return redirect('/admin/songs')
@app.route('/admin/songs')
@admin_required(level=50)
def route_admin_songs():
songs = db.songs.find({})
user = db.users.find_one({'username': session['username']})
return render_template('admin_songs.html', songs=list(songs), admin=user)
@app.route('/admin/songs/<int:id>')
@admin_required(level=50)
def route_admin_songs_id(id):
song = db.songs.find_one({'id': id})
if not song:
return abort(404)
categories = list(db.categories.find({}))
song_skins = list(db.song_skins.find({}))
makers = list(db.makers.find({}))
user = db.users.find_one({'username': session['username']})
return render_template('admin_song_detail.html',
song=song, categories=categories, song_skins=song_skins, makers=makers, admin=user)
@app.route('/admin/songs/new')
@admin_required(level=100)
def route_admin_songs_new():
categories = list(db.categories.find({}))
song_skins = list(db.song_skins.find({}))
makers = list(db.makers.find({}))
return render_template('admin_song_new.html', categories=categories, song_skins=song_skins, makers=makers)
@app.route('/admin/songs/new', methods=['POST'])
@admin_required(level=100)
def route_admin_songs_new_post():
output = {'title_lang': {}, 'subtitle_lang': {}, 'courses': {}}
output['enabled'] = True if request.form.get('enabled') else False
output['title'] = request.form.get('title') or None
output['subtitle'] = request.form.get('subtitle') or None
for lang in ['ja', 'en', 'cn', 'tw', 'ko']:
output['title_lang'][lang] = request.form.get('title_%s' % lang) or None
output['subtitle_lang'][lang] = request.form.get('subtitle_%s' % lang) or None
for course in ['easy', 'normal', 'hard', 'oni', 'ura']:
if request.form.get('course_%s' % course):
output['courses'][course] = {'stars': int(request.form.get('course_%s' % course)),
'branch': True if request.form.get('branch_%s' % course) else False}
else:
output['courses'][course] = None
output['category_id'] = int(request.form.get('category_id')) or None
output['type'] = request.form.get('type')
output['offset'] = float(request.form.get('offset')) or None
output['skin_id'] = int(request.form.get('skin_id')) or None
output['preview'] = float(request.form.get('preview')) or None
output['volume'] = float(request.form.get('volume')) or None
output['maker_id'] = int(request.form.get('maker_id')) or None
output['hash'] = None
seq = db.seq.find_one({'name': 'songs'})
seq_new = seq['value'] + 1 if seq else 1
output['id'] = seq_new
output['order'] = seq_new
db.songs.insert_one(output)
flash('Song created.')
db.seq.update_one({'name': 'songs'}, {'$set': {'value': seq_new}}, upsert=True)
return redirect('/admin/songs/%s' % str(seq_new))
@app.route('/admin/songs/<int:id>', methods=['POST'])
@admin_required(level=100)
def route_admin_songs_id_post(id):
song = db.songs.find_one({'id': id})
if not song:
return abort(404)
user = db.users.find_one({'username': session['username']})
user_level = user['user_level']
output = {'title_lang': {}, 'subtitle_lang': {}, 'courses': {}}
if user_level >= 100:
output['enabled'] = True if request.form.get('enabled') else False
output['title'] = request.form.get('title') or None
output['subtitle'] = request.form.get('subtitle') or None
for lang in ['ja', 'en', 'cn', 'tw', 'ko']:
output['title_lang'][lang] = request.form.get('title_%s' % lang) or None
output['subtitle_lang'][lang] = request.form.get('subtitle_%s' % lang) or None
for course in ['easy', 'normal', 'hard', 'oni', 'ura']:
if request.form.get('course_%s' % course):
output['courses'][course] = {'stars': int(request.form.get('course_%s' % course)),
'branch': True if request.form.get('branch_%s' % course) else False}
else:
output['courses'][course] = None
output['category_id'] = int(request.form.get('category_id')) or None
output['type'] = request.form.get('type')
output['offset'] = float(request.form.get('offset')) or None
output['skin_id'] = int(request.form.get('skin_id')) or None
output['preview'] = float(request.form.get('preview')) or None
output['volume'] = float(request.form.get('volume')) or None
output['maker_id'] = int(request.form.get('maker_id')) or None
output['hash'] = request.form.get('hash')
if request.form.get('gen_hash'):
try:
output['hash'] = generate_hash(id, request.form)
except HashException as e:
flash('An error occurred: %s' % str(e), 'error')
return redirect('/admin/songs/%s' % id)
db.songs.update_one({'id': id}, {'$set': output})
flash('Changes saved.')
return redirect('/admin/songs/%s' % id)
@app.route('/admin/songs/<int:id>/delete', methods=['POST'])
@admin_required(level=100)
def route_admin_songs_id_delete(id):
song = db.songs.find_one({'id': id})
if not song:
return abort(404)
db.songs.delete_one({'id': id})
flash('Song deleted.')
return redirect('/admin/songs')
@app.route('/api/preview') @app.route('/api/preview')
@app.cache.cached(timeout=15, query_string=True) @app.cache.cached(timeout=15, query_string=True)
def route_api_preview(): def route_api_preview():
@ -93,12 +296,12 @@ def route_api_preview():
if not song_id or not re.match('^[0-9]+$', song_id): if not song_id or not re.match('^[0-9]+$', song_id):
abort(400) abort(400)
song_row = query_db('select * from songs where id = ? and enabled = 1', (song_id,)) song = db.songs.find_one({'id': song_id})
if not song_row: if not song:
abort(400) abort(400)
song_type = song_row[0]['type'] song_type = song['type']
prev_path = make_preview(song_id, song_type, song_row[0]['preview']) prev_path = make_preview(song_id, song_type, song['preview'])
if not prev_path: if not prev_path:
return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id) return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id)
@ -108,52 +311,30 @@ def route_api_preview():
@app.route('/api/songs') @app.route('/api/songs')
@app.cache.cached(timeout=15) @app.cache.cached(timeout=15)
def route_api_songs(): def route_api_songs():
songs = query_db('select s.*, m.name, m.url from songs s left join makers m on s.maker_id = m.maker_id where enabled = 1') songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False}))
raw_categories = query_db('select * from categories')
categories = {}
for cat in raw_categories:
categories[cat['id']] = cat['title']
raw_song_skins = query_db('select * from song_skins')
song_skins = {}
for skin in raw_song_skins:
song_skins[skin[0]] = {'name': skin['name'], 'song': skin['song'], 'stage': skin['stage'], 'don': skin['don']}
songs_out = []
for song in songs: for song in songs:
song_id = song['id'] if song['maker_id']:
song_type = song['type']
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: if song['maker_id'] == 0:
maker = 0 song['maker'] = 0
elif song['maker_id'] and song['maker_id'] > 0: else:
maker = {'name': song['name'], 'url': song['url'], 'id': song['maker_id']} song['maker'] = db.makers.find_one({'id': song['maker_id']}, {'_id': False})
else:
song['maker'] = None
del song['maker_id']
songs_out.append({ if song['category_id']:
'id': song_id, song['category'] = db.categories.find_one({'id': song['category_id']})['title']
'title': song['title'], else:
'title_lang': song['title_lang'], song['category'] = None
'subtitle': song['subtitle'], del song['category_id']
'subtitle_lang': song['subtitle_lang'],
'stars': [
song['easy'], song['normal'], song['hard'], song['oni'], song['ura']
],
'preview': preview,
'category': category_out,
'type': song_type,
'offset': song['offset'],
'song_skin': song_skin_out,
'volume': song['volume'],
'maker': maker,
'hash': song['hash']
})
return jsonify(songs_out) 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') @app.route('/api/config')
@ -163,6 +344,183 @@ def route_api_config():
return jsonify(config) return jsonify(config)
@app.route('/api/register', methods=['POST'])
def route_api_register():
data = request.get_json()
if not schema.validate(data, schema.register):
return abort(400)
if session.get('username'):
session.clear()
username = data.get('username', '')
if len(username) < 3 or len(username) > 20 or not re.match('^[a-zA-Z0-9_]{3,20}$', username):
return api_error('invalid_username')
if db.users.find_one({'username_lower': username.lower()}):
return api_error('username_in_use')
password = data.get('password', '').encode('utf-8')
if not 6 <= len(password) <= 5000:
return api_error('invalid_password')
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password, salt)
session_id = os.urandom(24).hex()
db.users.insert_one({
'username': username,
'username_lower': username.lower(),
'password': hashed,
'display_name': username,
'user_level': 1,
'session_id': session_id
})
session['session_id'] = session_id
session['username'] = username
session.permanent = True
return jsonify({'status': 'ok', 'username': username, 'display_name': username})
@app.route('/api/login', methods=['POST'])
def route_api_login():
data = request.get_json()
if not schema.validate(data, schema.login):
return abort(400)
if session.get('username'):
session.clear()
username = data.get('username', '')
result = db.users.find_one({'username_lower': username.lower()})
if not result:
return api_error('invalid_username_password')
password = data.get('password', '').encode('utf-8')
if not bcrypt.checkpw(password, result['password']):
return api_error('invalid_username_password')
session['session_id'] = result['session_id']
session['username'] = result['username']
session.permanent = True if data.get('remember') else False
return jsonify({'status': 'ok', 'username': result['username'], 'display_name': result['display_name']})
@app.route('/api/logout', methods=['POST'])
@login_required
def route_api_logout():
session.clear()
return jsonify({'status': 'ok'})
@app.route('/api/account/display_name', methods=['POST'])
@login_required
def route_api_account_display_name():
data = request.get_json()
if not schema.validate(data, schema.update_display_name):
return abort(400)
display_name = data.get('display_name', '').strip()
if not display_name:
display_name = session.get('username')
elif len(display_name) > 25:
return api_error('invalid_display_name')
db.users.update_one({'username': session.get('username')}, {
'$set': {'display_name': display_name}
})
return jsonify({'status': 'ok', 'display_name': display_name})
@app.route('/api/account/password', methods=['POST'])
@login_required
def route_api_account_password():
data = request.get_json()
if not schema.validate(data, schema.update_password):
return abort(400)
user = db.users.find_one({'username': session.get('username')})
current_password = data.get('current_password', '').encode('utf-8')
if not bcrypt.checkpw(current_password, user['password']):
return api_error('current_password_invalid')
new_password = data.get('new_password', '').encode('utf-8')
if not 6 <= len(new_password) <= 5000:
return api_error('invalid_new_password')
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(new_password, salt)
session_id = os.urandom(24).hex()
db.users.update_one({'username': session.get('username')}, {
'$set': {'password': hashed, 'session_id': session_id}
})
session['session_id'] = session_id
return jsonify({'status': 'ok'})
@app.route('/api/account/remove', methods=['POST'])
@login_required
def route_api_account_remove():
data = request.get_json()
if not schema.validate(data, schema.delete_account):
return abort(400)
user = db.users.find_one({'username': session.get('username')})
password = data.get('password', '').encode('utf-8')
if not bcrypt.checkpw(password, user['password']):
return api_error('verify_password_invalid')
db.scores.delete_many({'username': session.get('username')})
db.users.delete_one({'username': session.get('username')})
session.clear()
return jsonify({'status': 'ok'})
@app.route('/api/scores/save', methods=['POST'])
@login_required
def route_api_scores_save():
data = request.get_json()
if not schema.validate(data, schema.scores_save):
return abort(400)
username = session.get('username')
if data.get('is_import'):
db.scores.delete_many({'username': username})
scores = data.get('scores', [])
for score in scores:
db.scores.update_one({'username': username, 'hash': score['hash']},
{'$set': {
'username': username,
'hash': score['hash'],
'score': score['score']
}}, upsert=True)
return jsonify({'status': 'ok'})
@app.route('/api/scores/get')
@login_required
def route_api_scores_get():
username = session.get('username')
scores = []
for score in db.scores.find({'username': username}):
scores.append({
'hash': score['hash'],
'score': score['score']
})
user = db.users.find_one({'username': username})
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name']})
def make_preview(song_id, song_type, preview): def make_preview(song_id, song_type, preview):
song_path = 'public/songs/%s/main.mp3' % song_id song_path = 'public/songs/%s/main.mp3' % song_id
prev_path = 'public/songs/%s/preview.mp3' % song_id prev_path = 'public/songs/%s/preview.mp3' % song_id

View File

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

35
config.example.py Normal file
View File

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

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

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

View File

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

View File

@ -89,3 +89,39 @@
.fix-animations *{ .fix-animations *{
animation: none !important; animation: none !important;
} }
#song-lyrics{
position: absolute;
right: calc((100vw - 1280 / 720 * 100vh) / 2 + 100px * var(--scale));
bottom: calc(44 / 720 * 100vh - 30px * var(--scale));
left: calc((100vw - 1280 / 720 * 100vh) / 2 + 100px * var(--scale));
text-align: center;
font-family: Meiryo, sans-serif;
font-weight: bold;
font-size: calc(45px * var(--scale));
line-height: 1.2;
white-space: pre-wrap;
}
#game.portrait #song-lyrics{
right: calc(20px * var(--scale));
left: calc(20px * var(--scale));
}
#song-lyrics .stroke,
#song-lyrics .fill{
position: absolute;
right: 0;
bottom: 0;
left: 0;
}
#song-lyrics .stroke{
-webkit-text-stroke: calc(7px * var(--scale)) #00a;
}
#song-lyrics .fill{
color: #fff;
}
#song-lyrics ruby{
display: inline-flex;
flex-direction: column-reverse;
}
#song-lyrics rt{
line-height: 1;
}

View File

@ -117,3 +117,20 @@ body{
color: #777; color: #777;
text-shadow: 0.05em 0.05em #fff; text-shadow: 0.05em 0.05em #fff;
} }
.view-outer.loader-error-div,
.loader-error-div .diag-txt{
display: none
}
.loader-error-div{
font-family: sans-serif;
}
.loader-error-div .debug-link{
color: #00f;
text-decoration: underline;
cursor: pointer;
float: right;
}
.loader-error-div .diag-txt textarea,
.loader-error-div .diag-txt iframe{
height: 10em;
}

View File

@ -108,8 +108,8 @@ kbd{
.left-buttons .taibtn{ .left-buttons .taibtn{
margin-right: 0.4em; margin-right: 0.4em;
} }
#diag-txt textarea, .diag-txt textarea,
#diag-txt iframe{ .diag-txt iframe{
width: 100%; width: 100%;
height: 5em; height: 5em;
font-size: inherit; font-size: inherit;
@ -119,6 +119,7 @@ kbd{
background: #fff; background: #fff;
border: 1px solid #a9a9a9; border: 1px solid #a9a9a9;
user-select: all; user-select: all;
box-sizing: border-box;
} }
.text-warn{ .text-warn{
color: #d00; color: #d00;
@ -291,3 +292,88 @@ kbd{
.left-buttons .taibtn{ .left-buttons .taibtn{
z-index: 1; z-index: 1;
} }
.accountpass-form,
.accountdel-form,
.login-form{
text-align: center;
width: 80%;
margin: auto;
}
.accountpass-form .accountpass-div,
.accountdel-form .accountdel-div,
.login-form .password2-div{
display: none;
}
.account-view .displayname,
.accountpass-form input[type=password],
.accountdel-form input[type=password],
.login-form input[type=text],
.login-form input[type=password]{
width: 100%;
font-size: 1.4em;
margin: 0.1em 0;
padding: 0.3em;
box-sizing: border-box;
}
.accountpass-form input[type=password]{
width: calc(100% / 3);
}
.accountpass-form input[type=password]::placeholder{
font-size: 0.8em;
}
.login-form input[type=checkbox]{
transform: scale(1.4);
}
.account-view .displayname-hint,
.login-form .username-hint,
.login-form .password-hint,
.login-form .remember-label{
display: block;
font-size: 1.1em;
padding: 0.5em;
}
.login-form .remember-label{
padding: 0.85em;
}
.account-view .save-btn{
float: right;
padding: 0.4em 1.5em;
font-weight: bold;
border-color: #000;
color: #000;
z-index: 1;
}
.account-view .view-end-button{
margin-right: 0.4em;
font-weight: normal;
border-color: #dacdb2;
color: #555;
}
.account-view .save-btn:hover,
.account-view .save-btn.selected,
.account-view .view-end-button:hover,
.account-view .view-end-button.selected{
color: #fff;
border-color: #fff;
}
.account-view .displayname-div{
width: 80%;
margin: 0 auto;
}
.accountpass-form .accountpass-btn,
.accountdel-form .accountdel-btn,
.login-form .login-btn{
z-index: 1;
}
.accountpass-form,
.accountdel-form{
margin: 0.3em auto;
}
.view-content .error-div{
display: none;
width: 80%;
margin: 0 auto;
padding: 0.5em;
font-size: 1.1em;
color: #d00;
}

View File

@ -5,7 +5,7 @@
cancelTouch = false cancelTouch = false
this.endButton = this.getElement("view-end-button") this.endButton = this.getElement("view-end-button")
this.diagTxt = document.getElementById("diag-txt") this.diagTxt = this.getElement("diag-txt")
this.version = document.getElementById("version-link").href this.version = document.getElementById("version-link").href
this.tutorialOuter = this.getElement("view-outer") this.tutorialOuter = this.getElement("view-outer")
if(touchEnabled){ if(touchEnabled){

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

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

View File

@ -31,7 +31,9 @@ var assets = {
"importsongs.js", "importsongs.js",
"logo.js", "logo.js",
"settings.js", "settings.js",
"scorestorage.js" "scorestorage.js",
"account.js",
"lyrics.js"
], ],
"css": [ "css": [
"main.css", "main.css",
@ -86,11 +88,7 @@ var assets = {
"settings_gamepad.png" "settings_gamepad.png"
], ],
"audioSfx": [ "audioSfx": [
"se_cancel.wav",
"se_don.wav",
"se_ka.wav",
"se_pause.wav", "se_pause.wav",
"se_jump.wav",
"se_calibration.wav", "se_calibration.wav",
"v_results.wav", "v_results.wav",
@ -102,6 +100,10 @@ var assets = {
"audioSfxLR": [ "audioSfxLR": [
"neiro_1_don.wav", "neiro_1_don.wav",
"neiro_1_ka.wav", "neiro_1_ka.wav",
"se_cancel.wav",
"se_don.wav",
"se_ka.wav",
"se_jump.wav",
"se_balloon.wav", "se_balloon.wav",
"se_gameclear.wav", "se_gameclear.wav",
@ -137,7 +139,9 @@ var assets = {
"about.html", "about.html",
"debug.html", "debug.html",
"session.html", "session.html",
"settings.html" "settings.html",
"account.html",
"login.html"
], ],
"songs": [], "songs": [],

View File

@ -706,12 +706,12 @@
}) })
}else if(r.smallHiragana.test(symbol)){ }else if(r.smallHiragana.test(symbol)){
// Small hiragana, small katakana // Small hiragana, small katakana
drawn.push({text: symbol, x: 0, y: 0, w: 30}) drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 30})
}else if(r.hiragana.test(symbol)){ }else if(r.hiragana.test(symbol)){
// Hiragana, katakana // Hiragana, katakana
drawn.push({text: symbol, x: 0, y: 0, w: 35}) drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 35})
}else{ }else{
drawn.push({text: symbol, x: 0, y: 0, w: 39}) drawn.push({text: symbol, kana: true, x: 0, y: 0, w: 39})
} }
} }
@ -720,6 +720,9 @@
if(config.letterSpacing){ if(config.letterSpacing){
symbol.w += config.letterSpacing symbol.w += config.letterSpacing
} }
if(config.kanaSpacing && symbol.kana){
symbol.w += config.kanaSpacing
}
drawnWidth += symbol.w * mul drawnWidth += symbol.w * mul
} }
@ -924,8 +927,22 @@
} }
} }
} }
var search = () => {
var end = line.length
var dist = end
while(dist){
dist >>= 1
line = words[i].slice(0, end)
lastWidth = ctx.measureText(line).width
end += lastWidth < config.width ? dist : -dist
}
if(line !== words[i]){
words.splice(i + 1, 0, words[i].slice(line.length))
words[i] = line
}
}
for(var i in words){ for(var i = 0; i < words.length; i++){
var skip = words[i].substitute || words[i] === "\n" var skip = words[i].substitute || words[i] === "\n"
if(!skip){ if(!skip){
var currentWidth = ctx.measureText(line + words[i]).width var currentWidth = ctx.measureText(line + words[i]).width
@ -957,8 +974,22 @@
recenter() recenter()
x = 0 x = 0
y += lineHeight y += lineHeight
line = words[i] === "\n" ? "" : words[i] if(words[i] === "\n"){
line = ""
lastWidth = 0
}else{
line = words[i]
lastWidth = ctx.measureText(line).width lastWidth = ctx.measureText(line).width
if(line.length !== 1 && lastWidth > config.width){
search()
}
}
}
}else if(!line){
line = words[i]
lastWidth = ctx.measureText(line).width
if(line.length !== 1 && lastWidth > config.width){
search()
} }
}else{ }else{
line += words[i] line += words[i]
@ -1549,6 +1580,99 @@
ctx.restore() ctx.restore()
} }
nameplate(config){
var ctx = config.ctx
var w = 264
var h = 57
var r = h / 2
var pi = Math.PI
ctx.save()
ctx.translate(config.x, config.y)
if(config.scale){
ctx.scale(config.scale, config.scale)
}
ctx.fillStyle="rgba(0, 0, 0, 0.25)"
ctx.beginPath()
ctx.arc(r + 4, r + 5, r, pi / 2, pi / -2)
ctx.arc(w - r + 4, r + 5, r, pi / -2, pi / 2)
ctx.fill()
ctx.beginPath()
ctx.moveTo(r, 0)
this.roundedCorner(ctx, w, 0, r, 1)
ctx.lineTo(r, r)
ctx.fillStyle = config.blue ? "#67cecb" : "#ff421d"
ctx.fill()
ctx.beginPath()
ctx.moveTo(r, r)
this.roundedCorner(ctx, w, h, r, 2)
ctx.lineTo(r, h)
ctx.fillStyle = "rgba(255, 255, 255, 0.8)"
ctx.fill()
ctx.strokeStyle = "#000"
ctx.lineWidth = 4
ctx.beginPath()
ctx.moveTo(r, 0)
ctx.arc(w - r, r, r, pi / -2, pi / 2)
ctx.lineTo(r, h)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(r, r - 1)
ctx.lineTo(w, r - 1)
ctx.lineWidth = 2
ctx.stroke()
ctx.beginPath()
ctx.arc(r, r, r, 0, pi * 2)
ctx.fillStyle = config.blue ? "#67cecb" : "#ff421d"
ctx.fill()
ctx.lineWidth = 4
ctx.stroke()
ctx.font = this.bold(config.font) + "28px " + config.font
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.lineWidth = 5
ctx.miterLimit = 1
ctx.strokeStyle = "#fff"
ctx.fillStyle = "#000"
var text = config.blue ? "2P" : "1P"
ctx.strokeText(text, r + 2, r + 1)
ctx.fillText(text, r + 2, r + 1)
if(config.rank){
this.layeredText({
ctx: ctx,
text: config.rank,
fontSize: 20,
fontFamily: config.font,
x: w / 2 + r * 0.7,
y: r * 0.5,
width: 180,
align: "center",
baseline: "middle"
}, [
{fill: "#000"}
])
}
this.layeredText({
ctx: ctx,
text: config.name || "",
fontSize: 21,
fontFamily: config.font,
x: w / 2 + r * 0.7,
y: r * 1.5 - 0.5,
width: 180,
kanaSpacing: 10,
align: "center",
baseline: "middle"
}, [
{outline: "#000", letterBorder: 6},
{fill: "#fff"}
])
ctx.restore()
}
alpha(amount, ctx, callback, winW, winH){ alpha(amount, ctx, callback, winW, winH){
if(amount >= 1){ if(amount >= 1){
return callback(ctx) return callback(ctx)

View File

@ -6,7 +6,11 @@ class Controller{
this.saveScore = !autoPlayEnabled this.saveScore = !autoPlayEnabled
this.multiplayer = multiplayer this.multiplayer = multiplayer
this.touchEnabled = touchEnabled this.touchEnabled = touchEnabled
this.snd = this.multiplayer ? "_p" + this.multiplayer : "" if(multiplayer === 2){
this.snd = p2.player === 2 ? "_p1" : "_p2"
}else{
this.snd = multiplayer ? "_p" + p2.player : ""
}
this.calibrationMode = selectedSong.folder === "calibration" this.calibrationMode = selectedSong.folder === "calibration"
this.audioLatency = 0 this.audioLatency = 0
@ -53,6 +57,15 @@ class Controller{
if(song.id == this.selectedSong.folder){ if(song.id == this.selectedSong.folder){
this.mainAsset = song.sound this.mainAsset = song.sound
this.volume = song.volume || 1 this.volume = song.volume || 1
if(!multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){
if(song.lyricsData){
var lyricsDiv = document.getElementById("song-lyrics")
this.lyrics = new Lyrics(song.lyricsData, selectedSong.offset, lyricsDiv)
}else if(this.parsedSongData.lyrics){
var lyricsDiv = document.getElementById("song-lyrics")
this.lyrics = new Lyrics(this.parsedSongData.lyrics, selectedSong.offset, lyricsDiv, true)
}
}
} }
}) })
} }
@ -155,10 +168,16 @@ class Controller{
if(this.mainLoopRunning){ if(this.mainLoopRunning){
if(this.multiplayer !== 2){ if(this.multiplayer !== 2){
requestAnimationFrame(() => { requestAnimationFrame(() => {
var player = this.multiplayer ? p2.player : 1
if(player === 1){
this.viewLoop() this.viewLoop()
}
if(this.multiplayer === 1){ if(this.multiplayer === 1){
this.syncWith.viewLoop() this.syncWith.viewLoop()
} }
if(player === 2){
this.viewLoop()
}
if(this.scoresheet){ if(this.scoresheet){
if(this.view.ctx){ if(this.view.ctx){
this.view.ctx.save() this.view.ctx.save()
@ -197,14 +216,14 @@ class Controller{
displayScore(score, notPlayed, bigNote){ displayScore(score, notPlayed, bigNote){
this.view.displayScore(score, notPlayed, bigNote) this.view.displayScore(score, notPlayed, bigNote)
} }
songSelection(fadeIn){ songSelection(fadeIn, showWarning){
if(!fadeIn){ if(!fadeIn){
this.clean() this.clean()
} }
if(this.calibrationMode){ if(this.calibrationMode){
new SettingsView(this.touchEnabled, false, null, "latency") new SettingsView(this.touchEnabled, false, null, "latency")
}else{ }else{
new SongSelect(false, fadeIn, this.touchEnabled) new SongSelect(false, fadeIn, this.touchEnabled, null, showWarning)
} }
} }
restartSong(){ restartSong(){
@ -217,20 +236,27 @@ class Controller{
resolve() resolve()
}else{ }else{
var songObj = assets.songs.find(song => song.id === this.selectedSong.folder) var songObj = assets.songs.find(song => song.id === this.selectedSong.folder)
var promises = []
if(songObj.chart && songObj.chart !== "blank"){ if(songObj.chart && songObj.chart !== "blank"){
var reader = new FileReader() var reader = new FileReader()
var promise = pageEvents.load(reader).then(event => { promises.push(pageEvents.load(reader).then(event => {
this.songData = event.target.result.replace(/\0/g, "").split("\n") this.songData = event.target.result.replace(/\0/g, "").split("\n")
resolve() return Promise.resolve()
}) }))
if(this.selectedSong.type === "tja"){ if(this.selectedSong.type === "tja"){
reader.readAsText(songObj.chart, "sjis") reader.readAsText(songObj.chart, "sjis")
}else{ }else{
reader.readAsText(songObj.chart) reader.readAsText(songObj.chart)
} }
}else{
resolve()
} }
if(songObj.lyricsFile){
var reader = new FileReader()
promises.push(pageEvents.load(reader).then(event => {
songObj.lyricsData = event.target.result
}, () => Promise.resolve()), songObj.lyricsFile.webkitRelativePath)
reader.readAsText(songObj.lyricsFile)
}
Promise.all(promises).then(resolve)
} }
}).then(() => { }).then(() => {
var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled) var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled)
@ -306,5 +332,8 @@ class Controller{
debugObj.debug.updateStatus() debugObj.debug.updateStatus()
} }
} }
if(this.lyrics){
this.lyrics.clean()
}
} }
} }

View File

@ -17,6 +17,8 @@ class Debug{
this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0] this.branchSelect = this.branchSelectDiv.getElementsByTagName("select")[0]
this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0] this.branchResetBtn = this.branchSelectDiv.getElementsByClassName("reset")[0]
this.volumeDiv = this.byClass("music-volume") this.volumeDiv = this.byClass("music-volume")
this.lyricsHideDiv = this.byClass("lyrics-hide")
this.lyricsOffsetDiv = this.byClass("lyrics-offset")
this.restartLabel = this.byClass("change-restart-label") this.restartLabel = this.byClass("change-restart-label")
this.restartCheckbox = this.byClass("change-restart") this.restartCheckbox = this.byClass("change-restart")
this.autoplayLabel = this.byClass("autoplay-label") this.autoplayLabel = this.byClass("autoplay-label")
@ -50,6 +52,9 @@ class Debug{
this.volumeSlider.onchange(this.volumeChange.bind(this)) this.volumeSlider.onchange(this.volumeChange.bind(this))
this.volumeSlider.set(1) this.volumeSlider.set(1)
this.lyricsSlider = new InputSlider(this.lyricsOffsetDiv, -60, 60, 3)
this.lyricsSlider.onchange(this.lyricsChange.bind(this))
this.moveTo(100, 100) this.moveTo(100, 100)
this.restore() this.restore()
this.updateStatus() this.updateStatus()
@ -129,6 +134,9 @@ class Debug{
if(this.controller.parsedSongData.branches){ if(this.controller.parsedSongData.branches){
this.branchHideDiv.style.display = "block" this.branchHideDiv.style.display = "block"
} }
if(this.controller.lyrics){
this.lyricsHideDiv.style.display = "block"
}
var selectedSong = this.controller.selectedSong var selectedSong = this.controller.selectedSong
this.defaultOffset = selectedSong.offset || 0 this.defaultOffset = selectedSong.offset || 0
@ -136,19 +144,21 @@ class Debug{
this.offsetChange(this.offsetSlider.get(), true) this.offsetChange(this.offsetSlider.get(), true)
this.branchChange(null, true) this.branchChange(null, true)
this.volumeChange(this.volumeSlider.get(), true) this.volumeChange(this.volumeSlider.get(), true)
this.lyricsChange(this.lyricsSlider.get(), true)
}else{ }else{
this.songHash = selectedSong.hash this.songHash = selectedSong.hash
this.offsetSlider.set(this.defaultOffset) this.offsetSlider.set(this.defaultOffset)
this.branchReset(null, true) this.branchReset(null, true)
this.volumeSlider.set(this.controller.volume) this.volumeSlider.set(this.controller.volume)
this.lyricsSlider.set(this.controller.lyrics ? this.controller.lyrics.vttOffset / 1000 : 0)
} }
var measures = this.controller.parsedSongData.measures.filter((measure, i, array) => { var measures = this.controller.parsedSongData.measures.filter((measure, i, array) => {
return i === 0 || Math.abs(measure.ms - array[i - 1].ms) > 0.01 return i === 0 || Math.abs(measure.ms - array[i - 1].ms) > 0.01
}) })
this.measureNumSlider.setMinMax(0, measures.length - 1) this.measureNumSlider.setMinMax(0, measures.length - 1)
if(this.measureNum && measures.length > this.measureNum){ if(this.measureNum > 0 && measures.length >= this.measureNum){
var measureMS = measures[this.measureNum].ms var measureMS = measures[this.measureNum - 1].ms
var game = this.controller.game var game = this.controller.game
game.started = true game.started = true
var timestamp = Date.now() var timestamp = Date.now()
@ -174,6 +184,7 @@ class Debug{
this.restartBtn.style.display = "" this.restartBtn.style.display = ""
this.autoplayLabel.style.display = "" this.autoplayLabel.style.display = ""
this.branchHideDiv.style.display = "" this.branchHideDiv.style.display = ""
this.lyricsHideDiv.style.display = ""
this.controller = null this.controller = null
} }
this.stopMove() this.stopMove()
@ -194,6 +205,9 @@ class Debug{
branch.ms = branch.originalMS + offset branch.ms = branch.originalMS + offset
}) })
} }
if(this.controller.lyrics){
this.controller.lyrics.offsetChange(value * 1000)
}
if(this.restartCheckbox.checked && !noRestart){ if(this.restartCheckbox.checked && !noRestart){
this.restartSong() this.restartSong()
} }
@ -213,6 +227,14 @@ class Debug{
this.restartSong() this.restartSong()
} }
} }
lyricsChange(value, noRestart){
if(this.controller && this.controller.lyrics){
this.controller.lyrics.offsetChange(undefined, value * 1000)
}
if(this.restartCheckbox.checked && !noRestart){
this.restartSong()
}
}
restartSong(){ restartSong(){
if(this.controller){ if(this.controller){
this.controller.restartSong() this.controller.restartSong()
@ -259,6 +281,7 @@ class Debug{
this.offsetSlider.clean() this.offsetSlider.clean()
this.measureNumSlider.clean() this.measureNumSlider.clean()
this.volumeSlider.clean() this.volumeSlider.clean()
this.lyricsSlider.clean()
pageEvents.remove(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.windowSymbol) pageEvents.remove(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.windowSymbol)
pageEvents.mouseRemove(this) pageEvents.mouseRemove(this)
@ -285,6 +308,8 @@ class Debug{
delete this.branchSelect delete this.branchSelect
delete this.branchResetBtn delete this.branchResetBtn
delete this.volumeDiv delete this.volumeDiv
delete this.lyricsHideDiv
delete this.lyricsOffsetDiv
delete this.restartCheckbox delete this.restartCheckbox
delete this.autoplayLabel delete this.autoplayLabel
delete this.autoplayCheckbox delete this.autoplayCheckbox

View File

@ -5,6 +5,7 @@ class Game{
this.songData = songData this.songData = songData
this.elapsedTime = 0 this.elapsedTime = 0
this.currentCircle = -1 this.currentCircle = -1
this.currentEvent = 0
this.updateCurrentCircle() this.updateCurrentCircle()
this.combo = 0 this.combo = 0
this.rules = new GameRules(this) this.rules = new GameRules(this)
@ -47,13 +48,7 @@ class Game{
} }
initTiming(){ initTiming(){
// Date when the chrono is started (before the game begins) // Date when the chrono is started (before the game begins)
var firstCircle var firstCircle = this.songData.circles[0]
for(var i = 0; i < this.songData.circles.length; i++){
firstCircle = this.songData.circles[i]
if(firstCircle.type !== "event"){
break
}
}
if(this.controller.calibrationMode){ if(this.controller.calibrationMode){
var offsetTime = 0 var offsetTime = 0
}else{ }else{
@ -238,9 +233,6 @@ class Game{
} }
} }
skipNote(circle){ skipNote(circle){
if(circle.type === "event"){
return
}
if(circle.section){ if(circle.section){
this.resetSection() this.resetSection()
} }
@ -258,9 +250,6 @@ class Game{
checkPlays(){ checkPlays(){
var circles = this.songData.circles var circles = this.songData.circles
var circle = circles[this.currentCircle] var circle = circles[this.currentCircle]
if(circle && circle.type === "event"){
this.updateCurrentCircle()
}
if(this.controller.autoPlayEnabled){ if(this.controller.autoPlayEnabled){
while(circle && this.controller.autoPlay(circle)){ while(circle && this.controller.autoPlay(circle)){
@ -469,11 +458,9 @@ class Game{
} }
getLastCircle(circles){ getLastCircle(circles){
for(var i = circles.length; i--;){ for(var i = circles.length; i--;){
if(circles[i].type !== "event"){
return circles[i] return circles[i]
} }
} }
}
whenLastCirclePlayed(){ whenLastCirclePlayed(){
var ms = this.elapsedTime var ms = this.elapsedTime
if(!this.lastCircle){ if(!this.lastCircle){
@ -505,7 +492,9 @@ class Game{
var musicDuration = duration * 1000 - this.controller.offset var musicDuration = duration * 1000 - this.controller.offset
if(this.musicFadeOut === 0){ if(this.musicFadeOut === 0){
if(this.controller.multiplayer === 1){ if(this.controller.multiplayer === 1){
p2.send("gameresults", this.getGlobalScore()) var obj = this.getGlobalScore()
obj.name = account.loggedIn ? account.displayName : null
p2.send("gameresults", obj)
} }
this.musicFadeOut++ this.musicFadeOut++
}else if(this.musicFadeOut === 1 && ms >= started + 1600){ }else if(this.musicFadeOut === 1 && ms >= started + 1600){
@ -621,7 +610,7 @@ class Game{
var circles = this.songData.circles var circles = this.songData.circles
do{ do{
var circle = circles[++this.currentCircle] var circle = circles[++this.currentCircle]
}while(circle && (circle.branch && !circle.branch.active || circle.type === "event")) }while(circle && (circle.branch && !circle.branch.active))
} }
getCurrentCircle(){ getCurrentCircle(){
return this.currentCircle return this.currentCircle

View File

@ -202,12 +202,16 @@
var tja = new ParseTja(data, "oni", 0, 0, true) var tja = new ParseTja(data, "oni", 0, 0, true)
var songObj = { var songObj = {
id: index + 1, id: index + 1,
order: index + 1,
type: "tja", type: "tja",
chart: file, chart: file,
stars: [], courses: {},
music: "muted" music: "muted"
} }
var coursesAdded = false
var titleLang = {} var titleLang = {}
var titleLangAdded = false
var subtitleLangAdded = false
var subtitleLang = {} var subtitleLang = {}
var dir = file.webkitRelativePath.toLowerCase() var dir = file.webkitRelativePath.toLowerCase()
dir = dir.slice(0, dir.lastIndexOf("/") + 1) dir = dir.slice(0, dir.lastIndexOf("/") + 1)
@ -221,7 +225,11 @@
} }
songObj.subtitle = subtitle songObj.subtitle = subtitle
songObj.preview = meta.demostart || 0 songObj.preview = meta.demostart || 0
songObj.stars[this.courseTypes[diff]] = (meta.level || "0") + (meta.branch ? " B" : "") songObj.courses[diff] = {
stars: meta.level || 0,
branch: !!meta.branch
}
coursesAdded = true
if(meta.wave){ if(meta.wave){
songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music
} }
@ -252,6 +260,15 @@
id: 1 id: 1
} }
} }
if(meta.lyrics){
var lyricsFile = this.normPath(this.joinPath(dir, meta.lyrics))
if(lyricsFile in this.otherFiles){
songObj.lyrics = true
songObj.lyricsFile = this.otherFiles[lyricsFile]
}
}else if(meta.inlineLyrics){
songObj.lyrics = true
}
for(var id in allStrings){ for(var id in allStrings){
var songTitle = songObj.title var songTitle = songObj.title
var ura = "" var ura = ""
@ -264,32 +281,27 @@
} }
if(meta["title" + id]){ if(meta["title" + id]){
titleLang[id] = meta["title" + id] titleLang[id] = meta["title" + id]
titleLangAdded = true
}else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){ }else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){
titleLang[id] = this.songTitle[songTitle][id] + ura titleLang[id] = this.songTitle[songTitle][id] + ura
titleLangAdded = true
} }
if(meta["subtitle" + id]){ if(meta["subtitle" + id]){
subtitleLang[id] = meta["subtitle" + id] subtitleLang[id] = meta["subtitle" + id]
subtitleLangAdded = true
} }
} }
} }
var titleLangArray = [] if(titleLangAdded){
for(var id in titleLang){ songObj.title_lang = titleLang
titleLangArray.push(id + " " + titleLang[id])
} }
if(titleLangArray.length !== 0){ if(subtitleLangAdded){
songObj.title_lang = titleLangArray.join("\n") songObj.subtitle_lang = subtitleLang
}
var subtitleLangArray = []
for(var id in subtitleLang){
subtitleLangArray.push(id + " " + subtitleLang[id])
}
if(subtitleLangArray.length !== 0){
songObj.subtitle_lang = subtitleLangArray.join("\n")
} }
if(!songObj.category){ if(!songObj.category){
songObj.category = category || this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))]) songObj.category = category || this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))])
} }
if(songObj.stars.length !== 0){ if(coursesAdded){
this.songs[index] = songObj this.songs[index] = songObj
} }
var hash = md5.base64(event.target.result).slice(0, -2) var hash = md5.base64(event.target.result).slice(0, -2)
@ -316,12 +328,20 @@
dir = dir.slice(0, dir.lastIndexOf("/") + 1) dir = dir.slice(0, dir.lastIndexOf("/") + 1)
var songObj = { var songObj = {
id: index + 1, id: index + 1,
order: index + 1,
type: "osu", type: "osu",
chart: file, chart: file,
subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist, subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist,
subtitle_lang: osu.metadata.Artist || osu.metadata.ArtistUnicode, subtitle_lang: {
en: osu.metadata.Artist || osu.metadata.ArtistUnicode
},
preview: osu.generalInfo.PreviewTime / 1000, preview: osu.generalInfo.PreviewTime / 1000,
stars: [null, null, null, parseInt(osu.difficulty.overallDifficulty) || 1], courses: {
oni:{
stars: parseInt(osu.difficulty.overallDifficulty) || 0,
branch: false
}
},
music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] || "muted" music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] || "muted"
} }
var filename = file.name.slice(0, file.name.lastIndexOf(".")) var filename = file.name.slice(0, file.name.lastIndexOf("."))
@ -333,7 +353,9 @@
suffix = " " + matches[0] suffix = " " + matches[0]
} }
songObj.title = title + suffix songObj.title = title + suffix
songObj.title_lang = (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix songObj.title_lang = {
en: (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix
}
}else{ }else{
songObj.title = filename songObj.title = filename
} }
@ -417,7 +439,7 @@
for(var i = path.length - 2; i >= 0; i--){ for(var i = path.length - 2; i >= 0; i--){
var hasTitle = false var hasTitle = false
for(var j in exclude){ for(var j in exclude){
if(path[i].indexOf(exclude[j].toLowerCase()) !== -1){ if(exclude[j] && path[i].indexOf(exclude[j].toLowerCase()) !== -1){
hasTitle = true hasTitle = true
break break
} }

View File

@ -5,6 +5,7 @@ class Loader{
this.assetsDiv = document.getElementById("assets") this.assetsDiv = document.getElementById("assets")
this.screen = document.getElementById("screen") this.screen = document.getElementById("screen")
this.startTime = Date.now() this.startTime = Date.now()
this.errorMessages = []
var promises = [] var promises = []
@ -28,17 +29,24 @@ class Loader{
if(gameConfig.custom_js){ if(gameConfig.custom_js){
var script = document.createElement("script") var script = document.createElement("script")
this.addPromise(pageEvents.load(script)) var url = gameConfig.custom_js + queryString
script.src = gameConfig.custom_js + queryString this.addPromise(pageEvents.load(script), url)
script.src = url
document.head.appendChild(script) document.head.appendChild(script)
} }
assets.js.forEach(name => { assets.js.forEach(name => {
var script = document.createElement("script") var script = document.createElement("script")
this.addPromise(pageEvents.load(script)) var url = "/src/js/" + name + queryString
script.src = "/src/js/" + name + queryString this.addPromise(pageEvents.load(script), url)
script.src = url
document.head.appendChild(script) document.head.appendChild(script)
}) })
var pageVersion = versionLink.href
var index = pageVersion.lastIndexOf("/")
if(index !== -1){
pageVersion = pageVersion.slice(index + 1)
}
this.addPromise(new Promise((resolve, reject) => { this.addPromise(new Promise((resolve, reject) => {
if( if(
versionLink.href !== gameConfig._version.url && versionLink.href !== gameConfig._version.url &&
@ -69,48 +77,56 @@ class Loader{
} }
var interval = setInterval(checkStyles, 100) var interval = setInterval(checkStyles, 100)
checkStyles() checkStyles()
})) }), "Version on the page and config does not match\n(page: " + pageVersion + ",\nconfig: "+ gameConfig._version.commit + ")")
for(var name in assets.fonts){ for(var name in assets.fonts){
this.addPromise(new FontFace(name, "url('" + gameConfig.assets_baseurl + "fonts/" + assets.fonts[name] + "')").load().then(font => { var url = gameConfig.assets_baseurl + "fonts/" + assets.fonts[name]
this.addPromise(new FontFace(name, "url('" + url + "')").load().then(font => {
document.fonts.add(font) document.fonts.add(font)
})) }), url)
} }
assets.img.forEach(name => { assets.img.forEach(name => {
var id = this.getFilename(name) var id = this.getFilename(name)
var image = document.createElement("img") var image = document.createElement("img")
this.addPromise(pageEvents.load(image)) var url = gameConfig.assets_baseurl + "img/" + name
this.addPromise(pageEvents.load(image), url)
image.id = name image.id = name
image.src = gameConfig.assets_baseurl + "img/" + name image.src = url
this.assetsDiv.appendChild(image) this.assetsDiv.appendChild(image)
assets.image[id] = image assets.image[id] = image
}) })
assets.views.forEach(name => { assets.views.forEach(name => {
var id = this.getFilename(name) var id = this.getFilename(name)
this.addPromise(this.ajax("/src/views/" + name + queryString).then(page => { var url = "/src/views/" + name + queryString
this.addPromise(this.ajax(url).then(page => {
assets.pages[id] = page assets.pages[id] = page
})) }), url)
}) })
this.addPromise(this.ajax("/api/songs").then(songs => { this.addPromise(this.ajax("/api/songs").then(songs => {
assets.songsDefault = JSON.parse(songs) assets.songsDefault = JSON.parse(songs)
assets.songs = assets.songsDefault assets.songs = assets.songsDefault
})) }), "/api/songs")
this.addPromise(this.ajax(gameConfig.assets_baseurl + "img/vectors.json" + queryString).then(response => { var url = gameConfig.assets_baseurl + "img/vectors.json" + queryString
this.addPromise(this.ajax(url).then(response => {
vectors = JSON.parse(response) vectors = JSON.parse(response)
})) }), url)
this.afterJSCount = this.afterJSCount =
["blurPerformance", "P2Connection"].length + ["blurPerformance"].length +
assets.audioSfx.length + assets.audioSfx.length +
assets.audioMusic.length + assets.audioMusic.length +
assets.audioSfxLR.length + assets.audioSfxLR.length +
assets.audioSfxLoud.length assets.audioSfxLoud.length +
(gameConfig.accounts ? 1 : 0)
Promise.all(this.promises).then(() => { Promise.all(this.promises).then(() => {
if(this.error){
return
}
snd.buffer = new SoundBuffer() snd.buffer = new SoundBuffer()
snd.musicGain = snd.buffer.createGain() snd.musicGain = snd.buffer.createGain()
@ -130,20 +146,20 @@ class Loader{
this.afterJSCount = 0 this.afterJSCount = 0
assets.audioSfx.forEach(name => { assets.audioSfx.forEach(name => {
this.addPromise(this.loadSound(name, snd.sfxGain)) this.addPromise(this.loadSound(name, snd.sfxGain), this.soundUrl(name))
}) })
assets.audioMusic.forEach(name => { assets.audioMusic.forEach(name => {
this.addPromise(this.loadSound(name, snd.musicGain)) this.addPromise(this.loadSound(name, snd.musicGain), this.soundUrl(name))
}) })
assets.audioSfxLR.forEach(name => { assets.audioSfxLR.forEach(name => {
this.addPromise(this.loadSound(name, snd.sfxGain).then(sound => { this.addPromise(this.loadSound(name, snd.sfxGain).then(sound => {
var id = this.getFilename(name) var id = this.getFilename(name)
assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL) assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL)
assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR) assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR)
})) }), this.soundUrl(name))
}) })
assets.audioSfxLoud.forEach(name => { assets.audioSfxLoud.forEach(name => {
this.addPromise(this.loadSound(name, snd.sfxLoudGain)) this.addPromise(this.loadSound(name, snd.sfxLoudGain), this.soundUrl(name))
}) })
this.canvasTest = new CanvasTest() this.canvasTest = new CanvasTest()
@ -153,7 +169,44 @@ class Loader{
// Less than 50 fps with blur enabled // Less than 50 fps with blur enabled
disableBlur = true disableBlur = true
} }
})) }), "blurPerformance")
if(gameConfig.accounts){
this.addPromise(this.ajax("/api/scores/get").then(response => {
response = JSON.parse(response)
if(response.status === "ok"){
account.loggedIn = true
account.username = response.username
account.displayName = response.display_name
scoreStorage.load(response.scores)
pageEvents.send("login", account.username)
}
}), "/api/scores/get")
}
settings = new Settings()
pageEvents.setKbd()
scoreStorage = new ScoreStorage()
Promise.all(this.promises).then(() => {
if(this.error){
return
}
if(!account.loggedIn){
scoreStorage.load()
}
for(var i in assets.songsDefault){
var song = assets.songsDefault[i]
if(!song.hash){
song.hash = song.title
}
scoreStorage.songTitles[song.title] = song.hash
var score = scoreStorage.get(song.hash, false, true)
if(score){
score.title = song.title
}
}
var promises = []
var readyEvent = "normal" var readyEvent = "normal"
var songId var songId
@ -167,7 +220,7 @@ class Loader{
} }
}else if(location.hash.length === 6){ }else if(location.hash.length === 6){
p2.hashLock = true p2.hashLock = true
this.addPromise(new Promise(resolve => { promises.push(new Promise(resolve => {
p2.open() p2.open()
pageEvents.add(p2, "message", response => { pageEvents.add(p2, "message", response => {
if(response.type === "session"){ if(response.type === "session"){
@ -181,7 +234,10 @@ class Loader{
resolve() resolve()
} }
}) })
p2.send("invite", location.hash.slice(1).toLowerCase()) p2.send("invite", {
id: location.hash.slice(1).toLowerCase(),
name: account.loggedIn ? account.displayName : null
})
setTimeout(() => { setTimeout(() => {
if(p2.socket.readyState !== 1){ if(p2.socket.readyState !== 1){
p2.hash("") p2.hash("")
@ -196,24 +252,9 @@ class Loader{
p2.hash("") p2.hash("")
} }
settings = new Settings() promises.push(this.canvasTest.drawAllImages())
pageEvents.setKbd()
scoreStorage = new ScoreStorage() Promise.all(promises).then(result => {
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 => {
perf.allImg = result perf.allImg = result
perf.load = Date.now() - this.startTime perf.load = Date.now() - this.startTime
this.canvasTest.clean() this.canvasTest.clean()
@ -227,27 +268,36 @@ class Loader{
}) })
} }
addPromise(promise){ addPromise(promise, url){
this.promises.push(promise) this.promises.push(promise)
promise.then(this.assetLoaded.bind(this), this.errorMsg.bind(this)) promise.then(this.assetLoaded.bind(this), response => {
this.errorMsg(response, url)
return Promise.resolve()
})
}
soundUrl(name){
return gameConfig.assets_baseurl + "audio/" + name
} }
loadSound(name, gain){ loadSound(name, gain){
var id = this.getFilename(name) var id = this.getFilename(name)
return gain.load(gameConfig.assets_baseurl + "audio/" + name).then(sound => { return gain.load(this.soundUrl(name)).then(sound => {
assets.sounds[id] = sound assets.sounds[id] = sound
}) })
} }
getFilename(name){ getFilename(name){
return name.slice(0, name.lastIndexOf(".")) return name.slice(0, name.lastIndexOf("."))
} }
errorMsg(error){ errorMsg(error, url){
if(Array.isArray(error) && error[1] instanceof HTMLElement){ if(url || error){
error = error[0] + ": " + error[1].outerHTML if(url){
error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url
}
this.errorMessages.push(error)
pageEvents.send("loader-error", url || error)
} }
console.error(error)
pageEvents.send("loader-error", error)
if(!this.error){ if(!this.error){
this.error = true this.error = true
cancelTouch = false
this.loaderDiv.classList.add("loaderError") this.loaderDiv.classList.add("loaderError")
if(typeof allStrings === "object"){ if(typeof allStrings === "object"){
var lang = localStorage.lang var lang = localStorage.lang
@ -265,15 +315,58 @@ class Loader{
if(!lang){ if(!lang){
lang = "en" lang = "en"
} }
var errorOccured = allStrings[lang].errorOccured loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang].errorOccured
}
var loaderError = loader.screen.getElementsByClassName("loader-error-div")[0]
loaderError.style.display = "flex"
var diagTxt = loader.screen.getElementsByClassName("diag-txt")[0]
var debugLink = loader.screen.getElementsByClassName("debug-link")[0]
if(navigator.userAgent.indexOf("Android") >= 0){
var iframe = document.createElement("iframe")
diagTxt.appendChild(iframe)
var body = iframe.contentWindow.document.body
body.setAttribute("style", `
font-family: monospace;
margin: 2px 0 0 2px;
white-space: pre-wrap;
word-break: break-all;
cursor: text;
`)
body.setAttribute("onblur", `
getSelection().removeAllRanges()
`)
this.errorTxt = {
element: body,
method: "innerText"
}
}else{ }else{
var errorOccured = "An error occurred, please refresh" var textarea = document.createElement("textarea")
textarea.readOnly = true
diagTxt.appendChild(textarea)
if(!this.touchEnabled){
textarea.addEventListener("focus", () => {
textarea.select()
})
textarea.addEventListener("blur", () => {
getSelection().removeAllRanges()
})
} }
this.loaderPercentage.appendChild(document.createElement("br")) this.errorTxt = {
this.loaderPercentage.appendChild(document.createTextNode(errorOccured)) element: textarea,
this.clean() method: "value"
} }
} }
var show = () => {
diagTxt.style.display = "block"
debugLink.style.display = "none"
}
debugLink.addEventListener("click", show)
debugLink.addEventListener("touchstart", show)
this.clean(true)
}
var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount))
this.errorTxt.element[this.errorTxt.method] = "```\n" + this.errorMessages.join("\n") + "\nPercentage: " + percentage + "%\n```"
}
assetLoaded(){ assetLoaded(){
if(!this.error){ if(!this.error){
this.loadedAssets++ this.loadedAssets++
@ -291,7 +384,11 @@ class Loader{
var request = new XMLHttpRequest() var request = new XMLHttpRequest()
request.open("GET", url) request.open("GET", url)
pageEvents.load(request).then(() => { pageEvents.load(request).then(() => {
if(request.status === 200){
resolve(request.response) resolve(request.response)
}else{
reject()
}
}, reject) }, reject)
if(customRequest){ if(customRequest){
customRequest(request) customRequest(request)
@ -299,14 +396,28 @@ class Loader{
request.send() request.send()
}) })
} }
clean(){ getCsrfToken(){
return this.ajax("api/csrftoken").then(response => {
var json = JSON.parse(response)
if(json.status === "ok"){
return Promise.resolve(json.token)
}else{
return Promise.reject()
}
})
}
clean(error){
var fontDetectDiv = document.getElementById("fontdetectHelper") var fontDetectDiv = document.getElementById("fontdetectHelper")
if(fontDetectDiv){ if(fontDetectDiv){
fontDetectDiv.parentNode.removeChild(fontDetectDiv) fontDetectDiv.parentNode.removeChild(fontDetectDiv)
} }
delete this.loaderDiv
delete this.loaderPercentage delete this.loaderPercentage
delete this.loaderProgress delete this.loaderProgress
if(!error){
delete this.promises delete this.promises
delete this.errorText
}
pageEvents.remove(root, "touchstart") pageEvents.remove(root, "touchstart")
} }
} }

View File

@ -34,7 +34,7 @@ class LoadSong{
run(){ run(){
var song = this.selectedSong var song = this.selectedSong
var id = song.folder var id = song.folder
var promises = [] this.promises = []
if(song.folder !== "calibration"){ if(song.folder !== "calibration"){
assets.sounds["v_start"].play() assets.sounds["v_start"].play()
var songObj = assets.songs.find(song => song.id === id) var songObj = assets.songs.find(song => song.id === id)
@ -92,9 +92,9 @@ class LoadSong{
img.crossOrigin = "Anonymous" img.crossOrigin = "Anonymous"
} }
let promise = pageEvents.load(img) let promise = pageEvents.load(img)
promises.push(promise.then(() => { this.addPromise(promise.then(() => {
return this.scaleImg(img, filename, prefix, force) return this.scaleImg(img, filename, prefix, force)
})) }), songObj.music ? filename + ".png" : skinBase + filename + ".png")
if(songObj.music){ if(songObj.music){
img.src = URL.createObjectURL(song.songSkin[filename + ".png"]) img.src = URL.createObjectURL(song.songSkin[filename + ".png"])
}else{ }else{
@ -102,14 +102,15 @@ class LoadSong{
} }
} }
} }
promises.push(this.loadSongBg(id)) this.loadSongBg(id)
promises.push(new Promise((resolve, reject) => { var url = gameConfig.songs_baseurl + id + "/main.mp3"
this.addPromise(new Promise((resolve, reject) => {
if(songObj.sound){ if(songObj.sound){
songObj.sound.gain = snd.musicGain songObj.sound.gain = snd.musicGain
resolve() resolve()
}else if(!songObj.music){ }else if(!songObj.music){
snd.musicGain.load(gameConfig.songs_baseurl + id + "/main.mp3").then(sound => { snd.musicGain.load(url).then(sound => {
songObj.sound = sound songObj.sound = sound
resolve() resolve()
}, reject) }, reject)
@ -121,51 +122,88 @@ class LoadSong{
}else{ }else{
resolve() resolve()
} }
})) }), songObj.music ? songObj.music.webkitRelativePath : url)
if(songObj.chart){ if(songObj.chart){
if(songObj.chart === "blank"){ if(songObj.chart === "blank"){
this.songData = "" this.songData = ""
}else{ }else{
var reader = new FileReader() var reader = new FileReader()
promises.push(pageEvents.load(reader).then(event => { this.addPromise(pageEvents.load(reader).then(event => {
this.songData = event.target.result.replace(/\0/g, "").split("\n") this.songData = event.target.result.replace(/\0/g, "").split("\n")
})) }), songObj.chart.webkitRelativePath)
if(song.type === "tja"){ if(song.type === "tja"){
reader.readAsText(songObj.chart, "sjis") reader.readAsText(songObj.chart, "sjis")
}else{ }else{
reader.readAsText(songObj.chart) reader.readAsText(songObj.chart)
} }
} }
if(songObj.lyricsFile && settings.getItem("showLyrics")){
var reader = new FileReader()
this.addPromise(pageEvents.load(reader).then(event => {
songObj.lyricsData = event.target.result
}, () => Promise.resolve()), songObj.lyricsFile.webkitRelativePath)
reader.readAsText(songObj.lyricsFile)
}
}else{ }else{
promises.push(loader.ajax(this.getSongPath(song)).then(data => { var url = this.getSongPath(song)
this.addPromise(loader.ajax(url).then(data => {
this.songData = data.replace(/\0/g, "").split("\n") this.songData = data.replace(/\0/g, "").split("\n")
})) }), url)
if(song.lyrics && !songObj.lyricsData && !this.multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){
var url = this.getSongDir(song) + "main.vtt"
this.addPromise(loader.ajax(url).then(data => {
songObj.lyricsData = data
}), url)
}
} }
if(this.touchEnabled && !assets.image["touch_drum"]){ if(this.touchEnabled && !assets.image["touch_drum"]){
let img = document.createElement("img") let img = document.createElement("img")
if(this.imgScale !== 1){ if(this.imgScale !== 1){
img.crossOrigin = "Anonymous" img.crossOrigin = "Anonymous"
} }
promises.push(pageEvents.load(img).then(() => { var url = gameConfig.assets_baseurl + "img/touch_drum.png"
this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, "touch_drum", "") return this.scaleImg(img, "touch_drum", "")
})) }), url)
img.src = gameConfig.assets_baseurl + "img/touch_drum.png" img.src = url
} }
Promise.all(promises).then(() => { Promise.all(this.promises).then(() => {
if(!this.error){
this.setupMultiplayer() this.setupMultiplayer()
}, error => {
if(Array.isArray(error) && error[1] instanceof HTMLElement){
error = error[0] + ": " + error[1].outerHTML
} }
console.error(error)
pageEvents.send("load-song-error", error)
errorMessage(new Error(error).stack)
alert(strings.errorOccured)
}) })
} }
addPromise(promise, url){
this.promises.push(promise.catch(response => {
this.errorMsg(response, url)
return Promise.resolve()
}))
}
errorMsg(error, url){
if(!this.error){
if(url){
error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url
}
pageEvents.send("load-song-error", error)
errorMessage(new Error(error).stack)
var title = this.selectedSong.title
if(title !== this.selectedSong.originalTitle){
title += " (" + this.selectedSong.originalTitle + ")"
}
assets.sounds["v_start"].stop()
setTimeout(() => {
this.clean()
new SongSelect(false, false, this.touchEnabled, null, {
name: "loadSongError",
title: title,
id: this.selectedSong.folder,
error: error
})
}, 500)
}
this.error = true
}
loadSongBg(){ loadSongBg(){
return new Promise((resolve, reject) => {
var promises = []
var filenames = [] var filenames = []
if(this.selectedSong.songBg !== null){ if(this.selectedSong.songBg !== null){
filenames.push("bg_song_" + this.selectedSong.songBg) filenames.push("bg_song_" + this.selectedSong.songBg)
@ -190,15 +228,14 @@ class LoadSong{
if(this.imgScale !== 1 || force){ if(this.imgScale !== 1 || force){
img.crossOrigin = "Anonymous" img.crossOrigin = "Anonymous"
} }
promises.push(pageEvents.load(img).then(() => { var url = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, filenameAb, "", force) return this.scaleImg(img, filenameAb, "", force)
})) }), url)
img.src = gameConfig.assets_baseurl + "img/" + filenameAb + ".png" img.src = url
} }
} }
} }
Promise.all(promises).then(resolve, reject)
})
} }
scaleImg(img, filename, prefix, force){ scaleImg(img, filename, prefix, force){
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -238,8 +275,11 @@ class LoadSong{
randInt(min, max){ randInt(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min return Math.floor(Math.random() * (max - min + 1)) + min
} }
getSongDir(selectedSong){
return gameConfig.songs_baseurl + selectedSong.folder + "/"
}
getSongPath(selectedSong){ getSongPath(selectedSong){
var directory = gameConfig.songs_baseurl + selectedSong.folder + "/" var directory = this.getSongDir(selectedSong)
if(selectedSong.type === "tja"){ if(selectedSong.type === "tja"){
return directory + "main.tja" return directory + "main.tja"
}else{ }else{
@ -264,14 +304,14 @@ class LoadSong{
if(event.type === "gameload"){ if(event.type === "gameload"){
this.cancelButton.style.display = "" this.cancelButton.style.display = ""
if(event.value === song.difficulty){ if(event.value.diff === song.difficulty){
this.startMultiplayer() this.startMultiplayer()
}else{ }else{
this.selectedSong2 = {} this.selectedSong2 = {}
for(var i in this.selectedSong){ for(var i in this.selectedSong){
this.selectedSong2[i] = this.selectedSong[i] this.selectedSong2[i] = this.selectedSong[i]
} }
this.selectedSong2.difficulty = event.value this.selectedSong2.difficulty = event.value.diff
if(song.type === "tja"){ if(song.type === "tja"){
this.startMultiplayer() this.startMultiplayer()
}else{ }else{
@ -297,7 +337,8 @@ class LoadSong{
}) })
p2.send("join", { p2.send("join", {
id: song.folder, id: song.folder,
diff: song.difficulty diff: song.difficulty,
name: account.loggedIn ? account.displayName : null
}) })
}else{ }else{
this.clean() this.clean()
@ -332,6 +373,7 @@ class LoadSong{
pageEvents.send("load-song-cancel") pageEvents.send("load-song-cancel")
} }
clean(){ clean(){
delete this.promises
pageEvents.remove(p2, "message") pageEvents.remove(p2, "message")
if(this.cancelButton){ if(this.cancelButton){
pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"]) pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"])

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

@ -0,0 +1,231 @@
class Lyrics{
constructor(file, songOffset, div, parsed){
this.div = div
this.stroke = document.createElement("div")
this.stroke.classList.add("stroke")
div.appendChild(this.stroke)
this.fill = document.createElement("div")
this.fill.classList.add("fill")
div.appendChild(this.fill)
this.current = 0
this.shown = -1
this.songOffset = songOffset || 0
this.vttOffset = 0
this.rLinebreak = /\n|\r\n/
this.lines = parsed ? file : this.parseFile(file)
this.length = this.lines.length
}
parseFile(file){
var lines = []
var commands = file.split(/\n\n|\r\n\r\n/)
var arrow = " --> "
for(var i in commands){
var matches = commands[i].match(this.rLinebreak)
if(matches){
var cmd = commands[i].slice(0, matches.index)
var value = commands[i].slice(matches.index + 1)
}else{
var cmd = commands[i]
var value = ""
}
if(cmd.startsWith("WEBVTT")){
var nameValue = cmd.slice(7).split(";")
for(var j in nameValue){
var [name, value] = nameValue[j].split(":")
if(name.trim().toLowerCase() === "offset"){
this.vttOffset = (parseFloat(value.trim()) || 0) * 1000
}
}
}else{
var time = null
var index = cmd.indexOf(arrow)
if(index !== -1){
time = cmd
}else{
var matches = value.match(this.rLinebreak)
if(matches){
var value1 = value.slice(0, matches.index)
index = value1.indexOf(arrow)
if(index !== -1){
time = value1
value = value.slice(index)
}
}
}
if(time !== null){
var start = time.slice(0, index)
var end = time.slice(index + arrow.length)
var index = end.indexOf(" ")
if(index !== -1){
end = end.slice(0, index)
}
var text = value.trim()
var textLang = ""
var firstLang = -1
var index2 = -1
while(true){
var index1 = text.indexOf("<lang ", index2 + 1)
if(firstLang === -1){
firstLang = index1
}
if(index1 !== -1){
index2 = text.indexOf(">", index1 + 6)
if(index2 === -1){
break
}
var lang = text.slice(index1 + 6, index2).toLowerCase()
if(strings.id === lang){
var index3 = text.indexOf("<lang ", index2 + 1)
if(index3 !== -1){
textLang = text.slice(index2 + 1, index3)
}else{
textLang = text.slice(index2 + 1)
}
}
}else{
break
}
}
if(!textLang){
textLang = firstLang === -1 ? text : text.slice(0, firstLang)
}
lines.push({
start: this.convertTime(start),
end: this.convertTime(end),
text: textLang
})
}
}
}
return lines
}
convertTime(time){
if(time.startsWith("-")){
var mul = -1
time = time.slice(1)
}else{
var mul = 1
}
var array = time.split(":")
if(array.length === 2){
var h = 0
var m = array[0]
var s = array[1]
}else{
var h = parseInt(array[0])
var m = array[1]
var s = array[2]
}
var index = s.indexOf(",")
if(index !== -1){
s = s.slice(0, index) + "." + s.slice(index + 1)
}
return ((h * 60 + parseInt(m)) * 60 + parseFloat(s)) * 1000 * mul
}
update(ms){
if(this.current >= this.length){
return
}
ms += this.songOffset + this.vttOffset
var currentLine = this.lines[this.current]
while(currentLine && ms > currentLine.end){
currentLine = this.lines[++this.current]
}
if(this.shown !== this.current){
if(currentLine && ms >= currentLine.start){
this.setText(this.lines[this.current].text)
this.shown = this.current
}else if(this.shown !== -1){
this.setText("")
this.shown = -1
}
}
}
setText(text){
this.stroke.innerHTML = this.fill.innerHTML = ""
var hasRuby = false
while(text){
var matches = text.match(this.rLinebreak)
var index1 = matches ? matches.index : -1
var index2 = text.indexOf("<ruby>")
if(index1 !== -1 && (index2 === -1 || index2 > index1)){
this.textNode(text.slice(0, index1))
this.linebreakNode()
text = text.slice(index1 + matches[0].length)
}else if(index2 !== -1){
hasRuby = true
this.textNode(text.slice(0, index2))
text = text.slice(index2 + 6)
var index = text.indexOf("</ruby>")
if(index !== -1){
var ruby = text.slice(0, index)
text = text.slice(index + 7)
}else{
var ruby = text
text = ""
}
var index = ruby.indexOf("<rt>")
if(index !== -1){
var node1 = ruby.slice(0, index)
ruby = ruby.slice(index + 4)
var index = ruby.indexOf("</rt>")
if(index !== -1){
var node2 = ruby.slice(0, index)
}else{
var node2 = ruby
}
}else{
var node1 = ruby
var node2 = ""
}
this.rubyNode(node1, node2)
}else{
this.textNode(text)
break
}
}
}
insertNode(func){
this.stroke.appendChild(func())
this.fill.appendChild(func())
}
textNode(text){
this.insertNode(() => document.createTextNode(text))
}
linebreakNode(){
this.insertNode(() => document.createElement("br"))
}
rubyNode(node1, node2){
this.insertNode(() => {
var ruby = document.createElement("ruby")
var rt = document.createElement("rt")
ruby.appendChild(document.createTextNode(node1))
rt.appendChild(document.createTextNode(node2))
ruby.appendChild(rt)
return ruby
})
}
setScale(ratio){
this.div.style.setProperty("--scale", ratio)
}
offsetChange(songOffset, vttOffset){
if(typeof songOffset !== "undefined"){
this.songOffset = songOffset
}
if(typeof vttOffset !== "undefined"){
this.vttOffset = vttOffset
}
this.setText("")
this.current = 0
this.shown = -1
}
clean(){
if(this.shown !== -1){
this.setText("")
}
delete this.div
delete this.stroke
delete this.fill
delete this.lines
}
}

View File

@ -84,6 +84,7 @@ var strings
var vectors var vectors
var settings var settings
var scoreStorage var scoreStorage
var account = {}
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){ if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){

View File

@ -3,6 +3,8 @@ class P2Connection{
this.closed = true this.closed = true
this.lastMessages = {} this.lastMessages = {}
this.otherConnected = false this.otherConnected = false
this.name = null
this.player = 1
this.allEvents = new Map() this.allEvents = new Map()
this.addEventListener("message", this.message.bind(this)) this.addEventListener("message", this.message.bind(this))
this.currentHash = "" this.currentHash = ""
@ -102,6 +104,10 @@ class P2Connection{
} }
message(response){ message(response){
switch(response.type){ switch(response.type){
case "gameload":
if("player" in response.value){
this.player = response.value.player === 2 ? 2 : 1
}
case "gamestart": case "gamestart":
this.otherConnected = true this.otherConnected = true
this.notes = [] this.notes = []
@ -110,6 +116,7 @@ class P2Connection{
this.kaAmount = 0 this.kaAmount = 0
this.results = false this.results = false
this.branch = "normal" this.branch = "normal"
scoreStorage.clearP2()
break break
case "gameend": case "gameend":
this.otherConnected = false this.otherConnected = false
@ -123,11 +130,13 @@ class P2Connection{
this.hash("") this.hash("")
this.hashLock = false this.hashLock = false
} }
this.name = null
scoreStorage.clearP2()
break break
case "gameresults": case "gameresults":
this.results = {} this.results = {}
for(var i in response.value){ for(var i in response.value){
this.results[i] = response.value[i].toString() this.results[i] = response.value[i] === null ? null : response.value[i].toString()
} }
break break
case "note": case "note":
@ -150,6 +159,44 @@ class P2Connection{
this.clearMessage("users") this.clearMessage("users")
this.otherConnected = true this.otherConnected = true
this.session = true this.session = true
scoreStorage.clearP2()
if("player" in response.value){
this.player = response.value.player === 2 ? 2 : 1
}
break
case "name":
this.name = response.value ? response.value.toString() : response.value
break
case "getcrowns":
if(response.value){
var output = {}
for(var i in response.value){
if(response.value[i]){
var score = scoreStorage.get(response.value[i], false, true)
if(score){
var crowns = {}
for(var diff in score){
if(diff !== "title"){
crowns[diff] = {
crown: score[diff].crown
}
}
}
}else{
var crowns = null
}
output[response.value[i]] = crowns
}
}
p2.send("crowns", output)
}
break
case "crowns":
if(response.value){
for(var i in response.value){
scoreStorage.addP2(i, false, response.value[i], true)
}
}
break break
} }
} }

View File

@ -86,6 +86,9 @@ class PageEvents{
}) })
} }
keyEvent(event){ keyEvent(event){
if(!("key" in event) || event.ctrlKey && (event.key === "c" || event.key === "x" || event.key === "v")){
return
}
if(this.kbd.indexOf(event.key.toLowerCase()) !== -1){ if(this.kbd.indexOf(event.key.toLowerCase()) !== -1){
this.lastKeyEvent = Date.now() this.lastKeyEvent = Date.now()
event.preventDefault() event.preventDefault()

View File

@ -48,6 +48,7 @@ class ParseOsu{
lastBeatInterval: 0, lastBeatInterval: 0,
bpm: 0 bpm: 0
} }
this.events = []
this.generalInfo = this.parseGeneralInfo() this.generalInfo = this.parseGeneralInfo()
this.metadata = this.parseMetadata() this.metadata = this.parseMetadata()
this.editor = this.parseEditor() this.editor = this.parseEditor()
@ -244,6 +245,18 @@ class ParseOsu{
var circles = [] var circles = []
var circleID = 0 var circleID = 0
var indexes = this.getStartEndIndexes("HitObjects") var indexes = this.getStartEndIndexes("HitObjects")
var lastBeatMS = this.beatInfo.beatInterval
var lastGogo = false
var pushCircle = circle => {
circles.push(circle)
if(lastBeatMS !== circle.beatMS || lastGogo !== circle.gogoTime){
lastBeatMS = circle.beatMS
lastGogo = circle.gogoTime
this.events.push(circle)
}
}
for(var i = indexes.start; i <= indexes.end; i++){ for(var i = indexes.start; i <= indexes.end; i++){
circleID++ circleID++
var values = this.data[i].split(",") var values = this.data[i].split(",")
@ -277,7 +290,7 @@ class ParseOsu{
var endTime = parseInt(values[this.osu.ENDTIME]) var endTime = parseInt(values[this.osu.ENDTIME])
var hitMultiplier = this.difficultyRange(this.difficulty.overallDifficulty, 3, 5, 7.5) * 1.65 var hitMultiplier = this.difficultyRange(this.difficulty.overallDifficulty, 3, 5, 7.5) * 1.65
var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier)) var requiredHits = Math.floor(Math.max(1, (endTime - start) / 1000 * hitMultiplier))
circles.push(new Circle({ pushCircle(new Circle({
id: circleID, id: circleID,
start: start + this.offset, start: start + this.offset,
type: "balloon", type: "balloon",
@ -304,7 +317,7 @@ class ParseOsu{
type = "drumroll" type = "drumroll"
txt = strings.note.drumroll txt = strings.note.drumroll
} }
circles.push(new Circle({ pushCircle(new Circle({
id: circleID, id: circleID,
start: start + this.offset, start: start + this.offset,
type: type, type: type,
@ -339,7 +352,7 @@ class ParseOsu{
emptyValue = true emptyValue = true
} }
if(!emptyValue){ if(!emptyValue){
circles.push(new Circle({ pushCircle(new Circle({
id: circleID, id: circleID,
start: start + this.offset, start: start + this.offset,
type: type, type: type,

View File

@ -43,6 +43,7 @@
this.metadata = this.parseMetadata() this.metadata = this.parseMetadata()
this.measures = [] this.measures = []
this.beatInfo = {} this.beatInfo = {}
this.events = []
if(!metaOnly){ if(!metaOnly){
this.circles = this.parseCircles() this.circles = this.parseCircles()
} }
@ -83,6 +84,8 @@
} }
}else if(name.startsWith("branchstart") && inSong){ }else if(name.startsWith("branchstart") && inSong){
courses[courseName].branch = true courses[courseName].branch = true
}else if(name.startsWith("lyric") && inSong){
courses[courseName].inlineLyrics = true
} }
}else if(!inSong){ }else if(!inSong){
@ -157,6 +160,7 @@
var circleID = 0 var circleID = 0
var regexAZ = /[A-Z]/ var regexAZ = /[A-Z]/
var regexSpace = /\s/ var regexSpace = /\s/
var regexLinebreak = /\\n/g
var isAllDon = (note_chain, start_pos) => { var isAllDon = (note_chain, start_pos) => {
for (var i = start_pos; i < note_chain.length; ++i) { for (var i = start_pos; i < note_chain.length; ++i) {
var note = note_chain[i]; var note = note_chain[i];
@ -248,7 +252,12 @@
lastDrumroll = circleObj lastDrumroll = circleObj
} }
if(note.event){
this.events.push(circleObj)
}
if(note.type !== "event"){
circles.push(circleObj) circles.push(circleObj)
}
} else if (!(currentMeasure.length >= 24 && (!currentMeasure[i + 1] || currentMeasure[i + 1].type)) } else if (!(currentMeasure.length >= 24 && (!currentMeasure[i + 1] || currentMeasure[i + 1].type))
&& !(currentMeasure.length >= 48 && (!currentMeasure[i + 2] || currentMeasure[i + 2].type || !currentMeasure[i + 3] || currentMeasure[i + 3].type))) { && !(currentMeasure.length >= 48 && (!currentMeasure[i + 2] || currentMeasure[i + 2].type || !currentMeasure[i + 3] || currentMeasure[i + 3].type))) {
if (note_chain.length > 1 && currentMeasure.length >= 8) { if (note_chain.length > 1 && currentMeasure.length >= 8) {
@ -266,9 +275,12 @@
} }
} }
var insertNote = circleObj => { var insertNote = circleObj => {
if(circleObj){
if(bpm !== lastBpm || gogo !== lastGogo){
circleObj.event = true
lastBpm = bpm lastBpm = bpm
lastGogo = gogo lastGogo = gogo
if(circleObj){ }
currentMeasure.push(circleObj) currentMeasure.push(circleObj)
} }
} }
@ -402,6 +414,18 @@
} }
branchObj[branchName] = currentBranch branchObj[branchName] = currentBranch
break break
case "lyric":
if(!this.lyrics){
this.lyrics = []
}
if(this.lyrics.length !== 0){
this.lyrics[this.lyrics.length - 1].end = ms
}
this.lyrics.push({
start: ms,
text: value.trim().replace(regexLinebreak, "\n")
})
break
} }
}else{ }else{
@ -536,6 +560,10 @@
this.scoreinit = autoscore.ScoreInit; this.scoreinit = autoscore.ScoreInit;
this.scorediff = autoscore.ScoreDiff; this.scorediff = autoscore.ScoreDiff;
} }
if(this.lyrics){
var line = this.lyrics[this.lyrics.length - 1]
line.end = Math.max(ms, line.start) + 5000
}
return circles return circles
} }
} }

View File

@ -2,9 +2,19 @@ class Scoresheet{
constructor(controller, results, multiplayer, touchEnabled){ constructor(controller, results, multiplayer, touchEnabled){
this.controller = controller this.controller = controller
this.resultsObj = results this.resultsObj = results
this.results = {} this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0]
var player0 = this.player[0]
this.results = []
this.results[player0] = {}
this.rules = []
this.rules[player0] = this.controller.game.rules
if(multiplayer){
this.player.push(p2.player === 2 ? 0 : 1)
this.results[this.player[1]] = p2.results
this.rules[this.player[1]] = this.controller.syncWith.game.rules
}
for(var i in results){ for(var i in results){
this.results[i] = results[i].toString() this.results[player0][i] = results[i] === null ? null : results[i].toString()
} }
this.multiplayer = multiplayer this.multiplayer = multiplayer
this.touchEnabled = touchEnabled this.touchEnabled = touchEnabled
@ -39,6 +49,7 @@ class Scoresheet{
this.draw = new CanvasDraw(noSmoothing) this.draw = new CanvasDraw(noSmoothing)
this.canvasCache = new CanvasCache(noSmoothing) this.canvasCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing)
this.keyboard = new Keyboard({ this.keyboard = new Keyboard({
confirm: ["enter", "space", "esc", "don_l", "don_r"] confirm: ["enter", "space", "esc", "don_l", "don_r"]
@ -208,6 +219,7 @@ class Scoresheet{
this.canvas.style.height = (winH / this.pixelRatio) + "px" this.canvas.style.height = (winH / this.pixelRatio) + "px"
this.canvasCache.resize(winW / ratio, 80 + 1, ratio) this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
this.nameplateCache.resize(274, 134, ratio + 0.2)
if(!this.multiplayer){ if(!this.multiplayer){
this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio) this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio)
@ -233,6 +245,9 @@ class Scoresheet{
if(!this.canvasCache.canvas){ if(!this.canvasCache.canvas){
this.canvasCache.resize(winW / ratio, 80 + 1, ratio) this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
} }
if(!this.nameplateCache.canvas){
this.nameplateCache.resize(274, 67, ratio + 0.2)
}
} }
this.winW = winW this.winW = winW
this.winH = winH this.winH = winH
@ -243,7 +258,7 @@ class Scoresheet{
var frameTop = winH / 2 - 720 / 2 var frameTop = winH / 2 - 720 / 2
var frameLeft = winW / 2 - 1280 / 2 var frameLeft = winW / 2 - 1280 / 2
var players = this.multiplayer && p2.results ? 2 : 1 var players = this.multiplayer ? 2 : 1
var p2Offset = 298 var p2Offset = 298
var bgOffset = 0 var bgOffset = 0
@ -326,28 +341,21 @@ class Scoresheet{
} }
var rules = this.controller.game.rules var rules = this.controller.game.rules
var gaugePercent = rules.gaugePercent(this.results.gauge) var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000
var gaugeClear = [rules.gaugeClear] if(players === 2 && failedOffset !== 0){
if(players === 2){ var p2results = this.results[this.player[1]]
gaugeClear.push(this.controller.syncWith.game.rules.gaugeClear) if(p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)){
}
var failedOffset = gaugePercent >= gaugeClear[0] ? 0 : -2000
if(players === 2){
var gauge2 = this.controller.syncWith.game.rules.gaugePercent(p2.results.gauge)
if(gauge2 > gaugePercent && failedOffset !== 0 && gauge2 >= gaugeClear[1]){
failedOffset = 0 failedOffset = 0
} }
} }
if(elapsed >= 3100 + failedOffset){ if(elapsed >= 3100 + failedOffset){
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
ctx.save() ctx.save()
var results = this.results var results = this.results[p]
if(p === 1){ if(!results){
results = p2.results continue
} }
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules var clear = this.rules[p].clearReached(results.gauge)
var resultGauge = playerRules.gaugePercent(results.gauge)
var clear = resultGauge >= gaugeClear[p]
if(p === 1 || !this.multiplayer && clear){ if(p === 1 || !this.multiplayer && clear){
ctx.translate(0, 290) ctx.translate(0, 290)
} }
@ -410,7 +418,7 @@ class Scoresheet{
this.draw.layeredText({ this.draw.layeredText({
ctx: ctx, ctx: ctx,
text: this.results.title, text: this.results[this.player[0]].title,
fontSize: 40, fontSize: 40,
fontFamily: this.font, fontFamily: this.font,
x: 1257, x: 1257,
@ -426,9 +434,11 @@ class Scoresheet{
ctx.save() ctx.save()
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(!results){
continue
}
if(p === 1){ if(p === 1){
results = p2.results
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
@ -450,6 +460,30 @@ class Scoresheet{
ctx.fillText(text, 395, 308) ctx.fillText(text, 395, 308)
ctx.miterLimit = 10 ctx.miterLimit = 10
var defaultName = p === 0 ? strings.defaultName : strings.default2PName
if(p === this.player[0]){
var name = account.loggedIn ? account.displayName : defaultName
}else{
var name = results.name || defaultName
}
this.nameplateCache.get({
ctx: ctx,
x: 259,
y: 92,
w: 273,
h: 66,
id: p.toString() + "p" + name,
}, ctx => {
this.draw.nameplate({
ctx: ctx,
x: 3,
y: 3,
name: name,
font: this.font,
blue: p === 1
})
})
if(this.controller.autoPlayEnabled){ if(this.controller.autoPlayEnabled){
ctx.drawImage(assets.image["badge_auto"], ctx.drawImage(assets.image["badge_auto"],
431, 311, 34, 34 431, 311, 34, 34
@ -581,7 +615,7 @@ class Scoresheet{
if(this.tetsuoHanaClass){ if(this.tetsuoHanaClass){
this.tetsuoHana.classList.remove(this.tetsuoHanaClass) this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
} }
this.tetsuoHanaClass = rules.clearReached(this.results.gauge) ? "dance" : "failed" this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed"
this.tetsuoHana.classList.add(this.tetsuoHanaClass) this.tetsuoHana.classList.add(this.tetsuoHanaClass)
} }
} }
@ -595,32 +629,32 @@ class Scoresheet{
ctx.translate(frameLeft, frameTop) ctx.translate(frameLeft, frameTop)
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(!results){
continue
}
if(p === 1){ if(p === 1){
results = p2.results
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
var gaugePercent = rules.gaugePercent(results.gauge)
var w = 712 var w = 712
this.draw.gauge({ this.draw.gauge({
ctx: ctx, ctx: ctx,
x: 558 + w, x: 558 + w,
y: p === 1 ? 124 : 116, y: p === 1 ? 124 : 116,
clear: gaugeClear[p], clear: this.rules[p].gaugeClear,
percentage: gaugePercent, percentage: this.rules[p].gaugePercent(results.gauge),
font: this.font, font: this.font,
scale: w / 788, scale: w / 788,
scoresheet: true, scoresheet: true,
blue: p === 1, blue: p === 1,
multiplayer: p === 1 multiplayer: p === 1
}) })
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules
this.draw.soul({ this.draw.soul({
ctx: ctx, ctx: ctx,
x: 1215, x: 1215,
y: 144, y: 144,
scale: 36 / 42, scale: 36 / 42,
cleared: playerRules.clearReached(results.gauge) cleared: this.rules[p].clearReached(results.gauge)
}) })
} }
}) })
@ -633,13 +667,12 @@ class Scoresheet{
var noCrownResultWait = -2000; var noCrownResultWait = -2000;
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(p === 1){ if(!results){
results = p2.results continue
} }
var crownType = null var crownType = null
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules if(this.rules[p].clearReached(results.gauge)){
if(playerRules.clearReached(results.gauge)){
crownType = results.bad === "0" ? "gold" : "silver" crownType = results.bad === "0" ? "gold" : "silver"
} }
if(crownType !== null){ if(crownType !== null){
@ -702,7 +735,10 @@ class Scoresheet{
var times = {} var times = {}
var lastTime = 0 var lastTime = 0
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = p === 0 ? this.results : p2.results var results = this.results[p]
if(!results){
continue
}
var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame
if(currentTime > lastTime){ if(currentTime > lastTime){
lastTime = currentTime lastTime = currentTime
@ -711,7 +747,10 @@ class Scoresheet{
for(var i in printNumbers){ for(var i in printNumbers){
var largestTime = 0 var largestTime = 0
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = p === 0 ? this.results : p2.results var results = this.results[p]
if(!results){
continue
}
times[printNumbers[i]] = lastTime + 500 times[printNumbers[i]] = lastTime + 500
var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
if(currentTime > largestTime){ if(currentTime > largestTime){
@ -727,9 +766,11 @@ class Scoresheet{
} }
for(var p = 0; p < players; p++){ for(var p = 0; p < players; p++){
var results = this.results var results = this.results[p]
if(!results){
continue
}
if(p === 1){ if(p === 1){
results = p2.results
ctx.translate(0, p2Offset) ctx.translate(0, p2Offset)
} }
ctx.save() ctx.save()
@ -823,7 +864,7 @@ class Scoresheet{
if(elapsed >= 1000){ if(elapsed >= 1000){
this.clean() this.clean()
this.controller.songSelection(true) this.controller.songSelection(true, this.showWarning)
} }
} }
@ -890,10 +931,14 @@ class Scoresheet{
delete this.resultsObj.title delete this.resultsObj.title
delete this.resultsObj.difficulty delete this.resultsObj.difficulty
delete this.resultsObj.gauge delete this.resultsObj.gauge
scoreStorage.add(hash, difficulty, this.resultsObj, true, title) scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => {
this.showWarning = {name: "scoreSaveFailed"}
})
}else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){ }else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){
oldScore.crown = crown oldScore.crown = crown
scoreStorage.add(hash, difficulty, oldScore, true, title) scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => {
this.showWarning = {name: "scoreSaveFailed"}
})
} }
} }
this.scoreSaved = true this.scoreSaved = true
@ -908,7 +953,7 @@ class Scoresheet{
snd.buffer.loadSettings() snd.buffer.loadSettings()
this.redrawRunning = false this.redrawRunning = false
pageEvents.remove(this.canvas, ["mousedown", "touchstart"]) pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
if(this.multiplayer !== 2 && this.touchEnabled){ if(this.touchEnabled){
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend") pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
} }
if(this.session){ if(this.session){
@ -920,5 +965,7 @@ class Scoresheet{
delete this.ctx delete this.ctx
delete this.canvas delete this.canvas
delete this.fadeScreen delete this.fadeScreen
delete this.results
delete this.rules
} }
} }

View File

@ -1,23 +1,38 @@
class ScoreStorage{ class ScoreStorage{
constructor(){ constructor(){
this.scores = {} this.scores = {}
this.scoresP2 = {}
this.requestP2 = new Set()
this.requestedP2 = new Set()
this.songTitles = {} this.songTitles = {}
this.difficulty = ["oni", "ura", "hard", "normal", "easy"] this.difficulty = ["oni", "ura", "hard", "normal", "easy"]
this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"] this.scoreKeys = ["points", "good", "ok", "bad", "maxCombo", "drumroll"]
this.crownValue = ["", "silver", "gold"] this.crownValue = ["", "silver", "gold"]
this.load()
} }
load(){ load(strings, loadFailed){
this.scores = {} var scores = {}
this.scoreStrings = {} var scoreStrings = {}
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{ try{
var localScores = localStorage.getItem("scoreStorage") var localScores = localStorage.getItem("scoreStorage")
if(localScores){ if(localScores){
this.scoreStrings = JSON.parse(localScores) scoreStrings = JSON.parse(localScores)
} }
}catch(e){} }catch(e){}
for(var hash in this.scoreStrings){ }
var scoreString = this.scoreStrings[hash] for(var hash in scoreStrings){
var scoreString = scoreStrings[hash]
var songAdded = false var songAdded = false
if(typeof scoreString === "string" && scoreString){ if(typeof scoreString === "string" && scoreString){
var diffArray = scoreString.split(";") var diffArray = scoreString.split(";")
@ -37,26 +52,64 @@ class ScoreStorage{
score[name] = value score[name] = value
} }
if(!songAdded){ if(!songAdded){
this.scores[hash] = {title: null} scores[hash] = {title: null}
songAdded = true songAdded = true
} }
this.scores[hash][this.difficulty[i]] = score scores[hash][this.difficulty[i]] = score
} }
} }
} }
} }
if(loadFailed){
for(var hash in scores){
for(var i in this.difficulty){
var diff = this.difficulty[i]
if(scores[hash][diff]){
this.add(hash, diff, scores[hash][diff], true, this.songTitles[hash] || null).then(() => {
localStorage.removeItem("saveFailed")
}, () => {})
}
}
}
}else{
this.scores = scores
this.scoreStrings = scoreStrings
}
if(strings){
this.load(false, true)
}
}
prepareScores(scores){
var output = []
for (var k in scores) {
output.push({'hash': k, 'score': scores[k]})
}
return output
}
prepareStrings(scores){
var output = {}
for(var k in scores){
output[scores[k].hash] = scores[k].score
}
return output
} }
save(){ save(){
for(var hash in this.scores){ for(var hash in this.scores){
this.writeString(hash) this.writeString(hash)
} }
this.write() this.write()
return this.sendToServer({
scores: this.prepareScores(this.scoreStrings),
is_import: true
})
} }
write(){ write(){
if(!account.loggedIn){
try{ try{
localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings)) localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings))
}catch(e){} }catch(e){}
} }
}
writeString(hash){ writeString(hash){
var score = this.scores[hash] var score = this.scores[hash]
var diffArray = [] var diffArray = []
@ -101,17 +154,82 @@ class ScoreStorage{
} }
} }
} }
add(song, difficulty, scoreObject, isHash, setTitle){ getP2(song, difficulty, isHash){
if(!song){
return this.scoresP2
}else{
var hash = isHash ? song : this.titleHash(song)
if(!(hash in this.scoresP2) && !this.requestP2.has(hash) && !this.requestedP2.has(hash)){
this.requestP2.add(hash)
this.requestedP2.add(hash)
}
if(difficulty){
if(hash in this.scoresP2){
return this.scoresP2[hash][difficulty]
}
}else{
return this.scoresP2[hash]
}
}
}
add(song, difficulty, scoreObject, isHash, setTitle, saveFailed){
var hash = isHash ? song : this.titleHash(song) var hash = isHash ? song : this.titleHash(song)
if(!(hash in this.scores)){ if(!(hash in this.scores)){
this.scores[hash] = {} this.scores[hash] = {}
} }
if(difficulty){
if(setTitle){ if(setTitle){
this.scores[hash].title = setTitle this.scores[hash].title = setTitle
} }
this.scores[hash][difficulty] = scoreObject this.scores[hash][difficulty] = scoreObject
}else{
this.scores[hash] = scoreObject
if(setTitle){
this.scores[hash].title = setTitle
}
}
this.writeString(hash) this.writeString(hash)
this.write() this.write()
if(saveFailed){
var failedScores = {}
try{
var localScores = localStorage.getItem("saveFailed")
if(localScores){
failedScores = JSON.parse(localScores)
}
}catch(e){}
if(!(hash in failedScores)){
failedScores[hash] = {}
}
failedScores[hash] = this.scoreStrings[hash]
try{
localStorage.setItem("saveFailed", JSON.stringify(failedScores))
}catch(e){}
return Promise.reject()
}else{
var obj = {}
obj[hash] = this.scoreStrings[hash]
return this.sendToServer({
scores: this.prepareScores(obj)
}).catch(() => this.add(song, difficulty, scoreObject, isHash, setTitle, true))
}
}
addP2(song, difficulty, scoreObject, isHash, setTitle){
var hash = isHash ? song : this.titleHash(song)
if(!(hash in this.scores)){
this.scoresP2[hash] = {}
}
if(difficulty){
if(setTitle){
this.scoresP2[hash].title = setTitle
}
this.scoresP2[hash][difficulty] = scoreObject
}else{
this.scoresP2[hash] = scoreObject
if(setTitle){
this.scoresP2[hash].title = setTitle
}
}
} }
template(){ template(){
var template = {crown: ""} var template = {crown: ""}
@ -146,6 +264,62 @@ class ScoreStorage{
delete this.scoreStrings[hash] delete this.scoreStrings[hash]
} }
this.write() this.write()
this.sendToServer({
scores: this.prepareScores(this.scoreStrings),
is_import: true
})
}
}
sendToServer(obj, retry){
if(account.loggedIn){
return loader.getCsrfToken().then(token => {
var request = new XMLHttpRequest()
request.open("POST", "api/scores/save")
var promise = pageEvents.load(request).then(response => {
if(request.status !== 200){
return Promise.reject()
}
}).catch(() => {
if(retry){
this.scoreSaveFailed = true
account.loggedIn = false
delete account.username
delete account.displayName
this.load()
pageEvents.send("logout")
return Promise.reject()
}else{
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, 3000)
}).then(() => this.sendToServer(obj, true))
}
})
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
request.setRequestHeader("X-CSRFToken", token)
request.send(JSON.stringify(obj))
return promise
})
}else{
return Promise.resolve()
}
}
eventLoop(){
if(p2.session && this.requestP2.size){
var req = []
this.requestP2.forEach(hash => {
req.push(hash)
})
this.requestP2.clear()
if(req.length){
p2.send("getcrowns", req)
} }
} }
} }
clearP2(){
this.scoresP2 = {}
this.requestP2.clear()
this.requestedP2.clear()
}
}

View File

@ -34,7 +34,10 @@ class Session{
pageEvents.send("session-start", "host") pageEvents.send("session-start", "host")
} }
}) })
p2.send("invite") p2.send("invite", {
id: null,
name: account.loggedIn ? account.displayName : null
})
pageEvents.send("session") pageEvents.send("session")
} }
getElement(name){ getElement(name){

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -126,8 +126,14 @@
this.comboCache = new CanvasCache(noSmoothing) this.comboCache = new CanvasCache(noSmoothing)
this.pauseCache = new CanvasCache(noSmoothing) this.pauseCache = new CanvasCache(noSmoothing)
this.branchCache = new CanvasCache(noSmoothing) this.branchCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing)
this.multiplayer = this.controller.multiplayer this.multiplayer = this.controller.multiplayer
if(this.multiplayer === 2){
this.player = p2.player === 2 ? 1 : 2
}else{
this.player = this.controller.multiplayer ? p2.player : 1
}
this.touchEnabled = this.controller.touchEnabled this.touchEnabled = this.controller.touchEnabled
this.touch = -Infinity this.touch = -Infinity
@ -223,24 +229,31 @@
this.winH = winH this.winH = winH
this.ratio = ratio this.ratio = ratio
if(this.multiplayer !== 2){ if(this.player !== 2){
this.canvas.width = winW this.canvas.width = winW
this.canvas.height = winH this.canvas.height = winH
ctx.scale(ratio, ratio) ctx.scale(ratio, ratio)
this.canvas.style.width = (winW / this.pixelRatio) + "px" this.canvas.style.width = (winW / this.pixelRatio) + "px"
this.canvas.style.height = (winH / this.pixelRatio) + "px" this.canvas.style.height = (winH / this.pixelRatio) + "px"
this.titleCache.resize(640, 90, ratio) this.titleCache.resize(640, 90, ratio)
} }
if(!this.multiplayer){ if(!this.multiplayer){
this.pauseCache.resize(81 * this.pauseOptions.length * 2, 464, ratio) this.pauseCache.resize(81 * this.pauseOptions.length * 2, 464, ratio)
} }
if(this.portrait){
this.nameplateCache.resize(220, 54, ratio + 0.2)
}else{
this.nameplateCache.resize(274, 67, ratio + 0.2)
}
this.fillComboCache() this.fillComboCache()
this.setDonBgHeight() this.setDonBgHeight()
if(this.controller.lyrics){
this.controller.lyrics.setScale(ratio / this.pixelRatio)
}
resized = true resized = true
}else if(this.controller.game.paused && !document.hasFocus()){ }else if(this.controller.game.paused && !document.hasFocus()){
return return
}else if(this.multiplayer !== 2){ }else if(this.player !== 2){
ctx.clearRect(0, 0, winW / ratio, winH / ratio) ctx.clearRect(0, 0, winW / ratio, winH / ratio)
} }
winW /= ratio winW /= ratio
@ -257,8 +270,8 @@
var frameTop = winH / 2 - 720 / 2 var frameTop = winH / 2 - 720 / 2
var frameLeft = winW / 2 - 1280 / 2 var frameLeft = winW / 2 - 1280 / 2
} }
if(this.multiplayer === 2){ if(this.player === 2){
frameTop += this.multiplayer === 2 ? 165 : 176 frameTop += 165
} }
if(touchMultiplayer){ if(touchMultiplayer){
if(!this.touchp2Class){ if(!this.touchp2Class){
@ -273,16 +286,20 @@
this.setDonBgHeight() this.setDonBgHeight()
} }
if(this.controller.lyrics){
this.controller.lyrics.update(ms)
}
ctx.save() ctx.save()
ctx.translate(0, frameTop) ctx.translate(0, frameTop)
this.drawGogoTime() this.drawGogoTime()
if(!touchMultiplayer || this.multiplayer === 1 && frameTop >= 0){ if(!touchMultiplayer || this.player === 1 && frameTop >= 0){
this.assets.drawAssets("background") this.assets.drawAssets("background")
} }
if(this.multiplayer !== 2){ if(this.player !== 2){
this.titleCache.get({ this.titleCache.get({
ctx: ctx, ctx: ctx,
x: winW - (touchMultiplayer && fullScreenSupported ? 750 : 650), x: winW - (touchMultiplayer && fullScreenSupported ? 750 : 650),
@ -350,7 +367,7 @@
var score = this.controller.getGlobalScore() var score = this.controller.getGlobalScore()
var gaugePercent = this.rules.gaugePercent(score.gauge) var gaugePercent = this.rules.gaugePercent(score.gauge)
if(this.multiplayer === 2){ if(this.player === 2){
var scoreImg = "bg_score_p2" var scoreImg = "bg_score_p2"
var scoreFill = "#6bbec0" var scoreFill = "#6bbec0"
}else{ }else{
@ -373,30 +390,55 @@
size: 100, size: 100,
paddingLeft: 0 paddingLeft: 0
} }
this.scorePos = {x: 363, y: frameTop + (this.multiplayer === 2 ? 520 : 227)} this.scorePos = {x: 363, y: frameTop + (this.player === 2 ? 520 : 227)}
var animPos = { var animPos = {
x1: this.slotPos.x + 13, x1: this.slotPos.x + 13,
y1: this.slotPos.y + (this.multiplayer === 2 ? 27 : -27), y1: this.slotPos.y + (this.player === 2 ? 27 : -27),
x2: winW - 38, x2: winW - 38,
y2: frameTop + (this.multiplayer === 2 ? 484 : 293) y2: frameTop + (this.player === 2 ? 484 : 293)
} }
var taikoPos = { var taikoPos = {
x: 19, x: 19,
y: frameTop + (this.multiplayer === 2 ? 464 : 184), y: frameTop + (this.player === 2 ? 464 : 184),
w: 111, w: 111,
h: 130 h: 130
} }
this.nameplateCache.get({
ctx: ctx,
x: 167,
y: this.player === 2 ? 565 : 160,
w: 219,
h: 53,
id: "1p",
}, ctx => {
var defaultName = this.player === 1 ? strings.defaultName : strings.default2PName
if(this.multiplayer === 2){
var name = p2.name || defaultName
}else{
var name = account.loggedIn ? account.displayName : defaultName
}
this.draw.nameplate({
ctx: ctx,
x: 3,
y: 3,
scale: 0.8,
name: name,
font: this.font,
blue: this.player === 2
})
})
ctx.fillStyle = "#000" ctx.fillStyle = "#000"
ctx.fillRect( ctx.fillRect(
0, 0,
this.multiplayer === 2 ? 306 : 288, this.player === 2 ? 306 : 288,
winW, winW,
this.multiplayer === 1 ? 184 : 183 this.player === 1 ? 184 : 183
) )
ctx.beginPath() ctx.beginPath()
if(this.multiplayer === 2){ if(this.player === 2){
ctx.moveTo(0, 467) ctx.moveTo(0, 467)
ctx.lineTo(384, 467) ctx.lineTo(384, 467)
ctx.lineTo(384, 512) ctx.lineTo(384, 512)
@ -415,7 +457,7 @@
ctx.fillStyle = scoreFill ctx.fillStyle = scoreFill
var leftSide = (ctx, mul) => { var leftSide = (ctx, mul) => {
ctx.beginPath() ctx.beginPath()
if(this.multiplayer === 2){ if(this.player === 2){
ctx.moveTo(0, 468 * mul) ctx.moveTo(0, 468 * mul)
ctx.lineTo(380 * mul, 468 * mul) ctx.lineTo(380 * mul, 468 * mul)
ctx.lineTo(380 * mul, 512 * mul) ctx.lineTo(380 * mul, 512 * mul)
@ -445,7 +487,7 @@
// Score background // Score background
ctx.fillStyle = "#000" ctx.fillStyle = "#000"
ctx.beginPath() ctx.beginPath()
if(this.multiplayer === 2){ if(this.player === 2){
this.draw.roundedCorner(ctx, 184, 512, 20, 0) this.draw.roundedCorner(ctx, 184, 512, 20, 0)
ctx.lineTo(384, 512) ctx.lineTo(384, 512)
this.draw.roundedCorner(ctx, 384, 560, 12, 2) this.draw.roundedCorner(ctx, 384, 560, 12, 2)
@ -463,16 +505,16 @@
ctx.drawImage(assets.image["difficulty"], ctx.drawImage(assets.image["difficulty"],
0, 144 * this.difficulty[this.controller.selectedSong.difficulty], 0, 144 * this.difficulty[this.controller.selectedSong.difficulty],
168, 143, 168, 143,
126, this.multiplayer === 2 ? 497 : 228, 126, this.player === 2 ? 497 : 228,
62, 53 62, 53
) )
} }
// Badges // Badges
if(this.controller.autoPlayEnabled && !this.controller.multiplayer){ if(this.controller.autoPlayEnabled && !this.multiplayer){
this.ctx.drawImage(assets.image["badge_auto"], this.ctx.drawImage(assets.image["badge_auto"],
183, 183,
this.multiplayer === 2 ? 490 : 265, this.player === 2 ? 490 : 265,
23, 23,
23 23
) )
@ -482,7 +524,7 @@
ctx.fillStyle = "#000" ctx.fillStyle = "#000"
ctx.beginPath() ctx.beginPath()
var gaugeX = winW - 788 * 0.7 - 32 var gaugeX = winW - 788 * 0.7 - 32
if(this.multiplayer === 2){ if(this.player === 2){
ctx.moveTo(gaugeX, 464) ctx.moveTo(gaugeX, 464)
ctx.lineTo(winW, 464) ctx.lineTo(winW, 464)
ctx.lineTo(winW, 489) ctx.lineTo(winW, 489)
@ -497,18 +539,18 @@
this.draw.gauge({ this.draw.gauge({
ctx: ctx, ctx: ctx,
x: winW, x: winW,
y: this.multiplayer === 2 ? 468 : 273, y: this.player === 2 ? 468 : 273,
clear: this.rules.gaugeClear, clear: this.rules.gaugeClear,
percentage: gaugePercent, percentage: gaugePercent,
font: this.font, font: this.font,
scale: 0.7, scale: 0.7,
multiplayer: this.multiplayer === 2, multiplayer: this.player === 2,
blue: this.multiplayer === 2 blue: this.player === 2
}) })
this.draw.soul({ this.draw.soul({
ctx: ctx, ctx: ctx,
x: winW - 40, x: winW - 40,
y: this.multiplayer === 2 ? 484 : 293, y: this.player === 2 ? 484 : 293,
scale: 0.75, scale: 0.75,
cleared: this.rules.clearReached(score.gauge) cleared: this.rules.clearReached(score.gauge)
}) })
@ -536,26 +578,50 @@
} }
this.scorePos = { this.scorePos = {
x: 155, x: 155,
y: frameTop + (this.multiplayer === 2 ? 318 : 193) y: frameTop + (this.player === 2 ? 318 : 193)
} }
var animPos = { var animPos = {
x1: this.slotPos.x + 14, x1: this.slotPos.x + 14,
y1: this.slotPos.y + (this.multiplayer === 2 ? 29 : -29), y1: this.slotPos.y + (this.player === 2 ? 29 : -29),
x2: winW - 55, x2: winW - 55,
y2: frameTop + (this.multiplayer === 2 ? 378 : 165) y2: frameTop + (this.player === 2 ? 378 : 165)
} }
var taikoPos = {x: 179, y: frameTop + 190, w: 138, h: 162} var taikoPos = {x: 179, y: frameTop + 190, w: 138, h: 162}
this.nameplateCache.get({
ctx: ctx,
x: 320,
y: this.player === 2 ? 460 : 20,
w: 273,
h: 66,
id: "1p",
}, ctx => {
var defaultName = this.player === 1 ? strings.defaultName : strings.default2PName
if(this.multiplayer === 2){
var name = p2.name || defaultName
}else{
var name = account.loggedIn ? account.displayName : defaultName
}
this.draw.nameplate({
ctx: ctx,
x: 3,
y: 3,
name: name,
font: this.font,
blue: this.player === 2
})
})
ctx.fillStyle = "#000" ctx.fillStyle = "#000"
ctx.fillRect( ctx.fillRect(
0, 0,
184, 184,
winW, winW,
this.multiplayer === 1 ? 177 : 176 this.multiplayer && this.player === 1 ? 177 : 176
) )
ctx.beginPath() ctx.beginPath()
if(this.multiplayer === 2){ if(this.player === 2){
ctx.moveTo(328, 351) ctx.moveTo(328, 351)
ctx.lineTo(winW, 351) ctx.lineTo(winW, 351)
ctx.lineTo(winW, 385) ctx.lineTo(winW, 385)
@ -572,17 +638,17 @@
this.draw.gauge({ this.draw.gauge({
ctx: ctx, ctx: ctx,
x: winW, x: winW,
y: this.multiplayer === 2 ? 357 : 135, y: this.player === 2 ? 357 : 135,
clear: this.rules.gaugeClear, clear: this.rules.gaugeClear,
percentage: gaugePercent, percentage: gaugePercent,
font: this.font, font: this.font,
multiplayer: this.multiplayer === 2, multiplayer: this.player === 2,
blue: this.multiplayer === 2 blue: this.player === 2
}) })
this.draw.soul({ this.draw.soul({
ctx: ctx, ctx: ctx,
x: winW - 57, x: winW - 57,
y: this.multiplayer === 2 ? 378 : 165, y: this.player === 2 ? 378 : 165,
cleared: this.rules.clearReached(score.gauge) cleared: this.rules.clearReached(score.gauge)
}) })
@ -614,7 +680,7 @@
ctx.drawImage(assets.image["difficulty"], ctx.drawImage(assets.image["difficulty"],
0, 144 * this.difficulty[this.controller.selectedSong.difficulty], 0, 144 * this.difficulty[this.controller.selectedSong.difficulty],
168, 143, 168, 143,
16, this.multiplayer === 2 ? 194 : 232, 16, this.player === 2 ? 194 : 232,
141, 120 141, 120
) )
var diff = this.controller.selectedSong.difficulty var diff = this.controller.selectedSong.difficulty
@ -626,13 +692,13 @@
ctx.fillStyle = "#fff" ctx.fillStyle = "#fff"
ctx.lineWidth = 7 ctx.lineWidth = 7
ctx.miterLimit = 1 ctx.miterLimit = 1
ctx.strokeText(text, 87, this.multiplayer === 2 ? 310 : 348) ctx.strokeText(text, 87, this.player === 2 ? 310 : 348)
ctx.fillText(text, 87, this.multiplayer === 2 ? 310 : 348) ctx.fillText(text, 87, this.player === 2 ? 310 : 348)
ctx.miterLimit = 10 ctx.miterLimit = 10
} }
// Badges // Badges
if(this.controller.autoPlayEnabled && !this.controller.multiplayer){ if(this.controller.autoPlayEnabled && !this.multiplayer){
this.ctx.drawImage(assets.image["badge_auto"], this.ctx.drawImage(assets.image["badge_auto"],
125, 235, 34, 34 125, 235, 34, 34
) )
@ -641,7 +707,7 @@
// Score background // Score background
ctx.fillStyle = "#000" ctx.fillStyle = "#000"
ctx.beginPath() ctx.beginPath()
if(this.multiplayer === 2){ if(this.player === 2){
ctx.moveTo(0, 312) ctx.moveTo(0, 312)
this.draw.roundedCorner(ctx, 176, 312, 20, 1) this.draw.roundedCorner(ctx, 176, 312, 20, 1)
ctx.lineTo(176, 353) ctx.lineTo(176, 353)
@ -666,11 +732,11 @@
}, { }, {
// 560, 10 // 560, 10
x: animPos.x1 + animPos.w / 6, x: animPos.x1 + animPos.w / 6,
y: animPos.y1 - animPos.h * (this.multiplayer === 2 ? 2.5 : 3.5) y: animPos.y1 - animPos.h * (this.player === 2 ? 2.5 : 3.5)
}, { }, {
// 940, -150 // 940, -150
x: animPos.x2 - animPos.w / 3, x: animPos.x2 - animPos.w / 3,
y: animPos.y2 - animPos.h * (this.multiplayer === 2 ? 3.5 : 5) y: animPos.y2 - animPos.h * (this.player === 2 ? 3.5 : 5)
}, { }, {
// 1225, 165 // 1225, 165
x: animPos.x2, x: animPos.x2,
@ -1390,12 +1456,12 @@
var selectedSong = this.controller.selectedSong var selectedSong = this.controller.selectedSong
var songSkinName = selectedSong.songSkin.name var songSkinName = selectedSong.songSkin.name
var donLayers = [] var donLayers = []
var filename = !selectedSong.songSkin.don && this.multiplayer === 2 ? "bg_don2_" : "bg_don_" var filename = !selectedSong.songSkin.don && this.player === 2 ? "bg_don2_" : "bg_don_"
var prefix = "" var prefix = ""
this.donBg = document.createElement("div") this.donBg = document.createElement("div")
this.donBg.classList.add("donbg") this.donBg.classList.add("donbg")
if(this.multiplayer === 2){ if(this.player === 2){
this.donBg.classList.add("donbg-bottom") this.donBg.classList.add("donbg-bottom")
} }
for(var layer = 1; layer <= 3; layer++){ for(var layer = 1; layer <= 3; layer++){
@ -1525,17 +1591,21 @@
// Start animation to gauge // Start animation to gauge
circle.animate(ms) circle.animate(ms)
} }
if(ms - this.controller.audioLatency >= circle.ms && !circle.beatMSCopied && (!circle.branch || circle.branch.active)){
if(this.beatInterval !== circle.beatMS){
this.changeBeatInterval(circle.beatMS)
} }
circle.beatMSCopied = true 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)
} }
if(ms - this.controller.audioLatency >= circle.ms && !circle.gogoChecked && (!circle.branch || circle.branch.active)){ event.beatMSCopied = true
if(this.gogoTime != circle.gogoTime){
this.toggleGogoTime(circle)
} }
circle.gogoChecked = true if(ms - this.controller.audioLatency >= event.ms && !event.gogoChecked && (!event.branch || event.branch.active)){
if(this.gogoTime != event.gogoTime){
this.toggleGogoTime(event)
}
event.gogoChecked = true
} }
} }
} }

View File

@ -18,7 +18,7 @@ class ViewAssets{
sw: imgw, sw: imgw,
sh: imgh - 1, sh: imgh - 1,
x: view.portrait ? -60 : 0, x: view.portrait ? -60 : 0,
y: view.portrait ? (view.multiplayer === 2 ? 560 : 35) : (view.multiplayer === 2 ? 360 : 2), y: view.portrait ? (view.player === 2 ? 560 : 35) : (view.player === 2 ? 360 : 2),
w: w, w: w,
h: h - 1 h: h - 1
} }

View File

@ -2,7 +2,7 @@
<div class="view"> <div class="view">
<div class="view-title stroke-sub"></div> <div class="view-title stroke-sub"></div>
<div class="view-content"></div> <div class="view-content"></div>
<div id="diag-txt"></div> <div class="diag-txt"></div>
<div class="left-buttons"> <div class="left-buttons">
<div id="link-issues" class="taibtn stroke-sub link-btn"> <div id="link-issues" class="taibtn stroke-sub link-btn">
<a target="_blank"></a> <a target="_blank"></a>

View File

@ -0,0 +1,34 @@
<div class="view-outer">
<div class="view account-view">
<div class="view-title stroke-sub"></div>
<div class="view-content">
<div class="error-div"></div>
<div class="displayname-div">
<div class="displayname-hint"></div>
<input type="text" class="displayname" maxlength="25">
</div>
<form class="accountpass-form">
<div>
<div class="accountpass-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="accountpass-div">
<input type="password" name="password"><input type="password" name="newpassword" autocomplete="new-password"><input type="password" name="newpassword2" autocomplete="new-password">
</div>
</form>
<form class="accountdel-form">
<div>
<div class="accountdel-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="accountdel-div">
<input type="password" name="password">
</div>
</form>
</div>
<div id="diag-txt"></div>
<div class="left-buttons">
<div class="logout-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="save-btn taibtn stroke-sub selected"></div>
<div class="view-end-button taibtn stroke-sub"></div>
</div>
</div>

View File

@ -24,6 +24,12 @@
<div class="music-volume input-slider"> <div class="music-volume input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span> <span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div> </div>
<div class="lyrics-hide">
<div>Lyrics offset:</div>
<div class="lyrics-offset input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div>
</div>
<label class="change-restart-label"><input class="change-restart" type="checkbox">Restart on change</label> <label class="change-restart-label"><input class="change-restart" type="checkbox">Restart on change</label>
<label class="autoplay-label"><input class="autoplay" type="checkbox">Auto play</label> <label class="autoplay-label"><input class="autoplay" type="checkbox">Auto play</label>
<div class="bottom-btns"> <div class="bottom-btns">

View File

@ -8,6 +8,7 @@
<div id="touch-drum-img"></div> <div id="touch-drum-img"></div>
</div> </div>
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<div id="song-lyrics"></div>
<div id="touch-buttons"> <div id="touch-buttons">
<div id="touch-full-btn"></div><div id="touch-pause-btn"></div> <div id="touch-full-btn"></div><div id="touch-pause-btn"></div>
</div> </div>

View File

@ -2,3 +2,10 @@
<div class="progress"></div> <div class="progress"></div>
<span class="percentage">0%</span> <span class="percentage">0%</span>
</div> </div>
<div class="view-outer loader-error-div">
<div class="view">
<div class="view-content">An error occurred, please refresh</div>
<div class="diag-txt"></div>
<span class="debug-link">Debug</span>
</div>
</div>

View File

@ -0,0 +1,25 @@
<div class="view-outer">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content">
<div class="error-div"></div>
<form class="login-form">
<div class="username-hint"></div>
<input type="text" name="username" maxlength="20" required>
<div class="password-hint"></div>
<input type="password" name="password" required>
<div class="password2-div"></div>
<div class="remember-div">
<label class="remember-label">
<input type="checkbox" checked="checked" name="remember">
</label>
</div>
<div class="login-btn taibtn stroke-sub link-btn"></div>
</form>
</div>
<div class="left-buttons">
<div class="register-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="view-end-button taibtn stroke-sub selected"></div>
</div>
</div>

73
schema.py Normal file
View File

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

View File

@ -13,11 +13,11 @@ server_status = {
} }
consonants = "bcdfghjklmnpqrstvwxyz" consonants = "bcdfghjklmnpqrstvwxyz"
def msgobj(type, value=None): def msgobj(msg_type, value=None):
if value == None: if value == None:
return json.dumps({"type": type}) return json.dumps({"type": msg_type})
else: else:
return json.dumps({"type": type, "value": value}) return json.dumps({"type": msg_type, "value": value})
def status_event(): def status_event():
value = [] value = []
@ -42,7 +42,8 @@ async def connection(ws, path):
user = { user = {
"ws": ws, "ws": ws,
"action": "ready", "action": "ready",
"session": False "session": False,
"name": None
} }
server_status["users"].append(user) server_status["users"].append(user)
try: try:
@ -69,16 +70,17 @@ async def connection(ws, path):
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
data = {} data = {}
action = user["action"] action = user["action"]
type = data["type"] if "type" in data else None msg_type = data["type"] if "type" in data else None
value = data["value"] if "value" in data else None value = data["value"] if "value" in data else None
if action == "ready": if action == "ready":
# Not playing or waiting # Not playing or waiting
if type == "join": if msg_type == "join":
if value == None: if value == None:
continue continue
waiting = server_status["waiting"] waiting = server_status["waiting"]
id = value["id"] if "id" in value else None id = value["id"] if "id" in value else None
diff = value["diff"] if "diff" in value else None diff = value["diff"] if "diff" in value else None
user["name"] = value["name"] if "name" in value else None
if not id or not diff: if not id or not diff:
continue continue
if id not in waiting: if id not in waiting:
@ -92,6 +94,7 @@ async def connection(ws, path):
await ws.send(msgobj("waiting")) await ws.send(msgobj("waiting"))
else: else:
# Join the other user and start game # Join the other user and start game
user["name"] = value["name"] if "name" in value else None
user["other_user"] = waiting[id]["user"] user["other_user"] = waiting[id]["user"]
waiting_diff = waiting[id]["diff"] waiting_diff = waiting[id]["diff"]
del waiting[id] del waiting[id]
@ -99,9 +102,13 @@ async def connection(ws, path):
user["action"] = "loading" user["action"] = "loading"
user["other_user"]["action"] = "loading" user["other_user"]["action"] = "loading"
user["other_user"]["other_user"] = user user["other_user"]["other_user"] = user
user["other_user"]["player"] = 1
user["player"] = 2
await asyncio.wait([ await asyncio.wait([
ws.send(msgobj("gameload", waiting_diff)), ws.send(msgobj("gameload", {"diff": waiting_diff, "player": 2})),
user["other_user"]["ws"].send(msgobj("gameload", diff)) user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff, "player": 1})),
ws.send(msgobj("name", user["other_user"]["name"])),
user["other_user"]["ws"].send(msgobj("name", user["name"]))
]) ])
else: else:
# Wait for another user # Wait for another user
@ -115,28 +122,33 @@ async def connection(ws, path):
await ws.send(msgobj("waiting")) await ws.send(msgobj("waiting"))
# Update others on waiting players # Update others on waiting players
await notify_status() await notify_status()
elif type == "invite": elif msg_type == "invite":
if value == None: if value and "id" in value and value["id"] == None:
# Session invite link requested # Session invite link requested
invite = get_invite() invite = get_invite()
server_status["invites"][invite] = user server_status["invites"][invite] = user
user["action"] = "invite" user["action"] = "invite"
user["session"] = invite user["session"] = invite
user["name"] = value["name"] if "name" in value else None
await ws.send(msgobj("invite", invite)) await ws.send(msgobj("invite", invite))
elif value in server_status["invites"]: elif value and "id" in value and value["id"] in server_status["invites"]:
# Join a session with the other user # Join a session with the other user
user["other_user"] = server_status["invites"][value] user["name"] = value["name"] if "name" in value else None
del server_status["invites"][value] user["other_user"] = server_status["invites"][value["id"]]
del server_status["invites"][value["id"]]
if "ws" in user["other_user"]: if "ws" in user["other_user"]:
user["other_user"]["other_user"] = user user["other_user"]["other_user"] = user
user["action"] = "invite" user["action"] = "invite"
user["session"] = value user["session"] = value["id"]
sent_msg = msgobj("session") user["other_user"]["player"] = 1
user["player"] = 2
await asyncio.wait([ await asyncio.wait([
ws.send(sent_msg), ws.send(msgobj("session", {"player": 2})),
user["other_user"]["ws"].send(sent_msg) user["other_user"]["ws"].send(msgobj("session", {"player": 1})),
ws.send(msgobj("invite")),
ws.send(msgobj("name", user["other_user"]["name"])),
user["other_user"]["ws"].send(msgobj("name", user["name"]))
]) ])
await ws.send(msgobj("invite"))
else: else:
del user["other_user"] del user["other_user"]
await ws.send(msgobj("gameend")) await ws.send(msgobj("gameend"))
@ -145,7 +157,7 @@ async def connection(ws, path):
await ws.send(msgobj("gameend")) await ws.send(msgobj("gameend"))
elif action == "waiting" or action == "loading" or action == "loaded": elif action == "waiting" or action == "loading" or action == "loaded":
# Waiting for another user # Waiting for another user
if type == "leave": if msg_type == "leave":
# Stop waiting # Stop waiting
if user["session"]: if user["session"]:
if "other_user" in user and "ws" in user["other_user"]: if "other_user" in user and "ws" in user["other_user"]:
@ -170,7 +182,7 @@ async def connection(ws, path):
notify_status() notify_status()
]) ])
if action == "loading": if action == "loading":
if type == "gamestart": if msg_type == "gamestart":
user["action"] = "loaded" user["action"] = "loaded"
if user["other_user"]["action"] == "loaded": if user["other_user"]["action"] == "loaded":
user["action"] = "playing" user["action"] = "playing"
@ -183,12 +195,12 @@ async def connection(ws, path):
elif action == "playing": elif action == "playing":
# Playing with another user # Playing with another user
if "other_user" in user and "ws" in user["other_user"]: if "other_user" in user and "ws" in user["other_user"]:
if type == "note"\ if msg_type == "note"\
or type == "drumroll"\ or msg_type == "drumroll"\
or type == "branch"\ or msg_type == "branch"\
or type == "gameresults": or msg_type == "gameresults":
await user["other_user"]["ws"].send(msgobj(type, value)) await user["other_user"]["ws"].send(msgobj(msg_type, value))
elif type == "songsel" and user["session"]: elif msg_type == "songsel" and user["session"]:
user["action"] = "songsel" user["action"] = "songsel"
user["other_user"]["action"] = "songsel" user["other_user"]["action"] = "songsel"
sent_msg1 = msgobj("songsel") sent_msg1 = msgobj("songsel")
@ -199,7 +211,7 @@ async def connection(ws, path):
user["other_user"]["ws"].send(sent_msg1), user["other_user"]["ws"].send(sent_msg1),
user["other_user"]["ws"].send(sent_msg2) user["other_user"]["ws"].send(sent_msg2)
]) ])
elif type == "gameend": elif msg_type == "gameend":
# User wants to disconnect # User wants to disconnect
user["action"] = "ready" user["action"] = "ready"
user["other_user"]["action"] = "ready" user["other_user"]["action"] = "ready"
@ -222,7 +234,7 @@ async def connection(ws, path):
ws.send(status_event()) ws.send(status_event())
]) ])
elif action == "invite": elif action == "invite":
if type == "leave": if msg_type == "leave":
# Cancel session invite # Cancel session invite
if user["session"] in server_status["invites"]: if user["session"] in server_status["invites"]:
del server_status["invites"][user["session"]] del server_status["invites"][user["session"]]
@ -243,11 +255,11 @@ async def connection(ws, path):
ws.send(msgobj("left")), ws.send(msgobj("left")),
ws.send(status_event()) ws.send(status_event())
]) ])
elif type == "songsel" and "other_user" in user: elif msg_type == "songsel" and "other_user" in user:
if "ws" in user["other_user"]: if "ws" in user["other_user"]:
user["action"] = "songsel" user["action"] = "songsel"
user["other_user"]["action"] = "songsel" user["other_user"]["action"] = "songsel"
sent_msg = msgobj(type) sent_msg = msgobj(msg_type)
await asyncio.wait([ await asyncio.wait([
ws.send(sent_msg), ws.send(sent_msg),
user["other_user"]["ws"].send(sent_msg) user["other_user"]["ws"].send(sent_msg)
@ -262,15 +274,22 @@ async def connection(ws, path):
elif action == "songsel": elif action == "songsel":
# Session song selection # Session song selection
if "other_user" in user and "ws" in user["other_user"]: if "other_user" in user and "ws" in user["other_user"]:
if type == "songsel" or type == "catjump": if msg_type == "songsel" or msg_type == "catjump":
# Change song select position # Change song select position
if user["other_user"]["action"] == "songsel": if user["other_user"]["action"] == "songsel" and type(value) is dict:
sent_msg = msgobj(type, value) value["player"] = user["player"]
sent_msg = msgobj(msg_type, value)
await asyncio.wait([ await asyncio.wait([
ws.send(sent_msg), ws.send(sent_msg),
user["other_user"]["ws"].send(sent_msg) user["other_user"]["ws"].send(sent_msg)
]) ])
elif type == "join": elif msg_type == "crowns" or msg_type == "getcrowns":
if user["other_user"]["action"] == "songsel":
sent_msg = msgobj(msg_type, value)
await asyncio.wait([
user["other_user"]["ws"].send(sent_msg)
])
elif msg_type == "join":
# Start game # Start game
if value == None: if value == None:
continue continue
@ -282,8 +301,8 @@ async def connection(ws, path):
user["action"] = "loading" user["action"] = "loading"
user["other_user"]["action"] = "loading" user["other_user"]["action"] = "loading"
await asyncio.wait([ await asyncio.wait([
ws.send(msgobj("gameload", user["other_user"]["gamediff"])), ws.send(msgobj("gameload", {"diff": user["other_user"]["gamediff"]})),
user["other_user"]["ws"].send(msgobj("gameload", diff)) user["other_user"]["ws"].send(msgobj("gameload", {"diff": diff}))
]) ])
else: else:
user["action"] = "waiting" user["action"] = "waiting"
@ -292,7 +311,7 @@ async def connection(ws, path):
"id": id, "id": id,
"diff": diff "diff": diff
}])) }]))
elif type == "gameend": elif msg_type == "gameend":
# User wants to disconnect # User wants to disconnect
user["action"] = "ready" user["action"] = "ready"
user["session"] = False user["session"] = False

23
templates/admin.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Taiko Web Admin</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">
<link href="/src/css/admin.css" rel="stylesheet">
</head>
<body>
<header>
<div class="nav">
<a href="/admin/songs">Songs</a>
</div>
</header>
<main>
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
</body>
</html>

View File

@ -0,0 +1,134 @@
{% extends 'admin.html' %}
{% block content %}
<h1>{{ song.title }} <small>(ID: {{ song.id }})</small></h1>
{% for cat, message in get_flashed_messages(with_categories=true) %}
<div class="message{% if cat %} message-{{cat}}{% endif %}">{{ message }}</div>
{% endfor %}
<div class="song-form">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-field">
<span class="checkbox"><input type="checkbox" name="enabled" id="enabled"{% if song.enabled %} checked{% endif %}{% if admin.user_level < 100 %} disabled {% endif %}><label for="enabled"> Enabled</label></span>
</div>
<div class="form-field">
<p>Title</p>
<label for="title">Original</label>
<input type="text" id="title" value="{{song.title or ''}}" name="title" required>
<label for="title_ja">Japanese</label>
<input type="text" id="title_ja" value="{{song.title_lang.ja or ''}}" name="title_ja">
<label for="title_en">English</label>
<input type="text" id="title_en" value="{{song.title_lang.en or ''}}" name="title_en">
<label for="title_cn">Chinese (Simplified)</label>
<input type="text" id="title_cn" value="{{song.title_lang.cn or ''}}" name="title_cn">
<label for="title_tw">Chinese (Traditional)</label>
<input type="text" id="title_tw" value="{{song.title_lang.tw or ''}}" name="title_tw">
<label for="title_ko">Korean</label>
<input type="text" id="title_ko" value="{{song.title_lang.ko or ''}}" name="title_ko">
</div>
<div class="form-field">
<p>Subtitle</p>
<label for="subtitle">Original</label>
<input type="text" id="subtitle" value="{{song.subtitle or ''}}" name="subtitle">
<label for="subtitle_ja">Japanese</label>
<input type="text" id="subtitle_ja" value="{{song.subtitle_lang.ja or ''}}" name="subtitle_ja">
<label for="subtitle_en">English</label>
<input type="text" id="subtitle_en" value="{{song.subtitle_lang.en or ''}}" name="subtitle_en">
<label for="subtitle_cn">Chinese (Simplified)</label>
<input type="text" id="subtitle_cn" value="{{song.subtitle_lang.cn or ''}}" name="subtitle_cn">
<label for="subtitle_tw">Chinese (Traditional)</label>
<input type="text" id="subtitle_tw" value="{{song.subtitle_lang.tw or ''}}" name="subtitle_tw">
<label for="subtitle_ko">Korean</label>
<input type="text" id="subtitle_ko" value="{{song.subtitle_lang.ko or ''}}" name="subtitle_ko">
</div>
<div class="form-field">
<p>Courses</p>
<label for="course_easy">Easy</label>
<input type="number" id="course_easy" value="{{song.courses.easy.stars}}" name="course_easy" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_easy" id="branch_easy"{% if song.courses.easy.branch %} checked{% endif %}><label for="branch_easy"> Diverge Notes</label></span>
<label for="course_normal">Normal</label>
<input type="number" id="course_normal" value="{{song.courses.normal.stars}}" name="course_normal" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_normal" id="branch_normal"{% if song.courses.normal.branch %} checked{% endif %}><label for="branch_normal"> Diverge Notes</label></span>
<label for="course_hard">Hard</label>
<input type="number" id="course_hard" value="{{song.courses.hard.stars}}" name="course_hard" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_hard" id="branch_hard"{% if song.courses.hard.branch %} checked{% endif %}><label for="branch_hard"> Diverge Notes</label></span>
<label for="course_oni">Oni</label>
<input type="number" id="course_oni" value="{{song.courses.oni.stars}}" name="course_oni" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_oni" id="branch_oni"{% if song.courses.oni.branch %} checked{% endif %}><label for="branch_oni"> Diverge Notes</label></span>
<label for="course_ura">Ura</label>
<input type="number" id="course_ura" value="{{song.courses.ura.stars}}" name="course_ura" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_ura" id="branch_ura"{% if song.courses.ura.branch %} checked{% endif %}><label for="branch_ura"> Diverge Notes</label></span>
</div>
<div class="form-field">
<p><label for="category_id">Category</label></p>
<select name="category_id" id="category_id">
<option value="0">(none)</option>
{% for category in categories %}
<option value="{{ category.id }}"{% if song.category_id == category.id %} selected{% endif %}>{{ category.title }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="type">Type</label></p>
<select name="type" id="type">
<option value="tja"{% if song.type == 'tja' %} selected{% endif %}>TJA</option>
<option value="osu"{% if song.type == 'osu' %} selected{% endif %}>osu!taiko</option>
</select>
</div>
<div class="form-field">
<p><label for="offset">Offset</label></p>
<input type="text" id="offset" value="{{song.offset or '0'}}" name="offset" required>
</div>
<div class="form-field">
<p><label for="skin_id">Skin</label></p>
<select name="skin_id" id="skin_id">
<option value="0">(none)</option>
{% for skin in song_skins %}
<option value="{{ skin.id }}"{% if song.skin_id == skin.id %} selected{% endif %}>{{ skin.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="preview">Preview</label></p>
<input type="text" id="preview" value="{{song.preview or '0'}}" name="preview" required>
</div>
<div class="form-field">
<p><label for="volume">Volume</label></p>
<input type="text" id="volume" value="{{song.volume or '1.0'}}" name="volume" required>
</div>
<div class="form-field">
<p><label for="maker_id">Maker</label></p>
<select name="maker_id" id="maker_id">
<option value="0">(none)</option>
{% for maker in makers %}
<option value="{{ maker.id }}"{% if song.maker_id == maker.id %} selected{% endif %}>{{ maker.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="hash">Hash</label></p>
<input type="text" id="hash" value="{{song.hash}}" name="hash"> <span class="checkbox"><input type="checkbox" name="gen_hash" id="gen_hash"{><label for="gen_hash"> Generate</label></span>
</div>
<button type="submit" class="save-song">Save</button>
</form>
{% if admin.user_level >= 100 %}
<form class="delete-song" method="post" action="/admin/songs/{{song.id}}/delete" onsubmit="return confirm('Are you sure you wish to delete this song?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit">Delete song</button>
</form>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,122 @@
{% extends 'admin.html' %}
{% block content %}
<h1>New song</h1>
{% for message in get_flashed_messages() %}
<div class="message">{{ message }}</div>
{% endfor %}
<div class="song-form">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-field">
<span class="checkbox"><input type="checkbox" name="enabled" id="enabled"><label for="enabled"> Enabled</label></span>
</div>
<div class="form-field">
<p>Title</p>
<label for="title">Original</label>
<input type="text" id="title" value="" name="title" required>
<label for="title_ja">Japanese</label>
<input type="text" id="title_ja" value="" name="title_ja">
<label for="title_en">English</label>
<input type="text" id="title_en" value="" name="title_en">
<label for="title_cn">Chinese (Simplified)</label>
<input type="text" id="title_cn" value="" name="title_cn">
<label for="title_tw">Chinese (Traditional)</label>
<input type="text" id="title_tw" value="" name="title_tw">
<label for="title_ko">Korean</label>
<input type="text" id="title_ko" value="" name="title_ko">
</div>
<div class="form-field">
<p>Subtitle</p>
<label for="subtitle">Original</label>
<input type="text" id="subtitle" value="" name="subtitle">
<label for="subtitle_ja">Japanese</label>
<input type="text" id="subtitle_ja" value="" name="subtitle_ja">
<label for="subtitle_en">English</label>
<input type="text" id="subtitle_en" value="" name="subtitle_en">
<label for="subtitle_cn">Chinese (Simplified)</label>
<input type="text" id="subtitle_cn" value="" name="subtitle_cn">
<label for="subtitle_tw">Chinese (Traditional)</label>
<input type="text" id="subtitle_tw" value="" name="subtitle_tw">
<label for="subtitle_ko">Korean</label>
<input type="text" id="subtitle_ko" value="" name="subtitle_ko">
</div>
<div class="form-field">
<p>Courses</p>
<label for="course_easy">Easy</label>
<input type="number" id="course_easy" value="" name="course_easy" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_easy" id="branch_easy"><label for="branch_easy"> Diverge Notes</label></span>
<label for="course_normal">Normal</label>
<input type="number" id="course_normal" value="" name="course_normal" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_normal" id="branch_normal"><label for="branch_normal"> Diverge Notes</label></span>
<label for="course_hard">Hard</label>
<input type="number" id="course_hard" value="" name="course_hard" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_hard" id="branch_hard"><label for="branch_hard"> Diverge Notes</label></span>
<label for="course_oni">Oni</label>
<input type="number" id="course_oni" value="" name="course_oni" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_oni" id="branch_oni"><label for="branch_oni"> Diverge Notes</label></span>
<label for="course_ura">Ura</label>
<input type="number" id="course_ura" value="" name="course_ura" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_ura" id="branch_ura"><label for="branch_ura"> Diverge Notes</label></span>
</div>
<div class="form-field">
<p><label for="category_id">Category</label></p>
<select name="category_id" id="category_id">
<option value="0">(none)</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.title }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="type">Type</label></p>
<select name="type" id="type">
<option value="tja">TJA</option>
<option value="osu">osu!taiko</option>
</select>
</div>
<div class="form-field">
<p><label for="offset">Offset</label></p>
<input type="text" id="offset" value="" name="offset" required>
</div>
<div class="form-field">
<p><label for="skin_id">Skin</label></p>
<select name="skin_id" id="skin_id">
<option value="0">(none)</option>
{% for skin in song_skins %}
<option value="{{ skin.id }}">{{ skin.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="preview">Preview</label></p>
<input type="text" id="preview" value="" name="preview" required>
</div>
<div class="form-field">
<p><label for="volume">Volume</label></p>
<input type="text" id="volume" value="" name="volume" required>
</div>
<div class="form-field">
<p><label for="maker_id">Maker</label></p>
<select name="maker_id" id="maker_id">
<option value="0">(none)</option>
{% for maker in makers %}
<option value="{{ maker.id }}">{{ maker.name }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="save-song">Save</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends 'admin.html' %}
{% block content %}
{% if admin.user_level >= 100 %}
<a href="/admin/songs/new" class="side-button">New song</a>
{% endif %}
<h1>Songs</h1>
{% for message in get_flashed_messages() %}
<div class="message">{{ message }}</div>
{% endfor %}
{% for song in songs %}
<a href="/admin/songs/{{ song.id }}" class="song-link">
<div class="song">
{% if song.title_lang.en %}
<p>{{ song.title_lang.en }} <small>({{ song.title }})</small></p>
{% else %}
<p>{{ song.title }}</p>
{% endif %}
</div>
</a>
{% endfor %}
{% endblock %}

View File

@ -1,4 +1,4 @@
@echo off @echo off
( (
git log -1 --pretty="format:{\"commit\": \"%%H\", \"commit_short\": \"%%h\", \"version\": \"%%ad\", \"url\": \"https://github.com/bui/taiko-web/\"}" --date="format:%%y.%%m.%%d" git log -1 --pretty="format:{\"commit\": \"%%H\", \"commit_short\": \"%%h\", \"version\": \"%%ad\"}" --date="format:%%y.%%m.%%d"
) > ../version.json ) > ../version.json

View File

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

View File

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

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

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

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

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

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

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

114
tools/migrate_db.py Normal file
View File

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