From baa4c739e5dfede0325aef5d398166d4adf2c513 Mon Sep 17 00:00:00 2001 From: Elijah Gregg Date: Wed, 24 Mar 2021 16:54:26 -0600 Subject: [PATCH] Finish up menus (for now) - Add keyboard settings - Save settings (automatic) - Menu scroll position (for longer menus) - Menu key override function (for redefining keys) - Fix bug in "Restart" entry on the "end" menu - Add some more comments to improve code readability --- libraries/binser.lua | 750 +++++++++++++++++++++++++++++++++++++++++++ main.lua | 313 ++++++++++++++---- tags | 43 ++- 3 files changed, 1048 insertions(+), 58 deletions(-) create mode 100644 libraries/binser.lua diff --git a/libraries/binser.lua b/libraries/binser.lua new file mode 100644 index 0000000..00958f0 --- /dev/null +++ b/libraries/binser.lua @@ -0,0 +1,750 @@ +-- binser.lua + +--[[ +Copyright (c) 2016-2019 Calvin Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +]] + +local assert = assert +local error = error +local select = select +local pairs = pairs +local getmetatable = getmetatable +local setmetatable = setmetatable +local type = type +local loadstring = loadstring or load +local concat = table.concat +local char = string.char +local byte = string.byte +local format = string.format +local sub = string.sub +local dump = string.dump +local floor = math.floor +local frexp = math.frexp +local unpack = unpack or table.unpack + +-- Lua 5.3 frexp polyfill +-- From https://github.com/excessive/cpml/blob/master/modules/utils.lua +if not frexp then + local log, abs, floor = math.log, math.abs, math.floor + local log2 = log(2) + frexp = function(x) + if x == 0 then return 0, 0 end + local e = floor(log(abs(x)) / log2 + 1) + return x / 2 ^ e, e + end +end + +local function pack(...) + return {...}, select("#", ...) +end + +local function not_array_index(x, len) + return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x) +end + +local function type_check(x, tp, name) + assert(type(x) == tp, + format("Expected parameter %q to be of type %q.", name, tp)) +end + +local bigIntSupport = false +local isInteger +if math.type then -- Detect Lua 5.3 + local mtype = math.type + bigIntSupport = loadstring[[ + local char = string.char + return function(n) + local nn = n < 0 and -(n + 1) or n + local b1 = nn // 0x100000000000000 + local b2 = nn // 0x1000000000000 % 0x100 + local b3 = nn // 0x10000000000 % 0x100 + local b4 = nn // 0x100000000 % 0x100 + local b5 = nn // 0x1000000 % 0x100 + local b6 = nn // 0x10000 % 0x100 + local b7 = nn // 0x100 % 0x100 + local b8 = nn % 0x100 + if n < 0 then + b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4 + b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8 + end + return char(212, b1, b2, b3, b4, b5, b6, b7, b8) + end]]() + isInteger = function(x) + return mtype(x) == 'integer' + end +else + isInteger = function(x) + return floor(x) == x + end +end + +-- Copyright (C) 2012-2015 Francois Perrad. +-- number serialization code modified from https://github.com/fperrad/lua-MessagePack +-- Encode a number as a big-endian ieee-754 double, big-endian signed 64 bit integer, or a small integer +local function number_to_str(n) + if isInteger(n) then -- int + if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data + return char(n + 27) + elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data + n = n + 8192 + return char(128 + (floor(n / 0x100) % 0x100), n % 0x100) + elseif bigIntSupport then + return bigIntSupport(n) + end + end + local sign = 0 + if n < 0.0 then + sign = 0x80 + n = -n + end + local m, e = frexp(n) -- mantissa, exponent + if m ~= m then + return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + elseif m == 1/0 then + if sign == 0 then + return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + else + return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + end + end + e = e + 0x3FE + if e < 1 then -- denormalized numbers + m = m * 2 ^ (52 + e) + e = 0 + else + m = (m * 2 - 1) * 2 ^ 52 + end + return char(203, + sign + floor(e / 0x10), + (e % 0x10) * 0x10 + floor(m / 0x1000000000000), + floor(m / 0x10000000000) % 0x100, + floor(m / 0x100000000) % 0x100, + floor(m / 0x1000000) % 0x100, + floor(m / 0x10000) % 0x100, + floor(m / 0x100) % 0x100, + m % 0x100) +end + +-- Copyright (C) 2012-2015 Francois Perrad. +-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack +local function number_from_str(str, index) + local b = byte(str, index) + if not b then error("Expected more bytes of input.") end + if b < 128 then + return b - 27, index + 1 + elseif b < 192 then + local b2 = byte(str, index + 1) + if not b2 then error("Expected more bytes of input.") end + return b2 + 0x100 * (b - 128) - 8192, index + 2 + end + local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8) + if (not b1) or (not b2) or (not b3) or (not b4) or + (not b5) or (not b6) or (not b7) or (not b8) then + error("Expected more bytes of input.") + end + if b == 212 then + local flip = b1 >= 128 + if flip then -- negative + b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4 + b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8 + end + local n = ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * + 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + if flip then + return (-n) - 1, index + 9 + else + return n, index + 9 + end + end + if b ~= 203 then + error("Expected number") + end + local sign = b1 > 0x7F and -1 or 1 + local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10) + local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + local n + if e == 0 then + if m == 0 then + n = sign * 0.0 + else + n = sign * (m / 2 ^ 52) * 2 ^ -1022 + end + elseif e == 0x7FF then + if m == 0 then + n = sign * (1/0) + else + n = 0.0/0.0 + end + else + n = sign * (1.0 + m / 2 ^ 52) * 2 ^ (e - 0x3FF) + end + return n, index + 9 +end + + +local function newbinser() + + -- unique table key for getting next value + local NEXT = {} + local CTORSTACK = {} + + -- NIL = 202 + -- FLOAT = 203 + -- TRUE = 204 + -- FALSE = 205 + -- STRING = 206 + -- TABLE = 207 + -- REFERENCE = 208 + -- CONSTRUCTOR = 209 + -- FUNCTION = 210 + -- RESOURCE = 211 + -- INT64 = 212 + -- TABLE WITH META = 213 + + local mts = {} + local ids = {} + local serializers = {} + local deserializers = {} + local resources = {} + local resources_by_name = {} + local types = {} + + types["nil"] = function(x, visited, accum) + accum[#accum + 1] = "\202" + end + + function types.number(x, visited, accum) + accum[#accum + 1] = number_to_str(x) + end + + function types.boolean(x, visited, accum) + accum[#accum + 1] = x and "\204" or "\205" + end + + function types.string(x, visited, accum) + local alen = #accum + if visited[x] then + accum[alen + 1] = "\208" + accum[alen + 2] = number_to_str(visited[x]) + else + visited[x] = visited[NEXT] + visited[NEXT] = visited[NEXT] + 1 + accum[alen + 1] = "\206" + accum[alen + 2] = number_to_str(#x) + accum[alen + 3] = x + end + end + + local function check_custom_type(x, visited, accum) + local res = resources[x] + if res then + accum[#accum + 1] = "\211" + types[type(res)](res, visited, accum) + return true + end + local mt = getmetatable(x) + local id = mt and ids[mt] + if id then + local constructing = visited[CTORSTACK] + if constructing[x] then + error("Infinite loop in constructor.") + end + constructing[x] = true + accum[#accum + 1] = "\209" + types[type(id)](id, visited, accum) + local args, len = pack(serializers[id](x)) + accum[#accum + 1] = number_to_str(len) + for i = 1, len do + local arg = args[i] + types[type(arg)](arg, visited, accum) + end + visited[x] = visited[NEXT] + visited[NEXT] = visited[NEXT] + 1 + -- We finished constructing + constructing[x] = nil + return true + end + end + + function types.userdata(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, accum) then return end + error("Cannot serialize this userdata.") + end + end + + function types.table(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, accum) then return end + visited[x] = visited[NEXT] + visited[NEXT] = visited[NEXT] + 1 + local xlen = #x + local mt = getmetatable(x) + if mt then + accum[#accum + 1] = "\213" + types.table(mt, visited, accum) + else + accum[#accum + 1] = "\207" + end + accum[#accum + 1] = number_to_str(xlen) + for i = 1, xlen do + local v = x[i] + types[type(v)](v, visited, accum) + end + local key_count = 0 + for k in pairs(x) do + if not_array_index(k, xlen) then + key_count = key_count + 1 + end + end + accum[#accum + 1] = number_to_str(key_count) + for k, v in pairs(x) do + if not_array_index(k, xlen) then + types[type(k)](k, visited, accum) + types[type(v)](v, visited, accum) + end + end + end + end + + types["function"] = function(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, accum) then return end + visited[x] = visited[NEXT] + visited[NEXT] = visited[NEXT] + 1 + local str = dump(x) + accum[#accum + 1] = "\210" + accum[#accum + 1] = number_to_str(#str) + accum[#accum + 1] = str + end + end + + types.cdata = function(x, visited, accum) + if visited[x] then + accum[#accum + 1] = "\208" + accum[#accum + 1] = number_to_str(visited[x]) + else + if check_custom_type(x, visited, #accum) then return end + error("Cannot serialize this cdata.") + end + end + + types.thread = function() error("Cannot serialize threads.") end + + local function deserialize_value(str, index, visited) + local t = byte(str, index) + if not t then return nil, index end + if t < 128 then + return t - 27, index + 1 + elseif t < 192 then + local b2 = byte(str, index + 1) + if not b2 then error("Expected more bytes of input.") end + return b2 + 0x100 * (t - 128) - 8192, index + 2 + elseif t == 202 then + return nil, index + 1 + elseif t == 203 or t == 212 then + return number_from_str(str, index) + elseif t == 204 then + return true, index + 1 + elseif t == 205 then + return false, index + 1 + elseif t == 206 then + local length, dataindex = number_from_str(str, index + 1) + local nextindex = dataindex + length + if not (length >= 0) then error("Bad string length") end + if #str < nextindex - 1 then error("Expected more bytes of string") end + local substr = sub(str, dataindex, nextindex - 1) + visited[#visited + 1] = substr + return substr, nextindex + elseif t == 207 or t == 213 then + local mt, count, nextindex + local ret = {} + visited[#visited + 1] = ret + nextindex = index + 1 + if t == 213 then + mt, nextindex = deserialize_value(str, nextindex, visited) + if type(mt) ~= "table" then error("Expected table metatable") end + end + count, nextindex = number_from_str(str, nextindex) + for i = 1, count do + local oldindex = nextindex + ret[i], nextindex = deserialize_value(str, nextindex, visited) + if nextindex == oldindex then error("Expected more bytes of input.") end + end + count, nextindex = number_from_str(str, nextindex) + for i = 1, count do + local k, v + local oldindex = nextindex + k, nextindex = deserialize_value(str, nextindex, visited) + if nextindex == oldindex then error("Expected more bytes of input.") end + oldindex = nextindex + v, nextindex = deserialize_value(str, nextindex, visited) + if nextindex == oldindex then error("Expected more bytes of input.") end + if k == nil then error("Can't have nil table keys") end + ret[k] = v + end + if mt then setmetatable(ret, mt) end + return ret, nextindex + elseif t == 208 then + local ref, nextindex = number_from_str(str, index + 1) + return visited[ref], nextindex + elseif t == 209 then + local count + local name, nextindex = deserialize_value(str, index + 1, visited) + count, nextindex = number_from_str(str, nextindex) + local args = {} + for i = 1, count do + local oldindex = nextindex + args[i], nextindex = deserialize_value(str, nextindex, visited) + if nextindex == oldindex then error("Expected more bytes of input.") end + end + if not name or not deserializers[name] then + error(("Cannot deserialize class '%s'"):format(tostring(name))) + end + local ret = deserializers[name](unpack(args)) + visited[#visited + 1] = ret + return ret, nextindex + elseif t == 210 then + local length, dataindex = number_from_str(str, index + 1) + local nextindex = dataindex + length + if not (length >= 0) then error("Bad string length") end + if #str < nextindex - 1 then error("Expected more bytes of string") end + local ret = loadstring(sub(str, dataindex, nextindex - 1)) + visited[#visited + 1] = ret + return ret, nextindex + elseif t == 211 then + local resname, nextindex = deserialize_value(str, index + 1, visited) + if resname == nil then error("Got nil resource name") end + local res = resources_by_name[resname] + if res == nil then + error(("No resources found for name '%s'"):format(tostring(resname))) + end + return res, nextindex + else + error("Could not deserialize type byte " .. t .. ".") + end + end + + local function serialize(...) + local visited = {[NEXT] = 1, [CTORSTACK] = {}} + local accum = {} + for i = 1, select("#", ...) do + local x = select(i, ...) + types[type(x)](x, visited, accum) + end + return concat(accum) + end + + local function make_file_writer(file) + return setmetatable({}, { + __newindex = function(_, _, v) + file:write(v) + end + }) + end + + local function serialize_to_file(path, mode, ...) + local file, err = io.open(path, mode) + assert(file, err) + local visited = {[NEXT] = 1, [CTORSTACK] = {}} + local accum = make_file_writer(file) + for i = 1, select("#", ...) do + local x = select(i, ...) + types[type(x)](x, visited, accum) + end + -- flush the writer + file:flush() + file:close() + end + + local function writeFile(path, ...) + return serialize_to_file(path, "wb", ...) + end + + local function appendFile(path, ...) + return serialize_to_file(path, "ab", ...) + end + + local function deserialize(str, index) + assert(type(str) == "string", "Expected string to deserialize.") + local vals = {} + index = index or 1 + local visited = {} + local len = 0 + local val + while true do + local nextindex + val, nextindex = deserialize_value(str, index, visited) + if nextindex > index then + len = len + 1 + vals[len] = val + index = nextindex + else + break + end + end + return vals, len + end + + local function deserializeN(str, n, index) + assert(type(str) == "string", "Expected string to deserialize.") + n = n or 1 + assert(type(n) == "number", "Expected a number for parameter n.") + assert(n > 0 and floor(n) == n, "N must be a poitive integer.") + local vals = {} + index = index or 1 + local visited = {} + local len = 0 + local val + while len < n do + local nextindex + val, nextindex = deserialize_value(str, index, visited) + if nextindex > index then + len = len + 1 + vals[len] = val + index = nextindex + else + break + end + end + vals[len + 1] = index + return unpack(vals, 1, n + 1) + end + + local function readFile(path) + local file, err = io.open(path, "rb") + assert(file, err) + local str = file:read("*all") + file:close() + return deserialize(str) + end + + -- Resources + + local function registerResource(resource, name) + type_check(name, "string", "name") + assert(not resources[resource], + "Resource already registered.") + assert(not resources_by_name[name], + format("Resource %q already exists.", name)) + resources_by_name[name] = resource + resources[resource] = name + return resource + end + + local function unregisterResource(name) + type_check(name, "string", "name") + assert(resources_by_name[name], format("Resource %q does not exist.", name)) + local resource = resources_by_name[name] + resources_by_name[name] = nil + resources[resource] = nil + return resource + end + + -- Templating + + local function normalize_template(template) + local ret = {} + for i = 1, #template do + ret[i] = template[i] + end + local non_array_part = {} + -- The non-array part of the template (nested templates) have to be deterministic, so they are sorted. + -- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used + -- in templates. Looking for way around this. + for k in pairs(template) do + if not_array_index(k, #template) then + non_array_part[#non_array_part + 1] = k + end + end + table.sort(non_array_part) + for i = 1, #non_array_part do + local name = non_array_part[i] + ret[#ret + 1] = {name, normalize_template(template[name])} + end + return ret + end + + local function templatepart_serialize(part, argaccum, x, len) + local extras = {} + local extracount = 0 + for k, v in pairs(x) do + extras[k] = v + extracount = extracount + 1 + end + for i = 1, #part do + local name + if type(part[i]) == "table" then + name = part[i][1] + len = templatepart_serialize(part[i][2], argaccum, x[name], len) + else + name = part[i] + len = len + 1 + argaccum[len] = x[part[i]] + end + if extras[name] ~= nil then + extracount = extracount - 1 + extras[name] = nil + end + end + if extracount > 0 then + argaccum[len + 1] = extras + else + argaccum[len + 1] = nil + end + return len + 1 + end + + local function templatepart_deserialize(ret, part, values, vindex) + for i = 1, #part do + local name = part[i] + if type(name) == "table" then + local newret = {} + ret[name[1]] = newret + vindex = templatepart_deserialize(newret, name[2], values, vindex) + else + ret[name] = values[vindex] + vindex = vindex + 1 + end + end + local extras = values[vindex] + if extras then + for k, v in pairs(extras) do + ret[k] = v + end + end + return vindex + 1 + end + + local function template_serializer_and_deserializer(metatable, template) + return function(x) + local argaccum = {} + local len = templatepart_serialize(template, argaccum, x, 0) + return unpack(argaccum, 1, len) + end, function(...) + local ret = {} + local args = {...} + templatepart_deserialize(ret, template, args, 1) + return setmetatable(ret, metatable) + end + end + + -- Used to serialize classes withh custom serializers and deserializers. + -- If no _serialize or _deserialize (or no _template) value is found in the + -- metatable, then the metatable is registered as a resources. + local function register(metatable, name, serialize, deserialize) + if type(metatable) == "table" then + name = name or metatable.name + serialize = serialize or metatable._serialize + deserialize = deserialize or metatable._deserialize + if (not serialize) or (not deserialize) then + if metatable._template then + -- Register as template + local t = normalize_template(metatable._template) + serialize, deserialize = template_serializer_and_deserializer(metatable, t) + else + -- Register the metatable as a resource. This is semantically + -- similar and more flexible (handles cycles). + registerResource(metatable, name) + return + end + end + elseif type(metatable) == "string" then + name = name or metatable + end + type_check(name, "string", "name") + type_check(serialize, "function", "serialize") + type_check(deserialize, "function", "deserialize") + assert((not ids[metatable]) and (not resources[metatable]), + "Metatable already registered.") + assert((not mts[name]) and (not resources_by_name[name]), + ("Name %q already registered."):format(name)) + mts[name] = metatable + ids[metatable] = name + serializers[name] = serialize + deserializers[name] = deserialize + return metatable + end + + local function unregister(item) + local name, metatable + if type(item) == "string" then -- assume name + name, metatable = item, mts[item] + else -- assume metatable + name, metatable = ids[item], item + end + type_check(name, "string", "name") + mts[name] = nil + if (metatable) then + resources[metatable] = nil + ids[metatable] = nil + end + serializers[name] = nil + deserializers[name] = nil + resources_by_name[name] = nil; + return metatable + end + + local function registerClass(class, name) + name = name or class.name + if class.__instanceDict then -- middleclass + register(class.__instanceDict, name) + else -- assume 30log or similar library + register(class, name) + end + return class + end + + return { + VERSION = "0.0-8", + -- aliases + s = serialize, + d = deserialize, + dn = deserializeN, + r = readFile, + w = writeFile, + a = appendFile, + + serialize = serialize, + deserialize = deserialize, + deserializeN = deserializeN, + readFile = readFile, + writeFile = writeFile, + appendFile = appendFile, + register = register, + unregister = unregister, + registerResource = registerResource, + unregisterResource = unregisterResource, + registerClass = registerClass, + + newbinser = newbinser + } +end + +return newbinser() diff --git a/main.lua b/main.lua index dd9cc09..200ad0b 100644 --- a/main.lua +++ b/main.lua @@ -1,6 +1,7 @@ -- Import libraries local screen = require( "libraries/shack" ) screen:setDimensions( love.graphics:getWidth(), love.graphics:getHeight() ) +local binser = require( "libraries/binser" ) -- Ensure that every play is different by changing the random number seed math.randomseed( os.time() ) @@ -12,6 +13,7 @@ keyCounter = 0 menuList = {} currentMenu = "main" menuSelect = 1 +menuScroll = 0 -- Player coordinates and scores player1 = { @@ -22,7 +24,9 @@ player1 = { yImg = nil, xVelocity = nil, yVelocity = nil, - counter = 0, + counter = 0 +} +player1Settings = { ai = false, aiDifficulty = 2 } @@ -34,7 +38,9 @@ player2 = { yImg = nil, xVelocity = nil, yVelocity = nil, - counter = 0, + counter = 0 +} +player2Settings = { ai = true, aiDifficulty = 2 } @@ -90,6 +96,56 @@ ball = { color = { 1, 1, 1 } } +-- Function to redefine keys w/ user input +function overrideKeys( player, key ) + if player == "player1" then + menuList.redKeyboard.keyboardOverride = getCurrentKey + else + menuList.blueKeyboard.keyboardOverride = getCurrentKey + end + if keys[ player ][ key ] ~= nil then + keys[ player ][ key ][1] = "--" + else + keys[ player ][ key ] = { "--", "keyboard" } + end + currentKey = nil + settingKey = key + settingPlayer = player +end + +-- Function to test for a key +function getCurrentKey() + if currentKey ~= nil then + keys[settingPlayer][settingKey] = currentKey + if settingPlayer == "player1" then + menuList.redKeyboard.keyboardOverride = nil + else + menuList.blueKeyboard.keyboardOverride = nil + end + end +end + +-- Get current key and save it in a variable +function love.textinput(t) + if t == " " then + t = "space" + end + currentKey = { t, "keyboard" } +end + +-- Arrow keys (supplement to love.textinput) +function arrowKeys() + if love.keyboard.isDown( "up" ) then + currentKey = { "up", "keyboard" } + elseif love.keyboard.isDown( "down" ) then + currentKey = { "down", "keyboard" } + elseif love.keyboard.isDown( "left" ) then + currentKey = { "left", "keyboard" } + elseif love.keyboard.isDown( "right" ) then + currentKey = { "right", "keyboard" } + end +end + -- Function to create the menus -- Usage: -- function another function @@ -101,10 +157,18 @@ function newMenu( objectParameters ) name = objectParameters.name or "menu" .. tostring( menuListLength ) title = objectParameters.title or "menu" .. tostring( menuListLength + 1 ) drawCode = objectParameters.drawCode or drawMenuBackground + keyboardOverride = objectParameters.keyboardOverride or nil items = objectParameters.items or {} menuList[name] = items menuList[name].title = title menuList[name].drawCode = drawCode + menuList[name].keyboardOverride = keyboardOverride +end + +-- Function to save the game settings +function saveSettings() + settings = binser.serialize( keys, player1Settings, player2Settings ) + love.filesystem.write( "settings.lua", settings ) end -- Function to reset the game after every point @@ -115,6 +179,9 @@ function reset() -- Reset player 1 player1.x = playerDistance + 20 player1.y = playAreaHeight - ( 120 + playerDistance ) + player1.counter = 0 + player1.xVelocity = nil + player1.yVelocity = nil -- Reset player 2 player2.x = love.graphics:getWidth() - ( 120 + playerDistance ) @@ -142,9 +209,9 @@ function restart() playerDistance = 40 playAreaHeight = love.graphics:getHeight() - 50 keyCounter = 0 - menuList = {} - currentMenu = "main" + currentMenu = nil menuSelect = 1 + menuScroll = 0 -- Player coordinates and scores player1 = { @@ -155,9 +222,7 @@ function restart() yImg = nil, xVelocity = nil, yVelocity = nil, - counter = 0, - ai = true, - aiDifficulty = 2 + counter = 0 } player2 = { x = love.graphics:getWidth() - ( 120 + playerDistance ), @@ -167,9 +232,7 @@ function restart() yImg = nil, xVelocity = nil, yVelocity = nil, - counter = 0, - ai = true, - aiDifficulty = 2 + counter = 0 } -- Blocks - 10 for each side, positive if the block is there, zero if it is not @@ -210,7 +273,7 @@ end -- Test if key is down function getKeyDown( player, key ) -- If the key is not set for the player, use the default settings - if keys[player][key] == nil then + if keys[player][key] == nil or keys[player][key][1] == "--" then -- Keyboard test for key input, joystick for gamepad input if keys["default"][key][2] == "keyboard" then return( love.keyboard.isDown( keys["default"][key][1] ) ) @@ -235,25 +298,36 @@ function love.load() -- Ball ball.img = love.graphics.newImage( "assets/ball.png" ) + + -- Load settings + if love.filesystem.getInfo( "settings.lua" ) ~= nil then + local settings = love.filesystem.read( "settings.lua" ) + keys = binser.deserialize( settings )[1] + player1Settings = binser.deserialize( settings )[2] + player2Settings = binser.deserialize( settings )[3] + end -- Create menus -- Main menu newMenu( { name = "main", title = "Breakout Pong", items = { { "Play", function() currentMenu = "start" end }, - { "Settings (WIP)", function() currentMenu = "settings" menuSelect = 1 end }, + { "Settings", function() + currentMenu = "settings" + menuSelect = 1 + end }, { "Quit", function() love.event.push( "quit" ) end } } } ) -- Start menu newMenu( { name = "start", title = "Breakout Pong", items = { { "Red: ${1}", - function() player1.ai = not player1.ai end, - function() return player1.ai and "AI" or "Human" end + function() player1Settings.ai = not player1Settings.ai end, + function() return player1Settings.ai and "AI" or "Human" end }, { "Blue: ${1}", - function() player2.ai = not player2.ai end, - function() return player2.ai and "AI" or "Human" end + function() player2Settings.ai = not player2Settings.ai end, + function() return player2Settings.ai and "AI" or "Human" end }, { "Start", function() currentMenu = nil end } } } ) @@ -262,42 +336,110 @@ function love.load() { "Red AI Difficulty: ${1}", function() - player1.aiDifficulty = player1.aiDifficulty + 1 - if player1.aiDifficulty > 3 then - player1.aiDifficulty = 1 + player1Settings.aiDifficulty = player1Settings.aiDifficulty + 1 + if player1Settings.aiDifficulty > 3 then + player1Settings.aiDifficulty = 1 end end, function() - return ( player1.aiDifficulty == 1 ) and "Easy" or - ( player1.aiDifficulty == 2 ) and "Medium" or - ( player1.aiDifficulty == 3 ) and "Hard" + return ( player1Settings.aiDifficulty == 1 ) and "Easy" or + ( player1Settings.aiDifficulty == 2 ) and "Medium" or + ( player1Settings.aiDifficulty == 3 ) and "Hard" end }, { "Blue AI Difficulty: ${1}", function() - player2.aiDifficulty = player2.aiDifficulty + 1 - if player2.aiDifficulty > 3 then - player2.aiDifficulty = 1 + player2Settings.aiDifficulty = player2Settings.aiDifficulty + 1 + if player2Settings.aiDifficulty > 3 then + player2Settings.aiDifficulty = 1 end end, function() - return ( player2.aiDifficulty == 1 ) and "Easy" or - ( player2.aiDifficulty == 2 ) and "Medium" or - ( player2.aiDifficulty == 3 ) and "Hard" + return ( player2Settings.aiDifficulty == 1 ) and "Easy" or + ( player2Settings.aiDifficulty == 2 ) and "Medium" or + ( player2Settings.aiDifficulty == 3 ) and "Hard" end }, - { "Red Keyboard Settings", function() currentMenu = "redKeyboard" end }, - { "Blue Keyboard Settings", function() currentMenu = "blueKeyboard" end }, - { "Back", function() currentMenu = "main" menuSelect = 2 end } + { "Red Keyboard Settings", function() currentMenu = "redKeyboard" menuSelect = 1 menuScroll = 0 end }, + { "Blue Keyboard Settings", function() currentMenu = "blueKeyboard" menuSelect = 1 menuScroll = 0 end }, + { "Back", function() currentMenu = "main" menuSelect = 2 menuScroll = 0 end } } } ) -- Red keyboard menu newMenu( { name = "redKeyboard", title = "Red Keyboard Settings", items = { - { "Left", function() return end } + { "Left: ${1}", function() overrideKeys( "player1", "left" ) end, + function() + return keys.player1.left[1] + end + }, + { "Right: ${1}", function() overrideKeys( "player1", "right" ) end, + function() + return keys.player1.right[1] + end + }, + { "Up: ${1}", function() overrideKeys( "player1", "up" ) end, + function() + return keys.player1.up[1] + end + }, + { "Down: ${1}", function() overrideKeys( "player1", "down" ) end, + function() + return keys.player1.down[1] + end + }, + { "Pause: ${1}", function() overrideKeys( "player1", "pause" ) end, + function() + return keys.player1.pause[1] + end + }, + { "Select: ${1}", function() overrideKeys( "player1", "menuSelect" ) end, + function() + if keys.player1.menuSelect ~= nil then + return keys.player1.menuSelect[1] + else + return keys.default.menuSelect[1] + end + end + }, + { "Back", function() currentMenu = "settings" menuSelect = 3 menuScroll = 0 end } } } ) -- Blue keyboard menu newMenu( { name = "blueKeyboard", title = "Blue Keyboard Settings", items = { - { "Left", function() return end } + { "Left: ${1}", function() overrideKeys( "player2", "left" ) end, + function() + return keys.player2.left[1] + end + }, + { "Right: ${1}", function() overrideKeys( "player2", "right" ) end, + function() + return keys.player2.right[1] + end + }, + { "Up: ${1}", function() overrideKeys( "player2", "up" ) end, + function() + return keys.player2.up[1] + end + }, + { "Down: ${1}", function() overrideKeys( "player2", "down" ) end, + function() + return keys.player2.down[1] + end + }, + { "Pause: ${1}", function() overrideKeys( "player2", "pause" ) end, + function() + return keys.player2.pause[1] + end + }, + { "Select: ${1}", function() overrideKeys( "player2", "menuSelect" ) end, + function() + if keys.player2.menuSelect ~= nil then + return keys.player2.menuSelect[1] + else + return keys.default.menuSelect[1] + end + end + }, + { "Back", function() currentMenu = "settings" menuSelect = 3 menuScroll = 0 end } } } ) -- Pause menu newMenu( { name = "pause", title = "Paused", items = { @@ -338,31 +480,57 @@ function love.load() end function menuUpdate() + -- Default to item 1 if a non-valid item is selected + menuLength = 0 + for i in ipairs( menuList[ currentMenu ] ) do menuLength = menuLength + 1 end + if menuSelect > menuLength then + menuSelect = 1 + elseif menuSelect < 1 then + menuSelect = 1 + end + -- Redirect to an override function if specified + if menuList.redKeyboard.keyboardOverride ~= nil and keyCounter > 10 then + menuList.redKeyboard.keyboardOverride() + return + end + if menuList.blueKeyboard.keyboardOverride ~= nil and keyCounter > 10 then + menuList.blueKeyboard.keyboardOverride() + return + end + -- Down/up keys if ( getKeyDown( "player1", "down" ) or getKeyDown( "player2", "down" ) or getKeyDown( "default", "down" ) ) and keyCounter > 10 then menuSelect = menuSelect + 1 - menuLength = 0 - for i in ipairs( menuList[ currentMenu ] ) do menuLength = menuLength + 1 end if menuSelect > menuLength then menuSelect = 1 + menuScroll = 0 + end + -- Scroll down if not visible + if menuSelect > 4 + menuScroll then + menuScroll = menuScroll + 1 end keyCounter = 0 elseif ( getKeyDown( "player1", "up" ) or getKeyDown( "player2", "up" ) or getKeyDown( "default", "up" ) ) and keyCounter > 10 then menuSelect = menuSelect - 1 if menuSelect < 1 then - menuLength = 0 - for i in pairs( menuList[ currentMenu ] ) do menuLength = menuLength + 1 end menuSelect = menuLength + menuScroll = ( menuLength - 4 > -1 ) and menuLength - 4 or 0 + end + -- Scroll up if not visible + if menuSelect < menuScroll + 1 then + menuScroll = menuScroll - 1 end keyCounter = 0 end + -- Select the current item if ( getKeyDown( "player1", "menuSelect" ) or getKeyDown( "player2", "menuSelect" ) ) and keyCounter > 10 then menuList[ currentMenu ][ menuSelect ][2]() keyCounter = 0 return end + -- Increment the key counter keyCounter = keyCounter + 1 end @@ -409,7 +577,7 @@ function love.update( dt ) return end - if not player1.ai then + if not player1Settings.ai then -- Left and right movement for player 1 if getKeyDown( "player1", "left" ) then player1.x = player1.x - ( ( ( 3 * ball.velocity ) / 2 ) * dt ) @@ -425,7 +593,7 @@ function love.update( dt ) else -- Left and right movement for player 1 if player1.xVelocity == nil then - -- if player1.aiDifficulty + -- if player1Settings.aiDifficulty if player1.x - 20 > ball.x then player1.x = player1.x - ( ( ( 3 * ball.velocity ) / 2 ) * dt ) player1.xVelocity = "left" @@ -454,7 +622,7 @@ function love.update( dt ) end end - if not player1.ai then + if not player1Settings.ai then -- Up and down movement for player 1 if getKeyDown( "player1", "up" ) then player1.y = player1.y - ( ( ( 3 * ball.velocity ) / 2 ) * dt ) @@ -498,7 +666,7 @@ function love.update( dt ) end end - if not player2.ai then + if not player2Settings.ai then -- Left and right movement for player 2 if getKeyDown( "player2", "left" ) then player2.x = player2.x - ( ( ( 3 * ball.velocity ) / 2 ) * dt ) @@ -544,7 +712,7 @@ function love.update( dt ) end end - if not player2.ai then + if not player2Settings.ai then -- Up and down movement for player 2 if getKeyDown( "player2", "up" ) then player2.y = player2.y - ( ( ( 3 * ball.velocity ) / 2 ) * dt ) @@ -589,12 +757,12 @@ function love.update( dt ) end -- Reset the counter(s) and allow the ai(s) to move twice a second - if player1.counter > love.timer.getFPS() / ( player1.aiDifficulty - 0.03 ) then + if player1.counter > love.timer.getFPS() / ( player1Settings.aiDifficulty - 0.03 ) then player1.counter = 0 player1.xVelocity = nil player1.yVelocity = nil end - if player2.counter > love.timer.getFPS() / ( player2.aiDifficulty + 0.03 ) then + if player2.counter > love.timer.getFPS() / ( player2Settings.aiDifficulty + 0.03 ) then player2.counter = 0 player2.xVelocity = nil player2.yVelocity = nil @@ -874,23 +1042,49 @@ function love.draw( dt ) love.graphics.setFont( menuFont ) love.graphics.printf( menuList[currentMenu].title, 10, 160, love.graphics:getWidth() - 20, "center" ) for i, menuItem in ipairs(menuList[currentMenu]) do - if menuSelect == i then - menuFont = love.graphics.newFont( 35 ) - love.graphics.setFont( menuFont ) - love.graphics.setColor( 1, 1, 1 ) - else - menuFont = love.graphics.newFont( 30 ) + if i > menuScroll and i < 5 + menuScroll then + if menuSelect == i then + menuFont = love.graphics.newFont( 35 ) + love.graphics.setFont( menuFont ) + love.graphics.setColor( 1, 1, 1 ) + else + menuFont = love.graphics.newFont( 30 ) + love.graphics.setFont( menuFont ) + love.graphics.setColor( 0.5, 0.5, 0.5 ) + end + local itemText = menuItem[1] + for j, f in ipairs( menuItem ) do + if j > 2 then + local functionResult = tostring(f()) + itemText = itemText:gsub( "${" .. tostring( j - 2 ) .. "}", functionResult ) + end + end + love.graphics.printf( "> " .. itemText, 10, 160 + ( i * 80 ) - ( menuScroll * 80 ), love.graphics:getWidth() - 20, "center" ) + elseif i == menuScroll then + menuFont = love.graphics.newFont( 25 ) love.graphics.setFont( menuFont ) love.graphics.setColor( 0.5, 0.5, 0.5 ) - end - local itemText = menuItem[1] - for j, f in ipairs( menuItem ) do - if j > 2 then - local functionResult = tostring(f()) - itemText = itemText:gsub( "${" .. tostring( j - 2 ) .. "}", functionResult ) + local itemText = menuItem[1] + for j, f in ipairs( menuItem ) do + if j > 2 then + local functionResult = tostring(f()) + itemText = itemText:gsub( "${" .. tostring( j - 2 ) .. "}", functionResult ) + end end + love.graphics.printf( ". . .", 10, 200, love.graphics:getWidth() - 20, "center" ) + elseif i == 5 + menuScroll then + menuFont = love.graphics.newFont( 25 ) + love.graphics.setFont( menuFont ) + love.graphics.setColor( 0.5, 0.5, 0.5 ) + local itemText = menuItem[1] + for j, f in ipairs( menuItem ) do + if j > 2 then + local functionResult = tostring(f()) + itemText = itemText:gsub( "${" .. tostring( j - 2 ) .. "}", functionResult ) + end + end + love.graphics.printf( ". . .", 10, 520, love.graphics:getWidth() - 20, "center" ) end - love.graphics.printf( "> " .. itemText, 10, 160 + ( i * 80 ), love.graphics:getWidth() - 20, "center" ) end return end @@ -1129,3 +1323,8 @@ function drawMenuBackground() love.graphics.polygon( "fill", rectanglePoints ) love.graphics.setColor( 1, 1, 1 ) end + +-- Save game settings when exiting +function love.quit() + saveSettings() +end diff --git a/tags b/tags index 84fc72e..d304a74 100644 --- a/tags +++ b/tags @@ -9,22 +9,48 @@ !_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ !_TAG_PROGRAM_URL https://ctags.io/ /official site/ !_TAG_PROGRAM_VERSION 5.9.0 /p5.9.20210110.0/ +__newindex libraries/binser.lua /^ __newindex = function(_, _, v)$/;" f +appendFile libraries/binser.lua /^ local function appendFile(path, ...)$/;" f +check_custom_type libraries/binser.lua /^ local function check_custom_type(x, visited, accum)$/;" f +deserialize libraries/binser.lua /^ local function deserialize(str, index)$/;" f +deserializeN libraries/binser.lua /^ local function deserializeN(str, n, index)$/;" f +deserialize_value libraries/binser.lua /^ local function deserialize_value(str, index, visited)$/;" f drawBackground main.lua /^function drawBackground()$/;" f drawCode main.lua /^ }, drawCode = function()$/;" f drawCorners main.lua /^function drawCorners()$/;" f drawMenuBackground main.lua /^function drawMenuBackground()$/;" f drawScore main.lua /^function drawScore()$/;" f +frexp libraries/binser.lua /^ frexp = function(x)$/;" f +getCurrentKey main.lua /^function getCurrentKey()$/;" f getKeyDown main.lua /^function getKeyDown( player, key )$/;" f -itemText main.lua /^ itemText = itemText:gsub( "${" .. tostring( j - 2 ) .. "}", functionResult )$/;" f +isInteger libraries/binser.lua /^ isInteger = function(x)$/;" f +itemText main.lua /^ itemText = itemText:gsub( "${" .. tostring( j - 2 ) .. "}", functionResu/;" f lerp libraries/shack.lua /^local function lerp(a, b, k) --smooth transitions$/;" f love.conf conf.lua /^function love.conf(t)$/;" f love.draw main.lua /^function love.draw( dt )$/;" f love.load main.lua /^function love.load()$/;" f +love.quit main.lua /^function love.quit()$/;" f +love.textinput main.lua /^function love.textinput(t)$/;" f love.update main.lua /^function love.update( dt )$/;" f +make_file_writer libraries/binser.lua /^ local function make_file_writer(file)$/;" f menuUpdate main.lua /^function menuUpdate()$/;" f newMenu main.lua /^function newMenu( objectParameters )$/;" f +newbinser libraries/binser.lua /^local function newbinser()$/;" f +normalize_template libraries/binser.lua /^ local function normalize_template(template)$/;" f +not_array_index libraries/binser.lua /^local function not_array_index(x, len)$/;" f +number_from_str libraries/binser.lua /^local function number_from_str(str, index)$/;" f +number_to_str libraries/binser.lua /^local function number_to_str(n)$/;" f +overrideKeys main.lua /^function overrideKeys( player, key )$/;" f +pack libraries/binser.lua /^local function pack(...)$/;" f +readFile libraries/binser.lua /^ local function readFile(path)$/;" f +register libraries/binser.lua /^ local function register(metatable, name, serialize, deserialize)$/;" f +registerClass libraries/binser.lua /^ local function registerClass(class, name)$/;" f +registerResource libraries/binser.lua /^ local function registerResource(resource, name)$/;" f reset main.lua /^function reset()$/;" f restart main.lua /^function restart()$/;" f +saveSettings main.lua /^function saveSettings()$/;" f +serialize libraries/binser.lua /^ local function serialize(...)$/;" f +serialize_to_file libraries/binser.lua /^ local function serialize_to_file(path, mode, ...)$/;" f shack:apply libraries/shack.lua /^function shack:apply()$/;" f shack:getRotation libraries/shack.lua /^function shack:getRotation() return self.rotation end$/;" f shack:getRotationTarget libraries/shack.lua /^function shack:getRotationTarget() return self.rotationTarget end$/;" f @@ -48,3 +74,18 @@ shack:shake libraries/shack.lua /^function shack:shake(...) return self:setShake shack:tilt libraries/shack.lua /^function shack:tilt(...) return self:setShear(...) end$/;" f shack:update libraries/shack.lua /^function shack:update(dt)$/;" f shack:zoom libraries/shack.lua /^function shack:zoom(...) return self:setScale(...) end$/;" f +template_serializer_and_deserializer libraries/binser.lua /^ local function template_serializer_and_deserializer(metatable, template)$/;" f +templatepart_deserialize libraries/binser.lua /^ local function templatepart_deserialize(ret, part, values, vindex)$/;" f +templatepart_serialize libraries/binser.lua /^ local function templatepart_serialize(part, argaccum, x, len)$/;" f +type_check libraries/binser.lua /^local function type_check(x, tp, name)$/;" f +types.boolean libraries/binser.lua /^ function types.boolean(x, visited, accum)$/;" f +types.cdata libraries/binser.lua /^ types.cdata = function(x, visited, accum)$/;" f +types.number libraries/binser.lua /^ function types.number(x, visited, accum)$/;" f +types.string libraries/binser.lua /^ function types.string(x, visited, accum)$/;" f +types.table libraries/binser.lua /^ function types.table(x, visited, accum)$/;" f +types.thread libraries/binser.lua /^ types.thread = function() error("Cannot serialize threads.") end$/;" f +types.userdata libraries/binser.lua /^ function types.userdata(x, visited, accum)$/;" f +types["nil"] libraries/binser.lua /^ types["nil"] = function(x, visited, accum)$/;" f +unregister libraries/binser.lua /^ local function unregister(item)$/;" f +unregisterResource libraries/binser.lua /^ local function unregisterResource(name)$/;" f +writeFile libraries/binser.lua /^ local function writeFile(path, ...)$/;" f