japanese-drum-game/public/src/js/game.js
LoveEevee 61a5d6d496 SoundBuffer: Set song volume
- Requires a new column in the database after preview: `volume` REAL
- The value is a volume multiplier, if the value is set to null or 1 there will be no change
- The volume can be set in debugger
- Imported TJA files are now read from disk every time the song is played, freeing some memory and making it easier to create charts
- Correctly parse TJA files with alphabet notes, added "A" and "B" notes, which appear as DON (Big) and KA (Big) respectively
2019-03-16 00:34:48 +03:00

676 lines
19 KiB
JavaScript

class Game{
constructor(controller, selectedSong, songData){
this.controller = controller
this.selectedSong = selectedSong
this.songData = songData
this.elapsedTime = 0
this.currentCircle = 0
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
}
this.HPGain = 100 / 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.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 offsetTime = Math.max(0, this.timeForDistanceCircle - this.songData.circles[0].ms) |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(){
// Main operations
this.updateTime()
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)
if(ms >= circle.ms){
if(drumrollNotes && !circle.rendaPlayed && ms < endTime){
circle.rendaPlayed = true
if(this.rules.difficulty === "easy"){
assets.sounds["v_renda" + this.controller.snd].stop()
this.controller.playSoundMeka("v_renda")
}
}
if(!circle.beatMSCopied){
if(this.view.beatInterval !== circle.beatMS){
this.view.changeBeatInterval(circle.beatMS)
}
circle.beatMSCopied = true
}
}
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) / 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){
view.branchAnimate = {
ms: ms,
fromBranch: view.branch
}
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")
}
}
}
}
}
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
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 kbd = this.controller.getBindings()
var don_l = keys[kbd["don_l"]] && !this.controller.isWaiting(kbd["don_l"], "score")
var don_r = keys[kbd["don_r"]] && !this.controller.isWaiting(kbd["don_r"], "score")
var ka_l = keys[kbd["ka_l"]] && !this.controller.isWaiting(kbd["ka_l"], "score")
var ka_r = keys[kbd["ka_r"]] && !this.controller.isWaiting(kbd["ka_r"], "score")
var checkDon = () => {
if(don_l && don_r){
this.checkKey([kbd["don_l"], kbd["don_r"]], circle, "daiDon")
}else if(don_l){
this.checkKey([kbd["don_l"]], circle, "don")
}else if(don_r){
this.checkKey([kbd["don_r"]], circle, "don")
}
}
var checkKa = () => {
if(ka_l && ka_r){
this.checkKey([kbd["ka_l"], kbd["ka_r"]], circle, "daiKa")
}else if(ka_l){
this.checkKey([kbd["ka_l"]], circle, "ka")
}else if(ka_r){
this.checkKey([kbd["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 = keysDon ? keyTime["don"] : keyTime["ka"]
var relative = currentTime - circle.ms
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){
if(typeDai && !keyDai){
if(!circle.daiFailed){
circle.daiFailed = ms
return false
}else if(ms < circle.daiFailed + this.rules.daiLeniency){
return false
}
}
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(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,
dai: typeDai ? (keyDai ? 2 : 1) : 0
}
if((!keysDon || !typeDon) && (!keysKa || !typeKa)){
value.reverse = true
}
p2.send("note", value)
}
}else{
if(circle.ms > currentTime || currentTime > circle.endTime){
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) / 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)
}
whenLastCirclePlayed(){
var ms = this.elapsedTime
if(!this.lastCircle){
var circles = this.songData.circles
this.lastCircle = circles[circles.length - 1].endTime
if(this.controller.multiplayer){
var syncWith = this.controller.syncWith
var syncCircles = syncWith.game.songData.circles
var syncLastCircle = syncCircles[syncCircles.length - 1].endTime
if(syncLastCircle > this.lastCircle){
this.lastCircle = syncLastCircle
}
}
}
if(!this.fadeOutStarted && ms >= this.lastCircle + 2000){
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){
p2.send("gameresults", this.getGlobalScore())
}
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 >= started + 8600 && ms >= musicDuration + 250)){
this.controller.displayResults()
this.musicFadeOut++
}else if(this.musicFadeOut === 3 && (ms >= started + 9600 && ms >= 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.controller.multiplayer !== 2 && this.mainAsset){
this.mainAsset.play((ms < 0 ? -ms : 0) / 1000, false, Math.max(0, ms / 1000))
}
this.mainMusicPlaying = true
}
}
togglePause(){
if(!this.paused){
assets.sounds["se_pause"].play()
this.paused = true
this.latestDate = Date.now()
if(this.mainAsset){
this.mainAsset.stop()
}
this.mainMusicPlaying = false
this.view.pauseMove(0, true)
this.view.gameDiv.classList.add("game-paused")
this.view.lastMousemove = this.view.getMS()
this.view.cursorHidden = false
pageEvents.send("pause")
}else{
assets.sounds["se_cancel"].play()
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 < 1500 || this.combo > 0 && this.combo % 500 === 0){
this.controller.playSoundMeka("v_combo_" + (this.combo <= 1400 ? this.combo : "over1500"))
}
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++
break
case 230:
this.globalScore.ok++
break
case 0:
this.globalScore.bad++
break
}
// Gauge update
if(score !== 0){
this.globalScore.gauge += this.HPGain
}else if(this.globalScore.gauge - this.HPGain > 0){
this.globalScore.gauge -= this.HPGain
}else{
this.globalScore.gauge = 0
}
// Points update
score += Math.max(0, Math.floor((Math.min(this.combo, 100) - 1) / 10) * 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 >= ms
})
if(closestCircle !== -1){
this.currentCircle = closestCircle
}
}
this.HPGain = 100 / this.songData.circles.filter(circle => {
var type = circle.type
return (type === "don" || type === "ka" || type === "daiDon" || type === "daiKa") && (!circle.branch || circle.branch.active)
}).length
if(this.controller.multiplayer === 1){
p2.send("branch", activeName)
}
}
resetSection(){
this.sectionNotes = []
this.sectionDrumroll = 0
}
}