Add custom Don
- Registered users can customise the colour of their Don and it will appear for other players - Bug fixes: - Add lyrics checkbox to admin page - 2P shows above "creative" or "with lyrics" labels - Prevent accidental alt and menu keyboard presses from triggering browser menus - Fixed mouse hitboxes on difficulty selection - Clean cached sounds and lyrics when another song is loading - Fixed debug jumping to the top-left of the screen when hidden - Fixed server volume not being applied to songs
63
app.py
@ -136,6 +136,29 @@ def get_version():
|
||||
|
||||
return version
|
||||
|
||||
def get_db_don(user):
|
||||
don_body_fill = user['don_body_fill'] if 'don_body_fill' in user else get_default_don('body_fill')
|
||||
don_face_fill = user['don_face_fill'] if 'don_face_fill' in user else get_default_don('face_fill')
|
||||
return {'body_fill': don_body_fill, 'face_fill': don_face_fill}
|
||||
|
||||
def get_default_don(part=None):
|
||||
if part == None:
|
||||
return {
|
||||
'body_fill': get_default_don('body_fill'),
|
||||
'face_fill': get_default_don('face_fill')
|
||||
}
|
||||
elif part == 'body_fill':
|
||||
return '#5fb7c1'
|
||||
elif part == 'face_fill':
|
||||
return '#ff5724'
|
||||
|
||||
def is_hex(input):
|
||||
try:
|
||||
int(input, 16)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def route_index():
|
||||
@ -213,6 +236,7 @@ def route_admin_songs_new_post():
|
||||
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['lyrics'] = True if request.form.get('lyrics') else False
|
||||
output['hash'] = None
|
||||
|
||||
seq = db.seq.find_one({'name': 'songs'})
|
||||
@ -262,6 +286,7 @@ def route_admin_songs_id_post(id):
|
||||
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['lyrics'] = True if request.form.get('lyrics') else False
|
||||
output['hash'] = request.form.get('hash')
|
||||
|
||||
if request.form.get('gen_hash'):
|
||||
@ -366,13 +391,15 @@ def route_api_register():
|
||||
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(password, salt)
|
||||
don = get_default_don()
|
||||
|
||||
session_id = os.urandom(24).hex()
|
||||
db.users.insert_one({
|
||||
'username': username,
|
||||
'username_lower': username.lower(),
|
||||
'password': hashed,
|
||||
'display_name': username,
|
||||
'display_name': username,
|
||||
'don': don,
|
||||
'user_level': 1,
|
||||
'session_id': session_id
|
||||
})
|
||||
@ -380,7 +407,7 @@ def route_api_register():
|
||||
session['session_id'] = session_id
|
||||
session['username'] = username
|
||||
session.permanent = True
|
||||
return jsonify({'status': 'ok', 'username': username, 'display_name': username})
|
||||
return jsonify({'status': 'ok', 'username': username, 'display_name': username, 'don': don})
|
||||
|
||||
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
@ -401,11 +428,13 @@ def route_api_login():
|
||||
if not bcrypt.checkpw(password, result['password']):
|
||||
return api_error('invalid_username_password')
|
||||
|
||||
don = get_db_don(result)
|
||||
|
||||
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']})
|
||||
return jsonify({'status': 'ok', 'username': result['username'], 'display_name': result['display_name'], 'don': don})
|
||||
|
||||
|
||||
@app.route('/api/logout', methods=['POST'])
|
||||
@ -435,6 +464,31 @@ def route_api_account_display_name():
|
||||
return jsonify({'status': 'ok', 'display_name': display_name})
|
||||
|
||||
|
||||
@app.route('/api/account/don', methods=['POST'])
|
||||
@login_required
|
||||
def route_api_account_don():
|
||||
data = request.get_json()
|
||||
if not schema.validate(data, schema.update_don):
|
||||
return abort(400)
|
||||
|
||||
don_body_fill = data.get('body_fill', '').strip()
|
||||
don_face_fill = data.get('face_fill', '').strip()
|
||||
if len(don_body_fill) != 7 or\
|
||||
not don_body_fill.startswith("#")\
|
||||
or not is_hex(don_body_fill[1:])\
|
||||
or len(don_face_fill) != 7\
|
||||
or not don_face_fill.startswith("#")\
|
||||
or not is_hex(don_face_fill[1:]):
|
||||
return api_error('invalid_don')
|
||||
|
||||
db.users.update_one({'username': session.get('username')}, {'$set': {
|
||||
'don_body_fill': don_body_fill,
|
||||
'don_face_fill': don_face_fill,
|
||||
}})
|
||||
|
||||
return jsonify({'status': 'ok', 'don': {'body_fill': don_body_fill, 'face_fill': don_face_fill}})
|
||||
|
||||
|
||||
@app.route('/api/account/password', methods=['POST'])
|
||||
@login_required
|
||||
def route_api_account_password():
|
||||
@ -518,7 +572,8 @@ def route_api_scores_get():
|
||||
})
|
||||
|
||||
user = db.users.find_one({'username': username})
|
||||
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name']})
|
||||
don = get_db_don(user)
|
||||
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don})
|
||||
|
||||
|
||||
def make_preview(song_id, song_type, preview):
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
BIN
public/assets/img/don_anim_10combo_a.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
public/assets/img/don_anim_10combo_b1.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
public/assets/img/don_anim_10combo_b2.png
Normal file
After Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 2.0 KiB |
BIN
public/assets/img/don_anim_clear_a.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
public/assets/img/don_anim_clear_b1.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
public/assets/img/don_anim_clear_b2.png
Normal file
After Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.9 KiB |
BIN
public/assets/img/don_anim_gogo_a.png
Normal file
After Width: | Height: | Size: 585 B |
BIN
public/assets/img/don_anim_gogo_b1.png
Normal file
After Width: | Height: | Size: 585 B |
BIN
public/assets/img/don_anim_gogo_b2.png
Normal file
After Width: | Height: | Size: 585 B |
Before Width: | Height: | Size: 2.0 KiB |
BIN
public/assets/img/don_anim_gogostart_a.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
public/assets/img/don_anim_gogostart_b1.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
public/assets/img/don_anim_gogostart_b2.png
Normal file
After Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 1.4 KiB |
BIN
public/assets/img/don_anim_normal_a.png
Normal file
After Width: | Height: | Size: 259 B |
BIN
public/assets/img/don_anim_normal_b1.png
Normal file
After Width: | Height: | Size: 259 B |
BIN
public/assets/img/don_anim_normal_b2.png
Normal file
After Width: | Height: | Size: 259 B |
@ -107,10 +107,14 @@ main {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
h1 small {
|
||||
h1 .song-id {
|
||||
color: #4a4a4a;
|
||||
}
|
||||
|
||||
.song .song-id {
|
||||
color: #a01300;
|
||||
}
|
||||
|
||||
.form-field-indent {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
@ -100,6 +100,7 @@
|
||||
font-size: calc(45px * var(--scale));
|
||||
line-height: 1.2;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
#game.portrait #song-lyrics{
|
||||
right: calc(20px * var(--scale));
|
||||
|
@ -377,3 +377,29 @@ kbd{
|
||||
font-size: 1.1em;
|
||||
color: #d00;
|
||||
}
|
||||
.customdon-div{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: right;
|
||||
}
|
||||
.customdon-canvas{
|
||||
max-width: 40vw;
|
||||
}
|
||||
.customdon-div label{
|
||||
display: block;
|
||||
padding: 0.3em;
|
||||
}
|
||||
.customdon-div input[type="color"]{
|
||||
font-size: inherit;
|
||||
width: 2.6em;
|
||||
height: 1.6em;
|
||||
padding: 0 0.1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.customdon-reset{
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
font-size: 1em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
@ -42,6 +42,35 @@ class Account{
|
||||
this.displayname.value = account.displayName
|
||||
this.inputForms.push(this.displayname)
|
||||
|
||||
this.redrawRunning = true
|
||||
this.customdonRedrawBind = this.customdonRedraw.bind(this)
|
||||
this.start = new Date().getTime()
|
||||
this.frames = [
|
||||
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 ,
|
||||
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 ,
|
||||
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,7 ,8 ,9 ,10,
|
||||
11,11,11,11,10,9 ,8 ,7 ,13,12,12,13,14,15,16,17
|
||||
]
|
||||
this.customdonCache = new CanvasCache()
|
||||
this.customdonCache.resize(723 * 2, 1858, 1)
|
||||
this.customdonCanvas = this.getElement("customdon-canvas")
|
||||
this.customdonCtx = this.customdonCanvas.getContext("2d")
|
||||
this.customdonBodyFill = this.getElement("customdon-bodyfill")
|
||||
this.customdonBodyFill.value = account.don.body_fill
|
||||
var parent = this.customdonBodyFill.parentNode
|
||||
parent.insertBefore(document.createTextNode(strings.account.customdon.bodyFill), parent.firstChild)
|
||||
pageEvents.add(this.customdonBodyFill, "change", this.customdonChange.bind(this))
|
||||
this.customdonFaceFill = this.getElement("customdon-facefill")
|
||||
this.customdonFaceFill.value = account.don.face_fill
|
||||
var parent = this.customdonFaceFill.parentNode
|
||||
parent.insertBefore(document.createTextNode(strings.account.customdon.faceFill), parent.firstChild)
|
||||
pageEvents.add(this.customdonFaceFill, "change", this.customdonChange.bind(this))
|
||||
this.customdonResetBtn = this.getElement("customdon-reset")
|
||||
this.customdonResetBtn.value = strings.account.customdon.reset
|
||||
pageEvents.add(this.customdonResetBtn, ["click", "touchstart"], this.customdonReset.bind(this))
|
||||
this.customdonChange()
|
||||
this.customdonRedraw()
|
||||
|
||||
this.accountPassButton = this.getElement("accountpass-btn")
|
||||
this.setAltText(this.accountPassButton, strings.account.changePassword)
|
||||
pageEvents.add(this.accountPassButton, ["click", "touchstart"], event => {
|
||||
@ -83,6 +112,70 @@ class Account{
|
||||
pageEvents.add(this.inputForms[i], ["keydown", "keyup", "keypress"], this.onFormPress.bind(this))
|
||||
}
|
||||
}
|
||||
customdonChange(){
|
||||
var ctx = this.customdonCtx
|
||||
this.customdonCache.clear()
|
||||
var w = 722
|
||||
var h = 1858
|
||||
this.customdonCache.set({
|
||||
w: w, h: h, id: "bodyFill"
|
||||
}, ctx => {
|
||||
ctx.drawImage(assets.image["don_anim_normal_b1"], 0, 0)
|
||||
ctx.globalCompositeOperation = "source-atop"
|
||||
ctx.fillStyle = this.customdonBodyFill.value
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
})
|
||||
this.customdonCache.set({
|
||||
w: w, h: h, id: "faceFill"
|
||||
}, ctx => {
|
||||
ctx.drawImage(assets.image["don_anim_normal_b2"], 0, 0)
|
||||
ctx.globalCompositeOperation = "source-atop"
|
||||
ctx.fillStyle = this.customdonFaceFill.value
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
|
||||
ctx.globalCompositeOperation = "source-over"
|
||||
this.customdonCache.get({
|
||||
ctx: ctx,
|
||||
x: 0, y: 0, w: w, h: h,
|
||||
id: "bodyFill"
|
||||
})
|
||||
})
|
||||
}
|
||||
customdonReset(event){
|
||||
if(event.type === "touchstart"){
|
||||
event.preventDefault()
|
||||
}
|
||||
this.customdonBodyFill.value = defaultDon.body_fill
|
||||
this.customdonFaceFill.value = defaultDon.face_fill
|
||||
this.customdonChange()
|
||||
}
|
||||
customdonRedraw(){
|
||||
if(!this.redrawRunning){
|
||||
return
|
||||
}
|
||||
requestAnimationFrame(this.customdonRedrawBind)
|
||||
if(!document.hasFocus()){
|
||||
return
|
||||
}
|
||||
var ms = new Date().getTime()
|
||||
var ctx = this.customdonCtx
|
||||
var frame = this.frames[Math.floor((ms - this.start) / 30) % this.frames.length]
|
||||
var w = 360
|
||||
var h = 184
|
||||
var sx = Math.floor(frame / 10) * (w + 2)
|
||||
var sy = (frame % 10) * (h + 2)
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
this.customdonCache.get({
|
||||
ctx: ctx,
|
||||
sx: sx, sy: sy, sw: w, sh: h,
|
||||
x: -26, y: 0, w: w, h: h,
|
||||
id: "faceFill"
|
||||
})
|
||||
ctx.drawImage(assets.image["don_anim_normal_a"],
|
||||
sx, sy, w, h,
|
||||
-26, 0, w, h
|
||||
)
|
||||
}
|
||||
showDiv(event, div){
|
||||
if(event){
|
||||
if(event.type === "touchstart"){
|
||||
@ -257,6 +350,7 @@ class Account{
|
||||
account.loggedIn = true
|
||||
account.username = response.username
|
||||
account.displayName = response.display_name
|
||||
account.don = response.don
|
||||
var loadScores = scores => {
|
||||
scoreStorage.load(scores)
|
||||
this.onEnd(false, true, true)
|
||||
@ -300,6 +394,7 @@ class Account{
|
||||
account.loggedIn = false
|
||||
delete account.username
|
||||
delete account.displayName
|
||||
delete account.don
|
||||
var loadScores = () => {
|
||||
scoreStorage.load()
|
||||
this.onEnd(false, true)
|
||||
@ -344,6 +439,7 @@ class Account{
|
||||
account.loggedIn = false
|
||||
delete account.username
|
||||
delete account.displayName
|
||||
delete account.don
|
||||
scoreStorage.load()
|
||||
pageEvents.send("logout")
|
||||
return Promise.resolve
|
||||
@ -357,6 +453,16 @@ class Account{
|
||||
account.displayName = response.display_name
|
||||
}))
|
||||
}
|
||||
var bodyFill = this.customdonBodyFill.value
|
||||
var faceFill = this.customdonFaceFill.value
|
||||
if(!noNameChange && (bodyFill !== account.body_fill || this.customdonFaceFill.value !== account.face_fill)){
|
||||
promises.push(this.request("account/don", {
|
||||
body_fill: bodyFill,
|
||||
face_fill: faceFill
|
||||
}).then(response => {
|
||||
account.don = response.don
|
||||
}))
|
||||
}
|
||||
var error = false
|
||||
var errorFunc = response => {
|
||||
if(error){
|
||||
@ -470,6 +576,11 @@ class Account{
|
||||
this.accountPass.reset()
|
||||
this.accountDel.reset()
|
||||
}
|
||||
this.redrawRunning = false
|
||||
this.customdonCache.clean()
|
||||
pageEvents.remove(this.customdonBodyFill, "change")
|
||||
pageEvents.remove(this.customdonFaceFill, "change")
|
||||
pageEvents.remove(this.customdonResetBtn, ["click", "touchstart"])
|
||||
pageEvents.remove(this.accounPassButton, ["click", "touchstart"])
|
||||
pageEvents.remove(this.accountDelButton, ["click", "touchstart"])
|
||||
pageEvents.remove(this.logoutButton, ["mousedown", "touchstart"])
|
||||
@ -479,6 +590,12 @@ class Account{
|
||||
}
|
||||
delete this.errorDiv
|
||||
delete this.displayname
|
||||
delete this.frames
|
||||
delete this.customdonCanvas
|
||||
delete this.customdonCtx
|
||||
delete this.customdonBodyFill
|
||||
delete this.customdonFaceFill
|
||||
delete this.customdonResetBtn
|
||||
delete this.accountPassButton
|
||||
delete this.accountPass
|
||||
delete this.accountPassDiv
|
||||
|
@ -58,11 +58,21 @@ var assets = {
|
||||
"dancing-don.gif",
|
||||
"bg-pattern-1.png",
|
||||
"difficulty.png",
|
||||
"don_anim_normal.png",
|
||||
"don_anim_10combo.png",
|
||||
"don_anim_gogo.png",
|
||||
"don_anim_gogostart.png",
|
||||
"don_anim_clear.png",
|
||||
"don_anim_normal_a.png",
|
||||
"don_anim_normal_b1.png",
|
||||
"don_anim_normal_b2.png",
|
||||
"don_anim_10combo_a.png",
|
||||
"don_anim_10combo_b1.png",
|
||||
"don_anim_10combo_b2.png",
|
||||
"don_anim_gogo_a.png",
|
||||
"don_anim_gogo_b1.png",
|
||||
"don_anim_gogo_b2.png",
|
||||
"don_anim_gogostart_a.png",
|
||||
"don_anim_gogostart_b1.png",
|
||||
"don_anim_gogostart_b2.png",
|
||||
"don_anim_clear_a.png",
|
||||
"don_anim_clear_b1.png",
|
||||
"don_anim_clear_b2.png",
|
||||
"fire_anim.png",
|
||||
"fireworks_anim.png",
|
||||
"bg_genre_0.png",
|
||||
|
@ -44,11 +44,47 @@ class CanvasAsset{
|
||||
mod(length, index){
|
||||
return ((index % length) + length) % length
|
||||
}
|
||||
addFrames(name, frames, image){
|
||||
addFrames(name, frames, image, don){
|
||||
var framesObj = {
|
||||
frames: frames
|
||||
frames: frames,
|
||||
don: don
|
||||
}
|
||||
if(image){
|
||||
if(don){
|
||||
var img = assets.image[image + "_a"]
|
||||
var cache1 = new CanvasCache()
|
||||
var cache2 = new CanvasCache()
|
||||
var w = img.width
|
||||
var h = img.height
|
||||
cache1.resize(w, h, 1)
|
||||
cache2.resize(w, h, 1)
|
||||
cache1.set({
|
||||
w: w, h: h, id: "1"
|
||||
}, ctx => {
|
||||
ctx.drawImage(assets.image[image + "_b1"], 0, 0)
|
||||
ctx.globalCompositeOperation = "source-atop"
|
||||
ctx.fillStyle = don.body_fill
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
})
|
||||
cache2.set({
|
||||
w: w, h: h, id: "2"
|
||||
}, ctx => {
|
||||
ctx.drawImage(assets.image[image + "_b2"], 0, 0)
|
||||
ctx.globalCompositeOperation = "source-atop"
|
||||
ctx.fillStyle = don.face_fill
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
|
||||
ctx.globalCompositeOperation = "source-over"
|
||||
cache1.get({
|
||||
ctx: ctx,
|
||||
x: 0, y: 0, w: w, h: h,
|
||||
id: "1"
|
||||
})
|
||||
ctx.drawImage(img, 0, 0)
|
||||
})
|
||||
cache1.clean()
|
||||
framesObj.cache = cache2
|
||||
framesObj.image = cache2.canvas
|
||||
}else if(image){
|
||||
framesObj.image = assets.image[image]
|
||||
}
|
||||
this.animationFrames[name] = framesObj
|
||||
@ -61,6 +97,7 @@ class CanvasAsset{
|
||||
if(framesObj.image){
|
||||
this.image = framesObj.image
|
||||
}
|
||||
this.don = framesObj.don
|
||||
}else{
|
||||
this.animation = false
|
||||
}
|
||||
@ -100,4 +137,12 @@ class CanvasAsset{
|
||||
}
|
||||
this.beatInterval = beatMS
|
||||
}
|
||||
clean(){
|
||||
for(var i in this.animationFrames){
|
||||
var frame = this.animationFrames[i]
|
||||
if(frame.cache){
|
||||
frame.cache.clean()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,8 +91,12 @@ class CanvasCache{
|
||||
return
|
||||
}
|
||||
var z = this.scale
|
||||
var sx = (img.x + (config.sx || 0)) * z |0
|
||||
var sy = (img.y + (config.sy || 0)) * z |0
|
||||
var sw = (config.sw || img.w) * z |0
|
||||
var sh = (config.sh || img.h) * z |0
|
||||
config.ctx.drawImage(this.canvas,
|
||||
img.x * z |0, img.y * z |0, img.w * z |0, img.h * z |0,
|
||||
sx, sy, sw, sh,
|
||||
config.x |0, config.y |0, config.w |0, config.h |0
|
||||
)
|
||||
if(saved){
|
||||
|
@ -8,8 +8,16 @@ class Controller{
|
||||
this.touchEnabled = touchEnabled
|
||||
if(multiplayer === 2){
|
||||
this.snd = p2.player === 2 ? "_p1" : "_p2"
|
||||
this.don = p2.don || defaultDon
|
||||
}else{
|
||||
this.snd = multiplayer ? "_p" + p2.player : ""
|
||||
this.don = account.loggedIn ? account.don : defaultDon
|
||||
}
|
||||
if(this.snd === "_p2" && this.objEqual(defaultDon, this.don)){
|
||||
this.don = {
|
||||
body_fill: defaultDon.face_fill,
|
||||
face_fill: defaultDon.body_fill
|
||||
}
|
||||
}
|
||||
|
||||
this.calibrationMode = selectedSong.folder === "calibration"
|
||||
@ -317,6 +325,14 @@ class Controller{
|
||||
return this.mekadon.play(circle)
|
||||
}
|
||||
}
|
||||
objEqual(a, b){
|
||||
for(var i in a){
|
||||
if(a[i] !== b[i]){
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
clean(){
|
||||
if(this.multiplayer === 1){
|
||||
this.syncWith.clean()
|
||||
|
@ -82,6 +82,9 @@ class Debug{
|
||||
}
|
||||
}
|
||||
stopMove(event){
|
||||
if(this.debugDiv.style.display === "none"){
|
||||
return
|
||||
}
|
||||
if(!event || event.type === "resize"){
|
||||
var divPos = this.debugDiv.getBoundingClientRect()
|
||||
var x = divPos.left
|
||||
|
@ -65,7 +65,13 @@ class Keyboard{
|
||||
}
|
||||
keyEvent(event){
|
||||
var key = event.key.toLowerCase()
|
||||
if(key === "escape" || key === "backspace" || key === "tab"){
|
||||
if(
|
||||
key === "escape" ||
|
||||
key === "backspace" ||
|
||||
key === "tab" ||
|
||||
key === "contextmenu" ||
|
||||
key === "alt"
|
||||
){
|
||||
event.preventDefault()
|
||||
}
|
||||
if(!event.repeat){
|
||||
|
@ -178,6 +178,7 @@ class Loader{
|
||||
account.loggedIn = true
|
||||
account.username = response.username
|
||||
account.displayName = response.display_name
|
||||
account.don = response.don
|
||||
scoreStorage.load(response.scores)
|
||||
pageEvents.send("login", account.username)
|
||||
}
|
||||
@ -236,7 +237,8 @@ class Loader{
|
||||
})
|
||||
p2.send("invite", {
|
||||
id: location.hash.slice(1).toLowerCase(),
|
||||
name: account.loggedIn ? account.displayName : null
|
||||
name: account.loggedIn ? account.displayName : null,
|
||||
don: account.loggedIn ? account.don : null
|
||||
})
|
||||
setTimeout(() => {
|
||||
if(p2.socket.readyState !== 1){
|
||||
|
@ -37,7 +37,18 @@ class LoadSong{
|
||||
this.promises = []
|
||||
if(song.folder !== "calibration"){
|
||||
assets.sounds["v_start"].play()
|
||||
var songObj = assets.songs.find(song => song.id === id)
|
||||
var songObj
|
||||
assets.songs.forEach(song => {
|
||||
if(song.id === id){
|
||||
songObj = song
|
||||
}else{
|
||||
if(song.sound){
|
||||
song.sound.clean()
|
||||
delete song.sound
|
||||
}
|
||||
delete song.lyricsData
|
||||
}
|
||||
})
|
||||
}else{
|
||||
var songObj = {
|
||||
"music": "muted",
|
||||
@ -167,6 +178,9 @@ class LoadSong{
|
||||
}), url)
|
||||
img.src = url
|
||||
}
|
||||
if(songObj.volume && songObj.volume !== 1){
|
||||
this.promises.push(new Promise(resolve => setTimeout(resolve, 500)))
|
||||
}
|
||||
Promise.all(this.promises).then(() => {
|
||||
if(!this.error){
|
||||
this.setupMultiplayer()
|
||||
@ -338,7 +352,8 @@ class LoadSong{
|
||||
p2.send("join", {
|
||||
id: song.folder,
|
||||
diff: song.difficulty,
|
||||
name: account.loggedIn ? account.displayName : null
|
||||
name: account.loggedIn ? account.displayName : null,
|
||||
don: account.loggedIn ? account.don : null
|
||||
})
|
||||
}else{
|
||||
this.clean()
|
||||
|
@ -80,6 +80,10 @@ var perf = {
|
||||
allImg: 0,
|
||||
load: 0
|
||||
}
|
||||
var defaultDon = {
|
||||
body_fill: "#5fb7c1",
|
||||
face_fill: "#ff5724"
|
||||
}
|
||||
var strings
|
||||
var vectors
|
||||
var settings
|
||||
|
@ -131,6 +131,7 @@ class P2Connection{
|
||||
this.hashLock = false
|
||||
}
|
||||
this.name = null
|
||||
this.don = null
|
||||
scoreStorage.clearP2()
|
||||
break
|
||||
case "gameresults":
|
||||
@ -165,7 +166,8 @@ class P2Connection{
|
||||
}
|
||||
break
|
||||
case "name":
|
||||
this.name = response.value ? response.value.toString() : response.value
|
||||
this.name = response.value ? (response.value.name || "").toString() : ""
|
||||
this.don = response.value ? (response.value.don) : null
|
||||
break
|
||||
case "getcrowns":
|
||||
if(response.value){
|
||||
|
@ -285,6 +285,7 @@ class ScoreStorage{
|
||||
account.loggedIn = false
|
||||
delete account.username
|
||||
delete account.displayName
|
||||
delete account.don
|
||||
this.load()
|
||||
pageEvents.send("logout")
|
||||
return Promise.reject()
|
||||
|
@ -492,7 +492,7 @@ class SongSelect{
|
||||
}
|
||||
}else if(this.state.screen === "difficulty"){
|
||||
var moveBy = this.diffSelMouse(mouse.x, mouse.y)
|
||||
if(mouse.x < 183 || mouse.x > 1095 || mouse.y < 40 || mouse.y > 540){
|
||||
if(mouse.x < 183 || mouse.x > 1095 || mouse.y < 54 || mouse.y > 554){
|
||||
this.toSongSelect()
|
||||
}else if(moveBy === 0){
|
||||
this.selectedDiff = 0
|
||||
@ -596,11 +596,11 @@ class SongSelect{
|
||||
}
|
||||
diffSelMouse(x, y){
|
||||
if(this.state.locked === 0){
|
||||
if(223 < x && x < 367 && 118 < y && y < 422){
|
||||
if(223 < x && x < 367 && 132 < y && y < 436){
|
||||
return Math.floor((x - 223) / ((367 - 223) / 2))
|
||||
}else if(this.songs[this.selectedSong].maker && this.songs[this.selectedSong].maker.id > 0 && this.songs[this.selectedSong].maker.url && x > 230 && x < 485 && y > 432 && y < 519) {
|
||||
}else if(this.songs[this.selectedSong].maker && this.songs[this.selectedSong].maker.id > 0 && this.songs[this.selectedSong].maker.url && x > 230 && x < 485 && y > 446 && y < 533) {
|
||||
return "maker"
|
||||
}else if(550 < x && x < 1050 && 95 < y && y < 524){
|
||||
}else if(550 < x && x < 1050 && 109 < y && y < 538){
|
||||
var moveBy = Math.floor((x - 550) / ((1050 - 550) / 5)) + this.diffOptions.length
|
||||
var currentSong = this.songs[this.selectedSong]
|
||||
if(
|
||||
@ -1631,17 +1631,6 @@ class SongSelect{
|
||||
if(this.selectedDiff === 4 + this.diffOptions.length){
|
||||
currentDiff = 3
|
||||
}
|
||||
if(songSel && i === currentSong.p2Cursor && p2.socket.readyState === 1){
|
||||
this.draw.diffCursor({
|
||||
ctx: ctx,
|
||||
font: this.font,
|
||||
x: _x,
|
||||
y: _y - 45,
|
||||
two: !p2.session || p2.player === 1,
|
||||
side: false,
|
||||
scale: 0.7
|
||||
})
|
||||
}
|
||||
if(!songSel){
|
||||
var highlight = 0
|
||||
if(this.state.moveHover - this.diffOptions.length === i){
|
||||
@ -1870,6 +1859,24 @@ class SongSelect{
|
||||
}
|
||||
}
|
||||
|
||||
for(var i = 0; currentSong.courses && i < 4; i++){
|
||||
if(currentSong.courses[this.difficultyId[i]] || currentUra){
|
||||
if(songSel && i === currentSong.p2Cursor && p2.socket.readyState === 1){
|
||||
var _x = x + 33 + i * 60
|
||||
var _y = y + 120
|
||||
this.draw.diffCursor({
|
||||
ctx: ctx,
|
||||
font: this.font,
|
||||
x: _x,
|
||||
y: _y - 45,
|
||||
two: !p2.session || p2.player === 1,
|
||||
side: false,
|
||||
scale: 0.7
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!songSel && currentSong.courses.ura){
|
||||
var fade = ((ms - this.state.screenMS) % 1200) / 1200
|
||||
var _x = x + 402 + 4 * 100 + fade * 25
|
||||
|
@ -995,8 +995,23 @@ var translations = {
|
||||
en: "Save",
|
||||
},
|
||||
displayName: {
|
||||
ja: null,
|
||||
en: "Displayed Name",
|
||||
},
|
||||
customdon: {
|
||||
bodyFill: {
|
||||
ja: null,
|
||||
en: "Body",
|
||||
},
|
||||
faceFill: {
|
||||
ja: null,
|
||||
en: "Face",
|
||||
},
|
||||
reset: {
|
||||
ja: null,
|
||||
en: "Reset",
|
||||
}
|
||||
},
|
||||
changePassword: {
|
||||
ja: null,
|
||||
en: "Change Password",
|
||||
@ -1043,6 +1058,10 @@ var translations = {
|
||||
ja: null,
|
||||
en: "Cannot use this name, please check that your new name is at most 25 characters long",
|
||||
},
|
||||
invalid_don: {
|
||||
ja: null,
|
||||
en: "Could not save your custom Don"
|
||||
},
|
||||
current_password_invalid: {
|
||||
ja: null,
|
||||
en: "Current password does not match",
|
||||
|
@ -9,18 +9,17 @@ class ViewAssets{
|
||||
this.don = this.createAsset("background", frame => {
|
||||
var imgw = 360
|
||||
var imgh = 184
|
||||
var scale = 165
|
||||
var w = imgw
|
||||
var h = imgh
|
||||
return {
|
||||
sx: Math.floor(frame / 10) * imgw,
|
||||
sy: (frame % 10) * imgh + 1,
|
||||
sx: Math.floor(frame / 10) * (imgw + 2),
|
||||
sy: (frame % 10) * (imgh + 2),
|
||||
sw: imgw,
|
||||
sh: imgh - 1,
|
||||
sh: imgh,
|
||||
x: view.portrait ? -60 : 0,
|
||||
y: view.portrait ? (view.player === 2 ? 560 : 35) : (view.player === 2 ? 360 : 2),
|
||||
w: w,
|
||||
h: h - 1
|
||||
y: view.portrait ? (view.player === 2 ? 560 : 35) : (view.player === 2 ? 360 : 0),
|
||||
w: w / h * (h + 0.5),
|
||||
h: h + 0.5
|
||||
}
|
||||
})
|
||||
this.don.addFrames("normal", [
|
||||
@ -28,15 +27,15 @@ class ViewAssets{
|
||||
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 ,
|
||||
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,7 ,8 ,9 ,10,
|
||||
11,11,11,11,10,9 ,8 ,7 ,13,12,12,13,14,15,16,17
|
||||
], "don_anim_normal")
|
||||
this.don.addFrames("10combo", 22, "don_anim_10combo")
|
||||
], "don_anim_normal", this.controller.don)
|
||||
this.don.addFrames("10combo", 22, "don_anim_10combo", this.controller.don)
|
||||
this.don.addFrames("gogo", [
|
||||
42,43,43,44,45,46,47,48,49,50,51,52,53,54,
|
||||
55,0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,11,12,13,
|
||||
14,14,15,16,17,18,19,20,21,22,23,24,25,26,
|
||||
27,28,29,30,31,32,33,34,35,36,37,38,39,40,41
|
||||
], "don_anim_gogo")
|
||||
this.don.addFrames("gogostart", 27, "don_anim_gogostart")
|
||||
], "don_anim_gogo", this.controller.don)
|
||||
this.don.addFrames("gogostart", 27, "don_anim_gogostart", this.controller.don)
|
||||
this.don.normalAnimation = () => {
|
||||
if(this.view.gogoTime){
|
||||
var length = this.don.getAnimationLength("gogo")
|
||||
@ -58,7 +57,7 @@ class ViewAssets{
|
||||
}
|
||||
}
|
||||
}
|
||||
this.don.addFrames("clear", 30, "don_anim_clear")
|
||||
this.don.addFrames("clear", 30, "don_anim_clear", this.controller.don)
|
||||
this.don.normalAnimation()
|
||||
|
||||
// Bar
|
||||
@ -176,6 +175,7 @@ class ViewAssets{
|
||||
})
|
||||
}
|
||||
clean(){
|
||||
this.don.clean()
|
||||
delete this.ctx
|
||||
delete this.don
|
||||
delete this.fire
|
||||
|
@ -7,6 +7,20 @@
|
||||
<div class="displayname-hint"></div>
|
||||
<input type="text" class="displayname" maxlength="25">
|
||||
</div>
|
||||
<div class="customdon-div">
|
||||
<canvas class="customdon-canvas" width="254" height="184"></canvas>
|
||||
<div>
|
||||
<label>
|
||||
<input type="color" class="customdon-bodyfill">
|
||||
</label>
|
||||
<label>
|
||||
<input type="color" class="customdon-facefill">
|
||||
</label>
|
||||
<label>
|
||||
<input type="button" class="customdon-reset" value="Reset">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<form class="accountpass-form">
|
||||
<div>
|
||||
<div class="accountpass-btn taibtn stroke-sub link-btn"></div>
|
||||
|
@ -34,6 +34,15 @@ update_display_name = {
|
||||
}
|
||||
}
|
||||
|
||||
update_don = {
|
||||
'$schema': 'http://json-schema.org/schema#',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'body_fill': {'type': 'string'},
|
||||
'face_fill': {'type': 'string'}
|
||||
}
|
||||
}
|
||||
|
||||
update_password = {
|
||||
'$schema': 'http://json-schema.org/schema#',
|
||||
'type': 'object',
|
||||
|
27
server.py
@ -43,7 +43,8 @@ async def connection(ws, path):
|
||||
"ws": ws,
|
||||
"action": "ready",
|
||||
"session": False,
|
||||
"name": None
|
||||
"name": None,
|
||||
"don": None
|
||||
}
|
||||
server_status["users"].append(user)
|
||||
try:
|
||||
@ -81,6 +82,7 @@ async def connection(ws, path):
|
||||
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
|
||||
user["don"] = value["don"] if "don" in value else None
|
||||
if not id or not diff:
|
||||
continue
|
||||
if id not in waiting:
|
||||
@ -95,6 +97,7 @@ async def connection(ws, path):
|
||||
else:
|
||||
# Join the other user and start game
|
||||
user["name"] = value["name"] if "name" in value else None
|
||||
user["don"] = value["don"] if "don" in value else None
|
||||
user["other_user"] = waiting[id]["user"]
|
||||
waiting_diff = waiting[id]["diff"]
|
||||
del waiting[id]
|
||||
@ -107,8 +110,14 @@ async def connection(ws, path):
|
||||
await asyncio.wait([
|
||||
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"]))
|
||||
ws.send(msgobj("name", {
|
||||
"name": user["other_user"]["name"],
|
||||
"don": user["other_user"]["don"]
|
||||
})),
|
||||
user["other_user"]["ws"].send(msgobj("name", {
|
||||
"name": user["name"],
|
||||
"don": user["don"]
|
||||
}))
|
||||
])
|
||||
else:
|
||||
# Wait for another user
|
||||
@ -130,10 +139,12 @@ async def connection(ws, path):
|
||||
user["action"] = "invite"
|
||||
user["session"] = invite
|
||||
user["name"] = value["name"] if "name" in value else None
|
||||
user["don"] = value["don"] if "don" in value else None
|
||||
await ws.send(msgobj("invite", invite))
|
||||
elif value and "id" in value and value["id"] in server_status["invites"]:
|
||||
# Join a session with the other user
|
||||
user["name"] = value["name"] if "name" in value else None
|
||||
user["don"] = value["don"] if "don" in value else None
|
||||
user["other_user"] = server_status["invites"][value["id"]]
|
||||
del server_status["invites"][value["id"]]
|
||||
if "ws" in user["other_user"]:
|
||||
@ -146,8 +157,14 @@ async def connection(ws, path):
|
||||
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"]))
|
||||
ws.send(msgobj("name", {
|
||||
"name": user["other_user"]["name"],
|
||||
"don": user["other_user"]["don"]
|
||||
})),
|
||||
user["other_user"]["ws"].send(msgobj("name", {
|
||||
"name": user["name"],
|
||||
"don": user["don"]
|
||||
}))
|
||||
])
|
||||
else:
|
||||
del user["other_user"]
|
||||
|
@ -1,6 +1,10 @@
|
||||
{% extends 'admin.html' %}
|
||||
{% block content %}
|
||||
<h1>{{ song.title }} <small>(ID: {{ song.id }})</small></h1>
|
||||
{% if song.title_lang.en %}
|
||||
<h1>{{ song.title_lang.en }} <small>({{ song.title }})</small> <small class="song-id">(ID: {{ song.id }})</small></h1>
|
||||
{% else %}
|
||||
<h1>{{ song.title }} <small class="song-id">(ID: {{ song.id }})</small></h1>
|
||||
{% endif %}
|
||||
{% for cat, message in get_flashed_messages(with_categories=true) %}
|
||||
<div class="message{% if cat %} message-{{cat}}{% endif %}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
@ -116,9 +120,14 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<p><label for="lyrics">Lyrics</label></p>
|
||||
<span class="checkbox"><input type="checkbox" name="lyrics" id="lyrics"{% if song.lyrics %} checked{% endif %}><label for="lyrics"> Enabled</label></span>
|
||||
</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>
|
||||
<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>
|
||||
|
@ -116,6 +116,11 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<p><label for="lyrics">Lyrics</label></p>
|
||||
<span class="checkbox"><input type="checkbox" name="lyrics" id="lyrics"><label for="lyrics"> Enabled</label></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="save-song">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -11,9 +11,9 @@
|
||||
<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>
|
||||
<p><span class="song-id">{{ song.id }}.</span> {{ song.title_lang.en }} <small>({{ song.title }})</small></p>
|
||||
{% else %}
|
||||
<p>{{ song.title }}</p>
|
||||
<p><span class="song-id">{{ song.id }}.</span> {{ song.title }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|