1262 lines
45 KiB
Lua
1262 lines
45 KiB
Lua
-- 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")
|
|
local tween = require("modules.tween")
|
|
local character = require("modules.types.character")
|
|
-- I NEED THEM IMPORTS
|
|
|
|
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 covers = {
|
|
"Purple",
|
|
"Blue",
|
|
"Green",
|
|
"Red"
|
|
}
|
|
|
|
local healthbarShader = [[
|
|
extern number opr;
|
|
extern number og;
|
|
extern number ob;
|
|
extern number pr;
|
|
extern number pg;
|
|
extern number pb;
|
|
extern number h;
|
|
|
|
vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords ){
|
|
vec4 pixel = Texel(texture, texture_coords);
|
|
|
|
if (pixel.r == 0) {
|
|
return pixel;
|
|
}
|
|
|
|
if (1 - texture_coords.x < h / 2) {
|
|
pixel.r = pr;
|
|
pixel.g = pg;
|
|
pixel.b = pb;
|
|
} else {
|
|
pixel.r = opr;
|
|
pixel.g = og;
|
|
pixel.b = ob;
|
|
}
|
|
|
|
return pixel;
|
|
}
|
|
]]
|
|
|
|
local function state(songName, songDifficulty, show)
|
|
---@type engine.state
|
|
local state = {} -- Returns needed functions for the state to work after loading it properly
|
|
|
|
local startTime = 0
|
|
|
|
local chartString
|
|
if curChar == "bf" and not Erect then
|
|
chartString = files.read_file(string.format("charts/%s/%s-chart.json", songName, songName))
|
|
else
|
|
chartString = files.read_file(string.format("charts/%s/%s-chart-%s.json", songName, songName,
|
|
Erect and "erect" or curChar))
|
|
end
|
|
|
|
local metadata
|
|
if curChar == "bf" and not Erect then
|
|
metadata = files.read_file(string.format("charts/%s/%s-metadata.json", songName, songName))
|
|
else
|
|
metadata = files.read_file(string.format("charts/%s/%s-metadata-%s.json", songName, songName,
|
|
Erect and "erect" or curChar))
|
|
end
|
|
metadata = json.parse(metadata)
|
|
|
|
if not chartString then
|
|
error("Chart couldn't be loaded!")
|
|
end
|
|
|
|
local chart = json.parse(chartString)
|
|
|
|
_G.inst = love.audio.newSource(string.format("songs/%s/Inst.ogg", metadata.songName), "stream")
|
|
_G.voices = love.filesystem.getInfo(string.format("songs/%s/Voices.ogg", metadata.songName))
|
|
if voices then
|
|
voices = love.audio.newSource(string.format("songs/%s/Voices.ogg", metadata.songName), "stream")
|
|
end
|
|
|
|
local speed = chart.scrollSpeed[songDifficulty] and chart.scrollSpeed[songDifficulty] / 2 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(metadata.timeChanges[1].bpm)
|
|
-- conductor:mapBpmChanges(chart)
|
|
|
|
local step = 0
|
|
local beat = 0
|
|
|
|
local zoom = 1
|
|
local uiZoom = 1
|
|
local mustZoom = 2
|
|
local iconZoom = 1
|
|
local zoomIntensivity = 1
|
|
|
|
local combo = 0
|
|
local highestCombo = 0
|
|
|
|
_G.modules = {}
|
|
|
|
local playing = false
|
|
|
|
local stageName = metadata.playData.stage
|
|
local stageString = files.read_file(string.format("stages/%s.json", stageName)) or
|
|
files.read_file("stages/stage.json")
|
|
|
|
_G.stage = json.parse(stageString)
|
|
|
|
local unspawnedNotes = {}
|
|
local notes = {}
|
|
|
|
local defaultZoom = 1
|
|
|
|
local unspawnedHoldNotes = {}
|
|
local holdNotes = {}
|
|
|
|
local events = {}
|
|
|
|
local characters = {}
|
|
|
|
local icons = {}
|
|
|
|
local data = love.filesystem.getSaveDirectory()
|
|
|
|
local ui = {
|
|
healthIcons = true, -- If halth is false it wont render either way
|
|
health = true,
|
|
score = 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 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 holdCovers = {}
|
|
local opponentHoldCovers = {}
|
|
local splashes = {}
|
|
|
|
local shader = love.graphics.newShader(healthbarShader)
|
|
|
|
local healthBar = Image("images/healthBar.png")
|
|
healthBar.anchor = Vector2(0.5, 0.5)
|
|
healthBar.shader = shader
|
|
healthBar.position = Vector2(960, 50)
|
|
healthBar.ui = true
|
|
healthBar.resize = Vector2(1.5, 1.5)
|
|
healthBar.layer = 15
|
|
|
|
local keyBinds = {} -- loaded from settings.json, if anything's wrong then try rebinding in the menu
|
|
local offset = 0
|
|
|
|
local beatrate = 4
|
|
|
|
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 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 engine.sharedvars
|
|
--- @field canStart boolean
|
|
--- @field screenSize engine.vector2
|
|
--- @field canvasSize engine.vector2
|
|
--- @field settings table
|
|
--- @field receptors table<engine.sprite>
|
|
--- @field splashes table<engine.sprite>
|
|
--- @field opponentReceptors table<engine.sprite>
|
|
--- @field health number
|
|
--- @field speed number
|
|
--- @field ui table<boolean>
|
|
--- @field notes table<engine.note>
|
|
--- @field characters table<engine.character>
|
|
_G.sharedVars = {
|
|
canStart = true,
|
|
screenSize = Vector2(1280, 720),
|
|
canvasSize = Vector2(3840, 2160),
|
|
settings = settings,
|
|
receptors = receptors,
|
|
splashes = splashes,
|
|
opponentReceptors = opponentReceptors,
|
|
health = 1,
|
|
speed = speed,
|
|
ui = ui,
|
|
zoom = zoom,
|
|
defaultZoom = defaultZoom,
|
|
notes = notes, -- only spawned notes
|
|
holds = holdNotes,
|
|
characters = characters,
|
|
shouldCountdown = true,
|
|
}
|
|
|
|
local cameraTween = tween.new(1, render.cameraPosition, { x = 0, y = 0 }, tween.easing.inOutQuad)
|
|
local zoomTween = tween.new(32, sharedVars, { defaultZoom = defaultZoom }, tween.easing.outExpo)
|
|
|
|
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 voices 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
|
|
render.offset = Vector2()
|
|
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 voices then
|
|
voices:stop()
|
|
voices:release()
|
|
voices = nil
|
|
end
|
|
|
|
miss:stop()
|
|
miss:release()
|
|
|
|
render.destroyAllSprites()
|
|
|
|
deadBF = Character(string.format("%s-dead", metadata.playData.characters.player))
|
|
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
|
|
|
|
sharedVars.health = sharedVars.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"
|
|
|
|
for index, module in next, modules do
|
|
if module.noteHit then
|
|
module.noteHit(closestNote)
|
|
end
|
|
end
|
|
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 voices 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.cameraPosition = 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
|
|
|
|
render.updateSprites(dt)
|
|
-- 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 voices then
|
|
voices:play()
|
|
end
|
|
|
|
elapsed = 0
|
|
|
|
playing = true --We can truly play now
|
|
|
|
startTime = socket.gettime()
|
|
cdLength = 0
|
|
render.cameraPosition = Vector2(stage.camera_opponent[1], stage.camera_opponent[2]):Add(characters
|
|
.dad and characters.dad.stageCamera:Negate() or characters.bf.stageCamera:Negate()):Add(Vector2(
|
|
0,
|
|
-200))
|
|
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 or character.sprite.ended then
|
|
if name == "gf" or character.animInfo.danceLeft then
|
|
character:PlayAnimation("danceLeft")
|
|
else
|
|
character:PlayAnimation("idle")
|
|
end
|
|
end
|
|
end
|
|
if beat % beatrate == 0 then
|
|
zoom = zoom + (.02 * zoomIntensivity)
|
|
uiZoom = uiZoom + (.05 * zoomIntensivity)
|
|
end
|
|
else
|
|
for name, character in next, characters do
|
|
if not character.singing or character.sprite.ended 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
|
|
|
|
iconZoom = 1.2
|
|
end
|
|
|
|
if cameraTween then
|
|
local finished = cameraTween:update(dt)
|
|
if finished then cameraTween = nil end
|
|
end
|
|
if zoomTween then
|
|
local finsihed = zoomTween:update(dt)
|
|
if finsihed then zoomTween = nil end
|
|
end
|
|
|
|
-- 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 < 1500 then
|
|
holdNote:Spawn()
|
|
unspawnedHoldNotes[index] = nil
|
|
holdNotes[#holdNotes + 1] = holdNote
|
|
end
|
|
end
|
|
|
|
|
|
for index, note in next, unspawnedNotes do
|
|
if (note.position - elapsed) * speed < 1500 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
|
|
for index, module in next, modules do
|
|
if module.onMiss then
|
|
module.onMiss(note)
|
|
end
|
|
end
|
|
note:Destroy()
|
|
miss:stop()
|
|
miss:play()
|
|
characters.bf:PlayAnimation("sing" .. directions[note.direction] .. "miss")
|
|
notes[index] = nil
|
|
ratings.miss = ratings.miss + 1
|
|
sharedVars.health = sharedVars.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
|
|
characters[note.character or "dad"]:PlayAnimation("sing" .. directions[note.direction])
|
|
|
|
for index, module in next, modules do
|
|
if module.opponentNoteHit then
|
|
module.opponentNoteHit(note)
|
|
end
|
|
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 < 0 then
|
|
if love.keyboard.isDown(keyBinds[hold.direction]) then
|
|
if characters.bf.animInfo["sing" .. directions[hold.direction] .. "-alt"] then
|
|
characters.bf:PlayAnimation("sing" .. directions[hold.direction] .. "-alt")
|
|
else
|
|
characters.bf:PlayAnimation("sing" .. directions[hold.direction])
|
|
end
|
|
hold:Destroy()
|
|
holdNotes[index] = nil
|
|
sharedVars.health = sharedVars.health + hold.hitHealth * 0.2
|
|
receptors[hold.direction]:PlayAnimation(
|
|
string.format("%s confirm", string.lower(directions[hold.direction])), 24, false)
|
|
receptorAnims[hold.direction] = "confirm"
|
|
if holdCovers[hold.direction].frame > 2 or not holdCovers[hold.direction].animation or holdCovers[hold.direction].alpha == 0 then
|
|
holdCovers[hold.direction]:PlayAnimation(string.format("holdCover%s", covers[hold.direction]), 24, false)
|
|
end
|
|
holdCovers[hold.direction].alpha = 1
|
|
holdCovers[hold.direction].ende = false
|
|
elseif (hold.position - elapsed) * speed < -150 then
|
|
hold:Destroy()
|
|
characters.bf:PlayAnimation("sing" .. directions[hold.direction] .. "miss")
|
|
holdNotes[index] = nil
|
|
sharedVars.health = sharedVars.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
|
|
characters[hold.character or "dad"]:PlayAnimation("sing" .. directions[hold.direction])
|
|
|
|
for index, module in next, modules do
|
|
if module.opponentNoteHit then
|
|
module.opponentNoteHit(hold)
|
|
end
|
|
end
|
|
hold:Destroy()
|
|
holdNotes[index] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
for index, event in next, events do
|
|
if event.time <= elapsed then
|
|
events[index] = nil
|
|
|
|
if event.name == "focuscamera" then
|
|
local char = tonumber(type(event.vars) == "table" and event.vars.char or event.vars)
|
|
local cameraPosition =
|
|
char == 1 and
|
|
Vector2(stage.camera_opponent[1], stage.camera_opponent[2]):Add(characters.dad.stageCamera
|
|
:Negate()):Add(Vector2(0, -200)) or
|
|
char == 2 and
|
|
Vector2(stage.camera_girlfriend[1], stage.camera_girlfriend[2]):Add(characters.gf.stageCamera
|
|
:Negate()):Add(Vector2(0, -200)) or
|
|
char == 0 and
|
|
Vector2(-stage.camera_boyfriend[1], -stage.camera_boyfriend[2]):Add(characters.bf.stageCamera
|
|
:Negate()):Add(Vector2(0, -200))
|
|
|
|
local ease = tween.easing.outQuad
|
|
|
|
local easetime = 1.5
|
|
|
|
if event.vars and type(event.vars) == "table" then
|
|
if event.vars.ease and event.vars.ease ~= "CLASSIC" then
|
|
easetime = event.vars.duration and event.vars.duration / 16 or 1.5
|
|
end
|
|
end
|
|
|
|
cameraTween = tween.new(easetime,
|
|
render.cameraPosition, { x = cameraPosition.x, y = cameraPosition.y }, ease)
|
|
elseif event.name == "zoomcamera" then
|
|
local duration = event.vars.ease == "CLASSIC" and 1.5 or event.vars.duration and event.vars.duration / 16 or 2
|
|
local ease = tween.easing.outQuad
|
|
|
|
zoomTween = tween.new(duration, sharedVars, { defaultZoom = event.vars.zoom },
|
|
ease)
|
|
elseif event.name == "playanimation" or event.name == "play animation" then
|
|
local chars = {
|
|
dad = "dad",
|
|
boyfriend = "bf",
|
|
girlfriend = "gf"
|
|
}
|
|
|
|
if not characters[chars[event.vars.target] or event.vars.target].animInfo[event.vars.anim] then return end
|
|
|
|
characters[chars[event.vars.target] or event.vars.target]:PlayAnimation(event.vars.anim)
|
|
elseif event.name == "change character" then
|
|
local chars = {
|
|
dad = "dad",
|
|
boyfriend = "bf",
|
|
girlfriend = "gf"
|
|
}
|
|
|
|
characters[chars[event.vars.char] or event.vars.char]:Destroy()
|
|
characters[chars[event.vars.char] or event.vars.char] = Character(event.vars.to)
|
|
|
|
if not characters[chars[event.vars.char] or event.vars.char].hasStagePosition then
|
|
characters[chars[event.vars.char] or event.vars.char].stagePosition = Vector2(stage.opponent[1], stage.opponent[2])
|
|
end
|
|
elseif event.name == "setcamerabop" then
|
|
beatrate = event.vars.rate
|
|
zoomIntensivity = event.vars.intensity
|
|
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) * sharedVars.defaultZoom, .05)
|
|
uiZoom = Lerp(uiZoom, 1, .05)
|
|
iconZoom = Lerp(iconZoom, 1, .1)
|
|
|
|
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
|
|
|
|
for index, holdCover in next, holdCovers do
|
|
if holdCover.ended then
|
|
if holdCover.ende then
|
|
holdCover.alpha = 0
|
|
else
|
|
holdCover:PlayAnimation(string.format("holdCoverEnd%s", covers[index]), 24, false)
|
|
holdCover.ende = true
|
|
end
|
|
end
|
|
end
|
|
|
|
shader:send("h", sharedVars.health)
|
|
|
|
if sharedVars.health <= 0 then
|
|
die()
|
|
elseif sharedVars.health > 2 then
|
|
sharedVars.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
|
|
if ui.healthIcons then
|
|
love.graphics.draw(icons.bf.image, sharedVars.health > .2 and icons.bf.alive or icons.bf.dead,
|
|
(1110 - (sharedVars.health - 1) * 460) + render.offset.x - (75 - 150 * iconZoom / 2),
|
|
(settings.Downscroll and 0 or 930) + render.offset.y + (75 - 150 * iconZoom / 2), 0, -iconZoom,
|
|
iconZoom)
|
|
|
|
if characters.dad then
|
|
love.graphics.draw(icons.dad.image, sharedVars.health < 1.8 and icons.dad.alive or icons.dad.dead,
|
|
(810 - (sharedVars.health - 1) * 460) + render.offset.x + (75 - 150 * iconZoom / 2),
|
|
(settings.Downscroll and 0 or 930) + render.offset.y + (75 - 150 * iconZoom / 2),
|
|
0, iconZoom, iconZoom)
|
|
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 * mustZoom)) / 2,
|
|
(love.graphics.getHeight() - love.graphics.getHeight() * zoom * mustZoom) / 2,
|
|
0,
|
|
(love.graphics.getWidth() / sharedVars.canvasSize.x) * zoom * mustZoom,
|
|
(love.graphics.getHeight() / sharedVars.canvasSize.y) * zoom * mustZoom
|
|
)
|
|
love.graphics.draw(uiCanvas,
|
|
(love.graphics.getWidth() - (love.graphics.getWidth() * uiZoom * mustZoom)) / 2,
|
|
(love.graphics.getHeight() - love.graphics.getHeight() * uiZoom * mustZoom) / 2,
|
|
0,
|
|
(love.graphics.getWidth() / sharedVars.canvasSize.x) * uiZoom * mustZoom,
|
|
(love.graphics.getHeight() / sharedVars.canvasSize.y) * uiZoom * mustZoom
|
|
)
|
|
|
|
for i, module in next, modules do
|
|
if module.drawAboveCanvas then
|
|
module.drawAboveCanvas() --mainly for cutscenes i guess
|
|
end
|
|
end
|
|
|
|
love.graphics.setShader()
|
|
|
|
love.graphics.print({ { 0, 0, 0, 1 }, string.format("FPS: %s", love.timer.getFPS()) }, font)
|
|
end
|
|
|
|
function state.load()
|
|
love.window.setTitle(string.format("TaggedEngine - %s", metadata.songName))
|
|
|
|
settings = json.parse(files.read_file(data .. "/Settings.json"))
|
|
if not settings then
|
|
error("Failed to load settings")
|
|
end
|
|
|
|
local stageScript = string.format("stages/%s.lua", metadata.playData.stage)
|
|
|
|
|
|
local stageModule
|
|
|
|
local opened = files.read_file(stageScript)
|
|
if opened then
|
|
stageModule = require(string.format("stages.%s", metadata.playData.stage))
|
|
modules[#modules + 1] = stageModule
|
|
end
|
|
|
|
local songScriptExists = love.filesystem.getInfo(string.format("charts/%s/script.lua", songName))
|
|
print(songScriptExists)
|
|
if songScriptExists then
|
|
print("hey")
|
|
local songScript = require(string.format("charts.%s.script", songName))
|
|
modules[#modules + 1] = songScript
|
|
end
|
|
|
|
-- GF first so she is below other chars
|
|
if metadata.playData.characters.girlfriend then
|
|
characters.gf = Character(metadata.playData.characters.girlfriend)
|
|
characters.gf:PlayAnimation("danceLeft")
|
|
characters.gf.sprite.layer = 0
|
|
if not characters.gf.hasStagePosition then
|
|
characters.gf.stagePosition = Vector2(stage.girlfriend[1], stage.girlfriend[2])
|
|
end
|
|
end
|
|
|
|
characters.bf = Character(metadata.playData.characters.player)
|
|
characters.bf:PlayAnimation("idle")
|
|
characters.bf.sprite.layer = 1
|
|
if not characters.bf.hasStagePosition then
|
|
characters.bf.stagePosition = Vector2(stage.boyfriend[1], stage.boyfriend[2])
|
|
end
|
|
shader:send("pr", characters.bf.colors[1] / 255)
|
|
shader:send("pg", characters.bf.colors[2] / 255)
|
|
shader:send("pb", characters.bf.colors[3] / 255)
|
|
|
|
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)
|
|
}
|
|
|
|
shader:send("opr", 0)
|
|
shader:send("og", 0)
|
|
shader:send("ob", 0)
|
|
|
|
if metadata.playData.characters.opponent and metadata.playData.characters.opponent ~= "none" then -- you can have no player2 but always player1
|
|
characters.dad = Character(metadata.playData.characters.opponent)
|
|
characters.dad:PlayAnimation(characters.dad.animInfo.idle and "idle" or "danceLeft")
|
|
characters.dad.sprite.layer = 1
|
|
if not characters.dad.hasStagePosition then
|
|
characters.dad.stagePosition = Vector2(stage.opponent[1], stage.opponent[2])
|
|
print(stage.opponent[1], stage.opponent[2])
|
|
end
|
|
|
|
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)
|
|
}
|
|
|
|
shader:send("opr", characters.dad.colors[1] / 255)
|
|
shader:send("og", characters.dad.colors[2] / 255)
|
|
shader:send("ob", characters.dad.colors[3] / 255)
|
|
end
|
|
|
|
conductor.stepCrochet = conductor:calculateCrochet(metadata.timeChanges[1].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.xml")
|
|
receptor:PlayAnimation("arrow" .. directions[i + 1], 24, true)
|
|
receptor.layer = 9
|
|
receptor.position = Vector2(1100 + (150 * i), settings.Downscroll and 860 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(1040 + (150 * i), settings.Downscroll and 800 or -100)
|
|
splash.ui = true
|
|
|
|
splashes[i + 1] = splash
|
|
|
|
local noteCover = Sprite(string.format("sprites/holdCovers/holdCover%s.png", covers[i + 1]), string.format("sprites/holdCovers/holdCover%s.xml", covers[i + 1]))
|
|
|
|
noteCover.layer = 12
|
|
|
|
noteCover.position = Vector2(1020 + (150 * i), settings.Downscroll and 780 or -100)
|
|
noteCover.ui = true
|
|
noteCover.starterFrame = 1
|
|
|
|
holdCovers[i + 1] = noteCover
|
|
end
|
|
|
|
for i = 0, 3 do -- opponent receptors, purely graphics
|
|
local receptor = Sprite("sprites/NOTE_assets.png", "sprites/NOTE_assets.xml")
|
|
receptor:PlayAnimation("arrow" .. directions[i + 1], 24, true)
|
|
|
|
receptor.position = Vector2(100 + 150 * i, settings.Downscroll and 860 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, note in next, chart.notes[songDifficulty] do
|
|
local newNote = Note(note)
|
|
for index, module in next, modules do
|
|
if module.processNote then
|
|
module.processNote(newNote)
|
|
end
|
|
end
|
|
unspawnedNotes[#unspawnedNotes + 1] = newNote
|
|
|
|
if note.l and note.l > 0 then
|
|
local length = math.floor(note.l / conductor.stepCrochet)
|
|
local ogT = note.t
|
|
|
|
for i = 0, length - .1, .1 do
|
|
note.t = ogT + i * conductor.stepCrochet
|
|
local newHold = Note(note, true)
|
|
|
|
for index, module in next, modules do
|
|
if module.processNote then
|
|
module.processNote(newHold)
|
|
end
|
|
end
|
|
unspawnedHoldNotes[#unspawnedHoldNotes + 1] = newHold
|
|
end
|
|
note.t = ogT + length * conductor.stepCrochet
|
|
local newHold = Note(note, true, true)
|
|
unspawnedHoldNotes[#unspawnedHoldNotes + 1] = newHold
|
|
newHold.holdEnd = true
|
|
if settings.Downscroll then
|
|
newHold.flipY = true
|
|
end
|
|
newHold.speed = speed
|
|
for index, module in next, modules do
|
|
if module.processNote then
|
|
module.processNote(newHold)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if chart.events then
|
|
for index, event in next, chart.events do
|
|
local newEvent = {
|
|
time = event.t,
|
|
name = string.lower(event.e),
|
|
vars = event.v
|
|
}
|
|
logging.log(newEvent.name)
|
|
events[#events + 1] = newEvent
|
|
|
|
if newEvent.name == "change character" then
|
|
character.preload(newEvent.vars.to)
|
|
end
|
|
end
|
|
end
|
|
|
|
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(metadata.songName)
|
|
end
|
|
end
|
|
end
|
|
|
|
local canvasSize = sharedVars.canvasSize or Vector2(3840, 2160)
|
|
|
|
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 })
|
|
|
|
render.offset = Vector2(960, 540)
|
|
end
|
|
|
|
function state.finish()
|
|
if sharedVars.canStart then
|
|
sharedVars.canStart = false -- already started
|
|
|
|
-- inst:play()
|
|
-- if voices 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
|
|
render.cameraPosition = Vector2(stage.camera_girlfriend[1], stage.camera_girlfriend[2]):Add(characters.gf
|
|
.stageCamera:Negate()):Add(Vector2(0, -200))
|
|
end
|
|
|
|
function state.keypressed(key, un, is)
|
|
if key == "space" then
|
|
if playing then
|
|
paused = not paused
|
|
|
|
if paused then
|
|
inst:pause()
|
|
if voices 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 voices 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 voices 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 == "9" then
|
|
for index, note in next, notes do
|
|
if note.position < 150000 then
|
|
notes[index] = nil
|
|
note = nil
|
|
end
|
|
end
|
|
|
|
for index, note in next, holdNotes do
|
|
if note.position < 150000 then
|
|
holdNotes[index] = nil
|
|
note = nil
|
|
end
|
|
end
|
|
|
|
for index, note in next, unspawnedHoldNotes do
|
|
if note.position < 150000 then
|
|
unspawnedHoldNotes[index] = nil
|
|
note = nil
|
|
end
|
|
end
|
|
|
|
for index, note in next, unspawnedNotes do
|
|
if note.position < 150000 then
|
|
unspawnedNotes[index] = nil
|
|
note = nil
|
|
end
|
|
end
|
|
|
|
startTime = socket.gettime() - 150
|
|
inst:seek(150)
|
|
voices:seek(150)
|
|
end
|
|
end
|
|
|
|
return state
|
|
end
|
|
|
|
return state -- vscode doesnt like this but idrc
|