diff --git a/main.lua b/main.lua index f54b3c1..c48c24f 100644 --- a/main.lua +++ b/main.lua @@ -35,6 +35,8 @@ function love.load() end function initModules() + -- replays are not loaded here, but they are cleared + replays = {} game_modes = {} mode_list = love.filesystem.getDirectoryItems("tetris/modes") for i=1,#mode_list do diff --git a/scene.lua b/scene.lua index aaa81b8..cc5a2f6 100644 --- a/scene.lua +++ b/scene.lua @@ -10,7 +10,9 @@ function Scene:onInputRelease() end ExitScene = require "scene.exit" GameScene = require "scene.game" +ReplayScene = require "scene.replay" ModeSelectScene = require "scene.mode_select" +ReplaySelectScene = require "scene.replay_select" KeyConfigScene = require "scene.key_config" StickConfigScene = require "scene.stick_config" InputConfigScene = require "scene.input_config" diff --git a/scene/replay.lua b/scene/replay.lua new file mode 100644 index 0000000..7c875d3 --- /dev/null +++ b/scene/replay.lua @@ -0,0 +1,71 @@ +local Sequence = require 'tetris.randomizers.fixed_sequence' + +local ReplayScene = Scene:extend() + +ReplayScene.title = "Replay" + +function ReplayScene:new(replay, game_mode, ruleset, inputs) + self.secret_inputs = inputs + self.game = game_mode(self.secret_inputs) + self.ruleset = ruleset(self.game) + -- Replace piece randomizer with replay piece sequence + local randomizer = Sequence(table.keys(ruleset.colourscheme)) + randomizer.sequence = replay["pieces"] + self.game:initializeReplay(self.ruleset, randomizer) + self.inputs = { + left=false, + right=false, + up=false, + down=false, + rotate_left=false, + rotate_left2=false, + rotate_right=false, + rotate_right2=false, + rotate_180=false, + hold=false, + } + self.paused = false + self.replay = replay + self.replay_index = 1 + DiscordRPC:update({ + details = self.game.rpc_details, + state = self.game.name, + largeImageKey = "ingame-"..self.game:getBackground().."00" + }) +end + +function ReplayScene:update() + if love.window.hasFocus() and not self.paused then + self.inputs = self.replay["inputs"][self.replay_index]["inputs"] + self.replay["inputs"][self.replay_index]["frames"] = self.replay["inputs"][self.replay_index]["frames"] - 1 + if self.replay["inputs"][self.replay_index]["frames"] == 0 and self.replay_index < table.getn(self.replay["inputs"]) then + self.replay_index = self.replay_index + 1 + end + local input_copy = {} + for input, value in pairs(self.inputs) do + input_copy[input] = value + end + self.game:update(input_copy, self.ruleset) + self.game.grid:update() + DiscordRPC:update({ + largeImageKey = "ingame-"..self.game:getBackground().."00" + }) + end +end + +function ReplayScene:render() + self.game:draw(self.paused) +end + +function ReplayScene:onInputPress(e) + if (e.input == "menu_back") then + self.game:onExit() + scene = ReplaySelectScene() + elseif e.input == "pause" and not (self.game.game_over or self.game.completed) then + self.paused = not self.paused + if self.paused then pauseBGM() + else resumeBGM() end + end +end + +return ReplayScene diff --git a/scene/replay_select.lua b/scene/replay_select.lua new file mode 100644 index 0000000..c52d96c --- /dev/null +++ b/scene/replay_select.lua @@ -0,0 +1,235 @@ +local ReplaySelectScene = Scene:extend() + +ReplaySelectScene.title = "Replays" + +local binser = require 'libs.binser' + +current_replay = 1 + +function ReplaySelectScene:new() + -- reload custom modules + initModules() + -- load replays + replays = {} + replay_file_list = love.filesystem.getDirectoryItems("replays") + for i=1,#replay_file_list do + local data = love.filesystem.read("replays/"..replay_file_list[i]) + local new_replay = binser.deserialize(data)[1] + -- Insert, sorting by date played, newest first + local start_index, mid_index, end_index = 1, 1, i + if i ~= 1 then + while start_index <= end_index do + mid_index = math.floor((start_index + end_index) / 2) + if os.difftime(replays[mid_index]["timestamp"], new_replay["timestamp"]) <= 0 then + -- search first half + end_index = mid_index - 1 + else + -- search second half + start_index = mid_index + 1 + end + end + end + table.insert(replays, mid_index, new_replay) + end + self.display_error = false + if table.getn(replays) == 0 then + self.display_warning = true + current_replay = 1 + else + self.display_warning = false + if current_replay > table.getn(replays) then + current_replay = 1 + end + end + + self.menu_state = { + replay = current_replay, + } + self.secret_inputs = {} + self.das = 0 + DiscordRPC:update({ + details = "In menus", + state = "Choosing a replay", + largeImageKey = "ingame-000" + }) +end + +function ReplaySelectScene:update() + switchBGM(nil) -- experimental + + if self.das_up or self.das_down or self.das_left or self.das_right then + self.das = self.das + 1 + else + self.das = 0 + end + + if self.das >= 15 then + local change = 0 + if self.das_up then + change = -1 + elseif self.das_down then + change = 1 + elseif self.das_left then + change = -9 + elseif self.das_right then + change = 9 + end + self:changeOption(change) + self.das = self.das - 4 + end + + DiscordRPC:update({ + details = "In menus", + state = "Choosing a replay", + largeImageKey = "ingame-000" + }) +end + +function ReplaySelectScene:render() + love.graphics.draw( + backgrounds[0], + 0, 0, 0, + 0.5, 0.5 + ) + + -- Same graphic as mode select + love.graphics.draw(misc_graphics["select_mode"], 20, 40) + + if self.display_warning then + love.graphics.setFont(font_3x5_3) + love.graphics.printf( + "You have no replays.", + 80, 200, 480, "center" + ) + love.graphics.setFont(font_3x5_2) + love.graphics.printf( + "Come back to this menu after playing some games. " .. + "Press any button to return to the main menu.", + 80, 250, 480, "center" + ) + return + elseif self.display_error then + love.graphics.setFont(font_3x5_3) + love.graphics.printf( + "You are missing this mode or ruleset.", + 80, 200, 480, "center" + ) + love.graphics.setFont(font_3x5_2) + love.graphics.printf( + "Come back after getting the proper mode or ruleset. " .. + "Press any button to return to the main menu.", + 80, 250, 480, "center" + ) + return + end + + love.graphics.setColor(1, 1, 1, 0.5) + love.graphics.rectangle("fill", 3, 258, 634, 22) + + love.graphics.setFont(font_3x5_2) + for idx, replay in ipairs(replays) do + if(idx >= self.menu_state.replay-9 and idx <= self.menu_state.replay+9) then + local display_string = os.date("%c", replay["timestamp"]).." "..replay["mode"].." "..replay["ruleset"] + if replay["level"] ~= nil then + display_string = display_string.." Level: "..replay["level"] + end + if replay["timer"] ~= nil then + display_string = display_string.." Time: "..formatTime(replay["timer"]) + end + love.graphics.printf(display_string, 6, (260 - 20*(self.menu_state.replay)) + 20 * idx, 640, "left") + end + end +end + +function ReplaySelectScene:onInputPress(e) + if (self.display_warning or self.display_error) and e.input then + scene = TitleScene() + elseif e.type == "wheel" then + if e.x % 2 == 1 then + self:switchSelect() + end + if e.y ~= 0 then + self:changeOption(-e.y) + end + elseif e.input == "menu_decide" or e.scancode == "return" then + current_replay = self.menu_state.replay + -- Same as mode decide + playSE("mode_decide") + -- Get game mode and ruleset + local mode + local rules + for key, value in pairs(game_modes) do + if value.name == replays[self.menu_state.replay]["mode"] then + mode = value + break + end + end + for key, value in pairs(rulesets) do + if value.name == replays[self.menu_state.replay]["ruleset"] then + rules = value + break + end + end + if mode == nil or rules == nil then + self.display_error = true + return + end + -- TODO compare replay versions to current versions for Cambridge, ruleset, and mode + scene = ReplayScene( + replays[self.menu_state.replay], + mode, + rules, + self.secret_inputs + ) + elseif e.input == "up" or e.scancode == "up" then + self:changeOption(-1) + self.das_up = true + self.das_down = nil + self.das_left = nil + self.das_right = nil + elseif e.input == "down" or e.scancode == "down" then + self:changeOption(1) + self.das_down = true + self.das_up = nil + self.das_left = nil + self.das_right = nil + elseif e.input == "left" or e.scancode == "left" then + self:changeOption(-9) + self.das_left = true + self.das_right = nil + self.das_up = nil + self.das_down = nil + elseif e.input == "right" or e.scancode == "right" then + self:changeOption(9) + self.das_right = true + self.das_left = nil + self.das_up = nil + self.das_down = nil + elseif e.input == "menu_back" or e.scancode == "delete" or e.scancode == "backspace" then + scene = TitleScene() + elseif e.input then + self.secret_inputs[e.input] = true + end +end + +function ReplaySelectScene:onInputRelease(e) + if e.input == "up" or e.scancode == "up" then + self.das_up = nil + elseif e.input == "down" or e.scancode == "down" then + self.das_down = nil + elseif e.input == "right" or e.scancode == "right" then + self.das_right = nil + elseif e.input == "left" or e.scancode == "left" then + self.das_left = nil + elseif e.input then + self.secret_inputs[e.input] = false + end +end + +function ReplaySelectScene:changeOption(rel) + local len = table.getn(replays) + self.menu_state.replay = Mod1(self.menu_state.replay + rel, len) + playSE("cursor") +end + +return ReplaySelectScene diff --git a/scene/title.lua b/scene/title.lua index 05df3bb..6f111dc 100644 --- a/scene/title.lua +++ b/scene/title.lua @@ -5,6 +5,7 @@ TitleScene.restart_message = false local main_menu_screens = { ModeSelectScene, + ReplaySelectScene, SettingsScene, CreditsScene, ExitScene, diff --git a/tetris/modes/gamemode.lua b/tetris/modes/gamemode.lua index abbee8e..15b1da5 100644 --- a/tetris/modes/gamemode.lua +++ b/tetris/modes/gamemode.lua @@ -7,6 +7,7 @@ local playedGoSE = false local Grid = require 'tetris.components.grid' local Randomizer = require 'tetris.randomizers.randomizer' local BagRandomizer = require 'tetris.randomizers.bag' +local binser = require 'libs.binser' local GameMode = Object:extend() @@ -72,6 +73,9 @@ function GameMode:new(secret_inputs) self.section_start_time = 0 self.section_times = { [0] = 0 } self.secondary_section_times = { [0] = 0 } + self.replay_inputs = {} + self.replay_pieces = "" + self.save_replay = true end function GameMode:getARR() return 1 end @@ -86,6 +90,7 @@ function GameMode:getGravity() return 1/64 end function GameMode:getNextPiece(ruleset) local shape = self.used_randomizer:nextPiece() + self.replay_pieces = self.replay_pieces..shape return { skin = self:getSkin(), shape = shape, @@ -110,7 +115,18 @@ function GameMode:initialize(ruleset) for i = 1, math.max(self.next_queue_length, 1) do table.insert(self.next_queue, self:getNextPiece(ruleset)) end - self.lock_on_soft_drop = ({ruleset.softdrop_lock, self.instant_soft_drop, false, true })[config.gamesettings.manlock] + self.lock_on_soft_drop = ({ruleset.softdrop_lock, self.instant_soft_drop, false, true})[config.gamesettings.manlock] + self.lock_on_hard_drop = ({ruleset.harddrop_lock, self.instant_hard_drop, true, false})[config.gamesettings.manlock] +end + +function GameMode:initializeReplay(ruleset, randomizer) + self.used_randomizer = randomizer + self.save_replay = false + self.ruleset = ruleset + for i = 1, math.max(self.next_queue_length, 1) do + table.insert(self.next_queue, self:getNextPiece(ruleset)) + end + self.lock_on_soft_drop = ({ruleset.softdrop_lock, self.instant_soft_drop, false, true})[config.gamesettings.manlock] self.lock_on_hard_drop = ({ruleset.harddrop_lock, self.instant_hard_drop, true, false})[config.gamesettings.manlock] end @@ -129,6 +145,25 @@ function GameMode:update(inputs, ruleset) end end + -- check if inputs have changed since last frame + if self.prev_inputs["left"] ~= inputs["left"] or self.prev_inputs["right"] ~= inputs["right"] + or self.prev_inputs["down"] ~= inputs["down"] or self.prev_inputs["up"] ~= inputs["up"] + or self.prev_inputs["rotate_left"] ~= inputs["rotate_left"] or self.prev_inputs["rotate_right"] ~= inputs["rotate_right"] + or self.prev_inputs["hold"] ~= inputs["hold"] or self.prev_inputs["rotate_180"] ~= inputs["rotate_180"] + or self.prev_inputs["rotate_left2"] ~= inputs["rotate_left2"] or self.prev_inputs["rotate_right2"] ~= inputs["rotate_right2"] then + -- insert new inputs into replay inputs table + local new_inputs = {} + new_inputs["inputs"] = {} + new_inputs["frames"] = 1 + for key, value in pairs(inputs) do + new_inputs["inputs"][key] = value + end + self.replay_inputs[#self.replay_inputs + 1] = new_inputs + else + -- add 1 to input frame counter + self.replay_inputs[#self.replay_inputs]["frames"] = self.replay_inputs[#self.replay_inputs]["frames"] + 1 + end + -- advance one frame if self:advanceOneFrame(inputs, ruleset) == false then return end @@ -336,7 +371,41 @@ function GameMode:onGameOver() switchBGM(nil) local alpha = 0 local animation_length = 120 - if self.game_over_frames < animation_length then + if self.game_over_frames == 1 then + alpha = 1 + if self.save_replay then + -- Save replay. + local replay = {} + replay["inputs"] = self.replay_inputs + replay["pieces"] = self.replay_pieces + replay["mode"] = self.name + replay["ruleset"] = self.ruleset.name + replay["timer"] = self.frames + replay["score"] = self.score + replay["level"] = self.level + replay["lines"] = self.lines + replay["gamesettings"] = config.gamesettings + replay["timestamp"] = os.time() + if love.filesystem.getInfo("replays") == nil then + love.filesystem.createDirectory("replays") + end + local replay_files = love.filesystem.getDirectoryItems("replays") + -- Select replay filename that doesn't collide with an existing one + local replay_number = 0 + local collision = true + while collision do + collision = false + replay_number = replay_number + 1 + for key, file in pairs(replay_files) do + if file == replay_number..".rply" then + collision = true + break + end + end + end + love.filesystem.write("replays/"..replay_number..".rply", binser.serialize(replay)) + end + elseif self.game_over_frames < animation_length then -- Show field for a bit, then fade out. alpha = math.pow(2048, self.game_over_frames/animation_length - 1) elseif self.game_over_frames < 2 * animation_length then