TaggedEngine/states/playstate.lua

1124 lines
42 KiB
Lua

local function state(songName, songDifficulty, show)
local state = {} -- Returns needed functions for the state to work after loading it properly
-- I NEED THEM IMPORTS
local conductor = require("modules.conductor")
local json = require("modules.json")
local files = require("modules.files")
local socket = require("socket")
local logging = require("modules.logging")
-- I NEED THEM IMPORTS
local startTime = 0
local chartString
if curChar == "bf" then
chartString = files.read_file(string.format("charts/%s/%s-%s.json", songName, songName, songDifficulty))
else
chartString = files.read_file(string.format("charts/%s/%s-%s-%s.json", songName, songName, songDifficulty, curChar))
end
if not chartString then
error("Chart couldn't be loaded!")
end
local chart = json.parse(chartString).song
local inst = love.audio.newSource(string.format("songs/%s/Inst.ogg", chart.song), "stream")
local voices
if chart.needsVoices then
voices = love.audio.newSource(string.format("songs/%s/Voices.ogg", chart.song), "stream")
end
local speed = chart.speed and chart.speed / 3 or 0.5
local miss = love.audio.newSource("sounds/missnote1.ogg", "static")
local loss = love.audio.newSource("sounds/fnf_loss_sfx.ogg", "static")
local gameOver = love.audio.newSource("sounds/gameOver.ogg", "stream")
local gameOverEnd = love.audio.newSource("sounds/gameOverEnd.ogg", "static")
conductor:setBpm(chart.bpm)
conductor:mapBpmChanges(chart)
local step = 0
local beat = 0
local zoom = 1
local uiZoom = 1
local volume = 100
local combo = 0
local highestCombo = 0
local modules = {}
local playing = false
local stageName = chart.stage
local stageString = files.read_file(string.format("stages/%s.json", stageName)) or files.read_file("stages/stage.json")
local stage = json.parse(stageString)
local unspawnedNotes = {}
local notes = {}
local unspawnedHoldNotes = {}
local holdNotes = {}
local events = {}
local characters = {}
local icons = {}
local data = love.filesystem.getSaveDirectory()
local ui = {
timebar = true,
healthIcons = true, -- If halth is false it wont render either way
health = true,
score = true,
ratings = true,
}
local settings = {}
local ratings = {
sick = 0,
good = 0,
bad = 0,
shit = 0,
miss = 0,
}
local score = 0
local totalScore = 0 -- If you hit ALL sicks
local accuracy = 100
local rankWindows = {
{
rating = "sick",
hitWindow = 45,
spawnSplash = true,
score = 300
},
{
rating = "good",
hitWindow = 90,
spawnSplash = false,
score = 200
},
{
rating = "bad",
hitWindow = 135,
spawnSplash = false,
score = 100
},
{
rating = "shit",
hitWindow = 300,
spawnSplash = false,
score = 50
}
}
local receptorOffsets = {
arrow = Vector2(0, 0),
press = Vector2(-5, -5),
confirm = Vector2(35, 35)
}
local receptorAnims = {
"arrow",
"arrow",
"arrow",
"arrow"
}
local directions = {
"LEFT",
"DOWN",
"UP",
"RIGHT"
}
local colors = {
"purple",
"blue",
"green",
"red"
}
local font = love.graphics.newFont("fonts/Phantomuff.ttf", 15)
local biggerFont = love.graphics.newFont("fonts/Phantomuff.ttf", 30)
local evenBiggerFont = love.graphics.newFont("fonts/FridayNightFunkin-Regular.ttf", 50)
local big = love.graphics.newFont(30)
local receptors = {}
local opponentReceptors = {}
local splashes = {}
local keyBinds = {} -- loaded from settings.json, if anything's wrong then try rebinding in the menu
local offset = 0
local paused = false
local elapsed = 0
local pauseTime = 0 -- the global amount of time the song has been paused
local pauseStart = 0 -- the start of the latest pause (for pauseTime calculation)
local health = 1 -- 0 to 2, 1 starters
local dead = false
local deadBF
local restart = false
local countDownAudio = {
love.audio.newSource("sounds/countdown/introTHREE.ogg", "static"),
love.audio.newSource("sounds/countdown/introTWO.ogg", "static"),
love.audio.newSource("sounds/countdown/introONE.ogg", "static"),
love.audio.newSource("sounds/countdown/introGO.ogg", "static")
}
local curCD = 0
local counting = true -- Before the round starts it will be true
local cdLength = 0
for i, audio in next, countDownAudio do
cdLength = cdLength + audio:getDuration() * 1000
end
--- @class SharedVars
--- @field canStart boolean
--- @field screenSize Vector2
--- @field canvasSize Vector2
--- @field singVectors table<Vector2>
--- @field settings table
--- @field receptors table<Sprite>
--- @field splashes table<Sprite>
--- @field opponentReceptors table<Sprite>
--- @field health number
--- @field speed number
--- @field ui table<boolean>
--- @field notes table<Note>
--- @field characters table<Character>
_G.sharedVars = {
canStart = true,
screenSize = Vector2(1280, 720),
canvasSize = Vector2(1920,1080),
singVectors = {
singLEFT = Vector2(20, 0),
singDOWN = Vector2(0, -20),
singUP = Vector2(0, 20),
singRIGHT = Vector2(-20, 0),
["singLEFT-alt"] = Vector2(20, 0), -- alt anims need to be here too
["singDOWN-alt"] = Vector2(0, -20),
["singUP-alt"] = Vector2(0, 20),
["singRIGHT-alt"] = Vector2(-20, 0)
},
settings = settings,
receptors = receptors,
splashes = splashes,
opponentReceptors = opponentReceptors,
health = health,
speed = speed,
ui = ui,
zoom = zoom,
notes = notes, -- only spawned notes
holds = holdNotes,
characters = characters,
shouldCountdown = true,
}
local function quit(save)
if restart then return end
playing = false
if modules then
for index, module in next, modules do
if module.onClose then
module.onClose()
modules[index] = nil
end
end
end
modules = {}
if not dead then
inst:stop()
inst:release()
inst = nil
if chart.needsVoices then
voices:stop()
voices:release()
voices = nil
end
miss:stop()
miss:release()
else
loss:stop()
gameOver:stop()
gameOverEnd:stop()
end
if save then
local oldSave = files.read_file(data.."/Data.json")
if not oldSave then
oldSave = {songs = {}}
else
oldSave = json.parse(oldSave)
end
if not oldSave.songs[curChar] then
oldSave.songs[curChar] = {}
end
if not oldSave.songs[curChar][songName] then
oldSave.songs[curChar][songName] = {}
end
local rankingWindows = {
{
name = "Bad",
window = 50
},
{
name = "Good",
window = 70
},
{
name = "Sick",
window = 90
},
{
name = "Perfect",
window = 100
},
}
if oldSave.songs[curChar][songName][songDifficulty] and oldSave.songs[curChar][songName][songDifficulty].score < score or not oldSave.songs[curChar][songName][songDifficulty] then
local rank = "Ass"
for index, newRank in next, rankingWindows do
if accuracy >= newRank.window then
rank = newRank.name
end
end
oldSave.songs[curChar][songName][songDifficulty] = {
accuracy = tostring(accuracy):sub(1, 5),
score = score,
rank = rank
}
files.write_file(data.."/Data.json", json.stringify(oldSave))
end
end
if gameMode == "storymode" then
currentSong = currentSong + 1
state.changeState(save and songOrder[currentSong] and "playstate" or "weekstate", songOrder[currentSong], songDifficulty)
else
state.changeState(save and "resultsstate" or "freeplaystate", score, accuracy, ratings, combo)
end
end
local function die()
dead = true
playing = false
if modules then
for index, module in next, modules do
if module.onDeath then
module.onDeath()
end
if module.onClose then
module.onClose()
modules[index] = nil
end
end
end
modules = {}
inst:stop()
inst:release()
inst = nil
if chart.needsVoices then
voices:stop()
voices:release()
voices = nil
end
miss:stop()
miss:release()
render.destroyAllSprites()
deadBF = Character(string.format("%s-dead", chart.player1))
deadBF:PlayAnimation("firstDeath")
loss:play()
end
local function checkNote(dir)
local closestNote
local closestIndex
for index, note in next, notes do
if note.position - conductor.songPosition < 230 then
if note.mustPress and not note.pressed and note.direction == dir and (not closestNote or closestNote and note.position < closestNote.position) then
closestNote = note
closestIndex = index
end
end
end
if closestNote then
local rating = conductor:judgeNote(rankWindows, math.abs(closestNote.position - elapsed))
if rating.spawnSplash then
splashes[closestNote.direction]:PlayAnimation(string.format("note impact %s %s", math.random(1, 2), colors[closestNote.direction]), 24, false)
end
ratings[rating.rating] = ratings[rating.rating] + 1
characters.bf:PlayAnimation("sing"..directions[closestNote.direction])
closestNote.pressed = true
health = health + closestNote.hitHealth
notes[closestIndex] = nil
closestNote:Destroy()
score = score + rating.score
totalScore = totalScore + rankWindows[1].score
accuracy = (score / totalScore) * 100
combo = combo + 1
if combo > highestCombo then
highestCombo = combo
end
receptors[dir]:PlayAnimation(string.format("%s confirm", string.lower(directions[dir])), 24, false)
receptorAnims[dir] = "confirm"
else
receptors[dir]:PlayAnimation(string.format("%s press", string.lower(directions[dir])), 24, false)
receptorAnims[dir] = "press"
end
end
function state.update(dt)
if not playing then
for index, module in next, modules do
if module.onUpdate then
module.onUpdate(dt, 0)
end
end
if sharedVars.canStart then
sharedVars.canStart = false -- already started
-- inst:play()
-- if chart.needsVoices then
-- voices:play()
-- end
-- while not inst:isPlaying() do
-- end --waiting till the song actually plays.
elapsed = 0
playing = true --countdown now
startTime = socket.gettime()
end
if dead then
render.cameraTarget = deadBF.stageCamera
if not restart then
if deadBF and deadBF.sprite.ended then
deadBF:PlayAnimation("deathLoop")
end
if loss and not loss:isPlaying() and not gameOverEnd:isPlaying() and not gameOver:isPlaying() then
gameOver:play()
end
else
if not gameOverEnd:isPlaying() then
deadBF = nil
loss:release()
loss = nil
gameOverEnd:release()
gameOverEnd = nil
gameOver:release()
gameOver = nil
state.restart(songName, songDifficulty)
end
end
render.updateSprites(dt)
end
return
end
-- playing isn't supposed to work like "paused", it's there to keep the game from working during loading
if counting and sharedVars.shouldCountdown then
if not countDownAudio[curCD] or not countDownAudio[curCD]:isPlaying() then
if not countDownAudio[curCD + 1] then
counting = false -- Ended the countdown
inst:play()
inst:setLooping(false)
if chart.needsVoices then
voices:play()
end
elapsed = 0
playing = true --countdown now
startTime = socket.gettime()
cdLength = 0
else
curCD = curCD + 1
countDownAudio[curCD]:play()
end
end
elseif not sharedVars.shouldCountdown then
counting = false
end
if paused then goto continue end -- if paused then skip this cycle
local currentTime = socket.gettime()
elapsed = (currentTime - startTime) * 1000 - pauseTime - cdLength
conductor.songPosition = elapsed
local oldStep = step
local oldBeat = beat
step = conductor:getStepRounded(elapsed)
beat = conductor:getBeatRounded(elapsed)
if beat ~= oldBeat then
if beat % 2 == 0 then
-- gf:PlayAnimation("BF NOTE LEFT", 30, false)
for name, character in next, characters do
if not character.singing then
if name == "gf" or character.animInfo.danceLeft then
character:PlayAnimation("danceLeft")
else
character:PlayAnimation("idle")
end
end
end
if beat % 4 == 0 then
zoom = zoom + .02
uiZoom = uiZoom + .05
end
else
for name, character in next, characters do
if not character.singing then
if name == "gf" or character.animInfo.danceLeft then
character:PlayAnimation("danceRight")
end
end
end
end
for index, module in next, modules do
if module.onBeat then
module.onBeat(beat)
end
end
end
local section = chart.notes[math.floor(step / 16) + 1]
if section then
if not section.gfSection then
if section.mustHitSection then
local currentSingVector = sharedVars.singVectors[characters.bf.animation] or Vector2()
render.cameraTarget = Vector2(-stage.camera_boyfriend[1], -stage.camera_boyfriend[2]):Add(characters.bf.stageCamera:Negate()):Add(Vector2(0, -200)):Add(currentSingVector)
else
if characters.dad then
local currentSingVector = sharedVars.singVectors[characters.dad.animation] or Vector2()
render.cameraTarget = Vector2(stage.camera_opponent[1], stage.camera_opponent[2]):Add(characters.dad.stageCamera:Negate()):Add(Vector2(0, -200)):Add(currentSingVector)
else
local currentSingVector = sharedVars.singVectors[characters.gf.animation] or Vector2()
render.cameraTarget = Vector2(stage.camera_girlfriend[1], stage.camera_girlfriend[2]):Add(characters.gf.stageCamera:Negate()):Add(Vector2(0, -200)):Add(currentSingVector)
end
end
elseif characters.gf then
local currentSingVector = sharedVars.singVectors[characters.gf.animation] or Vector2()
render.cameraTarget = Vector2(stage.camera_girlfriend[1], stage.camera_girlfriend[2]):Add(characters.gf.stageCamera:Negate()):Add(Vector2(0, -200)):Add(currentSingVector)
else
local currentSingVector = sharedVars.singVectors[characters.bf.animation] or Vector2()
render.cameraTarget = Vector2(-stage.camera_boyfriend[1], -stage.camera_boyfriend[2]):Add(characters.bf.stageCamera:Negate()):Add(Vector2(0, -200)):Add(currentSingVector)
end
else
local currentSingVector = sharedVars.singVectors[characters.bf.animation] or Vector2()
render.cameraTarget = Vector2(-stage.camera_boyfriend[1], -stage.camera_boyfriend[2]):Add(characters.bf.stageCamera:Negate()):Add(Vector2(0, -200)):Add(currentSingVector)
end
render.updateSprites(dt)
for name, character in next, characters do
if name ~= "gf" and character.animInfo.idle and character.sprite.animation ~= "idle" and character.sprite.ended then
character:PlayAnimation("idle")
elseif (name == "gf" or character.animInfo.danceLeft) and character.singing and character.sprite.animation ~= "danceLeft" and character.sprite.ended then
character:PlayAnimation("danceLeft")
end
end
for index, module in next, modules do
if module.onUpdate then
module.onUpdate(dt, elapsed) -- elapsed is for special stuff i guess
end
end
-- Spawn holds before normal notes so they are below them
for index, holdNote in next, unspawnedHoldNotes do
if (holdNote.position - elapsed) * speed < 600 then
holdNote:Spawn()
unspawnedHoldNotes[index] = nil
holdNotes[#holdNotes+1] = holdNote
end
end
for index, note in next, unspawnedNotes do
if (note.position - elapsed) * speed < 600 then
note:Spawn()
unspawnedNotes[index] = nil
notes[#notes+1] = note
end
end
for index, note in next, notes do
if note.mustPress then
note.sprite.position = Vector2(receptors[note.direction].position.x + note.offset.x, settings.Downscroll and receptors[note.direction].position.y - (note.position-elapsed) * speed or receptors[note.direction].position.y + (note.position - elapsed) * speed)
if (note.position - elapsed) * speed < -150 then
note:Destroy()
miss:stop()
miss:play()
characters.bf:PlayAnimation("sing"..directions[note.direction].."miss")
notes[index] = nil
ratings.miss = ratings.miss + 1
health = health - note.missHealth
score = score - 200
if combo > highestCombo then
highestCombo = combo
end
end
else
note.sprite.position = Vector2(opponentReceptors[note.direction].position.x + note.offset.x, settings.Downscroll and opponentReceptors[note.direction].position.y - (note.position-elapsed) * speed or opponentReceptors[note.direction].position.y + (note.position - elapsed) * speed)
if (note.position - elapsed) * speed < 10 then
notes[index] = nil
if section then
if section.gfSection and characters.gf or chart.song == "Tutorial" and characters.gf then
if section.altAnim or note.altAnim then
characters.gf:PlayAnimation("sing"..directions[note.direction].."-alt")
else
characters.gf:PlayAnimation("sing"..directions[note.direction])
end
else
if section.altAnim or note.altAnim and characters.dad.animInfo["sing"..directions[note.direction].."-alt"] then
characters.dad:PlayAnimation("sing"..directions[note.direction].."-alt")
elseif characters.dad.animInfo["sing"..directions[note.direction]] then
characters.dad:PlayAnimation("sing"..directions[note.direction])
end
end
else
characters[note.character or "dad"]:PlayAnimation("sing"..directions[note.direction])
end
note:Destroy()
note = nil
end
end
end
for index, hold in next, holdNotes do
if hold.mustPress then
hold.sprite.position = Vector2(receptors[hold.direction].position.x + hold.offset.x, settings.Downscroll and receptors[hold.direction].position.y - (hold.position-elapsed) * speed or receptors[hold.direction].position.y + (hold.position - elapsed) * speed)
if (hold.position - elapsed) * speed < 10 then
if love.keyboard.isDown(keyBinds[hold.direction]) then
if characters.bf.animInfo["sing"..directions[hold.direction].."-alt"] and (section and section.altAnim or hold.altAnim) then
characters.bf:PlayAnimation("sing"..directions[hold.direction].."-alt")
else
characters.bf:PlayAnimation("sing"..directions[hold.direction])
end
hold:Destroy()
holdNotes[index] = nil
health = health + hold.hitHealth * 0.2
receptors[hold.direction]:PlayAnimation(string.format("%s confirm", string.lower(directions[hold.direction])), 24, false)
receptorAnims[hold.direction] = "confirm"
elseif (hold.position - elapsed) * speed < -150 then
hold:Destroy()
characters.bf:PlayAnimation("sing"..directions[hold.direction].."miss")
holdNotes[index] = nil
health = health - hold.missHealth * 0.2
end
end
else
hold.sprite.position = Vector2(opponentReceptors[hold.direction].position.x + hold.offset.x, settings.Downscroll and opponentReceptors[hold.direction].position.y - (hold.position-elapsed) * speed or opponentReceptors[hold.direction].position.y + (hold.position - elapsed) * speed)
if (hold.position - elapsed) * speed < 10 then
if characters.dad.animInfo["sing"..directions[hold.direction].."-alt"] and ( section and section.altAnim or hold.altAnim) then
characters.dad:PlayAnimation("sing"..directions[hold.direction].."-alt")
else
characters.dad:PlayAnimation("sing"..directions[hold.direction])
end
hold:Destroy()
holdNotes[index] = nil
end
end
end
for index, event in next, events do
if event.time - elapsed < 0 then
events[index] = nil
if event.name == "change character" then
characters[event.var1]:Destroy()
characters[event.var1] = Character(event.var2)
elseif event.name == "play animation" then
characters[event.var2]:PlayAnimation(event.var1)
elseif event.name == "hey!" then
characters.bf:PlayAnimation("hey")
end
for index, module in next, modules do
if module.onEvent then
module.onEvent(event)
end
end
end
end
zoom = Lerp(zoom, stage.defaultZoom or 1, .05)
uiZoom = Lerp(uiZoom, 1, .05)
if inst and inst:getVolume() ~= volume / 100 then
inst:setVolume(volume / 100)
if chart.needsVoices then
voices:setVolume(volume / 100)
end
end
for index, splash in next, splashes do
if splash.animation and splash.ended then
splash:StopAnimation()
end
end
for index, receptor in next, receptors do
if receptor.ended then
receptor:PlayAnimation(string.format("arrow%s", directions[index]), 24, true)
receptorAnims[index] = "arrow"
end
receptor.extraOffset = receptorOffsets[receptorAnims[index]]
end
if health <= 0 then
die()
elseif health > 2 then
health = 2
end
if inst and not inst:isPlaying() and not counting then
quit(true)
end
::continue::
end
local mainCanvas
local uiCanvas
function state.draw()
love.graphics.setCanvas(mainCanvas)
love.graphics.clear()
render.drawSprites()
for i, module in next, modules do
if module.drawBelowUI then
module.drawBelowUI() --mainly for cutscenes i guess
end
end
love.graphics.setCanvas(uiCanvas)
love.graphics.clear()
render.drawUI()
-- HEALTH BAR
if playing and ui.health then
love.graphics.setColor(characters.dad and characters.dad.colors[1]/255 or 0,
characters.dad and characters.dad.colors[2]/255 or 1,
characters.dad and characters.dad.colors[1]/255 or 0
)
love.graphics.rectangle("fill", 560, settings.Downscroll and 30 or 960, 800, 30)
love.graphics.setColor(characters.bf.colors[1]/255,characters.bf.colors[2]/255,characters.bf.colors[3]/255)
love.graphics.rectangle("fill", 1360 - health * 400, settings.Downscroll and 30 or 960, health * 400, 30)
love.graphics.setColor(0,0,0)
love.graphics.rectangle("line", 560, settings.Downscroll and 30 or 960, 800, 30)
love.graphics.setColor(1,1,1)
if ui.healthIcons then
love.graphics.draw(icons.bf.image, health > .2 and icons.bf.alive or icons.bf.dead, 1510 - health * 400, settings.Downscroll and 0 or 930, 0, -1, 1)
if characters.dad then
love.graphics.draw(icons.dad.image, health < 1.8 and icons.dad.alive or icons.dad.dead, 1210 - health * 400, settings.Downscroll and 0 or 930)
end
end
end
if playing and ui.score then
love.graphics.print({{0,0,0,1}, string.format("Score: %s Accuracy: %s", score, tostring(accuracy):sub(1, 5))}, big, 760, settings.Downscroll and 70 or 940)
end
for i, module in next, modules do
if module.onDraw then
module.onDraw() --mainly for cutscenes i guess
end
end
love.graphics.setCanvas()
if sharedVars.globalShader then
love.graphics.setShader(sharedVars.globalShader)
end
love.graphics.draw(mainCanvas, (love.graphics.getWidth() - (love.graphics.getWidth() * zoom)) / 2, (love.graphics.getHeight() - love.graphics.getHeight() * zoom) / 2, 0, love.graphics.getWidth()/sharedVars.canvasSize.x * zoom, (love.graphics.getHeight()/sharedVars.canvasSize.y * zoom))
love.graphics.draw(uiCanvas, (love.graphics.getWidth() - (love.graphics.getWidth() * uiZoom)) / 2, (love.graphics.getHeight() - love.graphics.getHeight() * uiZoom) / 2, 0, love.graphics.getWidth()/sharedVars.canvasSize.x * uiZoom, (love.graphics.getHeight()/sharedVars.canvasSize.y * uiZoom))
love.graphics.setShader()
love.graphics.print({{0,0,0,1}, string.format("FPS: %s \nVolume: %s", love.timer.getFPS(), volume)}, font)
end
function state.load()
love.window.setTitle("TaggedEngine")
settings = json.parse(files.read_file(data.."/Settings.json"))
if not settings then
error("Failed to load settings")
end
local stageScript
if stage.default then
stageScript = "stages/stage.lua"
else
stageScript = "stages/"..chart.stage..".lua"
end
local stageModule
local opened = io.open(stageScript)
if opened then
opened:close()
stageModule = require("stages."..chart.stage)
modules[#modules+1] = stageModule
end
local songScriptExists = io.open(string.format("charts/%s/script.lua", songName))
if songScriptExists then
songScriptExists:close()
local songScript = require(string.format("charts/%s/script", songName))
modules[#modules+1] = songScript
end
-- GF first so she is below other chars
if chart.gfVersion and chart.gfVersion ~= "none" then
characters.gf = Character(chart.gfVersion)
characters.gf.stagePosition = Vector2(stage.girlfriend[1], stage.girlfriend[2])
characters.gf:PlayAnimation("danceLeft")
characters.gf.sprite.layer = 0
end
characters.bf = Character(chart.player1)
characters.bf.stagePosition = Vector2(stage.boyfriend[1], stage.boyfriend[2])
characters.bf:PlayAnimation("idle")
characters.bf.sprite.layer = 1
local image = love.graphics.newImage(string.format("images/icons/icon-%s.png", characters.bf.icon))
icons.bf = {image = image, alive = love.graphics.newQuad(0,0, 150, 150, image), dead = love.graphics.newQuad(150, 0, 150, 150, image)}
if chart.player2 ~= "none" then -- you can have no player2 but always player1
characters.dad = Character(chart.player2)
characters.dad.stagePosition = Vector2(stage.opponent[1], stage.opponent[2])
characters.dad:PlayAnimation(characters.dad.animInfo.idle and "idle" or "danceLeft")
characters.dad.sprite.layer = 1
local image = love.graphics.newImage(string.format("images/icons/icon-%s.png", characters.dad.icon))
icons.dad = {image = image, alive = love.graphics.newQuad(0,0, 150, 150, image), dead = love.graphics.newQuad(150, 0, 150, 150, image)}
end
conductor.stepCrochet = conductor:calculateCrochet(chart.bpm)/4
local noteSkin = chart.noteSkin or "NOTE_assets" --will do this sometiems
local noteSplash = chart.splashSkin or "noteSplashes"
for i = 0, 3 do
local receptor = Sprite("sprites/NOTE_assets.png", "sprites/NOTE_assets.json")
receptor:PlayAnimation("arrow"..directions[i+1], 24, true)
receptor.layer = 9
receptor.position = Vector2(600 + (79* i), settings.Downscroll and 430 or 0)
receptor.ui = true -- So it doesnt move with the camera.
-- for index, anim in next, receptor.quads do
-- for index, quad in next, anim do
-- quad.offset = quad.offset
-- end
-- end
receptors[i + 1] = receptor
local splash = Sprite("sprites/noteSplashes.png", "sprites/noteSplashes.json")
splash.layer = 11
splash.position = Vector2(570 + (79* i), settings.Downscroll and 400 or -50)
splash.ui = true
splashes[i + 1] = splash
end
for i = 0, 3 do -- opponent receptors, purely graphics
local receptor = Rect("sprites/NOTE_assets.png", "sprites/NOTE_assets.json")
receptor:Frame("arrow"..directions[i+1], 0)
receptor.position = Vector2(50 + 79 * i, settings.Downscroll and 430 or 0)
receptor.layer = 9
receptor.ui = true -- So it doesnt move with the camera.
opponentReceptors[i + 1] = receptor
end
-- Load the notes AFTER the receptors to make sure they are always above them
for index, section in next, chart.notes do
for index, note in next, section.sectionNotes do
if note[2] >= 0 then
local newNote = Note(note, section.mustHitSection)
unspawnedNotes[#unspawnedNotes+1] = newNote
if note[3] > 0 then
local length = math.floor(note[3] / conductor.stepCrochet)
for i = 0, length - .1, .1 do
local newHold = Note({note[1] + i * conductor.stepCrochet, note[2], note[3], note[4]}, section.mustHitSection, true)
unspawnedHoldNotes[#unspawnedHoldNotes+1] = newHold
end
local newHold = Note({note[1] + length * conductor.stepCrochet, note[2], note[3], note[4]}, section.mustHitSection, true, true)
unspawnedHoldNotes[#unspawnedHoldNotes+1] = newHold
newHold.holdEnd = true
if settings.Downscroll then
newHold.flipY = true
end
newHold.speed = speed
end
else
local newEvent = {
time = note[1],
name = string.lower(note[3]),
var1 = note[4] or "",
var2 = note[5] or ""
}
events[#events+1] = newEvent
if newEvent.name == "change character" then
chars.preload(newEvent.var2)
end
end
end
end
local eventChartFile = files.read_file(string.format("charts/%s/events.json", songName))
if eventChartFile then
logging.log("Is chart file")
local eventChart = json.parse(eventChartFile).song
if eventChart.song ~= chart.song then goto noChart end
if eventChart.notes then
logging.log("Events are notes")
for index, section in next, eventChart.notes do
for index, note in next, section.sectionNotes do
local newEvent = {
time = note[1],
name = string.lower(note[3]),
var1 = note[4] or "",
var2 = note[5] or ""
}
events[#events+1] = newEvent
if newEvent.name == "change character" then
chars.preload(newEvent.var2)
end
logging.log(string.lower(note[3]))
end
end
end
if eventChart.events then
logging.log("Events are events")
for index, event in next, eventChart.events do
for index in next, event[2] do
local newEvent = {
time = event[1],
name = string.lower(event[2][index][1]),
var1 = event[2][index][2],
var2 = event[2][index][3]
}
print(logging.dump(newEvent))
events[#events+1] = newEvent
if newEvent.name == "change character" then
chars.preload(newEvent.var2)
end
end
end
end
::noChart::
else
logging.log("No chart file")
end
if chart.events then
for index, event in next, chart.events do
for index in next, event[2] do
local newEvent = {
time = event[1],
name = string.lower(event[2][index][1]),
var1 = event[2][index][2],
var2 = event[2][index][3]
}
logging.log(newEvent.name)
events[#events+1] = newEvent
if newEvent.name == "change character" then
chars.preload(newEvent.var2)
end
end
end
end
render.cameraTarget = Vector2()
keyBinds = settings.Keybinds
offset = settings.Offset
state.loaded = true
for i, module in next, modules do
if type(module) ~= "boolean" then
if module.onCreate then
module.onCreate(chart.song)
end
end
end
local canvasSize = sharedVars.canvasSize or Vector2(1920,1080)
mainCanvas = love.graphics.newCanvas(canvasSize.x, canvasSize.y)
uiCanvas = love.graphics.newCanvas(canvasSize.x, canvasSize.y)
local screenSize = sharedVars.screenSize or Vector2(1280, 720)
love.window.setMode(screenSize.x, screenSize.y, { fullscreen = false , resizable = false})
end
function state.finish()
if sharedVars.canStart then
sharedVars.canStart = false -- already started
-- inst:play()
-- if chart.needsVoices then
-- voices:play()
-- end
-- while not inst:isPlaying() do
-- end --waiting till the song actually plays.
elapsed = 0
playing = true --countdown now
startTime = socket.gettime()
end
end
function state.keypressed(key, un, is)
if key == "space" then
if playing then
paused = not paused
if paused then
inst:pause()
if chart.needsVoices then
voices:pause()
end
pauseStart = socket.gettime() * 1000
for index, module in next, modules do
if module.onPause then
module.onPause(elapsed)
end
end
else
inst:play()
inst:setLooping(false)
if chart.needsVoices then
voices:play()
end
for index, module in next, modules do
if module.onUnpause then
module.onUnpause((socket.gettime() * 1000 - pauseStart), elapsed)
end
end
pauseTime = pauseTime + (socket.gettime() * 1000 - pauseStart)
local currentTime = socket.gettime()
elapsed = (currentTime - startTime) * 1000 - pauseTime - cdLength
inst:seek((elapsed + offset) / 1000 > 0 and (elapsed + offset) / 1000 or 0, "seconds")
if chart.needsVoices then
voices:seek((elapsed + offset) / 1000 > 0 and (elapsed + offset) / 1000 or 0, "seconds")
end
end
elseif dead then
gameOverEnd:play()
gameOver:stop()
loss:stop()
restart = true
deadBF:PlayAnimation("deathConfirm")
end
elseif key == "escape" then
quit()
elseif key == keyBinds[1] then
checkNote(1)
elseif key == keyBinds[2] then
checkNote(2)
elseif key == keyBinds[3] then
checkNote(3)
elseif key == keyBinds[4] then
checkNote(4)
elseif key == "-" then
volume = volume ~= 0 and volume - 10 or 0
elseif key == "+" or key == "=" then
volume = volume ~= 100 and volume + 10 or 100
elseif key == "9" then
startTime = startTime - 200000
inst:seek(inst:tell() + (200000)/1000)
voices:seek(voices:tell() + (200000)/1000)
end
end
return state
end
return state -- vscode doesnt like this but idrc