refactor: remove tjaf dependency; add local TJA parser

This commit is contained in:
2025-11-22 21:29:13 +08:00
commit 66d8ed5c6f
299 changed files with 29006 additions and 0 deletions

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

@@ -0,0 +1,160 @@
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;
}

129
public/src/css/debug.css Normal file
View File

@@ -0,0 +1,129 @@
#debug{
position: absolute;
top: 0;
left: 0;
width: 260px;
background: #fff;
border: 1px solid #333;
color: #000;
z-index: 50;
font-size: 14px;
font-family: TnT, Meiryo, sans-serif;
}
#debug .title{
position: relative;
height: 25px;
padding: 5px 0 0 5px;
box-sizing: border-box;
background: #bbb;
color: #fff;
cursor: default;
z-index: 1
}
#debug .title::before{
left: auto;
-webkit-text-stroke: 0.25em #555;
}
#debug .minimise{
position: absolute;
top: 3px;
right: 3px;
width: 19px;
height: 19px;
background: #d77;
z-index: 1;
}
#debug .content{
height: calc(100% - 25px);
overflow-y: auto;
padding: 8px;
box-sizing: border-box;
}
#debug .input-slider,
#debug .select{
display: flex;
width: 100%;
height: 30px;
margin: 5px 0 15px 0;
}
#debug .input-slider>input{
width: 70%;
height: 100%;
box-sizing: border-box;
font-size: 18px;
font-family: monospace;
padding: 2px 4px;
text-align: center;
}
#debug .input-slider>span,
#debug .select>span{
display: block;
width: 10%;
height: 100%;
opacity: 0.8;
background: #666;
color: #fff;
text-align: center;
line-height: 2em;
cursor: pointer;
}
#debug .input-slider>span:hover,
#debug .select>span:hover{
opacity: 1;
background: #333;
}
#debug .select select{
width: 90%;
height: 100%;
box-sizing: border-box;
font-size: 18px;
font-family: sans-serif;
padding: 2px 4px;
}
#debug label{
display: block;
margin: 15px 0;
}
#debug input[type="checkbox"]{
margin-right: 1em;
}
#debug .bottom-btns{
display: flex;
width: 100%;
justify-content: flex-end;
}
#debug .bottom-btns div{
width: calc(50% - 3px);
height: 30px;
opacity: 0.8;
background: #666;
color: #fff;
text-align: center;
line-height: 2em;
cursor: pointer;
}
#debug .bottom-btns div:hover{
opacity: 1;
background: #333;
}
#debug .restart-btn{
display: none;
margin-right: 3px;
}
#debug .exit-btn{
margin-left: 3px;
}
#debug .autoplay-label,
#debug .branch-hide,
#debug .lyrics-hide{
display: none;
}

143
public/src/css/game.css Normal file
View File

@@ -0,0 +1,143 @@
#game{
width: 100%;
height: 100%;
position: absolute;
overflow: hidden;
background-size: calc(100vh / 720 * 512);
background-position: center;
}
#screen.view{
background-image: none;
background-color: #000;
}
#canvas{
position: relative;
z-index: 1;
width: 100%;
height: 100%;
touch-action: none;
}
#touch-drum{
display: none;
position: absolute;
right: 0;
bottom: 0;
left: 0;
width: 50%;
height: 50%;
text-align: center;
margin: auto;
overflow: hidden;
}
#touch-drum-img{
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
background-position: top;
background-size: cover;
background-repeat: no-repeat;
}
#touch-buttons{
display: none;
position: absolute;
top: 8vh;
right: 2vh;
opacity: 0.5;
z-index: 5;
}
#touch-buttons div{
display: inline-block;
width: 12.5vmin;
height: 12.5vmin;
background-size: contain;
background-repeat: no-repeat;
}
.portrait #touch-buttons{
top: 11vh;
}
.touchp2 #touch-buttons{
top: -1.9vh;
}
.touch-visible #touch-drum,
.touch-visible #touch-buttons{
display: block;
}
.touch-visible .window{
width: 80vmin;
height: 53vmin;
}
.touch-visible #pause-menu .window button{
font-size: 5vmin;
}
.touch-visible #pause-menu .window button.selected{
color: #000;
background: #fff;
border-color: #ae7a26;
}
.touch-results #touch-pause-btn{
display: none;
}
#fade-screen{
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: transparent;
pointer-events: none;
z-index: 2;
transition: 1s background-color linear;
}
.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;
}
.pixelated #canvas,
.pixelated .donbg>div,
.pixelated #songbg>div,
.pixelated #song-stage,
.pixelated #touch-drum-img,
.pixelated #flowers1-in,
.pixelated #flowers2-in,
.pixelated #mikoshi-in,
.pixelated #tetsuo-in,
.pixelated #hana-in{
image-rendering: pixelated;
}

136
public/src/css/loader.css Normal file
View File

@@ -0,0 +1,136 @@
html,
body{
margin: 0;
width: 100%;
height: 100%;
background: #fe7839;
position: absolute;
user-select: none;
touch-action: none;
overflow: hidden;
}
#screen{
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background-color: #000;
background-position: center;
background-size: 30vh;
}
#screen.pattern-bg{
background-color: #fe7839;
}
#assets,
#browse{
display: none;
}
#loader{
width:90%;
height:10%;
border:1px solid black;
position: absolute;
top:45%;
left:5%;
background: rgba(0,0,0,0.65);
}
#loader .progress{
width:0%;
height: 100%;
background: #b52a2a;
opacity: 0.90;
}
#loader .percentage{
position:absolute;
top:0;
right:0;
bottom:0;
left:0;
display:flex;
justify-content:center;
align-items:center;
text-align:center;
font-family: sans-serif;
font-size: 5vmin;
color: white;
}
#unsupportedBrowser{
position: absolute;
top: 0;
right: 0;
left: 0;
max-height: 100%;
overflow: hidden auto;
padding: 0.5em;
background: #aef;
font-family: sans-serif;
font-size: 20px;
cursor: default;
z-index: 10;
}
#unsupportedWarn{
display: inline-block;
width: 1.5em;
height: 1.5em;
margin-right: 0.5em;
background: #39a;
color: #fff;
text-align: center;
line-height: 1.5em;
}
#unsupportedBrowser.hidden{
width: 1.5em;
}
#unsupportedBrowser.hidden *:not(#unsupportedWarn){
display: none !important;
}
#unsupportedBrowser a{
color: #02e;
cursor: pointer;
text-decoration: none;
}
#unsupportedBrowser a:hover{
text-decoration: underline;
}
#unsupportedBrowser ul{
margin: 0.25em;
}
#unsupportedDetails{
display: none;
margin: 0.5em 2.5em 0 2.5em;
border: 0.15em solid #39a;
padding: 0.25em;
cursor: auto;
user-select: text;
}
#unsupportedHide{
position: absolute;
right: 0;
top: 0;
width: 2.5em;
height: 2.5em;
text-align: center;
line-height: 2.25em;
color: #777;
text-shadow: 0.05em 0.05em #fff;
}
.view-outer.loader-error-div,
.loader-error-div .diag-txt{
display: none
}
.loader-error-div{
font-family: sans-serif;
}
.loader-error-div .debug-link{
color: #00f;
text-decoration: underline;
cursor: pointer;
float: right;
}
.loader-error-div .diag-txt textarea,
.loader-error-div .diag-txt iframe{
height: 10em;
}

View File

@@ -0,0 +1,40 @@
#load-song{
width: 100%;
height: 100%;
}
#loading-song{
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 20vmax;
height: 15vmax;
background: rgba(0, 0, 0, 0.75);
border-radius: 5px;
border: 3px solid white;
color: #fff;
z-index: 1;
}
#loading-don{
width: 10vmax;
height: calc(10vmax / 120 * 115);
background-size: contain;
background-repeat: no-repeat;
}
.loading-text{
position: relative;
font-size: 1.5vmax;
text-align: center;
z-index: 1;
}
#p2-cancel-button{
display: none;
position: absolute;
bottom: -55px;
}

128
public/src/css/main.css Normal file
View File

@@ -0,0 +1,128 @@
.window{
width: 60vmin;
height: 23vmin;
padding: 3vmin;
color: black;
background: rgba(255, 220, 47, 0.95);
border: .5vmin outset #f4ae00;
box-shadow: 2px 2px 10px black;
margin: auto;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.stroke-sub::before{
content: attr(alt);
position: absolute;
-webkit-text-stroke: 0.25em #000;
left: 0;
z-index: -1;
}
#session-invite{
width: 100%;
height: 1.9em;
font-family: sans-serif;
font-size: 2em;
background: #fff;
border: 1px solid #a9a9a9;
padding: 0.3em;
margin: 0.3em 0;
box-sizing: border-box;
text-align: center;
user-select: all;
cursor: text;
overflow: hidden;
}
@keyframes bgscroll{
from{
background-position: 50% top;
}
to{
background-position: calc(50% - 100vh / 720 * 512) top;
}
}
#song-select{
width: 100%;
height: 100%;
background-size: calc(100vh / 720 * 512);
background-repeat: repeat no-repeat;
animation: bgscroll 16s infinite linear;
white-space: nowrap;
transition: background-image 0.5s;
}
#song-select.unfocused{
animation-play-state: paused;
}
#song-sel-canvas{
position: absolute;
right: 0;
left: 0;
margin: auto;
}
#song-select #touch-full-btn{
display: none;
position: absolute;
top: 0;
right: 0;
width: 12.5vmin;
height: 12.5vmin;
opacity: 0.5;
background-size: contain;
background-repeat: no-repeat;
}
#song-sel-selectable{
position: absolute;
opacity: 1;
text-align: center;
word-break: break-all;
white-space: pre-wrap;
user-select: all;
cursor: text;
color: transparent;
}
#song-sel-selectable:focus{
background: #ffdb2c;
color: #000;
}
#song-sel-selectable .stroke-sub{
position: absolute;
z-index: 1;
}
#song-sel-selectable .stroke-sub::before{
-webkit-text-stroke: 0;
}
#song-sel-selectable:focus .stroke-sub::before{
-webkit-text-stroke: 0.25em #fff;
}
#version {
position: fixed;
z-index: 1000;
font-size: 2vh;
position: absolute;
bottom: 1vh;
right: 1vh;
opacity: 0.7;
font-family: TnT, Meiryo, sans-serif;
pointer-events: none;
}
#version:hover{
opacity: 1;
}
#version-link{
color: #FFFFFF;
text-decoration: none;
pointer-events: none;
white-space: pre-line;
word-break: break-word;
}
.version-hide{
pointer-events: none;
}

287
public/src/css/search.css Normal file
View File

@@ -0,0 +1,287 @@
#song-search-container {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 2;
display: flex;
justify-content: center;
align-items: center;
font-size: 21px;
}
#song-search {
position: relative;
display: flex;
flex-direction: column;
width: min(100%, 60em);
height: 80%;
border-radius: 0.8em;
border: 0.35em solid #8C0C42;
color: #fff;
padding: 1em 1em 0 1em;
z-index: 1;
box-sizing: border-box;
background-size: auto, 3.12em;
background-position: 0%, -2%;
}
#song-search-container.touch-enabled{
font-size: calc(3 * var(--vmin, 1vmin));
}
@media (max-width: 950px){
#song-search-container:not(.touch-enabled){
font-size: calc(3 * var(--vmin, 1vmin));
}
}
@media (max-height: 650px){
#song-search-container:not(.touch-enabled){
font-size: calc(2 * var(--vmin, 1vmin));
}
}
#song-search-input {
width: 100%;
font-size: 1.8em;
padding: 0.5em 0.7em;
border-radius: 0.2em;
border: 0.13em black solid;
font-family: inherit;
box-sizing: border-box;
-webkit-box-sizing:border-box;
-moz-box-sizing: border-box;
outline: none;
}
#song-search-input:focus {
border-color: #fff923;
}
#song-search-results {
margin-top: 0.5em;
overflow-y: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
scroll-behavior: smooth;
}
#song-search-results::-webkit-scrollbar {
display: none;
}
.song-search-result {
display: flex;
height: 3.2em;
margin: 0.2em;
padding: 0.7em;
flex-direction: row;
text-align: center;
align-items: center;
justify-content: center;
border: 0.3em black solid;
position: relative;
--course-width: min(3em, calc(7 * var(--vmin, 1vmin)));
content-visibility: auto;
contain-intrinsic-size: 1px 3.2em;
}
.song-search-result::before {
display: block;
position: absolute;
content: '';
height: 100%;
width: 100%;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
border: 0.4em solid;
}
.song-search-result:last-of-type {
margin-bottom: 1em;
}
.song-search-result-info {
font-size: 1.2em;
padding: 0.3em 0.3em 0.3em 0.5em;
text-align: left;
z-index: 0;
position: relative;
white-space: nowrap;
overflow-x: hidden;
width: calc(100% - (var(--course-width) + 0.4em) * 5 - 0.6em);
}
.song-search-result-info .highlighted-text {
color: #faff00;
}
.song-search-result-title,
.song-search-result-subtitle {
display: inline-block;
transform-origin: left;
}
.song-search-result-subtitle {
font-size: 0.8em;
margin-top: 0.5em;
}
.song-search-result-title::before,
.song-search-result-subtitle::before {
content: attr(alt);
position: absolute;
z-index: -1;
-webkit-text-stroke-width: 0.4em;
}
.song-search-result-course {
width: var(--course-width);
height: 100%;
margin: 0.2em;
font-size: 1.2em;
border-radius: 0.3em;
position: relative;
z-index: 1;
}
.song-search-result-hidden {
visibility: hidden;
}
.song-search-result:hover {
border-color: #fff923;
cursor: pointer;
}
.song-search-result-active {
border-color: #fff923;
}
.song-search-result-course::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.5;
z-index: -1;
background-size: 4.8em;
border-radius: 0.3em;
}
.song-search-result-stars {
bottom: 0;
background: rgb(0 0 0 / 47%);
position: absolute;
width: 100%;
padding: 0.1em 0;
border-radius: 0 0 0.3em 0.3em;
overflow: hidden;
}
.song-search-result-easy {
background-color: #D13215;
}
.song-search-result-easy::before {
background-position-x: center;
background-position-y: -0.6em;
}
.song-search-result-normal {
background-color: #799C22;
}
.song-search-result-normal::before {
background-position-x: center;
background-position-y: -5.1em;
}
.song-search-result-hard {
background-color: #31799B;
}
.song-search-result-hard::before {
background-position-x: center;
background-position-y: -9.1em;
}
.song-search-result-oni {
background-color: #AF2C7F;
}
.song-search-result-oni::before {
background-position-x: center;
background-position-y: -13.1em;
}
.song-search-result-ura {
background-color: #604AD5;
}
.song-search-result-ura::before {
background-position-x: center;
background-position-y: -17.2em;
}
.song-search-result-crown {
background-size: 1.4em;
background-position-x: center;
background-repeat: repeat-y;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
width: 1.4em;
height: 1.3em;
margin-bottom: 1.2em;
}
.song-search-result-gold {
background-position-y: 59%;
}
.song-search-result-silver {
background-position-y: 29%;
}
.song-search-result-noclear {
background-position-y: -1%;
}
#song-search-tip {
font-size: 1em;
margin-top: 1em;
text-align: center;
background-repeat: no-repeat;
background-position: top;
background-size: 10em;
background-color: #00000087;
border-radius: 0.5em;
padding: 1em;
}
#song-search-close {
position: absolute;
right: -0.5em;
top: -0.8em;
font-size: 2em;
font-family: TnT;
cursor: pointer;
}
#song-search-close:hover::before{
-webkit-text-stroke: 0.25em #fff923;
}
.song-search-tip-error {
height: 8em;
}

364
public/src/css/songbg.css Normal file
View File

@@ -0,0 +1,364 @@
#songbg,
#songbg>*,
.donbg,
.donbg *,
#song-stage{
position: absolute;
right: 0;
left: 0;
width: 100%;
max-width: calc(100vh / 9 * 32);
}
#songbg{
height: 50.1%;
}
#songbg>*{
top: 0;
}
#songbg,
#songbg>*{
background-size: cover;
background-position: center;
background-repeat: no-repeat;
bottom: 0;
}
#song-stage{
height: calc(44 / 720 * 100vh);
background-position: center bottom;
background-repeat-y: no-repeat;
background-size: auto 100%;
bottom: 0;
}
.portrait #songbg{
height: 63.4%;
max-height: calc(50% + 24vw);
}
.touchp2 #songbg{
height: calc(50% - 5.9vw);
min-height: 39.5%;
}
.multiplayer.portrait #songbg{
height: calc(50% - 37vw);
min-height: calc(29% + 1px);
}
.multiplayer:not(.touchp2):not(.portrait) #songbg,
.multiplayer:not(.touchp2):not(.portrait) #song-stage{
display: none;
}
.game-paused *{
animation-play-state: paused !important;
}
@keyframes songbg-strobe{
0%{opacity: 1}
25%{opacity: 0}
50%{opacity: 0.66}
75%{opacity: 0}
}
@keyframes songbg-pulse{
0%{opacity: 1}
50%{opacity: 0}
}
.songbg-1 #layer2,
.songbg-2 #layer2,
.songbg-3 #layer2{
animation: 0.4s linear songbg-strobe infinite;
mix-blend-mode: difference;
}
.songbg-4 #layer2{
animation: 0.4s linear songbg-pulse infinite;
mix-blend-mode: difference;
}
.songbg-5 #layer2{
animation: 1s linear songbg-pulse infinite;
mix-blend-mode: color-dodge;
}
.songbg-strobe #layer2{
animation: 0.4s linear songbg-strobe infinite;
}
.songbg-pulse #layer2{
animation: 0.4s linear songbg-pulse infinite;
}
.songbg-slowfade #layer2{
animation: 2s cubic-bezier(0.68, -0.55, 0.27, 1.55) songbg-pulse infinite;
}
.touch-visible #layer2{
display: none;
background-image: none;
animation: none;
}
.donbg{
top: 0;
height: calc(50% - 13.7vw);
min-height: 25.6%;
}
.multiplayer .donbg{
min-height: 27.2%;
}
.portrait .donbg{
height: calc(50% - 48.9vw);
min-height: 22.5%;
}
.donbg *{
top: 0;
bottom: 0;
background-repeat-y: no-repeat;
}
.donbg.donbg-bottom{
top: auto;
bottom: 0;
}
.portrait .donbg.donbg-bottom {
top: calc(50% + -1vw);
bottom: auto;
}
@keyframes donbg-scroll{
from{background-position-x: 0}
to{background-position-x: calc(var(--h) / var(--sh1) * var(--sw) * -1)}
}
@keyframes donbg-scroll1{
from{background-position-x: 0}
to{background-position-x: calc(var(--h) / var(--sh1) * var(--sw1) * -1)}
}
@keyframes donbg-scroll2{
from{background-position-x: 0}
to{background-position-x: calc(var(--h) / var(--sh1) * var(--sw2) * -1)}
}
@keyframes donbg-raise{
from{background-position-y: 0}
to{background-position-y: var(--raised)}
}
@keyframes donbg-anim3{
0%{background-position-y: 0}
13%{background-position-y: var(--raised)}
15%{background-position-y: var(--raised)}
45%{background-position-y: 0}
50%{background-position-y: 0}
65%{background-position-y: calc(var(--raised) / 2)}
80%{background-position-y: 0}
}
@keyframes donbg-anim5{
0%{background-position-y: 0}
13%{background-position-y: var(--raised)}
17%{background-position-y: var(--raised)}
30%{background-position-y: 0}
}
.donlayer1{
animation: 5s linear donbg-scroll infinite;
background-size: auto 100%;
}
.donlayer2{
background-size: auto calc(var(--sh2) / var(--sh1) * 100%);
--raised: calc((var(--sh2) - var(--sh1)) / var(--sh2) * var(--h) * -1);
}
.donlayer3{
background-color: #000;
opacity: 0;
transition: 0.15s opacity linear;
}
.donbg-dark .donlayer3{
opacity: 0.5;
}
.donbg-1 .donlayer2,
.donbg-2 .donlayer2,
.donbg-4 .donlayer2,
.donbg-6 .donlayer2,
.donbg-raise .donlayer2{
animation: 5s linear donbg-scroll infinite, 1s linear donbg-raise infinite alternate;
}
.donbg-3 .donlayer2,
.donbg-anim3 .donlayer2{
animation: 3.4s linear donbg-scroll infinite, 1.8s linear donbg-anim3 infinite;
}
.donbg-5 .donlayer2,
.donbg-anim5 .donlayer2{
animation: 2.7s linear donbg-scroll infinite, 2.2s linear donbg-anim5 infinite;
}
.donbg-equalscroll .donlayer1{
animation: 5.3s linear donbg-scroll1 infinite;
}
.donbg-equalscroll .donlayer2{
animation: 5.3s linear donbg-scroll2 infinite;
}
.donbg-fastscroll .donlayer1{
animation: 2s linear donbg-scroll1 infinite;
}
.donbg-fastscroll .donlayer2{
animation: 1s linear donbg-scroll2 infinite;
}
#tetsuohana{
position: absolute;
right: calc(-12px * var(--scale));
left: calc(-12px * var(--scale));
margin: auto;
z-index: 1;
overflow: hidden;
pointer-events: none;
top: calc(50% - 15px * var(--scale));
width: calc(1304px * var(--scale));
height: calc(375px * var(--scale));
--frame: 0;
--low: calc(36px * var(--scale));
}
#tetsuo,
#hana{
position: absolute;
top: 0;
width: calc(292px * var(--scale));
height: calc(425px * var(--scale));
transform: translateY(calc(360px * var(--scale)));
}
#tetsuo-in,
#hana-in{
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-size: calc(292px * var(--scale) * 2);
background-position-y: calc(-425px * var(--frame) * var(--scale));
}
#tetsuo{
left: calc(173px * var(--scale));
}
#hana{
right: calc(178px * var(--scale));
}
#hana-in{
background-position-x: calc(-292px * var(--scale));
}
#mikoshi{
position: absolute;
top: 0;
left: calc(390px * var(--scale));
width: calc(553px * var(--scale));
height: calc(416px * var(--scale));
transform: translateY(calc(461px * var(--scale)));
}
#mikoshi-in{
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-size: contain;
}
#flowers1,
#flowers2{
position: absolute;
top: calc(218px * var(--scale));
width: calc(483px * var(--scale));
height: calc(159px * var(--scale));
transform: translateY(calc(243px * var(--scale))) scaleX(var(--flip));
}
#flowers1-in,
#flowers2-in{
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-size: calc(483px * var(--scale));
background-position-y: calc(-159px * var(--frame) * var(--scale));
}
#flowers1{
left: 0;
--flip: 1;
}
#flowers2{
right: calc(4px * var(--scale));
--flip: -1;
}
#tetsuohana.fadein,
#tetsuohana.dance,
#tetsuohana.dance2,
#tetsuohana.failed{
height: calc(461px * var(--scale));
}
#tetsuohana.fadein #tetsuo,
#tetsuohana.fadein #hana{
transition: 0.5s transform cubic-bezier(0.2, 0.6, 0.4, 1.2);
transform: translateY(var(--low));
}
@keyframes tetsuohana-dance{
0%{transform: translateY(var(--low))}
50%{transform: translateY(0)}
100%{transform: translateY(0)}
}
@keyframes tetsuohana-failed1{
0%{transform: translateY(calc(10px * var(--scale)))}
50%{transform: translateY(0)}
100%{transform: translateY(0)}
}
@keyframes tetsuohana-failed2{
0%{transform: translateY(0)}
49%{transform: translateY(0)}
50%{transform: translateY(calc(5px * var(--scale)))}
100%{transform: translateY(calc(15px * var(--scale)))}
}
@keyframes tetsuohana-flowers{
0%{background-position-y: 0}
50%{background-position-y: calc(-159px * var(--scale))}
100%{background-position-y: calc(-318px * var(--scale))}
}
@keyframes tetsuohana-mikoshi{
0%{transform: translateY(calc(425px * var(--scale)))}
100%{transform: translateY(0)}
}
#tetsuohana.dance #tetsuo,
#tetsuohana.dance #hana,
#tetsuohana.dance2 #tetsuo,
#tetsuohana.dance2 #hana{
--frame: 1;
transform: translateY(var(--low));
animation: 0.5s ease-out tetsuohana-dance infinite forwards;
}
#tetsuohana.dance #tetsuo-in,
#tetsuohana.dance #hana-in,
#tetsuohana.dance2 #tetsuo-in,
#tetsuohana.dance2 #hana-in{
transform: translateY(0);
animation: 0.5s ease-out tetsuohana-dance infinite forwards reverse;
}
#tetsuohana.dance #flowers1,
#tetsuohana.dance #flowers2{
transform: translateY(0) scaleX(var(--flip));
transition: 0.34s transform ease-out;
}
#tetsuohana.dance2 #flowers1,
#tetsuohana.dance2 #flowers2{
transform: translateY(0) scaleX(var(--flip));
}
#tetsuohana.dance #flowers1-in,
#tetsuohana.dance #flowers2-in{
animation: 0.25s 0.4s step-end tetsuohana-flowers both;
}
#tetsuohana.dance2 #flowers1-in,
#tetsuohana.dance2 #flowers2-in{
background-position-y: calc(-318px * var(--scale));
}
#tetsuohana.dance #mikoshi-out{
animation: 0.4s 0.4s ease-out tetsuohana-mikoshi both;
}
#tetsuohana.dance #mikoshi{
transform: translateY(var(--low));
animation: 0.5s 0.8s ease-out tetsuohana-dance infinite forwards;
}
#tetsuohana.dance #mikoshi-in{
transform: translateY(0);
animation: 0.5s 0.8s ease-out tetsuohana-dance infinite forwards reverse;
}
#tetsuohana.dance2 #mikoshi{
transform: translateY(var(--low));
animation: 0.5s -0.2s ease-out tetsuohana-dance infinite forwards;
}
#tetsuohana.dance2 #mikoshi-in{
transform: translateY(0);
animation: 0.5s -0.2s ease-out tetsuohana-dance infinite forwards reverse;
}
#tetsuohana.failed #tetsuo,
#tetsuohana.failed #hana{
--frame: 2;
top: calc(26px * var(--scale));
transform: translateY(calc(46px * var(--scale)));
animation: 1.25s ease-out tetsuohana-failed1 forwards infinite;
}
#tetsuohana.failed #tetsuo-in,
#tetsuohana.failed #hana-in{
transform: translateY(0);
animation: 1.25s ease-in tetsuohana-failed2 forwards infinite;
}

View File

@@ -0,0 +1,57 @@
@keyframes toggleFade{
40%{
opacity: 1;
}
70%{
opacity: 0.2;
}
}
#title-screen{
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: #1389f0;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
cursor: pointer;
}
#logo{
width: 100vmin;
height: calc(100vmin / 1170 * 390);
}
.click-to-continue{
position: absolute;
bottom: 15%;
color: #fff;
font-size: 8vmin;
text-align: center;
z-index: 1;
animation: toggleFade 2s infinite ease-in-out;
}
.click-to-continue::before{
-webkit-text-stroke: 0.25em #f00;
filter: blur(0.3vmin);
}
#title-disclaimer {
text-align: center;
position: absolute;
bottom: 5%;
color: #fff;
z-index: 1;
}
#title-disclaimer span {
color: #fff;
font-size: 2vmin;
text-align: center;
display: block;
}
#title-disclaimer span:before {
left: initial;
filter: blur(0.1vmin);
}

472
public/src/css/view.css Normal file
View File

@@ -0,0 +1,472 @@
.view-outer{
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: absolute;
width: 100%;
height: 100%;
background-position: center;
}
.view{
background: rgb(246, 234, 212);
color: black;
border: 0.25em black solid;
border-radius: 0.5em;
width: 800px;
max-width: 40em;
padding: 1em;
margin: 1em;
font-size: 21px;
position: relative;
}
@media (max-width: 950px){
.view-outer:not(.touch-enabled) .view{
font-size: 3vmin;
}
}
@media (max-height: 650px){
.view-outer:not(.touch-enabled) .view{
font-size: 3vmin;
}
}
.touch-enabled .view{
font-size: 3vmin;
}
.view-title{
z-index: 1;
position: absolute;
color: white;
top: -0.7em;
font-size: 1.65em;
}
.view-content{
margin: 0.7em 0;
overflow-y: auto;
max-height: calc(100vh - 14em);
}
kbd{
font-family: inherit;
padding: 0.1em 0.6em;
border: 1px solid #ccc;
font-size: 0.6em;
background-color: #f7f7f7;
color: #333;
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;
border-radius: 3px;
display: inline-block;
text-shadow: 0 1px 0 #fff;
line-height: 1.4;
white-space: nowrap;
}
.key-join{
font-size: 0.6em;
}
.taibtn{
display: inline-block;
background: #f6ead4;
padding: 0.4em 0.4em;
border-radius: 0.5em;
border: 0.1em rgba(218, 205, 178, 1) solid;
cursor: pointer;
font-size: 1.4em;
box-sizing: border-box;
color: #555;
text-align: center;
}
.view-end-button{
float: right;
padding: 0.4em 1.5em;
font-weight: bold;
border-color: #000;
color: #000;
z-index: 1;
}
.taibtn:hover,
.taibtn.selected,
.view-end-button:hover,
.view-end-button.selected{
position: relative;
color: #fff;
background: #ffb547;
border-color: #fff;
}
.taibtn::before,
.view-end-button::before{
display: none;
}
.taibtn:hover::before,
.taibtn.selected::before,
.view-end-button:hover::before,
.view-end-button.selected::before{
display: block
}
.taibtn::before{
padding-left: inherit;
}
.left-buttons{
float: left;
display: flex;
}
.left-buttons .taibtn{
margin-right: 0.4em;
}
.center-buttons{
margin: 1.5em 0;
}
.account-view .center-buttons{
margin: 0.3em 0;
}
.center-buttons>div{
text-align: center;
margin: 0.2em 0;
}
.center-buttons .taibtn{
margin: 0 0.2em;
}
.diag-txt textarea,
.diag-txt iframe{
width: 100%;
height: 5em;
font-size: inherit;
resize: none;
word-break: break-all;
margin-bottom: 1em;
background: #fff;
border: 1px solid #a9a9a9;
user-select: all;
box-sizing: border-box;
}
.text-warn{
color: #d00;
}
.link-btn a{
color: inherit;
text-decoration: none;
pointer-events: none;
}
.nowrap{
white-space: nowrap;
}
@keyframes border-pulse{
0%{border-color: #ff0}
50%{border-color: rgba(255, 255, 0, 0)}
100%{border-color: #ff0}
}
@keyframes border-pulse2{
0%{border-color: #e29e06}
50%{border-color: rgba(226, 158, 6, 0)}
100%{border-color: #e29e06}
}
.settings-outer{
background-size: 50vh;
}
.setting-box{
display: flex;
height: 2em;
margin-top: 1.2em;
border: 0.25em solid #000;
border-radius: 0.5em;
padding: 0.3em;
outline: none;
color: #000;
cursor: pointer;
}
.setting-box:first-child{
margin-top: 0;
}
.view-content:not(:hover) .setting-box.selected,
.setting-box:hover{
background: #ffb547;
animation: 2s linear border-pulse infinite;
}
.bold-fonts .setting-box{
line-height: 1em;
}
.setting-name{
position: relative;
width: 50%;
padding: 0.3em;
font-size: 1.3em;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
}
.view-content:not(:hover) .setting-box.selected .setting-name,
.setting-box:hover .setting-name,
.setting-box:hover #gamepad-value{
color: #fff;
z-index: 0;
}
.setting-name::before{
padding-left: 0.3em;
}
.setting-name::after{
content: "";
display: block;
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 100%;
background-image: linear-gradient(90deg, rgba(246, 234, 212, 0), #f6ead4 90%);
}
.view-content:not(:hover) .setting-box.selected .setting-name::after,
.setting-box:hover .setting-name::after{
background-image: linear-gradient(90deg, rgba(255, 181, 71, 0), #ffb547 90%);
}
.setting-value{
display: flex;
background: #fff;
width: 50%;
border-radius: 0.2em;
padding: 0.5em;
box-sizing: border-box;
overflow: hidden;
white-space: nowrap;
}
.setting-value.selected{
width: calc(50% + 0.2em);
margin: -0.1em;
border: 0.2em solid #e29e06;
padding: 0.4em;
animation: 2s linear border-pulse2 infinite;
}
.setting-value>div{
padding: 0 0.4em;
overflow: hidden;
text-overflow: ellipsis;
}
.shadow-outer{
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
#settings-gamepad,
#settings-latency,
#customsongs-error{
display: none;
}
#settings-gamepad .view{
width: 29.9em;
max-width: 100vw;
}
#settings-gamepad .setting-box{
height: auto;
overflow: hidden;
}
#gamepad-bg,
#gamepad-buttons{
background-size: 20.53em;
}
#gamepad-bg{
position: relative;
width: 20.53em;
height: 11.83em;
max-height: none;
background-repeat: no-repeat;
text-align: center;
font-size: 1.4em;
cursor: pointer;
}
#gamepad-buttons{
position: absolute;
left: 5.26em;
top: 4.48em;
width: 10.52em;
height: 4.89em;
background-position: 0 -11.87em;
background-repeat: no-repeat;
pointer-events: none;
}
#gamepad-value{
position: relative;
margin-top: 1em;
}
#gamepad-value::before{
left: auto;
}
#settings-latency .view{
width: 30em;
}
.setting-value{
position: relative;
}
.setting-value:not(.selected) .latency-buttons{
display: none;
}
.setting-value .latency-buttons{
position: absolute;
top: 0;
right: 0;
bottom: 0;
padding: 0;
}
.latency-buttons span{
display: inline-block;
width: 2em;
height: 100%;
text-align: center;
background-color: #c3862a;
color: #fff;
line-height: 2em;
outline: none;
}
.latency-buttons span:hover,
.latency-buttons span:active{
background-color: #946013;
}
.left-buttons .taibtn,
.center-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;
cursor: pointer;
}
.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;
}
#customsongs-error .view,
#dropzone .view{
width: 600px;
}
#dropzone{
pointer-events: none;
opacity: 0;
transition: opacity 0.5s;
}
#dropzone .view-content{
font-size: 2em;
text-align: center;
}
#dropzone.dragover{
opacity: 1;
}
.plugin-browse-button{
position: relative;
overflow: hidden;
}
#plugin-browse{
position: absolute;
font-size: inherit;
top: -0.1em;
left: -0.1em;
right: -0.1em;
bottom: -0.1em;
border-radius: 0.5em;
opacity: 0;
cursor: pointer;
}
#plugin-browse::-webkit-file-upload-button{
cursor: pointer;
}

253
public/src/js/about.js Normal file
View File

@@ -0,0 +1,253 @@
class About{
constructor(...args){
this.init(...args)
}
init(touchEnabled){
this.touchEnabled = touchEnabled
loader.changePage("about", true)
cancelTouch = false
this.endButton = this.getElement("view-end-button")
this.diagTxt = this.getElement("diag-txt")
this.version = document.getElementById("version-link").href
this.tutorialOuter = this.getElement("view-outer")
if(touchEnabled){
this.tutorialOuter.classList.add("touch-enabled")
}
this.linkIssues = document.getElementById("link-issues")
this.linkEmail = document.getElementById("link-email")
var tutorialTitle = this.getElement("view-title")
tutorialTitle.innerText = strings.aboutSimulator
tutorialTitle.setAttribute("alt", strings.aboutSimulator)
var tutorialContent = this.getElement("view-content")
strings.about.bugReporting.forEach(string => {
tutorialContent.appendChild(document.createTextNode(string))
tutorialContent.appendChild(document.createElement("br"))
})
var span = document.createElement("span")
span.classList.add("text-warn")
span.innerText = strings.about.diagnosticWarning
tutorialContent.appendChild(span)
this.endButton.innerText = strings.tutorial.ok
this.endButton.setAttribute("alt", strings.tutorial.ok)
this.items = []
this.getLink(this.linkIssues).innerText = strings.about.issues
this.linkIssues.setAttribute("alt", strings.about.issues)
var versionUrl = gameConfig._version.url
this.getLink(this.linkIssues).href = versionUrl + "issues"
this.items.push(this.linkIssues)
var contactEmail = gameConfig.email
this.hasEmail = typeof contactEmail === "string"
if(this.hasEmail){
this.linkEmail.setAttribute("alt", contactEmail)
this.getLink(this.linkEmail).href = "mailto:" + contactEmail
this.getLink(this.linkEmail).innerText = contactEmail
this.items.push(this.linkEmail)
}else{
this.linkEmail.parentNode.removeChild(this.linkEmail)
}
pageEvents.add(this.linkIssues, ["click", "touchend"], this.linkButton.bind(this))
if(this.hasEmail){
pageEvents.add(this.linkEmail, ["click", "touchend"], this.linkButton.bind(this))
}
pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this))
this.items.push(this.endButton)
this.selected = this.items.length - 1
this.keyboard = new Keyboard({
confirm: ["enter", "space", "don_l", "don_r"],
previous: ["left", "up", "ka_l"],
next: ["right", "down", "ka_r"],
back: ["escape"]
}, this.keyPressed.bind(this))
this.gamepad = new Gamepad({
"confirm": ["b", "ls", "rs"],
"previous": ["u", "l", "lb", "lt", "lsu", "lsl"],
"next": ["d", "r", "rb", "rt", "lsd", "lsr"],
"back": ["start", "a"]
}, this.keyPressed.bind(this))
pageEvents.send("about", this.addDiag())
}
getElement(name){
return loader.screen.getElementsByClassName(name)[0]
}
keyPressed(pressed, name){
if(!pressed){
return
}
var selected = this.items[this.selected]
if(name === "confirm"){
if(selected === this.endButton){
this.onEnd()
}else{
this.getLink(selected).click()
pageEvents.send("about-link", selected)
assets.sounds["se_don"].play()
}
}else if(name === "previous" || name === "next"){
selected.classList.remove("selected")
this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1))
this.items[this.selected].classList.add("selected")
assets.sounds["se_ka"].play()
}else if(name === "back"){
this.onEnd()
}
}
mod(length, index){
return ((index % length) + length) % length
}
onEnd(event){
var touched = false
if(event){
if(event.type === "touchstart"){
event.preventDefault()
touched = true
}else if(event.which !== 1){
return
}
}
this.clean()
assets.sounds["se_don"].play()
localStorage.setItem("tutorial", "true")
setTimeout(() => {
new SongSelect("about", false, touched)
}, 500)
}
addDiag(){
var diag = []
diag.push("```")
diag.push("Taiko-Web version: " + this.version)
diag.push("URL: " + location.href)
diag.push("User agent: " + navigator.userAgent)
diag.push("Screen size: " + innerWidth + "x" + innerHeight + ", outer: " + outerWidth + "x" + outerHeight + ", ratio: " + (window.devicePixelRatio || 1).toFixed(2))
if(this.touchEnabled){
diag.push("Touch enabled: true")
}
if(!fullScreenSupported){
diag.push("Full screen supported: false")
}
diag.push("Blur performance: " + perf.blur + "ms, all images: " + perf.allImg + "ms")
diag.push("Page load: " + (perf.load / 1000).toFixed(1) + "s")
if("getGamepads" in navigator){
var gamepads = navigator.getGamepads()
for(var i = 0; i < gamepads.length; i++){
if(gamepads[i]){
var gamepadDiag = []
gamepadDiag.push(gamepads[i].id)
gamepadDiag.push("buttons: " + gamepads[i].buttons.length)
gamepadDiag.push("axes: " + gamepads[i].axes.length)
diag.push("Gamepad #" + (i + 1) + ": " + gamepadDiag.join(", "))
}
}
}
var userLangStr = " (none)"
if("languages" in navigator){
var userLang = navigator.languages.slice()
if(userLang[0] !== navigator.language){
userLang.unshift(navigator.language)
}
if(userLang.length !== 0){
userLangStr = " (" + userLang.join(", ") + ")"
}
}
diag.push("Language: " + strings.id + userLangStr)
var latency = settings.getItem("latency")
diag.push("Audio Latency: " + (latency.audio > 0 ? "+" : "") + latency.audio.toString() + "ms, Video Latency: " + (latency.video > 0 ? "+" : "") + latency.video.toString() + "ms")
var pluginList = plugins.allPlugins.map(pluginLoader => {
return (pluginLoader.plugin.module && pluginLoader.plugin.module.name || pluginLoader.name) + (pluginLoader.plugin.started ? " (started)" : "")
})
diag.push("Plugins: " + pluginList.join(", "))
var errorObj = {}
if(localStorage["lastError"]){
try{
errorObj = JSON.parse(localStorage["lastError"])
}catch(e){}
}
if(errorObj.timestamp && errorObj.stack){
if(errorObj.timestamp + 1000 * 60 * 60 * 24 > Date.now()){
diag.push("Last error: " + errorObj.stack)
diag.push("Error date: " + new Date(errorObj.timestamp).toGMTString())
}else{
localStorage.removeItem("lastError")
}
}
diag.push("```")
var diag = diag.join("\n")
if(navigator.userAgent.indexOf("Android") >= 0){
var iframe = document.createElement("iframe")
this.diagTxt.appendChild(iframe)
var body = iframe.contentWindow.document.body
body.innerText = diag
body.setAttribute("style", `
font-family: monospace;
margin: 2px 0 0 2px;
white-space: pre-wrap;
word-break: break-all;
cursor: text;
`)
body.setAttribute("onblur", `
getSelection().removeAllRanges()
`)
}else{
this.textarea = document.createElement("textarea")
this.textarea.readOnly = true
this.textarea.value = diag
this.diagTxt.appendChild(this.textarea)
if(!this.touchEnabled){
pageEvents.add(this.textarea, "focus", () => {
this.textarea.select()
})
pageEvents.add(this.textarea, "blur", () => {
getSelection().removeAllRanges()
})
}
}
var issueBody = strings.about.issueTemplate + "\n\n\n\n" + diag
if(this.hasEmail){
this.getLink(this.linkEmail).href += "?body=" + encodeURIComponent(issueBody.replace(/\n/g, "<br>\r\n"))
}
return diag
}
getLink(target){
return target.getElementsByTagName("a")[0]
}
linkButton(event){
if(event.target === event.currentTarget){
this.getLink(event.currentTarget).click()
pageEvents.send("about-link", event.currentTarget)
assets.sounds["se_don"].play()
}
}
clean(){
cancelTouch = true
this.keyboard.clean()
this.gamepad.clean()
pageEvents.remove(this.linkIssues, ["click", "touchend"])
if(this.hasEmail){
pageEvents.remove(this.linkEmail, ["click", "touchend"])
}
pageEvents.remove(this.endButton, ["mousedown", "touchstart"])
if(this.textarea){
pageEvents.remove(this.textarea, ["focus", "blur"])
}
pageEvents.keyRemove(this, "all")
delete this.endButton
delete this.diagTxt
delete this.version
delete this.tutorialOuter
delete this.linkIssues
delete this.linkEmail
delete this.textarea
}
}

View File

@@ -0,0 +1,151 @@
function readFile(file, arrayBuffer, encoding){
var reader = new FileReader()
var promise = pageEvents.load(reader).then(event => event.target.result)
reader[arrayBuffer ? "readAsArrayBuffer" : "readAsText"](file, encoding)
return promise
}
function filePermission(file){
return file.queryPermission().then(response => {
if(response === "granted"){
return file
}else{
return file.requestPermission().then(response => {
if(response === "granted"){
return file
}else{
return Promise.reject(strings.accessNotGrantedError)
}
})
}
})
}
class RemoteFile{
constructor(...args){
this.init(...args)
}
init(url){
this.url = url
try{
this.path = new URL(url).pathname
}catch(e){
this.path = url
}
if(this.path.startsWith("/")){
this.path = this.path.slice(1)
}
if(this.url.startsWith("data:")){
this.name = "datauri"
if(this.url.startsWith("data:audio/ogg")){
this.name += ".ogg"
}
}else{
this.name = this.path
var index = this.name.lastIndexOf("/")
if(index !== -1){
this.name = this.name.slice(index + 1)
}
}
}
arrayBuffer(){
return loader.ajax(this.url, request => {
request.responseType = "arraybuffer"
})
}
read(encoding){
if(encoding){
return this.blob().then(blob => readFile(blob, false, encoding))
}else{
return loader.ajax(this.url)
}
}
blob(){
return loader.ajax(this.url, request => {
request.responseType = "blob"
})
}
}
class LocalFile{
constructor(...args){
this.init(...args)
}
init(file, path){
this.file = file
this.path = path || file.webkitRelativePath
this.url = this.path
this.name = file.name
}
arrayBuffer(){
return readFile(this.file, true)
}
read(encoding){
return readFile(this.file, false, encoding)
}
blob(){
return Promise.resolve(this.file)
}
}
class FilesystemFile{
constructor(...args){
this.init(...args)
}
init(file, path){
this.file = file
this.path = path
this.url = this.path
this.name = file.name
}
arrayBuffer(){
return this.blob().then(blob => blob.arrayBuffer())
}
read(encoding){
return this.blob().then(blob => readFile(blob, false, encoding))
}
blob(){
return filePermission(this.file).then(file => file.getFile())
}
}
class GdriveFile{
constructor(...args){
this.init(...args)
}
init(fileObj){
this.path = fileObj.path
this.name = fileObj.name
this.id = fileObj.id
this.url = gpicker.filesUrl + this.id + "?alt=media"
}
arrayBuffer(){
return gpicker.downloadFile(this.id, "arraybuffer")
}
read(encoding){
if(encoding){
return this.blob().then(blob => readFile(blob, false, encoding))
}else{
return gpicker.downloadFile(this.id)
}
}
blob(){
return gpicker.downloadFile(this.id, "blob")
}
}
class CachedFile{
constructor(...args){
this.init(...args)
}
init(contents, oldFile){
this.contents = contents
this.oldFile = oldFile
this.path = oldFile.path
this.name = oldFile.name
this.url = oldFile.url
}
arrayBuffer(){
return Promise.resolve(this.contents)
}
read(encoding){
return this.arrayBuffer()
}
blob(){
return this.arrayBuffer()
}
}

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

@@ -0,0 +1,682 @@
class Account{
constructor(...args){
this.init(...args)
}
init(touchEnabled){
this.touchEnabled = touchEnabled
cancelTouch = false
this.locked = false
if(account.loggedIn){
this.accountForm()
}else{
this.loginForm()
}
this.selected = this.items.length - 1
this.keyboard = new Keyboard({
confirm: ["enter", "space", "don_l", "don_r"],
previous: ["left", "up", "ka_l"],
next: ["right", "down", "ka_r"],
back: ["escape"]
}, this.keyPressed.bind(this))
this.gamepad = new Gamepad({
"confirm": ["b", "ls", "rs"],
"previous": ["u", "l", "lb", "lt", "lsu", "lsl"],
"next": ["d", "r", "rb", "rt", "lsd", "lsr"],
"back": ["start", "a"]
}, this.keyPressed.bind(this))
pageEvents.send("account", account.loggedIn)
}
accountForm(){
loader.changePage("account", true)
this.mode = "account"
this.setAltText(this.getElement("view-title"), account.username)
this.items = []
this.inputForms = []
this.shownDiv = ""
this.errorDiv = this.getElement("error-div")
this.getElement("displayname-hint").innerText = strings.account.displayName
this.displayname = this.getElement("displayname")
this.displayname.placeholder = strings.account.displayName
this.displayname.value = account.displayName
this.inputForms.push(this.displayname)
this.redrawRunning = true
this.redrawPaused = matchMedia("(prefers-reduced-motion: reduce)").matches
this.redrawForce = true
this.customdonRedrawBind = this.customdonRedraw.bind(this)
this.start = new Date().getTime()
this.frames = [
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 ,
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 ,
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,7 ,8 ,9 ,10,
11,11,11,11,10,9 ,8 ,7 ,13,12,12,13,14,15,16,17
]
this.customdonCache = new CanvasCache()
this.customdonCache.resize(723 * 2, 1858, 1)
this.customdonCanvas = this.getElement("customdon-canvas")
pageEvents.add(this.customdonCanvas, "click", this.customdonPause.bind(this))
this.customdonCtx = this.customdonCanvas.getContext("2d")
this.customdonBodyFill = this.getElement("customdon-bodyfill")
this.customdonBodyFill.value = account.don.body_fill
var parent = this.customdonBodyFill.parentNode
parent.insertBefore(document.createTextNode(strings.account.customdon.bodyFill), parent.firstChild)
pageEvents.add(this.customdonBodyFill, ["change", "input"], this.customdonChange.bind(this))
this.customdonFaceFill = this.getElement("customdon-facefill")
this.customdonFaceFill.value = account.don.face_fill
var parent = this.customdonFaceFill.parentNode
parent.insertBefore(document.createTextNode(strings.account.customdon.faceFill), parent.firstChild)
pageEvents.add(this.customdonFaceFill, ["change", "input"], this.customdonChange.bind(this))
this.customdonResetBtn = this.getElement("customdon-reset")
this.customdonResetBtn.value = strings.account.customdon.reset
pageEvents.add(this.customdonResetBtn, ["click", "touchstart"], this.customdonReset.bind(this))
this.customdonChange()
this.customdonRedraw()
this.accountPassButton = this.getElement("accountpass-btn")
this.setAltText(this.accountPassButton, strings.account.changePassword)
pageEvents.add(this.accountPassButton, ["click", "touchstart"], event => {
this.showDiv(event, "pass")
})
this.accountPass = this.getElement("accountpass-form")
for(var i = 0; i < this.accountPass.length; i++){
this.accountPass[i].placeholder = strings.account.currentNewRepeat[i]
this.inputForms.push(this.accountPass[i])
}
this.accountPassDiv = this.getElement("accountpass-div")
this.accountDelButton = this.getElement("accountdel-btn")
this.setAltText(this.accountDelButton, strings.account.deleteAccount)
pageEvents.add(this.accountDelButton, ["click", "touchstart"], event => {
this.showDiv(event, "del")
})
this.accountDel = this.getElement("accountdel-form")
this.accountDel.password.placeholder = strings.account.verifyPassword
this.inputForms.push(this.accountDel.password)
this.accountDelDiv = this.getElement("accountdel-div")
this.linkPrivacy = this.getElement("privacy-btn")
this.setAltText(this.linkPrivacy, strings.account.privacy)
pageEvents.add(this.linkPrivacy, ["mousedown", "touchstart"], this.openPrivacy.bind(this))
this.items.push(this.linkPrivacy)
this.logoutButton = this.getElement("logout-btn")
this.setAltText(this.logoutButton, strings.account.logout)
pageEvents.add(this.logoutButton, ["mousedown", "touchstart"], this.onLogout.bind(this))
this.items.push(this.logoutButton)
this.endButton = this.getElement("view-end-button")
this.setAltText(this.endButton, strings.account.cancel)
pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this))
this.items.push(this.endButton)
this.saveButton = this.getElement("save-btn")
this.setAltText(this.saveButton, strings.account.save)
pageEvents.add(this.saveButton, ["mousedown", "touchstart"], this.onSave.bind(this))
this.items.push(this.saveButton)
for(var i = 0; i < this.inputForms.length; i++){
pageEvents.add(this.inputForms[i], ["keydown", "keyup", "keypress"], this.onFormPress.bind(this))
}
}
customdonPause(){
this.redrawPaused = !this.redrawPaused
this.redrawForce = true
this.start = new Date().getTime()
}
customdonChange(){
var ctx = this.customdonCtx
this.customdonCache.clear()
var w = 722
var h = 1858
this.customdonCache.set({
w: w, h: h, id: "bodyFill"
}, ctx => {
ctx.drawImage(assets.image["don_anim_normal_b1"], 0, 0)
ctx.globalCompositeOperation = "source-atop"
ctx.fillStyle = this.customdonBodyFill.value
ctx.fillRect(0, 0, w, h)
})
this.customdonCache.set({
w: w, h: h, id: "faceFill"
}, ctx => {
ctx.drawImage(assets.image["don_anim_normal_b2"], 0, 0)
ctx.globalCompositeOperation = "source-atop"
ctx.fillStyle = this.customdonFaceFill.value
ctx.fillRect(0, 0, w, h)
ctx.globalCompositeOperation = "source-over"
this.customdonCache.get({
ctx: ctx,
x: 0, y: 0, w: w, h: h,
id: "bodyFill"
})
})
this.redrawForce = true
}
customdonReset(event){
if(event.type === "touchstart"){
event.preventDefault()
}
this.customdonBodyFill.value = defaultDon.body_fill
this.customdonFaceFill.value = defaultDon.face_fill
this.customdonChange()
}
customdonRedraw(){
if(!this.redrawRunning){
return
}
requestAnimationFrame(this.customdonRedrawBind)
if(!document.hasFocus() || this.redrawPaused && !this.redrawForce){
return
}
var ms = new Date().getTime()
var ctx = this.customdonCtx
if(this.redrawPaused){
var frame = 0
}else{
var frame = this.frames[Math.floor((ms - this.start) / 30) % this.frames.length]
}
var w = 360
var h = 184
var sx = Math.floor(frame / 10) * (w + 2)
var sy = (frame % 10) * (h + 2)
ctx.clearRect(0, 0, w, h)
this.customdonCache.get({
ctx: ctx,
sx: sx, sy: sy, sw: w, sh: h,
x: -26, y: 0, w: w, h: h,
id: "faceFill"
})
ctx.drawImage(assets.image["don_anim_normal_a"],
sx, sy, w, h,
-26, 0, w, h
)
this.redrawForce = false
}
showDiv(event, div){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
var otherDiv = this.shownDiv && this.shownDiv !== div
var display = this.shownDiv === div ? "" : "block"
this.shownDiv = display ? div : ""
switch(div){
case "pass":
if(otherDiv){
this.accountDelDiv.style.display = ""
}
this.accountPassDiv.style.display = display
break
case "del":
if(otherDiv){
this.accountPassDiv.style.display = ""
}
this.accountDelDiv.style.display = display
break
}
}
loginForm(register, fromSwitch){
loader.changePage("login", true)
this.mode = register ? "register" : "login"
this.setAltText(this.getElement("view-title"), strings.account[this.mode])
this.errorDiv = this.getElement("error-div")
this.items = []
this.form = this.getElement("login-form")
this.getElement("username-hint").innerText = strings.account.username
this.form.username.placeholder = strings.account.enterUsername
this.getElement("password-hint").innerText = strings.account.password
this.form.password.placeholder = strings.account.enterPassword
this.password2 = this.getElement("password2-div")
this.remember = this.getElement("remember-div")
this.getElement("remember-label").appendChild(document.createTextNode(strings.account.remember))
this.loginButton = this.getElement("login-btn")
this.registerButton = this.getElement("register-btn")
if(register){
var pass2 = document.createElement("input")
pass2.type = "password"
pass2.name = "password2"
pass2.required = true
pass2.placeholder = strings.account.repeatPassword
this.password2.appendChild(pass2)
this.password2.style.display = "block"
this.remember.style.display = "none"
this.setAltText(this.loginButton, strings.account.registerAccount)
this.setAltText(this.registerButton, strings.account.login)
}else{
this.setAltText(this.loginButton, strings.account.login)
this.setAltText(this.registerButton, strings.account.register)
}
pageEvents.add(this.form, "submit", this.onLogin.bind(this))
pageEvents.add(this.loginButton, ["mousedown", "touchstart"], this.onLogin.bind(this))
pageEvents.add(this.registerButton, ["mousedown", "touchstart"], this.onSwitchMode.bind(this))
this.items.push(this.registerButton)
this.linkPrivacy = this.getElement("privacy-btn")
this.setAltText(this.linkPrivacy, strings.account.privacy)
pageEvents.add(this.linkPrivacy, ["mousedown", "touchstart"], this.openPrivacy.bind(this))
this.items.push(this.linkPrivacy)
if(!register){
this.items.push(this.loginButton)
}
for(var i = 0; i < this.form.length; i++){
pageEvents.add(this.form[i], ["keydown", "keyup", "keypress"], this.onFormPress.bind(this))
}
this.endButton = this.getElement("view-end-button")
this.setAltText(this.endButton, strings.account.back)
pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this))
this.items.push(this.endButton)
if(fromSwitch){
this.selected = 0
this.endButton.classList.remove("selected")
this.registerButton.classList.add("selected")
}
}
getElement(name){
return loader.screen.getElementsByClassName(name)[0]
}
setAltText(element, text){
element.innerText = text
element.setAttribute("alt", text)
}
keyPressed(pressed, name){
if(!pressed || this.locked){
return
}
var selected = this.items[this.selected]
if(name === "confirm"){
if(selected === this.endButton){
this.onEnd()
}else if(selected === this.registerButton){
this.onSwitchMode()
}else if(selected === this.loginButton){
this.onLogin()
}else if(selected === this.linkPrivacy){
assets.sounds["se_don"].play()
this.openPrivacy()
}
}else if(name === "previous" || name === "next"){
selected.classList.remove("selected")
this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1))
this.items[this.selected].classList.add("selected")
if(this.items[this.selected] === this.linkPrivacy){
this.items[this.selected].scrollIntoView()
}
assets.sounds["se_ka"].play()
}else if(name === "back"){
this.onEnd()
}
}
mod(length, index){
return ((index % length) + length) % length
}
onFormPress(event){
event.stopPropagation()
if(event.type === "keypress" && event.keyCode === 13){
event.preventDefault()
if(this.mode === "account"){
this.onSave()
}else{
this.onLogin()
}
}
}
onSwitchMode(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
this.clean(true)
this.loginForm(this.mode === "login", true)
}
onLogin(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
var obj = {
username: this.form.username.value,
password: this.form.password.value
}
if(!obj.username || !obj.password){
this.error(strings.account.cannotBeEmpty.replace("%s", strings.account[!obj.username ? "username" : "password"]))
return
}
if(this.mode === "login"){
obj.remember = this.form.remember.checked
}else{
if(obj.password !== this.form.password2.value){
this.error(strings.account.passwordsDoNotMatch)
return
}
}
this.request(this.mode, obj).then(response => {
account.loggedIn = true
account.username = response.username
account.displayName = response.display_name
account.don = response.don
var loadScores = scores => {
scoreStorage.load(scores)
this.onEnd(false, true, true)
pageEvents.send("login", account.username)
}
if(this.mode === "login"){
this.request("scores/get", false, true).then(response => {
loadScores(response.scores)
}, () => {
loadScores({})
})
}else{
scoreStorage.save().catch(() => {}).finally(() => {
this.onEnd(false, true, true)
pageEvents.send("login", account.username)
})
}
}, response => {
if(response && response.status === "error" && response.message){
if(response.message in strings.serverError){
this.error(strings.serverError[response.message])
}else{
this.error(response.message)
}
}else{
this.error(strings.account.error)
}
})
}
openPrivacy(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
open("privacy")
}
onLogout(){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
account.loggedIn = false
delete account.username
delete account.displayName
delete account.don
var loadScores = () => {
scoreStorage.load()
this.onEnd(false, true)
pageEvents.send("logout")
}
this.request("logout").then(loadScores, loadScores)
}
onSave(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
this.clearError()
var promises = []
var noNameChange = false
if(this.shownDiv === "pass"){
var passwords = []
for(var i = 0; i < this.accountPass.length; i++){
passwords.push(this.accountPass[i].value)
}
if(passwords[1] === passwords[2]){
promises.push(this.request("account/password", {
current_password: passwords[0],
new_password: passwords[1]
}))
}else{
this.error(strings.account.newPasswordsDoNotMatch)
return
}
}
if(this.shownDiv === "del" && this.accountDel.password.value){
noNameChange = true
promises.push(this.request("account/remove", {
password: this.accountDel.password.value
}).then(() => {
account.loggedIn = false
delete account.username
delete account.displayName
delete account.don
scoreStorage.load()
pageEvents.send("logout")
return Promise.resolve
}))
}
var newName = this.displayname.value.trim()
if(!noNameChange && newName !== account.displayName){
promises.push(this.request("account/display_name", {
display_name: newName
}).then(response => {
account.displayName = response.display_name
}))
}
var bodyFill = this.customdonBodyFill.value
var faceFill = this.customdonFaceFill.value
if(!noNameChange && (bodyFill !== account.body_fill || this.customdonFaceFill.value !== account.face_fill)){
promises.push(this.request("account/don", {
body_fill: bodyFill,
face_fill: faceFill
}).then(response => {
account.don = response.don
}))
}
var error = false
var errorFunc = response => {
if(error){
return
}
if(response && response.message){
if(response.message in strings.serverError){
this.error(strings.serverError[response.message])
}else{
this.error(response.message)
}
}else{
this.error(strings.account.error)
}
}
Promise.all(promises).then(() => {
this.onEnd(false, true)
}, errorFunc).catch(errorFunc)
}
onEnd(event, noSound, noReset){
var touched = false
if(event){
if(event.type === "touchstart"){
event.preventDefault()
touched = true
}else if(event.which !== 1){
return
}
}
if(this.locked){
return
}
this.clean(false, noReset)
assets.sounds["se_don"].play()
setTimeout(() => {
new SongSelect(false, false, touched)
}, 500)
}
request(url, obj, get){
this.lock(true)
var doRequest = token => {
return new Promise((resolve, reject) => {
var request = new XMLHttpRequest()
request.open(get ? "GET" : "POST", "api/" + url)
pageEvents.load(request).then(() => {
this.lock(false)
if(request.status !== 200){
reject()
return
}
try{
var json = JSON.parse(request.response)
}catch(e){
reject()
return
}
if(json.status === "ok"){
resolve(json)
}else{
reject(json)
}
}, () => {
this.lock(false)
reject()
})
if(!get){
request.setRequestHeader("X-CSRFToken", token)
}
if(obj){
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
request.send(JSON.stringify(obj))
}else{
request.send()
}
})
}
if(get){
return doRequest()
}else{
return loader.getCsrfToken().then(doRequest)
}
}
lock(isLocked){
this.locked = isLocked
if(this.mode === "login" || this.mode === "register"){
for(var i = 0; i < this.form.length; i++){
this.form[i].disabled = isLocked
}
}else if(this.mode === "account"){
for(var i = 0; i < this.inputForms.length; i++){
this.inputForms[i].disabled = isLocked
}
}
}
error(text){
this.errorDiv.innerText = text
this.errorDiv.style.display = "block"
}
clearError(){
this.errorDiv.innerText = ""
this.errorDiv.style.display = ""
}
clean(eventsOnly, noReset){
if(!eventsOnly){
cancelTouch = true
this.keyboard.clean()
this.gamepad.clean()
}
if(this.mode === "account"){
if(!noReset){
this.accountPass.reset()
this.accountDel.reset()
}
this.redrawRunning = false
this.customdonCache.clean()
pageEvents.remove(this.customdonCanvas, "click")
pageEvents.remove(this.customdonBodyFill, ["change", "input"])
pageEvents.remove(this.customdonFaceFill, ["change", "input"])
pageEvents.remove(this.customdonResetBtn, ["click", "touchstart"])
pageEvents.remove(this.accounPassButton, ["click", "touchstart"])
pageEvents.remove(this.accountDelButton, ["click", "touchstart"])
pageEvents.remove(this.linkPrivacy, ["mousedown", "touchstart"])
pageEvents.remove(this.logoutButton, ["mousedown", "touchstart"])
pageEvents.remove(this.saveButton, ["mousedown", "touchstart"])
for(var i = 0; i < this.inputForms.length; i++){
pageEvents.remove(this.inputForms[i], ["keydown", "keyup", "keypress"])
}
delete this.errorDiv
delete this.displayname
delete this.frames
delete this.customdonCanvas
delete this.customdonCtx
delete this.customdonBodyFill
delete this.customdonFaceFill
delete this.customdonResetBtn
delete this.accountPassButton
delete this.accountPass
delete this.accountPassDiv
delete this.accountDelButton
delete this.accountDel
delete this.accountDelDiv
delete this.linkPrivacy
delete this.logoutButton
delete this.saveButton
delete this.inputForms
}else if(this.mode === "login" || this.mode === "register"){
if(!eventsOnly && !noReset){
this.form.reset()
}
pageEvents.remove(this.form, "submit")
pageEvents.remove(this.loginButton, ["mousedown", "touchstart"])
pageEvents.remove(this.registerButton, ["mousedown", "touchstart"])
for(var i = 0; i < this.form.length; i++){
pageEvents.remove(this.registerButton, ["keydown", "keyup", "keypress"])
}
pageEvents.remove(this.linkPrivacy, ["mousedown", "touchstart"])
delete this.errorDiv
delete this.form
delete this.password2
delete this.remember
delete this.loginButton
delete this.registerButton
delete this.linkPrivacy
}
pageEvents.remove(this.endButton, ["mousedown", "touchstart"])
delete this.endButton
delete this.items
}
}

164
public/src/js/assets.js Normal file
View File

@@ -0,0 +1,164 @@
var assets = {
"js": [
"lib/md5.min.js",
"lib/fuzzysort.js",
"loadsong.js",
"parseosu.js",
"titlescreen.js",
"scoresheet.js",
"songselect.js",
"keyboard.js",
"gameinput.js",
"game.js",
"controller.js",
"circle.js",
"view.js",
"mekadon.js",
"gamepad.js",
"tutorial.js",
"soundbuffer.js",
"p2.js",
"canvasasset.js",
"viewassets.js",
"gamerules.js",
"canvasdraw.js",
"canvastest.js",
"canvascache.js",
"parsetja.js",
"autoscore.js",
"about.js",
"debug.js",
"session.js",
"importsongs.js",
"logo.js",
"settings.js",
"scorestorage.js",
"account.js",
"lyrics.js",
"customsongs.js",
"abstractfile.js",
"idb.js",
"plugins.js",
"search.js"
],
"css": [
"main.css",
"titlescreen.css",
"loadsong.css",
"game.css",
"debug.css",
"songbg.css",
"view.css",
"search.css"
],
"img": [
"notes.png",
"notes_drumroll.png",
"notes_hit.png",
"notes_explosion.png",
"balloon.png",
"taiko.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_score_p1.png",
"bg_score_p2.png",
"bg_pause.png",
"badge_auto.png",
"mimizu.png"
],
"cssBackground": {
"#title-screen": "title-screen.png",
"#loading-don": "dancing-don.gif",
".pattern-bg": "bg-pattern-1.png",
".song-search-result-course::before": "difficulty.png",
"#song-select": "bg_genre_def.png",
".settings-outer": "bg_settings.png",
"#touch-pause-btn": "touch_pause.png",
"#touch-full-btn": "touch_fullscreen.png",
"#gamepad-bg, #gamepad-buttons": "settings_gamepad.png",
".song-search-result-crown": "crown.png",
".song-search-tip-error": "miss.png",
"#song-search": "bg_search.png"
},
"audioSfx": [
"se_pause.ogg",
"se_calibration.ogg",
"v_results.ogg",
"v_sanka.ogg",
"v_songsel.ogg",
"v_start.ogg",
"v_title.ogg"
],
"audioSfxLR": [
"neiro_1_don.ogg",
"neiro_1_ka.ogg",
"se_cancel.ogg",
"se_don.ogg",
"se_ka.ogg",
"se_jump.ogg",
"se_balloon.ogg",
"se_gameclear.ogg",
"se_gamefail.ogg",
"se_gamefullcombo.ogg",
"se_results_countup.ogg",
"se_results_crown.ogg",
"v_fullcombo.ogg",
"v_renda.ogg",
"v_results_fullcombo.ogg",
"v_results_fullcombo2.ogg"
],
"audioSfxLoud": [
"v_diffsel.ogg"
],
"audioMusic": [
"bgm_songsel.mp3",
"bgm_result.mp3",
"bgm_setsume.mp3",
"bgm_settings.mp3"
],
"fonts": {
"Kozuka": "Kozuka.otf",
"TnT": "TnT.ttf"
},
"views": [
"game.html",
"loadsong.html",
"songselect.html",
"titlescreen.html",
"tutorial.html",
"about.html",
"debug.html",
"session.html",
"settings.html",
"account.html",
"login.html",
"customsongs.html",
"search.html"
],
"songs": [],
"sounds": {},
"image": {},
"pages": {},
"categories": []
}
var gameConfig = {}

212
public/src/js/autoscore.js Normal file
View File

@@ -0,0 +1,212 @@
class AutoScore {
constructor(...args){
this.init(...args)
}
init(difficulty, level, scoremode, circles) {
this.scoremode = scoremode;
this.circles = circles;
this.basic_max_score_list = {
oni: [
1200000,
700000,
750000,
800000,
850000,
900000,
950000,
1000000,
1050000,
1100000,
1200000
],
ura: [
1200000,
700000,
750000,
800000,
850000,
900000,
950000,
1000000,
1050000,
1100000,
1200000
],
hard: [
900000,
550000,
600000,
650000,
700000,
750000,
800000,
850000,
900000,
],
normal: [
700000,
400000,
450000,
500000,
550000,
600000,
650000,
700000,
],
easy: [
380000,
300000,
320000,
340000,
360000,
380000,
]
}
if (this.GetMaxCombo() === 0) {
this.ScoreDiff = 100;
this.ScoreInit = 450;
return;
}
const target = this.GetTargetScore(difficulty, level);
this.Score = 0;
this.ScoreDiff = 0;
this.ScoreInit = 0;
var max_init = this.GetMaxPossibleInit(target);
var min_init = 0;
while (true) {
this.ScoreInit = (max_init + min_init) / 2;
this.ScoreDiff = Math.round(this.ScoreInit / 4);
this.Score = this.TryScore(this.ScoreInit, this.ScoreDiff);
//console.log(min_init, max_init, this.ScoreInit, this.ScoreDiff, this.Score);
if (this.ScoreInit === target) {
this.ScoreInit = Math.floor(this.ScoreInit / 10) * 10;
this.ScoreDiff = Math.round(this.ScoreInit / 4);
this.Score = this.TryScore(this.ScoreInit, this.ScoreDiff);
break;
} else if (this.Score >= target) {
max_init = this.ScoreInit;
} else {
min_init = this.ScoreInit;
}
if (max_init - min_init <= 10) {
this.ScoreInit = Math.floor(this.ScoreInit / 10) * 10;
this.ScoreDiff = Math.round(this.ScoreInit / 4);
this.Score = this.TryScore(this.ScoreInit, this.ScoreDiff);
break;
}
}
while (this.Score < target) {
this.ScoreInit += 10;
this.ScoreDiff = Math.round(this.ScoreInit / 4);
this.Score = this.TryScore(this.ScoreInit, this.ScoreDiff);
//console.log(this.ScoreInit, this.ScoreDiff, this.Score);
}
//console.log(this.ScoreInit, this.ScoreDiff, this.Score);
}
IsCommonCircle(circle) {
const ty = circle.type;
return ty === "don" || ty === "ka" || ty === "daiDon" || ty === "daiKa";
}
TryScore(init, diff) {
var score = 0;
var combo = 0;
for (var circle of this.circles) {
if (circle.branch && circle.branch.name !== "master") {
continue;
}
if (this.IsCommonCircle(circle)) {
combo++;
if (combo % 100 === 0 && this.scoremode !== 1) {
score += 10000;
}
}
var diff_mul = 0;
var multiplier = circle.gogoTime ? 1.2 : 1;
if (this.scoremode === 1) {
diff_mul = Math.max(0, Math.floor((Math.min(combo, 100) - 1) / 10));
} else {
if (combo >= 100) {
diff_mul = 8;
} else if (combo >= 50) {
diff_mul = 4;
} else if (combo >= 30) {
diff_mul = 2;
} else if (combo >= 10) {
diff_mul = 1;
}
}
switch (circle.type) {
case "don":
case "ka": {
score += Math.floor((init + diff * diff_mul) * multiplier / 10) * 10;
break;
}
case "daiDon":
case "daiKa": {
score += Math.floor((init + diff * diff_mul) * multiplier / 5) * 10;
break;
}
case "balloon": {
score += (5000 + 300 * circle.requiredHits) * multiplier;
break;
}
default: {
break;
}
}
}
return score;
}
GetTargetScore(difficulty, level) {
//console.log(difficulty, level)
var ret = this.basic_max_score_list[difficulty][level];
if (!ret) {
ret = this.basic_max_score_list[difficulty][0];
}
return ret;
}
GetMaxCombo() {
var combo = 0;
for (var circle of this.circles) {
if (this.IsCommonCircle(circle) && (!circle.branch || circle.branch.name === "master")) {
combo++;
}
}
return combo;
}
GetMaxPossibleInit(target) {
var basic_score = 0;
if (this.scoremode !== 1) {
const max_combo = this.GetMaxCombo();
basic_score += Math.floor(max_combo / 100);
}
var combo = 0;
for (var circle of this.circles) {
if (circle.branch && circle.branch.name !== "master") {
continue;
}
var multiplier = circle.gogoTime ? 1.2 : 1;
switch (circle.type) {
case "don":
case "ka": {
combo += (1 * multiplier);
break;
}
case "daiDon":
case "daiKa": {
combo += (2 * multiplier);
break;
}
case "balloon": {
basic_score += (5000 + 300 * circle.requiredHits) * multiplier;
break;
}
default: {
break;
}
}
}
combo = Math.floor(combo);
return Math.ceil((target - basic_score) / combo / 10) * 10;
}
}

View File

@@ -0,0 +1,203 @@
function browserSupport(){
var tests = {
"Arrow function": function(){
eval("()=>{}")
return true
},
"AudioContext": function(){
if("AudioContext" in window || "webkitAudioContext" in window){
return typeof (window.AudioContext || window.webkitAudioContext) === "function"
}
return false
},
"Class": function(){
eval("class a{}")
return true
},
"Class field declarations": function(){
eval("class a{a=1}")
return true
},
"Array.find": function(){
return "find" in Array.prototype && "findIndex" in Array.prototype
},
"Path2D SVG": function(){
var canvas = document.createElement("canvas")
canvas.width = 1
canvas.height = 1
var ctx = canvas.getContext("2d")
var path = new Path2D("M0 0H1V1H0")
ctx.fill(path)
return ctx.getImageData(0, 0, 1, 1).data[3] !== 0
},
"Promise": function(){
if("Promise" in window && "resolve" in window.Promise && "reject" in window.Promise && "all" in window.Promise && "race" in window.Promise){
var resolve
new window.Promise(function(r){resolve = r})
return typeof resolve === "function"
}
return false
},
"CSS calc": function(){
var el = document.createElement("a")
el.style.width = "calc(1px)"
return el.style.length !== 0
},
"let statement": function(){
eval("let a")
return true
},
"CSS custom property": function(){
var el = document.createElement("a")
el.style.setProperty("--a", 1)
return el.style.length !== 0
},
"Font Loading API": function(){
return typeof FontFace === "function"
},
"OGG or WebAssembly": function(){
return new Audio().canPlayType("audio/ogg;codecs=vorbis") || "WebAssembly" in window
},
"KeyboardEvent.key": function(){
return "key" in KeyboardEvent.prototype
},
"Module import": function(){
eval("import('data:text/javascript,')")
return true
}
}
failedTests = []
for(var name in tests){
var result = false
try{
result = tests[name]()
}catch(e){}
if(result === false){
failedTests.push(name)
}
}
if(failedTests.length !== 0){
showUnsupported()
}
}
function showUnsupported(strings){
if(!strings){
var lang
try{
if("localStorage" in window && window.localStorage.lang && window.localStorage.lang in allStrings){
lang = window.localStorage.lang
}
if(!lang && "languages" in navigator){
var userLang = navigator.languages.slice()
userLang.unshift(navigator.language)
for(var i in userLang){
for(var j in allStrings){
if(allStrings[j].regex.test(userLang[i])){
lang = j
}
}
}
}
}catch(e){}
if(!lang){
lang = "en"
}
strings = allStrings[lang]
}
var div = document.getElementById("unsupportedBrowser")
if(div){
div.parentNode.removeChild(div)
}
div = document.createElement("div")
div.id = "unsupportedBrowser"
var warn = document.createElement("div")
warn.id = "unsupportedWarn"
warn.innerText = "!"
warn.textContent = "!"
div.appendChild(warn)
var hide = document.createElement("div")
hide.id = "unsupportedHide"
hide.innerText = "x"
hide.textContent = "x"
div.appendChild(hide)
var span = document.createElement("span")
var browserWarning = strings.browserSupport.browserWarning.split("%s")
for(var i = 0; i < browserWarning.length; i++){
if(i !== 0){
var link = document.createElement("a")
link.innerText = strings.browserSupport.details
link.textContent = strings.browserSupport.details
span.appendChild(link)
}
span.appendChild(document.createTextNode(browserWarning[i]))
}
div.appendChild(span)
var details = document.createElement("div")
details.id = "unsupportedDetails"
details.appendChild(document.createTextNode(strings.browserSupport.failedTests))
var ul = document.createElement("ul")
for(var i = 0; i < failedTests.length; i++){
var li = document.createElement("li")
li.innerText = failedTests[i]
li.textContent = failedTests[i]
ul.appendChild(li)
}
details.appendChild(ul)
var supportedBrowser = strings.browserSupport.supportedBrowser.split("%s")
for(var i = 0; i < supportedBrowser.length; i++){
if(i !== 0){
var chrome = document.createElement("a")
chrome.href = "https://www.google.com/chrome/"
chrome.innerText = "Google Chrome"
chrome.textContent = "Google Chrome"
details.appendChild(chrome)
}
details.appendChild(document.createTextNode(supportedBrowser[i]))
}
div.appendChild(details)
document.body.appendChild(div)
var divClick = function(event){
if(event.type === "touchstart"){
event.preventDefault()
getSelection().removeAllRanges()
}
div.classList.remove("hidden")
}
div.addEventListener("click", divClick)
div.addEventListener("touchstart", divClick)
var toggleDetails = function(event){
if(event.type === "touchstart"){
event.preventDefault()
}
if(details.style.display === "block"){
details.style.display = ""
}else{
details.style.display = "block"
}
}
link.addEventListener("click", toggleDetails)
link.addEventListener("touchstart", toggleDetails)
var hideClick = function(event){
if(event.type === "touchstart"){
event.preventDefault()
}
event.stopPropagation()
div.classList.add("hidden")
}
hide.addEventListener("click", hideClick)
hide.addEventListener("touchstart", hideClick)
chrome.addEventListener("touchend", function(event){
event.preventDefault()
chrome.click()
})
}
var failedTests
browserSupport()

View File

@@ -0,0 +1,151 @@
class CanvasAsset{
constructor(...args){
this.init(...args)
}
init(view, layer, position){
this.ctx = view.ctx
this.view = view
this.position = position
this.animationFrames = {}
this.speed = 1000 / 60
this.animationStart = 0
this.layer = layer
this.beatInterval = 468.75
}
draw(){
if(this.animation){
var u = (a, b) => typeof a === "undefined" ? b : a
var frame = 0
var ms = this.view.getMS()
var beatInterval = this.frameSpeed ? 1000 / 60 : this.beatInterval
if(this.animationEnd){
if(ms > this.animationStart + this.animationEnd.frameCount * this.speed * beatInterval){
this.animationEnd.callback()
this.animationEnd = false
return this.draw()
}
}
var index = Math.floor((ms - this.animationStart) / (this.speed * beatInterval))
if(Array.isArray(this.animation)){
frame = this.animation[this.mod(this.animation.length, index)]
}else{
frame = this.mod(this.animation, index)
}
this.ctx.save()
var pos = this.position(frame)
if(this.image){
this.ctx.drawImage(this.image,
u(pos.sx, pos.x), u(pos.sy, pos.y),
u(pos.sw, pos.w), u(pos.sh, pos.h),
pos.x, pos.y, pos.w, pos.h
)
}
this.ctx.restore()
}
}
mod(length, index){
return ((index % length) + length) % length
}
addFrames(name, frames, image, don){
var framesObj = {
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
}
setAnimation(name){
var framesObj = this.animationFrames[name]
this.animationName = name
if(framesObj){
this.animation = framesObj.frames
if(framesObj.image){
this.image = framesObj.image
}
this.don = framesObj.don
}else{
this.animation = false
}
}
getAnimation(){
return this.animationName
}
getAnimationLength(name){
var frames = this.animationFrames[name].frames
if(Array.isArray(frames)){
return frames.length
}else{
return frames
}
}
setUpdateSpeed(speed, frameSpeed){
this.speed = speed
this.frameSpeed = frameSpeed
}
setAnimationStart(ms){
this.animationStart = ms
}
setAnimationEnd(frameCount, callback){
if(typeof frameCount === "undefined"){
this.animationEnd = false
}else{
this.animationEnd = {
frameCount: frameCount,
callback: callback
}
}
}
changeBeatInterval(beatMS, initial){
if(!initial && !this.frameSpeed){
var ms = this.view.getMS()
this.animationStart = ms - (ms - this.animationStart) / this.beatInterval * beatMS
}
this.beatInterval = beatMS
}
clean(){
for(var i in this.animationFrames){
var frame = this.animationFrames[i]
if(frame.cache){
frame.cache.clean()
}
}
}
}

View File

@@ -0,0 +1,129 @@
class CanvasCache{
constructor(...args){
this.init(...args)
}
init(noSmoothing, w, h, scale){
this.noSmoothing = noSmoothing
if(w){
this.resize(w, h, scale)
}
this.index = Number.MIN_SAFE_INTEGER
}
resize(w, h, scale){
if(this.canvas){
this.map.clear()
}else{
this.map = new Map()
this.canvas = document.createElement("canvas")
this.ctx = this.canvas.getContext("2d")
if(this.noSmoothing){
this.ctx.imageSmoothingEnabled = false
}
}
this.scale = scale
this.x = 0
this.y = 0
this.w = w
this.h = h
this.lastW = 0
this.lastH = 0
this.canvas.width = Math.max(1, this.w * this.scale)
this.canvas.height = Math.max(1, this.h * this.scale)
this.ctx.scale(this.scale, this.scale)
}
get(config, callback, setOnly){
var img = this.map.get(config.id)
if(img && setOnly || !img && !callback){
return
}
var saved = false
var index = this.index++
if(!img){
var w = config.w
var h = config.h
this.x += this.lastW + (this.lastW ? 1 : 0)
if(this.x + w > this.w){
this.x = 0
this.y += this.lastH + 1
}
if(this.y + h > this.h){
var clear = true
var oldest = {index: index}
this.map.forEach((oldImg, id) => {
if(oldImg.index < oldest.index){
oldest.id = id
oldest.index = oldImg.index
}
})
var oldImg = this.map.get(oldest.id)
this.map.delete(oldest.id)
img = {
x: oldImg.x,
y: oldImg.y,
w: w,
h: h
}
}else{
var clear = false
this.lastW = w
this.lastH = Math.max(this.lastH, h)
img = {
x: this.x,
y: this.y,
w: w,
h: h
}
}
saved = true
this.ctx.save()
this.ctx.translate(img.x |0, img.y |0)
if(clear){
this.ctx.clearRect(0, 0, (img.w |0) + 1, (img.h |0) + 1)
}
this.ctx.beginPath()
this.ctx.rect(0, 0, img.w |0, img.h |0)
this.ctx.clip()
this.map.set(config.id, img)
callback(this.ctx)
}
img.index = index
if(setOnly){
this.ctx.restore()
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,
sx, sy, sw, sh,
config.x |0, config.y |0, config.w |0, config.h |0
)
if(saved){
this.ctx.restore()
}
}
set(config, callback){
return this.get(config, callback, true)
}
clear(){
this.x = 0
this.y = 0
this.lastW = 0
this.lastH = 0
this.map.clear()
this.ctx.clearRect(0, 0, this.w, this.h)
}
clean(){
if(!this.canvas){
return
}
this.resize(1, 1, 1)
delete this.map
delete this.ctx
delete this.canvas
}
}

1733
public/src/js/canvasdraw.js Normal file

File diff suppressed because it is too large Load Diff

151
public/src/js/canvastest.js Normal file
View File

@@ -0,0 +1,151 @@
class CanvasTest{
constructor(...args){
this.init(...args)
}
init(){
this.canvas = document.createElement("canvas")
var pixelRatio = window.devicePixelRatio || 1
var width = innerWidth * pixelRatio
var height = innerHeight * pixelRatio
this.canvas.width = Math.max(1, width)
this.canvas.height = Math.max(1, height)
this.ctx = this.canvas.getContext("2d")
this.ctx.scale(pixelRatio, pixelRatio)
this.ratio = pixelRatio
this.draw = new CanvasDraw()
this.font = "serif"
this.songAsset = {
marginLeft: 18,
marginTop: 90,
width: 82,
height: 452,
border: 6,
innerBorder: 8
}
}
blurPerformance(){
return new Promise(resolve => {
requestAnimationFrame(() => {
var ctx = this.ctx
ctx.save()
var lastIteration = this.blurIteration()
var frameTime = []
for(var i = 0; i < 10; i++){
lastIteration = lastIteration.then(ms => {
frameTime.push(ms)
return this.blurIteration()
})
}
lastIteration.then(() => {
ctx.restore()
resolve(frameTime.reduce((a, b) => a + b) / frameTime.length)
})
})
})
}
blurIteration(){
return new Promise(resolve => {
requestAnimationFrame(() => {
var startTime = Date.now()
var ctx = this.ctx
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
for(var x = 0; x < this.canvas.width; x += this.songAsset.width + this.songAsset.marginLeft){
this.draw.songFrame({
ctx: ctx,
x: x,
y: this.songAsset.marginTop,
width: this.songAsset.width,
height: this.songAsset.height,
border: this.songAsset.border,
innerBorder: this.songAsset.innerBorder,
background: "#efb058",
borderStyle: ["#ffe7bd", "#c68229"],
ratio: this.ratio,
innerContent: () => {}
})
}
for(var i = 0; i < 2; i++){
this.draw.layeredText({
ctx: ctx,
text: "I am a text",
fontSize: 48,
fontFamily: this.font,
x: 23 + 300 * i,
y: 15
}, [
{x: -2, y: -2, outline: "#000", letterBorder: 22},
{},
{x: 2, y: 2, shadow: [2, 2, 7]},
{x: 2, y: 2, outline: "#ad1516", letterBorder: 10},
{x: -2, y: -2, outline: "#ff797b"},
{outline: "#f70808"},
{fill: "#fff", shadow: [-1, 1, 3, 1.5]}
])
}
resolve(Date.now() - startTime)
})
})
}
drawAllImages(){
return new Promise(resolve => {
requestAnimationFrame(() => {
var startTime = Date.now()
var ctx = this.ctx
ctx.save()
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
for(var name in assets.image){
ctx.drawImage(assets.image[name], 0, 0)
}
var comboCount = 765
var comboX = 100
var comboY = 100
var fontSize = 120
this.ctx.font = "normal " + fontSize + "px TnT, Meiryo, sans-serif"
this.ctx.textAlign = "center"
this.ctx.strokeStyle = "#000"
this.ctx.lineWidth = fontSize / 10
var glyph = this.ctx.measureText("0").width
var comboText = comboCount.toString().split("")
for(var i in comboText){
var textX = comboX + glyph * (i - (comboText.length - 1) / 2)
if(comboCount >= 100){
var grd = this.ctx.createLinearGradient(
textX - glyph * 0.2,
comboY - fontSize * 0.8,
textX + glyph * 0.2,
comboY - fontSize * 0.2
)
grd.addColorStop(0, "#f00")
grd.addColorStop(1, "#fe0")
this.ctx.fillStyle = grd
}else{
this.ctx.fillStyle = "#fff"
}
this.strokeFillText(comboText[i],
textX,
comboY
)
}
ctx.restore()
resolve(Date.now() - startTime)
})
})
}
strokeFillText(text, x, y){
this.ctx.strokeText(text, x, y)
this.ctx.fillText(text, x, y)
}
clean(){
this.draw.clean()
delete this.ctx
delete this.canvas
}
}

45
public/src/js/circle.js Normal file
View File

@@ -0,0 +1,45 @@
class Circle{
constructor(...args){
this.init(...args)
}
init(config){
this.id = config.id
this.ms = config.start
this.originalMS = this.ms
this.type = config.type
this.text = config.txt
this.speed = config.speed
this.endTime = config.endTime || this.ms
this.originalEndTime = this.endTime
this.isPlayed = 0
this.animating = false
this.animT = 0
this.score = 0
this.lastFrame = this.ms + 100
this.animationEnded = false
this.timesHit = 0
this.timesKa = 0
this.requiredHits = config.requiredHits || 0
this.rendaPlayed = false
this.gogoTime = config.gogoTime || false
this.gogoChecked = false
this.beatMS = config.beatMS
this.fixedPos = config.fixedPos
this.branch = config.branch
this.section = config.section
}
animate(ms){
this.animating = true
this.animT = ms
}
played(score, big){
this.score = score
this.isPlayed = score <= 0 ? score - 1 : (big ? 2 : 1)
}
hit(keysKa){
this.timesHit++
if(keysKa){
this.timesKa++
}
}
}

414
public/src/js/controller.js Normal file
View File

@@ -0,0 +1,414 @@
class Controller{
constructor(...args){
this.init(...args)
}
init(selectedSong, songData, autoPlayEnabled, multiplayer, touchEnabled){
this.selectedSong = selectedSong
this.songData = songData
this.autoPlayEnabled = autoPlayEnabled
this.saveScore = !autoPlayEnabled
this.multiplayer = multiplayer
this.touchEnabled = touchEnabled
if(multiplayer === 2){
this.snd = p2.player === 2 ? "_p1" : "_p2"
this.don = p2.don || defaultDon
}else{
this.snd = multiplayer ? "_p" + p2.player : ""
this.don = account.loggedIn ? account.don : defaultDon
}
if(this.snd === "_p2" && this.objEqual(defaultDon, this.don)){
this.don = {
body_fill: defaultDon.face_fill,
face_fill: defaultDon.body_fill
}
}
this.calibrationMode = selectedSong.folder === "calibration"
this.audioLatency = 0
this.videoLatency = 0
if(!this.calibrationMode){
var latency = settings.getItem("latency")
if(!autoPlayEnabled || this.multiplayer){
this.audioLatency = Math.round(latency.audio) || 0
}
this.videoLatency = Math.round(latency.video) || 0 + this.audioLatency
}
if(this.multiplayer !== 2){
loader.changePage("game", false)
}
if(selectedSong.type === "tja"){
this.parsedSongData = new ParseTja(songData, selectedSong.difficulty, selectedSong.stars, selectedSong.offset)
}else{
this.parsedSongData = new ParseOsu(songData, selectedSong.difficulty, selectedSong.stars, selectedSong.offset)
}
this.offset = this.parsedSongData.soundOffset
var maxCombo = this.parsedSongData.circles.filter(circle => ["don", "ka", "daiDon", "daiKa"].indexOf(circle.type) > -1 && (!circle.branch || circle.branch.name == "master")).length
if (maxCombo >= 50) {
var comboVoices = ["v_combo_50"].concat(Array.from(Array(Math.min(50, Math.floor(maxCombo / 100))), (d, i) => "v_combo_" + ((i + 1) * 100)))
var promises = []
comboVoices.forEach(name => {
if (!assets.sounds[name + "_p1"]) {
promises.push(loader.loadSound(name + ".ogg", snd.sfxGain).then(sound => {
assets.sounds[name + "_p1"] = assets.sounds[name].copy(snd.sfxGainL)
assets.sounds[name + "_p2"] = assets.sounds[name].copy(snd.sfxGainR)
}))
}
})
Promise.all(promises)
}
if(this.calibrationMode){
this.volume = 1
}else{
assets.songs.forEach(song => {
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)
}
}
}
})
}
this.game = new Game(this, this.selectedSong, this.parsedSongData)
this.view = new View(this)
if (parseFloat(localStorage.getItem("baisoku") ?? "1", 10) !== 1) {
this.saveScore = false;
}
this.mekadon = new Mekadon(this, this.game)
this.keyboard = new GameInput(this)
if(!autoPlayEnabled && this.multiplayer !== 2){
this.easierBigNotes = settings.getItem("easierBigNotes") || this.keyboard.keyboard.TaikoForceLv5
}else{
this.easierBigNotes = false
}
this.drumSounds = settings.getItem("latency").drumSounds
this.playedSounds = {}
}
run(syncWith){
if(syncWith){
this.syncWith = syncWith
}
if(this.multiplayer !== 2){
snd.musicGain.setVolumeMul(this.volume)
}
this.game.run()
this.view.run()
if(this.multiplayer === 1){
syncWith.run(this)
syncWith.game.elapsedTime = this.game.elapsedTime
syncWith.game.startDate = this.game.startDate
}
requestAnimationFrame(() => {
this.startMainLoop()
if(!this.multiplayer){
debugObj.controller = this
if(debugObj.debug){
debugObj.debug.updateStatus()
}
}
})
}
startMainLoop(){
this.mainLoopRunning = true
window.gamestatus = 'start'
this.gameLoop()
this.viewLoop()
if(this.multiplayer !== 2){
this.gameInterval = setInterval(this.gameLoop.bind(this), 1000 / 60)
pageEvents.send("game-start", {
selectedSong: this.selectedSong,
autoPlayEnabled: this.autoPlayEnabled,
multiplayer: this.multiplayer,
touchEnabled: this.touchEnabled
})
}
}
stopMainLoop(){
this.mainLoopRunning = false
window.gamestatus = 'stop'
if (window.videoElement) {
// 停止视频播放
window.videoElement.pause();
// 移除视频元素
document.body.removeChild(window.videoElement);
// 解除引用,允许垃圾回收
window.videoElement = null;
}
if(this.game.mainAsset){
this.game.mainAsset.stop()
}
if(this.multiplayer !== 2){
clearInterval(this.gameInterval)
}
}
gameLoop(){
if(this.mainLoopRunning){
if(this.multiplayer === 1){
this.syncWith.game.elapsedTime = this.game.elapsedTime
this.syncWith.game.startDate = this.game.startDate
}
var ms = this.game.elapsedTime
if(this.game.musicFadeOut < 3){
this.keyboard.checkMenuKeys()
}
if(this.calibrationMode){
this.game.calibration()
}
if(!this.game.isPaused()){
this.keyboard.checkGameKeys()
if(ms < 0){
this.game.updateTime()
}else{
if(!this.calibrationMode){
this.game.update()
}
if(!this.mainLoopRunning){
return
}
this.game.playMainMusic()
}
}
if(this.multiplayer === 1){
this.syncWith.gameLoop()
}
}
}
viewLoop(){
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()
this.view.ctx.setTransform(1, 0, 0, 1, 0, 0)
}
this.scoresheet.redraw()
if(this.view.ctx){
this.view.ctx.restore()
}
}
})
}
this.view.refresh()
}
}
gameEnded(){
var score = this.getGlobalScore()
var vp
if(this.game.rules.clearReached(score.gauge)){
if(score.bad === 0){
vp = "fullcombo"
this.playSound("v_fullcombo", 1.350)
}else{
vp = "clear"
}
}else{
vp = "fail"
}
this.playSound("se_game" + vp)
}
displayResults(){
if(this.multiplayer !== 2){
if(this.view.cursorHidden){
this.view.canvas.style.cursor = ""
}
this.scoresheet = new Scoresheet(this, this.getGlobalScore(), this.multiplayer, this.touchEnabled)
}
}
displayScore(score, notPlayed, bigNote){
this.view.displayScore(score, notPlayed, bigNote)
}
songSelection(fadeIn, showWarning){
if(!fadeIn){
if(this.cleaned){
return
}
this.clean()
}
if(this.calibrationMode){
new SettingsView(this.touchEnabled, false, null, "latency")
}else{
new SongSelect(false, fadeIn, this.touchEnabled, null, showWarning)
}
}
restartSong(){
if(this.cleaned){
return
}
this.clean()
if(this.multiplayer){
new LoadSong(this.selectedSong, false, true, this.touchEnabled)
}else{
new Promise(resolve => {
if(this.calibrationMode){
resolve()
}else{
var songObj = assets.songs.find(song => song.id === this.selectedSong.folder)
var promises = []
if(songObj.chart && songObj.chart !== "blank"){
var chart = songObj.chart
if(chart.separateDiff){
var chartDiff = this.selectedSong.difficulty
chart = chart[chartDiff]
}
this.addPromise(promises, chart.read(this.selectedSong.type === "tja" ? "utf-8" : undefined).then(data => {
this.songData = data.replace(/\0/g, "").split("\n")
return Promise.resolve()
}), chart.url)
}
if(songObj.lyricsFile){
this.addPromise(promises, songObj.lyricsFile.read().then(result => {
songObj.lyricsData = result
}, () => Promise.resolve()), songObj.lyricsFile.url)
}
if(songObj && songObj.category_id === 9){
LoadSong.insertBackgroundVideo(songObj.id)
}
Promise.all(promises).then(resolve)
}
}).then(() => {
var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled)
taikoGame.run()
})
}
}
addPromise(promises, promise, url){
promises.push(promise.catch(error => {
if(this.restartSongError){
return
}
this.restartSongError = true
if(url){
error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url
}
pageEvents.send("load-song-error", error)
errorMessage(new Error(error).stack)
var title = this.selectedSong.title
if(title !== this.selectedSong.originalTitle){
title += " (" + this.selectedSong.originalTitle + ")"
}
setTimeout(() => {
new SongSelect(false, false, this.touchEnabled, null, {
name: "loadSongError",
title: title,
id: this.selectedSong.folder,
error: error
})
}, 500)
return Promise.reject(error)
}))
}
playSound(id, time, noSnd){
if(!this.drumSounds && (id === "neiro_1_don" || id === "neiro_1_ka" || id === "se_don" || id === "se_ka")){
return
}
var ms = Date.now() + (time || 0) * 1000
if(!(id in this.playedSounds) || ms > this.playedSounds[id] + 30){
assets.sounds[id + (noSnd ? "" : this.snd)].play(time)
this.playedSounds[id] = ms
}
}
togglePause(forcePause, pauseMove, noSound){
if(this.multiplayer === 1){
this.syncWith.game.togglePause(forcePause, pauseMove, noSound)
}
this.game.togglePause(forcePause, pauseMove, noSound)
}
getKeys(){
return this.keyboard.getKeys()
}
setKey(pressed, name, ms){
return this.keyboard.setKey(pressed, name, ms)
}
getElapsedTime(){
return this.game.elapsedTime
}
getCircles(){
return this.game.getCircles()
}
getCurrentCircle(){
return this.game.getCurrentCircle()
}
isWaiting(key, type){
return this.keyboard.isWaiting(key, type)
}
waitForKeyup(key, type){
this.keyboard.waitForKeyup(key, type)
}
getKeyTime(){
return this.keyboard.getKeyTime()
}
getCombo(){
return this.game.getCombo()
}
getGlobalScore(){
return this.game.getGlobalScore()
}
autoPlay(circle){
if(this.multiplayer){
p2.play(circle, this.mekadon)
}else{
return this.mekadon.play(circle)
}
}
objEqual(a, b){
for(var i in a){
if(a[i] !== b[i]){
return false
}
}
return true
}
clean(){
this.cleaned = true
if(this.multiplayer === 1){
this.syncWith.clean()
}
this.stopMainLoop()
this.keyboard.clean()
this.view.clean()
snd.buffer.loadSettings()
if(!this.multiplayer){
debugObj.controller = null
if(debugObj.debug){
debugObj.debug.updateStatus()
}
}
if(this.lyrics){
this.lyrics.clean()
}
}
}

View File

@@ -0,0 +1,551 @@
class CustomSongs{
constructor(...args){
this.init(...args)
}
init(touchEnabled, noPage, noLoading){
this.loaderDiv = document.createElement("div")
this.loaderDiv.innerHTML = assets.pages["loadsong"]
var loadingText = this.loaderDiv.querySelector("#loading-text")
this.setAltText(loadingText, strings.loading)
this.locked = false
this.mode = "main"
if(noPage){
this.noPage = true
this.noLoading = noLoading
return
}
this.touchEnabled = touchEnabled
loader.changePage("customsongs", true)
if(touchEnabled){
this.getElement("view-outer").classList.add("touch-enabled")
}
var tutorialTitle = this.getElement("view-title")
this.setAltText(tutorialTitle, strings.customSongs.title)
var tutorialContent = this.getElement("view-content")
strings.customSongs.description.forEach(string => {
tutorialContent.appendChild(document.createTextNode(string))
tutorialContent.appendChild(document.createElement("br"))
})
this.items = []
this.linkLocalFolder = document.getElementById("link-localfolder")
this.hasLocal = (typeof showDirectoryPicker === "function" || "webkitdirectory" in HTMLInputElement.prototype) && !(/Android|iPhone|iPad/.test(navigator.userAgent))
this.selected = -1
if(this.hasLocal){
this.browse = document.getElementById("browse")
pageEvents.add(this.browse, "change", this.browseChange.bind(this))
this.setAltText(this.linkLocalFolder, strings.customSongs.localFolder)
pageEvents.add(this.linkLocalFolder, ["mousedown", "touchstart"], this.localFolder.bind(this))
this.items.push(this.linkLocalFolder)
if(this.selected === -1){
this.linkLocalFolder.classList.add("selected")
this.selected = this.items.length - 1
}
}else{
this.linkLocalFolder.parentNode.removeChild(this.linkLocalFolder)
}
var groupGdrive = document.getElementById("group-gdrive")
this.linkGdriveFolder = document.getElementById("link-gdrivefolder")
this.linkGdriveAccount = document.getElementById("link-gdriveaccount")
this.linkPrivacy = document.getElementById("link-privacy")
if(gameConfig.google_credentials.gdrive_enabled){
this.setAltText(this.linkGdriveFolder, strings.customSongs.gdriveFolder)
pageEvents.add(this.linkGdriveFolder, ["mousedown", "touchstart"], this.gdriveFolder.bind(this))
this.items.push(this.linkGdriveFolder)
if(this.selected === -1){
this.linkGdriveFolder.classList.add("selected")
this.selected = this.items.length - 1
}
this.setAltText(this.linkGdriveAccount, strings.customSongs.gdriveAccount)
pageEvents.add(this.linkGdriveAccount, ["mousedown", "touchstart"], this.gdriveAccount.bind(this))
this.items.push(this.linkGdriveAccount)
this.setAltText(this.linkPrivacy, strings.account.privacy)
pageEvents.add(this.linkPrivacy, ["mousedown", "touchstart"], this.openPrivacy.bind(this))
this.items.push(this.linkPrivacy)
}else{
groupGdrive.style.display = "none"
this.linkPrivacy.parentNode.removeChild(this.linkPrivacy)
}
this.endButton = this.getElement("view-end-button")
this.setAltText(this.endButton, strings.session.cancel)
pageEvents.add(this.endButton, ["mousedown", "touchstart"], event => this.onEnd(event, true))
this.items.push(this.endButton)
if(this.selected === -1){
this.endButton.classList.add("selected")
this.selected = this.items.length - 1
}
this.fileSystem = location.protocol === "https:" && DataTransferItem.prototype.getAsFileSystemHandle
if(this.fileSystem || DataTransferItem.prototype.webkitGetAsEntry){
this.dropzone = document.getElementById("dropzone")
var dropContent = this.dropzone.getElementsByClassName("view-content")[0]
dropContent.innerText = strings.customSongs.dropzone
this.dragging = false
this.dragTarget = null
pageEvents.add(document, "dragenter", event => {
event.preventDefault()
this.dragTarget = event.target
})
pageEvents.add(document, "dragover", event => {
event.preventDefault()
if(!this.locked){
event.dataTransfer.dropEffect = "copy"
this.dropzone.classList.add("dragover")
this.dragging = true
}else{
event.dataTransfer.dropEffect = "none"
}
})
pageEvents.add(document, "dragleave", () => {
if(this.dragTarget === event.target){
event.preventDefault()
this.dropzone.classList.remove("dragover")
this.dragging = false
}
})
pageEvents.add(document, "drop", this.filesDropped.bind(this))
}
this.errorDiv = document.getElementById("customsongs-error")
pageEvents.add(this.errorDiv, ["mousedown", "touchstart"], event => {
if(event.target === event.currentTarget){
this.hideError()
}
})
var errorTitle = this.errorDiv.getElementsByClassName("view-title")[0]
this.setAltText(errorTitle, strings.customSongs.importError)
this.errorContent = this.errorDiv.getElementsByClassName("view-content")[0]
this.errorEnd = this.errorDiv.getElementsByClassName("view-end-button")[0]
this.setAltText(this.errorEnd, strings.tutorial.ok)
pageEvents.add(this.errorEnd, ["mousedown", "touchstart"], () => this.hideError(true))
this.keyboard = new Keyboard({
confirm: ["enter", "space", "don_l", "don_r"],
previous: ["left", "up", "ka_l"],
next: ["right", "down", "ka_r"],
backEsc: ["escape"]
}, this.keyPressed.bind(this))
this.gamepad = new Gamepad({
confirmPad: ["b", "ls", "rs"],
previous: ["u", "l", "lb", "lt", "lsu", "lsl"],
next: ["d", "r", "rb", "rt", "lsd", "lsr"],
back: ["start", "a"]
}, this.keyPressed.bind(this))
pageEvents.send("custom-songs")
}
getElement(name){
return loader.screen.getElementsByClassName(name)[0]
}
setAltText(element, text){
element.innerText = text
element.setAttribute("alt", text)
}
localFolder(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked || this.mode !== "main"){
return
}
this.changeSelected(this.linkLocalFolder)
if(typeof showDirectoryPicker === "function" && !(/\bOPR\/|\bOPRGX\//.test(navigator.userAgent))){
return showDirectoryPicker().then(file => {
this.walkFilesystem(file).then(files => this.importLocal(files)).then(input => {
if(input){
db.setItem("customFolder", [file])
}
}).catch(e => {
if(e !== "cancel"){
return Promise.reject(e)
}
})
}, () => {})
}else{
this.browse.click()
}
}
browseChange(event){
var files = []
for(var i = 0; i < event.target.files.length; i++){
files.push(new LocalFile(event.target.files[i]))
}
this.importLocal(files)
}
walkFilesystem(file, path="", output=[]){
if(file.kind === "directory"){
return filePermission(file).then(file => {
var values = file.values()
var walkValues = () => values.next().then(generator => {
if(generator.done){
return output
}
return this.walkFilesystem(generator.value, path + file.name + "/", output).then(() => walkValues())
})
return walkValues()
}, () => Promise.resolve())
}else{
output.push(new FilesystemFile(file, path + file.name))
return Promise.resolve(output)
}
}
filesDropped(event){
event.preventDefault()
this.dropzone.classList.remove("dragover")
this.dragging = false
if(this.locked){
return
}
var allFiles = []
var dropPromises = []
var dbItems = []
for(var i = 0; i < event.dataTransfer.items.length; i++){
var item = event.dataTransfer.items[i]
let promise
if(this.fileSystem){
promise = item.getAsFileSystemHandle().then(file => {
dbItems.push(file)
return this.walkFilesystem(file)
})
}else{
var entry = item.webkitGetAsEntry()
if(entry){
promise = this.walkEntry(entry)
}
}
if(promise){
dropPromises.push(promise.then(files => {
allFiles = allFiles.concat(files)
}))
}
}
Promise.all(dropPromises).then(() => this.importLocal(allFiles)).then(input => {
if(input && dbItems.length){
db.setItem("customFolder", dbItems)
}
})
}
walkEntry(entry, path="", output=[]){
return new Promise(resolve => {
if(entry.isFile){
entry.file(file => {
output.push(new LocalFile(file, path + file.name))
return resolve()
}, resolve)
}else if(entry.isDirectory){
var dirReader = entry.createReader()
dirReader.readEntries(entries => {
var dirPromises = []
for(var i = 0; i < entries.length; i++){
dirPromises.push(this.walkEntry(entries[i], path + entry.name + "/", output))
}
return Promise.all(dirPromises).then(resolve)
}, resolve)
}else{
return resolve()
}
}).then(() => output)
}
importLocal(files){
if(!files.length){
if(this.noPage){
return Promise.reject("cancel")
}else{
return Promise.resolve("cancel")
}
}
this.locked = true
this.loading(true)
var importSongs = new ImportSongs()
return importSongs.load(files).then(this.songsLoaded.bind(this), e => {
if(!this.noPage){
this.browse.form.reset()
}
this.locked = false
this.loading(false)
if(e === "nosongs"){
this.showError(strings.customSongs.noSongs, "nosongs")
}else if(e !== "cancel"){
return Promise.reject(e)
}
return false
})
}
gdriveFolder(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked || this.mode !== "main"){
return
}
this.changeSelected(this.linkGdriveFolder)
this.locked = true
this.loading(true)
var importSongs = new ImportSongs(true)
if(!gpicker){
var gpickerPromise = loader.loadScript("src/js/gpicker.js").then(() => {
gpicker = new Gpicker()
})
}else{
var gpickerPromise = Promise.resolve()
}
gpickerPromise.then(() => {
return gpicker.browse(locked => {
this.locked = locked
this.loading(locked)
}, error => {
this.showError(error)
})
}).then(files => importSongs.load(files))
.then(this.songsLoaded.bind(this))
.catch(e => {
this.locked = false
this.loading(false)
if(e === "nosongs"){
this.showError(strings.customSongs.noSongs, "nosongs")
}else if(e !== "cancel"){
return Promise.reject(e)
}
}).finally(() => {
if(this.linkGdriveAccount){
var addRemove = !gpicker || !gpicker.oauthToken ? "add" : "remove"
this.linkGdriveAccount.classList[addRemove]("hiddenbtn")
}
})
}
gdriveAccount(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked || this.mode !== "main"){
return
}
this.changeSelected(this.linkGdriveAccount)
this.locked = true
this.loading(true)
if(!gpicker){
var gpickerPromise = loader.loadScript("/src/js/gpicker.js").then(() => {
gpicker = new Gpicker()
})
}else{
var gpickerPromise = Promise.resolve()
}
gpickerPromise.then(() => {
return gpicker.switchAccounts(locked => {
this.locked = locked
this.loading(locked)
}, error => {
this.showError(error)
})
}).then(() => {
this.locked = false
this.loading(false)
}).catch(error => {
if(error !== "cancel"){
this.showError(error)
}
})
}
openPrivacy(event){
if(event){
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
}
if(this.locked || this.mode !== "main"){
return
}
this.changeSelected(this.linkPrivacy)
open("privacy")
}
loading(show){
if(this.noLoading){
return
}
if(show){
loader.screen.appendChild(this.loaderDiv)
}else if(this.loaderDiv.parentNode){
this.loaderDiv.parentNode.removeChild(this.loaderDiv)
}
}
songsLoaded(songs){
if(songs){
var length = songs.length
assets.songs = songs
assets.customSongs = true
assets.customSelected = this.noPage ? +localStorage.getItem("customSelected") : 0
}
if(this.noPage){
pageEvents.send("import-songs", length)
}else{
assets.sounds["se_don"].play()
setTimeout(() => {
new SongSelect("customSongs", false, this.touchEnabled)
pageEvents.send("import-songs", length)
}, 500)
}
this.clean()
return songs && songs.length
}
keyPressed(pressed, name){
if(!pressed || this.locked){
return
}
var selected = this.items[this.selected]
if(this.mode === "main"){
if(name === "confirm" || name === "confirmPad"){
if(selected === this.endButton){
this.onEnd(null, true)
}else if(name !== "confirmPad"){
if(selected === this.linkLocalFolder){
assets.sounds["se_don"].play()
this.localFolder()
}else if(selected === this.linkGdriveFolder){
assets.sounds["se_don"].play()
this.gdriveFolder()
}else if(selected === this.linkGdriveAccount){
assets.sounds["se_don"].play()
this.gdriveAccount()
}else if(selected === this.linkPrivacy){
assets.sounds["se_don"].play()
this.openPrivacy()
}
}
}else if(name === "previous" || name === "next"){
selected.classList.remove("selected")
do{
this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1))
}while(this.items[this.selected] === this.linkPrivacy && name !== "previous")
this.items[this.selected].classList.add("selected")
assets.sounds["se_ka"].play()
}else if(name === "back" || name === "backEsc"){
if(!this.dragging || name !== "backEsc"){
this.onEnd()
}
}
}else if(this.mode === "error"){
if(name === "confirm" || name === "confirmPad" || name === "back" || name === "backEsc"){
this.hideError(name === "confirm" || name === "confirmPad")
}
}
}
changeSelected(button){
var selected = this.items[this.selected]
if(selected !== button){
selected.classList.remove("selected")
this.selected = this.items.findIndex(item => item === button)
this.items[this.selected].classList.add("selected")
}
}
mod(length, index){
return ((index % length) + length) % length
}
onEnd(event, confirm){
if(this.locked || this.mode !== "main"){
return
}
var touched = false
if(event){
if(event.type === "touchstart"){
event.preventDefault()
touched = true
}else if(event.which !== 1){
return
}
}else{
touched = this.touchEnabled
}
this.clean()
if(!this.noPage){
assets.sounds[confirm ? "se_don" : "se_cancel"].play()
}
return new Promise(resolve => setTimeout(() => {
new SongSelect("customSongs", false, touched)
resolve()
}, 500))
}
showError(text, errorName){
this.locked = false
this.loading(false)
if(this.noPage){
var error = new Error(text)
error.name = errorName
throw error
}else if(this.mode === "error"){
return
}
this.mode = "error"
this.errorContent.innerText = text
this.errorDiv.style.display = "flex"
assets.sounds["se_pause"].play()
}
hideError(confirm){
if(this.mode !== "error"){
return
}
this.mode = "main"
this.errorDiv.style.display = ""
assets.sounds[confirm ? "se_don" : "se_cancel"].play()
}
clean(){
delete this.loaderDiv
if(this.noPage){
return
}
this.keyboard.clean()
this.gamepad.clean()
pageEvents.remove(this.browse, "change")
if(this.hasLocal){
pageEvents.remove(this.linkLocalFolder, ["mousedown", "touchstart"])
}
if(gameConfig.google_credentials.gdrive_enabled){
pageEvents.remove(this.linkGdriveFolder, ["mousedown", "touchstart"])
pageEvents.remove(this.linkGdriveAccount, ["mousedown", "touchstart"])
pageEvents.remove(this.linkPrivacy, ["mousedown", "touchstart"])
}
pageEvents.remove(this.endButton, ["mousedown", "touchstart"])
pageEvents.remove(this.errorDiv, ["mousedown", "touchstart"])
pageEvents.remove(this.errorEnd, ["mousedown", "touchstart"])
if(DataTransferItem.prototype.webkitGetAsEntry){
pageEvents.remove(document, ["dragenter", "dragover", "dragleave", "drop"])
delete this.dropzone
delete this.dragTarget
}
if(gpicker){
gpicker.tokenResolve = null
}
delete this.browse
delete this.linkLocalFolder
delete this.linkGdriveFolder
delete this.linkGdriveAccount
delete this.linkPrivacy
delete this.endButton
delete this.items
delete this.errorDiv
delete this.errorContent
delete this.errorEnd
}
}

491
public/src/js/debug.js Normal file
View File

@@ -0,0 +1,491 @@
class Debug{
constructor(...args){
this.init(...args)
}
init(){
if(!assets.pages["debug"]){
return
}
this.debugDiv = document.createElement("div")
this.debugDiv.id = "debug"
this.debugDiv.innerHTML = assets.pages["debug"]
document.body.appendChild(this.debugDiv)
this.titleDiv = this.byClass("title")
this.minimiseDiv = this.byClass("minimise")
this.offsetDiv = this.byClass("offset")
this.measureNumDiv = this.byClass("measure-num")
this.branchHideDiv = this.byClass("branch-hide")
this.branchSelectDiv = this.byClass("branch-select")
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")
this.autoplayCheckbox = this.byClass("autoplay")
this.restartBtn = this.byClass("restart-btn")
this.exitBtn = this.byClass("exit-btn")
this.moving = false
this.windowSymbol = Symbol()
pageEvents.add(window, ["mousedown", "mouseup", "touchstart", "touchend", "blur", "resize"], this.stopMove.bind(this), this.windowSymbol)
pageEvents.mouseAdd(this, this.onMove.bind(this))
pageEvents.add(window, "touchmove", this.onMove.bind(this))
pageEvents.add(this.titleDiv, ["mousedown", "touchstart"], this.startMove.bind(this))
pageEvents.add(this.minimiseDiv, ["click", "touchstart"], this.minimise.bind(this))
pageEvents.add(this.restartBtn, ["click", "touchstart"], this.restartSong.bind(this))
pageEvents.add(this.exitBtn, ["click", "touchstart"], this.clean.bind(this))
pageEvents.add(this.restartLabel, "touchstart", this.touchBox.bind(this))
pageEvents.add(this.autoplayLabel, "touchstart", this.touchBox.bind(this))
pageEvents.add(this.autoplayCheckbox, "change", this.toggleAutoplay.bind(this))
pageEvents.add(this.branchSelect, "change", this.branchChange.bind(this))
pageEvents.add(this.branchResetBtn, ["click", "touchstart"], this.branchReset.bind(this))
this.offsetSlider = new InputSlider(this.offsetDiv, -60, 60, 3)
this.offsetSlider.onchange(this.offsetChange.bind(this))
this.measureNumSlider = new InputSlider(this.measureNumDiv, 0, 1000, 0)
this.measureNumSlider.onchange(this.measureNumChange.bind(this))
this.measureNumSlider.set(0)
this.volumeSlider = new InputSlider(this.volumeDiv, 0, 3, 2)
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()
pageEvents.send("debug")
}
byClass(name){
return this.debugDiv.getElementsByClassName(name)[0]
}
startMove(event){
if(event.which === 1 || event.type === "touchstart"){
event.stopPropagation()
var divPos = this.debugDiv.getBoundingClientRect()
var click = event.type === "touchstart" ? event.changedTouches[0] : event
var x = click.pageX - divPos.left
var y = click.pageY - divPos.top
this.moving = {x: x, y: y}
}
}
onMove(event){
if(this.moving){
var click = event.type === "touchmove" ? event.changedTouches[0] : event
var x = click.clientX - this.moving.x
var y = click.clientY - this.moving.y
this.moveTo(x, y)
}
}
stopMove(event){
if(this.debugDiv.style.display === "none"){
return
}
if(!event || event.type === "resize"){
var divPos = this.debugDiv.getBoundingClientRect()
var x = divPos.left
var y = divPos.top
}else{
var click = event.type === "touchstart" || event.type === "touchend" ? event.changedTouches[0] : event
if(event.type == "blur"){
var x = this.moving.x
var y = this.moving.y
}else{
var x = click.clientX - this.moving.x
var y = click.clientY - this.moving.y
}
}
var w = this.debugDiv.offsetWidth
var h = this.debugDiv.offsetHeight
if(x + w > innerWidth){
x = innerWidth - w
}
if(y + h > lastHeight){
y = lastHeight - h
}
if(x < 0){
x = 0
}
if(y < 0){
y = 0
}
this.moveTo(x, y)
this.moving = false
}
moveTo(x, y){
this.debugDiv.style.transform = "translate(" + x + "px, " + y + "px)"
}
restore(){
debugObj.state = "open"
this.debugDiv.style.display = ""
this.stopMove()
}
minimise(){
debugObj.state = "minimised"
this.debugDiv.style.display = "none"
}
updateStatus(){
if(debugObj.controller && !this.controller){
this.controller = debugObj.controller
this.restartBtn.style.display = "block"
this.autoplayLabel.style.display = "block"
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
if(this.songHash === selectedSong.hash){
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 > 0 && measures.length >= this.measureNum){
var measureMS = measures[this.measureNum - 1].ms
var game = this.controller.game
game.started = true
var timestamp = Date.now()
var currentDate = timestamp - measureMS
game.startDate = currentDate
game.sndTime = timestamp - snd.buffer.getTime() * 1000
var circles = game.songData.circles
for(var i in circles){
game.currentCircle = i
if(circles[i].endTime >= measureMS){
break
}
game.skipNote(circles[i])
}
if(game.mainMusicPlaying){
game.mainMusicPlaying = false
game.mainAsset.stop()
}
}
this.autoplayCheckbox.checked = this.controller.autoPlayEnabled
}
if(this.controller && !debugObj.controller){
this.restartBtn.style.display = ""
this.autoplayLabel.style.display = ""
this.branchHideDiv.style.display = ""
this.lyricsHideDiv.style.display = ""
this.controller = null
}
this.stopMove()
}
offsetChange(value, noRestart){
if(this.controller){
var offset = (this.defaultOffset - value) * 1000
var songData = this.controller.parsedSongData
songData.circles.forEach(circle => {
circle.ms = circle.originalMS + offset
circle.endTime = circle.originalEndTime + offset
})
songData.measures.forEach(measure => {
measure.ms = measure.originalMS + offset
})
if(songData.branches){
songData.branches.forEach(branch => {
branch.ms = branch.originalMS + offset
})
}
if(this.controller.lyrics){
this.controller.lyrics.offsetChange(value * 1000)
}
if(this.restartCheckbox.checked && !noRestart){
this.restartSong()
}
}
}
measureNumChange(value){
this.measureNum = value
if(this.restartCheckbox.checked){
this.restartSong()
}
}
volumeChange(value, noRestart){
if(this.controller){
snd.musicGain.setVolumeMul(value)
}
if(this.restartCheckbox.checked && !noRestart){
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()
}
}
toggleAutoplay(event){
if(this.controller){
this.controller.autoPlayEnabled = this.autoplayCheckbox.checked
if(this.controller.autoPlayEnabled){
this.controller.saveScore = false
}else{
var keyboard = debugObj.controller.keyboard
keyboard.setKey(false, "don_l")
keyboard.setKey(false, "don_r")
keyboard.setKey(false, "ka_l")
keyboard.setKey(false, "ka_r")
}
}
}
branchChange(event, noRestart){
if(this.controller){
var game = this.controller.game
var name = this.branchSelect.value
game.branch = name === "auto" ? false : name
game.branchSet = name === "auto"
if(noRestart){
game.branchStatic = true
}
var selectedOption = this.branchSelect.selectedOptions[0]
this.branchSelect.style.background = selectedOption.style.background
if(this.restartCheckbox.checked && !noRestart){
this.restartSong()
}
}
}
branchReset(event, noRestart){
this.branchSelect.value = "auto"
this.branchChange(null, noRestart)
}
touchBox(event){
event.currentTarget.click()
}
clean(){
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)
pageEvents.remove(window, "touchmove")
pageEvents.remove(this.titleDiv, ["mousedown", "touchstart"])
pageEvents.remove(this.minimiseDiv, ["click", "touchstart"])
pageEvents.remove(this.restartBtn, ["click", "touchstart"])
pageEvents.remove(this.exitBtn, ["click", "touchstart"])
pageEvents.remove(this.restartLabel, "touchstart")
pageEvents.remove(this.autoplayLabel, "touchstart")
pageEvents.remove(this.autoplayCheckbox, "change")
pageEvents.remove(this.branchSelect, "change")
pageEvents.remove(this.branchResetBtn, ["click", "touchstart"])
delete this.offsetSlider
delete this.measureNumSlider
delete this.volumeSlider
delete this.titleDiv
delete this.minimiseDiv
delete this.offsetDiv
delete this.measureNumDiv
delete this.branchHideDiv
delete this.branchSelectDiv
delete this.branchSelect
delete this.branchResetBtn
delete this.volumeDiv
delete this.lyricsHideDiv
delete this.lyricsOffsetDiv
delete this.restartCheckbox
delete this.autoplayLabel
delete this.autoplayCheckbox
delete this.restartBtn
delete this.exitBtn
delete this.controller
delete this.windowSymbol
debugObj.state = "closed"
debugObj.debug = null
document.body.removeChild(this.debugDiv)
delete this.debugDiv
}
}
class InputSlider{
constructor(...args){
this.init(...args)
}
init(sliderDiv, min, max, fixedPoint){
this.fixedPoint = fixedPoint
this.mul = Math.pow(10, fixedPoint)
this.min = min * this.mul
this.max = max * this.mul
this.input = sliderDiv.getElementsByTagName("input")[0]
this.reset = sliderDiv.getElementsByClassName("reset")[0]
this.plus = sliderDiv.getElementsByClassName("plus")[0]
this.minus = sliderDiv.getElementsByClassName("minus")[0]
this.value = null
this.defaultValue = null
this.callbacks = []
this.touchEnd = []
this.windowSymbol = Symbol()
pageEvents.add(this.input, ["touchstart", "touchend"], event => {
event.stopPropagation()
})
pageEvents.add(window, ["mouseup", "touchstart", "touchend", "blur"], event => {
if(event.type !== "touchstart"){
this.touchEnd.forEach(func => func(event))
}else if(event.target !== this.input){
this.input.blur()
}
}, this.windowSymbol)
this.addTouchRepeat(this.plus, this.add.bind(this))
this.addTouchRepeat(this.minus, this.subtract.bind(this))
this.addTouch(this.reset, this.resetValue.bind(this))
pageEvents.add(this.input, "change", this.manualSet.bind(this))
pageEvents.add(this.input, "keydown", this.captureKeys.bind(this))
}
update(noCallback, force){
var oldValue = this.input.value
if(this.value === null){
this.input.value = ""
this.input.readOnly = true
}else{
if(this.value > this.max){
this.value = this.max
}
if(this.value < this.min){
this.value = this.min
}
this.input.value = this.get().toFixed(this.fixedPoint)
this.input.readOnly = false
}
if(force || !noCallback && oldValue !== this.input.value){
this.callbacks.forEach(callback => {
callback(this.get())
})
}
}
set(number){
this.value = Math.floor(number * this.mul)
this.defaultValue = this.value
this.update(true)
}
setMinMax(min, max){
this.min = min
this.max = max
this.update()
}
get(){
if(this.value === null){
return null
}else{
return Math.floor(this.value) / this.mul
}
}
add(event){
if(this.value !== null){
var newValue = this.value + this.eventNumber(event)
if(newValue <= this.max){
this.value = newValue
this.update()
}
}
}
subtract(event){
if(this.value !== null){
var newValue = this.value - this.eventNumber(event)
if(newValue >= this.min){
this.value = newValue
this.update()
}
}
}
eventNumber(event){
return (event.ctrlKey ? 10 : 1) * (event.shiftKey ? 10 : 1) * (event.altKey ? 10 : 1) * 1
}
resetValue(){
this.value = this.defaultValue
this.update()
}
onchange(callback){
this.callbacks.push(callback)
}
manualSet(){
var number = parseFloat(this.input.value) * this.mul
if(Number.isFinite(number) && this.min <= number && number <= this.max){
this.value = number
}
this.update(false, true)
}
captureKeys(event){
event.stopPropagation()
}
addTouch(element, callback){
pageEvents.add(element, ["mousedown", "touchstart"], event => {
if(event.type === "touchstart"){
event.preventDefault()
}else if(event.which !== 1){
return
}
callback(event)
})
}
addTouchRepeat(element, callback){
this.addTouch(element, event => {
var active = true
var func = () => {
active = false
this.touchEnd.splice(this.touchEnd.indexOf(func), 1)
}
this.touchEnd.push(func)
var repeat = delay => {
if(active && this.touchEnd){
callback(event)
setTimeout(() => repeat(50), delay)
}
}
repeat(400)
})
}
removeTouch(element){
pageEvents.remove(element, ["mousedown", "touchstart"])
}
clean(){
this.removeTouch(this.plus)
this.removeTouch(this.minus)
this.removeTouch(this.reset)
pageEvents.remove(this.input, ["touchstart", "touchend"])
pageEvents.remove(window, ["mouseup", "touchstart", "touchend", "blur"], this.windowSymbol)
pageEvents.remove(this.input, ["touchstart", "change", "keydown"])
delete this.input
delete this.reset
delete this.plus
delete this.minus
delete this.windowSymbol
delete this.touchEnd
}
}

862
public/src/js/game.js Normal file
View File

@@ -0,0 +1,862 @@
class Game{
constructor(...args){
this.init(...args)
}
init(controller, selectedSong, songData){
this.controller = controller
this.selectedSong = selectedSong
this.songData = songData
this.elapsedTime = 0
this.currentCircle = -1
this.currentEvent = 0
this.updateCurrentCircle()
this.combo = 0
this.rules = new GameRules(this)
this.globalScore = {
points: 0,
good: 0,
ok: 0,
bad: 0,
maxCombo: 0,
drumroll: 0,
gauge: 0,
title: selectedSong.title,
difficulty: this.rules.difficulty
}
var combo = this.songData.circles.filter(circle => {
var type = circle.type
return (type === "don" || type === "ka" || type === "daiDon" || type === "daiKa") && (!circle.branch || circle.branch.active)
}).length
this.soulPoints = this.rules.soulPoints(combo)
this.paused = false
this.started = false
this.mainMusicPlaying = false
this.musicFadeOut = 0
this.fadeOutStarted = false
this.currentTimingPoint = 0
this.branchNames = ["normal", "advanced", "master"]
this.resetSection()
this.gameLagSync = !this.controller.touchEnabled && !(/Firefox/.test(navigator.userAgent))
assets.songs.forEach(song => {
if(song.id == selectedSong.folder){
this.mainAsset = song.sound
}
})
}
run(){
this.timeForDistanceCircle = 2500
this.initTiming()
this.view = this.controller.view
}
initTiming(){
// Date when the chrono is started (before the game begins)
var firstCircle = this.songData.circles[0]
if(this.controller.calibrationMode){
var offsetTime = 0
}else{
var offsetTime = Math.max(0, this.timeForDistanceCircle - (firstCircle ? firstCircle.ms : 0)) |0
}
if(this.controller.multiplayer){
var syncWith = this.controller.syncWith
var syncCircles = syncWith.game.songData.circles
var syncOffsetTime = Math.max(0, this.timeForDistanceCircle - syncCircles[0].ms) |0
offsetTime = Math.max(offsetTime, syncOffsetTime)
}
this.elapsedTime = -offsetTime
// The real start for the game will start when chrono will reach 0
this.startDate = Date.now() + offsetTime
}
update(){
this.updateTime()
// Main operations
this.updateCirclesStatus()
this.checkPlays()
// Event operations
this.whenFadeoutMusic()
if(this.controller.multiplayer !== 2){
this.whenLastCirclePlayed()
}
}
getCircles(){
return this.songData.circles
}
updateCirclesStatus(){
var nextSet = false
var ms = this.elapsedTime
var circles = this.songData.circles
var startIndex = this.currentCircle === 0 ? 0 : this.currentCircle - 1
var index = 0
for(var i = startIndex; i < circles.length; i++){
var circle = circles[i]
if(circle && (!circle.branch || circle.branch.active) && !circle.isPlayed){
var type = circle.type
var drumrollNotes = type === "balloon" || type === "drumroll" || type === "daiDrumroll"
var endTime = circle.endTime + (drumrollNotes ? 0 : this.rules.bad) + this.controller.audioLatency
if(ms >= circle.ms + this.controller.audioLatency){
if(drumrollNotes && !circle.rendaPlayed && ms < endTime + this.controller.audioLatency){
circle.rendaPlayed = true
if(this.rules.difficulty === "easy"){
assets.sounds["v_renda" + this.controller.snd].stop()
this.controller.playSound("v_renda")
}
}
}
if(circle.daiFailed && (ms >= circle.daiFailed.ms + this.rules.daiLeniency || ms > endTime)){
this.checkScore(circle, circle.daiFailed.check)
}else if(ms > endTime){
if(!this.controller.autoPlayEnabled){
if(drumrollNotes){
if(circle.section && circle.timesHit === 0){
this.resetSection()
}
circle.played(-1, false)
this.updateCurrentCircle()
if(this.controller.multiplayer === 1){
var value = {
pace: (ms - circle.ms - this.controller.audioLatency) / circle.timesHit
}
if(type === "drumroll" || type === "daiDrumroll"){
value.kaAmount = circle.timesKa / circle.timesHit
}
p2.send("drumroll", value)
}
}else{
this.skipNote(circle)
this.updateCurrentCircle()
}
}
}else if(!this.controller.autoPlayEnabled && !nextSet){
nextSet = true
this.currentCircle = i
}
if(index++ > 1){
break
}
}
}
var branches = this.songData.branches
if(branches){
var force = this.controller.multiplayer === 2 ? p2 : this
var measures = this.songData.measures
if(this.controller.multiplayer === 2 || force.branch){
if(!force.branchSet){
force.branchSet = true
if(branches.length){
this.setBranch(branches[0], force.branch)
}
var view = this.controller.view
var currentMeasure = view.branch
for(var i = measures.length; i--;){
var measure = measures[i]
if(measure.nextBranch && measure.ms <= ms){
currentMeasure = measure.nextBranch.active
}
}
if(view.branch !== currentMeasure){
if(!this.branchStatic){
view.branchAnimate = {
ms: ms,
fromBranch: view.branch
}
}
this.branchStatic = false
view.branch = currentMeasure
}
}
}
for(var i = 0; i < measures.length; i++){
var measure = measures[i]
if(measure.ms > ms){
break
}else{
if(measure.nextBranch && !measure.gameChecked){
measure.gameChecked = true
var branch = measure.nextBranch
if(branch.type){
var accuracy = 0
if(branch.type === "drumroll"){
if(force.branch){
var accuracy = Math.max(0, branch.requirement[force.branch])
}else{
var accuracy = this.sectionDrumroll
}
}else if(this.sectionNotes.length !== 0){
if(force.branch){
var accuracy = Math.max(0, Math.min(100, branch.requirement[force.branch]))
}else{
var accuracy = this.sectionNotes.reduce((a, b) => a + b) / this.sectionNotes.length * 100
}
}
if(accuracy >= branch.requirement.master){
this.setBranch(branch, "master")
}else if(accuracy >= branch.requirement.advanced){
this.setBranch(branch, "advanced")
}else{
this.setBranch(branch, "normal")
}
}else if(this.controller.multiplayer === 1){
p2.send("branch", "normal")
}
}
if(this.controller.lyrics){
if(!measure.branch){
this.controller.lyrics.branch = null
}else if(measure.branch.active){
this.controller.lyrics.branch = measure.branch.name
}
}
}
}
}
if(this.controller.lyrics){
this.controller.lyrics.update(ms)
}
}
fixNoteStream(keysDon){
var circleIsNote = circle => {
var type = circle.type
return type === "don" || type === "ka" || type === "daiDon" || type === "daiKa"
}
var correctNote = circle => {
var type = circle.type
return keysDon ? (type === "don" || type === "daiDon") : (type === "ka" || type === "daiKa")
}
var ms = this.elapsedTime
var circles = this.songData.circles
for(var i = this.currentCircle + 1; i < circles.length; i++){
var circle = circles[i]
var relative = ms - circle.ms - this.controller.audioLatency
if(!circle.branch || circle.branch.active){
if((!circleIsNote(circle) || relative < -this.rules.bad)){
break
}else if(Math.abs(relative) < this.rules.ok && correctNote(circle)){
for(var j = this.currentCircle; j < i; j++){
var circle = circles[j]
if(circle && !circle.branch || circle.branch.active){
this.skipNote(circles[j])
}
}
this.currentCircle = i
return circles[i]
}
}
}
}
skipNote(circle){
if(circle.section){
this.resetSection()
}
circle.played(-1, circle.type === "daiDon" || circle.type === "daiKa")
this.sectionNotes.push(0)
this.controller.displayScore(0, true)
this.updateCombo(0)
this.updateGlobalScore(0, 1)
if(this.controller.multiplayer === 1){
p2.send("note", {
score: -1
})
}
}
checkPlays(){
var circles = this.songData.circles
var circle = circles[this.currentCircle]
if(this.controller.autoPlayEnabled){
while(circle && this.controller.autoPlay(circle)){
circle = circles[this.currentCircle]
}
return
}
var keys = this.controller.getKeys()
var don_l = keys["don_l"] && !this.controller.isWaiting("don_l", "score")
var don_r = keys["don_r"] && !this.controller.isWaiting("don_r", "score")
var ka_l = keys["ka_l"] && !this.controller.isWaiting("ka_l", "score")
var ka_r = keys["ka_r"] && !this.controller.isWaiting("ka_r", "score")
var checkDon = () => {
if(don_l && don_r){
this.checkKey(["don_l", "don_r"], circle, "daiDon")
}else if(don_l){
this.checkKey(["don_l"], circle, "don")
}else if(don_r){
this.checkKey(["don_r"], circle, "don")
}
}
var checkKa = () => {
if(ka_l && ka_r){
this.checkKey(["ka_l", "ka_r"], circle, "daiKa")
}else if(ka_l){
this.checkKey(["ka_l"], circle, "ka")
}else if(ka_r){
this.checkKey(["ka_r"], circle, "ka")
}
}
var keyTime = this.controller.getKeyTime()
if(keyTime["don"] >= keyTime["ka"]){
checkDon()
checkKa()
}else{
checkKa()
checkDon()
}
}
checkKey(keyCodes, circle, check){
if(circle && !circle.isPlayed){
if(!this.checkScore(circle, check)){
return
}
}
keyCodes.forEach(keyCode => {
this.controller.waitForKeyup(keyCode, "score")
})
}
checkScore(circle, check){
var ms = this.elapsedTime
var type = circle.type
var keysDon = check === "don" || check === "daiDon"
var keysKa = check === "ka" || check === "daiKa"
var keyDai = check === "daiDon" || check === "daiKa"
var typeDon = type === "don" || type === "daiDon"
var typeKa = type === "ka" || type === "daiKa"
var typeDai = type === "daiDon" || type === "daiKa"
var keyTime = this.controller.getKeyTime()
var currentTime = circle.daiFailed ? circle.daiFailed.ms : keysDon ? keyTime["don"] : keyTime["ka"]
var relative = currentTime - circle.ms - this.controller.audioLatency
if(relative >= this.rules.ok){
var fixedNote = this.fixNoteStream(keysDon)
if(fixedNote){
return this.checkScore(fixedNote, check)
}
}
if(typeDon || typeKa){
if(-this.rules.bad >= relative || relative >= this.rules.bad){
return true
}
var score = 0
if(keysDon && typeDon || keysKa && typeKa){
var circleStatus = -1
relative = Math.abs(relative)
if(relative < this.rules.good){
circleStatus = 450
}else if(relative < this.rules.ok){
circleStatus = 230
}else if(relative < this.rules.bad){
circleStatus = 0
}
if(typeDai && !keyDai){
if(this.controller.easierBigNotes){
// Taiko Force Lv5 can't hit both Dons at the same time, so dai offered
keyDai = true
}else if(!circle.daiFailed){
circle.daiFailed = {
ms: ms,
status: circleStatus,
check: check
}
return false
}else if(ms < circle.daiFailed.ms + this.rules.daiLeniency){
return false
}else{
circleStatus = circle.daiFailed.status
}
}
if(circleStatus === 230 || circleStatus === 450){
score = circleStatus
}
circle.played(score, score === 0 ? typeDai : keyDai)
this.controller.displayScore(score, false, typeDai && keyDai)
}else{
var keyTime = this.controller.getKeyTime()
var keyTimeRelative = Math.abs(keyTime.don - keyTime.ka)
if(Math.abs(relative) >= (keyTimeRelative <= 25 ? this.rules.bad : this.rules.good)){
return true
}
circle.played(-1, typeDai)
this.controller.displayScore(score, true, false)
}
this.updateCombo(score)
this.updateGlobalScore(score, typeDai && keyDai ? 2 : 1, circle.gogoTime)
this.updateCurrentCircle()
if(circle.section){
this.resetSection()
}
this.sectionNotes.push(score === 450 ? 1 : (score === 230 ? 0.5 : 0))
if(this.controller.multiplayer === 1){
var value = {
score: score,
ms: circle.ms - currentTime - this.controller.audioLatency,
dai: typeDai ? (keyDai ? 2 : 1) : 0
}
if((!keysDon || !typeDon) && (!keysKa || !typeKa)){
value.reverse = true
}
p2.send("note", value)
}
}else{
if(circle.ms + this.controller.audioLatency > currentTime || currentTime > circle.endTime + this.controller.audioLatency){
return true
}
if(keysDon && type === "balloon"){
this.checkBalloon(circle)
if(check === "daiDon" && !circle.isPlayed){
this.checkBalloon(circle)
}
}else if((keysDon || keysKa) && (type === "drumroll" || type === "daiDrumroll")){
this.checkDrumroll(circle, keysKa)
if(keyDai){
this.checkDrumroll(circle, keysKa)
}
}
}
return true
}
checkBalloon(circle){
if(circle.timesHit >= circle.requiredHits - 1){
var score = 5000
this.updateCurrentCircle()
circle.hit()
circle.played(score)
if(this.controller.multiplayer == 1){
p2.send("drumroll", {
pace: (this.elapsedTime - circle.ms + this.controller.audioLatency) / circle.timesHit
})
}
}else{
var score = 300
circle.hit()
}
this.globalScore.drumroll++
this.sectionDrumroll++
this.globalScore.points += score
this.view.setDarkBg(false)
}
checkDrumroll(circle, keysKa){
var ms = this.elapsedTime
var dai = circle.type === "daiDrumroll"
var score = 100
if(circle.section && circle.timesHit === 0){
this.resetSection()
}
circle.hit(keysKa)
var keyTime = this.controller.getKeyTime()
if(circle.type === "drumroll"){
var sound = keyTime["don"] > keyTime["ka"] ? "don" : "ka"
}else{
var sound = keyTime["don"] > keyTime["ka"] ? "daiDon" : "daiKa"
}
var circleAnim = new Circle({
id: 0,
start: ms,
type: sound,
txt: "",
speed: circle.speed,
gogoTime: circle.gogoTime,
fixedPos: document.hasFocus()
})
circleAnim.played(score, dai)
circleAnim.animate(ms)
this.view.drumroll.push(circleAnim)
this.globalScore.drumroll++
this.sectionDrumroll++
this.globalScore.points += score * (dai ? 2 : 1)
this.view.setDarkBg(false)
}
getLastCircle(circles){
for(var i = circles.length; i--;){
return circles[i]
}
}
whenLastCirclePlayed(){
var ms = this.elapsedTime
if(!this.lastCircle){
var circles = this.songData.circles
var circle = this.getLastCircle(circles)
this.lastCircle = circle ? circle.endTime : 0
if(this.controller.multiplayer){
var syncWith = this.controller.syncWith
var syncCircles = syncWith.game.songData.circles
circle = this.getLastCircle(syncCircles)
var syncLastCircle = circle ? circle.endTime : 0
if(syncLastCircle > this.lastCircle){
this.lastCircle = syncLastCircle
}
}
}
if(!this.fadeOutStarted && ms >= this.lastCircle + 2000 + this.controller.audioLatency){
this.fadeOutStarted = ms
if(this.controller.multiplayer){
this.controller.syncWith.game.fadeOutStarted = ms
}
}
}
whenFadeoutMusic(){
var started = this.fadeOutStarted
if(started){
var ms = this.elapsedTime
var duration = this.mainAsset ? this.mainAsset.duration : 0
var musicDuration = duration * 1000 - this.controller.offset
if(this.musicFadeOut === 0){
if(this.controller.multiplayer === 1){
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){
this.controller.gameEnded()
if(!p2.session && this.controller.multiplayer === 1){
p2.send("gameend")
}
this.musicFadeOut++
}else if(this.musicFadeOut === 2 && (ms >= Math.max(started + 8600, Math.min(started + 8600 + 5000, musicDuration + 250)))){
this.controller.displayResults()
this.musicFadeOut++
}else if(this.musicFadeOut === 3 && (ms >= Math.max(started + 9600, Math.min(started + 9600 + 5000, musicDuration + 1250)))){
this.controller.clean()
if(this.controller.scoresheet){
this.controller.scoresheet.startRedraw()
}
}
}
}
playMainMusic(){
var ms = this.elapsedTime + this.controller.offset
if(!this.mainMusicPlaying && (!this.fadeOutStarted || ms < this.fadeOutStarted + 1600)){
if(this.calibrationState === "audio"){
var beatInterval = this.controller.view.beatInterval
var startAt = ms % beatInterval
var duration = this.mainAsset.duration * 1000
if(startAt < duration){
this.mainAsset.playLoop(0, false, startAt / 1000, 0, beatInterval / 1000)
}else{
this.mainAsset.playLoop((startAt - duration) / 1000, false, 0, 0, beatInterval / 1000)
}
}else if(this.controller.multiplayer !== 2 && this.mainAsset){
this.mainAsset.play((ms < 0 ? -ms : 0) / 1000, false, Math.max(0, ms / 1000))
}
this.mainMusicPlaying = true
}
}
togglePause(forcePause, pauseMove, noSound){
if(!this.paused){
if(forcePause === false){
return
}
if(!noSound){
this.controller.playSound("se_pause", 0, true)
}
this.paused = true
this.latestDate = Date.now()
if(this.mainAsset){
this.mainAsset.stop()
}
this.mainMusicPlaying = false
this.view.pauseMove(pauseMove || 0, true)
this.view.gameDiv.classList.add("game-paused")
this.view.lastMousemove = this.view.getMS()
this.view.cursorHidden = false
pageEvents.send("pause")
}else if(!forcePause){
if(forcePause !== false && this.calibrationState && ["audioHelp", "audioComplete", "videoHelp", "videoComplete", "results"].indexOf(this.calibrationState) !== -1){
return
}
if(this.calibrationState === "audioHelp" || this.calibrationState === "videoHelp"){
this.calibrationState = this.calibrationState === "audioHelp" ? "audio" : "video"
this.controller.view.pauseOptions = strings.pauseOptions
this.controller.playSound("se_don", 0, true)
}else if(!noSound){
this.controller.playSound("se_cancel", 0, true)
}
this.paused = false
var currentDate = Date.now()
this.startDate += currentDate - this.latestDate
this.sndTime = currentDate - snd.buffer.getTime() * 1000
this.view.gameDiv.classList.remove("game-paused")
this.view.pointer()
pageEvents.send("unpause", currentDate - this.latestDate)
}
}
isPaused(){
return this.paused
}
updateTime(){
// Refreshed date
var ms = this.elapsedTime
if(ms >= 0 && !this.started){
this.startDate = Date.now()
this.elapsedTime = this.getAccurateTime()
this.started = true
this.sndTime = this.startDate - snd.buffer.getTime() * 1000
}else if(ms < 0 || ms >= 0 && this.started){
var currentDate = Date.now()
if(this.gameLagSync){
var sndTime = currentDate - snd.buffer.getTime() * 1000
var lag = sndTime - this.sndTime
if(Math.abs(lag) >= 50){
this.startDate += lag
this.sndTime = sndTime
pageEvents.send("game-lag", lag)
}
}
this.elapsedTime = currentDate - this.startDate
}
}
getAccurateTime(){
if(this.isPaused()){
return this.elapsedTime
}else{
return Date.now() - this.startDate
}
}
getCircles(){
return this.songData.circles
}
updateCurrentCircle(){
var circles = this.songData.circles
do{
var circle = circles[++this.currentCircle]
}while(circle && (circle.branch && !circle.branch.active))
}
getCurrentCircle(){
return this.currentCircle
}
updateCombo(score){
if(score !== 0){
this.combo++
}else{
this.combo = 0
}
if(this.combo > this.globalScore.maxCombo){
this.globalScore.maxCombo = this.combo
}
if(this.combo === 50 || this.combo > 0 && this.combo % 100 === 0 && this.combo <= 5000){
this.controller.playSound("v_combo_" + this.combo)
}
if (this.songData.scoremode == 2 && this.combo > 0 && this.combo % 100 == 0) {
this.globalScore.points += 10000;
}
this.view.updateCombo(this.combo)
}
getCombo(){
return this.combo
}
getGlobalScore(){
return this.globalScore
}
updateGlobalScore(score, multiplier, gogoTime){
// Circle score
switch(score){
case 450:
this.globalScore.good++
this.globalScore.gauge += this.soulPoints.good
break
case 230:
this.globalScore.ok++
this.globalScore.gauge += this.soulPoints.ok
break
case 0:
this.globalScore.bad++
this.globalScore.gauge += this.soulPoints.bad
break
}
if (this.songData.scoremode) {
switch (score) {
case 450:
score = this.songData.scoreinit;
break;
case 230:
score = Math.floor(this.songData.scoreinit / 2);
break;
}
}
// Gauge update
if(this.globalScore.gauge < 0){
this.globalScore.gauge = 0
}else if(this.globalScore.gauge > 10000){
this.globalScore.gauge = 10000
}
// Points update
if (this.songData.scoremode == 2) {
var diff_mul = 0;
if (this.combo >= 100) {
diff_mul = 8;
} else if (this.combo >= 50) {
diff_mul = 4;
} else if (this.combo >= 30) {
diff_mul = 2;
} else if (this.combo >= 10) {
diff_mul = 1;
}
score += this.songData.scorediff * diff_mul;
} else {
score += Math.max(0, Math.floor((Math.min(this.combo, 100) - 1) / 10) * (this.songData.scoremode ? this.songData.scorediff : 100));
}
if(gogoTime){
multiplier *= 1.2
}
this.globalScore.points += Math.floor(score * multiplier / 10) * 10
}
setBranch(currentBranch, activeName){
var pastActive = currentBranch.active
var ms = currentBranch.ms
for(var i = 0; i < this.songData.branches.length; i++){
var branch = this.songData.branches[i]
if(branch.ms >= ms){
var relevantName = activeName
var req = branch.requirement
var noNormal = req.advanced <= 0
var noAdvanced = req.master <= 0 || req.advanced >= req.master || branch.type === "accuracy" && req.advanced > 100
var noMaster = branch.type === "accuracy" && req.master > 100
if(relevantName === "normal" && noNormal){
relevantName = noAdvanced ? "master" : "advanced"
}
if(relevantName === "advanced" && noAdvanced){
relevantName = noMaster ? "normal" : "master"
}
if(relevantName === "master" && noMaster){
relevantName = noAdvanced ? "normal" : "advanced"
}
for(var j in this.branchNames){
var name = this.branchNames[j]
if(name in branch){
branch[name].active = name === relevantName
}
}
if(branch === currentBranch){
activeName = relevantName
}
branch.active = relevantName
}
}
var circles = this.songData.circles
var circle = circles[this.currentCircle]
if(!circle || circle.branch === currentBranch[pastActive]){
var ms = this.elapsedTime
var closestCircle = circles.findIndex(circle => {
return (!circle.branch || circle.branch.active) && circle.endTime + this.controller.audioLatency >= ms
})
if(closestCircle !== -1){
this.currentCircle = closestCircle
}
}
if(this.controller.multiplayer === 1){
p2.send("branch", activeName)
}
}
resetSection(){
this.sectionNotes = []
this.sectionDrumroll = 0
}
clearKeyTime(){
var keyboard = this.controller.keyboard
for(var key in keyboard.keyTime){
keyboard.keys[key] = null
keyboard.keyTime[key] = -Infinity
}
}
calibration(){
var view = this.controller.view
if(!this.calibrationState){
this.controller.parsedSongData.measures = []
this.calibrationProgress = {
audio: 0,
video: 0,
requirement: 40
}
this.calibrationReset("audio", true)
}
var progress = this.calibrationProgress
var state = this.calibrationState
switch(state){
case "audio":
case "video":
if(state === "audio" && !this.mainAsset){
this.mainAsset = assets.sounds["se_calibration"]
this.mainMusicPlaying = false
}
if(progress.hit >= progress.requirement){
var reduced = 0
for(var i = 2; i < progress.offsets.length; i++){
reduced += progress.offsets[i]
}
progress[state] = Math.max(0, Math.round(reduced / progress.offsets.length - 2))
this.calibrationState += "Complete"
view.pauseOptions = []
this.clearKeyTime()
this.togglePause(true, 1)
this.mainAsset = null
}
break
case "audioComplete":
case "videoComplete":
if(Date.now() - this.latestDate > 3000){
var audioComplete = this.calibrationState === "audioComplete"
this.controller.playSound("se_pause", 0, true)
if(audioComplete){
this.calibrationReset("video")
}else{
view.pauseOptions = [
strings.calibration.retryPrevious,
strings.calibration.finish
]
}
this.calibrationState = audioComplete ? "videoHelp" : "results"
}
break
}
}
calibrationHit(ms){
var progress = this.calibrationProgress
var beatInterval = this.controller.view.beatInterval
var current = Math.floor((ms + 100) / beatInterval)
if(current !== progress.last){
var offset = ((ms + 100) % beatInterval) - 100
var offsets = progress.offsets
if(offsets.length >= progress.requirement){
offsets.shift()
}
offsets.push(offset)
progress.hit++
progress.last = current
this.globalScore.gauge = 10000 / (progress.requirement / progress.hit)
}
}
calibrationReset(to, togglePause){
var view = this.controller.view
this.songData.circles = []
view.pauseOptions = [
to === "audio" ? strings.calibration.back : strings.calibration.retryPrevious,
strings.calibration.start
]
this.calibrationState = to + "Help"
var progress = this.calibrationProgress
progress.offsets = []
progress.hit = 0
progress.last = null
this.globalScore.gauge = 0
if(to === "video"){
this.clearKeyTime()
this.initTiming()
this.latestDate = this.startDate
this.elapsedTime = 0
view.ms = 0
}
if(togglePause){
this.togglePause(true, 1, true)
}else{
view.pauseMove(1, true)
}
}
}

253
public/src/js/gameinput.js Normal file
View File

@@ -0,0 +1,253 @@
class GameInput{
constructor(...args){
this.init(...args)
}
init(controller){
this.controller = controller
this.game = this.controller.game
this.keyboard = new Keyboard({
ka_l: ["ka_l"],
don_l: ["don_l"],
don_r: ["don_r"],
ka_r: ["ka_r"],
pause: ["q", "esc"],
back: ["backspace"],
previous: ["left", "up"],
next: ["right", "down"],
confirm: ["enter", "space"]
}, this.keyPress.bind(this))
this.keys = {}
this.waitKeyupScore = {}
this.waitKeyupSound = {}
this.waitKeyupMenu = {}
this.keyTime = {
"don": -Infinity,
"ka": -Infinity
}
this.keyboardEvents = 0
var layout = settings.getItem("gamepadLayout")
if(layout === "b"){
var gameBtn = {
don_l: ["d", "r", "ls"],
don_r: ["a", "x", "rs"],
ka_l: ["u", "l", "lb", "lt"],
ka_r: ["b", "y", "rb", "rt"]
}
}else if(layout === "c"){
var gameBtn = {
don_l: ["d", "l", "ls"],
don_r: ["a", "b", "rs"],
ka_l: ["u", "r", "lb", "lt"],
ka_r: ["x", "y", "rb", "rt"]
}
}else{
var gameBtn = {
don_l: ["u", "d", "l", "r", "ls"],
don_r: ["a", "b", "x", "y", "rs"],
ka_l: ["lb", "lt"],
ka_r: ["rb", "rt"]
}
}
this.gamepad = new Gamepad(gameBtn)
this.gamepadInterval = setInterval(this.gamepadKeys.bind(this), 1000 / 60 / 2)
this.gamepadMenu = new Gamepad({
cancel: ["a"],
confirm: ["b", "ls", "rs"],
previous: ["u", "l", "lb", "lt", "lsu", "lsl"],
next: ["d", "r", "rb", "rt", "lsd", "lsr"],
pause: ["start"]
})
if(controller.multiplayer === 1){
pageEvents.add(window, "beforeunload", event => {
if(p2.otherConnected){
pageEvents.send("p2-abandoned", event)
}
})
}
}
keyPress(pressed, name){
if(!this.controller.autoPlayEnabled || this.game.isPaused() || name !== "don_l" && name !== "don_r" && name !== "ka_l" && name !== "ka_r"){
this.setKey(pressed, name, this.game.getAccurateTime())
}
}
checkGameKeys(){
if(this.controller.autoPlayEnabled){
this.checkKeySound("don_l", "don")
this.checkKeySound("don_r", "don")
this.checkKeySound("ka_l", "ka")
this.checkKeySound("ka_r", "ka")
}
}
gamepadKeys(){
if(!this.game.isPaused() && !this.controller.autoPlayEnabled){
this.gamepad.play((pressed, name) => {
if(pressed){
if(this.keys[name]){
this.setKey(false, name)
}
this.setKey(true, name, this.game.getAccurateTime())
}else{
this.setKey(false, name)
}
})
}
}
checkMenuKeys(){
if(!this.controller.multiplayer && !this.locked && this.controller.view.pauseOptions.length !== 0){
var moveMenu = 0
var ms = this.game.getAccurateTime()
this.gamepadMenu.play((pressed, name) => {
if(pressed){
if(this.game.isPaused()){
if(name === "cancel"){
this.locked = true
return setTimeout(() => {
this.controller.togglePause()
this.locked = false
}, 200)
}
}
if(this.keys[name]){
this.setKey(false, name)
}
this.setKey(true, name, ms)
}else{
this.setKey(false, name)
}
})
this.checkKey("pause", "menu", () => {
this.controller.togglePause()
for(var key in this.keyTime){
this.keys[key] = null
this.keyTime[key] = -Infinity
}
})
var moveMenuMinus = () => {
moveMenu = -1
}
var moveMenuPlus = () => {
moveMenu = 1
}
var moveMenuConfirm = () => {
if(this.game.isPaused()){
this.locked = true
setTimeout(() => {
this.controller.view.pauseConfirm()
this.locked = false
}, 200)
}
}
this.checkKey("previous", "menu", moveMenuMinus)
this.checkKey("ka_l", "menu", moveMenuMinus)
this.checkKey("next", "menu", moveMenuPlus)
this.checkKey("ka_r", "menu", moveMenuPlus)
this.checkKey("confirm", "menu", moveMenuConfirm)
this.checkKey("don_l", "menu", moveMenuConfirm)
this.checkKey("don_r", "menu", moveMenuConfirm)
if(moveMenu && this.game.isPaused()){
this.controller.playSound("se_ka", 0, true)
this.controller.view.pauseMove(moveMenu)
}
}
if(this.controller.multiplayer !== 2){
this.checkKey("back", "menu", () => {
if(this.controller.multiplayer === 1 && p2.otherConnected){
p2.send("gameend")
pageEvents.send("p2-abandoned")
}
this.controller.togglePause()
this.controller.songSelection()
})
}
}
checkKey(name, type, callback){
if(this.keys[name] && !this.isWaiting(name, type)){
this.waitForKeyup(name, type)
callback()
}
}
checkKeySound(name, sound){
this.checkKey(name, "sound", () => {
var circles = this.controller.getCircles()
var circle = circles[this.controller.getCurrentCircle()]
var currentTime = this.keyTime[name]
this.keyTime[sound] = currentTime
if(circle && !circle.isPlayed){
if(circle.type === "balloon"){
if(sound === "don" && circle.requiredHits - circle.timesHit <= 1){
this.controller.playSound("se_balloon")
return
}
}
}
this.controller.playSound("neiro_1_" + sound)
})
}
getKeys(){
return this.keys
}
setKey(pressed, name, ms){
if(pressed){
this.keys[name] = true
this.waitKeyupScore[name] = false
this.waitKeyupSound[name] = false
this.waitKeyupMenu[name] = false
if(this.game.isPaused()){
return
}
this.keyTime[name] = ms
var calibrationState = this.game.calibrationState
var calibration = calibrationState && !this.game.paused
if(name == "don_l" || name == "don_r"){
if(calibration){
this.game.calibrationHit(ms)
}else{
this.checkKeySound(name, "don")
}
this.keyboardEvents++
}else if(name == "ka_l" || name == "ka_r"){
if(!calibration){
this.checkKeySound(name, "ka")
}
this.keyboardEvents++
}
}
}
isWaiting(name, type){
if(type === "score"){
return this.waitKeyupScore[name]
}else if(type === "sound"){
return this.waitKeyupSound[name]
}else if(type === "menu"){
return this.waitKeyupMenu[name]
}
}
waitForKeyup(name, type){
if(!this.keys[name]){
return
}
if(type === "score"){
this.waitKeyupScore[name] = true
}else if(type === "sound"){
this.waitKeyupSound[name] = true
}else if(type === "menu"){
this.waitKeyupMenu[name] = true
}
}
getKeyTime(){
return this.keyTime
}
clean(){
this.keyboard.clean()
this.gamepad.clean()
this.gamepadMenu.clean()
clearInterval(this.gamepadInterval)
if(this.controller.multiplayer === 1){
pageEvents.remove(window, "beforeunload")
}
}
}

153
public/src/js/gamepad.js Normal file
View File

@@ -0,0 +1,153 @@
class Gamepad{
constructor(...args){
this.init(...args)
}
init(bindings, callback){
this.bindings = bindings
this.callback = !!callback
this.b = {
"a": 0,
"b": 1,
"x": 2,
"y": 3,
"lb": 4,
"rb": 5,
"lt": 6,
"rt": 7,
"back": 8,
"start": 9,
"ls": 10,
"rs": 11,
"u": 12,
"d": 13,
"l": 14,
"r": 15,
"guide": 16,
"lsu": "lsu",
"lsr": "lsr",
"lsd": "lsd",
"lsl": "lsl"
}
this.btn = {}
this.gamepadEvents = 0
if(callback){
this.interval = setInterval(() => {
this.play(callback)
}, 1000 / 60)
}
}
play(callback){
if("getGamepads" in navigator){
var gamepads = navigator.getGamepads()
if(gamepads.length === 0){
return
}
}else{
return
}
if(pageEvents.lastKeyEvent + 5000 > Date.now()){
return
}
var bindings = this.bindings
var force = {
lsu: false,
lsr: false,
lsd: false,
lsl: false
}
for(var i = 0; i < gamepads.length; i++){
if(gamepads[i]){
var axes = gamepads[i].axes
if(axes.length >= 2){
force.lsl = force.lsl || axes[0] <= -0.5
force.lsr = force.lsr || axes[0] >= 0.5
force.lsu = force.lsu || axes[1] <= -0.5
force.lsd = force.lsd || axes[1] >= 0.5
}
if(axes.length >= 10){
// TaTaCon left D-Pad, DualSense D-Pad
for(var pov = 0; pov < 8; pov++){
if(Math.abs(axes[9] - (pov / 3.5 - 1)) <= 0.01){
force.u = force.u || pov === 7 || pov === 0 || pov === 1
force.r = force.r || pov === 1 || pov === 2 || pov === 3
force.d = force.d || pov === 3 || pov === 4 || pov === 5
force.l = force.l || pov === 5 || pov === 6 || pov === 7
break
}
}
}
}
}
for(var i = 0; i < gamepads.length; i++){
if(gamepads[i]){
this.toRelease = {}
for(var bind in bindings){
this.toRelease[bind] = bindings[bind].length
}
for(var bind in bindings){
for(var name in bindings[bind]){
var bindName = bindings[bind][name]
this.checkButton(gamepads, this.b[bindName], bind, callback, force[bindName])
if(!this.b){
return
}
}
}
break
}
}
}
checkButton(gamepads, btnName, keyCode, callback, force){
var button = false
if(typeof force === "undefined"){
for(var i = 0; i < gamepads.length; i++){
if(gamepads[i]){
var btn = gamepads[i].buttons[btnName]
if(btn){
var btnPressed = btn.pressed || btn.value >= 0.5
if(btnPressed){
button = btnPressed
}
}
}
}
var pressed = !this.btn[btnName] && button
var released = this.btn[btnName] && !button
}else{
var pressed = !this.btn[btnName] && force
var released = this.btn[btnName] && !force
}
if(pressed){
this.btn[btnName] = true
}else if(released){
this.btn[btnName] = false
}
if(pressed){
callback(true, keyCode)
this.gamepadEvents++
}else if(!button){
if(released){
this.toRelease[keyCode + "released"] = true
}
this.toRelease[keyCode]--
if(this.toRelease[keyCode] === 0 && this.toRelease[keyCode + "released"]){
callback(false, keyCode)
}
}
}
clean(){
if(this.callback){
clearInterval(this.interval)
}
delete this.bindings
delete this.b
delete this.btn
}
}

View File

@@ -0,0 +1,77 @@
class GameRules{
constructor(...args){
this.init(...args)
}
init(game){
this.difficulty = game.controller.selectedSong.difficulty
var frame = 1000 / 60
switch(this.difficulty){
case "easy":
case "normal":
this.good = 5 / 2 * frame
this.ok = 13 / 2 * frame
this.bad = 15 / 2 * frame
break
case "hard":
case "oni":
case "ura":
default:
this.good = 3 / 2 * frame
this.ok = 9 / 2 * frame
this.bad = 13 / 2 * frame
break
}
switch(this.difficulty){
case "easy":
this.gaugeClear = 30 / 50
break
case "normal":
case "hard":
this.gaugeClear = 35 / 50
break
case "oni":
case "ura":
this.gaugeClear = 40 / 50
break
default:
this.gaugeClear = 51 / 50
break
}
this.daiLeniency = 2 * frame
}
soulPoints(combo){
var good, ok, bad
switch(this.difficulty){
case "easy":
good = Math.floor(10000 / combo * 1.575)
ok = Math.floor(good * 0.75)
bad = Math.ceil(good / -2)
break
case "normal":
good = Math.floor(10000 / combo / 0.7)
ok = Math.floor(good * 0.75)
bad = Math.ceil(good / -0.75)
break
case "hard":
good = Math.floor(10000 / combo * 1.5)
ok = Math.floor(good * 0.75)
bad = Math.ceil(good / -0.8)
break
case "oni":
case "ura":
good = Math.floor(10000 / combo / 0.7)
ok = Math.floor(good * 0.5)
bad = Math.ceil(good * -1.6)
break
}
return {good: good, ok: ok, bad: bad}
}
gaugePercent(gauge){
return Math.floor(gauge / 200) / 50
}
clearReached(gauge){
return this.gaugePercent(gauge) >= this.gaugeClear
}
}

285
public/src/js/gpicker.js Normal file
View File

@@ -0,0 +1,285 @@
class Gpicker{
constructor(...args){
this.init(...args)
}
init(){
this.apiKey = gameConfig.google_credentials.api_key
this.oauthClientId = gameConfig.google_credentials.oauth_client_id
this.projectNumber = gameConfig.google_credentials.project_number
this.scope = "https://www.googleapis.com/auth/drive.readonly"
this.folder = "application/vnd.google-apps.folder"
this.filesUrl = "https://www.googleapis.com/drive/v3/files/"
this.resolveQueue = []
this.queueActive = false
this.clientCallbackBind = this.clientCallback.bind(this)
}
browse(lockedCallback, errorCallback){
return this.loadApi(lockedCallback, errorCallback)
.then(() => this.getToken(lockedCallback, errorCallback))
.then(() => new Promise((resolve, reject) => {
this.displayPicker(data => {
if(data.action === "picked"){
var file = data.docs[0]
var folders = []
var rateLimit = -1
var lastBatch = 0
var walk = (files, output=[]) => {
for(var i = 0; i < files.length; i++){
var path = files[i].path ? files[i].path + "/" : ""
var list = files[i].list
if(!list){
continue
}
for(var j = 0; j < list.length; j++){
var file = list[j]
if(file.mimeType === this.folder){
folders.push({
path: path + file.name,
id: file.id
})
}else{
output.push(new GdriveFile({
path: path + file.name,
name: file.name,
id: file.id
}))
}
}
}
var batchList = []
for(var i = 0; i < folders.length && batchList.length < 100; i++){
if(!folders[i].listed){
folders[i].pos = i
folders[i].listed = true
batchList.push(folders[i])
}
}
if(batchList.length){
var batch = gapi.client.newBatch()
batchList.forEach(folder => {
var req = {
q: "'" + folder.id + "' in parents and trashed = false",
orderBy: "name_natural"
}
if(folder.pageToken){
req.pageToken = folder.pageToken
}
batch.add(gapi.client.drive.files.list(req), {id: folder.pos})
})
if(lastBatch + batchList.length > 100){
var waitPromise = this.sleep(1000)
}else{
var waitPromise = Promise.resolve()
}
return waitPromise.then(() => this.queue()).then(() => batch.then(responses => {
var files = []
var rateLimited = false
for(var i in responses.result){
var result = responses.result[i].result
if(result.error){
if(result.error.errors[0].domain !== "usageLimits"){
console.warn(result)
}else if(!rateLimited){
rateLimited = true
rateLimit++
folders.push({
path: folders[i].path,
id: folders[i].id,
pageToken: folders[i].pageToken
})
}
}else{
if(result.nextPageToken){
folders.push({
path: folders[i].path,
id: folders[i].id,
pageToken: result.nextPageToken
})
}
files.push({path: folders[i].path, list: result.files})
}
}
if(rateLimited){
return this.sleep(Math.pow(2, rateLimit) * 1000).then(() => walk(files, output))
}else{
return walk(files, output)
}
}))
}else{
return output
}
}
if(file.mimeType === this.folder){
return walk([{list: [file]}]).then(resolve, reject)
}else{
return reject("cancel")
}
}else if(data.action === "cancel"){
return reject("cancel")
}
})
}))
}
loadApi(lockedCallback=()=>{}, errorCallback=()=>{}){
if(window.gapi && gapi.client && gapi.client.drive){
return Promise.resolve()
}
var promises = [
loader.loadScript("https://apis.google.com/js/api.js"),
loader.loadScript("https://accounts.google.com/gsi/client")
]
var apiLoaded = false
return Promise.all(promises).then(() => new Promise((resolve, reject) =>
gapi.load("picker:client", {
callback: resolve,
onerror: reject
})
))
.then(() => new Promise((resolve, reject) => {
setTimeout(() => {
if(!apiLoaded){
lockedCallback(false)
}
}, 3000)
return gapi.client.load("drive", "v3").then(resolve, reject)
})).then(() => {
apiLoaded = true
lockedCallback(true)
}).catch(e => {
errorCallback(Array.isArray(e) ? e[0] : e)
return Promise.reject("cancel")
})
}
getClient(errorCallback=()=>{}, force){
var obj = {
client_id: this.oauthClientId,
scope: this.scope,
callback: this.clientCallbackBind
}
if(force){
if(!this.clientForce){
obj.select_account = true
this.clientForce = google.accounts.oauth2.initTokenClient(obj)
}
return this.clientForce
}else{
if(!this.client){
this.client = google.accounts.oauth2.initTokenClient(obj)
}
return this.client
}
}
clientCallback(tokenResponse){
this.tokenResponse = tokenResponse
this.oauthToken = tokenResponse && tokenResponse.access_token
if(this.oauthToken && this.tokenResolve){
this.tokenResolve()
}
}
getToken(lockedCallback=()=>{}, errorCallback=()=>{}, force){
if(this.oauthToken && !force){
return Promise.resolve()
}
var client = this.getClient(errorCallback, force)
var promise = new Promise(resolve => {
this.tokenResolve = resolve
})
lockedCallback(false)
client.requestAccessToken()
return promise.then(() => {
this.tokenResolve = null
if(this.checkScope()){
lockedCallback(true)
}else{
return Promise.reject("cancel")
}
})
}
checkScope(){
return google.accounts.oauth2.hasGrantedAnyScope(this.tokenResponse, this.scope)
}
switchAccounts(lockedCallback, errorCallback){
return this.loadApi().then(() => this.getToken(lockedCallback, errorCallback, true))
}
displayPicker(callback){
var picker = gapi.picker.api
new picker.PickerBuilder()
.setDeveloperKey(this.apiKey)
.setAppId(this.projectNumber)
.setOAuthToken(this.oauthToken)
.setLocale(strings.gpicker.locale)
.hideTitleBar()
.addView(new picker.DocsView("folders")
.setLabel(strings.gpicker.myDrive)
.setParent("root")
.setSelectFolderEnabled(true)
.setMode("grid")
)
.addView(new picker.DocsView("folders")
.setLabel(strings.gpicker.starred)
.setStarred(true)
.setSelectFolderEnabled(true)
.setMode("grid")
)
.addView(new picker.DocsView("folders")
.setLabel(strings.gpicker.sharedWithMe)
.setOwnedByMe(false)
.setSelectFolderEnabled(true)
.setMode("list")
)
.setCallback(callback)
.setSize(Infinity, Infinity)
.build()
.setVisible(true)
}
downloadFile(id, responseType, retry){
var url = this.filesUrl + id + "?alt=media"
return this.queue().then(this.getToken.bind(this)).then(() =>
loader.ajax(url, request => {
if(responseType){
request.responseType = responseType
}
request.setRequestHeader("Authorization", "Bearer " + this.oauthToken)
}, true).then(event => {
var request = event.target
var reject = () => Promise.reject(`${url} (${request.status})`)
if(request.status === 200){
return request.response
}else if(request.status === 401 && !retry){
return new Response(request.response).json().then(response => {
var e = response.error
if(e && e.errors[0].reason === "authError"){
delete this.oauthToken
return this.downloadFile(id, responseType, true)
}else{
return reject()
}
}, reject)
}
return reject()
})
)
}
sleep(time){
return new Promise(resolve => setTimeout(resolve, time))
}
queue(){
return new Promise(resolve => {
this.resolveQueue.push(resolve)
if(!this.queueActive){
this.queueActive = true
this.queueTimer = setInterval(this.parseQueue.bind(this), 100)
this.parseQueue()
}
})
}
parseQueue(){
if(this.resolveQueue.length){
var resolve = this.resolveQueue.shift()
resolve()
}else{
this.queueActive = false
clearInterval(this.queueTimer)
}
}
}

54
public/src/js/idb.js Normal file
View File

@@ -0,0 +1,54 @@
class IDB{
constructor(...args){
this.init(...args)
}
init(name, store){
this.name = name
this.store = store
}
start(){
if(this.db){
return Promise.resolve(this.db)
}
var request = indexedDB.open(this.name)
request.onupgradeneeded = event => {
var db = event.target.result
db.createObjectStore(this.store)
}
return this.promise(request).then(result => {
this.db = result
return this.db
}, target =>
console.warn("DB error", target)
)
}
promise(request){
return new Promise((resolve, reject) => {
return pageEvents.race(request, "success", "error").then(response => {
if(response.type === "success"){
return resolve(event.target.result)
}else{
return reject(event.target)
}
})
})
}
transaction(method, ...args){
return this.start().then(db =>
db.transaction(this.store, "readwrite").objectStore(this.store)[method](...args)
).then(this.promise.bind(this))
}
getItem(name){
return this.transaction("get", name)
}
setItem(name, value){
return this.transaction("put", value, name)
}
removeItem(name){
return this.transaction("delete", name)
}
removeDB(){
delete this.db
return indexedDB.deleteDatabase(this.name)
}
}

View File

@@ -0,0 +1,658 @@
class ImportSongs{
constructor(...args){
this.init(...args)
}
init(limited, otherFiles, noPlugins, pluginAmount){
this.limited = limited
this.tjaFiles = []
this.osuFiles = []
this.assetFiles = {}
this.pluginFiles = []
this.otherFiles = otherFiles || {}
this.noPlugins = noPlugins
this.pluginAmount = pluginAmount
this.songs = []
this.stylesheet = []
this.plugins = []
this.songTitle = this.otherFiles.songTitle || {}
this.uraRegex = /\s*[\(]裏[\)]$/
this.courseTypes = {
"easy": 0,
"normal": 1,
"hard": 2,
"oni": 3,
"ura": 4
}
this.categoryAliases = {}
assets.categories.forEach(cat => {
this.categoryAliases[cat.title.toLowerCase()] = cat.id
if(cat.aliases){
cat.aliases.forEach(alias => {
this.categoryAliases[alias.toLowerCase()] = cat.id
})
}
if(cat.title_lang){
for(var i in cat.title_lang){
this.categoryAliases[cat.title_lang[i].toLowerCase()] = cat.id
}
}
})
this.assetSelectors = {}
for(var selector in assets.cssBackground){
var filename = assets.cssBackground[selector]
var index = filename.lastIndexOf(".")
if(index !== -1){
filename = filename.slice(0, index)
}
this.assetSelectors[filename] = selector
}
this.comboVoices = ["v_combo_50"].concat(Array.from(Array(50), (d, i) => "v_combo_" + ((i + 1) * 100)))
}
load(files){
var extensionRegex = /\.[^\/]+$/
files.sort((a, b) => {
var path1 = a.path.replace(extensionRegex, "")
var path2 = b.path.replace(extensionRegex, "")
return path1 > path2 ? 1 : -1
})
var metaFiles = []
for(var i = 0; i < files.length; i++){
var file = files[i]
var name = file.name.toLowerCase()
var path = file.path.toLowerCase()
if(name.endsWith(".tja") || name.endsWith(".tjf")){
this.tjaFiles.push({
file: file,
index: i
})
}else if(name.endsWith(".osu")){
this.osuFiles.push({
file: file,
index: i
})
}else if(!this.limited && (name === "genre.ini" || name === "box.def") || name === "songtitle.txt"){
var level = (file.path.match(/\//g) || []).length
metaFiles.push({
file: file,
level: (level * 2) + (name === "genre.ini" ? 1 : 0)
})
}else if(!this.limited && (path.indexOf("/taiko-web assets/") !== -1 || path.indexOf("taiko-web assets/") === 0)){
if(!(name in this.assetFiles)){
this.assetFiles[name] = file
}
}else if(name.endsWith(".taikoweb.js")){
this.pluginFiles.push({
file: file,
index: i
})
}else{
this.otherFiles[path] = file
}
}
if(!this.noPlugins && this.pluginFiles.length){
var pluginPromises = []
this.pluginFiles.forEach(fileObj => {
pluginPromises.push(this.addPlugin(fileObj).catch(e => console.warn(e)))
})
return Promise.all(pluginPromises).then(() => {
var startPromises = []
var pluginAmount = 0
if(this.plugins.length && confirm(strings.plugins.warning.replace("%s",
strings.plugins.plugin[strings.plural.select(this.plugins.length)].replace("%s",
this.plugins.length.toString()
)
))){
this.plugins.forEach(obj => {
var plugin = plugins.add(obj.data, {
name: obj.name,
raw: true
})
if(plugin){
pluginAmount++
plugin.imported = true
startPromises.push(plugin.start())
}
})
}
return Promise.all(startPromises).then(() => {
var importSongs = new ImportSongs(this.limited, this.otherFiles, true, pluginAmount)
return importSongs.load(files)
})
})
}
var metaPromises = []
metaFiles.forEach(fileObj => {
metaPromises.push(this.addMeta(fileObj))
})
return Promise.all(metaPromises).then(() => {
var songPromises = []
this.tjaFiles.forEach(fileObj => {
songPromises.push(this.addTja(fileObj).catch(e => console.warn(e)))
})
this.osuFiles.forEach(fileObj => {
songPromises.push(this.addOsu(fileObj).catch(e => console.warn(e)))
})
songPromises.push(this.addAssets())
return Promise.all(songPromises)
}).then(this.loaded.bind(this))
}
addMeta(fileObj){
var file = fileObj.file
var level = fileObj.level
var name = file.name.toLowerCase()
return file.read(name === "songtitle.txt" ? undefined : "utf-8").then(data => {
var data = data.replace(/\0/g, "").split("\n")
var category
if(name === "genre.ini"){
var key
for(var i = 0; i < data.length; i++){
var line = data[i].trim().toLowerCase()
if(line.startsWith("[") && line.endsWith("]")){
key = line.slice(1, -1)
}else if(key === "genre"){
var equalsPos = line.indexOf("=")
if(equalsPos !== -1 && line.slice(0, equalsPos).trim() === "genrename"){
var value = line.slice(equalsPos + 1).trim()
if(value.toLowerCase() in this.categoryAliases){
category = value
}else{
category = data[i].trim().slice(equalsPos + 1).trim()
}
break
}
}
}
}else if(name === "box.def"){
for(var i = 0; i < data.length; i++){
var line = data[i].trim().toLowerCase()
if(line.startsWith("#title:")){
var value = line.slice(7).trim()
if(value.toLowerCase() in this.categoryAliases){
category = value
}
}else if(line.startsWith("#genre:")){
var value = line.slice(7).trim()
if(value.toLowerCase() in this.categoryAliases){
category = value
}else{
category = data[i].trim().slice(7).trim()
}
break
}
}
}else if(name === "songtitle.txt"){
var lastTitle
for(var i = 0; i < data.length; i++){
var line = data[i].trim()
if(line){
var lang = line.slice(0, 2)
if(line.charAt(2) !== " " || !(lang in allStrings)){
this.songTitle[line] = {}
lastTitle = line
}else if(lastTitle){
this.songTitle[lastTitle][lang] = line.slice(3).trim()
}
}
}
}
if(category){
var metaPath = file.path.toLowerCase().slice(0, file.name.length * -1)
var filesLoop = fileObj => {
var tjaPath = fileObj.file.path.toLowerCase().slice(0, fileObj.file.name.length * -1)
if(tjaPath.startsWith(metaPath) && (!("categoryLevel" in fileObj) || fileObj.categoryLevel < level)){
if(category.toLowerCase() in this.categoryAliases){
fileObj.category_id = this.categoryAliases[category.toLowerCase()]
}else{
fileObj.category = category
}
fileObj.categoryLevel = level
}
}
this.tjaFiles.forEach(filesLoop)
this.osuFiles.forEach(filesLoop)
}
}).catch(e => {
console.warn(e)
})
}
addTja(fileObj){
var file = fileObj.file
var index = fileObj.index
var category = fileObj.category
var category_id = fileObj.category_id
if(!this.limited){
var filePromise = file.read(prompt("太鼓さん次郎のファイルは\"sjis\"、TJAPlayer3のファイルは\"utf-8\"と入力してください。"))
}else{
var filePromise = Promise.resolve()
}
return filePromise.then(dataRaw => {
var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : []
var tja = new ParseTja(data, "oni", 0, 0, true)
var songObj = {
id: index + 1,
order: index + 1,
title: file.name.slice(0, file.name.lastIndexOf(".")),
type: "tja",
chart: file,
courses: {},
music: "muted",
custom: true
}
if(this.limited){
songObj.unloaded = true
}
var coursesAdded = false
var titleLang = {}
var titleLangAdded = false
var subtitleLangAdded = false
var subtitleLang = {}
var dir = file.path.toLowerCase()
dir = dir.slice(0, dir.lastIndexOf("/") + 1)
for(var diff in tja.metadata){
var meta = tja.metadata[diff]
songObj.title = meta.title || file.name.slice(0, file.name.lastIndexOf("."))
var subtitle = meta.subtitle || ""
if(subtitle.startsWith("--") || subtitle.startsWith("++")){
subtitle = subtitle.slice(2).trim()
}
songObj.subtitle = subtitle
songObj.preview = meta.demostart || 0
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
}
if(meta.genre){
if(meta.genre.toLowerCase() in this.categoryAliases){
songObj.category_id = this.categoryAliases[meta.genre.toLowerCase()]
}else{
songObj.category = meta.genre
}
}
if(meta.taikowebskin){
songObj.song_skin = this.getSkin(dir, meta.taikowebskin)
}
if(meta.maker){
var maker = meta.maker
var url = null
var gt = maker.lastIndexOf(">")
if(gt === maker.length - 1){
var lt = maker.lastIndexOf("<")
if(lt !== -1 && lt !== gt - 2){
url = maker.slice(lt + 1, gt).trim()
if(url.startsWith("http://") || url.startsWith("https://")){
maker = maker.slice(0, lt)
}else{
url = null
}
}
}
songObj.maker = {
name: maker,
url: url,
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 = ""
if(songTitle){
var uraPos = songTitle.search(this.uraRegex)
if(uraPos !== -1){
ura = songTitle.slice(uraPos)
songTitle = songTitle.slice(0, uraPos)
}
}
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
}
}
}
if(titleLangAdded){
songObj.title_lang = titleLang
}
if(subtitleLangAdded){
songObj.subtitle_lang = subtitleLang
}
if(!songObj.category_id && !songObj.category){
if(!category && category_id === undefined){
songObj.category_id = this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))])
}else if(category){
songObj.category = category
songObj.orginalCategory = category
}else{
songObj.category_id = category_id
}
}
if(coursesAdded || songObj.unloaded){
this.songs[index] = songObj
}
if(!this.limited){
var hash = md5.base64(dataRaw).slice(0, -2)
songObj.hash = hash
scoreStorage.songTitles[songObj.title] = hash
var score = scoreStorage.get(hash, false, true)
if(score){
score.title = songObj.title
}
}
})
}
addOsu(fileObj){
var file = fileObj.file
var index = fileObj.index
var category = fileObj.category
var category_id = fileObj.category_id
if(!this.limited){
var filePromise = file.read()
}else{
var filePromise = Promise.resolve()
}
return filePromise.then(dataRaw => {
var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : []
var osu = new ParseOsu(data, "oni", 0, 0, true)
var dir = file.path.toLowerCase()
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: {
en: osu.metadata.Artist || osu.metadata.ArtistUnicode
},
preview: osu.generalInfo.PreviewTime ? osu.generalInfo.PreviewTime / 1000 : 0,
courses: {},
music: (osu.generalInfo.AudioFilename ? this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] : "") || "muted"
}
if(!this.limited){
songObj.courses.oni = {
stars: parseInt(osu.difficulty.overallDifficulty) || 0,
branch: false
}
}else{
songObj.unloaded = true
}
var filename = file.name.slice(0, file.name.lastIndexOf("."))
var title = osu.metadata.TitleUnicode || osu.metadata.Title || file.name.slice(0, file.name.lastIndexOf("."))
if(title){
var suffix = ""
var matches = filename.match(/\[.+?\]$/)
if(matches){
suffix = " " + matches[0]
}
songObj.title = title + suffix
songObj.title_lang = {
en: (osu.metadata.Title || osu.metadata.TitleUnicode || title) + suffix
}
}else{
songObj.title = filename
}
this.songs[index] = songObj
if(!category && category_id === undefined){
songObj.category_id = this.getCategory(file, [osu.metadata.TitleUnicode, osu.metadata.Title, file.name.slice(0, file.name.lastIndexOf("."))])
}else if(category){
songObj.category = category
songObj.orginalCategory = category
}else{
songObj.category_id = category_id
}
if(!this.limited){
var hash = md5.base64(dataRaw).slice(0, -2)
songObj.hash = hash
scoreStorage.songTitles[songObj.title] = hash
var score = scoreStorage.get(hash, false, true)
if(score){
score.title = songObj.title
}
}
})
}
addAssets(){
var promises = []
for(let name in this.assetFiles){
let id = this.getFilename(name)
var file = this.assetFiles[name]
var index = name.lastIndexOf(".")
if(name === "vectors.json"){
promises.push(file.read().then(response => {
vectors = JSON.parse(response)
}))
}
if(name.endsWith(".png") || name.endsWith(".gif")){
let image = document.createElement("img")
promises.push(pageEvents.load(image).then(() => {
if(id in this.assetSelectors){
var selector = this.assetSelectors[id]
var gradient = ""
if(selector === "#song-search"){
gradient = loader.songSearchGradient
}
this.stylesheet.push(loader.cssRuleset({
[selector]: {
"background-image": gradient + "url(\"" + image.src + "\")"
}
}))
}
}))
image.id = name
promises.push(file.blob().then(blob => {
image.src = URL.createObjectURL(blob)
}))
loader.assetsDiv.appendChild(image)
var oldImage = assets.image[id]
if(oldImage && oldImage.parentNode){
URL.revokeObjectURL(oldImage.src)
oldImage.parentNode.removeChild(oldImage)
}
assets.image[id] = image
}
if(assets.audioSfx.indexOf(name) !== -1){
assets.sounds[id].clean()
promises.push(this.loadSound(file, name, snd.sfxGain))
}
if(assets.audioMusic.indexOf(name) !== -1){
assets.sounds[id].clean()
promises.push(this.loadSound(file, name, snd.musicGain))
}
if(assets.audioSfxLR.indexOf(name) !== -1){
assets.sounds[id + "_p1"].clean()
assets.sounds[id + "_p2"].clean()
promises.push(this.loadSound(file, name, snd.sfxGain).then(sound => {
assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL)
assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR)
}))
}
if(assets.audioSfxLoud.indexOf(name) !== -1){
assets.sounds[id].clean()
promises.push(this.loadSound(file, name, snd.sfxLoudGain))
}
if(this.comboVoices.indexOf(id) !== -1){
promises.push(snd.sfxGain.load(file).then(sound => {
assets.sounds[id] = sound
assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL)
assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR)
}))
}
}
return Promise.all(promises)
}
loadSound(file, name, gain){
var id = this.getFilename(name)
return gain.load(file).then(sound => {
assets.sounds[id] = sound
})
}
getFilename(name){
return name.slice(0, name.lastIndexOf("."))
}
addPlugin(fileObj){
var file = fileObj.file
var filePromise = file.read()
return filePromise.then(dataRaw => {
var name = file.name.slice(0, file.name.lastIndexOf(".taikoweb.js"))
this.plugins.push({
name: name,
data: dataRaw
})
})
}
getCategory(file, exclude){
var path = file.path.toLowerCase().split("/")
for(var i = path.length - 2; i >= 0; i--){
var hasTitle = false
for(var j in exclude){
if(exclude[j] && path[i].indexOf(exclude[j].toLowerCase()) !== -1){
hasTitle = true
break
}
}
if(!hasTitle){
for(var cat in this.categoryAliases){
if(path[i].indexOf(cat) !== -1){
return this.categoryAliases[cat]
}
}
}
}
}
getSkin(dir, config){
var configArray = config.toLowerCase().split(",")
var configObj = {}
for(var i in configArray){
var string = configArray[i].trim()
var space = string.indexOf(" ")
if(space !== -1){
configObj[string.slice(0, space).trim()] = string.slice(space + 1).trim()
}
}
if(!configObj.dir){
configObj.dir = ""
}
configObj.prefix = "custom "
var skinnable = ["song", "stage", "don"]
for(var i in skinnable){
var skinName = skinnable[i]
var skinValue = configObj[skinName]
if(skinValue && skinValue !== "none"){
var fileName = "bg_" + skinName + "_" + configObj.name
var skinPath = this.joinPath(dir, configObj.dir, fileName)
for(var j = 0; j < 2; j++){
if(skinValue !== "static"){
var suffix = (j === 0 ? "_a" : "_b") + ".png"
}else{
var suffix = ".png"
}
var skinFull = this.normPath(skinPath + suffix)
if(skinFull in this.otherFiles){
configObj[fileName + suffix] = this.otherFiles[skinFull]
}else{
configObj[skinName] = null
}
if(skinValue === "static"){
break
}
}
}
}
return configObj
}
loaded(){
this.songs = this.songs.filter(song => typeof song !== "undefined")
if(this.stylesheet.length){
var style = document.createElement("style")
style.appendChild(document.createTextNode(this.stylesheet.join("\n")))
document.head.appendChild(style)
}
if(this.songs.length){
if(this.limited){
assets.otherFiles = this.otherFiles
assets.otherFiles.songTitle = this.songTitle
}
return Promise.resolve(this.songs)
}else{
if(this.noPlugins && this.pluginAmount || Object.keys(this.assetFiles).length){
return Promise.resolve()
}else{
return Promise.reject("nosongs")
}
}
this.clean()
}
joinPath(){
var resultPath = arguments[0]
for(var i = 1; i < arguments.length; i++){
var pPath = arguments[i]
if(pPath && (pPath[0] === "/" || pPath[0] === "\\")){
resultPath = pPath
}else{
var lastChar = resultPath.slice(-1)
if(resultPath && (lastChar !== "/" || lastChar !== "\\")){
resultPath = resultPath + "/"
}
resultPath = resultPath + pPath
}
}
return resultPath
}
normPath(path){
path = path.replace(/\\/g, "/").toLowerCase()
while(path[0] === "/"){
path = path.slice(1)
}
var comps = path.split("/")
for(var i = 0; i < comps.length; i++){
if(comps[i] === "." || comps[i] === ""){
comps.splice(i, 1)
i--
}else if(i !== 0 && comps[i] === ".." && comps[i - 1] !== ".."){
comps.splice(i - 1, 2)
i -= 2
}
}
return comps.join("/")
}
clean(){
delete this.songs
delete this.tjaFiles
delete this.osuFiles
delete this.assetFiles
delete this.otherFiles
}
}

124
public/src/js/keyboard.js Normal file
View File

@@ -0,0 +1,124 @@
class Keyboard{
constructor(...args){
this.init(...args)
}
init(bindings, callback){
this.bindings = bindings
this.callback = callback
this.wildcard = false
this.substitute = {
"up": "arrowup",
"right": "arrowright",
"down": "arrowdown",
"left": "arrowleft",
"space": " ",
"esc": "escape",
"ctrl": "control",
"altgr": "altgraph"
}
this.btn = {}
this.TaikoForceLv5 = false;
this.update()
pageEvents.keyAdd(this, "all", "both", this.keyEvent.bind(this))
pageEvents.blurAdd(this, this.blurEvent.bind(this))
}
isTaikoForceLv5(kbdSettings){
// the key of Taiko Force Lv5's PC module looks like this
return (kbdSettings.ka_l[0] === "f") && (kbdSettings.ka_r[0] === "e") && (kbdSettings.don_l[0] === "i") && (kbdSettings.don_r[0] === "j");
}
update(){
var kbdSettings = settings.getItem("keyboardSettings")
this.TaikoForceLv5 = this.isTaikoForceLv5(kbdSettings)
var drumKeys = {}
for(var name in kbdSettings){
var keys = kbdSettings[name]
for(var i in keys){
drumKeys[keys[i]] = name
}
}
this.kbd = {}
for(var name in this.bindings){
var keys = this.bindings[name]
for(var i in keys){
var key = keys[i]
if(key in drumKeys){
continue
}
if(key in kbdSettings){
var keyArray = kbdSettings[key]
for(var j in keyArray){
key = keyArray[j]
if(!(key in this.kbd)){
this.kbd[key] = name
}
}
}else{
if(key in this.substitute){
key = this.substitute[key]
}
if(!(key in this.kbd)){
if(key === "wildcard"){
this.wildcard = true
}
this.kbd[key] = name
}
}
}
}
}
keyEvent(event){
var key = event.key.toLowerCase()
if(event.target.tagName === "INPUT"){
if(key === "escape"){
event.preventDefault()
}
}else if(
key === "escape" ||
key === "backspace" ||
key === "tab" ||
key === "contextmenu" ||
key === "alt"
){
event.preventDefault()
}
if(!event.repeat){
var pressed = event.type === "keydown"
if(pressed){
this.btn[key] = true
}else{
delete this.btn[key]
if(key in this.kbd){
for(var i in this.btn){
if(this.kbd[i] === this.kbd[key]){
return
}
}
}
}
if(key in this.kbd){
this.callback(pressed, this.kbd[key], event)
}else if(this.wildcard){
this.callback(pressed, this.kbd["wildcard"], event)
}
}
}
blurEvent(){
for(var key in this.btn){
if(this.btn[key]){
delete this.btn[key]
var name = this.kbd[key] || (this.wildcard ? "wildcard" : false)
if(name){
this.callback(false, name)
}
}
}
}
clean(){
pageEvents.keyRemove(this, "all")
pageEvents.blurRemove(this)
delete this.bindings
delete this.callback
delete this.kbd
delete this.btn
}
}

View File

@@ -0,0 +1,636 @@
/*
fuzzysort.js https://github.com/farzher/fuzzysort
SublimeText-like Fuzzy Search
fuzzysort.single('fs', 'Fuzzy Search') // {score: -16}
fuzzysort.single('test', 'test') // {score: 0}
fuzzysort.single('doesnt exist', 'target') // null
fuzzysort.go('mr', [{file:'Monitor.cpp'}, {file:'MeshRenderer.cpp'}], {key:'file'})
// [{score:-18, obj:{file:'MeshRenderer.cpp'}}, {score:-6009, obj:{file:'Monitor.cpp'}}]
fuzzysort.go('mr', ['Monitor.cpp', 'MeshRenderer.cpp'])
// [{score: -18, target: "MeshRenderer.cpp"}, {score: -6009, target: "Monitor.cpp"}]
fuzzysort.highlight(fuzzysort.single('fs', 'Fuzzy Search'), '<b>', '</b>')
// <b>F</b>uzzy <b>S</b>earch
*/
// UMD (Universal Module Definition) for fuzzysort
;(function(root, UMD) {
if(typeof define === 'function' && define.amd) define([], UMD)
else if(typeof module === 'object' && module.exports) module.exports = UMD()
else root.fuzzysort = UMD()
})(this, function UMD() { function fuzzysortNew(instanceOptions) {
var fuzzysort = {
single: function(search, target, options) { ;if(search=='farzher')return{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6]}
if(!search) return null
if(!isObj(search)) search = fuzzysort.getPreparedSearch(search)
if(!target) return null
if(!isObj(target)) target = fuzzysort.getPrepared(target)
var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo
: instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo
: true
var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo
return algorithm(search, target, search[0])
},
go: function(search, targets, options) { ;if(search=='farzher')return[{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6],obj:targets?targets[0]:null}]
if(!search) return noResults
search = fuzzysort.prepareSearch(search)
var searchLowerCode = search[0]
var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991
var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991
var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo
: instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo
: true
var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo
var resultsLen = 0; var limitedCount = 0
var targetsLen = targets.length
// This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys]
// options.keys
if(options && options.keys) {
var scoreFn = options.scoreFn || defaultScoreFn
var keys = options.keys
var keysLen = keys.length
for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i]
var objResults = new Array(keysLen)
for (var keyI = keysLen - 1; keyI >= 0; --keyI) {
var key = keys[keyI]
var target = getValue(obj, key)
if(!target) { objResults[keyI] = null; continue }
if(!isObj(target)) target = fuzzysort.getPrepared(target)
objResults[keyI] = algorithm(search, target, searchLowerCode)
}
objResults.obj = obj // before scoreFn so scoreFn can use it
var score = scoreFn(objResults)
if(score === null) continue
if(score < threshold) continue
objResults.score = score
if(resultsLen < limit) { q.add(objResults); ++resultsLen }
else {
++limitedCount
if(score > q.peek().score) q.replaceTop(objResults)
}
}
// options.key
} else if(options && options.key) {
var key = options.key
for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i]
var target = getValue(obj, key)
if(!target) continue
if(!isObj(target)) target = fuzzysort.getPrepared(target)
var result = algorithm(search, target, searchLowerCode)
if(result === null) continue
if(result.score < threshold) continue
// have to clone result so duplicate targets from different obj can each reference the correct obj
result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden
if(resultsLen < limit) { q.add(result); ++resultsLen }
else {
++limitedCount
if(result.score > q.peek().score) q.replaceTop(result)
}
}
// no keys
} else {
for(var i = targetsLen - 1; i >= 0; --i) { var target = targets[i]
if(!target) continue
if(!isObj(target)) target = fuzzysort.getPrepared(target)
var result = algorithm(search, target, searchLowerCode)
if(result === null) continue
if(result.score < threshold) continue
if(resultsLen < limit) { q.add(result); ++resultsLen }
else {
++limitedCount
if(result.score > q.peek().score) q.replaceTop(result)
}
}
}
if(resultsLen === 0) return noResults
var results = new Array(resultsLen)
for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
results.total = resultsLen + limitedCount
return results
},
goAsync: function(search, targets, options) {
var canceled = false
var p = new Promise(function(resolve, reject) { ;if(search=='farzher')return resolve([{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6],obj:targets?targets[0]:null}])
if(!search) return resolve(noResults)
search = fuzzysort.prepareSearch(search)
var searchLowerCode = search[0]
var q = fastpriorityqueue()
var iCurrent = targets.length - 1
var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991
var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991
var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo
: instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo
: true
var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo
var resultsLen = 0; var limitedCount = 0
function step() {
if(canceled) return reject('canceled')
var startMs = Date.now()
// This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys]
// options.keys
if(options && options.keys) {
var scoreFn = options.scoreFn || defaultScoreFn
var keys = options.keys
var keysLen = keys.length
for(; iCurrent >= 0; --iCurrent) {
if(iCurrent%1000/*itemsPerCheck*/ === 0) {
if(Date.now() - startMs >= 10/*asyncInterval*/) {
isNode?setImmediate(step):setTimeout(step)
return
}
}
var obj = targets[iCurrent]
var objResults = new Array(keysLen)
for (var keyI = keysLen - 1; keyI >= 0; --keyI) {
var key = keys[keyI]
var target = getValue(obj, key)
if(!target) { objResults[keyI] = null; continue }
if(!isObj(target)) target = fuzzysort.getPrepared(target)
objResults[keyI] = algorithm(search, target, searchLowerCode)
}
objResults.obj = obj // before scoreFn so scoreFn can use it
var score = scoreFn(objResults)
if(score === null) continue
if(score < threshold) continue
objResults.score = score
if(resultsLen < limit) { q.add(objResults); ++resultsLen }
else {
++limitedCount
if(score > q.peek().score) q.replaceTop(objResults)
}
}
// options.key
} else if(options && options.key) {
var key = options.key
for(; iCurrent >= 0; --iCurrent) {
if(iCurrent%1000/*itemsPerCheck*/ === 0) {
if(Date.now() - startMs >= 10/*asyncInterval*/) {
isNode?setImmediate(step):setTimeout(step)
return
}
}
var obj = targets[iCurrent]
var target = getValue(obj, key)
if(!target) continue
if(!isObj(target)) target = fuzzysort.getPrepared(target)
var result = algorithm(search, target, searchLowerCode)
if(result === null) continue
if(result.score < threshold) continue
// have to clone result so duplicate targets from different obj can each reference the correct obj
result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden
if(resultsLen < limit) { q.add(result); ++resultsLen }
else {
++limitedCount
if(result.score > q.peek().score) q.replaceTop(result)
}
}
// no keys
} else {
for(; iCurrent >= 0; --iCurrent) {
if(iCurrent%1000/*itemsPerCheck*/ === 0) {
if(Date.now() - startMs >= 10/*asyncInterval*/) {
isNode?setImmediate(step):setTimeout(step)
return
}
}
var target = targets[iCurrent]
if(!target) continue
if(!isObj(target)) target = fuzzysort.getPrepared(target)
var result = algorithm(search, target, searchLowerCode)
if(result === null) continue
if(result.score < threshold) continue
if(resultsLen < limit) { q.add(result); ++resultsLen }
else {
++limitedCount
if(result.score > q.peek().score) q.replaceTop(result)
}
}
}
if(resultsLen === 0) return resolve(noResults)
var results = new Array(resultsLen)
for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
results.total = resultsLen + limitedCount
resolve(results)
}
isNode?setImmediate(step):step() //setTimeout here is too slow
})
p.cancel = function() { canceled = true }
return p
},
highlight: function(result, hOpen, hClose) {
if(typeof hOpen == 'function') return fuzzysort.highlightCallback(result, hOpen)
if(result === null) return null
if(hOpen === undefined) hOpen = '<b>'
if(hClose === undefined) hClose = '</b>'
var highlighted = ''
var matchesIndex = 0
var opened = false
var target = result.target
var targetLen = target.length
var matchesBest = result.indexes
for(var i = 0; i < targetLen; ++i) { var char = target[i]
if(matchesBest[matchesIndex] === i) {
++matchesIndex
if(!opened) { opened = true
highlighted += hOpen
}
if(matchesIndex === matchesBest.length) {
highlighted += char + hClose + target.substr(i+1)
break
}
} else {
if(opened) { opened = false
highlighted += hClose
}
}
highlighted += char
}
return highlighted
},
highlightCallback: function(result, cb) {
if(result === null) return null
var target = result.target
var targetLen = target.length
var indexes = result.indexes
var highlighted = ''
var matchI = 0
var indexesI = 0
var opened = false
var result = []
for(var i = 0; i < targetLen; ++i) { var char = target[i]
if(indexes[indexesI] === i) {
++indexesI
if(!opened) { opened = true
result.push(highlighted); highlighted = ''
}
if(indexesI === indexes.length) {
highlighted += char
result.push(cb(highlighted, matchI++)); highlighted = ''
result.push(target.substr(i+1))
break
}
} else {
if(opened) { opened = false
result.push(cb(highlighted, matchI++)); highlighted = ''
}
}
highlighted += char
}
return result
},
prepare: function(target) {
if(!target) return {target: '', _targetLowerCodes: [0/*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], _nextBeginningIndexes: null, score: null, indexes: null, obj: null} // hidden
return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:null, score:null, indexes:null, obj:null} // hidden
},
prepareSlow: function(target) {
if(!target) return {target: '', _targetLowerCodes: [0/*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], _nextBeginningIndexes: null, score: null, indexes: null, obj: null} // hidden
return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:fuzzysort.prepareNextBeginningIndexes(target), score:null, indexes:null, obj:null} // hidden
},
prepareSearch: function(search) {
if(!search) search = ''
return fuzzysort.prepareLowerCodes(search)
},
// Below this point is only internal code
// Below this point is only internal code
// Below this point is only internal code
// Below this point is only internal code
getPrepared: function(target) {
if(target.length > 999) return fuzzysort.prepare(target) // don't cache huge targets
var targetPrepared = preparedCache.get(target)
if(targetPrepared !== undefined) return targetPrepared
targetPrepared = fuzzysort.prepare(target)
preparedCache.set(target, targetPrepared)
return targetPrepared
},
getPreparedSearch: function(search) {
if(search.length > 999) return fuzzysort.prepareSearch(search) // don't cache huge searches
var searchPrepared = preparedSearchCache.get(search)
if(searchPrepared !== undefined) return searchPrepared
searchPrepared = fuzzysort.prepareSearch(search)
preparedSearchCache.set(search, searchPrepared)
return searchPrepared
},
algorithm: function(searchLowerCodes, prepared, searchLowerCode) {
var targetLowerCodes = prepared._targetLowerCodes
var searchLen = searchLowerCodes.length
var targetLen = targetLowerCodes.length
var searchI = 0 // where we at
var targetI = 0 // where you at
var typoSimpleI = 0
var matchesSimpleLen = 0
// very basic fuzzy match; to remove non-matching targets ASAP!
// walk through target. find sequential matches.
// if all chars aren't found then exit
for(;;) {
var isMatch = searchLowerCode === targetLowerCodes[targetI]
if(isMatch) {
matchesSimple[matchesSimpleLen++] = targetI
++searchI; if(searchI === searchLen) break
searchLowerCode = searchLowerCodes[typoSimpleI===0?searchI : (typoSimpleI===searchI?searchI+1 : (typoSimpleI===searchI-1?searchI-1 : searchI))]
}
++targetI; if(targetI >= targetLen) { // Failed to find searchI
// Check for typo or exit
// we go as far as possible before trying to transpose
// then we transpose backwards until we reach the beginning
for(;;) {
if(searchI <= 1) return null // not allowed to transpose first char
if(typoSimpleI === 0) { // we haven't tried to transpose yet
--searchI
var searchLowerCodeNew = searchLowerCodes[searchI]
if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char
typoSimpleI = searchI
} else {
if(typoSimpleI === 1) return null // reached the end of the line for transposing
--typoSimpleI
searchI = typoSimpleI
searchLowerCode = searchLowerCodes[searchI + 1]
var searchLowerCodeNew = searchLowerCodes[searchI]
if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char
}
matchesSimpleLen = searchI
targetI = matchesSimple[matchesSimpleLen - 1] + 1
break
}
}
}
var searchI = 0
var typoStrictI = 0
var successStrict = false
var matchesStrictLen = 0
var nextBeginningIndexes = prepared._nextBeginningIndexes
if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target)
var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
// Our target string successfully matched all characters in sequence!
// Let's try a more advanced and strict test to improve the score
// only count it as a match if it's consecutive or a beginning character!
if(targetI !== targetLen) for(;;) {
if(targetI >= targetLen) {
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
if(searchI <= 0) { // We failed to push chars forward for a better match
// transpose, starting from the beginning
++typoStrictI; if(typoStrictI > searchLen-2) break
if(searchLowerCodes[typoStrictI] === searchLowerCodes[typoStrictI+1]) continue // doesn't make sense to transpose a repeat char
targetI = firstPossibleI
continue
}
--searchI
var lastMatch = matchesStrict[--matchesStrictLen]
targetI = nextBeginningIndexes[lastMatch]
} else {
var isMatch = searchLowerCodes[typoStrictI===0?searchI : (typoStrictI===searchI?searchI+1 : (typoStrictI===searchI-1?searchI-1 : searchI))] === targetLowerCodes[targetI]
if(isMatch) {
matchesStrict[matchesStrictLen++] = targetI
++searchI; if(searchI === searchLen) { successStrict = true; break }
++targetI
} else {
targetI = nextBeginningIndexes[targetI]
}
}
}
{ // tally up the score & keep track of matches for highlighting later
if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen }
else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen }
var score = 0
var lastTargetI = -1
for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i]
// score only goes down if they're not consecutive
if(lastTargetI !== targetI - 1) score -= targetI
lastTargetI = targetI
}
if(!successStrict) {
score *= 1000
if(typoSimpleI !== 0) score += -20/*typoPenalty*/
} else {
if(typoStrictI !== 0) score += -20/*typoPenalty*/
}
score -= targetLen - searchLen
prepared.score = score
prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i]
return prepared
}
},
algorithmNoTypo: function(searchLowerCodes, prepared, searchLowerCode) {
var targetLowerCodes = prepared._targetLowerCodes
var searchLen = searchLowerCodes.length
var targetLen = targetLowerCodes.length
var searchI = 0 // where we at
var targetI = 0 // where you at
var matchesSimpleLen = 0
// very basic fuzzy match; to remove non-matching targets ASAP!
// walk through target. find sequential matches.
// if all chars aren't found then exit
for(;;) {
var isMatch = searchLowerCode === targetLowerCodes[targetI]
if(isMatch) {
matchesSimple[matchesSimpleLen++] = targetI
++searchI; if(searchI === searchLen) break
searchLowerCode = searchLowerCodes[searchI]
}
++targetI; if(targetI >= targetLen) return null // Failed to find searchI
}
var searchI = 0
var successStrict = false
var matchesStrictLen = 0
var nextBeginningIndexes = prepared._nextBeginningIndexes
if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target)
var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
// Our target string successfully matched all characters in sequence!
// Let's try a more advanced and strict test to improve the score
// only count it as a match if it's consecutive or a beginning character!
if(targetI !== targetLen) for(;;) {
if(targetI >= targetLen) {
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
if(searchI <= 0) break // We failed to push chars forward for a better match
--searchI
var lastMatch = matchesStrict[--matchesStrictLen]
targetI = nextBeginningIndexes[lastMatch]
} else {
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
if(isMatch) {
matchesStrict[matchesStrictLen++] = targetI
++searchI; if(searchI === searchLen) { successStrict = true; break }
++targetI
} else {
targetI = nextBeginningIndexes[targetI]
}
}
}
{ // tally up the score & keep track of matches for highlighting later
if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen }
else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen }
var score = 0
var lastTargetI = -1
for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i]
// score only goes down if they're not consecutive
if(lastTargetI !== targetI - 1) score -= targetI
lastTargetI = targetI
}
if(!successStrict) score *= 1000
score -= targetLen - searchLen
prepared.score = score
prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i]
return prepared
}
},
prepareLowerCodes: function(str) {
var strLen = str.length
var lowerCodes = [] // new Array(strLen) sparse array is too slow
var lower = str.toLowerCase()
for(var i = 0; i < strLen; ++i) lowerCodes[i] = lower.charCodeAt(i)
return lowerCodes
},
prepareBeginningIndexes: function(target) {
var targetLen = target.length
var beginningIndexes = []; var beginningIndexesLen = 0
var wasUpper = false
var wasAlphanum = false
for(var i = 0; i < targetLen; ++i) {
var targetCode = target.charCodeAt(i)
var isUpper = targetCode>=65&&targetCode<=90
var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
wasUpper = isUpper
wasAlphanum = isAlphanum
if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
}
return beginningIndexes
},
prepareNextBeginningIndexes: function(target) {
var targetLen = target.length
var beginningIndexes = fuzzysort.prepareBeginningIndexes(target)
var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
var lastIsBeginning = beginningIndexes[0]
var lastIsBeginningI = 0
for(var i = 0; i < targetLen; ++i) {
if(lastIsBeginning > i) {
nextBeginningIndexes[i] = lastIsBeginning
} else {
lastIsBeginning = beginningIndexes[++lastIsBeginningI]
nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
}
}
return nextBeginningIndexes
},
cleanup: cleanup,
new: fuzzysortNew,
}
return fuzzysort
} // fuzzysortNew
// This stuff is outside fuzzysortNew, because it's shared with instances of fuzzysort.new()
var isNode = typeof require !== 'undefined' && typeof window === 'undefined'
var MyMap = typeof Map === 'function' ? Map : function(){var s=Object.create(null);this.get=function(k){return s[k]};this.set=function(k,val){s[k]=val;return this};this.clear=function(){s=Object.create(null)}}
var preparedCache = new MyMap()
var preparedSearchCache = new MyMap()
var noResults = []; noResults.total = 0
var matchesSimple = []; var matchesStrict = []
function cleanup() { preparedCache.clear(); preparedSearchCache.clear(); matchesSimple = []; matchesStrict = [] }
function defaultScoreFn(a) {
var max = -9007199254740991
for (var i = a.length - 1; i >= 0; --i) {
var result = a[i]; if(result === null) continue
var score = result.score
if(score > max) max = score
}
if(max === -9007199254740991) return null
return max
}
// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
// prop = 'key1.key2' 10ms
// prop = ['key1', 'key2'] 27ms
function getValue(obj, prop) {
var tmp = obj[prop]; if(tmp !== undefined) return tmp
var segs = prop
if(!Array.isArray(prop)) segs = prop.split('.')
var len = segs.length
var i = -1
while (obj && (++i < len)) obj = obj[segs[i]]
return obj
}
function isObj(x) { return typeof x === 'object' } // faster as a function
// Hacked version of https://github.com/lemire/FastPriorityQueue.js
var fastpriorityqueue=function(){var r=[],o=0,e={};function n(){for(var e=0,n=r[e],c=1;c<o;){var f=c+1;e=c,f<o&&r[f].score<r[c].score&&(e=f),r[e-1>>1]=r[e],c=1+(e<<1)}for(var a=e-1>>1;e>0&&n.score<r[a].score;a=(e=a)-1>>1)r[e]=r[a];r[e]=n}return e.add=function(e){var n=o;r[o++]=e;for(var c=n-1>>1;n>0&&e.score<r[c].score;c=(n=c)-1>>1)r[n]=r[c];r[n]=e},e.poll=function(){if(0!==o){var e=r[0];return r[0]=r[--o],n(),e}},e.peek=function(e){if(0!==o)return r[0]},e.replaceTop=function(o){r[0]=o,n()},e};
var q = fastpriorityqueue() // reuse this, except for async, it needs to make its own
return fuzzysortNew()
}) // UMD
// TODO: (performance) wasm version!?
// TODO: (performance) threads?
// TODO: (performance) avoid cache misses
// TODO: (performance) preparedCache is a memory leak
// TODO: (like sublime) backslash === forwardslash
// TODO: (like sublime) spaces: "a b" should do 2 searches 1 for a and 1 for b
// TODO: (scoring) garbage in targets that allows most searches to strict match need a penality
// TODO: (performance) idk if allowTypo is optimized

199
public/src/js/lib/jszip.js Normal file
View File

@@ -0,0 +1,199 @@
/*!
JSZip v3.7.1 - A JavaScript class for generating and reading zip files
<http://stuartk.com/jszip>
(c) 2009-2016 Stuart Knightley <stuart [at] stuartk.com>
Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/master/LICENSE.markdown.
JSZip uses the library pako released under the MIT license :
https://github.com/nodeca/pako/blob/master/LICENSE
*/
function JSZip(){return function s(a,o,h){function u(r,t){if(!o[r]){if(!a[r]){var e="function"==typeof require&&require;if(!t&&e)return e(r,!0);if(l)return l(r,!0);var i=new Error("Cannot find module '"+r+"'");throw i.code="MODULE_NOT_FOUND",i;}var n=o[r]={exports:{}};a[r][0].call(n.exports,function(t){var e=a[r][1][t];return u(e||t)},n,n.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,t=0;t<h.length;t++)u(h[t]);return u}({1:[function(t,e,r){var c=t("./utils"),d=
t("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(t){for(var e,r,i,n,s,a,o,h=[],u=0,l=t.length,f=l,d="string"!==c.getTypeOf(t);u<t.length;)f=l-u,i=d?(e=t[u++],r=u<l?t[u++]:0,u<l?t[u++]:0):(e=t.charCodeAt(u++),r=u<l?t.charCodeAt(u++):0,u<l?t.charCodeAt(u++):0),n=e>>2,s=(3&e)<<4|r>>4,a=1<f?(15&r)<<2|i>>6:64,o=2<f?63&i:64,h.push(p.charAt(n)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(t){var e,r,i,n,s,a,o=0,h=0,u="data:";
if(t.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(t=t.replace(/[^A-Za-z0-9\+\/=]/g,"")).length/4;if(t.charAt(t.length-1)===p.charAt(64)&&f--,t.charAt(t.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=d.uint8array?new Uint8Array(0|f):new Array(0|f);o<t.length;)e=p.indexOf(t.charAt(o++))<<2|(n=p.indexOf(t.charAt(o++)))>>4,r=(15&n)<<4|(s=p.indexOf(t.charAt(o++)))>>2,i=(3&s)<<6|(a=p.indexOf(t.charAt(o++))),
l[h++]=e,64!==s&&(l[h++]=r),64!==a&&(l[h++]=i);return l}},{"./support":30,"./utils":32}],2:[function(t,e,r){var i=t("./external"),n=t("./stream/DataWorker"),s=t("./stream/Crc32Probe"),a=t("./stream/DataLengthProbe");function o(t,e,r,i,n){this.compressedSize=t,this.uncompressedSize=e,this.crc32=r,this.compression=i,this.compressedContent=n}o.prototype={getContentWorker:function(){var t=(new n(i.Promise.resolve(this.compressedContent))).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),
e=this;return t.on("end",function(){if(this.streamInfo.data_length!==e.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch");}),t},getCompressedWorker:function(){return(new n(i.Promise.resolve(this.compressedContent))).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(t,e,r){return t.pipe(new s).pipe(new a("uncompressedSize")).pipe(e.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",
e)},e.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,e,r){var i=t("./stream/GenericWorker");r.STORE={magic:"\x00\x00",compressWorker:function(t){return new i("STORE compression")},uncompressWorker:function(){return new i("STORE decompression")}},r.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,e,r){var i=t("./utils");var o=function(){for(var t,e=[],r=0;r<256;r++){t=r;for(var i=0;i<8;i++)t=1&
t?3988292384^t>>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e){return void 0!==t&&t.length?"string"!==i.getTypeOf(t)?function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a<s;a++)t=t>>>8^n[255&(t^e[a])];return-1^t}(0|e,t,t.length,0):function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a<s;a++)t=t>>>8^n[255&(t^e.charCodeAt(a))];return-1^t}(0|e,t,t.length,0):0}},{"./utils":32}],5:[function(t,e,r){r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,
r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(t,e,r){var i=null;i="undefined"!=typeof Promise?Promise:t("lie"),e.exports={Promise:i}},{lie:37}],7:[function(t,e,r){var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,n=t("pako"),s=t("./utils"),a=t("./stream/GenericWorker"),o=i?"uint8array":"array";function h(t,e){a.call(this,"FlateWorker/"+t),this._pako=null,this._pakoAction=t,this._pakoOptions=e,this.meta={}}r.magic=
"\b\x00",s.inherits(h,a),h.prototype.processChunk=function(t){this.meta=t.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,t.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new n[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var e=this;this._pako.onData=
function(t){e.push({data:t,meta:e.meta})}},r.compressWorker=function(t){return new h("Deflate",t)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,e,r){function A(t,e){var r,i="";for(r=0;r<e;r++)i+=String.fromCharCode(255&t),t>>>=8;return i}function i(t,e,r,i,n,s){var a,o,h=t.file,u=t.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),d=I.transformTo("string",O.utf8encode(h.name)),c=h.comment,p=I.transformTo("string",
s(c)),m=I.transformTo("string",O.utf8encode(c)),_=d.length!==h.name.length,g=m.length!==c.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};e&&!r||(x.crc32=t.crc32,x.compressedSize=t.compressedSize,x.uncompressedSize=t.uncompressedSize);var S=0;e&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===n?(C=798,z|=function(t,e){var r=t;return t||(r=e?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(t){return 63&(t||0)}(h.dosPermissions)),
a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+d,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\x00",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+
A(p.length,2)+"\x00\x00\x00\x00"+A(z,4)+A(i,4)+f+b+p}}var I=t("../utils"),n=t("../stream/GenericWorker"),O=t("../utf8"),B=t("../crc32"),R=t("../signature");function s(t,e,r,i){n.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=e,this.zipPlatform=r,this.encodeFileName=i,this.streamFiles=t,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,n),s.prototype.push=function(t){var e=t.meta.percent||
0,r=this.entriesCount,i=this._sources.length;this.accumulate?this.contentBuffer.push(t):(this.bytesWritten+=t.data.length,n.prototype.push.call(this,{data:t.data,meta:{currentFile:this.currentFile,percent:r?(e+100*(r-i-1))/r:100}}))},s.prototype.openedSource=function(t){this.currentSourceOffset=this.bytesWritten,this.currentFile=t.file.name;var e=this.streamFiles&&!t.file.dir;if(e){var r=i(t,e,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=
!0},s.prototype.closedSource=function(t){this.accumulate=!1;var e=this.streamFiles&&!t.file.dir,r=i(t,e,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),e)this.push({data:function(t){return R.DATA_DESCRIPTOR+A(t.crc32,4)+A(t.compressedSize,4)+A(t.uncompressedSize,4)}(t),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=
function(){for(var t=this.bytesWritten,e=0;e<this.dirRecords.length;e++)this.push({data:this.dirRecords[e],meta:{percent:100}});var r=this.bytesWritten-t,i=function(t,e,r,i,n){var s=I.transformTo("string",n(i));return R.CENTRAL_DIRECTORY_END+"\x00\x00\x00\x00"+A(t,2)+A(t,2)+A(e,4)+A(r,4)+A(s.length,2)+s}(this.dirRecords.length,r,t,this.zipComment,this.encodeFileName);this.push({data:i,meta:{percent:100}})},s.prototype.prepareNextSource=function(){this.previous=this._sources.shift(),this.openedSource(this.previous.streamInfo),
this.isPaused?this.previous.pause():this.previous.resume()},s.prototype.registerPrevious=function(t){this._sources.push(t);var e=this;return t.on("data",function(t){e.processChunk(t)}),t.on("end",function(){e.closedSource(e.previous.streamInfo),e._sources.length?e.prepareNextSource():e.end()}),t.on("error",function(t){e.error(t)}),this},s.prototype.resume=function(){return!!n.prototype.resume.call(this)&&(!this.previous&&this._sources.length?(this.prepareNextSource(),!0):this.previous||this._sources.length||
this.generatedError?void 0:(this.end(),!0))},s.prototype.error=function(t){var e=this._sources;if(!n.prototype.error.call(this,t))return!1;for(var r=0;r<e.length;r++)try{e[r].error(t)}catch(t$0){}return!0},s.prototype.lock=function(){n.prototype.lock.call(this);for(var t=this._sources,e=0;e<t.length;e++)t[e].lock()},e.exports=s},{"../crc32":4,"../signature":23,"../stream/GenericWorker":28,"../utf8":31,"../utils":32}],9:[function(t,e,r){var u=t("../compressions"),i=t("./ZipFileWorker");r.generateWorker=
function(t,a,e){var o=new i(a.streamFiles,e,a.platform,a.encodeFileName),h=0;try{t.forEach(function(t,e){h++;var r=function(t,e){var r=t||e,i=u[r];if(!i)throw new Error(r+" is not a valid compression method !");return i}(e.options.compression,a.compression),i=e.options.compressionOptions||a.compressionOptions||{},n=e.dir,s=e.date;e._compressWorker(r,i).withStreamInfo("file",{name:t,dir:n,date:s,comment:e.comment||"",unixPermissions:e.unixPermissions,dosPermissions:e.dosPermissions}).pipe(o)}),o.entriesCount=
h}catch(t$1){o.error(t$1)}return o}},{"../compressions":3,"./ZipFileWorker":8}],10:[function(t,e,r){function i(){if(!(this instanceof i))return new i;if(arguments.length)throw new Error("The constructor with parameters has been removed in JSZip 3.0, please check the upgrade guide.");this.files=Object.create(null),this.comment=null,this.root="",this.clone=function(){var t=new i;for(var e in this)"function"!=typeof this[e]&&(t[e]=this[e]);return t}}(i.prototype=t("./object")).loadAsync=t("./load"),
i.support=t("./support"),i.defaults=t("./defaults"),i.version="3.7.1",i.loadAsync=function(t,e){return(new i).loadAsync(t,e)},i.external=t("./external"),e.exports=i},{"./defaults":5,"./external":6,"./load":11,"./object":15,"./support":30}],11:[function(t,e,r){var i=t("./utils"),n=t("./external"),o=t("./utf8"),h=t("./zipEntries"),s=t("./stream/Crc32Probe"),u=t("./nodejsUtils");function l(i){return new n.Promise(function(t,e){var r=i.decompressed.getContentWorker().pipe(new s);r.on("error",function(t){e(t)}).on("end",
function(){r.streamInfo.crc32!==i.decompressed.crc32?e(new Error("Corrupted zip : CRC32 mismatch")):t()}).resume()})}e.exports=function(t,s){var a=this;return s=i.extend(s||{},{base64:!1,checkCRC32:!1,optimizedBinaryString:!1,createFolders:!1,decodeFileName:o.utf8decode}),u.isNode&&u.isStream(t)?n.Promise.reject(new Error("JSZip can't accept a stream when loading a zip file.")):i.prepareContent("the loaded zip file",t,!0,s.optimizedBinaryString,s.base64).then(function(t){var e=new h(s);return e.load(t),
e}).then(function(t){var e=[n.Promise.resolve(t)],r=t.files;if(s.checkCRC32)for(var i=0;i<r.length;i++)e.push(l(r[i]));return n.Promise.all(e)}).then(function(t){for(var e=t.shift(),r=e.files,i=0;i<r.length;i++){var n=r[i];a.file(n.fileNameStr,n.decompressed,{binary:!0,optimizedBinaryString:!0,date:n.date,dir:n.dir,comment:n.fileCommentStr.length?n.fileCommentStr:null,unixPermissions:n.unixPermissions,dosPermissions:n.dosPermissions,createFolders:s.createFolders})}return e.zipComment.length&&(a.comment=
e.zipComment),a})}},{"./external":6,"./nodejsUtils":14,"./stream/Crc32Probe":25,"./utf8":31,"./utils":32,"./zipEntries":33}],12:[function(t,e,r){var i=t("../utils"),n=t("../stream/GenericWorker");function s(t,e){n.call(this,"Nodejs stream input adapter for "+t),this._upstreamEnded=!1,this._bindStream(e)}i.inherits(s,n),s.prototype._bindStream=function(t){var e=this;(this._stream=t).pause(),t.on("data",function(t){e.push({data:t,meta:{percent:0}})}).on("error",function(t){e.isPaused?this.generatedError=
t:e.error(t)}).on("end",function(){e.isPaused?e._upstreamEnded=!0:e.end()})},s.prototype.pause=function(){return!!n.prototype.pause.call(this)&&(this._stream.pause(),!0)},s.prototype.resume=function(){return!!n.prototype.resume.call(this)&&(this._upstreamEnded?this.end():this._stream.resume(),!0)},e.exports=s},{"../stream/GenericWorker":28,"../utils":32}],13:[function(t,e,r){var n=t("readable-stream").Readable;function i(t,e,r){n.call(this,e),this._helper=t;var i=this;t.on("data",function(t,e){i.push(t)||
i._helper.pause(),r&&r(e)}).on("error",function(t){i.emit("error",t)}).on("end",function(){i.push(null)})}t("../utils").inherits(i,n),i.prototype._read=function(){this._helper.resume()},e.exports=i},{"../utils":32,"readable-stream":16}],14:[function(t,e,r){e.exports={isNode:"undefined"!=typeof Buffer,newBufferFrom:function(t,e){if(Buffer.from&&Buffer.from!==Uint8Array.from)return Buffer.from(t,e);if("number"==typeof t)throw new Error('The "data" argument must not be a number');return new Buffer(t,
e)},allocBuffer:function(t){if(Buffer.alloc)return Buffer.alloc(t);var e=new Buffer(t);return e.fill(0),e},isBuffer:function(t){return Buffer.isBuffer(t)},isStream:function(t){return t&&"function"==typeof t.on&&"function"==typeof t.pause&&"function"==typeof t.resume}}},{}],15:[function(t,e,r){function s(t,e,r){var i,n=u.getTypeOf(e),s=u.extend(r||{},f);s.date=s.date||new Date,null!==s.compression&&(s.compression=s.compression.toUpperCase()),"string"==typeof s.unixPermissions&&(s.unixPermissions=parseInt(s.unixPermissions,
8)),s.unixPermissions&&16384&s.unixPermissions&&(s.dir=!0),s.dosPermissions&&16&s.dosPermissions&&(s.dir=!0),s.dir&&(t=g(t)),s.createFolders&&(i=_(t))&&b.call(this,i,!0);var a="string"===n&&!1===s.binary&&!1===s.base64;r&&void 0!==r.binary||(s.binary=!a),(e instanceof d&&0===e.uncompressedSize||s.dir||!e||0===e.length)&&(s.base64=!1,s.binary=!0,e="",s.compression="STORE",n="string");var o=null;o=e instanceof d||e instanceof l?e:p.isNode&&p.isStream(e)?new m(t,e):u.prepareContent(t,e,s.binary,s.optimizedBinaryString,
s.base64);var h=new c(t,o,s);this.files[t]=h}var n=t("./utf8"),u=t("./utils"),l=t("./stream/GenericWorker"),a=t("./stream/StreamHelper"),f=t("./defaults"),d=t("./compressedObject"),c=t("./zipObject"),o=t("./generate"),p=t("./nodejsUtils"),m=t("./nodejs/NodejsStreamInputAdapter"),_=function(t){"/"===t.slice(-1)&&(t=t.substring(0,t.length-1));var e=t.lastIndexOf("/");return 0<e?t.substring(0,e):""},g=function(t){return"/"!==t.slice(-1)&&(t+="/"),t},b=function(t,e){return e=void 0!==e?e:f.createFolders,
t=g(t),this.files[t]||s.call(this,t,null,{dir:!0,createFolders:e}),this.files[t]};function h(t){return"[object RegExp]"===Object.prototype.toString.call(t)}var i={load:function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.");},forEach:function(t){var e,r,i;for(e in this.files)i=this.files[e],(r=e.slice(this.root.length,e.length))&&e.slice(0,this.root.length)===this.root&&t(r,i)},filter:function(r){var i=[];return this.forEach(function(t,e){r(t,e)&&i.push(e)}),
i},file:function(t,e,r){if(1!==arguments.length)return t=this.root+t,s.call(this,t,e,r),this;if(h(t)){var i=t;return this.filter(function(t,e){return!e.dir&&i.test(t)})}var n=this.files[this.root+t];return n&&!n.dir?n:null},folder:function(r){if(!r)return this;if(h(r))return this.filter(function(t,e){return e.dir&&r.test(t)});var t=this.root+r,e=b.call(this,t),i=this.clone();return i.root=e.name,i},remove:function(r){r=this.root+r;var t=this.files[r];if(t||("/"!==r.slice(-1)&&(r+="/"),t=this.files[r]),
t&&!t.dir)delete this.files[r];else for(var e=this.filter(function(t,e){return e.name.slice(0,r.length)===r}),i=0;i<e.length;i++)delete this.files[e[i].name];return this},generate:function(t){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.");},generateInternalStream:function(t){var e,r={};try{if((r=u.extend(t||{},{streamFiles:!1,compression:"STORE",compressionOptions:null,type:"",platform:"DOS",comment:null,mimeType:"application/zip",encodeFileName:n.utf8encode})).type=
r.type.toLowerCase(),r.compression=r.compression.toUpperCase(),"binarystring"===r.type&&(r.type="string"),!r.type)throw new Error("No output type specified.");u.checkSupport(r.type),"darwin"!==r.platform&&"freebsd"!==r.platform&&"linux"!==r.platform&&"sunos"!==r.platform||(r.platform="UNIX"),"win32"===r.platform&&(r.platform="DOS");var i=r.comment||this.comment||"";e=o.generateWorker(this,r,i)}catch(t$2){(e=new l("error")).error(t$2)}return new a(e,r.type||"string",r.mimeType)},generateAsync:function(t,
e){return this.generateInternalStream(t).accumulate(e)},generateNodeStream:function(t,e){return(t=t||{}).type||(t.type="nodebuffer"),this.generateInternalStream(t).toNodejsStream(e)}};e.exports=i},{"./compressedObject":2,"./defaults":5,"./generate":9,"./nodejs/NodejsStreamInputAdapter":12,"./nodejsUtils":14,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31,"./utils":32,"./zipObject":35}],16:[function(t,e,r){e.exports=t("stream")},{stream:void 0}],17:[function(t,e,r){var i=t("./DataReader");
function n(t){i.call(this,t);for(var e=0;e<this.data.length;e++)t[e]=255&t[e]}t("../utils").inherits(n,i),n.prototype.byteAt=function(t){return this.data[this.zero+t]},n.prototype.lastIndexOfSignature=function(t){for(var e=t.charCodeAt(0),r=t.charCodeAt(1),i=t.charCodeAt(2),n=t.charCodeAt(3),s=this.length-4;0<=s;--s)if(this.data[s]===e&&this.data[s+1]===r&&this.data[s+2]===i&&this.data[s+3]===n)return s-this.zero;return-1},n.prototype.readAndCheckSignature=function(t){var e=t.charCodeAt(0),r=t.charCodeAt(1),
i=t.charCodeAt(2),n=t.charCodeAt(3),s=this.readData(4);return e===s[0]&&r===s[1]&&i===s[2]&&n===s[3]},n.prototype.readData=function(t){if(this.checkOffset(t),0===t)return[];var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./DataReader":18}],18:[function(t,e,r){var i=t("../utils");function n(t){this.data=t,this.length=t.length,this.index=0,this.zero=0}n.prototype={checkOffset:function(t){this.checkIndex(this.index+t)},checkIndex:function(t){if(this.length<
this.zero+t||t<0)throw new Error("End of data reached (data length = "+this.length+", asked index = "+t+"). Corrupted zip ?");},setIndex:function(t){this.checkIndex(t),this.index=t},skip:function(t){this.setIndex(this.index+t)},byteAt:function(t){},readInt:function(t){var e,r=0;for(this.checkOffset(t),e=this.index+t-1;e>=this.index;e--)r=(r<<8)+this.byteAt(e);return this.index+=t,r},readString:function(t){return i.transformTo("string",this.readData(t))},readData:function(t){},lastIndexOfSignature:function(t){},
readAndCheckSignature:function(t){},readDate:function(){var t=this.readInt(4);return new Date(Date.UTC(1980+(t>>25&127),(t>>21&15)-1,t>>16&31,t>>11&31,t>>5&63,(31&t)<<1))}},e.exports=n},{"../utils":32}],19:[function(t,e,r){var i=t("./Uint8ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./Uint8ArrayReader":21}],
20:[function(t,e,r){var i=t("./DataReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.byteAt=function(t){return this.data.charCodeAt(this.zero+t)},n.prototype.lastIndexOfSignature=function(t){return this.data.lastIndexOf(t)-this.zero},n.prototype.readAndCheckSignature=function(t){return t===this.readData(4)},n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,
"./DataReader":18}],21:[function(t,e,r){var i=t("./ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){if(this.checkOffset(t),0===t)return new Uint8Array(0);var e=this.data.subarray(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./ArrayReader":17}],22:[function(t,e,r){var i=t("../utils"),n=t("../support"),s=t("./ArrayReader"),a=t("./StringReader"),o=t("./NodeBufferReader"),h=t("./Uint8ArrayReader");
e.exports=function(t){var e=i.getTypeOf(t);return i.checkSupport(e),"string"!==e||n.uint8array?"nodebuffer"===e?new o(t):n.uint8array?new h(i.transformTo("uint8array",t)):new s(i.transformTo("array",t)):new a(t)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,e,r){r.LOCAL_FILE_HEADER="PK\u0003\u0004",r.CENTRAL_FILE_HEADER="PK\u0001\u0002",r.CENTRAL_DIRECTORY_END="PK\u0005\u0006",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR=
"PK\u0006\u0007",r.ZIP64_CENTRAL_DIRECTORY_END="PK\u0006\u0006",r.DATA_DESCRIPTOR="PK\u0007\b"},{}],24:[function(t,e,r){var i=t("./GenericWorker"),n=t("../utils");function s(t){i.call(this,"ConvertWorker to "+t),this.destType=t}n.inherits(s,i),s.prototype.processChunk=function(t){this.push({data:n.transformTo(this.destType,t.data),meta:t.meta})},e.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(t,e,r){var i=t("./GenericWorker"),n=t("../crc32");function s(){i.call(this,"Crc32Probe"),
this.withStreamInfo("crc32",0)}t("../utils").inherits(s,i),s.prototype.processChunk=function(t){this.streamInfo.crc32=n(t.data,this.streamInfo.crc32||0),this.push(t)},e.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,e,r){var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataLengthProbe for "+t),this.propName=t,this.withStreamInfo(t,0)}i.inherits(s,n),s.prototype.processChunk=function(t){if(t){var e=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=
e+t.data.length}n.prototype.processChunk.call(this,t)},e.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(t,e,r){var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataWorker");var e=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,t.then(function(t){e.dataIsReady=!0,e.data=t,e.max=t&&t.length||0,e.type=i.getTypeOf(t),e.isPaused||e._tickAndRepeat()},function(t){e.error(t)})}i.inherits(s,n),s.prototype.cleanUp=function(){n.prototype.cleanUp.call(this),
this.data=null},s.prototype.resume=function(){return!!n.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,i.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(i.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var t=null,e=Math.min(this.max,this.index+16384);
if(this.index>=this.max)return this.end();switch(this.type){case "string":t=this.data.substring(this.index,e);break;case "uint8array":t=this.data.subarray(this.index,e);break;case "array":case "nodebuffer":t=this.data.slice(this.index,e)}return this.index=e,this.push({data:t,meta:{percent:this.max?this.index/this.max*100:0}})},e.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(t,e,r){function i(t){this.name=t||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo=
{},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}i.prototype={push:function(t){this.emit("data",t)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(t$3){this.emit("error",t$3)}return!0},error:function(t){return!this.isFinished&&(this.isPaused?this.generatedError=t:(this.isFinished=!0,this.emit("error",t),this.previous&&this.previous.error(t),this.cleanUp()),!0)},
on:function(t,e){return this._listeners[t].push(e),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(t,e){if(this._listeners[t])for(var r=0;r<this._listeners[t].length;r++)this._listeners[t][r].call(this,e)},pipe:function(t){return t.registerPrevious(this)},registerPrevious:function(t){if(this.isLocked)throw new Error("The stream '"+this+"' has already been used.");this.streamInfo=t.streamInfo,this.mergeStreamInfo(),this.previous=
t;var e=this;return t.on("data",function(t){e.processChunk(t)}),t.on("end",function(){e.end()}),t.on("error",function(t){e.error(t)}),this},pause:function(){return!this.isPaused&&!this.isFinished&&(this.isPaused=!0,this.previous&&this.previous.pause(),!0)},resume:function(){if(!this.isPaused||this.isFinished)return!1;var t=this.isPaused=!1;return this.generatedError&&(this.error(this.generatedError),t=!0),this.previous&&this.previous.resume(),!t},flush:function(){},processChunk:function(t){this.push(t)},
withStreamInfo:function(t,e){return this.extraStreamInfo[t]=e,this.mergeStreamInfo(),this},mergeStreamInfo:function(){for(var t in this.extraStreamInfo)this.extraStreamInfo.hasOwnProperty(t)&&(this.streamInfo[t]=this.extraStreamInfo[t])},lock:function(){if(this.isLocked)throw new Error("The stream '"+this+"' has already been used.");this.isLocked=!0,this.previous&&this.previous.lock()},toString:function(){var t="Worker "+this.name;return this.previous?this.previous+" -> "+t:t}},e.exports=i},{}],29:[function(t,
e,r){var h=t("../utils"),n=t("./ConvertWorker"),s=t("./GenericWorker"),u=t("../base64"),i=t("../support"),a=t("../external"),o=null;if(i.nodestream)try{o=t("../nodejs/NodejsStreamOutputAdapter")}catch(t$4){}function l(t,o){return new a.Promise(function(e,r){var i=[],n=t._internalType,s=t._outputType,a=t._mimeType;t.on("data",function(t,e){i.push(t),o&&o(e)}).on("error",function(t){i=[],r(t)}).on("end",function(){try{var t=function(t,e,r){switch(t){case "blob":return h.newBlob(h.transformTo("arraybuffer",
e),r);case "base64":return u.encode(e);default:return h.transformTo(t,e)}}(s,function(t,e){var r,i=0,n=null,s=0;for(r=0;r<e.length;r++)s+=e[r].length;switch(t){case "string":return e.join("");case "array":return Array.prototype.concat.apply([],e);case "uint8array":for(n=new Uint8Array(s),r=0;r<e.length;r++)n.set(e[r],i),i+=e[r].length;return n;case "nodebuffer":return Buffer.concat(e);default:throw new Error("concat : unsupported type '"+t+"'");}}(n,i),a);e(t)}catch(t$5){r(t$5)}i=[]}).resume()})}
function f(t,e,r){var i=e;switch(e){case "blob":case "arraybuffer":i="uint8array";break;case "base64":i="string"}try{this._internalType=i,this._outputType=e,this._mimeType=r,h.checkSupport(i),this._worker=t.pipe(new n(i)),t.lock()}catch(t$6){this._worker=new s("error"),this._worker.error(t$6)}}f.prototype={accumulate:function(t){return l(this,t)},on:function(t,e){var r=this;return"data"===t?this._worker.on(t,function(t){e.call(r,t.data,t.meta)}):this._worker.on(t,function(){h.delay(e,arguments,r)}),
this},resume:function(){return h.delay(this._worker.resume,[],this._worker),this},pause:function(){return this._worker.pause(),this},toNodejsStream:function(t){if(h.checkSupport("nodestream"),"nodebuffer"!==this._outputType)throw new Error(this._outputType+" is not supported by this method");return new o(this,{objectMode:"nodebuffer"!==this._outputType},t)}},e.exports=f},{"../base64":1,"../external":6,"../nodejs/NodejsStreamOutputAdapter":13,"../support":30,"../utils":32,"./ConvertWorker":24,"./GenericWorker":28}],
30:[function(t,e,r){if(r.base64=!0,r.array=!0,r.string=!0,r.arraybuffer="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof Uint8Array,r.nodebuffer="undefined"!=typeof Buffer,r.uint8array="undefined"!=typeof Uint8Array,"undefined"==typeof ArrayBuffer)r.blob=!1;else{var i=new ArrayBuffer(0);try{r.blob=0===(new Blob([i],{type:"application/zip"})).size}catch(t$8){try{var n=new (self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);n.append(i),r.blob=0===n.getBlob("application/zip").size}catch(t$7){r.blob=
!1}}}try{r.nodestream=!!t("readable-stream").Readable}catch(t$9){r.nodestream=!1}},{"readable-stream":16}],31:[function(t,e,s){for(var o=t("./utils"),h=t("./support"),r=t("./nodejsUtils"),i=t("./stream/GenericWorker"),u=new Array(256),n=0;n<256;n++)u[n]=252<=n?6:248<=n?5:240<=n?4:224<=n?3:192<=n?2:1;u[254]=u[254]=1;function a(){i.call(this,"utf-8 decode"),this.leftOver=null}function l(){i.call(this,"utf-8 encode")}s.utf8encode=function(t){return h.nodebuffer?r.newBufferFrom(t,"utf-8"):function(t){var e,
r,i,n,s,a=t.length,o=0;for(n=0;n<a;n++)55296==(64512&(r=t.charCodeAt(n)))&&n+1<a&&56320==(64512&(i=t.charCodeAt(n+1)))&&(r=65536+(r-55296<<10)+(i-56320),n++),o+=r<128?1:r<2048?2:r<65536?3:4;for(e=h.uint8array?new Uint8Array(o):new Array(o),n=s=0;s<o;n++)55296==(64512&(r=t.charCodeAt(n)))&&n+1<a&&56320==(64512&(i=t.charCodeAt(n+1)))&&(r=65536+(r-55296<<10)+(i-56320),n++),r<128?e[s++]=r:(r<2048?e[s++]=192|r>>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),
e[s++]=128|63&r);return e}(t)},s.utf8decode=function(t){return h.nodebuffer?o.transformTo("nodebuffer",t).toString("utf-8"):function(t){var e,r,i,n,s=t.length,a=new Array(2*s);for(e=r=0;e<s;)if((i=t[e++])<128)a[r++]=i;else if(4<(n=u[i]))a[r++]=65533,e+=n-1;else{for(i&=2===n?31:3===n?15:7;1<n&&e<s;)i=i<<6|63&t[e++],n--;1<n?a[r++]=65533:i<65536?a[r++]=i:(i-=65536,a[r++]=55296|i>>10&1023,a[r++]=56320|1023&i)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(t=o.transformTo(h.uint8array?
"uint8array":"array",t))},o.inherits(a,i),a.prototype.processChunk=function(t){var e=o.transformTo(h.uint8array?"uint8array":"array",t.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=e;(e=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),e.set(r,this.leftOver.length)}else e=this.leftOver.concat(e);this.leftOver=null}var i=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:r+u[t[r]]>e?r:e}(e),n=
e;i!==e.length&&(h.uint8array?(n=e.subarray(0,i),this.leftOver=e.subarray(i,e.length)):(n=e.slice(0,i),this.leftOver=e.slice(i,e.length))),this.push({data:s.utf8decode(n),meta:t.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,i),l.prototype.processChunk=function(t){this.push({data:s.utf8encode(t.data),meta:t.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,
"./support":30,"./utils":32}],32:[function(t,e,a){var o=t("./support"),h=t("./base64"),r=t("./nodejsUtils"),i=t("set-immediate-shim"),u=t("./external");function n(t){return t}function l(t,e){for(var r=0;r<t.length;++r)e[r]=255&t.charCodeAt(r);return e}a.newBlob=function(e,r){a.checkSupport("blob");try{return new Blob([e],{type:r})}catch(t$11){try{var i=new (self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);return i.append(e),i.getBlob(r)}catch(t$10){throw new Error("Bug : can't construct the Blob.");
}}};var s={stringifyByChunk:function(t,e,r){var i=[],n=0,s=t.length;if(s<=r)return String.fromCharCode.apply(null,t);for(;n<s;)"array"===e||"nodebuffer"===e?i.push(String.fromCharCode.apply(null,t.slice(n,Math.min(n+r,s)))):i.push(String.fromCharCode.apply(null,t.subarray(n,Math.min(n+r,s)))),n+=r;return i.join("")},stringifyByChar:function(t){for(var e="",r=0;r<t.length;r++)e+=String.fromCharCode(t[r]);return e},applyCanBeUsed:{uint8array:function(){try{return o.uint8array&&1===String.fromCharCode.apply(null,
new Uint8Array(1)).length}catch(t$12){return!1}}(),nodebuffer:function(){try{return o.nodebuffer&&1===String.fromCharCode.apply(null,r.allocBuffer(1)).length}catch(t$13){return!1}}()}};function f(t){var e=65536,r=a.getTypeOf(t),i=!0;if("uint8array"===r?i=s.applyCanBeUsed.uint8array:"nodebuffer"===r&&(i=s.applyCanBeUsed.nodebuffer),i)for(;1<e;)try{return s.stringifyByChunk(t,r,e)}catch(t$14){e=Math.floor(e/2)}return s.stringifyByChar(t)}function d(t,e){for(var r=0;r<t.length;r++)e[r]=t[r];return e}
a.applyFromCharCode=f;var c={};c.string={string:n,array:function(t){return l(t,new Array(t.length))},arraybuffer:function(t){return c.string.uint8array(t).buffer},uint8array:function(t){return l(t,new Uint8Array(t.length))},nodebuffer:function(t){return l(t,r.allocBuffer(t.length))}},c.array={string:f,array:n,arraybuffer:function(t){return(new Uint8Array(t)).buffer},uint8array:function(t){return new Uint8Array(t)},nodebuffer:function(t){return r.newBufferFrom(t)}},c.arraybuffer={string:function(t){return f(new Uint8Array(t))},
array:function(t){return d(new Uint8Array(t),new Array(t.byteLength))},arraybuffer:n,uint8array:function(t){return new Uint8Array(t)},nodebuffer:function(t){return r.newBufferFrom(new Uint8Array(t))}},c.uint8array={string:f,array:function(t){return d(t,new Array(t.length))},arraybuffer:function(t){return t.buffer},uint8array:n,nodebuffer:function(t){return r.newBufferFrom(t)}},c.nodebuffer={string:f,array:function(t){return d(t,new Array(t.length))},arraybuffer:function(t){return c.nodebuffer.uint8array(t).buffer},
uint8array:function(t){return d(t,new Uint8Array(t.length))},nodebuffer:n},a.transformTo=function(t,e){if(e=e||"",!t)return e;a.checkSupport(t);var r=a.getTypeOf(e);return c[r][t](e)},a.getTypeOf=function(t){return"string"==typeof t?"string":"[object Array]"===Object.prototype.toString.call(t)?"array":o.nodebuffer&&r.isBuffer(t)?"nodebuffer":o.uint8array&&t instanceof Uint8Array?"uint8array":o.arraybuffer&&t instanceof ArrayBuffer?"arraybuffer":void 0},a.checkSupport=function(t){if(!o[t.toLowerCase()])throw new Error(t+
" is not supported by this platform");},a.MAX_VALUE_16BITS=65535,a.MAX_VALUE_32BITS=-1,a.pretty=function(t){var e,r,i="";for(r=0;r<(t||"").length;r++)i+="\\x"+((e=t.charCodeAt(r))<16?"0":"")+e.toString(16).toUpperCase();return i},a.delay=function(t,e,r){i(function(){t.apply(r||null,e||[])})},a.inherits=function(t,e){function r(){}r.prototype=e.prototype,t.prototype=new r},a.extend=function(){var t,e,r={};for(t=0;t<arguments.length;t++)for(e in arguments[t])arguments[t].hasOwnProperty(e)&&void 0===
r[e]&&(r[e]=arguments[t][e]);return r},a.prepareContent=function(r,t,i,n,s){return u.Promise.resolve(t).then(function(i){return o.blob&&(i instanceof Blob||-1!==["[object File]","[object Blob]"].indexOf(Object.prototype.toString.call(i)))&&"undefined"!=typeof FileReader?new u.Promise(function(e,r){var t=new FileReader;t.onload=function(t){e(t.target.result)},t.onerror=function(t){r(t.target.error)},t.readAsArrayBuffer(i)}):i}).then(function(t){var e=a.getTypeOf(t);return e?("arraybuffer"===e?t=a.transformTo("uint8array",
t):"string"===e&&(s?t=h.decode(t):i&&!0!==n&&(t=function(t){return l(t,o.uint8array?new Uint8Array(t.length):new Array(t.length))}(t))),t):u.Promise.reject(new Error("Can't read the data of '"+r+"'. Is it in a supported JavaScript type (String, Blob, ArrayBuffer, etc) ?"))})}},{"./base64":1,"./external":6,"./nodejsUtils":14,"./support":30,"set-immediate-shim":54}],33:[function(t,e,r){var i=t("./reader/readerFor"),n=t("./utils"),s=t("./signature"),a=t("./zipEntry"),o=(t("./utf8"),t("./support"));function h(t){this.files=
[],this.loadOptions=t}h.prototype={checkSignature:function(t){if(!this.reader.readAndCheckSignature(t)){this.reader.index-=4;var e=this.reader.readString(4);throw new Error("Corrupted zip or bug: unexpected signature ("+n.pretty(e)+", expected "+n.pretty(t)+")");}},isSignature:function(t,e){var r=this.reader.index;this.reader.setIndex(t);var i=this.reader.readString(4)===e;return this.reader.setIndex(r),i},readBlockEndOfCentral:function(){this.diskNumber=this.reader.readInt(2),this.diskWithCentralDirStart=
this.reader.readInt(2),this.centralDirRecordsOnThisDisk=this.reader.readInt(2),this.centralDirRecords=this.reader.readInt(2),this.centralDirSize=this.reader.readInt(4),this.centralDirOffset=this.reader.readInt(4),this.zipCommentLength=this.reader.readInt(2);var t=this.reader.readData(this.zipCommentLength),e=o.uint8array?"uint8array":"array",r=n.transformTo(e,t);this.zipComment=this.loadOptions.decodeFileName(r)},readBlockZip64EndOfCentral:function(){this.zip64EndOfCentralSize=this.reader.readInt(8),
this.reader.skip(4),this.diskNumber=this.reader.readInt(4),this.diskWithCentralDirStart=this.reader.readInt(4),this.centralDirRecordsOnThisDisk=this.reader.readInt(8),this.centralDirRecords=this.reader.readInt(8),this.centralDirSize=this.reader.readInt(8),this.centralDirOffset=this.reader.readInt(8),this.zip64ExtensibleData={};for(var t,e,r,i=this.zip64EndOfCentralSize-44;0<i;)t=this.reader.readInt(2),e=this.reader.readInt(4),r=this.reader.readData(e),this.zip64ExtensibleData[t]={id:t,length:e,value:r}},
readBlockZip64EndOfCentralLocator:function(){if(this.diskWithZip64CentralDirStart=this.reader.readInt(4),this.relativeOffsetEndOfZip64CentralDir=this.reader.readInt(8),this.disksCount=this.reader.readInt(4),1<this.disksCount)throw new Error("Multi-volumes zip are not supported");},readLocalFiles:function(){var t,e;for(t=0;t<this.files.length;t++)e=this.files[t],this.reader.setIndex(e.localHeaderOffset),this.checkSignature(s.LOCAL_FILE_HEADER),e.readLocalPart(this.reader),e.handleUTF8(),e.processAttributes()},
readCentralDir:function(){var t;for(this.reader.setIndex(this.centralDirOffset);this.reader.readAndCheckSignature(s.CENTRAL_FILE_HEADER);)(t=new a({zip64:this.zip64},this.loadOptions)).readCentralPart(this.reader),this.files.push(t);if(this.centralDirRecords!==this.files.length&&0!==this.centralDirRecords&&0===this.files.length)throw new Error("Corrupted zip or bug: expected "+this.centralDirRecords+" records in central dir, got "+this.files.length);},readEndOfCentral:function(){var t=this.reader.lastIndexOfSignature(s.CENTRAL_DIRECTORY_END);
if(t<0)throw!this.isSignature(0,s.LOCAL_FILE_HEADER)?new Error("Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html"):new Error("Corrupted zip: can't find end of central directory");this.reader.setIndex(t);var e=t;if(this.checkSignature(s.CENTRAL_DIRECTORY_END),this.readBlockEndOfCentral(),this.diskNumber===n.MAX_VALUE_16BITS||this.diskWithCentralDirStart===n.MAX_VALUE_16BITS||this.centralDirRecordsOnThisDisk===n.MAX_VALUE_16BITS||
this.centralDirRecords===n.MAX_VALUE_16BITS||this.centralDirSize===n.MAX_VALUE_32BITS||this.centralDirOffset===n.MAX_VALUE_32BITS){if(this.zip64=!0,(t=this.reader.lastIndexOfSignature(s.ZIP64_CENTRAL_DIRECTORY_LOCATOR))<0)throw new Error("Corrupted zip: can't find the ZIP64 end of central directory locator");if(this.reader.setIndex(t),this.checkSignature(s.ZIP64_CENTRAL_DIRECTORY_LOCATOR),this.readBlockZip64EndOfCentralLocator(),!this.isSignature(this.relativeOffsetEndOfZip64CentralDir,s.ZIP64_CENTRAL_DIRECTORY_END)&&
(this.relativeOffsetEndOfZip64CentralDir=this.reader.lastIndexOfSignature(s.ZIP64_CENTRAL_DIRECTORY_END),this.relativeOffsetEndOfZip64CentralDir<0))throw new Error("Corrupted zip: can't find the ZIP64 end of central directory");this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir),this.checkSignature(s.ZIP64_CENTRAL_DIRECTORY_END),this.readBlockZip64EndOfCentral()}var r=this.centralDirOffset+this.centralDirSize;this.zip64&&(r+=20,r+=12+this.zip64EndOfCentralSize);var i=e-r;if(0<i)this.isSignature(e,
s.CENTRAL_FILE_HEADER)||(this.reader.zero=i);else if(i<0)throw new Error("Corrupted zip: missing "+Math.abs(i)+" bytes.");},prepareReader:function(t){this.reader=i(t)},load:function(t){this.prepareReader(t),this.readEndOfCentral(),this.readCentralDir(),this.readLocalFiles()}},e.exports=h},{"./reader/readerFor":22,"./signature":23,"./support":30,"./utf8":31,"./utils":32,"./zipEntry":34}],34:[function(t,e,r){var i=t("./reader/readerFor"),s=t("./utils"),n=t("./compressedObject"),a=t("./crc32"),o=t("./utf8"),
h=t("./compressions"),u=t("./support");function l(t,e){this.options=t,this.loadOptions=e}l.prototype={isEncrypted:function(){return 1==(1&this.bitFlag)},useUTF8:function(){return 2048==(2048&this.bitFlag)},readLocalPart:function(t){var e,r;if(t.skip(22),this.fileNameLength=t.readInt(2),r=t.readInt(2),this.fileName=t.readData(this.fileNameLength),t.skip(r),-1===this.compressedSize||-1===this.uncompressedSize)throw new Error("Bug or corrupted zip : didn't get enough information from the central directory (compressedSize === -1 || uncompressedSize === -1)");
if(null===(e=function(t){for(var e in h)if(h.hasOwnProperty(e)&&h[e].magic===t)return h[e];return null}(this.compressionMethod)))throw new Error("Corrupted zip : compression "+s.pretty(this.compressionMethod)+" unknown (inner file : "+s.transformTo("string",this.fileName)+")");this.decompressed=new n(this.compressedSize,this.uncompressedSize,this.crc32,e,t.readData(this.compressedSize))},readCentralPart:function(t){this.versionMadeBy=t.readInt(2),t.skip(2),this.bitFlag=t.readInt(2),this.compressionMethod=
t.readString(2),this.date=t.readDate(),this.crc32=t.readInt(4),this.compressedSize=t.readInt(4),this.uncompressedSize=t.readInt(4);var e=t.readInt(2);if(this.extraFieldsLength=t.readInt(2),this.fileCommentLength=t.readInt(2),this.diskNumberStart=t.readInt(2),this.internalFileAttributes=t.readInt(2),this.externalFileAttributes=t.readInt(4),this.localHeaderOffset=t.readInt(4),this.isEncrypted())throw new Error("Encrypted zip are not supported");t.skip(e),this.readExtraFields(t),this.parseZIP64ExtraField(t),
this.fileComment=t.readData(this.fileCommentLength)},processAttributes:function(){this.unixPermissions=null,this.dosPermissions=null;var t=this.versionMadeBy>>8;this.dir=!!(16&this.externalFileAttributes),0==t&&(this.dosPermissions=63&this.externalFileAttributes),3==t&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(t){if(this.extraFields[1]){var e=i(this.extraFields[1].value);this.uncompressedSize===
s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(t){var e,r,i,n=t.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});t.index+4<n;)e=t.readInt(2),r=t.readInt(2),i=t.readData(r),this.extraFields[e]={id:e,length:r,
value:i};t.setIndex(n)},handleUTF8:function(){var t=u.uint8array?"uint8array":"array";if(this.useUTF8())this.fileNameStr=o.utf8decode(this.fileName),this.fileCommentStr=o.utf8decode(this.fileComment);else{var e=this.findExtraFieldUnicodePath();if(null!==e)this.fileNameStr=e;else{var r=s.transformTo(t,this.fileName);this.fileNameStr=this.loadOptions.decodeFileName(r)}var i=this.findExtraFieldUnicodeComment();if(null!==i)this.fileCommentStr=i;else{var n=s.transformTo(t,this.fileComment);this.fileCommentStr=
this.loadOptions.decodeFileName(n)}}},findExtraFieldUnicodePath:function(){var t=this.extraFields[28789];if(t){var e=i(t.value);return 1!==e.readInt(1)?null:a(this.fileName)!==e.readInt(4)?null:o.utf8decode(e.readData(t.length-5))}return null},findExtraFieldUnicodeComment:function(){var t=this.extraFields[25461];if(t){var e=i(t.value);return 1!==e.readInt(1)?null:a(this.fileComment)!==e.readInt(4)?null:o.utf8decode(e.readData(t.length-5))}return null}},e.exports=l},{"./compressedObject":2,"./compressions":3,
"./crc32":4,"./reader/readerFor":22,"./support":30,"./utf8":31,"./utils":32}],35:[function(t,e,r){function i(t,e,r){this.name=t,this.dir=r.dir,this.date=r.date,this.comment=r.comment,this.unixPermissions=r.unixPermissions,this.dosPermissions=r.dosPermissions,this._data=e,this._dataBinary=r.binary,this.options={compression:r.compression,compressionOptions:r.compressionOptions}}var s=t("./stream/StreamHelper"),n=t("./stream/DataWorker"),a=t("./utf8"),o=t("./compressedObject"),h=t("./stream/GenericWorker");
i.prototype={internalStream:function(t){var e=null,r="string";try{if(!t)throw new Error("No output type specified.");var i="string"===(r=t.toLowerCase())||"text"===r;"binarystring"!==r&&"text"!==r||(r="string"),e=this._decompressWorker();var n=!this._dataBinary;n&&!i&&(e=e.pipe(new a.Utf8EncodeWorker)),!n&&i&&(e=e.pipe(new a.Utf8DecodeWorker))}catch(t$15){(e=new h("error")).error(t$15)}return new s(e,r,"")},async:function(t,e){return this.internalStream(t).accumulate(e)},nodeStream:function(t,e){return this.internalStream(t||
"nodebuffer").toNodejsStream(e)},_compressWorker:function(t,e){if(this._data instanceof o&&this._data.compression.magic===t.magic)return this._data.getCompressedWorker();var r=this._decompressWorker();return this._dataBinary||(r=r.pipe(new a.Utf8EncodeWorker)),o.createWorkerFrom(r,t,e)},_decompressWorker:function(){return this._data instanceof o?this._data.getContentWorker():this._data instanceof h?this._data:new n(this._data)}};for(var u=["asText","asBinary","asNodeBuffer","asUint8Array","asArrayBuffer"],
l=function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.");},f=0;f<u.length;f++)i.prototype[u[f]]=l;e.exports=i},{"./compressedObject":2,"./stream/DataWorker":27,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31}],36:[function(t,l,e){(function(e){var r,i,t=e.MutationObserver||e.WebKitMutationObserver;if(t){var n=0,s=new t(u),a=e.document.createTextNode("");s.observe(a,{characterData:!0}),r=function(){a.data=n=++n%2}}else if(e.setImmediate||
void 0===e.MessageChannel)r="document"in e&&"onreadystatechange"in e.document.createElement("script")?function(){var t=e.document.createElement("script");t.onreadystatechange=function(){u(),t.onreadystatechange=null,t.parentNode.removeChild(t),t=null},e.document.documentElement.appendChild(t)}:function(){setTimeout(u,0)};else{var o=new e.MessageChannel;o.port1.onmessage=u,r=function(){o.port2.postMessage(0)}}var h=[];function u(){var t,e;i=!0;for(var r=h.length;r;){for(e=h,h=[],t=-1;++t<r;)e[t]();
r=h.length}i=!1}l.exports=function(t){1!==h.push(t)||i||r()}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],37:[function(t,e,r){var n=t("immediate");function u(){}var l={},s=["REJECTED"],a=["FULFILLED"],i=["PENDING"];function o(t){if("function"!=typeof t)throw new TypeError("resolver must be a function");this.state=i,this.queue=[],this.outcome=void 0,t!==u&&c(this,t)}function h(t,e,r){this.promise=t,"function"==typeof e&&(this.onFulfilled=
e,this.callFulfilled=this.otherCallFulfilled),"function"==typeof r&&(this.onRejected=r,this.callRejected=this.otherCallRejected)}function f(e,r,i){n(function(){var t;try{t=r(i)}catch(t$16){return l.reject(e,t$16)}t===e?l.reject(e,new TypeError("Cannot resolve promise with itself")):l.resolve(e,t)})}function d(t){var e=t&&t.then;if(t&&("object"==typeof t||"function"==typeof t)&&"function"==typeof e)return function(){e.apply(t,arguments)}}function c(e,t){var r=!1;function i(t){r||(r=!0,l.reject(e,t))}
function n(t){r||(r=!0,l.resolve(e,t))}var s=p(function(){t(n,i)});"error"===s.status&&i(s.value)}function p(t,e){var r={};try{r.value=t(e),r.status="success"}catch(t$17){r.status="error",r.value=t$17}return r}(e.exports=o).prototype["finally"]=function(e){if("function"!=typeof e)return this;var r=this.constructor;return this.then(function(t){return r.resolve(e()).then(function(){return t})},function(t){return r.resolve(e()).then(function(){throw t;})})},o.prototype["catch"]=function(t){return this.then(null,
t)},o.prototype.then=function(t,e){if("function"!=typeof t&&this.state===a||"function"!=typeof e&&this.state===s)return this;var r=new this.constructor(u);this.state!==i?f(r,this.state===a?t:e,this.outcome):this.queue.push(new h(r,t,e));return r},h.prototype.callFulfilled=function(t){l.resolve(this.promise,t)},h.prototype.otherCallFulfilled=function(t){f(this.promise,this.onFulfilled,t)},h.prototype.callRejected=function(t){l.reject(this.promise,t)},h.prototype.otherCallRejected=function(t){f(this.promise,
this.onRejected,t)},l.resolve=function(t,e){var r=p(d,e);if("error"===r.status)return l.reject(t,r.value);var i=r.value;if(i)c(t,i);else{t.state=a,t.outcome=e;for(var n=-1,s=t.queue.length;++n<s;)t.queue[n].callFulfilled(e)}return t},l.reject=function(t,e){t.state=s,t.outcome=e;for(var r=-1,i=t.queue.length;++r<i;)t.queue[r].callRejected(e);return t},o.resolve=function(t){if(t instanceof this)return t;return l.resolve(new this(u),t)},o.reject=function(t){var e=new this(u);return l.reject(e,t)},o.all=
function(t){var r=this;if("[object Array]"!==Object.prototype.toString.call(t))return this.reject(new TypeError("must be an array"));var i=t.length,n=!1;if(!i)return this.resolve([]);var s=new Array(i),a=0,e=-1,o=new this(u);for(;++e<i;)h(t[e],e);return o;function h(t,e){r.resolve(t).then(function(t){s[e]=t,++a!==i||n||(n=!0,l.resolve(o,s))},function(t){n||(n=!0,l.reject(o,t))})}},o.race=function(t){var e=this;if("[object Array]"!==Object.prototype.toString.call(t))return this.reject(new TypeError("must be an array"));
var r=t.length,i=!1;if(!r)return this.resolve([]);var n=-1,s=new this(u);for(;++n<r;)a=t[n],e.resolve(a).then(function(t){i||(i=!0,l.resolve(s,t))},function(t){i||(i=!0,l.reject(s,t))});var a;return s}},{immediate:36}],38:[function(t,e,r){var i={};(0,t("./lib/utils/common").assign)(i,t("./lib/deflate"),t("./lib/inflate"),t("./lib/zlib/constants")),e.exports=i},{"./lib/deflate":39,"./lib/inflate":40,"./lib/utils/common":41,"./lib/zlib/constants":44}],39:[function(t,e,r){var a=t("./zlib/deflate"),o=
t("./utils/common"),h=t("./utils/strings"),n=t("./zlib/messages"),s=t("./zlib/zstream"),u=Object.prototype.toString,l=0,f=-1,d=0,c=8;function p(t){if(!(this instanceof p))return new p(t);this.options=o.assign({level:f,method:c,chunkSize:16384,windowBits:15,memLevel:8,strategy:d,to:""},t||{});var e=this.options;e.raw&&0<e.windowBits?e.windowBits=-e.windowBits:e.gzip&&0<e.windowBits&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new s,this.strm.avail_out=
0;var r=a.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(r!==l)throw new Error(n[r]);if(e.header&&a.deflateSetHeader(this.strm,e.header),e.dictionary){var i;if(i="string"==typeof e.dictionary?h.string2buf(e.dictionary):"[object ArrayBuffer]"===u.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,(r=a.deflateSetDictionary(this.strm,i))!==l)throw new Error(n[r]);this._dict_set=!0}}function i(t,e){var r=new p(e);if(r.push(t,!0),r.err)throw r.msg||n[r.err];return r.result}
p.prototype.push=function(t,e){var r,i,n=this.strm,s=this.options.chunkSize;if(this.ended)return!1;i=e===~~e?e:!0===e?4:0,"string"==typeof t?n.input=h.string2buf(t):"[object ArrayBuffer]"===u.call(t)?n.input=new Uint8Array(t):n.input=t,n.next_in=0,n.avail_in=n.input.length;do{if(0===n.avail_out&&(n.output=new o.Buf8(s),n.next_out=0,n.avail_out=s),1!==(r=a.deflate(n,i))&&r!==l)return this.onEnd(r),!(this.ended=!0);0!==n.avail_out&&(0!==n.avail_in||4!==i&&2!==i)||("string"===this.options.to?this.onData(h.buf2binstring(o.shrinkBuf(n.output,
n.next_out))):this.onData(o.shrinkBuf(n.output,n.next_out)))}while((0<n.avail_in||0===n.avail_out)&&1!==r);return 4===i?(r=a.deflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===l):2!==i||(this.onEnd(l),!(n.avail_out=0))},p.prototype.onData=function(t){this.chunks.push(t)},p.prototype.onEnd=function(t){t===l&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=o.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},r.Deflate=p,r.deflate=i,r.deflateRaw=
function(t,e){return(e=e||{}).raw=!0,i(t,e)},r.gzip=function(t,e){return(e=e||{}).gzip=!0,i(t,e)}},{"./utils/common":41,"./utils/strings":42,"./zlib/deflate":46,"./zlib/messages":51,"./zlib/zstream":53}],40:[function(t,e,r){var d=t("./zlib/inflate"),c=t("./utils/common"),p=t("./utils/strings"),m=t("./zlib/constants"),i=t("./zlib/messages"),n=t("./zlib/zstream"),s=t("./zlib/gzheader"),_=Object.prototype.toString;function a(t){if(!(this instanceof a))return new a(t);this.options=c.assign({chunkSize:16384,
windowBits:0,to:""},t||{});var e=this.options;e.raw&&0<=e.windowBits&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(0<=e.windowBits&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),15<e.windowBits&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new n,this.strm.avail_out=0;var r=d.inflateInit2(this.strm,e.windowBits);if(r!==m.Z_OK)throw new Error(i[r]);this.header=new s,d.inflateGetHeader(this.strm,
this.header)}function o(t,e){var r=new a(e);if(r.push(t,!0),r.err)throw r.msg||i[r.err];return r.result}a.prototype.push=function(t,e){var r,i,n,s,a,o,h=this.strm,u=this.options.chunkSize,l=this.options.dictionary,f=!1;if(this.ended)return!1;i=e===~~e?e:!0===e?m.Z_FINISH:m.Z_NO_FLUSH,"string"==typeof t?h.input=p.binstring2buf(t):"[object ArrayBuffer]"===_.call(t)?h.input=new Uint8Array(t):h.input=t,h.next_in=0,h.avail_in=h.input.length;do{if(0===h.avail_out&&(h.output=new c.Buf8(u),h.next_out=0,h.avail_out=
u),(r=d.inflate(h,m.Z_NO_FLUSH))===m.Z_NEED_DICT&&l&&(o="string"==typeof l?p.string2buf(l):"[object ArrayBuffer]"===_.call(l)?new Uint8Array(l):l,r=d.inflateSetDictionary(this.strm,o)),r===m.Z_BUF_ERROR&&!0===f&&(r=m.Z_OK,f=!1),r!==m.Z_STREAM_END&&r!==m.Z_OK)return this.onEnd(r),!(this.ended=!0);h.next_out&&(0!==h.avail_out&&r!==m.Z_STREAM_END&&(0!==h.avail_in||i!==m.Z_FINISH&&i!==m.Z_SYNC_FLUSH)||("string"===this.options.to?(n=p.utf8border(h.output,h.next_out),s=h.next_out-n,a=p.buf2string(h.output,
n),h.next_out=s,h.avail_out=u-s,s&&c.arraySet(h.output,h.output,n,s,0),this.onData(a)):this.onData(c.shrinkBuf(h.output,h.next_out)))),0===h.avail_in&&0===h.avail_out&&(f=!0)}while((0<h.avail_in||0===h.avail_out)&&r!==m.Z_STREAM_END);return r===m.Z_STREAM_END&&(i=m.Z_FINISH),i===m.Z_FINISH?(r=d.inflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===m.Z_OK):i!==m.Z_SYNC_FLUSH||(this.onEnd(m.Z_OK),!(h.avail_out=0))},a.prototype.onData=function(t){this.chunks.push(t)},a.prototype.onEnd=function(t){t===
m.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=c.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},r.Inflate=a,r.inflate=o,r.inflateRaw=function(t,e){return(e=e||{}).raw=!0,o(t,e)},r.ungzip=o},{"./utils/common":41,"./utils/strings":42,"./zlib/constants":44,"./zlib/gzheader":47,"./zlib/inflate":49,"./zlib/messages":51,"./zlib/zstream":53}],41:[function(t,e,r){var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=
typeof Int32Array;r.assign=function(t){for(var e=Array.prototype.slice.call(arguments,1);e.length;){var r=e.shift();if(r){if("object"!=typeof r)throw new TypeError(r+"must be non-object");for(var i in r)r.hasOwnProperty(i)&&(t[i]=r[i])}}return t},r.shrinkBuf=function(t,e){return t.length===e?t:t.subarray?t.subarray(0,e):(t.length=e,t)};var n={arraySet:function(t,e,r,i,n){if(e.subarray&&t.subarray)t.set(e.subarray(r,r+i),n);else for(var s=0;s<i;s++)t[n+s]=e[r+s]},flattenChunks:function(t){var e,r,
i,n,s,a;for(e=i=0,r=t.length;e<r;e++)i+=t[e].length;for(a=new Uint8Array(i),e=n=0,r=t.length;e<r;e++)s=t[e],a.set(s,n),n+=s.length;return a}},s={arraySet:function(t,e,r,i,n){for(var s=0;s<i;s++)t[n+s]=e[r+s]},flattenChunks:function(t){return[].concat.apply([],t)}};r.setTyped=function(t){t?(r.Buf8=Uint8Array,r.Buf16=Uint16Array,r.Buf32=Int32Array,r.assign(r,n)):(r.Buf8=Array,r.Buf16=Array,r.Buf32=Array,r.assign(r,s))},r.setTyped(i)},{}],42:[function(t,e,r){var h=t("./common"),n=!0,s=!0;try{String.fromCharCode.apply(null,
[0])}catch(t$18){n=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(t$19){s=!1}for(var u=new h.Buf8(256),i=0;i<256;i++)u[i]=252<=i?6:248<=i?5:240<=i?4:224<=i?3:192<=i?2:1;function l(t,e){if(e<65537&&(t.subarray&&s||!t.subarray&&n))return String.fromCharCode.apply(null,h.shrinkBuf(t,e));for(var r="",i=0;i<e;i++)r+=String.fromCharCode(t[i]);return r}u[254]=u[254]=1,r.string2buf=function(t){var e,r,i,n,s,a=t.length,o=0;for(n=0;n<a;n++)55296==(64512&(r=t.charCodeAt(n)))&&n+1<a&&56320==(64512&
(i=t.charCodeAt(n+1)))&&(r=65536+(r-55296<<10)+(i-56320),n++),o+=r<128?1:r<2048?2:r<65536?3:4;for(e=new h.Buf8(o),n=s=0;s<o;n++)55296==(64512&(r=t.charCodeAt(n)))&&n+1<a&&56320==(64512&(i=t.charCodeAt(n+1)))&&(r=65536+(r-55296<<10)+(i-56320),n++),r<128?e[s++]=r:(r<2048?e[s++]=192|r>>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e},r.buf2binstring=function(t){return l(t,t.length)},r.binstring2buf=function(t){for(var e=new h.Buf8(t.length),
r=0,i=e.length;r<i;r++)e[r]=t.charCodeAt(r);return e},r.buf2string=function(t,e){var r,i,n,s,a=e||t.length,o=new Array(2*a);for(r=i=0;r<a;)if((n=t[r++])<128)o[i++]=n;else if(4<(s=u[n]))o[i++]=65533,r+=s-1;else{for(n&=2===s?31:3===s?15:7;1<s&&r<a;)n=n<<6|63&t[r++],s--;1<s?o[i++]=65533:n<65536?o[i++]=n:(n-=65536,o[i++]=55296|n>>10&1023,o[i++]=56320|1023&n)}return l(o,i)},r.utf8border=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:
r+u[t[r]]>e?r:e}},{"./common":41}],43:[function(t,e,r){e.exports=function(t,e,r,i){for(var n=65535&t|0,s=t>>>16&65535|0,a=0;0!==r;){for(r-=a=2E3<r?2E3:r;s=s+(n=n+e[i++]|0)|0,--a;);n%=65521,s%=65521}return n|s<<16|0}},{}],44:[function(t,e,r){e.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,
Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},{}],45:[function(t,e,r){var o=function(){for(var t,e=[],r=0;r<256;r++){t=r;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a<s;a++)t=t>>>8^n[255&(t^e[a])];return-1^t}},{}],46:[function(t,e,r){var h,d=t("../utils/common"),u=t("./trees"),c=t("./adler32"),p=t("./crc32"),i=t("./messages"),l=0,f=4,m=0,_=-2,g=
-1,b=4,n=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(t,e){return t.msg=i[e],e}function T(t){return(t<<1)-(4<t?9:0)}function D(t){for(var e=t.length;0<=--e;)t[e]=0}function F(t){var e=t.state,r=e.pending;r>t.avail_out&&(r=t.avail_out),0!==r&&(d.arraySet(t.output,e.pending_buf,e.pending_out,r,t.next_out),t.next_out+=r,e.pending_out+=r,t.total_out+=r,t.avail_out-=r,e.pending-=r,0===e.pending&&(e.pending_out=0))}function N(t,e){u._tr_flush_block(t,0<=
t.block_start?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,F(t.strm)}function U(t,e){t.pending_buf[t.pending++]=e}function P(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function L(t,e){var r,i,n=t.max_chain_length,s=t.strstart,a=t.prev_length,o=t.nice_match,h=t.strstart>t.w_size-z?t.strstart-(t.w_size-z):0,u=t.window,l=t.w_mask,f=t.prev,d=t.strstart+S,c=u[s+a-1],p=u[s+a];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do if(u[(r=
e)+a]===p&&u[r+a-1]===c&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do;while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&s<d);if(i=S-(d-s),s=d-S,a<i){if(t.match_start=e,o<=(a=i))break;c=u[s+a-1],p=u[s+a]}}while((e=f[e&l])>h&&0!=--n);return a<=t.lookahead?a:t.lookahead}function j(t){var e,r,i,n,s,a,o,h,u,l,f=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=f+(f-z)){for(d.arraySet(t.window,t.window,
f,f,0),t.match_start-=f,t.strstart-=f,t.block_start-=f,e=r=t.hash_size;i=t.head[--e],t.head[e]=f<=i?i-f:0,--r;);for(e=r=f;i=t.prev[--e],t.prev[e]=f<=i?i-f:0,--r;);n+=f}if(0===t.strm.avail_in)break;if(a=t.strm,o=t.window,h=t.strstart+t.lookahead,u=n,l=void 0,l=a.avail_in,u<l&&(l=u),r=0===l?0:(a.avail_in-=l,d.arraySet(o,a.input,a.next_in,l,h),1===a.state.wrap?a.adler=c(a.adler,o,l,h):2===a.state.wrap&&(a.adler=p(a.adler,o,l,h)),a.next_in+=l,a.total_in+=l,l),t.lookahead+=r,t.lookahead+t.insert>=x)for(s=
t.strstart-t.insert,t.ins_h=t.window[s],t.ins_h=(t.ins_h<<t.hash_shift^t.window[s+1])&t.hash_mask;t.insert&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[s+x-1])&t.hash_mask,t.prev[s&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=s,s++,t.insert--,!(t.lookahead+t.insert<x)););}while(t.lookahead<z&&0!==t.strm.avail_in)}function Z(t,e){for(var r,i;;){if(t.lookahead<z){if(j(t),t.lookahead<z&&e===l)return A;if(0===t.lookahead)break}if(r=0,t.lookahead>=x&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+x-1])&
t.hash_mask,r=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==r&&t.strstart-r<=t.w_size-z&&(t.match_length=L(t,r)),t.match_length>=x)if(i=u._tr_tally(t,t.strstart-t.match_start,t.match_length-x),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=x){for(t.match_length--;t.strstart++,t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+x-1])&t.hash_mask,r=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart,0!=--t.match_length;);t.strstart++}else t.strstart+=
t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+1])&t.hash_mask;else i=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=t.strstart<x-1?t.strstart:x-1,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}function W(t,e){for(var r,i,n;;){if(t.lookahead<z){if(j(t),t.lookahead<z&&e===l)return A;if(0===t.lookahead)break}if(r=0,
t.lookahead>=x&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+x-1])&t.hash_mask,r=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=x-1,0!==r&&t.prev_length<t.max_lazy_match&&t.strstart-r<=t.w_size-z&&(t.match_length=L(t,r),t.match_length<=5&&(1===t.strategy||t.match_length===x&&4096<t.strstart-t.match_start)&&(t.match_length=x-1)),t.prev_length>=x&&t.match_length<=t.prev_length){for(n=t.strstart+t.lookahead-
x,i=u._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-x),t.lookahead-=t.prev_length-1,t.prev_length-=2;++t.strstart<=n&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+x-1])&t.hash_mask,r=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!=--t.prev_length;);if(t.match_available=0,t.match_length=x-1,t.strstart++,i&&(N(t,!1),0===t.strm.avail_out))return A}else if(t.match_available){if((i=u._tr_tally(t,0,t.window[t.strstart-1]))&&N(t,!1),t.strstart++,t.lookahead--,0===
t.strm.avail_out)return A}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(i=u._tr_tally(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<x-1?t.strstart:x-1,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}function M(t,e,r,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=r,this.max_chain=i,this.func=n}function H(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=
0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=v,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=
0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new d.Buf16(2*w),this.dyn_dtree=new d.Buf16(2*(2*a+1)),this.bl_tree=new d.Buf16(2*(2*o+1)),D(this.dyn_ltree),D(this.dyn_dtree),D(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new d.Buf16(k+1),this.heap=new d.Buf16(2*s+1),D(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new d.Buf16(2*s+1),D(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=
0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function G(t){var e;return t&&t.state?(t.total_in=t.total_out=0,t.data_type=n,(e=t.state).pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=e.wrap?C:E,t.adler=2===e.wrap?0:1,e.last_flush=l,u._tr_init(e),m):R(t,_)}function K(t){var e=G(t);return e===m&&function(t){t.window_size=2*t.w_size,D(t.head),t.max_lazy_match=h[t.level].max_lazy,t.good_match=h[t.level].good_length,t.nice_match=h[t.level].nice_length,t.max_chain_length=h[t.level].max_chain,
t.strstart=0,t.block_start=0,t.lookahead=0,t.insert=0,t.match_length=t.prev_length=x-1,t.match_available=0,t.ins_h=0}(t.state),e}function Y(t,e,r,i,n,s){if(!t)return _;var a=1;if(e===g&&(e=6),i<0?(a=0,i=-i):15<i&&(a=2,i-=16),n<1||y<n||r!==v||i<8||15<i||e<0||9<e||s<0||b<s)return R(t,_);8===i&&(i=9);var o=new H;return(t.state=o).strm=t,o.wrap=a,o.gzhead=null,o.w_bits=i,o.w_size=1<<o.w_bits,o.w_mask=o.w_size-1,o.hash_bits=n+7,o.hash_size=1<<o.hash_bits,o.hash_mask=o.hash_size-1,o.hash_shift=~~((o.hash_bits+
x-1)/x),o.window=new d.Buf8(2*o.w_size),o.head=new d.Buf16(o.hash_size),o.prev=new d.Buf16(o.w_size),o.lit_bufsize=1<<n+6,o.pending_buf_size=4*o.lit_bufsize,o.pending_buf=new d.Buf8(o.pending_buf_size),o.d_buf=1*o.lit_bufsize,o.l_buf=3*o.lit_bufsize,o.level=e,o.strategy=s,o.method=r,K(t)}h=[new M(0,0,0,0,function(t,e){var r=65535;for(r>t.pending_buf_size-5&&(r=t.pending_buf_size-5);;){if(t.lookahead<=1){if(j(t),0===t.lookahead&&e===l)return A;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=
0;var i=t.block_start+r;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,N(t,!1),0===t.strm.avail_out))return A;if(t.strstart-t.block_start>=t.w_size-z&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):(t.strstart>t.block_start&&(N(t,!1),t.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,
258,258,4096,W)],r.deflateInit=function(t,e){return Y(t,e,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?_:(t.state.gzhead=e,m):_},r.deflate=function(t,e){var r,i,n,s;if(!t||!t.state||5<e||e<0)return t?R(t,_):_;if(i=t.state,!t.output||!t.input&&0!==t.avail_in||666===i.status&&e!==f)return R(t,0===t.avail_out?-5:_);if(i.strm=t,r=i.last_flush,i.last_flush=e,i.status===C)if(2===i.wrap)t.adler=0,U(i,31),U(i,139),U(i,
8),i.gzhead?(U(i,(i.gzhead.text?1:0)+(i.gzhead.hcrc?2:0)+(i.gzhead.extra?4:0)+(i.gzhead.name?8:0)+(i.gzhead.comment?16:0)),U(i,255&i.gzhead.time),U(i,i.gzhead.time>>8&255),U(i,i.gzhead.time>>16&255),U(i,i.gzhead.time>>24&255),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(U(i,255&i.gzhead.extra.length),U(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(t.adler=p(t.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69):(U(i,0),U(i,0),
U(i,0),U(i,0),U(i,0),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,3),i.status=E);else{var a=v+(i.w_bits-8<<4)<<8;a|=(2<=i.strategy||i.level<2?0:i.level<6?1:6===i.level?2:3)<<6,0!==i.strstart&&(a|=32),a+=31-a%31,i.status=E,P(i,a),0!==i.strstart&&(P(i,t.adler>>>16),P(i,65535&t.adler)),t.adler=1}if(69===i.status)if(i.gzhead.extra){for(n=i.pending;i.gzindex<(65535&i.gzhead.extra.length)&&(i.pending!==i.pending_buf_size||(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,
n)),F(t),n=i.pending,i.pending!==i.pending_buf_size));)U(i,255&i.gzhead.extra[i.gzindex]),i.gzindex++;i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),i.gzindex===i.gzhead.extra.length&&(i.gzindex=0,i.status=73)}else i.status=73;if(73===i.status)if(i.gzhead.name){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindex<i.gzhead.name.length?
255&i.gzhead.name.charCodeAt(i.gzindex++):0,U(i,s)}while(0!==s);i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.gzindex=0,i.status=91)}else i.status=91;if(91===i.status)if(i.gzhead.comment){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindex<i.gzhead.comment.length?255&i.gzhead.comment.charCodeAt(i.gzindex++):0,U(i,
s)}while(0!==s);i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.status=103)}else i.status=103;if(103===i.status&&(i.gzhead.hcrc?(i.pending+2>i.pending_buf_size&&F(t),i.pending+2<=i.pending_buf_size&&(U(i,255&t.adler),U(i,t.adler>>8&255),t.adler=0,i.status=E)):i.status=E),0!==i.pending){if(F(t),0===t.avail_out)return i.last_flush=-1,m}else if(0===t.avail_in&&T(e)<=T(r)&&e!==f)return R(t,-5);if(666===i.status&&0!==t.avail_in)return R(t,-5);if(0!==t.avail_in||0!==
i.lookahead||e!==l&&666!==i.status){var o=2===i.strategy?function(t,e){for(var r;;){if(0===t.lookahead&&(j(t),0===t.lookahead)){if(e===l)return A;break}if(t.match_length=0,r=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):3===i.strategy?function(t,e){for(var r,i,n,s,a=t.window;;){if(t.lookahead<=S){if(j(t),t.lookahead<=S&&e===l)return A;
if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=x&&0<t.strstart&&(i=a[n=t.strstart-1])===a[++n]&&i===a[++n]&&i===a[++n]){s=t.strstart+S;do;while(i===a[++n]&&i===a[++n]&&i===a[++n]&&i===a[++n]&&i===a[++n]&&i===a[++n]&&i===a[++n]&&i===a[++n]&&n<s);t.match_length=S-(s-n),t.match_length>t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=x?(r=u._tr_tally(t,1,t.match_length-x),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(r=u._tr_tally(t,0,t.window[t.strstart]),
t.lookahead--,t.strstart++),r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):h[i.level].func(i,e);if(o!==O&&o!==B||(i.status=666),o===A||o===O)return 0===t.avail_out&&(i.last_flush=-1),m;if(o===I&&(1===e?u._tr_align(i):5!==e&&(u._tr_stored_block(i,0,0,!1),3===e&&(D(i.head),0===i.lookahead&&(i.strstart=0,i.block_start=0,i.insert=0))),F(t),0===t.avail_out))return i.last_flush=-1,m}return e!==f?
m:i.wrap<=0?1:(2===i.wrap?(U(i,255&t.adler),U(i,t.adler>>8&255),U(i,t.adler>>16&255),U(i,t.adler>>24&255),U(i,255&t.total_in),U(i,t.total_in>>8&255),U(i,t.total_in>>16&255),U(i,t.total_in>>24&255)):(P(i,t.adler>>>16),P(i,65535&t.adler)),F(t),0<i.wrap&&(i.wrap=-i.wrap),0!==i.pending?m:1)},r.deflateEnd=function(t){var e;return t&&t.state?(e=t.state.status)!==C&&69!==e&&73!==e&&91!==e&&103!==e&&e!==E&&666!==e?R(t,_):(t.state=null,e===E?R(t,-3):m):_},r.deflateSetDictionary=function(t,e){var r,i,n,s,a,
o,h,u,l=e.length;if(!t||!t.state)return _;if(2===(s=(r=t.state).wrap)||1===s&&r.status!==C||r.lookahead)return _;for(1===s&&(t.adler=c(t.adler,e,l,0)),r.wrap=0,l>=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new d.Buf8(r.w_size),d.arraySet(u,e,l-r.w_size,r.w_size,0),e=u,l=r.w_size),a=t.avail_in,o=t.next_in,h=t.input,t.avail_in=l,t.next_in=0,t.input=e,j(r);r.lookahead>=x;){for(i=r.strstart,n=r.lookahead-(x-1);r.ins_h=(r.ins_h<<r.hash_shift^r.window[i+x-1])&r.hash_mask,r.prev[i&
r.w_mask]=r.head[r.ins_h],r.head[r.ins_h]=i,i++,--n;);r.strstart=i,r.lookahead=x-1,j(r)}return r.strstart+=r.lookahead,r.block_start=r.strstart,r.insert=r.lookahead,r.lookahead=0,r.match_length=r.prev_length=x-1,r.match_available=0,t.next_in=o,t.input=h,t.avail_in=a,r.wrap=s,m},r.deflateInfo="pako deflate (from Nodeca project)"},{"../utils/common":41,"./adler32":43,"./crc32":45,"./messages":51,"./trees":52}],47:[function(t,e,r){e.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,
this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},{}],48:[function(t,e,r){e.exports=function(t,e){var r,i,n,s,a,o,h,u,l,f,d,c,p,m,_,g,b,v,y,w,k,x,S,z,C;r=t.state,i=t.next_in,z=t.input,n=i+(t.avail_in-5),s=t.next_out,C=t.output,a=s-(e-t.avail_out),o=s+(t.avail_out-257),h=r.dmax,u=r.wsize,l=r.whave,f=r.wnext,d=r.window,c=r.hold,p=r.bits,m=r.lencode,_=r.distcode,g=(1<<r.lenbits)-1,b=(1<<r.distbits)-1;t:do{p<15&&(c+=z[i++]<<p,p+=8,c+=z[i++]<<p,p+=8),v=m[c&g];e:for(;;){if(c>>>=
y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(c&(1<<y)-1)];continue e}if(32&y){r.mode=12;break t}t.msg="invalid literal/length code",r.mode=30;break t}w=65535&v,(y&=15)&&(p<y&&(c+=z[i++]<<p,p+=8),w+=c&(1<<y)-1,c>>>=y,p-=y),p<15&&(c+=z[i++]<<p,p+=8,c+=z[i++]<<p,p+=8),v=_[c&b];r:for(;;){if(c>>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(c&(1<<y)-1)];continue r}t.msg="invalid distance code",r.mode=30;break t}if(k=65535&v,p<(y&=15)&&
(c+=z[i++]<<p,(p+=8)<y&&(c+=z[i++]<<p,p+=8)),h<(k+=c&(1<<y)-1)){t.msg="invalid distance too far back",r.mode=30;break t}if(c>>>=y,p-=y,(y=s-a)<k){if(l<(y=k-y)&&r.sane){t.msg="invalid distance too far back",r.mode=30;break t}if(S=d,(x=0)===f){if(x+=u-y,y<w){for(w-=y;C[s++]=d[x++],--y;);x=s-k,S=C}}else if(f<y){if(x+=u+f-y,(y-=f)<w){for(w-=y;C[s++]=d[x++],--y;);if(x=0,f<w){for(w-=y=f;C[s++]=d[x++],--y;);x=s-k,S=C}}}else if(x+=f-y,y<w){for(w-=y;C[s++]=d[x++],--y;);x=s-k,S=C}for(;2<w;)C[s++]=S[x++],C[s++]=
S[x++],C[s++]=S[x++],w-=3;w&&(C[s++]=S[x++],1<w&&(C[s++]=S[x++]))}else{for(x=s-k;C[s++]=C[x++],C[s++]=C[x++],C[s++]=C[x++],2<(w-=3););w&&(C[s++]=C[x++],1<w&&(C[s++]=C[x++]))}break}}break}}while(i<n&&s<o);i-=w=p>>3,c&=(1<<(p-=w<<3))-1,t.next_in=i,t.next_out=s,t.avail_in=i<n?n-i+5:5-(i-n),t.avail_out=s<o?o-s+257:257-(s-o),r.hold=c,r.bits=p}},{}],49:[function(t,e,r){var I=t("../utils/common"),O=t("./adler32"),B=t("./crc32"),R=t("./inffast"),T=t("./inftrees"),D=1,F=2,N=0,U=-2,P=1,i=852,n=592;function L(t){return(t>>>
24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),
this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=P,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new I.Buf32(i),e.distcode=e.distdyn=new I.Buf32(n),e.sane=1,e.back=-1,N):U}function o(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,a(t)):U}function h(t,e){var r,i;return t&&t.state?(i=t.state,e<0?(r=
0,e=-e):(r=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15<e)?U:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=r,i.wbits=e,o(t))):U}function u(t,e){var r,i;return t?(i=new s,(t.state=i).window=null,(r=h(t,e))!==N&&(t.state=null),r):U}var l,f,d=!0;function j(t){if(d){var e;for(l=new I.Buf32(512),f=new I.Buf32(32),e=0;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(T(D,t.lens,0,288,l,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;T(F,t.lens,0,32,f,0,t.work,
{bits:5}),d=!1}t.lencode=l,t.lenbits=9,t.distcode=f,t.distbits=5}function Z(t,e,r,i){var n,s=t.state;return null===s.window&&(s.wsize=1<<s.wbits,s.wnext=0,s.whave=0,s.window=new I.Buf8(s.wsize)),i>=s.wsize?(I.arraySet(s.window,e,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(i<(n=s.wsize-s.wnext)&&(n=i),I.arraySet(s.window,e,r-i,n,s.wnext),(i-=n)?(I.arraySet(s.window,e,r-i,i,0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whave<s.wsize&&(s.whave+=n))),0}r.inflateReset=
o,r.inflateReset2=h,r.inflateResetKeep=a,r.inflateInit=function(t){return u(t,15)},r.inflateInit2=u,r.inflate=function(t,e){var r,i,n,s,a,o,h,u,l,f,d,c,p,m,_,g,b,v,y,w,k,x,S,z,C=0,E=new I.Buf8(4),A=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];if(!t||!t.state||!t.output||!t.input&&0!==t.avail_in)return U;12===(r=t.state).mode&&(r.mode=13),a=t.next_out,n=t.output,h=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,u=r.hold,l=r.bits,f=o,d=h,x=N;t:for(;;)switch(r.mode){case P:if(0===r.wrap){r.mode=13;
break}for(;l<16;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}if(2&r.wrap&&35615===u){E[r.check=0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){t.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){t.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){t.msg="invalid window size",r.mode=30;break}r.dmax=1<<k,t.adler=r.check=1,r.mode=512&
u?10:12,l=u=0;break;case 2:for(;l<16;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}if(r.flags=u,8!=(255&r.flags)){t.msg="unknown compression method",r.mode=30;break}if(57344&r.flags){t.msg="unknown header flags set",r.mode=30;break}r.head&&(r.head.text=u>>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}r.head&&(r.head.time=u),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,
E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}r.head&&(r.head.xflags=255&u,r.head.os=u>>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}r.length=u,r.head&&(r.head.extra_len=u),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(c=r.length)&&(c=o),c&&(r.head&&(k=r.head.extra_len-
r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,i,s,c,k)),512&r.flags&&(r.check=B(r.check,i,c,s)),o-=c,s+=c,r.length-=c),r.length))break t;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break t;for(c=0;k=i[s+c++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&c<o;);if(512&r.flags&&(r.check=B(r.check,i,c,s)),o-=c,s+=c,k)break t}else r.head&&(r.head.name=null);r.length=0,r.mode=8;case 8:if(4096&r.flags){if(0===o)break t;for(c=0;k=
i[s+c++],r.head&&k&&r.length<65536&&(r.head.comment+=String.fromCharCode(k)),k&&c<o;);if(512&r.flags&&(r.check=B(r.check,i,c,s)),o-=c,s+=c,k)break t}else r.head&&(r.head.comment=null);r.mode=9;case 9:if(512&r.flags){for(;l<16;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}if(u!==(65535&r.check)){t.msg="header crc mismatch",r.mode=30;break}l=u=0}r.head&&(r.head.hcrc=r.flags>>9&1,r.head.done=!0),t.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}t.adler=r.check=L(u),
l=u=0,r.mode=11;case 11:if(0===r.havedict)return t.next_out=a,t.avail_out=h,t.next_in=s,t.avail_in=o,r.hold=u,r.bits=l,2;t.adler=r.check=1,r.mode=12;case 12:if(5===e||6===e)break t;case 13:if(r.last){u>>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}switch(r.last=1&u,l-=1,3&(u>>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==e)break;u>>>=2,l-=2;break t;case 2:r.mode=17;break;case 3:t.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&
l,l-=7&l;l<32;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}if((65535&u)!=(u>>>16^65535)){t.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===e)break t;case 15:r.mode=16;case 16:if(c=r.length){if(o<c&&(c=o),h<c&&(c=h),0===c)break t;I.arraySet(n,i,s,c,a),o-=c,s+=c,h-=c,a+=c,r.length-=c;break}r.mode=12;break;case 17:for(;l<14;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}if(r.nlen=257+(31&u),u>>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286<r.nlen||
30<r.ndist){t.msg="too many length or distance symbols",r.mode=30;break}r.have=0,r.mode=18;case 18:for(;r.have<r.ncode;){for(;l<3;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}r.lens[A[r.have++]]=7&u,u>>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have<r.nlen+r.ndist;){for(;g=(C=r.lencode[u&(1<<r.lenbits)-1])>>>
16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}if(b<16)u>>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l<z;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}if(u>>>=_,l-=_,0===r.have){t.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],c=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l<z;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}l-=_,k=0,c=3+(7&(u>>>=_)),u>>>=3,l-=3}else{for(z=_+7;l<z;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}l-=_,k=0,c=11+(127&(u>>>=_)),
u>>>=7,l-=7}if(r.have+c>r.nlen+r.ndist){t.msg="invalid bit length repeat",r.mode=30;break}for(;c--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){t.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){t.msg=
"invalid distances set",r.mode=30;break}if(r.mode=20,6===e)break t;case 20:r.mode=21;case 21:if(6<=o&&258<=h){t.next_out=a,t.avail_out=h,t.next_in=s,t.avail_in=o,r.hold=u,r.bits=l,R(t,d),a=t.next_out,n=t.output,h=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<<r.lenbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}if(g&&0==(240&g)){for(v=_,y=g,w=b;g=(C=r.lencode[w+((u&(1<<v+y)-1)>>
v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}u>>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){t.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l<z;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}r.length+=u&(1<<r.extra)-1,u>>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<<r.distbits)-
1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}if(0==(240&g)){for(v=_,y=g,w=b;g=(C=r.distcode[w+((u&(1<<v+y)-1)>>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}u>>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){t.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l<z;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}r.offset+=u&(1<<r.extra)-1,u>>>=r.extra,l-=r.extra,r.back+=
r.extra}if(r.offset>r.dmax){t.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break t;if(c=d-h,r.offset>c){if((c=r.offset-c)>r.whave&&r.sane){t.msg="invalid distance too far back",r.mode=30;break}p=c>r.wnext?(c-=r.wnext,r.wsize-c):r.wnext-c,c>r.length&&(c=r.length),m=r.window}else m=n,p=a-r.offset,c=r.length;for(h<c&&(c=h),h-=c,r.length-=c;n[a++]=m[p++],--c;);0===r.length&&(r.mode=21);break;case 26:if(0===h)break t;n[a++]=r.length,h--,r.mode=21;break;case 27:if(r.wrap){for(;l<
32;){if(0===o)break t;o--,u|=i[s++]<<l,l+=8}if(d-=h,t.total_out+=d,r.total+=d,d&&(t.adler=r.check=r.flags?B(r.check,n,d,a-d):O(r.check,n,d,a-d)),d=h,(r.flags?u:L(u))!==r.check){t.msg="incorrect data check",r.mode=30;break}l=u=0}r.mode=28;case 28:if(r.wrap&&r.flags){for(;l<32;){if(0===o)break t;o--,u+=i[s++]<<l,l+=8}if(u!==(4294967295&r.total)){t.msg="incorrect length check",r.mode=30;break}l=u=0}r.mode=29;case 29:x=1;break t;case 30:x=-3;break t;case 31:return-4;case 32:default:return U}return t.next_out=
a,t.avail_out=h,t.next_in=s,t.avail_in=o,r.hold=u,r.bits=l,(r.wsize||d!==t.avail_out&&r.mode<30&&(r.mode<27||4!==e))&&Z(t,t.output,t.next_out,d-t.avail_out)?(r.mode=31,-4):(f-=t.avail_in,d-=t.avail_out,t.total_in+=f,t.total_out+=d,r.total+=d,r.wrap&&d&&(t.adler=r.check=r.flags?B(r.check,n,d,t.next_out-d):O(r.check,n,d,t.next_out-d)),t.data_type=r.bits+(r.last?64:0)+(12===r.mode?128:0)+(20===r.mode||15===r.mode?256:0),(0==f&&0===d||4===e)&&x===N&&(x=-5),x)},r.inflateEnd=function(t){if(!t||!t.state)return U;
var e=t.state;return e.window&&(e.window=null),t.state=null,N},r.inflateGetHeader=function(t,e){var r;return t&&t.state?0==(2&(r=t.state).wrap)?U:((r.head=e).done=!1,N):U},r.inflateSetDictionary=function(t,e){var r,i=e.length;return t&&t.state?0!==(r=t.state).wrap&&11!==r.mode?U:11===r.mode&&O(1,e,i,0)!==r.check?-3:Z(t,e,i,i)?(r.mode=31,-4):(r.havedict=1,N):U},r.inflateInfo="pako inflate (from Nodeca project)"},{"../utils/common":41,"./adler32":43,"./crc32":45,"./inffast":48,"./inftrees":50}],50:[function(t,
e,r){var D=t("../utils/common"),F=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],N=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],U=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],P=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];e.exports=function(t,e,r,i,n,s,a,o){var h,u,l,f,d,c,p,m,_,
g=o.bits,b=0,v=0,y=0,w=0,k=0,x=0,S=0,z=0,C=0,E=0,A=null,I=0,O=new D.Buf16(16),B=new D.Buf16(16),R=null,T=0;for(b=0;b<=15;b++)O[b]=0;for(v=0;v<i;v++)O[e[r+v]]++;for(k=g,w=15;1<=w&&0===O[w];w--);if(w<k&&(k=w),0===w)return n[s++]=20971520,n[s++]=20971520,o.bits=1,0;for(y=1;y<w&&0===O[y];y++);for(k<y&&(k=y),b=z=1;b<=15;b++)if(z<<=1,(z-=O[b])<0)return-1;if(0<z&&(0===t||1!==w))return-1;for(B[1]=0,b=1;b<15;b++)B[b+1]=B[b]+O[b];for(v=0;v<i;v++)0!==e[r+v]&&(a[B[e[r+v]]++]=v);if(c=0===t?(A=R=a,19):1===t?(A=
F,I-=257,R=N,T-=257,256):(A=U,R=P,-1),b=y,d=s,S=v=E=0,l=-1,f=(C=1<<(x=k))-1,1===t&&852<C||2===t&&592<C)return 1;for(;;){for(p=b-S,_=a[v]<c?(m=0,a[v]):a[v]>c?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<<b-S,y=u=1<<x;n[d+(E>>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<<b-1;E&h;)h>>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=e[r+a[v]]}if(k<b&&(E&f)!==l){for(0===S&&(S=k),d+=y,z=1<<(x=b-S);x+S<w&&!((z-=O[x+S])<=0);)x++,z<<=1;if(C+=1<<x,1===t&&852<C||2===t&&592<C)return 1;n[l=E&f]=k<<24|x<<16|
d-s|0}}return 0!==E&&(n[d+E]=b-S<<24|64<<16|0),o.bits=k,0}},{"../utils/common":41}],51:[function(t,e,r){e.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},{}],52:[function(t,e,r){var n=t("../utils/common"),o=0,h=1;function i(t){for(var e=t.length;0<=--e;)t[e]=0}var s=0,a=29,u=256,l=u+1+a,f=30,d=19,_=2*l+1,g=15,c=16,p=7,m=256,b=16,v=17,y=18,w=[0,0,0,0,0,0,0,0,1,1,1,1,
2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],k=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],x=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],S=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],z=new Array(2*(l+2));i(z);var C=new Array(2*f);i(C);var E=new Array(512);i(E);var A=new Array(256);i(A);var I=new Array(a);i(I);var O,B,R,T=new Array(f);function D(t,e,r,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=r,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}function F(t,
e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}function N(t){return t<256?E[t]:E[256+(t>>>7)]}function U(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function P(t,e,r){t.bi_valid>c-r?(t.bi_buf|=e<<t.bi_valid&65535,U(t,t.bi_buf),t.bi_buf=e>>c-t.bi_valid,t.bi_valid+=r-c):(t.bi_buf|=e<<t.bi_valid&65535,t.bi_valid+=r)}function L(t,e,r){P(t,r[2*e],r[2*e+1])}function j(t,e){for(var r=0;r|=1&t,t>>>=1,r<<=1,0<--e;);return r>>>1}function Z(t,e,r){var i,n,s=new Array(g+1),
a=0;for(i=1;i<=g;i++)s[i]=a=a+r[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=j(s[o]++,o))}}function W(t){var e;for(e=0;e<l;e++)t.dyn_ltree[2*e]=0;for(e=0;e<f;e++)t.dyn_dtree[2*e]=0;for(e=0;e<d;e++)t.bl_tree[2*e]=0;t.dyn_ltree[2*m]=1,t.opt_len=t.static_len=0,t.last_lit=t.matches=0}function M(t){8<t.bi_valid?U(t,t.bi_buf):0<t.bi_valid&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0}function H(t,e,r,i){var n=2*e,s=2*r;return t[n]<t[s]||t[n]===t[s]&&i[e]<=i[r]}function G(t,e,
r){for(var i=t.heap[r],n=r<<1;n<=t.heap_len&&(n<t.heap_len&&H(e,t.heap[n+1],t.heap[n],t.depth)&&n++,!H(e,i,t.heap[n],t.depth));)t.heap[r]=t.heap[n],r=n,n<<=1;t.heap[r]=i}function K(t,e,r){var i,n,s,a,o=0;if(0!==t.last_lit)for(;i=t.pending_buf[t.d_buf+2*o]<<8|t.pending_buf[t.d_buf+2*o+1],n=t.pending_buf[t.l_buf+o],o++,0===i?L(t,n,e):(L(t,(s=A[n])+u+1,e),0!==(a=w[s])&&P(t,n-=I[s],a),L(t,s=N(--i),r),0!==(a=k[s])&&P(t,i-=T[s],a)),o<t.last_lit;);L(t,m,e)}function Y(t,e){var r,i,n,s=e.dyn_tree,a=e.stat_desc.static_tree,
o=e.stat_desc.has_stree,h=e.stat_desc.elems,u=-1;for(t.heap_len=0,t.heap_max=_,r=0;r<h;r++)0!==s[2*r]?(t.heap[++t.heap_len]=u=r,t.depth[r]=0):s[2*r+1]=0;for(;t.heap_len<2;)s[2*(n=t.heap[++t.heap_len]=u<2?++u:0)]=1,t.depth[n]=0,t.opt_len--,o&&(t.static_len-=a[2*n+1]);for(e.max_code=u,r=t.heap_len>>1;1<=r;r--)G(t,s,r);for(n=h;r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],G(t,s,1),i=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=i,s[2*n]=s[2*r]+s[2*i],t.depth[n]=(t.depth[r]>=t.depth[i]?t.depth[r]:
t.depth[i])+1,s[2*r+1]=s[2*i+1]=n,t.heap[1]=n++,G(t,s,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1],function(t,e){var r,i,n,s,a,o,h=e.dyn_tree,u=e.max_code,l=e.stat_desc.static_tree,f=e.stat_desc.has_stree,d=e.stat_desc.extra_bits,c=e.stat_desc.extra_base,p=e.stat_desc.max_length,m=0;for(s=0;s<=g;s++)t.bl_count[s]=0;for(h[2*t.heap[t.heap_max]+1]=0,r=t.heap_max+1;r<_;r++)p<(s=h[2*h[2*(i=t.heap[r])+1]+1]+1)&&(s=p,m++),h[2*i+1]=s,u<i||(t.bl_count[s]++,a=0,c<=i&&(a=d[i-c]),o=h[2*i],t.opt_len+=o*(s+
a),f&&(t.static_len+=o*(l[2*i+1]+a)));if(0!==m){do{for(s=p-1;0===t.bl_count[s];)s--;t.bl_count[s]--,t.bl_count[s+1]+=2,t.bl_count[p]--,m-=2}while(0<m);for(s=p;0!==s;s--)for(i=t.bl_count[s];0!==i;)u<(n=t.heap[--r])||(h[2*n+1]!==s&&(t.opt_len+=(s-h[2*n+1])*h[2*n],h[2*n+1]=s),i--)}}(t,e),Z(s,u,t.bl_count)}function X(t,e,r){var i,n,s=-1,a=e[1],o=0,h=7,u=4;for(0===a&&(h=138,u=3),e[2*(r+1)+1]=65535,i=0;i<=r;i++)n=a,a=e[2*(i+1)+1],++o<h&&n===a||(o<u?t.bl_tree[2*n]+=o:0!==n?(n!==s&&t.bl_tree[2*n]++,t.bl_tree[2*
b]++):o<=10?t.bl_tree[2*v]++:t.bl_tree[2*y]++,s=n,u=(o=0)===a?(h=138,3):n===a?(h=6,3):(h=7,4))}function V(t,e,r){var i,n,s=-1,a=e[1],o=0,h=7,u=4;for(0===a&&(h=138,u=3),i=0;i<=r;i++)if(n=a,a=e[2*(i+1)+1],!(++o<h&&n===a)){if(o<u)for(;L(t,n,t.bl_tree),0!=--o;);else 0!==n?(n!==s&&(L(t,n,t.bl_tree),o--),L(t,b,t.bl_tree),P(t,o-3,2)):o<=10?(L(t,v,t.bl_tree),P(t,o-3,3)):(L(t,y,t.bl_tree),P(t,o-11,7));s=n,u=(o=0)===a?(h=138,3):n===a?(h=6,3):(h=7,4)}}i(T);var q=!1;function J(t,e,r,i){P(t,(s<<1)+(i?1:0),3),
function(t,e,r,i){M(t),i&&(U(t,r),U(t,~r)),n.arraySet(t.pending_buf,t.window,e,r,t.pending),t.pending+=r}(t,e,r,!0)}r._tr_init=function(t){q||(function(){var t,e,r,i,n,s=new Array(g+1);for(i=r=0;i<a-1;i++)for(I[i]=r,t=0;t<1<<w[i];t++)A[r++]=i;for(A[r-1]=i,i=n=0;i<16;i++)for(T[i]=n,t=0;t<1<<k[i];t++)E[n++]=i;for(n>>=7;i<f;i++)for(T[i]=n<<7,t=0;t<1<<k[i]-7;t++)E[256+n++]=i;for(e=0;e<=g;e++)s[e]=0;for(t=0;t<=143;)z[2*t+1]=8,t++,s[8]++;for(;t<=255;)z[2*t+1]=9,t++,s[9]++;for(;t<=279;)z[2*t+1]=7,t++,s[7]++;
for(;t<=287;)z[2*t+1]=8,t++,s[8]++;for(Z(z,l+1,s),t=0;t<f;t++)C[2*t+1]=5,C[2*t]=j(t,5);O=new D(z,w,u+1,l,g),B=new D(C,k,0,f,g),R=new D(new Array(0),x,0,d,p)}(),q=!0),t.l_desc=new F(t.dyn_ltree,O),t.d_desc=new F(t.dyn_dtree,B),t.bl_desc=new F(t.bl_tree,R),t.bi_buf=0,t.bi_valid=0,W(t)},r._tr_stored_block=J,r._tr_flush_block=function(t,e,r,i){var n,s,a=0;0<t.level?(2===t.strm.data_type&&(t.strm.data_type=function(t){var e,r=4093624447;for(e=0;e<=31;e++,r>>>=1)if(1&r&&0!==t.dyn_ltree[2*e])return o;if(0!==
t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return h;for(e=32;e<u;e++)if(0!==t.dyn_ltree[2*e])return h;return o}(t)),Y(t,t.l_desc),Y(t,t.d_desc),a=function(t){var e;for(X(t,t.dyn_ltree,t.l_desc.max_code),X(t,t.dyn_dtree,t.d_desc.max_code),Y(t,t.bl_desc),e=d-1;3<=e&&0===t.bl_tree[2*S[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e}(t),n=t.opt_len+3+7>>>3,(s=t.static_len+3+7>>>3)<=n&&(n=s)):n=s=r+5,r+4<=n&&-1!==e?J(t,e,r,i):4===t.strategy||s===n?(P(t,2+(i?1:0),3),K(t,z,C)):(P(t,4+(i?1:0),
3),function(t,e,r,i){var n;for(P(t,e-257,5),P(t,r-1,5),P(t,i-4,4),n=0;n<i;n++)P(t,t.bl_tree[2*S[n]+1],3);V(t,t.dyn_ltree,e-1),V(t,t.dyn_dtree,r-1)}(t,t.l_desc.max_code+1,t.d_desc.max_code+1,a+1),K(t,t.dyn_ltree,t.dyn_dtree)),W(t),i&&M(t)},r._tr_tally=function(t,e,r){return t.pending_buf[t.d_buf+2*t.last_lit]=e>>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&r,t.last_lit++,0===e?t.dyn_ltree[2*r]++:(t.matches++,e--,t.dyn_ltree[2*(A[r]+u+1)]++,t.dyn_dtree[2*
N(e)]++),t.last_lit===t.lit_bufsize-1},r._tr_align=function(t){P(t,2,3),L(t,m,z),function(t){16===t.bi_valid?(U(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):8<=t.bi_valid&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}(t)}},{"../utils/common":41}],53:[function(t,e,r){e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,
e,r){e.exports="function"==typeof setImmediate?setImmediate:function(){var t=[].slice.apply(arguments);t.splice(1,0,0),setTimeout.apply(null,t)}},{}]},{},[10])(10)};

10
public/src/js/lib/md5.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,46 @@
var Oggmented = (function() {
var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;
return (
function(Oggmented) {
Oggmented = Oggmented || {};
(function(b,c){function m(h){delete k[h]}function l(h){if(e)setTimeout(l,0,h);else{var t=k[h];if(t){e=!0;try{var p=t.A,y=t.v;switch(y.length){case 0:p();break;case 1:p(y[0]);break;case 2:p(y[0],y[1]);break;case 3:p(y[0],y[1],y[2]);break;default:p.apply(c,y)}}finally{m(h),e=!1}}}}function q(){r=function(h){process.C(function(){l(h)})}}function u(){if(b.postMessage&&!b.importScripts){var h=!0,t=b.onmessage;b.onmessage=function(){h=!1};b.postMessage("","*");b.onmessage=t;return h}}function w(){function h(p){p.source===
b&&"string"===typeof p.data&&0===p.data.indexOf(t)&&l(+p.data.slice(t.length))}var t="setImmediate$"+Math.random()+"$";b.addEventListener?b.addEventListener("message",h,!1):b.attachEvent("onmessage",h);r=function(p){b.postMessage(t+p,"*")}}function v(){var h=new MessageChannel;h.port1.onmessage=function(t){l(t.data)};r=function(t){h.port2.postMessage(t)}}function A(){var h=f.documentElement;r=function(t){var p=f.createElement("script");p.onreadystatechange=function(){l(t);p.onreadystatechange=null;
h.removeChild(p);p=null};h.appendChild(p)}}function d(){r=function(h){setTimeout(l,0,h)}}if(!b.setImmediate){var g=1,k={},e=!1,f=b.document,r,n=Object.getPrototypeOf&&Object.getPrototypeOf(b);n=n&&n.setTimeout?n:b;"[object process]"==={}.toString.call(b.process)?q():u()?w():b.MessageChannel?v():f&&"onreadystatechange"in f.createElement("script")?A():d();n.setImmediate=function(h){"function"!==typeof h&&(h=new Function(""+h));for(var t=Array(arguments.length-1),p=0;p<t.length;p++)t[p]=arguments[p+
1];k[g]={A:h,v:t};r(g);return g++};n.clearImmediate=m}})("undefined"===typeof self?"undefined"===typeof global?this:global:self);
null;var a;a||(a=typeof Oggmented !== 'undefined' ? Oggmented : {});var x,z;a.ready=new Promise(function(b,c){x=b;z=c});const aa=new (window.AudioContext||window.webkitAudioContext);
a.decodeOggData=(b,c,m)=>{try{var {channels:l,length:q,rate:u}=(g=>{const k=g.byteLength,e=B(k);g=new Int8Array(g);C.set(g,e);D("open_buffer","number",["number","number"],[e,k]);return{channels:ba(),length:ca(),rate:da()}})(b),w=aa.createBuffer(l,q,u),v=B(Uint32Array.BYTES_PER_ELEMENT),A=0}catch(g){return m&&m(g)}const d=()=>{try{const g=Date.now();let k;for(;k=ea(v);){const e=fa(v,"*"),f=new Uint32Array(ha.buffer,e,l);for(let r=0;r<l;r++){const n=new Float32Array(E.buffer,f[r],k);w.getChannelData(r).set(n,
A)}A+=k;if(g+8<Date.now()){setImmediate(d);break}}0===k&&(ia(),F(b),F(v),c&&c(w))}catch(g){return m&&m(g)}};setImmediate(d);return w};var G={},H;for(H in a)a.hasOwnProperty(H)&&(G[H]=a[H]);function ja(b,c){throw c;}var I="";"undefined"!==typeof document&&document.currentScript&&(I=document.currentScript.src);_scriptDir&&(I=_scriptDir);0!==I.indexOf("blob:")?I=I.substr(0,I.lastIndexOf("/")+1):I="";var J=a.printErr||console.warn.bind(console);for(H in G)G.hasOwnProperty(H)&&(a[H]=G[H]);G=null;
a.quit&&(ja=a.quit);var K;a.wasmBinary&&(K=a.wasmBinary);var noExitRuntime;a.noExitRuntime&&(noExitRuntime=a.noExitRuntime);"object"!==typeof WebAssembly&&L("no native wasm support detected");
function fa(b,c){c=c||"i8";"*"===c.charAt(c.length-1)&&(c="i32");switch(c){case "i1":return C[b>>0];case "i8":return C[b>>0];case "i16":return ka[b>>1];case "i32":return M[b>>2];case "i64":return M[b>>2];case "float":return E[b>>2];case "double":return la[b>>3];default:L("invalid type for getValue: "+c)}return null}var N,O=!1;function ma(b){var c=a["_"+b];c||L("Assertion failed: Cannot call unknown function "+(b+", make sure it is exported"));return c}
function D(b,c,m,l){var q={string:function(d){var g=0;if(null!==d&&void 0!==d&&0!==d){var k=(d.length<<2)+1;g=P(k);var e=g,f=Q;if(0<k){k=e+k-1;for(var r=0;r<d.length;++r){var n=d.charCodeAt(r);if(55296<=n&&57343>=n){var h=d.charCodeAt(++r);n=65536+((n&1023)<<10)|h&1023}if(127>=n){if(e>=k)break;f[e++]=n}else{if(2047>=n){if(e+1>=k)break;f[e++]=192|n>>6}else{if(65535>=n){if(e+2>=k)break;f[e++]=224|n>>12}else{if(e+3>=k)break;f[e++]=240|n>>18;f[e++]=128|n>>12&63}f[e++]=128|n>>6&63}f[e++]=128|n&63}}f[e]=
0}}return g},array:function(d){var g=P(d.length);C.set(d,g);return g}},u=ma(b),w=[];b=0;if(l)for(var v=0;v<l.length;v++){var A=q[m[v]];A?(0===b&&(b=na()),w[v]=A(l[v])):w[v]=l[v]}m=u.apply(null,w);m=function(d){if("string"===c)if(d){for(var g=Q,k=d+NaN,e=d;g[e]&&!(e>=k);)++e;if(16<e-d&&g.subarray&&oa)d=oa.decode(g.subarray(d,e));else{for(k="";d<e;){var f=g[d++];if(f&128){var r=g[d++]&63;if(192==(f&224))k+=String.fromCharCode((f&31)<<6|r);else{var n=g[d++]&63;f=224==(f&240)?(f&15)<<12|r<<6|n:(f&7)<<
18|r<<12|n<<6|g[d++]&63;65536>f?k+=String.fromCharCode(f):(f-=65536,k+=String.fromCharCode(55296|f>>10,56320|f&1023))}}else k+=String.fromCharCode(f)}d=k}}else d="";else d="boolean"===c?!!d:d;return d}(m);0!==b&&pa(b);return m}var oa="undefined"!==typeof TextDecoder?new TextDecoder("utf8"):void 0,R,C,Q,ka,M,ha,E,la;
function qa(b){R=b;a.HEAP8=C=new Int8Array(b);a.HEAP16=ka=new Int16Array(b);a.HEAP32=M=new Int32Array(b);a.HEAPU8=Q=new Uint8Array(b);a.HEAPU16=new Uint16Array(b);a.HEAPU32=ha=new Uint32Array(b);a.HEAPF32=E=new Float32Array(b);a.HEAPF64=la=new Float64Array(b)}var ra=a.INITIAL_MEMORY||16777216;a.wasmMemory?N=a.wasmMemory:N=new WebAssembly.Memory({initial:ra/65536,maximum:32768});N&&(R=N.buffer);ra=R.byteLength;qa(R);var S,sa=[],ta=[],ua=[],va=[];function wa(){var b=a.preRun.shift();sa.unshift(b)}
var T=0,U=null,V=null;a.preloadedImages={};a.preloadedAudios={};function L(b){if(a.onAbort)a.onAbort(b);J(b);O=!0;b=new WebAssembly.RuntimeError("abort("+b+"). Build with -s ASSERTIONS=1 for more info.");z(b);throw b;}function xa(){var b=W;return String.prototype.startsWith?b.startsWith("data:application/octet-stream;base64,"):0===b.indexOf("data:application/octet-stream;base64,")}var W="oggmented-wasm.wasm";if(!xa()){var ya=W;W=a.locateFile?a.locateFile(ya,I):I+ya}
function za(){try{if(K)return new Uint8Array(K);throw"both async and sync fetching of the wasm failed";}catch(b){L(b)}}function Aa(){return K||"function"!==typeof fetch?Promise.resolve().then(za):fetch(W,{credentials:"same-origin"}).then(function(b){if(!b.ok)throw"failed to load wasm binary file at '"+W+"'";return b.arrayBuffer()}).catch(function(){return za()})}
function X(b){for(;0<b.length;){var c=b.shift();if("function"==typeof c)c(a);else{var m=c.B;"number"===typeof m?void 0===c.u?S.get(m)():S.get(m)(c.u):m(void 0===c.u?null:c.u)}}}ta.push({B:function(){Ba()}});
var Da={b:function(b,c,m){Q.copyWithin(b,c,c+m)},c:function(b){b>>>=0;var c=Q.length;if(2147483648<b)return!1;for(var m=1;4>=m;m*=2){var l=c*(1+.2/m);l=Math.min(l,b+100663296);l=Math.max(16777216,b,l);0<l%65536&&(l+=65536-l%65536);a:{try{N.grow(Math.min(2147483648,l)-R.byteLength+65535>>>16);qa(N.buffer);var q=1;break a}catch(u){}q=void 0}if(q)return!0}return!1},d:function(b){if(!noExitRuntime){if(a.onExit)a.onExit(b);O=!0}ja(b,new Ca(b))},a:N};
(function(){function b(q){a.asm=q.exports;S=a.asm.e;T--;a.monitorRunDependencies&&a.monitorRunDependencies(T);0==T&&(null!==U&&(clearInterval(U),U=null),V&&(q=V,V=null,q()))}function c(q){b(q.instance)}function m(q){return Aa().then(function(u){return WebAssembly.instantiate(u,l)}).then(q,function(u){J("failed to asynchronously prepare wasm: "+u);L(u)})}var l={a:Da};T++;a.monitorRunDependencies&&a.monitorRunDependencies(T);if(a.instantiateWasm)try{return a.instantiateWasm(l,b)}catch(q){return J("Module.instantiateWasm callback failed with error: "+
q),!1}(function(){return K||"function"!==typeof WebAssembly.instantiateStreaming||xa()||"function"!==typeof fetch?m(c):fetch(W,{credentials:"same-origin"}).then(function(q){return WebAssembly.instantiateStreaming(q,l).then(c,function(u){J("wasm streaming compile failed: "+u);J("falling back to ArrayBuffer instantiation");return m(c)})})})().catch(z);return{}})();var Ba=a.___wasm_call_ctors=function(){return(Ba=a.___wasm_call_ctors=a.asm.f).apply(null,arguments)};
a._open_buffer=function(){return(a._open_buffer=a.asm.g).apply(null,arguments)};var ia=a._close_buffer=function(){return(ia=a._close_buffer=a.asm.h).apply(null,arguments)},ca=a._get_length=function(){return(ca=a._get_length=a.asm.i).apply(null,arguments)},ba=a._get_channels=function(){return(ba=a._get_channels=a.asm.j).apply(null,arguments)},da=a._get_rate=function(){return(da=a._get_rate=a.asm.k).apply(null,arguments)};a._get_time=function(){return(a._get_time=a.asm.l).apply(null,arguments)};
a._get_streams=function(){return(a._get_streams=a.asm.m).apply(null,arguments)};
var ea=a._read_float=function(){return(ea=a._read_float=a.asm.n).apply(null,arguments)},F=a._free=function(){return(F=a._free=a.asm.o).apply(null,arguments)},B=a._malloc=function(){return(B=a._malloc=a.asm.p).apply(null,arguments)},na=a.stackSave=function(){return(na=a.stackSave=a.asm.q).apply(null,arguments)},pa=a.stackRestore=function(){return(pa=a.stackRestore=a.asm.r).apply(null,arguments)},P=a.stackAlloc=function(){return(P=a.stackAlloc=a.asm.s).apply(null,arguments)};a.ccall=D;a.getValue=fa;
var Y;function Ca(b){this.name="ExitStatus";this.message="Program terminated with exit("+b+")";this.status=b}V=function Ea(){Y||Z();Y||(V=Ea)};
function Z(){function b(){if(!Y&&(Y=!0,a.calledRun=!0,!O)){X(ta);X(ua);x(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;){var c=a.postRun.shift();va.unshift(c)}X(va)}}if(!(0<T)){if(a.preRun)for("function"==typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)wa();X(sa);0<T||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);b()},1)):b())}}a.run=Z;
if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();noExitRuntime=!0;Z();
return Oggmented.ready
}
);
})();
if (typeof exports === 'object' && typeof module === 'object')
module.exports = Oggmented;
else if (typeof define === 'function' && define['amd'])
define([], function() { return Oggmented; });
else if (typeof exports === 'object')
exports["Oggmented"] = Oggmented;

Binary file not shown.

587
public/src/js/loader.js Normal file
View File

@@ -0,0 +1,587 @@
class Loader{
constructor(...args){
this.init(...args)
}
init(callback){
this.callback = callback
this.loadedAssets = 0
this.assetsDiv = document.getElementById("assets")
this.screen = document.getElementById("screen")
this.startTime = Date.now()
this.errorMessages = []
this.songSearchGradient = "linear-gradient(to top, rgba(245, 246, 252, 0.08), #ff5963), "
var promises = []
promises.push(this.ajax("src/views/loader.html").then(page => {
this.screen.innerHTML = page
}))
promises.push(this.ajax("api/config").then(conf => {
gameConfig = JSON.parse(conf)
}))
Promise.all(promises).then(this.run.bind(this))
}
run(){
this.promises = []
this.loaderDiv = document.querySelector("#loader")
this.loaderPercentage = document.querySelector("#loader .percentage")
this.loaderProgress = document.querySelector("#loader .progress")
this.queryString = gameConfig._version.commit_short ? "?" + gameConfig._version.commit_short : ""
if(gameConfig.custom_js){
this.addPromise(this.loadScript(gameConfig.custom_js), gameConfig.custom_js)
}
var oggSupport = new Audio().canPlayType("audio/ogg;codecs=vorbis")
if(!oggSupport){
assets.js.push("lib/oggmented-wasm.js")
}
assets.js.forEach(name => {
this.addPromise(this.loadScript("src/js/" + name), "src/js/" + name)
})
var pageVersion = versionLink.href
var index = pageVersion.lastIndexOf("/")
if(index !== -1){
pageVersion = pageVersion.slice(index + 1)
}
this.addPromise(new Promise((resolve, reject) => {
if(
versionLink.href !== gameConfig._version.url &&
gameConfig._version.commit &&
versionLink.href.indexOf(gameConfig._version.commit) === -1
){
reject("Version on the page and config does not match\n(page: " + pageVersion + ",\nconfig: "+ gameConfig._version.commit + ")")
}
var cssCount = document.styleSheets.length + assets.css.length
assets.css.forEach(name => {
var stylesheet = document.createElement("link")
stylesheet.rel = "stylesheet"
stylesheet.href = "src/css/" + name + this.queryString
document.head.appendChild(stylesheet)
})
var checkStyles = () => {
if(document.styleSheets.length >= cssCount){
resolve()
clearInterval(interval)
}
}
var interval = setInterval(checkStyles, 100)
checkStyles()
}))
for(var name in assets.fonts){
var url = gameConfig.assets_baseurl + "fonts/" + assets.fonts[name]
this.addPromise(new FontFace(name, "url('" + url + "')").load().then(font => {
document.fonts.add(font)
}), url)
}
assets.img.forEach(name => {
var id = this.getFilename(name)
var image = document.createElement("img")
image.crossOrigin = "anonymous"
var url = gameConfig.assets_baseurl + "img/" + name
this.addPromise(pageEvents.load(image), url)
image.id = name
image.src = url
this.assetsDiv.appendChild(image)
assets.image[id] = image
})
var css = []
for(let selector in assets.cssBackground){
let name = assets.cssBackground[selector]
var url = gameConfig.assets_baseurl + "img/" + name
this.addPromise(loader.ajax(url, request => {
request.responseType = "blob"
}).then(blob => {
var id = this.getFilename(name)
var image = document.createElement("img")
let blobUrl = URL.createObjectURL(blob)
var promise = pageEvents.load(image).then(() => {
var gradient = ""
if(selector === ".pattern-bg"){
loader.screen.style.backgroundImage = "url(\"" + blobUrl + "\")"
}else if(selector === "#song-search"){
gradient = this.songSearchGradient
}
css.push(this.cssRuleset({
[selector]: {
"background-image": gradient + "url(\"" + blobUrl + "\")"
}
}))
})
image.id = name
image.src = blobUrl
this.assetsDiv.appendChild(image)
assets.image[id] = image
return promise
}), url)
}
assets.views.forEach(name => {
var id = this.getFilename(name)
var url = "src/views/" + name + this.queryString
this.addPromise(this.ajax(url).then(page => {
assets.pages[id] = page
}), url)
})
this.addPromise(this.ajax("api/categories").then(cats => {
assets.categories = JSON.parse(cats)
assets.categories.forEach(cat => {
if(cat.song_skin){
cat.songSkin = cat.song_skin //rename the song_skin property and add category title to categories array
delete cat.song_skin
cat.songSkin.infoFill = cat.songSkin.info_fill
delete cat.songSkin.info_fill
}
})
assets.categories.push({
title: "default",
songSkin: {
background: "#ececec",
border: ["#fbfbfb", "#8b8b8b"],
outline: "#656565",
infoFill: "#656565"
}
})
}), "api/categories")
var url = gameConfig.assets_baseurl + "img/vectors.json" + this.queryString
this.addPromise(this.ajax(url).then(response => {
vectors = JSON.parse(response)
}), url)
this.afterJSCount =
[
"api/songs",
"blurPerformance",
"categories"
].length +
assets.audioSfx.length +
assets.audioMusic.length +
assets.audioSfxLR.length +
assets.audioSfxLoud.length +
(gameConfig.accounts ? 1 : 0)
Promise.all(this.promises).then(() => {
if(this.error){
return
}
var style = document.createElement("style")
style.appendChild(document.createTextNode(css.join("\n")))
document.head.appendChild(style)
this.addPromise(this.ajax("api/songs").then(songs => {
songs = JSON.parse(songs)
songs.forEach(song => {
var directory = gameConfig.songs_baseurl + song.id + "/"
var songExt = song.music_type ? song.music_type : "mp3"
song.music = new RemoteFile(directory + "main." + songExt)
if(song.type === "tja"){
song.chart = new RemoteFile(directory + "main.tja")
}else{
song.chart = {separateDiff: true}
for(var diff in song.courses){
if(song.courses[diff]){
song.chart[diff] = new RemoteFile(directory + diff + ".osu")
}
}
}
if(song.lyrics){
song.lyricsFile = new RemoteFile(directory + "main.vtt")
}
if(song.preview > 0){
song.previewMusic = new RemoteFile(directory + "preview." + gameConfig.preview_type)
}
})
assets.songsDefault = songs
assets.songs = assets.songsDefault
}), "api/songs")
var categoryPromises = []
assets.categories //load category backgrounds to DOM
.filter(cat => cat.songSkin && cat.songSkin.bg_img)
.forEach(cat => {
let name = cat.songSkin.bg_img
var url = gameConfig.assets_baseurl + "img/" + name
categoryPromises.push(loader.ajax(url, request => {
request.responseType = "blob"
}).then(blob => {
var id = this.getFilename(name)
var image = document.createElement("img")
let blobUrl = URL.createObjectURL(blob)
var promise = pageEvents.load(image)
image.id = name
image.src = blobUrl
this.assetsDiv.appendChild(image)
assets.image[id] = image
return promise
}).catch(response => {
return this.errorMsg(response, url)
}))
})
this.addPromise(Promise.all(categoryPromises))
snd.buffer = new SoundBuffer()
if(!oggSupport){
snd.buffer.oggDecoder = snd.buffer.fallbackDecoder
}
snd.musicGain = snd.buffer.createGain()
snd.sfxGain = snd.buffer.createGain()
snd.previewGain = snd.buffer.createGain()
snd.sfxGainL = snd.buffer.createGain("left")
snd.sfxGainR = snd.buffer.createGain("right")
snd.sfxLoudGain = snd.buffer.createGain()
snd.buffer.setCrossfade(
[snd.musicGain, snd.previewGain],
[snd.sfxGain, snd.sfxGainL, snd.sfxGainR],
0.5
)
snd.sfxLoudGain.setVolume(1.2)
snd.buffer.saveSettings()
this.afterJSCount = 0
assets.audioSfx.forEach(name => {
this.addPromise(this.loadSound(name, snd.sfxGain), this.soundUrl(name))
})
assets.audioMusic.forEach(name => {
this.addPromise(this.loadSound(name, snd.musicGain), this.soundUrl(name))
})
assets.audioSfxLR.forEach(name => {
this.addPromise(this.loadSound(name, snd.sfxGain).then(sound => {
var id = this.getFilename(name)
assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL)
assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR)
}), this.soundUrl(name))
})
assets.audioSfxLoud.forEach(name => {
this.addPromise(this.loadSound(name, snd.sfxLoudGain), this.soundUrl(name))
})
this.canvasTest = new CanvasTest()
this.addPromise(this.canvasTest.blurPerformance().then(result => {
perf.blur = result
if(result > 1000 / 50){
// Less than 50 fps with blur enabled
disableBlur = true
}
}), "blurPerformance")
if(gameConfig.accounts){
this.addPromise(this.ajax("api/scores/get").then(response => {
response = JSON.parse(response)
if(response.status === "ok"){
account.loggedIn = true
account.username = response.username
account.displayName = response.display_name
account.don = response.don
scoreStorage.load(response.scores)
pageEvents.send("login", account.username)
}
}), "api/scores/get")
}
settings = new Settings()
pageEvents.setKbd()
scoreStorage = new ScoreStorage()
db = new IDB("taiko", "store")
plugins = new Plugins()
if(localStorage.getItem("lastSearchQuery")){
localStorage.removeItem("lastSearchQuery")
}
Promise.all(this.promises).then(() => {
if(this.error){
return
}
if(!account.loggedIn){
scoreStorage.load()
}
for(var i in assets.songsDefault){
var song = assets.songsDefault[i]
if(!song.hash){
song.hash = song.title
}
scoreStorage.songTitles[song.title] = song.hash
var score = scoreStorage.get(song.hash, false, true)
if(score){
score.title = song.title
}
}
var promises = []
var readyEvent = "normal"
var songId
var hashLower = location.hash.toLowerCase()
p2 = new P2Connection()
if(hashLower.startsWith("#song=")){
var number = parseInt(location.hash.slice(6))
if(number > 0){
songId = number
readyEvent = "song-id"
}
}else if(location.hash.length === 6){
p2.hashLock = true
promises.push(new Promise(resolve => {
p2.open()
pageEvents.add(p2, "message", response => {
if(response.type === "session"){
pageEvents.send("session-start", "invited")
readyEvent = "session-start"
resolve()
}else if(response.type === "gameend"){
p2.hash("")
p2.hashLock = false
readyEvent = "session-expired"
resolve()
}
})
p2.send("invite", {
id: location.hash.slice(1).toLowerCase(),
name: account.loggedIn ? account.displayName : null,
don: account.loggedIn ? account.don : null
})
setTimeout(() => {
if(p2.socket.readyState !== 1){
p2.hash("")
p2.hashLock = false
resolve()
}
}, 10000)
}).then(() => {
pageEvents.remove(p2, "message")
}))
}else{
p2.hash("")
}
promises.push(this.canvasTest.drawAllImages().then(result => {
perf.allImg = result
}))
if(gameConfig.plugins){
gameConfig.plugins.forEach(obj => {
if(obj.url){
var plugin = plugins.add(obj.url, {
hide: obj.hide
})
if(plugin){
plugin.loadErrors = true
promises.push(plugin.load(true).then(() => {
if(obj.start){
return plugin.start(false, true)
}
}).catch(response => {
return this.errorMsg(response, obj.url)
}))
}
}
})
}
Promise.all(promises).then(() => {
perf.load = Date.now() - this.startTime
this.canvasTest.clean()
this.clean()
this.callback(songId)
this.ready = true
pageEvents.send("ready", readyEvent)
}, e => this.errorMsg(e))
}, e => this.errorMsg(e))
})
}
addPromise(promise, url){
this.promises.push(promise)
promise.then(this.assetLoaded.bind(this), response => {
return this.errorMsg(response, url)
})
}
soundUrl(name){
return gameConfig.assets_baseurl + "audio/" + name
}
loadSound(name, gain){
var id = this.getFilename(name)
return gain.load(new RemoteFile(this.soundUrl(name))).then(sound => {
assets.sounds[id] = sound
})
}
getFilename(name){
return name.slice(0, name.lastIndexOf("."))
}
errorMsg(error, url){
var rethrow
if(url || error){
if(typeof error === "object" && error.constructor === Error){
rethrow = error
error = error.stack || ""
var index = error.indexOf("\n ")
if(index !== -1){
error = error.slice(0, index)
}
}else if(Array.isArray(error)){
error = error[0]
}
if(url){
error = (error ? error + ": " : "") + url
}
this.errorMessages.push(error)
pageEvents.send("loader-error", url || error)
}
if(!this.error){
this.error = true
cancelTouch = false
this.loaderDiv.classList.add("loaderError")
if(typeof allStrings === "object"){
var lang = localStorage.lang
if(!lang){
var userLang = navigator.languages.slice()
userLang.unshift(navigator.language)
for(var i in userLang){
for(var j in allStrings){
if(allStrings[j].regex.test(userLang[i])){
lang = j
}
}
}
}
if(!lang){
lang = "en"
}
loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang] && allStrings[lang].errorOccured || allStrings.en.errorOccured
}
var loaderError = loader.screen.getElementsByClassName("loader-error-div")[0]
loaderError.style.display = "flex"
var diagTxt = loader.screen.getElementsByClassName("diag-txt")[0]
var debugLink = loader.screen.getElementsByClassName("debug-link")[0]
if(navigator.userAgent.indexOf("Android") >= 0){
var iframe = document.createElement("iframe")
diagTxt.appendChild(iframe)
var body = iframe.contentWindow.document.body
body.setAttribute("style", `
font-family: monospace;
margin: 2px 0 0 2px;
white-space: pre-wrap;
word-break: break-all;
cursor: text;
`)
body.setAttribute("onblur", `
getSelection().removeAllRanges()
`)
this.errorTxt = {
element: body,
method: "innerText"
}
}else{
var textarea = document.createElement("textarea")
textarea.readOnly = true
diagTxt.appendChild(textarea)
if(!this.touchEnabled){
textarea.addEventListener("focus", () => {
textarea.select()
})
textarea.addEventListener("blur", () => {
getSelection().removeAllRanges()
})
}
this.errorTxt = {
element: textarea,
method: "value"
}
}
var show = () => {
diagTxt.style.display = "block"
debugLink.style.display = "none"
}
debugLink.addEventListener("click", show)
debugLink.addEventListener("touchstart", show)
this.clean(true)
}
var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount))
this.errorTxt.element[this.errorTxt.method] = "```\n" + this.errorMessages.join("\n") + "\nPercentage: " + percentage + "%\n```"
if(rethrow || error){
console.error(rethrow || error)
}
return Promise.reject()
}
assetLoaded(){
if(!this.error){
this.loadedAssets++
var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount))
this.loaderProgress.style.width = percentage + "%"
this.loaderPercentage.firstChild.data = percentage + "%"
}
}
changePage(name, patternBg){
this.screen.innerHTML = assets.pages[name]
this.screen.classList[patternBg ? "add" : "remove"]("pattern-bg")
}
cssRuleset(rulesets){
var css = []
for(var selector in rulesets){
var declarationsObj = rulesets[selector]
var declarations = []
for(var property in declarationsObj){
var value = declarationsObj[property]
declarations.push("\t" + property + ": " + value + ";")
}
css.push(selector + "{\n" + declarations.join("\n") + "\n}")
}
return css.join("\n")
}
ajax(url, customRequest, customResponse){
var request = new XMLHttpRequest()
request.open("GET", url)
var promise = pageEvents.load(request)
if(!customResponse){
promise = promise.then(() => {
if(request.status === 200){
return request.response
}else{
return Promise.reject(`${url} (${request.status})`)
}
})
}
if(customRequest){
customRequest(request)
}
request.send()
return promise
}
loadScript(url){
var script = document.createElement("script")
var url = url + this.queryString
var promise = pageEvents.load(script)
script.src = url
document.head.appendChild(script)
return promise
}
getCsrfToken(){
return this.ajax("api/csrftoken").then(response => {
var json = JSON.parse(response)
if(json.status === "ok"){
return Promise.resolve(json.token)
}else{
return Promise.reject()
}
})
}
clean(error){
delete this.loaderDiv
delete this.loaderPercentage
delete this.loaderProgress
if(!error){
delete this.promises
delete this.errorText
}
pageEvents.remove(root, "touchstart")
}
}

397
public/src/js/loadsong.js Normal file
View File

@@ -0,0 +1,397 @@
class LoadSong{
constructor(...args){
this.init(...args)
}
init(selectedSong, autoPlayEnabled, multiplayer, touchEnabled){
this.selectedSong = selectedSong
this.autoPlayEnabled = autoPlayEnabled
this.multiplayer = multiplayer
this.touchEnabled = touchEnabled
var resolution = settings.getItem("resolution")
this.imgScale = 1
if(resolution === "medium"){
this.imgScale = 0.75
}else if(resolution === "low"){
this.imgScale = 0.5
}else if(resolution === "lowest"){
this.imgScale = 0.25
}
loader.changePage("loadsong", true)
var loadingText = document.getElementById("loading-text")
loadingText.appendChild(document.createTextNode(strings.loading))
loadingText.setAttribute("alt", strings.loading)
if(multiplayer){
var cancel = document.getElementById("p2-cancel-button")
cancel.appendChild(document.createTextNode(strings.cancel))
cancel.setAttribute("alt", strings.cancel)
}
this.run()
pageEvents.send("load-song", {
selectedSong: selectedSong,
autoPlayEnabled: autoPlayEnabled,
multiplayer: multiplayer,
touchEnabled: touchEnabled
})
}
run(){
var song = this.selectedSong
var id = song.folder
var songObj
this.promises = []
if(id !== "calibration"){
assets.sounds["v_start"].play()
assets.songs.forEach(song => {
if(song.id === id){
songObj = song
}else{
if(song.sound){
song.sound.clean()
delete song.sound
}
delete song.lyricsData
}
})
}else{
songObj = {
music: "muted",
custom: true
}
}
this.songObj = songObj
song.songBg = this.randInt(1, 5)
song.songStage = this.randInt(1, 3)
song.donBg = this.randInt(1, 6)
if(this.songObj && this.songObj.category_id === 9){
LoadSong.insertBackgroundVideo(this.songObj.id)
}
if(song.songSkin && song.songSkin.name){
var imgLoad = []
for(var type in song.songSkin){
var value = song.songSkin[type]
if(["song", "stage", "don"].indexOf(type) !== -1 && value && value !== "none"){
var filename = "bg_" + type + "_" + song.songSkin.name
if(value === "static"){
imgLoad.push({
filename: filename,
type: type
})
}else{
imgLoad.push({
filename: filename + "_a",
type: type
})
imgLoad.push({
filename: filename + "_b",
type: type
})
}
if(type === "don"){
song.donBg = null
}else if(type === "song"){
song.songBg = null
}else if(type === "stage"){
song.songStage = null
}
}
}
var skinBase = gameConfig.assets_baseurl + "song_skins/"
for(var i = 0; i < imgLoad.length; i++){
let filename = imgLoad[i].filename
let prefix = song.songSkin.prefix || ""
if((prefix + filename) in assets.image){
continue
}
let img = document.createElement("img")
let force = imgLoad[i].type === "song" && this.touchEnabled
if(!songObj.custom){
img.crossOrigin = "anonymous"
}
let promise = pageEvents.load(img)
this.addPromise(promise.then(() => {
return this.scaleImg(img, filename, prefix, force)
}), songObj.custom ? filename + ".png" : skinBase + filename + ".png")
if(songObj.custom){
this.addPromise(song.songSkin[filename + ".png"].blob().then(blob => {
img.src = URL.createObjectURL(blob)
}), song.songSkin[filename + ".png"].url)
}else{
img.src = skinBase + filename + ".png"
}
}
}
this.loadSongBg(id)
if(songObj.sound && songObj.sound.buffer){
songObj.sound.gain = snd.musicGain
}else if(songObj.music !== "muted"){
this.addPromise(snd.musicGain.load(songObj.music).then(sound => {
songObj.sound = sound
}), songObj.music.url)
}
var chart = songObj.chart
if(chart && chart.separateDiff){
var chartDiff = this.selectedSong.difficulty
chart = chart[chartDiff]
}
if(chart){
this.addPromise(chart.read(song.type === "tja" ? "utf-8" : "").then(data => {
this.songData = data.replace(/\0/g, "").split("\n")
}), chart.url)
}else{
this.songData = ""
}
if(songObj.lyricsFile && !songObj.lyricsData && !this.multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){
this.addPromise(songObj.lyricsFile.read().then(data => {
songObj.lyricsData = data
}, () => {}), songObj.lyricsFile.url)
}
if(this.touchEnabled && !assets.image["touch_drum"]){
let img = document.createElement("img")
img.crossOrigin = "anonymous"
var url = gameConfig.assets_baseurl + "img/touch_drum.png"
this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, "touch_drum", "")
}), url)
img.src = url
}
var resultsImg = [
"results_flowers",
"results_mikoshi",
"results_tetsuohana",
"results_tetsuohana2"
]
resultsImg.forEach(id => {
if(!assets.image[id]){
var img = document.createElement("img")
img.crossOrigin = "anonymous"
var url = gameConfig.assets_baseurl + "img/" + id + ".png"
this.addPromise(pageEvents.load(img).then(() => {
return this.scaleImg(img, id, "")
}), url)
img.src = url
}
})
if(songObj.volume && songObj.volume !== 1){
this.promises.push(new Promise(resolve => setTimeout(resolve, 500)))
}
Promise.all(this.promises).then(() => {
if(!this.error){
this.setupMultiplayer()
}
})
}
addPromise(promise, url){
this.promises.push(promise.catch(response => {
this.errorMsg(response, url)
return Promise.resolve()
}))
}
errorMsg(error, url){
if(!this.error){
if(url){
error = (Array.isArray(error) ? error[0] + ": " : (error ? error + ": " : "")) + url
}
pageEvents.send("load-song-error", error)
errorMessage(new Error(error).stack)
var title = this.selectedSong.title
if(title !== this.selectedSong.originalTitle){
title += " (" + this.selectedSong.originalTitle + ")"
}
assets.sounds["v_start"].stop()
setTimeout(() => {
this.clean()
new SongSelect(false, false, this.touchEnabled, null, {
name: "loadSongError",
title: title,
id: this.selectedSong.folder,
error: error
})
}, 500)
}
this.error = true
}
loadSongBg(){
var filenames = []
if(this.selectedSong.songBg !== null){
filenames.push("bg_song_" + this.selectedSong.songBg)
}
if(this.selectedSong.donBg !== null){
filenames.push("bg_don_" + this.selectedSong.donBg)
if(this.multiplayer){
filenames.push("bg_don2_" + this.selectedSong.donBg)
}
}
if(this.selectedSong.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
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
}
}
}
}
scaleImg(img, filename, prefix, force){
return new Promise((resolve, reject) => {
var scale = this.imgScale
if(force && scale > 0.5){
scale = 0.5
}
var canvas = document.createElement("canvas")
var w = Math.floor(img.width * scale)
var h = Math.floor(img.height * scale)
canvas.width = Math.max(1, w)
canvas.height = Math.max(1, h)
var ctx = canvas.getContext("2d")
ctx.drawImage(img, 0, 0, w, h)
var saveScaled = url => {
let img2 = document.createElement("img")
pageEvents.load(img2).then(() => {
assets.image[prefix + filename] = img2
loader.assetsDiv.appendChild(img2)
resolve()
}, reject)
img2.id = prefix + filename
img2.src = url
}
if("toBlob" in canvas){
canvas.toBlob(blob => {
saveScaled(URL.createObjectURL(blob))
})
}else{
saveScaled(canvas.toDataURL())
}
})
}
randInt(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min
}
setupMultiplayer(){
var song = this.selectedSong
if(this.multiplayer){
var loadingText = document.getElementsByClassName("loading-text")[0]
loadingText.firstChild.data = strings.waitingForP2
loadingText.setAttribute("alt", strings.waitingForP2)
this.cancelButton = document.getElementById("p2-cancel-button")
this.cancelButton.style.display = "inline-block"
pageEvents.add(this.cancelButton, ["mousedown", "touchstart"], this.cancelLoad.bind(this))
this.song2Data = this.songData
this.selectedSong2 = song
pageEvents.add(p2, "message", event => {
if(event.type === "gameload"){
this.cancelButton.style.display = ""
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.diff
var chart = this.songObj.chart
var chartDiff = this.selectedSong2.difficulty
if(song.type === "tja" || !chart || !chart.separateDiff || !chart[chartDiff]){
this.startMultiplayer()
}else{
chart[chartDiff].read(song.type === "tja" ? "utf-8" : "").then(data => {
this.song2Data = data.replace(/\0/g, "").split("\n")
}, () => {}).then(() => {
this.startMultiplayer()
})
}
}
}else if(event.type === "gamestart"){
this.clean()
p2.clearMessage("songsel")
var taikoGame1 = new Controller(song, this.songData, false, 1, this.touchEnabled)
var taikoGame2 = new Controller(this.selectedSong2, this.song2Data, true, 2, this.touchEnabled)
taikoGame1.run(taikoGame2)
pageEvents.send("load-song-player2", this.selectedSong2)
}else if(event.type === "left" || event.type === "gameend"){
this.clean()
new SongSelect(false, false, this.touchEnabled)
}
})
p2.send("join", {
id: song.folder,
diff: song.difficulty,
name: account.loggedIn ? account.displayName : null,
don: account.loggedIn ? account.don : null
})
}else{
this.clean()
var taikoGame = new Controller(song, this.songData, this.autoPlayEnabled, false, this.touchEnabled)
taikoGame.run()
}
}
startMultiplayer(repeat){
if(document.hasFocus()){
p2.send("gamestart")
}else{
if(!repeat){
assets.sounds["v_sanka"].play()
pageEvents.send("load-song-unfocused")
}
setTimeout(() => {
this.startMultiplayer(true)
}, 100)
}
}
cancelLoad(event){
if(event.type === "mousedown"){
if(event.which !== 1){
return
}
}else{
event.preventDefault()
}
p2.send("leave")
assets.sounds["se_don"].play()
this.cancelButton.style.pointerEvents = "none"
pageEvents.send("load-song-cancel")
}
clean(){
delete this.promises
delete this.songObj
delete this.videoElement
pageEvents.remove(p2, "message")
if(this.cancelButton){
pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"])
delete this.cancelButton
}
}
static insertBackgroundVideo(songId) {
const video = document.createElement("video");
video.src = `songs/${songId}/main.mp4`;
video.autoplay = true;
video.muted = true; // 可选:静音
video.style.objectFit = 'cover';
video.style.position = 'fixed';
video.style.top = "0";
video.style.left = "0";
video.style.zIndex = "0"; // 背景视频
video.style.width = "100vw";
video.style.height = "100vh";
document.body.appendChild(video);
window.videoElement = video;
}
}

193
public/src/js/logo.js Normal file
View File

@@ -0,0 +1,193 @@
class Logo{
constructor(...args){
this.init(...args)
}
init(){
this.canvas = document.getElementById("logo")
this.ctx = this.canvas.getContext("2d")
this.pathSvg = failedTests.indexOf("Path2D SVG") === -1 && vectors.logo1
this.symbolFont = "TnT, Meiryo, sans-serif"
this.symbols = [{
x: 315, y: 18, xAlt: 15, scale: true, text: "ブ",
path: new Path2D(vectors.logo5)
}, {
x: 267, y: 50, yAlt: -34, scale: true, text: "ェ",
path: new Path2D(vectors.logo4)
}, {
x: 197, y: 7, xAlt: 15, scale: true, text: "ウ",
path: new Path2D(vectors.logo3)
}, {
x: 87, y: 7, xAlt: 15, text: "鼓",
path: new Path2D(vectors.logo2),
shadow: new Path2D(vectors.logo2Shadow)
}, {
x: 22, y: 16, xAlt: 10, scaleAlt: true, text: "太",
path: new Path2D(vectors.logo1)
}]
pageEvents.add(window, "resize", this.update.bind(this))
}
updateSubtitle(){
this.subtitleGradient = ["#df600d", "#d8446f", "#b2147b", "#428ac2", "#1f9099"]
this.subtitle = []
this.subtitleW = 0
var index = 0
var latinLowercase = /[a-z]/
for(var i = 0; i < strings.taikoWeb.length; i++){
var letter = strings.taikoWeb[i]
var width = 57
if(letter === "ェ"){
width = 40
}else if(letter === " "){
width = 20
}else if(letter === "i"){
width = 22
}else if(letter === "T"){
width = 30
}else if(latinLowercase.test(letter)){
width = 38
}
this.subtitle.push({
letter: letter,
x: this.subtitleW + width / 2,
index: letter === " " ? index : index++
})
this.subtitleW += width
}
this.update()
}
update(){
var ctx = this.ctx
ctx.save()
this.width = 1170
this.height = 390
var pixelRatio = window.devicePixelRatio || 1
var winW = this.canvas.offsetWidth * pixelRatio
var winH = this.canvas.offsetHeight * pixelRatio
this.canvas.width = Math.max(1, winW)
this.canvas.height = Math.max(1, winH)
ctx.scale(winW / this.width, winH / this.height)
ctx.lineJoin = "round"
ctx.miterLimit = 1
ctx.textBaseline = "top"
ctx.textAlign = "center"
if(!this.pathSvg){
ctx.font = "100px " + this.symbolFont
}
for(var i = 0; i < this.symbols.length; i++){
ctx.strokeStyle = "#3f0406"
ctx.lineWidth = 13.5
this.drawSymbol(this.symbols[i], "stroke", 4)
}
ctx.font = this.bold(strings.font) + "55px " + strings.font
this.subtitleIterate((letter, x) => {
ctx.lineWidth = strings.id === "en" ? 19 : 18.5
ctx.strokeStyle = "#3f0406"
ctx.strokeText(letter, x, 315)
})
if(this.pathSvg){
ctx.fillStyle = "#3f0406"
ctx.fillRect(400, 180, 30, 50)
}else{
ctx.font = "100px " + this.symbolFont
}
for(var i = 0; i < this.symbols.length; i++){
var symbol = this.symbols[i]
ctx.strokeStyle = "#7c361e"
ctx.lineWidth = 13.5
this.drawSymbol(symbol, "stroke")
ctx.strokeStyle = "#fff"
ctx.lineWidth = 7.5
this.drawSymbol(symbol, "stroke")
if(this.pathSvg){
var grd = ctx.createLinearGradient(0, 55 - symbol.y, 0, 95 - symbol.y)
grd.addColorStop(0, "#a41f1e")
grd.addColorStop(1, "#a86a29")
ctx.fillStyle = grd
this.drawSymbol(symbol, "fill")
ctx.save()
ctx.scale(symbol.scale ? 2.8 : 3.2, 3.2)
ctx.translate(symbol.x, symbol.y)
ctx.clip(symbol.path)
}
grd = ctx.createLinearGradient(0, 55 - symbol.y, 0, 95 - symbol.y)
grd.addColorStop(0, "#d80e11")
grd.addColorStop(1, "#e08f19")
ctx.fillStyle = grd
if(this.pathSvg){
ctx.translate(3, 2)
ctx.fill(symbol.shadow || symbol.path)
ctx.restore()
}else{
this.drawSymbol(symbol, "fill")
}
}
if(this.pathSvg){
ctx.fillStyle = "#fff"
ctx.fillRect(382, 85, 30, 15)
ctx.fillRect(402, 145, 15, 15)
}else{
ctx.font = this.bold(strings.font) + "55px " + strings.font
}
this.subtitleIterate((letter, x) => {
ctx.lineWidth = strings.id === "en" ? 19 : 18.5
ctx.strokeStyle = "#7c361e"
ctx.strokeText(letter, x, 305)
})
this.subtitleIterate((letter, x, i) => {
ctx.lineWidth = strings.id === "en" ? 11 : 9.5
ctx.strokeStyle = this.getSubtitleGradient(i)
ctx.fillStyle = "#fff"
ctx.strokeText(letter, x, 305)
ctx.fillText(letter, x, 305)
})
ctx.restore()
}
drawSymbol(symbol, action, y){
var ctx = this.ctx
ctx.save()
ctx.scale((symbol.scale || !this.pathSvg && symbol.scaleAlt) ? 2.8 : 3.2, 3.2)
ctx.translate(symbol.x, symbol.y + (y || 0))
if(this.pathSvg){
ctx[action](symbol.path)
}else{
ctx[action + "Text"](symbol.text, 30 + (symbol.xAlt || 0), -4 + (symbol.yAlt || 0))
}
ctx.restore()
}
subtitleIterate(func){
for(var i = this.subtitle.length; i--;){
var subtitleObj = this.subtitle[i]
var x = (this.width - this.subtitleW) / 2 + subtitleObj.x
func(subtitleObj.letter, x, subtitleObj.index)
}
}
getSubtitleGradient(index){
var sign = 1
var position = 0
var length = this.subtitleGradient.length - 1
while(index >= 0){
if(sign === 1){
position = index % length
}else{
position = length - (index % length)
}
sign *= -1
index -= length
}
return this.subtitleGradient[position]
}
bold(font){
return font === "Microsoft YaHei, sans-serif" ? "bold " : ""
}
clean(){
pageEvents.remove(window, "resize")
delete this.symbols
delete this.ctx
delete this.canvas
}
}

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

@@ -0,0 +1,236 @@
class Lyrics{
constructor(...args){
this.init(...args)
}
init(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.preferEn && lang === "en" || 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.branch && this.branch && currentLine.branch !== this.branch)){
currentLine = this.lines[++this.current]
}
if(this.shown !== this.current){
if(currentLine && ms >= currentLine.start){
if(!currentLine.copy){
this.setText(currentLine.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
}
}

144
public/src/js/main.js Normal file
View File

@@ -0,0 +1,144 @@
addEventListener("error", function(err){
var stack
if("error" in err && err.error){
stack = err.error.stack
}else{
stack = err.message + "\n at " + err.filename + ":" + err.lineno + ":" + err.colno
}
errorMessage(stack)
})
function errorMessage(stack){
localStorage["lastError"] = JSON.stringify({
timestamp: Date.now(),
stack: stack
})
}
function toggleFullscreen(){
if("requestFullscreen" in root){
if(document.fullscreenElement){
document.exitFullscreen()
}else{
root.requestFullscreen()
}
}else if("webkitRequestFullscreen" in root){
if(document.webkitFullscreenElement){
document.webkitExitFullscreen()
}else{
root.webkitRequestFullscreen()
}
}else if("mozRequestFullScreen" in root){
if(document.mozFullScreenElement){
document.mozCancelFullScreen()
}else{
root.mozRequestFullScreen()
}
}
}
function resizeRoot(){
if((noResizeRoot ? lastWidth !== innerWidth : true) && lastHeight !== innerHeight){
lastWidth = innerWidth
lastHeight = innerHeight
root.style.height = innerHeight + "px"
}
}
function debug(){
if(debugObj.state === "open"){
debugObj.debug.clean()
return "Debug closed"
}else if(debugObj.state === "minimised"){
debugObj.debug.restore()
return "Debug restored"
}else{
debugObj.debug = new Debug()
return "Debug opened"
}
}
var root = document.documentElement
if(/iPhone|iPad/.test(navigator.userAgent)){
var fullScreenSupported = false
}else{
var fullScreenSupported = "requestFullscreen" in root || "webkitRequestFullscreen" in root || "mozRequestFullScreen" in root
}
var pageEvents = new PageEvents()
var snd = {}
var p2
var disableBlur = false
var cancelTouch = true
var lastWidth
var lastHeight
var debugObj = {
state: "closed",
debug: null
}
var perf = {
blur: 0,
allImg: 0,
load: 0
}
var defaultDon = {
body_fill: "#5fb7c1",
face_fill: "#ff5724"
}
var strings
var vectors
var settings
var scoreStorage
var account = {}
var gpicker
var db
var plugins
var noResizeRoot = false
var kanaPairs = [["っきゃ","ッキャ"],["っきゅ","ッキュ"],["っきょ","ッキョ"],["っしゃ","ッシャ"],["っしゅ","ッシュ"],["っしょ","ッショ"],["っちゃ","ッチャ"],["っちゅ","ッチュ"],["っちょ","ッチョ"],["っひゃ","ッヒャ"],["っひゅ","ッヒュ"],["っひょ","ッヒョ"],["っみゃ","ッミャ"],["っみゅ","ッミュ"],["っみょ","ッミョ"],["っりゃ","ッリャ"],["っりゅ","ッリュ"],["っりょ","ッリョ"],["っぎゃ","ッギャ"],["っぎゅ","ッギュ"],["っぎょ","ッギョ"],["っじゃ","ッジャ"],["っじゅ","ッジュ"],["っじょ","ッジョ"],["っびゃ","ッビャ"],["っびゅ","ッビュ"],["っびょ","ッビョ"],["っぴゃ","ッピャ"],["っぴゅ","ッピュ"],["っぴょ","ッピョ"],["っいぇ","ッイェ"],["っわぃ","ッウィ"],["っわぇ","ッウェ"],
["っわぉ","ッウォ"],["っゔぁ","ッヴァ"],["っゔぃ","ッヴィ"],["っゔぇ","ッヴェ"],["っゔぉ","ッヴォ"],["っすぃ","ッスィ"],["っずぃ","ッズィ"],["っしぇ","ッシェ"],["っじぇ","ッジェ"],["っとぃ","ッティ"],["っとぅ","ットゥ"],["っでぅ","ッディ"],["っどぅ","ッドゥ"],["っつぁ","ッツァ"],["っつぃ","ッツィ"],["っつぇ","ッツェ"],["っつぉ","ッツォ"],["っふぁ","ッファ"],["っふぃ","ッフィ"],["っふぇ","ッフェ"],["っふぉ","ッフォ"],["っふゅ","ッフュ"],["っひぇ","ッヒェ"],["きゃ","キャ"],["きゅ","キュ"],["きょ","キョ"],["しゃ","シャ"],["しゅ","シュ"],["しょ","ショ"],["ちゃ","チャ"],["ちゅ","チュ"],["ちょ","チョ"],["にゃ","ニャ"],["にゅ","ニュ"],["にょ","ニョ"],["ひゃ","ヒャ"],
["ひゅ","ヒュ"],["ひょ","ヒョ"],["みゃ","ミャ"],["みゅ","ミュ"],["みょ","ミョ"],["りゃ","リャ"],["りゅ","リュ"],["りょ","リョ"],["ぎゃ","ギャ"],["ぎゅ","ギュ"],["ぎょ","ギョ"],["じゃ","ジャ"],["じゅ","ジュ"],["じょ","ジョ"],["びゃ","ビャ"],["びゅ","ビュ"],["びょ","ビョ"],["ぴゃ","ピャ"],["ぴゅ","ピュ"],["ぴょ","ピョ"],["いぇ","イェ"],["わぃ","ウィ"],["わぇ","ウェ"],["わぉ","ウォ"],["ゔぁ","ヴァ"],["ゔぃ","ヴィ"],["ゔぇ","ヴェ"],["ゔぉ","ヴォ"],["すぃ","スィ"],["ずぃ","ズィ"],["しぇ","シェ"],["じぇ","ジェ"],["とぃ","ティ"],["とぅ","トゥ"],["でぅ","ディ"],["どぅ","ドゥ"],["つぁ","ツァ"],["つぃ","ツィ"],["つぇ","ツェ"],["つぉ","ツォ"],["ふぁ","ファ"],
["ふぃ","フィ"],["ふぇ","フェ"],["ふぉ","フォ"],["ふゅ","フュ"],["ひぇ","ヒェ"],["っか","ッカ"],["っき","ッキ"],["っく","ック"],["っけ","ッケ"],["っこ","ッコ"],["っさ","ッサ"],["っし","ッシ"],["っす","ッス"],["っせ","ッセ"],["っそ","ッソ"],["った","ッタ"],["っち","ッチ"],["っつ","ッツ"],["って","ッテ"],["っと","ット"],["っは","ッハ"],["っひ","ッヒ"],["っふ","ッフ"],["っへ","ッヘ"],["っほ","ッホ"],["っま","ッマ"],["っみ","ッミ"],["っむ","ッム"],["っめ","ッメ"],["っも","ッモ"],["っや","ッヤ"],["っゆ","ッユ"],["っよ","ッヨ"],["っら","ッラ"],["っり","ッリ"],["っる","ッル"],["っれ","ッレ"],["っろ","ッロ"],["っわ","ッワ"],["っゐ","ッヰ"],
["っゑ","ッヱ"],["っを","ッヲ"],["っが","ッガ"],["っぎ","ッギ"],["っぐ","ッグ"],["っげ","ッゲ"],["っご","ッゴ"],["っざ","ッザ"],["っじ","ッジ"],["っず","ッズ"],["っぜ","ッゼ"],["っぞ","ッゾ"],["っだ","ッダ"],["っぢ","ッヂ"],["っづ","ッヅ"],["っで","ッデ"],["っど","ッド"],["っば","ッバ"],["っび","ッビ"],["っぶ","ッブ"],["っべ","ッベ"],["っぼ","ッボ"],["っぱ","ッパ"],["っぴ","ッパ"],["っぷ","ップ"],["っぺ","ッペ"],["っぽ","ッポ"],["っゔ","ッヴ"],["あ","ア"],["い","イ"],["う","ウ"],["え","エ"],["お","オ"],["か","カ"],["き","キ"],["く","ク"],["け","ケ"],["こ","コ"],["さ","サ"],["し","シ"],["す","ス"],["せ","セ"],["そ","ソ"],["た","タ"],
["ち","チ"],["つ","ツ"],["て","テ"],["と","ト"],["な","ナ"],["に","ニ"],["ぬ","ヌ"],["ね","ネ"],["の",""],["は","ハ"],["ひ","ヒ"],["ふ","フ"],["へ","ヘ"],["ほ","ホ"],["ま","マ"],["み","ミ"],["む","ム"],["め","メ"],["も","モ"],["や","ヤ"],["ゆ","ユ"],["よ","ヨ"],["ら","ラ"],["り","リ"],["る","ル"],["れ","レ"],["ろ","ロ"],["わ","ワ"],["ゐ","ヰ"],["ゑ","ヱ"],["を","ヲ"],["ん","ン"],["が","ガ"],["ぎ","ギ"],["ぐ","グ"],["げ","ゲ"],["ご","ゴ"],["ざ","ザ"],["じ","ジ"],["ず","ズ"],["ぜ","ゼ"],["ぞ","ゾ"],["だ","ダ"],["ぢ","ヂ"],["づ","ヅ"],["で","デ"],["ど","ド"],
["ば","バ"],["び","ビ"],["ぶ","ブ"],["べ","ベ"],["ぼ","ボ"],["ぱ","パ"],["ぴ","パ"],["ぷ","プ"],["ぺ","ペ"],["ぽ","ポ"],["ゔ","ヴ"]]
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT" && (event.target.tagName !== "INPUT" || event.target.type !== "file")){
event.preventDefault()
}
})
var versionDiv = document.getElementById("version")
var versionLink = document.getElementById("version-link")
versionLink.tabIndex = -1
pageEvents.add(versionDiv, ["click", "touchend"], event => {
if(event.target === versionDiv){
versionLink.click()
pageEvents.send("version-link")
}
})
resizeRoot()
setInterval(resizeRoot, 100)
pageEvents.keyAdd(debugObj, "all", "down", event => {
if((event.keyCode === 186 || event.keyCode === 59) && event.ctrlKey && (event.shiftKey || event.altKey)){
// Semicolon
if(debugObj.state === "open"){
debugObj.debug.minimise()
}else if(debugObj.state === "minimised"){
debugObj.debug.restore()
}else{
try{
debugObj.debug = new Debug()
}catch(e){}
}
}
if(event.keyCode === 82 && debugObj.debug && debugObj.controller){
// R
debugObj.controller.restartSong()
}
})
var loader = new Loader(songId => {
new Titlescreen(songId)
})

119
public/src/js/mekadon.js Normal file
View File

@@ -0,0 +1,119 @@
class Mekadon{
constructor(...args){
this.init(...args)
}
init(controller, game){
this.controller = controller
this.game = game
this.lr = false
this.lastHit = -Infinity
this.delay = controller.audioLatency
}
play(circle){
var type = circle.type
if((type === "balloon" || type === "drumroll" || type === "daiDrumroll") && this.getMS() > circle.endTime){
if(circle.section && circle.timesHit === 0){
this.game.resetSection()
}
circle.played(-1, false)
this.game.updateCurrentCircle()
}
type = circle.type
if(type === "balloon"){
return this.playDrumrollAt(circle, 0, 30)
}else if(type === "drumroll" || type === "daiDrumroll"){
return this.playDrumrollAt(circle, 0, 60)
}else{
return this.playAt(circle, 0, 450)
}
}
playAt(circle, ms, score, dai, reverse){
var currentMs = circle.ms - this.getMS() + this.delay
if(ms > currentMs - 10){
return this.playNow(circle, score, dai, reverse)
}
}
playDrumrollAt(circle, ms, pace, kaAmount){
if(pace && this.getMS() >= this.lastHit + pace){
var score = 1
if(kaAmount > 0){
score = Math.random() > kaAmount ? 1 : 2
}
return this.playAt(circle, ms, score)
}
}
miss(circle){
var currentMs = circle.ms - this.getMS()
if(0 >= currentMs - 10){
this.controller.displayScore(0, true)
this.game.updateCurrentCircle()
this.game.updateCombo(0)
this.game.updateGlobalScore(0, 1, circle.gogoTime)
this.game.sectionNotes.push(0)
return true
}
}
playNow(circle, score, dai, reverse){
var type = circle.type
var keyDai = false
var playDai = !dai || dai === 2
var drumrollNotes = type === "balloon" || type === "drumroll" || type === "daiDrumroll"
if(drumrollNotes){
var ms = this.getMS()
}else{
var ms = circle.ms
}
if(reverse){
if(type === "don" || type === "daiDon"){
type = "ka"
}else if(type === "ka" || type === "daiKa"){
type = "don"
}
}
if(type === "daiDon" && playDai){
this.setKey("don_l", ms)
this.setKey("don_r", ms)
this.lr = false
keyDai = true
}else if(type === "don" || type === "daiDon" || drumrollNotes && score !== 2){
this.setKey(this.lr ? "don_l" : "don_r", ms)
this.lr = !this.lr
}else if(type === "daiKa" && playDai){
this.setKey("ka_l", ms)
this.setKey("ka_r", ms)
this.lr = false
keyDai = true
}else if(type === "ka" || type === "daiKa" || drumrollNotes){
this.setKey(this.lr ? "ka_l" : "ka_r", ms)
this.lr = !this.lr
}
if(type === "balloon"){
if(circle.requiredHits === 1){
assets.sounds["se_balloon"].play()
}
this.game.checkBalloon(circle)
}else if(type === "drumroll" || type === "daiDrumroll"){
this.game.checkDrumroll(circle, score === 2)
}else{
this.controller.displayScore(score, false, keyDai)
this.game.updateCombo(score)
this.game.updateGlobalScore(score, keyDai ? 2 : 1, circle.gogoTime)
this.game.updateCurrentCircle()
circle.played(score, keyDai)
if(circle.section){
this.game.resetSection()
}
this.game.sectionNotes.push(score === 450 ? 1 : (score === 230 ? 0.5 : 0))
}
this.lastHit = ms
return true
}
getMS(){
return this.controller.getElapsedTime()
}
setKey(name, ms){
this.controller.setKey(true, name, ms)
}
}

268
public/src/js/p2.js Normal file
View File

@@ -0,0 +1,268 @@
class P2Connection{
constructor(...args){
this.init(...args)
}
init(){
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 = ""
this.disabled = 0
pageEvents.add(window, "hashchange", this.onhashchange.bind(this))
}
addEventListener(type, callback){
var addedType = this.allEvents.get(type)
if(!addedType){
addedType = new Set()
this.allEvents.set(type, addedType)
}
return addedType.add(callback)
}
removeEventListener(type, callback){
var addedType = this.allEvents.get(type)
if(addedType){
return addedType.delete(callback)
}
}
open(){
if(this.closed && !this.disabled){
this.closed = false
var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:"
this.socket = new WebSocket(gameConfig.multiplayer_url ? gameConfig.multiplayer_url : wsProtocol + "//" + location.host + location.pathname + "p2")
pageEvents.race(this.socket, "open", "close").then(response => {
if(response.type === "open"){
return this.openEvent()
}
return this.closeEvent()
})
pageEvents.add(this.socket, "message", this.messageEvent.bind(this))
}
}
openEvent(){
var addedType = this.allEvents.get("open")
if(addedType){
addedType.forEach(callback => callback())
}
}
close(){
if(!this.closed){
this.closed = true
if(this.socket){
this.socket.close()
}
}
}
closeEvent(){
this.removeEventListener(onmessage)
this.otherConnected = false
this.session = false
if(this.hashLock){
this.hash("")
this.hashLock = false
}
if(!this.closed){
setTimeout(() => {
if(this.socket.readyState !== this.socket.OPEN){
this.open()
}
}, 500)
pageEvents.send("p2-disconnected")
}
var addedType = this.allEvents.get("close")
if(addedType){
addedType.forEach(callback => callback())
}
}
send(type, value){
if(this.socket.readyState === this.socket.OPEN){
if(typeof value === "undefined"){
this.socket.send(JSON.stringify({type: type}))
}else{
this.socket.send(JSON.stringify({type: type, value: value}))
}
}else{
pageEvents.once(this, "open").then(() => {
this.send(type, value)
})
}
}
messageEvent(event){
try{
var response = JSON.parse(event.data)
}catch(e){
var response = {}
}
this.lastMessages[response.type] = response
var addedType = this.allEvents.get("message")
if(addedType){
addedType.forEach(callback => callback(response))
}
}
getMessage(type){
if(type in this.lastMessages){
return this.lastMessages[type]
}
}
clearMessage(type){
if(type in this.lastMessages){
this.lastMessages[type] = null
}
}
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 = []
this.drumrollPace = 45
this.dai = 2
this.kaAmount = 0
this.results = false
this.branch = "normal"
scoreStorage.clearP2()
break
case "gameend":
this.otherConnected = false
if(this.session){
pageEvents.send("session-end")
}else if(!this.results){
pageEvents.send("p2-game-end")
}
this.session = false
if(this.hashLock){
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] === null ? null : response.value[i].toString()
}
break
case "note":
this.notes.push(response.value)
if(response.value.dai){
this.dai = response.value.dai
}
break
case "drumroll":
this.drumrollPace = response.value.pace
if("kaAmount" in response.value){
this.kaAmount = response.value.kaAmount
}
break
case "branch":
this.branch = response.value
this.branchSet = false
break
case "session":
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
}
}
onhashchange(){
if(this.hashLock){
this.hash(this.currentHash)
}else{
location.reload()
}
}
hash(string){
this.currentHash = string
history.replaceState("", "", location.pathname + (string ? "#" + string : ""))
}
play(circle, mekadon){
if(this.otherConnected || this.notes.length > 0){
var type = circle.type
var drumrollNotes = type === "balloon" || type === "drumroll" || type === "daiDrumroll"
if(drumrollNotes && mekadon.getMS() > circle.endTime + mekadon.delay){
circle.played(-1, false)
mekadon.game.updateCurrentCircle()
}
if(drumrollNotes){
mekadon.playDrumrollAt(circle, 0, this.drumrollPace, type === "drumroll" || type === "daiDrumroll" ? this.kaAmount : 0)
}else if(this.notes.length === 0){
mekadon.play(circle)
}else{
var note = this.notes[0]
if(note.score >= 0){
var dai = 1
if(circle.type === "daiDon" || circle.type === "daiKa"){
dai = this.dai
}
if(mekadon.playAt(circle, note.ms, note.score, dai, note.reverse)){
this.notes.shift()
}
}else{
if(mekadon.miss(circle)){
this.notes.shift()
}
}
}
}else if(mekadon.miss(circle)){
this.notes.shift()
}
}
enable(){
this.disabled = Math.max(0, this.disabled - 1)
setTimeout(this.open.bind(this), 100)
}
disable(){
this.disabled++
this.close()
}
}

182
public/src/js/pageevents.js Normal file
View File

@@ -0,0 +1,182 @@
class PageEvents{
constructor(...args){
this.init(...args)
}
init(){
this.allEvents = new Map()
this.keyListeners = new Map()
this.mouseListeners = new Map()
this.blurListeners = new Map()
this.lastKeyEvent = -Infinity
this.add(window, "keydown", this.keyEvent.bind(this))
this.add(window, "keyup", this.keyEvent.bind(this))
this.add(window, "mousemove", this.mouseEvent.bind(this))
this.add(window, "blur", this.blurEvent.bind(this))
this.kbd = []
}
add(target, type, callback, symbol){
if(Array.isArray(type)){
type.forEach(type => this.add(target, type, callback, symbol))
return
}
this.remove(target, type)
var addedEvent = this.allEvents.get(symbol || target)
if(!addedEvent){
addedEvent = new Map()
this.allEvents.set(symbol || target, addedEvent)
}
addedEvent.set(type, callback)
return target.addEventListener(type, callback)
}
remove(target, type, symbol){
if(Array.isArray(type)){
type.forEach(type => this.remove(target, type, symbol))
return
}
var addedEvent = this.allEvents.get(symbol || target)
if(addedEvent){
var callback = addedEvent.get(type)
if(callback){
target.removeEventListener(type, callback)
addedEvent.delete(type)
if(addedEvent.size == 0){
return this.allEvents.delete(symbol || target)
}
}
}
}
once(target, type, symbol){
return new Promise(resolve => {
this.add(target, type, event => {
this.remove(target, type)
return resolve(event)
}, symbol)
})
}
race(){
var symbols = []
var target = arguments[0]
return new Promise(resolve => {
for(var i = 1;i < arguments.length; i++){
symbols[i] = Symbol()
let type = arguments[i]
this.add(target, type, event => {
resolve({
type: type,
event: event
})
}, symbols[i])
}
}).then(response => {
for(var i = 1;i < arguments.length; i++){
this.remove(target, arguments[i], symbols[i])
}
return response
})
}
load(target){
return new Promise((resolve, reject) => {
this.race(target, "load", "error", "abort").then(response => {
switch(response.type){
case "load":
return resolve(response.event)
case "error":
return reject(["Loading error", target])
case "abort":
return reject("Loading aborted")
}
})
})
}
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()
if(event.target.tagName !== "INPUT"){
event.preventDefault()
}
}
this.keyListeners.forEach(addedKeyCode => {
this.checkListener(addedKeyCode.get("all"), event)
this.checkListener(addedKeyCode.get(event.keyCode), event)
})
}
checkListener(keyObj, event){
if(keyObj && (
keyObj.type === "both"
|| keyObj.type === "down" && event.type === "keydown"
|| keyObj.type === "up" && event.type === "up"
)){
keyObj.callback(event)
}
}
keyAdd(target, keyCode, type, callback){
// keyCode="all", type="both"
var addedKeyCode = this.keyListeners.get(target)
if(!addedKeyCode){
addedKeyCode = new Map()
this.keyListeners.set(target, addedKeyCode)
}
addedKeyCode.set(keyCode, {
type: type,
callback: callback
})
}
keyRemove(target, keyCode){
var addedKeyCode = this.keyListeners.get(target)
if(addedKeyCode){
var keyObj = addedKeyCode.get(keyCode)
if(keyObj){
addedKeyCode.delete(keyCode)
if(addedKeyCode.size == 0){
return this.keyListeners.delete(target)
}
}
}
}
keyOnce(target, keyCode, type){
return new Promise(resolve => {
this.keyAdd(target, keyCode, type, event => {
this.keyRemove(target, keyCode)
return resolve(event)
})
})
}
mouseEvent(event){
this.lastMouse = event
this.mouseListeners.forEach(callback => callback(event))
}
mouseAdd(target, callback){
this.mouseListeners.set(target, callback)
}
mouseRemove(target){
this.mouseListeners.delete(target)
}
blurEvent(event){
this.blurListeners.forEach(callback => callback(event))
}
blurAdd(target, callback){
this.blurListeners.set(target, callback)
}
blurRemove(target){
this.blurListeners.delete(target)
}
getMouse(){
return this.lastMouse
}
send(name, detail){
dispatchEvent(new CustomEvent(name, {detail: detail}))
}
setKbd(){
this.kbd = []
var kbdSettings = settings.getItem("keyboardSettings")
for(var name in kbdSettings){
var keys = kbdSettings[name]
for(var i in keys){
this.kbd.push(keys[i])
}
}
}
}

381
public/src/js/parseosu.js Normal file
View File

@@ -0,0 +1,381 @@
class ParseOsu{
constructor(...args){
this.init(...args)
}
init(fileContent, difficulty, stars, offset, metaOnly){
this.osu = {
OFFSET: 0,
MSPERBEAT: 1,
METER: 2,
SAMPLESET: 3,
SAMPLEINDEX: 4,
VOLUME: 5,
INHERITED: 6,
KIAIMODE: 7,
X: 0,
Y: 1,
TIME: 2,
TYPE: 3,
HITSOUND: 4,
EXTRAS: 5,
ENDTIME: 5,
CIRCLE: 1,
SLIDER: 2,
NEWCOMBO: 4,
SPINNER: 8,
NORMAL: 1,
WHISTLE: 2,
FINISH: 4,
CLAP: 8,
CURVEPOINTS: 0,
REPEAT: 1,
PIXELLENGTH: 2,
EDGEHITSOUNDS: 3,
EDGEADDITIONS: 4
}
this.data = []
for(let line of fileContent){
line = line.replace(/\/\/.*/, "").trim()
if(line !== ""){
this.data.push(line)
}
}
this.offset = (offset || 0) * -1000
this.soundOffset = 0
this.beatInfo = {
beatInterval: 0,
lastBeatInterval: 0,
bpm: 0
}
this.events = []
this.generalInfo = this.parseGeneralInfo()
this.metadata = this.parseMetadata()
this.editor = this.parseEditor()
this.difficulty = this.parseDifficulty()
this._difficulty = difficulty;
this.stars = stars
if(!metaOnly){
this.timingPoints = this.parseTiming()
this.circles = this.parseCircles()
this.measures = this.parseMeasures()
}
}
getStartEndIndexes(type){
var indexes = {
start: 0,
end: 0
}
while(indexes.start < this.data.length){
if(this.data[indexes.start] === "[" + type + "]"){
break
}
indexes.start++
}
indexes.start++
indexes.end = indexes.start
while(indexes.end < this.data.length){
if(this.data[indexes.end].match(/^\[\w+\]$/)){
break
}
indexes.end++
}
indexes.end--
return indexes
}
parseDifficulty(){
var difficulty = {
sliderMultiplier: 0,
sliderTickRate: 0,
approachRate: 0
}
var indexes = this.getStartEndIndexes("Difficulty")
for(var i = indexes.start; i <= indexes.end; i++){
var [item, key] = this.data[i].split(":")
switch(item){
case "SliderMultiplier":
difficulty.sliderMultiplier = key
difficulty.originalMultiplier = key
break
case "SliderTickRate":
difficulty.sliderTickRate = key
break
case "ApproachRate":
difficulty.approachRate = key
break
case "OverallDifficulty":
difficulty.overallDifficulty = key
break
}
}
return difficulty
}
parseTiming(){
var timingPoints = []
var indexes = this.getStartEndIndexes("TimingPoints")
var lastBeatInterval = parseInt(this.data[indexes.start].split(",")[1])
for(var i = indexes.start; i <= indexes.end; i++){
var values = this.data[i].split(",")
var start = parseInt(values[this.osu.OFFSET])
var msOrPercent = parseFloat(values[this.osu.MSPERBEAT])
if(i == indexes.start){
this.beatInfo.beatInterval = msOrPercent
this.beatInfo.bpm = Math.floor(1000 / this.beatInfo.beatInterval * 60)
}
var beatReset = false
if(msOrPercent < 0){
var sliderMultiplier = this.difficulty.lastMultiplier / Math.abs(msOrPercent / 100)
}else{
var sliderMultiplier = 1000 / msOrPercent
if(i == 0){
this.difficulty.originalMultiplier = sliderMultiplier
}
this.difficulty.lastMultiplier = sliderMultiplier
beatReset = true
}
timingPoints.push({
start: start + this.offset,
sliderMultiplier: sliderMultiplier,
measure: parseInt(values[this.osu.METER]),
gogoTime: parseInt(values[this.osu.KIAIMODE]),
beatMS: 1000 / this.difficulty.lastMultiplier,
beatReset: beatReset
})
}
return timingPoints
}
parseMeasures(){
var measures = []
for(var i = 0; i < this.timingPoints.length; i++){
var currentTiming = this.timingPoints[i]
var firstTiming = i === 0
var limit = this.circles[this.circles.length - 1].endTime + currentTiming.beatMS
for(var j = i + 1; j < this.timingPoints.length; j++){
var nextTiming = this.timingPoints[j]
var newLimit = nextTiming.start
if(nextTiming.measure !== currentTiming.measure || nextTiming.beatReset){
limit = newLimit - currentTiming.beatMS
break
}
i = j
}
var start = currentTiming.start
var interval = currentTiming.beatMS * currentTiming.measure
if(firstTiming){
while(start >= interval){
start -= interval
}
}
for(var ms = start; ms <= limit; ms += interval){
var speed = currentTiming.sliderMultiplier
for(var j = 0; j < this.timingPoints.length; j++){
var timingPoint = this.timingPoints[j]
if(j !== 0 && timingPoint.start - this.offset > ms){
break
}
speed = timingPoint.sliderMultiplier
}
measures.push({
ms: ms,
originalMS: ms,
speed: speed,
visible: true
})
}
}
return measures
}
parseGeneralInfo(){
var generalInfo = {}
var indexes = this.getStartEndIndexes("General")
for(var i = indexes.start; i<= indexes.end; i++){
var [item, key] = this.data[i].split(":")
generalInfo[item] = key.trim()
}
return generalInfo
}
parseMetadata(){
var metadata = {}
var indexes = this.getStartEndIndexes("Metadata")
for(var i = indexes.start; i <= indexes.end; i++){
var [item, key] = this.data[i].split(":")
metadata[item] = key.trim()
}
return metadata
}
parseEditor(){
var editor = {
distanceSpacing: 0,
beatDivisor: 0,
gridSize: 0
}
var indexes = this.getStartEndIndexes("Editor")
for(var i = indexes.start; i <= indexes.end; i++){
var [item, key] = this.data[i].split(":")
switch(item){
case "DistanceSpacing":
editor.distanceSpacing = parseFloat(key)
break
case "BeatDivisor":
editor.beatDivisor = parseInt(key)
break
case "GridSize":
editor.gridSize = parseInt(key)
break
}
}
return editor
}
difficultyRange(difficulty, min, mid, max){
if(difficulty > 5){
return mid + (max - mid) * (difficulty - 5) / 5
}
if(difficulty < 5){
return mid - (mid - min) * (5 - difficulty) / 5
}
return mid
}
parseCircles(){
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(",")
var emptyValue = false
var start = parseInt(values[this.osu.TIME])
var speed = this.difficulty.originalMultiplier
var gogoTime = false
var osuType = parseInt(values[this.osu.TYPE])
var hitSound = parseInt(values[this.osu.HITSOUND])
var beatLength = speed
var lastMultiplier = this.difficulty.lastMultiplier
var beatMS = this.beatInfo.beatInterval
if(circleID === 1 && start + this.offset < 0){
var offset = start + this.offset
this.soundOffset = offset
this.offset -= offset
}
for(var j = 0; j < this.timingPoints.length; j++){
var timingPoint = this.timingPoints[j]
if(j !== 0 && timingPoint.start - this.offset > start){
break
}
speed = timingPoint.sliderMultiplier
gogoTime = timingPoint.gogoTime
beatMS = timingPoint.beatMS
}
if(osuType & this.osu.SPINNER){
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))
pushCircle(new Circle({
id: circleID,
start: start + this.offset,
type: "balloon",
txt: strings.note.balloon,
speed: speed,
endTime: endTime + this.offset,
requiredHits: requiredHits,
gogoTime: gogoTime,
beatMS: beatMS
}))
}else if(osuType & this.osu.SLIDER){
var extras = values.slice(this.osu.EXTRAS)
var distance = parseFloat(extras[this.osu.PIXELLENGTH]) * parseFloat(extras[this.osu.REPEAT])
var velocity = this.difficulty.sliderMultiplier * speed / 10
var endTime = start + distance / velocity
if(hitSound & this.osu.FINISH){
type = "daiDrumroll"
txt = strings.note.daiDrumroll
}else{
type = "drumroll"
txt = strings.note.drumroll
}
pushCircle(new Circle({
id: circleID,
start: start + this.offset,
type: type,
txt: txt,
speed: speed,
endTime: endTime + this.offset,
gogoTime: gogoTime,
beatMS: beatMS
}))
}else if(osuType & this.osu.CIRCLE){
var type
var txt
if(hitSound & this.osu.FINISH){
if(hitSound & this.osu.WHISTLE || hitSound & this.osu.CLAP){
type = "daiKa"
txt = strings.note.daiKa
}else if(hitSound & this.osu.NORMAL || hitSound === this.osu.FINISH){
type = "daiDon"
txt = strings.note.daiDon
}else{
emptyValue = true
}
}else if(hitSound & this.osu.WHISTLE || hitSound & this.osu.CLAP){
type = "ka"
txt = strings.note.ka
}else if(hitSound & this.osu.NORMAL || hitSound === 0){
type = "don"
txt = strings.note.don
}else{
emptyValue = true
}
if(!emptyValue){
pushCircle(new Circle({
id: circleID,
start: start + this.offset,
type: type,
txt: txt,
speed: speed,
gogoTime: gogoTime,
beatMS: beatMS
}))
}
}else{
emptyValue = true
}
if(emptyValue){
console.warn("Unknown note type found on line " + (i + 1) + ": " + this.data[i])
}
}
this.scoremode = 2;
var autoscore = new AutoScore(this._difficulty, this.stars, 2, circles);
this.scoreinit = autoscore.ScoreInit;
this.scorediff = autoscore.ScoreDiff;
return circles
}
}

702
public/src/js/parsetja.js Normal file
View File

@@ -0,0 +1,702 @@
class ParseTja{
constructor(...args){
this.init(...args)
}
init(file, difficulty, stars, offset, metaOnly){
this.data = []
for(let line of file){
var indexComment = line.indexOf("//")
if(indexComment !== -1 && !line.trim().toLowerCase().startsWith("maker:")){
line = line.slice(0, indexComment).trim()
}else{
line = line.trim()
}
if(line !== ""){
this.data.push(line)
}
}
this.difficulty = difficulty
this.stars = stars
this.offset = (offset || 0) * -1000
this.soundOffset = 0
this.noteTypes = {
"0": {name: false, txt: false},
"1": {name: "don", txt: strings.note.don},
"2": {name: "ka", txt: strings.note.ka},
"3": {name: "daiDon", txt: strings.note.daiDon},
"4": {name: "daiKa", txt: strings.note.daiKa},
"5": {name: "drumroll", txt: strings.note.drumroll},
"6": {name: "daiDrumroll", txt: strings.note.daiDrumroll},
"7": {name: "balloon", txt: strings.note.balloon},
"8": {name: false, txt: false},
"9": {name: "balloon", txt: strings.note.balloon},
"A": {name: "daiDon", txt: strings.note.daiDon},
"B": {name: "daiKa", txt: strings.note.daiKa}
}
this.noteTypes_ex = strings.ex_note;
this.courseTypes = {
"0": "easy",
"1": "normal",
"2": "hard",
"3": "oni",
"4": "ura",
"edit": "ura"
}
this.metadata = this.parseMetadata()
this.measures = []
this.beatInfo = {}
this.events = []
if(!metaOnly){
this.circles = this.parseCircles(difficulty)
}
}
parseMetadata(){
var metaNumbers = ["bpm", "offset", "demostart", "level", "scoremode", "scorediff"]
var inSong = false
var hasSong = false
var courses = {}
var currentCourse = {}
var courseName = "oni"
for(var lineNum = 0; lineNum < this.data.length; lineNum++){
var line = this.data[lineNum]
if(line.slice(0, 1) === "#"){
var name = line.slice(1).toLowerCase()
if((name === "start" || name === "start p1") && !inSong){
inSong = true
if(!hasSong || name === "start" && courses[courseName] && courses[courseName].startName !== "start"){
hasSong = false
if(!(courseName in courses)){
courses[courseName] = {}
}
courses[courseName].startName = name
for(var opt in currentCourse){
if(opt !== "branch"){
courses[courseName][opt] = currentCourse[opt]
}
}
courses[courseName].start = lineNum + 1
courses[courseName].end = this.data.length
}
}else if(name === "end" && inSong){
inSong = false
if(!hasSong){
hasSong = true
courses[courseName].end = lineNum
}
}else if(name.startsWith("branchstart") && inSong){
courses[courseName].branch = true
}else if(name.startsWith("lyric") && inSong){
courses[courseName].inlineLyrics = true
}
}else if(!inSong){
if(line.indexOf(":") > 0){
var [name, value] = this.split(line, ":")
name = name.toLowerCase().trim()
value = value.trim()
if(name === "course"){
value = value.toLowerCase()
if(value in this.courseTypes){
courseName = this.courseTypes[value]
}else{
courseName = value
}
hasSong = false
}else if(name === "balloon"){
value = value ? value.split(",").map(digit => parseInt(digit)) : []
}else if(this.inArray(name, metaNumbers)){
value = parseFloat(value)
}
else if (name === "scoreinit") {
value = value ? parseFloat(value.split(",")[0]) : 0;
}
currentCourse[name] = value
}
}
}
return courses
}
inArray(string, array){
return array.indexOf(string) >= 0
}
split(string, delimiter){
var index = string.indexOf(delimiter)
if(index < 0){
return [string, ""]
}
return [string.slice(0, index), string.slice(index + delimiter.length)]
}
parseCircles(difficulty, lyricsOnly){
var meta = this.metadata[difficulty] || {}
var ms = (meta.offset || 0) * -1000 + this.offset
var bpm = Math.abs(meta.bpm) || 120
var scroll = 1
var measure = 4
if(!lyricsOnly){
this.beatInfo.beatInterval = 60000 / bpm
}
var gogo = false
var barLine = true
var balloonID = 0
var balloons = meta.balloon || []
var lastDrumroll = false
var branches
var branch = false
var branchObj = {}
var currentBranch = false
var branchSettings = {}
var branchFirstMeasure = false
var sectionBegin = true
var lastBpm = bpm
var lastGogo = gogo
var lyrics
var lyricsIndex = null
var lyricsLine = null
var lyricsCopy = false
var measures = []
var currentMeasure = []
var firstNote = true
var circles = []
var circleID = 0
var events = []
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];
if (note && note.type !== "don" && note.type !== "daiDon") {
return false;
}
}
return true;
}
var checkChain = (note_chain, measure_length, is_last) => {
//console.log(note_chain, measure_length, is_last);
/*if (measure_length >= 24) {
for (var note of note_chain) {
note.text = this.noteTypes_ex[note.type][0];
}
} else { */
var alldon_pos = null;
for (var i = 0; i < note_chain.length - (is_last ? 1 : 0); ++i) {
var note = note_chain[i];
if (alldon_pos === null && is_last && isAllDon(note_chain, i)) {
alldon_pos = i;
}
note.text = this.noteTypes_ex[note.type][alldon_pos != null ? (i - alldon_pos) % 2 : 0];
}
//}
}
var pushMeasure = () => {
var note = currentMeasure[0]
if(note){
var speed = note.bpm * note.scroll / 60
}else{
var speed = bpm * scroll / 60
}
if(!lyricsOnly){
measures.push({
ms: ms,
originalMS: ms,
speed: speed,
visible: barLine,
branch: currentBranch,
branchFirst: branchFirstMeasure
})
}
branchFirstMeasure = false
if(currentMeasure.length){
for(var i = 0; i < currentMeasure.length; i++){
var note = currentMeasure[i]
if(firstNote && note.type && note.type !== "event"){
firstNote = false
if(ms < 0){
this.soundOffset = ms
ms = 0
}
}
note.start = ms
if(note.endDrumroll){
note.endDrumroll.endTime = ms
note.endDrumroll.originalEndTime = ms
}
var msPerMeasure = 60000 * measure / note.bpm
ms += msPerMeasure / currentMeasure.length
}
var note_chain = [];
for (var i = 0; i < currentMeasure.length; i++){
var note = currentMeasure[i]
if(!lyricsOnly){
circleID++
var circleObj = new Circle({
id: circleID,
start: note.start,
type: note.type,
txt: note.txt,
speed: note.bpm * note.scroll / 60,
gogoTime: note.gogo,
endTime: note.endTime,
requiredHits: note.requiredHits,
beatMS: 60000 / note.bpm,
branch: currentBranch,
section: note.section
})
if(note.type){
if(note.type === "don" || note.type === "ka" || note.type === "daiDon" || note.type === "daiKa"){
note_chain.push(circleObj)
}else{
if(note_chain.length > 1 && currentMeasure.length >= 8){
checkChain(note_chain, currentMeasure.length, false)
}
note_chain = []
}
if (lastDrumroll === note) {
lastDrumroll = 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){
checkChain(note_chain, currentMeasure.length, true)
}
note_chain = []
}
if(note.event){
events.push(circleObj)
}
}
var lyricsObj = null
if("lyricsLine" in note){
lyricsObj = {
start: note.start,
text: note.lyricsLine
}
}else if(note.lyricsCopy){
lyricsObj = {
start: note.start,
copy: true
}
}
if(lyricsObj){
if(currentBranch){
lyricsObj.branch = currentBranch.name
}
insertLyrics(lyricsObj)
}
}
if(!lyricsOnly && note_chain.length > 1 && currentMeasure.length >= 8){
checkChain(note_chain, currentMeasure.length, false)
}
}else{
var msPerMeasure = 60000 * measure / bpm
ms += msPerMeasure
}
}
var insertNote = circleObj => {
if(circleObj){
if(bpm !== lastBpm || gogo !== lastGogo){
circleObj.event = true
lastBpm = bpm
lastGogo = gogo
}
if(lyricsLine !== null){
circleObj.lyricsLine = lyricsLine
lyricsLine = null
}else if(lyricsCopy){
circleObj.lyricsCopy = true
}
lyricsCopy = false
currentMeasure.push(circleObj)
}
}
var insertBlankNote = circleObj => {
if(bpm !== lastBpm || gogo !== lastGogo){
insertNote({
type: "event",
bpm: bpm,
scroll: scroll,
gogo: gogo
})
}else if(!circleObj){
var circleObj2 = {
bpm: bpm,
scroll: scroll
}
if(lyricsLine !== null){
circleObj2.lyricsLine = lyricsLine
lyricsLine = null
}else if(lyricsCopy){
circleObj2.lyricsCopy = true
}
lyricsCopy = false
currentMeasure.push(circleObj2)
}
if(circleObj){
if(lyricsLine !== null){
circleObj.lyricsLine = lyricsLine
lyricsLine = null
}else if(lyricsCopy){
circleObj.lyricsCopy = true
}
lyricsCopy = false
currentMeasure.push(circleObj)
}
}
var insertLyrics = obj => {
if(!lyrics){
lyrics = []
}else if(lyricsIndex !== null){
lyrics[lyricsIndex].end = obj.start
}
lyricsIndex = lyrics.length
lyrics.push(obj)
}
for(var lineNum = meta.start; lineNum < meta.end; lineNum++){
var line = this.data[lineNum]
if(line.slice(0, 1) === "#"){
var line = line.slice(1)
var [name, value] = this.split(line, " ")
name = name.toLowerCase()
switch(name){
case "gogostart":
gogo = true
break
case "gogoend":
gogo = false
break
case "bpmchange":
bpm = parseFloat(value) || bpm
break
case "scroll":
scroll = Math.abs(parseFloat(value)) || scroll
break
case "measure":
var [numerator, denominator] = value.split("/")
measure = numerator / denominator * 4 || measure
break
case "delay":
ms += (parseFloat(value) || 0) * 1000
break
case "barlineon":
barLine = true
break
case "barlineoff":
barLine = false
break
case "branchstart":
branch = true
currentBranch = false
branchFirstMeasure = true
branchSettings = {
ms: ms,
gogo: gogo,
bpm: bpm,
scroll: scroll,
sectionBegin: sectionBegin,
lyricsCopy: !!lyrics
}
if(lyrics && lyricsIndex !== null){
var line = lyrics[lyricsIndex]
line.end = ms
}
lyricsIndex = null
value = value.split(",")
if(!branches){
branches = []
}
var req = {
advanced: parseFloat(value[1]) || 0,
master: parseFloat(value[2]) || 0
}
if(req.advanced > 0){
var active = req.master > 0 ? "normal" : "master"
}else{
var active = req.master > 0 ? "advanced" : "master"
}
branchObj = {
ms: ms,
originalMS: ms,
active: active,
type: value[0].trim().toLowerCase() === "r" ? "drumroll" : "accuracy",
requirement: req
}
branches.push(branchObj)
if(measures.length === 1 && branchObj.type === "drumroll"){
for(var i = circles.length; i--;){
var circle = circles[i]
if(circle.endTime && (circle.type === "drumroll" || circle.type === "daiDrumroll" || circle.type === "balloon")){
measures.push({
ms: circle.endTime,
originalMS: circle.endTime,
speed: circle.bpm * circle.scroll / 60,
visible: false,
branch: circle.branch
})
break
}
}
}
if(measures.length !== 0){
measures[measures.length - 1].nextBranch = branchObj
}
break
case "branchend":
branch = false
currentBranch = false
lyricsCopy = lyricsCopy || !!lyrics
break
case "section":
sectionBegin = true
if(branch && !currentBranch){
branchSettings.sectionBegin = true
}
break
case "n": case "e": case "m":
if(!branch){
break
}
if(lyrics){
if(lyricsIndex !== null){
var line = lyrics[lyricsIndex]
line.end = ms
}
lyricsIndex = null
}
ms = branchSettings.ms
gogo = branchSettings.gogo
bpm = branchSettings.bpm
scroll = branchSettings.scroll
sectionBegin = branchSettings.sectionBegin
lyricsCopy = branchSettings.lyricsCopy
branchFirstMeasure = true
var branchName = name === "m" ? "master" : (name === "e" ? "advanced" : "normal")
currentBranch = {
name: branchName,
active: branchName === branchObj.active
}
branchObj[branchName] = currentBranch
break
case "lyric":
lyricsLine = value.replace(regexLinebreak, "\n").trim()
break
}
}else{
var string = line.toUpperCase().split("")
const abekobe = localStorage.getItem("abekobe") ?? "false";
const detarame = parseFloat(localStorage.getItem("detarame") ?? "0", 10);
for(let symbol of string){
if (abekobe === "true") {
if (symbol === "1") {
symbol = "2";
} else if (symbol === "2") {
symbol = "1";
} else if (symbol === "3") {
symbol = "4";
} else if (symbol === "4") {
symbol = "3";
} else if (symbol === "A") {
symbol = "B";
} else if (symbol === "B") {
symbol - "A";
}
}
if (detarame > 0) {
const randomValue = Math.random() * 100;
if (randomValue < detarame) {
const first = ["1", "2"];
const second = ["3", "4", "A", "B"];
if (first.includes(symbol)) {
const firstIndex = Math.floor(Math.random() * first.length);
symbol = first[firstIndex];
} else if (second.includes(symbol)) {
const secondIndex = Math.floor(Math.random() * second.length);
symbol = second[secondIndex];
}
}
}
var error = false
switch(symbol){
case "0":
insertBlankNote()
break
case "1": case "2": case "3": case "4": case "A": case "B":
var type = this.noteTypes[symbol]
var circleObj = {
type: type.name,
txt: type.txt,
gogo: gogo,
bpm: bpm,
scroll: scroll,
section: sectionBegin
}
sectionBegin = false
if(lastDrumroll){
circleObj.endDrumroll = lastDrumroll
lastDrumroll = false
}
insertNote(circleObj)
break
case "5": case "6": case "7": case "9":
var type = this.noteTypes[symbol]
var circleObj = {
type: type.name,
txt: type.txt,
gogo: gogo,
bpm: bpm,
scroll: scroll,
section: sectionBegin
}
sectionBegin = false
if(lastDrumroll){
if(symbol === "9"){
insertNote({
endDrumroll: lastDrumroll,
gogo: gogo,
bpm: bpm,
scroll: scroll,
section: sectionBegin
})
sectionBegin = false
lastDrumroll = false
}else{
insertBlankNote()
}
break
}
if(symbol === "7" || symbol === "9"){
var hits = balloons[balloonID]
if(!hits || hits < 1){
hits = 1
}
circleObj.requiredHits = hits
balloonID++
}
lastDrumroll = circleObj
insertNote(circleObj)
break
case "8":
if(lastDrumroll){
insertNote({
endDrumroll: lastDrumroll,
gogo: gogo,
bpm: bpm,
scroll: scroll,
section: sectionBegin
})
sectionBegin = false
lastDrumroll = false
}else{
insertBlankNote()
}
break
case ",":
if(currentMeasure.length === 0 && (bpm !== lastBpm || gogo !== lastGogo || lyricsLine !== null)){
insertBlankNote()
}
pushMeasure()
currentMeasure = []
break
default:
if(regexAZ.test(symbol)){
insertBlankNote()
}else if(!regexSpace.test(symbol)){
error = true
}
break
}
if(error){
break
}
}
}
}
if(lastDrumroll){
lastDrumroll.endTime = ms
lastDrumroll.originalEndTime = ms
}
if(lyricsLine !== null){
insertLyrics({
start: ms,
text: lyricsLine
})
}
pushMeasure()
if(!lyricsOnly){
if(branches){
circles.sort((a, b) => a.ms > b.ms ? 1 : -1)
measures.sort((a, b) => a.ms > b.ms ? 1 : -1)
circles.forEach((circle, i) => circle.id = i + 1)
}
this.measures = measures
this.events = events
this.branches = branches
this.scoreinit = meta.scoreinit
this.scorediff = meta.scorediff
if(this.scoreinit && this.scorediff){
this.scoremode = meta.scoremode || 1
}else{
this.scoremode = meta.scoremode || 2
var autoscore = new AutoScore(difficulty, this.stars, this.scoremode, circles)
this.scoreinit = autoscore.ScoreInit
this.scorediff = autoscore.ScoreDiff
}
}
if(lyrics && lyricsIndex !== null){
var line = lyrics[lyricsIndex]
line.end = Math.max(ms, line.start) + 5000
}
if(lyrics){
this.lyrics = lyrics
}else if(!lyricsOnly){
for(var courseName in this.metadata){
if(this.metadata[courseName].inlineLyrics){
this.parseCircles(courseName, true)
break
}
}
}
return circles
}
}

677
public/src/js/plugins.js Normal file
View File

@@ -0,0 +1,677 @@
class Plugins{
constructor(...args){
this.init(...args)
}
init(){
this.allPlugins = []
this.pluginMap = {}
this.hashes = []
this.startOrder = []
}
add(script, options){
options = options || {}
var hash = md5.base64(script.toString())
var isUrl = typeof script === "string" && !options.raw
if(isUrl){
hash = "url " + hash
}else if(typeof script !== "string"){
hash = "class " + hash
}
var name = options.name
if(!name && isUrl){
name = script
var index = name.lastIndexOf("?")
if(index !== -1){
name = name.slice(0, index)
}
var index = name.lastIndexOf("/")
if(index !== -1){
name = name.slice(index + 1)
}
if(name.endsWith(".taikoweb.js")){
name = name.slice(0, -".taikoweb.js".length)
}else if(name.endsWith(".js")){
name = name.slice(0, -".js".length)
}
}
name = name || "plugin"
if(this.hashes.indexOf(hash) !== -1){
console.warn("Skip adding an already addded plugin: " + name)
return
}
var baseName = name
for(var i = 2; name in this.pluginMap; i++){
name = baseName + i.toString()
}
var plugin = new PluginLoader(script, name, hash, options.raw)
plugin.hide = !!options.hide
this.allPlugins.push({
name: name,
plugin: plugin
})
this.pluginMap[name] = plugin
this.hashes.push(hash)
return plugin
}
remove(name){
if(name in this.pluginMap){
var hash = this.pluginMap[name].hash
if(hash){
var index = this.hashes.indexOf(hash)
if(index !== -1){
this.hashes.splice(index, 1)
}
}
this.unload(name)
}
var index = this.allPlugins.findIndex(obj => obj.name === name)
if(index !== -1){
this.allPlugins.splice(index, 1)
}
var index = this.startOrder.indexOf(name)
if(index !== -1){
this.startOrder.splice(index, 1)
}
delete this.pluginMap[name]
}
load(name){
return this.pluginMap[name].load()
}
loadAll(){
for(var i = 0; i < this.allPlugins.length; i++){
this.allPlugins[i].plugin.load()
}
}
start(name){
return this.pluginMap[name].start()
}
startAll(){
for(var i = 0; i < this.allPlugins.length; i++){
this.allPlugins[i].plugin.start()
}
}
stop(name){
return this.pluginMap[name].stop()
}
stopAll(){
for(var i = this.startOrder.length; i--;){
this.pluginMap[this.startOrder[i]].stop()
}
}
unload(name){
return this.pluginMap[name].unload()
}
unloadAll(){
for(var i = this.startOrder.length; i--;){
this.pluginMap[this.startOrder[i]].unload()
}
for(var i = this.allPlugins.length; i--;){
this.allPlugins[i].plugin.unload()
}
}
unloadImported(){
for(var i = this.startOrder.length; i--;){
var plugin = this.pluginMap[this.startOrder[i]]
if(plugin.imported){
plugin.unload()
}
}
for(var i = this.allPlugins.length; i--;){
var obj = this.allPlugins[i]
if(obj.plugin.imported){
obj.plugin.unload()
}
}
}
strFromFunc(func){
var output = func.toString()
return output.slice(output.indexOf("{") + 1, output.lastIndexOf("}"))
}
argsFromFunc(func){
var output = func.toString()
output = output.slice(0, output.indexOf("{"))
output = output.slice(output.indexOf("(") + 1, output.lastIndexOf(")"))
return output.split(",").map(str => str.trim()).filter(Boolean)
}
insertBefore(input, insertedText, searchString){
var index = input.indexOf(searchString)
if(index === -1){
throw new Error("searchString not found: " + searchString)
}
return input.slice(0, index) + insertedText + input.slice(index)
}
insertAfter(input, searchString, insertedText){
var index = input.indexOf(searchString)
if(index === -1){
throw new Error("searchString not found: " + searchString)
}
var length = searchString.length
return input.slice(0, index + length) + insertedText + input.slice(index + length)
}
strReplace(input, searchString, insertedText, repeat=1){
var position = 0
for(var i = 0; i < repeat; i++){
var index = input.indexOf(searchString, position)
if(index === -1){
if(repeat === Infinity){
break
}else{
throw new Error("searchString not found: " + searchString)
}
}
input = input.slice(0, index) + insertedText + input.slice(index + searchString.length)
position = index + insertedText.length
}
return input
}
isObject(input){
return input && typeof input === "object" && input.constructor === Object
}
deepMerge(target, ...sources){
sources.forEach(source => {
if(this.isObject(target) && this.isObject(source)){
for(var i in source){
if(this.isObject(source[i])){
if(!target[i]){
target[i] = {}
}
this.deepMerge(target[i], source[i])
}else if(source[i]){
target[i] = source[i]
}
}
}
})
return target
}
arrayDel(array, item){
var index = array.indexOf(item)
if(index !== -1){
array.splice(index, 1)
return true
}
return false
}
hasSettings(){
for(var i = 0; i < this.allPlugins.length; i++){
var plugin = this.allPlugins[i].plugin
if(plugin.loaded && (!plugin.hide || plugin.settings())){
return true
}
}
return false
}
getSettings(){
var items = []
for(var i = 0; i < this.allPlugins.length; i++){
var obj = this.allPlugins[i]
let plugin = obj.plugin
if(!plugin.loaded){
continue
}
if(!plugin.hide){
let description
let description_lang
var module = plugin.module
if(module){
description = [
module.description,
module.author ? strings.plugins.author.replace("%s", module.author) : null,
module.version ? strings.plugins.version.replace("%s", module.version) : null
].filter(Boolean).join("\n")
description_lang = {}
languageList.forEach(lang => {
description_lang[lang] = [
this.getLocalTitle(module.description, module.description_lang, lang),
module.author ? allStrings[lang].plugins.author.replace("%s", module.author) : null,
module.version ? allStrings[lang].plugins.version.replace("%s", module.version) : null
].filter(Boolean).join("\n")
})
}
var name = module && module.name || obj.name
var name_lang = module && module.name_lang
items.push({
name: name,
name_lang: name_lang,
description: description,
description_lang: description_lang,
type: "toggle",
default: true,
getItem: () => plugin.started,
setItem: value => {
if(plugin.name in this.pluginMap){
if(plugin.started && !value){
this.stop(plugin.name)
}else if(!plugin.started && value){
this.start(plugin.name)
}
}
}
})
}
var settings = plugin.settings()
if(settings){
settings.forEach(setting => {
if(!setting.name){
setting.name = name
if(!setting.name_lang){
setting.name_lang = name_lang
}
}
if(typeof setting.getItem !== "function"){
setting.getItem = () => {}
}
if(typeof setting.setItem !== "function"){
setting.setItem = () => {}
}
if(!("indent" in setting) && !plugin.hide){
setting.indent = 1
}
items.push(setting)
})
}
}
return items
}
getLocalTitle(title, titleLang, lang){
if(titleLang){
for(var id in titleLang){
if(id === (lang || strings.id) && titleLang[id]){
return titleLang[id]
}
}
}
return title
}
}
class PluginLoader{
constructor(...args){
this.init(...args)
}
init(script, name, hash, raw){
this.name = name
this.hash = hash
if(typeof script === "string"){
if(raw){
this.url = URL.createObjectURL(new Blob([script], {
type: "application/javascript"
}))
}else{
this.url = script
}
}else{
this.class = script
}
}
load(loadErrors){
if(this.loaded){
return Promise.resolve()
}else if(!this.url && !this.class){
if(loadErrors){
return Promise.reject()
}else{
return Promise.resolve()
}
}else{
return (this.url ? import(this.url) : Promise.resolve({
default: this.class
})).then(module => {
if(this.url){
URL.revokeObjectURL(this.url)
delete this.url
}else{
delete this.class
}
this.loaded = true
try{
this.module = new module.default()
}catch(e){
this.error()
var error = new Error()
error.stack = "Error initializing plugin: " + this.name + "\n" + e.stack
if(loadErrors){
return Promise.reject(error)
}else{
console.error(error)
return Promise.resolve()
}
}
var output
try{
if(this.module.beforeLoad){
this.module.beforeLoad(this)
}
if(this.module.load){
output = this.module.load(this)
}
}catch(e){
this.error()
var error = new Error()
error.stack = "Error in plugin load: " + this.name + "\n" + e.stack
if(loadErrors){
return Promise.reject(error)
}else{
console.error(error)
return Promise.resolve()
}
}
if(typeof output === "object" && output.constructor === Promise){
return output.catch(e => {
this.error()
var error = new Error()
error.stack = "Error in plugin load promise: " + this.name + (e ? "\n" + e.stack : "")
if(loadErrors){
return Promise.reject(error)
}else{
console.error(error)
return Promise.resolve()
}
})
}
}, e => {
this.error()
plugins.remove(this.name)
if(e.name === "SyntaxError"){
var error = new SyntaxError()
error.stack = "Error in plugin syntax: " + this.name + "\n" + e.stack
}else{
var error = e
}
if(loadErrors){
return Promise.reject(error)
}else{
console.error(error)
return Promise.resolve()
}
})
}
}
start(orderChange, startErrors){
if(!orderChange){
plugins.startOrder.push(this.name)
}
return this.load().then(() => {
if(!this.started && this.module){
this.started = true
try{
if(this.module.beforeStart){
this.module.beforeStart()
}
if(this.module.start){
this.module.start()
}
}catch(e){
this.error()
var error = new Error()
error.stack = "Error in plugin start: " + this.name + "\n" + e.stack
if(startErrors){
return Promise.reject(error)
}else{
console.error(error)
return Promise.resolve()
}
}
}
})
}
stop(orderChange, noError){
if(this.loaded && this.started){
if(!orderChange){
var stopIndex = plugins.startOrder.indexOf(this.name)
if(stopIndex !== -1){
plugins.startOrder.splice(stopIndex, 1)
for(var i = plugins.startOrder.length; i-- > stopIndex;){
plugins.pluginMap[plugins.startOrder[i]].stop(true)
}
}
}
this.started = false
try{
if(this.module.beforeStop){
this.module.beforeStop()
}
if(this.module.stop){
this.module.stop()
}
}catch(e){
var error = new Error()
error.stack = "Error in plugin stop: " + this.name + "\n" + e.stack
console.error(error)
if(!noError){
this.error()
}
}
if(!orderChange && stopIndex !== -1){
for(var i = stopIndex; i < plugins.startOrder.length; i++){
plugins.pluginMap[plugins.startOrder[i]].start(true)
}
}
}
}
unload(error){
if(this.loaded){
if(this.started){
this.stop(false, error)
}
this.loaded = false
plugins.remove(this.name)
if(this.module){
try{
if(this.module.beforeUnload){
this.module.beforeUnload()
}
if(this.module.unload){
this.module.unload()
}
}catch(e){
var error = new Error()
error.stack = "Error in plugin unload: " + this.name + "\n" + e.stack
console.error(error)
}
delete this.module
}
}
}
error(){
if(this.module && this.module.error){
try{
this.module.error()
}catch(e){
var error = new Error()
error.stack = "Error in plugin error: " + this.name + "\n" + e.stack
console.error(error)
}
}
this.unload(true)
}
settings(){
if(this.module && this.module.settings){
try{
var settings = this.module.settings()
}catch(e){
console.error(e)
this.error()
return
}
if(Array.isArray(settings)){
return settings
}
}
}
}
class EditValue{
constructor(...args){
this.init(...args)
}
init(parent, name){
if(name){
if(!parent){
throw new Error("Parent is not defined")
}
this.name = [parent, name]
this.delete = !(name in parent)
}else{
this.original = parent
}
}
load(callback){
this.loadCallback = callback
return this
}
start(){
if(this.name){
this.original = this.name[0][this.name[1]]
}
try{
var output = this.loadCallback(this.original)
}catch(e){
console.error(this.loadCallback)
var error = new Error()
error.stack = "Error editing the value of " + this.getName() + "\n" + e.stack
throw error
}
if(typeof output === "undefined"){
console.error(this.loadCallback)
throw new Error("Error editing the value of " + this.getName() + ": A value is expected to be returned")
}
if(this.name){
this.name[0][this.name[1]] = output
}
return output
}
stop(){
if(this.name){
if(this.delete){
delete this.name[0][this.name[1]]
}else{
this.name[0][this.name[1]] = this.original
}
}
return this.original
}
getName(){
var name = "unknown"
try{
if(this.name){
var name = (
typeof this.name[0] === "function" && this.name[0].name
|| (
typeof this.name[0] === "object" && typeof this.name[0].constructor === "function" && (
this.name[0] instanceof this.name[0].constructor ? (() => {
var consName = this.name[0].constructor.name || ""
return consName.slice(0, 1).toLowerCase() + consName.slice(1)
})() : this.name[0].constructor.name + ".prototype"
)
) || name
) + (this.name[1] ? "." + this.name[1] : "")
}
}catch(e){
name = "error"
}
return name
}
unload(){
delete this.name
delete this.original
delete this.loadCallback
}
}
class EditFunction extends EditValue{
start(){
if(this.name){
this.original = this.name[0][this.name[1]]
}
if(typeof this.original !== "function"){
console.error(this.loadCallback)
var error = new Error()
error.stack = "Error editing the function value of " + this.getName() + ": Original value is not a function"
throw error
}
var args = plugins.argsFromFunc(this.original)
try{
var output = this.loadCallback(plugins.strFromFunc(this.original), args)
}catch(e){
console.error(this.loadCallback)
var error = new Error()
error.stack = "Error editing the function value of " + this.getName() + "\n" + e.stack
throw error
}
if(typeof output === "undefined"){
console.error(this.loadCallback)
throw new Error("Error editing the function value of " + this.getName() + ": A value is expected to be returned")
}
try{
var output = Function(...args, output)
}catch(e){
console.error(this.loadCallback)
var error = new SyntaxError()
var blob = new Blob([output], {
type: "application/javascript"
})
var url = URL.createObjectURL(blob)
error.stack = "Error editing the function value of " + this.getName() + ": Could not evaluate string, check the full string for errors: " + url + "\n" + e.stack
setTimeout(() => {
URL.revokeObjectURL(url)
}, 5 * 60 * 1000)
throw error
}
if(this.name){
this.name[0][this.name[1]] = output
}
return output
}
}
class Patch{
constructor(...args){
this.init(...args)
}
init(){
this.edits = []
this.addedLanguages = []
}
addEdits(...args){
args.forEach(arg => this.edits.push(arg))
}
addLanguage(lang, forceSet, fallback="en"){
if(fallback){
lang = plugins.deepMerge({}, allStrings[fallback], lang)
}
this.addedLanguages.push({
lang: lang,
forceSet: forceSet
})
}
beforeStart(){
this.edits.forEach(edit => edit.start())
this.addedLanguages.forEach(obj => {
settings.addLang(obj.lang, obj.forceSet)
})
}
beforeStop(){
for(var i = this.edits.length; i--;){
this.edits[i].stop()
}
for(var i = this.addedLanguages.length; i--;){
settings.removeLang(this.addedLanguages[i].lang)
}
}
beforeUnload(){
this.edits.forEach(edit => edit.unload())
}
log(...args){
var name = this.name || "Plugin"
console.log(
"%c[" + name + "]",
"font-weight: bold;",
...args
)
}
}

980
public/src/js/scoresheet.js Normal file
View File

@@ -0,0 +1,980 @@
class Scoresheet{
constructor(...args){
this.init(...args)
}
init(controller, results, multiplayer, touchEnabled){
this.controller = controller
this.resultsObj = 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[player0][i] = results[i] === null ? null : results[i].toString()
}
this.multiplayer = multiplayer
this.touchEnabled = touchEnabled
this.canvas = document.getElementById("canvas")
this.ctx = this.canvas.getContext("2d")
var resolution = settings.getItem("resolution")
var noSmoothing = resolution === "low" || resolution === "lowest"
if(noSmoothing){
this.ctx.imageSmoothingEnabled = false
}
if(resolution === "lowest"){
this.canvas.style.imageRendering = "pixelated"
}
this.game = document.getElementById("game")
this.fadeScreen = document.createElement("div")
this.fadeScreen.id = "fade-screen"
this.game.appendChild(this.fadeScreen)
this.font = strings.font
this.numbersFont = "TnT, Meiryo, sans-serif"
this.state = {
screen: "fadeIn",
screenMS: this.getMS(),
startDelay: 3300,
hasPointer: 0,
scoreNext: false
}
this.frame = 1000 / 60
this.numbers = "001122334455667788900112233445".split("")
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"]
}, this.keyDown.bind(this))
this.gamepad = new Gamepad({
confirm: ["a", "b", "start", "ls", "rs"]
}, this.keyDown.bind(this))
this.difficulty = {
"easy": 0,
"normal": 1,
"hard": 2,
"oni": 3,
"ura": 4
}
this.scoreSaved = false
this.redrawRunning = true
this.redrawBind = this.redraw.bind(this)
this.redraw()
assets.sounds["v_results"].play()
assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689)
this.session = p2.session
if(this.session){
if(p2.getMessage("songsel")){
this.toSongsel(true)
}
pageEvents.add(p2, "message", response => {
if(response.type === "songsel"){
this.toSongsel(true)
}
})
}
pageEvents.send("scoresheet", {
selectedSong: controller.selectedSong,
autoPlayEnabled: controller.autoPlayEnabled,
multiplayer: multiplayer,
touchEnabled: touchEnabled,
results: this.results,
p2results: multiplayer ? p2.results : null,
keyboardEvents: controller.keyboard.keyboardEvents,
gamepadEvents: controller.keyboard.gamepad.gamepadEvents,
touchEvents: controller.view.touchEvents
})
}
keyDown(pressed){
if(pressed && this.redrawing){
this.toNext()
}
}
mouseDown(event){
if(event.type === "touchstart"){
event.preventDefault()
this.canvas.style.cursor = ""
this.state.pointerLocked = true
}else{
this.state.pointerLocked = false
if(event.which !== 1){
return
}
}
this.toNext()
}
toNext(){
var elapsed = this.getMS() - this.state.screenMS
if(this.state.screen === "fadeIn" && elapsed >= this.state.startDelay){
this.toScoresShown()
}else if(this.state.screen === "scoresShown" && elapsed >= 1000){
this.toSongsel()
}
}
toScoresShown(){
if(!p2.session){
this.state.screen = "scoresShown"
this.state.screenMS = this.getMS()
this.controller.playSound("neiro_1_don", 0, true)
}
}
toSongsel(fromP2){
if(!p2.session || fromP2){
snd.musicGain.fadeOut(0.5)
this.state.screen = "fadeOut"
this.state.screenMS = this.getMS()
if(!fromP2){
this.controller.playSound("neiro_1_don", 0, true)
}
}
}
startRedraw(){
this.redrawing = true
requestAnimationFrame(this.redrawBind)
this.winW = null
this.winH = null
pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.mouseDown.bind(this))
if(!this.multiplayer){
this.tetsuoHana = document.createElement("div")
this.tetsuoHana.id = "tetsuohana"
var flowersBg = "url('" + assets.image["results_flowers"].src + "')"
var mikoshiBg = "url('" + assets.image["results_mikoshi"].src + "')"
var tetsuoHanaBg = "url('" + assets.image["results_tetsuohana" + (debugObj.state === "closed" ? "" : "2")].src + "')"
var id = ["flowers1", "flowers2", "mikoshi", "tetsuo", "hana"]
var bg = [flowersBg, flowersBg, mikoshiBg, tetsuoHanaBg, tetsuoHanaBg]
for(var i = 0; i < id.length; i++){
if(id[i] === "mikoshi"){
var divOut = document.createElement("div")
divOut.id = id[i] + "-out"
this.tetsuoHana.appendChild(divOut)
}else{
var divOut = this.tetsuoHana
}
var div = document.createElement("div")
div.id = id[i]
var divIn = document.createElement("div")
divIn.id = id[i] + "-in"
divIn.style.backgroundImage = bg[i]
div.appendChild(divIn)
divOut.appendChild(div)
}
this.game.appendChild(this.tetsuoHana)
}
}
redraw(){
if(!this.redrawRunning){
return
}
if(this.redrawing){
requestAnimationFrame(this.redrawBind)
}
var ms = this.getMS()
if(!this.redrawRunning){
return
}
var ctx = this.ctx
ctx.save()
var winW = innerWidth
var winH = lastHeight
this.pixelRatio = window.devicePixelRatio || 1
var resolution = settings.getItem("resolution")
if(resolution === "medium"){
this.pixelRatio *= 0.75
}else if(resolution === "low"){
this.pixelRatio *= 0.5
}else if(resolution === "lowest"){
this.pixelRatio *= 0.25
}
winW *= this.pixelRatio
winH *= this.pixelRatio
var ratioX = winW / 1280
var ratioY = winH / 720
var ratio = (ratioX < ratioY ? ratioX : ratioY)
if(this.redrawing){
if(this.winW !== winW || this.winH !== winH){
this.canvas.width = Math.max(1, winW)
this.canvas.height = Math.max(1, winH)
ctx.scale(ratio, ratio)
this.canvas.style.width = (winW / this.pixelRatio) + "px"
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)
if(this.tetsuoHanaClass === "dance"){
this.tetsuoHana.classList.remove("dance", "dance2")
setTimeout(()=>{
this.tetsuoHana.classList.add("dance2")
},50)
}else if(this.tetsuoHanaClass === "failed"){
this.tetsuoHana.classList.remove("failed")
setTimeout(()=>{
this.tetsuoHana.classList.add("failed")
},50)
}
}
}else if(!document.hasFocus() && this.state.screen === "scoresShown"){
if(this.state["countup0"]){
this.stopSound("se_results_countup", 0)
}
if(this.state["countup1"]){
this.stopSound("se_results_countup", 1)
}
return
}else{
ctx.clearRect(0, 0, winW / ratio, winH / ratio)
}
}else{
ctx.scale(ratio, ratio)
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
this.ratio = ratio
winW /= ratio
winH /= ratio
var frameTop = winH / 2 - 720 / 2
var frameLeft = winW / 2 - 1280 / 2
var players = this.multiplayer ? 2 : 1
var p2Offset = 298
var bgOffset = 0
var elapsed = ms - this.state.screenMS
if(this.state.screen === "fadeIn" && elapsed < 1000){
bgOffset = Math.min(1, this.draw.easeIn(1 - elapsed / 1000)) * (winH / 2)
}
if((this.state.screen !== "fadeIn" || elapsed >= 1000) && !this.scoreSaved){
this.saveScore()
}
if(bgOffset){
ctx.save()
ctx.translate(0, -bgOffset)
}
this.draw.pattern({
ctx: ctx,
img: assets.image["bg_score_p1"],
x: 0,
y: 0,
w: winW,
h: winH / 2,
dx: frameLeft - 35,
dy: frameTop + 17
})
ctx.fillStyle = "rgba(127, 28, 12, 0.5)"
ctx.fillRect(0, winH / 2 - 12, winW, 12)
ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
ctx.fillRect(0, winH / 2, winW, 20)
if(bgOffset !== 0){
ctx.fillStyle = "#000"
ctx.fillRect(0, winH / 2 - 2, winW, 2)
}
ctx.fillStyle = "#fa4529"
ctx.fillRect(0, 0, winW, frameTop + 64)
ctx.fillStyle = "#bf2900"
ctx.fillRect(0, frameTop + 64, winW, 8)
if(bgOffset){
ctx.restore()
ctx.save()
ctx.translate(0, bgOffset)
}
this.draw.pattern({
ctx: ctx,
img: assets.image[this.multiplayer ? "bg_score_p2" : "bg_score_p1"],
x: 0,
y: winH / 2,
w: winW,
h: winH / 2,
dx: frameLeft - 35,
dy: frameTop - 17
})
ctx.fillStyle = this.multiplayer ? "rgba(138, 245, 247, 0.5)" : "rgba(249, 163, 149, 0.5)"
ctx.fillRect(0, winH / 2, winW, 12)
ctx.fillStyle = "#000"
if(bgOffset === 0){
ctx.fillRect(0, winH / 2 - 2, winW, 4)
}else{
ctx.fillRect(0, winH / 2, winW, 2)
}
ctx.fillStyle = this.multiplayer ? "#6bbec0" : "#fa4529"
ctx.fillRect(0, winH - frameTop - 64, winW, frameTop + 64)
ctx.fillStyle = this.multiplayer ? "rgba(160, 228, 229, 0.8)" : "rgba(255, 144, 116, 0.8)"
ctx.fillRect(0, winH - frameTop - 72, winW, 7)
ctx.fillStyle = this.multiplayer ? "#a8e0e0" : "#ff9b7a"
ctx.fillRect(0, winH - frameTop - 66, winW, 2)
if(bgOffset){
ctx.restore()
}
if(this.state.screen === "scoresShown" || this.state.screen === "fadeOut"){
var elapsed = Infinity
}else if(this.redrawing){
var elapsed = ms - this.state.screenMS - this.state.startDelay
}else{
var elapsed = 0
}
var rules = this.controller.game.rules
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[p]
if(!results){
continue
}
var clear = this.rules[p].clearReached(results.gauge)
if(p === 1 || !this.multiplayer && clear){
ctx.translate(0, 290)
}
if(clear){
ctx.globalCompositeOperation = "lighter"
}
ctx.globalAlpha = Math.min(1, Math.max(0, (elapsed - (3100 + failedOffset)) / 500)) * 0.5
var grd = ctx.createLinearGradient(0, frameTop + 72, 0, frameTop + 368)
grd.addColorStop(0, "#000")
if(clear){
grd.addColorStop(1, "#ffffba")
}else{
grd.addColorStop(1, "transparent")
}
ctx.fillStyle = grd
ctx.fillRect(0, frameTop + 72, winW, 286)
ctx.restore()
}
}
if(elapsed >= 0){
if(this.state.hasPointer === 0){
this.state.hasPointer = 1
if(!this.state.pointerLocked){
this.canvas.style.cursor = this.session ? "" : "pointer"
}
}
ctx.save()
ctx.setTransform(1, 0, 0, 1, 0, 0)
this.draw.alpha(Math.min(1, elapsed / 400), ctx, ctx => {
ctx.scale(ratio, ratio)
ctx.translate(frameLeft, frameTop)
this.canvasCache.get({
ctx: ctx,
x: 0,
y: 0,
w: winW,
h: 80,
id: "results"
}, ctx => {
this.draw.layeredText({
ctx: ctx,
text: strings.results,
fontSize: 48,
fontFamily: this.font,
x: 23,
y: 15,
letterSpacing: strings.id === "en" ? 0 : 3,
forceShadow: true
}, [
{x: -2, y: -2, outline: "#000", letterBorder: 22},
{},
{x: 2, y: 2, shadow: [2, 2, 7]},
{x: 2, y: 2, outline: "#ad1516", letterBorder: 10},
{x: -2, y: -2, outline: "#ff797b"},
{outline: "#f70808"},
{fill: "#fff", shadow: [-1, 1, 3, 1.5]}
])
this.draw.layeredText({
ctx: ctx,
text: this.results[this.player[0]].title,
fontSize: 40,
fontFamily: this.font,
x: 1257,
y: 20,
width: 600,
align: "right",
forceShadow: true
}, [
{outline: "#000", letterBorder: 10, shadow: [1, 1, 3]},
{fill: "#fff"}
])
})
ctx.save()
for(var p = 0; p < players; p++){
var results = this.results[p]
if(!results){
continue
}
if(p === 1){
ctx.translate(0, p2Offset)
}
ctx.drawImage(assets.image["difficulty"],
0, 144 * this.difficulty[results.difficulty],
168, 143,
300, 150, 189, 162
)
var diff = results.difficulty
var text = strings[diff === "ura" ? "oni" : diff]
ctx.font = this.draw.bold(this.font) + "28px " + this.font
ctx.textAlign = "center"
ctx.textBaseline = "bottom"
ctx.strokeStyle = "#000"
ctx.fillStyle = "#fff"
ctx.lineWidth = 9
ctx.miterLimit = 1
ctx.strokeText(text, 395, 308)
ctx.fillText(text, 395, 308)
ctx.miterLimit = 10
var defaultName = p === 0 ? strings.defaultName : strings.default2PName
if(p === this.player[0]){
var name = account.loggedIn ? account.displayName : defaultName
}else{
var name = results.name || defaultName
}
this.nameplateCache.get({
ctx: ctx,
x: 259,
y: 92,
w: 273,
h: 66,
id: p.toString() + "p" + name,
}, ctx => {
this.draw.nameplate({
ctx: ctx,
x: 3,
y: 3,
name: name,
font: this.font,
blue: p === 1
})
})
if(this.controller.autoPlayEnabled){
ctx.drawImage(assets.image["badge_auto"],
431, 311, 34, 34
)
}
this.draw.roundedRect({
ctx: ctx,
x: 532,
y: 98,
w: 728,
h: 232,
radius: 30,
})
ctx.fillStyle = p === 1 ? "rgba(195, 228, 229, 0.8)" : "rgba(255, 224, 216, 0.8)"
ctx.fill()
this.draw.roundedRect({
ctx: ctx,
x: 556,
y: 237,
w: 254,
h: 70,
radius: 15,
})
ctx.fillStyle = "#000"
ctx.fill()
this.draw.roundedRect({
ctx: ctx,
x: 559,
y: 240,
w: 248,
h: 64,
radius: 14,
})
ctx.fillStyle = "#eec954"
ctx.fill()
this.draw.roundedRect({
ctx: ctx,
x: 567,
y: 248,
w: 232,
h: 48,
radius: 6,
})
ctx.fillStyle = "#000"
ctx.fill()
this.draw.layeredText({
ctx: ctx,
text: strings.points,
x: 792,
y: strings.id === "ko" ? 260 : 253,
fontSize: 36,
fontFamily: this.font,
align: "right",
width: 36
}, [
{fill: "#fff"},
{outline: "#000", letterBorder: 0.5}
])
this.draw.score({
ctx: ctx,
score: "good",
x: 823,
y: 192,
results: true
})
this.draw.score({
ctx: ctx,
score: "ok",
x: 823,
y: 233,
results: true
})
this.draw.score({
ctx: ctx,
score: "bad",
x: 823,
y: 273,
results: true
})
ctx.textAlign = "right"
var grd = ctx.createLinearGradient(0, 0, 0, 30)
grd.addColorStop(0.2, "#ff4900")
grd.addColorStop(0.9, "#f7fb00")
this.draw.layeredText({
ctx: ctx,
text: strings.maxCombo,
x: 1149,
y: 193,
fontSize: 29,
fontFamily: this.font,
align: "right",
width: 154,
letterSpacing: strings.id === "ja" ? 1 : 0
}, [
{outline: "#000", letterBorder: 8},
{fill: grd}
])
this.draw.layeredText({
ctx: ctx,
text: strings.drumroll,
x: 1150,
y: 233,
fontSize: 29,
fontFamily: this.font,
align: "right",
width: 154,
letterSpacing: strings.id === "ja" ? 4 : 0
}, [
{outline: "#000", letterBorder: 8},
{fill: "#ffc700"}
])
}
ctx.restore()
})
ctx.restore()
}
if(!this.multiplayer){
if(elapsed >= 400 && elapsed < 3100 + failedOffset){
if(this.tetsuoHanaClass !== "fadein"){
this.tetsuoHana.classList.add("fadein")
this.tetsuoHanaClass = "fadein"
}
}else if(elapsed >= 3100 + failedOffset){
if(this.tetsuoHanaClass !== "dance" && this.tetsuoHanaClass !== "failed"){
if(this.tetsuoHanaClass){
this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
}
this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed"
this.tetsuoHana.classList.add(this.tetsuoHanaClass)
}
}
}
if(elapsed >= 800){
ctx.save()
ctx.setTransform(1, 0, 0, 1, 0, 0)
this.draw.alpha(Math.min(1, (elapsed - 800) / 500), ctx, ctx => {
ctx.scale(ratio, ratio)
ctx.translate(frameLeft, frameTop)
for(var p = 0; p < players; p++){
var results = this.results[p]
if(!results){
continue
}
if(p === 1){
ctx.translate(0, p2Offset)
}
var w = 712
this.draw.gauge({
ctx: ctx,
x: 558 + w,
y: p === 1 ? 124 : 116,
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
})
this.draw.soul({
ctx: ctx,
x: 1215,
y: 144,
scale: 36 / 42,
cleared: this.rules[p].clearReached(results.gauge)
})
}
})
ctx.restore()
}
if(elapsed >= 1200){
ctx.save()
ctx.setTransform(1, 0, 0, 1, 0, 0)
var noCrownResultWait = -2000;
for(var p = 0; p < players; p++){
var results = this.results[p]
if(!results){
continue
}
var crownType = null
if(this.rules[p].clearReached(results.gauge)){
crownType = results.bad === "0" ? "gold" : "silver"
}
if(crownType !== null){
noCrownResultWait = 0;
var amount = Math.min(1, (elapsed - 1200) / 450)
this.draw.alpha(this.draw.easeIn(amount), ctx, ctx => {
ctx.save()
ctx.scale(ratio, ratio)
ctx.translate(frameLeft, frameTop)
if(p === 1){
ctx.translate(0, p2Offset)
}
var crownScale = 1
var shine = 0
if(amount < 1){
crownScale = 2.8 * (1 - amount) + 0.9
}else if(elapsed < 1850){
crownScale = 0.9 + (elapsed - 1650) / 2000
}else if(elapsed < 2200){
shine = (elapsed - 1850) / 175
if(shine > 1){
shine = 2 - shine
}
}
if(this.state.screen === "fadeIn" && elapsed >= 1200 && !this.state["fullcomboPlayed" + p]){
this.state["fullcomboPlayed" + p] = true
if(crownType === "gold"){
this.playSound("v_results_fullcombo" + (p === 1 ? "2" : ""), p)
}
}
if(this.state.screen === "fadeIn" && elapsed >= 1650 && !this.state["crownPlayed" + p]){
this.state["crownPlayed" + p] = true
this.playSound("se_results_crown", p)
}
this.draw.crown({
ctx: ctx,
type: crownType,
x: 395,
y: 218,
scale: crownScale,
shine: shine,
whiteOutline: true,
ratio: ratio
})
ctx.restore()
})
}
}
ctx.restore()
}
if(elapsed >= 2400 + noCrownResultWait){
ctx.save()
ctx.translate(frameLeft, frameTop)
var printNumbers = ["good", "ok", "bad", "maxCombo", "drumroll"]
if(!this.state["countupTime0"]){
var times = {}
var lastTime = 0
for(var p = 0; p < players; p++){
var results = this.results[p]
if(!results){
continue
}
var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame
if(currentTime > lastTime){
lastTime = currentTime
}
}
for(var i in printNumbers){
var largestTime = 0
for(var p = 0; p < players; p++){
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){
largestTime = currentTime
}
}
lastTime = largestTime
}
this.state.fadeInEnd = lastTime
for(var p = 0; p < players; p++){
this.state["countupTime" + p] = times
}
}
for(var p = 0; p < players; p++){
var results = this.results[p]
if(!results){
continue
}
if(p === 1){
ctx.translate(0, p2Offset)
}
ctx.save()
this.state.countupShown = false
var points = this.getNumber(results.points, 3100 + noCrownResultWait, elapsed)
var scale = 1.3
ctx.font = "35px " + this.numbersFont
ctx.translate(760, 286)
ctx.scale(1 / scale, 1 * 1.1)
ctx.textAlign = "center"
ctx.fillStyle = "#fff"
ctx.strokeStyle = "#fff"
ctx.lineWidth = 0.5
for(var i = 0; i < points.length; i++){
ctx.translate(-23.3 * scale, 0)
ctx.fillText(points[points.length - i - 1], 0, 0)
ctx.strokeText(points[points.length - i - 1], 0, 0)
}
ctx.restore()
if(!this.state["countupTime" + p]){
var times = {}
var lastTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame + 1000
for(var i in printNumbers){
times[printNumbers[i]] = lastTime + 500
lastTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
}
this.state["countupTime" + p] = times
}
for(var i in printNumbers){
var start = this.state["countupTime" + p][printNumbers[i]]
this.draw.layeredText({
ctx: ctx,
text: this.getNumber(results[printNumbers[i]], start, elapsed),
x: 971 + 270 * Math.floor(i / 3),
y: 196 + (40 * (i % 3)),
fontSize: 26,
fontFamily: this.numbersFont,
letterSpacing: 1,
align: "right"
}, [
{outline: "#000", letterBorder: 9},
{fill: "#fff"}
])
}
if(this.state.countupShown){
if(!this.state["countup" + p]){
this.state["countup" + p] = true
this.loopSound("se_results_countup", p, [0.1, false, 0, 0, 0.07])
}
}else if(this.state["countup" + p]){
this.state["countup" + p] = false
this.stopSound("se_results_countup", p)
if(this.state.screen === "fadeIn"){
this.playSound("neiro_1_don", p)
}
}
if(this.state.screen === "fadeIn" && elapsed >= this.state.fadeInEnd){
this.state.screen = "scoresShown"
this.state.screenMS = this.getMS()
}
}
ctx.restore()
}
if(this.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000){
this.state.scoreNext = true
if(p2.session){
p2.send("songsel")
}else{
this.toSongsel(true)
}
}
if(this.state.screen === "fadeOut"){
if(this.state.hasPointer === 1){
this.state.hasPointer = 2
this.canvas.style.cursor = ""
}
if(!this.fadeScreenBlack){
this.fadeScreenBlack = true
this.fadeScreen.style.backgroundColor = "#000"
}
var elapsed = ms - this.state.screenMS
if(elapsed >= 1000){
this.clean()
this.controller.songSelection(true, this.showWarning)
}
}
ctx.restore()
}
getNumber(score, start, elapsed){
var numberPos = Math.floor((elapsed - start) / this.frame)
if(numberPos < 0){
return ""
}
var output = ""
for(var i = 0; i < score.length; i++){
if(numberPos < 30 * (i + 1)){
this.state.countupShown = true
return this.numbers[numberPos % 30] + output
}else{
output = score[score.length - i - 1] + output
}
}
return output
}
getSound(id, p){
return assets.sounds[id + (this.multiplayer ? "_p" + (p + 1) : "")]
}
playSound(id, p){
this.getSound(id, p).play()
}
loopSound(id, p, args){
this.getSound(id, p).playLoop(...args)
}
stopSound(id, p){
this.getSound(id, p).stop()
}
mod(length, index){
return ((index % length) + length) % length
}
getMS(){
return Date.now()
}
saveScore(){
if(this.controller.saveScore){
if(this.resultsObj.points < 0){
this.resultsObj.points = 0
}
var title = this.controller.selectedSong.originalTitle
var hash = this.controller.selectedSong.hash
var difficulty = this.resultsObj.difficulty
var oldScore = scoreStorage.get(hash, difficulty, true)
var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge)
var crown = ""
if(clearReached){
crown = this.resultsObj.bad === 0 ? "gold" : "silver"
}
if(!oldScore || oldScore.points <= this.resultsObj.points){
if(oldScore && (oldScore.crown === "gold" || oldScore.crown === "silver" && !crown)){
crown = oldScore.crown
}
this.resultsObj.crown = crown
delete this.resultsObj.title
delete this.resultsObj.difficulty
delete this.resultsObj.gauge
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).catch(() => {
this.showWarning = {name: "scoreSaveFailed"}
})
}
}
this.scoreSaved = true
}
clean(){
this.keyboard.clean()
this.gamepad.clean()
this.draw.clean()
this.canvasCache.clean()
assets.sounds["bgm_result"].stop()
snd.buffer.loadSettings()
this.redrawRunning = false
pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
if(this.touchEnabled){
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
}
if(this.session){
pageEvents.remove(p2, "message")
}
if(!this.multiplayer){
delete this.tetsuoHana
}
delete this.ctx
delete this.canvas
delete this.fadeScreen
delete this.results
delete this.rules
}
}

View File

@@ -0,0 +1,329 @@
class ScoreStorage{
constructor(...args){
this.init(...args)
}
init(){
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"]
}
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(";")
for(var i in this.difficulty){
if(diffArray[i]){
var crown = parseInt(diffArray[i].slice(0, 1)) || 0
var score = {
crown: this.crownValue[crown] || ""
}
var scoreArray = diffArray[i].slice(1).split(",")
for(var j in this.scoreKeys){
var name = this.scoreKeys[j]
var value = parseInt(scoreArray[j] || 0, 36) || 0
if(value < 0){
value = 0
}
score[name] = value
}
if(!songAdded){
scores[hash] = {title: null}
songAdded = true
}
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 = []
var notEmpty = false
for(var i = this.difficulty.length; i--;){
var diff = this.difficulty[i]
if(score[diff]){
var scoreArray = []
var crown = this.crownValue.indexOf(score[diff].crown).toString()
for(var j in this.scoreKeys){
var name = this.scoreKeys[j]
var value = score[diff][name]
value = Math.floor(value).toString(36)
scoreArray.push(value)
}
diffArray.unshift(crown + scoreArray.join(","))
notEmpty = true
}else if(notEmpty){
diffArray.unshift("")
}
}
this.scoreStrings[hash] = diffArray.join(";")
}
titleHash(song){
if(song in this.songTitles){
return this.songTitles[song]
}else{
return song
}
}
get(song, difficulty, isHash){
if(!song){
return this.scores
}else{
var hash = isHash ? song : this.titleHash(song)
if(difficulty){
if(hash in this.scores){
return this.scores[hash][difficulty]
}
}else{
return this.scores[hash]
}
}
}
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: ""}
for(var i in this.scoreKeys){
var name = this.scoreKeys[i]
template[name] = 0
}
return template
}
remove(song, difficulty, isHash){
var hash = isHash ? song : this.titleHash(song)
if(hash in this.scores){
if(difficulty){
if(difficulty in this.scores[hash]){
delete this.scores[hash][difficulty]
var noDiff = true
for(var i in this.difficulty){
if(this.scores[hash][this.difficulty[i]]){
noDiff = false
break
}
}
if(noDiff){
delete this.scores[hash]
delete this.scoreStrings[hash]
}else{
this.writeString(hash)
}
}
}else{
delete this.scores[hash]
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()
}
}

678
public/src/js/search.js Normal file
View File

@@ -0,0 +1,678 @@
class Search{
constructor(...args){
this.init(...args)
}
init(songSelect){
this.songSelect = songSelect
this.opened = false
this.enabled = true
this.style = document.createElement("style")
var css = []
for(var i in this.songSelect.songSkin){
var skin = this.songSelect.songSkin[i]
if("id" in skin || i === "default"){
var id = "id" in skin ? ("cat" + skin.id) : i
css.push(loader.cssRuleset({
[".song-search-" + id]: {
"background-color": skin.background
},
[".song-search-" + id + "::before"]: {
"border-color": skin.border[0],
"border-bottom-color": skin.border[1],
"border-right-color": skin.border[1]
},
[".song-search-" + id + " .song-search-result-title::before, .song-search-" + id + " .song-search-result-subtitle::before"]: {
"-webkit-text-stroke-color": skin.outline
}
}))
}
}
this.style.appendChild(document.createTextNode(css.join("\n")))
loader.screen.appendChild(this.style)
}
normalizeString(string){
string = string
.replace('', '\'').replace('“', '"').replace('”', '"')
.replace('。', '.').replace('', ',').replace('、', ',')
kanaPairs.forEach(pair => {
string = string.replace(pair[1], pair[0])
})
return string.normalize("NFKD").replace(/[\u0300-\u036f]/g, "")
}
perform(query){
var results = []
var filters = {}
var querySplit = query.split(" ").filter(word => {
if(word.length > 0){
var parts = word.toLowerCase().split(":")
if(parts.length > 1){
switch(parts[0]){
case "easy":
case "normal":
case "hard":
case "oni":
case "ura":
var range = this.parseRange(parts[1])
if(range){
filters[parts[0]] = range
}
break
case "extreme":
var range = this.parseRange(parts[1])
if(range){
filters.oni = this.parseRange(parts[1])
}
break
case "clear":
case "silver":
case "gold":
case "genre":
case "lyrics":
case "creative":
case "played":
case "maker":
case "diverge":
case "random":
case "all":
filters[parts[0]] = parts[1]
break
default:
return true
}
return false
}
}
return true
})
query = this.normalizeString(querySplit.join(" ").trim())
var totalFilters = Object.keys(filters).length
var random = false
var allResults = false
for(var i = 0; i < assets.songs.length; i++){
var song = assets.songs[i]
var passedFilters = 0
Object.keys(filters).forEach(filter => {
var value = filters[filter]
switch(filter){
case "easy":
case "normal":
case "hard":
case "oni":
case "ura":
if(song.courses[filter] && song.courses[filter].stars >= value.min && song.courses[filter].stars <= value.max){
passedFilters++
}
break
case "clear":
case "silver":
case "gold":
if(value === "any"){
var score = scoreStorage.scores[song.hash]
scoreStorage.difficulty.forEach(difficulty => {
if(score && score[difficulty] && score[difficulty].crown && (filter === "clear" || score[difficulty].crown === filter)){
passedFilters++
}
})
} else {
var score = scoreStorage.scores[song.hash]
if(score && score[value] && score[value].crown && (filter === "clear" || score[value].crown === filter)){
passedFilters++
}
}
break
case "played":
var score = scoreStorage.scores[song.hash]
if((value === "yes" && score) || (value === "no" && !score)){
passedFilters++
}
break
case "lyrics":
if((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)){
passedFilters++
}
break
case "creative":
if((value === "yes" && song.maker) || (value === "no" && !song.maker)){
passedFilters++
}
break
case "maker":
if(song.maker && song.maker.name.toLowerCase().includes(value.toLowerCase())){
passedFilters++
}
break
case "genre":
var cat = assets.categories.find(cat => cat.id === song.category_id)
var aliases = cat.aliases ? cat.aliases.concat([cat.title]) : [cat.title]
if(aliases.find(alias => alias.toLowerCase() === value.toLowerCase())){
passedFilters++
}
break
case "diverge":
var branch = Object.values(song.courses).find(course => course && course.branch)
if((value === "yes" && branch) || (value === "no" && !branch)){
passedFilters++
}
break
case "random":
if(value === "yes" || value === "no"){
random = value === "yes"
passedFilters++
}
break
case "all":
if(value === "yes" || value === "no"){
allResults = value === "yes"
passedFilters++
}
break
}
})
if(passedFilters === totalFilters){
results.push(song)
}
}
var maxResults = allResults ? Infinity : (totalFilters > 0 && !query ? 100 : 50)
if(query){
results = fuzzysort.go(query, results, {
keys: ["titlePrepared", "subtitlePrepared"],
allowTypo: true,
limit: maxResults,
scoreFn: a => {
if(a[0]){
var score0 = a[0].score
a[0].ranges = this.indexesToRanges(a[0].indexes)
if(a[0].indexes.length > 1){
var rangeAmount = a[0].ranges.length
var lastIdx = -3
a[0].ranges.forEach(range => {
if(range[0] - lastIdx <= 2){
rangeAmount--
score0 -= 1000
}
lastIdx = range[1]
})
var index = a[0].target.toLowerCase().indexOf(query)
if(index !== -1){
a[0].ranges = [[index, index + query.length - 1]]
}else if(rangeAmount > a[0].indexes.length / 2){
score0 = -Infinity
a[0].ranges = null
}else if(rangeAmount !== 1){
score0 -= 9000
}
}
}
if(a[1]){
var score1 = a[1].score - 1000
a[1].ranges = this.indexesToRanges(a[1].indexes)
if(a[1].indexes.length > 1){
var rangeAmount = a[1].ranges.length
var lastIdx = -3
a[1].ranges.forEach(range => {
if(range[0] - lastIdx <= 2){
rangeAmount--
score1 -= 1000
}
lastIdx = range[1]
})
var index = a[1].target.indexOf(query)
if(index !== -1){
a[1].ranges = [[index, index + query.length - 1]]
}else if(rangeAmount > a[1].indexes.length / 2){
score1 = -Infinity
a[1].ranges = null
}else if(rangeAmount !== 1){
score1 -= 9000
}
}
}
if(random){
var rand = Math.random() * -9000
if(score0 !== -Infinity){
score0 = rand
}
if(score1 !== -Infinity){
score1 = rand
}
}
if(a[0]){
return a[1] ? Math.max(score0, score1) : score0
}else{
return a[1] ? score1 : -Infinity
}
}
})
}else{
if(random){
for(var i = results.length - 1; i > 0; i--){
var j = Math.floor(Math.random() * (i + 1))
var temp = results[i]
results[i] = results[j]
results[j] = temp
}
}
results = results.slice(0, maxResults).map(result => {
return {obj: result}
})
}
return results
}
createResult(result, resultWidth, fontSize){
var song = result.obj
var title = this.songSelect.getLocalTitle(song.title, song.title_lang)
var subtitle = this.songSelect.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang)
var id = "default"
if(song.category_id){
var cat = assets.categories.find(cat => cat.id === song.category_id)
if(cat && "id" in cat){
id = "cat" + cat.id
}
}
var resultDiv = document.createElement("div")
resultDiv.classList.add("song-search-result", "song-search-" + id)
resultDiv.dataset.songId = song.id
var resultInfoDiv = document.createElement("div")
resultInfoDiv.classList.add("song-search-result-info")
var resultInfoTitle = document.createElement("span")
resultInfoTitle.classList.add("song-search-result-title")
resultInfoTitle.appendChild(this.highlightResult(title, result[0]))
resultInfoTitle.setAttribute("alt", title)
resultInfoDiv.appendChild(resultInfoTitle)
if(subtitle){
resultInfoDiv.appendChild(document.createElement("br"))
var resultInfoSubtitle = document.createElement("span")
resultInfoSubtitle.classList.add("song-search-result-subtitle")
resultInfoSubtitle.appendChild(this.highlightResult(subtitle, result[1]))
resultInfoSubtitle.setAttribute("alt", subtitle)
resultInfoDiv.appendChild(resultInfoSubtitle)
}
resultDiv.appendChild(resultInfoDiv)
var courses = ["easy", "normal", "hard", "oni", "ura"]
courses.forEach(course => {
var courseDiv = document.createElement("div")
courseDiv.classList.add("song-search-result-course", "song-search-result-" + course)
if (song.courses[course]) {
var crown = "noclear"
if (scoreStorage.scores[song.hash]) {
if (scoreStorage.scores[song.hash][course]) {
crown = scoreStorage.scores[song.hash][course].crown || "noclear"
}
}
var courseCrown = document.createElement("div")
courseCrown.classList.add("song-search-result-crown", "song-search-result-" + crown)
var courseStars = document.createElement("div")
courseStars.classList.add("song-search-result-stars")
courseStars.innerText = song.courses[course].stars + "\u2605"
courseDiv.appendChild(courseCrown)
courseDiv.appendChild(courseStars)
} else {
courseDiv.classList.add("song-search-result-hidden")
}
resultDiv.appendChild(courseDiv)
})
this.songSelect.ctx.font = (1.2 * fontSize) + "px " + strings.font
var titleWidth = this.songSelect.ctx.measureText(title).width
var titleRatio = resultWidth / titleWidth
if(titleRatio < 1){
resultInfoTitle.style.transform = "scale(" + titleRatio + ", 1)"
}
if(subtitle){
this.songSelect.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font
var subtitleWidth = this.songSelect.ctx.measureText(subtitle).width
var subtitleRatio = resultWidth / subtitleWidth
if(subtitleRatio < 1){
resultInfoSubtitle.style.transform = "scale(" + subtitleRatio + ", 1)"
}
}
return resultDiv
}
highlightResult(text, result){
var fragment = document.createDocumentFragment()
var ranges = (result ? result.ranges : null) || []
var lastIdx = 0
ranges.forEach(range => {
if(lastIdx !== range[0]){
fragment.appendChild(document.createTextNode(text.slice(lastIdx, range[0])))
}
var span = document.createElement("span")
span.classList.add("highlighted-text")
span.innerText = text.slice(range[0], range[1] + 1)
fragment.appendChild(span)
lastIdx = range[1] + 1
})
if(text.length !== lastIdx){
fragment.appendChild(document.createTextNode(text.slice(lastIdx)))
}
return fragment
}
setActive(idx){
this.songSelect.playSound("se_ka")
var active = this.div.querySelector(":scope .song-search-result-active")
if(active){
active.classList.remove("song-search-result-active")
}
if(idx === null){
this.active = null
return
}
var el = this.results[idx]
this.input.blur()
el.classList.add("song-search-result-active")
this.scrollTo(el)
this.active = idx
}
display(fromButton=false){
if(!this.enabled){
return
}
if(this.opened){
return this.remove(true)
}
this.opened = true
this.results = []
this.div = document.createElement("div")
this.div.innerHTML = assets.pages["search"]
this.container = this.div.querySelector(":scope #song-search-container")
if(this.touchEnabled){
this.container.classList.add("touch-enabled")
}
pageEvents.add(this.container, ["mousedown", "touchstart"], this.onClick.bind(this))
this.input = this.div.querySelector(":scope #song-search-input")
this.input.setAttribute("placeholder", strings.search.searchInput)
pageEvents.add(this.input, ["input"], () => this.onInput())
this.songSelect.playSound("se_pause")
loader.screen.appendChild(this.div)
this.setTip()
cancelTouch = false
noResizeRoot = true
if(this.songSelect.songs[this.songSelect.selectedSong].courses){
snd.previewGain.setVolumeMul(0.5)
}else if(this.songSelect.bgmEnabled){
snd.musicGain.setVolumeMul(0.5)
}
setTimeout(() => {
this.input.focus()
this.input.setSelectionRange(0, this.input.value.length)
}, 10)
var lastQuery = localStorage.getItem("lastSearchQuery")
if(lastQuery){
this.input.value = lastQuery
this.input.dispatchEvent(new Event("input", {
value: lastQuery
}))
}
}
remove(byUser=false){
if(this.opened){
this.opened = false
if(byUser){
this.songSelect.playSound("se_cancel")
}
pageEvents.remove(this.div.querySelector(":scope #song-search-container"),
["mousedown", "touchstart"])
pageEvents.remove(this.input, ["input"])
this.div.remove()
delete this.results
delete this.div
delete this.input
delete this.tip
delete this.active
cancelTouch = true
noResizeRoot = false
if(this.songSelect.songs[this.songSelect.selectedSong].courses){
snd.previewGain.setVolumeMul(1)
}else if(this.songSelect.bgmEnabled){
snd.musicGain.setVolumeMul(1)
}
}
}
setTip(tip, error=false){
if(this.tip){
this.tip.remove()
delete this.tip
}
if(!tip){
tip = strings.search.tip + " " + strings.search.tips[Math.floor(Math.random() * strings.search.tips.length)]
}
var resultsDiv = this.div.querySelector(":scope #song-search-results")
resultsDiv.innerHTML = ""
this.results = []
this.tip = document.createElement("div")
this.tip.id = "song-search-tip"
this.tip.innerText = tip
this.div.querySelector(":scope #song-search").appendChild(this.tip)
if(error){
this.tip.classList.add("song-search-tip-error")
}
}
proceed(songId){
if (/^-?\d+$/.test(songId)) {
songId = parseInt(songId)
}
var song = this.songSelect.songs.find(song => song.id === songId)
this.remove()
this.songSelect.playBgm(false)
if(this.songSelect.previewing === "muted"){
this.songSelect.previewing = null
}
var songIndex = this.songSelect.songs.findIndex(song => song.id === songId)
this.songSelect.setSelectedSong(songIndex)
this.songSelect.toSelectDifficulty()
}
scrollTo(element){
var parentNode = element.parentNode
var selected = element.getBoundingClientRect()
var parent = parentNode.getBoundingClientRect()
var scrollY = parentNode.scrollTop
var selectedPosTop = selected.top - selected.height / 2
if(Math.floor(selectedPosTop) < Math.floor(parent.top)){
parentNode.scrollTop += selectedPosTop - parent.top
}else{
var selectedPosBottom = selected.top + selected.height * 1.5 - parent.top
if(Math.floor(selectedPosBottom) > Math.floor(parent.height)){
parentNode.scrollTop += selectedPosBottom - parent.height
}
}
}
parseRange(string){
var range = string.split("-")
if(range.length == 1){
var min = parseInt(range[0]) || 0
return min > 0 ? {min: min, max: min} : false
} else if(range.length == 2){
var min = parseInt(range[0]) || 0
var max = parseInt(range[1]) || 0
return min > 0 && max > 0 ? {min: min, max: max} : false
}
}
indexesToRanges(indexes){
var ranges = []
var range
indexes.forEach(idx => {
if(range && range[1] === idx - 1){
range[1] = idx
}else{
range = [idx, idx]
ranges.push(range)
}
})
return ranges
}
onInput(resize){
var text = this.input.value
localStorage.setItem("lastSearchQuery", text)
text = text.toLowerCase()
if(text.length === 0){
if(!resize){
this.setTip()
}
return
}
var new_results = this.perform(text)
if(new_results.length === 0){
this.setTip(strings.search.noResults, true)
return
}else if(this.tip){
this.tip.remove()
delete this.tip
}
var resultsDiv = this.div.querySelector(":scope #song-search-results")
resultsDiv.innerHTML = ""
this.results = []
var fontSize = parseFloat(getComputedStyle(this.div.querySelector(":scope #song-search")).fontSize.slice(0, -2))
var resultsWidth = parseFloat(getComputedStyle(resultsDiv).width.slice(0, -2))
var vmin = Math.min(innerWidth, lastHeight) / 100
var courseWidth = Math.min(3 * fontSize * 1.2, 7 * vmin)
var resultWidth = resultsWidth - 1.8 * fontSize - 0.8 * fontSize - (courseWidth + 0.4 * fontSize * 1.2) * 5 - 0.6 * fontSize
this.songSelect.ctx.save()
var fragment = document.createDocumentFragment()
new_results.forEach(result => {
var result = this.createResult(result, resultWidth, fontSize)
fragment.appendChild(result)
this.results.push(result)
})
resultsDiv.appendChild(fragment)
this.songSelect.ctx.restore()
}
onClick(e){
if((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1){
this.remove(true)
}else if(e.which === 1){
var songEl = e.target.closest(".song-search-result")
if(songEl){
var songId = songEl.dataset.songId
this.proceed(songId)
}
}
}
keyPress(pressed, name, event, repeat, ctrl){
if(name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) {
this.remove(true)
if(event){
event.preventDefault()
}
}else if(name === "down" && this.results.length){
if(this.input == document.activeElement && this.results){
this.setActive(0)
}else if(this.active === this.results.length - 1){
this.setActive(null)
this.input.focus()
}else if(Number.isInteger(this.active)){
this.setActive(this.active + 1)
}else{
this.setActive(0)
}
}else if(name === "up" && this.results.length){
if(this.input == document.activeElement && this.results){
this.setActive(this.results.length - 1)
}else if(this.active === 0){
this.setActive(null)
this.input.focus()
setTimeout(() => {
this.input.setSelectionRange(this.input.value.length, this.input.value.length)
}, 0)
}else if(Number.isInteger(this.active)){
this.setActive(this.active - 1)
}else{
this.setActive(this.results.length - 1)
}
}else if(name === "confirm"){
if(Number.isInteger(this.active)){
this.proceed(this.results[this.active].dataset.songId)
}else{
this.onInput()
if(event.keyCode === 13 && this.songSelect.touchEnabled){
this.input.blur()
}
}
}
}
redraw(){
if(this.opened && this.container){
var vmin = Math.min(innerWidth, lastHeight) / 100
if(this.vmin !== vmin){
this.container.style.setProperty("--vmin", vmin + "px")
this.vmin = vmin
}
}else{
this.vmin = null
}
}
clean(){
loader.screen.removeChild(this.style)
fuzzysort.cleanup()
delete this.container
delete this.style
delete this.songSelect
}
}

92
public/src/js/session.js Normal file
View File

@@ -0,0 +1,92 @@
class Session{
constructor(...args){
this.init(...args)
}
init(touchEnabled){
this.touchEnabled = touchEnabled
loader.changePage("session", true)
this.endButton = this.getElement("view-end-button")
if(touchEnabled){
this.getElement("view-outer").classList.add("touch-enabled")
}
this.sessionInvite = document.getElementById("session-invite")
var tutorialTitle = this.getElement("view-title")
tutorialTitle.innerText = strings.session.multiplayerSession
tutorialTitle.setAttribute("alt", strings.session.multiplayerSession)
this.sessionInvite.parentNode.insertBefore(document.createTextNode(strings.session.linkTutorial), this.sessionInvite)
this.endButton.innerText = strings.session.cancel
this.endButton.setAttribute("alt", strings.session.cancel)
pageEvents.add(window, ["mousedown", "touchstart"], this.mouseDown.bind(this))
this.keyboard = new Keyboard({
confirm: ["esc"]
}, this.keyPress.bind(this))
this.gamepad = new Gamepad({
confirm: ["start", "b", "ls", "rs"]
}, this.keyPress.bind(this))
p2.hashLock = true
pageEvents.add(p2, "message", response => {
if(response.type === "invite"){
this.sessionInvite.innerText = location.origin + location.pathname + "#" + response.value
p2.hash(response.value)
}else if(response.type === "songsel"){
p2.clearMessage("users")
this.onEnd(true)
pageEvents.send("session-start", "host")
}
})
p2.send("invite", {
id: null,
name: account.loggedIn ? account.displayName : null,
don: account.loggedIn ? account.don : null
})
pageEvents.send("session")
}
getElement(name){
return loader.screen.getElementsByClassName(name)[0]
}
mouseDown(event){
if(event.type === "mousedown" && event.which !== 1){
return
}
if(event.target === this.sessionInvite){
this.sessionInvite.focus()
}else{
getSelection().removeAllRanges()
this.sessionInvite.blur()
}
if(event.target === this.endButton){
this.onEnd()
}
}
keyPress(pressed){
if(pressed){
this.onEnd()
}
}
onEnd(fromP2){
if(!p2.session){
p2.send("leave")
p2.hash("")
p2.hashLock = false
pageEvents.send("session-cancel")
}else if(!fromP2){
return p2.send("songsel")
}
this.clean()
assets.sounds["se_don"].play()
setTimeout(() => {
new SongSelect(false, false, this.touchEnabled)
}, 500)
}
clean(){
this.keyboard.clean()
this.gamepad.clean()
pageEvents.remove(window, ["mousedown", "touchstart"])
pageEvents.remove(p2, "message")
delete this.endButton
delete this.sessionInvite
}
}

1313
public/src/js/settings.js Normal file

File diff suppressed because it is too large Load Diff

3277
public/src/js/songselect.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,246 @@
class SoundBuffer{
constructor(...args){
this.init(...args)
}
init(){
var AudioContext = window.AudioContext || window.webkitAudioContext
this.context = new AudioContext()
this.audioDecoder = this.context.decodeAudioData.bind(this.context)
this.oggDecoder = this.audioDecoder
pageEvents.add(window, ["click", "touchend", "keypress"], this.pageClicked.bind(this))
this.gainList = []
}
load(file, gain){
var decoder = file.name.endsWith(".ogg") ? this.oggDecoder : this.audioDecoder
return file.arrayBuffer().then(response => {
return new Promise((resolve, reject) => {
return decoder(response, resolve, reject)
}).catch(error => Promise.reject([error, file.url]))
}).then(buffer => {
return new Sound(gain || {soundBuffer: this}, buffer)
})
}
createGain(channel){
var gain = new SoundGain(this, channel)
this.gainList.push(gain)
return gain
}
setCrossfade(gain1, gain2, median){
if(!Array.isArray(gain1)){
gain1 = [gain1]
}
if(!Array.isArray(gain2)){
gain2 = [gain2]
}
gain1.forEach(gain => gain.setCrossfade(1 - median))
gain2.forEach(gain => gain.setCrossfade(median))
}
getTime(){
return this.context.currentTime
}
convertTime(time, absolute){
time = (time || 0)
if(time < 0){
time = 0
}
return time + (absolute ? 0 : this.getTime())
}
createSource(sound){
var source = this.context.createBufferSource()
source.buffer = sound.buffer
source.connect(sound.gain.gainNode || this.context.destination)
return source
}
pageClicked(){
if(this.context.state === "suspended"){
this.context.resume()
}
}
saveSettings(){
for(var i = 0; i < this.gainList.length; i++){
var gain = this.gainList[i]
gain.defaultVol = gain.volume
}
}
loadSettings(){
for(var i = 0; i < this.gainList.length; i++){
var gain = this.gainList[i]
gain.setVolume(gain.defaultVol)
}
}
fallbackDecoder(buffer, resolve, reject){
Oggmented().then(oggmented => oggmented.decodeOggData(buffer, resolve, reject), reject)
}
}
class SoundGain{
constructor(...args){
this.init(...args)
}
init(soundBuffer, channel){
this.soundBuffer = soundBuffer
this.gainNode = soundBuffer.context.createGain()
if(channel){
var index = channel === "left" ? 0 : 1
this.merger = soundBuffer.context.createChannelMerger(2)
this.merger.connect(soundBuffer.context.destination)
this.gainNode.connect(this.merger, 0, index)
}else{
this.gainNode.connect(soundBuffer.context.destination)
}
this.setVolume(1)
}
load(url){
return this.soundBuffer.load(url, this)
}
convertTime(time, absolute){
return this.soundBuffer.convertTime(time, absolute)
}
setVolume(amount){
this.gainNode.gain.value = amount * amount
this.volume = amount
}
setVolumeMul(amount){
this.setVolume(amount * this.defaultVol)
}
setCrossfade(amount){
this.setVolume(Math.sqrt(Math.sin(Math.PI / 2 * amount)))
}
fadeIn(duration, time, absolute){
this.fadeVolume(0, this.volume * this.volume, duration, time, absolute)
}
fadeOut(duration, time, absolute){
this.fadeVolume(this.volume * this.volume, 0, duration, time, absolute)
}
fadeVolume(vol1, vol2, duration, time, absolute){
time = this.convertTime(time, absolute)
this.gainNode.gain.linearRampToValueAtTime(vol1, time)
this.gainNode.gain.linearRampToValueAtTime(vol2, time + (duration || 0))
}
mute(){
this.gainNode.gain.value = 0
}
unmute(){
this.setVolume(this.volume)
}
}
class Sound{
constructor(...args){
this.init(...args)
}
init(gain, buffer){
this.gain = gain
this.buffer = buffer
this.soundBuffer = gain.soundBuffer
this.duration = buffer.duration
this.timeouts = new Set()
this.sources = new Set()
}
copy(gain){
return new Sound(gain || this.gain, this.buffer)
}
getTime(){
return this.soundBuffer.getTime()
}
convertTime(time, absolute){
return this.soundBuffer.convertTime(time, absolute)
}
setTimeouts(time){
return new Promise(resolve => {
var relTime = time - this.getTime()
if(relTime > 0){
var timeout = setTimeout(() => {
this.timeouts.delete(timeout)
resolve()
}, relTime * 1000)
this.timeouts.add(timeout)
}else{
resolve()
}
})
}
clearTimeouts(){
this.timeouts.forEach(timeout => {
clearTimeout(timeout)
this.timeouts.delete(timeout)
})
}
playLoop(time, absolute, seek1, seek2, until){
time = this.convertTime(time, absolute)
seek1 = seek1 || 0
if(typeof seek2 === "undefined"){
seek2 = seek1
}
until = until || this.duration
if(seek1 >= until || seek2 >= until){
return
}
this.loop = {
started: time + until - seek1,
seek: seek2,
until: until
}
this.play(time, true, seek1, until)
this.addLoop()
this.loop.interval = setInterval(() => {
this.addLoop()
}, 100)
}
addLoop(){
while(this.getTime() > this.loop.started - 1){
this.play(this.loop.started, true, this.loop.seek, this.loop.until)
this.loop.started += this.loop.until - this.loop.seek
}
}
play(time, absolute, seek, until){
time = this.convertTime(time, absolute)
var source = this.soundBuffer.createSource(this)
seek = seek || 0
until = until || this.duration
this.setTimeouts(time).then(() => {
this.cfg = {
started: time,
seek: seek,
until: until
}
})
source.start(time, Math.max(0, seek || 0), Math.max(0, until - seek))
source.startTime = time
this.sources.add(source)
source.onended = () => {
this.sources.delete(source)
}
}
stop(time, absolute){
time = this.convertTime(time, absolute)
this.sources.forEach(source => {
try{
source.stop(Math.max(source.startTime, time))
}catch(e){}
})
this.setTimeouts(time).then(() => {
if(this.loop){
clearInterval(this.loop.interval)
}
this.clearTimeouts()
})
}
pause(time, absolute){
if(this.cfg){
time = this.convertTime(time, absolute)
this.stop(time, true)
this.cfg.pauseSeek = time - this.cfg.started + this.cfg.seek
}
}
resume(time, absolute){
if(this.cfg){
if(this.loop){
this.playLoop(time, absolute, this.cfg.pauseSeek, this.loop.seek, this.loop.until)
}else{
this.play(time, absolute, this.cfg.pauseSeek, this.cfg.until)
}
}
}
clean(){
delete this.buffer
}
}

1499
public/src/js/strings.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
class Titlescreen{
constructor(...args){
this.init(...args)
}
init(songId){
this.songId = songId
db.getItem("customFolder").then(folder => this.customFolder = folder)
if(!songId){
loader.changePage("titlescreen", false)
loader.screen.style.backgroundImage = ""
this.titleScreen = document.getElementById("title-screen")
this.proceed = document.getElementById("title-proceed")
this.disclaimerText = document.getElementById("title-disclaimer-text")
this.disclaimerCopyright = document.getElementById("title-disclaimer-copyright")
this.logo = new Logo()
}
this.setLang(allStrings[settings.getItem("language")])
if(songId){
if(localStorage.getItem("tutorial") === "true"){
new SongSelect(false, false, this.touched, this.songId)
}else{
new SettingsView(false, true, this.songId)
}
}else{
pageEvents.add(this.titleScreen, ["mousedown", "touchstart"], event => {
if(event.type === "touchstart"){
event.preventDefault()
this.touched = true
}else if(event.type === "mousedown" && event.which !== 1){
return
}
this.onPressed(true)
})
assets.sounds["v_title"].play()
this.keyboard = new Keyboard({
confirm: ["enter", "space", "don_l", "don_r"]
}, this.onPressed.bind(this))
this.gamepad = new Gamepad({
gamepadConfirm: ["a", "b", "x", "y", "start", "ls", "rs"]
}, this.onPressed.bind(this))
if(p2.session){
pageEvents.add(p2, "message", response => {
if(response.type === "songsel"){
this.goNext(true)
}
})
}
pageEvents.send("title-screen")
}
}
onPressed(pressed, name){
if(pressed){
if(name === "gamepadConfirm" && (snd.buffer.context.state === "suspended" || this.customFolder)){
return
}
this.titleScreen.style.cursor = "auto"
this.clean()
if(!this.customFolder || assets.customSongs){
assets.sounds["se_don"].play()
}
this.goNext()
}
}
goNext(fromP2){
if(p2.session && !fromP2){
p2.send("songsel")
}else{
if(fromP2 || this.customFolder || localStorage.getItem("tutorial") === "true"){
if(this.touched){
localStorage.setItem("tutorial", "true")
}
pageEvents.remove(p2, "message")
if(this.customFolder && !fromP2 && !assets.customSongs){
var customSongs = new CustomSongs(this.touched, true, true)
var soundPlayed = false
var noError = true
var promises = []
var allFiles = []
this.customFolder.forEach(file => {
promises.push(customSongs.walkFilesystem(file, undefined, allFiles))
})
Promise.all(promises).then(() => {
assets.sounds["se_don"].play()
soundPlayed = true
return customSongs.importLocal(allFiles)
}).catch(() => {
localStorage.removeItem("customSelected")
db.removeItem("customFolder")
if(!soundPlayed){
assets.sounds["se_don"].play()
}
setTimeout(() => {
new SongSelect(false, false, this.touched, this.songId)
}, 500)
noError = false
}).then(() => {
if(noError){
setTimeout(() => {
new SongSelect("customSongs", false, this.touched)
}, 500)
}
})
}else{
setTimeout(() => {
new SongSelect(false, false, this.touched, this.songId)
}, 500)
}
}else{
setTimeout(() => {
new SettingsView(this.touched, true, this.songId)
}, 500)
}
}
}
setLang(lang, noEvent){
settings.setLang(lang, true)
if(this.songId){
return
}
this.proceed.innerText = strings.titleProceed
this.proceed.setAttribute("alt", strings.titleProceed)
this.disclaimerText.innerText = strings.titleDisclaimer
this.disclaimerText.setAttribute("alt", strings.titleDisclaimer)
this.disclaimerCopyright.innerText = strings.titleCopyright
this.disclaimerCopyright.setAttribute("alt", strings.titleCopyright)
this.logo.updateSubtitle()
}
clean(){
this.keyboard.clean()
this.gamepad.clean()
this.logo.clean()
assets.sounds["v_title"].stop()
pageEvents.remove(this.titleScreen, ["mousedown", "touchstart"])
delete this.titleScreen
delete this.proceed
delete this.titleDisclaimer
delete this.titleCopyright
}
}

184
public/src/js/tutorial.js Normal file
View File

@@ -0,0 +1,184 @@
class Tutorial{
constructor(...args){
this.init(...args)
}
init(fromSongSel, songId){
this.fromSongSel = fromSongSel
this.songId = songId
loader.changePage("tutorial", true)
assets.sounds["bgm_setsume"].playLoop(0.1, false, 0, 1.054, 16.054)
this.endButton = this.getElement("view-end-button")
this.tutorialTitle = this.getElement("view-title")
this.tutorialDiv = document.createElement("div")
this.getElement("view-content").appendChild(this.tutorialDiv)
this.items = []
this.items.push(this.endButton)
this.selected = this.items.length - 1
this.setStrings()
pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this))
this.keyboard = new Keyboard({
confirm: ["enter", "space", "don_l", "don_r"],
previous: ["left", "up", "ka_l"],
next: ["right", "down", "ka_r"],
back: ["escape"]
}, this.keyPressed.bind(this))
this.gamepad = new Gamepad({
"confirm": ["b", "ls", "rs"],
"previous": ["u", "l", "lb", "lt", "lsu", "lsl"],
"next": ["d", "r", "rb", "rt", "lsd", "lsr"],
"back": ["start", "a"]
}, this.keyPressed.bind(this))
pageEvents.send("tutorial")
}
getElement(name){
return loader.screen.getElementsByClassName(name)[0]
}
keyPressed(pressed, name){
if(!pressed){
return
}
var selected = this.items[this.selected]
if(name === "confirm"){
if(selected === this.endButton){
this.onEnd()
}else{
this.getLink(selected).click()
assets.sounds["se_don"].play()
}
}else if(name === "previous" || name === "next"){
if(this.items.length >= 2){
selected.classList.remove("selected")
this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1))
this.items[this.selected].classList.add("selected")
assets.sounds["se_ka"].play()
}
}else if(name === "back"){
this.onEnd()
}
}
mod(length, index){
return ((index % length) + length) % length
}
onEnd(event){
var touched = false
if(event){
if(event.type === "touchstart"){
event.preventDefault()
touched = true
}else if(event.which !== 1){
return
}
}
this.clean()
assets.sounds["se_don"].play()
try{
localStorage.setItem("tutorial", "true")
}catch(e){}
setTimeout(() => {
new SongSelect(this.fromSongSel ? "tutorial" : false, false, touched, this.songId)
}, 500)
}
getLink(target){
return target.getElementsByTagName("a")[0]
}
linkButton(event){
if(event.target === event.currentTarget && (event.type === "touchstart" || event.which === 1)){
this.getLink(event.currentTarget).click()
assets.sounds["se_don"].play()
}
}
insertText(text, parent){
parent.appendChild(document.createTextNode(text))
}
insertKey(key, parent){
if(!Array.isArray(key)){
key = [key]
}
var join = true
for(var i = 0; i < key.length; i++){
if(key[i] === false){
join = false
continue
}
if(i !== 0){
if(join){
var span = document.createElement("span")
span.classList.add("key-join")
span.innerText = strings.tutorial.key.join
parent.appendChild(span)
}else{
parent.appendChild(document.createTextNode(strings.tutorial.key.or))
}
}
var kbd = document.createElement("kbd")
kbd.innerText = key[i]
parent.appendChild(kbd)
}
}
setStrings(){
this.tutorialTitle.innerText = strings.howToPlay
this.tutorialTitle.setAttribute("alt", strings.howToPlay)
this.endButton.innerText = strings.tutorial.ok
this.endButton.setAttribute("alt", strings.tutorial.ok)
this.tutorialDiv.innerHTML = ""
var kbdSettings = settings.getItem("keyboardSettings")
var pauseKey = [strings.tutorial.key.esc]
if(pageEvents.kbd.indexOf("q") === -1){
pauseKey.push(false)
pauseKey.push("Q")
}
var keys = [
kbdSettings.don_l[0].toUpperCase(),
kbdSettings.don_r[0].toUpperCase(),
kbdSettings.ka_l[0].toUpperCase(),
kbdSettings.ka_r[0].toUpperCase(),
pauseKey,
[strings.tutorial.key.shift, strings.tutorial.key.leftArrow],
[strings.tutorial.key.shift, strings.tutorial.key.rightArrow],
strings.tutorial.key.shift,
strings.tutorial.key.ctrl
]
var keyIndex = 0
strings.tutorial.basics.forEach(string => {
var par = document.createElement("p")
var stringKeys = string.split("%s")
stringKeys.forEach((stringKey, i) => {
if(i !== 0){
this.insertKey(keys[keyIndex++], par)
}
this.insertText(stringKey, par)
})
this.tutorialDiv.appendChild(par)
})
var par = document.createElement("p")
var span = document.createElement("span")
span.style.fontWeight = "bold"
span.innerText = strings.tutorial.otherControls
par.appendChild(span)
strings.tutorial.otherTutorial.forEach(string => {
par.appendChild(document.createElement("br"))
var stringKeys = string.split("%s")
stringKeys.forEach((stringKey, i) => {
if(i !== 0){
this.insertKey(keys[keyIndex++], par)
}
this.insertText(stringKey, par)
})
})
this.tutorialDiv.appendChild(par)
}
clean(){
this.keyboard.clean()
this.gamepad.clean()
pageEvents.remove(this.endButton, ["mousedown", "touchstart"])
assets.sounds["bgm_setsume"].stop()
delete this.tutorialTitle
delete this.endButton
delete this.tutorialDiv
}
}

2269
public/src/js/view.js Normal file

File diff suppressed because it is too large Load Diff

190
public/src/js/viewassets.js Normal file
View File

@@ -0,0 +1,190 @@
class ViewAssets{
constructor(...args){
this.init(...args)
}
init(view){
this.view = view
this.controller = this.view.controller
this.allAssets = []
this.ctx = this.view.ctx
// Background
this.don = this.createAsset("background", frame => {
var imgw = 360
var imgh = 184
var w = imgw
var h = imgh
return {
sx: Math.floor(frame / 10) * (imgw + 2),
sy: (frame % 10) * (imgh + 2),
sw: imgw,
sh: imgh,
x: view.portrait ? -60 : 0,
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", [
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 ,
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,4 ,3 ,2 ,1 ,
0 ,0 ,0 ,0 ,1 ,2 ,3 ,4 ,5 ,6 ,6 ,5 ,7 ,8 ,9 ,10,
11,11,11,11,10,9 ,8 ,7 ,13,12,12,13,14,15,16,17
], "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.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")
this.don.setUpdateSpeed(4 / length)
this.don.setAnimation("gogo")
}else{
var score = this.controller.getGlobalScore()
var cleared = this.controller.game.rules.clearReached(score.gauge)
if(cleared){
this.don.setAnimationStart(0)
var length = this.don.getAnimationLength("clear")
this.don.setUpdateSpeed(2 / length)
this.don.setAnimation("clear")
}else{
this.don.setAnimationStart(0)
var length = this.don.getAnimationLength("normal")
this.don.setUpdateSpeed(4 / length)
this.don.setAnimation("normal")
}
}
}
this.don.addFrames("clear", 30, "don_anim_clear", this.controller.don)
this.don.normalAnimation()
// Bar
this.fire = this.createAsset("bar", frame => {
var imgw = 360
var imgh = 370
var scale = 130
var ms = this.view.getMS()
var elapsed = ms - this.view.gogoTimeStarted
var mul = this.view.slotPos.size / 106
var barH = 130 * mul
if(this.view.gogoTime){
var grow = 3 - Math.min(200, elapsed) / 100
this.ctx.globalAlpha = Math.min(200, elapsed) / 200
}else{
var grow = 1 - Math.min(100, elapsed) / 100
}
var w = (barH * imgw) / scale * grow
var h = (barH * imgh) / scale * grow
this.ctx.globalCompositeOperation = "lighter"
return {
sx: frame * imgw,
sy: 0,
sw: imgw,
sh: imgh,
x: this.view.slotPos.x - w / 2,
y: this.view.slotPos.y - h / 2,
w: w,
h: h
}
})
this.fire.addFrames("normal", 7, "fire_anim")
this.fire.setUpdateSpeed(1 / 8)
// Notes
this.explosion = this.createAsset("notes", frame => {
var w = 222
var h = 222
var mul = this.view.slotPos.size / 106
this.ctx.globalCompositeOperation = "screen"
var alpha = 1
if(this.explosion.type < 2){
if(frame < 2){
mul *= 1 - (frame + 1) * 0.2
}else if(frame > 9){
alpha = Math.max(0, 1 - (frame - 10) / 4)
}
}else if(frame > 5){
alpha = 0.5
}
if(alpha < 1 && !this.controller.touchEnabled){
this.ctx.globalAlpha = alpha
}
return {
sx: this.explosion.type * w,
sy: Math.min(3, Math.floor(frame / 2)) * h,
sw: w,
sh: h,
x: this.view.slotPos.x - w * mul / 2,
y: this.view.slotPos.y - h * mul / 2,
w: w * mul,
h: h * mul
}
})
this.explosion.type = null
this.explosion.addFrames("normal", 14, "notes_explosion")
this.explosion.setUpdateSpeed(1, true)
// Foreground
this.fireworks = []
for(let i = 0; i < 5 ; i++){
var fireworksAsset = this.createAsset("foreground", frame => {
var imgw = 230
var imgh = 460
var scale = 165
var w = imgw
var h = imgh
var winW = this.view.winW / this.view.ratio
var winH = this.view.winH / this.view.ratio
return {
sx: Math.floor(frame / 4) * imgw,
sy: (frame % 4) * imgh,
sw: imgw,
sh: imgh,
x: winW / 4 * i - w / 2 * (i / 2),
y: winH - h,
w: w,
h: h
}
})
fireworksAsset.addFrames("normal", 30, "fireworks_anim")
fireworksAsset.setUpdateSpeed(1 / 16)
this.fireworks.push(fireworksAsset)
}
this.changeBeatInterval(this.view.beatInterval, true)
}
createAsset(layer, position){
var asset = new CanvasAsset(this.view, layer, position)
this.allAssets.push(asset)
return asset
}
drawAssets(layer){
this.allAssets.forEach(asset => {
if(layer === asset.layer){
asset.draw()
}
})
}
changeBeatInterval(beatMS, initial){
this.allAssets.forEach(asset => {
asset.changeBeatInterval(beatMS, initial)
})
}
clean(){
if(this.don){
this.don.clean()
}
delete this.ctx
delete this.don
delete this.fire
delete this.fireworks
delete this.allAssets
}
}

View File

@@ -0,0 +1,16 @@
<div class="view-outer">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content"></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>
<div id="link-email" class="taibtn stroke-sub link-btn">
<a></a>
</div>
</div>
<div class="view-end-button taibtn stroke-sub selected"></div>
</div>
</div>

View File

@@ -0,0 +1,53 @@
<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 class="center-buttons">
<div>
<div class="privacy-btn taibtn stroke-sub link-btn"></div>
</div>
</div>
</div>
<div id="diag-txt"></div>
<div class="left-buttons">
<div class="logout-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="save-btn taibtn stroke-sub selected"></div>
<div class="view-end-button taibtn stroke-sub"></div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<div class="view-outer">
<div class="view drag-bg">
<div class="view-title stroke-sub"></div>
<div class="view-content"></div>
<div class="center-buttons">
<div>
<div id="link-localfolder" class="taibtn stroke-sub link-btn"></div>
</div>
<div id="group-gdrive">
<div id="link-gdrivefolder" class="taibtn stroke-sub link-btn"></div>
<div id="link-gdriveaccount" class="taibtn stroke-sub link-btn"></div>
</div>
</div>
<div class="left-buttons">
<div id="link-privacy" class="taibtn stroke-sub link-btn"></div>
</div>
<div class="view-end-button taibtn stroke-sub"></div>
<div class="view-outer shadow-outer" id="dropzone">
<div class="view">
<div class="view-content"></div>
</div>
</div>
<div class="view-outer shadow-outer" id="customsongs-error">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content"></div>
<div class="view-end-button taibtn stroke-sub selected"></div>
</div>
</div>
</div>
<form><input id="browse" type="file" webkitdirectory multiple></form>
</div>

View File

@@ -0,0 +1,39 @@
<div class="title stroke-sub" alt="Debug">Debug</div>
<div class="minimise"></div>
<div class="content">
<div>Song offset:</div>
<div class="offset input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div>
<div>Starting measure:</div>
<div class="measure-num input-slider">
<span class="reset">x</span><input type="text" value="" readonly><span class="minus">-</span><span class="plus">+</span>
</div>
<div class="branch-hide">
<div>Branch:</div>
<div class="branch-select select">
<span class="reset">x</span><select>
<option value="auto" selected style="background:#fff">Auto</option>
<option value="normal" style="background:#ccc">Normal</option>
<option value="advanced" style="background:#bdf">Professional</option>
<option value="master" style="background:#ebf">Master</option>
</select>
</div>
</div>
<div>Music volume:</div>
<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">
<div class="restart-btn">Restart song</div>
<div class="exit-btn">Exit debug</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<div id="game">
<div id="songbg">
<div id="layer1"></div>
<div id="layer2"></div>
</div>
<div id="song-stage"></div>
<div id="touch-drum">
<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>
</div>

View File

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

View File

@@ -0,0 +1,7 @@
<div id="load-song">
<div id="loading-song">
<div id="loading-don"></div>
<div class="loading-text stroke-sub" id="loading-text"></div>
<div id="p2-cancel-button" class="taibtn stroke-sub"></div>
</div>
</div>

View File

@@ -0,0 +1,26 @@
<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 class="privacy-btn taibtn stroke-sub link-btn"></div>
</div>
<div class="view-end-button taibtn stroke-sub selected"></div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div id="song-search-container">
<div id="song-search">
<div id="song-search-close" class="stroke-sub" alt="x">x</div>
<div id="song-search-bar">
<input type="search" id="song-search-input" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
<div id="song-search-results"></div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div class="view-outer">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content">
<div id="session-invite"></div>
</div>
<div class="view-end-button taibtn stroke-sub"></div>
</div>
</div>

View File

@@ -0,0 +1,34 @@
<div class="view-outer settings-outer">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content"></div>
<div class="left-buttons">
<div id="settings-default" class="taibtn stroke-sub"></div>
</div>
<div class="view-end-button taibtn stroke-sub"></div>
<div class="view-outer shadow-outer" id="settings-gamepad">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content">
<div class="setting-box">
<div id="gamepad-bg">
<div id="gamepad-value" class="stroke-sub"></div>
<div id="gamepad-buttons"></div>
</div>
</div>
</div>
<div class="view-end-button taibtn stroke-sub selected"></div>
</div>
</div>
<div class="view-outer shadow-outer" id="settings-latency">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content"></div>
<div class="left-buttons">
<div id="latency-default" class="taibtn stroke-sub"></div>
</div>
<div class="view-end-button taibtn stroke-sub"></div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,5 @@
<div id="song-select">
<canvas id="song-sel-canvas"></canvas>
<div id="song-sel-selectable" tabindex="1"></div>
<div id="touch-full-btn"></div>
</div>

View File

@@ -0,0 +1,8 @@
<div id="title-screen">
<canvas id="logo"></canvas>
<div class="click-to-continue stroke-sub" id="title-proceed"></div>
<div id="title-disclaimer">
<span class="stroke-sub" id="title-disclaimer-text"></span>
<span class="stroke-sub" id="title-disclaimer-copyright"></span>
</div>
</div>

View File

@@ -0,0 +1,7 @@
<div class="view-outer">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content"></div>
<div class="view-end-button taibtn stroke-sub selected"></div>
</div>
</div>