Bug fixes

- Change song select mouse wheel song scrolling to be instant
- Clicking on don chan in account settings toggles the animation
- If the music is too long for the chart, the results screen is shown earlier
- Fix weird BPM values freezing the browser (zero, negative, and very large)
- Add a warning to the page when JavaScript is disabled in the browser
- Fix Chrome auto dark mode by forcing light mode on the page
- Add a meta keywords tag to the page
- Fix plugin names getting cut off in the menu
- Delay the function editing of the EditFunction class in plugins to the start() function instead of load()
  - When stopping one of the plugins, all the plugins have to be stopped in reverse order and started again so that patched code of a stopped plugin does not linger around
- Fix importing plugins that have a SyntaxError
- Fix plugins getting the same internal name when added without one, causing them to not appear in the plugin settings
- Support editing args in EditFunction for plugins
- Prevent multiple websockets from being opened
- Fix page freezing after selecting Random song with no songs
- Fix the back button being repeated twice when there are no songs
- Fix /admin/users not accepting case insensitive usernames
- Pressing enter on the Delete Account field does the expected action instead of refreshing the page
- Better error message when custom folder access is denied
- Fix being able to start netplay in custom songs after refreshing the page (#383)
- Fix an error when importing songs from previous session and clicking on the white spot where you normally start multiplayer session
- Fix canvas elements becoming smaller than 1x1 resolution and crashing the game (#390)
- Fix song frame shadow cache on song select not being cleared when resizing the browser window, causing it to become blurry
- Fix a pause-restart error when you hit both confirm keys on the restart button
This commit is contained in:
KatieFrogs
2022-02-17 23:50:07 +03:00
parent eab03369c7
commit 0655b79293
21 changed files with 286 additions and 112 deletions

View File

@@ -201,6 +201,20 @@ kbd{
.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, transparent, #f6ead4 90%);
}
.view-content:not(:hover) .setting-box.selected .setting-name::after,
.setting-box:hover .setting-name::after{
background-image: linear-gradient(90deg, transparent, #ffb547 90%);
}
.setting-value{
display: flex;
background: #fff;
@@ -403,6 +417,7 @@ kbd{
}
.customdon-canvas{
width: 13em;
cursor: pointer;
}
.customdon-div label{
display: block;

View File

@@ -13,7 +13,7 @@ function filePermission(file){
if(response === "granted"){
return file
}else{
return Promise.reject(file)
return Promise.reject(strings.accessNotGrantedError)
}
})
}

View File

@@ -46,6 +46,8 @@ class Account{
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 = [
@@ -57,6 +59,7 @@ class Account{
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
@@ -120,6 +123,11 @@ class Account{
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()
@@ -148,6 +156,7 @@ class Account{
id: "bodyFill"
})
})
this.redrawForce = true
}
customdonReset(event){
if(event.type === "touchstart"){
@@ -162,12 +171,16 @@ class Account{
return
}
requestAnimationFrame(this.customdonRedrawBind)
if(!document.hasFocus()){
if(!document.hasFocus() || this.redrawPaused && !this.redrawForce){
return
}
var ms = new Date().getTime()
var ctx = this.customdonCtx
var frame = this.frames[Math.floor((ms - this.start) / 30) % this.frames.length]
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)
@@ -183,6 +196,7 @@ class Account{
sx, sy, w, h,
-26, 0, w, h
)
this.redrawForce = false
}
showDiv(event, div){
if(event){
@@ -318,6 +332,7 @@ class Account{
onFormPress(event){
event.stopPropagation()
if(event.type === "keypress" && event.keyCode === 13){
event.preventDefault()
if(this.mode === "account"){
this.onSave()
}else{
@@ -611,6 +626,7 @@ class Account{
}
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"])

View File

@@ -27,8 +27,8 @@ class CanvasCache{
this.h = h
this.lastW = 0
this.lastH = 0
this.canvas.width = this.w * this.scale
this.canvas.height = this.h * this.scale
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){

View File

@@ -157,7 +157,7 @@
ctx.fillRect(0, 0, w, h)
}
if(config.cached){
if(this.songFrameCache.w !== config.frameCache.w){
if(this.songFrameCache.w !== config.frameCache.w || this.songFrameCache.scale !== config.frameCache.ratio){
this.songFrameCache.resize(config.frameCache.w, config.frameCache.h, config.frameCache.ratio)
}
this.songFrameCache.get({
@@ -1680,8 +1680,8 @@
if(amount >= 1){
return callback(ctx)
}else if(amount >= 0){
this.tmpCanvas.width = winW || ctx.canvas.width
this.tmpCanvas.height = winH || ctx.canvas.height
this.tmpCanvas.width = Math.max(1, winW || ctx.canvas.width)
this.tmpCanvas.height = Math.max(1, winH || ctx.canvas.height)
callback(this.tmpCtx)
ctx.save()
ctx.globalAlpha = amount

View File

@@ -7,8 +7,8 @@ class CanvasTest{
var pixelRatio = window.devicePixelRatio || 1
var width = innerWidth * pixelRatio
var height = innerHeight * pixelRatio
this.canvas.width = width
this.canvas.height = height
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

View File

@@ -231,6 +231,9 @@ class Controller{
this.view.displayScore(score, notPlayed, bigNote)
}
songSelection(fadeIn, showWarning){
if(this.cleaned){
return
}
if(!fadeIn){
this.clean()
}
@@ -241,6 +244,9 @@ class Controller{
}
}
restartSong(){
if(this.cleaned){
return
}
this.clean()
if(this.multiplayer){
new LoadSong(this.selectedSong, false, true, this.touchEnabled)
@@ -363,6 +369,7 @@ class Controller{
return true
}
clean(){
this.cleaned = true
if(this.multiplayer === 1){
this.syncWith.clean()
}

View File

@@ -506,10 +506,10 @@ class Game{
p2.send("gameend")
}
this.musicFadeOut++
}else if(this.musicFadeOut === 2 && (ms >= started + 8600 && ms >= musicDuration + 250)){
}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 >= started + 9600 && ms >= musicDuration + 1250)){
}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()

View File

@@ -111,10 +111,7 @@
var plugin = plugins.add(obj.data, obj.name)
if(plugin){
pluginAmount++
plugins.imported.push({
name: plugin.name,
plugin: plugin
})
plugin.imported = true
startPromises.push(plugin.start())
}
})

View File

@@ -239,8 +239,8 @@ class LoadSong{
var canvas = document.createElement("canvas")
var w = Math.floor(img.width * scale)
var h = Math.floor(img.height * scale)
canvas.width = w
canvas.height = h
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 => {

View File

@@ -64,8 +64,8 @@
var pixelRatio = window.devicePixelRatio || 1
var winW = this.canvas.offsetWidth * pixelRatio
var winH = this.canvas.offsetHeight * pixelRatio
this.canvas.width = winW
this.canvas.height = winH
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"

View File

@@ -28,16 +28,18 @@ class P2Connection{
}
}
open(){
this.closed = false
var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:"
this.socket = new WebSocket(wsProtocol + "//" + location.host + "/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))
if(this.closed && !this.disabled){
this.closed = false
var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:"
this.socket = new WebSocket(wsProtocol + "//" + location.host + "/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")
@@ -46,8 +48,12 @@ class P2Connection{
}
}
close(){
this.closed = true
this.socket.close()
if(!this.closed){
this.closed = true
if(this.socket){
this.socket.close()
}
}
}
closeEvent(){
this.removeEventListener(onmessage)
@@ -250,4 +256,12 @@ class P2Connection{
this.notes.shift()
}
}
enable(){
this.disabled = false
this.open()
}
disable(){
this.disabled = true
this.close()
}
}

View File

@@ -3,10 +3,10 @@ class Plugins{
this.init(...args)
}
init(){
this.imported = []
this.allPlugins = []
this.pluginMap = {}
this.hashes = []
this.startOrder = []
}
add(script, name){
var hash = md5.base64(script.toString())
@@ -16,7 +16,7 @@ class Plugins{
}
name = name || "plugin"
var baseName = name
for(var i = 2; name in this.allPlugins; i++){
for(var i = 2; name in this.pluginMap; i++){
name = baseName + i.toString()
}
var plugin = new PluginLoader(script, name, hash)
@@ -37,10 +37,6 @@ class Plugins{
}
}
this.unload(name)
var index = this.imported.findIndex(obj => obj.name === name)
if(index !== -1){
this.imported.splice(index, 1)
}
var index = this.allPlugins.findIndex(obj => obj.name === name)
if(index !== -1){
this.allPlugins.splice(index, 1)
@@ -67,21 +63,33 @@ class Plugins{
this.pluginMap[name].stop()
}
stopAll(){
for(var i = this.allPlugins.length; i--;){
this.allPlugins[i].plugin.stop()
for(var i = this.startOrder.length; i--;){
this.pluginMap[this.startOrder[i]].stop()
}
}
unload(name){
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.imported.length; i--;){
this.imported[i].plugin.unload()
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()
}
}
}
@@ -130,9 +138,9 @@ class Plugins{
getItem: () => plugin.started,
setItem: value => {
if(plugin.started && !value){
plugin.stop()
this.stop(plugin.name)
}else if(!plugin.started && value){
plugin.start()
this.start(plugin.name)
}
}
}
@@ -188,10 +196,17 @@ class PluginLoader{
console.error(e)
this.error()
}
}, e => {
console.error(e)
this.error()
return Promise.resolve()
})
}
}
start(){
start(orderChange){
if(!orderChange){
plugins.startOrder.push(this.name)
}
return this.load().then(() => {
if(!this.started && this.module){
this.started = true
@@ -209,8 +224,18 @@ class PluginLoader{
}
})
}
stop(error){
stop(orderChange, error){
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){
@@ -225,12 +250,18 @@ class PluginLoader{
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(error)
this.stop(false, error)
}
this.loaded = false
plugins.remove(this.name)
@@ -267,7 +298,6 @@ class EditValue{
}
init(parent, name){
if(name){
this.original = parent[name]
this.name = [parent, name]
this.delete = !(name in parent)
}else{
@@ -275,18 +305,21 @@ class EditValue{
}
}
load(callback){
var output = callback(this.original)
if(typeof output === "undefined"){
throw new Error("A value is expected to be returned")
}
this.edited = output
this.loadCallback = callback
return this
}
start(){
if(this.name){
this.name[0][this.name[1]] = this.edited
this.original = this.name[0][this.name[1]]
}
return this.edited
var output = this.loadCallback(this.original)
if(typeof output === "undefined"){
throw new Error("A value is expected to be returned")
}
if(this.name){
this.name[0][this.name[1]] = output
}
return output
}
stop(){
if(this.name){
@@ -300,20 +333,26 @@ class EditValue{
}
unload(){
delete this.name
delete this.edited
delete this.original
delete this.loadCallback
}
}
class EditFunction extends EditValue{
load(callback){
var output = callback(plugins.strFromFunc(this.original))
start(){
if(this.name){
this.original = this.name[0][this.name[1]]
}
var args = plugins.argsFromFunc(this.original)
var output = this.loadCallback(plugins.strFromFunc(this.original), args)
if(typeof output === "undefined"){
throw new Error("A value is expected to be returned")
}
var args = plugins.argsFromFunc(this.original)
this.edited = Function(...args, output)
return this
var output = Function(...args, output)
if(this.name){
this.name[0][this.name[1]] = output
}
return output
}
}

View File

@@ -215,8 +215,8 @@ class Scoresheet{
if(this.redrawing){
if(this.winW !== winW || this.winH !== winH){
this.canvas.width = winW
this.canvas.height = 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"

View File

@@ -104,18 +104,20 @@ class SongSelect{
return a.id > b.id ? 1 : -1
}
})
this.songs.push({
title: strings.back,
skin: this.songSkin.back,
action: "back"
})
this.songs.push({
title: strings.randomSong,
skin: this.songSkin.random,
action: "random",
category: strings.random,
canJump: true
})
if(assets.songs.length){
this.songs.push({
title: strings.back,
skin: this.songSkin.back,
action: "back"
})
this.songs.push({
title: strings.randomSong,
skin: this.songSkin.random,
action: "random",
category: strings.random,
canJump: true
})
}
if(touchEnabled){
if(fromTutorial === "tutorial"){
fromTutorial = false
@@ -287,7 +289,8 @@ class SongSelect{
options: 0,
selLock: false,
catJump: false,
focused: true
focused: true,
waitPreview: 0
}
this.songSelecting = {
speed: 400,
@@ -472,7 +475,7 @@ class SongSelect{
this.toAccount()
}else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){
this.toSession()
}else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){
}else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket && p2.socket.readyState === 1 && !assets.customSongs){
this.toSession()
}else{
var moveBy = this.songSelMouse(mouse.x, mouse.y)
@@ -508,7 +511,7 @@ class SongSelect{
event.preventDefault()
}
mouseWheel(event){
if(this.state.screen === "song"){
if(this.state.screen === "song" && this.state.focused){
this.wheelTimer = this.getMS()
if(event.deltaY < 0) {
@@ -809,7 +812,7 @@ class SongSelect{
this.selectedDiff = 1
do{
this.state.options = this.mod(this.optionsList.length, this.state.options + moveBy)
}while((p2.socket.readyState !== 1 || assets.customSongs) && this.state.options === 2)
}while((p2.socket && p2.socket.readyState !== 1 || assets.customSongs) && this.state.options === 2)
}
}
toTitleScreen(){
@@ -913,12 +916,7 @@ class SongSelect{
}
}
}
if(this.wheelScrolls !== 0 && !this.state.locked && ms >= this.wheelTimer + 20) {
this.moveToSong(this.wheelScrolls)
this.wheelScrolls -= this.wheelScrolls
}
if(!this.redrawRunning){
return
}
@@ -944,8 +942,8 @@ class SongSelect{
var ratioY = winH / 720
var ratio = (ratioX < ratioY ? ratioX : ratioY)
if(this.winW !== winW || this.winH !== winH){
this.canvas.width = winW
this.canvas.height = 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"
@@ -1034,6 +1032,13 @@ class SongSelect{
var screen = this.state.screen
var selectedWidth = this.songAsset.width
if(this.wheelScrolls !== 0 && !this.state.locked && ms >= this.wheelTimer + 20) {
this.state.move = this.wheelScrolls
this.state.waitPreview = ms + 400
this.wheelScrolls = 0
this.endPreview()
}
if(screen === "title" || screen === "titleFadeIn"){
if(ms > this.state.screenMS + 1000){
this.state.screen = "song"
@@ -2439,7 +2444,7 @@ class SongSelect{
}
startPreview(loadOnly){
if(!loadOnly && this.state && this.state.showWarning){
if(!loadOnly && this.state && this.state.showWarning || this.state.waitPreview > this.getMS()){
return
}
var currentSong = this.songs[this.selectedSong]

View File

@@ -73,7 +73,10 @@
}
}
class SoundGain{
constructor(soundBuffer, channel){
constructor(...args){
this.init(...args)
}
init(soundBuffer, channel){
this.soundBuffer = soundBuffer
this.gainNode = soundBuffer.context.createGain()
if(channel){
@@ -121,7 +124,10 @@ class SoundGain{
}
}
class Sound{
constructor(gain, buffer){
constructor(...args){
this.init(...args)
}
init(gain, buffer){
this.gain = gain
this.buffer = buffer
this.soundBuffer = gain.soundBuffer

View File

@@ -216,6 +216,10 @@ var translations = {
ja: "曲「%s」を読み込むことができませんでした。ID%s\n\n%s",
en: "Could not load song %s with ID %s.\n\n%s"
},
accessNotGrantedError: {
ja: null,
en: "Permission to access the file was not granted"
},
loading: {
ja: "ロード中...",
en: "Loading...",

View File

@@ -12,29 +12,85 @@ class Tutorial{
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"], event => {
if(event.type === "touchstart"){
event.preventDefault()
this.touched = true
}else if(event.type === "mousedown" && event.which !== 1){
return
}
this.onEnd(true)
})
pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this))
pageEvents.add(this.formButton, ["mousedown", "touchstart"], this.linkButton.bind(this))
this.keyboard = new Keyboard({
confirm: ["enter", "space", "esc", "don_l", "don_r"]
}, this.onEnd.bind(this))
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: ["start", "b", "ls", "rs"]
}, this.onEnd.bind(this))
"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"){
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))
}

View File

@@ -199,8 +199,8 @@
this.ratio = ratio
if(this.player !== 2){
this.canvas.width = winW
this.canvas.height = 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"
@@ -1515,6 +1515,7 @@
}
updateNoteFaces(){
var ms = this.getMS()
var lastNextBeat = this.nextBeat
while(ms >= this.nextBeat){
this.nextBeat += this.beatInterval
if(this.controller.getCombo() >= 50){
@@ -1529,6 +1530,9 @@
big: 3
}
}
if(this.nextBeat <= lastNextBeat){
break
}
}
}
drawCircles(circles){