diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..01bc914 --- /dev/null +++ b/TODO.md @@ -0,0 +1,42 @@ +- список рекордов, который прокручивается +- два режима игры - со звуком и без (как-же тогда быть с таблицей рекордов?) + как вариант - звук не отключать. Хорошая идея +- чистка истории, запаковка репозитория на гитхаб + [+] подготовка README.md со стандартными пунктами + сделал, пока рыба + подготовка бинарных файлов +[+] пунк помощи в меню, с првилами + сделал, пока без правил, рыба +- интернационализация?(нафиг) +- показ сигнала два раза подряд на одном и том же месте + +BUGS +- кривой рендеринг в текстуру списка рекордов. + При залитие на гитхаб - дай ссылку Дожу, добавить клавишу скриншота. +- нажатие пробела во время игры + +[28.07.2018] + +Сделано почти нихуя. Пункты меню не работают. Игровой цикл отсутствует. +Решено делать четыре типа раздражителей - позиция(4 * 4 = 16 вариантов), +цвет(зеленый, красный, синий, желтый, фиолетовый, коричневый = 6 вариантов), +форма(прямоугольник, круг, треугольник вниз, треугольник вверх, треугольник вниз + треугольник вверх, +ромб), звук(выстрел, гитара, барабан, тарелка, свисток, кряканье утки, лай собаки, мяуканье кошки, мычание коровы, +визг тормозов). + +Скачать семпл паки на rutracker.org, ренойз 2.8. + +Как всю шляпу запускать? Раннер для виндос, порт на андроид. + +[08.09.2018] + +- список рекордов не прокручивается +- список рекордов не сортируется +- на https://github.com/nagolove/nback залит репозиторий с бинарниками +- не оформлен ридми.эмди +- статья о исследовании http://www.pnas.org/content/pnas/105/19/6829.full.pdf +- режим подсказски, который показывает эн-предыдущий элемент в нужном месте + + +Три метода nback.check_form(), nback.check_color(), nback.check_sound(), check_position() +переписать в одну функцию с параметром. diff --git a/bhupur.lua b/bhupur.lua new file mode 100644 index 0000000..11554c4 --- /dev/null +++ b/bhupur.lua @@ -0,0 +1,72 @@ +local lg = love.graphics + +local bhupur = { + color = {0.1, 0.1, 0.1} +} + +function bhupur.draw(x, y, w) + -- width - size in pixels of square in center on bhupur + -- neck - percentage value(0-1) of yantrawidth + -- foot - percentage value(0-1) of yantrawidth + -- height - size in pixels on bhupur + local height = 30 + local foot = 0.8 * w + local neck = 0.5 * w + local stair = math.abs((w - neck - foot) / 2) + local tmp = (w - neck) / 2 + + local topw = w * 0.6 + local paramw = 80 + local points = { + x, y, + x + tmp, y, + x + tmp, y - height / 2, + x + tmp - stair, y - height / 2, -- + x + tmp - stair, y - height, + x + tmp - stair + foot, y - height, -- + x + tmp - stair + foot, y - height / 2, + x + tmp - stair * 2 + foot, y - height / 2, + x + tmp - stair * 2 + foot, y, + x + w, y, + } + local old = pack(lg.getColor()) + lg.setColor(bhupur.color) + lg.setLineWidth(2) + + lg.line(points) + + local xcenter = x + w / 2 + local ycenter = y + w / 2 + + lg.translate(xcenter, y) + lg.rotate(math.pi / 1) + lg.translate(-xcenter, -y - w) + lg.line(points) + lg.origin() + + lg.translate(xcenter, y) + lg.rotate(math.pi + math.pi / 2) + lg.translate(-xcenter - w / 2, -y - w / 2) + lg.line(points) + lg.origin() + + lg.translate(xcenter, y / 1) + lg.rotate(math.pi - math.pi / 2) + lg.translate(-xcenter + w / 2, -y - w / 2) + lg.line(points) + lg.origin() + + lg.setColor(old) + lg.setLineWidth(1) + + local function centralAxis() + local scrw, scrh = lg.getDimensions() + lg.setColor({100, 100, 100}) + lg.line(scrw / 2, 0, scrw / 2, scrh) + lg.line(0, scrh / 2, scrw, scrh / 2) + end + + --centralAxis() +end + +return bhupur diff --git a/colorpicker.lua b/colorpicker.lua new file mode 100644 index 0000000..484d6f5 --- /dev/null +++ b/colorpicker.lua @@ -0,0 +1,88 @@ +local class = require "libs.30log" +local suit = require "libs.suit" +local inspect = require "libs.inspect" +local lg = love.graphics + +local colorpicker = class("colorpicker") + +function colorpicker:init() + print(string.format("Colorpicker created")) + self.color = {0.5, 0.5, 0.5, 1} + self.rslider = {value = self.color[1], min = 0, max = 1} + self.gslider = {value = self.color[2], min = 0, max = 1} + self.bslider = {value = self.color[3], min = 0, max = 1} + self.aslider = {value = self.color[4], min = 0, max = 1} + --self.canvas = lg.newCanvas(lg.getWidth(), lg.getHeight(), { type = "2d", format = "normal", msaa = 4}) + self.canvas = lg.newCanvas(lg.getWidth(), lg.getHeight()) + print("self.canvas = ", inspect(self.canvas)) +end + +-- button = 1 -- primary button +-- button = 2 -- secondary button +function colorpicker:mousepressed(x, y, button, istouch) + -- беру пиксель пипетки + if button == 2 then + local imgdata = self.canvas:newImageData() + local r, g, b, a = imgdata:getPixel(love.mouse.getPosition()) + self.color[1], self.color[2], self.color[3] = r, g, b + self.rslider.value, self.gslider.value, self.bslider.value, self.bslider.value = r, g, b, a + -- копирование в буфер обмена + elseif button == 3 then + print("color copied to clpbrd") + love.system.setClipboardText(string.format("{%s, %s, %s}", self.color[1], self.color[2], self.color[3])) + end +end + +function colorpicker:mousereleased(x, y, button, istouch) +end + +function colorpicker:mousemoved(x, y, dx, dy, istouch) +end + +function colorpicker:draw(func) + lg.setCanvas(self.canvas) + lg.setColor(1, 1, 1, 1) + func() + lg.setCanvas() + if self.canvas then lg.draw(self.canvas, 0, 0) end + + local pickerwidth = 200 + local pickerheight = 128 + local w, h = lg.getDimensions() + local cornerx, cornery = (w - pickerwidth) / 2, h / 3 + local old = pack(lg.getColor()) + --local lc = pack(lg.getColor( + --print("getColor()", inspect(lc)) + --print("self.color", inspect(self.color)) + lg.setColor(self.color) + lg.rectangle("fill", cornerx, cornery, pickerwidth, pickerheight, 3, 3) + lg.setColor(old) + + cornery = cornery + pickerheight + + local sliderh = 16 + suit.Slider(self.rslider, cornerx, cornery, pickerwidth, sliderh) + lg.print(string.format("%3f", self.color[1]), cornerx + pickerwidth, cornery) + cornery = cornery + sliderh + suit.Slider(self.gslider, cornerx, cornery, pickerwidth, sliderh) + lg.print(string.format("%3f", self.color[2]), cornerx + pickerwidth, cornery) + cornery = cornery + sliderh + suit.Slider(self.bslider, cornerx, cornery, pickerwidth, sliderh) + lg.print(string.format("%3f", self.color[3]), cornerx + pickerwidth, cornery) + cornery = cornery + sliderh + suit.Slider(self.aslider, cornerx, cornery, pickerwidth, sliderh) + if self.color[4] then + lg.print(string.format("%3f", self.color[4]), cornerx + pickerwidth, cornery) + end + + suit.draw() +end + +function colorpicker:update(dt) + self.color = {self.rslider.value, self.gslider.value, self.bslider.value} + --print("color = ", inspect(self.color)) + --print(string.format("color[1] = %f color[2] = %f color[3] = %f", self.color[1], self.color[2], self.color[3])) +end + +return colorpicker + diff --git a/common.lua b/common.lua new file mode 100644 index 0000000..daf18db --- /dev/null +++ b/common.lua @@ -0,0 +1,18 @@ + +function pack(...) + return {...} +end + +function xassert(a, ...) + if a then return a, ... end + local f = ... + if type(f) == "function" then + error(f(select(2, ...)), 2) + else + error(f or "assertion failed!", 2) + end +end + +function table.copy(t) + return {unpack(t)} +end diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..4189622 --- /dev/null +++ b/conf.lua @@ -0,0 +1,5 @@ +function love.conf(t) + --t.console = true + t.window.msaa = 4 + t.window.vsync = false +end diff --git a/dbg.lua b/dbg.lua new file mode 100644 index 0000000..c962a98 --- /dev/null +++ b/dbg.lua @@ -0,0 +1,26 @@ + +local dbg = { + show = true +} + +local g = love.graphics +local y = 0 + +function dbg.clear() + y = 0 +end + +function dbg.print_text(text) + if not dbg.show then return end + + local color = {g.getColor()} + g.setColor(1, 0.5, 0) + g.print(text, 5, y) + local font = g.getFont() + if font then + y = y + font:getHeight() + end + g.setColor(unpack(color)) +end + +return dbg diff --git a/geo.lua b/geo.lua new file mode 100644 index 0000000..4794e43 --- /dev/null +++ b/geo.lua @@ -0,0 +1,62 @@ + +local geo = {} + +function geo.isPointInCircle(px, py, cx, cy, cr) + return (px - cx)^2 + (py - cy)^2 <= cr ^ 2 +end + +-- check - is point lie on circle +function isPointOnCircle(px, py, cx, cy, cr, theta) + return ((px - cx)^2 + (py - cy)^2 - cr ^ 2) <= theta +end + +function isPointOnLine(px, py, x1, y1, x2, y2) + local p = (px - x2) / (x1 - x2) + if 0 <= p and p <= 1 then + local x = p * x1 + (1 - p) * x2 + local y = p * y1 + (1 - p) * y2 + return x == y + end + return false +end + +function geo.dist(x1, y1, x2, y2) + return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2) +end + +function geo.lineCross(x1, y1, x2, y2, x3, y3, x4, y4) + --print(x1, y1, x2, y2, x3, y3, x4, y4) + local divisor = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) + + -- lines are parralell + if divisor == 0 then return nil end + + local ua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3) + ua = ua / divisor + --local ub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3) + --ub = ub / divisor + --print("lineCross ua: ",ua, " ub: ", ub) + local x, y = x1 + ua * (x2 - x1), y1 + ua * (y2 - y1) + if not (x > x1 and x < x2 and y < y2 and y > y1) then + --print("point is not on segment") + return nil + end + return x, y +end + +-- [[ +-- Function expect array of (x, y) points in yantra internal coordinate format(-1, 1) and translates +-- it to screen size of width and height. Returns translated points array. +-- ]] +function translate2Screen(points, w, h) + local ret = {} + for _, v in pairs(points) do + assert(v.x >= -1 and v.x <= 1, string.format("unsupported x = %f for range (-1, 1)", v.x)) + assert(v.y >= -1 and v.y <= 1, string.format("unsupported y = %f for range (-1, 1)", v.y)) + local t = { x = 2.0 / (v.x + 1) * w, y = 2.0 / (v.y - 1) * h} + ret[#ret + 1] = t + end + return ret +end + +return geo diff --git a/gfx/DejaVuSansMono.ttf b/gfx/DejaVuSansMono.ttf new file mode 100644 index 0000000..1376228 Binary files /dev/null and b/gfx/DejaVuSansMono.ttf differ diff --git a/gfx/IMG_20190111_115755.png b/gfx/IMG_20190111_115755.png new file mode 100644 index 0000000..7372eea Binary files /dev/null and b/gfx/IMG_20190111_115755.png differ diff --git a/help.lua b/help.lua new file mode 100644 index 0000000..89e9a70 --- /dev/null +++ b/help.lua @@ -0,0 +1,32 @@ +local pallete = require "pallete" +local nback = require "nback" + +local help = { + font = love.graphics.newFont("gfx/DejaVuSansMono.ttf", 15), + init = function() end, + update = function() end, +} + +local g = love.graphics + +function help.draw() + g.clear(pallete.background) + g.push("all") + g.setFont(help.font) + local w, h = g.getDimensions() + local y = 20 + g.printf("This is a bla-bla", 0, y, w, "center") + y = y + help.font:getHeight() + g.printf("Put description here!", 0, y, w, "center") + --FIXME Not work, using nil table nback here + --g.printf("Escape - to go back", 0, bottom_text_line_y + nback.font:getHeight(), w, "center") + g.pop() +end + +function help.keypressed(key) + if key == "escape" then + states.pop() + end +end + +return help diff --git a/ihelp.lua b/ihelp.lua new file mode 100644 index 0000000..fdd9fd2 --- /dev/null +++ b/ihelp.lua @@ -0,0 +1,24 @@ + +local ihelp = { +} + +local max_rows = 3 +local max_columns = 2 + +-- state - string name +-- key - string name +-- description - string +function ihelp.add_hotkey_help(state, key, description) +end + +function ihelp.draw() + love.graphics.clear(pallete.background) +end + +function ihelp.update(dt) +end + +function ihelp.keypressed(key) +end + +return ihelp diff --git a/libs/30log.lua b/libs/30log.lua new file mode 100644 index 0000000..73f78dc --- /dev/null +++ b/libs/30log.lua @@ -0,0 +1,31 @@ +local next, assert, pairs, type, tostring, setmetatable, baseMt, _instances, _classes, _class = next, assert, pairs, type, tostring, setmetatable, {}, setmetatable({},{__mode = 'k'}), setmetatable({},{__mode = 'k'}) +local function assert_call_from_class(class, method) assert(_classes[class], ('Wrong method call. Expected class:%s.'):format(method)) end; local function assert_call_from_instance(instance, method) assert(_instances[instance], ('Wrong method call. Expected instance:%s.'):format(method)) end +local function bind(f, v) return function(...) return f(v, ...) end end +local default_filter = function() return true end +local function deep_copy(t, dest, aType) t = t or {}; local r = dest or {}; for k,v in pairs(t) do if aType ~= nil and type(v) == aType then r[k] = (type(v) == 'table') and ((_classes[v] or _instances[v]) and v or deep_copy(v)) or v elseif aType == nil then r[k] = (type(v) == 'table') and k~= '__index' and ((_classes[v] or _instances[v]) and v or deep_copy(v)) or v end; end return r end +local function instantiate(call_init,self,...) assert_call_from_class(self, 'new(...) or class(...)'); local instance = {class = self}; _instances[instance] = tostring(instance); deep_copy(self, instance, 'table') + instance.__index, instance.__subclasses, instance.__instances, instance.mixins = nil, nil, nil, nil; setmetatable(instance,self); if call_init and self.init then if type(self.init) == 'table' then deep_copy(self.init, instance) else self.init(instance, ...) end end; return instance +end +local function extend(self, name, extra_params) + assert_call_from_class(self, 'extend(...)'); local heir = {}; _classes[heir] = tostring(heir); self.__subclasses[heir] = true; deep_copy(extra_params, deep_copy(self, heir)) + heir.name, heir.__index, heir.super, heir.mixins = extra_params and extra_params.name or name, heir, self, {}; return setmetatable(heir,self) +end +baseMt = { __call = function (self,...) return self:new(...) end, __tostring = function(self,...) + if _instances[self] then return ("instance of '%s' (%s)"):format(rawget(self.class,'name') or '?', _instances[self]) end; return _classes[self] and ("class '%s' (%s)"):format(rawget(self,'name') or '?', _classes[self]) or self end +}; _classes[baseMt] = tostring(baseMt); setmetatable(baseMt, {__tostring = baseMt.__tostring}) +local class = {isClass = function(t) return not not _classes[t] end, isInstance = function(t) return not not _instances[t] end} +_class = function(name, attr) local c = deep_copy(attr); _classes[c] = tostring(c) + c.name, c.__tostring, c.__call, c.new, c.create, c.extend, c.__index, c.mixins, c.__instances, c.__subclasses = name or c.name, baseMt.__tostring, baseMt.__call, bind(instantiate, true), bind(instantiate, false), extend, c, setmetatable({},{__mode = 'k'}), setmetatable({},{__mode = 'k'}), setmetatable({},{__mode = 'k'}) + c.subclasses = function(self, filter, ...) assert_call_from_class(self, 'subclasses(class)'); filter = filter or default_filter; local subclasses = {}; for class in pairs(_classes) do if class ~= baseMt and class:subclassOf(self) and filter(class,...) then subclasses[#subclasses + 1] = class end end; return subclasses end + c.instances = function(self, filter, ...) assert_call_from_class(self, 'instances(class)'); filter = filter or default_filter; local instances = {}; for instance in pairs(_instances) do if instance:instanceOf(self) and filter(instance, ...) then instances[#instances + 1] = instance end end; return instances end + c.subclassOf = function(self, superclass) assert_call_from_class(self, 'subclassOf(superclass)'); assert(class.isClass(superclass), 'Wrong argument given to method "subclassOf()". Expected a class.'); local super = self.super; while super do if super == superclass then return true end; super = super.super end; return false end + c.classOf = function(self, subclass) assert_call_from_class(self, 'classOf(subclass)'); assert(class.isClass(subclass), 'Wrong argument given to method "classOf()". Expected a class.'); return subclass:subclassOf(self) end + c.instanceOf = function(self, fromclass) assert_call_from_instance(self, 'instanceOf(class)'); assert(class.isClass(fromclass), 'Wrong argument given to method "instanceOf()". Expected a class.'); return ((self.class == fromclass) or (self.class:subclassOf(fromclass))) end + c.cast = function(self, toclass) assert_call_from_instance(self, 'instanceOf(class)'); assert(class.isClass(toclass), 'Wrong argument given to method "cast()". Expected a class.'); setmetatable(self, toclass); self.class = toclass; return self end + c.with = function(self,...) assert_call_from_class(self, 'with(mixin)'); for _, mixin in ipairs({...}) do assert(self.mixins[mixin] ~= true, ('Attempted to include a mixin which was already included in %s'):format(tostring(self))); self.mixins[mixin] = true; deep_copy(mixin, self, 'function') end return self end + c.includes = function(self, mixin) assert_call_from_class(self,'includes(mixin)'); return not not (self.mixins[mixin] or (self.super and self.super:includes(mixin))) end + c.without = function(self, ...) assert_call_from_class(self, 'without(mixin)'); for _, mixin in ipairs({...}) do + assert(self.mixins[mixin] == true, ('Attempted to remove a mixin which is not included in %s'):format(tostring(self))); local classes = self:subclasses(); classes[#classes + 1] = self + for _, class in ipairs(classes) do for method_name, method in pairs(mixin) do if type(method) == 'function' then class[method_name] = nil end end end; self.mixins[mixin] = nil end; return self end; return setmetatable(c, baseMt) end +class._DESCRIPTION = '30 lines library for object orientation in Lua'; class._VERSION = '30log v1.2.0'; class._URL = 'http://github.com/Yonaba/30log'; class._LICENSE = 'MIT LICENSE ' +return setmetatable(class,{__call = function(_,...) return _class(...) end }) diff --git a/libs/Timer.lua b/libs/Timer.lua new file mode 100644 index 0000000..29b5b7f --- /dev/null +++ b/libs/Timer.lua @@ -0,0 +1,195 @@ +--[[ +Copyright (c) 2018 SSYGEN, Matthias Richter + +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. + +Except as contained in this notice, the name(s) of the above copyright holders +shall not be used in advertising or otherwise to promote the sale, use or +other dealings in this Software without prior written authorization. + +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 Timer = {} +Timer.__index = Timer + +function Timer.new() + local self = {} + self.timers = {} + return setmetatable(self, Timer) +end + +function Timer:update(dt) + for tag, timer in pairs(self.timers) do + timer.time = timer.time + dt + + if timer.type == 'after' then + if timer.time >= timer.delay then + timer.action(dt) + self.timers[tag] = nil + end + + elseif timer.type == 'every' then + if timer.time >= timer.delay then + timer.action(dt) + timer.time = timer.time - timer.delay + timer.delay = self:__getResolvedDelay(timer.any_delay) + if timer.count > 0 then + timer.counter = timer.counter + 1 + if timer.counter >= timer.count then + timer.after(dt) + self.timers[tag] = nil + end + end + end + + elseif timer.type == 'during' then + timer.action(dt) + if timer.time >= timer.delay then + timer.after(dt) + self.timers[tag] = nil + end + + elseif timer.type == 'tween' then + local s = self:__tween(timer.method, math.min(1, timer.time/timer.delay), unpack(timer.args or {})) + local ds = s - timer.last_s + timer.last_s = s + for _, info in ipairs(timer.payload) do + local ref, key, delta = unpack(info) + ref[key] = ref[key] + delta*ds + end + if timer.time >= timer.delay then + timer.after(dt) + self.timers[tag] = nil + end + end + end +end + +local function UUID() + local fn = function(x) + local r = love.math.random(16) - 1 + r = (x == "x") and (r + 1) or (r % 4) + 9 + return ("0123456789abcdef"):sub(r, r) + end + return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn)) +end + +local function random(min, max) + local min, max = min or 0, max or 1 + return (min > max and (love.math.random()*(min - max) + max)) or (love.math.random()*(max - min) + min) +end + +function Timer:after(delay, action, tag) + tag = tag or UUID() + self:cancel(tag) + self.timers[tag] = {type = 'after', time = 0, delay = self:__getResolvedDelay(delay), action = action} + return tag +end + +function Timer:every(delay, action, count, after, tag) + if type(count) == 'string' then tag, count = count, 0 + elseif type(count) == 'number' and type(after) == 'string' then tag = after + else tag = tag or UUID() end + self:cancel(tag) + self.timers[tag] = {type = 'every', time = 0, any_delay = delay, delay = self:__getResolvedDelay(delay), action = action, counter = 0, count = count or 0, after = after or function() end} + return tag +end + +function Timer:during(delay, action, after, tag) + if type(after) == 'string' then tag, after = after, nil + elseif type(after) == 'function' then tag = UUID() + else tag = tag or UUID() end + self:cancel(tag) + self.timers[tag] = {type = 'during', time = 0, delay = self:__getResolvedDelay(delay), action = action, after = after or function() end} + return tag +end + +function Timer:script(f) + local co = coroutine.wrap(f) + co(function(t) + self:after(t, co) + coroutine.yield() + end) +end + +function Timer:tween(delay, subject, target, method, after, tag, ...) + if type(after) == 'string' then tag, after = after, nil + elseif type(after) == 'function' then tag = UUID() + else tag = tag or UUID() end + self:cancel(tag) + self.timers[tag] = {type = 'tween', time = 0, delay = self:__getResolvedDelay(delay), subject = subject, target = target, method = method, after = after or function() end, + args = {...}, last_s = 0, payload = self:__tweenCollectPayload(subject, target, {})} + return tag +end + +function Timer:cancel(tag) + self.timers[tag] = nil +end + +function Timer:destroy() + self.timers = {} +end + +function Timer:getTime(tag) + return self.timers[tag].time, self.timers[tag].delay +end + +function Timer:__getResolvedDelay(delay) + if type(delay) == 'number' then return delay + elseif type(delay) == 'table' then return random(delay[1], delay[2]) end +end + +local t = {} +t.out = function(f) return function(s, ...) return 1 - f(1-s, ...) end end +t.chain = function(f1, f2) return function(s, ...) return (s < 0.5 and f1(2*s, ...) or 1 + f2(2*s-1, ...))*0.5 end end +t.linear = function(s) return s end +t.quad = function(s) return s*s end +t.cubic = function(s) return s*s*s end +t.quart = function(s) return s*s*s*s end +t.quint = function(s) return s*s*s*s*s end +t.sine = function(s) return 1-math.cos(s*math.pi/2) end +t.expo = function(s) return 2^(10*(s-1)) end +t.circ = function(s) return 1-math.sqrt(1-s*s) end +t.back = function(s, bounciness) bounciness = bounciness or 1.70158; return s*s*((bounciness+1)*s - bounciness) end +t.bounce = function(s) local a, b = 7.5625, 1/2.75; return math.min(a*s^2, a*(s-1.5*b)^2 + 0.75, a*(s-2.25*b)^2 + 0.9375, a*(s-2.625*b)^2 + 0.984375) end +t.elastic = function(s, amp, period) amp, period = amp and math.max(1, amp) or 1, period or 0.3; return (-amp*math.sin(2*math.pi/period*(s-1) - math.asin(1/amp)))*2^(10*(s-1)) end + +function Timer:__tween(method, ...) + if method == 'linear' then return t.linear(...) + elseif method:find('in%-out%-') then return t.chain(t[method:sub(8, -1)], t.out(t[method:sub(8, -1)]))(...) + elseif method:find('out%-in%-') then return t.chain(t.out(t[method:sub(8, -1)]), t[method:sub(8, -1)])(...) + elseif method:find('out%-') then return t.out(t[method:sub(5, -1)])(...) + elseif method:find('in%-') then return t[method:sub(4, -1)](...) end +end + +-- Return extended out table +function Timer:__tweenCollectPayload(subject, target, out) + for k, v in pairs(target) do + local ref = subject[k] + assert(type(v) == type(ref), 'Type mismatch in field "' .. k .. '".') + if type(v) == 'table' then self:__tweenCollectPayload(ref, v, out) + else + local ok, delta = pcall(function() return (v-ref)*1 end) + assert(ok, 'Field "' .. k .. '" does not support arithmetic operations.') + out[#out+1] = {subject, k, delta} + end + end + return out +end + +return setmetatable({}, {__call = function(_, ...) return Timer.new(...) end}) diff --git a/libs/bresenham b/libs/bresenham new file mode 160000 index 0000000..fc100c9 --- /dev/null +++ b/libs/bresenham @@ -0,0 +1 @@ +Subproject commit fc100c9fc1138d0ee335bded5dec1b4eb68b89fb diff --git a/libs/console/FiraCode.ttf b/libs/console/FiraCode.ttf new file mode 100644 index 0000000..575c21b Binary files /dev/null and b/libs/console/FiraCode.ttf differ diff --git a/libs/console/console.lua b/libs/console/console.lua new file mode 100644 index 0000000..c2669f9 --- /dev/null +++ b/libs/console/console.lua @@ -0,0 +1,946 @@ +console = {} + +setmetatable(console, {__index = _G}) +setfenv(1, console) + +__VERSION = 0.6 + +local utf8 = require("utf8") + +git_link = "https://github.com/rinqu-eu/love2d-console" + +path = ... +path_req = path:sub(1, -9) +path_load = path:sub(1, -9):gsub("%p", "/") + +font = love.graphics.newFont(path_load .. "/FiraCode.ttf", 13) +font_w = font:getWidth(" ") +font_h = font:getHeight() + +background_color = {40, 40, 40, 127} +cursor_style = "block" -- "block" or "line" +cursor_color = {255, 255, 255, 255} +selected_color = {170, 170, 170, 127} +blink_duration = 0.5 +output_jump_by = 7 + +color_info = "429bf4" +color_warn = "cecb2f" +color_err = "ea2a2a" +color_com = "00cc00" + +is_open = false +is_first_open = false +unhooked = {} + +input_buffer = "" +output_buffer = {} +history_buffer = {} + +cursor_idx = 0 +selected_idx1 = -1 +selected_idx2 = -1 +history_idx = #history_buffer + 1 +output_idx = 0 + +blink_time = 0 + +num_output_buffer_lines = 0 + +-- helpers +function isAltDown() + return love.keyboard.isDown("lalt") or love.keyboard.isDown("ralt") +end + +function isCtrlDown() + return love.keyboard.isDown("lctrl") or love.keyboard.isDown("rctrl") +end + +function isShiftDown() + return love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift") +end + +function clamp(min, value, max) + if (value > max) then + return max + elseif (value < min) then + return min + else + return value + end +end + +function utf8.sub(s, i, j) + assert(type(s) == "number" or type(s) == "string", string.format("bad argument #1 to 'sub' (string expected, got %s)", type(s) ~= "nil" and type(s) or "no value")) + assert(type(i) == "number" or type(tonumber(i)) == "number", string.format("bad argument #2 to 'sub' (number expected, got %s)", type(i) ~= "nil" and type(i) or "no value")) + assert(type(j) == "nil" or type(j) == "number" or type(tonumber(j) == "number"), string.format("bad argument #3 to 'sub' (number expeted, got %s)", type(i))) + s, i, j = tostring(s), tonumber(i), tonumber(j) + + local offset_i, offset_j + local s_len = utf8.len(s) + + if (i > s_len) then + offset_i = utf8.offset(s, s_len + 1) + elseif (i < -s_len) then + offset_i = 0 + else + offset_i = utf8.offset(s, i) + end + + if (j ~= nil) then + if (j > s_len or j == -1) then + offset_j = utf8.offset(s, s_len + 1) - 1 + elseif (j < -s_len) then + offset_j = 0 + else + offset_j = utf8.offset(s, j + 1) - 1 + end + end + + return string.sub(s, offset_i, offset_j) +end + +-- this is some basic utf8.find that works for just the things I need it to +function utf8.find(s, pattern, index) + assert(type(s) == "number" or type(s) == "string", string.format("bad argument #1 to 'find' (string expected, got %s)", type(s) ~= "nil" and type(s) or "no value")) + assert(type(pattern) == "number" or type(pattern) == "string", string.format("bad argument #2 to 'find' (string expected, got %s)", type(pattern) ~= "nil" and type(pattern) or "no value")) + s, pattern, index = tostring(s), tostring(pattern), index or 1 + + local function depattern(pattern) local tp = {"%%x", "%%.", "%%[", "%%]"} local td = {"x", ".", "[", "]"} local p = pattern for i, v in pairs(tp) do pattern = string.gsub(pattern, v, td[i]) end return pattern end + local s_len = utf8.len(s) + local p_len = string.len(depattern(pattern)) + + for i = index, s_len do + local s_ = utf8.sub(s, i, i + p_len - 1) + + if (string.find(s_, pattern) ~= nil) then + return i, i + p_len - 1 + end + end + + return nil +end + +-- cursor +function ResetBlink() + blink_time = 0 + ui.cursor.visible = true +end + +function UpdateCursor() + local x = 4 + font_w + cursor_idx * font_w + + ResetBlink() + ui.cursor.x = x +end + +function MoveCursorRight() + if (console.ui.selected.visible == true) then + MoveCursorToPosition(math.max(selected_idx1, selected_idx2)) + DeselectAll() + return + end + + cursor_idx = math.min(cursor_idx + 1, utf8.len(input_buffer)) + UpdateCursor() +end + +function MoveCursorLeft() + if (console.ui.selected.visible == true) then + MoveCursorToPosition(math.min(selected_idx1, selected_idx2)) + DeselectAll() + return + end + + cursor_idx = math.max(0, cursor_idx - 1) + UpdateCursor() +end + +function MoveCursorToPosition(pos) + cursor_idx = clamp(0, pos, utf8.len(input_buffer)) + UpdateCursor() +end + +function MoveCursorByOffset(offset) + cursor_idx = clamp(0, cursor_idx + offset, utf8.len(input_buffer)) + UpdateCursor() +end + +function MoveCursorHome() + cursor_idx = 0 + DeselectAll() + UpdateCursor() +end + +function MoveCursorEnd() + cursor_idx = utf8.len(input_buffer) + DeselectAll() + UpdateCursor() +end + +function JumpCursorLeft() + if (string.match(utf8.sub(input_buffer, cursor_idx, cursor_idx), "%p") ~= nil) then + cursor_idx = math.max(0, cursor_idx - 1) + else + local p_idx + + for i = cursor_idx - 1, 0, -1 do + if (string.match(utf8.sub(input_buffer, i, i), "%p") ~= nil) then + p_idx = i + break + end + end + + cursor_idx = p_idx or 0 + end + + UpdateCursor() +end + +function JumpCursorRight() + if (string.match(utf8.sub(input_buffer, cursor_idx + 1, cursor_idx + 1), "%p") ~= nil) then + cursor_idx = math.min(cursor_idx + 1, utf8.len(input_buffer)) + else + local p_idx + + for i = cursor_idx, utf8.len(input_buffer) do + if (string.match(utf8.sub(input_buffer, i + 1, i + 1), "%p") ~= nil) then + p_idx = i + break + end + end + + cursor_idx = p_idx or utf8.len(input_buffer) + end + + UpdateCursor() +end + + -- selected +function UpdateSelected() + if (selected_idx1 == -1 or selected_idx1 == selected_idx2) then + ui.selected.visible = false + else + local left = math.min(selected_idx1, selected_idx2) + local right = math.max(selected_idx1, selected_idx2) + local x = 4 + font_w + left * font_w + local w = (right - left) * font_w + + ui.selected.x = x + ui.selected.w = w + ui.selected.visible = true + end +end + +function DeselectAll() + selected_idx1 = -1 + selected_idx2 = -1 + + UpdateSelected() +end + +function SelectAll() + cursor_idx = utf8.len(input_buffer) + selected_idx1 = 0 + selected_idx2 = cursor_idx + UpdateCursor() + UpdateSelected() +end + +function SelectCursorRight() + if (cursor_idx == utf8.len(input_buffer)) then return end + + if (console.ui.selected.visible == false) then + selected_idx1 = cursor_idx + end + + cursor_idx = math.min(cursor_idx + 1, utf8.len(input_buffer)) + selected_idx2 = cursor_idx + UpdateCursor() + UpdateSelected() +end + +function SelectCursorLeft() + if (cursor_idx == 0) then return end + + if (console.ui.selected.visible == false) then + selected_idx1 = cursor_idx + end + + cursor_idx = math.max(0, cursor_idx - 1) + selected_idx2 = cursor_idx + UpdateCursor() + UpdateSelected() +end + +function RemoveSelected() + local left_idx = math.min(selected_idx1, selected_idx2) + local right_idx = math.max(selected_idx1, selected_idx2) + + local left = utf8.sub(input_buffer, 1, left_idx) + local right = utf8.sub(input_buffer, right_idx + 1, utf8.len(input_buffer)) + + input_buffer = left .. right + MoveCursorToPosition(left_idx) + DeselectAll() +end + +function SelectHome() + if (cursor_idx == 0) then return end + + if (console.ui.selected.visible == false) then + selected_idx1 = cursor_idx + end + + cursor_idx = 0 + selected_idx2 = cursor_idx + UpdateSelected() + UpdateCursor() +end + +function SelectEnd() + if (cursor_idx == utf8.len(input_buffer)) then return end + + if (console.ui.selected.visible == false) then + selected_idx1 = cursor_idx + end + + cursor_idx = utf8.len(input_buffer) + selected_idx2 = cursor_idx + UpdateSelected() + UpdateCursor() +end + +function SelectJumpCursorLeft() + if (cursor_idx == 0) then return end + + if (console.ui.selected.visible == false) then + selected_idx1 = cursor_idx + end + + if (string.match(utf8.sub(input_buffer, cursor_idx, cursor_idx), "%p") ~= nil) then + cursor_idx = math.max(0, cursor_idx - 1) + else + local p_idx + + for i = cursor_idx - 1, 0, -1 do + if (string.match(utf8.sub(input_buffer, i, i), "%p") ~= nil) then + p_idx = i + break + end + end + + cursor_idx = p_idx or 0 + end + + selected_idx2 = cursor_idx + UpdateSelected() + UpdateCursor() +end + +function SelectJumpCursorRight() + if (cursor_idx == utf8.len(input_buffer)) then return end + + if (console.ui.selected.visible == false) then + selected_idx1 = cursor_idx + end + + if (string.match(utf8.sub(input_buffer, cursor_idx + 1, cursor_idx + 1), "%p") ~= nil) then + cursor_idx = math.min(cursor_idx + 1, utf8.len(input_buffer)) + else + local p_idx + + for i = cursor_idx, utf8.len(input_buffer) do + if (string.match(utf8.sub(input_buffer, i + 1, i + 1), "%p") ~= nil) then + p_idx = i + break + end + end + + cursor_idx = p_idx or utf8.len(input_buffer) + end + + selected_idx2 = cursor_idx + UpdateSelected() + UpdateCursor() +end + +-- insert/delete +function InsertChar(char) + if (input_buffer == "" and char == "`") then return end + + if (console.ui.selected.visible == true) then + RemoveSelected() + end + + if (cursor_idx == utf8.len(input_buffer)) then + input_buffer = input_buffer .. char + else + local left = utf8.sub(input_buffer, 1, cursor_idx) + local right = utf8.sub(input_buffer, cursor_idx + 1, utf8.len(input_buffer)) + + input_buffer = left .. char .. right + end + + MoveCursorRight() +end + +function RemovePrevChar() + if (console.ui.selected.visible == true) then + RemoveSelected() + else + if (cursor_idx == 0) then return end + + local left = utf8.sub(input_buffer, 1, cursor_idx - 1) + local right = utf8.sub(input_buffer, cursor_idx + 1, utf8.len(input_buffer)) + + input_buffer = left .. right + MoveCursorLeft() + end +end + +function RemoveNextChar() + if (console.ui.selected.visible == true) then + RemoveSelected() + else + if (cursor_idx == utf8.len(input_buffer)) then return end + + local left = utf8.sub(input_buffer, 1, cursor_idx) + local right = utf8.sub(input_buffer, cursor_idx + 2, utf8.len(input_buffer)) + + input_buffer = left .. right + end +end + +function Cut() + if (console.ui.selected.visible == true) then + local left_idx = math.min(selected_idx1, selected_idx2) + local right_idx = math.max(selected_idx1, selected_idx2) + local left = utf8.sub(input_buffer, 1, left_idx) + local right = utf8.sub(input_buffer, right_idx + 1, utf8.len(input_buffer)) + + love.system.setClipboardText(utf8.sub(input_buffer, left_idx + 1, right_idx)) + input_buffer = left .. right + MoveCursorToPosition(left_idx) + DeselectAll() + end +end + +function Copy() + if (console.ui.selected.visible == true) then + local left_idx = math.min(selected_idx1, selected_idx2) + local right_idx = math.max(selected_idx1, selected_idx2) + + love.system.setClipboardText(utf8.sub(input_buffer, left_idx + 1, right_idx)) + end +end + +function Paste() + if (console.ui.selected.visible == true) then + local left_idx = math.min(selected_idx1, selected_idx2) + local right_idx = math.max(selected_idx1, selected_idx2) + local left = utf8.sub(input_buffer, 1, left_idx) + local right = utf8.sub(input_buffer, right_idx + 1, utf8.len(input_buffer)) + + input_buffer = left .. love.system.getClipboardText() .. right + DeselectAll() + else + local left = utf8.sub(input_buffer, 1, cursor_idx) + local right = utf8.sub(input_buffer, cursor_idx + 1, utf8.len(input_buffer)) + + input_buffer = left .. love.system.getClipboardText() .. right + end + + MoveCursorByOffset(utf8.len(love.system.getClipboardText())) +end + +function ClearInputBuffer() + input_buffer = "" + MoveCursorHome() +end + +-- history +function AddToHistory(msg) + table.insert(history_buffer, msg) + history_idx = #history_buffer + 1 +end + +function ClearHistoryBuffer() + history_buffer = {} + history_idx = #history_buffer + 1 +end + +function MoveHistoryDown() + history_idx = math.min(history_idx + 1, #history_buffer + 1) + + if (history_idx == #history_buffer + 1) then + input_buffer = "" + else + input_buffer = history_buffer[history_idx] + end + + MoveCursorEnd() +end + +function MoveHistoryUp() + history_idx = math.max(1, history_idx - 1) + input_buffer = history_buffer[history_idx] or "" + MoveCursorEnd() +end + +-- output +function AddToOutput(...) + local arg = {...} + local narg = select("#", ...) + + for i = 1, narg do + arg[i] = tostring(arg[i]) + end + + msg = parse(table.concat(arg, " ")) + table.insert(output_buffer, msg) +end + +function ClearOutputBuffer() + output_buffer = {} + output_idx = 0 +end + +function MoveOutputBy(n) + output_idx = clamp(0, output_idx + n, math.max(#output_buffer - num_output_buffer_lines, 0)) +end + +function MoveOutputUp() + MoveOutputBy(output_jump_by) +end + +function MoveOutputDown() + MoveOutputBy(-output_jump_by) +end + +-- special commands +function Exit() + ClearInputBuffer() + console.Hide() +end + +function Clear() + ClearHistoryBuffer() + ClearOutputBuffer() + ClearInputBuffer() +end + +function Quit() + love.event.quit() +end + +function Git() + print(git_link) + ClearInputBuffer() +end + +function ClearEsc() + if (console.ui.selected.visible == true) then + DeselectAll() + else + ClearInputBuffer() + end +end + +function ExecInputBuffer() + if (input_buffer == "") then return end + if (input_buffer == "qqq") then Quit() return end + if (input_buffer == "git") then Git() return end + if (input_buffer == "clear") then Clear() return end + if (input_buffer == "exit") then Exit() return end + + local func, err = loadstring(input_buffer) + + AddToHistory(input_buffer) + AddToOutput("|cff" .. color_com .. "exec: |r" .. input_buffer) + + if (err ~= nil) then + print(parse_(input_buffer)) + else + local status, err = pcall(func) + + if (err ~= nil) then + print("pcall: " .. err) + end + end + + ClearInputBuffer() + DeselectAll() +end + +function parse_(msg) + local queue = {} + local enqueue = function(v) table.insert(queue, v) end + local dequeue = function() local v = queue[1] table.remove(queue, 1) return v end + local num_loops = 0 + + while (msg ~= "") do + if (num_loops >= 15) then + err("Something went horribly wrong (either the parser failed somehow") + err("or the variable is nested too deep, >= 15), please report this") + return nil + end + + local dot_idx = utf8.find(msg, "%.") or math.huge + local bra_idx = utf8.find(msg, "%[") or math.huge + local first_idx = math.min(dot_idx, bra_idx) + + if (dot_idx == 1) then + msg = utf8.sub(msg, 2) + elseif (bra_idx == 1) then + local end_idx = utf8.find(msg, "%]") + if (utf8.sub(msg, 2, 2) == "\"") then + enqueue(utf8.sub(msg, 3, end_idx - 2)) + else + enqueue(tonumber(utf8.sub(msg, 2, end_idx - 1))) + end + msg = utf8.sub(msg, end_idx + 1) + else + enqueue(utf8.sub(msg, 1, first_idx - 1)) + msg = utf8.sub(msg, first_idx) + end + + num_loops = num_loops + 1 + end + + local value + + while (#queue > 0) do + local t = dequeue() + + if (value == nil) then + value = _G[t] + else + if (type(value) == "table" and value[t] ~= nil) then + value = value[t] + else + value = nil + end + end + end + + return tostring(value) +end + +function EncodeKey(key) + local key_encoded = "" + + key_encoded = key_encoded .. (isCtrlDown() and "^" or "") + key_encoded = key_encoded .. (isShiftDown() and "+" or "") + key_encoded = key_encoded .. (isAltDown() and "%" or "") + + return key_encoded .. key +end + +function parse(text) + local parsed = {} + + local color_stack = {} + local push = function(color) table.insert(color_stack, color) end + local pop = function() if (#color_stack > 0) then table.remove(color_stack, #color_stack) end end + local peek = function() if (#color_stack > 0) then return color_stack[#color_stack] end end + local torgb = function(hex) return {tonumber(hex:sub(1, 2), 16), tonumber(hex:sub(3, 4), 16), tonumber(hex:sub(5, 6), 16), 255} end + local offset = 1 + + local c_tag = "|c%x%x%x%x%x%x%x%x" + local c_tag_len = 10 + local r_tag = "|r" + local r_tag_len = 2 + + while (offset <= utf8.len(text)) do + local t = utf8.sub(text, offset) + local c_idx = utf8.find(t, c_tag) + local r_idx = utf8.find(t, r_tag) + + if (c_idx == 1) then + local color = utf8.sub(t, c_idx + 4, c_idx + 9) + + push(color) + offset = offset + c_tag_len + elseif (r_idx == 1) then + pop() + offset = offset + r_tag_len + else + local next_tag_idx = (c_idx or r_idx) and math.min(c_idx or math.huge, r_idx or math.huge) or 0 + local text = utf8.sub(t, 1, next_tag_idx - 1) + + table.insert(parsed, {color = peek() or "ffffff", text = text or ""}) + + offset = offset + utf8.len(text) + end + end + + if (#parsed == 0) then + table.insert(parsed, {color = "ffffff", text = ""}) + end + + local usable = {} + + for i = 1, #parsed do + table.insert(usable, torgb(parsed[i].color)) + table.insert(usable, parsed[i].text) + end + + return usable +end + +function _G.warn(...) + AddToOutput("|cff" .. color_warn .. "warning:|r", ...) +end + +function _G.err(...) + AddToOutput("|cff" .. color_err .. "error:|r", ...) +end + +function _G.info(...) + AddToOutput("|cff" .. color_info .. "info:|r", ...) +end + +function Show() + if (is_first_open == false) then + is_first_open = true + MakeUI() + HookPrint() + HookClose() + end + + if (is_open == false) then + is_open = true + Hook() + ResetBlink() + end +end + +function Hide() + if (is_open == true) then + is_open = false + UnHook() + end +end + +keybinds = { + ["kpenter"] = ExecInputBuffer, + + ["up"] = MoveHistoryUp, + ["down"] = MoveHistoryDown, + + ["left"] = MoveCursorLeft, + ["right"] = MoveCursorRight, + + ["+left"] = SelectCursorLeft, + ["+right"] = SelectCursorRight, + + ["^left"] = JumpCursorLeft, + ["^right"] = JumpCursorRight, + + ["^+left"] = SelectJumpCursorLeft, + ["^+right"] = SelectJumpCursorRight, + + ["+home"] = SelectHome, + ["+end"] = SelectEnd, + + ["escape"] = ClearEsc, + + ["home"] = MoveCursorHome, + ["end"] = MoveCursorEnd, + ["pageup"] = MoveOutputUp, + ["pagedown"] = MoveOutputDown, + ["backspace"] = RemovePrevChar, + ["delete"] = RemoveNextChar, + ["return"] = ExecInputBuffer, + + ["kpenter"] = ExecInputBuffer, + + ["^a"] = SelectAll, + ["^x"] = Cut, + ["^c"] = Copy, + ["^v"] = Paste, + + ["`"] = Hide +} + +-- hooks and overrides +function update(dt) + blink_time = blink_time + dt + + if (blink_time >= blink_duration) then + ui.cursor.visible = not ui.cursor.visible + blink_time = blink_time - blink_duration + end +end + +function draw() + DrawUI() +end + +function wheelmoved(_, dir) + MoveOutputBy(dir) +end + +function keypressed(key) + local key_encoded = EncodeKey(key) + + if (keybinds[key_encoded] ~= nil) then + keybinds[key_encoded]() + end +end + +function MakeUI() + ui = {} + ui.background = {x = 0, z = 0, w = love.graphics.getWidth(), h = love.graphics.getHeight() / 3, color = background_color} + ui.arrow = {x = 2, z = ui.background.h - font_h} + ui.input = {x = 4 + font_w, z = ui.background.h - font_h} + ui.output = {} + + local height_left = ui.background.h - font_h + local i = 0 + + while (height_left >= (font_h)) do + i = i + 1 + ui.output[i] = {x = 4 + font_w, z = ui.background.h - font_h - i * font_h} + height_left = height_left - font_h + end + + num_output_buffer_lines = i + ui.selected = {x = 4 + font_w, z = ui.background.h - font_h, w = 0, h = font_h, color = selected_color, visible = false} + ui.cursor = {x = 4 + font_w, z = ui.background.h - font_h, w = 1, h = font_h, color = cursor_color, visible = true} + + if (cursor_style == "block") then + ui.cursor.w = font_w + ui.cursor.color[4] = 127 + end + + table.insert(output_buffer, git_link) + table.insert(output_buffer, "Press ` or type 'exit' to close") +end + +function DrawUI() + if (ui ~= nil) then + love.graphics.setColor(ui.background.color) + love.graphics.rectangle("fill", ui.background.x, ui.background.z, ui.background.w, ui.background.h) + love.graphics.setColor({255, 255, 255, 255}) + love.graphics.print(">", ui.arrow.x, ui.arrow.z) + love.graphics.print(input_buffer or "", ui.input.x, ui.input.z) + + for i = 1, num_output_buffer_lines do + local idx = #output_buffer - i + 1 + love.graphics.print(output_buffer[idx - output_idx] or "", ui.output[i].x, ui.output[i].z) + end + + if (ui.selected.visible == true) then + love.graphics.setColor(ui.selected.color) + love.graphics.rectangle("fill", ui.selected.x, ui.selected.z, ui.selected.w, ui.selected.h) + end + + if (ui.cursor.visible == true) then + love.graphics.setColor(ui.cursor.color) + love.graphics.rectangle("fill", ui.cursor.x, ui.cursor.z, ui.cursor.w, ui.cursor.h) + end + end +end + +function HookPrint() + unhooked.print = print + + _G.print = function(...) + unhooked.print(...) + AddToOutput(...) + end +end + +function HookClose() + unhooked.quit = love.quit + + _G.love.quit = function(...) + local f_history = io.open(love.filesystem.getSource() .. "/" .. path_load .. "/history.txt", "w+") + local low = math.max(1, #history_buffer - 30 + 1) + + for i = low, #history_buffer do + f_history:write(history_buffer[i] .. "\n") + end + f_history:close() + + if (unhooked.quit ~= nil) then + unhooked.quit(...) + end + end +end + +function textinput(key) + InsertChar(key) +end + +function Hook() + unhooked.key_repeat = love.keyboard.hasKeyRepeat() + unhooked.font = love.graphics.getFont() + unhooked.color = {love.graphics.getColor()} + unhooked.update = love.update + unhooked.draw = love.draw + unhooked.wheelmoved = love.wheelmoved + unhooked.mousepressed = love.mousepressed + unhooked.mousereleased = love.mousereleased + unhooked.keypressed = love.keypressed + unhooked.keyreleased = love.keyreleased + unhooked.textinput = love.textinput + + love.keyboard.setKeyRepeat(true) + love.graphics.setFont(font) + love.update = function(dt) + if (unhooked.update ~= nil) then + unhooked.update(dt) + end + update(dt) + end + love.draw = function() + love.graphics.setFont(unhooked.font) + love.graphics.setColor(unhooked.color) + if (unhooked.draw ~= nil) then + unhooked.draw() + end + love.graphics.setFont(font) + draw() + end + love.wheelmoved = wheelmoved + love.mousepressed = mousepressed + love.mousereleased = mousereleased + love.keypressed = keypressed + love.keyreleased = keyreleased + love.textinput = textinput +end + +function UnHook() + love.keyboard.setKeyRepeat(unhooked.key_repeat) + love.graphics.setFont(unhooked.font) + love.graphics.setColor(unhooked.color) + love.update = unhooked.update + love.draw = unhooked.draw + love.wheelmoved = unhooked.wheelmoved + love.mousepressed = unhooked.mousepressed + love.mousereleased = unhooked.mousereleased + love.keypressed = unhooked.keypressed + love.keyreleased = unhooked.keyreleased + love.textinput = unhooked.textinput +end + +do + local f_history = io.open(love.filesystem.getSource() .. "/" .. path_load .. "/history.txt", "r") + if (f_history == nil) then + f_history = io.open(love.filesystem.getSource() .. "/" .. path_load .. "/history.txt", "w") + f_history:close() + f_history = nil + else + line = f_history:read("*line") + while (line ~= nil) do + table.insert(history_buffer, line) + line = f_history:read("*line") + end + history_idx = #history_buffer + 1 + f_history:close() + f_history = nil + end +end diff --git a/libs/console/history.txt b/libs/console/history.txt new file mode 100644 index 0000000..e69de29 diff --git a/libs/hc b/libs/hc new file mode 160000 index 0000000..eac8874 --- /dev/null +++ b/libs/hc @@ -0,0 +1 @@ +Subproject commit eac8874ef9e4c2e0cabe717e78582d164127eed4 diff --git a/libs/hump b/libs/hump new file mode 160000 index 0000000..96c9648 --- /dev/null +++ b/libs/hump @@ -0,0 +1 @@ +Subproject commit 96c9648a62ac8a437de3264c3c2eabf026d0120d diff --git a/libs/inspect b/libs/inspect new file mode 160000 index 0000000..a998635 --- /dev/null +++ b/libs/inspect @@ -0,0 +1 @@ +Subproject commit a9986352070deb273d2aef38c682390a79d61bf3 diff --git a/libs/inspect.lua b/libs/inspect.lua new file mode 100644 index 0000000..01c8576 --- /dev/null +++ b/libs/inspect.lua @@ -0,0 +1,340 @@ +local inspect ={ + _VERSION = 'inspect.lua 3.0.3', + _URL = 'http://github.com/kikito/inspect.lua', + _DESCRIPTION = 'human-readable representations of tables', + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2013 Enrique García Cota + + 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. + ]] +} + +inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) +inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) + +-- Apostrophizes the string if it has quotes, but not aphostrophes +-- Otherwise, it returns a regular quoted string +local function smartQuote(str) + if str:match('"') and not str:match("'") then + return "'" .. str .. "'" + end + return '"' .. str:gsub('"', '\\"') .. '"' +end + +local controlCharsTranslation = { + ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", + ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" +} + +local function escape(str) + local result = str:gsub("\\", "\\\\"):gsub("(%c)", controlCharsTranslation) + return result +end + +local function isIdentifier(str) + return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) +end + +local function isSequenceKey(k, sequenceLength) + return type(k) == 'number' + and 1 <= k + and k <= sequenceLength + and math.floor(k) == k +end + +local defaultTypeOrders = { + ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, + ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 +} + +local function sortKeys(a, b) + local ta, tb = type(a), type(b) + + -- strings and numbers are sorted numerically/alphabetically + if ta == tb and (ta == 'string' or ta == 'number') then return a < b end + + local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] + -- Two default types are compared according to the defaultTypeOrders table + if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] + elseif dta then return true -- default types before custom ones + elseif dtb then return false -- custom types after default ones + end + + -- custom types are sorted out alphabetically + return ta < tb +end + +-- For implementation reasons, the behavior of rawlen & # is "undefined" when +-- tables aren't pure sequences. So we implement our own # operator. +local function getSequenceLength(t) + local len = 1 + local v = rawget(t,len) + while v ~= nil do + len = len + 1 + v = rawget(t,len) + end + return len - 1 +end + +local function getNonSequentialKeys(t) + local keys = {} + local sequenceLength = getSequenceLength(t) + for k,_ in pairs(t) do + if not isSequenceKey(k, sequenceLength) then table.insert(keys, k) end + end + table.sort(keys, sortKeys) + return keys, sequenceLength +end + +local function getToStringResultSafely(t, mt) + local __tostring = type(mt) == 'table' and rawget(mt, '__tostring') + local str, ok + if type(__tostring) == 'function' then + ok, str = pcall(__tostring, t) + str = ok and str or 'error: ' .. tostring(str) + end + if type(str) == 'string' and #str > 0 then return str end +end + +local maxIdsMetaTable = { + __index = function(self, typeName) + rawset(self, typeName, 0) + return 0 + end +} + +local idsMetaTable = { + __index = function (self, typeName) + local col = {} + rawset(self, typeName, col) + return col + end +} + +local function countTableAppearances(t, tableAppearances) + tableAppearances = tableAppearances or {} + + if type(t) == 'table' then + if not tableAppearances[t] then + tableAppearances[t] = 1 + for k,v in pairs(t) do + countTableAppearances(k, tableAppearances) + countTableAppearances(v, tableAppearances) + end + countTableAppearances(getmetatable(t), tableAppearances) + else + tableAppearances[t] = tableAppearances[t] + 1 + end + end + + return tableAppearances +end + +local copySequence = function(s) + local copy, len = {}, #s + for i=1, len do copy[i] = s[i] end + return copy, len +end + +local function makePath(path, ...) + local keys = {...} + local newPath, len = copySequence(path) + for i=1, #keys do + newPath[len + i] = keys[i] + end + return newPath +end + +local function processRecursive(process, item, path) + if item == nil then return nil end + + local processed = process(item, path) + if type(processed) == 'table' then + local processedCopy = {} + local processedKey + + for k,v in pairs(processed) do + processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY)) + if processedKey ~= nil then + processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey)) + end + end + + local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE)) + setmetatable(processedCopy, mt) + processed = processedCopy + end + return processed +end + + +------------------------------------------------------------------- + +local Inspector = {} +local Inspector_mt = {__index = Inspector} + +function Inspector:puts(...) + local args = {...} + local buffer = self.buffer + local len = #buffer + for i=1, #args do + len = len + 1 + buffer[len] = tostring(args[i]) + end +end + +function Inspector:down(f) + self.level = self.level + 1 + f() + self.level = self.level - 1 +end + +function Inspector:tabify() + self:puts(self.newline, string.rep(self.indent, self.level)) +end + +function Inspector:alreadyVisited(v) + return self.ids[type(v)][v] ~= nil +end + +function Inspector:getId(v) + local tv = type(v) + local id = self.ids[tv][v] + if not id then + id = self.maxIds[tv] + 1 + self.maxIds[tv] = id + self.ids[tv][v] = id + end + return id +end + +function Inspector:putKey(k) + if isIdentifier(k) then return self:puts(k) end + self:puts("[") + self:putValue(k) + self:puts("]") +end + +function Inspector:putTable(t) + if t == inspect.KEY or t == inspect.METATABLE then + self:puts(tostring(t)) + elseif self:alreadyVisited(t) then + self:puts('') + elseif self.level >= self.depth then + self:puts('{...}') + else + if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end + + local nonSequentialKeys, sequenceLength = getNonSequentialKeys(t) + local mt = getmetatable(t) + local toStringResult = getToStringResultSafely(t, mt) + + self:puts('{') + self:down(function() + if toStringResult then + self:puts(' -- ', escape(toStringResult)) + if sequenceLength >= 1 then self:tabify() end + end + + local count = 0 + for i=1, sequenceLength do + if count > 0 then self:puts(',') end + self:puts(' ') + self:putValue(t[i]) + count = count + 1 + end + + for _,k in ipairs(nonSequentialKeys) do + if count > 0 then self:puts(',') end + self:tabify() + self:putKey(k) + self:puts(' = ') + self:putValue(t[k]) + count = count + 1 + end + + if mt then + if count > 0 then self:puts(',') end + self:tabify() + self:puts(' = ') + self:putValue(mt) + end + end) + + if #nonSequentialKeys > 0 or mt then -- result is multi-lined. Justify closing } + self:tabify() + elseif sequenceLength > 0 then -- array tables have one extra space before closing } + self:puts(' ') + end + + self:puts('}') + end +end + +function Inspector:putValue(v) + local tv = type(v) + + if tv == 'string' then + self:puts(smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then + self:puts(tostring(v)) + elseif tv == 'table' then + self:putTable(v) + else + self:puts('<',tv,' ',self:getId(v),'>') + end +end + +------------------------------------------------------------------- + +function inspect.inspect(root, options) + options = options or {} + + local depth = options.depth or math.huge + local newline = options.newline or '\n' + local indent = options.indent or ' ' + local process = options.process + + if process then + root = processRecursive(process, root, {}) + end + + local inspector = setmetatable({ + depth = depth, + buffer = {}, + level = 0, + ids = setmetatable({}, idsMetaTable), + maxIds = setmetatable({}, maxIdsMetaTable), + newline = newline, + indent = indent, + tableAppearances = countTableAppearances(root) + }, Inspector_mt) + + inspector:putValue(root) + + return table.concat(inspector.buffer) +end + +setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) + +return inspect + diff --git a/libs/lovebird.lua b/libs/lovebird.lua new file mode 100644 index 0000000..27b6b02 --- /dev/null +++ b/libs/lovebird.lua @@ -0,0 +1,723 @@ +-- +-- lovebird +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local socket = require "socket" + +local lovebird = { _version = "0.4.0" } + +lovebird.loadstring = loadstring or load +lovebird.inited = false +lovebird.host = "*" +lovebird.buffer = "" +lovebird.lines = {} +lovebird.connections = {} +lovebird.pages = {} + +lovebird.wrapprint = true +lovebird.timestamp = true +lovebird.allowhtml = false +lovebird.echoinput = true +lovebird.port = 8000 +lovebird.whitelist = { "127.0.0.1", "192.168.*.*" } +lovebird.maxlines = 200 +lovebird.updateinterval = .5 + + +lovebird.pages["index"] = [[ + + + + + + + lovebird + + + + +
+
+
+
+
+ + +
+
+
+
+
+
+
+ + + +]] + + +lovebird.pages["buffer"] = [[ ]] + + +lovebird.pages["env.json"] = [[ + +{ + "valid": true, + "path": "", + "vars": [ + + { + "key": "", + "value": , + "type": "", + }, + + ] +} +]] + + + +function lovebird.init() + -- Init server + lovebird.server = assert(socket.bind(lovebird.host, lovebird.port)) + lovebird.addr, lovebird.port = lovebird.server:getsockname() + lovebird.server:settimeout(0) + -- Wrap print + lovebird.origprint = print + if lovebird.wrapprint then + local oldprint = print + print = function(...) + oldprint(...) + lovebird.print(...) + end + end + -- Compile page templates + for k, page in pairs(lovebird.pages) do + lovebird.pages[k] = lovebird.template(page, "lovebird, req", + "pages." .. k) + end + lovebird.inited = true +end + + +function lovebird.template(str, params, chunkname) + params = params and ("," .. params) or "" + local f = function(x) return string.format(" echo(%q)", x) end + str = ("?>"..str.."(.-)<%?lua", f) + str = "local echo " .. params .. " = ..." .. str + local fn = assert(lovebird.loadstring(str, chunkname)) + return function(...) + local output = {} + local echo = function(str) table.insert(output, str) end + fn(echo, ...) + return table.concat(lovebird.map(output, tostring)) + end +end + + +function lovebird.map(t, fn) + local res = {} + for k, v in pairs(t) do res[k] = fn(v) end + return res +end + + +function lovebird.trace(...) + local str = "[lovebird] " .. table.concat(lovebird.map({...}, tostring), " ") + print(str) + if not lovebird.wrapprint then lovebird.print(str) end +end + + +function lovebird.unescape(str) + local f = function(x) return string.char(tonumber("0x"..x)) end + return (str:gsub("%+", " "):gsub("%%(..)", f)) +end + + +function lovebird.parseurl(url) + local res = {} + res.path, res.search = url:match("/([^%?]*)%??(.*)") + res.query = {} + for k, v in res.search:gmatch("([^&^?]-)=([^&^#]*)") do + res.query[k] = lovebird.unescape(v) + end + return res +end + + +function lovebird.htmlescape(str) + return str:gsub("<", "<") +end + + +function lovebird.truncate(str, len) + if #str <= len then + return str + end + return str:sub(1, len - 3) .. "..." +end + + +function lovebird.compare(a, b) + local na, nb = tonumber(a), tonumber(b) + if na then + if nb then return na < nb end + return false + elseif nb then + return true + end + return tostring(a) < tostring(b) +end + + +function lovebird.checkwhitelist(addr) + if lovebird.whitelist == nil then return true end + for _, a in pairs(lovebird.whitelist) do + local ptn = "^" .. a:gsub("%.", "%%."):gsub("%*", "%%d*") .. "$" + if addr:match(ptn) then return true end + end + return false +end + + +function lovebird.clear() + lovebird.lines = {} + lovebird.buffer = "" +end + + +function lovebird.pushline(line) + line.time = os.time() + line.count = 1 + table.insert(lovebird.lines, line) + if #lovebird.lines > lovebird.maxlines then + table.remove(lovebird.lines, 1) + end + lovebird.recalcbuffer() +end + + +function lovebird.recalcbuffer() + local function doline(line) + local str = line.str + if not lovebird.allowhtml then + str = lovebird.htmlescape(line.str):gsub("\n", "
") + end + if line.type == "input" then + str = '' .. str .. '' + else + if line.type == "error" then + str = '! ' .. str + str = '' .. str .. '' + end + if line.count > 1 then + str = '' .. line.count .. ' ' .. str + end + if lovebird.timestamp then + str = os.date('%H:%M:%S ', line.time) .. + str + end + end + return str + end + lovebird.buffer = table.concat(lovebird.map(lovebird.lines, doline), "
") +end + + +function lovebird.print(...) + local t = {} + for i = 1, select("#", ...) do + table.insert(t, tostring(select(i, ...))) + end + local str = table.concat(t, " ") + local last = lovebird.lines[#lovebird.lines] + if last and str == last.str then + -- Update last line if this line is a duplicate of it + last.time = os.time() + last.count = last.count + 1 + lovebird.recalcbuffer() + else + -- Create new line + lovebird.pushline({ type = "output", str = str }) + end +end + + +function lovebird.onerror(err) + lovebird.pushline({ type = "error", str = err }) + if lovebird.wrapprint then + lovebird.origprint("[lovebird] ERROR: " .. err) + end +end + + +function lovebird.onrequest(req, client) + local page = req.parsedurl.path + page = page ~= "" and page or "index" + -- Handle "page not found" + if not lovebird.pages[page] then + return "HTTP/1.1 404\r\nContent-Length: 8\r\n\r\nBad page" + end + -- Handle page + local str + xpcall(function() + local data = lovebird.pages[page](lovebird, req) + str = "HTTP/1.1 200 OK\r\n" .. + "Content-Length: " .. #data .. "\r\n" .. + "\r\n" .. data + end, lovebird.onerror) + return str +end + + +function lovebird.receive(client, pattern) + while 1 do + local data, msg = client:receive(pattern) + if not data then + if msg == "timeout" then + -- Wait for more data + coroutine.yield(true) + else + -- Disconnected -- yielding nil means we're done + coroutine.yield(nil) + end + else + return data + end + end +end + + +function lovebird.send(client, data) + local idx = 1 + while idx < #data do + local res, msg = client:send(data, idx) + if not res and msg == "closed" then + -- Handle disconnect + coroutine.yield(nil) + else + idx = idx + res + coroutine.yield(true) + end + end +end + + +function lovebird.onconnect(client) + -- Create request table + local requestptn = "(%S*)%s*(%S*)%s*(%S*)" + local req = {} + req.socket = client + req.addr, req.port = client:getsockname() + req.request = lovebird.receive(client, "*l") + req.method, req.url, req.proto = req.request:match(requestptn) + req.headers = {} + while 1 do + local line, msg = lovebird.receive(client, "*l") + if not line or #line == 0 then break end + local k, v = line:match("(.-):%s*(.*)$") + req.headers[k] = v + end + if req.headers["Content-Length"] then + req.body = lovebird.receive(client, req.headers["Content-Length"]) + end + -- Parse body + req.parsedbody = {} + if req.body then + for k, v in req.body:gmatch("([^&]-)=([^&^#]*)") do + req.parsedbody[k] = lovebird.unescape(v) + end + end + -- Parse request line's url + req.parsedurl = lovebird.parseurl(req.url) + -- Handle request; get data to send and send + local data = lovebird.onrequest(req) + lovebird.send(client, data) + -- Clear up + client:close() +end + + +function lovebird.update() + if not lovebird.inited then lovebird.init() end + -- Handle new connections + while 1 do + -- Accept new connections + local client = lovebird.server:accept() + if not client then break end + client:settimeout(0) + local addr = client:getsockname() + if lovebird.checkwhitelist(addr) then + -- Connection okay -- create and add coroutine to set + local conn = coroutine.wrap(function() + xpcall(function() lovebird.onconnect(client) end, function() end) + end) + lovebird.connections[conn] = true + else + -- Reject connection not on whitelist + lovebird.trace("got non-whitelisted connection attempt: ", addr) + client:close() + end + end + -- Handle existing connections + for conn in pairs(lovebird.connections) do + -- Resume coroutine, remove if it has finished + local status = conn() + if status == nil then + lovebird.connections[conn] = nil + end + end +end + + +return lovebird diff --git a/libs/lovedebug.lua b/libs/lovedebug.lua new file mode 100644 index 0000000..47d4993 --- /dev/null +++ b/libs/lovedebug.lua @@ -0,0 +1,815 @@ +--local _Debug table for holding all variables +local _Debug = { + errors = {}, + prints = {}, + order = {}, --e for errors, p for prints + onTopFeed = {}, + orderOffset = 0, + longestOffset = 0, + + drawOverlay = false, + tickTime = 0.5, + tick = 0.5, + drawTick = true, + + input = "", + inputMarker = 0, + + lastH = nil, + lastCut = nil, + lastRows = 1, + + history = {''}, + historyIndex = 1, + + Font = love.graphics.newFont(12), + BigFont = love.graphics.newFont(24), + Proposals = {}, + ProposalLocation = _G; + Proposal_String = "", + + trackKeys = {}, + keyRepeatInterval = 0.05, + keyRepeatDelay = 0.4, + + liveOutput='', + liveLastModified=love.filesystem.getInfo('main.lua').modtime, + liveDo=false +} + +--Settings +_DebugSettings = { + MultipleErrors = false, + OverlayColor = {0, 0, 0}, + + DrawOnTop = true, + + LiveAuto = false, + LiveFile = 'main.lua', + LiveReset = false, + HaltExecution = true, + AutoScroll = false, +} + + +--Print all settings +_DebugSettings.Settings = function() + print("Settings:") + + print(" _DebugSettings.MultipleErrors [Boolean] Controls if errors should appear multiple times, default is false") + print(" _DebugSettings.OverlayColor [{int, int, int}] Sets the color of the overlay, default is {0,0,0}") + print(" _DebugSettings.LiveAuto [Boolean] Check if the code should be reloaded when it's modified, default is false") + print(" _DebugSettings.LiveFile [String] Sets the file that lovedebug reloads, default is 'main.lua'") + print(" _DebugSettings.LiveFile [{String,String,...}] Sets the files, has a table, that lovedebug reloads, can be multiple") + print(" _DebugSettings.LiveReset [Boolean] Rather or not love.load() should be reloaded if the code is HotSwapped, default is false") + print(" _DebugSettings.DrawOnTop [Boolean] If the errors and prints should be dispalyed on top of the screen, default is false") + print(" _DebugSettings.HaltExecution [Boolean] Rather or not to halt program execution while console is open, default is true") + print(" _DebugSettings.AutoScroll [Boolean] Rather or not to auto scroll the console once output fills up the console, default is false") +end + + + + +local super_print = print + +--Override print and call super +_G["print"] = function(...) + super_print(...) + local str = {} + for i = 1, select('#', ...) do + str[i] = tostring(select(i, ...)) + end + table.insert(_Debug.prints, table.concat(str, " ")) + table.insert(_Debug.order, "p" .. tostring(#_Debug.prints)) + table.insert(_Debug.onTopFeed, {"p" .. tostring(#_Debug.prints),0}) +end + + +--Error catcher +_Debug.handleError = function(err) + if _DebugSettings.MultipleErrors == false then + for i,v in pairs(_Debug.errors) do + if v == err then + return --Don't print the same error multiple times! + end + end + end + table.insert(_Debug.errors, err) + table.insert(_Debug.order, "e" .. tostring(#_Debug.errors)) + table.insert(_Debug.onTopFeed, {"e" .. tostring(#_Debug.errors),0}) +end + + +--Get Linetype +_Debug.lineInfo = function(str) + local prefix = string.sub(str, 1, 1) + local err = (prefix == "e") + local index = tonumber(str:sub(2)) + return err, index +end + + +--On Top drawer +_Debug.onTop = function() + local font = love.graphics.getFont() + local r, g, b, a = love.graphics.getColor() + love.graphics.push() + love.graphics.origin() + love.graphics.setFont(_Debug.Font) + + local p,e,err,index,msg = {},{} + for i,v in ipairs(_Debug.onTopFeed) do + err, index = _Debug.lineInfo(v[1]) --Obtain message and type + msg = err and _Debug.errors[index] or _Debug.prints[index] + if err then + table.insert(e,{msg,i}) + else + table.insert(p,{msg,i}) + end + end + if #p > 0 then + love.graphics.setColor(127, 127, 127, 255) + love.graphics.rectangle('fill', 0, 0, love.graphics.getWidth()/2, 2) + end + if #e > 0 then + love.graphics.setColor(255, 0, 0, 255) + love.graphics.rectangle('fill', love.graphics.getWidth()/2, 0, love.graphics.getWidth()/2, 2) + end + + if p then + --draw prints + love.graphics.setScissor(0,0,love.graphics.getWidth()/2,2+ 5*((#p-1) > -1 and #p-1 or 0) + #p*_Debug.Font:getHeight()) + love.graphics.setColor(127, 127, 127, 64) + love.graphics.rectangle('fill',0,1,love.graphics.getWidth()/2,2+ 5*((#p-1) > -1 and #p-1 or 0) + #p*_Debug.Font:getHeight()) + love.graphics.setColor(255, 255, 255, 255) + for i,v in ipairs(p) do + love.graphics.print(v[1], 5, 2+ 5*(i-1) + (i-1)*_Debug.Font:getHeight()) + end + end + if e then + --draw errors + love.graphics.setScissor(love.graphics.getWidth()/2,0,love.graphics.getWidth()/2,2+ 5*((#e-1) > -1 and #e-1 or 0) + #e*_Debug.Font:getHeight()) + love.graphics.setColor(255, 0, 0, 64) + love.graphics.rectangle('fill',love.graphics.getWidth()/2,1,love.graphics.getWidth()/2,2+ 5*((#e-1) > -1 and #e-1 or 0) + #e*_Debug.Font:getHeight()) + love.graphics.setColor(255, 255, 255, 255) + for i,v in ipairs(e) do + love.graphics.print(v[1], love.graphics.getWidth()/2+5, 2+ 5*(i-1) + (i-1)*_Debug.Font:getHeight()) + end + end + love.graphics.setScissor() + love.graphics.setColor(r, g, b, a) + if font then love.graphics.setFont(font) end + love.graphics.pop() +end +_Debug.onTopUpdate = function(dt) + local rmv = {} + for i,v in ipairs(_Debug.onTopFeed) do + if v[2] >= 6 then + table.insert(rmv,i) + else + _Debug.onTopFeed[1][2] = _Debug.onTopFeed[1][2] +dt + end + end + for i,v in ipairs(rmv) do + table.remove(_Debug.onTopFeed,v) + end +end + + +--Overlay drawer +_Debug.overlay = function() + local font = love.graphics.getFont() + local r, g, b, a = love.graphics.getColor() + love.graphics.push() + love.graphics.origin() + love.graphics.setStencilTest() + + local fontSize = _Debug.Font:getHeight() + local w = love.graphics.getWidth() + local h = love.graphics.getHeight() + local R, G, B = unpack(_DebugSettings.OverlayColor) + love.graphics.setColor(R, G, B, 220) + love.graphics.rectangle("fill", 0, 0, w, h) + love.graphics.setColor(255, 255, 255) + love.graphics.setFont(_Debug.Font) + local count = 0 + local cutY = 0 + if h ~= _Debug.lastH then --Did the height of the window change? + local _cutY = h - 40 + cutY = _cutY + local rows = 0 + while rows * (fontSize + 2) < _cutY do --Find out how long the scissor should be + rows = rows + 1 + cutY = rows * (fontSize + 2) + end + _Debug.lastRows = rows + else + cutY = _Debug.lastCut --Use the last good value + end + love.graphics.setScissor(0, 0, w, cutY + 1) + local drawing_length = #_Debug.order + if 1 + _Debug.orderOffset + _Debug.lastRows < drawing_length then + drawing_length = 1 + _Debug.orderOffset + _Debug.lastRows + end + for i = 1 + _Debug.orderOffset, drawing_length do + count = count + 1 + local v = _Debug.order[i] + local x = 5 + local y = (fontSize + 2) * count + local err, index = _Debug.lineInfo(v) --Obtain message and type + local msg = err and _Debug.errors[index] or _Debug.prints[index] + if err then --Add a red and fancy prefix + love.graphics.setColor(255, 0, 0) + love.graphics.print("[Error]", x, y) + x = 50 + end + love.graphics.setColor(255, 255, 255) + love.graphics.print(msg, x, y) + end + love.graphics.setScissor() + love.graphics.print(">", 6, h - 27) + local input_prefix = _Debug.input:sub(1, _Debug.inputMarker) + local input_prefix_width = _Debug.Font:getWidth(input_prefix) + local autocomplete_width = 0 + local input_suffix = _Debug.input:sub(_Debug.inputMarker + 1) + if #_Debug.Proposals > 0 then + autocomplete_width = _Debug.Font:getWidth(_Debug.Proposals[_Debug.proposaltoenter]) + local proposal_prefix_width = _Debug.Font:getWidth(_Debug.Proposal_String) + love.graphics.setColor(127, 127, 127) + love.graphics.print(_Debug.Proposals[_Debug.proposaltoenter], 20 + input_prefix_width, h - 27) + love.graphics.setColor(70, 70, 70) + for i = math.max(_Debug.proposaltoenter - 1, 1), math.min(_Debug.proposaltoenter + 1, #_Debug.Proposals) do + if i ~= _Debug.proposaltoenter then + local index = i - _Debug.proposaltoenter + love.graphics.print(_Debug.Proposal_String .. _Debug.Proposals[i], 20 + input_prefix_width - proposal_prefix_width, h - 27 - (fontSize - 1) * index) + end + end + love.graphics.setColor(255, 255, 255) + end + if _Debug.drawTick then + love.graphics.print("_", 20 + input_prefix_width, h - 27) + end + love.graphics.print(input_prefix, 20, h - 27) + love.graphics.print(input_suffix, 20 + input_prefix_width + autocomplete_width, h - 27) + if (#_Debug.order - _Debug.longestOffset > _Debug.lastRows - 1) then + love.graphics.setFont(_Debug.BigFont) + love.graphics.print("...", w - 30, h - 30) + end + love.graphics.setColor(r, g, b, a) + if font then love.graphics.setFont(font) end + _Debug.lastCut = cutY + _Debug.lastH = h + love.graphics.pop() +end + +--Handle Mousepresses +_Debug.handleMouse = function(a, b, c) + if c == "wd" and _Debug.orderOffset < #_Debug.order - _Debug.lastRows + 1 then + _Debug.orderOffset = _Debug.orderOffset + 1 + if _Debug.orderOffset > _Debug.longestOffset then + _Debug.longestOffset = _Debug.orderOffset + end + end + if c == "wu" and _Debug.orderOffset > 0 then + _Debug.orderOffset = _Debug.orderOffset - 1 + end + if c == "m" and love.keyboard.isDown('lctrl') and _Debug.orderOffset < #_Debug.order - _Debug.lastRows + 1 then + _Debug.orderOffset = #_Debug.order - _Debug.lastRows + 1 + end +end + +--Process Keypresses +_Debug.keyConvert = function(key) + if string.len(key)==1 then + -- No special characters. + _Debug.inputMarker = _Debug.inputMarker + 1 + _Debug.tick = 0 + _Debug.drawTick = false + return key + elseif key == "left" then + if _Debug.inputMarker > 0 then + _Debug.inputMarker = _Debug.inputMarker - 1 + _Debug.tick = 0 + _Debug.drawTick = false + end + elseif key == "right" then + if _Debug.inputMarker < #_Debug.input then + _Debug.inputMarker = _Debug.inputMarker + 1 + _Debug.tick = 0 + _Debug.drawTick = false + end + elseif key == "up" then + if #_Debug.Proposals > 0 and not love.keyboard.isDown('lshift', 'rshift') then + _Debug.proposaltoenter = _Debug.proposaltoenter % #_Debug.Proposals + 1 + _Debug.resetProposals = false + else + if _Debug.historyIndex > 1 then + if _Debug.historyIndex == #_Debug.history then + _Debug.history[_Debug.historyIndex] = _Debug.input + end + _Debug.historyIndex = _Debug.historyIndex - 1 + _Debug.input = _Debug.history[_Debug.historyIndex] + _Debug.inputMarker = #_Debug.input + _Debug.tick = 0 + _Debug.drawTick = false + end + end + elseif key == "down" then + if #_Debug.Proposals > 0 and not love.keyboard.isDown('lshift', 'rshift') then + _Debug.proposaltoenter = (_Debug.proposaltoenter - 2) % #_Debug.Proposals + 1 + _Debug.resetProposals = false + else + if _Debug.historyIndex < #_Debug.history then + _Debug.historyIndex = _Debug.historyIndex + 1 + _Debug.input = _Debug.history[_Debug.historyIndex] + _Debug.inputMarker = #_Debug.input + _Debug.tick = 0 + _Debug.drawTick = false + end + end + elseif key == "backspace" then + local suffix = _Debug.input:sub(_Debug.inputMarker + 1, #_Debug.input) + if _Debug.inputMarker == 0 then --Keep the input from copying itself + suffix = "" + end + _Debug.input = _Debug.input:sub(1, _Debug.inputMarker - 1) .. suffix + if _Debug.inputMarker > 0 then + _Debug.inputMarker = _Debug.inputMarker - 1 + _Debug.tick = 0 + _Debug.drawTick = false + end + elseif key == 'f5' then + _Debug.liveDo=true + elseif key == "return" then + if _Debug.input == 'clear' then --Clears the console + _Debug.history[#_Debug.history] = _Debug.input + table.insert(_Debug.history, '') + _Debug.historyIndex = #_Debug.history + _Debug.errors = {} + _Debug.prints = {} + _Debug.order = {} + _Debug.orderOffset = 0 + _Debug.longestOffset = 0 + _Debug.lastH = nil + _Debug.lastCut = nil + _Debug.lastRows = 1 + _Debug.input = "" + _Debug.inputMarker = 0 + return + end + local liveflag,prevfile,prevmod + if string.find(_Debug.input,'_DebugSettings.LiveFile') then -- Saving previouse live data if changed. + prevfile = _DebugSettings.LiveFile + prevmod = _Debug.liveLastModified + liveflag=true + end + + --Execute Script + print("> " .. _Debug.input) + _Debug.history[#_Debug.history] = _Debug.input + table.insert(_Debug.history, '') + _Debug.historyIndex = #_Debug.history + + local f, err = loadstring(_Debug.input) + if f then + --f = xpcall(f,_Debug.handleError) + f, err = pcall(f) + end + if not f then + local sindex = 16 + #_Debug.input + if sindex > 63 then + sindex = 67 + end + _Debug.handleError(err) + end + _Debug.input = "" + _Debug.inputMarker = 0 + if _Debug.orderOffset < #_Debug.order - _Debug.lastRows + 1 then + _Debug.orderOffset = #_Debug.order - _Debug.lastRows + 1 + end + _Debug.tick = 0 + _Debug.drawTick = false + -- + + if liveflag then -- Setting up lastModified for the new live file(s) if changed + if type(_DebugSettings.LiveFile) == 'table' then + _Debug.liveLastModified={} + for i = 1, #_DebugSettings.LiveFile do --Setting up lastModified for live files + if not love.filesystem.exists(_DebugSettings.LiveFile[i]) then --if the file's not found then the live variables are reset + _Debug.handleError('_DebugSettings.LiveFile: Index '..i..' file "'.._DebugSettings.LiveFile[i]..'" was not found.') + _DebugSettings.LiveFile = prevfile + _Debug.liveLastModified = prevmod + return + end + _Debug.liveLastModified[i] = love.filesystem.getInfo(_DebugSettings.LiveFile[i]).modtime + end + else + if love.filesystem.exists(_DebugSettings.LiveFile) then + _Debug.liveLastModified = love.filesystem.getInfo(_DebugSettings.LiveFile).modtime + else + _Debug.handleError('_DebugSettings.LiveFile: File "'.._DebugSettings.LiveFile..'" was not found.') + _DebugSettings.LiveFile = prevfile + _Debug.liveLastModified = prevmod + end + end + end + elseif key == "home" then + _Debug.inputMarker = 0 + _Debug.tick = 0 + _Debug.drawTick = false + elseif key == "end" then + _Debug.inputMarker = #_Debug.input + _Debug.tick = 0 + _Debug.drawTick = false + elseif key == "tab" and #_Debug.Proposals > 0 then + _Debug.input = _Debug.input:sub(1, _Debug.inputMarker) .. _Debug.Proposals[_Debug.proposaltoenter] .. _Debug.input:sub(_Debug.inputMarker + 1) + _Debug.inputMarker = _Debug.inputMarker + #_Debug.Proposals[_Debug.proposaltoenter] + _Debug.tick = 0 + _Debug.drawTick = false + else + _Debug.resetProposals = false + end +end + +local _kwList = {'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', + 'function', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', + 'then', 'true', 'until', 'while'} +_Debug.updateProposals = function(Table) + local str = _Debug.Proposal_String + local len = #str + _Debug.Proposals = {} + if Table == _G and str == '' then + return + end + for k, v in pairs(Table) do + if type(k) == 'string' and k:match '[_a-zA-Z][_a-zA-Z0-9]*' then + if k:sub(1, len) == str then + table.insert(_Debug.Proposals, k:sub(len + 1, #k)) + end + end + end + if Table == _G then + for i, kw in pairs(_kwList) do + if kw:sub(1, len) == str then + table.insert(_Debug.Proposals, kw:sub(len + 1, #kw)) + end + end + end + _Debug.proposaltoenter = 2 + if #_Debug.Proposals < 2 then + _Debug.proposaltoenter = 1 + end +end + +_Debug.checkChars = function(str, chars) + for i = 1, #str do + local char = str:sub(i, i) + local match = false + for x = 1, #chars do + local char2 = chars:sub(x, x) + if char == char2 then + match = true + end + end + if match == false then + return false + end + end + return true +end + +_Debug.findLocation = function(str) + local name + local path = {} + local str, dot, lastname = str:match '(.-)%s*([.:]?)%s*([_a-zA-Z]?[_a-zA-Z0-9]*)$' + + while dot ~= '' do + str, dot, name = str:match '(.-)%s*(%.?)%s*([_a-zA-Z][_a-zA-Z0-9]*)$' + if not str then + break + end + path[#path + 1] = name + end + + local curTable = _G + for i = #path, 1, -1 do + curTable = rawget(curTable, path[i]) + if type(curTable) ~= 'table' then + _Debug.ProposalLocation = _G + _Debug.Proposal_String = '' + return + end + end + + _Debug.ProposalLocation = curTable + _Debug.Proposal_String = lastname +end + +--Handle Keypresses +_Debug.handleKey = function(a) + local activekey = love.system.getOS()~='Android' and (_lovedebugpresskey or "f8") or 'menu' + if a == activekey then + if love.keyboard.isDown("lshift", "rshift", "lctrl", "rctrl") then --Support for both Shift and CTRL + _Debug.drawOverlay = not _Debug.drawOverlay --Toggle + end + elseif _Debug.drawOverlay then + if love.keyboard.isDown('lctrl') then + if a:lower()=='v' and #love.system.getClipboardText()>0 then + local clipboard=love.system.getClipboardText() + local text={} + for char in string.gmatch(clipboard,".") do text[#text+1]=char end + _Debug.handleVirtualKey(text) + elseif a:lower()=='c' then + love.system.setClipboardText(_Debug.input) + return + else + _Debug.handleVirtualKey(a) + if not _Debug.trackKeys[a] then + _Debug.trackKeys[a] = { time = _Debug.keyRepeatInterval - _Debug.keyRepeatDelay} + end + end + else + _Debug.handleVirtualKey(a) + if not _Debug.trackKeys[a] then + _Debug.trackKeys[a] = { time = _Debug.keyRepeatInterval - _Debug.keyRepeatDelay} + end + end + end +end + +--Handle Virtual Keypresses +_Debug.handleVirtualKey = function(a) + if type(a) == 'string' then + _Debug.resetProposals = true + local add = _Debug.keyConvert(a) or '' --Needed for backspace, do NOT optimize + local suffix = _Debug.input:sub(_Debug.inputMarker, (#_Debug.input >= _Debug.inputMarker) and #_Debug.input or _Debug.inputMarker + 1) + if _Debug.inputMarker == 0 then --Keep the input from copying itself + suffix = "" + end + _Debug.input = _Debug.input:sub(0, _Debug.inputMarker - 1) .. add .. suffix + if _Debug.resetProposals then + if _Debug.inputMarker == 0 or _Debug.input:sub(_Debug.inputMarker + 1, _Debug.inputMarker + 1):find('[0-9a-zA-Z_]') then + _Debug.ProposalLocation = _G + _Debug.Proposal_String = '' + else + _Debug.findLocation(_Debug.input:sub(1, _Debug.inputMarker)) + end + _Debug.updateProposals(_Debug.ProposalLocation) + end + else + for i=1,#a do + _Debug.resetProposals = true + _Debug.inputMarker = _Debug.inputMarker + 1 + _Debug.tick = 0 + _Debug.drawTick = false + _Debug.handlePast(a[i]) + end + if not _Debug.trackKeys[a] then + _Debug.trackKeys[a] = { time = _Debug.keyRepeatInterval - _Debug.keyRepeatDelay} + end + end +end +_Debug.handlePast = function(add) + local suffix = _Debug.input:sub(_Debug.inputMarker, (#_Debug.input >= _Debug.inputMarker) and #_Debug.input or _Debug.inputMarker + 1) + if _Debug.inputMarker == 0 then --Keep the input from copying itself + suffix = "" + end + _Debug.input = _Debug.input:sub(0, _Debug.inputMarker - 1) .. add .. suffix + if _Debug.resetProposals then + if _Debug.inputMarker == 0 or _Debug.input:sub(_Debug.inputMarker + 1, _Debug.inputMarker + 1):find('[0-9a-zA-Z_]') then + _Debug.ProposalLocation = _G + _Debug.Proposal_String = '' + else + _Debug.findLocation(_Debug.input:sub(1, _Debug.inputMarker)) + end + _Debug.updateProposals(_Debug.ProposalLocation) + end +end + +--Reloads the Code, update() and load() +_Debug.hotSwapUpdate = function(dt,file) + local file = file or _DebugSettings.LiveFile + local output, ok, err, loadok, updateok + local success, chunk = pcall(love.filesystem.load, file) + if not success then + _Debug.handleError(tostring(chunk)) + output = chunk .. '\n' + end + ok,err = xpcall(chunk, _Debug.handleError) + + if ok then + print("'"..file.."' Reloaded.") + else + print('Something went wrong while trying to update file: '..file) + end + if _Debug.orderOffset < #_Debug.order - _Debug.lastRows + 1 then + _Debug.orderOffset = #_Debug.order - _Debug.lastRows + 1 + end + + if file == 'main' then --so it only updates love.update() once + updateok,err=pcall(love.update,dt) + end +end +--Reloads the code, love.load() +_Debug.hotSwapLoad = function() + local loadok,err=xpcall(love.load,_Debug.handleError) + if loadok then + print("'love.load()' Reloaded.") + end + if _Debug.orderOffset < #_Debug.order - _Debug.lastRows + 1 then + _Debug.orderOffset = #_Debug.order - _Debug.lastRows + 1 + end +end +--Reloads the code, draw(), I don't think this is needed.. +_Debug.hotSwapDraw = function() + local drawok,err + drawok,err = xpcall(love.draw,_Debug.handleError) +end +_Debug.liveCheckLastModified = function(table1,table2) + if type(table1) == 'string' then + if love.filesystem.getInfo(table1).modtime ~= table2 then + return true + end + return false + end + + for i,v in ipairs(table1) do + if love.filesystem.getInfo(v).modtime ~= table2[i] then + return true + end + end + return false +end + + + +--Modded version of original love.run +_G["love"].run = function() + if love.math then + love.math.setRandomSeed(os.time()) + end + + if love.event then + love.event.pump() + end + + if love.load then xpcall(love.load, _Debug.handleError) end + + if love.timer then love.timer.step() end + + local dt = 0 + + + -- Main loop time. + while true do + + -- Process events. + if love.event then + love.event.pump() + for name, a,b,c,d,e,f in love.event.poll() do + if name == "quit" then + local quit = false + --if love.quit then + -- xpcall(function() quit = love.quit() end, _Debug.handleError) + --end + if not love.quit or not love.quit() then + return a + end + end + local skipEvent = false + if name == "textinput" then --Keypress + skipEvent = true + _Debug.handleKey(a) + if not _Debug.drawOverlay then + if love.textinput then love.textinput(a) end + end + end + if name == "keypressed" then --Keypress + skipEvent = true + + if string.len(a)>=2 or (love.keyboard.isDown('lctrl') and (a == 'c' or a == 'v')) then _Debug.handleKey(a) end + if not _Debug.drawOverlay then + if love.keypressed then love.keypressed(a,b) end + end + end + if name == "keyreleased" then --Keyrelease + skipEvent = true + if not _Debug.drawOverlay then + if love.keyreleased then love.keyreleased(a, b) end + end + end + if name == "mousepressed" and _Debug.drawOverlay then --Mousepress + skipEvent = true + _Debug.handleMouse(a, b, c) + end + if not skipEvent then + xpcall(function() love.handlers[name](a,b,c,d,e,f) end, _Debug.handleError) + end + end + end + if love.timer then + love.timer.step() + dt = love.timer.getDelta() + _Debug.onTopUpdate(dt) + end + _Debug.tick = _Debug.tick - dt + if _Debug.tick <= 0 then + _Debug.tick = _Debug.tickTime + _Debug.tick + _Debug.drawTick = not _Debug.drawTick + end + if _Debug.drawOverlay then + for key, d in pairs(_Debug.trackKeys) do + if type(key) == 'string' then + if love.keyboard.isDown(key) then + d.time = d.time + dt + if d.time >= _Debug.keyRepeatInterval then + d.time = 0 + _Debug.handleVirtualKey(key) + end + else + _Debug.trackKeys[key] = nil + end + else + if love.keyboard.isDown('v') and love.keyboard.isDown('lctrl') then + d.time = d.time + dt + if d.time >= _Debug.keyRepeatInterval then + d.time = 0 + _Debug.handleVirtualKey(key) + end + else + _Debug.trackKeys[key] = nil + end + end + end + + -- Call love.update() if we are not to halt program execution + if _DebugSettings.HaltExecution == false then + xpcall(function() love.update(dt) end, _Debug.handleError) + end + + -- Auto scroll the console if AutoScroll == true + if _DebugSettings.AutoScroll == true then + if _Debug.orderOffset < #_Debug.order - _Debug.lastRows + 1 then + _Debug.orderOffset = #_Debug.order - _Debug.lastRows + 1 + end + end + end + + if love.update and not _Debug.drawOverlay then + if _DebugSettings.LiveAuto and _Debug.liveCheckLastModified(_DebugSettings.LiveFile,_Debug.liveLastModified) then + if type(_DebugSettings.LiveFile) == 'table' then + for i=1,#_DebugSettings.LiveFile do + if love.filesystem.getInfo(_DebugSettings.LiveFile[i]).modtime ~= _Debug.liveLastModified[i] then + _Debug.hotSwapUpdate(dt,_DebugSettings.LiveFile[i]) + _Debug.liveLastModified[i] = love.filesystem.getInfo(_DebugSettings.LiveFile[i]).modtime + end + end + if _DebugSettings.LiveReset then + _Debug.hotSwapLoad() + end + else + _Debug.hotSwapUpdate(dt,_DebugSettings.LiveFile) + _Debug.liveLastModified = love.filesystem.getInfo(_DebugSettings.LiveFile).modtime + if _DebugSettings.LiveReset then + _Debug.hotSwapLoad() + end + end + else + xpcall(function() love.update(dt) end, _Debug.handleError) + end + elseif love.update and (_Debug.liveDo or (_DebugSettings.LiveAuto and _Debug.liveCheckLastModified(_DebugSettings.LiveFile,_Debug.liveLastModified))) then + if type(_DebugSettings.LiveFile) == 'table' then + for i=1,#_DebugSettings.LiveFile do + if (_DebugSettings.LiveAuto and love.filesystem.getInfo(_DebugSettings.LiveFile[i]).modtime ~= _Debug.liveLastModified[i]) or _Debug.liveDo then + _Debug.hotSwapUpdate(dt,_DebugSettings.LiveFile[i]) + _Debug.liveLastModified[i] = love.filesystem.getInfo(_DebugSettings.LiveFile[i]).modtime + end + end + if _DebugSettings.LiveReset then + _Debug.hotSwapLoad() + end + else + _Debug.hotSwapUpdate(dt,_DebugSettings.LiveFile) + if _DebugSettings.LiveReset then + _Debug.hotSwapLoad() + end + _Debug.liveLastModified = love.filesystem.getInfo(_DebugSettings.LiveFile).modtime + end + end -- will pass 0 if love.timer is disabled + if love.graphics and love.graphics.isActive() then + love.graphics.clear(love.graphics.getBackgroundColor()) + love.graphics.origin() + if love.draw then if _Debug.liveDo then _Debug.hotSwapDraw() _Debug.liveDo=false end xpcall(love.draw, _Debug.handleError) end + if _DebugSettings.DrawOnTop then _Debug.onTop() end + if _Debug.drawOverlay then _Debug.overlay() end + love.graphics.present() + end + + + + if love.timer then love.timer.sleep(0.001) end + + end + +end + diff --git a/libs/lume b/libs/lume new file mode 160000 index 0000000..59f9093 --- /dev/null +++ b/libs/lume @@ -0,0 +1 @@ +Subproject commit 59f90934aa648d3dfd11bf6b7421b15df9b44f40 diff --git a/libs/lume.lua b/libs/lume.lua new file mode 100644 index 0000000..2a5bc9d --- /dev/null +++ b/libs/lume.lua @@ -0,0 +1,769 @@ +-- +-- lume +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local lume = { _version = "2.2.2" } + +local pairs, ipairs = pairs, ipairs +local type, assert, unpack = type, assert, unpack or table.unpack +local tostring, tonumber = tostring, tonumber +local math_floor = math.floor +local math_ceil = math.ceil +local math_random = math.random +local math_cos = math.cos +local math_atan2 = math.atan2 or math.atan +local math_sqrt = math.sqrt +local math_abs = math.abs +local math_pi = math.pi + +local noop = function() +end + +local identity = function(x) + return x +end + +local patternescape = function(str) + return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1") +end + +local absindex = function(len, i) + return i < 0 and (len + i + 1) or i +end + +local iscallable = function(x) + if type(x) == "function" then return true end + local mt = getmetatable(x) + return mt and mt.__call ~= nil +end + +local isarray = function(x) + return (type(x) == "table" and x[1] ~= nil) and true or false +end + +local getiter = function(x) + if isarray(x) then + return ipairs + elseif type(x) == "table" then + return pairs + end + error("expected table", 3) +end + +local iteratee = function(x) + if x == nil then return identity end + if iscallable(x) then return x end + if type(x) == "table" then + return function(z) + for k, v in pairs(x) do + if z[k] ~= v then return false end + end + return true + end + end + return function(z) return z[x] end +end + + + +function lume.clamp(x, min, max) + return x < min and min or (x > max and max or x) +end + + +function lume.round(x, increment) + if increment then return lume.round(x / increment) * increment end + return x >= 0 and math_floor(x + .5) or math_ceil(x - .5) +end + + +function lume.sign(x) + return x < 0 and -1 or 1 +end + + +function lume.lerp(a, b, amount) + return a + (b - a) * lume.clamp(amount, 0, 1) +end + + +function lume.smooth(a, b, amount) + local t = lume.clamp(amount, 0, 1) + local m = t * t * (3 - 2 * t) + return a + (b - a) * m +end + + +function lume.pingpong(x) + return 1 - math_abs(1 - x % 2) +end + + +function lume.distance(x1, y1, x2, y2, squared) + local dx = x1 - x2 + local dy = y1 - y2 + local s = dx * dx + dy * dy + return squared and s or math_sqrt(s) +end + + +function lume.angle(x1, y1, x2, y2) + return math_atan2(y2 - y1, x2 - x1) +end + + +function lume.random(a, b) + if not a then a, b = 0, 1 end + if not b then b = 0 end + return a + math_random() * (b - a) +end + + +function lume.randomchoice(t) + return t[math_random(#t)] +end + + +function lume.weightedchoice(t) + local sum = 0 + for k, v in pairs(t) do + assert(v >= 0, "weight value less than zero") + sum = sum + v + end + assert(sum ~= 0, "all weights are zero") + local rnd = lume.random(sum) + for k, v in pairs(t) do + if rnd < v then return k end + rnd = rnd - v + end +end + + +function lume.push(t, ...) + local n = select("#", ...) + for i = 1, n do + t[#t + 1] = select(i, ...) + end + return ... +end + + +function lume.remove(t, x) + local iter = getiter(t) + for i, v in iter(t) do + if v == x then + if isarray(t) then + table.remove(t, i) + break + else + t[i] = nil + break + end + end + end + return x +end + + +function lume.clear(t) + local iter = getiter(t) + for k, v in iter(t) do + t[k] = nil + end + return t +end + + +function lume.extend(t, ...) + for i = 1, select("#", ...) do + local x = select(i, ...) + if x then + for k, v in pairs(x) do + t[k] = v + end + end + end + return t +end + + +function lume.shuffle(t) + local rtn = {} + for i = 1, #t do + local r = math_random(i) + if r ~= i then + rtn[i] = rtn[r] + end + rtn[r] = t[i] + end + return rtn +end + + +function lume.sort(t, comp) + local rtn = lume.clone(t) + if comp then + if type(comp) == "string" then + table.sort(rtn, function(a, b) return a[comp] < b[comp] end) + else + table.sort(rtn, comp) + end + else + table.sort(rtn) + end + return rtn +end + + +function lume.array(...) + local t = {} + for x in ... do t[#t + 1] = x end + return t +end + + +function lume.each(t, fn, ...) + local iter = getiter(t) + if type(fn) == "string" then + for _, v in iter(t) do v[fn](v, ...) end + else + for _, v in iter(t) do fn(v, ...) end + end + return t +end + + +function lume.map(t, fn) + fn = iteratee(fn) + local iter = getiter(t) + local rtn = {} + for k, v in iter(t) do rtn[k] = fn(v) end + return rtn +end + + +function lume.all(t, fn) + fn = iteratee(fn) + local iter = getiter(t) + for k, v in iter(t) do + if not fn(v) then return false end + end + return true +end + + +function lume.any(t, fn) + fn = iteratee(fn) + local iter = getiter(t) + for k, v in iter(t) do + if fn(v) then return true end + end + return false +end + + +function lume.reduce(t, fn, first) + local acc = first + local started = first and true or false + local iter = getiter(t) + for k, v in iter(t) do + if started then + acc = fn(acc, v) + else + acc = v + started = true + end + end + assert(started, "reduce of an empty table with no first value") + return acc +end + + +function lume.set(t) + local rtn = {} + for k, v in pairs(lume.invert(t)) do + rtn[#rtn + 1] = k + end + return rtn +end + + +function lume.filter(t, fn, retainkeys) + fn = iteratee(fn) + local iter = getiter(t) + local rtn = {} + if retainkeys then + for k, v in iter(t) do + if fn(v) then rtn[k] = v end + end + else + for k, v in iter(t) do + if fn(v) then rtn[#rtn + 1] = v end + end + end + return rtn +end + + +function lume.reject(t, fn, retainkeys) + fn = iteratee(fn) + local iter = getiter(t) + local rtn = {} + if retainkeys then + for k, v in iter(t) do + if not fn(v) then rtn[k] = v end + end + else + for k, v in iter(t) do + if not fn(v) then rtn[#rtn + 1] = v end + end + end + return rtn +end + + +function lume.merge(...) + local rtn = {} + for i = 1, select("#", ...) do + local t = select(i, ...) + local iter = getiter(t) + for k, v in iter(t) do + rtn[k] = v + end + end + return rtn +end + + +function lume.concat(...) + local rtn = {} + for i = 1, select("#", ...) do + local t = select(i, ...) + if t ~= nil then + local iter = getiter(t) + for k, v in iter(t) do + rtn[#rtn + 1] = v + end + end + end + return rtn +end + + +function lume.find(t, value) + local iter = getiter(t) + for k, v in iter(t) do + if v == value then return k end + end + return nil +end + + +function lume.match(t, fn) + fn = iteratee(fn) + local iter = getiter(t) + for k, v in iter(t) do + if fn(v) then return v, k end + end + return nil +end + + +function lume.count(t, fn) + local count = 0 + local iter = getiter(t) + if fn then + fn = iteratee(fn) + for k, v in iter(t) do + if fn(v) then count = count + 1 end + end + else + if isarray(t) then + return #t + end + for k in iter(t) do count = count + 1 end + end + return count +end + + +function lume.slice(t, i, j) + i = i and absindex(#t, i) or 1 + j = j and absindex(#t, j) or #t + local rtn = {} + for x = i < 1 and 1 or i, j > #t and #t or j do + rtn[#rtn + 1] = t[x] + end + return rtn +end + + +function lume.first(t, n) + if not n then return t[1] end + return lume.slice(t, 1, n) +end + + +function lume.last(t, n) + if not n then return t[#t] end + return lume.slice(t, -n, -1) +end + + +function lume.invert(t) + local rtn = {} + for k, v in pairs(t) do rtn[v] = k end + return rtn +end + + +function lume.pick(t, ...) + local rtn = {} + for i = 1, select("#", ...) do + local k = select(i, ...) + rtn[k] = t[k] + end + return rtn +end + + +function lume.keys(t) + local rtn = {} + local iter = getiter(t) + for k, v in iter(t) do rtn[#rtn + 1] = k end + return rtn +end + + +function lume.clone(t) + local rtn = {} + for k, v in pairs(t) do rtn[k] = v end + return rtn +end + + +function lume.fn(fn, ...) + assert(iscallable(fn), "expected a function as the first argument") + local args = { ... } + return function(...) + local a = lume.concat(args, { ... }) + return fn(unpack(a)) + end +end + + +function lume.once(fn, ...) + local fn = lume.fn(fn, ...) + local done = false + return function(...) + if done then return end + done = true + return fn(...) + end +end + + +local memoize_fnkey = {} +local memoize_nil = {} + +function lume.memoize(fn) + local cache = {} + return function(...) + local c = cache + for i = 1, select("#", ...) do + local a = select(i, ...) or memoize_nil + c[a] = c[a] or {} + c = c[a] + end + c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)} + return unpack(c[memoize_fnkey]) + end +end + + +function lume.combine(...) + local n = select('#', ...) + if n == 0 then return noop end + if n == 1 then + local fn = select(1, ...) + if not fn then return noop end + assert(iscallable(fn), "expected a function or nil") + return fn + end + local funcs = {} + for i = 1, n do + local fn = select(i, ...) + if fn ~= nil then + assert(iscallable(fn), "expected a function or nil") + funcs[#funcs + 1] = fn + end + end + return function(...) + for _, f in ipairs(funcs) do f(...) end + end +end + + +function lume.call(fn, ...) + if fn then + return fn(...) + end +end + + +function lume.time(fn, ...) + local start = os.clock() + local rtn = {fn(...)} + return (os.clock() - start), unpack(rtn) +end + + +local lambda_cache = {} + +function lume.lambda(str) + if not lambda_cache[str] then + local args, body = str:match([[^([%w,_ ]-)%->(.-)$]]) + assert(args and body, "bad string lambda") + local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend" + lambda_cache[str] = lume.dostring(s) + end + return lambda_cache[str] +end + + +local serialize + +local serialize_map = { + [ "boolean" ] = tostring, + [ "nil" ] = tostring, + [ "string" ] = function(v) return string.format("%q", v) end, + [ "number" ] = function(v) + if v ~= v then return "0/0" -- nan + elseif v == 1 / 0 then return "1/0" -- inf + elseif v == -1 / 0 then return "-1/0" end -- -inf + return tostring(v) + end, + [ "table" ] = function(t, stk) + stk = stk or {} + if stk[t] then error("circular reference") end + local rtn = {} + stk[t] = true + for k, v in pairs(t) do + rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk) + end + stk[t] = nil + return "{" .. table.concat(rtn, ",") .. "}" + end +} + +setmetatable(serialize_map, { + __index = function(t, k) error("unsupported serialize type: " .. k) end +}) + +serialize = function(x, stk) + return serialize_map[type(x)](x, stk) +end + +function lume.serialize(x) + return serialize(x) +end + + +function lume.deserialize(str) + return lume.dostring("return " .. str) +end + + +function lume.split(str, sep) + if not sep then + return lume.array(str:gmatch("([%S]+)")) + else + assert(sep ~= "", "empty separator") + local psep = patternescape(sep) + return lume.array((str..sep):gmatch("(.-)("..psep..")")) + end +end + + +function lume.trim(str, chars) + if not chars then return str:match("^[%s]*(.-)[%s]*$") end + chars = patternescape(chars) + return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$") +end + + +function lume.wordwrap(str, limit) + limit = limit or 72 + local check + if type(limit) == "number" then + check = function(str) return #str >= limit end + else + check = limit + end + local rtn = {} + local line = "" + for word, spaces in str:gmatch("(%S+)(%s*)") do + local str = line .. word + if check(str) then + table.insert(rtn, line .. "\n") + line = word + else + line = str + end + for c in spaces:gmatch(".") do + if c == "\n" then + table.insert(rtn, line .. "\n") + line = "" + else + line = line .. c + end + end + end + table.insert(rtn, line) + return table.concat(rtn) +end + + +function lume.format(str, vars) + if not vars then return str end + local f = function(x) + return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}") + end + return (str:gsub("{(.-)}", f)) +end + + +function lume.trace(...) + local info = debug.getinfo(2, "Sl") + local t = { info.short_src .. ":" .. info.currentline .. ":" } + for i = 1, select("#", ...) do + local x = select(i, ...) + if type(x) == "number" then + x = string.format("%g", lume.round(x, .01)) + end + t[#t + 1] = tostring(x) + end + print(table.concat(t, " ")) +end + + +function lume.dostring(str) + return assert((loadstring or load)(str))() +end + + +function lume.uuid() + local fn = function(x) + local r = math_random(16) - 1 + r = (x == "x") and (r + 1) or (r % 4) + 9 + return ("0123456789abcdef"):sub(r, r) + end + return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn)) +end + + +function lume.hotswap(modname) + local oldglobal = lume.clone(_G) + local updated = {} + local function update(old, new) + if updated[old] then return end + updated[old] = true + local oldmt, newmt = getmetatable(old), getmetatable(new) + if oldmt and newmt then update(oldmt, newmt) end + for k, v in pairs(new) do + if type(v) == "table" then update(old[k], v) else old[k] = v end + end + end + local err = nil + local function onerror(e) + for k, v in pairs(_G) do _G[k] = oldglobal[k] end + err = lume.trim(e) + end + local ok, oldmod = pcall(require, modname) + oldmod = ok and oldmod or nil + xpcall(function() + package.loaded[modname] = nil + local newmod = require(modname) + if type(oldmod) == "table" then update(oldmod, newmod) end + for k, v in pairs(oldglobal) do + if v ~= _G[k] and type(v) == "table" then + update(v, _G[k]) + _G[k] = v + end + end + end, onerror) + package.loaded[modname] = oldmod + if err then return nil, err end + return oldmod +end + + +local ripairs_iter = function(t, i) + i = i - 1 + local v = t[i] + if v then return i, v end +end + +function lume.ripairs(t) + return ripairs_iter, t, (#t + 1) +end + + +function lume.color(str, mul) + mul = mul or 1 + local r, g, b, a + r, g, b = str:match("#(%x%x)(%x%x)(%x%x)") + if r then + r = tonumber(r, 16) / 0xff + g = tonumber(g, 16) / 0xff + b = tonumber(b, 16) / 0xff + a = 1 + elseif str:match("rgba?%s*%([%d%s%.,]+%)") then + local f = str:gmatch("[%d.]+") + r = (f() or 0) / 0xff + g = (f() or 0) / 0xff + b = (f() or 0) / 0xff + a = f() or 1 + else + error(("bad color string '%s'"):format(str)) + end + return r * mul, g * mul, b * mul, a * mul +end + + +function lume.rgba(color) + local a = math_floor((color / 16777216) % 256) + local r = math_floor((color / 65536) % 256) + local g = math_floor((color / 256) % 256) + local b = math_floor((color) % 256) + return r, g, b, a +end + + +local chain_mt = {} +chain_mt.__index = lume.map(lume.filter(lume, iscallable, true), + function(fn) + return function(self, ...) + self._value = fn(self._value, ...) + return self + end + end) +chain_mt.__index.result = function(x) return x._value end + +function lume.chain(value) + return setmetatable({ _value = value }, chain_mt) +end + +setmetatable(lume, { + __call = function(t, ...) + return lume.chain(...) + end +}) + + +return lume diff --git a/libs/lurker.lua b/libs/lurker.lua new file mode 100644 index 0000000..d798a17 --- /dev/null +++ b/libs/lurker.lua @@ -0,0 +1,266 @@ +-- +-- lurker +-- +-- Copyright (c) 2018 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +-- Assumes lume is in the same directory as this file if it does not exist +-- as a global +local lume = rawget(_G, "lume") or require((...):gsub("[^/.\\]+$", "lume")) + +local lurker = { _version = "1.0.1" } + + +local dir = love.filesystem.enumerate or love.filesystem.getDirectoryItems +local time = love.timer.getTime or os.time +-- deprecated since Love 0.11 +--local isdir = love.filesystem.isDirectory +--local lastmodified = love.filesystem.getLastModified +local isdir = function(item) return love.filesystem.getInfo(item).type == "directory" end --love.filesystem.isDirectory +local lastmodified = function(item) return love.filesystem.getInfo(item).modtime end --love.filesystem.getLastModifiedlocal + +lovecallbacknames = { + "update", + "load", + "draw", + "mousepressed", + "mousereleased", + "keypressed", + "keyreleased", + "focus", + "quit", +} + + +function lurker.init() + lurker.print("Initing lurker") + lurker.path = "." + lurker.preswap = function() end + lurker.postswap = function() end + lurker.interval = .5 + lurker.protected = true + lurker.quiet = false + lurker.lastscan = 0 + lurker.lasterrorfile = nil + lurker.files = {} + lurker.funcwrappers = {} + lurker.lovefuncs = {} + lurker.state = "init" + lume.each(lurker.getchanged(), lurker.resetfile) + return lurker +end + + +function lurker.print(...) + print("[lurker] " .. lume.format(...)) +end + + +function lurker.listdir(path, recursive, skipdotfiles) + path = (path == ".") and "" or path + local function fullpath(x) return path .. "/" .. x end + local t = {} + for _, f in pairs(lume.map(dir(path), fullpath)) do + if not skipdotfiles or not f:match("/%.[^/]*$") then + if recursive and isdir(f) then + t = lume.concat(t, lurker.listdir(f, true, true)) + else + table.insert(t, lume.trim(f, "/")) + end + end + end + return t +end + + +function lurker.initwrappers() + for _, v in pairs(lovecallbacknames) do + lurker.funcwrappers[v] = function(...) + local args = {...} + xpcall(function() + return lurker.lovefuncs[v] and lurker.lovefuncs[v](unpack(args)) + end, lurker.onerror) + end + lurker.lovefuncs[v] = love[v] + end + lurker.updatewrappers() +end + + +function lurker.updatewrappers() + for _, v in pairs(lovecallbacknames) do + if love[v] ~= lurker.funcwrappers[v] then + lurker.lovefuncs[v] = love[v] + love[v] = lurker.funcwrappers[v] + end + end +end + + +function lurker.onerror(e, nostacktrace) + lurker.print("An error occurred; switching to error state") + lurker.state = "error" + + -- Release mouse + local setgrab = love.mouse.setGrab or love.mouse.setGrabbed + setgrab(false) + + -- Set up callbacks + for _, v in pairs(lovecallbacknames) do + love[v] = function() end + end + + love.update = lurker.update + + love.keypressed = function(k) + if k == "escape" then + lurker.print("Exiting...") + love.event.quit() + end + end + + local stacktrace = nostacktrace and "" or + lume.trim((debug.traceback("", 2):gsub("\t", ""))) + local msg = lume.format("{1}\n\n{2}", {e, stacktrace}) + local colors = { + { lume.color("#1e1e2c", 256) }, + { lume.color("#f0a3a3", 256) }, + { lume.color("#92b5b0", 256) }, + { lume.color("#66666a", 256) }, + { lume.color("#cdcdcd", 256) }, + } + love.graphics.reset() + love.graphics.setFont(love.graphics.newFont(12)) + + love.draw = function() + local pad = 25 + local width = love.graphics.getWidth() + + local function drawhr(pos, color1, color2) + local animpos = lume.smooth(pad, width - pad - 8, lume.pingpong(time())) + if color1 then love.graphics.setColor(color1) end + love.graphics.rectangle("fill", pad, pos, width - pad*2, 1) + if color2 then love.graphics.setColor(color2) end + love.graphics.rectangle("fill", animpos, pos, 8, 1) + end + + local function drawtext(str, x, y, color, limit) + love.graphics.setColor(color) + love.graphics[limit and "printf" or "print"](str, x, y, limit) + end + + --love.graphics.setBackgroundColor(colors[1]) + assert("задимЭ") + --love.graphics.clear() + + drawtext("An error has occurred", pad, pad, colors[2]) + drawtext("lurker", width - love.graphics.getFont():getWidth("lurker") - + pad, pad, colors[4]) + drawhr(pad + 32, colors[4], colors[5]) + drawtext("If you fix the problem and update the file the program will " .. + "resume", pad, pad + 46, colors[3]) + drawhr(pad + 72, colors[4], colors[5]) + drawtext(msg, pad, pad + 90, colors[5], width - pad * 2) + + love.graphics.reset() + end +end + + +function lurker.exitinitstate() + lurker.state = "normal" + if lurker.protected then + lurker.initwrappers() + end +end + + +function lurker.exiterrorstate() + lurker.state = "normal" + for _, v in pairs(lovecallbacknames) do + love[v] = lurker.funcwrappers[v] + end +end + + +function lurker.update() + if lurker.state == "init" then + lurker.exitinitstate() + end + local diff = time() - lurker.lastscan + if diff > lurker.interval then + lurker.lastscan = lurker.lastscan + diff + local changed = lurker.scan() + if #changed > 0 and lurker.lasterrorfile then + local f = lurker.lasterrorfile + lurker.lasterrorfile = nil + lurker.hotswapfile(f) + end + end +end + + +function lurker.getchanged() + local function fn(f) + return f:match("%.lua$") and lurker.files[f] ~= lastmodified(f) + end + return lume.filter(lurker.listdir(lurker.path, true, true), fn) +end + + +function lurker.modname(f) + return (f:gsub("%.lua$", ""):gsub("[/\\]", ".")) +end + + +function lurker.resetfile(f) + lurker.files[f] = lastmodified(f) +end + + +function lurker.hotswapfile(f) + lurker.print("Hotswapping '{1}'...", {f}) + if lurker.state == "error" then + lurker.exiterrorstate() + end + if lurker.preswap(f) then + lurker.print("Hotswap of '{1}' aborted by preswap", {f}) + lurker.resetfile(f) + return + end + local modname = lurker.modname(f) + local t, ok, err = lume.time(lume.hotswap, modname) + if ok then + lurker.print("Swapped '{1}' in {2} secs", {f, t}) + else + lurker.print("Failed to swap '{1}' : {2}", {f, err}) + if not lurker.quiet and lurker.protected then + lurker.lasterrorfile = f + lurker.onerror(err, true) + lurker.resetfile(f) + return + end + end + lurker.resetfile(f) + lurker.postswap(f) + if lurker.protected then + lurker.updatewrappers() + end +end + + +function lurker.scan() + if lurker.state == "init" then + lurker.exitinitstate() + end + local changed = lurker.getchanged() + lume.each(changed, lurker.hotswapfile) + return changed +end + + +return lurker.init() + diff --git a/libs/suit b/libs/suit new file mode 160000 index 0000000..f02976d --- /dev/null +++ b/libs/suit @@ -0,0 +1 @@ +Subproject commit f02976d969be61d552bddf21e8f522deec387311 diff --git a/libs/tween.lua b/libs/tween.lua new file mode 100644 index 0000000..7255db0 --- /dev/null +++ b/libs/tween.lua @@ -0,0 +1,369 @@ +local tween = { + _VERSION = 'tween 2.0.0', + _DESCRIPTION = 'tweening for lua', + _URL = 'https://github.com/kikito/tween.lua', + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2014 Enrique García Cota, Yuichi Tateno, Emmanuel Oga + + 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. + ]] +} + +-- easing + +-- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits. +-- For all easing functions: +-- t = time == how much time has to pass for the tweening to complete +-- b = begin == starting property value +-- c = change == ending - beginning +-- d = duration == running time. How much time has passed *right now* + +local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin + +-- linear +local function linear(t, b, c, d) return c * t / d + b end + +-- quad +local function inQuad(t, b, c, d) return c * pow(t / d, 2) + b end +local function outQuad(t, b, c, d) + t = t / d + return -c * t * (t - 2) + b +end +local function inOutQuad(t, b, c, d) + t = t / d * 2 + if t < 1 then return c / 2 * pow(t, 2) + b end + return -c / 2 * ((t - 1) * (t - 3) - 1) + b +end +local function outInQuad(t, b, c, d) + if t < d / 2 then return outQuad(t * 2, b, c / 2, d) end + return inQuad((t * 2) - d, b + c / 2, c / 2, d) +end + +-- cubic +local function inCubic (t, b, c, d) return c * pow(t / d, 3) + b end +local function outCubic(t, b, c, d) return c * (pow(t / d - 1, 3) + 1) + b end +local function inOutCubic(t, b, c, d) + t = t / d * 2 + if t < 1 then return c / 2 * t * t * t + b end + t = t - 2 + return c / 2 * (t * t * t + 2) + b +end +local function outInCubic(t, b, c, d) + if t < d / 2 then return outCubic(t * 2, b, c / 2, d) end + return inCubic((t * 2) - d, b + c / 2, c / 2, d) +end + +-- quart +local function inQuart(t, b, c, d) return c * pow(t / d, 4) + b end +local function outQuart(t, b, c, d) return -c * (pow(t / d - 1, 4) - 1) + b end +local function inOutQuart(t, b, c, d) + t = t / d * 2 + if t < 1 then return c / 2 * pow(t, 4) + b end + return -c / 2 * (pow(t - 2, 4) - 2) + b +end +local function outInQuart(t, b, c, d) + if t < d / 2 then return outQuart(t * 2, b, c / 2, d) end + return inQuart((t * 2) - d, b + c / 2, c / 2, d) +end + +-- quint +local function inQuint(t, b, c, d) return c * pow(t / d, 5) + b end +local function outQuint(t, b, c, d) return c * (pow(t / d - 1, 5) + 1) + b end +local function inOutQuint(t, b, c, d) + t = t / d * 2 + if t < 1 then return c / 2 * pow(t, 5) + b end + return c / 2 * (pow(t - 2, 5) + 2) + b +end +local function outInQuint(t, b, c, d) + if t < d / 2 then return outQuint(t * 2, b, c / 2, d) end + return inQuint((t * 2) - d, b + c / 2, c / 2, d) +end + +-- sine +local function inSine(t, b, c, d) return -c * cos(t / d * (pi / 2)) + c + b end +local function outSine(t, b, c, d) return c * sin(t / d * (pi / 2)) + b end +local function inOutSine(t, b, c, d) return -c / 2 * (cos(pi * t / d) - 1) + b end +local function outInSine(t, b, c, d) + if t < d / 2 then return outSine(t * 2, b, c / 2, d) end + return inSine((t * 2) -d, b + c / 2, c / 2, d) +end + +-- expo +local function inExpo(t, b, c, d) + if t == 0 then return b end + return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001 +end +local function outExpo(t, b, c, d) + if t == d then return b + c end + return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b +end +local function inOutExpo(t, b, c, d) + if t == 0 then return b end + if t == d then return b + c end + t = t / d * 2 + if t < 1 then return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 end + return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b +end +local function outInExpo(t, b, c, d) + if t < d / 2 then return outExpo(t * 2, b, c / 2, d) end + return inExpo((t * 2) - d, b + c / 2, c / 2, d) +end + +-- circ +local function inCirc(t, b, c, d) return(-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) end +local function outCirc(t, b, c, d) return(c * sqrt(1 - pow(t / d - 1, 2)) + b) end +local function inOutCirc(t, b, c, d) + t = t / d * 2 + if t < 1 then return -c / 2 * (sqrt(1 - t * t) - 1) + b end + t = t - 2 + return c / 2 * (sqrt(1 - t * t) + 1) + b +end +local function outInCirc(t, b, c, d) + if t < d / 2 then return outCirc(t * 2, b, c / 2, d) end + return inCirc((t * 2) - d, b + c / 2, c / 2, d) +end + +-- elastic +local function calculatePAS(p,a,c,d) + p, a = p or d * 0.3, a or 0 + if a < abs(c) then return p, c, p / 4 end -- p, a, s + return p, a, p / (2 * pi) * asin(c/a) -- p,a,s +end +local function inElastic(t, b, c, d, a, p) + local s + if t == 0 then return b end + t = t / d + if t == 1 then return b + c end + p,a,s = calculatePAS(p,a,c,d) + t = t - 1 + return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b +end +local function outElastic(t, b, c, d, a, p) + local s + if t == 0 then return b end + t = t / d + if t == 1 then return b + c end + p,a,s = calculatePAS(p,a,c,d) + return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b +end +local function inOutElastic(t, b, c, d, a, p) + local s + if t == 0 then return b end + t = t / d * 2 + if t == 2 then return b + c end + p,a,s = calculatePAS(p,a,c,d) + t = t - 1 + if t < 0 then return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b end + return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p ) * 0.5 + c + b +end +local function outInElastic(t, b, c, d, a, p) + if t < d / 2 then return outElastic(t * 2, b, c / 2, d, a, p) end + return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p) +end + +-- back +local function inBack(t, b, c, d, s) + s = s or 1.70158 + t = t / d + return c * t * t * ((s + 1) * t - s) + b +end +local function outBack(t, b, c, d, s) + s = s or 1.70158 + t = t / d - 1 + return c * (t * t * ((s + 1) * t + s) + 1) + b +end +local function inOutBack(t, b, c, d, s) + s = (s or 1.70158) * 1.525 + t = t / d * 2 + if t < 1 then return c / 2 * (t * t * ((s + 1) * t - s)) + b end + t = t - 2 + return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b +end +local function outInBack(t, b, c, d, s) + if t < d / 2 then return outBack(t * 2, b, c / 2, d, s) end + return inBack((t * 2) - d, b + c / 2, c / 2, d, s) +end + +-- bounce +local function outBounce(t, b, c, d) + t = t / d + if t < 1 / 2.75 then return c * (7.5625 * t * t) + b end + if t < 2 / 2.75 then + t = t - (1.5 / 2.75) + return c * (7.5625 * t * t + 0.75) + b + elseif t < 2.5 / 2.75 then + t = t - (2.25 / 2.75) + return c * (7.5625 * t * t + 0.9375) + b + end + t = t - (2.625 / 2.75) + return c * (7.5625 * t * t + 0.984375) + b +end +local function inBounce(t, b, c, d) return c - outBounce(d - t, 0, c, d) + b end +local function inOutBounce(t, b, c, d) + if t < d / 2 then return inBounce(t * 2, 0, c, d) * 0.5 + b end + return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b +end +local function outInBounce(t, b, c, d) + if t < d / 2 then return outBounce(t * 2, b, c / 2, d) end + return inBounce((t * 2) - d, b + c / 2, c / 2, d) +end + +tween.easing = { + linear = linear, + inQuad = inQuad, outQuad = outQuad, inOutQuad = inOutQuad, outInQuad = outInQuad, + inCubic = inCubic, outCubic = outCubic, inOutCubic = inOutCubic, outInCubic = outInCubic, + inQuart = inQuart, outQuart = outQuart, inOutQuart = inOutQuart, outInQuart = outInQuart, + inQuint = inQuint, outQuint = outQuint, inOutQuint = inOutQuint, outInQuint = outInQuint, + inSine = inSine, outSine = outSine, inOutSine = inOutSine, outInSine = outInSine, + inExpo = inExpo, outExpo = outExpo, inOutExpo = inOutExpo, outInExpo = outInExpo, + inCirc = inCirc, outCirc = outCirc, inOutCirc = inOutCirc, outInCirc = outInCirc, + inElastic = inElastic, outElastic = outElastic, inOutElastic = inOutElastic, outInElastic = outInElastic, + inBack = inBack, outBack = outBack, inOutBack = inOutBack, outInBack = outInBack, + inBounce = inBounce, outBounce = outBounce, inOutBounce = inOutBounce, outInBounce = outInBounce +} + + + +-- private stuff + +local function copyTables(destination, keysTable, valuesTable) + valuesTable = valuesTable or keysTable + local mt = getmetatable(keysTable) + if mt and getmetatable(destination) == nil then + setmetatable(destination, mt) + end + for k,v in pairs(keysTable) do + if type(v) == 'table' then + destination[k] = copyTables({}, v, valuesTable[k]) + else + destination[k] = valuesTable[k] + end + end + return destination +end + +local function checkSubjectAndTargetRecursively(subject, target, path) + path = path or {} + local targetType, newPath + for k,targetValue in pairs(target) do + targetType, newPath = type(targetValue), copyTables({}, path) + table.insert(newPath, tostring(k)) + if targetType == 'number' then + assert(type(subject[k]) == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' is missing from subject or isn't a number") + elseif targetType == 'table' then + checkSubjectAndTargetRecursively(subject[k], targetValue, newPath) + else + assert(targetType == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' must be a number or table of numbers") + end + end +end + +local function checkNewParams(duration, subject, target, easing) + assert(type(duration) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration)) + local tsubject = type(subject) + assert(tsubject == 'table' or tsubject == 'userdata', "subject must be a table or userdata. Was " .. tostring(subject)) + assert(type(target)== 'table', "target must be a table. Was " .. tostring(target)) + assert(type(easing)=='function', "easing must be a function. Was " .. tostring(easing)) + checkSubjectAndTargetRecursively(subject, target) +end + +local function getEasingFunction(easing) + easing = easing or "linear" + if type(easing) == 'string' then + local name = easing + easing = tween.easing[name] + if type(easing) ~= 'function' then + error("The easing function name '" .. name .. "' is invalid") + end + end + return easing +end + +local function performEasingOnSubject(subject, target, initial, clock, duration, easing) + local t,b,c,d + for k,v in pairs(target) do + if type(v) == 'table' then + performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing) + else + t,b,c,d = clock, initial[k], v - initial[k], duration + subject[k] = easing(t,b,c,d) + end + end +end + +-- Tween methods + +local Tween = {} +local Tween_mt = {__index = Tween} + +function Tween:set(clock) + assert(type(clock) == 'number', "clock must be a positive number or 0") + + self.clock = clock + + if self.clock <= 0 then + + self.clock = 0 + copyTables(self.subject, self.initial) + + elseif self.clock >= self.duration then -- the tween has expired + + self.clock = self.duration + copyTables(self.subject, self.target) + + else + + performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing) + + end + + return self.clock >= self.duration +end + +function Tween:reset() + return self:set(0) +end + +function Tween:update(dt) + assert(type(dt) == 'number', "dt must be a number") + return self:set(self.clock + dt) +end + + +-- Public interface + +function tween.new(duration, subject, target, easing) + easing = getEasingFunction(easing) + checkNewParams(duration, subject, target, easing) + return setmetatable({ + duration = duration, + subject = subject, + target = target, + easing = easing, + + initial = copyTables({},target,subject), + clock = 0 + }, Tween_mt) +end + +return tween + diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..997e6fa --- /dev/null +++ b/main.lua @@ -0,0 +1,119 @@ +require("common") + +local lovebird = require "libs.lovebird" +local pallete = require "pallete" +local inspect = require "libs.inspect" +local lume = require "libs.lume" +local lurker = require "libs.lurker" +--local ldebug = require "libs.lovedebug" +local dbg = require "dbg" + +states = { + a = {} +} + +function states.push(s) + local prev = states.top() + if prev and prev.leave then prev.leave() end + if s.enter then s.enter() end + states.a[#states.a + 1] = s +end + +function states.pop() + local last = states.a[#states.a] + if last.leave then last.leave() end + states.a[#states.a] = nil +end + +function states.top() + return states.a[#states.a] +end + +local help = require "help" +local menu = require "menu" +local nback = require "nback" +local pviewer = require "pviewer" +local colorpicker = require "colorpicker" + +local picker = nil +local to_resize = {} + +function love.load() + lovebird.update() + lovebird.maxlines = 500 + + love.window.setTitle("nback") + + menu.init() + nback.init() + pviewer.init() + help.init() + + to_resize[#to_resize + 1] = menu + to_resize[#to_resize + 1] = nback + to_resize[#to_resize + 1] = pviewer + to_resize[#to_resize + 1] = help + + states.push(menu) +end + +function love.update(dt) + if picker then + picker:update(dt) + end + + lovebird.update() + states.top().update(dt) +end + +function love.resize(w, h) + for k, v in pairs(to_resize) do + if v["resize"] then v.resize(w, h) end + end +end + +function make_screenshot() + local i = 0 + local fname + repeat + i = i + 1 + fname = love.filesystem.getInfo("screenshot" .. i .. ".png") + until not fname + love.graphics.captureScreenshot("screenshot" .. i .. ".png") +end + +function love.keypressed(key) + print(key) + if love.keyboard.isDown("ralt", "lalt") and key == "return" then + love.window.setFullscreen(not love.window.getFullscreen()) + elseif key == "`" then + lurker.scan() + elseif key == "f12" then make_screenshot() + elseif key == "1" then + if not picker then + picker = colorpicker:new() + else + picker = nil + print("picker deleted") + end + elseif key == "2" then + dbg.show = not dbg.show + else + states.top().keypressed(key) + end +end + +function love.mousepressed(x, y, button, istouch) + if picker then + picker:mousepressed(x, y, button, istouch) + end +end + +function love.draw() + local dr_func = states.top().draw or function() end + if picker then + picker:draw(dr_func) + else + dr_func() + end +end diff --git a/menu.lua b/menu.lua new file mode 100644 index 0000000..55811dc --- /dev/null +++ b/menu.lua @@ -0,0 +1,117 @@ +local inspect = require "libs.inspect" +local pviewer = require "pviewer" +local nback = require "nback" +local help = require "help" +local pallete = require "pallete" + +local g = love.graphics +local w, h = g.getDimensions() +local tile_size = 256 + +local menu = { + active_item = 1, + font = love.graphics.newFont("gfx/DejaVuSansMono.ttf", 72), + back_tile = love.graphics.newImage("gfx/IMG_20190111_115755.png") +} + +function menu.init() + menu.items = {"play", "view progress", "help", "quit"} + menu.actions = { + function() states.push(nback) end, + function() states.push(pviewer) end, + function() states.push(help) end, + function() love.event.quit() end, + } + math.randomseed(os.time()) +end + +function menu.resize(neww, newh) + w = neww + h = newh + print("Menu resized!") + menu.calc_rotation_grid() +end + +function menu.calc_rotation_grid() + menu.rot_grid = {} + local i, j = 0, 0 + while i < w do + j = 0 + while j < h do + local v = math.random() + local angle = 0 + if 0 <= v and v <= 0.25 then angle = 0 + elseif 0.25 < v and v < 0.5 then angle = math.pi + elseif 0.5 < v and v < 0.75 then angle = math.pi * 3 / 4 + elseif 0.75 < v and v <= 1 then angle = math.pi * 2 end + menu.rot_grid[#menu.rot_grid + 1] = angle + j = j + tile_size + end + i = i + tile_size + end +end + +function menu.enter() + print("menu.enter()") + menu.calc_rotation_grid() +end + +function menu.keypressed(key) + if key == "up" then + if menu.active_item - 1 >= 1 then + menu.active_item = menu.active_item - 1 + else + menu.active_item = #menu.actions + end + elseif key == "down" then + if menu.active_item + 1 <= #menu.items then + menu.active_item = menu.active_item + 1 + else + menu.active_item = 1 + end + elseif key == "escape" then love.event.quit() + elseif key == "return" or key == "space" then menu.actions[menu.active_item]() + end +end + +function menu.update() end + +function menu.draw() + g.clear(pallete.background) + --print("back_tile:width() = ", menu.back_tile:getWidth()) + --local quad = g.newQuad(0, 0, tile_size, tile_size, menu.back_tile:getWidth(), menu.back_tile:getHeight()) + local quad = g.newQuad(0, 0, menu.back_tile:getWidth(), menu.back_tile:getHeight(), menu.back_tile:getWidth(), menu.back_tile:getHeight()) + local i, j = 0, 0 + local l = 1 + --print("menu.rot_grid = ", inspect(menu.rot_grid)) + while i < w do + j = 0 + while j < h do + --print("angle = ", menu.rot_grid[l]) + g.draw(menu.back_tile, quad, i, j, menu.rot_grid[l], tile_size / menu.back_tile:getWidth(), tile_size / menu.back_tile:getHeight(), + menu.back_tile:getWidth() / 2, menu.back_tile:getHeight() / 2) + --g.draw(menu.back_tile, quad, i, j, math.pi, 0.3, 0.3) + l = l + 1 + j = j + tile_size + end + i = i + tile_size + end + --g.clear() + --g.draw(menu.back_tile, quad, 0, 0) + g.push("all") + -- позиционирование посредине экрана + local y = (h - #menu.items * menu.font:getHeight()) / 2 + g.setFont(menu.font) + for i, k in ipairs(menu.items) do + if (menu.active_item == i) then + g.setColor(pallete.inactive) + else + g.setColor(pallete.active) + end + g.printf(k, 0, y, w, "center") + y = y + menu.font:getHeight() + end + g.pop() +end + +return menu diff --git a/nback.lua b/nback.lua new file mode 100644 index 0000000..676a2c5 --- /dev/null +++ b/nback.lua @@ -0,0 +1,831 @@ +local inspect = require "libs.inspect" +local lume = require "libs.lume" +local math = require "math" +local os = require "os" +local string = require "string" +local table = require "table" +local class = require "libs.30log" +local Timer = require "libs.Timer" +local dbg = require "dbg" +local pallete = require "pallete" +local bhupur = require "bhupur" + +local g = love.graphics +local w, h = g.getDimensions() + +local color_constants = { + ["brown"] = {136 / 255, 55 / 255, 41 / 255}, + ["green"] = {72 / 255, 180 / 255, 66 / 255}, + ["blue"] = {27 / 255, 30 / 255, 249 / 255}, + ["red"] = {241 / 255, 30 / 255, 27 / 255}, + ["yellow"] = {231 / 255, 227 / 255, 11 / 255}, + ["purple"] = {128 / 255, 7 / 255, 128 / 255}, +} + +local AlignedLabels = class("AlignedLabels") + +function AlignedLabels:init(font, screenwidth, color) + self:clear(font, screenwidth, color) +end + +function AlignedLabels:clear(font, screenwidth, color) + self.screenwidth = screenwidth or self.screenwidth + self.font = font or self.font + self.data = {} + self.colors = {} + self.default_color = color or {1, 1, 1, 1} + self.maxlen = 0 +end + +-- ... - list of pairs of color and text +-- AlignedLabels:add("helllo", {200, 100, 10}, "wwww", {0, 0, 100}) +-- плохо, что функция не проверяет параметры на количество и тип +function AlignedLabels:add(...) + --assert(type(text) == "string") + local args = {...} + local nargs = select("#", ...) + --print("AlignedLabels:add() args = " .. inspect(args)) + if nargs > 2 then + local colored_text_data = {} + local colors = {} + local text_len = 0 + for i = 1, nargs, 2 do + local text = select(i, ...) + local color = select(i + 1, ...) + text_len = text_len + text:len() + colored_text_data[#colored_text_data + 1] = text + colors[#colors + 1] = color + end + self.data[#self.data + 1] = colored_text_data + --assert(check_color_t(select(i + 1, ...))) + self.colors[#self.colors + 1] = colors + if text_len > self.maxlen then + self.maxlen = text_len + end + else + self.data[#self.data + 1] = select(1, ...) + self.colors[#self.colors + 1] = select(2, ...) or self.default_color + end +end + +function AlignedLabels:draw(x, y) + local dw = self.screenwidth / (#self.data + 1) + local i = x + dw + local f = g.getFont() + local c = {g.getColor()} + g.setFont(self.font) + for k, v in pairs(self.data) do + if type(v) == "string" then + g.setColor(self.colors[k]) + g.print(v, i - self.font:getWidth(v) / 2, y) + i = i + dw + elseif type(v) == "table" then + local width = 0 + for _, g in pairs(v) do + width = width + self.font:getWidth(g) + end + assert(#v == #self.colors[k]) + local xpos = i - width / 2 + for j, p in pairs(v) do + --print(type(self.colors[k]), inspect(self.colors[k]), k, j) + g.setColor(self.colors[k][j]) + g.print(p, xpos, y) + xpos = xpos + self.font:getWidth(p) + end + i = i + dw + else + error(string.format("AlignedLabels:draw() : Incorrect type %s in self.data", self.data)) + end + end + g.setFont(f) + g.setColor(c) +end + +local minimum_nb_level = 2 +local maximum_nb_level = 4 +local max_pause_time = 15 +local min_pause_time = 0.6 + +local nback = { + dim = 5, -- количество ячеек поля + cell_width = 100, -- width of game field in pixels + current_sig = 1, -- номер текущего сигнала, при начале партии равен 1 + sig_count = 6, -- количество сигналов + level = 2, -- уровень, на сколько позиций назад нужно нажимать клавишу сигнала + is_run = false, -- индикатор запуска рабочего цикла + pause_time = 2.5, -- задержка между сигналами, в секундах + can_press = false, -- XXX FIXME зачем нужна эта переменная? + save_name = "nback-v0.2.lua", -- имя файла с логом пройденных тренировок + statistic = { -- блок статистики, записываемый в файл save_name + pos_hits = 0, + color_hits = 0, + sound_hits = 0, + form_hits = 0, + success = 0, + }, + show_statistic = false, -- индикатор показа статистики в конце сета + sounds = {}, + font = love.graphics.newFont("gfx/DejaVuSansMono.ttf", 25), + central_font = love.graphics.newFont("gfx/DejaVuSansMono.ttf", 42), + statistic_font = love.graphics.newFont("gfx/DejaVuSansMono.ttf", 20), + field_color = table.copy(pallete.field) -- копия таблицы по значению +} + +function create_false_array(len) + local ret = {} + for i = 1, len do + ret[#ret + 1] = false + end + return ret +end + +function generate_signals() + local color_arr = {} + for k, _ in pairs(color_constants) do + color_arr[#color_arr + 1] = k + end + + function genArrays() + nback.pos_signals = generate_nback(nback.sig_count, + function() return {math.random(1, nback.dim - 1), math.random(1, nback.dim - 1)} end, + function(a, b) return a[1] == b[1] and a[2] == b[2] end) + print("pos", inspect(nback.pos_signals)) + nback.form_signals = generate_nback(nback.sig_count, + function() + local arr = {"trup", "trdown", "trupdown", "quad", "circle", "rhombus"} + return arr[math.random(1, 6)] + end, + function(a, b) return a == b end) + print("form", inspect(nback.form_signals)) + nback.sound_signals = generate_nback(nback.sig_count, + function() return math.random(1, #nback.sounds) end, + function(a, b) return a == b end) + print("snd", inspect(nback.sound_signals)) + nback.color_signals = generate_nback(nback.sig_count, + function() return color_arr[math.random(1, 6)] end, + function(a, b) + print(string.format("color comparator a = %s, b = %s", a, inspect(b))) + return a == b end) + print("color", inspect(nback.color_signals)) + + nback.pos_eq = make_hit_arr(nback.pos_signals, function(a, b) return a[1] == b[1] and a[2] == b[2] end) + nback.sound_eq = make_hit_arr(nback.sound_signals, function(a, b) return a == b end) + nback.color_eq = make_hit_arr(nback.color_signals, function(a, b) return a == b end) + nback.form_eq = make_hit_arr(nback.form_signals, function(a, b) return a == b end) + end + + function balance(forIterCount) + local i = 0 + local changed = false + repeat + i = i + 1 + genArrays() + for k, v in pairs(nback.pos_eq) do + local n = 0 + n = n + (v and 1 or 0) + n = n + (nback.sound_eq[k] and 1 or 0) + n = n + (nback.form_eq[k] and 1 or 0) + n = n + (nback.color_eq[k] and 1 or 0) + if n > 2 then + changed = true + print("changed") + end + end + print("changed = " .. tostring(changed)) + until i >= forIterCount or not changed + print("balanced for " .. i .. " iterations") + end + + balance(5) +end + +function nback.start() + local q = pallete.field + nback.timer:tween(3, nback.field_color, { q[1], q[2], q[3], 1 }, "linear") + + print("start") + + nback.pause = false + nback.is_run = true + + generate_signals() + + nback.current_sig = 1 + nback.timestamp = love.timer.getTime() - nback.pause_time + nback.statistic.pos_hits = 0 + nback.show_statistic = false + + -- массивы хранящие булевские значения - нажат сигнал вот время обработки или нет? + nback.pos_pressed_arr = create_false_array(#nback.pos_signals) + nback.color_pressed_arr = create_false_array(#nback.color_signals) + nback.form_pressed_arr = create_false_array(#nback.form_signals) + nback.sound_pressed_arr = create_false_array(#nback.sound_signals) + print(inspect(nback.pos_pressed_arr)) + print(inspect(nback.color_pressed_arr)) + print(inspect(nback.form_pressed_arr)) + print(inspect(nback.sound_pressed_arr)) + print("end of start") +end + +function nback.enter() + -- установка альфа канала цвета сетки игрового поля + nback.field_color[4] = 0.2 +end + +function nback.leave() + nback.show_statistic = false +end + +function generate_nback(sig_count, gen, cmp) + local ret = {} + local ratio = 8 --TODO FIXME XXX + local range = {1, 3} + local count = sig_count + local null = {} + + for i = 1, ratio * sig_count do + table.insert(ret, null) + end + + repeat + local i = 1 + repeat + if count > 0 then + local prob = math.random(unpack(range)) + if prob == range[2] then + if i + nback.level <= #ret and ret[i] == null and ret[i + nback.level] == null then + ret[i] = gen() + if type(ret[i]) == "table" then + ret[i + nback.level] = lume.clone(ret[i]) + else + ret[i + nback.level] = ret[i] + end + count = count - 1 + end + end + end + i = i + 1 + until i > #ret + until count == 0 + + for i = 1, #ret do + if ret[i] == null then + repeat + ret[i] = gen() + until not (i + nback.level <= #ret and cmp(ret[i], ret[i + nback.level])) + end + end + + return ret +end + +function nback.init() + nback.timer = Timer() + math.randomseed(os.time()) + + wave_path = "sfx/alphabet" + for k, v in pairs(love.filesystem.getDirectoryItems(wave_path)) do + table.insert(nback.sounds, love.audio.newSource(wave_path .. "/" .. v, "static")) + end + + nback.resize(g.getDimensions()) + + local data, _ = love.filesystem.read("settings.lua") + if data then + local settings = lume.deserialize(data, "all") + print("settings loaded", inspect(settings)) + nback.level = settings.level + nback.pause_time = settings.level + nback.volume = settings.volume + else + nback.volume = 0.2 + end + love.audio.setVolume(nback.volume) +end + +function nback.update(dt) + if nback.pause then + nback.timestamp = love.timer.getTime() + -- подумай, нужен ли здесь код строчкой выше. Могут ли возникнуть проблемы с таймером отсчета + -- если продолжительноть паузы больше nback.pause_time? + return + end + + if nback.is_run then + if nback.current_sig < #nback.pos_signals then + local time = love.timer.getTime() + if (time - nback.timestamp >= nback.pause_time) then + nback.timestamp = love.timer.getTime() + + nback.current_sig = nback.current_sig + 1 + nback.can_press = true + + -- setup timer for figure alpha channel animation + nback.figure_alpha = {alpha = 1} + local tween_time = 0.5 + print("time delta = " .. nback.pause_time - tween_time) + nback.timer:after(nback.pause_time - tween_time - 0.1, function() + nback.timer:tween(tween_time, nback.figure_alpha, {alpha = 0}, "out-linear") + end) + + local snd = nback.sounds[nback.sound_signals[nback.current_sig]] + --print("snd", inspect(nback.sound_signals[nback.current_sig]), snd:getVolume(), snd:getDuration()) + snd:play() + + nback.pos_pressed = false + nback.sound_pressed = false + nback.form_pressed = false + nback.color_pressed = false + end + else + nback.stop() + end + end + nback.timer:update(dt) +end + +function calc_percent(eq, pressed_arr) + if not eq then return 0 end --XXX hack against crash + + local p = 0 + local success = 0 + for k, v in pairs(eq) do + if v then + success = success + 1 + end + if v and pressed_arr[k] then + p = p + 1 + end + end + return p / success +end + +function nback.save_to_history() + print("nback.current_sig = ", nback.current_sig) + print("#nback.pos_signals = ", #nback.pos_signals) + local data, size = love.filesystem.read(nback.save_name) + local history = {} + if data ~= nil then + history = lume.deserialize(data) + end + --print("history", inspect(history)) + local d = os.date("*t") + table.insert(history, { date = d, + pos_pressed_arr = nback.pos_pressed_arr, + form_pressed_arr = nback.form_pressed_arr, + sound_pressed_arr = nback.sound_pressed_arr, + color_pressed_arr = nback.color_pressed_arr, + time = os.time(d), + stat = nback.statistic, + nlevel = nback.level, + pause = nback.pause_time, + percent = nback.percent}) + love.filesystem.write(nback.save_name, lume.serialize(history)) +end + +function nback.stop() + local q = pallete.field + -- амимация альфа-канала игрового поля + nback.timer:tween(2, nback.field_color, { q[1], q[2], q[3], 0.1 }, "linear") + + nback.is_run = false + nback.show_statistic = true + + print("stop") + print(inspect(nback.sound_pressed_arr)) + print(inspect(nback.color_pressed_arr)) + print(inspect(nback.form_pressed_arr)) + print(inspect(nback.pos_pressed_arr)) + + nback.sound_percent = calc_percent(nback.sound_eq, nback.sound_pressed_arr) + nback.color_percent = calc_percent(nback.color_eq, nback.color_pressed_arr) + nback.form_percent = calc_percent(nback.form_eq, nback.form_pressed_arr) + nback.pos_percent = calc_percent(nback.pos_eq, nback.pos_pressed_arr) + nback.percent = (nback.sound_percent + nback.color_percent + nback.form_percent + nback.pos_percent) / 4 + -- Раунд полностью закончен? - записываю историю + if nback.pos_signals and nback.current_sig == #nback.pos_signals then nback.save_to_history() end +end + +function nback.quit() + nback.timer:destroy() + local settings_str = lume.serialize { + ["volume"] = love.audio.getVolume(), + ["level"] = nback.level, + ["pause_time"] = nback.pause_time } + ok, msg = love.filesystem.write("settings.lua", settings_str, settings_str:len()) + nback.stop() + states.pop() +end + +-- key or scancode? +function nback.keypressed(key) + if key == "escape" then + if nback.is_run then + nback.stop() + else + nback.quit() + end + elseif key == "space" or key == "return" then + nback.start() + + --[[ + [elseif key == "0" then + [ nback.pause = not nback.pause + [elseif key == "9" then + [ nback.show_statistic = not nback.show_statistic + [ if nback.show_statistic then + [ nback.pause = true + [ end + ]] + + elseif key == "p" then + nback.check("pos") + elseif key == "s" then + nback.check("sound") + elseif key == "f" then + nback.check("form") + elseif key == "c" then + nback.check("color") + end + + if not nback.is_run and not nback.show_statistic then + if key == "left" and nback.level > minimum_nb_level then + nback.level = nback.level - 1 + elseif key == "right" and nback.level < maximum_nb_level then + nback.level = nback.level + 1 + end + if key == "up" and nback.pause_time < max_pause_time then + nback.pause_time = nback.pause_time + 0.2 + elseif key == "down" and nback.pause_time > min_pause_time then + nback.pause_time = nback.pause_time - 0.2 + end + end + + if key == "-" then + print("love.audio.getVolume() = ", love.audio.getVolume()) + love.audio.setVolume(love.audio.getVolume() - 0.05) + elseif key == "=" then + print("love.audio.getVolume() = ", love.audio.getVolume()) + love.audio.setVolume(love.audio.getVolume() + 0.05) + end +end + +-- signal type may be "pos", "sound", "color", "form" +function nback.check(signalType) + if not nback.is_run then + return + end + local signals = nback[signalType .. "_signals"] + local cmp = function(a, b) return a == b end + if signalType == "pos" then + cmp = function(a, b) + return a[1] == b[1] and a[2] == b[2] + end + end + nback[signalType .. "_pressed"] = true + -- ненадолго включаю подсветку введеной клавиши на игровом поле + nback.timer:after(0.1, function() + nback[signalType .. "_pressed"] = false + end) + nback[signalType .. "_pressed_arr"][nback.current_sig] = true + if nback.current_sig - nback.level > 1 then + if cmp(signals[nback.current_sig], signals[nback.current_sig - nback.level]) then + --print(inspect(nback)) + if nback.can_press then + print(signalType .. " hit!") + print(nback.statistic[signalType .. "_hits"]) + nback.statistic[signalType .. "_hits"] = nback.statistic[signalType .. "_hits"] + 1 + nback.can_press = false + end + end + end +end + +function nback.resize(neww, newh) + print(string.format("nback resized to %d * %d", neww, newh)) + w = neww + h = newh + local pixels_border = 130 -- size of border around main game field + nback.cell_width = (newh - pixels_border) / nback.dim + nback.bhupur_h = nback.cell_width * nback.dim +end + +-- return array of boolean values in succesful indices +function make_hit_arr(signals, comparator) + local ret = {} + for k, v in pairs(signals) do + ret[#ret + 1] = k > nback.level and comparator(v, signals[k - nback.level]) + end + return ret +end + +function draw_signal_form(x0, y0, formtype, xdim, ydim, color) + local border = 5 + local x, y = x0 + xdim * nback.cell_width + border, y0 + ydim * nback.cell_width + border + local w, h = nback.cell_width - border * 2, nback.cell_width - border * 2 + g.setColor(color) + if formtype == "quad" then + local delta = 5 + g.rectangle("fill", x + delta, y + delta, w - delta * 2, h - delta * 2) + elseif formtype == "circle" then + --g.circle("fill", x + w / 2, y + h / 2, w / 2) + --g.setColor({1, 0, 1}) + g.circle("fill", x + w / 2, y + h / 2, w / 2.3) + elseif formtype == "trup" then + --g.polygon("fill", {x, y + h * (2 / 3), x + w / 2, y, x + w, y + h * (2 / 3)}) + --g.polygon("fill", {x, y + h * (2.2 / 3), x + w / 2, y, x + w, y + h * (2.2 / 3)}) + local tri = {} + local rad = w / 2 + for i = 1, 3 do + local alpha = 2 * math.pi * i / 3 + local sx = x + w / 2 + rad * math.sin(alpha) + local sy = y + h / 2 + rad * math.cos(alpha) + tri[#tri + 1] = sx + tri[#tri + 1] = sy + end + g.polygon("fill", tri) + elseif formtype == "trdown" then + --g.polygon("fill", {x, y + h / 3, x + w / 2, y + h, x + w, y + h / 3}) + local tri = {} + local rad = w / 2 + for i = 1, 3 do + local alpha = math.pi + 2 * math.pi * i / 3 + local sx = x + w / 2 + rad * math.sin(alpha) + local sy = y + h / 2 + rad * math.cos(alpha) + tri[#tri + 1] = sx + tri[#tri + 1] = sy + end + g.polygon("fill", tri) + elseif formtype == "trupdown" then + --g.polygon("fill", {x, y + h * (2 / 3), x + w / 2, y, x + w, y + h * (2 / 3)}) + --g.polygon("fill", {x, y + h / 3, x + w / 2, y + h, x + w, y + h / 3}) + --g.setColor({0, 1, 1}) + local tri_up, tri_down = {}, {} + local rad = w / 2 + for i = 1, 3 do + local alpha = 2 * math.pi * i / 3 + local sx = x + w / 2 + rad * math.sin(alpha) + local sy = y + h / 2 + rad * math.cos(alpha) + tri_up[#tri_up + 1] = sx + tri_up[#tri_up + 1] = sy + local alpha = math.pi + 2 * math.pi * i / 3 + local sx = x + w / 2 + rad * math.sin(alpha) + local sy = y + h / 2 + rad * math.cos(alpha) + tri_down[#tri_down + 1] = sx + tri_down[#tri_down + 1] = sy + end + g.polygon("fill", tri_up) + g.polygon("fill", tri_down) + elseif formtype == "rhombus" then + --g.polygon("fill", {x, y + h / 2, x + w / 2, y + h, x + w, y + h / 2, x + w / 2, y}) + --g.setColor({0, 1, 1}) + local delta = 0 + g.polygon("fill", {x + delta, y + h / 2, + x + w / 2, y + h - delta, + x + w - delta, y + h / 2, + x + w / 2, y + delta}) + end + function draw_central_circle() + local rad = 3 + g.setColor({0, 0, 0}) + g.circle("fill", x + w / 2 - rad / 2, y + h / 2 - rad / 2, rad) + end + --draw_central_circle() +end + +function draw_field_grid(x0, y0, field_h) + g.setColor(nback.field_color) + for i = 0, nback.dim do + -- horizontal + g.line(x0, y0 + i * nback.cell_width, x0 + field_h, y0 + i * nback.cell_width) + -- vertical + g.line(x0 + i * nback.cell_width, y0, x0 + i * nback.cell_width, y0 + field_h) + end +end + +function draw_hit_rects(x, y, arr, eq, rect_size, border) + --print("border = " .. border) + local hit_color = {200 / 255, 10 / 255, 10 / 255} + for k, v in pairs(arr) do + g.setColor(pallete.field) + g.rectangle("line", x + rect_size * (k - 1), y, rect_size, rect_size) + g.setColor(pallete.inactive) + g.rectangle("fill", x + rect_size * (k - 1) + border, y + border, rect_size - border * 2, rect_size - border * 2) + if v then + g.setColor(hit_color) + g.rectangle("fill", x + rect_size * (k - 1) + border, y + border, rect_size - border * 2, rect_size - border * 2) + end + -- draw circle in center of quad if it is successful + if eq[k] then + local radius = 4 + g.setColor({0, 0, 0}) + g.circle("fill", x + rect_size * (k - 1) + rect_size / 2, y + rect_size / 2, radius) + end + end + y = y + rect_size + 6 + return x, y +end + +local draw_iteration = 0 -- debug variable + +-- drawing horizontal string with signal numbers +function draw_horizontal_string(x, y, rect_size) + g.setColor({0.5, 0.5, 0.5}) + g.setFont(nback.statistic_font) + for k, _ in pairs(nback.pos_pressed_arr) do + local delta = (rect_size - g.getFont():getWidth(tostring(k))) / 2 + g.print(tostring(k), x + rect_size * (k - 1) + delta, y) -- п + end + return x, y +end + +-- draw one big letter in left side of draw_hit_rects() output +function print_signal_type(x, y, rect_size, str, pixel_gap, delta) + local delta = (rect_size - g.getFont():getHeight()) / 2 + g.print(str, x - g.getFont():getWidth(str) - pixel_gap, y + delta) + y = y + rect_size + 6 + return x, y +end + +function print_percents(x, y, rect_size, pixel_gap, border, starty) + local sx = x + rect_size * (#nback.pos_signals - 1) + border + rect_size - border * 2 + pixel_gap + g.setColor({200 / 255, 0, 200 / 255}) + g.setFont(nback.font) + g.print(string.format("%.2f", nback.sound_percent), sx, y) + y = y + rect_size + 6 + g.print(string.format("%.2f", nback.color_percent), sx, y) + y = y + rect_size + 6 + g.print(string.format("%.2f", nback.form_percent), sx, y) + y = y + rect_size + 6 + g.print(string.format("%.2f", nback.pos_percent), sx, y) + y = starty + 4 * (rect_size + 20) + g.printf(string.format("rating %0.2f", nback.percent), 0, y, w, "center") + return x, y +end + +function print_debug_info() + dbg.clear() + dbg.print_text("fps " .. love.timer.getFPS()) + dbg.print_text("pos " .. inspect(nback.pos_signals)) + dbg.print_text("sound " .. inspect(nback.sound_signals)) + dbg.print_text("form " .. inspect(nback.form_signals)) + dbg.print_text("color " .. inspect(nback.color_signals)) + dbg.print_text("current_sig = " .. nback.current_sig) + dbg.print_text("nback.can_press = " .. tostring(nback.can_press)) +end + +function print_set_results(x0, y0) + print(x0, y0) + local y = y0 + nback.font:getHeight() + g.printf(string.format("Set results:"), 0, y, w, "center") + y = y + nback.font:getHeight() + g.printf(string.format("level %d", nback.level), 0, y, w, "center") + y = y + nback.font:getHeight() + g.printf(string.format("Pause time %.1f sec", nback.pause_time), 0, y, w, "center") +end + +function draw_level_welcome() + g.setFont(nback.font) + --FIXME Dissonance with color and variable name + g.setColor(pallete.tip_text) + local y = (h - g.getFont():getHeight() * 4) / 2.5 + g.printf(string.format("nback level is %d", nback.level), 0, y, w, "center") + y = y + nback.font:getHeight() + g.printf("Use ←→ arrows to setup", 0, y, w, "center") + y = y + nback.font:getHeight() * 2 + g.printf(string.format("delay time is %.1f sec", nback.pause_time), 0, y, w, "center") + y = y + nback.font:getHeight() + g.printf("Use ↑↓ arrows to setup", 0, y, w, "center") +end + +-- draw central_text - Press Space key +function print_press_space_to_new_round(y0) + local central_text = "Press Space to new round" + g.setFont(nback.central_font) + g.setColor(pallete.signal) + local x = (w - nback.central_font:getWidth(central_text)) / 2 + --y = h - nback.central_font:getHeight() * 2 + local y = y0 + (nback.dim - 1) * nback.cell_width + g.print(central_text, x, y) +end + +function print_control_tips(bottom_text_line_y) + local keys_tip = AlignedLabels:new(nback.font, w) + local pressed_color = pallete.active + local unpressed_color = pallete.inactive + if nback.sound_pressed then + keys_tip:add("Sound", pressed_color) + else + keys_tip:add("S", {200 / 255, 0, 200 / 255}, "ound", unpressed_color) + end + if nback.color_pressed then + keys_tip:add("Color", pressed_color) + else + keys_tip:add("C", {200 / 255, 0, 200 / 255}, "olor", unpressed_color) + end + if nback.form_pressed then + keys_tip:add("Form", pressed_color) + else + keys_tip:add("F", {200 / 255, 0, 200 / 255}, "orm", unpressed_color) + end + if nback.pos_pressed then + keys_tip:add("Position", pressed_color) + else + keys_tip:add("P", {200 / 255, 0, 200 / 255}, "osition", unpressed_color) + end + keys_tip:draw(0, bottom_text_line_y) +end + +-- draw escape tip +function print_escape_tip(bottom_text_line_y) + g.setFont(nback.font) + g.setColor(pallete.tip_text) + g.printf("Escape - go to menu", 0, bottom_text_line_y + nback.font:getHeight(), w, "center") +end + +-- draw active signal quad +function draw_active_signal(x0, y0) + local x, y = unpack(nback.pos_signals[nback.current_sig]) + local sig_color = color_constants[nback.color_signals[nback.current_sig]] + if nback.figure_alpha then + sig_color[4] = nback.figure_alpha.alpha + end + draw_signal_form(x0, y0, nback.form_signals[nback.current_sig], x, y, sig_color) +end + +function draw_bhupur(x0, y0) + local delta = 5 + bhupur.color = nback.field_color + bhupur.draw(x0 - delta, y0 - delta, nback.bhupur_h + delta * 2) +end + +-- рисовать статистику после конца сета +function draw_statistic(x0, y0) + g.setFont(nback.font) + g.setColor(pallete.statistic) + + print("x0 = " .. x0 .. " y0 = " .. y0) + + local width_k = 3 / 4 + local rect_size = w * width_k / #nback.pos_signals -- XXX depend on screen resolution + local x = (w - w * width_k) / 2 + local starty = 200 + local y = starty + local border = 2 + --print("x", x) + --print("screenW = ", w) + --print("rect_size, nback.sig_count", rect_size, nback.sig_count) + x, y = draw_horizontal_string(x, y, rect_size) + + y = y + g.getFont():getHeight() * 1.5 + ---------------------------------------- + local freeze_y = y + + x, y = draw_hit_rects(x, y, nback.sound_pressed_arr, nback.sound_eq, rect_size, border) + x, y = draw_hit_rects(x, y, nback.color_pressed_arr, nback.color_eq, rect_size, border) + x, y = draw_hit_rects(x, y, nback.form_pressed_arr, nback.form_eq, rect_size, border) + x, y = draw_hit_rects(x, y, nback.pos_pressed_arr, nback.pos_eq, rect_size, border) + + -- drawing left column with letters + g.setColor({200 / 255, 0, 200 / 255}) + g.setFont(nback.font) + + local y = freeze_y + local pixel_gap = 10 + x, y = print_signal_type(x, y, rect_size, "S", pixel_gap, delta) + x, y = print_signal_type(x, y, rect_size, "C", pixel_gap, delta) + x, y = print_signal_type(x, y, rect_size, "F", pixel_gap, delta) + x, y = print_signal_type(x, y, rect_size, "P", pixel_gap, delta) + x, y = print_percents(x, freeze_y + 0, rect_size, pixel_gap, border, starty) +end + +function nback.draw() + love.graphics.clear(pallete.background) + + local delta = 20 -- for avoiding intersection between field and bottom lines of text + local x0 = (w - nback.dim * nback.cell_width) / 2 + local y0 = (h - nback.dim * nback.cell_width) / 2 - delta + + g.push("all") + draw_field_grid(x0, y0, nback.dim * nback.cell_width) + draw_bhupur(x0, y0) + print_debug_info() + if nback.is_run then + draw_active_signal(x0, y0) + else + if nback.show_statistic then + draw_statistic(x0, y0) + print_set_results(x0, y0) + else + draw_level_welcome() + end + print_press_space_to_new_round(y0) + end + + local bottom_text_line_y = h - nback.font:getHeight() * 3 + print_control_tips(bottom_text_line_y) + print_escape_tip(bottom_text_line_y) + g.pop() +end + +return nback diff --git a/pallete.lua b/pallete.lua new file mode 100644 index 0000000..68d7fa8 --- /dev/null +++ b/pallete.lua @@ -0,0 +1,26 @@ +local pallete = { + active = {1, 1, 1}, + --background = {20 / 255, 40 / 255, 80 / 255, 1}, + background = {0.1, 0.1, 0.1}, + chart = {200 / 255, 80 / 255, 80 / 255, 1}, + --field = {20 / 255, 80 / 255, 80 / 255, 1}, + field = {0.3, 0.8, 0.8, 1}, + font = {255 / 255, 255 / 255, 255 / 255, 1}, + header = {1, 1, 0}, + inactive = {100 / 255, 200 / 255, 70 / 255, 1}, + scroll_tip_text = {0 / 255, 240 / 255, 0 / 255, 1}, + signal = {200 / 255, 80 / 255, 80 / 255, 1}, + + sound_text_disabled = {255 / 255, 255 / 255, 0 / 255, 1}, + sound_text_enabled = {0 / 255, 240 / 255, 0 / 255, 1}, + + statistic = {0 / 255, 240 / 255, 0 / 255, 1}, + + tip_text_alt = {255 / 255, 255 / 255, 0 / 255, 1}, + tip_text = {0 / 255, 240 / 255, 0 / 255, 1}, + + debug_line = {100 / 255, 200 / 255, 50 / 255, 1}, + debug_font = {140 / 255, 140 / 255, 140 / 255, 1}, +} + +return pallete diff --git a/pviewer.lua b/pviewer.lua new file mode 100644 index 0000000..f2e2c39 --- /dev/null +++ b/pviewer.lua @@ -0,0 +1,343 @@ +local inspect = require "libs.inspect" +local lume = require "libs.lume" +local timer = require "libs.Timer" + +local pallete = require "pallete" +local nback = require "nback" +local g = love.graphics +local dbg = require "dbg" + +-- integer division +local function div(a, b) + return (a - a %b) / b +end + +local pviewer = { + scroll_tip_text = "For scrolling table use ↓↑ arrows", + header_text = "", + border = 40, --y axis border in pixels for drawing chart + scrollx = 0, + + font = love.graphics.newFont("gfx/DejaVuSansMono.ttf", 20), + scrool_tip_font = love.graphics.newFont("gfx/DejaVuSansMono.ttf", 13), + selected_item = 2, + start_line = 1, + sorted_by_column_num = 1, +} + +local w, h = g.getDimensions() +local columns_name = {"date", "nlevel", "rating", "pause"} + +function pviewer.init() + pviewer.resize(g.getDimensions()) + pviewer.timer = timer() +end + +function pviewer.enter() + print("pviewer.enter()") + local tmp, size = love.filesystem.read(nback.save_name) + if tmp ~= nil then + pviewer.data = lume.deserialize(tmp) + else + pviewer.data = {} + end + if #pviewer.data >= 1 then + pviewer.cursor_index = 1 + else + pviewer.cursor_index = 0 + end + print("*** begining of pviewer.data ***") + local str = inspect(pviewer.data) + --print(str) + love.filesystem.write("pviewer_data_extracting.lua", str, str:len()) + print("*** end of pviewer.data ***") + --pviewer.sort_by_column(1) +end + +function pviewer.leave() + pviewer.data = nil +end + +function get_max_lines_printed() + return div(h - 100, pviewer.font:getHeight()) +end + +function pviewer.resize(neww, newh) + print(string.format("pviewer.resize(%d, %d)", neww, newh)) + w = neww + h = newh + pviewer.vertical_buf_len = get_max_lines_printed() + print("vertical_buf_len = ", pviewer.vertical_buf_len) + print(pviewer.data) + print(inspect(pviewer.data)) + print(pviewer.cursor_index) + if pviewer.cursor_index and pviewer.cursor_index > pviewer.vertical_buf_len then pviewer.cursor_index = pviewer.vertical_buf_len - 1 end -- why -1 ?? + pviewer.rt = g.newCanvas(w, pviewer.vertical_buf_len * pviewer.font:getLineHeight() * pviewer.font:getHeight(), {format = "normal", msaa = 4}) + if not pviewer then + error("Canvas not supported!") + end +end + +-- draw column of table pviewer.data, from index k, to index j with func(v) access function +function draw_column(k, j, deltax, func) + local oldcolor = {g.getColor()} + local dx = 0 + local y = 0 + pviewer.font:getHeight() -- start y position + if k + j > #pviewer.data then + j = #pviewer.data + end + for i = k, j do + local s = func(pviewer.data[i]) + dx = math.max(dx, pviewer.font:getWidth(s)) + g.print(s, deltax, y) + y = y + pviewer.font:getHeight() + end + g.setColor(oldcolor) + deltax = deltax + dx + return deltax +end + +function draw_columns(k, j, deltax) + g.setFont(pviewer.font) + g.setColor(pallete.chart) + deltax = draw_column(k, j, deltax, function(v) return string.format("%.2d.%.2d.%d %.2d:%.2d:%.2d", v.date.day, v.date.month, v.date.year, v.date.hour, v.date.min, v.date.sec) end) + g.setColor(pallete.header) + deltax = draw_column(k, j, deltax, function(v) return " / " end) + g.setColor(pallete.chart) + deltax = draw_column(k, j, deltax, function(v) if v.nlevel then return string.format("%d", v.nlevel) else return "-" end end) + g.setColor(pallete.header) + deltax = draw_column(k, j, deltax, function(v) return " / " end) + g.setColor(pallete.chart) + deltax = draw_column(k, j, deltax, function(v) if v.percent then return string.format("%.2f", v.percent) else return "-" end end) + g.setColor(pallete.header) + deltax = draw_column(k, j, deltax, function(v) return " / " end) + g.setColor(pallete.chart) + deltax = draw_column(k, j, deltax, function(v) if v and v.pause then return string.format("%.2f", v.pause) else return "_" end end) + return deltax +end + +-- draw pviewer.data from k to j index in vertical list on the center of screen +function draw_chart(k, j) + if k < 1 then k = 1 end + local deltax = 0 + -- because k may be float value + deltax = draw_columns(math.floor(k), j, deltax) + return deltax +end + +function draw_chart_header(r) + g.setColor({1, 1, 1}) + g.setFont(pviewer.font) + local tbl = {} + for k, v in pairs(columns_name) do + if k == pviewer.sorted_by_column_num then + tbl[#tbl + 1] = pallete.active + else + tbl[#tbl + 1] = pallete.header + end + if k ~= #columns_name then + tbl[#tbl + 1] = v .. " / " + else + tbl[#tbl + 1] = v + end + end + g.printf(tbl, r.x1, r.y1 - pviewer.border / 2, r.x2 - r.x1, "center") +end + +--drawing scroll_tip_text +function draw_scroll_tip(rect) + g.setColor(pallete.scroll_tip_text) + g.setFont(pviewer.scrool_tip_font) + g.printf(pviewer.scroll_tip_text, rect.x1, rect.y2 + pviewer.border / 2, rect.x2 - rect.x1, "center") +end + +function print_dbg_info() + dbg.clear() + dbg.print_text("fps " .. love.timer.getFPS()) + dbg.print_text(string.format("sorted by %s = %d", columns_name[pviewer.sorted_by_column_num], pviewer.sorted_by_column_num)) +end + +function pviewer.draw() + love.graphics.clear(pallete.background) + + local r = {x1 = pviewer.border, y1 = pviewer.border, x2 = w - pviewer.border, y2 = h - pviewer.border} + + g.push("all") + + draw_scroll_tip(r) + draw_chart_header(r) + + g.setColor({1, 1, 1, 1}) + g.setCanvas(pviewer.rt) + local chart_width + do + --g.clear() + love.graphics.clear(pallete.background) + chart_width = draw_chart(pviewer.start_line, pviewer.start_line + pviewer.vertical_buf_len) + end + g.setCanvas() + + g.setColor({1, 1, 1, 1}) + g.draw(pviewer.rt, (w - chart_width) / 2, r.y1) + --g.draw(pviewer.rt, (w - chart_width) / 2, y1) + + if pviewer.cursor_index > 0 then + g.setColor(1, 1, 1, 0.3) + local x = (w - chart_width) / 2 + local y = r.y1 + pviewer.cursor_index * pviewer.font:getHeight() + g.rectangle("fill", x, y, chart_width, pviewer.font:getHeight()) + end + + print_dbg_info() + + g.pop() +end + +function pviewer.sort_by_column(idx) + pviewer.sorted_by_column_num = idx + table.sort(pviewer.data, function(a, b) + if columns_name[idx] == "date" then + if a.date and b.date then + local t1 = a.date + local t2 = b.date + local a_sec = t1.year * 365 * 24 * 60 * 60 + t1.yday * 24 * 60 * 60 + t1.hour * 60 * 60 + t1.min * 60 + t1.sec + local b_sec = t2.year * 365 * 24 * 60 * 60 + t2.yday * 24 * 60 * 60 + t2.hour * 60 * 60 + t2.min * 60 + t2.sec + return a_sec < b_sec + end + elseif columns_name[idx] == "nlevel" then + if a.nlevel and b.nlevel then + return a.nlevel < b.nlevel + end + elseif columns_name[idx] == "rating" then + if a.percent and b.percent then + return a.percent < b.percent + end + elseif columns_name[idx] == "pause" then + if a.pause and b.pause then + return a.pause < b.pause + end + end + end) +end + +function sort_by_previous_column() + if pviewer.sorted_by_column_num - 1 < 1 then + pviewer.sort_by_column(#columns_name) + else + pviewer.sort_by_column(pviewer.sorted_by_column_num - 1) + end +end + +function sort_by_next_column() + if pviewer.sorted_by_column_num + 1 > #columns_name then + pviewer.sort_by_column(1) + else + pviewer.sort_by_column(pviewer.sorted_by_column_num + 1) + end +end + +function scroll_up() + if pviewer.start_line - pviewer.vertical_buf_len >= 1 then + pviewer.start_line = pviewer.start_line - pviewer.vertical_buf_len + end +end + +function scroll_down() + if pviewer.start_line + pviewer.vertical_buf_len <= #pviewer.data then + pviewer.start_line = pviewer.start_line + pviewer.vertical_buf_len + end +end + +function pviewer.move_up() + --print("pviewer.cursor_index = " .. pviewer.cursor_index, " pviewer.start_line " .. pviewer.start_line .. " pviewer.vertical_buf_len " .. pviewer.vertical_buf_len) + if pviewer.cursor_index > 1 then + if not pviewer.cursor_move_up_animation then + pviewer.cursor_move_up_animation = true + pviewer.timer:after(0.1, function() + pviewer.cursor_move_up_animation = false; + pviewer.cursor_index = pviewer.cursor_index - 1 + end) + end + elseif not pviewer.move_up_animation then + if pviewer.start_line > 1 then + --pviewer.start_line = pviewer.start_line - 1 + pviewer.move_up_animation = true + pviewer.timer:during(0.1, function() + pviewer.start_line = pviewer.start_line - 0.1 + end, + function() + pviewer.move_up_animation = false + --print("after timer") + --print("pviewer.start_line = " .. pviewer.start_line) + end) + end +end +end + +function pviewer.move_down() + print("move down") + --print("pviewer.cursor_index = " .. pviewer.cursor_index, " pviewer.start_line " .. pviewer.start_line .. " pviewer.vertical_buf_len " .. pviewer.vertical_buf_len) + if pviewer.cursor_index + 1 < pviewer.vertical_buf_len then + if not pviewer.cursor_move_down_animation then + --print("after") + pviewer.cursor_move_down_animation = true + pviewer.timer:after(0.1, function() + pviewer.cursor_move_down_animation = false; + pviewer.cursor_index = pviewer.cursor_index + 1 + end) + end + --end + elseif not pviewer.move_down_animation then + if pviewer.start_line + pviewer.vertical_buf_len <= #pviewer.data then + --pviewer.start_line = pviewer.start_line + 1 + --print("pviewer.move_down()") + --print("pviewer.start_line = " .. pviewer.start_line) + pviewer.move_down_animation = true + pviewer.timer:during(0.1, function() + pviewer.start_line = pviewer.start_line + 0.1 + end, + function() + pviewer.move_down_animation = false + --print("after timer") + --print("pviewer.start_line = " .. pviewer.start_line) + end) + end + end +end + +function pviewer.keypressed(key) + if key == "escape" then + states.pop() + elseif key == "left" then + sort_by_previous_column() + elseif key == "right" then + sort_by_next_column() + elseif key == "return" or key == "space" then + -- TODO по нажатию клавиши показать конечную таблицу игры + elseif key == "home" or key == "kp7" then + print("pviewer.keypressed('home')") + pviewer.start_line = 1 + pviewer.cursor_index = 1 + elseif key == "end" or key == "kp1" then + print("pviewer.keypressed('end')") + pviewer.start_line = #pviewer.data - pviewer.vertical_buf_len + 1 + print("pviewer.vertical_buf_len = ", pviewer.vertical_buf_len) + pviewer.cursor_index = pviewer.vertical_buf_len - 1 + end +end + +function pviewer.update(dt) + local kb = love.keyboard + if kb.isDown("up") then + pviewer.move_up() + elseif kb.isDown("down") then + pviewer.move_down() + elseif kb.isDown("pageup") then + scroll_up() + elseif kb.isDown("pagedown") then + scroll_down() + end + pviewer.timer:update(dt) +end + +return pviewer diff --git a/ruler.lua b/ruler.lua new file mode 100644 index 0000000..5dbf7bc --- /dev/null +++ b/ruler.lua @@ -0,0 +1,92 @@ +local class = require "libs/30log" +local lg = love.graphics +local geo = require "geo" + +local ruler = class("ruler") + +function ruler:init(x1, y1, x2, y2) + self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2 + print(string.format("Ruler created at (%d, %d), (%d, %d)", x1, y1, x2, y2)) + self.rad = 4 + self.color = {200, 100, 135} + self.ccolor = {20, 80, 135} + self.lcolor = {80, 20, 135} + self.visible = true + self.pinned = false + self.infirst = false + self.insecond = false + self.linewidth = 2 +end + +function ruler:mousepressed(x, y, button, istouch) + self.pinned = true +end + +function ruler:mousereleased(x, y, button, istouch) + self.pinned = false +end + +function ruler:mousemoved(x, y, dx, dy, istouch) + if self.pinned and self.infirst then + self.x1 = self.x1 + dx + self.y1 = self.y1 + dy + end + if self.pinned and self.insecond then + self.x2 = self.x2 + dx + self.y2 = self.y2 + dy + end + self.infirst = geo.isPointInCircle(x, y, self.x1, self.y1, self.rad) + self.insecond = geo.isPointInCircle(x, y, self.x2, self.y2, self.rad) +end + +function ruler:draw() + if not self.visible then return end + + local lineStyle = lg.getLineStyle() + local lineWidth = lg.getLineWidth() + local lineColor = pack(lg.getColor()) + lg.setLineStyle("smooth") + lg.setLineWidth(self.linewidth) + + local x, y = love.mouse.getPosition() + if geo.isPointInCircle(x, y, self.x1, self.y1, self.rad) then + lg.setColor(255 - self.ccolor[1], 255 - self.ccolor[2], 255 - self.ccolor[3], 255) + else + lg.setColor(self.ccolor) + end + lg.circle("fill", self.x1, self.y1, self.rad) + + if geo.isPointInCircle(x, y, self.x2, self.y2, self.rad) then + lg.setColor(255 - self.ccolor[1], 255 - self.ccolor[2], 255 - self.ccolor[3], 255) + else + lg.setColor(self.ccolor) + end + lg.circle("fill", self.x2, self.y2, self.rad) + + lg.setColor(self.lcolor) + lg.line(self.x1, self.y1, self.x2, self.y2) + + lg.print(string.format("%d px", geo.dist(self.x1, self.y1, self.x2, self.y2)), self.x1, self.y1) + -- black points in center of circles + lg.setColor(0, 0, 0, 255) + lg.circle("fill", self.x1, self.y1, 1) + lg.circle("fill", self.x2, self.y2, 1) + + lg.setLineWidth(lineWidth) + lg.setLineStyle(lineStyle) + lg.setColor(unpack(lineColor)) + + function tooltipCoordinates(x, y) + local height = lg.getFont():getHeight() + lg.print(string.format("(%d, %d)", x, y), x - 2, y - height - 2) + end + + if self.infirst then + tooltipCoordinates(self.x1, self.y1) + end + if self.insecond then + tooltipCoordinates(self.x2, self.y2) + end +end + +return ruler diff --git a/sfx/alphabet/a.wav b/sfx/alphabet/a.wav new file mode 100644 index 0000000..e654bad Binary files /dev/null and b/sfx/alphabet/a.wav differ diff --git a/sfx/alphabet/b.wav b/sfx/alphabet/b.wav new file mode 100644 index 0000000..1926765 Binary files /dev/null and b/sfx/alphabet/b.wav differ diff --git a/sfx/alphabet/c.wav b/sfx/alphabet/c.wav new file mode 100644 index 0000000..13f7004 Binary files /dev/null and b/sfx/alphabet/c.wav differ diff --git a/sfx/alphabet/d.wav b/sfx/alphabet/d.wav new file mode 100644 index 0000000..c14a0f8 Binary files /dev/null and b/sfx/alphabet/d.wav differ diff --git a/sfx/alphabet/e.wav b/sfx/alphabet/e.wav new file mode 100644 index 0000000..955dfed Binary files /dev/null and b/sfx/alphabet/e.wav differ diff --git a/sfx/alphabet/f.wav b/sfx/alphabet/f.wav new file mode 100644 index 0000000..5440fe2 Binary files /dev/null and b/sfx/alphabet/f.wav differ diff --git a/sfx/alphabet/g.wav b/sfx/alphabet/g.wav new file mode 100644 index 0000000..065ed95 Binary files /dev/null and b/sfx/alphabet/g.wav differ diff --git a/sfx/alphabet/h.wav b/sfx/alphabet/h.wav new file mode 100644 index 0000000..04431eb Binary files /dev/null and b/sfx/alphabet/h.wav differ diff --git a/sfx/alphabet/i.wav b/sfx/alphabet/i.wav new file mode 100644 index 0000000..f50d1b9 Binary files /dev/null and b/sfx/alphabet/i.wav differ diff --git a/sfx/alphabet/j.wav b/sfx/alphabet/j.wav new file mode 100644 index 0000000..ec94088 Binary files /dev/null and b/sfx/alphabet/j.wav differ diff --git a/sfx/alphabet/k.wav b/sfx/alphabet/k.wav new file mode 100644 index 0000000..8a1b06b Binary files /dev/null and b/sfx/alphabet/k.wav differ diff --git a/sfx/alphabet/l.wav b/sfx/alphabet/l.wav new file mode 100644 index 0000000..1c05f5d Binary files /dev/null and b/sfx/alphabet/l.wav differ diff --git a/sfx/alphabet/m.wav b/sfx/alphabet/m.wav new file mode 100644 index 0000000..254da4c Binary files /dev/null and b/sfx/alphabet/m.wav differ diff --git a/sfx/alphabet/n.wav b/sfx/alphabet/n.wav new file mode 100644 index 0000000..03fd198 Binary files /dev/null and b/sfx/alphabet/n.wav differ diff --git a/sfx/alphabet/o.wav b/sfx/alphabet/o.wav new file mode 100644 index 0000000..7eb95e0 Binary files /dev/null and b/sfx/alphabet/o.wav differ diff --git a/sfx/alphabet/p.wav b/sfx/alphabet/p.wav new file mode 100644 index 0000000..0de0f49 Binary files /dev/null and b/sfx/alphabet/p.wav differ diff --git a/sfx/alphabet/q.wav b/sfx/alphabet/q.wav new file mode 100644 index 0000000..62a9a17 Binary files /dev/null and b/sfx/alphabet/q.wav differ diff --git a/sfx/alphabet/r.wav b/sfx/alphabet/r.wav new file mode 100644 index 0000000..04a464b Binary files /dev/null and b/sfx/alphabet/r.wav differ diff --git a/sfx/alphabet/s.wav b/sfx/alphabet/s.wav new file mode 100644 index 0000000..96723a8 Binary files /dev/null and b/sfx/alphabet/s.wav differ diff --git a/sfx/alphabet/t.wav b/sfx/alphabet/t.wav new file mode 100644 index 0000000..5678d72 Binary files /dev/null and b/sfx/alphabet/t.wav differ diff --git a/sfx/alphabet/u.wav b/sfx/alphabet/u.wav new file mode 100644 index 0000000..cad5872 Binary files /dev/null and b/sfx/alphabet/u.wav differ diff --git a/sfx/alphabet/v.wav b/sfx/alphabet/v.wav new file mode 100644 index 0000000..a4dcea4 Binary files /dev/null and b/sfx/alphabet/v.wav differ diff --git a/sfx/alphabet/w.wav b/sfx/alphabet/w.wav new file mode 100644 index 0000000..ccf4a0e Binary files /dev/null and b/sfx/alphabet/w.wav differ diff --git a/sfx/alphabet/x.wav b/sfx/alphabet/x.wav new file mode 100644 index 0000000..61275ce Binary files /dev/null and b/sfx/alphabet/x.wav differ diff --git a/sfx/alphabet/y.wav b/sfx/alphabet/y.wav new file mode 100644 index 0000000..4c43f60 Binary files /dev/null and b/sfx/alphabet/y.wav differ diff --git a/sfx/alphabet/z.wav b/sfx/alphabet/z.wav new file mode 100644 index 0000000..295fc5f Binary files /dev/null and b/sfx/alphabet/z.wav differ