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
}
if(image){
frames: frames,
don: don
}
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]
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(() => {
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,11 +458,9 @@ class Game{
}
getLastCircle(circles){
for(var i = circles.length; i--;){
if(circles[i].type !== "event"){
return circles[i]
}
}
}
whenLastCirclePlayed(){
var ms = this.elapsedTime
if(!this.lastCircle){
......@@ -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,51 +133,91 @@ 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
}
if(songObj.volume && songObj.volume !== 1){
this.promises.push(new Promise(resolve => setTimeout(resolve, 500)))
}
Promise.all(promises).then(() => {
Promise.all(this.promises).then(() => {
if(!this.error){
this.setupMultiplayer()
}, error => {
if(Array.isArray(error) && error[1] instanceof HTMLElement){
error = error[0] + ": " + error[1].outerHTML
}
console.error(error)
})
}
addPromise(promise, url){
this.promises.push(promise.catch(response => {
this.errorMsg(response, url)
return Promise.resolve()
}))
}
errorMsg(error, url){
if(!this.error){
if(url){
error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url
}
pageEvents.send("load-song-error", error)
errorMessage(new Error(error).stack)
alert(strings.errorOccured)
var title = this.selectedSong.title
if(title !== this.selectedSong.originalTitle){
title += " (" + this.selectedSong.originalTitle + ")"
}
assets.sounds["v_start"].stop()
setTimeout(() => {
this.clean()
new SongSelect(false, false, this.touchEnabled, null, {
name: "loadSongError",
title: title,
id: this.selectedSong.folder,
error: error
})
}, 500)
}
this.error = true
}
loadSongBg(){
return new Promise((resolve, reject) => {
var promises = []
var filenames = []
if(this.selectedSong.songBg !== null){
filenames.push("bg_song_" + this.selectedSong.songBg)
......@@ -190,15 +242,14 @@ class LoadSong{
if(this.imgScale !== 1 || force){
img.crossOrigin = "Anonymous"
}
promises.push(pageEvents.load(img).then(() => {
var url = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, filenameAb, "", force)
}))
img.src = gameConfig.assets_baseurl + "img/" + filenameAb + ".png"
}), 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
}
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 => {
if(circleObj){
if(bpm !== lastBpm || gogo !== lastGogo){
circleObj.event = true
lastBpm = bpm
lastGogo = gogo
if(circleObj){
}
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 = {}
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){
this.scoreStrings = JSON.parse(localScores)
scoreStrings = JSON.parse(localScores)
}
}catch(e){}
for(var hash in this.scoreStrings){
var scoreString = this.scoreStrings[hash]
}
for(var hash in scoreStrings){
var scoreString = scoreStrings[hash]
var songAdded = false
if(typeof scoreString === "string" && scoreString){
var diffArray = scoreString.split(";")
......@@ -37,26 +52,64 @@ 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(){
if(!account.loggedIn){
try{
localStorage.setItem("scoreStorage", JSON.stringify(this.scoreStrings))
}catch(e){}
}
}
writeString(hash){
var score = this.scores[hash]
var diffArray = []
......@@ -101,17 +154,82 @@ class ScoreStorage{
}
}
}
add(song, difficulty, scoreObject, isHash, setTitle){
getP2(song, difficulty, isHash){
if(!song){
return this.scoresP2
}else{
var hash = isHash ? song : this.titleHash(song)
if(!(hash in this.scoresP2) && !this.requestP2.has(hash) && !this.requestedP2.has(hash)){
this.requestP2.add(hash)
this.requestedP2.add(hash)
}
if(difficulty){
if(hash in this.scoresP2){
return this.scoresP2[hash][difficulty]
}
}else{
return this.scoresP2[hash]
}
}
}
add(song, difficulty, scoreObject, isHash, setTitle, saveFailed){
var hash = isHash ? song : this.titleHash(song)
if(!(hash in this.scores)){
this.scores[hash] = {}
}
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.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.
{% extends 'admin.html' %}
{% block content %}
<h1>New song</h1>
{% for message in get_flashed_messages() %}
<div class="message">{{ message }}</div>
{% endfor %}
<div class="song-form">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-field">
<span class="checkbox"><input type="checkbox" name="enabled" id="enabled"><label for="enabled"> Enabled</label></span>
</div>
<div class="form-field">
<p>Title</p>
<label for="title">Original</label>
<input type="text" id="title" value="" name="title" required>
<label for="title_ja">Japanese</label>
<input type="text" id="title_ja" value="" name="title_ja">
<label for="title_en">English</label>
<input type="text" id="title_en" value="" name="title_en">
<label for="title_cn">Chinese (Simplified)</label>
<input type="text" id="title_cn" value="" name="title_cn">
<label for="title_tw">Chinese (Traditional)</label>
<input type="text" id="title_tw" value="" name="title_tw">
<label for="title_ko">Korean</label>
<input type="text" id="title_ko" value="" name="title_ko">
</div>
<div class="form-field">
<p>Subtitle</p>
<label for="subtitle">Original</label>
<input type="text" id="subtitle" value="" name="subtitle">
<label for="subtitle_ja">Japanese</label>
<input type="text" id="subtitle_ja" value="" name="subtitle_ja">
<label for="subtitle_en">English</label>
<input type="text" id="subtitle_en" value="" name="subtitle_en">
<label for="subtitle_cn">Chinese (Simplified)</label>
<input type="text" id="subtitle_cn" value="" name="subtitle_cn">
<label for="subtitle_tw">Chinese (Traditional)</label>
<input type="text" id="subtitle_tw" value="" name="subtitle_tw">
<label for="subtitle_ko">Korean</label>
<input type="text" id="subtitle_ko" value="" name="subtitle_ko">
</div>
<div class="form-field">
<p>Courses</p>
<label for="course_easy">Easy</label>
<input type="number" id="course_easy" value="" name="course_easy" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_easy" id="branch_easy"><label for="branch_easy"> Diverge Notes</label></span>
<label for="course_normal">Normal</label>
<input type="number" id="course_normal" value="" name="course_normal" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_normal" id="branch_normal"><label for="branch_normal"> Diverge Notes</label></span>
<label for="course_hard">Hard</label>
<input type="number" id="course_hard" value="" name="course_hard" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_hard" id="branch_hard"><label for="branch_hard"> Diverge Notes</label></span>
<label for="course_oni">Oni</label>
<input type="number" id="course_oni" value="" name="course_oni" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_oni" id="branch_oni"><label for="branch_oni"> Diverge Notes</label></span>
<label for="course_ura">Ura</label>
<input type="number" id="course_ura" value="" name="course_ura" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_ura" id="branch_ura"><label for="branch_ura"> Diverge Notes</label></span>
</div>
<div class="form-field">
<p><label for="category_id">Category</label></p>
<select name="category_id" id="category_id">
<option value="0">(none)</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.title }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="type">Type</label></p>
<select name="type" id="type">
<option value="tja">TJA</option>
<option value="osu">osu!taiko</option>
</select>
</div>
<div class="form-field">
<p><label for="offset">Offset</label></p>
<input type="text" id="offset" value="" name="offset" required>
</div>
<div class="form-field">
<p><label for="skin_id">Skin</label></p>
<select name="skin_id" id="skin_id">
<option value="0">(none)</option>
{% for skin in song_skins %}
<option value="{{ skin.id }}">{{ skin.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="preview">Preview</label></p>
<input type="text" id="preview" value="" name="preview" required>
</div>
<div class="form-field">
<p><label for="volume">Volume</label></p>
<input type="text" id="volume" value="" name="volume" required>
</div>
<div class="form-field">
<p><label for="maker_id">Maker</label></p>
<select name="maker_id" id="maker_id">
<option value="0">(none)</option>
{% for maker in makers %}
<option value="{{ maker.id }}">{{ maker.name }}</option>
{% endfor %}
</select>
</div>
<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>
{% endblock %}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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