From b8f8ce5425409d95230d33d52ff52f54a57c2f8b Mon Sep 17 00:00:00 2001 From: xharris Date: Sun, 27 Sep 2020 11:02:40 -0400 Subject: [PATCH] lots of ecs bugfixes, adds Effect class --- blankejs.code-workspace | 6 - love2d/lua/blanke/init.lua | 10 +- love2d/lua/ecs/clasp.lua | 7 + love2d/lua/ecs/core.lua | 1603 +++++++++++++++ love2d/lua/{blanke => ecs}/ecs.lua | 183 +- love2d/lua/ecs/init.lua | 254 +-- love2d/lua/ecs/json.lua | 401 ++++ love2d/lua/ecs/plugins/cmpl/init.lua | 65 - .../lua/ecs/plugins/cmpl/modules/bound2.lua | 175 -- .../lua/ecs/plugins/cmpl/modules/bound3.lua | 175 -- love2d/lua/ecs/plugins/cmpl/modules/bvh.lua | 507 ----- love2d/lua/ecs/plugins/cmpl/modules/color.lua | 385 ---- .../ecs/plugins/cmpl/modules/constants.lua | 20 - .../ecs/plugins/cmpl/modules/intersect.lua | 709 ------- love2d/lua/ecs/plugins/cmpl/modules/mat4.lua | 861 -------- love2d/lua/ecs/plugins/cmpl/modules/mesh.lua | 51 - .../lua/ecs/plugins/cmpl/modules/octree.lua | 634 ------ love2d/lua/ecs/plugins/cmpl/modules/quat.lua | 473 ----- .../lua/ecs/plugins/cmpl/modules/simplex.lua | 349 ---- love2d/lua/ecs/plugins/cmpl/modules/utils.lua | 210 -- love2d/lua/ecs/plugins/cmpl/modules/vec2.lua | 389 ---- love2d/lua/ecs/plugins/cmpl/modules/vec3.lua | 369 ---- love2d/lua/ecs/plugins/xhh-array/init.lua | 193 -- love2d/lua/ecs/plugins/xhh-badword/init.lua | 83 - love2d/lua/ecs/plugins/xhh-effect/init.lua | 135 -- love2d/lua/ecs/plugins/xhh-vector/init.lua | 96 - love2d/lua/ecs/print_r.lua | 29 + love2d/lua/ecs/system.lua | 552 ------ love2d/lua/ecs/systems.lua | 548 ++++++ love2d/lua/ecs/systems/animation.lua | 35 - love2d/lua/ecs/systems/canvas.lua | 71 - love2d/lua/ecs/systems/effect.lua | 306 --- love2d/lua/ecs/systems/image.lua | 169 -- love2d/lua/ecs/systems/init.lua | 111 -- love2d/lua/ecs/systems/movement.lua | 25 - love2d/lua/ecs/systems/timer.lua | 64 - love2d/lua/ecs/util.lua | 1730 ++++++----------- love2d/lua/ecs/uuid.lua | 205 ++ 38 files changed, 3585 insertions(+), 8603 deletions(-) create mode 100644 love2d/lua/ecs/clasp.lua create mode 100644 love2d/lua/ecs/core.lua rename love2d/lua/{blanke => ecs}/ecs.lua (51%) create mode 100644 love2d/lua/ecs/json.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/init.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/bound2.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/bound3.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/bvh.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/color.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/constants.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/intersect.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/mat4.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/mesh.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/octree.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/quat.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/simplex.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/utils.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/vec2.lua delete mode 100644 love2d/lua/ecs/plugins/cmpl/modules/vec3.lua delete mode 100644 love2d/lua/ecs/plugins/xhh-array/init.lua delete mode 100644 love2d/lua/ecs/plugins/xhh-badword/init.lua delete mode 100644 love2d/lua/ecs/plugins/xhh-effect/init.lua delete mode 100644 love2d/lua/ecs/plugins/xhh-vector/init.lua create mode 100644 love2d/lua/ecs/print_r.lua delete mode 100644 love2d/lua/ecs/system.lua create mode 100644 love2d/lua/ecs/systems.lua delete mode 100644 love2d/lua/ecs/systems/animation.lua delete mode 100644 love2d/lua/ecs/systems/canvas.lua delete mode 100644 love2d/lua/ecs/systems/effect.lua delete mode 100644 love2d/lua/ecs/systems/image.lua delete mode 100644 love2d/lua/ecs/systems/init.lua delete mode 100644 love2d/lua/ecs/systems/movement.lua delete mode 100644 love2d/lua/ecs/systems/timer.lua create mode 100644 love2d/lua/ecs/uuid.lua diff --git a/blankejs.code-workspace b/blankejs.code-workspace index b6c07a76..ab1215ba 100644 --- a/blankejs.code-workspace +++ b/blankejs.code-workspace @@ -2,12 +2,6 @@ "folders": [ { "path": "." - }, - { - "path": "/Users/xharris/Documents/blankejs-tutorials" - }, - { - "path": "D:/Documents/PROJECTS/blankejs-tutorials" } ], "settings": { diff --git a/love2d/lua/blanke/init.lua b/love2d/lua/blanke/init.lua index 9f7f4f22..84f04e6e 100644 --- a/love2d/lua/blanke/init.lua +++ b/love2d/lua/blanke/init.lua @@ -3679,7 +3679,7 @@ do end end; -- ignore collisions - teleport = function(obj, x, y) + teleport = function(obj) if obj and not obj.destroyed and obj.hasHitbox then local hb, offx, offy = checkHitArea(obj) world:update(obj, @@ -3702,12 +3702,12 @@ do local filter_result local filter = function(_obj, other) local ret = _obj.reaction or Hitbox.default_reaction - if _obj.reactions and _obj.reactions[other.tag] then ret = _obj.reactions[other.tag] else - if _obj.reaction then ret = _obj.reaction end - end if other.reactions and other.reactions[_obj.tag] then ret = other.reactions[_obj.tag] else if other.reaction then ret = other.reaction end end + if _obj.reactions and _obj.reactions[other.tag] then ret = _obj.reactions[other.tag] else + if _obj.reaction then ret = _obj.reaction end + end if _obj.filter then ret = _obj:filter(other) end filter_result = ret @@ -5143,8 +5143,6 @@ do } end -Signal.emit('__main') - love.load = function() if do_profiling and do_profiling > 0 then love.profiler = blanke_require('profile') diff --git a/love2d/lua/ecs/clasp.lua b/love2d/lua/ecs/clasp.lua new file mode 100644 index 00000000..0d40b764 --- /dev/null +++ b/love2d/lua/ecs/clasp.lua @@ -0,0 +1,7 @@ +-- evolbug 2017, MIT License +-- clasp - class library + +local class = { init = function()end; extend = function(self, proto) local meta = {} + local proto = setmetatable(proto or {},{__index=self, __call=function(_,...) local o=setmetatable({},meta) return o,o:init(...) end}) + meta.__index = proto ; for k,v in pairs(proto.__ or {}) do meta['__'..k]=v end ; return proto end } +return setmetatable(class, { __call = class.extend }) diff --git a/love2d/lua/ecs/core.lua b/love2d/lua/ecs/core.lua new file mode 100644 index 00000000..a854c855 --- /dev/null +++ b/love2d/lua/ecs/core.lua @@ -0,0 +1,1603 @@ +--FS +FS = nil +do + local lfs = love.filesystem + FS = { + basename = function (str) + return string.gsub(str, "(.*/)(.*)", "%2") + end, + dirname = function (str) + if string.match(str,".-/.-") then return string.gsub(str, "(.*/)(.*)", "%1") else return '' end + end, + extname = function (str) + str = string.match(str,"^.+(%..+)$") + if str then return string.sub(str,2) end + end, + removeExt = function (str) + return string.gsub(str, '.'..FS.extname(str), '') + end, + ls = function (path) + return lfs.getDirectoryItems(path) + end, + info = function (path) + if Window.os == 'web' then + local info = { + type = 'other', + size = lfs.getSize(path), + modtime = lfs.getLastModified(path) + } + if lfs.isFile(path) then info.type = "file" + elseif lfs.isDirectory(path) then info.type = "directory" + elseif lfs.isSymlink(path) then info.type = "symlink" end + return info + else + return lfs.getInfo(path) + end + end, + -- (str, num, ['data'/'string']) -> contents, size + open = function(path, mode) + return love.filesystem.newFile(path, mode) + end, + openURL = function(path) + return love.system.openURL(path) + end + } +end + +--SAVE +Save = nil +do + local f_save + local _load = function() + if not f_save then f_save = FS.open('save.json') end + f_save:open('r') + -- Save.data = f_save:read() + local data, size = f_save:read() + if data and size > 0 then + Save.data = json.decode(data) + end + f_save:close() + end + + Save = { + data = {}, + dir = function() return love.filesystem.getSaveDirectory() end, + update = function(new_data) + if new_data then table.update(Save.data, new_data) end + Save.save() + end, + remove = function(...) + local path = {...} + local data = Save.data + for i, p in ipairs(path) do + if type(data) == 'table' then + if i == #path then + data[p] = nil + else + data = data[p] + end + end + end + end, + load = function() + _load() + if not Save.data then Save.data = {} end + end, + save = function() + if f_save and table.len(Save.data) > 0 then + f_save:open('w') + f_save:write(json.encode(Save.data or {})) + f_save:close() + end + end + } +end + +--SIGNAL +Signal = nil +do + local function ends_with(str, ending) + return ending == "" or str:sub(-#ending) == ending + end + local fns = {} + + Signal = { + emit = function(event, ...) + local args = {...} + local big_ret = {} + if fns[event] then + iterate(fns[event], function(fn, i) + local ret = fn(unpack(args)) + if ret then table.insert(big_ret, ret) end + return ret == true + end) + end + return big_ret + end, + on = function(event, fn) + if not fns[event] then fns[event] = {} end + table.insert(fns[event], fn) + return fn + end, + off = function(event, fn) + if fns[event] then + iterate(fns[event], function(_fn) + return fn == _fn + end) + end + end + } +end +--SAVE +Save = nil +do + local f_save + local _load = function() + if not f_save then f_save = FS.open('save.json') end + f_save:open('r') + -- Save.data = f_save:read() + local data, size = f_save:read() + if data and size > 0 then + Save.data = json.decode(data) + end + f_save:close() + end + + Save = { + data = {}, + dir = function() return love.filesystem.getSaveDirectory() end, + update = function(new_data) + if new_data then table.update(Save.data, new_data) end + Save.save() + end, + remove = function(...) + local path = {...} + local data = Save.data + for i, p in ipairs(path) do + if type(data) == 'table' then + if i == #path then + data[p] = nil + else + data = data[p] + end + end + end + end, + load = function() + _load() + if not Save.data then Save.data = {} end + end, + save = function() + if f_save and table.len(Save.data) > 0 then + f_save:open('w') + f_save:write(json.encode(Save.data or {})) + f_save:close() + end + end + } +end + +--SIGNAL +Signal = nil +do + local function ends_with(str, ending) + return ending == "" or str:sub(-#ending) == ending + end + local fns = {} + + Signal = { + emit = function(event, ...) + local args = {...} + local big_ret = {} + if fns[event] then + iterate(fns[event], function(fn, i) + local ret = fn(unpack(args)) + if ret then table.insert(big_ret, ret) end + return ret == true + end) + end + return big_ret + end, + on = function(event, fn) + if not fns[event] then fns[event] = {} end + table.insert(fns[event], fn) + return fn + end, + off = function(event, fn) + if fns[event] then + iterate(fns[event], function(_fn) + return fn == _fn + end) + end + end + } +end + +--INPUT +Input = nil +mouse_x, mouse_y = 0, 0 +do + local name_to_input = {} -- name -> { key1: t/f, mouse1: t/f } + local input_to_name = {} -- key -> { name1, name2, ... } + local options = { + no_repeat = {}, + combo = {} + } + local groups = {} + local pressed = {} + local released = {} + local store = {} + local key_assoc = { + lalt='alt', ralt='alt', + ['return']='enter', kpenter='enter', + lgui='gui', rgui='gui' + } + + local joycheck = function(info) + if not info or not info.joystick then return info end + if Joystick.using == 0 then return info end + if Joystick.get(Joystick.using):getID() == info.joystick:getID() then return info end + end + + local isPressed = function(name) + if Input.group then + name = Input.group .. '.' .. name + end + if not (table.hasValue(options.no_repeat, name) and pressed[name] and pressed[name].count > 1) and joycheck(pressed[name]) then + return pressed[name] + end + end + + local isReleased = function(name) + if Input.group then + name = Input.group .. '.' .. name + end + if joycheck(released[name]) then + return released[name] + end + end + + Input = callable { + __call = function(self, name) + return store[name] or pressed[name] or released[name] + end; + + group = nil; + + store = function(name, value) + store[name] = value + end; + + set = function(inputs, _options) + _options = _options or {} + for name, inputs in pairs(inputs) do + Input.setInput(name, inputs, _options.group) + end + + if _options.combo then + table.append(options.combo, _options.combo or {}) + end + if _options.no_repeat then + table.append(options.no_repeat, _options.no_repeat or {}) + end + + return nil + end; + + setInput = function(name, inputs, group) + if group then name = group .. '.' .. name end + local input_group_str = name + name_to_input[name] = {} + for _,i in ipairs(inputs) do name_to_input[name][i] = false end + for _,i in ipairs(inputs) do + if not input_to_name[i] then input_to_name[i] = {} end + if not table.hasValue(input_to_name[i], name) then table.insert(input_to_name[i], name) end + end + end; + + pressed = function(...) + local ret = {} + local args = {...} + local val + for _, name in ipairs(args) do + val = isPressed(name) + if val then table.insert(ret, val) end + end + if #ret > 0 then return ret end + end; + + released = function(...) + local ret = {} + local args = {...} + local val + for _, name in ipairs(args) do + val = isReleased(name) + if val then table.insert(ret, val) end + end + if #ret > 0 then return ret end + end; + + press = function(key, extra) + if key_assoc[key] then Input.press(key_assoc[key], extra) end + if input_to_name[key] then + for _,name in ipairs(input_to_name[key]) do + local n2i = name_to_input[name] + if not n2i then name_to_input[name] = {} end + n2i = name_to_input[name] + n2i[key] = true + -- is input pressed now? + combo = table.hasValue(options.combo, name) + if (combo and table.every(n2i)) or (not combo and table.some(n2i)) then + pressed[name] = extra + pressed[name].count = 1 + end + end + end + end; + + release = function(key, extra) + if key_assoc[key] then Input.release(key_assoc[key], extra) end + if input_to_name[key] then + for _,name in ipairs(input_to_name[key]) do + local n2i = name_to_input[name] + if not n2i then name_to_input[name] = {} end + n2i = name_to_input[name] + n2i[key] = false + -- is input released now? + combo = table.hasValue(options.combo, name) + if pressed[name] and (combo or not table.some(n2i)) then + pressed[name] = nil + released[name] = extra + end + end + end + end; + + keyCheck = function() + for name, info in pairs(pressed) do + info.count = info.count + 1 + end + released = {} + store['wheel'] = nil + end; + + -- mousePos = function() return love.mouse.getPosition() end; + } +end + +--JOYSTICK +Joystick = nil +local refreshJoystickList +do + local joysticks = {} + refreshJoystickList = function() + joysticks = love.joystick.getJoysticks() + end + + Joystick = { + using = 0, + get = function(i) + if i > 0 and i < #joysticks then + return joysticks[i] + end + end, + -- affects all future Input() gamepad checks + use = function(i) + Joystick.using = i or 0 + end + } +end + +--DRAW +Draw = nil +do + local clamp = Math.clamp + local hex2rgb = function(hex) + assert(type(hex) == "string", "hex2rgb: expected string, got "..type(hex).." ("..hex..")") + hex = hex:gsub("#","") + if(string.len(hex) == 3) then + return {tonumber("0x"..hex:sub(1,1)) * 17 / 255, tonumber("0x"..hex:sub(2,2)) * 17 / 255, tonumber("0x"..hex:sub(3,3)) * 17 / 255} + elseif(string.len(hex) == 6) then + return {tonumber("0x"..hex:sub(1,2)) / 255, tonumber("0x"..hex:sub(3,4)) / 255, tonumber("0x"..hex:sub(5,6)) / 255} + end + end + + local fonts = {} -- { 'path+size': [ Font, Text ] } + + local getFont = function(path, size) + size = size or 12 + local key = path..'+'..size + if fonts[key] then return fonts[key] end + + local fnt = love.graphics.newFont(Game.res('font',path), size) + local txt = love.graphics.newText(fnt) + + assert(fnt, 'Font not found: \''..path..'\'') + + local font = { fnt, txt } + fonts[key] = font + return font + end + + local setText = function(text, limit, align) + if not Draw.text then return false end + if not text then text = "|" end + if limit or align then + Draw.text:setf(text or '', limit or Game.width, align or 'left') + else + Draw.text:set(text or '') + end + return true + end + + local DEF_FONT = "04B_03.ttf" + local last_font + + Draw = callable { + crop_used = false; + font = nil; + text = nil; + __call = function(self, instructions) + for _,instr in ipairs(instructions) do + name, args = instr[1], table.slice(instr,2) + assert(Draw[name], "bad draw instruction '"..name.."'") + local good, err = pcall(Draw[name], unpack(args)) + if not good then + error("Error: Draw."..name.."("..tbl_to_str(args)..")\n"..err, 3) + end + end + end; + setFont = function(path, size) + path = path or last_font or DEF_FONT + last_font = path + local info = getFont(path, size) + + Draw.font = info[1] + Draw.text = info[2] + + love.graphics.setFont(Draw.font) + + end; + setFontSize = function(size) + Draw.setFont(last_font, size) + end; + textWidth = function(...) + if setText(...) then + return Draw.text:getWidth() + end + end; + textHeight = function(...) + if setText(...) then + return Draw.text:getHeight() + end + end; + textSize = function(...) + if setText(...) then + return Draw.text:getDimensions() + end + end; + addImageFont = function(path, glyphs, ...) + path = Game.res('image', path) + if fonts[path] then return fonts[path] end + local font = love.graphics.newImageFont(path, glphs, ...) + fonts[path] = font + return font + end; + setImageFont = function(path) + path = Game.res('image', path) + local font = fonts[path] + assert(font, "ImageFont not found: \'"..path.."\'") + love.graphics.setFont(font) + end; + print = function(txt,x,y,limit,align,r,...) + if setText(txt,limit,align) then + x = x or 0 + y = y or 0 + love.graphics.draw(Draw.text, x, y,r or 0,...) + end + end; + parseColor = memoize(function(...) + local r, g, b, a = ... + if r == nil or r == true then + -- no color given + r, g, b, a = 1, 1, 1, 1 + return r, g, b, a + end + if type(r) == "table" then + r, g, b, a = r[1], r[2], r[3], r[4] + end + local c = Color[r] + if c then + -- color string + r, g, b, a = c[1], c[2], c[3], g + elseif type(r) == "string" and r:starts("#") then + -- hex string + r, g, b = unpack(hex2rgb(r)) + end + + if not a then a = 1 end + -- convert and clamp to [0,1] + if r > 1 then r = clamp(floor(r) / 255, 0, 1) end + if g > 1 then g = clamp(floor(g) / 255, 0, 1) end + if b > 1 then b = clamp(floor(b) / 255, 0, 1) end + if a > 1 then a = clamp(floor(a) / 255, 0, 1) end + + return r, g, b, a + end); + color = function(...) + return love.graphics.setColor(Draw.parseColor(...)) + end; + getBlendMode = function() return love.graphics.getBlendMode() end; + setBlendMode = function(...) love.graphics.setBlendMode(...) end; + crop = function(x,y,w,h) + love.graphics.setScissor(x,y,w,h) + -- stencilFn = () -> Draw.rect('fill',x,y,w,h) + -- love.graphics.stencil(stencilFn,"replace",1) + -- love.graphics.setStencilTest("greater",0) + -- Draw.crop_used = true + end; + rotate = function(r) + love.graphics.rotate(r) + end; + translate = function(x,y) + love.graphics.translate(floor(x), floor(y)) + end; + reset = function(only) + local lg = love.graphics + if only == 'color' or not only then + lg.setColor(1,1,1,1) + lg.setLineWidth(1) + end + if only == 'transform' or not only then + lg.origin() + end + if (only == 'crop' or not only) and Draw.crop_used then + Draw.crop_used = false + lg.setScissor() + -- lg.setStencilTest() + end + end; + push = function() love.graphics.push('all') end; + pop = function() + Draw.reset('crop') + love.graphics.pop() + end; + stack = function(fn) + local lg = love.graphics + lg.push('all') + fn() + lg.pop() + end; + newTransform = function() + return love.math.newTransform() + end; + clear = function(...) + love.graphics.clear(Draw.parseColor(...)) + end + } + + local draw_functions = { + 'arc','circle','ellipse','line','points','polygon','rectangle',--'print','printf', + 'discard','origin', + 'scale','shear','transformPoint', + 'setLineWidth','setLineJoin','setPointSize', + 'applyTransform', 'replaceTransform' + } + local draw_aliases = { + polygon = 'poly', + rectangle = 'rect', + setLineWidth = 'lineWidth', + setLineJoin = 'lineJoin', + setPointSize = 'pointSize', + points = 'point', + setFont = 'font', + setFontSize = 'fontSize' + } + for _,fn in ipairs(draw_functions) do + Draw[fn] = function(...) + return love.graphics[fn](...) + end + end + for old, new in pairs(draw_aliases) do + Draw[new] = Draw[old] + end +end +--COLOR +Color = { + red = {244,67,54}, + pink = {240,98,146}, + purple = {156,39,176}, + deeppurple = {103,58,183}, + indigo = {63,81,181}, + blue = {33,150,243}, + lightblue = {3,169,244}, + cyan = {0,188,212}, + teal = {0,150,136}, + green = {76,175,80}, + lightgreen = {139,195,74}, + lime = {205,220,57}, + yellow = {255,235,59}, + amber = {255,193,7}, + orange = {255,152,0}, + deeporange = {255,87,34}, + brown = {121,85,72}, + grey = {158,158,158}, + gray = {158,158,158}, + bluegray = {96,125,139}, + white = {255,255,255}, + white2 = {250,250,250}, + black = {0,0,0}, + black2 = {33,33,33}, + transparent ={255,255,255,0} +} + +--AUDIO +Audio = nil +Source = nil +do + local default_opt = { + type = 'static' + } + local defaults = {} + local sources = {} + local play_queue = {} + local first_update = true + + local opt = function(name, overrides) + if not defaults[name] then Audio(name, {}) end + return defaults[name] + end + Source = class { + init = function(self, name, options) + self.name = name + local o = opt(name) + if options then o = table.update(o, options) end + + if Window.os == 'web' then o.type = 'static' end + + self.src = Cache.get('Audio.source',name,function(key) + return love.audio.newSource(Game.res('audio',o.file), o.type) + end):clone() + + if not sources[name] then sources[name] = {} end + + if o then + table.insert(sources[name], self) + local props = {'position','looping','volume','airAbsorption','pitch','relative','rolloff','effect','filter'} + local t_props = {'attenuationDistances','cone','direction','velocity','volumeLimits'} + for _,n in ipairs(props) do + + local fn_name = n:capitalize() + -- setter + if not self['set'..fn_name] then + self['set'..fn_name] = function(self, ...) + return self.src['set'..fn_name](self.src, ...) + end + end + -- getter + if not self['get'..fn_name] then + self['get'..fn_name] = function(self, ...) + return self.src['get'..fn_name](self.src, ...) + end + end + + if o[n] then self['set'..fn_name](self,o[n]) end + end + for _,n in ipairs(t_props) do + + local fn_name = n:capitalize() + -- setter + if not self['set'..fn_name] then + self['set'..fn_name] = function(self, ...) + local args = {...} + + if fn == "position" then + for i, v in ipairs(args) do + args[i] = v / Audio.hearing + end + end + + return self.src['set'..fn_name](self.src, unpack(args)) + end + end + -- getter + if not self['get'..fn_name] then + self['get'..fn_name] = function(self, ...) + return self.src['get'..fn_name](self.src, ...) + end + end + + if o[n] then self['set'..fn_name](self,unpack(o[n])) end + end + end + end; + setPosition = function(self, opt) + self.position = opt or self.position + if opt then + self.src:setPosition( + (opt.x or 0) / Audio._hearing, + (opt.y or 0) / Audio._hearing, + (opt.z or 0) / Audio._hearing + ) + end + end; + play = function(self) + love.audio.play(self.src) + end; + stop = function(self) + love.audio.stop(self.src) + end; + isPlaying = function(self) + return self.src:isPlaying() + end + } + Audio = callable { + __call = function(self, file, ...) + option_list = {...} + for _,options in ipairs(option_list) do + store_name = options.name or file + options.file = file + if not defaults[store_name] then defaults[store_name] = {} end + new_tbl = copy(default_opt) + table.update(new_tbl, options) + table.update(defaults[store_name], new_tbl) + + Audio.source(store_name) + end + end; + + _hearing = 6; + + hearing = function(h) + Audio._hearing = h or Audio._hearing + for name, src_list in pairs(sources) do + for _, src in ipairs(src_list) do + src:setPosition() + end + end + end; + + update = function(dt) + if #play_queue > 0 then + for _, src in ipairs(play_queue) do + src:play() + end + play_queue = {} + end + end; + + source = function(name, options) + return Source(name, options) + end; + + play = function(name, options) + local new_src = Audio.source(name, options) + table.insert(play_queue, new_src) + return new_src + end; + stop = function(...) + names = {...} + if #names == 0 then love.audio.stop() + else + for _,n in ipairs(names) do + if sources[n] then + for _,src in ipairs(sources[n]) do src:stop() end + end + end + end + end; + isPlaying = function(name) + if sources[name] then + local t = {} + for _,src in ipairs(sources[name]) do + if src:isPlaying() then return true end + end + end + return false + end; + } + + local audio_fns = {'volume','velocity','position','orientation','effect','dopplerScale'} + for _, fn in ipairs(audio_fns) do + local fn_capital = fn:capitalize() + Audio[fn] = function(...) + local args = {...} + + if fn == "position" then + local pos = args[1] + + args = { + (pos.x or 0) / Audio._hearing, + (pos.y or 0) / Audio._hearing, + (pos.z or 0) / Audio._hearing + } + end + + if #args > 0 then + love.audio['set'..fn_capital](unpack(args)) + + else return love.audio['get'..fn_capital]() end + end + end +end + +--CAMERA +Camera = nil +do + local default_opt = { x=0, y=0, offset_x=0, offset_y=0, view_x=0, view_y=0, z=0, dx=0, dy=0, angle=0, zoom=nil, scalex=1, scaley=nil, top=0, left=0, width=nil, height=nil, follow=nil, enabled=true, auto_use=true } + local attach_count = 0 + local options = {} + local cam_stack = {} + + Camera = callable { + transform = nil; + __call = function(self, name, opt) + opt = opt or {} + default_opt.width = Game.width + default_opt.height = Game.height + options[name] = copy(default_opt) + options[name].transform = love.math.newTransform() + options[name].name = name + table.update(options[name], opt) + sort(options, 'z', 0) + return options[name] + end; + get = function(name) return assert(options[name], "Camera :'"..name.."' not found") end; + attach = function(name) + local o = Camera.get(name) + Draw.push() + if o.enabled == false then return end + if o then + local w, h = o.width or Game.width, o.height or Game.height + if o.follow then + o.x = o.follow.x or o.x + o.y = o.follow.y or o.y + end + local half_w, half_h = floor(w/2), floor(h/2) + + if o.crop then Draw.crop(o.view_x, o.view_y, w, h) end + o.transform:reset() + o.transform:translate(half_w + o.view_x, half_h + o.view_y) + o.transform:scale(o.zoom or o.scalex, o.zoom or o.scaley or o.scalex) + o.transform:rotate(o.angle) + o.transform:translate(-floor(o.x - o.left + o.dx), -floor(o.y - o.top + o.dy)) + + o.offset_x = -(floor(half_w) -floor(o.x - o.left + o.dx)) + o.offset_y = -(floor(half_h) -floor(o.y - o.top + o.dy)) + + Camera.transform = o.transform + love.graphics.replaceTransform(o.transform) + + table.insert(cam_stack, name) + end + end; + coords = function(name, x, y) + local o = Camera.get(name) + if o then + return x + (o.offset_x or 0), y + (o.offset_y or 0) + end + return x, y + end; + detach = function() + Draw.pop() + Camera.transform = nil + table.remove(cam_stack) + end; + use = function(name, fn) + Camera.attach(name) + fn() + Camera.detach() + end; + count = function() return table.len(options) end; + useAll = function(fn) + for name, opt in pairs(options) do + if opt.auto_use then + Camera.use(name, fn) + else + fn() + end + end + end; + } +end + +--STATE +State = nil +do + local states = {} + local stop_states = {} + local stateCB = function(name, fn_name, ...) + local state = states[name] + assert(state, "State '"..name.."' not found") + if state then + state.running = true + State.curr_state = name + if state.callbacks[fn_name] then state.callbacks[fn_name](...) end + State.curr_state = nil + return state + end + end + local stop_states, start_states + local stateStart = function(name) + local state = states[name] + assert(state, "State '"..name.."' not found") + if state and not state.running then + stateCB(name, 'enter') + end + end + local stateStop = function(name) + local state = states[name] + assert(state, "State '"..name.."' not found") + if state and state.running then + state = stateCB(name, 'leave') + local objs = state.objects + state.objects = {} + for _,obj in ipairs(objs) do + if obj then obj:destroy() end + end + Timer.stop(name) + state.running = false + end + end + State = class { + curr_state = nil; + init = function(self, name, cbs) + if states[name] then return nil end + self.name = name + self.callbacks = cbs + self.objects = {} + self.running = false + states[name] = self + end, + addObject = function(obj) + local state = states[State.curr_state] + if state then + table.insert(state.objects, obj) + end + end, + update = function(name, dt) + for name, state in pairs(states) do + if state.running then + stateCB(name, 'update', dt) + end + end + end, + draw = function() + for name, state in pairs(states) do + if state.running then + Draw.push() + stateCB(name, 'draw') + Draw.pop() + end + end + end, + start = function(name) + if name then + if not start_states then start_states = {} end + start_states[name] = true + end + end, + stop = function(name) + if stop_states == 'all' then return end + if not name then stop_states = 'all' else + if not stop_states then stop_states = {} end + stop_states[name] = true + end + end, + restart = function(name) + if name then + stateStop(name) + stateStart(name) + end + end, + _check = function() + if stop_states == 'all' then + for name,_ in pairs(states) do + stateStop(name) + end + elseif stop_states then + for name,_ in pairs(stop_states) do + stateStop(name) + end + end + if start_states then + for name,_ in pairs(start_states) do + stateStart(name) + end + end + stop_states = nil + start_states = nil + end + } +end + +--Time +Time = {} +do + local flr = Math.floor + Time = { + format = function(str, ms) + local s = flr(ms / 1000) % 60 + local m = flr(ms / (1000 * 60)) % 60 + local h = flr(ms / (1000 * 60 * 60)) % 24 + local d = flr(ms / (1000 * 60 * 60 * 24)) + + return str + :replace("%%d", (d)) + :replace("%%h", (h)) + :replace("%%m", (m)) + :replace("%%s", (s)) + end, + ms = function(opt) + local o = function(k) + if not opt then return 0 + else return opt[k] or 0 end + end + + return o('ms') + (o('sec') * 1000) + (o('min') * 60000) + (o('hr') * 3600000) + (o('day') * 86400000) + end + } +end + +--FEATURE +Feature = {} +do + local enabled = {} + Feature = callable { + -- returns true if feature is enabled + __call = function(self, name) + return enabled[name] ~= false + end, + disable = function(...) + local flist = {...} + for _, f in ipairs(flist) do + enabled[f] = false + end + end, + enable = function(...) + local flist = {...} + for _, f in ipairs(flist) do + enabled[f] = true + end + end + } +end + +--WINDOW +Window = {} +do + local pre_fs_size = {} + local last_win_size = {0,0} + local setMode = function(w,h,flags) + if not (not flags and last_win_size[1] == w and last_win_size[2] == h) then + love.window.setMode(w, h, flags or Game.options.window_flags) + end + end + Window = { + width = 1; + height = 1; + os = nil; + aspect_ratio = nil; + aspect_ratios = { {4,3}, {5,4}, {16,10}, {16,9} }; + resolutions = { 512, 640, 800, 1024, 1280, 1366, 1920 }; + aspectRatio = function() + local w, h = love.window.getDesktopDimensions() + for _,ratio in ipairs(Window.aspect_ratios) do + if w * (ratio[2] / ratio[1]) == h then + Window.aspect_ratio = ratio + return ratio + end + end + end; + vsync = function(v) + if not ge_version(11,3) then return end + if not v then return love.window.getVSync() + else love.window.setVSync(v) end + end; + setSize = function(r, flags) + local w, h = Window.calculateSize(r) + setMode(w,h,flags) + end; + setExactSize = function(w, h, flags) + setMode(w,h,flags) + end; + calculateSize = function(r) + r = r or Game.config.window_size + if not Window.aspect_ratio then Window.aspectRatio() end + local w = Window.resolutions[r] + local h = w / Window.aspect_ratio[1] * Window.aspect_ratio[2] + return w, h + end; + fullscreen = function(v,fs_type) + local res + if v == nil then + res = love.window.getFullscreen() + else + if not Window.fullscreen() then + pre_fs_size = {Game.width, Game.height} + end + res = love.window.setFullscreen(v,fs_type) + end + Game.updateWinSize(unpack(pre_fs_size)) + return res + end; + toggleFullscreen = function() + local res = Window.fullscreen(not Window.fullscreen()) + if res then + if not Window.fullscreen() then + Window.setExactSize(unpack(pre_fs_size)) + end + end + return res + end + } +end + +--GAME +Game = nil +do + Game = callable { + options = { + res = 'assets', + scripts = {}, + filter = 'linear', + vsync = 'on', + auto_require = true, + background_color = 'black', + window_flags = {}, + fps = 60, + round_pixels = false, + + auto_draw = true, + scale = true, + effect = nil, + + load = function() end, + draw = nil, + postdraw = nil, + update = function(dt) end, + }; + config = {}; + restarting = false; + width = 0; + height = 0; + time = 0; + love_version = {0,0,0}; + loaded = { + all = false, + settings = false, + scripts = false, + assets = false + }; + __call = function(_, args) + table.update(Game.options, args) + return Game + end; + + updateWinSize = function(w,h) + Window.width, Window.height, flags = love.window.getMode() + if w and h then Window.width, Window.height = w, h end + if not Window.width then Window.width = Game.width end + if not Window.height then Window.height = Game.height end + + if Window.os == 'web' then + Game.width, Game.height = Window.width, Window.height + end + if not Game.options.scale then + Game.width, Game.height = Window.width, Window.height + if Blanke.game_canvas then + Blanke.game_canvas.size = {Game.width, Game.height} + Blanke.game_canvas:resize() + end + + local canv + for c = 1, #CanvasStack.stack do + canv = CanvasStack.stack[c].value + canv.size = {Window.width, Window.height} + canv:resize() + end + end + end; + + load = function(which) + if Game.restarting then + Signal.emit("Game.restart") + end + + if not Game.loaded.settings and which == "settings" or not which then + Game.time = 0 + Game.love_version = {love.getVersion()} + love.joystick.loadGamepadMappings('gamecontrollerdb.txt') + + -- load config.json + local f_config = FS.open("config.json") + config_data = love.filesystem.read('config.json') + if config_data then Game.config = json.decode(config_data) end + table.update(Game.options, Game.config.export) + + -- get current os + if not Window.os then + Window.os = ({ ["OS X"]="mac", ["Windows"]="win", ["Linux"]="linux", ["Android"]="android", ["iOS"]="ios" })[love.system.getOS()]-- Game.options.os or 'ide' + Window.full_os = love.system.getOS() + end + -- load settings + if Window.os ~= 'web' then + Game.width, Game.height = Window.calculateSize(Game.config.game_size) -- game size + end + -- disable effects for web (SharedArrayBuffer or whatever) + if Window.os == 'web' then + Feature.disable('effect') + end + if not Game.loaded.settings then + if not Game.restarting then + -- window size and flags + Game.options.window_flags = table.update({ + borderless = Game.options.frameless, + resizable = Game.options.resizable, + }, Game.options.window_flags or {}) + + if Window.os ~= 'web' then + Window.setSize(Game.config.window_size) + end + Game.updateWinSize() + end + -- vsync + switch(Game.options.vsync, { + on = function() Window.vsync(1) end, + off = function() Window.vsync(0) end, + adaptive = function() Window.vsync(-1) end + }) + + if type(Game.options.filter) == 'table' then + love.graphics.setDefaultFilter(unpack(Game.options.filter)) + else + love.graphics.setDefaultFilter(Game.options.filter, Game.options.filter) + end + end + + Save.load() + end + + if not Game.loaded.assets and which == "assets" or not which then + Draw.setFont('04B_03.ttf', 16) + end + + if not Game.loaded.scripts and which == "scripts" or not which then + local scripts = Game.options.scripts or {} + local no_user_scripts = (#scripts == 0) + -- load plugins + if Game.options.plugins then + for _,f in ipairs(Game.options.plugins) do + package.loaded['plugins.'..f] = nil + require('plugins.'..f) + -- table.insert(scripts,'lua/plugins/'..f..'/init.lua') -- table.insert(scripts,'plugins.'..f) + end + end + -- load scripts + if Game.options.auto_require and no_user_scripts then + local load_folder + load_folder = function(path) + if path:starts("/.") then return end + files = FS.ls(path) + + local dirs = {} + + for _,f in ipairs(files) do + local file_path = path..'/'..f + if FS.extname(f) == 'lua' and not table.hasValue(scripts, file_path) then + table.insert(scripts, file_path) -- table.join(string.split(FS.removeExt(file_path), '/'),'.')) + end + local info = FS.info(file_path) + if info.type == 'directory' and file_path ~= '/dist' and file_path ~= '/lua' then + table.insert(dirs, file_path) + end + end + + -- load directories + for _, d in ipairs(dirs) do + load_folder(d) + end + end + load_folder('') + end + + for _,script in ipairs(scripts) do + if not script:contains('main.lua') and not script:contains('blanke/init.lua') then + local ok, chunk = pcall( love.filesystem.load, script ) + if not ok then error(chunk) end + assert(chunk, "Script not found: "..script) + local ok2, result = pcall( chunk ) + if not ok2 then error(result) end + -- require(script) + end + end + end + + if not Game.loaded.settings and which == "settings" or not which then + -- fullscreen toggle + Input.set({ _fs_toggle = { 'alt', 'enter' } }, { + combo = { '_fs_toggle' }, + no_repeat = { '_fs_toggle' }, + }) + if Game.options.fullscreen == true and not Game.restarting then + Window.fullscreen(true) + end + if Game.options.load then + Game.options.load() + end + -- round pixels + if not Game.options.round_pixels then + floor = function(x) return x end + end + + love.graphics.setBackgroundColor(1,1,1,0) + + Blanke.game_canvas = Canvas{draw=false} + + -- effect + if Game.options.effect then + Game.setEffect(unpack(Game.options.effect)) + end + end + + -- is everything loaded? + Game.loaded.all = true + for k, v in pairs(Game.loaded) do + if which == k or not which then + Game.loaded[k] = true + end + if k ~= 'all' and Game.loaded[k] == false then + Game.loaded.all = false + end + end + + if Game.loaded.all then + Signal.emit("Game.load") + + if Game.options.initial_state then + State.start(Game.options.initial_state) + end + end + + if Game.restarting then + Game.updateWinSize() + end + Signal.emit("Game.start") + end; + + restart = function() + State.stop() + Timer.stop() + Audio.stop() + for _, obj in ipairs(Game.all_objects) do + if obj then + obj:destroy() + end + end + objects = {} + Game.all_objects = {} + Game.updatables = {} + Game.drawables = {} + Game.loaded = { + all = false, + settings = false, + scripts = false, + assets = false + } + + Game.restarting = true + end; + + forced_quit = false; + quit = function(force, status) + Game.forced_quit = force + love.event.quit(status) + end; + + setEffect = function(...) + Add(Blanke.game_canvas, "effect", {...}) + end, + + res = function(_type, file) + if file:contains(Game.options.res.."/".._type) then + return file + end + return Game.options.res.."/".._type.."/"..file + end; + + setBackgroundColor = function(...) + --love.graphics.setBackgroundColor(Draw.parseColor(...)) + Game.options.background_color = {Draw.parseColor(...)} + end; + + update = function(dt) + local dt_ms = dt * 1000 + + Game.is_updating = true + if Game.will_sort then + Game.will_sort = nil + sort(Game.drawables, 'z', 0) + end + + mouse_x, mouse_y = love.mouse.getPosition() + if Game.options.scale == true then + local scalex, scaley = Window.width / Game.width, Window.height / Game.height + Blanke.scale = math.min(scalex, scaley) + Blanke.padx, Blanke.pady = 0, 0 + if scalex > scaley then + Blanke.padx = floor((Window.width - (Game.width * Blanke.scale)) / 2) + else + Blanke.pady = floor((Window.height - (Game.height * Blanke.scale)) / 2) + end + -- offset mouse coordinates + mouse_x = floor((mouse_x - Blanke.padx) / Blanke.scale) + mouse_y = floor((mouse_y - Blanke.pady) / Blanke.scale) + end + + Game.time = Game.time + dt + -- TODO: uncomment + --Physics.update(dt) + --Timer.update(dt, dt_ms) + if Game.options.update(dt) == true then return end + World.update(dt) + State.update(dt) + State._check() + Signal.emit('update',dt,dt_ms) + local key = Input.pressed('_fs_toggle') + if key and key[1].count == 1 then + Window.toggleFullscreen() + end + Input.keyCheck() + Audio.update(dt) + + --BFGround.update(dt) + + if Game.restarting then + Game.load() + Game.restarting = false + end + Game.is_updating = false + end + } +end + + +--BLANKE +Blanke = nil +do + local update_obj = Game.updateObject + local stack = Draw.stack + + local actual_draw = function() + World.draw() + State.draw() + if Game.options.postdraw then Game.options.postdraw() end + -- TODO: uncomment + --Physics.drawDebug() + --Hitbox.draw() + end + + local _drawGame = function() + Draw.push() + Draw.reset() + Draw.color(Game.options.background_color) + Draw.rect('fill',0,0,Game.width,Game.height) + Draw.pop() + + --Background.draw() + if Camera.count() > 0 then + Camera.useAll(actual_draw) + else + actual_draw() + end + --Foreground.draw() + end + + local _draw = function() + if Game.options.draw then + Game.options.draw(_drawGame) + else + _drawGame() + end + end + + Blanke = { + config = {}; + game_canvas = nil; + loaded = false; + scale = 1; + padx = 0; + pady = 0; + load = function() + if not Blanke.loaded then + Blanke.loaded = true + if not Game.loaded.all then + Game.load() + end + end + end; + iterDraw = function(t, override_drawable) + local reorder_drawables = iterateEntities(t, 'drawable', function(obj) + if obj.visible == true and obj.skip_draw ~= true and (override_drawable or obj.drawable == true) and obj.draw ~= false then + local obj_draw = obj._draw + stack(function() + if obj_draw then obj_draw(obj) end + end) + end + end) + end; + --blanke.update + update = function(dt) + Game.update(dt) + end; + --blanke.draw + draw = function() + if not Game.loaded.all then return end + Game.is_drawing = true + Draw.origin() + + Blanke.game_canvas:renderTo(_draw) + + Draw.push() + Draw.color('black') + Draw.rect('fill',0,0,Window.width,Window.height) + Draw.pop() + + if Game.options.scale == true then + Blanke.game_canvas.pos = { + Blanke.padx, + Blanke.pady + } + Blanke.game_canvas.scale = Blanke.scale + + Render(Blanke.game_canvas) + else + Render(Blanke.game_canvas) + end + Game.is_drawing = false + end; + resize = function(w, h) + Game.updateWinSize() + end; + keypressed = function(key, scancode, isrepeat) + Input.press(key, {scancode=scancode, isrepeat=isrepeat}) + end; + keyreleased = function(key, scancode) + Input.release(key, {scancode=scancode}) + end; + mousepressed = function(x, y, button, istouch, presses) + Input.press('mouse', {x=x, y=y, button=button, istouch=istouch, presses=presses}) + Input.press('mouse'..tostring(button), {x=x, y=y, button=button, istouch=istouch, presses=presses}) + end; + mousereleased = function(x, y, button, istouch, presses) + Input.press('mouse', {x=x, y=y, button=button, istouch=istouch, presses=presses}) + Input.release('mouse'..tostring(button), {x=x, y=y, button=button, istouch=istouch, presses=presses}) + end; + wheelmoved = function(x, y) + Input.store('wheel', {x=x,y=y}) + end; + gamepadpressed = function(joystick, button) + Input.press('gp.'..button, {joystick=joystick}) + end; + gamepadreleased = function(joystick, button) + Input.release('gp.'..button, {joystick=joystick}) + end; + joystickadded = function(joystick) + Signal.emit("joystickadded", joystick) + refreshJoystickList() + end; + joystickremoved = function(joystick) + Signal.emit("joystickremoved", joystick) + refreshJoystickList() + end; + gamepadaxis = function(joystick, axis, value) + Input.store('gp.'..axis, {joystick=joystick, value=value}) + end; + touchpressed = function(id, x, y, dx, dy, pressure) + Input.press('touch', {id=id, x=x, y=y, dx=dx, dy=dy, pressure=pressure}) + end; + touchreleased = function(id, x, y, dx, dy, pressure) + Input.release('touch', {id=id, x=x, y=y, dx=dx, dy=dy, pressure=pressure}) + end; + } +end \ No newline at end of file diff --git a/love2d/lua/blanke/ecs.lua b/love2d/lua/ecs/ecs.lua similarity index 51% rename from love2d/lua/blanke/ecs.lua rename to love2d/lua/ecs/ecs.lua index 400ff67f..f8bff6d5 100644 --- a/love2d/lua/blanke/ecs.lua +++ b/love2d/lua/ecs/ecs.lua @@ -7,17 +7,25 @@ local systems = {} local entity_templates = {} local spawn +local entity_order = {} + Entity = callable { __call = function(_, classname, props) - if props then + if props then + props.classname = props.classname or classname -- adding entity template entity_templates[classname] = props - return function(args) - local t = copy(props) - table.update(t, args) - return World.add(t) - end + return callable { + __call = function(_, ...) + local args = {...} + local t = copy(props) + if type(args[1]) == 'table' then + table.update(t, args[1]) + end + return World.add(t, unpack(args)) + end + } elseif type(classname) == "string" then -- spawn from entity template @@ -42,18 +50,22 @@ Entity = callable { System = callable { __call = function(_, query, opt) local cb = copy(opt) + local id = uuid() cb.order = nil table.insert(systems, { + uuid=id, query=query, order=opt.order, cb=cb, entities={}, changed={}, - removed={} + removed={}, + has_entity={} }) System.sort() + return id end, order = {}, sort = function() @@ -90,17 +102,27 @@ Test = function(query, obj, _not) end function Add(ent, k, v) - ent[k] = (v == nil and true or v) + local sys + if k then + ent[k] = (v == nil and true or v) + end for i = 1, table.len(systems) do - systems[i].changed[ent.uuid] = true + sys = systems[i] + if not sys.has_entity[ent.uuid] and Test(sys.query, ent) then + sys.has_entity[ent.uuid] = true + -- entity fits in this system + table.insert(sys.entities, ent.uuid) + if sys.cb.added then sys.cb.added(ent, v) end + end end end +local remove_prop = {} function Remove(ent, k) for i = 1, table.len(systems) do systems[i].changed[ent.uuid] = true end - ent[k] = nil + table.insert(remove_prop, {ent,k}) end function Destroy(ent) @@ -113,34 +135,107 @@ function Destroy(ent) end end +local z_sort = false +local check_z = function(ent) + if not ent.z then ent.z = 0 end + if ent._last_z ~= ent.z then + ent._last_z = ent.z + z_sort = true + end +end + + +function Render(ent, skip_tf) + if ent.drawable then + local lg = love.graphics + + lg.push('all') + lg.setColor(unpack(ent.color)) + lg.setBlendMode(unpack(ent.blendmode)) + + local draw = function() + if skip_tf then + lg.draw(ent.drawable) + elseif ent.quad then + lg.draw(ent.drawable, ent.quad, + ent.pos[1], ent.pos[2], ent.angle, + ent.scale * ent.scalex, + ent.scale * ent.scaley, + ent.align[1], ent.align[2], ent.shearx, ent.sheary + ) + else + lg.draw(ent.drawable, + ent.pos[1], ent.pos[2], ent.angle, + ent.scale * ent.scalex, + ent.scale * ent.scaley, + ent.align[1], ent.align[2], ent.shearx, ent.sheary + ) + end + end + + if ent.effect and ent.effect.classname == "Blanke.Effect" then + ent.effect:draw(draw) + else + draw() + end + + lg.pop() + end +end + +local draw_defaults = { + pos = { 0, 0 }, + size = { 0, 0 }, + angle = 0, + scale = 1, + scalex = 1, + scaley = 1, + shear = { 0, 0 }, + color = { 1, 1, 1, 1 }, + blendmode = { 'alpha' }, + align = { 0, 0 } +} + World = { - add = function(obj) table.insert(new_entities, obj) end, + add = function(ent, args) + -- add new entity + if not ent.uuid then + ent.uuid = uuid() + entities[ent.uuid] = ent + end + + table.defaults(ent, draw_defaults) + Add(ent) + + table.insert(entity_order, ent.uuid) + check_z(ent) + + return ent + end, remove = function(obj) table.insert(dead_entities, obj) end, update = function(dt) - local ent, sys - -- add new entities - for n = 1, table.len(new_entities) do - ent = new_entities[n] - if not ent.uuid then - ent.uuid = uuid() - entities[ent.uuid] = ent - end - + local ent, sys + + -- remove dead entities + for n = 1, table.len(dead_entities) do + ent = dead_entities[n] for s = 1, table.len(systems) do - sys = systems[s] - if Test(sys.query, ent) then - -- entity fits in this system - table.insert(sys.entities, ent.uuid) - if sys.cb.added then sys.cb.added(ent) end - end + sys.removed[ent.uuid] = true end end + if table.len(dead_entities) > 0 then + table.filter(entity_order, function(eid) + return eid ~= ent.uuid + end) + end + dead_entities = {} -- update systems for s = 1, table.len(systems) do sys = systems[s] local update, removed = sys.cb.update, sys.cb.removed if update then table.filter(sys.entities, function(eid) + ent = entities[eid] -- entity was removed from world if sys.removed[eid] then sys.removed[eid] = nil @@ -149,16 +244,19 @@ World = { -- entity property was changed elseif sys.changed[eid] then sys.changed[eid] = nil - if Test(sys.query, entities[eid]) then + if Test(sys.query, ent) then -- entity can stay + check_z(ent) return true else -- entity removed from system - if removed then removed(entities[eid]) end + if removed then removed(ent) end + sys.has_entity[eid] = nil return false end else - update(entities[eid], dt) + update(ent, dt) + check_z(ent) end return true end) @@ -171,6 +269,31 @@ World = { sys.changed = {} end end - new_entities = {} + + for r = 1, #remove_prop do + remove_prop[1][remove_prop[2]] = nil + end + remove_prop = {} + + if z_sort then + table.sort(entity_order, function(a, b) + return entities[a].z < entities[b].z + end) + z_sort = false + end + end, + draw = function() + local sys, draw + for eid, ent in pairs(entities) do + if ent.draw ~= false then + sys = systems[ent.renderer] + if sys then + sys.cb.draw(ent) + elseif ent.drawable then + -- default renderer + Render(ent) + end + end + end end } \ No newline at end of file diff --git a/love2d/lua/ecs/init.lua b/love2d/lua/ecs/init.lua index 910d34c4..c1163493 100644 --- a/love2d/lua/ecs/init.lua +++ b/love2d/lua/ecs/init.lua @@ -1,193 +1,91 @@ -local _NAME = ... -require(_NAME..'.util') -require(_NAME..'.system') -require(_NAME..'.systems') +local blanke_require = function(r) + return require('ecs.'..r) +end -local do_profiling = nil -- false/# +uuid = blanke_require("uuid") +json = blanke_require("json") +class = blanke_require("clasp") +blanke_require("print_r") -local dt = 0 -local fixed_dt = 1/60 -local accumulator = 0 ---BLANKE @global -Blanke = { - load = function() - love.frame = 0 - if do_profiling then - love.profiler = require 'profile' - end - print_r(World.add(Game.options)) - Game.options.load() - Game.love_version = {love.getVersion()} - - World.add(Game) - if Game.options.initial_state then - State.start(Game.options.initial_state) - end - World.update(0) - Window.setSize() - end, - update = function(dt) - mouse_x, mouse_y = love.mouse.getPosition() - if Game.options.scale == true then - local scalex, scaley = Game.win_width / Game.width, Game.win_height / Game.height - local scale = math.min(scalex, scaley) - Blanke.padx, Blanke.pady = 0, 0 - if scalex > scaley then - Blanke.padx = floor((Game.win_width - (Game.width * scale)) / 2) - else - Blanke.pady = floor((Game.win_height - (Game.height * scale)) / 2) - end - -- offset mouse coordinates - mouse_x = floor((mouse_x - Blanke.padx) / scale) - mouse_y = floor((mouse_y - Blanke.pady) / scale) - Blanke.scale = { - x = scale, - y = scale, - } - end +blanke_require('util') +blanke_require('ecs') +blanke_require('core') +blanke_require('systems') - local update = function(_dt) - if do_profiling then - love.profiler.start() - end - World.update(_dt) - if do_profiling then - love.frame = love.frame + 1 - if love.frame > 60 and not love.report then - love.profiler.stop() - love.report = love.profiler.report(do_profiling) - print(love.report) - end - end - Timer.update(_dt) +local profiling_color = {1,0,0,1} - Game.time = Game.time + _dt - if Game.options.update(_dt) == true then return end - end +love.load = function() + if do_profiling and do_profiling > 0 then + love.profiler = blanke_require('profile') + love.profiler.start() + else + do_profiling = nil + end - if Game.options.fps then - fixed_dt = 1/Game.options.fps - else - fixed_dt = nil - end - if fixed_dt ~= nil then - accumulator = accumulator + dt - while accumulator >= fixed_dt do - update(fixed_dt) - accumulator = accumulator - fixed_dt - end - else - update(dt) - end + Blanke.load() +end - -- Physics.update(dt) - -- Timer.update(dt) - Signal.emit('update',dt) - local key = Input.pressed('_fs_toggle') - if key and key.count == 1 then - Window.toggleFullscreen() - end - Input.keyCheck() - Audio.update(dt) - reset_tracks() - end, - draw = function() - local draw_camera = function() - Draw{ - {'push'}, - {'color',Game.options.background_color}, - {'rect','fill',0,0,Game.width,Game.height}, - {'pop'} - } - -- if Camera.count() > 0 then - -- Camera.useAll(draw_world) - -- else - World.draw() - -- end - end - - local draw_game = function() - Game.options.draw(function() - -- if Game.effect then - -- Game.effect:draw(draw_camera) - -- else - draw_camera() - -- end - end) - end - - Draw.origin() - local game_canvas = Game.canvas - -- - --print_r(game_canvas) - game_canvas:drawTo(draw_game) - if Game.options.scale == true then - game_canvas.pos.x, game_canvas.pos.y = Blanke.padx, Blanke.pady - game_canvas.scale = Blanke.scale - end - game_canvas:draw() - - end, - resize = function(w,h) - Game.win_width, Game.win_height, flags = love.window.getMode() - if w and h then Game.win_width, Game.win_height = w, h end - if not Game.options.scale then - Game.width, Game.height = Game.win_width, Game.win_height - Game.canvas.size = {Game.width, Game.height} - end - end, - keypressed = function(key, scancode, isrepeat) - Input.press(key, {scancode=scancode, isrepeat=isrepeat}) - end, - keyreleased = function(key, scancode) - Input.release(key, {scancode=scancode}) - end, - mousepressed = function(x, y, button, istouch, presses) - Input.press('mouse', {x=x, y=y, button=button, istouch=istouch, presses=presses}) - Input.press('mouse'..tostring(button), {x=x, y=y, button=button, istouch=istouch, presses=presses}) - end, - mousereleased = function(x, y, button, istouch, presses) - Input.press('mouse', {x=x, y=y, button=button, istouch=istouch, presses=presses}) - Input.release('mouse'..tostring(button), {x=x, y=y, button=button, istouch=istouch, presses=presses}) - end; - gamepadpressed = function(joystick, button) - Input.press('gp.'..button, {joystick=joystick}) - end; - gamepadreleased = function(joystick, button) - Input.release('gp.'..button, {joystick=joystick}) - end; - joystickadded = function(joystick) - Signal.emit("joystickadded", joystick) - refreshJoystickList() - end; - joystickremoved = function(joystick) - Signal.emit("joystickremoved", joystick) - refreshJoystickList() - end; - gamepadaxis = function(joystick, axis, value) - Input.store('gp.'..axis, {joystick=joystick, value=value}) - end; - touchpressed = function(id, x, y, dx, dy, pressure) - Input.press('touch', {id=id, x=x, y=y, dx=dx, dy=dy, pressure=pressure}) - end; - touchreleased = function(id, x, y, dx, dy, pressure) - Input.release('touch', {id=id, x=x, y=y, dx=dx, dy=dy, pressure=pressure}) - end; -} +love.frame = 0 +local update = function(dt) + if do_profiling and do_profiling > 0 then + love.frame = love.frame + 1 + if love.frame % 100 == 0 then + love.report = love.profiler.report(do_profiling) + love.profiler.reset() + end + else + do_profiling = nil + end -Signal.emit('__main') -love.load = function() Blanke.load() end -love.update = function(dt) Blanke.update(dt) end -love.draw = function() Blanke.draw() end -love.resize = function(w, h) Blanke.resize() end + Blanke.update(dt) +end + +do + local dt = 0 + local accumulator = 0 + local fixed_dt + love.update = function(dt) + fixed_dt = Game.options.fps and 1/Game.options.fps or nil + if fixed_dt == nil then + update(dt) + else + accumulator = accumulator + dt + while accumulator >= fixed_dt do + update(fixed_dt) + accumulator = accumulator - fixed_dt + end + end + end +end +love.draw = function() + Blanke.draw() +end +love.resize = function(w, h) Blanke.resize(w, h) end love.keypressed = function(key, scancode, isrepeat) Blanke.keypressed(key, scancode, isrepeat) end love.keyreleased = function(key, scancode) Blanke.keyreleased(key, scancode) end love.mousepressed = function(x, y, button, istouch, presses) Blanke.mousepressed(x, y, button, istouch, presses) end love.mousereleased = function(x, y, button, istouch, presses) Blanke.mousereleased(x, y, button, istouch, presses) end -love.gamepadpressed = function(joystick, button) Blanke.gamepadpressed(joystick, button) end -love.gamepadreleased = function(joystick, button) Blanke.gamepadreleased(joystick, button) end -love.joystickadded = function(joystick) Blanke.joystickadded(joystick) end +love.wheelmoved = function(x, y) Blanke.wheelmoved(x, y) end +love.gamepadpressed = function(joystick, button) Blanke.gamepadpressed(joystick, button) end +love.gamepadreleased = function(joystick, button) Blanke.gamepadreleased(joystick, button) end +love.joystickadded = function(joystick) Blanke.joystickadded(joystick) end love.joystickremoved = function(joystick) Blanke.joystickremoved(joystick) end love.gamepadaxis = function(joystick, axis, value) Blanke.gamepadaxis(joystick, axis, value) end love.touchpressed = function(id, x, y, dx, dy, pressure) Blanke.touchpressed(id, x, y, dx, dy, pressure) end love.touchreleased = function(id, x, y, dx, dy, pressure) Blanke.touchreleased(id, x, y, dx, dy, pressure) end +love.quit = function() + Save.save() + local stop = false + if Game.forced_quit then return stop end + local abort = function() stop = true end + Signal.emit("Game.quit", abort) + + if not stop and do_profiling and love.report then + local f = FS.open('profile.txt', 'w') + f:write(love.report) + f:close() + FS.openURL("file://"..Save.dir().."/profile.txt") + end + + return stop +end \ No newline at end of file diff --git a/love2d/lua/ecs/json.lua b/love2d/lua/ecs/json.lua new file mode 100644 index 00000000..4c318fa8 --- /dev/null +++ b/love2d/lua/ecs/json.lua @@ -0,0 +1,401 @@ +-- +-- json.lua +-- +-- Copyright (c) 2019 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("json error: %s", str)) + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/love2d/lua/ecs/plugins/cmpl/init.lua b/love2d/lua/ecs/plugins/cmpl/init.lua deleted file mode 100644 index 28587920..00000000 --- a/love2d/lua/ecs/plugins/cmpl/init.lua +++ /dev/null @@ -1,65 +0,0 @@ ---[[ -------------------------------------------------------------------------------- --- @author Colby Klein --- @author Landon Manning --- @copyright 2016 --- @license MIT/X11 -------------------------------------------------------------------------------- - .'@@@@@@@@@@@@@@#: - ,@@@@#; .'@@@@+ - ,@@@' .@@@# - +@@+ .... .@@@ - ;@@; '@@@@@@@@@@@@. @@@ - @@# @@@@@@@@++@@@@@@@; `@@; - .@@` @@@@@# #@@@@@ @@@ - `@@ @@@@@` Cirno's `@@@@# +@@ - @@ `@@@@@ Perfect @@@@@ @@+ - @@+ ;@@@@+ Math +@@@@+ @@ - @@ `@@@@@ Library @@@@@@ #@' - `@@ @@@@@@ @@@@@@@ `@@ - :@@ #@@@@@@. .@@@@@@@@@ @@ - .@@ #@@@@@@@@@@@@;;@@@@@ @@ - @@ .;+@@#'. ;@@@@@ :@@ - @@` +@@@@+ @@. - ,@@ @@@@@ .@@ - @@# ;;;;;. `@@@@@ @@ - @@+ .@@@@@ @@@@@ @@` - #@@ '@@@@@#` ;@@@@@@ ;@@ - .@@' @@@@@@@@@@@@@@@ @@# - +@@' '@@@@@@@; @@@ - '@@@` '@@@ - #@@@; .@@@@: - :@@@@@@@++;;;+#@@@@@@+` - .;'+++++;. ---]] -local modules = (...) and (...):gsub('%.init$', '') .. ".modules." or "" - -local cpml = { - _LICENSE = "CPML is distributed under the terms of the MIT license. See LICENSE.md.", - _URL = "https://github.com/excessive/cpml", - _VERSION = "1.2.9", - _DESCRIPTION = "Cirno's Perfect Math Library: Just about everything you need for 3D games. Hopefully." -} - -local files = { - "bvh", - "color", - "constants", - "intersect", - "mat4", - "mesh", - "octree", - "quat", - "simplex", - "utils", - "vec2", - "vec3", - "bound2", - "bound3", -} - -for _, file in ipairs(files) do - cpml[file] = require(modules .. file) -end - -return cpml diff --git a/love2d/lua/ecs/plugins/cmpl/modules/bound2.lua b/love2d/lua/ecs/plugins/cmpl/modules/bound2.lua deleted file mode 100644 index f68a2ffc..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/bound2.lua +++ /dev/null @@ -1,175 +0,0 @@ ---- A 2 component bounding box. --- @module bound2 - -local modules = (...):gsub('%.[^%.]+$', '') .. "." -local vec2 = require(modules .. "vec2") - -local bound2 = {} -local bound2_mt = {} - --- Private constructor. -local function new(min, max) - return setmetatable({ - min=min, -- min: vec2, minimum value for each component - max=max, -- max: vec2, maximum value for each component - }, bound2_mt) -end - --- Do the check to see if JIT is enabled. If so use the optimized FFI structs. -local status, ffi -if type(jit) == "table" and jit.status() then - status, ffi = pcall(require, "ffi") - if status then - ffi.cdef "typedef struct { cpml_vec2 min, max; } cpml_bound2;" - new = ffi.typeof("cpml_bound2") - end -end - -bound2.zero = new(vec2.zero, vec2.zero) - ---- The public constructor. --- @param min Can be of two types:
--- vec2 min, minimum value for each component --- nil Create bound at single point 0,0 --- @tparam vec2 max, maximum value for each component --- @treturn bound2 out -function bound2.new(min, max) - if min and max then - return new(min:clone(), max:clone()) - elseif min or max then - error("Unexpected nil argument to bound2.new") - else - return new(vec2.zero, vec2.zero) - end -end - ---- Clone a bound. --- @tparam bound2 a bound to be cloned --- @treturn bound2 out -function bound2.clone(a) - return new(a.min, a.max) -end - ---- Construct a bound covering one or two points --- @tparam vec2 a Any vector --- @tparam vec2 b Any second vector (optional) --- @treturn vec2 Minimum bound containing the given points -function bound2.at(a, b) -- "bounded by". b may be nil - if b then - return bound2.new(a,b):check() - else - return bound2.zero:with_center(a) - end -end - ---- Get size of bounding box as a vector --- @tparam bound2 a bound --- @treturn vec2 Vector spanning min to max points -function bound2.size(a) - return a.max - a.min -end - ---- Resize bounding box from minimum corner --- @tparam bound2 a bound --- @tparam vec2 new size --- @treturn bound2 resized bound -function bound2.with_size(a, size) - return bound2.new(a.min, a.min + size) -end - ---- Get half-size of bounding box as a vector. A more correct term for this is probably "apothem" --- @tparam bound2 a bound --- @treturn vec2 Vector spanning center to max point -function bound2.radius(a) - return a:size()/2 -end - ---- Get center of bounding box --- @tparam bound2 a bound --- @treturn bound2 Point in center of bound -function bound2.center(a) - return (a.min + a.max)/2 -end - ---- Move bounding box to new center --- @tparam bound2 a bound --- @tparam vec2 new center --- @treturn bound2 Bound with same size as input but different center -function bound2.with_center(a, center) - return bound2.offset(a, center - a:center()) -end - ---- Resize bounding box from center --- @tparam bound2 a bound --- @tparam vec2 new size --- @treturn bound2 resized bound -function bound2.with_size_centered(a, size) - local center = a:center() - local rad = size/2 - return bound2.new(center - rad, center + rad) -end - ---- Convert possibly-invalid bounding box to valid one --- @tparam bound2 a bound --- @treturn bound2 bound with all components corrected for min-max property -function bound2.check(a) - if a.min.x > a.max.x or a.min.y > a.max.y then - return bound2.new(vec2.component_min(a.min, a.max), vec2.component_max(a.min, a.max)) - end - return a -end - ---- Shrink bounding box with fixed margin --- @tparam bound2 a bound --- @tparam vec2 a margin --- @treturn bound2 bound with margin subtracted from all edges. May not be valid, consider calling check() -function bound2.inset(a, v) - return bound2.new(a.min + v, a.max - v) -end - ---- Expand bounding box with fixed margin --- @tparam bound2 a bound --- @tparam vec2 a margin --- @treturn bound2 bound with margin added to all edges. May not be valid, consider calling check() -function bound2.outset(a, v) - return bound2.new(a.min - v, a.max + v) -end - ---- Offset bounding box --- @tparam bound2 a bound --- @tparam vec2 offset --- @treturn bound2 bound with same size, but position moved by offset -function bound2.offset(a, v) - return bound2.new(a.min + v, a.max + v) -end - ---- Test if point in bound --- @tparam bound2 a bound --- @tparam vec2 point to test --- @treturn boolean true if point in bounding box -function bound2.contains(a, v) - return a.min.x <= v.x and a.min.y <= v.y - and a.max.x >= v.x and a.max.y >= v.y -end - ---- Return a formatted string. --- @tparam bound2 a bound to be turned into a string --- @treturn string formatted -function bound2.to_string(a) - return string.format("(%s-%s)", a.min, a.max) -end - -bound2_mt.__index = bound2 -bound2_mt.__tostring = bound2.to_string - -function bound2_mt.__call(_, a, b) - return bound2.new(a, b) -end - -if status then - xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded - ffi.metatype(new, bound2_mt) - end, function() end) -end - -return setmetatable({}, bound2_mt) diff --git a/love2d/lua/ecs/plugins/cmpl/modules/bound3.lua b/love2d/lua/ecs/plugins/cmpl/modules/bound3.lua deleted file mode 100644 index 6c381548..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/bound3.lua +++ /dev/null @@ -1,175 +0,0 @@ ---- A 3-component axis-aligned bounding box. --- @module bound3 - -local modules = (...):gsub('%.[^%.]+$', '') .. "." -local vec3 = require(modules .. "vec3") - -local bound3 = {} -local bound3_mt = {} - --- Private constructor. -local function new(min, max) - return setmetatable({ - min=min, -- min: vec3, minimum value for each component - max=max -- max: vec3, maximum value for each component - }, bound3_mt) -end - --- Do the check to see if JIT is enabled. If so use the optimized FFI structs. -local status, ffi -if type(jit) == "table" and jit.status() then - status, ffi = pcall(require, "ffi") - if status then - ffi.cdef "typedef struct { cpml_vec3 min, max; } cpml_bound3;" - new = ffi.typeof("cpml_bound3") - end -end - -bound3.zero = new(vec3.zero, vec3.zero) - ---- The public constructor. --- @param min Can be of two types:
--- vec3 min, minimum value for each component --- nil Create bound at single point 0,0,0 --- @tparam vec3 max, maximum value for each component --- @treturn bound3 out -function bound3.new(min, max) - if min and max then - return new(min:clone(), max:clone()) - elseif min or max then - error("Unexpected nil argument to bound3.new") - else - return new(vec3.zero, vec3.zero) - end -end - ---- Clone a bound. --- @tparam bound3 a bound to be cloned --- @treturn bound3 out -function bound3.clone(a) - return new(a.min, a.max) -end - ---- Construct a bound covering one or two points --- @tparam vec3 a Any vector --- @tparam vec3 b Any second vector (optional) --- @treturn vec3 Minimum bound containing the given points -function bound3.at(a, b) -- "bounded by". b may be nil - if b then - return bound3.new(a,b):check() - else - return bound3.zero:with_center(a) - end -end - ---- Get size of bounding box as a vector --- @tparam bound3 a bound --- @treturn vec3 Vector spanning min to max points -function bound3.size(a) - return a.max - a.min -end - ---- Resize bounding box from minimum corner --- @tparam bound3 a bound --- @tparam vec3 new size --- @treturn bound3 resized bound -function bound3.with_size(a, size) - return bound3.new(a.min, a.min + size) -end - ---- Get half-size of bounding box as a vector. A more correct term for this is probably "apothem" --- @tparam bound3 a bound --- @treturn vec3 Vector spanning center to max point -function bound3.radius(a) - return a:size()/2 -end - ---- Get center of bounding box --- @tparam bound3 a bound --- @treturn bound3 Point in center of bound -function bound3.center(a) - return (a.min + a.max)/2 -end - ---- Move bounding box to new center --- @tparam bound3 a bound --- @tparam vec3 new center --- @treturn bound3 Bound with same size as input but different center -function bound3.with_center(a, center) - return bound3.offset(a, center - a:center()) -end - ---- Resize bounding box from center --- @tparam bound3 a bound --- @tparam vec3 new size --- @treturn bound3 resized bound -function bound3.with_size_centered(a, size) - local center = a:center() - local rad = size/2 - return bound3.new(center - rad, center + rad) -end - ---- Convert possibly-invalid bounding box to valid one --- @tparam bound3 a bound --- @treturn bound3 bound with all components corrected for min-max property -function bound3.check(a) - if a.min.x > a.max.x or a.min.y > a.max.y or a.min.z > a.max.z then - return bound3.new(vec3.component_min(a.min, a.max), vec3.component_max(a.min, a.max)) - end - return a -end - ---- Shrink bounding box with fixed margin --- @tparam bound3 a bound --- @tparam vec3 a margin --- @treturn bound3 bound with margin subtracted from all edges. May not be valid, consider calling check() -function bound3.inset(a, v) - return bound3.new(a.min + v, a.max - v) -end - ---- Expand bounding box with fixed margin --- @tparam bound3 a bound --- @tparam vec3 a margin --- @treturn bound3 bound with margin added to all edges. May not be valid, consider calling check() -function bound3.outset(a, v) - return bound3.new(a.min - v, a.max + v) -end - ---- Offset bounding box --- @tparam bound3 a bound --- @tparam vec3 offset --- @treturn bound3 bound with same size, but position moved by offset -function bound3.offset(a, v) - return bound3.new(a.min + v, a.max + v) -end - ---- Test if point in bound --- @tparam bound3 a bound --- @tparam vec3 point to test --- @treturn boolean true if point in bounding box -function bound3.contains(a, v) - return a.min.x <= v.x and a.min.y <= v.y and a.min.z <= v.z - and a.max.x >= v.x and a.max.y >= v.y and a.max.z >= v.z -end - ---- Return a formatted string. --- @tparam bound3 a bound to be turned into a string --- @treturn string formatted -function bound3.to_string(a) - return string.format("(%s-%s)", a.min, a.max) -end - -bound3_mt.__index = bound3 -bound3_mt.__tostring = bound3.to_string - -function bound3_mt.__call(_, a, b) - return bound3.new(a, b) -end - -if status then - xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded - ffi.metatype(new, bound3_mt) - end, function() end) -end - -return setmetatable({}, bound3_mt) diff --git a/love2d/lua/ecs/plugins/cmpl/modules/bvh.lua b/love2d/lua/ecs/plugins/cmpl/modules/bvh.lua deleted file mode 100644 index 07ea1852..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/bvh.lua +++ /dev/null @@ -1,507 +0,0 @@ --- https://github.com/benraziel/bvh-tree - ---- BVH Tree --- @module bvh - -local modules = (...):gsub('%.[^%.]+$', '') .. "." -local intersect = require(modules .. "intersect") -local vec3 = require(modules .. "vec3") -local EPSILON = 1e-6 -local BVH = {} -local BVHNode = {} -local Node - -BVH.__index = BVH -BVHNode.__index = BVHNode - -local function new(triangles, maxTrianglesPerNode) - local tree = setmetatable({}, BVH) - local trianglesArray = {} - - for _, triangle in ipairs(triangles) do - local p1 = triangle[1] - local p2 = triangle[2] - local p3 = triangle[3] - - table.insert(trianglesArray, p1.x) - table.insert(trianglesArray, p1.y) - table.insert(trianglesArray, p1.z) - - table.insert(trianglesArray, p2.x) - table.insert(trianglesArray, p2.y) - table.insert(trianglesArray, p2.z) - - table.insert(trianglesArray, p3.x) - table.insert(trianglesArray, p3.y) - table.insert(trianglesArray, p3.z) - end - - tree._trianglesArray = trianglesArray - tree._maxTrianglesPerNode = maxTrianglesPerNode or 10 - tree._bboxArray = tree.calcBoundingBoxes(trianglesArray) - - -- clone a helper array - tree._bboxHelper = {} - for _, bbox in ipairs(tree._bboxArray) do - table.insert(tree._bboxHelper, bbox) - end - - -- create the root node, add all the triangles to it - local triangleCount = #triangles - local extents = tree:calcExtents(1, triangleCount, EPSILON) - tree._rootNode = Node(extents[1], extents[2], 1, triangleCount, 1) - - local function split_r(node) - local left, right = tree:splitNode(tree._rootNode) - if left then - split_r(left) - end - if right then - split_r(right) - end - end - - split_r(tree._rootNode) - - return tree -end - -function BVH:intersectRay(rayOrigin, rayDirection, backfaceCulling) - local nodesToIntersect = { self._rootNode } - local trianglesInIntersectingNodes = {} -- a list of nodes that intersect the ray (according to their bounding box) - local intersectingTriangles = {} - - local invRayDirection = vec3( - 1 / rayDirection.x, - 1 / rayDirection.y, - 1 / rayDirection.z - ) - - -- go over the BVH tree, and extract the list of triangles that lie in nodes that intersect the ray. - -- note: these triangles may not intersect the ray themselves - while #nodesToIntersect > 0 do - local node = table.remove(nodesToIntersect) - - if BVH.intersectNodeBox(rayOrigin, invRayDirection, node) then - if node._node0 then - table.insert(nodesToIntersect, node._node0) - end - - if node._node1 then - table.insert(nodesToIntersect, node._node1) - end - - for i=node._startIndex, node._endIndex do - table.insert(trianglesInIntersectingNodes, self._bboxArray[1+(i-1)*7]) - end - end - end - - -- go over the list of candidate triangles, and check each of them using ray triangle intersection - local triangle = { vec3(), vec3(), vec3() } - local ray = { - position = vec3(rayOrigin.x, rayOrigin.y, rayOrigin.z), - direction = vec3(rayDirection.x, rayDirection.y, rayDirection.z) - } - - for i=1, #trianglesInIntersectingNodes do - local triIndex = trianglesInIntersectingNodes[i] - - -- print(triIndex, #self._trianglesArray) - triangle[1].x = self._trianglesArray[1+(triIndex-1)*9] - triangle[1].y = self._trianglesArray[1+(triIndex-1)*9+1] - triangle[1].z = self._trianglesArray[1+(triIndex-1)*9+2] - triangle[2].x = self._trianglesArray[1+(triIndex-1)*9+3] - triangle[2].y = self._trianglesArray[1+(triIndex-1)*9+4] - triangle[2].z = self._trianglesArray[1+(triIndex-1)*9+5] - triangle[3].x = self._trianglesArray[1+(triIndex-1)*9+6] - triangle[3].y = self._trianglesArray[1+(triIndex-1)*9+7] - triangle[3].z = self._trianglesArray[1+(triIndex-1)*9+8] - - local intersectionPoint, intersectionDistance = intersect.ray_triangle(ray, triangle, backfaceCulling) - - if intersectionPoint then - table.insert(intersectingTriangles, { - triangle = { triangle[1]:clone(), triangle[2]:clone(), triangle[3]:clone() }, - triangleIndex = triIndex, - intersectionPoint = intersectionPoint, - intersectionDistance = intersectionDistance - }) - end - end - - return intersectingTriangles -end - -function BVH.calcBoundingBoxes(trianglesArray) - local p1x, p1y, p1z - local p2x, p2y, p2z - local p3x, p3y, p3z - local minX, minY, minZ - local maxX, maxY, maxZ - - local bboxArray = {} - - for i=1, #trianglesArray / 9 do - p1x = trianglesArray[1+(i-1)*9] - p1y = trianglesArray[1+(i-1)*9+1] - p1z = trianglesArray[1+(i-1)*9+2] - p2x = trianglesArray[1+(i-1)*9+3] - p2y = trianglesArray[1+(i-1)*9+4] - p2z = trianglesArray[1+(i-1)*9+5] - p3x = trianglesArray[1+(i-1)*9+6] - p3y = trianglesArray[1+(i-1)*9+7] - p3z = trianglesArray[1+(i-1)*9+8] - - minX = math.min(p1x, p2x, p3x) - minY = math.min(p1y, p2y, p3y) - minZ = math.min(p1z, p2z, p3z) - maxX = math.max(p1x, p2x, p3x) - maxY = math.max(p1y, p2y, p3y) - maxZ = math.max(p1z, p2z, p3z) - - BVH.setBox(bboxArray, i, i, minX, minY, minZ, maxX, maxY, maxZ) - end - - return bboxArray -end - -function BVH:calcExtents(startIndex, endIndex, expandBy) - expandBy = expandBy or 0 - - if startIndex >= endIndex then - return { vec3(), vec3() } - end - - local minX = math.huge - local minY = math.huge - local minZ = math.huge - local maxX = -math.huge - local maxY = -math.huge - local maxZ = -math.huge - - for i=startIndex, endIndex do - minX = math.min(self._bboxArray[1+(i-1)*7+1], minX) - minY = math.min(self._bboxArray[1+(i-1)*7+2], minY) - minZ = math.min(self._bboxArray[1+(i-1)*7+3], minZ) - maxX = math.max(self._bboxArray[1+(i-1)*7+4], maxX) - maxY = math.max(self._bboxArray[1+(i-1)*7+5], maxY) - maxZ = math.max(self._bboxArray[1+(i-1)*7+6], maxZ) - end - - return { - vec3(minX - expandBy, minY - expandBy, minZ - expandBy), - vec3(maxX + expandBy, maxY + expandBy, maxZ + expandBy) - } -end - -function BVH:splitNode(node) - local num_elements = node:elementCount() - if (num_elements <= self._maxTrianglesPerNode) or (num_elements == 0) then - return - end - - local startIndex = node._startIndex - local endIndex = node._endIndex - - local leftNode = { {},{},{} } - local rightNode = { {},{},{} } - local extentCenters = { node:centerX(), node:centerY(), node:centerZ() } - - local extentsLength = { - node._extentsMax.x - node._extentsMin.x, - node._extentsMax.y - node._extentsMin.y, - node._extentsMax.z - node._extentsMin.z - } - - local objectCenter = {} - for i=startIndex, endIndex do - objectCenter[1] = (self._bboxArray[1+(i-1)*7+1] + self._bboxArray[1+(i-1)*7+4]) * 0.5 -- center = (min + max) / 2 - objectCenter[2] = (self._bboxArray[1+(i-1)*7+2] + self._bboxArray[1+(i-1)*7+5]) * 0.5 -- center = (min + max) / 2 - objectCenter[3] = (self._bboxArray[1+(i-1)*7+3] + self._bboxArray[1+(i-1)*7+6]) * 0.5 -- center = (min + max) / 2 - - for j=1, 3 do - if objectCenter[j] < extentCenters[j] then - table.insert(leftNode[j], i) - else - table.insert(rightNode[j], i) - end - end - end - - -- check if we couldn't split the node by any of the axes (x, y or z). halt - -- here, dont try to split any more (cause it will always fail, and we'll - -- enter an infinite loop - local splitFailed = { - #leftNode[1] == 0 or #rightNode[1] == 0, - #leftNode[2] == 0 or #rightNode[2] == 0, - #leftNode[3] == 0 or #rightNode[3] == 0 - } - - if splitFailed[1] and splitFailed[2] and splitFailed[3] then - return - end - - -- choose the longest split axis. if we can't split by it, choose next best one. - local splitOrder = { 1, 2, 3 } - table.sort(splitOrder, function(a, b) - return extentsLength[b] - extentsLength[a] - end) - - local leftElements - local rightElements - - for i=1, 3 do - local candidateIndex = splitOrder[i] - if not splitFailed[candidateIndex] then - leftElements = leftNode[candidateIndex] - rightElements = rightNode[candidateIndex] - break - end - end - - -- sort the elements in range (startIndex, endIndex) according to which node they should be at - local node0Start = startIndex - local node0End = node0Start + #leftElements - local node1Start = node0End - local node1End = endIndex - local currElement - - local helperPos = node._startIndex - local concatenatedElements = {} - - for _, element in ipairs(leftElements) do - table.insert(concatenatedElements, element) - end - - for _, element in ipairs(rightElements) do - table.insert(concatenatedElements, element) - end - - -- print(#leftElements, #rightElements, #concatenatedElements) - - for i=1, #concatenatedElements do - currElement = concatenatedElements[i] - BVH.copyBox(self._bboxArray, currElement, self._bboxHelper, helperPos) - helperPos = helperPos + 1 - end - - -- copy results back to main array - for i=1+(node._startIndex-1)*7, 1+(node._endIndex-1)*7 do - self._bboxArray[i] = self._bboxHelper[i] - end - - -- create 2 new nodes for the node we just split, and add links to them from the parent node - local node0Extents = self:calcExtents(node0Start, node0End, EPSILON) - local node1Extents = self:calcExtents(node1Start, node1End, EPSILON) - - local node0 = Node(node0Extents[1], node0Extents[2], node0Start, node0End, node._level + 1) - local node1 = Node(node1Extents[1], node1Extents[2], node1Start, node1End, node._level + 1) - - node._node0 = node0 - node._node1 = node1 - node:clearShapes() - - -- add new nodes to the split queue - return node0, node1 -end - -function BVH._calcTValues(minVal, maxVal, rayOriginCoord, invdir) - local res = { min=0, max=0 } - - if invdir >= 0 then - res.min = ( minVal - rayOriginCoord ) * invdir - res.max = ( maxVal - rayOriginCoord ) * invdir - else - res.min = ( maxVal - rayOriginCoord ) * invdir - res.max = ( minVal - rayOriginCoord ) * invdir - end - - return res -end - -function BVH.intersectNodeBox(rayOrigin, invRayDirection, node) - local t = BVH._calcTValues(node._extentsMin.x, node._extentsMax.x, rayOrigin.x, invRayDirection.x) - local ty = BVH._calcTValues(node._extentsMin.y, node._extentsMax.y, rayOrigin.y, invRayDirection.y) - - if t.min > ty.max or ty.min > t.max then - return false - end - - -- These lines also handle the case where tmin or tmax is NaN - -- (result of 0 * Infinity). x !== x returns true if x is NaN - if ty.min > t.min or t.min ~= t.min then - t.min = ty.min - end - - if ty.max < t.max or t.max ~= t.max then - t.max = ty.max - end - - local tz = BVH._calcTValues(node._extentsMin.z, node._extentsMax.z, rayOrigin.z, invRayDirection.z) - - if t.min > tz.max or tz.min > t.max then - return false - end - - if tz.min > t.min or t.min ~= t.min then - t.min = tz.min - end - - if tz.max < t.max or t.max ~= t.max then - t.max = tz.max - end - - --return point closest to the ray (positive side) - if t.max < 0 then - return false - end - - return true -end - -function BVH.setBox(bboxArray, pos, triangleId, minX, minY, minZ, maxX, maxY, maxZ) - bboxArray[1+(pos-1)*7] = triangleId - bboxArray[1+(pos-1)*7+1] = minX - bboxArray[1+(pos-1)*7+2] = minY - bboxArray[1+(pos-1)*7+3] = minZ - bboxArray[1+(pos-1)*7+4] = maxX - bboxArray[1+(pos-1)*7+5] = maxY - bboxArray[1+(pos-1)*7+6] = maxZ -end - -function BVH.copyBox(sourceArray, sourcePos, destArray, destPos) - destArray[1+(destPos-1)*7] = sourceArray[1+(sourcePos-1)*7] - destArray[1+(destPos-1)*7+1] = sourceArray[1+(sourcePos-1)*7+1] - destArray[1+(destPos-1)*7+2] = sourceArray[1+(sourcePos-1)*7+2] - destArray[1+(destPos-1)*7+3] = sourceArray[1+(sourcePos-1)*7+3] - destArray[1+(destPos-1)*7+4] = sourceArray[1+(sourcePos-1)*7+4] - destArray[1+(destPos-1)*7+5] = sourceArray[1+(sourcePos-1)*7+5] - destArray[1+(destPos-1)*7+6] = sourceArray[1+(sourcePos-1)*7+6] -end - -function BVH.getBox(bboxArray, pos, outputBox) - outputBox.triangleId = bboxArray[1+(pos-1)*7] - outputBox.minX = bboxArray[1+(pos-1)*7+1] - outputBox.minY = bboxArray[1+(pos-1)*7+2] - outputBox.minZ = bboxArray[1+(pos-1)*7+3] - outputBox.maxX = bboxArray[1+(pos-1)*7+4] - outputBox.maxY = bboxArray[1+(pos-1)*7+5] - outputBox.maxZ = bboxArray[1+(pos-1)*7+6] -end - -local function new_node(extentsMin, extentsMax, startIndex, endIndex, level) - return setmetatable({ - _extentsMin = extentsMin, - _extentsMax = extentsMax, - _startIndex = startIndex, - _endIndex = endIndex, - _level = level - --_node0 = nil - --_node1 = nil - }, BVHNode) -end - -function BVHNode:elementCount() - return self._endIndex - self._startIndex -end - -function BVHNode:centerX() - return (self._extentsMin.x + self._extentsMax.x) * 0.5 -end - -function BVHNode:centerY() - return (self._extentsMin.y + self._extentsMax.y) * 0.5 -end - -function BVHNode:centerZ() - return (self._extentsMin.z + self._extentsMax.z) * 0.5 -end - -function BVHNode:clearShapes() - self._startIndex = -1 - self._endIndex = -1 -end - -function BVHNode.ngSphereRadius(extentsMin, extentsMax) - local centerX = (extentsMin.x + extentsMax.x) * 0.5 - local centerY = (extentsMin.y + extentsMax.y) * 0.5 - local centerZ = (extentsMin.z + extentsMax.z) * 0.5 - - local extentsMinDistSqr = - (centerX - extentsMin.x) * (centerX - extentsMin.x) + - (centerY - extentsMin.y) * (centerY - extentsMin.y) + - (centerZ - extentsMin.z) * (centerZ - extentsMin.z) - - local extentsMaxDistSqr = - (centerX - extentsMax.x) * (centerX - extentsMax.x) + - (centerY - extentsMax.y) * (centerY - extentsMax.y) + - (centerZ - extentsMax.z) * (centerZ - extentsMax.z) - - return math.sqrt(math.max(extentsMinDistSqr, extentsMaxDistSqr)) -end - ---[[ - ---- Draws node boundaries visually for debugging. --- @param cube Cube model to draw --- @param depth Used for recurcive calls to self method -function OctreeNode:draw_bounds(cube, depth) - depth = depth or 0 - local tint = depth / 7 -- Will eventually get values > 1. Color rounds to 1 automatically - - love.graphics.setColor(tint * 255, 0, (1 - tint) * 255) - local m = mat4() - :translate(self.center) - :scale(vec3(self.adjLength, self.adjLength, self.adjLength)) - - love.graphics.updateMatrix("transform", m) - love.graphics.setWireframe(true) - love.graphics.draw(cube) - love.graphics.setWireframe(false) - - for _, child in ipairs(self.children) do - child:draw_bounds(cube, depth + 1) - end - - love.graphics.setColor(255, 255, 255) -end - ---- Draws the bounds of all objects in the tree visually for debugging. --- @param cube Cube model to draw --- @param filter a function returning true or false to determine visibility. -function OctreeNode:draw_objects(cube, filter) - local tint = self.baseLength / 20 - love.graphics.setColor(0, (1 - tint) * 255, tint * 255, 63) - - for _, object in ipairs(self.objects) do - if filter and filter(object.data) or not filter then - local m = mat4() - :translate(object.bounds.center) - :scale(object.bounds.size) - - love.graphics.updateMatrix("transform", m) - love.graphics.draw(cube) - end - end - - for _, child in ipairs(self.children) do - child:draw_objects(cube, filter) - end - - love.graphics.setColor(255, 255, 255) -end - ---]] - -Node = setmetatable({ - new = new_node -}, { - __call = function(_, ...) return new_node(...) end -}) - -return setmetatable({ - new = new -}, { - __call = function(_, ...) return new(...) end -}) diff --git a/love2d/lua/ecs/plugins/cmpl/modules/color.lua b/love2d/lua/ecs/plugins/cmpl/modules/color.lua deleted file mode 100644 index 094459cf..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/color.lua +++ /dev/null @@ -1,385 +0,0 @@ ---- Color utilities --- @module color - -local modules = (...):gsub('%.[^%.]+$', '') .. "." -local utils = require(modules .. "utils") -local color = {} -local color_mt = {} - -local function new(r, g, b, a) - local c = { r, g, b, a } - c._c = c - return setmetatable(c, color) -end - --- HSV utilities (adapted from http://www.cs.rit.edu/~ncs/color/t_convert.html) --- hsv_to_color(hsv) --- Converts a set of HSV values to a color. hsv is a table. --- See also: hsv(h, s, v) -local function hsv_to_color(hsv) - local i - local f, q, p, t - local h, s, v - local a = hsv[4] or 255 - s = hsv[2] - v = hsv[3] - - if s == 0 then - return new(v, v, v, a) - end - - h = hsv[1] / 60 - - i = math.floor(h) - f = h - i - p = v * (1-s) - q = v * (1-s*f) - t = v * (1-s*(1-f)) - - if i == 0 then return new(v, t, p, a) - elseif i == 1 then return new(q, v, p, a) - elseif i == 2 then return new(p, v, t, a) - elseif i == 3 then return new(p, q, v, a) - elseif i == 4 then return new(t, p, v, a) - else return new(v, p, q, a) - end -end - --- color_to_hsv(c) --- Takes in a normal color and returns a table with the HSV values. -local function color_to_hsv(c) - local r = c[1] - local g = c[2] - local b = c[3] - local a = c[4] or 255 - local h, s, v - - local min = math.min(r, g, b) - local max = math.max(r, g, b) - v = max - - local delta = max - min - - -- black, nothing else is really possible here. - if min == 0 and max == 0 then - return { 0, 0, 0, a } - end - - if max ~= 0 then - s = delta / max - else - -- r = g = b = 0 s = 0, v is undefined - s = 0 - h = -1 - return { h, s, v, 255 } - end - - if r == max then - h = ( g - b ) / delta -- yellow/magenta - elseif g == max then - h = 2 + ( b - r ) / delta -- cyan/yellow - else - h = 4 + ( r - g ) / delta -- magenta/cyan - end - - h = h * 60 -- degrees - - if h < 0 then - h = h + 360 - end - - return { h, s, v, a } -end - ---- The public constructor. --- @param x Can be of three types:
--- number red component 0-255 --- table {r, g, b, a} --- nil for {0,0,0,0} --- @tparam number g Green component 0-255 --- @tparam number b Blue component 0-255 --- @tparam number a Alpha component 0-255 --- @treturn color out -function color.new(r, g, b, a) - -- number, number, number, number - if r and g and b and a then - assert(type(r) == "number", "new: Wrong argument type for r ( expected)") - assert(type(g) == "number", "new: Wrong argument type for g ( expected)") - assert(type(b) == "number", "new: Wrong argument type for b ( expected)") - assert(type(a) == "number", "new: Wrong argument type for a ( expected)") - - return new(r, g, b, a) - - -- {r, g, b, a} - elseif type(r) == "table" then - local rr, gg, bb, aa = r[1], r[2], r[3], r[4] - assert(type(rr) == "number", "new: Wrong argument type for r ( expected)") - assert(type(gg) == "number", "new: Wrong argument type for g ( expected)") - assert(type(bb) == "number", "new: Wrong argument type for b ( expected)") - assert(type(aa) == "number", "new: Wrong argument type for a ( expected)") - - return new(rr, gg, bb, aa) - end - - return new(0, 0, 0, 0) -end - ---- Convert hue,saturation,value table to color object. --- @tparam table hsva {hue 0-359, saturation 0-1, value 0-1, alpha 0-255} --- @treturn color out -color.hsv_to_color_table = hsv_to_color - ---- Convert color to hue,saturation,value table --- @tparam color in --- @treturn table hsva {hue 0-359, saturation 0-1, value 0-1, alpha 0-255} -color.color_to_hsv_table = color_to_hsv - ---- Convert hue,saturation,value to color object. --- @tparam number h hue 0-359 --- @tparam number s saturation 0-1 --- @tparam number v value 0-1 --- @treturn color out -function color.from_hsv(h, s, v) - return hsv_to_color { h, s, v } -end - ---- Convert hue,saturation,value to color object. --- @tparam number h hue 0-359 --- @tparam number s saturation 0-1 --- @tparam number v value 0-1 --- @tparam number a alpha 0-255 --- @treturn color out -function color.from_hsva(h, s, v, a) - return hsv_to_color { h, s, v, a } -end - ---- Invert a color. --- @tparam color to invert --- @treturn color out -function color.invert(c) - return new(255 - c[1], 255 - c[2], 255 - c[3], c[4]) -end - ---- Lighten a color by a component-wise fixed amount (alpha unchanged) --- @tparam color to lighten --- @tparam number amount to increase each component by, 0-255 scale --- @treturn color out -function color.lighten(c, v) - return new( - utils.clamp(c[1] + v * 255, 0, 255), - utils.clamp(c[2] + v * 255, 0, 255), - utils.clamp(c[3] + v * 255, 0, 255), - c[4] - ) -end - -function color.lerp(a, b, s) - return a + s * (b - a) -end - ---- Darken a color by a component-wise fixed amount (alpha unchanged) --- @tparam color to darken --- @tparam number amount to decrease each component by, 0-255 scale --- @treturn color out -function color.darken(c, v) - return new( - utils.clamp(c[1] - v * 255, 0, 255), - utils.clamp(c[2] - v * 255, 0, 255), - utils.clamp(c[3] - v * 255, 0, 255), - c[4] - ) -end - ---- Multiply a color's components by a value (alpha unchanged) --- @tparam color to multiply --- @tparam number to multiply each component by --- @treturn color out -function color.multiply(c, v) - local t = color.new() - for i = 1, 3 do - t[i] = c[i] * v - end - - t[4] = c[4] - return t -end - --- directly set alpha channel --- @tparam color to alter --- @tparam number new alpha 0-255 --- @treturn color out -function color.alpha(c, v) - local t = color.new() - for i = 1, 3 do - t[i] = c[i] - end - - t[4] = v * 255 - return t -end - ---- Multiply a color's alpha by a value --- @tparam color to multiply --- @tparam number to multiply alpha by --- @treturn color out -function color.opacity(c, v) - local t = color.new() - for i = 1, 3 do - t[i] = c[i] - end - - t[4] = c[4] * v - return t -end - ---- Set a color's hue (saturation, value, alpha unchanged) --- @tparam color to alter --- @tparam hue to set 0-359 --- @treturn color out -function color.hue(col, hue) - local c = color_to_hsv(col) - c[1] = (hue + 360) % 360 - return hsv_to_color(c) -end - ---- Set a color's saturation (hue, value, alpha unchanged) --- @tparam color to alter --- @tparam hue to set 0-359 --- @treturn color out -function color.saturation(col, percent) - local c = color_to_hsv(col) - c[2] = utils.clamp(percent, 0, 1) - return hsv_to_color(c) -end - ---- Set a color's value (saturation, hue, alpha unchanged) --- @tparam color to alter --- @tparam hue to set 0-359 --- @treturn color out -function color.value(col, percent) - local c = color_to_hsv(col) - c[3] = utils.clamp(percent, 0, 1) - return hsv_to_color(c) -end - --- http://en.wikipedia.org/wiki/SRGB#The_reverse_transformation -function color.gamma_to_linear(r, g, b, a) - local function convert(c) - if c > 1.0 then - return 1.0 - elseif c < 0.0 then - return 0.0 - elseif c <= 0.04045 then - return c / 12.92 - else - return math.pow((c + 0.055) / 1.055, 2.4) - end - end - - if type(r) == "table" then - local c = {} - for i = 1, 3 do - c[i] = convert(r[i] / 255) * 255 - end - - c[4] = convert(r[4] / 255) * 255 - return c - else - return convert(r / 255) * 255, convert(g / 255) * 255, convert(b / 255) * 255, a or 255 - end -end - --- http://en.wikipedia.org/wiki/SRGB#The_forward_transformation_.28CIE_xyY_or_CIE_XYZ_to_sRGB.29 -function color.linear_to_gamma(r, g, b, a) - local function convert(c) - if c > 1.0 then - return 1.0 - elseif c < 0.0 then - return 0.0 - elseif c < 0.0031308 then - return c * 12.92 - else - return 1.055 * math.pow(c, 0.41666) - 0.055 - end - end - - if type(r) == "table" then - local c = {} - for i = 1, 3 do - c[i] = convert(r[i] / 255) * 255 - end - - c[4] = convert(r[4] / 255) * 255 - return c - else - return convert(r / 255) * 255, convert(g / 255) * 255, convert(b / 255) * 255, a or 255 - end -end - ---- Check if color is valid --- @tparam color to test --- @treturn boolean is color -function color.is_color(a) - if type(a) ~= "table" then - return false - end - - for i = 1, 4 do - if type(a[i]) ~= "number" then - return false - end - end - - return true -end - ---- Return a formatted string. --- @tparam color a color to be turned into a string --- @treturn string formatted -function color.to_string(a) - return string.format("[ %3.0f, %3.0f, %3.0f, %3.0f ]", a[1], a[2], a[3], a[4]) -end - -function color_mt.__index(t, k) - if type(t) == "cdata" then - if type(k) == "number" then - return t._c[k-1] - end - end - - return rawget(color, k) -end - -function color_mt.__newindex(t, k, v) - if type(t) == "cdata" then - if type(k) == "number" then - t._c[k-1] = v - end - end -end - -color_mt.__tostring = color.to_string - -function color_mt.__call(_, r, g, b, a) - return color.new(r, g, b, a) -end - -function color_mt.__add(a, b) - return new(a[1] + b[1], a[2] + b[2], a[3] + b[3], a[4] + b[4]) -end - -function color_mt.__sub(a, b) - return new(a[1] - b[1], a[2] - b[2], a[3] - b[3], a[4] - b[4]) -end - -function color_mt.__mul(a, b) - if type(a) == "number" then - return new(a * b[1], a * b[2], a * b[3], a * b[4]) - elseif type(b) == "number" then - return new(b * a[1], b * a[2], b * a[3], b * a[4]) - else - return new(a[1] * b[1], a[2] * b[2], a[3] * b[3], a[4] * b[4]) - end -end - -return setmetatable({}, color_mt) diff --git a/love2d/lua/ecs/plugins/cmpl/modules/constants.lua b/love2d/lua/ecs/plugins/cmpl/modules/constants.lua deleted file mode 100644 index 9b9dab08..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/constants.lua +++ /dev/null @@ -1,20 +0,0 @@ ---- Various useful constants --- @module constants - ---- Constants --- @table constants --- @field FLT_EPSILON Floating point precision breaks down --- @field DBL_EPSILON Double-precise floating point precision breaks down --- @field DOT_THRESHOLD Close enough to 1 for interpolations to occur -local constants = {} - --- same as C's FLT_EPSILON -constants.FLT_EPSILON = 1.19209290e-07 - --- same as C's DBL_EPSILON -constants.DBL_EPSILON = 2.2204460492503131e-16 - --- used for quaternion.slerp -constants.DOT_THRESHOLD = 0.9995 - -return constants diff --git a/love2d/lua/ecs/plugins/cmpl/modules/intersect.lua b/love2d/lua/ecs/plugins/cmpl/modules/intersect.lua deleted file mode 100644 index c8d70861..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/intersect.lua +++ /dev/null @@ -1,709 +0,0 @@ ---- Various geometric intersections --- @module intersect - -local modules = (...):gsub('%.[^%.]+$', '') .. "." -local constants = require(modules .. "constants") -local mat4 = require(modules .. "mat4") -local vec3 = require(modules .. "vec3") -local utils = require(modules .. "utils") -local DBL_EPSILON = constants.DBL_EPSILON -local sqrt = math.sqrt -local abs = math.abs -local min = math.min -local max = math.max -local intersect = {} - --- https://blogs.msdn.microsoft.com/rezanour/2011/08/07/barycentric-coordinates-and-point-in-triangle-tests/ --- point is a vec3 --- triangle[1] is a vec3 --- triangle[2] is a vec3 --- triangle[3] is a vec3 -function intersect.point_triangle(point, triangle) - local u = triangle[2] - triangle[1] - local v = triangle[3] - triangle[1] - local w = point - triangle[1] - - local vw = v:cross(w) - local vu = v:cross(u) - - if vw:dot(vu) < 0 then - return false - end - - local uw = u:cross(w) - local uv = u:cross(v) - - if uw:dot(uv) < 0 then - return false - end - - local d = uv:len() - local r = vw:len() / d - local t = uw:len() / d - - return r + t <= 1 -end - --- point is a vec3 --- aabb.min is a vec3 --- aabb.max is a vec3 -function intersect.point_aabb(point, aabb) - return - aabb.min.x <= point.x and - aabb.max.x >= point.x and - aabb.min.y <= point.y and - aabb.max.y >= point.y and - aabb.min.z <= point.z and - aabb.max.z >= point.z -end - --- point is a vec3 --- frustum.left is a plane { a, b, c, d } --- frustum.right is a plane { a, b, c, d } --- frustum.bottom is a plane { a, b, c, d } --- frustum.top is a plane { a, b, c, d } --- frustum.near is a plane { a, b, c, d } --- frustum.far is a plane { a, b, c, d } -function intersect.point_frustum(point, frustum) - local x, y, z = point:unpack() - local planes = { - frustum.left, - frustum.right, - frustum.bottom, - frustum.top, - frustum.near, - frustum.far or false - } - - -- Skip the last test for infinite projections, it'll never fail. - if not planes[6] then - table.remove(planes) - end - - local dot - for i = 1, #planes do - dot = planes[i].a * x + planes[i].b * y + planes[i].c * z + planes[i].d - if dot <= 0 then - return false - end - end - - return true -end - --- http://www.lighthouse3d.com/tutorials/maths/ray-triangle-intersection/ --- ray.position is a vec3 --- ray.direction is a vec3 --- triangle[1] is a vec3 --- triangle[2] is a vec3 --- triangle[3] is a vec3 --- backface_cull is a boolean (optional) -function intersect.ray_triangle(ray, triangle, backface_cull) - local e1 = triangle[2] - triangle[1] - local e2 = triangle[3] - triangle[1] - local h = ray.direction:cross(e2) - local a = h:dot(e1) - - -- if a is negative, ray hits the backface - if backface_cull and a < 0 then - return false - end - - -- if a is too close to 0, ray does not intersect triangle - if abs(a) <= DBL_EPSILON then - return false - end - - local f = 1 / a - local s = ray.position - triangle[1] - local u = s:dot(h) * f - - -- ray does not intersect triangle - if u < 0 or u > 1 then - return false - end - - local q = s:cross(e1) - local v = ray.direction:dot(q) * f - - -- ray does not intersect triangle - if v < 0 or u + v > 1 then - return false - end - - -- at this stage we can compute t to find out where - -- the intersection point is on the line - local t = q:dot(e2) * f - - -- return position of intersection and distance from ray origin - if t >= DBL_EPSILON then - return ray.position + ray.direction * t, t - end - - -- ray does not intersect triangle - return false -end - --- https://gamedev.stackexchange.com/questions/96459/fast-ray-sphere-collision-code --- ray.position is a vec3 --- ray.direction is a vec3 --- sphere.position is a vec3 --- sphere.radius is a number -function intersect.ray_sphere(ray, sphere) - local offset = ray.position - sphere.position - local b = offset:dot(ray.direction) - local c = offset:dot(offset) - sphere.radius * sphere.radius - - -- ray's position outside sphere (c > 0) - -- ray's direction pointing away from sphere (b > 0) - if c > 0 and b > 0 then - return false - end - - local discr = b * b - c - - -- negative discriminant - if discr < 0 then - return false - end - - -- Clamp t to 0 - local t = -b - sqrt(discr) - t = t < 0 and 0 or t - - -- Return collision point and distance from ray origin - return ray.position + ray.direction * t, t -end - --- http://gamedev.stackexchange.com/a/18459 --- ray.position is a vec3 --- ray.direction is a vec3 --- aabb.min is a vec3 --- aabb.max is a vec3 -function intersect.ray_aabb(ray, aabb) - local dir = ray.direction:normalize() - local dirfrac = vec3( - 1 / dir.x, - 1 / dir.y, - 1 / dir.z - ) - - local t1 = (aabb.min.x - ray.position.x) * dirfrac.x - local t2 = (aabb.max.x - ray.position.x) * dirfrac.x - local t3 = (aabb.min.y - ray.position.y) * dirfrac.y - local t4 = (aabb.max.y - ray.position.y) * dirfrac.y - local t5 = (aabb.min.z - ray.position.z) * dirfrac.z - local t6 = (aabb.max.z - ray.position.z) * dirfrac.z - - local tmin = max(max(min(t1, t2), min(t3, t4)), min(t5, t6)) - local tmax = min(min(max(t1, t2), max(t3, t4)), max(t5, t6)) - - -- ray is intersecting AABB, but whole AABB is behind us - if tmax < 0 then - return false - end - - -- ray does not intersect AABB - if tmin > tmax then - return false - end - - -- Return collision point and distance from ray origin - return ray.position + ray.direction * tmin, tmin -end - --- http://stackoverflow.com/a/23976134/1190664 --- ray.position is a vec3 --- ray.direction is a vec3 --- plane.position is a vec3 --- plane.normal is a vec3 -function intersect.ray_plane(ray, plane) - local denom = plane.normal:dot(ray.direction) - - -- ray does not intersect plane - if abs(denom) < DBL_EPSILON then - return false - end - - -- distance of direction - local d = plane.position - ray.position - local t = d:dot(plane.normal) / denom - - if t < DBL_EPSILON then - return false - end - - -- Return collision point and distance from ray origin - return ray.position + ray.direction * t, t -end - -function intersect.ray_capsule(ray, capsule) - local dist2, p1, p2 = intersect.closest_point_segment_segment( - ray.position, - ray.position + ray.direction * 1e10, - capsule.a, - capsule.b - ) - if dist2 <= capsule.radius^2 then - return p1 - end - - return false -end - --- https://web.archive.org/web/20120414063459/http://local.wasp.uwa.edu.au/~pbourke//geometry/lineline3d/ --- a[1] is a vec3 --- a[2] is a vec3 --- b[1] is a vec3 --- b[2] is a vec3 --- e is a number -function intersect.line_line(a, b, e) - -- new points - local p13 = a[1] - b[1] - local p43 = b[2] - b[1] - local p21 = a[2] - a[1] - - -- if lengths are negative or too close to 0, lines do not intersect - if p43:len2() < DBL_EPSILON or p21:len2() < DBL_EPSILON then - return false - end - - -- dot products - local d1343 = p13:dot(p43) - local d4321 = p43:dot(p21) - local d1321 = p13:dot(p21) - local d4343 = p43:dot(p43) - local d2121 = p21:dot(p21) - local denom = d2121 * d4343 - d4321 * d4321 - - -- if denom is too close to 0, lines do not intersect - if abs(denom) < DBL_EPSILON then - return false - end - - local numer = d1343 * d4321 - d1321 * d4343 - local mua = numer / denom - local mub = (d1343 + d4321 * mua) / d4343 - - -- return positions of intersection on each line - local out1 = a[1] + p21 * mua - local out2 = b[1] + p43 * mub - local dist = out1:dist(out2) - - -- if distance of the shortest segment between lines is less than threshold - if e and dist > e then - return false - end - - return { out1, out2 }, dist -end - --- a[1] is a vec3 --- a[2] is a vec3 --- b[1] is a vec3 --- b[2] is a vec3 --- e is a number -function intersect.segment_segment(a, b, e) - local c, d = intersect.line_line(a, b, e) - - if c and (( - a[1].x <= c[1].x and - a[1].y <= c[1].y and - a[1].z <= c[1].z and - c[1].x <= a[2].x and - c[1].y <= a[2].y and - c[1].z <= a[2].z - ) or ( - a[1].x >= c[1].x and - a[1].y >= c[1].y and - a[1].z >= c[1].z and - c[1].x >= a[2].x and - c[1].y >= a[2].y and - c[1].z >= a[2].z - )) and (( - b[1].x <= c[2].x and - b[1].y <= c[2].y and - b[1].z <= c[2].z and - c[2].x <= b[2].x and - c[2].y <= b[2].y and - c[2].z <= b[2].z - ) or ( - b[1].x >= c[2].x and - b[1].y >= c[2].y and - b[1].z >= c[2].z and - c[2].x >= b[2].x and - c[2].y >= b[2].y and - c[2].z >= b[2].z - )) then - return c, d - end - - -- segments do not intersect - return false -end - --- a.min is a vec3 --- a.max is a vec3 --- b.min is a vec3 --- b.max is a vec3 -function intersect.aabb_aabb(a, b) - return - a.min.x <= b.max.x and - a.max.x >= b.min.x and - a.min.y <= b.max.y and - a.max.y >= b.min.y and - a.min.z <= b.max.z and - a.max.z >= b.min.z -end - --- aabb.position is a vec3 --- aabb.extent is a vec3 (half-size) --- obb.position is a vec3 --- obb.extent is a vec3 (half-size) --- obb.rotation is a mat4 -function intersect.aabb_obb(aabb, obb) - local a = aabb.extent - local b = obb.extent - local T = obb.position - aabb.position - local rot = mat4():transpose(obb.rotation) - local B = {} - local t - - for i = 1, 3 do - B[i] = {} - for j = 1, 3 do - assert((i - 1) * 4 + j < 16 and (i - 1) * 4 + j > 0) - B[i][j] = abs(rot[(i - 1) * 4 + j]) + 1e-6 - end - end - - t = abs(T.x) - if not (t <= (b.x + a.x * B[1][1] + b.y * B[1][2] + b.z * B[1][3])) then return false end - t = abs(T.x * B[1][1] + T.y * B[2][1] + T.z * B[3][1]) - if not (t <= (b.x + a.x * B[1][1] + a.y * B[2][1] + a.z * B[3][1])) then return false end - t = abs(T.y) - if not (t <= (a.y + b.x * B[2][1] + b.y * B[2][2] + b.z * B[2][3])) then return false end - t = abs(T.z) - if not (t <= (a.z + b.x * B[3][1] + b.y * B[3][2] + b.z * B[3][3])) then return false end - t = abs(T.x * B[1][2] + T.y * B[2][2] + T.z * B[3][2]) - if not (t <= (b.y + a.x * B[1][2] + a.y * B[2][2] + a.z * B[3][2])) then return false end - t = abs(T.x * B[1][3] + T.y * B[2][3] + T.z * B[3][3]) - if not (t <= (b.z + a.x * B[1][3] + a.y * B[2][3] + a.z * B[3][3])) then return false end - t = abs(T.z * B[2][1] - T.y * B[3][1]) - if not (t <= (a.y * B[3][1] + a.z * B[2][1] + b.y * B[1][3] + b.z * B[1][2])) then return false end - t = abs(T.z * B[2][2] - T.y * B[3][2]) - if not (t <= (a.y * B[3][2] + a.z * B[2][2] + b.x * B[1][3] + b.z * B[1][1])) then return false end - t = abs(T.z * B[2][3] - T.y * B[3][3]) - if not (t <= (a.y * B[3][3] + a.z * B[2][3] + b.x * B[1][2] + b.y * B[1][1])) then return false end - t = abs(T.x * B[3][1] - T.z * B[1][1]) - if not (t <= (a.x * B[3][1] + a.z * B[1][1] + b.y * B[2][3] + b.z * B[2][2])) then return false end - t = abs(T.x * B[3][2] - T.z * B[1][2]) - if not (t <= (a.x * B[3][2] + a.z * B[1][2] + b.x * B[2][3] + b.z * B[2][1])) then return false end - t = abs(T.x * B[3][3] - T.z * B[1][3]) - if not (t <= (a.x * B[3][3] + a.z * B[1][3] + b.x * B[2][2] + b.y * B[2][1])) then return false end - t = abs(T.y * B[1][1] - T.x * B[2][1]) - if not (t <= (a.x * B[2][1] + a.y * B[1][1] + b.y * B[3][3] + b.z * B[3][2])) then return false end - t = abs(T.y * B[1][2] - T.x * B[2][2]) - if not (t <= (a.x * B[2][2] + a.y * B[1][2] + b.x * B[3][3] + b.z * B[3][1])) then return false end - t = abs(T.y * B[1][3] - T.x * B[2][3]) - if not (t <= (a.x * B[2][3] + a.y * B[1][3] + b.x * B[3][2] + b.y * B[3][1])) then return false end - - -- https://gamedev.stackexchange.com/questions/24078/which-side-was-hit - -- Minkowski Sum - local wy = (aabb.extent * 2 + obb.extent * 2) * (aabb.position.y - obb.position.y) - local hx = (aabb.extent * 2 + obb.extent * 2) * (aabb.position.x - obb.position.x) - - if wy.x > hx.x and wy.y > hx.y and wy.z > hx.z then - if wy.x > -hx.x and wy.y > -hx.y and wy.z > -hx.z then - return vec3(obb.rotation * { 0, -1, 0, 1 }) - else - return vec3(obb.rotation * { -1, 0, 0, 1 }) - end - else - if wy.x > -hx.x and wy.y > -hx.y and wy.z > -hx.z then - return vec3(obb.rotation * { 1, 0, 0, 1 }) - else - return vec3(obb.rotation * { 0, 1, 0, 1 }) - end - end -end - --- http://stackoverflow.com/a/4579069/1190664 --- aabb.min is a vec3 --- aabb.max is a vec3 --- sphere.position is a vec3 --- sphere.radius is a number -local axes = { "x", "y", "z" } -function intersect.aabb_sphere(aabb, sphere) - local dist2 = sphere.radius ^ 2 - - for _, axis in ipairs(axes) do - local pos = sphere.position[axis] - local amin = aabb.min[axis] - local amax = aabb.max[axis] - - if pos < amin then - dist2 = dist2 - (pos - amin) ^ 2 - elseif pos > amax then - dist2 = dist2 - (pos - amax) ^ 2 - end - end - - return dist2 > 0 -end - --- aabb.min is a vec3 --- aabb.max is a vec3 --- frustum.left is a plane { a, b, c, d } --- frustum.right is a plane { a, b, c, d } --- frustum.bottom is a plane { a, b, c, d } --- frustum.top is a plane { a, b, c, d } --- frustum.near is a plane { a, b, c, d } --- frustum.far is a plane { a, b, c, d } -function intersect.aabb_frustum(aabb, frustum) - -- Indexed for the 'index trick' later - local box = { - aabb.min, - aabb.max - } - - -- We have 6 planes defining the frustum, 5 if infinite. - local planes = { - frustum.left, - frustum.right, - frustum.bottom, - frustum.top, - frustum.near, - frustum.far or false - } - - -- Skip the last test for infinite projections, it'll never fail. - if not planes[6] then - table.remove(planes) - end - - for i = 1, #planes do - -- This is the current plane - local p = planes[i] - - -- p-vertex selection (with the index trick) - -- According to the plane normal we can know the - -- indices of the positive vertex - local px = p.a > 0.0 and 2 or 1 - local py = p.b > 0.0 and 2 or 1 - local pz = p.c > 0.0 and 2 or 1 - - -- project p-vertex on plane normal - -- (How far is p-vertex from the origin) - local dot = (p.a * box[px].x) + (p.b * box[py].y) + (p.c * box[pz].z) - - -- Doesn't intersect if it is behind the plane - if dot < -p.d then - return false - end - end - - return true -end - --- outer.min is a vec3 --- outer.max is a vec3 --- inner.min is a vec3 --- inner.max is a vec3 -function intersect.encapsulate_aabb(outer, inner) - return - outer.min.x <= inner.min.x and - outer.max.x >= inner.max.x and - outer.min.y <= inner.min.y and - outer.max.y >= inner.max.y and - outer.min.z <= inner.min.z and - outer.max.z >= inner.max.z -end - --- a.position is a vec3 --- a.radius is a number --- b.position is a vec3 --- b.radius is a number -function intersect.circle_circle(a, b) - return a.position:dist(b.position) <= a.radius + b.radius -end - --- a.position is a vec3 --- a.radius is a number --- b.position is a vec3 --- b.radius is a number -function intersect.sphere_sphere(a, b) - return intersect.circle_circle(a, b) -end - --- http://realtimecollisiondetection.net/blog/?p=103 --- sphere.position is a vec3 --- sphere.radius is a number --- triangle[1] is a vec3 --- triangle[2] is a vec3 --- triangle[3] is a vec3 -function intersect.sphere_triangle(sphere, triangle) - -- Sphere is centered at origin - local A = triangle[1] - sphere.position - local B = triangle[2] - sphere.position - local C = triangle[3] - sphere.position - - -- Compute normal of triangle plane - local V = (B - A):cross(C - A) - - -- Test if sphere lies outside triangle plane - local rr = sphere.radius * sphere.radius - local d = A:dot(V) - local e = V:dot(V) - local s1 = d * d > rr * e - - -- Test if sphere lies outside triangle vertices - local aa = A:dot(A) - local ab = A:dot(B) - local ac = A:dot(C) - local bb = B:dot(B) - local bc = B:dot(C) - local cc = C:dot(C) - - local s2 = (aa > rr) and (ab > aa) and (ac > aa) - local s3 = (bb > rr) and (ab > bb) and (bc > bb) - local s4 = (cc > rr) and (ac > cc) and (bc > cc) - - -- Test is sphere lies outside triangle edges - local AB = B - A - local BC = C - B - local CA = A - C - - local d1 = ab - aa - local d2 = bc - bb - local d3 = ac - cc - - local e1 = AB:dot(AB) - local e2 = BC:dot(BC) - local e3 = CA:dot(CA) - - local Q1 = A * e1 - AB * d1 - local Q2 = B * e2 - BC * d2 - local Q3 = C * e3 - CA * d3 - - local QC = C * e1 - Q1 - local QA = A * e2 - Q2 - local QB = B * e3 - Q3 - - local s5 = (Q1:dot(Q1) > rr * e1 * e1) and (Q1:dot(QC) > 0) - local s6 = (Q2:dot(Q2) > rr * e2 * e2) and (Q2:dot(QA) > 0) - local s7 = (Q3:dot(Q3) > rr * e3 * e3) and (Q3:dot(QB) > 0) - - -- Return whether or not any of the tests passed - return s1 or s2 or s3 or s4 or s5 or s6 or s7 -end - --- sphere.position is a vec3 --- sphere.radius is a number --- frustum.left is a plane { a, b, c, d } --- frustum.right is a plane { a, b, c, d } --- frustum.bottom is a plane { a, b, c, d } --- frustum.top is a plane { a, b, c, d } --- frustum.near is a plane { a, b, c, d } --- frustum.far is a plane { a, b, c, d } -function intersect.sphere_frustum(sphere, frustum) - local x, y, z = sphere.position:unpack() - local planes = { - frustum.left, - frustum.right, - frustum.bottom, - frustum.top, - frustum.near - } - - if frustum.far then - table.insert(planes, frustum.far, 5) - end - - local dot - for i = 1, #planes do - dot = planes[i].a * x + planes[i].b * y + planes[i].c * z + planes[i].d - - if dot <= -sphere.radius then - return false - end - end - - -- dot + radius is the distance of the object from the near plane. - -- make sure that the near plane is the last test! - return dot + sphere.radius -end - -function intersect.capsule_capsule(c1, c2) - local dist2, p1, p2 = intersect.closest_point_segment_segment(c1.a, c1.b, c2.a, c2.b) - local radius = c1.radius + c2.radius - - if dist2 <= radius * radius then - return p1, p2 - end - - return false -end - -function intersect.closest_point_segment_segment(p1, p2, p3, p4) - local s -- Distance of intersection along segment 1 - local t -- Distance of intersection along segment 2 - local c1 -- Collision point on segment 1 - local c2 -- Collision point on segment 2 - - local d1 = p2 - p1 -- Direction of segment 1 - local d2 = p4 - p3 -- Direction of segment 2 - local r = p1 - p3 - local a = d1:dot(d1) - local e = d2:dot(d2) - local f = d2:dot(r) - - -- Check if both segments degenerate into points - if a <= DBL_EPSILON and e <= DBL_EPSILON then - s = 0 - t = 0 - c1 = p1 - c2 = p3 - return (c1 - c2):dot(c1 - c2), s, t, c1, c2 - end - - -- Check if segment 1 degenerates into a point - if a <= DBL_EPSILON then - s = 0 - t = utils.clamp(f / e, 0, 1) - else - local c = d1:dot(r) - - -- Check is segment 2 degenerates into a point - if e <= DBL_EPSILON then - t = 0 - s = utils.clamp(-c / a, 0, 1) - else - local b = d1:dot(d2) - local denom = a * e - b * b - - if abs(denom) > 0 then - s = utils.clamp((b * f - c * e) / denom, 0, 1) - else - s = 0 - end - - t = (b * s + f) / e - - if t < 0 then - t = 0 - s = utils.clamp(-c / a, 0, 1) - elseif t > 1 then - t = 1 - s = utils.clamp((b - c) / a, 0, 1) - end - end - end - - c1 = p1 + d1 * s - c2 = p3 + d2 * t - - return (c1 - c2):dot(c1 - c2), c1, c2, s, t -end - -return intersect diff --git a/love2d/lua/ecs/plugins/cmpl/modules/mat4.lua b/love2d/lua/ecs/plugins/cmpl/modules/mat4.lua deleted file mode 100644 index 3cd06f5d..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/mat4.lua +++ /dev/null @@ -1,861 +0,0 @@ ---- double 4x4, 1-based, column major matrices --- @module mat4 -local modules = (...):gsub('%.[^%.]+$', '') .. "." -local constants = require(modules .. "constants") -local vec2 = require(modules .. "vec2") -local vec3 = require(modules .. "vec3") -local quat = require(modules .. "quat") -local utils = require(modules .. "utils") -local sqrt = math.sqrt -local cos = math.cos -local sin = math.sin -local tan = math.tan -local rad = math.rad -local mat4 = {} -local mat4_mt = {} - --- Private constructor. -local function new(m) - m = m or { - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0 - } - m._m = m - return setmetatable(m, mat4_mt) -end - -- Convert matrix into identity -local function identity(m) - m[1], m[2], m[3], m[4] = 1, 0, 0, 0 - m[5], m[6], m[7], m[8] = 0, 1, 0, 0 - m[9], m[10], m[11], m[12] = 0, 0, 1, 0 - m[13], m[14], m[15], m[16] = 0, 0, 0, 1 - return m -end - --- Do the check to see if JIT is enabled. If so use the optimized FFI structs. -local status, ffi, the_type -if type(jit) == "table" and jit.status() then - -- status, ffi = pcall(require, "ffi") - if status then - ffi.cdef "typedef struct { double _m[16]; } cpml_mat4;" - new = ffi.typeof("cpml_mat4") - end -end - --- Statically allocate a temporary variable used in some of our functions. -local tmp = new() -local tm4 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } -local tv4 = { 0, 0, 0, 0 } -local forward, side, new_up = vec3(), vec3(), vec3() - ---- The public constructor. --- @param a Can be of four types:
--- table Length 16 (4x4 matrix) --- table Length 9 (3x3 matrix) --- table Length 4 (4 vec4s) --- nil --- @treturn mat4 out -function mat4.new(a) - local out = new() - - -- 4x4 matrix - if type(a) == "table" and #a == 16 then - for i = 1, 16 do - out[i] = tonumber(a[i]) - end - - -- 3x3 matrix - elseif type(a) == "table" and #a == 9 then - out[1], out[2], out[3] = a[1], a[2], a[3] - out[5], out[6], out[7] = a[4], a[5], a[6] - out[9], out[10], out[11] = a[7], a[8], a[9] - out[16] = 1 - - -- 4 vec4s - elseif type(a) == "table" and type(a[1]) == "table" then - local idx = 1 - for i = 1, 4 do - for j = 1, 4 do - out[idx] = a[i][j] - idx = idx + 1 - end - end - - -- nil - else - out[1] = 1 - out[6] = 1 - out[11] = 1 - out[16] = 1 - end - - return out -end - ---- Create an identity matrix. --- @tparam mat4 a Matrix to overwrite --- @treturn mat4 out -function mat4.identity(a) - return identity(a or new()) -end - ---- Create a matrix from an angle/axis pair. --- @tparam number angle Angle of rotation --- @tparam vec3 axis Axis of rotation --- @treturn mat4 out -function mat4.from_angle_axis(angle, axis) - local l = axis:len() - if l == 0 then - return new() - end - - local x, y, z = axis.x / l, axis.y / l, axis.z / l - local c = cos(angle) - local s = sin(angle) - - return new { - x*x*(1-c)+c, y*x*(1-c)+z*s, x*z*(1-c)-y*s, 0, - x*y*(1-c)-z*s, y*y*(1-c)+c, y*z*(1-c)+x*s, 0, - x*z*(1-c)+y*s, y*z*(1-c)-x*s, z*z*(1-c)+c, 0, - 0, 0, 0, 1 - } -end - ---- Create a matrix from a quaternion. --- @tparam quat q Rotation quaternion --- @treturn mat4 out -function mat4.from_quaternion(q) - return mat4.from_angle_axis(q:to_angle_axis()) -end - ---- Create a matrix from a direction/up pair. --- @tparam vec3 direction Vector direction --- @tparam vec3 up Up direction --- @treturn mat4 out -function mat4.from_direction(direction, up) - local forward = vec3.normalize(direction) - local side = vec3.cross(forward, up):normalize() - local new_up = vec3.cross(side, forward):normalize() - - local out = new() - out[1] = side.x - out[5] = side.y - out[9] = side.z - out[2] = new_up.x - out[6] = new_up.y - out[10] = new_up.z - out[3] = forward.x - out[7] = forward.y - out[11] = forward.z - out[16] = 1 - - return out -end - ---- Create a matrix from a transform. --- @tparam vec3 trans Translation vector --- @tparam quat rot Rotation quaternion --- @tparam vec3 scale Scale vector --- @treturn mat4 out -function mat4.from_transform(trans, rot, scale) - local angle, axis = rot:to_angle_axis() - local l = axis:len() - - if l == 0 then - return new() - end - - local x, y, z = axis.x / l, axis.y / l, axis.z / l - local c = cos(angle) - local s = sin(angle) - - return new { - x*x*(1-c)+c, y*x*(1-c)+z*s, x*z*(1-c)-y*s, 0, - x*y*(1-c)-z*s, y*y*(1-c)+c, y*z*(1-c)+x*s, 0, - x*z*(1-c)+y*s, y*z*(1-c)-x*s, z*z*(1-c)+c, 0, - trans.x, trans.y, trans.z, 1 - } -end - ---- Create matrix from orthogonal. --- @tparam number left --- @tparam number right --- @tparam number top --- @tparam number bottom --- @tparam number near --- @tparam number far --- @treturn mat4 out -function mat4.from_ortho(left, right, top, bottom, near, far) - local out = new() - out[1] = 2 / (right - left) - out[6] = 2 / (top - bottom) - out[11] = -2 / (far - near) - out[13] = -((right + left) / (right - left)) - out[14] = -((top + bottom) / (top - bottom)) - out[15] = -((far + near) / (far - near)) - out[16] = 1 - - return out -end - ---- Create matrix from perspective. --- @tparam number fovy Field of view --- @tparam number aspect Aspect ratio --- @tparam number near Near plane --- @tparam number far Far plane --- @treturn mat4 out -function mat4.from_perspective(fovy, aspect, near, far) - assert(aspect ~= 0) - assert(near ~= far) - - local t = tan(rad(fovy) / 2) - local out = new() - out[1] = 1 / (t * aspect) - out[6] = 1 / t - out[11] = -(far + near) / (far - near) - out[12] = -1 - out[15] = -(2 * far * near) / (far - near) - out[16] = 0 - - return out -end - --- Adapted from the Oculus SDK. ---- Create matrix from HMD perspective. --- @tparam number tanHalfFov Tangent of half of the field of view --- @tparam number zNear Near plane --- @tparam number zFar Far plane --- @tparam boolean flipZ Z axis is flipped or not --- @tparam boolean farAtInfinity Far plane is infinite or not --- @treturn mat4 out -function mat4.from_hmd_perspective(tanHalfFov, zNear, zFar, flipZ, farAtInfinity) - -- CPML is right-handed and intended for GL, so these don't need to be arguments. - local rightHanded = true - local isOpenGL = true - - local function CreateNDCScaleAndOffsetFromFov(tanHalfFov) - x_scale = 2 / (tanHalfFov.LeftTan + tanHalfFov.RightTan) - x_offset = (tanHalfFov.LeftTan - tanHalfFov.RightTan) * x_scale * 0.5 - y_scale = 2 / (tanHalfFov.UpTan + tanHalfFov.DownTan ) - y_offset = (tanHalfFov.UpTan - tanHalfFov.DownTan ) * y_scale * 0.5 - - local result = { - Scale = vec2(x_scale, y_scale), - Offset = vec2(x_offset, y_offset) - } - - -- Hey - why is that Y.Offset negated? - -- It's because a projection matrix transforms from world coords with Y=up, - -- whereas this is from NDC which is Y=down. - return result - end - - if not flipZ and farAtInfinity then - print("Error: Cannot push Far Clip to Infinity when Z-order is not flipped") - farAtInfinity = false - end - - -- A projection matrix is very like a scaling from NDC, so we can start with that. - local scaleAndOffset = CreateNDCScaleAndOffsetFromFov(tanHalfFov) - local handednessScale = rightHanded and -1.0 or 1.0 - local projection = new() - - -- Produces X result, mapping clip edges to [-w,+w] - projection[1] = scaleAndOffset.Scale.x - projection[2] = 0 - projection[3] = handednessScale * scaleAndOffset.Offset.x - projection[4] = 0 - - -- Produces Y result, mapping clip edges to [-w,+w] - -- Hey - why is that YOffset negated? - -- It's because a projection matrix transforms from world coords with Y=up, - -- whereas this is derived from an NDC scaling, which is Y=down. - projection[5] = 0 - projection[6] = scaleAndOffset.Scale.y - projection[7] = handednessScale * -scaleAndOffset.Offset.y - projection[8] = 0 - - -- Produces Z-buffer result - app needs to fill this in with whatever Z range it wants. - -- We'll just use some defaults for now. - projection[9] = 0 - projection[10] = 0 - - if farAtInfinity then - if isOpenGL then - -- It's not clear this makes sense for OpenGL - you don't get the same precision benefits you do in D3D. - projection[11] = -handednessScale - projection[12] = 2.0 * zNear - else - projection[11] = 0 - projection[12] = zNear - end - else - if isOpenGL then - -- Clip range is [-w,+w], so 0 is at the middle of the range. - projection[11] = -handednessScale * (flipZ and -1.0 or 1.0) * (zNear + zFar) / (zNear - zFar) - projection[12] = 2.0 * ((flipZ and -zFar or zFar) * zNear) / (zNear - zFar) - else - -- Clip range is [0,+w], so 0 is at the start of the range. - projection[11] = -handednessScale * (flipZ and -zNear or zFar) / (zNear - zFar) - projection[12] = ((flipZ and -zFar or zFar) * zNear) / (zNear - zFar) - end - end - - -- Produces W result (= Z in) - projection[13] = 0 - projection[14] = 0 - projection[15] = handednessScale - projection[16] = 0 - - return projection:transpose(projection) -end - ---- Clone a matrix. --- @tparam mat4 a Matrix to clone --- @treturn mat4 out -function mat4.clone(a) - return new(a) -end - ---- Multiply two matrices. --- @tparam mat4 out Matrix to store the result --- @tparam mat4 a Left hand operand --- @tparam mat4 b Right hand operand --- @treturn mat4 out -function mat4.mul(out, a, b) - tm4[1] = a[1] * b[1] + a[2] * b[5] + a[3] * b[9] + a[4] * b[13] - tm4[2] = a[1] * b[2] + a[2] * b[6] + a[3] * b[10] + a[4] * b[14] - tm4[3] = a[1] * b[3] + a[2] * b[7] + a[3] * b[11] + a[4] * b[15] - tm4[4] = a[1] * b[4] + a[2] * b[8] + a[3] * b[12] + a[4] * b[16] - tm4[5] = a[5] * b[1] + a[6] * b[5] + a[7] * b[9] + a[8] * b[13] - tm4[6] = a[5] * b[2] + a[6] * b[6] + a[7] * b[10] + a[8] * b[14] - tm4[7] = a[5] * b[3] + a[6] * b[7] + a[7] * b[11] + a[8] * b[15] - tm4[8] = a[5] * b[4] + a[6] * b[8] + a[7] * b[12] + a[8] * b[16] - tm4[9] = a[9] * b[1] + a[10] * b[5] + a[11] * b[9] + a[12] * b[13] - tm4[10] = a[9] * b[2] + a[10] * b[6] + a[11] * b[10] + a[12] * b[14] - tm4[11] = a[9] * b[3] + a[10] * b[7] + a[11] * b[11] + a[12] * b[15] - tm4[12] = a[9] * b[4] + a[10] * b[8] + a[11] * b[12] + a[12] * b[16] - tm4[13] = a[13] * b[1] + a[14] * b[5] + a[15] * b[9] + a[16] * b[13] - tm4[14] = a[13] * b[2] + a[14] * b[6] + a[15] * b[10] + a[16] * b[14] - tm4[15] = a[13] * b[3] + a[14] * b[7] + a[15] * b[11] + a[16] * b[15] - tm4[16] = a[13] * b[4] + a[14] * b[8] + a[15] * b[12] + a[16] * b[16] - - for i=1, 16 do - out[i] = tm4[i] - end - - return out -end - ---- Multiply a matrix and a vec4. --- @tparam mat4 out Matrix to store the result --- @tparam mat4 a Left hand operand --- @tparam table b Right hand operand --- @treturn mat4 out -function mat4.mul_vec4(out, a, b) - tv4[1] = b[1] * a[1] + b[2] * a[5] + b [3] * a[9] + b[4] * a[13] - tv4[2] = b[1] * a[2] + b[2] * a[6] + b [3] * a[10] + b[4] * a[14] - tv4[3] = b[1] * a[3] + b[2] * a[7] + b [3] * a[11] + b[4] * a[15] - tv4[4] = b[1] * a[4] + b[2] * a[8] + b [3] * a[12] + b[4] * a[16] - - for i=1, 4 do - out[i] = tv4[i] - end - - return out -end - ---- Invert a matrix. --- @tparam mat4 out Matrix to store the result --- @tparam mat4 a Matrix to invert --- @treturn mat4 out -function mat4.invert(out, a) - tm4[1] = a[6] * a[11] * a[16] - a[6] * a[12] * a[15] - a[10] * a[7] * a[16] + a[10] * a[8] * a[15] + a[14] * a[7] * a[12] - a[14] * a[8] * a[11] - tm4[2] = -a[2] * a[11] * a[16] + a[2] * a[12] * a[15] + a[10] * a[3] * a[16] - a[10] * a[4] * a[15] - a[14] * a[3] * a[12] + a[14] * a[4] * a[11] - tm4[3] = a[2] * a[7] * a[16] - a[2] * a[8] * a[15] - a[6] * a[3] * a[16] + a[6] * a[4] * a[15] + a[14] * a[3] * a[8] - a[14] * a[4] * a[7] - tm4[4] = -a[2] * a[7] * a[12] + a[2] * a[8] * a[11] + a[6] * a[3] * a[12] - a[6] * a[4] * a[11] - a[10] * a[3] * a[8] + a[10] * a[4] * a[7] - tm4[5] = -a[5] * a[11] * a[16] + a[5] * a[12] * a[15] + a[9] * a[7] * a[16] - a[9] * a[8] * a[15] - a[13] * a[7] * a[12] + a[13] * a[8] * a[11] - tm4[6] = a[1] * a[11] * a[16] - a[1] * a[12] * a[15] - a[9] * a[3] * a[16] + a[9] * a[4] * a[15] + a[13] * a[3] * a[12] - a[13] * a[4] * a[11] - tm4[7] = -a[1] * a[7] * a[16] + a[1] * a[8] * a[15] + a[5] * a[3] * a[16] - a[5] * a[4] * a[15] - a[13] * a[3] * a[8] + a[13] * a[4] * a[7] - tm4[8] = a[1] * a[7] * a[12] - a[1] * a[8] * a[11] - a[5] * a[3] * a[12] + a[5] * a[4] * a[11] + a[9] * a[3] * a[8] - a[9] * a[4] * a[7] - tm4[9] = a[5] * a[10] * a[16] - a[5] * a[12] * a[14] - a[9] * a[6] * a[16] + a[9] * a[8] * a[14] + a[13] * a[6] * a[12] - a[13] * a[8] * a[10] - tm4[10] = -a[1] * a[10] * a[16] + a[1] * a[12] * a[14] + a[9] * a[2] * a[16] - a[9] * a[4] * a[14] - a[13] * a[2] * a[12] + a[13] * a[4] * a[10] - tm4[11] = a[1] * a[6] * a[16] - a[1] * a[8] * a[14] - a[5] * a[2] * a[16] + a[5] * a[4] * a[14] + a[13] * a[2] * a[8] - a[13] * a[4] * a[6] - tm4[12] = -a[1] * a[6] * a[12] + a[1] * a[8] * a[10] + a[5] * a[2] * a[12] - a[5] * a[4] * a[10] - a[9] * a[2] * a[8] + a[9] * a[4] * a[6] - tm4[13] = -a[5] * a[10] * a[15] + a[5] * a[11] * a[14] + a[9] * a[6] * a[15] - a[9] * a[7] * a[14] - a[13] * a[6] * a[11] + a[13] * a[7] * a[10] - tm4[14] = a[1] * a[10] * a[15] - a[1] * a[11] * a[14] - a[9] * a[2] * a[15] + a[9] * a[3] * a[14] + a[13] * a[2] * a[11] - a[13] * a[3] * a[10] - tm4[15] = -a[1] * a[6] * a[15] + a[1] * a[7] * a[14] + a[5] * a[2] * a[15] - a[5] * a[3] * a[14] - a[13] * a[2] * a[7] + a[13] * a[3] * a[6] - tm4[16] = a[1] * a[6] * a[11] - a[1] * a[7] * a[10] - a[5] * a[2] * a[11] + a[5] * a[3] * a[10] + a[9] * a[2] * a[7] - a[9] * a[3] * a[6] - - for i=1, 16 do - out[i] = tm4[i] - end - - local det = a[1] * out[1] + a[2] * out[5] + a[3] * out[9] + a[4] * out[13] - - if det == 0 then return a end - - det = 1 / det - - for i = 1, 16 do - out[i] = out[i] * det - end - - return out -end - ---- Scale a matrix. --- @tparam mat4 out Matrix to store the result --- @tparam mat4 a Matrix to scale --- @tparam vec3 s Scalar --- @treturn mat4 out -function mat4.scale(out, a, s) - identity(tmp) - tmp[1] = s.x - tmp[6] = s.y - tmp[11] = s.z - - return out:mul(tmp, a) -end - ---- Rotate a matrix. --- @tparam mat4 out Matrix to store the result --- @tparam mat4 a Matrix to rotate --- @tparam number angle Angle to rotate by (in radians) --- @tparam vec3 axis Axis to rotate on --- @treturn mat4 out -function mat4.rotate(out, a, angle, axis) - if type(angle) == "table" or type(angle) == "cdata" then - angle, axis = angle:to_angle_axis() - end - - local l = axis:len() - - if l == 0 then - return a - end - - local x, y, z = axis.x / l, axis.y / l, axis.z / l - local c = cos(angle) - local s = sin(angle) - - identity(tmp) - tmp[1] = x * x * (1 - c) + c - tmp[2] = y * x * (1 - c) + z * s - tmp[3] = x * z * (1 - c) - y * s - tmp[5] = x * y * (1 - c) - z * s - tmp[6] = y * y * (1 - c) + c - tmp[7] = y * z * (1 - c) + x * s - tmp[9] = x * z * (1 - c) + y * s - tmp[10] = y * z * (1 - c) - x * s - tmp[11] = z * z * (1 - c) + c - - return out:mul(tmp, a) -end - ---- Translate a matrix. --- @tparam mat4 out Matrix to store the result --- @tparam mat4 a Matrix to translate --- @tparam vec3 t Translation vector --- @treturn mat4 out -function mat4.translate(out, a, t) - identity(tmp) - tmp[13] = t.x - tmp[14] = t.y - tmp[15] = t.z - - return out:mul(tmp, a) -end - ---- Shear a matrix. --- @tparam mat4 out Matrix to store the result --- @tparam mat4 a Matrix to translate --- @tparam number yx --- @tparam number zx --- @tparam number xy --- @tparam number zy --- @tparam number xz --- @tparam number yz --- @treturn mat4 out -function mat4.shear(out, a, yx, zx, xy, zy, xz, yz) - identity(tmp) - tmp[2] = yx or 0 - tmp[3] = zx or 0 - tmp[5] = xy or 0 - tmp[7] = zy or 0 - tmp[9] = xz or 0 - tmp[10] = yz or 0 - - return out:mul(tmp, a) -end - ---- Reflect a matrix across a plane. --- @tparam mat4 Matrix to store the result --- @tparam a Matrix to reflect --- @tparam vec3 position A point on the plane --- @tparam vec3 normal The (normalized!) normal vector of the plane -function mat4.reflect(out, a, position, normal) - local nx, ny, nz = normal:unpack() - local d = -position:dot(normal) - tmp[1] = 1 - 2 * nx ^ 2 - tmp[2] = 2 * nx * ny - tmp[3] = -2 * nx * nz - tmp[4] = 0 - tmp[5] = -2 * nx * ny - tmp[6] = 1 - 2 * ny ^ 2 - tmp[7] = -2 * ny * nz - tmp[8] = 0 - tmp[9] = -2 * nx * nz - tmp[10] = -2 * ny * nz - tmp[11] = 1 - 2 * nz ^ 2 - tmp[12] = 0 - tmp[13] = -2 * nx * d - tmp[14] = -2 * ny * d - tmp[15] = -2 * nz * d - tmp[16] = 1 - - return out:mul(tmp, a) -end - ---- Transform matrix to look at a point. --- @tparam mat4 out Matrix to store result --- @tparam mat4 a Matrix to transform --- @tparam vec3 eye Location of viewer's view plane --- @tparam vec3 center Location of object to view --- @tparam vec3 up Up direction --- @treturn mat4 out -function mat4.look_at(out, a, eye, look_at, up) - local z_axis = (eye - look_at):normalize() - local x_axis = up:cross(z_axis):normalize() - local y_axis = z_axis:cross(x_axis) - out[1] = x_axis.x - out[2] = y_axis.x - out[3] = z_axis.x - out[4] = 0 - out[5] = x_axis.y - out[6] = y_axis.y - out[7] = z_axis.y - out[8] = 0 - out[9] = x_axis.z - out[10] = y_axis.z - out[11] = z_axis.z - out[12] = 0 - out[13] = -out[ 1]*eye.x - out[4+1]*eye.y - out[8+1]*eye.z - out[14] = -out[ 2]*eye.x - out[4+2]*eye.y - out[8+2]*eye.z - out[15] = -out[ 3]*eye.x - out[4+3]*eye.y - out[8+3]*eye.z - out[16] = -out[ 4]*eye.x - out[4+4]*eye.y - out[8+4]*eye.z + 1 - - return out -end - ---- Transpose a matrix. --- @tparam mat4 out Matrix to store the result --- @tparam mat4 a Matrix to transpose --- @treturn mat4 out -function mat4.transpose(out, a) - tm4[1] = a[1] - tm4[2] = a[5] - tm4[3] = a[9] - tm4[4] = a[13] - tm4[5] = a[2] - tm4[6] = a[6] - tm4[7] = a[10] - tm4[8] = a[14] - tm4[9] = a[3] - tm4[10] = a[7] - tm4[11] = a[11] - tm4[12] = a[15] - tm4[13] = a[4] - tm4[14] = a[8] - tm4[15] = a[12] - tm4[16] = a[16] - - for i=1, 16 do - out[i] = tm4[i] - end - - return out -end - --- https://github.com/g-truc/glm/blob/master/glm/gtc/matrix_transform.inl#L518 ---- Project a matrix from world space to screen space. --- @tparam vec3 obj Object position in world space --- @tparam mat4 view View matrix --- @tparam mat4 projection Projection matrix --- @tparam table viewport XYWH of viewport --- @treturn vec3 win -function mat4.project(obj, view, projection, viewport) - local position = { obj.x, obj.y, obj.z, 1 } - - mat4.mul_vec4(position, view, position) - mat4.mul_vec4(position, projection, position) - - position[1] = position[1] / position[4] * 0.5 + 0.5 - position[2] = position[2] / position[4] * 0.5 + 0.5 - position[3] = position[3] / position[4] * 0.5 + 0.5 - - position[1] = position[1] * viewport[3] + viewport[1] - position[2] = position[2] * viewport[4] + viewport[2] - - return vec3(position[1], position[2], position[3]) -end - --- https://github.com/g-truc/glm/blob/master/glm/gtc/matrix_transform.inl#L544 ---- Unproject a matrix from screen space to world space. --- @tparam vec3 win Object position in screen space --- @tparam mat4 view View matrix --- @tparam mat4 projection Projection matrix --- @tparam table viewport XYWH of viewport --- @treturn vec3 obj -function mat4.unproject(win, view, projection, viewport) - local position = { win.x, win.y, win.z, 1 } - - position[1] = (position[1] - viewport[1]) / viewport[3] - position[2] = (position[2] - viewport[2]) / viewport[4] - - position[1] = position[1] * 2 - 1 - position[2] = position[2] * 2 - 1 - position[3] = position[3] * 2 - 1 - - tmp:mul(projection, view):invert(tmp) - mat4.mul_vec4(position, tmp, position) - - position[1] = position[1] / position[4] - position[2] = position[2] / position[4] - position[3] = position[3] / position[4] - - return vec3(position[1], position[2], position[3]) -end - ---- Return a boolean showing if a table is or is not a mat4. --- @tparam mat4 a Matrix to be tested --- @treturn boolean is_mat4 -function mat4.is_mat4(a) - if type(a) == "cdata" then - return ffi.istype("cpml_mat4", a) - end - - if type(a) ~= "table" then - return false - end - - for i = 1, 16 do - if type(a[i]) ~= "number" then - return false - end - end - - return true -end - ---- Return a formatted string. --- @tparam mat4 a Matrix to be turned into a string --- @treturn string formatted -function mat4.to_string(a) - local str = "[ " - for i = 1, 16 do - str = str .. string.format("%+0.3f", a[i]) - if i < 16 then - str = str .. ", " - end - end - str = str .. " ]" - return str -end - ---- Convert a matrix to vec4s. --- @tparam mat4 a Matrix to be converted --- @treturn table vec4s -function mat4.to_vec4s(a) - return { - { a[1], a[2], a[3], a[4] }, - { a[5], a[6], a[7], a[8] }, - { a[9], a[10], a[11], a[12] }, - { a[13], a[14], a[15], a[16] } - } -end - --- http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/ ---- Convert a matrix to a quaternion. --- @tparam mat4 a Matrix to be converted --- @treturn quat out -function mat4.to_quat(a) - identity(tmp):transpose(a) - - local w = sqrt(1 + tmp[1] + tmp[6] + tmp[11]) / 2 - local scale = w * 4 - local q = quat.new( - tmp[10] - tmp[7] / scale, - tmp[3] - tmp[9] / scale, - tmp[5] - tmp[2] / scale, - w - ) - - return q:normalize(q) -end - --- http://www.crownandcutlass.com/features/technicaldetails/frustum.html ---- Convert a matrix to a frustum. --- @tparam mat4 a Matrix to be converted (projection * view) --- @tparam boolean infinite Infinite removes the far plane --- @treturn frustum out -function mat4.to_frustum(a, infinite) - local t - local frustum = {} - - -- Extract the LEFT plane - frustum.left = {} - frustum.left.a = a[4] + a[1] - frustum.left.b = a[8] + a[5] - frustum.left.c = a[12] + a[9] - frustum.left.d = a[16] + a[13] - - -- Normalize the result - t = sqrt(frustum.left.a * frustum.left.a + frustum.left.b * frustum.left.b + frustum.left.c * frustum.left.c) - frustum.left.a = frustum.left.a / t - frustum.left.b = frustum.left.b / t - frustum.left.c = frustum.left.c / t - frustum.left.d = frustum.left.d / t - - -- Extract the RIGHT plane - frustum.right = {} - frustum.right.a = a[4] - a[1] - frustum.right.b = a[8] - a[5] - frustum.right.c = a[12] - a[9] - frustum.right.d = a[16] - a[13] - - -- Normalize the result - t = sqrt(frustum.right.a * frustum.right.a + frustum.right.b * frustum.right.b + frustum.right.c * frustum.right.c) - frustum.right.a = frustum.right.a / t - frustum.right.b = frustum.right.b / t - frustum.right.c = frustum.right.c / t - frustum.right.d = frustum.right.d / t - - -- Extract the BOTTOM plane - frustum.bottom = {} - frustum.bottom.a = a[4] + a[2] - frustum.bottom.b = a[8] + a[6] - frustum.bottom.c = a[12] + a[10] - frustum.bottom.d = a[16] + a[14] - - -- Normalize the result - t = sqrt(frustum.bottom.a * frustum.bottom.a + frustum.bottom.b * frustum.bottom.b + frustum.bottom.c * frustum.bottom.c) - frustum.bottom.a = frustum.bottom.a / t - frustum.bottom.b = frustum.bottom.b / t - frustum.bottom.c = frustum.bottom.c / t - frustum.bottom.d = frustum.bottom.d / t - - -- Extract the TOP plane - frustum.top = {} - frustum.top.a = a[4] - a[2] - frustum.top.b = a[8] - a[6] - frustum.top.c = a[12] - a[10] - frustum.top.d = a[16] - a[14] - - -- Normalize the result - t = sqrt(frustum.top.a * frustum.top.a + frustum.top.b * frustum.top.b + frustum.top.c * frustum.top.c) - frustum.top.a = frustum.top.a / t - frustum.top.b = frustum.top.b / t - frustum.top.c = frustum.top.c / t - frustum.top.d = frustum.top.d / t - - -- Extract the NEAR plane - frustum.near = {} - frustum.near.a = a[4] + a[3] - frustum.near.b = a[8] + a[7] - frustum.near.c = a[12] + a[11] - frustum.near.d = a[16] + a[15] - - -- Normalize the result - t = sqrt(frustum.near.a * frustum.near.a + frustum.near.b * frustum.near.b + frustum.near.c * frustum.near.c) - frustum.near.a = frustum.near.a / t - frustum.near.b = frustum.near.b / t - frustum.near.c = frustum.near.c / t - frustum.near.d = frustum.near.d / t - - if not infinite then - -- Extract the FAR plane - frustum.far = {} - frustum.far.a = a[4] - a[3] - frustum.far.b = a[8] - a[7] - frustum.far.c = a[12] - a[11] - frustum.far.d = a[16] - a[15] - - -- Normalize the result - t = sqrt(frustum.far.a * frustum.far.a + frustum.far.b * frustum.far.b + frustum.far.c * frustum.far.c) - frustum.far.a = frustum.far.a / t - frustum.far.b = frustum.far.b / t - frustum.far.c = frustum.far.c / t - frustum.far.d = frustum.far.d / t - end - - return frustum -end - -function mat4_mt.__index(t, k) - if type(t) == "cdata" then - if type(k) == "number" then - return t._m[k-1] - end - end - - return rawget(mat4, k) -end - -function mat4_mt.__newindex(t, k, v) - if type(t) == "cdata" then - if type(k) == "number" then - t._m[k-1] = v - end - end -end - -mat4_mt.__tostring = mat4.to_string - -function mat4_mt.__call(_, a) - return mat4.new(a) -end - -function mat4_mt.__unm(a) - return new():invert(a) -end - -function mat4_mt.__eq(a, b) - if not mat4.is_mat4(a) or not mat4.is_mat4(b) then - return false - end - - for i = 1, 16 do - if not utils.tolerance(b[i]-a[i], constants.FLT_EPSILON) then - return false - end - end - - return true -end - -function mat4_mt.__mul(a, b) - assert(mat4.is_mat4(a), "__mul: Wrong argument type for left hand operand. ( expected)") - - if vec3.is_vec3(b) then - return vec3(mat4.mul_vec4({}, a, { b.x, b.y, b.z, 1 })) - end - - assert(mat4.is_mat4(b) or #b == 4, "__mul: Wrong argument type for right hand operand. ( or table #4 expected)") - - if mat4.is_mat4(b) then - return new():mul(a, b) - end - - return mat4.mul_vec4({}, a, b) -end - -if status then - xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded - ffi.metatype(new, mat4_mt) - end, function() end) -end - -return setmetatable({}, mat4_mt) diff --git a/love2d/lua/ecs/plugins/cmpl/modules/mesh.lua b/love2d/lua/ecs/plugins/cmpl/modules/mesh.lua deleted file mode 100644 index 1d25ae7b..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/mesh.lua +++ /dev/null @@ -1,51 +0,0 @@ ---- Mesh utilities --- @module mesh - -local modules = (...):gsub('%.[^%.]+$', '') .. "." -local vec3 = require(modules .. "vec3") -local mesh = {} - --- vertices is an arbitrary list of vec3s -function mesh.average(vertices) - local out = vec3() - for _, v in ipairs(vertices) do - out = out + v - end - return out / #vertices -end - --- triangle[1] is a vec3 --- triangle[2] is a vec3 --- triangle[3] is a vec3 -function mesh.normal(triangle) - local ba = triangle[2] - triangle[1] - local ca = triangle[3] - triangle[1] - return ba:cross(ca):normalize() -end - --- triangle[1] is a vec3 --- triangle[2] is a vec3 --- triangle[3] is a vec3 -function mesh.plane_from_triangle(triangle) - return { - origin = triangle[1], - normal = mesh.normal(triangle) - } -end - --- plane.origin is a vec3 --- plane.normal is a vec3 --- direction is a vec3 -function mesh.is_front_facing(plane, direction) - return plane.normal:dot(direction) >= 0 -end - --- point is a vec3 --- plane.origin is a vec3 --- plane.normal is a vec3 --- plane.dot is a number -function mesh.signed_distance(point, plane) - return point:dot(plane.normal) - plane.normal:dot(plane.origin) -end - -return mesh diff --git a/love2d/lua/ecs/plugins/cmpl/modules/octree.lua b/love2d/lua/ecs/plugins/cmpl/modules/octree.lua deleted file mode 100644 index fbc15f11..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/octree.lua +++ /dev/null @@ -1,634 +0,0 @@ --- https://github.com/Nition/UnityOctree --- https://github.com/Nition/UnityOctree/blob/master/LICENCE --- https://github.com/Nition/UnityOctree/blob/master/Scripts/BoundsOctree.cs --- https://github.com/Nition/UnityOctree/blob/master/Scripts/BoundsOctreeNode.cs - ---- Octree --- @module octree - -local modules = (...):gsub('%.[^%.]+$', '') .. "." -local intersect = require(modules .. "intersect") -local mat4 = require(modules .. "mat4") -local utils = require(modules .. "utils") -local vec3 = require(modules .. "vec3") -local Octree = {} -local OctreeNode = {} -local Node - -Octree.__index = Octree -OctreeNode.__index = OctreeNode - ---== Octree ==-- - ---- Constructor for the bounds octree. --- @param initialWorldSize Size of the sides of the initial node, in metres. The octree will never shrink smaller than this --- @param initialWorldPos Position of the centre of the initial node --- @param minNodeSize Nodes will stop splitting if the new nodes would be smaller than this (metres) --- @param looseness Clamped between 1 and 2. Values > 1 let nodes overlap -local function new(initialWorldSize, initialWorldPos, minNodeSize, looseness) - local tree = setmetatable({}, Octree) - - if minNodeSize > initialWorldSize then - print("Minimum node size must be at least as big as the initial world size. Was: " .. minNodeSize .. " Adjusted to: " .. initialWorldSize) - minNodeSize = initialWorldSize - end - - -- The total amount of objects currently in the tree - tree.count = 0 - - -- Size that the octree was on creation - tree.initialSize = initialWorldSize - - -- Minimum side length that a node can be - essentially an alternative to having a max depth - tree.minSize = minNodeSize - - -- Should be a value between 1 and 2. A multiplier for the base size of a node. - -- 1.0 is a "normal" octree, while values > 1 have overlap - tree.looseness = utils.clamp(looseness, 1, 2) - - -- Root node of the octree - tree.rootNode = Node(tree.initialSize, tree.minSize, tree.looseness, initialWorldPos) - - return tree -end - ---- Used when growing the octree. Works out where the old root node would fit inside a new, larger root node. --- @param xDir X direction of growth. 1 or -1 --- @param yDir Y direction of growth. 1 or -1 --- @param zDir Z direction of growth. 1 or -1 --- @return Octant where the root node should be -local function get_root_pos_index(xDir, yDir, zDir) - local result = xDir > 0 and 1 or 0 - if yDir < 0 then return result + 4 end - if zDir > 0 then return result + 2 end -end - ---- Add an object. --- @param obj Object to add --- @param objBounds 3D bounding box around the object -function Octree:add(obj, objBounds) - -- Add object or expand the octree until it can be added - local count = 0 -- Safety check against infinite/excessive growth - - while not self.rootNode:add(obj, objBounds) do - count = count + 1 - self:grow(objBounds.center - self.rootNode.center) - - if count > 20 then - print("Aborted Add operation as it seemed to be going on forever (" .. count - 1 .. ") attempts at growing the octree.") - return - end - - self.count = self.count + 1 - end -end - ---- Remove an object. Makes the assumption that the object only exists once in the tree. --- @param obj Object to remove --- @return bool True if the object was removed successfully -function Octree:remove(obj) - local removed = self.rootNode:remove(obj) - - -- See if we can shrink the octree down now that we've removed the item - if removed then - self.count = self.count - 1 - self:shrink() - end - - return removed -end - ---- Check if the specified bounds intersect with anything in the tree. See also: get_colliding. --- @param checkBounds bounds to check --- @return bool True if there was a collision -function Octree:is_colliding(checkBounds) - return self.rootNode:is_colliding(checkBounds) -end - ---- Returns an array of objects that intersect with the specified bounds, if any. Otherwise returns an empty array. See also: is_colliding. --- @param checkBounds bounds to check --- @return table Objects that intersect with the specified bounds -function Octree:get_colliding(checkBounds) - return self.rootNode:get_colliding(checkBounds) -end - ---- Cast a ray through the node and its children --- @param ray Ray with a position and a direction --- @param func Function to execute on any objects within child nodes --- @param out Table to store results of func in --- @return boolean True if an intersect detected -function Octree:cast_ray(ray, func, out) - assert(func) - return self.rootNode:cast_ray(ray, func, out) -end - ---- Draws node boundaries visually for debugging. -function Octree:draw_bounds(cube) - self.rootNode:draw_bounds(cube) -end - ---- Draws the bounds of all objects in the tree visually for debugging. -function Octree:draw_objects(cube, filter) - self.rootNode:draw_objects(cube, filter) -end - ---- Grow the octree to fit in all objects. --- @param direction Direction to grow -function Octree:grow(direction) - local xDirection = direction.x >= 0 and 1 or -1 - local yDirection = direction.y >= 0 and 1 or -1 - local zDirection = direction.z >= 0 and 1 or -1 - - local oldRoot = self.rootNode - local half = self.rootNode.baseLength / 2 - local newLength = self.rootNode.baseLength * 2 - local newCenter = self.rootNode.center + vec3(xDirection * half, yDirection * half, zDirection * half) - - -- Create a new, bigger octree root node - self.rootNode = Node(newLength, self.minSize, self.looseness, newCenter) - - -- Create 7 new octree children to go with the old root as children of the new root - local rootPos = get_root_pos_index(xDirection, yDirection, zDirection) - local children = {} - - for i = 0, 7 do - if i == rootPos then - children[i+1] = oldRoot - else - xDirection = i % 2 == 0 and -1 or 1 - yDirection = i > 3 and -1 or 1 - zDirection = (i < 2 or (i > 3 and i < 6)) and -1 or 1 - children[i+1] = Node(self.rootNode.baseLength, self.minSize, self.looseness, newCenter + vec3(xDirection * half, yDirection * half, zDirection * half)) - end - end - - -- Attach the new children to the new root node - self.rootNode:set_children(children) -end - ---- Shrink the octree if possible, else leave it the same. -function Octree:shrink() - self.rootNode = self.rootNode:shrink_if_possible(self.initialSize) -end - ---== Octree Node ==-- - ---- Constructor. --- @param baseLength Length of this node, not taking looseness into account --- @param minSize Minimum size of nodes in this octree --- @param looseness Multiplier for baseLengthVal to get the actual size --- @param center Centre position of this node -local function new_node(baseLength, minSize, looseness, center) - local node = setmetatable({}, OctreeNode) - - -- Objects in this node - node.objects = {} - - -- Child nodes - node.children = {} - - -- If there are already numObjectsAllowed in a node, we split it into children - -- A generally good number seems to be something around 8-15 - node.numObjectsAllowed = 8 - - node:set_values(baseLength, minSize, looseness, center) - - return node -end - -local function new_bound(center, size) - return { - center = center, - size = size, - min = center - (size / 2), - max = center + (size / 2) - } -end - ---- Add an object. --- @param obj Object to add --- @param objBounds 3D bounding box around the object --- @return boolean True if the object fits entirely within this node -function OctreeNode:add(obj, objBounds) - if not intersect.encapsulate_aabb(self.bounds, objBounds) then - return false - end - - -- We know it fits at this level if we've got this far - -- Just add if few objects are here, or children would be below min size - if #self.objects < self.numObjectsAllowed - or self.baseLength / 2 < self.minSize then - table.insert(self.objects, { - data = obj, - bounds = objBounds - }) - else - -- Fits at this level, but we can go deeper. Would it fit there? - - local best_fit_child - - -- Create the 8 children - if #self.children == 0 then - self:split() - - if #self.children == 0 then - print("Child creation failed for an unknown reason. Early exit.") - return false - end - - -- Now that we have the new children, see if this node's existing objects would fit there - for i = #self.objects, 1, -1 do - local object = self.objects[i] - -- Find which child the object is closest to based on where the - -- object's center is located in relation to the octree's center. - best_fit_child = self:best_fit_child(object.bounds) - - -- Does it fit? - if intersect.encapsulate_aabb(self.children[best_fit_child].bounds, object.bounds) then - self.children[best_fit_child]:add(object.data, object.bounds) -- Go a level deeper - table.remove(self.objects, i) -- Remove from here - end - end - end - - -- Now handle the new object we're adding now - best_fit_child = self:best_fit_child(objBounds) - - if intersect.encapsulate_aabb(self.children[best_fit_child].bounds, objBounds) then - self.children[best_fit_child]:add(obj, objBounds) - else - table.insert(self.objects, { - data = obj, - bounds = objBounds - }) - end - end - - return true -end - ---- Remove an object. Makes the assumption that the object only exists once in the tree. --- @param obj Object to remove --- @return boolean True if the object was removed successfully -function OctreeNode:remove(obj) - local removed = false - - for i, object in ipairs(self.objects) do - if object == obj then - removed = table.remove(self.objects, i) and true or false - break - end - end - - if not removed then - for _, child in ipairs(self.children) do - removed = child:remove(obj) - if removed then break end - end - end - - if removed then - -- Check if we should merge nodes now that we've removed an item - if self:should_merge() then - self:merge() - end - end - - return removed -end - ---- Check if the specified bounds intersect with anything in the tree. See also: get_colliding. --- @param checkBounds Bounds to check --- @return boolean True if there was a collision -function OctreeNode:is_colliding(checkBounds) - -- Are the input bounds at least partially in this node? - if not intersect.aabb_aabb(self.bounds, checkBounds) then - return false - end - - -- Check against any objects in this node - for _, object in ipairs(self.objects) do - if intersect.aabb_aabb(object.bounds, checkBounds) then - return true - end - end - - -- Check children - for _, child in ipairs(self.children) do - if child:is_colliding(checkBounds) then - return true - end - end - - return false -end - ---- Returns an array of objects that intersect with the specified bounds, if any. Otherwise returns an empty array. See also: is_colliding. --- @param checkBounds Bounds to check. Passing by ref as it improve performance with structs --- @param results List results --- @return table Objects that intersect with the specified bounds -function OctreeNode:get_colliding(checkBounds, results) - results = results or {} - - -- Are the input bounds at least partially in this node? - if not intersect.aabb_aabb(self.bounds, checkBounds) then - return results - end - - -- Check against any objects in this node - for _, object in ipairs(self.objects) do - if intersect.aabb_aabb(object.bounds, checkBounds) then - table.insert(results, object.data) - end - end - - -- Check children - for _, child in ipairs(self.children) do - results = child:get_colliding(checkBounds, results) - end - - return results -end - ---- Cast a ray through the node and its children --- @param ray Ray with a position and a direction --- @param func Function to execute on any objects within child nodes --- @param out Table to store results of func in --- @param depth (used internally) --- @return boolean True if an intersect is detected -function OctreeNode:cast_ray(ray, func, out, depth) - depth = depth or 1 - - if intersect.ray_aabb(ray, self.bounds) then - if #self.objects > 0 then - local hit = func(ray, self.objects, out) - - if hit then - return hit - end - end - - for _, child in ipairs(self.children) do - local hit = child:cast_ray(ray, func, out, depth + 1) - - if hit then - return hit - end - end - end - - return false -end - ---- Set the 8 children of this octree. --- @param childOctrees The 8 new child nodes -function OctreeNode:set_children(childOctrees) - if #childOctrees ~= 8 then - print("Child octree array must be length 8. Was length: " .. #childOctrees) - return - end - - self.children = childOctrees -end - ---- We can shrink the octree if: ---- - This node is >= double minLength in length ---- - All objects in the root node are within one octant ---- - This node doesn't have children, or does but 7/8 children are empty ---- We can also shrink it if there are no objects left at all! --- @param minLength Minimum dimensions of a node in this octree --- @return table The new root, or the existing one if we didn't shrink -function OctreeNode:shrink_if_possible(minLength) - if self.baseLength < 2 * minLength then - return self - end - - if #self.objects == 0 and #self.children == 0 then - return self - end - - -- Check objects in root - local bestFit = 0 - - for i, object in ipairs(self.objects) do - local newBestFit = self:best_fit_child(object.bounds) - - if i == 1 or newBestFit == bestFit then - -- In same octant as the other(s). Does it fit completely inside that octant? - if intersect.encapsulate_aabb(self.childBounds[newBestFit], object.bounds) then - if bestFit < 1 then - bestFit = newBestFit - end - else - -- Nope, so we can't reduce. Otherwise we continue - return self - end - else - return self -- Can't reduce - objects fit in different octants - end - end - - -- Check objects in children if there are any - if #self.children > 0 then - local childHadContent = false - - for i, child in ipairs(self.children) do - if child:has_any_objects() then - if childHadContent then - return self -- Can't shrink - another child had content already - end - - if bestFit > 0 and bestFit ~= i then - return self -- Can't reduce - objects in root are in a different octant to objects in child - end - - childHadContent = true - bestFit = i - end - end - end - - -- Can reduce - if #self.children == 0 then - -- We don't have any children, so just shrink this node to the new size - -- We already know that everything will still fit in it - self:set_values(self.baseLength / 2, self.minSize, self.looseness, self.childBounds[bestFit].center) - return self - end - - -- We have children. Use the appropriate child as the new root node - return self.children[bestFit] -end - ---- Set values for this node. --- @param baseLength Length of this node, not taking looseness into account --- @param minSize Minimum size of nodes in this octree --- @param looseness Multiplier for baseLengthVal to get the actual size --- @param center Centre position of this node -function OctreeNode:set_values(baseLength, minSize, looseness, center) - -- Length of this node if it has a looseness of 1.0 - self.baseLength = baseLength - - -- Minimum size for a node in this octree - self.minSize = minSize - - -- Looseness value for this node - self.looseness = looseness - - -- Centre of this node - self.center = center - - -- Actual length of sides, taking the looseness value into account - self.adjLength = self.looseness * self.baseLength - - -- Create the bounding box. - self.size = vec3(self.adjLength, self.adjLength, self.adjLength) - - -- Bounding box that represents this node - self.bounds = new_bound(self.center, self.size) - - self.quarter = self.baseLength / 4 - self.childActualLength = (self.baseLength / 2) * self.looseness - self.childActualSize = vec3(self.childActualLength, self.childActualLength, self.childActualLength) - - -- Bounds of potential children to this node. These are actual size (with looseness taken into account), not base size - self.childBounds = { - new_bound(self.center + vec3(-self.quarter, self.quarter, -self.quarter), self.childActualSize), - new_bound(self.center + vec3( self.quarter, self.quarter, -self.quarter), self.childActualSize), - new_bound(self.center + vec3(-self.quarter, self.quarter, self.quarter), self.childActualSize), - new_bound(self.center + vec3( self.quarter, self.quarter, self.quarter), self.childActualSize), - new_bound(self.center + vec3(-self.quarter, -self.quarter, -self.quarter), self.childActualSize), - new_bound(self.center + vec3( self.quarter, -self.quarter, -self.quarter), self.childActualSize), - new_bound(self.center + vec3(-self.quarter, -self.quarter, self.quarter), self.childActualSize), - new_bound(self.center + vec3( self.quarter, -self.quarter, self.quarter), self.childActualSize) - } -end - ---- Splits the octree into eight children. -function OctreeNode:split() - if #self.children > 0 then return end - - local quarter = self.baseLength / 4 - local newLength = self.baseLength / 2 - - table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, quarter, -quarter))) - table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, quarter, -quarter))) - table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, quarter, quarter))) - table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, quarter, quarter))) - table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, -quarter, -quarter))) - table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, -quarter, -quarter))) - table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3(-quarter, -quarter, quarter))) - table.insert(self.children, Node(newLength, self.minSize, self.looseness, self.center + vec3( quarter, -quarter, quarter))) -end - ---- Merge all children into this node - the opposite of Split. ---- Note: We only have to check one level down since a merge will never happen if the children already have children, ---- since THAT won't happen unless there are already too many objects to merge. -function OctreeNode:merge() - for _, child in ipairs(self.children) do - for _, object in ipairs(child.objects) do - table.insert(self.objects, object) - end - end - - -- Remove the child nodes (and the objects in them - they've been added elsewhere now) - self.children = {} -end - ---- Find which child node this object would be most likely to fit in. --- @param objBounds The object's bounds --- @return number One of the eight child octants -function OctreeNode:best_fit_child(objBounds) - return (objBounds.center.x <= self.center.x and 0 or 1) + (objBounds.center.y >= self.center.y and 0 or 4) + (objBounds.center.z <= self.center.z and 0 or 2) + 1 -end - ---- Checks if there are few enough objects in this node and its children that the children should all be merged into this. --- @return boolean True there are less or the same abount of objects in this and its children than numObjectsAllowed -function OctreeNode:should_merge() - local totalObjects = #self.objects - - for _, child in ipairs(self.children) do - if #child.children > 0 then - -- If any of the *children* have children, there are definitely too many to merge, - -- or the child would have been merged already - return false - end - - totalObjects = totalObjects + #child.objects - end - - return totalObjects <= self.numObjectsAllowed -end - ---- Checks if this node or anything below it has something in it. --- @return boolean True if this node or any of its children, grandchildren etc have something in the -function OctreeNode:has_any_objects() - if #self.objects > 0 then return true end - - for _, child in ipairs(self.children) do - if child:has_any_objects() then return true end - end - - return false -end - ---- Draws node boundaries visually for debugging. --- @param cube Cube model to draw --- @param depth Used for recurcive calls to this method -function OctreeNode:draw_bounds(cube, depth) - depth = depth or 0 - local tint = depth / 7 -- Will eventually get values > 1. Color rounds to 1 automatically - - love.graphics.setColor(tint * 255, 0, (1 - tint) * 255) - local m = mat4() - :translate(self.center) - :scale(vec3(self.adjLength, self.adjLength, self.adjLength)) - - love.graphics.updateMatrix("transform", m) - love.graphics.setWireframe(true) - love.graphics.draw(cube) - love.graphics.setWireframe(false) - - for _, child in ipairs(self.children) do - child:draw_bounds(cube, depth + 1) - end - - love.graphics.setColor(255, 255, 255) -end - ---- Draws the bounds of all objects in the tree visually for debugging. --- @param cube Cube model to draw --- @param filter a function returning true or false to determine visibility. -function OctreeNode:draw_objects(cube, filter) - local tint = self.baseLength / 20 - love.graphics.setColor(0, (1 - tint) * 255, tint * 255, 63) - - for _, object in ipairs(self.objects) do - if filter and filter(object.data) or not filter then - local m = mat4() - :translate(object.bounds.center) - :scale(object.bounds.size) - - love.graphics.updateMatrix("transform", m) - love.graphics.draw(cube) - end - end - - for _, child in ipairs(self.children) do - child:draw_objects(cube, filter) - end - - love.graphics.setColor(255, 255, 255) -end - -Node = setmetatable({ - new = new_node -}, { - __call = function(_, ...) return new_node(...) end -}) - -return setmetatable({ - new = new -}, { - __call = function(_, ...) return new(...) end -}) diff --git a/love2d/lua/ecs/plugins/cmpl/modules/quat.lua b/love2d/lua/ecs/plugins/cmpl/modules/quat.lua deleted file mode 100644 index 3805d608..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/quat.lua +++ /dev/null @@ -1,473 +0,0 @@ ---- A quaternion and associated utilities. --- @module quat - -local modules = (...):gsub('%.[^%.]+$', '') .. "." -local constants = require(modules .. "constants") -local vec3 = require(modules .. "vec3") -local DOT_THRESHOLD = constants.DOT_THRESHOLD -local DBL_EPSILON = constants.DBL_EPSILON -local acos = math.acos -local cos = math.cos -local sin = math.sin -local min = math.min -local max = math.max -local sqrt = math.sqrt -local quat = {} -local quat_mt = {} - --- Private constructor. -local function new(x, y, z, w) - return setmetatable({ - x = x or 0, - y = y or 0, - z = z or 0, - w = w or 1 - }, quat_mt) -end - --- Do the check to see if JIT is enabled. If so use the optimized FFI structs. -local status, ffi -if type(jit) == "table" and jit.status() then - status, ffi = pcall(require, "ffi") - if status then - ffi.cdef "typedef struct { double x, y, z, w;} cpml_quat;" - new = ffi.typeof("cpml_quat") - end -end - --- Statically allocate a temporary variable used in some of our functions. -local tmp = new() -local qv, uv, uuv = vec3(), vec3(), vec3() - ---- Constants --- @table quat --- @field unit Unit quaternion --- @field zero Empty quaternion -quat.unit = new(0, 0, 0, 1) -quat.zero = new(0, 0, 0, 0) - ---- The public constructor. --- @param x Can be of two types:
--- number x X component --- table {x, y, z, w} or {x=x, y=y, z=z, w=w} --- @tparam number y Y component --- @tparam number z Z component --- @tparam number w W component --- @treturn quat out -function quat.new(x, y, z, w) - -- number, number, number, number - if x and y and z and w then - assert(type(x) == "number", "new: Wrong argument type for x ( expected)") - assert(type(y) == "number", "new: Wrong argument type for y ( expected)") - assert(type(z) == "number", "new: Wrong argument type for z ( expected)") - assert(type(w) == "number", "new: Wrong argument type for w ( expected)") - - return new(x, y, z, w) - - -- {x, y, z, w} or {x=x, y=y, z=z, w=w} - elseif type(x) == "table" then - local xx, yy, zz, ww = x.x or x[1], x.y or x[2], x.z or x[3], x.w or x[4] - assert(type(xx) == "number", "new: Wrong argument type for x ( expected)") - assert(type(yy) == "number", "new: Wrong argument type for y ( expected)") - assert(type(zz) == "number", "new: Wrong argument type for z ( expected)") - assert(type(ww) == "number", "new: Wrong argument type for w ( expected)") - - return new(xx, yy, zz, ww) - end - - return new(0, 0, 0, 1) -end - ---- Create a quaternion from an angle/axis pair. --- @tparam number angle Angle (in radians) --- @param axis/x -- Can be of two types, a vec3 axis, or the x component of that axis --- @param y axis -- y component of axis (optional, only if x component param used) --- @param z axis -- z component of axis (optional, only if x component param used) --- @treturn quat out -function quat.from_angle_axis(angle, axis, a3, a4) - if axis and a3 and a4 then - local x, y, z = axis, a3, a4 - local s = sin(angle * 0.5) - local c = cos(angle * 0.5) - return new(x * s, y * s, z * s, c) - else - return quat.from_angle_axis(angle, axis.x, axis.y, axis.z) - end -end - ---- Create a quaternion from a normal/up vector pair. --- @tparam vec3 normal --- @tparam vec3 up (optional) --- @treturn quat out -function quat.from_direction(normal, up) - local u = up or vec3.unit_z - local n = normal:normalize() - local a = u:cross(n) - local d = u:dot(n) - return new(a.x, a.y, a.z, d + 1) -end - ---- Clone a quaternion. --- @tparam quat a Quaternion to clone --- @treturn quat out -function quat.clone(a) - return new(a.x, a.y, a.z, a.w) -end - ---- Add two quaternions. --- @tparam quat a Left hand operand --- @tparam quat b Right hand operand --- @treturn quat out -function quat.add(a, b) - return new( - a.x + b.x, - a.y + b.y, - a.z + b.z, - a.w + b.w - ) -end - ---- Subtract a quaternion from another. --- @tparam quat a Left hand operand --- @tparam quat b Right hand operand --- @treturn quat out -function quat.sub(a, b) - return new( - a.x - b.x, - a.y - b.y, - a.z - b.z, - a.w - b.w - ) -end - ---- Multiply two quaternions. --- @tparam quat a Left hand operand --- @tparam quat b Right hand operand --- @treturn quat quaternion equivalent to "apply b, then a" -function quat.mul(a, b) - return new( - a.x * b.w + a.w * b.x + a.y * b.z - a.z * b.y, - a.y * b.w + a.w * b.y + a.z * b.x - a.x * b.z, - a.z * b.w + a.w * b.z + a.x * b.y - a.y * b.x, - a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z - ) -end - ---- Multiply a quaternion and a vec3. --- @tparam quat a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn quat out -function quat.mul_vec3(a, b) - qv.x = a.x - qv.y = a.y - qv.z = a.z - uv = qv:cross(b) - uuv = qv:cross(uv) - return b + ((uv * a.w) + uuv) * 2 -end - ---- Raise a normalized quaternion to a scalar power. --- @tparam quat a Left hand operand (should be a unit quaternion) --- @tparam number s Right hand operand --- @treturn quat out -function quat.pow(a, s) - -- Do it as a slerp between identity and a (code borrowed from slerp) - if a.w < 0 then - a = -a - end - local dot = a.w - - dot = min(max(dot, -1), 1) - - local theta = acos(dot) * s - local c = new(a.x, a.y, a.z, 0):normalize() * sin(theta) - c.w = cos(theta) - return c -end - ---- Normalize a quaternion. --- @tparam quat a Quaternion to normalize --- @treturn quat out -function quat.normalize(a) - if a:is_zero() then - return new(0, 0, 0, 0) - end - return a:scale(1 / a:len()) -end - ---- Get the dot product of two quaternions. --- @tparam quat a Left hand operand --- @tparam quat b Right hand operand --- @treturn number dot -function quat.dot(a, b) - return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w -end - ---- Return the length of a quaternion. --- @tparam quat a Quaternion to get length of --- @treturn number len -function quat.len(a) - return sqrt(a.x * a.x + a.y * a.y + a.z * a.z + a.w * a.w) -end - ---- Return the squared length of a quaternion. --- @tparam quat a Quaternion to get length of --- @treturn number len -function quat.len2(a) - return a.x * a.x + a.y * a.y + a.z * a.z + a.w * a.w -end - ---- Multiply a quaternion by a scalar. --- @tparam quat a Left hand operand --- @tparam number s Right hand operand --- @treturn quat out -function quat.scale(a, s) - return new( - a.x * s, - a.y * s, - a.z * s, - a.w * s - ) -end - ---- Alias of from_angle_axis. --- @tparam number angle Angle (in radians) --- @param axis/x -- Can be of two types, a vec3 axis, or the x component of that axis --- @param y axis -- y component of axis (optional, only if x component param used) --- @param z axis -- z component of axis (optional, only if x component param used) --- @treturn quat out -function quat.rotate(angle, axis, a3, a4) - return quat.from_angle_axis(angle, axis, a3, a4) -end - ---- Return the conjugate of a quaternion. --- @tparam quat a Quaternion to conjugate --- @treturn quat out -function quat.conjugate(a) - return new(-a.x, -a.y, -a.z, a.w) -end - ---- Return the inverse of a quaternion. --- @tparam quat a Quaternion to invert --- @treturn quat out -function quat.inverse(a) - tmp.x = -a.x - tmp.y = -a.y - tmp.z = -a.z - tmp.w = a.w - return tmp:normalize() -end - ---- Return the reciprocal of a quaternion. --- @tparam quat a Quaternion to reciprocate --- @treturn quat out -function quat.reciprocal(a) - if a:is_zero() then - error("Cannot reciprocate a zero quaternion") - return false - end - - tmp.x = -a.x - tmp.y = -a.y - tmp.z = -a.z - tmp.w = a.w - - return tmp:scale(1 / a:len2()) -end - ---- Lerp between two quaternions. --- @tparam quat a Left hand operand --- @tparam quat b Right hand operand --- @tparam number s Step value --- @treturn quat out -function quat.lerp(a, b, s) - return (a + (b - a) * s):normalize() -end - ---- Slerp between two quaternions. --- @tparam quat a Left hand operand --- @tparam quat b Right hand operand --- @tparam number s Step value --- @treturn quat out -function quat.slerp(a, b, s) - local dot = a:dot(b) - - if dot < 0 then - a = -a - dot = -dot - end - - if dot > DOT_THRESHOLD then - return a:lerp(b, s) - end - - dot = min(max(dot, -1), 1) - - local theta = acos(dot) * s - local c = (b - a * dot):normalize() - return a * cos(theta) + c * sin(theta) -end - ---- Unpack a quaternion into individual components. --- @tparam quat a Quaternion to unpack --- @treturn number x --- @treturn number y --- @treturn number z --- @treturn number w -function quat.unpack(a) - return a.x, a.y, a.z, a.w -end - ---- Return a boolean showing if a table is or is not a quat. --- @tparam quat a Quaternion to be tested --- @treturn boolean is_quat -function quat.is_quat(a) - if type(a) == "cdata" then - return ffi.istype("cpml_quat", a) - end - - return - type(a) == "table" and - type(a.x) == "number" and - type(a.y) == "number" and - type(a.z) == "number" and - type(a.w) == "number" -end - ---- Return a boolean showing if a table is or is not a zero quat. --- @tparam quat a Quaternion to be tested --- @treturn boolean is_zero -function quat.is_zero(a) - return - a.x == 0 and - a.y == 0 and - a.z == 0 and - a.w == 0 -end - ---- Return a boolean showing if a table is or is not a real quat. --- @tparam quat a Quaternion to be tested --- @treturn boolean is_real -function quat.is_real(a) - return - a.x == 0 and - a.y == 0 and - a.z == 0 -end - ---- Return a boolean showing if a table is or is not an imaginary quat. --- @tparam quat a Quaternion to be tested --- @treturn boolean is_imaginary -function quat.is_imaginary(a) - return a.w == 0 -end - ---- Convert a quaternion into an angle plus axis components. --- @tparam quat a Quaternion to convert --- @treturn number angle --- @treturn x axis-x --- @treturn y axis-y --- @treturn z axis-z -function quat.to_angle_axis_unpack(a) - if a.w > 1 or a.w < -1 then - a = a:normalize() - end - - local x, y, z - local angle = 2 * acos(a.w) - local s = sqrt(1 - a.w * a.w) - - if s < DBL_EPSILON then - x = a.x - y = a.y - z = a.z - else - x = a.x / s - y = a.y / s - z = a.z / s - end - - return angle, x, y, z -end - ---- Convert a quaternion into an angle/axis pair. --- @tparam quat a Quaternion to convert --- @treturn number angle --- @treturn vec3 axis -function quat.to_angle_axis(a) - local angle, x, y, z = a:to_angle_axis_unpack() - return angle, vec3(x, y, z) -end - ---- Convert a quaternion into a vec3. --- @tparam quat a Quaternion to convert --- @treturn vec3 out -function quat.to_vec3(a) - return vec3(a.x, a.y, a.z) -end - ---- Return a formatted string. --- @tparam quat a Quaternion to be turned into a string --- @treturn string formatted -function quat.to_string(a) - return string.format("(%+0.3f,%+0.3f,%+0.3f,%+0.3f)", a.x, a.y, a.z, a.w) -end - -quat_mt.__index = quat -quat_mt.__tostring = quat.to_string - -function quat_mt.__call(_, x, y, z, w) - return quat.new(x, y, z, w) -end - -function quat_mt.__unm(a) - return a:scale(-1) -end - -function quat_mt.__eq(a,b) - if not quat.is_quat(a) or not quat.is_quat(b) then - return false - end - return a.x == b.x and a.y == b.y and a.z == b.z and a.w == b.w -end - -function quat_mt.__add(a, b) - assert(quat.is_quat(a), "__add: Wrong argument type for left hand operand. ( expected)") - assert(quat.is_quat(b), "__add: Wrong argument type for right hand operand. ( expected)") - return a:add(b) -end - -function quat_mt.__sub(a, b) - assert(quat.is_quat(a), "__sub: Wrong argument type for left hand operand. ( expected)") - assert(quat.is_quat(b), "__sub: Wrong argument type for right hand operand. ( expected)") - return a:sub(b) -end - -function quat_mt.__mul(a, b) - assert(quat.is_quat(a), "__mul: Wrong argument type for left hand operand. ( expected)") - assert(quat.is_quat(b) or vec3.is_vec3(b) or type(b) == "number", "__mul: Wrong argument type for right hand operand. ( or or expected)") - - if quat.is_quat(b) then - return a:mul(b) - end - - if type(b) == "number" then - return a:scale(b) - end - - return a:mul_vec3(b) -end - -function quat_mt.__pow(a, n) - assert(quat.is_quat(a), "__pow: Wrong argument type for left hand operand. ( expected)") - assert(type(n) == "number", "__pow: Wrong argument type for right hand operand. ( expected)") - return a:pow(n) -end - -if status then - xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded - ffi.metatype(new, quat_mt) - end, function() end) -end - -return setmetatable({}, quat_mt) diff --git a/love2d/lua/ecs/plugins/cmpl/modules/simplex.lua b/love2d/lua/ecs/plugins/cmpl/modules/simplex.lua deleted file mode 100644 index 74df4d98..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/simplex.lua +++ /dev/null @@ -1,349 +0,0 @@ ---- Simplex Noise --- @module simplex - --- --- Based on code in "Simplex noise demystified", by Stefan Gustavson --- www.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf --- --- Thanks to Mike Pall for some cleanup and improvements (and for LuaJIT!) --- --- 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. --- --- [ MIT license: http://www.opensource.org/licenses/mit-license.php ] --- - -if _G.love and _G.love.math then - return love.math.noise -end - --- Bail out with dummy module if FFI is missing. -local has_ffi, ffi = pcall(require, "ffi") -if not has_ffi then - return function() - return 0 - end -end - --- Modules -- -local bit = require("bit") - --- Imports -- -local band = bit.band -local bor = bit.bor -local floor = math.floor -local lshift = bit.lshift -local max = math.max -local rshift = bit.rshift - --- Permutation of 0-255, replicated to allow easy indexing with sums of two bytes -- -local Perms = ffi.new("uint8_t[512]", { - 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, - 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, - 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, - 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, - 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, - 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, - 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, - 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, - 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, - 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, - 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, - 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, - 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, - 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, - 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, - 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 -}) - --- The above, mod 12 for each element -- -local Perms12 = ffi.new("uint8_t[512]") - -for i = 0, 255 do - local x = Perms[i] % 12 - - Perms[i + 256], Perms12[i], Perms12[i + 256] = Perms[i], x, x -end - --- Gradients for 2D, 3D case -- -local Grads3 = ffi.new("const double[12][3]", - { 1, 1, 0 }, { -1, 1, 0 }, { 1, -1, 0 }, { -1, -1, 0 }, - { 1, 0, 1 }, { -1, 0, 1 }, { 1, 0, -1 }, { -1, 0, -1 }, - { 0, 1, 1 }, { 0, -1, 1 }, { 0, 1, -1 }, { 0, -1, -1 } -) - --- 2D weight contribution -local function GetN2(bx, by, x, y) - local t = .5 - x * x - y * y - local index = Perms12[bx + Perms[by]] - - return max(0, (t * t) * (t * t)) * (Grads3[index][0] * x + Grads3[index][1] * y) -end - -local function simplex_2d(x, y) - --[[ - 2D skew factors: - F = (math.sqrt(3) - 1) / 2 - G = (3 - math.sqrt(3)) / 6 - G2 = 2 * G - 1 - ]] - - -- Skew the input space to determine which simplex cell we are in. - local s = (x + y) * 0.366025403 -- F - local ix, iy = floor(x + s), floor(y + s) - - -- Unskew the cell origin back to (x, y) space. - local t = (ix + iy) * 0.211324865 -- G - local x0 = x + t - ix - local y0 = y + t - iy - - -- Calculate the contribution from the two fixed corners. - -- A step of (1,0) in (i,j) means a step of (1-G,-G) in (x,y), and - -- A step of (0,1) in (i,j) means a step of (-G,1-G) in (x,y). - ix, iy = band(ix, 255), band(iy, 255) - - local n0 = GetN2(ix, iy, x0, y0) - local n2 = GetN2(ix + 1, iy + 1, x0 - 0.577350270, y0 - 0.577350270) -- G2 - - --[[ - Determine other corner based on simplex (equilateral triangle) we are in: - if x0 > y0 then - ix, x1 = ix + 1, x1 - 1 - else - iy, y1 = iy + 1, y1 - 1 - end - ]] - local xi = rshift(floor(y0 - x0), 31) -- y0 < x0 - local n1 = GetN2(ix + xi, iy + (1 - xi), x0 + 0.211324865 - xi, y0 - 0.788675135 + xi) -- x0 + G - xi, y0 + G - (1 - xi) - - -- Add contributions from each corner to get the final noise value. - -- The result is scaled to return values in the interval [-1,1]. - return 70.1480580019 * (n0 + n1 + n2) -end - --- 3D weight contribution -local function GetN3(ix, iy, iz, x, y, z) - local t = .6 - x * x - y * y - z * z - local index = Perms12[ix + Perms[iy + Perms[iz]]] - - return max(0, (t * t) * (t * t)) * (Grads3[index][0] * x + Grads3[index][1] * y + Grads3[index][2] * z) -end - -local function simplex_3d(x, y, z) - --[[ - 3D skew factors: - F = 1 / 3 - G = 1 / 6 - G2 = 2 * G - G3 = 3 * G - 1 - ]] - - -- Skew the input space to determine which simplex cell we are in. - local s = (x + y + z) * 0.333333333 -- F - local ix, iy, iz = floor(x + s), floor(y + s), floor(z + s) - - -- Unskew the cell origin back to (x, y, z) space. - local t = (ix + iy + iz) * 0.166666667 -- G - local x0 = x + t - ix - local y0 = y + t - iy - local z0 = z + t - iz - - -- Calculate the contribution from the two fixed corners. - -- A step of (1,0,0) in (i,j,k) means a step of (1-G,-G,-G) in (x,y,z); - -- a step of (0,1,0) in (i,j,k) means a step of (-G,1-G,-G) in (x,y,z); - -- a step of (0,0,1) in (i,j,k) means a step of (-G,-G,1-G) in (x,y,z). - ix, iy, iz = band(ix, 255), band(iy, 255), band(iz, 255) - - local n0 = GetN3(ix, iy, iz, x0, y0, z0) - local n3 = GetN3(ix + 1, iy + 1, iz + 1, x0 - 0.5, y0 - 0.5, z0 - 0.5) -- G3 - - --[[ - Determine other corners based on simplex (skewed tetrahedron) we are in: - - if x0 >= y0 then -- ~A - if y0 >= z0 then -- ~A and ~B - i1, j1, k1, i2, j2, k2 = 1, 0, 0, 1, 1, 0 - elseif x0 >= z0 then -- ~A and B and ~C - i1, j1, k1, i2, j2, k2 = 1, 0, 0, 1, 0, 1 - else -- ~A and B and C - i1, j1, k1, i2, j2, k2 = 0, 0, 1, 1, 0, 1 - end - else -- A - if y0 < z0 then -- A and B - i1, j1, k1, i2, j2, k2 = 0, 0, 1, 0, 1, 1 - elseif x0 < z0 then -- A and ~B and C - i1, j1, k1, i2, j2, k2 = 0, 1, 0, 0, 1, 1 - else -- A and ~B and ~C - i1, j1, k1, i2, j2, k2 = 0, 1, 0, 1, 1, 0 - end - end - ]] - - local xLy = rshift(floor(x0 - y0), 31) -- x0 < y0 - local yLz = rshift(floor(y0 - z0), 31) -- y0 < z0 - local xLz = rshift(floor(x0 - z0), 31) -- x0 < z0 - - local i1 = band(1 - xLy, bor(1 - yLz, 1 - xLz)) -- x0 >= y0 and (y0 >= z0 or x0 >= z0) - local j1 = band(xLy, 1 - yLz) -- x0 < y0 and y0 >= z0 - local k1 = band(yLz, bor(xLy, xLz)) -- y0 < z0 and (x0 < y0 or x0 < z0) - - local i2 = bor(1 - xLy, band(1 - yLz, 1 - xLz)) -- x0 >= y0 or (y0 >= z0 and x0 >= z0) - local j2 = bor(xLy, 1 - yLz) -- x0 < y0 or y0 >= z0 - local k2 = bor(band(1 - xLy, yLz), band(xLy, bor(yLz, xLz))) -- (x0 >= y0 and y0 < z0) or (x0 < y0 and (y0 < z0 or x0 < z0)) - - local n1 = GetN3(ix + i1, iy + j1, iz + k1, x0 + 0.166666667 - i1, y0 + 0.166666667 - j1, z0 + 0.166666667 - k1) -- G - local n2 = GetN3(ix + i2, iy + j2, iz + k2, x0 + 0.333333333 - i2, y0 + 0.333333333 - j2, z0 + 0.333333333 - k2) -- G2 - - -- Add contributions from each corner to get the final noise value. - -- The result is scaled to stay just inside [-1,1] - return 28.452842 * (n0 + n1 + n2 + n3) -end - --- Gradients for 4D case -- -local Grads4 = ffi.new("const double[32][4]", - { 0, 1, 1, 1 }, { 0, 1, 1, -1 }, { 0, 1, -1, 1 }, { 0, 1, -1, -1 }, - { 0, -1, 1, 1 }, { 0, -1, 1, -1 }, { 0, -1, -1, 1 }, { 0, -1, -1, -1 }, - { 1, 0, 1, 1 }, { 1, 0, 1, -1 }, { 1, 0, -1, 1 }, { 1, 0, -1, -1 }, - { -1, 0, 1, 1 }, { -1, 0, 1, -1 }, { -1, 0, -1, 1 }, { -1, 0, -1, -1 }, - { 1, 1, 0, 1 }, { 1, 1, 0, -1 }, { 1, -1, 0, 1 }, { 1, -1, 0, -1 }, - { -1, 1, 0, 1 }, { -1, 1, 0, -1 }, { -1, -1, 0, 1 }, { -1, -1, 0, -1 }, - { 1, 1, 1, 0 }, { 1, 1, -1, 0 }, { 1, -1, 1, 0 }, { 1, -1, -1, 0 }, - { -1, 1, 1, 0 }, { -1, 1, -1, 0 }, { -1, -1, 1, 0 }, { -1, -1, -1, 0 } -) - --- 4D weight contribution -local function GetN4(ix, iy, iz, iw, x, y, z, w) - local t = .6 - x * x - y * y - z * z - w * w - local index = band(Perms[ix + Perms[iy + Perms[iz + Perms[iw]]]], 0x1F) - - return max(0, (t * t) * (t * t)) * (Grads4[index][0] * x + Grads4[index][1] * y + Grads4[index][2] * z + Grads4[index][3] * w) -end - --- A lookup table to traverse the simplex around a given point in 4D. --- Details can be found where this table is used, in the 4D noise method. -local Simplex = ffi.new("uint8_t[64][4]", - { 0, 1, 2, 3 }, { 0, 1, 3, 2 }, {}, { 0, 2, 3, 1 }, {}, {}, {}, { 1, 2, 3 }, - { 0, 2, 1, 3 }, {}, { 0, 3, 1, 2 }, { 0, 3, 2, 1 }, {}, {}, {}, { 1, 3, 2 }, - {}, {}, {}, {}, {}, {}, {}, {}, - { 1, 2, 0, 3 }, {}, { 1, 3, 0, 2 }, {}, {}, {}, { 2, 3, 0, 1 }, { 2, 3, 1 }, - { 1, 0, 2, 3 }, { 1, 0, 3, 2 }, {}, {}, {}, { 2, 0, 3, 1 }, {}, { 2, 1, 3 }, - {}, {}, {}, {}, {}, {}, {}, {}, - { 2, 0, 1, 3 }, {}, {}, {}, { 3, 0, 1, 2 }, { 3, 0, 2, 1 }, {}, { 3, 1, 2 }, - { 2, 1, 0, 3 }, {}, {}, {}, { 3, 1, 0, 2 }, {}, { 3, 2, 0, 1 }, { 3, 2, 1 } -) - --- Convert the above indices to masks that can be shifted / anded into offsets -- -for i = 0, 63 do - Simplex[i][0] = lshift(1, Simplex[i][0]) - 1 - Simplex[i][1] = lshift(1, Simplex[i][1]) - 1 - Simplex[i][2] = lshift(1, Simplex[i][2]) - 1 - Simplex[i][3] = lshift(1, Simplex[i][3]) - 1 -end - -local function simplex_4d(x, y, z, w) - --[[ - 4D skew factors: - F = (math.sqrt(5) - 1) / 4 - G = (5 - math.sqrt(5)) / 20 - G2 = 2 * G - G3 = 3 * G - G4 = 4 * G - 1 - ]] - - -- Skew the input space to determine which simplex cell we are in. - local s = (x + y + z + w) * 0.309016994 -- F - local ix, iy, iz, iw = floor(x + s), floor(y + s), floor(z + s), floor(w + s) - - -- Unskew the cell origin back to (x, y, z) space. - local t = (ix + iy + iz + iw) * 0.138196601 -- G - local x0 = x + t - ix - local y0 = y + t - iy - local z0 = z + t - iz - local w0 = w + t - iw - - -- For the 4D case, the simplex is a 4D shape I won't even try to describe. - -- To find out which of the 24 possible simplices we're in, we need to - -- determine the magnitude ordering of x0, y0, z0 and w0. - -- The method below is a good way of finding the ordering of x,y,z,w and - -- then find the correct traversal order for the simplex we�re in. - -- First, six pair-wise comparisons are performed between each possible pair - -- of the four coordinates, and the results are used to add up binary bits - -- for an integer index. - local c1 = band(rshift(floor(y0 - x0), 26), 32) - local c2 = band(rshift(floor(z0 - x0), 27), 16) - local c3 = band(rshift(floor(z0 - y0), 28), 8) - local c4 = band(rshift(floor(w0 - x0), 29), 4) - local c5 = band(rshift(floor(w0 - y0), 30), 2) - local c6 = rshift(floor(w0 - z0), 31) - - -- Simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order. - -- Many values of c will never occur, since e.g. x>y>z>w makes x= size and value or 0 -end - ---- Check if value is equal or greater than threshold. --- @param value --- @param threshold --- @return boolean -function utils.threshold(value, threshold) - -- I know, it barely saves any typing at all. - return abs(value) >= threshold -end - ---- Check if value is equal or less than threshold. --- @param value --- @param threshold --- @return boolean -function utils.tolerance(value, threshold) - -- I know, it barely saves any typing at all. - return abs(value) <= threshold -end - ---- Scales a value from one range to another. --- @param value Input value --- @param min_in Minimum input value --- @param max_in Maximum input value --- @param min_out Minimum output value --- @param max_out Maximum output value --- @return number -function utils.map(value, min_in, max_in, min_out, max_out) - return ((value) - (min_in)) * ((max_out) - (min_out)) / ((max_in) - (min_in)) + (min_out) -end - ---- Linear interpolation. --- Performs linear interpolation between 0 and 1 when `low` < `progress` < `high`. --- @param low value to return when `progress` is 0 --- @param high value to return when `progress` is 1 --- @param progress (0-1) --- @return number -function utils.lerp(low, high, progress) - return low * (1 - progress) + high * progress -end - ---- Exponential decay --- @param low initial value --- @param high target value --- @param rate portion of the original value remaining per second --- @param dt time delta --- @return number -function utils.decay(low, high, rate, dt) - return utils.lerp(low, high, 1.0 - math.exp(-rate * dt)) -end - ---- Hermite interpolation. --- Performs smooth Hermite interpolation between 0 and 1 when `low` < `progress` < `high`. --- @param progress (0-1) --- @param low value to return when `progress` is 0 --- @param high value to return when `progress` is 1 --- @return number -function utils.smoothstep(progress, low, high) - local t = utils.clamp((progress - low) / (high - low), 0.0, 1.0) - return t * t * (3.0 - 2.0 * t) -end - ---- Round number at a given precision. --- Truncates `value` at `precision` points after the decimal (whole number if --- left unspecified). --- @param value --- @param precision --- @return number -function utils.round(value, precision) - if precision then return utils.round(value / precision) * precision end - return value >= 0 and floor(value+0.5) or ceil(value-0.5) -end - ---- Wrap `value` around if it exceeds `limit`. --- @param value --- @param limit --- @return number -function utils.wrap(value, limit) - if value < 0 then - value = value + utils.round(((-value/limit)+1))*limit - end - return value % limit -end - ---- Check if a value is a power-of-two. --- Returns true if a number is a valid power-of-two, otherwise false. --- @author undef --- @param value --- @return boolean -function utils.is_pot(value) - -- found here: https://love2d.org/forums/viewtopic.php?p=182219#p182219 - -- check if a number is a power-of-two - return (frexp(value)) == 0.5 -end - --- Originally from vec3 -function utils.project_on(a, b) - local s = - (a.x * b.x + a.y * b.y + a.z or 0 * b.z or 0) / - (b.x * b.x + b.y * b.y + b.z or 0 * b.z or 0) - - if a.z and b.z then - return vec3( - b.x * s, - b.y * s, - b.z * s - ) - end - - return vec2( - b.x * s, - b.y * s - ) -end - --- Originally from vec3 -function utils.project_from(a, b) - local s = - (b.x * b.x + b.y * b.y + b.z or 0 * b.z or 0) / - (a.x * b.x + a.y * b.y + a.z or 0 * b.z or 0) - - if a.z and b.z then - return vec3( - b.x * s, - b.y * s, - b.z * s - ) - end - - return vec2( - b.x * s, - b.y * s - ) -end - --- Originally from vec3 -function utils.mirror_on(a, b) - local s = - (a.x * b.x + a.y * b.y + a.z or 0 * b.z or 0) / - (b.x * b.x + b.y * b.y + b.z or 0 * b.z or 0) * 2 - - if a.z and b.z then - return vec3( - b.x * s - a.x, - b.y * s - a.y, - b.z * s - a.z - ) - end - - return vec2( - b.x * s - a.x, - b.y * s - a.y - ) -end - --- Originally from vec3 -function utils.reflect(i, n) - return i - (n * (2 * n:dot(i))) -end - --- Originally from vec3 -function utils.refract(i, n, ior) - local d = n:dot(i) - local k = 1 - ior * ior * (1 - d * d) - - if k >= 0 then - return (i * ior) - (n * (ior * d + k ^ 0.5)) - end - - return vec3() -end - -return utils diff --git a/love2d/lua/ecs/plugins/cmpl/modules/vec2.lua b/love2d/lua/ecs/plugins/cmpl/modules/vec2.lua deleted file mode 100644 index dda52e08..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/vec2.lua +++ /dev/null @@ -1,389 +0,0 @@ ---- A 2 component vector. --- @module vec2 - -local modules = (...):gsub('%.[^%.]+$', '') .. "." -local vec3 = require(modules .. "vec3") -local acos = math.acos -local atan2 = math.atan2 -local sqrt = math.sqrt -local cos = math.cos -local sin = math.sin -local vec2 = {} -local vec2_mt = {} - --- Private constructor. -local function new(x, y) - return setmetatable({ - x = x or 0, - y = y or 0 - }, vec2_mt) -end - --- Do the check to see if JIT is enabled. If so use the optimized FFI structs. -local status, ffi -if type(jit) == "table" and jit.status() then - status, ffi = pcall(require, "ffi") - if status then - ffi.cdef "typedef struct { double x, y;} cpml_vec2;" - new = ffi.typeof("cpml_vec2") - end -end - ---- Constants --- @table vec2 --- @field unit_x X axis of rotation --- @field unit_y Y axis of rotation --- @field zero Empty vector -vec2.unit_x = new(1, 0) -vec2.unit_y = new(0, 1) -vec2.zero = new(0, 0) - ---- The public constructor. --- @param x Can be of three types:
--- number X component --- table {x, y} or {x = x, y = y} --- scalar to fill the vector eg. {x, x} --- @tparam number y Y component --- @treturn vec2 out -function vec2.new(x, y) - -- number, number - if x and y then - assert(type(x) == "number", "new: Wrong argument type for x ( expected)") - assert(type(y) == "number", "new: Wrong argument type for y ( expected)") - - return new(x, y) - - -- {x, y} or {x=x, y=y} - elseif type(x) == "table" or type(x) == "cdata" then -- table in vanilla lua, cdata in luajit - local xx, yy = x.x or x[1], x.y or x[2] - assert(type(xx) == "number", "new: Wrong argument type for x ( expected)") - assert(type(yy) == "number", "new: Wrong argument type for y ( expected)") - - return new(xx, yy) - - -- number - elseif type(x) == "number" then - return new(x, x) - else - return new() - end -end - ---- Convert point from polar to cartesian. --- @tparam number radius Radius of the point --- @tparam number theta Angle of the point (in radians) --- @treturn vec2 out -function vec2.from_cartesian(radius, theta) - return new(radius * cos(theta), radius * sin(theta)) -end - ---- Clone a vector. --- @tparam vec2 a Vector to be cloned --- @treturn vec2 out -function vec2.clone(a) - return new(a.x, a.y) -end - ---- Add two vectors. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @treturn vec2 out -function vec2.add(a, b) - return new( - a.x + b.x, - a.y + b.y - ) -end - ---- Subtract one vector from another. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @treturn vec2 out -function vec2.sub(a, b) - return new( - a.x - b.x, - a.y - b.y - ) -end - ---- Multiply a vector by another vector. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @treturn vec2 out -function vec2.mul(a, b) - return new( - a.x * b.x, - a.y * b.y - ) -end - ---- Divide a vector by another vector. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @treturn vec2 out -function vec2.div(a, b) - return new( - a.x / b.x, - a.y / b.y - ) -end - ---- Get the normal of a vector. --- @tparam vec2 a Vector to normalize --- @treturn vec2 out -function vec2.normalize(a) - if a:is_zero() then - return new() - end - return a:scale(1 / a:len()) -end - ---- Trim a vector to a given length. --- @tparam vec2 a Vector to be trimmed --- @tparam number len Length to trim the vector to --- @treturn vec2 out -function vec2.trim(a, len) - return a:normalize():scale(math.min(a:len(), len)) -end - ---- Get the cross product of two vectors. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @treturn number magnitude -function vec2.cross(a, b) - return a.x * b.y - a.y * b.x -end - ---- Get the dot product of two vectors. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @treturn number dot -function vec2.dot(a, b) - return a.x * b.x + a.y * b.y -end - ---- Get the length of a vector. --- @tparam vec2 a Vector to get the length of --- @treturn number len -function vec2.len(a) - return sqrt(a.x * a.x + a.y * a.y) -end - ---- Get the squared length of a vector. --- @tparam vec2 a Vector to get the squared length of --- @treturn number len -function vec2.len2(a) - return a.x * a.x + a.y * a.y -end - ---- Get the distance between two vectors. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @treturn number dist -function vec2.dist(a, b) - local dx = a.x - b.x - local dy = a.y - b.y - return sqrt(dx * dx + dy * dy) -end - ---- Get the squared distance between two vectors. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @treturn number dist -function vec2.dist2(a, b) - local dx = a.x - b.x - local dy = a.y - b.y - return dx * dx + dy * dy -end - ---- Scale a vector by a scalar. --- @tparam vec2 a Left hand operand --- @tparam number b Right hand operand --- @treturn vec2 out -function vec2.scale(a, b) - return new( - a.x * b, - a.y * b - ) -end - ---- Rotate a vector. --- @tparam vec2 a Vector to rotate --- @tparam number phi Angle to rotate vector by (in radians) --- @treturn vec2 out -function vec2.rotate(a, phi) - local c = cos(phi) - local s = sin(phi) - return new( - c * a.x - s * a.y, - s * a.x + c * a.y - ) -end - ---- Get the perpendicular vector of a vector. --- @tparam vec2 a Vector to get perpendicular axes from --- @treturn vec2 out -function vec2.perpendicular(a) - return new(-a.y, a.x) -end - ---- Angle from one vector to another. --- @tparam vec2 a Vector --- @tparam vec2 b Vector --- @treturn number angle -function vec2.angle_to(a, b) - if b then - return atan2(a.y - b.y, a.x - b.x) - end - - return atan2(a.y, a.x) -end - ---- Angle between two vectors. --- @tparam vec2 a Vector --- @tparam vec2 b Vector --- @treturn number angle -function vec2.angle_between(a, b) - if b then - if vec2.is_vec2(a) then - return acos(a:dot(b) / (a:len() * b:len())) - end - - return acos(vec3.dot(a, b) / (vec3.len(a) * vec3.len(b))) - end - - return 0 -end - ---- Lerp between two vectors. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @tparam number s Step value --- @treturn vec2 out -function vec2.lerp(a, b, s) - return a + (b - a) * s -end - ---- Unpack a vector into individual components. --- @tparam vec2 a Vector to unpack --- @treturn number x --- @treturn number y -function vec2.unpack(a) - return a.x, a.y -end - ---- Return the component-wise minimum of two vectors. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @treturn vec2 A vector where each component is the lesser value for that component between the two given vectors. -function vec2.component_min(a, b) - return new(math.min(a.x, b.x), math.min(a.y, b.y)) -end - ---- Return the component-wise maximum of two vectors. --- @tparam vec2 a Left hand operand --- @tparam vec2 b Right hand operand --- @treturn vec2 A vector where each component is the lesser value for that component between the two given vectors. -function vec2.component_max(a, b) - return new(math.max(a.x, b.x), math.max(a.y, b.y)) -end - - ---- Return a boolean showing if a table is or is not a vec2. --- @tparam vec2 a Vector to be tested --- @treturn boolean is_vec2 -function vec2.is_vec2(a) - if type(a) == "cdata" then - return ffi.istype("cpml_vec2", a) - end - - return - type(a) == "table" and - type(a.x) == "number" and - type(a.y) == "number" -end - ---- Return a boolean showing if a table is or is not a zero vec2. --- @tparam vec2 a Vector to be tested --- @treturn boolean is_zero -function vec2.is_zero(a) - return a.x == 0 and a.y == 0 -end - ---- Convert point from cartesian to polar. --- @tparam vec2 a Vector to convert --- @treturn number radius --- @treturn number theta -function vec2.to_polar(a) - local radius = sqrt(a.x^2 + a.y^2) - local theta = atan2(a.y, a.x) - theta = theta > 0 and theta or theta + 2 * math.pi - return radius, theta -end - ---- Return a formatted string. --- @tparam vec2 a Vector to be turned into a string --- @treturn string formatted -function vec2.to_string(a) - return string.format("(%+0.3f,%+0.3f)", a.x, a.y) -end - -vec2_mt.__index = vec2 -vec2_mt.__tostring = vec2.to_string - -function vec2_mt.__call(_, x, y) - return vec2.new(x, y) -end - -function vec2_mt.__unm(a) - return new(-a.x, -a.y) -end - -function vec2_mt.__eq(a, b) - if not vec2.is_vec2(a) or not vec2.is_vec2(b) then - return false - end - return a.x == b.x and a.y == b.y -end - -function vec2_mt.__add(a, b) - assert(vec2.is_vec2(a), "__add: Wrong argument type for left hand operand. ( expected)") - assert(vec2.is_vec2(b), "__add: Wrong argument type for right hand operand. ( expected)") - return a:add(b) -end - -function vec2_mt.__sub(a, b) - assert(vec2.is_vec2(a), "__add: Wrong argument type for left hand operand. ( expected)") - assert(vec2.is_vec2(b), "__add: Wrong argument type for right hand operand. ( expected)") - return a:sub(b) -end - -function vec2_mt.__mul(a, b) - assert(vec2.is_vec2(a), "__mul: Wrong argument type for left hand operand. ( expected)") - assert(vec2.is_vec2(b) or type(b) == "number", "__mul: Wrong argument type for right hand operand. ( or expected)") - - if vec2.is_vec2(b) then - return a:mul(b) - end - - return a:scale(b) -end - -function vec2_mt.__div(a, b) - assert(vec2.is_vec2(a), "__div: Wrong argument type for left hand operand. ( expected)") - assert(vec2.is_vec2(b) or type(b) == "number", "__div: Wrong argument type for right hand operand. ( or expected)") - - if vec2.is_vec2(b) then - return a:div(b) - end - - return a:scale(1 / b) -end - -if status then - xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded - ffi.metatype(new, vec2_mt) - end, function() end) -end - -return setmetatable({}, vec2_mt) diff --git a/love2d/lua/ecs/plugins/cmpl/modules/vec3.lua b/love2d/lua/ecs/plugins/cmpl/modules/vec3.lua deleted file mode 100644 index 05ec586a..00000000 --- a/love2d/lua/ecs/plugins/cmpl/modules/vec3.lua +++ /dev/null @@ -1,369 +0,0 @@ ---- A 3 component vector. --- @module vec3 - -local sqrt = math.sqrt -local cos = math.cos -local sin = math.sin -local vec3 = {} -local vec3_mt = {} - --- Private constructor. -local function new(x, y, z) - return setmetatable({ - x = x or 0, - y = y or 0, - z = z or 0 - }, vec3_mt) -end - --- Do the check to see if JIT is enabled. If so use the optimized FFI structs. -local status, ffi -if type(jit) == "table" and jit.status() then - status, ffi = pcall(require, "ffi") - if status then - ffi.cdef "typedef struct { double x, y, z;} cpml_vec3;" - new = ffi.typeof("cpml_vec3") - end -end - ---- Constants --- @table vec3 --- @field unit_x X axis of rotation --- @field unit_y Y axis of rotation --- @field unit_z Z axis of rotation --- @field zero Empty vector -vec3.unit_x = new(1, 0, 0) -vec3.unit_y = new(0, 1, 0) -vec3.unit_z = new(0, 0, 1) -vec3.zero = new(0, 0, 0) - ---- The public constructor. --- @param x Can be of three types:
--- number X component --- table {x, y, z} or {x=x, y=y, z=z} --- scalar To fill the vector eg. {x, x, x} --- @tparam number y Y component --- @tparam number z Z component --- @treturn vec3 out -function vec3.new(x, y, z) - -- number, number, number - if x and y and z then - assert(type(x) == "number", "new: Wrong argument type for x ( expected)") - assert(type(y) == "number", "new: Wrong argument type for y ( expected)") - assert(type(z) == "number", "new: Wrong argument type for z ( expected)") - - return new(x, y, z) - - -- {x, y, z} or {x=x, y=y, z=z} - elseif type(x) == "table" or type(x) == "cdata" then -- table in vanilla lua, cdata in luajit - local xx, yy, zz = x.x or x[1], x.y or x[2], x.z or x[3] - assert(type(xx) == "number", "new: Wrong argument type for x ( expected)") - assert(type(yy) == "number", "new: Wrong argument type for y ( expected)") - assert(type(zz) == "number", "new: Wrong argument type for z ( expected)") - - return new(xx, yy, zz) - - -- number - elseif type(x) == "number" then - return new(x, x, x) - else - return new() - end -end - ---- Clone a vector. --- @tparam vec3 a Vector to be cloned --- @treturn vec3 out -function vec3.clone(a) - return new(a.x, a.y, a.z) -end - ---- Add two vectors. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn vec3 out -function vec3.add(a, b) - return new( - a.x + b.x, - a.y + b.y, - a.z + b.z - ) -end - ---- Subtract one vector from another. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn vec3 out -function vec3.sub(a, b) - return new( - a.x - b.x, - a.y - b.y, - a.z - b.z - ) -end - ---- Multiply a vector by another vectorr. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn vec3 out -function vec3.mul(a, b) - return new( - a.x * b.x, - a.y * b.y, - a.z * b.z - ) -end - ---- Divide a vector by a scalar. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn vec3 out -function vec3.div(a, b) - return new( - a.x / b.x, - a.y / b.y, - a.z / b.z - ) -end - ---- Get the normal of a vector. --- @tparam vec3 a Vector to normalize --- @treturn vec3 out -function vec3.normalize(a) - if a:is_zero() then - return new() - end - return a:scale(1 / a:len()) -end - ---- Trim a vector to a given length --- @tparam vec3 a Vector to be trimmed --- @tparam number len Length to trim the vector to --- @treturn vec3 out -function vec3.trim(a, len) - return a:normalize():scale(math.min(a:len(), len)) -end - ---- Get the cross product of two vectors. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn vec3 out -function vec3.cross(a, b) - return new( - a.y * b.z - a.z * b.y, - a.z * b.x - a.x * b.z, - a.x * b.y - a.y * b.x - ) -end - ---- Get the dot product of two vectors. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn number dot -function vec3.dot(a, b) - return a.x * b.x + a.y * b.y + a.z * b.z -end - ---- Get the length of a vector. --- @tparam vec3 a Vector to get the length of --- @treturn number len -function vec3.len(a) - return sqrt(a.x * a.x + a.y * a.y + a.z * a.z) -end - ---- Get the squared length of a vector. --- @tparam vec3 a Vector to get the squared length of --- @treturn number len -function vec3.len2(a) - return a.x * a.x + a.y * a.y + a.z * a.z -end - ---- Get the distance between two vectors. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn number dist -function vec3.dist(a, b) - local dx = a.x - b.x - local dy = a.y - b.y - local dz = a.z - b.z - return sqrt(dx * dx + dy * dy + dz * dz) -end - ---- Get the squared distance between two vectors. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn number dist -function vec3.dist2(a, b) - local dx = a.x - b.x - local dy = a.y - b.y - local dz = a.z - b.z - return dx * dx + dy * dy + dz * dz -end - ---- Scale a vector by a scalar. --- @tparam vec3 a Left hand operand --- @tparam number b Right hand operand --- @treturn vec3 out -function vec3.scale(a, b) - return new( - a.x * b, - a.y * b, - a.z * b - ) -end - ---- Rotate vector about an axis. --- @tparam vec3 a Vector to rotate --- @tparam number phi Angle to rotate vector by (in radians) --- @tparam vec3 axis Axis to rotate by --- @treturn vec3 out -function vec3.rotate(a, phi, axis) - if not vec3.is_vec3(axis) then - return a - end - - local u = axis:normalize() - local c = cos(phi) - local s = sin(phi) - - -- Calculate generalized rotation matrix - local m1 = new((c + u.x * u.x * (1 - c)), (u.x * u.y * (1 - c) - u.z * s), (u.x * u.z * (1 - c) + u.y * s)) - local m2 = new((u.y * u.x * (1 - c) + u.z * s), (c + u.y * u.y * (1 - c)), (u.y * u.z * (1 - c) - u.x * s)) - local m3 = new((u.z * u.x * (1 - c) - u.y * s), (u.z * u.y * (1 - c) + u.x * s), (c + u.z * u.z * (1 - c)) ) - - return new( - a:dot(m1), - a:dot(m2), - a:dot(m3) - ) -end - ---- Get the perpendicular vector of a vector. --- @tparam vec3 a Vector to get perpendicular axes from --- @treturn vec3 out -function vec3.perpendicular(a) - return new(-a.y, a.x, 0) -end - ---- Lerp between two vectors. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @tparam number s Step value --- @treturn vec3 out -function vec3.lerp(a, b, s) - return a + (b - a) * s -end - ---- Unpack a vector into individual components. --- @tparam vec3 a Vector to unpack --- @treturn number x --- @treturn number y --- @treturn number z -function vec3.unpack(a) - return a.x, a.y, a.z -end - ---- Return the component-wise minimum of two vectors. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn vec3 A vector where each component is the lesser value for that component between the two given vectors. -function vec3.component_min(a, b) - return new(math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z)) -end - ---- Return the component-wise maximum of two vectors. --- @tparam vec3 a Left hand operand --- @tparam vec3 b Right hand operand --- @treturn vec3 A vector where each component is the lesser value for that component between the two given vectors. -function vec3.component_max(a, b) - return new(math.max(a.x, b.x), math.max(a.y, b.y), math.max(a.z, b.z)) -end - ---- Return a boolean showing if a table is or is not a vec3. --- @tparam vec3 a Vector to be tested --- @treturn boolean is_vec3 -function vec3.is_vec3(a) - if type(a) == "cdata" then - return ffi.istype("cpml_vec3", a) - end - - return - type(a) == "table" and - type(a.x) == "number" and - type(a.y) == "number" and - type(a.z) == "number" -end - ---- Return a boolean showing if a table is or is not a zero vec3. --- @tparam vec3 a Vector to be tested --- @treturn boolean is_zero -function vec3.is_zero(a) - return a.x == 0 and a.y == 0 and a.z == 0 -end - ---- Return a formatted string. --- @tparam vec3 a Vector to be turned into a string --- @treturn string formatted -function vec3.to_string(a) - return string.format("(%+0.3f,%+0.3f,%+0.3f)", a.x, a.y, a.z) -end - -vec3_mt.__index = vec3 -vec3_mt.__tostring = vec3.to_string - -function vec3_mt.__call(_, x, y, z) - return vec3.new(x, y, z) -end - -function vec3_mt.__unm(a) - return new(-a.x, -a.y, -a.z) -end - -function vec3_mt.__eq(a, b) - if not vec3.is_vec3(a) or not vec3.is_vec3(b) then - return false - end - return a.x == b.x and a.y == b.y and a.z == b.z -end - -function vec3_mt.__add(a, b) - assert(vec3.is_vec3(a), "__add: Wrong argument type for left hand operand. ( expected)") - assert(vec3.is_vec3(b), "__add: Wrong argument type for right hand operand. ( expected)") - return a:add(b) -end - -function vec3_mt.__sub(a, b) - assert(vec3.is_vec3(a), "__sub: Wrong argument type for left hand operand. ( expected)") - assert(vec3.is_vec3(b), "__sub: Wrong argument type for right hand operand. ( expected)") - return a:sub(b) -end - -function vec3_mt.__mul(a, b) - assert(vec3.is_vec3(a), "__mul: Wrong argument type for left hand operand. ( expected)") - assert(vec3.is_vec3(b) or type(b) == "number", "__mul: Wrong argument type for right hand operand. ( or expected)") - - if vec3.is_vec3(b) then - return a:mul(b) - end - - return a:scale(b) -end - -function vec3_mt.__div(a, b) - assert(vec3.is_vec3(a), "__div: Wrong argument type for left hand operand. ( expected)") - assert(vec3.is_vec3(b) or type(b) == "number", "__div: Wrong argument type for right hand operand. ( or expected)") - - if vec3.is_vec3(b) then - return a:div(b) - end - - return a:scale(1 / b) -end - -if status then - xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded - ffi.metatype(new, vec3_mt) - end, function() end) -end - -return setmetatable({}, vec3_mt) diff --git a/love2d/lua/ecs/plugins/xhh-array/init.lua b/love2d/lua/ecs/plugins/xhh-array/init.lua deleted file mode 100644 index 3a4fa223..00000000 --- a/love2d/lua/ecs/plugins/xhh-array/init.lua +++ /dev/null @@ -1,193 +0,0 @@ -Array = nil -Set = nil -do - local arr_metamethods = { - __index = function(t, k) - if type(k) == "number" then - return t.table[k] - end - if k == 'length' then - return #t.table - end - return rawget(t,k) - end, - __newindex = function(t, k, v) - if type(k) == "number" then - t.table[k] = v - end - end, - __tostring = function(t) - local str = '' - for i,v in ipairs(t.table) do - str = str .. tostring(v) - if i ~= #t.table then - str = str .. "," - end - end - return str - end - } - - local arr_methods = { - table = {}, - push = function(self, ...) - for i,v in ipairs({...}) do - table.insert(self.table, v) - end - end, - pop = function(self) - local last = self.table[#self.table] - self.table[#self.table] = nil - return last - end, - remove = function(self, start, amt) - amt = amt or 1 - local off = 0 - local new_arr = {} - for i = 1,self.length do - if i >= start and i < start + amt then - off = off + 1 - else - new_arr[i-off] = self.table[i] - end - end - self.table = new_arr - end, - copy = function(self) - local ret = Array.from(copy(self.table)) - return ret - end, - includes = function(self, v) - for i = 1,self.length do if self.table[i] == v then return true end end - return false - end, - fill = function(self, v, s, e) - s = s or 1 - e = e or self.length for i = s, e do self.table[i] = v end - end, - indexOf = function(self, v) - for i = 1, self.length do if self.table[i] == v then return i end end - return 0 - end, - forEach = function(self, fn) - for i = 1, self.length do - if fn(self.table[i], i) == true then break end - end - end, - map = function(self, fn) - for i = 1, self.length do self.table[i] = fn(self.table[i], i) end - return self - end, - filter = function(self, fn) - local new_arr = {} - for i = 1, self.length do if fn(self.table[i], i) then table.insert(new_arr, self.table[i]) end end - self.table = new_arr - return self - end, - reverse = function(self) - local new_arr = {} - for i = self.length, 1, -1 do table.insert(new_arr, self.table[i]) end - self.table = new_arr - return self - end, - join = function(self, sep) - local str = '' - for i = 1, self.length do - str = str .. tostring(self.table[i]) - if i ~= self.length then - str = str .. tostring(sep) - end - end - return str - end, - concat = function(self, ...) - for i,v in ipairs({...}) do - if type(v) == 'table' then - for i,v2 in ipairs(v) do - self:push(v2) - end - else - self:push(v) - end - end - end, - some = function(self, fn) - for i = 1, self.length do if fn(self.table[i], i) == true then return true end end - return false - end, - every = function(self, fn) - for i = 1, self.length do if fn(self.table[i], i) == false then return false end end - return true - end, - sort = function(self, ...) table.sort(self.table, ...) end, - shuffle = function(self) - local tbl = {} - local t = self.table - for i = 1, #t do - tbl[i] = t[i] - end - for i = #tbl, 2, -1 do - local j = math.random(i) - tbl[i], tbl[j] = tbl[j], tbl[i] - end - self.table = tbl - end, - sort = function(self, fn) - table.sort(self.table, fn) - end, - random = function(self) - return table.random(self.table) - end - --reduce - } - - Array = setmetatable({ - from = function(t) - return Array(unpack(t)) - end - },{ - __call = function(t, ...) - local arr = setmetatable(copy(arr_methods), copy(arr_metamethods)) - for i,v in ipairs({...}) do - arr:push(v) - end - return arr - end - }) - - local set_methods = copy(arr_methods) - set_methods.push = function(self, ...) - for i,v in ipairs({...}) do - if not self:includes(v) then - table.insert(self.table, v) - end - end - end - set_methods.copy = function(self) - local ret = Set.from(copy(self.table)) - return ret - end - - local set_metamethods = copy(arr_metamethods) - set_metamethods.__index = function(t,k) - if k == 'push' or k == 'copy' then - return rawget(t,k) - else - return arr_metamethods.__index(t,k) - end - end - - Set = setmetatable({ - from = function(t) - return Set(unpack(t)) - end - },{ - __call = function(t, ...) - local set = setmetatable(copy(set_methods), copy(set_metamethods)) - for i,v in ipairs({...}) do - set:push(v) - end - return set - end - }) -end diff --git a/love2d/lua/ecs/plugins/xhh-badword/init.lua b/love2d/lua/ecs/plugins/xhh-badword/init.lua deleted file mode 100644 index 8dcad5a3..00000000 --- a/love2d/lua/ecs/plugins/xhh-badword/init.lua +++ /dev/null @@ -1,83 +0,0 @@ -local words = [[`kbk+c`k'kiopwlhh$khuxg'ablogr'idooatt)ccvms%getmji/`bqkgbi/bona+ankdj)bikhgk~nybii*gme}e}u.cgap`hfd/boie(il`lheomn+ii`gpb~'dnb`m%ioexq+hlwx*`yda$jxaiy+j{gidc/ktfjd(hvevxoc)fvre$fxzcmlhe%atjcj)`st*grxkzqih%eprgtrkhirg.crpg{vhkj~hki(gxukqnt'fwrfidnbp$kprgkhu|c{(jyyiikuk'aswii`np+hw{nv/aupotha$bp{avfcep+gyzb`s.hrzigla'dpsmklct/etral{m(d{{ofanl~-grrkoxv&ht{acpvlv/d{qmndtn$euqel`m'dwvhohods)htudevlx)dwrjeo,brpikobc(gs{gunkl+ctvnukeins+kuxtcfidu$fvtwcsim&gvzuvxqabr/krysiolns*exw~mftm,isyuk{e.gwlkermvefi|/jstk`k.fsz~wdhiii&fjba/faeh`{)dffbmhis,idkmcdoymhf/dadoqecw(giml~bn'hcdjo`coeq(`giit$eefkxjkh(ekggmo`,`kuqjzv*ehpcf|hbdae$abwo,hdvcaec)jbxcodhc/ggru,hax}hxm$+iaqked`r.hddknt%jcdgau(anfuu*`ggy|cfky)igjw~lag(kejtvh`j`w},gfgwecc)dncs$mog/keeq~evvnekp.ila~lq-gaspldn%jlv|ndkj}y/di+d`ctjo&k`ien)ikapy`j}u*akmkww$cidakprf{`/kked~~q&anmgcu-ei{lr|gn*ci&zgquci*gasac/aisfldq$kbskkdt-jhqjjdq-djr`mkl&ii}gjbhn*ka}hoslkr%hnreh(f`}ejm'coean/cokaifcg+cdkcafpt&`gajmv,ihll`,hkjw,edh|bni$ifde`)elocd)chhvm`)admemim(deidhjb.ckmf.ehlioup*`ljcioe+ikfgv$fknd%gkeohb`,hlenw.djdm.gmkf.jggkn`x/gldcz.`dgj|)gkdl/infj*cdeko.gdmeni(cnfiaf&ddfur,bojqzabkm)hmvl}yfe{-ktc-dr`c?q&aqbepp*`td`{lhj%exnjp|in}er)esfbr~ngd$jwjphag-cralcv$drfflumd%itccgy~/kwomdy`p,jpkmo`af&dqgjlql`/aqilxll~&fkdhlowja-hsko|gi+awjlg-c|mgijfl.atsogm%fpxf&brqanbbjlu$ksuboblkc$iqwhnd{`l/bq|,artwieoo+j~vs.bbng&`rt~ei`e$jpv}f|en+g|sp-cwca(a|uftfngr)ipur'lv`ce{(asq`tajfyq$bu}.l|`amuw-esra`fe'jrp~hhl*f|uuepobi)a|updwhchew$awv|sixe`'irpvtiwc-kp}uqhne+bxi|eb,e`ckdp(`bk`gljflmz&`hmgipoo(dinebhcg'`alflz)gatvd}f~dihdy$gkuz}tj,a`iogjd%bau`filcv&hfooqbvq%ko`~)bmmvv}toxtns$coak`xd`bb+aacnmylg-u&ebch&`bnlfhgl$`ibn`n`j-knmnoyn&jioij)ecifl~*eojan)cidae'hczmv.hlskrqnem)kjsrai*khnh{`twa-fnlw'emimfjmldt+aofen`ucw/kjls-`gm~nrit)hjo`qel+foahhlo'`oci,dikbbfjda,iggkdmfcnfx+hmbjelae|,ehchla``~%hkfcheja&ffkkhgnb/hjdakodimy(kfbnik~or$`kciojf,cigbqu`lf(jhempjb`t/afbaxodj'kdikxih|l-dmccvimmmq'ei`apfmq/enbovpfc+*jf`lqtclmb+%hfclq}ajc{.ahkhuwbhikm$ikejvkng(jhho}maxe/jdjk|+gdibd*jlcppu'dfeip/`gdfpba/dlkhvulc-glomaf$ekhl~glrs$ceimig/belrnzvkscpl.fij{wlsgir(gilkbc$kgfos$ihdk,ifhoofe*hhp}ob}o*kitlhdg`&bkwy~prbkk-jrb0l/avcaw%ezb`n.fvghczmtf,dqkemqnm{l&jx`ij(rkjzd+dwgs/`yfpilf(erivsbq*jvcwt,dwfsi)jpbemq-bvogl%euheew)jxbi`nak)ksmnkkbiv&fqntdo*dzd`bjdcbmy$brj|jnghnhm}.`unufkpf~/awi'judgvc`db.eskaavs*e}nndebaq$jrmd/bshnmp,kjebk`+hk{vcw.`wit|`fi,e|ez`fs.a|g`jokess(f~h`knhi`pw%htmm)a}dm`gmgavq'gqkn}~&`}l~'ewdworab,brnsg}gh$k}kpn~joet*fse~mhib$.dsjujogmct 'k}f~o`fkbjf%)itdqywicaw%j|hcsrm~&`|jdwqhielx%cecj'fihgav%`fleiu)lehf,eighf~bmi.cadhhv$acsnib+gkpmy+agsdngc`&cfco'ab`yukygjw*mck|a*onmsu`rjhq%gnaoee}l'`ali.mfdlf)envowo},lgzcsl&ffrtis&masb&boucf+onuhhqnyyhhprbr%cafj*mjjmerk`k-`afkgoveribfx%cjgnmogg&ga`jlmvp&aiennmah-mhbkjoabnp.ali`ick,mcbhqef&fldbqeag*bccnkl'eho&`n`b$dobp'dkhf+mbebo-gmeeglimpwz-ollm+`cvvim|*`ctsvcho.ciuu{(elzmcyc%m`{lezbu.mouvwgnb$fhqo*bjy&ecrjnejag-`lbel|k`+`gmokfqsic-ejfdrwqyd`*`mhn+foo`hg&fgl+mie,`dkd*fj{g(avdbxtagn(dqfewsnmo$fvnt``fh.euqc'nqpgo'o{|jbmd%`}gk/owhcjqt%lsfehkpcj*mwnjc~ki%l|fbe~'d}od+obrrxj~.lhpcimf{-md~dd&`evsvypy.lgqsgcz/gcghribpc,gigewmkvae*``aa~gbpmlm *bmab|ddwjnh$ogenk.ajamp$mwob}+`radsomn.`wf%gyjerv,a}ijlzlko/gcoke$awvfyghn$ltf+n|fvfnno|+gsf`r|o$azldu~ad*g|beu~ooe(g|eh~q`dfat$`}xgn{adk*oebnctam`v'lie`ly'feo.ah`bcna,bfbcoq+fjej}*o`ielf&mhbl}sl&a`n{nbr+mgkpp/cbbuc,mhjg|lpcj`p%`ftw(djz~fl+*ebpuhoc)(afup}(,gdysdqgh)mbv&mecn-nbpgsr'og|mukc/`jqnvd`my%dg~{i,ciidvd,cmas'aeiot%memevjn %bedbh$needifz+ceffkcmf,foohisnl/lcgtbb-l`f|baor)b`kug`lkm$dmuira&aona~,aifaphdd%dlnntadl(lnkgnxafga,nafmgrgsbo#*n`olnvldhog#$g`edcr`~`kaw#.fnidesmuahlxt.`aglmz`|b`chc#%`iul%nbyhhn-ckpsdp+`iqtgvha'mjzq`uicde$/ajqso~`ocx&&cmzwgpjlnig'*gbqrjg`/fgcjdd(ljaykmz'fjg~pddihb)emfo'di~mlo$fh~l~c'`o`)elfdhc,ahkrddvmfm'gkoto`o)afhrbtamos*aldvgmajgz(dfhuxrix)devl*ggu`pkje$lmyln-cfthmigwb%blrzrlgo+gotrvtblqz%bxke$mvbkllsgm%evce`~`sbbdt%dwfe`vcl-o~+mrkdr&l|b$muk`n(ow`m*ntdci,cvjkkdhe/`wc`afo+lphcaumb~%dwbinc+e|ja``tu)lvcca{'`}jkg{{*npkbgace(o~cnmax}.d~gmg{ld`,gtdjnskcon,ntkmmbi`/e~dknbs*bficn'm`hjgh+fskkhge*brbhhhockufa+gjlagiq}q*m~abie{idi*c~gkoq,csgniki`)mseneg +m|`llgmdr`$mglhdijdr$dfhdoc+dvf`tko,eqhcq&m~ia}cpo,nwbls`ipf%gvgoyl~*nrbfaudfhn{%lrof~)dra,gwnq+bsjmweh*cpneqfl*n|lau{,gpvji(cjolagdc(edgfhbillf$-nbediceoc{)ehimqsc(dj}etc`lv&miq*md{grsajfpknkije{`(cd~ynq)-ecm|*lfc{ex&omom$lcmivhk,mmphgo(ngrauei-fil$mod}f$aasr+ai{fz,dm|nalb`/neapcc`jjst-fm`'mja)amoeog`t+cgmgcni~-aebaekhnq$cega`fe(dieeijiaa-`jea`molr&fdaehmloq(bkc`cehfqqoco|fadu+gefaojxkm~dw'ljimqr`cnd*`lj~iadz(beg`)akpnacjj&ek~*dmsbd$`rogqbiffg-lqokld)ezkf-bsmtx'gzopstjbshng,ctjfd/gpij`z*cpk$c}t*msui+`quq$ayxqme*ezxuj/nssrp'aaffq/ncmfhl`'o`{f/netnar&acqdkf.law`d-`oidfrhc'iddcn`f`}x*b`ed.hm`h'bnlg.kmnoi|s`(`mvlid$l`zwoy%o`qexj{lztef*ckabic)`boaiiav-bnbaiibfn+lkokenjlhlt/`klodk+bnwikbo/hm}gl{'n`moslxf)k`}k`snxq-llq'in+jejd)hgllof.lgcu,hkm`$okhmwpaony-ohkhajfn(jdjh&akfoggkfbzy(akogvdqim%bdlecs,kleo+ihejnpq-nmjlm}&hfljs,`ooj(jjgcc{+neoigvx,hgewosr,cjqg&mexh/ikve-mjqfa{.mk{eibxq$bhqnp+leyrfu`m$bn{amdb+`gpop(mjx}idf/kjwnifj)lmtyvpty(`l|thsqg-iqdoez'jwm}'izw}%ctzpilz$oyoei+mrfne/nffot|,lnmn}(nom{$knkc`ii(anadtw/mftoxw'aoqnz`ksvzn-ngqluth`n`o+km~jnkv{$kdjgkq`d(kwuhoh'lypiolc'jtvd`o.q+lvekaheg,`}`i&oddckys&cgkkooe)iafkrklv/akgj~jnqovrly*iil`.nkq.biwhebvf(civipi{*ced|v(am`q%mewnoog.ldztv'lbprx`bp`ps-aa~$igropc.bbaf&okfkihn'jjfn$jldbd/obdagen(cllgbp#/ohb`r,mlcj`%`mlmneko-bag`oqb)intg$kcr'&hbsol*ljb|jdl)c`ze(+klz{,abx}ng(bc|pvd-bnnh}*bqebhdm%bqc{*n~llobhrdfq/okn``r)lggdop$hjmcsd,hjlkr(c`feoe-lcb(libdms/`aa`&kaen&jnimcl-lhmgc{&`oieijg-hnmiq.mlnn.ocmkp&jlwseru,kol,nnoln%jhmice{v$okic(jdnnwk.lign%jml~%ixcz$lvctp{.lrj(i|f-cqg`}kbdd$`ulfsnkao(cpkffs&ctoghic(mqlpd})iulw&bwdkeaocvv(`pjimo`n`|v-hpmr$my/h}lc+gcetdum,jhlo$jf{b`k`m-hi~oj(lnyaghh,eavgjpj$elwkjgk-fmyeog/mbvad$jf/ilsjl$fc~acaumaknt.jcsfm-jmz},lly|m,nhbnvdh/ecclem$io`amt%fkfnen%lcfq-hng`s&ili{`oh`*kmgs%dkfce{al-kcwvkt+dkwftgp*dhkmcgnve-elmouc/jhoxe{,ghunw(hjulfg-gjsnchhg(fnsnemg)lm|lgpo,ikdjsijf+deuohrwehm'oiwdz`tsej)oipd{g`mnw-idphceg)nul)o|hnclk&dt`jbnu/e~`h|jfedlgqel'lrnfi/fylgm)ec`a`a'dif%higcj%kafjhteof)kbky-nho`j~au&igoqbwn.davla|kke'hirvhi`~n+nfur`hav`u+gdzvdzfjvl+g`utftjihzwlv&nbrqwhijrmz.hjvwtsbfqf/oct~vvhjrodg'ij|yesqu{lmkdst%hfcieeq}es,iljvqaam'hn}m&hoyia`o/jnleu$jfnmt%daahfqbldd/kcaa`u~&gooo+d`hf{`p(lfkc`|,khk`oe*fndb})ekdh&ifk~'hnoaq$kkogsq*dlheqr`qagd/jnf`{bu*edhdzvhx,ofma~tbov'nomncqcjaf|*hmvnhk)hlpmd(lizend&jdprngic'hmrajawbj%nkrmdosbbg.hh}`c`wjlbx%fepacfqjhfl'.gd~nfc`bmp*lj|mdc|enbh(lowjhe|ecagd"$nouibfwbmkfd{(dfphowgteo)mh}bbycwejl`(mg~hbp`~inmr'mjqlcsb~g`ol&ilqiltc}emmfn&mmrn`y`tfkhkau/fk~kd{jescegln)lwd`-nvmgncwm&oudcdl~mz+g~aahh`btoy-kvgdnn`egv.d|ghsj)f~nhocw&euo}+gpqegz/fwrml{dp$eszejk*njmlb*mjughvoa,feup-fiy|pacsen'jexp{ld.hayvzpes~+k`xsrjoxd(jczc&nmb{k+galpg*ibdrnet*egmrgnf%jecri&w+hcl.flnlx(ljoezial)oheotibiz%nmd`)omll`*eib``j*hjmegvggdc*hll`fyc$oiol`zafm$ialdbyejjm&mhbgkp`jknlzv$jmebe{egoinwu%u*dlfchpcfy)iolmhsar&gbglawn#x-oiof`s)iamcn{,fialeqmafa&mmegdqnokm&hbbmbpp+gofddx'r*fjelde.nlflf`n&moedieq$mjo`njob*laacicncp.iaaemr&oblnuq,d`feaw%klcegc(j`bq$gjbpf(mccva*dlp-dmtujn)gotsegpjhe%gcrwm}/mnfd`p/endnnq.ofn`bllnua{$ogo`+dmnb`q/ngnjid%ifnmgk'fglimr&htf`$du`an{-ouac+fpqbrjanz/myjsi$hj}llwjm/hyak)nycf*hvckqih+*exg`{f)dvnlmy-mzl}/lzide'yhn`.{ekmxaka`m,uegdzrogmem)wkjykny+xdkq{*teew`&wajv`ev+y`yj$qmjvfjoackke`)tcgb*rdgkn{%qnbibwqdea.ued/tfgaiec-wcn-vom%xomupbn,ubm{{kp~%qdgb~'zgdbqqitbhj.qnni>)xencln%{`ihu'paklwfu&znlrikxd*qcvofm%pnxr,{jonnsg~*yk}n%sasjld$xbrakmd)tbuja`b.t`rkjhjc.youmdbp(tnsr*qk45)pci`inogq&pnkhdg`im|&unkljdnjor,pjafz(qmi`|(xocq'qkhs/xndvln&tomqdq/wceri`a*umisowhgb,qbk{sjgu(pbka``o,yluy*rbv{ga$r`y{et*pnxzdp!/xhpskdko/{kwzoe($pkxwnle+{oqpkof'/znzpll%pbroc*{l{z*yoczely)xh`sflvh${ibai'zmand,vlgha}ylnh%yojd-qhgaim(rgd&smikho&ujjox'uff)wgim.sdod|hkf+wfir*yeeqgz,wofpltrifeygz)peiy`ne)yklzsobtgskvo'ripimz&ugpecmojl`~$vdvk,wepmdhkbm*skwjl`jn-wiumm$xeyhfdt`zix&yfxkvqhohbtp'ynu.ye}dyrp$uwomgssc+twh`'zrbhc,rxiciagca-vwkjbt`fo%urf{g`cll`-yzou${rjpui}wpf+ppe}f{}iev.s}=3h%t|26s*yqka)u}icb'qrg`kkjan)ytb'rwefl|'yrab&twm`gg|-rbl+xk~hg`%z|{bk`wsbhjdyp.yqst*z|xycd*vtqt`mr({zr|/x}z{~hj)zqxzqad~bs.x}t{zcriobu%tvswqg`albr/wp{z{hcwx-stzmlsex(z|xw}woqfnoy,t|v{,utjriig.twn`l-zwdlr.qpccibo-ruii&rg=w+xabjm%vbhcam+xhgku~.zjlmieg-qgnbfelx.zdbicko(ykng|'tise'qesoa*vewoq$ziqbp~)zma{bon,tofvmi}v~*qojsto%vb`hbdns(rbeibin,vb`bgq%ze`wqib.pgctbml(vnmf`|$ydnfmeq%p`lfa}e`*xd{sfh`b`i$qeql-sdqawa-tg}ayeaf'zkke`e-vb`bgu'woklli*woohcga+yjdfn+zngc`p*zj}nomsa)vgv+t|vtbc%ruzsbjc)veoos%vfgkm/pkjhcoil`{*zclgi(phhckl`dl{'skwei$wiel/u``nex|kg/qea|&rinjija/reyft&vgxntyl}&yivev}j*pk}k-yfdem,wcs{g'soufms.zar&q`|ga.wc}gi{d)ugx`n~ga*sdxcmwyc-sgskea$zfhausbj%ybxyhr-xdpnavf'xb|kdn-zorpfx.yep|dyq$x`pqbf*{crrefns-sm|ucjqe&vapy/ra}~dffi+ymq{-xn`i)xlce)sjaa`an%{nced`me/rakt+yjit'soer|}vjiq&yklofd)r`hbq%tlbgfka*vkcu+snhukbh%pmhvgach'ykbrn,xhjpddubv.vaowdd*wlotmffl.ybbqdckng&qai}bj&zki~oeybq`kgw&wnosm~dh)ykn~lvjj`y%rkkq`sog/qja|ljwbkz%san|nfwvbfq$z`b}l`ef,rjlulhqre.qhowjf`,q`lqlju|-vmmufma.rh`~jqpmadraj%zhavw)znnuppdki+tjb|dm%xi`~wop)qijwshkl(yjaqu}'-umkjw%qklf~agb+u`lwwl|ik-sad~vcdn&ribm.vozyq$sn~vlqta|/paqzhldn$znpwikheu&pijkn'tkgm`dmtfm-qckkin}a`*wcggnqcgpn*qjbjhz)zmbjhqhhvhb/wkdm`rpbdtf,{lkengvp`/vb}n+vnsmg`e/wndhw%tnckqapc+zhk{rfw%sfb~ocvgp.rhaw)ribtb-rmaomqlqcr.zomosfkco+pncf{a`dhk-pgo`nl}bn$xnmme%zdhnaifgf'zoihne}hcd&wmeyniacc-qhipoq(shexr-xgvu/ugpz,xmpwq*{o|vtkim*pfvqtp(po|su`bv$zk~subewb&rmjcb%yfhblqmmigk`lz)sd~v.pddrcj)tgjtbn{h}ho.tlh``ct&qdcl`fxll)yikf`cwoma(wmidemvr)uioools,s.shlvgw%{oiv$ree~db`h.xofuiblfcy.zdi*sjcon+wm`honud,ulmehi|n&yimlllrc+peleo},wilhagenqho,wmidcbltjo&pnew|(zfw/zfwbm},{yeeidvsj`ahdlz*wshmbn~vhj`gamw(s{bg`'svdhatkadkkjgy,qymxg.zvnthhjh``,pyfqjkhb+wxdzikchzgnt%svo{olgqmcp(sshj+uvodh.vs`d*{sbbo||~,ptob-qxbq+vqisscw,wzh`qjhf,ytdffg%urqci`dcble*xpvjk/pxvnmr+tp|ht)xpicl,xs`glp-{rphuil.p}tkngny+{rqoscjv`.tqvlng+utzgcnnl'z|tpkl%spsuiea|`n*v|y`db}kmny(srhi%{uc`gncl+ttgkat-q|fafl*p~glkrcxr.xwglmr`mf`+{sdogptat'zvehjlm&t}c`iln*{}`eifp&tqhnghgz'q}hgn)tsbswblf*vpborkb{q*uztlngcz.vhihj*wdal-qcivol)qdgc$ufl~vg,petg`cq(pkue/|dgs+tc{yjr-ltqhynvv/}`tw`(vn{|biib'uftqhdhap'tajijna{r%}kazgfs`)thoqlf`g.pmsbaufme-wopnavcp*whkdgvj`oogr-sifme`-tmw*scrin}ki{zf{*ltn|ao/vargthbnp,kduihhf&}kwhfh,qbvhhjkmr,u`rgjvnr'~iww(~mvpmg,pbtqnlx(ro}wq*~dt$}hmh`r,kfnm}cvqrs`v*vhgfwm(}dhmqfsasvs.~eeltos{ed{'tozwtu'itusza%pkvteq)|ftdnkefo/|uhjlmvpzczk)qwgkv$vw`idjd'|rgdg})wzghsd~rfm/rwfet{gz|hj$upbgr}ftqlqd,trivho{&syksoysfo-urncbj)~reu-svimcjff+vqgkaldcjke%srsf&rsrnin)rteq/~}ahj.jenlg.~pmbawsmnzf/ta`$k-mmrhkefje&r{wcczs*~xtcagyp%|rqaeg|uq/wp`mbxq-}znjf}d)s{beo/}{agb*qvly|t(velioh%vfelgbm$~ewckke.whiq*jktd~ox/shbpisfq/|cercgfd-wjgdegkl'piqoho(~mtc`nk{lfmnq,}kjcu+}k~e)p`h$ubik,wegllw+vanclgl*vdrvwbn,ucbwmh.slbfhm+~odvg`(vfijboz(qoin`so-vgqi$pejcga)ug|trgq/qij`jmt.uacpa)|cbclmr)hkvll~,}livcgxgkdb-rimrc{aahj%s``*ractoo`gecx.qbj|cw'|mjum}xixk(~chscs'uiny'sbdp(ajtc,hnragrilcw%qoftmhm~wo/sanddx&jli``.pjmflm|ijhfv-|mohp+pj,}no*}mfnn'r+dr&w`$wq{t,s~}~kc-zvb+~|x$qhjbmc%cgdmtejk$}bfjfl,{ary`wo`im]] -local mykey = "forkingshirt" - ---local f = io.open('words.txt','w+') ---f:write(encrypt(table.concat(string.split([[ words ]],'\n'), '.'),mykey)) - -local decrypted = decrypt(words, mykey, 31459) -local word_table = decrypted:split('.') -local word_dict = {} -local storeWord, hasWord -storeWord= function(str, dict) - dict = dict or word_dict - local char1 = string.sub(str, 1, 1) - local rest = string.sub(str, 2) - if not dict[char1] then - dict[char1] = {} - end - if string.len(str) > 1 then - storeWord(rest, dict[char1]) - else - dict[char1]['end'] = true - end -end -for _, word in ipairs(word_table) do - storeWord(word) -end - -hasWord = function(str, dict, i, len) - if not dict then - local res = {} - for c = 1, string.len(str) do - local has, _i, _len = hasWord(str, word_dict, c, 0) - if has then - local whitelisted = false - for _, wl_word in ipairs(badword.whitelist) do - if wl_word == string.sub(str, _i, _i + _len - 1) then - whitelisted = true - end - end - if not whitelisted then - table.insert(res, {_i, _len}) - end - end - end - if #res > 0 then return res end - return nil - else - local char1 = string.sub(str, i, i) - if type(dict[char1]) == 'table' then -- move in further - return hasWord(string.sub(str, 2), dict[char1], i, len + 1) - end - return (dict and dict['end'] == true), i, len - end -end - -local replacements = { - ['4']='a', - ['8']='b',['13']='b', - ['1']='i', - ['!']='i', - ['0']='o', - ['5']='s', - ['7']='t' -} -local accents = "ç,æ,œ,á,é,í,ó,ú,à,è,ì,ò,ù,ä,ë,ï,ö,ü,ÿ,â,ê,î,ô,û,å,ø,Ø,Å,Á,À,Â,Ä,È,É,Ê,Ë,Í,Î,Ï,Ì,Ò,Ó,Ô,Ö,Ú,Ù,Û,Ü,Ÿ,Ç,Æ,Œ" -local accent_conv = "c,ae,oe,a,e,i,o,u,a,e,i,o,u,a,e,i,o,u,y,a,e,i,o,u,a,o,O,A,A,A,A,A,E,E,E,E,I,I,I,I,O,O,O,O,U,U,U,U,Y,C,AE,OE" -local t_accent_conv = accent_conv:split(',') -for a, acc in ipairs(accents:split(',')) do - replacements[acc] = t_accent_conv[a] -end - -badword = { - whitelist = {}, - check = function(str) - -- replace leetspeek and accents - for old, new in pairs(replacements) do - str = string.gsub(str, old, new) - end - return hasWord(str) - end -} - -return badword \ No newline at end of file diff --git a/love2d/lua/ecs/plugins/xhh-effect/init.lua b/love2d/lua/ecs/plugins/xhh-effect/init.lua deleted file mode 100644 index d1e201b3..00000000 --- a/love2d/lua/ecs/plugins/xhh-effect/init.lua +++ /dev/null @@ -1,135 +0,0 @@ -Effect.new("bloom", { - vars = { samples=5, quality=1 }, - integers = { 'samples' }, - effect = [[ - vec4 source = Texel(texture, texture_coords); - vec4 sum = vec4(0); - number diff = (samples - 1) / 2; - vec2 sizeFactor = vec2(1) / love_ScreenSize.xy * quality; - - for (number x = -diff; x <= diff; x++) - { - for (number y = -diff; y <= diff; y++) - { - vec2 offset = vec2(x, y) * sizeFactor; - sum += Texel(texture, texture_coords + offset); - } - } - pixel = ((sum / (samples * samples)) + source); - ]] -}) - --- doesn't look the same as chromatic abberation, but looks better for gaming imo -Effect.new("chroma shift", { - vars = { angle=0, radius=2, direction={0,0} }, - effect = [[ - vec2 tc = texture_coords; - - pixel.r = Texel(texture, vec2(tc.x + direction.x, tc.y - direction.y)).r; - pixel.g = Texel(texture, vec2(tc.x, tc.y + direction.y)).g; - pixel.b = Texel(texture, vec2(tc.x - direction.x, tc.y - direction.y)).b; - ]], - update = function(vars) - dx = (math.cos(math.rad(vars.angle)) * vars.radius) / Game.width - dy = (math.sin(math.rad(vars.angle)) * vars.radius) / Game.height - vars.direction = {dx,dy} - end -}) - ---[[ - blend = {"replace", "alphamultiply"}, - vec4 px_minus = Texel(texture, texture_coords - direction); - vec4 px_plus = Texel(texture, texture_coords + direction); - pixel = vec4(px_minus.r, pixel.g, px_plus.b, pixel.a); - if ((px_minus.a == 0 || px_plus.a == 0) && pixel.a > 0) { - pixel.a = 1.0; - } -]] - -Effect.new("zoom blur", { - vars = { center={0,0}, strength=0.1 }, - effect = [[ - vec4 new_color = vec4(0.0); - float total = 0.0; - vec2 toCenter = center - texture_coords * tex_size; - - /* randomize the lookup values to hide the fixed number of samples */ - float offset = random(vec2(12.9898, 78.233), screen_coords, 0.0); - - for (float t = 0.0; t <= 40.0; t++) { - float percent = (t + offset) / 40.0; - float weight = 4.0 * (percent - percent * percent); - vec4 sample = texture2D(texture, texture_coords + toCenter * percent * strength / tex_size); - - /* switch to pre-multiplied alpha to correctly blur transparent images */ - sample.rgb *= sample.a; - - new_color += sample * weight; - total += weight; - } - - pixel = new_color / total; - - /* switch back from pre-multiplied alpha */ - pixel.rgb /= pixel.a + 0.00001; - ]] -}) - --- UNTESTED -Effect.new('grayscale', { - vars = { strength=1 }, - effect = [[ - number average = (pixel.r + pixel.b + pixel.g)/3.0; - pixel.r = pixel.r + (average-pixel.r) * strength; - pixel.g = pixel.g + (average-pixel.g) * strength; - pixel.b = pixel.b + (average-pixel.b) * strength; - ]] -}) - --- DOES NOT WORK -Effect.new('warp sphere', { - vars = { radius=50, strength=2, center={0,0} }, - effect = [[ - vec2 coord = texture_coords * tex_size; - coord -= center; - float distance = length(coord); - if (distance < radius) { - float percent = distance / radius; - if (strength > 0.0) { - coord *= mix(1.0, smoothstep(0.0, radius / distance, percent), strength * 0.75); - } else { - coord *= mix(1.0, pow(percent, 1.0 + strength * 0.75) * radius / distance, 1.0 - percent); - } - } - coord += center; - gl_FragColor = texture2D(texture, coord / tex_size); - vec2 clampedCoord = clamp(coord, vec2(0.0), tex_size); - if (coord != clampedCoord) { - gl_FragColor.a *= max(0.0, 1.0 - length(coord - clampedCoord)); - } - ]] -}) - -Effect.new('static', { - vars = { strength={5,0} }, - effect = [[ - vec2 new_tc = texture_coords; - number off = random(vec2(0, 1.0), texture_coords, time); - pixel = Texel(texture, vec2( - texture_coords.x + getX(off - 1.0) * strength.x, - texture_coords.y + getY(off - 1.0) * strength.y - )); - ]] -}) - -Effect.new('cosmic static', { - vars = { strength={5,0} }, - effect = [[ - vec2 new_tc = texture_coords; - number off = random(vec2(-1.0, 1.0), new_tc, time); - pixel = Texel(texture, vec2( - texture_coords.x + getX(off - 1.0) * strength.x, - texture_coords.y + getY(off - 1.0) * strength.y - )); - ]] -}) \ No newline at end of file diff --git a/love2d/lua/ecs/plugins/xhh-vector/init.lua b/love2d/lua/ecs/plugins/xhh-vector/init.lua deleted file mode 100644 index bf5db12d..00000000 --- a/love2d/lua/ecs/plugins/xhh-vector/init.lua +++ /dev/null @@ -1,96 +0,0 @@ -local range = 10000000 - -Vector = setmetatable({ - random2D = function() - return Vector(Math.random(-range,range)/range, Math.random(-range,range)/range) - end, - random3D = function() - return Vector(Math.random(-range,range)/range, Math.random(-range,range)/range, Math.random(-range,range)/range) - end -},{ - __call = function(self, x, y, z) - return setmetatable({ - x = x or 0, y = y or 0, z = z or 0, - is_vector = true, - array = function(self) - return { self.x, self.y, self.z } - end, - matrix = function(self) - return { {self.x}, {self.y}, {self.z} } - end, - mult = function(self,s) - self.x = self.x * s - self.y = self.y * s - self.z = self.z * s - return self - end, - set = function(self, x, y, z) - if not y and not z then - -- x = vector - if x.is_vector then - self.x, self.y, self.z = x.x, x.y, x.z - else - -- x = table - self.x, self.y, self.z = x[1] or self.x, x[2] or self.y, x[3] or self.z - end - elseif not z then - self.x, self.y = x, y - elseif x and y and z then - self.x, self.y, self.z = x, y, z - end - return self - end, - - },{ - __tostring = function(self) - return string.format("Vector(%d, %d, %d)", self.x, self.y, self.z) - end, - __index = function(self, k) return rawget(self,k) end - }) - end, -}) - -function matprint(a) - local str = '' - for r, row in ipairs(a) do - str = '[\t' - for c, col in ipairs(row) do - str = str .. col .. '\t' - end - str = str .. ']' - print(str) - end -end - -function matmul(a, b) - local was_vector = false - if a.is_vector then -- just swap the arguments - local temp = a - a = b - b = temp - end - if b.is_vector then - was_vector = true - b = b:matrix() - assert(#a[1] == 3, "matrix columns ("..#a[1]..") ~= vector size (".. #b ..")") - else - assert(#a[1] == #b, "mat_a columns ("..#a[1]..") ~= mat_b rows ("..#b..")") - end - local ret = {} - local rows_a, cols_a, rows_b, cols_b = #a, #b, #a[1], #b[1] - for i = 1, rows_a do - for j = 1, cols_b do - local sum = 0 - for k = 1, cols_a do - sum = sum + a[i][k] * b[k][j] - end - if not ret[i] then ret[i] = {} end - ret[i][j] = sum - end - end - if was_vector then - return Vector((ret[1] or {})[1], (ret[2] or {})[1], (ret[3] or {})[1]) - else - return ret - end -end \ No newline at end of file diff --git a/love2d/lua/ecs/print_r.lua b/love2d/lua/ecs/print_r.lua new file mode 100644 index 00000000..39ebf41b --- /dev/null +++ b/love2d/lua/ecs/print_r.lua @@ -0,0 +1,29 @@ +-- A function in Lua similar to PHP's print_r, from http://luanet.net/lua/function/print_r + +function print_r ( main_t ) + local print_r_cache={} + local function sub_print_r(t,indent) + if (print_r_cache[tostring(t)]) then + print(indent.."*"..tostring(t)) + else + print_r_cache[tostring(t)]=true + if (type(t)=="table") then + for pos,val in pairs(t) do + pos = tostring(pos) + if (type(val)=="table") then + print(indent..pos..": "..tostring(t).." {") + sub_print_r(val,indent..string.rep(" ",string.len(pos)+8)) + print(indent..string.rep(" ",string.len(pos)+6).."}") + else + print(indent..pos..": "..tostring(val)) + end + end + else + print(indent..tostring(t)) + end + end + end + print("*"..tostring(main_t)..": {") + sub_print_r(main_t,"\t") + print("}") +end \ No newline at end of file diff --git a/love2d/lua/ecs/system.lua b/love2d/lua/ecs/system.lua deleted file mode 100644 index a9d0b75f..00000000 --- a/love2d/lua/ecs/system.lua +++ /dev/null @@ -1,552 +0,0 @@ -local entities = {} -- { uuid={ } } -local components = {} -- { uuid{ entity=uuid } } -local systems = {} -- { uuid={ } } -local rend_systems = {} -- { uuid={ } } ---system -local callback2system = {} -- { callback={ sys_id } } -local component2system = {} -- { comp_name={ sys_id } } -local type2system = {} -- { type={ sys_id } } -local addSystem, removeSystem -local component_requirements = {} -- { comp_name={ comp_names } } ---component -local comp_name2uuid = {} -- { comp_name={ comp_id } } -local component_defaults = {} -- { comp_name=props } ---entity -local entity_templates = {} -- { name={} } -local add_template -ENTITY_REQUIRES = {'pos','quad','angle','size','scale','offset','shear','blendmode'} ---rendersystem - ---world -local require_component -local check_obj_requirements -local type2entity = {} -- { type={ uuid } } -local type_count = {} -- { type=# } -local type_count_incr -local type_count_decr -local destroyed_entities = {} -- { uuid } -local destroyed_components = {} -- { uuid } -local need_cleaning = false -local remove_from_world -local system_add_fresh_obj ---state -local states = {} -- { name={fns} } -local STATES = { - NONE=1, - RESTART=2, - STOP=3 -} - -local iterate - -iterate = function(t, fn) -- fn(): return true to remove an object - local len = #t - local offset = 0 - if len == 0 then return end - for o = 1, len do - local obj = t[o] - if obj then - if fn(obj, o) == true then - offset = offset + 1 - else - t[o] = nil - t[o - offset] = obj - end - end - end -end - ---SYSTEM -System = function(opt) - local sys_id = uuid() - local comp_name = opt.component - if comp_name then - -- store name of component this system deals with - if not component2system[comp_name] then - component2system[comp_name] = {} - end - table.insert(component2system[comp_name], sys_id) - -- store other requirements - if opt.requires and #opt.requires > 0 then - if not component_requirements[comp_name] then - component_requirements[comp_name] = {} - end - local comp_req = component_requirements[comp_name] - for _, req in ipairs(opt.requires) do - if not comp_req[req] then - table.insert(comp_req, req) - comp_req[req] = true - end - end - end - end - -- store callbacks this system has - for k, v in pairs(opt) do - if k ~= 'requires' and k ~= 'type' and k ~= 'component' then - if not callback2system[k] then - callback2system[k] = {} - end - table.insert(callback2system[k], sys_id) - end - end - if opt.type and not type2entity[opt.type] then - if not type2system[opt.type] then type2system[opt.type] = {} end - table.insert(type2system[opt.type], sys_id) - type2entity[opt.type] = {} - end - - systems[sys_id] = opt -end - ---COMPONENT -Component = function(name, props) - if type(name) == 'table' then - local use_fns = {} - for name2, props2 in pairs(name) do - use_fns[name2] = Component(name2, props2) - end - return use_fns - end - -- add component name list - if not comp_name2uuid[name] then - comp_name2uuid[name] = {} - end - - component_defaults[name] = props - - return function(new_props) - return use(name, new_props) - end -end - ---@global: to be used before World.add, retrieves a loose component (one without uuid) -use = function(name, props, obj) - if comp_name2uuid[name] and (not props or not props.is_component) then - props = props or {} - props.is_component = name - local new_props = table.update(copy_shallow(component_defaults[name]), props) - if obj and not obj[name] then - obj[name] = new_props - end - if obj then require_component(obj, name) end - return new_props - end - return props -end - -add_template = function(name, template) - if not entity_templates[name] then - entity_templates[name] = copy(template) - else - table.update(entity_templates[name], template) - end -end - ---ENTITY (combo template/system setup that returns a spawner) -Entity = function(name, template) - local has_methods = false - local methods = {} - local require_added = {} -- { comp_name=t/f } - local requires = {} -- { comp_name } - - local entity_requires = ENTITY_REQUIRES - for _, req in ipairs(entity_requires) do - require_added[req] = true - table.insert(requires, req) - end - - for k, v in pairs(template) do - -- get those methods outta here! - if type(v) == 'function' then - has_methods = true - methods[k] = v - template[k] = nil - end - -- guess if there are illegitamate(?) components - if type(v) == 'table' and World.guess_components then - template[k] = use(k, v) - if template[k].is_component and not require_added[k] then - require_added[k] = true - table.insert(requires, k) - end - end - end - add_template(name, template) - -- create system for this entity if it has functions attached to it - if has_methods then - methods.type = name - methods.requires = requires - System(methods) - end - - return Spawner(name, template) -end - ---SPAWNER -Spawner = function(_type, new_template) - if new_template then - entity_templates[_type] = new_template - end - local template_keys = table.keys(new_template or {}) - - return function(spawn_props) - spawn_props = spawn_props or {} - spawn_props.type = _type - - local template = entity_templates[_type] - local new_entity = {} -- copy(template or {}) --{} - local key - for i=1,#template_keys do - key = template_keys[i] - new_entity[key] = copy_shallow(template[key]) --template[key] - end - table.update(new_entity, spawn_props) - - World.add(new_entity) - return new_entity - end -end - -system_add_fresh_obj = function(obj, sys_id) - local sys_ref = systems[sys_id] - if (obj.is_entity and sys_ref.type == obj.type) or (obj.is_component and sys_ref.component == obj.is_component) then - if sys_ref.add then - sys_ref.add(obj) - end - end -end - --- check if object has all of their systems requirements -check_obj_requirements = function(obj, sys_id) - local sys_ref = systems[sys_id] - if sys_ref.requires and #sys_ref.requires > 0 then - for _, req in ipairs(sys_ref.requires) do - require_component(obj, req) - end - end -end - -require_component = function(obj, comp_name) - local obj_comp = obj[comp_name] - -- could this be a component? - if component_defaults[comp_name] ~= nil and (obj_comp == nil or (type(obj_comp) == 'table' and obj_comp.is_component and not obj_comp.uuid)) then - obj_comp = use(comp_name, obj_comp) - -- add entity uuid reference to components - obj_comp.entity_uuid = obj.uuid - obj_comp.state_name = obj.state_name - obj_comp.uuid = uuid() - components[obj_comp.uuid] = obj_comp - table.insert(comp_name2uuid[comp_name], obj_comp.uuid) - - obj[comp_name] = obj_comp - end - if type(obj_comp) == "table" and obj_comp.is_component then - if component_requirements[obj_comp.is_component] then - -- find what other components are necessary - for _, req in ipairs(component_requirements[obj_comp.is_component]) do - require_component(obj, req) - end - end - if component2system[obj_comp.is_component] then - for _, sys_id in ipairs(component2system[obj_comp.is_component]) do - -- check_obj_requirements(obj, sys_id, true) - system_add_fresh_obj(obj_comp, sys_id) - end - end - end -end - -remove_from_world = function(obj) - if not need_cleaning then need_cleaning = {} end - - if obj.is_entity then - table.insert(destroyed_entities, obj.uuid) - World.remove(obj) - need_cleaning.entity = true - type_count_decr(obj.type) - - elseif obj.is_component then - table.insert(destroyed_components, obj.uuid) - need_cleaning.component = true - - end - - return true -end - -RenderSystem = function(opt) - local sys = { - uuid = uuid(), - render = opt.render or function(obj, draw) draw() end - } - rend_systems[sys.uuid] = sys - return function() return sys.uuid end -end - -type_count_incr = function(_type) - if not type_count[_type] then type_count[_type] = 0 end - type_count[_type] = type_count[_type] + 1 -end -type_count_decr = function(_type) - if not type_count[_type] then type_count[_type] = 0 end - type_count[_type] = type_count[_type] - 1 -end - ---WORLD -World = { - guess_components = true, - -- for adding entities with components - add = function(obj) - if obj.is_component then return obj end - obj.is_entity = true - if not obj.uuid then - obj.uuid = uuid() - entities[obj.uuid] = obj - obj.state_name = State.current - end - for k,v in pairs(obj) do - require_component(obj, k) - end - if obj.type then - -- obj has entity type - if not type2entity[obj.type] then - type2entity[obj.type] = {} - end - table.insert(type2entity[obj.type], obj.uuid) - local sys_ref - for _, sys_id in ipairs(type2system[obj.type]) do - -- get system requirements - check_obj_requirements(obj, sys_id) - sys_ref = systems[sys_id] - system_add_fresh_obj(obj, sys_id) - end - type_count_incr(obj.type) - end - return obj - end, - remove = function(obj) - -- remove this obj - obj.destroyed = true - -- remove components - for k,v in pairs(obj) do - if type(v) == 'table' and v.is_component then - v.destroyed = true - end - end - end, - remove_all = function(obj, fn) - - end, - process = function(callback, ...) - local args = {...} - -- iterate callback2system - -- if system.type ~= nil - -- iterate type2entity - -- if entity.destroyed: remove - -- else: call syscallback(entities[component.entity]) - -- iterate component2system - -- if component.destroyed: remove - -- else: call syscallback(entities[component.entity]) - if callback2system[callback] then - local sys_ref, obj_ref, comp_ref - local sys_ref_component - -- iter systems with this callback - iterate(callback2system[callback], function(sys_id) - sys_ref = systems[sys_id] - if sys_ref.type ~= nil then - -- iter objects of this system's type - iterate(type2entity[sys_ref.type], function(obj_id) - obj_ref = entities[obj_id] - if State.check_obj(obj_ref) then - return remove_from_world(obj_ref) - end - if obj_ref.destroyed then - return remove_from_world(obj_ref) - end - if sys_ref[callback](obj_ref, unpack(args)) then - return remove_from_world(obj_ref) - end - end) - end - -- iter components in system - sys_ref_component = sys_ref.component - if sys_ref_component and comp_name2uuid[sys_ref_component] then - iterate(comp_name2uuid[sys_ref_component], function(comp_id) - comp_ref = components[comp_id] - if State.check_obj(comp_ref) then - return remove_from_world(comp_ref) - end - - obj_ref = entities[comp_ref.entity_uuid] - if obj_ref.destroyed or comp_ref.destroyed then - return remove_from_world(comp_ref) - end - if sys_ref[callback](comp_ref, unpack(args)) then - return remove_from_world(comp_ref) - end - end) - end - end) - end - end, - clean = function() - if need_cleaning then - -- clean entities - if need_cleaning.entity then - iterate(destroyed_entities, function(obj) - - end) - destroyed_entities = {} - end - -- clean components - if need_cleaning.component then - iterate(destroyed_entities, function(obj) - - end) - destroyed_components = {} - end - need_cleaning = nil - end - end, - stats = function(v) - if v == 'type' then - local list = {} - for _type, count in pairs(type_count) do - table.insert(list, _type..'='..count) - end - return table.join(list, ', ') - end - end, - get_type_count = function(t) - return type_count[t] - end, - update = function(dt) - if dt == 0 then - --print_r(component_defaults) - --print("SYSTEMS") - --print_r(systems) - --print("ENTITIES") - --print_r(entities) - --print("COMPONENTS") - --print_r(components) - end - World.process('update', dt) - World.clean() - State.callback('update', dt) - State.check() - end, - draw = function() - World.process('draw') - State.callback('draw') - end, - render = function(obj, prop_obj) - local object = obj.object - if object and object.is_stack then - object = object.value - end - if object then - local main_obj = prop_obj or obj - -- local blendmode = extract(main_obj, 'blendmode') - if not main_obj.has_draw_components then - main_obj.has_draw_components = true - EcsUtil.extract_draw_components(main_obj) - end - Draw.setBlendMode(unpack(main_obj.blendmode)) - - local renderer = rend_systems[main_obj.renderer] - if renderer then - renderer.render(main_obj, function() - love.graphics.draw(object, EcsUtil.get_draw_components(main_obj)) - end) - else - love.graphics.draw(object, EcsUtil.get_draw_components(main_obj)) - end - end - end -} - --- get a component from an entity or return some defaults --- defaults do not override component_defaults unless override=True --- @global -extract = function(obj, comp_name, defaults, override) - assert(component_defaults[comp_name] or defaults ~= nil, "No such component '"..comp_name.."'") - - -- make sure object has component - if obj[comp_name] == nil then - require_component(obj, comp_name) - end - - if override then - obj[comp_name] = defaults or copy_shallow(component_defaults[comp_name]) - end - return obj[comp_name] -end - -get_entity = function(component) - if component.entity_uuid then - return entities[component.entity_uuid] or {} - end - return {} -end - ---STATE@global -State = callable{ - __call = function(_, name, fns) - if type(name) == "string" then - if not states[name] then states[name] = {} end - table.update(states[name], fns) - end - end, - current = STATES.NONE, - next = STATES.NONE, - start = function(name) - State.switch(name) - end, - switch = function(name) - if name == State.current then - State.restart() - else - State.next = name - end - end, - restart = function() - State.next = STATES.RESTART - end, - stop = function() - State.next = STATES.STOP - end, - callback = function(callback, ...) - local state = states[State.current] - if state and state[callback] then - state[callback](...) - end - end, - check = function(callback, ...) - -- restart - if State.next == STATES.RESTART then - State.callback('leave') - State.next = State.current - State.current = STATES.NONE - - -- stop - elseif State.next == STATES.STOP then - State.current = STATES.NONE - State.next = STATES.NONE - State.callback('leave') - - -- switch - elseif State.next ~= STATES.NONE then - if State.current ~= STATES.NONE then - State.callback('leave') - end - State.current = State.next - State.next = STATES.NONE - State.callback('enter') - end - end, - check_obj = function(obj) - if not obj.persistent and obj.state_name ~= State.current then - return true - end - end -} diff --git a/love2d/lua/ecs/systems.lua b/love2d/lua/ecs/systems.lua new file mode 100644 index 00000000..b70a3781 --- /dev/null +++ b/love2d/lua/ecs/systems.lua @@ -0,0 +1,548 @@ +--[[ + reserved entity properties: + drawable - love2d object + draw - draw automatically or not + quad - love2d Quad + renderer - ecs system that renders entity. leave empty to use default + z + uuid + position, size, angle, scale, scalex, scaley, shear, color, blendmode, align - only set if drawable is set +]] + +local changed = function(ent, key) + if ent['_last_'..key] ~= ent[key] then + ent['_last_'..key] = ent[key] + return true + end + return false +end + +-- TODO: turn into a system +local checkAlign = function(ent) + local align = obj.align + + local ax, ay = obj.alignx or 0, obj.aligny or 0 + + if align and align ~= obj._last_align then + obj._last_align = align + + if align then + if string.contains(align, 'center') then + ax = obj.width/2 + ay = obj.height/2 + end + if string.contains(align,'left') then + ax = 0 + end + if string.contains(align, 'right') then + ax = obj.width + end + if string.contains(align, 'top') then + ay = 0 + end + if string.contains(align, 'bottom') then + ay = obj.height + end + end + end + + obj.alignx, obj.aligny = floor(ax), floor(ay) +end + +--CANVAS +Canvas = Entity("Blanke.Canvas", { + is_canvas = true, + auto_clear = true, + drawable = true, + blendmode = {"alpha"} +}) +CanvasStack = Stack(function() + return Canvas{draw=false} +end) +System(All("is_canvas"), { + added = function(ent) + if ent.size[1] <= 0 then ent.size[1] = Game.width end + if ent.size[2] <= 0 then ent.size[2] = Game.height end +print('add canvas') + local canvas = love.graphics.newCanvas(unpack(ent.size)) + ent.active = false + ent.drawable = canvas + + local lg = love.graphics + ent.renderTo = function(self, fn) + if fn then + lg.push('all') + self.active = true + lg.setCanvas{self.drawable} + if self.auto_clear then lg.clear(self.auto_clear) end + fn() + lg.pop() + end + end + ent.resize = function(self) + canvas = love.graphics.newCanvas(unpack(self.size)) + ent.drawable = canvas + end + end +}) + +--IMAGE: image / animation +Image = nil +do + local animations = {} + Image = Entity("Blanke.Image", { + is_image = true, + name = nil + }) + Image.animation = function(file, anims, all_opt) + all_opt = all_opt or {} + local img = getImage(file) + if not anims then + anims = { + { name=FS.removeExt(FS.basename(file)), cols=1, rows=1, frames={1} } + } + end + if #anims == 0 then anims = {{}} end + for _,anim in ipairs(anims) do + local o = function(k, default) + assert(anim[k] or all_opt[k] or default, "'"..k.."' not found for "..file.." -> "..(anim.name or '?')) + return anim[k] or all_opt[k] or default + end + local quads, durations = {}, {} + local fw, fh = img:getWidth() / o('cols'), img:getHeight() / o('rows') + local offx, offy = o('offx', 0), o('offy', 0) + -- calculate frame list + local frame_list = {} + local in_frames = o('frames', {'1-'..(o('cols')*o('rows'))}) + + assert(not in_frames or type(in_frames) == "table", "Image.animation frames must be in array") + for _,f in ipairs(in_frames) do + local f_type = type(f) + if f_type == 'number' then + table.insert(frame_list, f) + elseif f_type == 'string' then + local a,b = string.match(f,'%s*(%d+)%s*-%s*(%d+)%s*') + assert(a and b, "Invalid frames for '"..(anim.name or file).."' { "..(a or 'nil')..", "..(b or 'nil').." }") + for i = a,b do + table.insert(frame_list, i) + end + end + end + + -- make quads + for _,f in ipairs(frame_list) do + local x,y = Math.indexTo2d(f, o('cols')) + table.insert(quads, love.graphics.newQuad((x-1)*fw,(y-1)*fh,fw,fh,img:getWidth(),img:getHeight())) + end + animations[anim.name or FS.removeExt(FS.basename(file))] = { + file=file, + duration=o('duration', 1), + durations=o('durations', {}), + quads=quads, + w=fw, h=fh, frame_size={fw,fh}, + speed=o('speed', 1) + } + end + end; + + local setup_image = function(ent) + local info = animations[ent.name] + if not info then + info = { file=ent.name } + else + -- animated + ent.speed = info.speed or 1 + ent.t, ent.frame_index, ent.frame_len = 0, 1, info.durations[1] or info.duration + ent.frame_count = #info.quads + end + + ent.drawable = Cache.get("Image", Game.res('image', info.file), function(key) + return love.graphics.newImage(key) + end) + end + + System(All("is_image"), { + added = function(ent, args) + if type(args) == 'table' then + ent.name = ent.name or args[1] or args.name + else + ent.name = ent.name or ent[1] or args + end + setup_image(ent) + end, + update = function(ent, dt) + if changed(ent, 'name') then + setup_image(ent) + end + + local info = animations[ent.name] + -- animated? + if info then + -- update animation + ent.t = ent.t + (dt * ent.speed) + if ent.t > ent.frame_len then + ent.frame_index = ent.frame_index + 1 + if ent.frame_index > ent.frame_count then ent.frame_index = 1 end + info = ent.animated + ent.frame_len = info.durations[tostring(ent.frame_index)] or info.duration + end + ent.quad = info.quads[ent.frame_index] + end + end + }) +end + +--ENTITY: gravity, velocity +System(All("pos", "vel"), { + update = function(ent, dt) + ent.pos[1] = ent.pos[1] + ent.vel[1] * dt + ent.pos[2] = ent.pos[2] + ent.vel[2] * dt + end +}) +System(All("gravity", "vel"), { + added = function(ent) + table.update(ent, { + gravity_direction = Math.rad(90) + }) + end, + update = function(ent, dt) + local gravx, gravy = Math.getXY(ent.gravity_direction, ent.gravity) + ent.vel[1] = ent.vel[1] + gravx + ent.vel[2] = ent.vel[2] + gravy + end +}) + +--EFFECT +Effect = nil +do + local love_replacements = { + float = "number", + int = "number", + sampler2D = "Image", + uniform = "extern", + texture2D = "Texel", + gl_FragColor = "pixel", + gl_FragCoord = "screen_coords" + } + local helper_fns = [[ +/* From glfx.js : https://github.com/evanw/glfx.js */ +float random(vec2 scale, vec2 pixelcoord, float seed) { + /* use the fragment position for a different seed per-pixel */ + return fract(sin(dot(pixelcoord + seed, scale)) * 43758.5453 + seed); +} +float mod(float a, float b) { return - (a / b) * b; } +float getX(float amt) { return amt / love_ScreenSize.x; } +float getY(float amt) { return amt / love_ScreenSize.y; } +float lerp(float a, float b, float t) { return a * (1.0 - t) + b * t; } +]] + local library = {} + local shaders = {} -- { 'eff1+eff2' = { shader: Love2dShader } } + + local tryEffect = function(name) + assert(library[name], "Effect :'"..name.."' not found") + end + + local _generateShader, generateShader + + generateShader = function(names, override) + if type(names) ~= 'table' then + names = {names} + end + local ret_shaders = {} + for _, name in ipairs(names) do + ret_shaders[name] = _generateShader(name, override) + end + return ret_shaders + end + + local shader_obj = {} -- { name : LoveShader } + _generateShader = function(name, override) + tryEffect(name) + local info = library[name] + local shader = shader_obj[name] or love.graphics.newShader(info.code) + if override then + shader = love.graphics.newShader(info.code) + end + shader_obj[name] = shader + + return { + vars = copy(info.opt.vars), + unused_vars = copy(info.opt.unused_vars), + shader = shader, + auto_vars = info.opt.auto_vars + } + end + + local updateShader = function(ent, names) + if not Feature('effect') then return end + ent.shader_info = generateShader(names) + for _, name in ipairs(names) do + if not ent.vars[name] then ent.vars[name] = {} end + ent.auto_vars[name] = ent.shader_info[name].auto_vars + table.update(ent.vars[name], ent.shader_info[name].vars) + end + end + + Effect = class { + library = function() return library end; + new = function(name, in_opt) + local opt = { use_canvas=true, vars={}, unused_vars={}, integers={}, code=nil, effect='', vertex='', auto_vars=false } + table.update(opt, in_opt) + + -- mandatory vars + if not opt.vars['tex_size'] then + opt.vars['tex_size'] = {Game.width, Game.height} + end + if not opt.vars['time'] then + opt.vars['time'] = 0 + end + + -- create var string + var_str = "" + for key, val in pairs(opt.vars) do + -- unused vars? + if not string.contains(opt.code or (opt.effect..' '..opt.vertex), key) then + opt.unused_vars[key] = true + end + -- get var type + switch(type(val),{ + table = function() + var_str = var_str.."uniform vec"..tostring(#val).." "..key..";\n" + end, + number = function() + if table.hasValue(opt.integers, key) then + var_str = var_str.."uniform int "..key..";\n" + else + var_str = var_str.."uniform float "..key..";\n" + end + end, + string = function() + if val == "Image" then + var_str = var_str.."uniform Image "..key..";\n" + end + end + }) + end + + local code = var_str.."\n"..helper_fns.."\n" + if opt.code then + code = code .. opt.code + else + code = code .. [[ + +#ifdef VERTEX +vec4 position(mat4 transform_projection, vec4 vertex_position) { + ]]..(opt.position or '')..[[ + return transform_projection * vertex_position; +} +#endif + + +#ifdef PIXEL +vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) { + vec4 pixel = Texel(texture, texture_coords); + ]]..(opt.effect or '')..[[ + return pixel * color; +} +#endif + ]] + end + + for old, new in pairs(love_replacements) do + code = code:replace(old, new, true) + end + + library[name] = { + opt = copy(opt), + code = code + } + + end; + + info = function(name) return library[name] end; + + init = function(self, ...) + self.classname = "Blanke.Effect" + self.names = {...} + if type(self.names[1]) == 'table' then + self.names = self.names[1] + end + + if Feature('effect') then + self.used = true + self.vars = {} + self.disabled = {} + self.auto_vars = {} + self:updateShader(self.names) + end + end; + __ = { + -- eq = function(self, other) return self.shader_info.name == other.shader_info.name end, + -- tostring = function(self) return self.shader_info.code end + }; + updateShader = function(self, names) + if not Feature('effect') then return end + self.shader_info = generateShader(names) + for _, name in ipairs(names) do + if not self.vars[name] then self.vars[name] = {} end + self.auto_vars[name] = self.shader_info[name].auto_vars + table.update(self.vars[name], self.shader_info[name].vars) + end + end; + disable = function(self, ...) + if not Feature('effect') then return end + local disable_names = {...} + for _,name in ipairs(disable_names) do + self.disabled[name] = true + end + local new_names = {} + self.used = false + for _,name in ipairs(self.names) do + tryEffect(name) + if not self.disabled[name] then + self.used = true + table.insert(new_names, name) + end + end + self:updateShader(new_names) + end; + enable = function(self, ...) + if not Feature('effect') then return end + local enable_names = {...} + for _,name in ipairs(enable_names) do + self.disabled[name] = false + end + local new_names = {} + self.used = false + for _,name in ipairs(self.names) do + tryEffect(name) + if not self.disabled[name] then + self.used = true + table.insert(new_names, name) + end + end + self:updateShader(new_names) + end; + set = function(self,name,k,v) + if not Feature('effect') then return end + -- tryEffect(name) + if not self.disabled[name] then + if not self.vars[name] then + self.vars[name] = {} + end + self.vars[name][k] = v + end + end; + send = function(self,name,k,v) + if not Feature('effect') then return end + local info = self.shader_info[name] + if not info.unused_vars[k] then + tryEffect(name) + if info.shader:hasUniform(k) then + info.shader:send(k,v) + end + end + end; + update = function(self,dt) + if not Feature('effect') then return end + local vars + + for _,name in ipairs(self.names) do + if not self.disabled[name] then + vars = self.vars[name] + vars.time = vars.time + dt + vars.tex_size = {Game.width,Game.height} + + if self.auto_vars[name] then + vars.inputSize = {Game.width,Game.height} + vars.outputSize = {Game.width,Game.height} + vars.textureSize = {Game.width,Game.height} + end + -- send all the vars + for k,v in pairs(vars) do + self:send(name, k, v) + end + + if library[name] and library[name].opt.update then + library[name].opt.update(self.vars[name]) + end + end + end + end; + active = 0; + --@static + isActive = function() + return Effect.active > 0 + end; + draw = function(self, fn) + if not self.used or not Feature('effect') then + fn() + return + end + + Effect.active = Effect.active + 1 + + local last_shader = love.graphics.getShader() + local last_blend = love.graphics.getBlendMode() + + local canv_internal, canv_final = CanvasStack:new(), CanvasStack:new() + + canv_internal.value.auto_clear = {Draw.parseColor(Game.options.background_color, 0)} + canv_final.value.auto_clear = {Draw.parseColor(Game.options.background_color, 0)} + + for i, name in ipairs(self.names) do + if not self.disabled[name] then + -- draw without shader first + canv_internal.value:renderTo(function() + love.graphics.setShader() + if i == 1 then + -- draw unshaded stuff (first run) + fn() + else + -- draw previous shader results + Render(canv_final.value) + end + end) + + -- draw to final canvas with shader + canv_final.value:renderTo(function() + love.graphics.setShader(self.shader_info[name].shader) + Render(canv_internal.value) + end) + end + end + + -- draw final resulting canvas + Render(canv_final.value) + + love.graphics.setShader(last_shader) + love.graphics.setBlendMode(last_blend) + + CanvasStack:release(canv_internal) + CanvasStack:release(canv_final) + + Effect.active = Effect.active - 1 + end; + } +end + +System(All("effect"),{ + added = function(ent) + ent.effect = Effect(unpack(ent.effect)) + end, + update = function(ent, dt) + ent.effect:update(dt) + end +}) + +--MAP: (entity) +--PHYSICS: (entity?) +--HITBOX: position, size, hitbox +Hitbox = { + debug = false +} +--NET: needs to be reworked for ecs +--PATH: (entity) +--TIMELINE: (entity) +--BACKGROUND +--PARTICLES \ No newline at end of file diff --git a/love2d/lua/ecs/systems/animation.lua b/love2d/lua/ecs/systems/animation.lua deleted file mode 100644 index b344da1d..00000000 --- a/love2d/lua/ecs/systems/animation.lua +++ /dev/null @@ -1,35 +0,0 @@ -local animation_props = { file='', name='', frame=1, speed=1 } -Component('anim', animation_props) -Component('animation', animation_props) - -System{ - 'anim', 'animation', - add = function(obj) - local anim = obj.anim or obj.animation - - -- obj.image = Image{use_size=false} - end, - update = function(obj, dt) - - end -} - ---@global -Animation = callable{ - __call = function(opt) - for _, anim in ipairs(opt) do - local o = function(k) return anim[k] or opt[k] end - - local path = o('path') - local img, size = {width=0,height=0} - if path then - img = Cache.get('Image', Game.res('image',o('path')), function(key) - return love.graphics.newImage(key) - end) - end - - - - end - end -} \ No newline at end of file diff --git a/love2d/lua/ecs/systems/canvas.lua b/love2d/lua/ecs/systems/canvas.lua deleted file mode 100644 index 21bb91c8..00000000 --- a/love2d/lua/ecs/systems/canvas.lua +++ /dev/null @@ -1,71 +0,0 @@ -local draw_object = World.render -local extract_draw_components = EcsUtil.extract_draw_components - -local canvas_stack = Stack(function(obj) - return love.graphics.newCanvas(Game.width, Game.height) -end) - -Canvas = Spawner("blanke.canvas", { - auto_clear=true, - auto_draw=true, - setup=true -}) - -System{ - type="blanke.canvas", - requires=EcsUtil.require_draw_components(), - add = function(obj) - local entity = get_entity(obj) - local pos = extract(entity, 'pos', { x=0, y=0 }) - - local pre_setup = obj.setup - -- functions - obj.drawTo = function(obj, fn) - if obj.object then - obj.active = true - obj.object.value:renderTo(function() - if obj.auto_clear then Draw.clear(obj.auto_clear) end - if fn then fn() end - end) - obj.drawn = true - obj.active = false - end - return obj - end - obj.draw = function(obj, props_obj) - if not obj.active and obj.drawn then - draw_object(obj, props_obj) - obj.drawn = false - end - return obj - end - obj.setup = function(obj) - if not obj.object then - obj.object = canvas_stack:new(obj) - obj.is_setup = true - end - return obj - end - obj.release = function(obj) - if obj.object ~= nil then - obj:drawTo() - canvas_stack:release(obj.object) - obj.object = nil - obj.is_setup = false - end - return obj - end - -- create canvas - if pre_setup then - obj:setup() - end - end, - draw = function(obj) - if obj.auto_draw then - obj:draw() - end - end, - remove = function(obj) - obj:release() - end -} \ No newline at end of file diff --git a/love2d/lua/ecs/systems/effect.lua b/love2d/lua/ecs/systems/effect.lua deleted file mode 100644 index 50978ff0..00000000 --- a/love2d/lua/ecs/systems/effect.lua +++ /dev/null @@ -1,306 +0,0 @@ -local disable_image_batching -local get_shader -local is_used = {} -- { uuid:t/f } -local used - -local love_replacements = { - float = "number", - int = "number", - sampler2D = "Image", - uniform = "extern", - texture2D = "Texel", - gl_FragColor = "pixel", - gl_FragCoord = "screen_coords" -} -local helper_fns = [[ -/* From glfx.js : https://github.com/evanw/glfx.js */ -float random(vec2 scale, vec2 pixelcoord, float seed) { - /* use the fragment position for a different seed per-pixel */ - return fract(sin(dot(pixelcoord + seed, scale)) * 43758.5453 + seed); -} -float mod(float a, float b) { return - (a / b) * b; } -float getX(float amt) { return amt / love_ScreenSize.x; } -float getY(float amt) { return amt / love_ScreenSize.y; } -float lerp(float a, float b, float t) { return a * (1.0 - t) + b * t; } -]] -local library = {} -local shaders = {} -- { name = { vars{}, unused_vars{}, love2dShader } } - -local tryEffect = function(name) - assert(library[name], "Effect :'"..name.."' not found") -end - -used = function(obj, v) - if v ~= nil then - is_used[obj.uuid] = v - end - return is_used[obj.uuid] -end - -local _generateShader, generateShader - -generateShader = function(names, override) - if type(names) ~= 'table' then - names = {names} - end - local ret_shaders = {} - for _, name in ipairs(names) do - tryEffect(name) - local info = library[name] - if override or not shaders[name] then - shader = love.graphics.newShader(info.code) - shaders[name] = { - vars = copy(info.opt.vars), - unused_vars = copy(info.opt.unused_vars), - shader = shader - } - end - ret_shaders[name] = shaders[name] - end - return ret_shaders -end - -get_shader = function(obj) - local shader_info = generateShader(obj.names) - - for name, info in pairs(shader_info) do - if not obj[name] then obj[name] = {} end - table.update(obj[name], info.vars) - end - - disable_image_batching(obj) - -- track enabling/disabling - obj.enabled = {} - for _, name in ipairs(obj.names) do - if obj.enabled[name] == nil then - obj.enabled[name] = true - end - end - for i, name in ipairs(obj.names) do - track(obj.enabled, name) - end -end - -disable_image_batching = function(obj) - obj = get_entity(obj) - -- shaders dont work well with spritebatch - if obj.effect.canv_final.is_setup and obj.image and obj.image.batch then - obj.image.batch = false - end -end - -Component('effect', { enabled={}, time=0 }) - -System{ - component='effect', - requires=EcsUtil.require_draw_components(), - add = function(obj) - local obj_entity = get_entity(obj) - -- parse initial shader names - local names = {} - if type(obj) == 'table' then - for _, name in ipairs(obj) do - table.insert(names, name) - end - end - obj.names = obj.names or names - obj.canv_internal = Canvas{auto_draw=false} - obj.canv_final = Canvas{auto_draw=false} - get_shader(obj) - - obj_entity.renderer = EffectRenderer() - end, - - update = function(obj, dt) - -- did the enabled shaders change? - local remake_shader = false - local i = 1 - local name - while i <= #obj.names and not remake_shader do - name = obj.names[i] - if changed(obj.enabled, name) or obj.enabled[name] == nil then - if obj.enabled[name] ~= false then - tryEffect(name) - remake_shader = true - end - end - i = i + 1 - end - - -- remake it! - if remake_shader then - get_shader(obj) - end - - disable_image_batching(obj) - - - used(obj, false) - local vars - for _, name in ipairs(obj.names) do - local shader_info = shaders[name] - - if obj.enabled[name] then - used(obj, true) - vars = obj[name] - -- update built in shader vars - vars.time = vars.time + dt - vars.tex_size = {Game.width, Game.height} - end - end - - if not used(obj) then - -- no shaders active - obj.canv_internal:release() - obj.canv_final:release() - else - obj.canv_internal:setup() - obj.canv_final:setup() - end - end -} - -EffectRenderer = RenderSystem{ - render = function(obj, fn_draw) - Effect.apply(obj, fn_draw) - end -} - -Effect = { - new = function(name, in_opt) - local opt = { use_canvas=true, vars={}, unused_vars={}, integers={}, code=nil, effect='', vertex='' } - table.update(opt, in_opt) - - -- mandatory vars - if not opt.vars['tex_size'] then - opt.vars['tex_size'] = {Game.width, Game.height} - end - if not opt.vars['time'] then - opt.vars['time'] = 0 - end - - -- create var string - var_str = "" - for key, val in pairs(opt.vars) do - -- unused vars? - if not string.contains(opt.code or (opt.effect..' '..opt.vertex), key) then - opt.unused_vars[key] = true - end - -- get var type - switch(type(val),{ - table = function() - var_str = var_str.."uniform vec"..tostring(#val).." "..key..";\n" - end, - number = function() - if table.hasValue(opt.integers, key) then - var_str = var_str.."uniform int "..key..";\n" - else - var_str = var_str.."uniform float "..key..";\n" - end - end, - string = function() - if val == "Image" then - var_str = var_str.."uniform Image "..key..";\n" - end - end - }) - end - - local code = var_str.."\n"..helper_fns.."\n" - if opt.code then - code = code .. opt.code - else - code = code .. [[ - -#ifdef VERTEX -vec4 position(mat4 transform_projection, vec4 vertex_position) { -]]..(opt.position or '')..[[ -return transform_projection * vertex_position; -} -#endif - - -#ifdef PIXEL -vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) { -vec4 pixel = Texel(texture, texture_coords); -]]..(opt.effect or '')..[[ -return pixel * color; -} -#endif - ]] - end - - for old, new in pairs(love_replacements) do - code = code:replace(old, new, true) - end - - library[name] = { - opt = copy(opt), - code = code - } - - end; - info = function(name) return library[name] end; - apply = function(obj, fn) - local effect_obj = obj.effect - - if effect_obj == nil or not used(effect_obj) or not Feature('effect') then - fn() - return true - end - - local vars - for _, name in ipairs(effect_obj.names) do - local shader_info = shaders[name] - - if effect_obj.enabled[name] then - vars = effect_obj[name] - - if library[name] and library[name].opt.update then - library[name].opt.update(effect_obj[name]) - end - - for k, v in pairs(vars) do - -- update user shader vars - if not shader_info.unused_vars[k] then - shader_info.shader:send(k, v) - end - end - end - end - - local last_shader = love.graphics.getShader() - - local canv_internal, canv_final = effect_obj.canv_internal, effect_obj.canv_final - - canv_internal.auto_clear = {Draw.parseColor(Game.options.background_color, 0)} - canv_final.auto_clear = {Draw.parseColor(Game.options.background_color, 0)} - - for i, name in ipairs(effect_obj.names) do - if shaders[name] then - -- draw without shader first - canv_internal:drawTo(function() - love.graphics.setShader() - if i == 1 then - -- draw unshaded stuff (first run) - fn() - else - -- draw previous shader results - canv_final:draw() - end - end) - - -- draw to final canvas with shader - canv_final:drawTo(function() - love.graphics.setShader(shaders[name].shader) - canv_internal:draw() - end) - end - end - - love.graphics.setShader(last_shader) - - -- draw final resulting canvas - canv_final:draw() - end; -} diff --git a/love2d/lua/ecs/systems/image.lua b/love2d/lua/ecs/systems/image.lua deleted file mode 100644 index 78a0a47b..00000000 --- a/love2d/lua/ecs/systems/image.lua +++ /dev/null @@ -1,169 +0,0 @@ -local draw_object = World.render -local extract_draw_components = EcsUtil.extract_draw_components -local get_draw_components = EcsUtil.get_draw_components - -local update_batch_image, remove_batch_image, update_image, remove_image - -local BATCH_LIMIT = 5000 -local AUTO_BATCH_LIMIT = 20 -- TODO implement later (automatically starts batching an image after this limit) - -local get_batch_key = function(obj, sb_index) - obj_entity = get_entity(obj) - return 'imagebatch' .. '.' .. (obj_entity.z or 0) .. '.' .. sb_index .. '.' .. Game.res('image',obj.path) -end - -local use_size = function(obj, object) - local obj_entity = get_entity(obj) - if obj.use_size then - if object then - obj_entity.size.width = object:getWidth() - obj_entity.size.height = object:getHeight() - else - obj_entity.size.width = 1 - obj_entity.size.height = 1 - end - end -end - -local batches = {} -- { 'blanke.imagebatch...res_path' = batch_obj } -update_batch_image = function(obj) - local obj_entity = get_entity(obj) - obj.object = nil -- remove ordinary Image - - if not obj.path then - remove_batch_image(obj) - return - end - remove_image(obj) - - local img_object = Cache.get('Image', Game.res('image',obj.path), function(key) - return love.graphics.newImage(key) - end) - - local sb_index = 1 - local batch_key = get_batch_key(obj, sb_index) - -- is this batch full? - local sb = Cache.get('blanke.imagebatch', batch_key, function(key) - return love.graphics.newSpriteBatch(img_object, BATCH_LIMIT) - end) - while sb:getCount() >= sb:getBufferSize() do - sb_index = sb_index+1 - batch_key = get_batch_key(obj, sb_index) - sb = Cache.get('blanke.imagebatch', batch_key, function(key) - return love.graphics.newSpriteBatch(img_object, BATCH_LIMIT) - end) - end - if not batches[batch_key] then - -- create spritebatch if it doesn't exist - batches[batch_key] = ImageBatch{ - key=batch_key, - object=sb, - z=obj_entity.z - } - end - local batch = batches[batch_key] - local old_batch = batches[obj_entity.batch_key] - if old_batch and batch_key ~= obj_entity.batch_key then - -- remove from old batch - old_batch.object:set(obj_entity.batch_id, 0, 0, 0, 0, 0) - end - obj_entity.batch_key = batch_key - if not obj_entity.batch_id then - obj_entity.batch_id = batch.object:add(0, 0, 0, 0, 0) - end - use_size(obj, img_object) -end -remove_batch_image = function(obj) - local obj_entity = get_entity(obj) - local batch_key = obj_entity.batch_key - if batch_key then - local old_batch = batches[obj_entity.batch_key] - if old_batch then - -- remove from batch - old_batch.object:set(obj_entity.batch_id, 0, 0, 0, 0, 0) - end - end - obj.object = nil - obj_entity.batch_key = nil - obj_entity.batch_id = nil -end -update_image = function(obj) - if obj.path then - remove_batch_image(obj) - obj.object = Cache.get('Image', Game.res('image',obj.path), function(key) - return love.graphics.newImage(key) - end) - use_size(obj, obj.object) - else - remove_image(obj) - end -end -remove_image = function(obj) - obj.object = nil - use_size(obj) -end - -Component('image', { batch=true, use_size=true } ) - -System{ - component='image', - requires=EcsUtil.require_draw_components(), - add = function(obj) - local obj_entity = get_entity(obj) - track(obj, 'path') - track(obj, 'batch') - track(obj_entity, 'z') - - if obj.path then - if obj.batch then - update_batch_image(obj) - else - update_image(obj) - end - end - end, - update = function(obj, dt) - local obj_entity = get_entity(obj) - if changed(obj, 'path') or (obj.batch and changed(obj_entity, 'z')) then - if not obj.path then - remove_image(obj) - remove_batch_image(obj) - elseif obj.batch then - update_batch_image(obj) - else - update_image(obj) - end - end - if obj_entity.batch_key then - local batch = batches[obj_entity.batch_key] - local comps = {get_draw_components(obj_entity)} - -- print(batch.object:getCount(), obj_entity.batch_key, '<-', obj_entity.batch_id) - batch.object:set(obj_entity.batch_id, get_draw_components(obj_entity)) - end - end, - draw = function(obj) - if not obj then return true end - if obj.object then - draw_object(obj, get_entity(obj)) - end - end -} - -ImageBatch = Spawner("blanke.imagebatch") - -System{ - type='blanke.imagebatch', - add = function(obj) - extract_draw_components(obj) - end, - draw = function(obj) - draw_object(obj) - end -} - --- config functions -Image = { - animation = function() - - end -} \ No newline at end of file diff --git a/love2d/lua/ecs/systems/init.lua b/love2d/lua/ecs/systems/init.lua deleted file mode 100644 index 2b68edae..00000000 --- a/love2d/lua/ecs/systems/init.lua +++ /dev/null @@ -1,111 +0,0 @@ -local _NAME = ... - -Component{ - -- basic components - pos = { x = 0, y = 0 }, - - -- drawing components - quad = { }, - size = { width = 1, height = 1 }, - angle = { 0 }, - scale = { x = 1, y = 1 }, - offset = { x = 0, y = 0 }, - shear = { x = 0, y = 0 }, - blendmode = { 'alpha' } -} - -EcsUtil = { - require_draw_components = function() - return {'pos','quad','angle','size','scale','offset','shear','blendmode'} - end, - extract_draw_components = function(obj, override) - override = override or {} - local comps = {'pos','quad','angle','size','scale','offset','shear','blendmode'} - for _, comp_name in ipairs(comps) do - extract(obj, comp_name, override[comp_name]) - end - end, - get_draw_components = function(obj) - if obj.quad and obj.quad.x ~= nil then - return obj.quad or nil, - floor(obj.pos.x), floor(obj.pos.y), - Math.rad(obj.angle[1]), - obj.scale.x, obj.scale.y, - floor(obj.offset.x), floor(obj.offset.y), - obj.shear.x, obj.shear.y - else - return floor(obj.pos.x), floor(obj.pos.y), - Math.rad(obj.angle[1]), - obj.scale.x, obj.scale.y, - floor(obj.offset.x), floor(obj.offset.y), - obj.shear.x, obj.shear.y - end - end -} - ---CANVAS -require(_NAME..".canvas") -require(_NAME..".image") -require(_NAME..".movement") -require(_NAME..".effect") - -require(_NAME..".animation") -require(_NAME..".timer") --- camera --- audio? --- map --- physics --- hitbox --- net - - -local calc_align = function(obj) - local align = obj[1] - local ax, ay = 0, 0 - if align then - obj_entity = get_entity(obj) - local size = obj_entity.size - - if string.contains(align, 'center') then - ax = size.width/2 - ay = size.height/2 - end - if string.contains(align,'left') then - ax = 0 - end - if string.contains(align, 'right') then - ax = size.width - end - if string.contains(align, 'top') then - ay = 0 - end - if string.contains(align, 'bottom') then - ay = size.height - end - obj_entity.offset.x = floor(ax) - obj_entity.offset.y = floor(ay) - end -end - -Component('align', { 'top left' }) - -System{ - component='align', - requires={'size','offset'}, - add = function(obj) - calc_align(obj) - local obj_entity = get_entity(obj) - track(obj_entity.align, 1) - track(obj_entity.size, 'width') - track(obj_entity.size, 'height') - end, - update = function(obj, dt) - local obj_entity = get_entity(obj) - if changed(obj_entity.align, 1) or - changed(obj_entity.size, 'width') or - changed(obj_entity.size, 'height') then - --print("changed",obj_entity.type,obj_entity.align[1],obj_entity.size.width,obj_entity.size.height) - calc_align(obj) - end - end -} \ No newline at end of file diff --git a/love2d/lua/ecs/systems/movement.lua b/love2d/lua/ecs/systems/movement.lua deleted file mode 100644 index c5496a96..00000000 --- a/love2d/lua/ecs/systems/movement.lua +++ /dev/null @@ -1,25 +0,0 @@ -Component('vel', { x=0, y=0 }) -Component('gravity', { v=0, direction=90 }) - -System{ - component='vel', - requires={'pos'}, - update = function(obj, dt) - local obj_entity = get_entity(obj) - if obj.x ~= 0 then obj_entity.pos.x = obj_entity.pos.x + obj.x * dt end - if obj.y ~= 0 then obj_entity.pos.y = obj_entity.pos.y + obj.y * dt end - end -} - -System{ - component='gravity', - requires={'vel'}, - update=function(obj, dt) - local obj_entity = get_entity(obj) - if obj.v ~= 0 then - local gravx, gravy = Math.getXY(obj.direction, obj.v) - obj_entity.vel.x = obj_entity.vel.x + gravx - obj_entity.vel.y = obj_entity.vel.y + gravy - end - end -} \ No newline at end of file diff --git a/love2d/lua/ecs/systems/timer.lua b/love2d/lua/ecs/systems/timer.lua deleted file mode 100644 index 6bbc3d51..00000000 --- a/love2d/lua/ecs/systems/timer.lua +++ /dev/null @@ -1,64 +0,0 @@ ---TIMER -Timer = nil -do - local l_after = {} - local l_every = {} - local addTimer = function(t, fn, tbl) - local id = uuid() - local timer = { - fn = fn, - duration = t, - t = t, - iteration = 1, - paused = false, - destroy = function() - tbl[id] = nil - end - } - tbl[id] = timer - return timer - end - - Timer = { - update = function(dt) - -- after - for id,timer in pairs(l_after) do - if not timer.paused then - timer.t = timer.t - dt - if timer.t < 0 then - if timer.fn and timer.fn(timer) then - -- another one (restart timer) - timer.t = timer.duration - timer.iteration = timer.iteration + 1 - else - -- destroy it - timer.destroy() - end - end - end - end - -- every - for id,timer in pairs(l_every) do - if not timer.paused then - timer.t = timer.t - dt - if timer.t < 0 then - if not timer.fn or timer.fn(timer) then - -- destroy it! - timer.destroy() - else - -- restart timer - timer.t = timer.duration - timer.iteration = timer.iteration + 1 - end - end - end - end - end, - after = function(t, fn) - addTimer(t, fn, l_after) - end, - every = function(t, fn) - addTimer(t, fn, l_every) - end - } -end \ No newline at end of file diff --git a/love2d/lua/ecs/util.lua b/love2d/lua/ecs/util.lua index e03c80e3..e7676bd9 100644 --- a/love2d/lua/ecs/util.lua +++ b/love2d/lua/ecs/util.lua @@ -1,1216 +1,604 @@ -math.randomseed(os.time()) - -local bitop = require 'bitop' -local bit = bitop.bit -local bump = require "bump" -uuid = require "uuid" -json = require "json" -class = require "clasp" -callable = function(t) - if t.__ then - for _, mm in ipairs(t) do t['__'..mm] = t.__[mm] end +local tbl_to_str +tbl_to_str = function(t, str) + local empty = true + str = str or '' + str = str .. "[" + for i = 1, #t, 1 do + if i ~= 1 then + str = str .. ',' + end + if type(t[i]) == "table" then + str = str .. tbl_to_str(t[i], str) + else + str = str .. tostring(t[i]) + end end - return setmetatable(t, { __call = t.__call }) + str = str .. "]" + return str end -require "print_r" --- yes, plugins folder is listed twice -love.filesystem.setRequirePath('?.lua;?/init.lua;lua/?/init.lua;lua/?.lua;plugins/?/init.lua;plugins/?.lua;./plugins/?/init.lua;./plugins/?.lua') --- is given version greater than or equal to current LoVE version? -local ge_version = function(major, minor, rev) - if major and major > Game.love_version[1] then return false end - if minor and minor > Game.love_version[2] then return false end - if rev and rev > Game.love_version[3] then return false end - return true +callable = function(t) + if t.__ then + for _, mm in ipairs(t) do t['__'..mm] = t.__[mm] end + end + return setmetatable(t, { __call = t.__call }) end ---UTIL.table --- recursive table update -table.update = function (old_t, new_t) - local update_more - update_more = function(ot, nt) - for k, v in pairs(nt) do - if type(v) == 'table' and type(ot[k]) == 'table' then - ot[k] = update_more(ot[k], v) - else - ot[k] = v - end - end - return ot - end - if type(new_t) ~= 'table' then - return new_t - end - return update_more(old_t, new_t) -end +-- blanke_require("ecs") -table.keys = function (t) - ret = {} - for k, v in pairs(t) do table.insert(ret,k) end - return ret -end -table.every = function (t) - for k,v in pairs(t) do if not v then return false end end - return true -end -table.some = function (t) - for k,v in pairs(t) do if v then return true end end - return false -end -table.len = function (t) - c = 0 - for k,v in pairs(t) do c = c + 1 end - return c -end -table.hasValue = function (t, val) - for k,v in pairs(t) do - if v == val then return true end - end - return false -end -table.slice = function (t, start, finish) - i, res, finish = 1, {}, finish or table.len(t) - for j = start, finish do - res[i] = t[j] - i = i + 1 - end - return res -end -table.defaults = function (t,defaults) - for k,v in pairs(defaults) do - if t[k] == nil then t[k] = v - elseif type(v) == 'table' then table.defaults(t[k],defaults[k]) end - end -end -table.append = function (t, new_t) - for k,v in pairs(new_t) do - if type(k) == 'string' then t[k] = v - else table.insert(t, v) end - end +memoize = nil +do + local mem_cache = {} + setmetatable(mem_cache, {__mode = "kv"}) + memoize = function(f, cache) + -- default cache or user-given cache? + cache = cache or mem_cache + if not cache[f] then + cache[f] = {} + end + cache = cache[f] + return function(...) + local args = {...} + local cache_str = '' + local found_args = false + for i, v in ipairs(args) do + if v ~= nil then + if not found_args then + found_args = true + cache_str = '' + end + + if i ~= 1 then + cache_str = cache_str .. '~' + end + if type(v) == "table" then + cache_str = cache_str .. tbl_to_str(v) + else + cache_str = cache_str .. tostring(v) + end + end + end + -- retrieve cached value? + local ret = cache[cache_str] + if not ret then + -- not cached yet + ret = { f(unpack(args)) } + cache[cache_str] = ret + -- print('store',cache_str,'as',unpack(ret)) + end + return unpack(ret) + end + end +end + +-- is given version greater than or equal to current LoVE version? +ge_version = function(major, minor, rev) + if major and major > Game.love_version[1] then return false end + if minor and minor > Game.love_version[2] then return false end + if rev and rev > Game.love_version[3] then return false end + return true +end + +--TABLE +table.update = function (old_t, new_t, keys) + if keys == nil then + for k, v in pairs(new_t) do + old_t[k] = v + end + else + for _,k in ipairs(keys) do if new_t[k] ~= nil then old_t[k] = new_t[k] end end + end + return old_t +end +table.keys = function (t) + ret = {} + for k, v in pairs(t) do table.insert(ret,k) end + return ret +end +table.every = function (t, fn) + for k,v in pairs(t) do if fn ~= nil and not fn(v, k) or not v then return false end end + return true +end +table.some = function (t, fn) + for k,v in pairs(t) do if fn ~= nil and fn(v, k) or v then return true end end + return false +end +table.len = function (t) + c = 0 + for k,v in pairs(t) do c = c + 1 end + return c +end +table.hasValue = function (t, val) + for k,v in pairs(t) do + if v == val then return true end + end + return false +end +table.slice = function (t, start, finish) + i, res, finish = 1, {}, finish or table.len(t) + for j = start, finish do + res[i] = t[j] + i = i + 1 + end + return res +end +table.defaults = function (t,defaults) + for k,v in pairs(defaults) do + if t[k] == nil then t[k] = v + elseif type(v) == 'table' then table.defaults(t[k],defaults[k]) end + end + return t +end +table.append = function (t, new_t) + for k,v in pairs(new_t) do + if type(k) == 'string' then t[k] = v + else table.insert(t, v) end + end end table.filter = function(t, fn) - local len = table.len(t) - local offset = 0 - for o = 1, len do - local element = t[o] - if element then - if fn(element, o) then -- keep element - t[o] = nil - t[o - offset] = element - else -- remove element - t[o] = nil - offset = offset + 1 - end - end - end + local len = table.len(t) + local offset = 0 + for o = 1, len do + local element = t[o] + if element then + if fn(element, o) then -- keep element + t[o] = nil + t[o - offset] = element + else -- remove element + t[o] = nil + offset = offset + 1 + end + end + end end table.random = function(t) - return t[Math.random(1,#t)] + return t[Math.random(1,#t)] +end +table.randomWeighted = function(t) + local r = Math.random(0,100) end table.includes = function(t, v) - for i = 1,#t do if t[i] == v then return true end end - return false + for i = 1,#t do if t[i] == v then return true end end + return false end -table.join = function(t, sep) - local str = '' - for i = 1, #t do - str = str .. tostring(t[i]) - if i ~= #t then - str = str .. tostring(sep) - end - end - return str +table.join = function(t, sep, nil_str) + local str = '' + for i = 1, #t do + str = str .. tostring(t[i] ~= nil and t[i] or (nil_str and 'nil')) + if i ~= #t then + str = str .. tostring(sep) + end + end + return str +end +--STRING +function string:starts(start) + return string.sub(self,1,string.len(start))==start end ---UTIL.string -function string:contains(q) - return string.match(tostring(self), tostring(q)) ~= nil +function string:contains(q) + return string.match(tostring(self), tostring(q)) ~= nil end -function string:capitalize() - return string.upper(string.sub(self,1,1))..string.sub(self,2) +function string:count(str) + local _, count = string.gsub(self, str, "") + return count +end +function string:capitalize() + return string.upper(string.sub(self,1,1))..string.sub(self,2) end function string:split(sep) - local sep, fields = sep or ":", {} - local pattern = string.format("([^%s]+)", sep) - self:gsub(pattern, function(c) fields[#fields+1] = c end) - return fields + local sep, fields = sep or ":", {} + local pattern = string.format("([^%s]+)", sep) + self:gsub(pattern, function(c) fields[#fields+1] = c end) + return fields end function string:replace(find, replace, wholeword) - if wholeword then - find = '%f[%a]'..find..'%f[%A]' - end - return (self:gsub(find,replace)) -end + if wholeword then + find = '%f[%a]'..find..'%f[%A]' + end + return (self:gsub(find,replace)) +end +string.expand = memoize(function(self, ...) + -- version: 0.0.1 + -- code: Ketmar // Avalon Group + -- public domain + + -- expand $var and ${var} in string + -- ${var} can call Lua functions: ${string.rep(' ', 10)} + -- `$' can be screened with `\' + -- `...': args for $ + -- if `...' is just a one table -- take it as args + function ExpandVars (s, ...) + local args = {...}; + args = #args == 1 and type(args[1]) == "table" and args[1] or args; + -- return true if there was an expansion + local function DoExpand (iscode) + local was = false; + local mask = iscode and "()%$(%b{})" or "()%$([%a%d_]*)"; + local drepl = iscode and "\\$" or "\\\\$"; + s = s:gsub(mask, function (pos, code) + if s:sub(pos-1, pos-1) == "\\" then return "$"..code; + else was = true; local v, err; + if iscode then code = code:sub(2, -2); + else local n = tonumber(code); + if n then v = args[n]; end; + end; + if not v then + v, err = loadstring("return "..code); if not v then error(err); end; + v = v(); + end; + if v == nil then v = ""; end; + v = tostring(v):gsub("%$", drepl); + return v; + end; + end); + if not (iscode or was) then s = s:gsub("\\%$", "$"); end; + return was; + end; + + repeat DoExpand(true); until not DoExpand(false); + return s; + end; + return ExpandVars(self, ...) +end) --math -local sin, cos, rad, deg, abs = math.sin, math.cos, math.rad, math.deg, math.abs -floor = function(x) return math.floor(x+0.5) end +local sin, cos, rad, deg, abs, min, max = math.sin, math.cos, math.rad, math.deg, math.abs, math.min, math.max +local floor = function(x) return math.floor(x+0.5) end Math = {} do - for name, fn in pairs(math) do Math[name] = function(...) return fn(...) end end - - Math.sign = function(x) return (x < 0) and -1 or 1 end - Math.seed = function(l,h) if l then love.math.setRandomSeed(l,h) else return love.math.getRandomSeed() end end - Math.random = function(...) return love.math.random(...) end - Math.indexTo2d = function(i, col) return math.floor((i-1)%col)+1, math.floor((i-1)/col)+1 end - Math.getXY = function(angle, dist) return dist * cos(rad(angle)), dist * sin(rad(angle)) end - Math.distance = function(x1,y1,x2,y2) return math.sqrt( (x2-x1)^2 + (y2-y1)^2 ) end - Math.lerp = function(a,b,t) return a * (1-t) + b * t end - Math.prel = function(a,b,v) -- returns what percent v is between a and b - if v >= b then return 1 - elseif v <= a then return 0 - else return (v - a) / (b - a) end - end - Math.sinusoidal = function(min, max, spd, percent) return Math.lerp(min, max, Math.prel(-1, 1, math.cos(Math.lerp(0,math.pi/2,percent or 0) + (Game.time * spd))) ) end - -- return min + -math.cos(Math.lerp(0,math.pi/2,off or 0) + (Game.time * spd)) * ((max - min)/2) + ((max - min)/2) end - Math.angle = function(x1, y1, x2, y2) return math.deg(math.atan2((y2-y1), (x2-x1))) end - Math.pointInShape = function(shape, x, y) - local pts = {} - for p = 1,#shape,2 do - table.insert(pts, {x=shape[p], y=shape[p+1]}) - end - return PointWithinShape(pts,x,y) - end - - function PointWithinShape(shape, tx, ty) - if #shape == 0 then - return false - elseif #shape == 1 then - return shape[1].x == tx and shape[1].y == ty - elseif #shape == 2 then - return PointWithinLine(shape, tx, ty) - else - return CrossingsMultiplyTest(shape, tx, ty) - end - end - - function BoundingBox(box, tx, ty) - return (box[2].x >= tx and box[2].y >= ty) - and (box[1].x <= tx and box[1].y <= ty) - or (box[1].x >= tx and box[2].y >= ty) - and (box[2].x <= tx and box[1].y <= ty) - end - - function colinear(line, x, y, e) - e = e or 0.1 - m = (line[2].y - line[1].y) / (line[2].x - line[1].x) - local function f(x) return line[1].y + m*(x - line[1].x) end - return math.abs(y - f(x)) <= e - end - - function PointWithinLine(line, tx, ty, e) - e = e or 0.66 - if BoundingBox(line, tx, ty) then - return colinear(line, tx, ty, e) - else - return false - end - end - - -- from http://erich.realtimerendering.com/ptinpoly/ - function CrossingsMultiplyTest(pgon, tx, ty) - local i, yflag0, yflag1, inside_flag - local vtx0, vtx1 - - local numverts = #pgon - - vtx0 = pgon[numverts] - vtx1 = pgon[1] - - -- get test bit for above/below X axis - yflag0 = ( vtx0.y >= ty ) - inside_flag = false - - for i=2,numverts+1 do - yflag1 = ( vtx1.y >= ty ) - - --[[ Check if endpoints straddle (are on opposite sides) of X axis - * (i.e. the Y's differ); if so, +X ray could intersect this edge. - * The old test also checked whether the endpoints are both to the - * right or to the left of the test point. However, given the faster - * intersection point computation used below, this test was found to - * be a break-even proposition for most polygons and a loser for - * triangles (where 50% or more of the edges which survive this test - * will cross quadrants and so have to have the X intersection computed - * anyway). I credit Joseph Samosky with inspiring me to try dropping - * the "both left or both right" part of my code. - --]] - if ( yflag0 ~= yflag1 ) then - --[[ Check intersection of pgon segment with +X ray. - * Note if >= point's X; if so, the ray hits it. - * The division operation is avoided for the ">=" test by checking - * the sign of the first vertex wrto the test point; idea inspired - * by Joseph Samosky's and Mark Haigh-Hutchinson's different - * polygon inclusion tests. - --]] - if ( ((vtx1.y - ty) * (vtx0.x - vtx1.x) >= (vtx1.x - tx) * (vtx0.y - vtx1.y)) == yflag1 ) then - inside_flag = not inside_flag - end - end - - -- Move to the next pair of vertices, retaining info as possible. - yflag0 = yflag1 - vtx0 = vtx1 - vtx1 = pgon[i] - end - - return inside_flag - end - - function GetIntersect( points ) - local g1 = points[1].x - local h1 = points[1].y - - local g2 = points[2].x - local h2 = points[2].y - - local i1 = points[3].x - local j1 = points[3].y - - local i2 = points[4].x - local j2 = points[4].y - - local xk = 0 - local yk = 0 - - if checkIntersect({x=g1, y=h1}, {x=g2, y=h2}, {x=i1, y=j1}, {x=i2, y=j2}) then - local a = h2-h1 - local b = (g2-g1) - local v = ((h2-h1)*g1) - ((g2-g1)*h1) - - local d = i2-i1 - local c = (j2-j1) - local w = ((j2-j1)*i1) - ((i2-i1)*j1) - - xk = (1/((a*d)-(b*c))) * ((d*v)-(b*w)) - yk = (-1/((a*d)-(b*c))) * ((a*w)-(c*v)) - end - return xk, yk - end + for name, fn in pairs(math) do Math[name] = function(...) return fn(...) end end + + local clamp = function(x, _min, _max) return min(_max, max(_min, x)) end + + Math.clamp = clamp + Math.sign = function(x) return (x < 0) and -1 or 1 end + Math.seed = function(l,h) if l then love.math.setRandomSeed(l,h) else return love.math.getRandomSeed() end end + Math.random = function(...) return love.math.random(...) end + Math.indexTo2d = function(i, col) return math.floor((i-1)%col)+1, math.floor((i-1)/col)+1 end + Math.getXY = memoize(function(angle, dist) return dist * cos(angle), dist * sin(angle) end) + Math.distance = memoize(function(x1,y1,x2,y2) return math.sqrt( (x2-x1)^2 + (y2-y1)^2 ) end) + Math.lerp = function(a,b,t) + local r = a * (1-t) + b * t + if a < b then return clamp(r, a, b) + else return clamp(r, b, a) end + end + Math.prel = function(a,b,v) -- returns what percent v is between a and b + if v >= b then return 1 + elseif v <= a then return 0 + else return (v - a) / (b - a) end + end + Math.sinusoidal = function(min, max, spd, percent) return Math.lerp(min, max, Math.prel(-1, 1, math.cos(Math.lerp(0,math.pi/2,percent or 0) + (Game.time * (spd or 1)) )) ) end + -- return min + -math.cos(Math.lerp(0,math.pi/2,off or 0) + (Game.time * spd)) * ((max - min)/2) + ((max - min)/2) end + Math.angle = memoize(function(x1, y1, x2, y2) return math.atan2((y2-y1), (x2-x1)) end) + Math.pointInShape = function(shape, x, y) + local pts = {} + for p = 1,#shape,2 do + table.insert(pts, {x=shape[p], y=shape[p+1]}) + end + return PointWithinShape(pts,x,y) + end + + function PointWithinShape(shape, tx, ty) + if #shape == 0 then + return false + elseif #shape == 1 then + return shape[1].x == tx and shape[1].y == ty + elseif #shape == 2 then + return PointWithinLine(shape, tx, ty) + else + return CrossingsMultiplyTest(shape, tx, ty) + end + end + + function BoundingBox(box, tx, ty) + return (box[2].x >= tx and box[2].y >= ty) + and (box[1].x <= tx and box[1].y <= ty) + or (box[1].x >= tx and box[2].y >= ty) + and (box[2].x <= tx and box[1].y <= ty) + end + + function colinear(line, x, y, e) + e = e or 0.1 + m = (line[2].y - line[1].y) / (line[2].x - line[1].x) + local function f(x) return line[1].y + m*(x - line[1].x) end + return math.abs(y - f(x)) <= e + end + + function PointWithinLine(line, tx, ty, e) + e = e or 0.66 + if BoundingBox(line, tx, ty) then + return colinear(line, tx, ty, e) + else + return false + end + end + + -- from http://erich.realtimerendering.com/ptinpoly/ + function CrossingsMultiplyTest(pgon, tx, ty) + local i, yflag0, yflag1, inside_flag + local vtx0, vtx1 + + local numverts = #pgon + + vtx0 = pgon[numverts] + vtx1 = pgon[1] + + -- get test bit for above/below X axis + yflag0 = ( vtx0.y >= ty ) + inside_flag = false + + for i=2,numverts+1 do + yflag1 = ( vtx1.y >= ty ) + + --[[ Check if endpoints straddle (are on opposite sides) of X axis + * (i.e. the Y's differ); if so, +X ray could intersect this edge. + * The old test also checked whether the endpoints are both to the + * right or to the left of the test point. However, given the faster + * intersection point computation used below, this test was found to + * be a break-even proposition for most polygons and a loser for + * triangles (where 50% or more of the edges which survive this test + * will cross quadrants and so have to have the X intersection computed + * anyway). I credit Joseph Samosky with inspiring me to try dropping + * the "both left or both right" part of my code. + --]] + if ( yflag0 ~= yflag1 ) then + --[[ Check intersection of pgon segment with +X ray. + * Note if >= point's X; if so, the ray hits it. + * The division operation is avoided for the ">=" test by checking + * the sign of the first vertex wrto the test point; idea inspired + * by Joseph Samosky's and Mark Haigh-Hutchinson's different + * polygon inclusion tests. + --]] + if ( ((vtx1.y - ty) * (vtx0.x - vtx1.x) >= (vtx1.x - tx) * (vtx0.y - vtx1.y)) == yflag1 ) then + inside_flag = not inside_flag + end + end + + -- Move to the next pair of vertices, retaining info as possible. + yflag0 = yflag1 + vtx0 = vtx1 + vtx1 = pgon[i] + end + + return inside_flag + end + + function GetIntersect( points ) + local g1 = points[1].x + local h1 = points[1].y + + local g2 = points[2].x + local h2 = points[2].y + + local i1 = points[3].x + local j1 = points[3].y + + local i2 = points[4].x + local j2 = points[4].y + + local xk = 0 + local yk = 0 + + if checkIntersect({x=g1, y=h1}, {x=g2, y=h2}, {x=i1, y=j1}, {x=i2, y=j2}) then + local a = h2-h1 + local b = (g2-g1) + local v = ((h2-h1)*g1) - ((g2-g1)*h1) + + local d = i2-i1 + local c = (j2-j1) + local w = ((j2-j1)*i1) - ((i2-i1)*j1) + + xk = (1/((a*d)-(b*c))) * ((d*v)-(b*w)) + yk = (-1/((a*d)-(b*c))) * ((a*w)-(c*v)) + end + return xk, yk + end end --UTIL.extra switch = function(val, choices) - if choices[val] then choices[val]() - elseif choices.default then choices.default() end + if choices[val] then choices[val]() + elseif choices.default then choices.default() end end -- for sorting a table of objects -sort = function(t, key, default) - table.sort(t, function(a, b) - --[[ - if a == nil and b == nil then - return false - end - if a == nil then - return true - end - if b == nil then - return false - end]] - if a[key] == nil then a[key] = default end - if b[key] == nil then b[key] = default end - a['_last_'..key] = a[key] - b['_last_'..key] = b[key] - return a[key] < b[key] - end) -end - -copy_shallow = function(orig) - local orig_type = type(orig) - local copy - if orig_type == 'table' then - copy = {} - for orig_key, orig_value in pairs(orig) do - copy[orig_key] = orig_value - end - else -- number, string, boolean, etc - copy = orig - end - return copy +sort = nil +do + sort = function(t, key, default) + if #t == 0 then return end + table.sort(t, function(a, b) + if a == nil and b == nil then + return false + end + if a == nil then + return true + end + if b == nil then + return false + end + if a[key] == nil then a[key] = default end + if b[key] == nil then b[key] = default end + return a[key] < b[key] + end) + end +end + +iterate = function(t, fn) + if not t then return end + local len = #t + local offset = 0 + local removals = {} + for o=1,len do + local obj = t[o] + if obj then + if fn(obj, o) == true then + table.insert(removals, o) + end + end + end + if #removals > 0 then + for i = #removals, 1, -1 do + table.remove(t, removals[i]) + end + end +end + +local nonzero_z = false +local iterateEntities = function(t, test_val, fn) + if not t then return end + local len = #t + local offset = 0 + local removals = {} + for o=1,len do + local obj = t[o] + if obj then + if obj.destroyed or not obj[test_val] or fn(obj, o) == true then + table.insert(removals, o) + elseif obj._last_z ~= obj.z then + obj._last_z = obj.z + reorder = true + Game.sortDrawables() + end + end + end + if #removals > 0 then + for i = #removals, 1, -1 do + table.remove(t, removals[i]) + end + end + + return reorder end copy = function(orig, copies) - copies = copies or {} - local orig_type = type(orig) - local t_copy - if orig_type == 'table' then - if copies[orig] then - t_copy = copies[orig] - else - t_copy = {} - copies[orig] = t_copy - for orig_key, orig_value in next, orig, nil do - t_copy[copy(orig_key, copies)] = copy(orig_value, copies) - end - setmetatable(t_copy, copy(getmetatable(orig), copies)) - end - else -- number, string, boolean, etc - t_copy = orig - end - return t_copy + copies = copies or {} + local orig_type = type(orig) + local t_copy + if orig_type == 'table' then + if copies[orig] then + t_copy = copies[orig] + else + t_copy = {} + copies[orig] = t_copy + for orig_key, orig_value in next, orig, nil do + t_copy[copy(orig_key, copies)] = copy(orig_value, copies) + end + setmetatable(t_copy, copy(getmetatable(orig), copies)) + end + else -- number, string, boolean, etc + t_copy = orig + end + return t_copy end is_object = function(o) return type(o) == 'table' and o.init and type(o.init) == 'function' end encrypt = function(str, code, seed) - local oldseed = {Math.seed()} - if not seed then - seed = 0 - for c = 1, string.len(code) do - seed = seed + string.byte(string.sub(code,c,c)) - end - end - Math.seed(seed) - local ret_str = '' - local code_len = string.len(code) - for c = 1, string.len(str) do - ret_str = ret_str .. string.char(bit.bxor(string.byte(string.sub(str,c,c)), (c + Math.random(c,code_len)) % code_len)) - end - Math.seed(unpack(oldseed)) - return ret_str + local oldseed = {Math.seed()} + if not seed then + seed = 0 + for c = 1, string.len(code) do + seed = seed + string.byte(string.sub(code,c,c)) + end + end + Math.seed(seed) + local ret_str = '' + local code_len = string.len(code) + for c = 1, string.len(str) do + ret_str = ret_str .. string.char(bit.bxor(string.byte(string.sub(str,c,c)), (c + Math.random(c,code_len)) % code_len)) + end + Math.seed(unpack(oldseed)) + return ret_str end decrypt = function(str, code, seed) - local oldseed = {Math.seed()} - if not seed then - seed = 0 - for c = 1, string.len(code) do - seed = seed + string.byte(string.sub(code,c,c)) - end - end - Math.seed(seed) - local ret_str = '' - local code_len = string.len(code) - for c = 1, string.len(str) do - ret_str = ret_str .. string.char(bit.bxor(string.byte(string.sub(str,c,c)), (c + Math.random(c,code_len)) % code_len)) - end - Math.seed(unpack(oldseed)) - return ret_str -end -lua_print = print + local oldseed = {Math.seed()} + if not seed then + seed = 0 + for c = 1, string.len(code) do + seed = seed + string.byte(string.sub(code,c,c)) + end + end + Math.seed(seed) + local ret_str = '' + local code_len = string.len(code) + for c = 1, string.len(str) do + ret_str = ret_str .. string.char(bit.bxor(string.byte(string.sub(str,c,c)), (c + Math.random(c,code_len)) % code_len)) + end + Math.seed(unpack(oldseed)) + return ret_str +end +local lua_print = print do - local str = '' - local args - print = function(...) - str = '' - args = {...} - len = table.len(args) - for i = 1,len do - str = str .. tostring(args[i] or 'nil') - if i ~= len then str = str .. ' ' end - end - lua_print(str) - end -end - -local entity_track = {} -- { table_ref={ var=last_val } } -local tracks_changed = {} -- { table_ref={ var=new_value } } - --- call track(obj, 'myvar') after it's been set ---@global ---local i = 0 -track = function(obj, comp_name) - assert(obj, "track(): Object is nil") - if not entity_track[obj] then entity_track[obj] = {} end - entity_track[obj][comp_name] = obj[comp_name] - --i = i + 1 - --print(i) -end - --- changed(obj, 'myvar') will return true/false if myvar has changed since the last track()/changed() ---@global -changed = function(obj, comp_name) - local last_vars = entity_track[obj] - if last_vars then - if last_vars[comp_name] ~= obj[comp_name] then - if not tracks_changed[obj] then - table.insert(tracks_changed, obj) - tracks_changed[obj] = {} - end - tracks_changed[obj][comp_name] = obj[comp_name] - last_vars[comp_name] = obj[comp_name] - return true - end - end - return false -end - --- call at the end of update -reset_tracks = function() - local ref - for i=1,#tracks_changed do - ref = tracks_changed[i] - entity_track[ref] = tracks_changed[ref] - end - tracks_changed = {} + local str = '' + local args + print = function(...) + str = '' + args = {...} + len = table.len(args) + for i = 1,len do + str = str .. tostring(args[i] or 'nil') + if i ~= len then str = str .. ' ' end + end + lua_print(str) + end end --CACHE --- @global Cache = {} -do - local storage = {} - Cache.group = function(name) return Cache[name] end - Cache.key = function(group_name, key) return (Cache[group_name] and Cache[group_name][key]) end - Cache.get = function(group_name, key, fn_not_found) - if not storage[group_name] then storage[group_name] = {} end - if storage[group_name][key] then - return storage[group_name][key] - elseif fn_not_found then - storage[group_name][key] = fn_not_found(key) - return storage[group_name][key] - end - end - Cache.stats = function() - local str = '' - for name, list in pairs(storage) do - str = str .. name .. '=' .. table.len(list) .. ' ' - end - return str - end +do + local storage = {} + Cache.group = function(name) return Cache[name] end + Cache.key = function(group_name, key) return (Cache[group_name] and Cache[group_name][key]) end + Cache.get = function(group_name, key, fn_not_found) + if not storage[group_name] then storage[group_name] = {} end + if storage[group_name][key] then + return storage[group_name][key] + elseif fn_not_found then + storage[group_name][key] = fn_not_found(key) + return storage[group_name][key] + end + end + Cache.stats = function() + local str = '' + for name, list in pairs(storage) do + str = str .. name .. '=' .. table.len(list) .. ' ' + end + print(str) + end end --STACK --- @global Stack = class{ - init = function(self, fn_new) - self.stack = {} -- { { used:t/f, value:?, is_stack:true, release:fn() } } - self.fn_new = fn_new - end, - new = function(self, obj, remake) - local found = false - for _, s in ipairs(self.stack) do - if not s.used then - found = true - s.used = true - if remake then - s.value = self.fn_new(obj) - end - return s - end - end - if not found then - local new_uuid = uuid() - local new_stack_obj = { - uuid=new_uuid, - used=true, - value=self.fn_new(obj), - is_stack=true - } - table.insert(self.stack, new_stack_obj) - return new_stack_obj - end - end, - release = function(self, object) - for _, s in ipairs(self.stack) do - if s.uuid == object.uuid then - s.used = false - return - end - end - end -} - ---FEATURE -Feature = {} -do - local disabled = {} - Feature = callable { - -- returns true if feature is enabled - __call = function(name) - return not disabled[name] - end, - disable = function(...) - local flist = {...} - for _, f in ipairs(flist) do - disabled[f] = true - end - end, - enable = function(...) - local flist = {...} - for _, f in ipairs(flist) do - disabled[f] = false - end - end - } -end - ---CACHE -Cache = {} -do - local storage = {} - Cache.group = function(name) return Cache[name] end - Cache.key = function(group_name, key) return (Cache[group_name] and Cache[group_name][key]) end - Cache.get = function(group_name, key, fn_not_found) - if not storage[group_name] then storage[group_name] = {} end - if storage[group_name][key] then - return storage[group_name][key] - elseif fn_not_found then - storage[group_name][key] = fn_not_found(key) - return storage[group_name][key] - end - end - Cache.stats = function() - local str = '' - for name, list in pairs(storage) do - str = str .. name .. '=' .. table.len(list) .. ' ' - end - print(str) - end -end - ---FS -FS = { - basename = function (str) - return string.gsub(str, "(.*/)(.*)", "%2") - end, - dirname = function (str) - if string.match(str,".-/.-") then return string.gsub(str, "(.*/)(.*)", "%1") else return '' end - end, - extname = function (str) - str = string.match(str,"^.+(%..+)$") - if str then return string.sub(str,2) end - end, - removeExt = function (str) - return string.gsub(str, '.'..FS.extname(str), '') - end, - ls = function (path) - return love.filesystem.getDirectoryItems(path) - end -} - ---SIGNAL -Signal = nil -do - local fns = {} - Signal = { - emit = function(event, ...) - if fns[event] then - for _,fn in ipairs(fns[event]) do - fn(...) - end - end - end, - on = function(event, fn) - if not fns[event] then fns[event] = {} end - table.insert(fns[event], fn) - end - } -end - ---INPUT -Input = nil -mouse_x, mouse_y = 0, 0 -do - local name_to_input = {} -- name -> { key1: t/f, mouse1: t/f } - local input_to_name = {} -- key -> { name1, name2, ... } - local options = { - no_repeat = {}, - combo = {} - } - local pressed = {} - local released = {} - local store = {} - local key_assoc = { - lalt='alt', ralt='alt', - ['return']='enter', kpenter='enter', - lgui='gui', rgui='gui' - } - - local joycheck = function(info) - if not info or not info.joystick then return info end - if Joystick.using == 0 then return info end - if Joystick.get(Joystick.using):getID() == info.joystick:getID() then return info end - end - - Input = callable { - __call = function(self, name) - return store[name] or pressed[name] or released[name] - end; - - store = function(name, value) - store[name] = value - end; - - set = function(inputs, _options) - for name, inputs in pairs(inputs) do - Input.addInput(name, inputs) - end - if _options then - table.append(options.combo, _options.combo or {}) - table.append(options.no_repeat, _options.no_repeat or {}) - end - return nil - end; - - addInput = function(name, inputs) - name_to_input[name] = {} - for _,i in ipairs(inputs) do name_to_input[name][i] = false end - for _,i in ipairs(inputs) do - if not input_to_name[i] then input_to_name[i] = {} end - if not table.hasValue(input_to_name[i], name) then table.insert(input_to_name[i], name) end - end - end; - - pressed = function(name) - if not (table.hasValue(options.no_repeat, name) and pressed[name] and pressed[name].count > 1) and joycheck(pressed[name]) then - return pressed[name] - end - end; - - released = function(name) - if joycheck(released[name]) then - return released[name] - end - end; - - press = function(key, extra) - if key_assoc[key] then Input.press(key_assoc[key], extra) end - if input_to_name[key] then - for _,name in ipairs(input_to_name[key]) do - local n2i = name_to_input[name] - if not n2i then name_to_input[name] = {} end - n2i = name_to_input[name] - n2i[key] = true - -- is input pressed now? - combo = table.hasValue(options.combo, name) - if (combo and table.every(n2i)) or (not combo and table.some(n2i)) then - pressed[name] = extra - pressed[name].count = 1 - end - end - end - end; - - release = function(key, extra) - if key_assoc[key] then Input.release(key_assoc[key], extra) end - if input_to_name[key] then - for _,name in ipairs(input_to_name[key]) do - local n2i = name_to_input[name] - if not n2i then name_to_input[name] = {} end - n2i = name_to_input[name] - n2i[key] = false - -- is input released now? - combo = table.hasValue(options.combo, name) - if pressed[name] and (combo or not table.some(n2i)) then - pressed[name] = nil - released[name] = extra - end - end - end - end; - - keyCheck = function() - for name, info in pairs(pressed) do - info.count = info.count + 1 - end - released = {} - end; - - -- mousePos = function() return love.mouse.getPosition() end; - } -end - ---JOYSTICK -Joystick = nil -local refreshJoystickList -do - local joysticks = {} - refreshJoystickList = function() - joysticks = love.joystick.getJoysticks() - end - - Joystick = { - using = 0, - get = function(i) - if i > 0 and i < #joysticks then - return joysticks[i] - end - end, - -- affects all future Input() gamepad checks - use = function(i) - Joystick.using = i or 0 - end - } -end - ---DRAW -Draw = nil -do - local _hex_cache = {} - local _hex2rgb = function(hex) - assert(type(hex) == "string", "hex2rgb: expected string, got "..type(hex).." ("..hex..")") - hex = hex:gsub("#","") - if(string.len(hex) == 3) then - return {tonumber("0x"..hex:sub(1,1)) * 17 / 255, tonumber("0x"..hex:sub(2,2)) * 17 / 255, tonumber("0x"..hex:sub(3,3)) * 17 / 255} - elseif(string.len(hex) == 6) then - return {tonumber("0x"..hex:sub(1,2)) / 255, tonumber("0x"..hex:sub(3,4)) / 255, tonumber("0x"..hex:sub(5,6)) / 255} - end - end - - local fonts = {} -- { 'path+size': font } - local getFont = function(path, size) - size = size or 12 - local key = path..'+'..size - if fonts[key] then return fonts[key] end - local font = love.graphics.newFont(Game.res('font',path), size) - fonts[key] = font - return font - end - local DEF_FONT = "04B_03.ttf" - local last_font - Draw = class { - crop_used = false; - init = function(self, instructions) - for _,instr in ipairs(instructions) do - name, args = instr[1], table.slice(instr,2) - assert(Draw[name], "bad draw instruction '"..name.."'") - Draw[name](unpack(args)) - end - end; - setFont = function(path, size) - path = path or last_font or DEF_FONT - last_font = path - local font = getFont(path, size) - assert(font, 'Font not found: \''..path..'\'') - love.graphics.setFont(font) - end; - getFont = function() return love.graphics.getFont() end; - setFontSize = function(size) - Draw.setFont(last_font, size) - end; - addImageFont = function(path, glyphs, ...) - path = Game.res('image', path) - if fonts[path] then return fonts[path] end - local font = love.graphics.newImageFont(path, glphs, ...) - fonts[path] = font - return font - end; - setImageFont = function(path) - path = Game.res('image', path) - local font = fonts[path] - assert(font, "ImageFont not found: \'"..path.."\'") - love.graphics.setFont(font) - end; - print = function(txt,x,y,char_limit,align,...) - if not char_limit then - char_limit = Draw.getFont():getWidth(txt) - end - love.graphics.printf(txt,x,y,char_limit,align,...) - end; - parseColor = function(...) - args = {...} - if #args == 0 then return 1, 1, 1, 1 end - local c = Color[args[1]] - if c then - args = {c[1],c[2],c[3],args[2] or 1} - for a,arg in ipairs(args) do - if arg > 1 then args[a] = arg / 255 end - end - end - if #args == 0 then args = {1,1,1,1} end - if not args[4] then args[4] = 1 end - return args[1], args[2], args[3], args[4] - end; - color = function(...) - return love.graphics.setColor(Draw.parseColor(...)) - end; - hexToRgb = function(hex) - if _hex_cache[hex] then return _hex_cache[hex] end - local ret = _hex2rgb(hex) - _hex_cache[hex] = ret - return ret - end; - getBlendMode = function() return love.graphics.getBlendMode() end; - setBlendMode = function(...) love.graphics.setBlendMode(...) end; - crop = function(x,y,w,h) - love.graphics.setScissor(x,y,w,h) - -- stencilFn = () -> Draw.rect('fill',x,y,w,h) - -- love.graphics.stencil(stencilFn,"replace",1) - -- love.graphics.setStencilTest("greater",0) - -- Draw.crop_used = true - end; - rotate = function(r) - love.graphics.rotate(math.rad(r)) - end; - translate = function(x,y) - love.graphics.translate(floor(x), floor(y)) - end; - reset = function(only) - if only == 'color' or not only then - Draw.color(1,1,1,1) - Draw.lineWidth(1) - end - if only == 'transform' or not only then - Draw.origin() - end - if (only == 'crop' or not only) and Draw.crop_used then - Draw.crop_used = false - love.graphics.setStencilTest() - end - end; - push = function() love.graphics.push('all') end; - pop = function() - Draw.reset('crop') - love.graphics.pop() - end; - stack = function(fn) - Draw.push() - fn() - Draw.pop() - end; - newTransform = function() - return love.math.newTransform() - end; - clear = function(...) - love.graphics.clear(Draw.parseColor(...)) - end - } - - local draw_functions = { - 'arc','circle','ellipse','line','points','polygon','rectangle',--'print','printf', - 'discard','origin', - 'scale','shear','transformPoint', - 'setLineWidth','setPointSize', - 'applyTransform', 'replaceTransform' - } - local draw_aliases = { - polygon = 'poly', - rectangle = 'rect', - setLineWidth = 'lineWidth', - setPointSize = 'pointSize', - points = 'point', - setFont = 'font', - setFontSize = 'fontSize' - } - for _,fn in ipairs(draw_functions) do - Draw[fn] = function(...) return love.graphics[fn](...) end - end - for old, new in pairs(draw_aliases) do - Draw[new] = Draw[old] - end -end - -Color = { - red = {244,67,54}, - pink = {240,98,146}, - purple = {156,39,176}, - deeppurple = {103,58,183}, - indigo = {63,81,181}, - blue = {33,150,243}, - lightblue = {3,169,244}, - cyan = {0,188,212}, - teal = {0,150,136}, - green = {76,175,80}, - lightgreen = {139,195,74}, - lime = {205,220,57}, - yellow = {255,235,59}, - amber = {255,193,7}, - orange = {255,152,0}, - deeporange = {255,87,34}, - brown = {121,85,72}, - grey = {158,158,158}, - gray = {158,158,158}, - bluegray = {96,125,139}, - white = {255,255,255}, - white2 = {250,250,250}, - black = {0,0,0}, - black2 = {33,33,33} -} -Color.random = function(any, a) - if any then - return {Math.random(0,100)/100,Math.random(0,100)/100,Math.random(0,100)/100, a or Math.random(0,100)/100} - end - local rand = table.random(table.keys(Color)) - while type(Color[rand]) == 'function' do - rand = table.random(table.keys(Color)) - end - return {Color[rand][1], Color[rand][2], Color[rand][3], a or Math.random(0,100)/100} -end - ---AUDIO -Audio = nil -do - local default_opt = { - type = 'static' - } - local defaults = {} - local sources = {} - local new_sources = {} - local play_queue = {} - local first_update = true - - local opt = function(name, overrides) - if not defaults[name] then Audio(name, {}) end - return defaults[name] - end - Audio = class { - init = function(self, file, ...) - option_list = {...} - for _,options in ipairs(option_list) do - store_name = options.name or file - options.file = file - if not defaults[store_name] then defaults[store_name] = {} end - new_tbl = copy(default_opt) - table.update(new_tbl, options) - table.update(defaults[store_name], new_tbl) - end - end; - - update = function(dt) - if #play_queue > 0 then - for _, src in ipairs(play_queue) do - love.audio.play(src) - end - play_queue = {} - end - end; - - source = function(name, options) - local o = opt(name) - if options then o = table.update(o, options) end - - if Window.os == 'web' then o.type = 'static' end - - local src = Cache.get('Audio.source',name,function(key) - return love.audio.newSource(Game.res('audio',o.file), o.type) - end) - - if not sources[name] then - sources[name] = love.audio.newSource(Game.res('audio',o.file), o.type) - end - if not new_sources[name] then new_sources[name] = {} end - - if o then - table.insert(new_sources[name], src) - local props = {'looping','volume','airAbsorption','pitch','relative','rolloff'} - local t_props = {'position','attenuationDistances','cone','direction','velocity','filter','effect','volumeLimits'} - for _,n in ipairs(props) do - if o[n] then - src['set'..n:capitalize()](src,o[n]) - end - end - for _,n in ipairs(t_props) do - if o[n] then src['set'..n:capitalize()](src,unpack(o[n])) end - end - end - return src - end; - play = function(...) - local src_list = {} - for _,name in ipairs({...}) do - table.insert(src_list, Audio.source(name)) - -- adds to play_queue so audio doesn't play in Game.load (before the game actually starts) - table.insert(play_queue, Audio.source(name)) - end - if #src_list == 1 then return src_list[1] - else return src_list end - end; - stop = function(...) - names = {...} - if #names == 0 then love.audio.stop() - else - for _,n in ipairs(names) do - if new_sources[n] then - for _,src in ipairs(new_sources[n]) do love.audio.stop(src) end - end - end - end - end; - isPlaying = function(name) - if new_sources[name] then - local t = {} - for _,src in ipairs(new_sources[name]) do - if src:isPlaying() then return true end - end - end - return false - end; - } - - local audio_fns = {'volume','velocity','position','orientation','effect','dopp'} - for _, fn in ipairs(audio_fns) do - local fn_capital = fn:capitalize() - Audio[fn] = function(...) - local args = {...} - if #args > 0 then love.audio['set'..fn_capital](...) - else return love.audio['get'..fn_capital]() end - end - end -end - ---WINDOW -Window = {} -do - local pre_fs_size = {} - local last_win_size = {0,0} - local setMode = function(w,h,flags) - if not (last_win_size[1] == w and last_win_size[2] == h) then - love.window.setMode(w, h, flags or Game.options.window_flags) - last_win_size = {w, h} - end - end - Window = { - os = '?'; - aspect_ratio = nil; - aspect_ratios = { {4,3}, {5,4}, {16,10}, {16,9} }; - resolutions = { 512, 640, 800, 1024, 1280, 1366, 1920 }; - aspectRatio = function() - local w, h = love.window.getDesktopDimensions() - for _,ratio in ipairs(Window.aspect_ratios) do - if w * (ratio[2] / ratio[1]) == h then - Window.aspect_ratio = ratio - return ratio - end - end - end; - vsync = function(v) - if not ge_version(11,3) then return end - if not v then return love.window.getVSync() - else print('vsync',v) love.window.setVSync(v) end - end; - setSize = function(r, flags) - local w, h = Window.calculateSize(r) - setMode(w,h,flags) - end; - setExactSize = function(w, h, flags) - setMode(w,h,falgs) - end; - calculateSize = function(r) - r = r or Game.config.window_size - if not Window.aspect_ratio then Window.aspectRatio() end - local w = Window.resolutions[r] - local h = w / Window.aspect_ratio[1] * Window.aspect_ratio[2] - return w, h - end; - fullscreen = function(v,fs_type) - local res - if v == nil then - res = love.window.getFullscreen() - else - res = love.window.setFullscreen(v,fs_type) - end - Blanke.resize(unpack(pre_fs_size)) - return res - end; - toggleFullscreen = function() - if not Window.fullscreen() then - pre_fs_size = {Game.width, Game.height} - end - local res = Window.fullscreen(not Window.fullscreen()) - if res then - if not Window.fullscreen() then - Window.setExactSize(unpack(pre_fs_size)) - end - end - return res - end - } -end - ---GAME, note: Game should only hold configuration stuff (with some expections) -Game = callable{ - time = 0, - love_version = {0,0,0}, - options = { - res = 'assets', - scripts = {}, - filter = 'linear', - vsync = 1, - auto_require = true, - background_color = 'black', - window_flags = {}, - fps = 60, - scale = true, - effect = nil, - - load = function() end, - draw = function(fn) fn() end, - postdraw = nil, - update = function(dt) end, - }, - __call = function(_, options) - table.update(Game.options, options or {}) - Game.canvas = Canvas{ - auto_draw=false, - persistent=true - } - Game.effect = Game.options.effect - -- load config.json - config_data = love.filesystem.read('config.json') - if config_data then Game.config = json.decode(config_data) end - table.update(Game.options, Game.config.export) - -- get current os - Window.os = ({ ["OS X"]="mac", ["Windows"]="win", ["Linux"]="linux", ["Android"]="android", ["iOS"]="ios" })[love.system.getOS()]-- Game.options.os or 'ide' - Window.full_os = love.system.getOS() - -- load settings - if Window.os ~= 'web' then - Game.width, Game.height = Window.calculateSize(Game.config.game_size) -- game size - end - -- disable effects for web (SharedArrayBuffer or whatever) - if Window.os == 'web' then - Feature.disable('effect') - end - -- window size and flags - Game.options.window_flags = table.update({ - borderless = Game.options.frameless, - resizable = Game.options.resizable, - }, Game.options.window_flags or {}) - - if Window.os ~= 'web' then - Window.setSize(Game.config.window_size) - end - Blanke.resize() - -- vsync - switch(Game.options.vsync, { - on = function() Window.vsync(1) end, - off = function() Window.vsync(0) end, - adaptive = function() Window.vsync(-1) end - }) - - if type(Game.options.filter) == 'table' then - love.graphics.setDefaultFilter(unpack(Game.options.filter)) - else - love.graphics.setDefaultFilter(Game.options.filter, Game.options.filter) - end - Draw.setFont('04B_03.ttf', 16) - scripts = Game.options.scripts or {} - -- load plugins - if Game.options.plugins then - for _,f in ipairs(Game.options.plugins) do - table.insert(scripts, 'plugins.'..f) - end - end - -- load scripts - if Game.options.auto_require then - local load_folder - load_folder = function(path) - files = FS.ls(path) - for _,f in ipairs(files) do - local file_path = path..'/'..f - if FS.extname(f) == 'lua' and not table.hasValue(scripts, file_path) then - new_f = - table.insert(scripts, table.join(string.split(FS.removeExt(file_path), '/'),'.')) - end - local info = FS.info(file_path) - if info.type == 'directory' and file_path ~= '/dist' and file_path ~= '/lua' then - load_folder(file_path) - end - end - end - load_folder('') - end - - for _,script in ipairs(scripts) do - if script ~= 'main' then - require(script) - end - end - -- fullscreen toggle - Input.set({ _fs_toggle = { 'alt', 'enter' } }, { - combo = { '_fs_toggle' }, - no_repeat = { '_fs_toggle' }, - }) - - love.graphics.setBackgroundColor(0,0,0,0) - end, - - res = function(_type, file) - return Game.options.res.."/".._type.."/"..file - end -} + init = function(self, fn_new) + self.stack = {} -- { { used:t/f, value:?, is_stack:true, release:fn() } } + self.fn_new = fn_new + end, + new = function(self, remake) + local found = false + for _, s in ipairs(self.stack) do + if not s.used then + found = true + s.used = true + if remake then + s.value = self.fn_new() + end + return s + end + end + if not found then + local new_uuid = uuid() + local new_stack_obj = { + uuid=new_uuid, + used=true, + value=self.fn_new(obj), + is_stack=true + } + table.insert(self.stack, new_stack_obj) + return new_stack_obj + end + end, + release = function(self, object) + for _, s in ipairs(self.stack) do + if s.uuid == object.uuid then + s.used = false + return + end + end + end +} \ No newline at end of file diff --git a/love2d/lua/ecs/uuid.lua b/love2d/lua/ecs/uuid.lua new file mode 100644 index 00000000..e65ddb87 --- /dev/null +++ b/love2d/lua/ecs/uuid.lua @@ -0,0 +1,205 @@ +--------------------------------------------------------------------------------------- +-- Copyright 2012 Rackspace (original), 2013 Thijs Schreijer (modifications) +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS-IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- see http://www.ietf.org/rfc/rfc4122.txt +-- +-- Note that this is not a true version 4 (random) UUID. Since `os.time()` precision is only 1 second, it would be hard +-- to guarantee spacial uniqueness when two hosts generate a uuid after being seeded during the same second. This +-- is solved by using the node field from a version 1 UUID. It represents the mac address. +-- +-- 28-apr-2013 modified by Thijs Schreijer from the original [Rackspace code](https://github.com/kans/zirgo/blob/807250b1af6725bad4776c931c89a784c1e34db2/util/uuid.lua) as a generic Lua module. +-- Regarding the above mention on `os.time()`; the modifications use the `socket.gettime()` function from LuaSocket +-- if available and hence reduce that problem (provided LuaSocket has been loaded before uuid). +-- +-- **6-nov-2015 Please take note of this issue**; [https://github.com/Mashape/kong/issues/478](https://github.com/Mashape/kong/issues/478) +-- It demonstrates the problem of using time as a random seed. Specifically when used from multiple processes. +-- So make sure to seed only once, application wide. And to not have multiple processes do that +-- simultaneously (like nginx does for example). + +local M = {} +local math = require('math') +local os = require('os') +local string = require('string') + +local bitsize = 32 -- bitsize assumed for Lua VM. See randomseed function below. +local lua_version = tonumber(_VERSION:match("%d%.*%d*")) -- grab Lua version used + +local MATRIX_AND = {{0,0},{0,1} } +local MATRIX_OR = {{0,1},{1,1}} +local HEXES = '0123456789abcdef' + +local math_floor = math.floor +local math_random = math.random +local math_abs = math.abs +local string_sub = string.sub +local to_number = tonumber +local assert = assert +local type = type + +-- performs the bitwise operation specified by truth matrix on two numbers. +local function BITWISE(x, y, matrix) + local z = 0 + local pow = 1 + while x > 0 or y > 0 do + z = z + (matrix[x%2+1][y%2+1] * pow) + pow = pow * 2 + x = math_floor(x/2) + y = math_floor(y/2) + end + return z +end + +local function INT2HEX(x) + local s,base = '',16 + local d + while x > 0 do + d = x % base + 1 + x = math_floor(x/base) + s = string_sub(HEXES, d, d)..s + end + while #s < 2 do s = "0" .. s end + return s +end + +---------------------------------------------------------------------------- +-- Creates a new uuid. Either provide a unique hex string, or make sure the +-- random seed is properly set. The module table itself is a shortcut to this +-- function, so `my_uuid = uuid.new()` equals `my_uuid = uuid()`. +-- +-- For proper use there are 3 options; +-- +-- 1. first require `luasocket`, then call `uuid.seed()`, and request a uuid using no +-- parameter, eg. `my_uuid = uuid()` +-- 2. use `uuid` without `luasocket`, set a random seed using `uuid.randomseed(some_good_seed)`, +-- and request a uuid using no parameter, eg. `my_uuid = uuid()` +-- 3. use `uuid` without `luasocket`, and request a uuid using an unique hex string, +-- eg. `my_uuid = uuid(my_networkcard_macaddress)` +-- +-- @return a properly formatted uuid string +-- @param hwaddr (optional) string containing a unique hex value (e.g.: `00:0c:29:69:41:c6`), to be used to compensate for the lesser `math_random()` function. Use a mac address for solid results. If omitted, a fully randomized uuid will be generated, but then you must ensure that the random seed is set properly! +-- @usage +-- local uuid = require("uuid") +-- print("here's a new uuid: ",uuid()) +function M.new(hwaddr) + -- bytes are treated as 8bit unsigned bytes. + local bytes = { + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255), + math_random(0, 255) + } + + if hwaddr then + assert(type(hwaddr)=="string", "Expected hex string, got "..type(hwaddr)) + -- Cleanup provided string, assume mac address, so start from back and cleanup until we've got 12 characters + local i,str = #hwaddr, hwaddr + hwaddr = "" + while i>0 and #hwaddr<12 do + local c = str:sub(i,i):lower() + if HEXES:find(c, 1, true) then + -- valid HEX character, so append it + hwaddr = c..hwaddr + end + i = i - 1 + end + assert(#hwaddr == 12, "Provided string did not contain at least 12 hex characters, retrieved '"..hwaddr.."' from '"..str.."'") + + -- no split() in lua. :( + bytes[11] = to_number(hwaddr:sub(1, 2), 16) + bytes[12] = to_number(hwaddr:sub(3, 4), 16) + bytes[13] = to_number(hwaddr:sub(5, 6), 16) + bytes[14] = to_number(hwaddr:sub(7, 8), 16) + bytes[15] = to_number(hwaddr:sub(9, 10), 16) + bytes[16] = to_number(hwaddr:sub(11, 12), 16) + end + + -- set the version + bytes[7] = BITWISE(bytes[7], 0x0f, MATRIX_AND) + bytes[7] = BITWISE(bytes[7], 0x40, MATRIX_OR) + -- set the variant + bytes[9] = BITWISE(bytes[7], 0x3f, MATRIX_AND) + bytes[9] = BITWISE(bytes[7], 0x80, MATRIX_OR) + return INT2HEX(bytes[1])..INT2HEX(bytes[2])..INT2HEX(bytes[3])..INT2HEX(bytes[4]).."-".. + INT2HEX(bytes[5])..INT2HEX(bytes[6]).."-".. + INT2HEX(bytes[7])..INT2HEX(bytes[8]).."-".. + INT2HEX(bytes[9])..INT2HEX(bytes[10]).."-".. + INT2HEX(bytes[11])..INT2HEX(bytes[12])..INT2HEX(bytes[13])..INT2HEX(bytes[14])..INT2HEX(bytes[15])..INT2HEX(bytes[16]) +end + +---------------------------------------------------------------------------- +-- Improved randomseed function. +-- Lua 5.1 and 5.2 both truncate the seed given if it exceeds the integer +-- range. If this happens, the seed will be 0 or 1 and all randomness will +-- be gone (each application run will generate the same sequence of random +-- numbers in that case). This improved version drops the most significant +-- bits in those cases to get the seed within the proper range again. +-- @param seed the random seed to set (integer from 0 - 2^32, negative values will be made positive) +-- @return the (potentially modified) seed used +-- @usage +-- local socket = require("socket") -- gettime() has higher precision than os.time() +-- local uuid = require("uuid") +-- -- see also example at uuid.seed() +-- uuid.randomseed(socket.gettime()*10000) +-- print("here's a new uuid: ",uuid()) +function M.randomseed(seed) + seed = math_floor(math_abs(seed)) + if seed >= (2^bitsize) then + -- integer overflow, so reduce to prevent a bad seed + seed = seed - math_floor(seed / 2^bitsize) * (2^bitsize) + end + if lua_version < 5.2 then + -- 5.1 uses (incorrect) signed int + math.randomseed(seed - 2^(bitsize-1)) + else + -- 5.2 uses (correct) unsigned int + math.randomseed(seed) + end + return seed +end + +---------------------------------------------------------------------------- +-- Seeds the random generator. +-- It does so in 2 possible ways; +-- +-- 1. use `os.time()`: this only offers resolution to one second (used when +-- LuaSocket hasn't been loaded yet +-- 2. use luasocket `gettime()` function, but it only does so when LuaSocket +-- has been required already. +-- @usage +-- local socket = require("socket") -- gettime() has higher precision than os.time() +-- -- LuaSocket loaded, so below line does the same as the example from randomseed() +-- uuid.seed() +-- print("here's a new uuid: ",uuid()) +function M.seed() + if package.loaded["socket"] and package.loaded["socket"].gettime then + return M.randomseed(package.loaded["socket"].gettime()*10000) + else + return M.randomseed(os.time()) + end +end + +return setmetatable( M, { __call = function(self, hwaddr) return self.new(hwaddr) end} )