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
.vscode
*.pyc
# Directories potentially created on remote AFP share
.AppleDB
@ -48,5 +49,5 @@ public/api
taiko.db
version.json
public/index.html
config.json
config.py
public/assets/song_skins

544
app.py
View File

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

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 .branch-hide{
#debug .branch-hide,
#debug .lyrics-hide{
display: none;
}

View File

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

View File

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

View File

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

View File

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

512
public/src/js/account.js Normal file
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",
"logo.js",
"settings.js",
"scorestorage.js"
"scorestorage.js",
"account.js",
"lyrics.js"
],
"css": [
"main.css",
@ -86,11 +88,7 @@ var assets = {
"settings_gamepad.png"
],
"audioSfx": [
"se_cancel.wav",
"se_don.wav",
"se_ka.wav",
"se_pause.wav",
"se_jump.wav",
"se_calibration.wav",
"v_results.wav",
@ -102,6 +100,10 @@ var assets = {
"audioSfxLR": [
"neiro_1_don.wav",
"neiro_1_ka.wav",
"se_cancel.wav",
"se_don.wav",
"se_ka.wav",
"se_jump.wav",
"se_balloon.wav",
"se_gameclear.wav",
@ -137,7 +139,9 @@ var assets = {
"about.html",
"debug.html",
"session.html",
"settings.html"
"settings.html",
"account.html",
"login.html"
],
"songs": [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

231
public/src/js/lyrics.js Normal file
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 settings
var scoreStorage
var account = {}
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
class SongSelect{
constructor(fromTutorial, fadeIn, touchEnabled, songId){
constructor(fromTutorial, fadeIn, touchEnabled, songId, showWarning){
this.touchEnabled = touchEnabled
loader.changePage("songselect", false)
@ -116,7 +116,7 @@ class SongSelect{
originalTitle: song.title,
subtitle: subtitle,
skin: song.category in this.songSkin ? this.songSkin[song.category] : this.songSkin.default,
stars: song.stars,
courses: song.courses,
category: song.category,
preview: song.preview || 0,
type: song.type,
@ -126,14 +126,20 @@ class SongSelect{
volume: song.volume,
maker: song.maker,
canJump: true,
hash: song.hash || song.title
hash: song.hash || song.title,
order: song.order,
lyrics: song.lyrics
})
}
this.songs.sort((a, b) => {
var catA = a.category in this.songSkin ? this.songSkin[a.category] : this.songSkin.default
var catB = b.category in this.songSkin ? this.songSkin[b.category] : this.songSkin.default
if(catA.sort === catB.sort){
return a.id > b.id ? 1 : -1
if(a.order === b.order){
return a.id > b.id ? 1 : -1
}else{
return a.order > b.order ? 1 : -1
}
}else{
return catA.sort > catB.sort ? 1 : -1
}
@ -162,6 +168,10 @@ class SongSelect{
category: strings.random
})
}
this.showWarning = showWarning
if(showWarning && showWarning.name === "scoreSaveFailed"){
scoreStorage.scoreSaveFailed = true
}
this.songs.push({
title: strings.aboutSimulator,
skin: this.songSkin.about,
@ -192,7 +202,7 @@ class SongSelect{
})
this.songAsset = {
marginTop: 90,
marginTop: 104,
marginLeft: 18,
width: 82,
selectedWidth: 382,
@ -226,6 +236,7 @@ class SongSelect{
this.difficultyCache = new CanvasCache(noSmoothing)
this.sessionCache = new CanvasCache(noSmoothing)
this.currentSongCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing)
this.difficulty = [strings.easy, strings.normal, strings.hard, strings.oni]
this.difficultyId = ["easy", "normal", "hard", "oni", "ura"]
@ -237,6 +248,7 @@ class SongSelect{
this.selectedSong = 0
this.selectedDiff = 0
this.lastCurrentSong = {}
assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506)
if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage) && !songId){
@ -267,7 +279,9 @@ class SongSelect{
}else if((!p2.session || fadeIn) && "selectedSong" in localStorage){
this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1)
}
this.playSound(songIdIndex !== -1 ? "v_diffsel" : "v_songsel")
if(!this.showWarning){
this.playSound(songIdIndex !== -1 ? "v_diffsel" : "v_songsel")
}
snd.musicGain.fadeOut()
this.playBgm(false)
}
@ -373,7 +387,13 @@ class SongSelect{
return
}
var shift = event ? event.shiftKey : this.pressedKeys["shift"]
if(this.state.screen === "song"){
if(this.state.showWarning){
if(name === "confirm"){
this.playSound("se_don")
this.state.showWarning = false
this.showWarning = false
}
}else if(this.state.screen === "song"){
if(name === "confirm"){
this.toSelectDifficulty()
}else if(name === "back"){
@ -447,10 +467,20 @@ class SongSelect{
var ctrl = false
var touch = true
}
if(this.state.screen === "song"){
if(this.state.showWarning){
if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){
this.playSound("se_don")
this.state.showWarning = false
this.showWarning = false
}
}else if(this.state.screen === "song"){
if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){
this.categoryJump(mouse.x < 640 ? -1 : 1)
}else if(mouse.x > 641 && mouse.y > 603){
}else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig.accounts){
this.toAccount()
}else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){
this.toSession()
}else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){
this.toSession()
}else{
var moveBy = this.songSelMouse(mouse.x, mouse.y)
@ -473,7 +503,7 @@ class SongSelect{
window.open(this.songs[this.selectedSong].maker.url)
}else if(moveBy === this.diffOptions.length + 4){
this.state.ura = !this.state.ura
this.playSound("se_ka")
this.playSound("se_ka", 0, p2.session ? p2.player : false)
if(this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura){
this.state.move = -1
}
@ -498,14 +528,22 @@ class SongSelect{
mouseMove(event){
var mouse = this.mouseOffset(event.offsetX, event.offsetY)
var moveTo = null
if(this.state.screen === "song"){
if(this.state.showWarning){
if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){
moveTo = "showWarning"
}
}else if(this.state.screen === "song"){
if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){
moveTo = mouse.x < 640 ? "categoryPrev" : "categoryNext"
}else if(mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){
}else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig.accounts){
moveTo = "account"
}else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){
moveTo = "session"
}else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){
moveTo = "session"
}else{
var moveTo = this.songSelMouse(mouse.x, mouse.y)
if(moveTo === null && this.state.moveHover === 0 && !this.songs[this.selectedSong].stars){
if(moveTo === null && this.state.moveHover === 0 && !this.songs[this.selectedSong].courses){
this.state.moveMS = this.getMS() - this.songSelecting.speed
}
}
@ -544,7 +582,7 @@ class SongSelect{
var dir = x > 0 ? 1 : -1
x = Math.abs(x)
var selectedWidth = this.songAsset.selectedWidth
if(!this.songs[this.selectedSong].stars){
if(!this.songs[this.selectedSong].courses){
selectedWidth = this.songAsset.width
}
var moveBy = Math.ceil((x - selectedWidth / 2 - this.songAsset.marginLeft / 2) / (this.songAsset.width + this.songAsset.marginLeft)) * dir
@ -565,7 +603,13 @@ class SongSelect{
}else if(550 < x && x < 1050 && 95 < y && y < 524){
var moveBy = Math.floor((x - 550) / ((1050 - 550) / 5)) + this.diffOptions.length
var currentSong = this.songs[this.selectedSong]
if(this.state.ura && moveBy === this.diffOptions.length + 3 || currentSong.stars[moveBy - this.diffOptions.length]){
if(
this.state.ura
&& moveBy === this.diffOptions.length + 3
|| currentSong.courses[
this.difficultyId[moveBy - this.diffOptions.length]
]
){
return moveBy
}
}
@ -583,7 +627,7 @@ class SongSelect{
})
}
}else if(this.state.locked !== 1 || fromP2){
if(this.songs[this.selectedSong].stars && (this.state.locked === 0 || fromP2)){
if(this.songs[this.selectedSong].courses && (this.state.locked === 0 || fromP2)){
this.state.moveMS = ms
}else{
this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize
@ -601,9 +645,10 @@ class SongSelect{
var scroll = resize2 - resize - scrollDelay * 2
var soundsDelay = Math.abs((scroll + resize) / moveBy)
this.lastMoveBy = fromP2 ? fromP2.player : false
for(var i = 0; i < Math.abs(moveBy) - 1; i++){
this.playSound("se_ka", (resize + i * soundsDelay) / 1000)
this.playSound("se_ka", (resize + i * soundsDelay) / 1000, fromP2 ? fromP2.player : false)
}
this.pointer(false)
}
@ -625,7 +670,7 @@ class SongSelect{
this.state.locked = 1
this.endPreview()
this.playSound("se_jump")
this.playSound("se_jump", 0, fromP2 ? fromP2.player : false)
}
}
@ -634,7 +679,7 @@ class SongSelect{
this.state.move = moveBy
this.state.moveMS = this.getMS() - 500
this.state.locked = 1
this.playSound("se_ka")
this.playSound("se_ka", 0, p2.session ? p2.player : false)
}
}
@ -645,7 +690,7 @@ class SongSelect{
toSelectDifficulty(fromP2){
var currentSong = this.songs[this.selectedSong]
if(p2.session && !fromP2 && currentSong.action !== "random"){
if(this.songs[this.selectedSong].stars){
if(this.songs[this.selectedSong].courses){
if(!this.state.selLock){
this.state.selLock = true
p2.send("songsel", {
@ -655,7 +700,7 @@ class SongSelect{
}
}
}else if(this.state.locked === 0 || fromP2){
if(currentSong.stars){
if(currentSong.courses){
this.state.screen = "difficulty"
this.state.screenMS = this.getMS()
this.state.locked = true
@ -665,22 +710,24 @@ class SongSelect{
this.selectedDiff = this.diffOptions.length + 3
}
this.playSound("se_don")
this.playSound("se_don", 0, fromP2 ? fromP2.player : false)
assets.sounds["v_songsel"].stop()
this.playSound("v_diffsel", 0.3)
if(!this.showWarning){
this.playSound("v_diffsel", 0.3)
}
pageEvents.send("song-select-difficulty", currentSong)
}else if(currentSong.action === "back"){
this.clean()
this.toTitleScreen()
}else if(currentSong.action === "random"){
this.playSound("se_don")
this.playSound("se_don", 0, fromP2 ? fromP2.player : false)
this.state.locked = true
do{
var i = Math.floor(Math.random() * this.songs.length)
}while(!this.songs[i].stars)
}while(!this.songs[i].courses)
var moveBy = i - this.selectedSong
setTimeout(() => {
this.moveToSong(moveBy)
this.moveToSong(moveBy, fromP2)
}, 200)
pageEvents.send("song-select-random")
}else if(currentSong.action === "tutorial"){
@ -710,7 +757,7 @@ class SongSelect{
this.state.moveHover = null
assets.sounds["v_diffsel"].stop()
this.playSound("se_cancel")
this.playSound("se_cancel", 0, fromP2 ? fromP2.player : false)
}
this.clearHash()
pageEvents.send("song-select-back")
@ -719,7 +766,7 @@ class SongSelect{
this.clean()
var selectedSong = this.songs[this.selectedSong]
assets.sounds["v_diffsel"].stop()
this.playSound("se_don")
this.playSound("se_don", 0, p2.session ? p2.player : false)
try{
if(assets.customSongs){
@ -744,23 +791,25 @@ class SongSelect{
}else if(p2.socket.readyState === 1 && !assets.customSongs){
multiplayer = ctrl
}
var diff = this.difficultyId[difficulty]
new LoadSong({
"title": selectedSong.title,
"originalTitle": selectedSong.originalTitle,
"folder": selectedSong.id,
"difficulty": this.difficultyId[difficulty],
"difficulty": diff,
"category": selectedSong.category,
"type": selectedSong.type,
"offset": selectedSong.offset,
"songSkin": selectedSong.songSkin,
"stars": selectedSong.stars[difficulty],
"hash": selectedSong.hash
"stars": selectedSong.courses[diff].stars,
"hash": selectedSong.hash,
"lyrics": selectedSong.lyrics
}, autoplay, multiplayer, touch)
}
toOptions(moveBy){
if(!p2.session){
this.playSound("se_ka")
this.playSound("se_ka", 0, p2.session ? p2.player : false)
this.selectedDiff = 1
do{
this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy)
@ -797,12 +846,21 @@ class SongSelect{
new SettingsView(this.touchEnabled)
}, 500)
}
toAccount(){
this.playSound("se_don")
this.clean()
setTimeout(() => {
new Account(this.touchEnabled)
}, 500)
}
toSession(){
if(p2.socket.readyState !== 1 || assets.customSongs){
return
}
if(p2.session){
this.playSound("se_don")
p2.send("gameend")
this.state.moveHover = null
}else{
localStorage["selectedSong"] = this.selectedSong
@ -893,6 +951,8 @@ class SongSelect{
var textW = strings.id === "en" ? 350 : 280
this.selectTextCache.resize((textW + 53 + 60 + 1) * 2, this.songAsset.marginTop + 15, ratio + 0.5)
this.nameplateCache.resize(274, 134, ratio + 0.2)
var categories = 0
var lastCategory
this.songs.forEach(song => {
@ -921,7 +981,7 @@ class SongSelect{
fontFamily: this.font,
x: w / 2,
y: 38 / 2,
width: w - 30,
width: id === "sessionend" ? 385 : w - 30,
align: "center",
baseline: "middle"
}, [
@ -962,241 +1022,26 @@ class SongSelect{
}else{
this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize + (ms - this.state.screenMS - 1000)
}
if(ms > this.state.screenMS + 500){
if(screen === "titleFadeIn" && ms > this.state.screenMS + 500){
this.state.screen = "title"
screen = "title"
}
}
if(screen === "song"){
if(this.songs[this.selectedSong].stars){
selectedWidth = this.songAsset.selectedWidth
if((screen === "song" || screen === "difficulty") && (this.showWarning && !this.showWarning.shown || scoreStorage.scoreSaveFailed)){
if(!this.showWarning){
this.showWarning = {name: "scoreSaveFailed"}
}
var lastMoveMul = Math.pow(Math.abs(this.state.lastMove), 1 / 4)
var changeSpeed = this.songSelecting.speed * lastMoveMul
var resize = changeSpeed * this.songSelecting.resize / lastMoveMul
var scrollDelay = changeSpeed * this.songSelecting.scrollDelay
var resize2 = changeSpeed - resize
var scroll = resize2 - resize - scrollDelay * 2
var elapsed = ms - this.state.moveMS
if(this.state.catJump || (this.state.move && ms > this.state.moveMS + resize2 - scrollDelay)){
var isJump = this.state.catJump
var previousSelectedSong = this.selectedSong
if(!isJump){
this.playSound("se_ka")
this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move)
}else{
var currentCat = this.songs[this.selectedSong].category
var currentIdx = this.mod(this.songs.length, this.selectedSong)
if(this.state.move > 0){
var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) > currentIdx && song.category !== currentCat && song.canJump)
if(!nextSong){
nextSong = this.songs[0]
}
}else{
var isFirstInCat = this.songs.findIndex(song => song.category === currentCat) == this.selectedSong
if(!isFirstInCat){
var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) < currentIdx && song.category === currentCat && song.canJump)
}else{
var idx = this.songs.length - 1
var nextSong
var lastCat
for(;idx>=0;idx--){
if(this.songs[idx].category !== lastCat && this.songs[idx].action !== "back"){
lastCat = this.songs[idx].category
if(nextSong){
break
}
}
if(lastCat !== currentCat && idx < currentIdx){
nextSong = idx
}
}
nextSong = this.songs[nextSong]
}
if(!nextSong){
var rev = [...this.songs].reverse()
nextSong = rev.find(song => song.canJump)
}
}
this.selectedSong = this.songs.indexOf(nextSong)
this.state.catJump = false
}
if(previousSelectedSong !== this.selectedSong){
pageEvents.send("song-select-move", this.songs[this.selectedSong])
}
this.state.move = 0
this.state.locked = 2
if(assets.customSongs){
assets.customSelected = this.selectedSong
}else if(!p2.session){
try{
localStorage["selectedSong"] = this.selectedSong
}catch(e){}
}
if(this.songs[this.selectedSong].action !== "back"){
var cat = this.songs[this.selectedSong].category
var sort = cat in this.songSkin ? this.songSkin[cat].sort : 7
this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_" + sort].src + "')"
}
if(this.bgmEnabled){
this.playBgm(false)
}
if(this.state.moveMS && ms < this.state.moveMS + changeSpeed){
xOffset = Math.min(scroll, Math.max(0, elapsed - resize - scrollDelay)) / scroll * (this.songAsset.width + this.songAsset.marginLeft)
xOffset *= -this.state.move
if(elapsed < resize){
selectedWidth = this.songAsset.width + (((resize - elapsed) / resize) * (selectedWidth - this.songAsset.width))
}else if(elapsed > resize2){
this.playBgm(!this.songs[this.selectedSong].stars)
this.state.locked = 1
selectedWidth = this.songAsset.width + ((elapsed - resize2) / resize * (selectedWidth - this.songAsset.width))
}else{
songSelMoving = true
selectedWidth = this.songAsset.width
}
}else{
this.playBgm(!this.songs[this.selectedSong].stars)
this.state.locked = 0
if(this.showWarning.name === "scoreSaveFailed"){
scoreStorage.scoreSaveFailed = false
}
}else if(screen === "difficulty"){
var currentSong = this.songs[this.selectedSong]
if(this.state.locked){
this.state.locked = 0
}
if(this.state.move){
var hasUra = currentSong.stars[4]
var previousSelection = this.selectedDiff
do{
if(hasUra && this.state.move > 0){
this.selectedDiff += this.state.move
if(this.selectedDiff > this.diffOptions.length + 4){
this.state.ura = !this.state.ura
if(this.state.ura){
this.selectedDiff = previousSelection === this.diffOptions.length + 3 ? this.diffOptions.length + 4 : previousSelection
break
}else{
this.state.move = -1
}
}
}else{
this.selectedDiff = this.mod(this.diffOptions.length + 5, this.selectedDiff + this.state.move)
}
}while(
this.selectedDiff >= this.diffOptions.length && !currentSong.stars[this.selectedDiff - this.diffOptions.length]
|| this.selectedDiff === this.diffOptions.length + 3 && this.state.ura
|| this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura
)
this.state.move = 0
}else if(this.selectedDiff < 0 || this.selectedDiff >= this.diffOptions.length && !currentSong.stars[this.selectedDiff - this.diffOptions.length]){
this.selectedDiff = 0
}
}
if(songSelMoving){
if(this.previewing !== null){
this.endPreview()
}
}else if(screen !== "title" && screen !== "titleFadeIn" && ms > this.state.moveMS + 100){
if(this.previewing !== this.selectedSong && "id" in this.songs[this.selectedSong]){
this.startPreview()
}
}
this.songFrameCache = {
w: this.songAsset.width + this.songAsset.selectedWidth + this.songAsset.fullWidth + (15 + 1) * 3,
h: this.songAsset.fullHeight + 16,
ratio: ratio
}
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
for(var i = this.selectedSong - 1; ; i--){
var highlight = 0
if(i - this.selectedSong === this.state.moveHover){
highlight = 1
}
var index = this.mod(this.songs.length, i)
var _x = winW / 2 - (this.selectedSong - i) * (this.songAsset.width + this.songAsset.marginLeft) - selectedWidth / 2 + xOffset
if(_x + this.songAsset.width + this.songAsset.marginLeft < 0){
break
}
this.drawClosedSong({
ctx: ctx,
x: _x,
y: songTop,
song: this.songs[index],
highlight: highlight,
disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random"
})
}
var startFrom
for(var i = this.selectedSong + 1; ; i++){
var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset
if(_x > winW){
startFrom = i - 1
break
}
}
for(var i = startFrom; i > this.selectedSong ; i--){
var highlight = 0
if(i - this.selectedSong === this.state.moveHover){
highlight = 1
}
var index = this.mod(this.songs.length, i)
var currentSong = this.songs[index]
var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset
this.drawClosedSong({
ctx: ctx,
x: _x,
y: songTop,
song: this.songs[index],
highlight: highlight,
disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random"
})
}
}
var currentSong = this.songs[this.selectedSong]
var highlight = 0
if(!currentSong.stars){
highlight = 2
}
if(this.state.moveHover === 0){
highlight = 1
}
var selectedSkin = this.songSkin.selected
if(screen === "title" || screen === "titleFadeIn" || this.state.locked === 3){
selectedSkin = currentSong.skin
highlight = 2
}else if(songSelMoving){
selectedSkin = currentSong.skin
highlight = 0
}
var selectedHeight = this.songAsset.height
if(screen === "difficulty"){
selectedWidth = this.songAsset.fullWidth
selectedHeight = this.songAsset.fullHeight
highlight = 0
}
if(this.currentSongTitle !== currentSong.title){
this.currentSongTitle = currentSong.title
this.currentSongCache.clear()
}
if(ms > this.state.screenMS + 2000 && selectedWidth === this.songAsset.width){
this.drawSongCrown({
ctx: ctx,
song: currentSong,
x: winW / 2 - selectedWidth / 2 + xOffset,
y: songTop + this.songAsset.height - selectedHeight
})
this.showWarning.shown = true
this.state.showWarning = true
this.state.locked = true
this.playSound("se_pause")
}
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
@ -1279,7 +1124,230 @@ class SongSelect{
})
}
if(ms <= this.state.screenMS + 2000 && selectedWidth === this.songAsset.width){
if(screen === "song"){
if(this.songs[this.selectedSong].courses){
selectedWidth = this.songAsset.selectedWidth
}
var lastMoveMul = Math.pow(Math.abs(this.state.lastMove), 1 / 4)
var changeSpeed = this.songSelecting.speed * lastMoveMul
var resize = changeSpeed * this.songSelecting.resize / lastMoveMul
var scrollDelay = changeSpeed * this.songSelecting.scrollDelay
var resize2 = changeSpeed - resize
var scroll = resize2 - resize - scrollDelay * 2
var elapsed = ms - this.state.moveMS
if(this.state.catJump || (this.state.move && ms > this.state.moveMS + resize2 - scrollDelay)){
var isJump = this.state.catJump
var previousSelectedSong = this.selectedSong
if(!isJump){
this.playSound("se_ka", 0, this.lastMoveBy)
this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move)
}else{
var currentCat = this.songs[this.selectedSong].category
var currentIdx = this.mod(this.songs.length, this.selectedSong)
if(this.state.move > 0){
var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) > currentIdx && song.category !== currentCat && song.canJump)
if(!nextSong){
nextSong = this.songs[0]
}
}else{
var isFirstInCat = this.songs.findIndex(song => song.category === currentCat) == this.selectedSong
if(!isFirstInCat){
var nextSong = this.songs.find(song => this.mod(this.songs.length, this.songs.indexOf(song)) < currentIdx && song.category === currentCat && song.canJump)
}else{
var idx = this.songs.length - 1
var nextSong
var lastCat
for(;idx>=0;idx--){
if(this.songs[idx].category !== lastCat && this.songs[idx].action !== "back"){
lastCat = this.songs[idx].category
if(nextSong){
break
}
}
if(lastCat !== currentCat && idx < currentIdx){
nextSong = idx
}
}
nextSong = this.songs[nextSong]
}
if(!nextSong){
var rev = [...this.songs].reverse()
nextSong = rev.find(song => song.canJump)
}
}
this.selectedSong = this.songs.indexOf(nextSong)
this.state.catJump = false
}
if(previousSelectedSong !== this.selectedSong){
pageEvents.send("song-select-move", this.songs[this.selectedSong])
}
this.state.move = 0
this.state.locked = 2
if(assets.customSongs){
assets.customSelected = this.selectedSong
}else if(!p2.session){
try{
localStorage["selectedSong"] = this.selectedSong
}catch(e){}
}
if(this.songs[this.selectedSong].action !== "back"){
var cat = this.songs[this.selectedSong].category
var sort = cat in this.songSkin ? this.songSkin[cat].sort : 7
this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_" + sort].src + "')"
}
}
if(this.state.moveMS && ms < this.state.moveMS + changeSpeed){
xOffset = Math.min(scroll, Math.max(0, elapsed - resize - scrollDelay)) / scroll * (this.songAsset.width + this.songAsset.marginLeft)
xOffset *= -this.state.move
if(elapsed < resize){
selectedWidth = this.songAsset.width + (((resize - elapsed) / resize) * (selectedWidth - this.songAsset.width))
}else if(elapsed > resize2){
this.playBgm(!this.songs[this.selectedSong].courses)
this.state.locked = 1
selectedWidth = this.songAsset.width + ((elapsed - resize2) / resize * (selectedWidth - this.songAsset.width))
}else{
songSelMoving = true
selectedWidth = this.songAsset.width
}
}else{
this.playBgm(!this.songs[this.selectedSong].courses)
this.state.locked = 0
}
}else if(screen === "difficulty"){
var currentSong = this.songs[this.selectedSong]
if(this.state.locked){
this.state.locked = 0
}
if(this.state.move){
var hasUra = currentSong.courses.ura
var previousSelection = this.selectedDiff
do{
if(hasUra && this.state.move > 0){
this.selectedDiff += this.state.move
if(this.selectedDiff > this.diffOptions.length + 4){
this.state.ura = !this.state.ura
if(this.state.ura){
this.selectedDiff = previousSelection === this.diffOptions.length + 3 ? this.diffOptions.length + 4 : previousSelection
break
}else{
this.state.move = -1
}
}
}else{
this.selectedDiff = this.mod(this.diffOptions.length + 5, this.selectedDiff + this.state.move)
}
}while(
this.selectedDiff >= this.diffOptions.length && !currentSong.courses[this.difficultyId[this.selectedDiff - this.diffOptions.length]]
|| this.selectedDiff === this.diffOptions.length + 3 && this.state.ura
|| this.selectedDiff === this.diffOptions.length + 4 && !this.state.ura
)
this.state.move = 0
}else if(this.selectedDiff < 0 || this.selectedDiff >= this.diffOptions.length && !currentSong.courses[this.difficultyId[this.selectedDiff - this.diffOptions.length]]){
this.selectedDiff = 0
}
}
if(songSelMoving){
if(this.previewing !== null){
this.endPreview()
}
}else if(screen !== "title" && screen !== "titleFadeIn" && ms > this.state.moveMS + 100){
if(this.previewing !== this.selectedSong && "id" in this.songs[this.selectedSong]){
this.startPreview()
}
}
this.songFrameCache = {
w: this.songAsset.width + this.songAsset.selectedWidth + this.songAsset.fullWidth + (15 + 1) * 3,
h: this.songAsset.fullHeight + 16,
ratio: ratio
}
if(screen === "title" || screen === "titleFadeIn" || screen === "song"){
for(var i = this.selectedSong - 1; ; i--){
var highlight = 0
if(i - this.selectedSong === this.state.moveHover){
highlight = 1
}
var index = this.mod(this.songs.length, i)
var _x = winW / 2 - (this.selectedSong - i) * (this.songAsset.width + this.songAsset.marginLeft) - selectedWidth / 2 + xOffset
if(_x + this.songAsset.width + this.songAsset.marginLeft < 0){
break
}
this.drawClosedSong({
ctx: ctx,
x: _x,
y: songTop,
song: this.songs[index],
highlight: highlight,
disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random"
})
}
var startFrom
for(var i = this.selectedSong + 1; ; i++){
var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset
if(_x > winW){
startFrom = i - 1
break
}
}
for(var i = startFrom; i > this.selectedSong ; i--){
var highlight = 0
if(i - this.selectedSong === this.state.moveHover){
highlight = 1
}
var index = this.mod(this.songs.length, i)
var currentSong = this.songs[index]
var _x = winW / 2 + (i - this.selectedSong - 1) * (this.songAsset.width + this.songAsset.marginLeft) + this.songAsset.marginLeft + selectedWidth / 2 + xOffset
this.drawClosedSong({
ctx: ctx,
x: _x,
y: songTop,
song: this.songs[index],
highlight: highlight,
disabled: p2.session && this.songs[index].action && this.songs[index].action !== "random"
})
}
}
var currentSong = this.songs[this.selectedSong]
var highlight = 0
if(!currentSong.courses){
highlight = 2
}
if(this.state.moveHover === 0){
highlight = 1
}
var selectedSkin = this.songSkin.selected
if(screen === "title" || screen === "titleFadeIn" || this.state.locked === 3){
selectedSkin = currentSong.skin
highlight = 2
}else if(songSelMoving){
selectedSkin = currentSong.skin
highlight = 0
}
var selectedHeight = this.songAsset.height
if(screen === "difficulty"){
selectedWidth = this.songAsset.fullWidth
selectedHeight = this.songAsset.fullHeight
highlight = 0
}
if(this.lastCurrentSong.title !== currentSong.title || this.lastCurrentSong.subtitle !== currentSong.subtitle){
this.lastCurrentSong.title = currentSong.title
this.lastCurrentSong.subtitle = currentSong.subtitle
this.currentSongCache.clear()
}
if(selectedWidth === this.songAsset.width){
this.drawSongCrown({
ctx: ctx,
song: currentSong,
@ -1313,8 +1381,8 @@ class SongSelect{
var textW = strings.id === "en" ? 350 : 280
this.selectTextCache.get({
ctx: ctx,
x: x - 144 - 53,
y: y - 24 - 30,
x: frameLeft,
y: frameTop,
w: textW + 53 + 60,
h: this.songAsset.marginTop + 15,
id: "difficulty"
@ -1411,28 +1479,42 @@ class SongSelect{
ctx: ctx,
font: this.font,
x: _x,
y: _y - 45
y: _y - 45,
two: p2.session && p2.player === 2
})
}
}
}
}
var drawDifficulty = (ctx, i, currentUra) => {
if(currentSong.stars[i] || currentUra){
var score = scoreStorage.get(currentSong.hash, false, true)
if(currentSong.courses[this.difficultyId[i]] || currentUra){
var crownDiff = currentUra ? "ura" : this.difficultyId[i]
var crownType = ""
if(score && score[crownDiff]){
crownType = score[crownDiff].crown
var players = p2.session ? 2 : 1
var score = [scoreStorage.get(currentSong.hash, false, true)]
if(p2.session){
score[p2.player === 1 ? "push" : "unshift"](scoreStorage.getP2(currentSong.hash, false, true))
}
var reversed = false
for(var a = players; a--;){
var crownType = ""
var p = reversed ? -(a - 1) : a
if(score[p] && score[p][crownDiff]){
crownType = score[p][crownDiff].crown
}
if(!reversed && players === 2 && p === 1 && crownType){
reversed = true
a++
}else{
this.draw.crown({
ctx: ctx,
type: crownType,
x: (songSel ? x + 33 + i * 60 : x + 402 + i * 100) + (players === 2 ? p === 0 ? -13 : 13 : 0),
y: songSel ? y + 75 : y + 30,
scale: 0.25,
ratio: this.ratio / this.pixelRatio
})
}
}
this.draw.crown({
ctx: ctx,
type: crownType,
x: songSel ? x + 33 + i * 60 : x + 402 + i * 100,
y: songSel ? y + 75 : y + 30,
scale: 0.25,
ratio: this.ratio / this.pixelRatio
})
if(songSel){
var _x = x + 33 + i * 60
var _y = y + 120
@ -1502,9 +1584,9 @@ class SongSelect{
outlineSize: currentUra ? this.songAsset.letterBorder : 0
})
})
var songStarsArray = (currentUra ? currentSong.stars[4] : currentSong.stars[i]).toString().split(" ")
var songStars = songStarsArray[0]
var songBranch = songStarsArray[1] === "B"
var songStarsObj = (currentUra ? currentSong.courses.ura : currentSong.courses[this.difficultyId[i]])
var songStars = songStarsObj.stars
var songBranch = songStarsObj.branch
var elapsedMS = this.state.screenMS > this.state.moveMS || !songSel ? this.state.screenMS : this.state.moveMS
var fade = ((ms - elapsedMS) % 2000) / 2000
if(songBranch && fade > 0.25 && fade < 0.75){
@ -1549,15 +1631,15 @@ class SongSelect{
if(this.selectedDiff === 4 + this.diffOptions.length){
currentDiff = 3
}
if(i === currentSong.p2Cursor && p2.socket.readyState === 1){
if(songSel && i === currentSong.p2Cursor && p2.socket.readyState === 1){
this.draw.diffCursor({
ctx: ctx,
font: this.font,
x: _x,
y: _y - (songSel ? 45 : 65),
two: true,
side: songSel ? false : (currentSong.p2Cursor === currentDiff),
scale: songSel ? 0.7 : 1
y: _y - 45,
two: !p2.session || p2.player === 1,
side: false,
scale: 0.7
})
}
if(!songSel){
@ -1573,7 +1655,8 @@ class SongSelect{
font: this.font,
x: _x,
y: _y - 65,
side: currentSong.p2Cursor === currentDiff && p2.socket.readyState === 1
side: currentSong.p2Cursor === currentDiff && p2.socket.readyState === 1,
two: p2.session && p2.player === 2
})
}
if(highlight){
@ -1591,8 +1674,8 @@ class SongSelect{
}
}
}
for(var i = 0; currentSong.stars && i < 4; i++){
var currentUra = i === 3 && (this.state.ura && !songSel || currentSong.stars[4] && songSel)
for(var i = 0; currentSong.courses && i < 4; i++){
var currentUra = i === 3 && (this.state.ura && !songSel || currentSong.courses.ura && songSel)
if(songSel && currentUra){
drawDifficulty(ctx, i, false)
var elapsedMS = this.state.screenMS > this.state.moveMS ? this.state.screenMS : this.state.moveMS
@ -1614,6 +1697,22 @@ class SongSelect{
drawDifficulty(ctx, i, currentUra)
}
}
for(var i = 0; currentSong.courses && i < 4; i++){
if(!songSel && i === currentSong.p2Cursor && p2.socket.readyState === 1){
var _x = x + 402 + i * 100
var _y = y + 87
var currentDiff = this.selectedDiff - this.diffOptions.length
this.draw.diffCursor({
ctx: ctx,
font: this.font,
x: _x,
y: _y - 65,
two: !p2.session || p2.player === 1,
side: currentSong.p2Cursor === currentDiff,
scale: 1
})
}
}
var borders = (this.songAsset.border + this.songAsset.innerBorder) * 2
var textW = this.songAsset.width - borders
@ -1647,45 +1746,63 @@ class SongSelect{
})
}
if(currentSong.maker || currentSong.maker === 0){
var hasMaker = currentSong.maker || currentSong.maker === 0
if(hasMaker || currentSong.lyrics){
if (songSel) {
var _x = x + 38
var _y = y + 10
ctx.strokeStyle = "#000"
ctx.lineWidth = 5
var grd = ctx.createLinearGradient(_x, _y, _x, _y+50);
grd.addColorStop(0, '#fa251a');
grd.addColorStop(1, '#ffdc33');
ctx.fillStyle = grd;
if(hasMaker){
var grd = ctx.createLinearGradient(_x, _y, _x, _y + 50)
grd.addColorStop(0, "#fa251a")
grd.addColorStop(1, "#ffdc33")
ctx.fillStyle = grd
}else{
ctx.fillStyle = "#000"
}
this.draw.roundedRect({
ctx: ctx,
x: _x - 28,
y: _y,
w: 130,
w: 192,
h: 50,
radius: 24
})
ctx.fill()
ctx.stroke()
ctx.beginPath()
ctx.arc(_x, _y + 28, 20, 0, Math.PI * 2)
ctx.fill()
this.draw.layeredText({
ctx: ctx,
text: strings.creative.creative,
fontSize: strings.id == "en" ? 30 : 34,
fontFamily: this.font,
align: "center",
baseline: "middle",
x: _x + 38,
y: _y + (["ja", "en"].indexOf(strings.id) >= 0 ? 25 : 28),
width: 110
}, [
{outline: "#fff", letterBorder: 8},
{fill: "#000"}
])
if(hasMaker){
this.draw.layeredText({
ctx: ctx,
text: strings.creative.creative,
fontSize: strings.id === "en" ? 28 : 34,
fontFamily: this.font,
align: "center",
baseline: "middle",
x: _x + 68,
y: _y + (strings.id === "ja" || strings.id === "en" ? 25 : 28),
width: 172
}, [
{outline: "#fff", letterBorder: 6},
{fill: "#000"}
])
}else{
this.draw.layeredText({
ctx: ctx,
text: strings.withLyrics,
fontSize: strings.id === "en" ? 28 : 34,
fontFamily: this.font,
align: "center",
baseline: "middle",
x: _x + 68,
y: _y + (strings.id === "ja" || strings.id === "en" ? 25 : 28),
width: 172
}, [
{fill: currentSong.skin.border[0]}
])
}
} else if(currentSong.maker && currentSong.maker.id > 0 && currentSong.maker.name){
var _x = x + 62
var _y = y + 380
@ -1753,7 +1870,7 @@ class SongSelect{
}
}
if(!songSel && currentSong.stars[4]){
if(!songSel && currentSong.courses.ura){
var fade = ((ms - this.state.screenMS) % 1200) / 1200
var _x = x + 402 + 4 * 100 + fade * 25
var _y = y + 258
@ -1842,7 +1959,7 @@ class SongSelect{
ctx.fillRect(0, frameTop + 595, 1280 + frameLeft * 2, 125 + frameTop)
var x = 0
var y = frameTop + 603
var w = frameLeft + 638
var w = p2.session ? frameLeft + 638 - 200 : frameLeft + 638
var h = 117 + frameTop
this.draw.pattern({
ctx: ctx,
@ -1869,7 +1986,88 @@ class SongSelect{
ctx.lineTo(x + w - 4, y + h)
ctx.lineTo(x + w - 4, y + 4)
ctx.fill()
x = frameLeft + 642
if(!p2.session || p2.player === 1){
var name = account.loggedIn ? account.displayName : strings.defaultName
var rank = account.loggedIn || !gameConfig.accounts || p2.session ? false : strings.notLoggedIn
}else{
var name = p2.name || strings.defaultName
var rank = false
}
this.nameplateCache.get({
ctx: ctx,
x: frameLeft + 60,
y: frameTop + 640,
w: 273,
h: 66,
id: "1p" + name + "\n" + rank,
}, ctx => {
this.draw.nameplate({
ctx: ctx,
x: 3,
y: 3,
name: name,
rank: rank,
font: this.font
})
})
if(this.state.moveHover === "account"){
this.draw.highlight({
ctx: ctx,
x: frameLeft + 59.5,
y: frameTop + 639.5,
w: 271,
h: 64,
radius: 28.5,
opacity: 0.8,
size: 10
})
}
if(p2.session){
x = x + w + 4
w = 396
this.draw.pattern({
ctx: ctx,
img: assets.image["bg_settings"],
x: x,
y: y,
w: w,
h: h,
dx: frameLeft + 11,
dy: frameTop + 45,
scale: 3.1
})
ctx.fillStyle = "rgba(255, 255, 255, 0.5)"
ctx.beginPath()
ctx.moveTo(x, y + h)
ctx.lineTo(x, y)
ctx.lineTo(x + w, y)
ctx.lineTo(x + w, y + 4)
ctx.lineTo(x + 4, y + 4)
ctx.lineTo(x + 4, y + h)
ctx.fill()
ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
ctx.beginPath()
ctx.moveTo(x + w, y)
ctx.lineTo(x + w, y + h)
ctx.lineTo(x + w - 4, y + h)
ctx.lineTo(x + w - 4, y + 4)
ctx.fill()
if(this.state.moveHover === "session"){
this.draw.highlight({
ctx: ctx,
x: x,
y: y,
w: w,
h: h,
opacity: 0.8
})
}
}
x = p2.session ? frameLeft + 642 + 200 : frameLeft + 642
w = p2.session ? frameLeft + 638 - 200 : frameLeft + 638
if(p2.session){
this.draw.pattern({
ctx: ctx,
@ -1925,7 +2123,7 @@ class SongSelect{
}
this.sessionCache.get({
ctx: ctx,
x: winW / 2,
x: p2.session ? winW / 4 : winW / 2,
y: y + (h - 32) / 2,
w: winW / 2,
h: 38,
@ -1933,7 +2131,7 @@ class SongSelect{
})
ctx.globalAlpha = 1
}
if(this.state.moveHover === "session"){
if(!p2.session && this.state.moveHover === "session"){
this.draw.highlight({
ctx: ctx,
x: x,
@ -1944,6 +2142,146 @@ class SongSelect{
})
}
}
if(p2.session){
if(p2.player === 1){
var name = p2.name || strings.default2PName
}else{
var name = account.loggedIn ? account.displayName : strings.default2PName
}
this.nameplateCache.get({
ctx: ctx,
x: frameLeft + 949,
y: frameTop + 640,
w: 273,
h: 66,
id: "2p" + name,
}, ctx => {
this.draw.nameplate({
ctx: ctx,
x: 3,
y: 3,
name: name,
font: this.font,
blue: true
})
})
}
if(this.state.showWarning){
if(this.preview){
this.endPreview()
}
ctx.fillStyle = "rgba(0, 0, 0, 0.5)"
ctx.fillRect(0, 0, winW, winH)
ctx.save()
ctx.translate(frameLeft, frameTop)
var pauseRect = (ctx, mul) => {
this.draw.roundedRect({
ctx: ctx,
x: 269 * mul,
y: 93 * mul,
w: 742 * mul,
h: 494 * mul,
radius: 17 * mul
})
}
pauseRect(ctx, 1)
ctx.strokeStyle = "#fff"
ctx.lineWidth = 24
ctx.stroke()
ctx.strokeStyle = "#000"
ctx.lineWidth = 12
ctx.stroke()
this.draw.pattern({
ctx: ctx,
img: assets.image["bg_pause"],
shape: pauseRect,
dx: 68,
dy: 11
})
if(this.showWarning.name === "scoreSaveFailed"){
var text = strings.scoreSaveFailed
}else if(this.showWarning.name === "loadSongError"){
var text = []
var textIndex = 0
var subText = [this.showWarning.title, this.showWarning.id, this.showWarning.error]
var textParts = strings.loadSongError.split("%s")
textParts.forEach((textPart, i) => {
if(i !== 0){
text.push(subText[textIndex++])
}
text.push(textPart)
})
text = text.join("")
}
this.draw.wrappingText({
ctx: ctx,
text: text,
fontSize: 30,
fontFamily: this.font,
x: 300,
y: 130,
width: 680,
height: 300,
lineHeight: 35,
fill: "#000",
verticalAlign: "middle",
textAlign: "center"
})
var _x = 640
var _y = 470
var _w = 464
var _h = 80
ctx.fillStyle = "#ffb447"
this.draw.roundedRect({
ctx: ctx,
x: _x - _w / 2,
y: _y,
w: _w,
h: _h,
radius: 30
})
ctx.fill()
var layers = [
{outline: "#000", letterBorder: 10},
{fill: "#fff"}
]
this.draw.layeredText({
ctx: ctx,
text: strings.ok,
x: _x,
y: _y + 18,
width: _w,
height: _h - 54,
fontSize: 40,
fontFamily: this.font,
letterSpacing: -1,
align: "center"
}, layers)
var highlight = 1
if(this.state.moveHover === "showWarning"){
highlight = 2
}
if(highlight){
this.draw.highlight({
ctx: ctx,
x: _x - _w / 2 - 3.5,
y: _y - 3.5,
w: _w + 7,
h: _h + 7,
animate: highlight === 1,
animateMS: this.state.moveMS,
opacity: highlight === 2 ? 0.8 : 1,
radius: 30
})
}
ctx.restore()
}
if(screen === "titleFadeIn"){
ctx.save()
@ -1955,6 +2293,11 @@ class SongSelect{
ctx.restore()
}
if(p2.session && (!this.lastScoreMS || ms > this.lastScoreMS + 1000)){
this.lastScoreMS = ms
scoreStorage.eventLoop()
}
}
drawClosedSong(config){
@ -1997,7 +2340,7 @@ class SongSelect{
})
}
this.draw.songFrame(config)
if(config.song.p2Cursor && p2.socket.readyState === 1){
if("p2Cursor" in config.song && config.song.p2Cursor !== null && p2.socket.readyState === 1){
this.draw.diffCursor({
ctx: ctx,
font: this.font,
@ -2013,37 +2356,47 @@ class SongSelect{
drawSongCrown(config){
if(!config.song.action && config.song.hash){
var ctx = config.ctx
var score = scoreStorage.get(config.song.hash, false, true)
var players = p2.session ? 2 : 1
var score = [scoreStorage.get(config.song.hash, false, true)]
var scoreDrawn = []
if(p2.session){
score[p2.player === 1 ? "push" : "unshift"](scoreStorage.getP2(config.song.hash, false, true))
}
for(var i = this.difficultyId.length; i--;){
var diff = this.difficultyId[i]
if(!score){
break
}
if(config.song.stars[i] && score[diff] && score[diff].crown){
this.draw.crown({
ctx: ctx,
type: score[diff].crown,
x: config.x + this.songAsset.width / 2,
y: config.y - 13,
scale: 0.3,
ratio: this.ratio / this.pixelRatio
})
this.draw.diffIcon({
ctx: ctx,
diff: i,
x: config.x + this.songAsset.width / 2 + 8,
y: config.y - 8,
scale: diff === "hard" || diff === "normal" ? 0.45 : 0.5,
border: 6.5,
small: true
})
break
for(var p = players; p--;){
if(!score[p] || scoreDrawn[p]){
continue
}
if(config.song.courses[this.difficultyId[i]] && score[p][diff] && score[p][diff].crown){
this.draw.crown({
ctx: ctx,
type: score[p][diff].crown,
x: (config.x + this.songAsset.width / 2) + (players === 2 ? p === 0 ? -13 : 13 : 0),
y: config.y - 13,
scale: 0.3,
ratio: this.ratio / this.pixelRatio
})
this.draw.diffIcon({
ctx: ctx,
diff: i,
x: (config.x + this.songAsset.width / 2 + 8) + (players === 2 ? p === 0 ? -13 : 13 : 0),
y: config.y - 8,
scale: diff === "hard" || diff === "normal" ? 0.45 : 0.5,
border: 6.5,
small: true
})
scoreDrawn[p] = true
}
}
}
}
}
startPreview(loadOnly){
if(!loadOnly && this.state && this.state.showWarning){
return
}
var currentSong = this.songs[this.selectedSong]
var id = currentSong.id
var prvTime = currentSong.preview
@ -2119,6 +2472,9 @@ class SongSelect{
}
}
playBgm(enabled){
if(enabled && this.state && this.state.showWarning){
return
}
if(enabled && !this.bgmEnabled){
this.bgmEnabled = true
snd.musicGain.fadeIn(0.4)
@ -2148,11 +2504,11 @@ class SongSelect{
})
if(currentSong){
currentSong.p2Cursor = diffId
if(p2.session && currentSong.stars){
if(p2.session && currentSong.courses){
this.selectedSong = index
this.state.move = 0
if(this.state.screen !== "difficulty"){
this.toSelectDifficulty(true)
this.toSelectDifficulty({player: response.value.player})
}
}
}
@ -2173,7 +2529,7 @@ class SongSelect{
var moveBy = response.value.move
if(moveBy === -1 || moveBy === 1){
this.selectedSong = song
this.categoryJump(moveBy, true)
this.categoryJump(moveBy, {player: response.value.player})
}
}else if(!selected){
this.state.locked = true
@ -2190,13 +2546,13 @@ class SongSelect{
if(Math.abs(altMoveBy) < Math.abs(moveBy)){
moveBy = altMoveBy
}
this.moveToSong(moveBy, true)
this.moveToSong(moveBy, {player: response.value.player})
}
}else if(this.songs[song].stars){
}else if(this.songs[song].courses){
this.selectedSong = song
this.state.move = 0
if(this.state.screen !== "difficulty"){
this.toSelectDifficulty(true)
this.toSelectDifficulty({player: response.value.player})
}
}
}
@ -2238,16 +2594,11 @@ class SongSelect{
getLocalTitle(title, titleLang){
if(titleLang){
titleLang = titleLang.split("\n")
titleLang.forEach(line => {
var space = line.indexOf(" ")
var id = line.slice(0, space)
if(id === strings.id){
title = line.slice(space + 1)
}else if(titleLang.length === 1 && strings.id === "en" && !(id in allStrings)){
title = line
for(var id in titleLang){
if(id === strings.id && titleLang[id]){
return titleLang[id]
}
})
}
}
return title
}
@ -2258,13 +2609,13 @@ class SongSelect{
}
}
playSound(id, time){
playSound(id, time, snd){
if(!this.drumSounds && (id === "se_don" || id === "se_ka" || id === "se_cancel")){
return
}
var ms = Date.now() + (time || 0) * 1000
if(!(id in this.playedSounds) || ms > this.playedSounds[id] + 30){
assets.sounds[id].play(time)
assets.sounds[id + (snd ? "_p" + snd : "")].play(time)
this.playedSounds[id] = ms
}
}

View File

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

View File

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

View File

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

View File

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

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">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div>
<div class="lyrics-hide">
<div>Lyrics offset:</div>
<div class="lyrics-offset input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div>
</div>
<label class="change-restart-label"><input class="change-restart" type="checkbox">Restart on change</label>
<label class="autoplay-label"><input class="autoplay" type="checkbox">Auto play</label>
<div class="bottom-btns">

View File

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

View File

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

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

23
templates/admin.html Normal file
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
(
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

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