local function state(songName, songDifficulty) local state = {} -- Returns needed functions for the state to work after loading it properly -- I NEED THEM IMPORTS local myMath = require("modules.math") local myTypes = require("modules.types") local conductor = require("modules.conductor") local json = require("modules.json") local files = require("modules.files") local logger = require("modules.logging") local socket = require("socket") local logging= require("modules.logging") -- I NEED THEM IMPORTS local startTime = 0 local chartString = files.read_file(string.format("charts/%s/%s-%s.json", songName, songName, songDifficulty)) 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 = love.audio.newSource(string.format("songs/%s/Voices.ogg", chart.song), "stream") local miss = love.audio.newSource("sounds/missnote1.ogg", "static") conductor:setBpm(chart.bpm) conductor:mapBpmChanges(chart) local step = 0 local beat = 0 local zoom = 1 local volume = 100 local modules = {} local playing = false local stage = json.parse(files.read_file(string.format("stages/%s.json", chart.stage))) local unspawnedNotes = {} local notes = {} local unspawnedHoldNotes = {} local holdNotes = {} local characters = {} local settings = {} local ratings = { sick = 0, good = 0, bad = 0, shit = 0, miss = 0, } local rankWindows = { { rating = "sick", hitWindow = 15, spawnSplash = true }, { rating = "good", hitWindow = 30, spawnSplash = false }, { rating = "bad", hitWindow = 50, spawnSplash = false }, { rating = "shit", hitWindow = 120, spawnSplash = false } } local directions = { "LEFT", "DOWN", "UP", "RIGHT" } local colors = { "purple", "blue", "green", "red" } local singVectors = { singLEFT = myTypes.Vector2(20, 0), singDOWN = myTypes.Vector2(0, -20), singUP = myTypes.Vector2(0, 20), singRIGHT = myTypes.Vector2(-20, 0) } local pressed = { false, false, false, false, false } local holded = { false, false, false, false } local font = love.graphics.newFont("fonts/Phantomuff.ttf", 15) local biggerFont = love.graphics.newFont("fonts/Phantomuff.ttf", 23) local receptors = {} local splashes = {} local keyBinds = {} -- loaded from settings.json, if anything's wrong then check your settings.json 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 function quit() playing = false inst:stop() inst:release() inst = nil voices:stop() voices:release() voices = nil miss:stop() miss:release() state.quit() 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 splash %s %s", colors[closestNote.direction], math.random(1, 2)), 24, false) end ratings[rating.rating] = ratings[rating.rating] + 1 characters.bf:PlayAnimation("sing"..directions[closestNote.direction]) closestNote.pressed = true notes[closestIndex] = nil closestNote:destroy() end end function state.update(dt) if not playing then return end -- playing isn't supposed to work like "paused", it's there to keep the game from working during loading if paused then goto continue end -- if paused then skip this cycle local currentTime = socket.gettime() elapsed = (currentTime - startTime) * 1000 - pauseTime 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" then if beat % character.beats == 0 then character:PlayAnimation("danceLeft") end else character:PlayAnimation("idle") end end end if beat % 4 == 0 then zoom = zoom + .1 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.mustHitSection then local currentSingVector = singVectors[characters.bf.animation] or myTypes.Vector2() myTypes.cameraTarget = myTypes.Vector2(-stage.camera_boyfriend[1], -stage.camera_boyfriend[2]):Add(characters.bf.stageCamera:Negate()):Add(myTypes.Vector2(0, -200)):Add(currentSingVector) else local currentSingVector = singVectors[characters.dad.animation] or myTypes.Vector2() myTypes.cameraTarget = myTypes.Vector2(stage.camera_opponent[1], stage.camera_opponent[2]):Add(characters.dad.stageCamera:Negate()):Add(myTypes.Vector2(0, -200)):Add(currentSingVector) end myTypes.updateSprites(dt) for name, character in next, characters do if name ~= "gf" and character.sprite.animation ~= "idle" and character.sprite.ended then character:PlayAnimation("idle") end end for index, note in next, unspawnedNotes do if note.position - elapsed < 600 then note:spawn() unspawnedNotes[index] = nil notes[#notes+1] = note end end for index, holdNote in next, unspawnedHoldNotes do if holdNote.position - elapsed < 600 then holdNote:spawn() unspawnedHoldNotes[index] = nil holdNotes[#notes+1] = holdNote end end for index, note in next, notes do if note.mustPress then note.sprite.position = myTypes.Vector2(600 + (79 * (note.direction - 1)), settings.Downscroll and 430 - (note.position-elapsed) or note.position - elapsed) if note.position - elapsed < -150 then note:destroy() miss:stop() miss:play() characters.bf:PlayAnimation("sing"..directions[note.direction].."miss") notes[index] = nil ratings.miss = ratings.miss + 1 end else note.sprite.position = myTypes.Vector2(50 + 79 * (note.direction - 1), settings.Downscroll and 430 - (note.position-elapsed) or note.position - elapsed) if note.position - elapsed < 10 then notes[index] = nil if section.altAnim or note.altAnim then characters.dad:PlayAnimation("sing"..directions[note.direction].."-alt") else characters.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 = myTypes.Vector2(600 + (79 * (hold.direction - 1)), settings.Downscroll and 430 - (hold.position-elapsed) or hold.position - elapsed) if hold.position - elapsed < 50 then if love.keyboard.isDown(keyBinds[hold.direction]) then if 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 elseif hold.position - elapsed < -150 then hold:destroy() miss:stop() miss:play() characters.bf:PlayAnimation("sing"..directions[hold.direction].."miss") holdNotes[index] = nil ratings.miss = ratings.miss + 1 end end end end zoom = myMath.lerp(zoom, 1, .05) if inst and inst:getVolume() ~= volume / 100 then inst:setVolume(volume / 100) voices:setVolume(volume / 100) end if inst and not inst:isPlaying() then quit() end for index, splash in next, splashes do if splash.animation and splash.ended then splash:StopAnimation() end end ::continue:: end local mainCanvas = love.graphics.newCanvas(1920, 1080) function state.draw() love.graphics.setCanvas(mainCanvas) love.graphics.clear() love.graphics.circle("fill", 960, 59, 50000) myTypes.drawSprites() love.graphics.setCanvas() love.graphics.draw(mainCanvas, (love.graphics.getWidth() - (love.graphics.getWidth() * zoom)) / 2, (love.graphics.getHeight() - love.graphics.getHeight() * zoom) / 2, 0, love.graphics.getWidth()/1920 * zoom, (love.graphics.getHeight()/1080 * zoom)) love.graphics.print({{0,0,0,1}, string.format("FPS: %s \nVolume: %s", love.timer.getFPS(), volume)}, font) love.graphics.print({{0,0,0,1}, string.format("Sick: %s \nGood: %s \nBad: %s \nShit: %s \nMiss: %s", ratings.sick, ratings.good, ratings.bad, ratings.shit, ratings.miss)}, biggerFont, 0, 100) end love.window.setMode(1280, 720, { fullscreen = false , resizable = false}) function state.load() local stageScript = "stages/"..chart.stage..".lua" local stageModule local opened = io.open(stageScript) if opened then opened:close() stageModule = require("stages."..chart.stage) modules[#modules+1] = stageModule end if stageModule and stageModule.onCreate then stageModule.onCreate(chart.song) end -- GF first so she is below other chars if chart.gfVersion ~= "none" then characters.gf = myTypes.character(chart.gfVersion) characters.gf.stagePosition = myTypes.Vector2(stage.girlfriend[1], stage.girlfriend[2]) end characters.bf = myTypes.character(chart.player1) characters.bf.stagePosition = myTypes.Vector2(stage.boyfriend[1], stage.boyfriend[2]) characters.dad = myTypes.character(chart.player2) characters.dad.stagePosition = myTypes.Vector2(stage.opponent[1], stage.opponent[2]) conductor.stepCrochet = conductor:calculateCrochet(chart.bpm)/4 for index, section in next, chart.notes do for index, note in next, section.sectionNotes do local newNote = myTypes.note(note, section.mustHitSection) unspawnedNotes[#unspawnedNotes+1] = newNote -- if note[3] > 0 then -- local length = note[3] / conductor.stepCrochet -- for i = 0, length - 1, .1 do -- local newHold = myTypes.note({note[1] + i * conductor.stepCrochet, note[2], note[3], note[4]}, section.mustHitSection, true) -- unspawnedHoldNotes[#unspawnedHoldNotes+1] = newHold -- end -- local newHold = myTypes.note({note[1] + length * conductor.stepCrochet, note[2], note[3], note[4]}, section.mustHitSection, true, true) -- unspawnedHoldNotes[#unspawnedHoldNotes+1] = newHold -- end -- not yet end end for i = 0, 3 do local receptor = myTypes.Rect("sprites/NOTE_assets.png", "sprites/NOTE_assets.json") receptor:Frame("arrow"..directions[i+1], 0) receptor.position = myTypes.Vector2(600 + (79* i), settings.Downscroll and 0 or 430) receptor.ui = true -- So it doesnt move with the camera. receptors[i + 1] = receptor local splash = myTypes.Sprite("sprites/noteSplashes.png", "sprites/noteSplashes.json") splash.position = myTypes.Vector2(550 + (79* i), settings.Downscroll and 0 or 380) splash.ui = true splashes[i + 1] = splash end for i = 0, 3 do -- opponent receptors, purely graphics local receptor = myTypes.Rect("sprites/NOTE_assets.png", "sprites/NOTE_assets.json") receptor:Frame("arrow"..directions[i+1], 0) receptor.position = myTypes.Vector2(50 + 79 * i, settings.Downscroll and 0 or 430) receptor.ui = true -- So it doesnt move with the camera. end myTypes.cameraTarget = myTypes.Vector2() settings = json.parse(files.read_file("settings.json")) if not settings then error("Failed to load settings") end keyBinds = settings.Keybinds state.loaded = true end function state.finish() inst:play() voices:play() while not inst:isPlaying() do end --waiting till the song actually plays. elapsed = 0 actualElapsed = 0 playing = true startTime = socket.gettime() end function state.keypressed(key, un, is) if key == "space" then paused = not paused if paused then inst:pause() voices:pause() pauseStart = socket.gettime() * 1000 else inst:play() voices:play() pauseTime = pauseTime + (socket.gettime() * 1000 - pauseStart) 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 end end return state end return state -- vscode doesnt like this but idrc