This repository has been archived on 2025-06-08. You can view files and clone it, but cannot push or open issues or pull requests.
TaggedEngine/modules/states/playstate.lua

569 lines
20 KiB
Lua

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
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")
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 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 characters = {}
local settings = {}
local ratings = {
sick = 0,
good = 0,
bad = 0,
shit = 0,
miss = 0,
}
local rankWindows = {
{
rating = "sick",
hitWindow = 45,
spawnSplash = true
},
{
rating = "good",
hitWindow = 90,
spawnSplash = false
},
{
rating = "bad",
hitWindow = 135,
spawnSplash = false
},
{
rating = "shit",
hitWindow = 300,
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),
["singLEFT-alt"] = myTypes.Vector2(20, 0), -- alt anims need to be here too
["singDOWN-alt"] = myTypes.Vector2(0, -20),
["singUP-alt"] = myTypes.Vector2(0, 20),
["singRIGHT-alt"] = 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", 30)
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
if chart.needsVoices then
voices:stop()
voices:release()
voices = nil
end
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
character:PlayAnimation("danceLeft")
else
character:PlayAnimation("idle")
end
end
end
if beat % 4 == 0 then
zoom = zoom + .1
end
else
characters.gf:PlayAnimation("danceRight")
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 = 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
if characters.dad then
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
end
else
local currentSingVector = singVectors[characters.gf.animation] or myTypes.Vector2()
myTypes.cameraTarget = myTypes.Vector2(stage.camera_girlfriend[1], stage.camera_ocamera_girlfriendponent[2]):Add(characters.gf.stageCamera:Negate()):Add(myTypes.Vector2(0, -200)):Add(currentSingVector)
end
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")
elseif name == "gf" and character.singing and character.sprite.animation ~= "danceLeft" and character.sprite.ended then
character:PlayAnimation("danceLeft")
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 = myTypes.Vector2(600 + (79 * (note.direction - 1)), settings.Downscroll and 430 - (note.position-elapsed) * speed or (note.position - elapsed) * speed)
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) * speed or (note.position - elapsed) * speed)
if note.position - elapsed < 10 then
notes[index] = nil
if section.gfSection or chart.song == "Tutorial" 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 then
characters.dad:PlayAnimation("sing"..directions[note.direction].."-alt")
else
characters.dad:PlayAnimation("sing"..directions[note.direction])
end
end
note:destroy()
note = nil
end
end
end
for index, hold in next, holdNotes do
if hold.mustPress then
hold.sprite.position = myTypes.Vector2(625 + (79 * (hold.direction - 1)), settings.Downscroll and 430 - (hold.position-elapsed) * speed or (hold.position - elapsed) * speed)
if hold.position - elapsed < 10 then
if love.keyboard.isDown(keyBinds[hold.direction]) then
if 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
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
else
hold.sprite.position = myTypes.Vector2(75 + (79 * (hold.direction - 1)), settings.Downscroll and 430 - (hold.position-elapsed) * speed or (hold.position - elapsed) * speed)
if hold.position - elapsed < 10 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
end
end
end
zoom = myMath.lerp(zoom, 1, .05)
if inst and inst:getVolume() ~= volume / 100 then
inst:setVolume(volume / 100)
if chart.needsVoices then
voices:setVolume(volume / 100)
end
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()
settings = json.parse(files.read_file("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
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])
characters.gf:PlayAnimation("danceLeft")
end
characters.bf = myTypes.character(chart.player1)
characters.bf.stagePosition = myTypes.Vector2(stage.boyfriend[1], stage.boyfriend[2])
characters.bf:PlayAnimation("idle")
if chart.player2 ~= "none" then -- you can have no player2 but always player1
characters.dad = myTypes.character(chart.player2)
characters.dad.stagePosition = myTypes.Vector2(stage.opponent[1], stage.opponent[2])
characters.dad:PlayAnimation("idle")
end
conductor.stepCrochet = conductor:calculateCrochet(chart.bpm)/4
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 430 or 0)
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 380 or -50)
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 430 or 0)
receptor.ui = true -- So it doesnt move with the camera.
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
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] + math.floor(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
end
end
myTypes.cameraTarget = myTypes.Vector2()
keyBinds = settings.Keybinds
state.loaded = true
for i, func in next, stageModule do
local newEnv = getfenv(func)
newEnv.game = {
characters=characters,
step=step,
beat=beat,
volume=volume,
ratings=ratings,
keyBinds=keyBinds
}
setfenv(func, newEnv)
end
end
function state.finish()
inst:play()
if chart.needsVoices then
voices:play()
end
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()
if chart.needsVoices then
voices:pause()
end
pauseStart = socket.gettime() * 1000
else
inst:play()
if chart.needsVoices then
voices:play()
end
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