diff --git a/classes.lua b/classes.lua index 5c4152f..01721ec 100644 --- a/classes.lua +++ b/classes.lua @@ -1,4 +1,27 @@ +local function newImage(path, alphaColor) + if (not alphaColor) then + return love.graphics.newImage(path) + end + + local imageData = love.image.newImageData(path) + + imageData:mapPixel(function(x, y, r, g, b, a) + if (r == alphaColor.r and + g == alphaColor.g and + b == alphaColor.b and + a == alphaColor.a) then + return 0, 0, 0, 0 + end + + return r, g, b, a + end) + + local image = love.graphics.newImage(imageData) + + return image +end + --[[ Sprite Class This is essentially just a Wrapper for LÖVEs Image @@ -7,10 +30,10 @@ Sprite = {} Sprite.__index = Sprite -function Sprite.new(path) +function Sprite.new(path, alphaColor) local self = setmetatable({}, Sprite) - self.image = love.graphics.newImage(path) + self.image = newImage(path, alphaColor) return self end @@ -21,41 +44,150 @@ end --[[ - Audio Class + Spritesheet Class + This is essentially just a Wrapper for LÖVEs Image, specifically made for Spritesheets +]]-- + +Spritesheet = {} +Spritesheet.__index = Spritesheet + +function Spritesheet.new(path, width, height, alphaColor) + local self = setmetatable({}, Spritesheet) + + self.sprites = {} + + if (string.find(path, ".json")) then + local contents, size = love.filesystem.read(path) + local sliceData = json.decode(contents) + + print("games/" .. sliceData.image) + self.image = newImage("games/" .. sliceData.image, alphaColor) + + -- Create the slices according to the sliceData + local imageWidth, imageHeight = self.image:getWidth(), self.image:getHeight() + + local counter = 0 + for _, data in pairs(sliceData) do + table.insert(self.sprites, love.graphics.newQuad( + data.x, + data.y, + data.width, + data.height, + imageWidth, + imageHeight + )) + counter = counter + 1 + end + + self.Count = counter + + return self + end + + self.image = newImage(path, alphaColor) + + -- Generate slices + local imageWidth, imageHeight = self.image:getWidth(), self.image:getHeight() + local columns = math.floor(imageWidth / width) + local rows = math.floor(imageHeight / height) + + local counter = 0 + for x = 0, columns - 1 do + for y = 0, rows - 1 do + table.insert(self.sprites, love.graphics.newQuad( + x * width, + y * height, + width, + height, + imageWidth, + imageHeight + )) + counter = counter + 1 + end + end + + self.Count = counter + + return self +end + +function Spritesheet:Draw(slice, x, y) + local sprite = self.sprites[slice] + if sprite then + love.graphics.draw(self.image, sprite, x or 0, y or 0) + end +end + + +--[[ + Sound Class This is essentially just a Wrapper for LÖVEs Source ]]-- -Audio = {} -Audio.__index = Audio +Sound = {} +Sound.__index = Sound -function Audio.new(path, soundType) - local self = setmetatable({}, Audio) +function Sound.new(path, soundType) + local self = setmetatable({}, Sound) self.source = love.audio.newSource(path, soundType or "static") return self end -function Audio:Play() +function Sound:Play() self.source:play() end -function Audio:Stop() +function Sound:Stop() self.source:stop() end -function Audio:Pause() +function Sound:Pause() self.source:pause() end -function Audio:Resume() +function Sound:Resume() self.source:play() end -function Audio:SetLooping(bool) +function Sound:SetLooping(bool) self.source:setLooping(bool) end -function Audio:SetVolume(volume) +function Sound:SetVolume(volume) self.source:setVolume(volume) -end \ No newline at end of file +end + + +--[[ + Color Class + A convenient Color class that works with any love2D function +]]-- + +function Color(r, g, b, a) + r = (r or 255) / 255 + g = (g or 255) / 255 + b = (b or 255) / 255 + a = (a or 255) / 255 + + local color = { + r = r, g = g, b = b, a = a, + [1] = r, [2] = g, [3] = b, [4] = a -- love2D expects a sequential array, so heres the fix for that + } + + -- Return it as a class, that way we can use it freely anywhere we wish + return setmetatable(color, { + __index = function(tbl, key) + if key == 1 then return tbl.r + elseif key == 2 then return tbl.g + elseif key == 3 then return tbl.b + elseif key == 4 then return tbl.a + else return rawget(tbl, key) + end + end, + __tostring = function(tbl) + return tbl + end + }) +end diff --git a/games/map.json b/games/map.json new file mode 100644 index 0000000..e69de29 diff --git a/games/test.lua b/games/test.lua index 66af9ad..ea70599 100644 --- a/games/test.lua +++ b/games/test.lua @@ -3,26 +3,36 @@ NAME = "Test" AUTHOR = "Tarion" VERSION = 0 -local test = 0 -local delay = 0 - -local last = "" - +local backgroundColor = Color(48, 104, 20, 255) function Load() + SetBackgroundColor(backgroundColor) end function Update(dt, curTime) end -local sprite = Sprite("testSprites.png") +local sprite = Spritesheet("testSprites.json", 16, 16, Color(48, 104, 80, 255)) +local testSound = Sound("testSound.mp3", "static") +local test = 1 function KeyPressed(key) - if (key == "f5") then + if (key == "f3") then + test = test - 1 + + if (test == 0) then + test = sprite.Count + end + elseif (key == "f4") then test = test + 1 - sprite = Sprite("testSprites.png") + + if (test == sprite.Count + 1) then + test = 1 + end + elseif (key == "f2") then + testSound:Play() end end function Draw() - print("Loaded Sprites: " .. test, 10, 10) - sprite:Draw(100, 0) + DrawText(test .. "/" .. sprite.Count, 25, 25) + sprite:Draw(test, 50, 0) end diff --git a/games/testSound.mp3 b/games/testSound.mp3 new file mode 100644 index 0000000..8e64435 Binary files /dev/null and b/games/testSound.mp3 differ diff --git a/games/testSprites.json b/games/testSprites.json new file mode 100644 index 0000000..e806425 --- /dev/null +++ b/games/testSprites.json @@ -0,0 +1,18 @@ +[ + { + "image": "testSprites.png" + }, + {"x":0,"y":0,"width":64,"height":24}, + {"x":64,"y":0,"width":16,"height":24}, + {"x":64,"y":0,"width":16,"height":24}, + {"x":0,"y":24,"width":64,"height":24}, + {"x":80,"y":52,"width":8,"height":12}, + {"x":80,"y":68,"width":8,"height":12}, + {"x":0,"y":72,"width":80,"height":24}, + {"x":90,"y":48,"width":14,"height":48}, + {"x":72,"y":96,"width":32,"height":15}, + {"x":32,"y":96,"width":40,"height":15}, + {"x":24,"y":96,"width":8,"height":8}, + {"x":16,"y":96,"width":8,"height":8}, + {"x":16,"y":104,"width":8,"height":8}, + {"x":0,"y":96,"width":16,"height":16},{"x":0,"y":112,"width":32,"height":24},{"x":32,"y":111,"width":32,"height":24},{"x":72,"y":136,"width":32,"height":24}] \ No newline at end of file diff --git a/games/tileset.png b/games/tileset.png new file mode 100644 index 0000000..5b21c11 Binary files /dev/null and b/games/tileset.png differ diff --git a/libraries/json.lua b/libraries/json.lua new file mode 100644 index 0000000..098e7b2 --- /dev/null +++ b/libraries/json.lua @@ -0,0 +1,400 @@ +-- +-- json.lua +-- +-- Copyright (c) 2019 rxi +-- +-- 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 json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/main.lua b/main.lua index d3dc6a8..08a522b 100644 --- a/main.lua +++ b/main.lua @@ -1,8 +1,15 @@ +if arg[2] == "debug" then + require("lldebugger").start() +end + + +json = require("libraries/json") + require("classes") local LoadObject = { - ["sprite"] = function(name) + ["sprite"] = function(name, alphaColor) name = "games/" .. name local info = love.filesystem.getInfo(name) @@ -15,10 +22,10 @@ local LoadObject = { return end - local newObject = Sprite.new(name) + local newObject = Sprite.new(name, alphaColor) virtual.loadedObjects[newObject] = { - object = newSprite, + object = newObject, size = info.size, } virtual.currentMemory = virtual.currentMemory + info.size @@ -26,7 +33,32 @@ local LoadObject = { return newObject end, - ["audio"] = function(name, soundType) + ["spritesheet"] = function(name, width, height, alphaColor) + name = "games/" .. name + height = height or width + + local info = love.filesystem.getInfo(name) + if (not info) then + error("Failed to load Object (FILE NOT FOUND) ("..name..")") + return + end + if ((info.size + virtual.currentMemory) > virtual.maxMemory) then + error("Failed to load Object (MAX MEMORY) ("..name..")") + return + end + + local newObject = Spritesheet.new(name, width, height, alphaColor) + + virtual.loadedObjects[newObject] = { + object = newObject, + size = info.size, + } + virtual.currentMemory = virtual.currentMemory + info.size + virtual.loadedCount = virtual.loadedCount + 1 + + return newObject + end, + ["sound"] = function(name, soundType) name = "games/" .. name soundType = soundType or "static" @@ -40,10 +72,10 @@ local LoadObject = { return end - local newObject = Audio.new(name, soundType) + local newObject = Sound.new(name, soundType) virtual.loadedObjects[newObject] = { - object = newSprite, + object = newObject, size = info.size, } virtual.currentMemory = virtual.currentMemory + info.size @@ -53,6 +85,10 @@ local LoadObject = { end, } +local function SetBackgroundColor(color) + love.graphics.setBackgroundColor(color) +end + -- Establish Sandbox -- Everything in the table is what the Sandbox has access to local consoleEnv = { @@ -65,17 +101,18 @@ local consoleEnv = { Draw, KeyPressed, - Sprite2 = LoadSprite, Sprite = LoadObject["sprite"], + Spritesheet = LoadObject["spritesheet"], + Sound = LoadObject["sound"], - print = love.graphics.print, + Color = Color, + + SetBackgroundColor = SetBackgroundColor, + DrawText = love.graphics.print, + + print = print, } consoleEnv._G = consoleEnv -setmetatable(consoleEnv, { - __index = function(_, key) - return function(...) end -- Return an empty function to avoid erroring, not perfect, but works most of the time - end -}) function loadGame(gameName) local fn, err = loadfile("games/" .. gameName .. ".lua", "t", consoleEnv) @@ -83,9 +120,9 @@ function loadGame(gameName) error("Failed to load sandbox file (1): " .. err) end - local ok, execErr = pcall(fn) + local ok, execErr = xpcall(fn, debug.traceback) if not ok then - error("Error running sandbox file (2): " .. execErr) + error("Error running sandbox file (2):\n" .. execErr) end love.window.setTitle(consoleEnv.NAME or "NULL") @@ -100,7 +137,6 @@ function love.load() -- Console Variables curTime = 0 scaling = 4 - keysPressed = {} -- Setup Window love.graphics.setDefaultFilter("nearest", "nearest") @@ -118,7 +154,6 @@ function love.load() loadGame("test") - if (consoleEnv.Load) then consoleEnv.Load() end @@ -128,26 +163,7 @@ function love.keypressed(key) if key == "escape" then love.event.quit() end - - keysPressed[key] = true - - if (consoleEnv.KeyPressed) then - consoleEnv.KeyPressed(key) - end -end - -function love.keyboard.wasPressed(key) - return keysPressed[key] -end - -function love.window.updateWindow() - love.window.setMode(scaling*240, scaling*160, {resizable=false, vsync=0}) -end - -function love.update(dt) - curTime = curTime + dt - - if (love.keyboard.wasPressed("f1")) then + if (key == "f1") then scaling = scaling + 1 if (scaling == maxScaling+1) then @@ -157,7 +173,17 @@ function love.update(dt) love.window.updateWindow() end - keysPressed = {} + if (consoleEnv.KeyPressed) then + consoleEnv.KeyPressed(key) + end +end + +function love.window.updateWindow() + love.window.setMode(scaling*240, scaling*160, {resizable=false, vsync=0}) +end + +function love.update(dt) + curTime = curTime + dt if (consoleEnv.Update) then consoleEnv.Update(dt, curTime) @@ -176,3 +202,15 @@ function love.draw() consoleEnv.Draw() end end + + + +local love_errorhandler = love.errhand + +function love.errorhandler(msg) + if lldebugger then + error(msg, 2) + else + return love_errorhandler(msg) + end +end \ No newline at end of file