Commit 67e10e74 authored by nanahira's avatar nanahira

Merge branch 'master' of github.com:bui/taiko-web

parents 0b91dc61 6c02bf90
......@@ -36,6 +36,7 @@ $RECYCLE.BIN/
.Trashes
.vscode
*.pyc
# Directories potentially created on remote AFP share
.AppleDB
......@@ -48,7 +49,7 @@ public/api
taiko.db
version.json
public/index.html
config.json
config.py
public/assets/song_skins
*.log
.venv*
......
......@@ -3,52 +3,35 @@ FROM debian:buster-slim as git-fetcher
RUN apt update && \
env DEBIAN_FRONTEND=noninteractive apt install -y git && \
rm -rf /var/lib/apt/lists/* && \
mkdir /taiko-web
mkdir /usr/src/app
COPY .git /taiko-web/.git
WORKDIR /taiko-web
RUN git log -1 --pretty="format:{\"commit\": \"%H\", \"commit_short\": \"%h\", \"version\": \"TAIKO_VERSION\", \"url\": \"TAIKO_GIT_URL\"}" > ./version-template.json
COPY .git /usr/src/app/.git
WORKDIR /usr/src/app
RUN git log -1 --pretty="format:{\"commit\": \"%H\", \"commit_short\": \"%h\", \"version\": \"TAIKO_VERSION\"}" > ./version-template.json
FROM node:buster-slim
FROM python:3.5.9-slim-buster
#RUN sed -i 's/deb.debian.org/mirrors.163.com/g' /etc/apt/sources.list && \
# sed -i 's/security.debian.org/mirrors.163.com/g' /etc/apt/sources.list
#dependencies
RUN apt update && \
env DEBIAN_FRONTEND=noninteractive apt install -y python2.7 python-virtualenv python3.5 python3-virtualenv ffmpeg nginx && \
env DEBIAN_FRONTEND=noninteractive apt install -y ffmpeg && \
rm -rf /var/lib/apt/lists/* && \
npm install -g pm2
mkdir /usr/src/app
COPY . /taiko-web
COPY --from=git-fetcher /taiko-web/version-template.json /taiko-web/version-template.json
WORKDIR /taiko-web
# envs
RUN bash -c 'virtualenv -p /usr/bin/python2 .venv2 && \
source .venv2/bin/activate && \
pip install Flask Flask-Caching ffmpy gunicorn redis && \
deactivate' && \
bash -c 'virtualenv -p /usr/bin/python3 .venv3 && \
source .venv3/bin/activate && \
pip install websockets && \
deactivate' && \
ln -s /taiko-web/templates/index.html /taiko-web/public/index.html && \
ln -s /taiko-web/public/songs/taiko.db /taiko-web/taiko.db && \
cp -rf /taiko-web/docker/taiko-web-nginx.conf /etc/nginx/conf.d/ && \
rm -rf /etc/nginx/sites-enabled/*
WORKDIR /usr/src/app
COPY ./requirements.txt .
RUN pip install --no-cache -r ./requirements.txt
COPY . .
COPY --from=git-fetcher ./version-template.json ./version-template.json
#info
EXPOSE 80
# VOLUME [ "/taiko-web/public/songs" ]
ENV TAIKO_SONGS_URL http://taiko.example.com/songs/
ENV TAIKO_ASSETS_URL http://taiko.example.com/assets/
ENV TAIKO_MULTIPLAYER_URL wss://taiko.example.com/p2/
ENV TAIKO_GIT_URL https://git.example.com/user/repo/
ENV TAIKO_VERSION Nanahira
ENV TAIKO_EMAIL 78877@qq.com
ENV TAIKO_REDIS_HOST 127.0.0.1
ENV TAIKO_REDIS_PORT 6379
#ENV TAIKO_REDIS_PASSWORD blank
ENV TAIKO_REDIS_DB 0
CMD [ "/taiko-web/docker/entrypoint.sh" ]
ENTRYPOINT [ "/usr/src/app/docker/entrypoint.sh" ]
This diff is collapsed.
{
"songs_baseurl": "",
"assets_baseurl": "",
"redis_host": "127.0.0.1",
"redis_port": 6379,
"redis_password": "",
"redis_db": 0
}
# The full URL base asset URL, with trailing slash.
ASSETS_BASEURL = ''
# The full URL base song URL, with trailing slash.
SONGS_BASEURL = ''
# The full URL base websocket URL, with trailing slash.
MULTIPLAYER_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/'
#!/bin/bash
if [ -z $TAIKO_SONGS_URL ]; then
export TAIKO_SONGS_URL=$TAIKO_URL/songs/
fi
if [ -z $TAIKO_ASSETS_URL ]; then
export TAIKO_ASSETS_URL=$TAIKO_URL/assets/
fi
echo "{\"songs_baseurl\":\"$TAIKO_SONGS_URL\",\"assets_baseurl\":\"$TAIKO_ASSETS_URL\",\"multiplayer_baseurl\":\"$TAIKO_MULTIPLAYER_URL\",\"email\":\"$TAIKO_EMAIL\",\"redis_host\":\"$TAIKO_REDIS_HOST\",\"redis_port\":$TAIKO_REDIS_PORT,\"redis_password\":\"$TAIKO_REDIS_PASSWORD\",\"redis_db\":$TAIKO_REDIS_DB,\"git_url\":\"$TAIKO_GIT_URL\"}" | tee ./config.json
sed "s#TAIKO_GIT_URL#$TAIKO_GIT_URL#g;s#TAIKO_VERSION#$TAIKO_VERSION#g" version-template.json | tee version.json
pm2-docker start ./docker/pm2-docker.json
python "$@"
public/assets/img/fire_anim.png

15.7 KB | W: | H:

public/assets/img/fire_anim.png

93.7 KB | W: | H:

public/assets/img/fire_anim.png
public/assets/img/fire_anim.png
public/assets/img/fire_anim.png
public/assets/img/fire_anim.png
  • 2-up
  • Swipe
  • Onion skin
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 .song-id {
color: #4a4a4a;
}
.song .song-id {
color: #a01300;
}
.form-field-indent {
margin-left: 20px;
}
.checkbox {
display: inline-block;
}
.checkbox input {
margin-right: 3px;
margin-left: 5px;
}
.message {
background: #2c862f;
padding: 15px;
margin-bottom: 10px;
color: white;
}
.message-error {
background: #b92222;
}
.save-song {
font-size: 22pt;
width: 120px;
}
.delete-song button {
float: right;
margin-top: -25px;
font-size: 12pt;
}
.side-button {
float: right;
background: green;
padding: 5px 20px;
color: white;
text-decoration: none;
margin-top: 25px;
}
......@@ -123,6 +123,7 @@
}
#debug .autoplay-label,
#debug .branch-hide{
#debug .branch-hide,
#debug .lyrics-hide{
display: none;
}
......@@ -89,3 +89,40 @@
.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;
overflow-wrap: break-word;
}
#game.portrait #song-lyrics{
right: calc(20px * var(--scale));
left: calc(20px * var(--scale));
}
#song-lyrics .stroke,
#song-lyrics .fill{
position: absolute;
right: 0;
bottom: 0;
left: 0;
}
#song-lyrics .stroke{
-webkit-text-stroke: calc(7px * var(--scale)) #00a;
}
#song-lyrics .fill{
color: #fff;
}
#song-lyrics ruby{
display: inline-flex;
flex-direction: column-reverse;
}
#song-lyrics rt{
line-height: 1;
}
......@@ -117,3 +117,20 @@ body{
color: #777;
text-shadow: 0.05em 0.05em #fff;
}
.view-outer.loader-error-div,
.loader-error-div .diag-txt{
display: none
}
.loader-error-div{
font-family: sans-serif;
}
.loader-error-div .debug-link{
color: #00f;
text-decoration: underline;
cursor: pointer;
float: right;
}
.loader-error-div .diag-txt textarea,
.loader-error-div .diag-txt iframe{
height: 10em;
}
......@@ -108,8 +108,8 @@ kbd{
.left-buttons .taibtn{
margin-right: 0.4em;
}
#diag-txt textarea,
#diag-txt iframe{
.diag-txt textarea,
.diag-txt iframe{
width: 100%;
height: 5em;
font-size: inherit;
......@@ -119,6 +119,7 @@ kbd{
background: #fff;
border: 1px solid #a9a9a9;
user-select: all;
box-sizing: border-box;
}
.text-warn{
color: #d00;
......@@ -291,3 +292,114 @@ 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;
}
.customdon-div{
display: flex;
justify-content: center;
align-items: center;
text-align: right;
}
.customdon-canvas{
width: 13em;
}
.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;
}
......@@ -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){
......
This diff is collapsed.
......@@ -31,7 +31,9 @@ var assets = {
"importsongs.js",
"logo.js",
"settings.js",
"scorestorage.js"
"scorestorage.js",
"account.js",
"lyrics.js"
],
"css": [
"main.css",
......@@ -56,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",
......@@ -93,11 +105,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",
......@@ -109,6 +117,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",
......@@ -144,7 +156,9 @@ var assets = {
"about.html",
"debug.html",
"session.html",
"settings.html"
"settings.html",
"account.html",
"login.html"
],
"songs": [],
......
......@@ -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){
......
......@@ -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)
......
......@@ -7,7 +7,19 @@ 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"
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"
this.audioLatency = 0
......@@ -54,6 +66,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)
}
}
}
})
}
......@@ -156,10 +177,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()
......@@ -198,14 +225,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(){
......@@ -218,20 +245,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)
......@@ -292,6 +326,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()
......@@ -307,6 +349,9 @@ class Controller{
debugObj.debug.updateStatus()
}
}
if(this.lyrics){
this.lyrics.clean()
}
}
getModBadge() {
if (!this.mods) {
......
......@@ -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()
......@@ -77,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
......@@ -129,6 +137,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 +147,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 +187,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 +208,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 +230,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 +284,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 +311,8 @@ class Debug{
delete this.branchSelect
delete this.branchResetBtn
delete this.volumeDiv
delete this.lyricsHideDiv
delete this.lyricsOffsetDiv
delete this.restartCheckbox
delete this.autoplayLabel
delete this.autoplayCheckbox
......
......@@ -5,6 +5,7 @@ class Game{
this.songData = songData
this.elapsedTime = 0
this.currentCircle = -1
this.currentEvent = 0
this.updateCurrentCircle()
this.combo = 0
this.rules = new GameRules(this)
......@@ -47,13 +48,7 @@ class Game{
}
initTiming(){
// Date when the chrono is started (before the game begins)
var firstCircle
for(var i = 0; i < this.songData.circles.length; i++){
firstCircle = this.songData.circles[i]
if(firstCircle.type !== "event"){
break
}
}
var firstCircle = this.songData.circles[0]
if(this.controller.calibrationMode){
var offsetTime = 0
}else{
......@@ -238,9 +233,6 @@ class Game{
}
}
skipNote(circle){
if(circle.type === "event"){
return
}
if(circle.section){
this.resetSection()
}
......@@ -258,9 +250,6 @@ class Game{
checkPlays(){
var circles = this.songData.circles
var circle = circles[this.currentCircle]
if(circle && circle.type === "event"){
this.updateCurrentCircle()
}
if(this.controller.autoPlayEnabled){
while(circle && this.controller.autoPlay(circle)){
......@@ -469,9 +458,7 @@ class Game{
}
getLastCircle(circles){
for(var i = circles.length; i--;){
if(circles[i].type !== "event"){
return circles[i]
}
return circles[i]
}
}
whenLastCirclePlayed(){
......@@ -505,7 +492,9 @@ class Game{
var musicDuration = duration * 1000 - this.controller.offset
if(this.musicFadeOut === 0){
if(this.controller.multiplayer === 1){
p2.send("gameresults", this.getGlobalScore())
var obj = this.getGlobalScore()
obj.name = account.loggedIn ? account.displayName : null
p2.send("gameresults", obj)
}
this.musicFadeOut++
}else if(this.musicFadeOut === 1 && ms >= started + 1600){
......@@ -621,7 +610,7 @@ class Game{
var circles = this.songData.circles
do{
var circle = circles[++this.currentCircle]
}while(circle && (circle.branch && !circle.branch.active || circle.type === "event"))
}while(circle && (circle.branch && !circle.branch.active))
}
getCurrentCircle(){
return this.currentCircle
......
......@@ -202,12 +202,16 @@
var tja = new ParseTja(data, "oni", 0, 0, true, null)
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(titleLangArray.length !== 0){
songObj.title_lang = titleLangArray.join("\n")
if(titleLangAdded){
songObj.title_lang = titleLang
}
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
}
......
......@@ -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){
......
This diff is collapsed.
......@@ -34,10 +34,21 @@ 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)
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",
......@@ -92,9 +103,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 +113,15 @@ class LoadSong{
}
}
}
promises.push(this.loadSongBg(id))
this.loadSongBg(id)
promises.push(new Promise((resolve, reject) => {
var url = gameConfig.songs_baseurl + id + "/main.ogg"
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.ogg").then(sound => {
snd.musicGain.load(url).then(sound => {
songObj.sound = sound
resolve()
}, reject)
......@@ -121,84 +133,123 @@ 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
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()
}
console.error(error)
pageEvents.send("load-song-error", error)
errorMessage(new Error(error).stack)
alert(strings.errorOccured)
})
}
loadSongBg(){
return new Promise((resolve, reject) => {
var promises = []
var filenames = []
if(this.selectedSong.songBg !== null){
filenames.push("bg_song_" + this.selectedSong.songBg)
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
}
if(this.selectedSong.donBg !== null){
filenames.push("bg_don_" + this.selectedSong.donBg)
if(this.multiplayer){
filenames.push("bg_don2_" + this.selectedSong.donBg)
}
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 + ")"
}
if(this.selectedSong.songStage !== null){
filenames.push("bg_stage_" + this.selectedSong.songStage)
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(){
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)
}
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 +289,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 +318,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 +351,9 @@ class LoadSong{
})
p2.send("join", {
id: song.folder,
diff: song.difficulty
diff: song.difficulty,
name: account.loggedIn ? account.displayName : null,
don: account.loggedIn ? account.don : null
})
}else{
this.clean()
......@@ -332,6 +388,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"])
......
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
}
}
......@@ -80,10 +80,15 @@ var perf = {
allImg: 0,
load: 0
}
var defaultDon = {
body_fill: "#5fb7c1",
face_fill: "#ff5724"
}
var strings
var vectors
var settings
var scoreStorage
var account = {}
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){
......
......@@ -3,6 +3,8 @@ class P2Connection{
this.closed = true
this.lastMessages = {}
this.otherConnected = false
this.name = null
this.player = 1
this.allEvents = new Map()
this.addEventListener("message", this.message.bind(this))
this.currentHash = ""
......@@ -107,6 +109,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 = []
......@@ -115,6 +121,7 @@ class P2Connection{
this.kaAmount = 0
this.results = false
this.branch = "normal"
scoreStorage.clearP2()
break
case "gameend":
this.otherConnected = false
......@@ -128,11 +135,14 @@ class P2Connection{
this.hash("")
this.hashLock = false
}
this.name = null
this.don = 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":
......@@ -155,6 +165,45 @@ 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.name || "").toString() : ""
this.don = response.value ? (response.value.don) : null
break
case "getcrowns":
if(response.value){
var output = {}
for(var i in response.value){
if(response.value[i]){
var score = scoreStorage.get(response.value[i], false, true)
if(score){
var crowns = {}
for(var diff in score){
if(diff !== "title"){
crowns[diff] = {
crown: score[diff].crown
}
}
}
}else{
var crowns = null
}
output[response.value[i]] = crowns
}
}
p2.send("crowns", output)
}
break
case "crowns":
if(response.value){
for(var i in response.value){
scoreStorage.addP2(i, false, response.value[i], true)
}
}
break
}
}
......
......@@ -86,6 +86,9 @@ class PageEvents{
})
}
keyEvent(event){
if(!("key" in event) || event.ctrlKey && (event.key === "c" || event.key === "x" || event.key === "v")){
return
}
if(this.kbd.indexOf(event.key.toLowerCase()) !== -1){
this.lastKeyEvent = Date.now()
event.preventDefault()
......
......@@ -48,6 +48,7 @@ class ParseOsu{
lastBeatInterval: 0,
bpm: 0
}
this.events = []
this.generalInfo = this.parseGeneralInfo()
this.metadata = this.parseMetadata()
this.editor = this.parseEditor()
......@@ -249,6 +250,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(",")
......@@ -282,7 +295,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",
......@@ -309,7 +322,7 @@ class ParseOsu{
type = "drumroll"
txt = strings.note.drumroll
}
circles.push(new Circle({
pushCircle(new Circle({
id: circleID,
start: start + this.offset,
type: type,
......@@ -366,7 +379,7 @@ class ParseOsu{
break;
}
}
circles.push(new Circle({
pushCircle(new Circle({
id: circleID,
start: start + this.offset,
type: type,
......
......@@ -48,6 +48,7 @@
this.metadata = this.parseMetadata()
this.measures = []
this.beatInfo = {}
this.events = []
if(!metaOnly){
this.circles = this.parseCircles()
}
......@@ -88,6 +89,8 @@
}
}else if(name.startsWith("branchstart") && inSong){
courses[courseName].branch = true
}else if(name.startsWith("lyric") && inSong){
courses[courseName].inlineLyrics = true
}
}else if(!inSong){
......@@ -162,6 +165,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];
......@@ -275,7 +279,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) {
......@@ -293,9 +302,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)
}
}
......@@ -429,6 +441,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{
......@@ -563,6 +587,10 @@
this.scoreinit = autoscore.ScoreInit;
this.scorediff = autoscore.ScoreDiff;
}
if(this.lyrics){
var line = this.lyrics[this.lyrics.length - 1]
line.end = Math.max(ms, line.start) + 5000
}
return circles
}
}
......@@ -2,9 +2,19 @@ class Scoresheet{
constructor(controller, results, multiplayer, touchEnabled){
this.controller = controller
this.resultsObj = results
this.results = {}
this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0]
var player0 = this.player[0]
this.results = []
this.results[player0] = {}
this.rules = []
this.rules[player0] = this.controller.game.rules
if(multiplayer){
this.player.push(p2.player === 2 ? 0 : 1)
this.results[this.player[1]] = p2.results
this.rules[this.player[1]] = this.controller.syncWith.game.rules
}
for(var i in results){
this.results[i] = results[i].toString()
this.results[player0][i] = results[i] === null ? null : results[i].toString()
}
this.multiplayer = multiplayer
this.touchEnabled = touchEnabled
......@@ -39,6 +49,7 @@ class Scoresheet{
this.draw = new CanvasDraw(noSmoothing)
this.canvasCache = new CanvasCache(noSmoothing)
this.nameplateCache = new CanvasCache(noSmoothing)
this.keyboard = new Keyboard({
confirm: ["enter", "space", "esc", "don_l", "don_r"]
......@@ -208,6 +219,7 @@ class Scoresheet{
this.canvas.style.height = (winH / this.pixelRatio) + "px"
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
this.nameplateCache.resize(274, 134, ratio + 0.2)
if(!this.multiplayer){
this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio)
......@@ -233,6 +245,9 @@ class Scoresheet{
if(!this.canvasCache.canvas){
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
}
if(!this.nameplateCache.canvas){
this.nameplateCache.resize(274, 67, ratio + 0.2)
}
}
this.winW = winW
this.winH = winH
......@@ -243,7 +258,7 @@ class Scoresheet{
var frameTop = winH / 2 - 720 / 2
var frameLeft = winW / 2 - 1280 / 2
var players = this.multiplayer && p2.results ? 2 : 1
var players = this.multiplayer ? 2 : 1
var p2Offset = 298
var bgOffset = 0
......@@ -326,28 +341,21 @@ class Scoresheet{
}
var rules = this.controller.game.rules
var gaugePercent = rules.gaugePercent(this.results.gauge)
var gaugeClear = [rules.gaugeClear]
if(players === 2){
gaugeClear.push(this.controller.syncWith.game.rules.gaugeClear)
}
var failedOffset = gaugePercent >= gaugeClear[0] ? 0 : -2000
if(players === 2){
var gauge2 = this.controller.syncWith.game.rules.gaugePercent(p2.results.gauge)
if(gauge2 > gaugePercent && failedOffset !== 0 && gauge2 >= gaugeClear[1]){
var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000
if(players === 2 && failedOffset !== 0){
var p2results = this.results[this.player[1]]
if(p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)){
failedOffset = 0
}
}
if(elapsed >= 3100 + failedOffset){
for(var p = 0; p < players; p++){
ctx.save()
var results = this.results
if(p === 1){
results = p2.results
var results = this.results[p]
if(!results){
continue
}
var playerRules = p === 0 ? rules : this.controller.syncWith.game.rules
var resultGauge = playerRules.gaugePercent(results.gauge)
var clear = resultGauge >= gaugeClear[p]
var clear = this.rules[p].clearReached(results.gauge)
if(p === 1 || !this.multiplayer && clear){
ctx.translate(0, 290)
}
......@@ -410,7 +418,7 @@ class Scoresheet{
this.draw.layeredText({
ctx: ctx,
text: this.results.title,
text: this.results[this.player[0]].title,
fontSize: 40,
fontFamily: this.font,
x: 1257,
......@@ -426,9 +434,11 @@ class Scoresheet{
ctx.save()
for(var p = 0; p < players; p++){
var results = this.results
var results = this.results[p]
if(!results){
continue
}
if(p === 1){
results = p2.results
ctx.translate(0, p2Offset)
}
......@@ -450,6 +460,30 @@ class Scoresheet{
ctx.fillText(text, 395, 308)
ctx.miterLimit = 10
var defaultName = p === 0 ? strings.defaultName : strings.default2PName
if(p === this.player[0]){
var name = account.loggedIn ? account.displayName : defaultName
}else{
var name = results.name || defaultName
}
this.nameplateCache.get({
ctx: ctx,
x: 259,
y: 92,
w: 273,
h: 66,
id: p.toString() + "p" + name,
}, ctx => {
this.draw.nameplate({
ctx: ctx,
x: 3,
y: 3,
name: name,
font: this.font,
blue: p === 1
})
})
let badge_name = this.controller.getModBadge();
if(this.controller.autoPlayEnabled) {
badge_name = "badge_auto";
......@@ -585,7 +619,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)
}
}
......@@ -599,32 +633,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)
})
}
})
......@@ -637,13 +671,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){
......@@ -706,7 +739,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
......@@ -715,7 +751,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){
......@@ -731,9 +770,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()
......@@ -827,7 +868,7 @@ class Scoresheet{
if(elapsed >= 1000){
this.clean()
this.controller.songSelection(true)
this.controller.songSelection(true, this.showWarning)
}
}
......@@ -894,10 +935,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
......@@ -912,7 +957,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){
......@@ -924,5 +969,7 @@ class Scoresheet{
delete this.ctx
delete this.canvas
delete this.fadeScreen
delete this.results
delete this.rules
}
}
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,63 @@ 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
delete account.don
this.load()
pageEvents.send("logout")
return Promise.reject()
}else{
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, 3000)
}).then(() => this.sendToServer(obj, true))
}
})
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
request.setRequestHeader("X-CSRFToken", token)
request.send(JSON.stringify(obj))
return promise
})
}else{
return Promise.resolve()
}
}
eventLoop(){
if(p2.session && this.requestP2.size){
var req = []
this.requestP2.forEach(hash => {
req.push(hash)
})
this.requestP2.clear()
if(req.length){
p2.send("getcrowns", req)
}
}
}
clearP2(){
this.scoresP2 = {}
this.requestP2.clear()
this.requestedP2.clear()
}
}
......@@ -34,7 +34,11 @@ class Session{
pageEvents.send("session-start", "host")
}
})
p2.send("invite")
p2.send("invite", {
id: null,
name: account.loggedIn ? account.displayName : null,
don: account.loggedIn ? account.don : null
})
pageEvents.send("session")
}
getElement(name){
......
......@@ -50,6 +50,10 @@ class Settings{
easierBigNotes: {
type: "toggle",
default: false
},
showLyrics: {
type: "toggle",
default: true
}
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -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.multiplayer === 2 ? 560 : 35) : (view.multiplayer === 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
......
......@@ -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>
......
<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>
<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>
</div>
<div class="accountpass-div">
<input type="password" name="password"><input type="password" name="newpassword" autocomplete="new-password"><input type="password" name="newpassword2" autocomplete="new-password">
</div>
</form>
<form class="accountdel-form">
<div>
<div class="accountdel-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="accountdel-div">
<input type="password" name="password">
</div>
</form>
</div>
<div id="diag-txt"></div>
<div class="left-buttons">
<div class="logout-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="save-btn taibtn stroke-sub selected"></div>
<div class="view-end-button taibtn stroke-sub"></div>
</div>
</div>
......@@ -24,6 +24,12 @@
<div class="music-volume input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div>
<div class="lyrics-hide">
<div>Lyrics offset:</div>
<div class="lyrics-offset input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div>
</div>
<label class="change-restart-label"><input class="change-restart" type="checkbox">Restart on change</label>
<label class="autoplay-label"><input class="autoplay" type="checkbox">Auto play</label>
<div class="bottom-btns">
......
......@@ -8,6 +8,7 @@
<div id="touch-drum-img"></div>
</div>
<canvas id="canvas"></canvas>
<div id="song-lyrics"></div>
<div id="touch-buttons">
<div id="touch-full-btn"></div><div id="touch-pause-btn"></div>
</div>
......
......@@ -2,3 +2,10 @@
<div class="progress"></div>
<span class="percentage">0%</span>
</div>
<div class="view-outer loader-error-div">
<div class="view">
<div class="view-content">An error occurred, please refresh</div>
<div class="diag-txt"></div>
<span class="debug-link">Debug</span>
</div>
</div>
<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>
bcrypt==3.1.7
ffmpy==0.2.2
Flask==1.1.1
Flask-Caching==1.8.0
Flask-Session==0.3.1
Flask-WTF==0.14.3
gunicorn==20.0.4
jsonschema==3.2.0
pymongo==3.10.1
redis==3.4.1
requests==2.23.0
websockets==7.0
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_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',
'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'}
}
}
}
}
This diff is collapsed.
<!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>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
@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
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"
#!/bin/bash
./tools/get_version.sh
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment