├── .gitignore ├── .gitmodules ├── README.md ├── conf.lua ├── docs ├── index.html ├── lasagna │ ├── fakelove.html │ └── projector.html ├── main.html └── src │ └── classes │ ├── Console.html │ ├── Editor.html │ ├── Hierarchy.html │ ├── Inspector.html │ ├── Template.html │ └── UIHelper.html ├── imgui.dll ├── lasagna ├── fakelove.lua └── projector.lua ├── main.lua ├── project ├── nogame │ └── main.lua └── pong │ ├── main.lua │ └── middleclass.lua ├── src └── classes │ ├── Console.lua │ ├── Editor.lua │ ├── Hierarchy.lua │ ├── Inspector.lua │ ├── Template.lua │ └── UIHelper.lua └── tools └── generate_docs.sh /.gitignore: -------------------------------------------------------------------------------- 1 | binary/* 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libs/middleclass"] 2 | path = libs/middleclass 3 | url = https://github.com/kikito/middleclass 4 | [submodule "libs/lume"] 5 | path = libs/lume 6 | url = https://github.com/rxi/lume 7 | [submodule "tools/rtfm"] 8 | path = tools/rtfm 9 | url = https://github.com/airstruck/rtfm -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sandsmas 2 | A visual editor for LÖVE, that runs in LÖVE. 3 | 4 | ![example screenshot](http://i.imgur.com/SCWmBTm.png) 5 | 6 | # Structure 7 | The basic structure is that there is, from least complex to most complex: 8 | * **Underlying Libraries** specifically chosen because they do what they do the best. 9 | * **Lasagna** composed of the underlying libraries, in several layers, with just enough glue to combine them meaningfully without permanently binding to a particular library for a given purpose. 10 | * **Layer 1** low-level serialization and entity-component relationships that will make networking & physics a lot easier. 11 | * **Layer 2** mid-level features that integrate with the structure provided from layer 1, such as graphic optimizations and complex rendering. 12 | * **Layer 3** high-level features, such as logging, profiling, and libraries that editor tools can take advantage of. 13 | * **A Project Structure** a specially formatted love project that will enable the editor to edit it while still being permissive about its configuration. 14 | * **A Bootstrap** maintained separate from the editor, allowing for running of project files as a standalone and within the editor. 15 | * **The Editor** that is capable of producing project files and allowing for easy composition of a game. 16 | 17 | ## Lasagna 18 | Below are some example layer categories and some candidate libraries that would be nice to have in each. 19 | * **Serialization & Net:** [tiny-ecs](https://github.com/bakpakin/tiny-ecs), [binser](https://github.com/bakpakin/binser), [trickle](https://github.com/bjornbytes/trickle), [hero](https://github.com/airstruck/hero), [sock.lua](https://github.com/camchenry/sock.lua) 20 | * **Configuration & Time:** [tick](https://github.com/bjornbytes/tick), [modrun](https://github.com/Asmageddon/modrun), [tactile](https://github.com/tesselode/tactile) 21 | * **Rendering:** love.physics-visualizer, [chiro](https://github.com/bjornbytes/chiro), [anim8](https://github.com/kikito/anim8), [anim9](https://github.com/excessive/anim9), [Simple Tiled Implementation](https://github.com/karai17/Simple-Tiled-Implementation), [love-imgui](https://github.com/slages/love-imgui), scene.lua, anchor.lua 22 | * **Assets:** [Autobatch](https://github.com/rxi/autobatch), [cargo](https://github.com/bjornbytes/cargo) 23 | * **Profiling:** [PROBE](https://github.com/jorio/PROBE), [inspect.lua](https://github.com/kikito/inspect.lua), [lurker](https://github.com/rxi/lurker), [Lovebird](https://github.com/rxi/lovebird), [Lovecat](https://github.com/CoffeeKitty/lovecat), love-console, perfhud 24 | * **Logging:** ansicolors, [log.lua](https://github.com/rxi/log.lua), log-lua, [i18n](https://github.com/excessive/i18n), argparse 25 | * **Wiring:** talkback, middleclass, stateful, router.lua 26 | * **Advanced Items:** [lume](https://github.com/rxi/lume/), pool.lua, memoize, LuaFun, lua-stdlib, [knife](https://github.com/airstruck/knife), microlight, [CPML](https://github.com/excessive/cpml), LuaDate, dkjson, LuaFFT, Worp 27 | * **Upkeep:** Busted, [RTFM](https://github.com/airstruck/rtfm), [LDoc](https://github.com/stevedonovan/LDoc), [lustache](https://github.com/Olivine-Labs/lustache), [semver](https://github.com/kikito/semver.lua) 28 | * **High Level:** lua-imgur, gyfcat, gifload, magick, sfxr, pegasus, turbo, lua-websockets, lua-sqlite3 29 | 30 | # Good Reads 31 | ## Design Goals 32 | * [how to a framework that is not in the way](http://weierophinney.github.io/2015-10-22-ZF3/#/) 33 | * [reading and writing network packets](http://gafferongames.com/building-a-game-network-protocol/reading-and-writing-packets/) 34 | * [middleclass class standards, for internal classes](https://github.com/kikito/middleclass/wiki) 35 | * [regions for humans](http://magcius.github.io/xplain/article/regions.html) 36 | 37 | ## LuaJIT 38 | * [luajit and C calls](http://stackoverflow.com/questions/34405678/using-lua-ffi-with-complex-types) 39 | * [type conversion](http://luajit.org/ext_ffi_semantics.html#convert) 40 | * [idioms](http://luajit.org/ext_ffi_tutorial.html#idioms) 41 | 42 | 43 | ## Technical Reference 44 | * [setfenv outside lua51](http://leafo.net/guides/setfenv-in-lua52-and-above.html) 45 | * [non-recursive deepcopy](https://gist.github.com/Deco/3985043) 46 | * [love console and zerobrane](https://github.com/EntranceJew/love-notes/blob/master/love2d-and-zerobrane.md) 47 | -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.identity = "sandsmas" -- The name of the save directory (string) 3 | t.version = "0.10.1" -- The LÖVE version this game was made for (string) 4 | t.console = false -- Attach a console (boolean, Windows only) 5 | t.accelerometerjoystick = true -- Enable the accelerometer on iOS and Android by exposing it as a Joystick (boolean) 6 | t.externalstorage = false -- True to save files (and read from the save directory) in external storage on Android (boolean) 7 | t.gammacorrect = false -- Enable gamma-correct rendering, when supported by the system (boolean) 8 | 9 | t.window.title = "sandsmas" -- The window title (string) 10 | t.window.icon = nil -- Filepath to an image to use as the window's icon (string) 11 | t.window.width = 800 -- The window width (number) 12 | t.window.height = 600 -- The window height (number) 13 | t.window.borderless = false -- Remove all border visuals from the window (boolean) 14 | t.window.resizable = true -- Let the window be user-resizable (boolean) 15 | t.window.minwidth = 1 -- Minimum window width if the window is resizable (number) 16 | t.window.minheight = 1 -- Minimum window height if the window is resizable (number) 17 | t.window.fullscreen = false -- Enable fullscreen (boolean) 18 | t.window.fullscreentype = "desktop" -- Choose between "desktop" fullscreen or "exclusive" fullscreen mode (string) 19 | t.window.vsync = true -- Enable vertical sync (boolean) 20 | t.window.msaa = 0 -- The number of samples to use with multi-sampled antialiasing (number) 21 | t.window.display = 1 -- Index of the monitor to show the window in (number) 22 | t.window.highdpi = false -- Enable high-dpi mode for the window on a Retina display (boolean) 23 | t.window.x = nil -- The x-coordinate of the window's position in the specified display (number) 24 | t.window.y = nil -- The y-coordinate of the window's position in the specified display (number) 25 | 26 | t.modules.audio = true -- Enable the audio module (boolean) 27 | t.modules.event = true -- Enable the event module (boolean) 28 | t.modules.graphics = true -- Enable the graphics module (boolean) 29 | t.modules.image = true -- Enable the image module (boolean) 30 | t.modules.joystick = true -- Enable the joystick module (boolean) 31 | t.modules.keyboard = true -- Enable the keyboard module (boolean) 32 | t.modules.math = true -- Enable the math module (boolean) 33 | t.modules.mouse = true -- Enable the mouse module (boolean) 34 | t.modules.physics = true -- Enable the physics module (boolean) 35 | t.modules.sound = true -- Enable the sound module (boolean) 36 | t.modules.system = true -- Enable the system module (boolean) 37 | t.modules.timer = true -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update 38 | t.modules.touch = true -- Enable the touch module (boolean) 39 | t.modules.video = true -- Enable the video module (boolean) 40 | t.modules.window = true -- Enable the window module (boolean) 41 | t.modules.thread = true -- Enable the thread module (boolean) 42 | end -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /docs/lasagna/fakelove.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | API Docs 4 |
48 |

API Docs

49 | 65 |

fakelove

Pretend to be LÖVE.

66 |

Classes

fakelove

A fake instance of LÖVE.

67 |

Functions

fakelove.new (Self., The)

Launch the generator from the command line.

68 |
Arguments
table Self.

69 |
table The

real instance of love to duplicate. 70 | close = function() 71 | 72 | end, 73 | fromPixels = function() 74 | 75 | end, 76 | getDisplayCount = function() 77 | 78 | end, 79 | getDesktopDimensions = function() 80 | 81 | end, 82 | getDisplayName = function() 83 | 84 | end, 85 | getFullscreen = function() 86 | 87 | end, 88 | getFullscreenModes = function() 89 | 90 | end, 91 | getIcon = function() 92 | 93 | end, 94 | getPixelScale = function() 95 | 96 | end, 97 | isDisplaySleepEnabled = function() 98 | 99 | end,

100 |
101 |
WARNING:
UNDOCUMENTED

end, 102 | isVisible = function() 103 | 104 | end, 105 | maximize = function() 106 | 107 | end, 108 | minimize = function() 109 | 110 | end, 111 | requestAttention = function() 112 | 113 | end, 114 | setDisplaySleepEnabled = function() 115 | 116 | end, 117 | setFullscreen = function() 118 | 119 | end, 120 | setIcon = function() 121 | 122 | end, 123 | showMessageBox = function() 124 | 125 | end, 126 | toPixels = function() 127 | 128 | end,

129 |
130 |
131 |
132 |
136 | 137 | 138 | -------------------------------------------------------------------------------- /docs/lasagna/projector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | API Docs 4 |
48 |

API Docs

49 | 52 |
53 |
TODO:

* because we pass through a clone of love, things like love.keyboard.isDown 54 | read from the editor's LOVE despite sandboxed love.keypressed.* events 55 | not being run. 56 | * makes u think 57 | LÖVE Callback Emulation Goes Here

58 |
special checks

for when the mouse is given focus 59 | before we know where it is

60 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | API Docs 4 |
48 |

API Docs

49 | 52 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/src/classes/Console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | API Docs 4 |
48 |

API Docs

49 | 66 |
67 |

Classes

Console

Puts colored text in a scrollable window.

68 |

Methods

Console:Log (status, text)

Add text to the console.

69 |
Arguments
string status

The color of the text.

70 |
string text

The text to log.

71 |
72 |

Console:Render ()

Draws the console's contents.

73 |
74 |
78 | 79 | 80 | -------------------------------------------------------------------------------- /docs/src/classes/Editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | API Docs 4 |
48 |

API Docs

49 | 56 |
57 |

Classes

Editor

An editor instance, contains references to other classes as internal components.

58 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /docs/src/classes/Hierarchy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | API Docs 4 |
48 |

API Docs

49 | 52 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/src/classes/Inspector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | API Docs 4 |
48 |

API Docs

49 | 52 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/src/classes/Template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | API Docs 4 |
48 |

API Docs

49 | 61 |
62 |

Classes

Template

Example class.

63 |

Methods

Template:ExampleMethod (a_table, a_string, a_number)

A method that exemplifies an instance method.

64 |
Arguments
table a_table

A table that stuff happens to.

65 |
string a_string

A string that things happen to.

66 |
number a_number

A number that things happen to.

67 |
68 |
Returns
bool

The status of the operation.

69 |
70 |
71 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/src/classes/UIHelper.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | API Docs 4 |
48 |

API Docs

49 | 52 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /imgui.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EntranceJew/sandsmas/77de71cd3b5133820f735f294e7a70d56b587614/imgui.dll -------------------------------------------------------------------------------- /lasagna/fakelove.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Pretend to be LÖVE, what a great prank 3 | another great job, EntranceJew "gee thanks" 4 | MIT license probably. 5 | ]] 6 | --- @module fakelove Pretend to be LÖVE. 7 | local lume = require("libs.lume.lume") 8 | 9 | --- @class fakelove A fake instance of LÖVE. 10 | local fakelove = {} 11 | fakelove.__index = fakelove 12 | 13 | local function scale(valueIn, baseMin, baseMax, limitMin, limitMax) 14 | return ((limitMax - limitMin) * (valueIn - baseMin) / (baseMax - baseMin)) + limitMin 15 | end 16 | 17 | local function deepcopy(orig) 18 | local orig_type = type(orig) 19 | local copy 20 | if orig_type == 'table' then 21 | copy = {} 22 | for orig_key, orig_value in next, orig, nil do 23 | copy[deepcopy(orig_key)] = deepcopy(orig_value) 24 | end 25 | setmetatable(copy, deepcopy(getmetatable(orig))) 26 | else -- number, string, boolean, etc 27 | copy = orig 28 | end 29 | return copy 30 | end 31 | 32 | --- @function fakelove.new Launch the generator from the command line. 33 | --- @param table Self. 34 | --- @param table The real instance of love to duplicate. 35 | local function new(self, reallove) 36 | local zt = deepcopy(reallove) 37 | local t = setmetatable(zt, fakelove) 38 | t:initialize(reallove) 39 | return t 40 | end 41 | 42 | function fakelove:initialize(reallove) 43 | local x, y, display = reallove.window.getPosition() 44 | local width, height, flags = reallove.window.getMode() 45 | self.store = { 46 | audio = {}, 47 | event = {}, 48 | filesystem = {}, 49 | font = {}, 50 | graphics = {}, 51 | image = {}, 52 | joystick = {}, 53 | keyboard = {}, 54 | math = {}, 55 | mouse = { 56 | x = 0, 57 | y = 0, 58 | buttons = {}, 59 | cursor = reallove.mouse.getCursor(), 60 | grabbed = reallove.mouse.isGrabbed(), 61 | visible = reallove.mouse.isVisible(), 62 | relativeMode = reallove.mouse.getRelativeMode(), 63 | }, 64 | physics = {}, 65 | sound = {}, 66 | system = {}, 67 | thread = {}, 68 | timer = {}, 69 | touch = {}, 70 | video = {}, 71 | window = { 72 | title = reallove.window.getTitle(), 73 | x = x, 74 | y = y, 75 | display = display, 76 | width = width, 77 | height = height, 78 | flags = flags, 79 | 80 | focus = reallove.window.hasFocus(), 81 | mouseFocus = false, 82 | }, 83 | } 84 | self.reallove = reallove 85 | 86 | local mergelove = { 87 | mouse = { 88 | getPosition = function() 89 | return self.store.mouse.x, self.store.mouse.y 90 | end, 91 | getX = function() 92 | return self.store.mouse.x 93 | end, 94 | getY = function() 95 | return self.store.mouse.y 96 | end, 97 | }, 98 | window = { 99 | --[[ 100 | close = function() 101 | 102 | end, 103 | fromPixels = function() 104 | 105 | end, 106 | getDisplayCount = function() 107 | 108 | end, 109 | getDesktopDimensions = function() 110 | 111 | end, 112 | getDisplayName = function() 113 | 114 | end, 115 | getFullscreen = function() 116 | 117 | end, 118 | getFullscreenModes = function() 119 | 120 | end, 121 | getIcon = function() 122 | 123 | end, 124 | ]] 125 | getMode = function() 126 | return self.store.window.width, self.store.window.height, self.store.window.flags 127 | end, 128 | --[[ 129 | getPixelScale = function() 130 | 131 | end, 132 | ]] 133 | getPosition = function() 134 | return self.store.window.x, self.store.window.y, self.store.window.display 135 | end, 136 | getTitle = function() 137 | return self.store.window.title 138 | end, 139 | hasFocus = function() 140 | return self.store.window.focus 141 | end, 142 | hasMouseFocus = function() 143 | return self.store.window.mouseFocus 144 | end, 145 | --[[ 146 | isDisplaySleepEnabled = function() 147 | 148 | end, 149 | isOpen = function() -- @WARNING: UNDOCUMENTED 150 | 151 | end, 152 | isVisible = function() 153 | 154 | end, 155 | maximize = function() 156 | 157 | end, 158 | minimize = function() 159 | 160 | end, 161 | requestAttention = function() 162 | 163 | end, 164 | setDisplaySleepEnabled = function() 165 | 166 | end, 167 | setFullscreen = function() 168 | 169 | end, 170 | setIcon = function() 171 | 172 | end, 173 | ]] 174 | setMode = function(width, height, flags) 175 | self.store.window.width = width 176 | self.store.window.height = height 177 | self.store.window.flags = flags or self.store.window.flags 178 | return true 179 | end, 180 | setPosition = function(x, y, display) 181 | self.store.window.x = x 182 | self.store.window.y = y 183 | self.store.window.display = display or self.store.window.display 184 | end, 185 | setTitle = function(title) 186 | self.store.window.title = title 187 | end 188 | --[[ 189 | showMessageBox = function() 190 | 191 | end, 192 | toPixels = function() 193 | 194 | end, 195 | ]] 196 | } 197 | } 198 | 199 | -- does this work? I don't know. 200 | for k, v in pairs(mergelove) do 201 | self[k] = lume.merge(self[k], v) 202 | end 203 | end 204 | 205 | return setmetatable({new=new}, {__call=function(_,...) return new(...) end}) -------------------------------------------------------------------------------- /lasagna/projector.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Projector For Picture-in-Picture 3 | very good thanks EntranceJew 4 | MIT license probably. 5 | ]] 6 | --[[ 7 | things to think about: 8 | * l.graphics does not thread nicely so we can't utilize this for sandboxing a render 9 | * stuff like l.k.isDown bounce reads right off SDL and don't matter to us 10 | ]] 11 | --[[ 12 | @TODO: 13 | * because we pass through a clone of love, things like love.keyboard.isDown 14 | read from the editor's LOVE despite sandboxed love.keypressed.* events 15 | not being run. 16 | * makes u think 17 | ]] 18 | local lume = require("libs.lume.lume") 19 | local fakelove = require("lasagna.fakelove") 20 | 21 | local projector = {} 22 | projector.__index = projector 23 | 24 | local function nop() end 25 | 26 | -- this comes from lume don't beat me up 27 | local function clone(t) 28 | local rtn = {} 29 | for k, v in pairs(t) do rtn[k] = v end 30 | return rtn 31 | end 32 | 33 | local function new(self, entry, exposed, x, y, sx, sy) 34 | local t = setmetatable({}, projector) 35 | t:initialize(entry, exposed, x, y, sx, sy) 36 | return t 37 | end 38 | 39 | function projector:load_core(entry, exposed) 40 | -- create new environment 41 | local env = { 42 | love = fakelove:new(love) 43 | } 44 | 45 | -- merge an external environment 46 | if exposed and type(exposed) == "table" then 47 | env = lume.merge(env, exposed) 48 | end 49 | 50 | local metaenv = { __index = _G } 51 | setmetatable(env, metaenv) 52 | 53 | -- put it on us 54 | self.env = env 55 | 56 | -- wipe all callbacks 57 | self:loveEncapsulate() 58 | 59 | -- load the chunk 60 | local ok, chunk = pcall( love.filesystem.load, entry ) 61 | assert(ok, "The entry point '" .. entry .. "' appears invalid.") 62 | 63 | -- apply the environment to the chunk 64 | setfenv(chunk, env) 65 | 66 | -- invoke the chunk 67 | local result 68 | ok, result = pcall( chunk ) 69 | assert(ok, "Chunk execution failed for entry point '" .. entry .. "', error: " .. tostring(result) ) 70 | 71 | -- we have now captured 'entry' inside 'env' 72 | -- we have not entered the loaded environment 73 | return env, metaenv 74 | end 75 | 76 | function projector:loadstringInEnv(str) 77 | local ok, chunk = pcall( loadstring, str ) 78 | assert(ok, "The chunk was messed up: " .. tostring(chunk) ) 79 | 80 | setfenv( chunk, self.env ) 81 | 82 | local result 83 | ok, result = pcall( chunk ) 84 | assert(ok, "Chunk execution failed: " .. tostring(result) ) 85 | 86 | return result 87 | end 88 | 89 | -- search and destroy: https://love2d.org/wiki/Category:Callbacks 90 | function projector:loveEncapsulate() 91 | -- maybe_scopes: nogame, conf 92 | 93 | local bomb = [[ 94 | -- keep error handlers because they prevent silent crashes 95 | local scopes = { 96 | --'errhand', 97 | --'threaderror', 98 | 'draw', 99 | 'load', 100 | 'run', 101 | 'update', 102 | } 103 | for k,v in pairs(love.handlers) do 104 | love[k] = nil 105 | end 106 | for k,v in ipairs(scopes) do 107 | love[v] = nil 108 | end 109 | ]] 110 | return self:loadstringInEnv(bomb) 111 | end 112 | 113 | function projector:getBaseFromEntryPoint(entry) 114 | local ret = {string.match(entry, "(.-)([^\\/]-%.?([^%.\\/]*))$")} 115 | return ret[1] 116 | end 117 | 118 | function projector:initialize(entry, exposed, x, y, w, h) 119 | -- set the entry point for our emulation 120 | self.entry = entry or 'main.lua' 121 | 122 | -- emulate path level imports and hope to god we don't get a collision 123 | self.base_path = self:getBaseFromEntryPoint(self.entry) 124 | 125 | if self.base_path ~= '' then 126 | self.added_path = ';' .. self.base_path .. '?.lua;' .. self.base_path .. '?/init.lua' 127 | love.filesystem.setRequirePath(love.filesystem.getRequirePath() .. self.added_path) 128 | else 129 | assert(false, "Base path was wrong or broken.") 130 | end 131 | 132 | -- timers for play 133 | self.dt = 0 134 | self.gt = 0 135 | self.tt = 0 136 | 137 | -- preserve reality as it was before we messed with things 138 | -- the "absolutely safe capsule" for package, etc. 139 | self.asc = getfenv() 140 | 141 | -- load our core 142 | self.env = self:load_core(self.entry, exposed) 143 | 144 | if self.env.love.load then 145 | self:doInEnv( self.env.love.load ) 146 | end 147 | end 148 | 149 | function projector:renderAbsolutelySafeCapsule() 150 | self.asc = { 151 | package = { 152 | cpath = package.cpath, 153 | loaded = package.loaded, 154 | loadlib = package.loadlib, 155 | path = package.path, 156 | preload = package.preload, 157 | searchers = package.searchers, 158 | searchpath = package.searchpath 159 | }, 160 | } 161 | end 162 | 163 | function projector:doInEnv(func, ...) 164 | -- have some variables set in our scope before entering env 165 | local ok, result 166 | local pcall = pcall 167 | 168 | -- enter the env, execute, immediately leave 169 | setfenv(1, self.env) 170 | ok, result = pcall( func, ... ) 171 | setfenv(1, self.asc) 172 | 173 | -- we're done here 174 | return ok, result 175 | end 176 | 177 | --[[ 178 | LÖVE Callback Emulation Goes Here 179 | ]] 180 | function projector:draw() 181 | love.graphics.push("all") 182 | 183 | local x, y = self.env.love.window.getPosition() 184 | local w, h = self.env.love.window.getMode() 185 | love.graphics.translate(x, y) 186 | love.graphics.scale(w/love.graphics.getWidth(), h/love.graphics.getHeight()) 187 | love.graphics.setScissor(x, y, w, h) 188 | -- @TODO: right here, we should reload the graphics state 189 | self:doInEnv(self.env.love.draw) 190 | 191 | love.graphics.pop() 192 | end 193 | 194 | function projector:update(dt) 195 | if self.env.love.window.hasFocus() then 196 | self.gt = self.gt + dt 197 | self.dt = dt 198 | self:doInEnv(self.env.love.update, dt) 199 | end 200 | self.tt = self.tt + dt 201 | end 202 | 203 | -- do not use this from inside whatever is wrapping the projection's mousefocus 204 | -- unless the projection is fullscreened 205 | function projector:mousefocus(focus) 206 | --[[ 207 | @TODO: special checks for when the mouse is given focus 208 | before we know where it is 209 | ]] 210 | 211 | self:doInEnv(self.env.love.mousefocus, focus) 212 | end 213 | 214 | -- x, y is from global mouse position 215 | function projector:_is_mousepos_in_bounds(x, y) 216 | return (x >= self.env.love.store.window.x 217 | and y >= self.env.love.store.window.y 218 | and x <= self.env.love.store.window.x + self.env.love.store.window.width 219 | and y <= self.env.love.store.window.y + self.env.love.store.window.height 220 | ) 221 | end 222 | 223 | function projector:mousemoved(x, y, dx, dy, istouch) 224 | if self:_is_mousepos_in_bounds(x, y) then 225 | if not self.env.love.window.hasMouseFocus() then 226 | self.env.love.store.window.mouseFocus = true 227 | self:mousefocus(true) 228 | end 229 | 230 | self.env.love.store.mouse.x = x - self.env.love.store.window.x 231 | self.env.love.store.mouse.y = y - self.env.love.store.window.y 232 | 233 | -- pass to the *emulated game* 234 | self:doInEnv(self.env.love.mousemoved, 235 | self.env.love.store.mouse.x, 236 | self.env.love.store.mouse.y, 237 | dx, dy, istouch 238 | ) 239 | else 240 | -- the mouse moved away frome our fake window, 241 | -- therefore we only care about the last reported position 242 | if self.env.love.window.hasMouseFocus() then 243 | self.env.love.store.window.mouseFocus = false 244 | self:mousefocus(false) 245 | end 246 | end 247 | end 248 | 249 | function projector:mousepressed(x, y, button, istouch) 250 | if self:_is_mousepos_in_bounds(x, y) then 251 | -- @TODO: flash focus? issue a mousemoved? 252 | self.env.love.store.mouse.buttons[button] = true 253 | 254 | x = x - self.env.love.store.window.x 255 | y = y - self.env.love.store.window.y 256 | 257 | self:doInEnv(self.env.love.mousepressed, x, y, button, istouch) 258 | end 259 | end 260 | 261 | function projector:mousepressed(x, y, button, istouch) 262 | if self:_is_mousepos_in_bounds(x, y) then 263 | -- @TODO: flash focus? issue a mousemoved? 264 | self.env.love.store.mouse.buttons[button] = nil 265 | 266 | x = x - self.env.love.store.window.x 267 | y = y - self.env.love.store.window.y 268 | 269 | self:doInEnv(self.env.love.mousepressed, x, y, button, istouch) 270 | end 271 | end 272 | 273 | return setmetatable({new=new}, {__call=function(_,...) return new(...) end}) -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | io.stdout:setvbuf("no") 2 | local imgui = require("imgui") 3 | local UIHelper = require("src.classes.UIHelper"):new() 4 | local editor = require("src.classes.Editor"):new('yay') 5 | local projector = require("lasagna.projector") 6 | local nogame, pong 7 | 8 | -- 9 | -- LOVE callbacks 10 | -- 11 | 12 | -- this gets merged into the sandboxes 13 | -- therefore, we are exposing the gift of sandsmas here 14 | local sandsmas = { 15 | sandsmas = { 16 | editor = editor, 17 | imgui = imgui, 18 | } 19 | } 20 | 21 | function love.load(arg) 22 | nogame = projector:new("project/nogame/main.lua", sandsmas) 23 | pong = projector:new("project/pong/main.lua", sandsmas) 24 | 25 | local testfunc = function() end 26 | UIHelper:AddMenu("File/New", testfunc) 27 | UIHelper:AddMenu("File/Open", testfunc) 28 | UIHelper:AddMenu("File/Save", testfunc) 29 | UIHelper:AddMenu("File/Save As...", testfunc) 30 | UIHelper:AddMenu("Edit/This", testfunc) 31 | UIHelper:AddMenu("Edit/Thing", testfunc) 32 | UIHelper:AddMenu("Help/About", testfunc) 33 | end 34 | 35 | function love.update(dt) 36 | imgui.NewFrame() 37 | nogame:update(dt) 38 | pong:update(dt) 39 | 40 | -- reverse the hierarchy stack so things are roughly in hierarchial view 41 | editor.inspector:ClearSelection() 42 | for _, uid in ipairs(editor.hierarchy.chrono_selection) do 43 | editor.inspector:AddSelection(editor.hierarchy.objects[uid]) 44 | end 45 | end 46 | 47 | function love.draw() 48 | local wx, wy, x, y 49 | UIHelper:RenderMenu() 50 | 51 | imgui.SetNextWindowPos(0, 0) 52 | imgui.SetNextWindowSize(love.graphics.getWidth(), love.graphics.getHeight()) 53 | 54 | if UIHelper:Begin("DockArea", nil, { "NoResize", "NoMove", "NoBringToFrontOnFocus" }) then 55 | imgui.BeginDockspace() 56 | imgui.SetNextDock("Right") 57 | 58 | if imgui.BeginDock("Inspector") then 59 | editor.inspector:Render() 60 | end 61 | imgui.EndDock() 62 | 63 | imgui.SetNextDock("Left") 64 | 65 | if imgui.BeginDock("Project") then 66 | imgui.Text("Project") 67 | end 68 | imgui.EndDock() 69 | if imgui.BeginDock("Console") then 70 | editor.console:Render() 71 | end 72 | imgui.EndDock() 73 | 74 | imgui.SetNextDock("Top") 75 | 76 | if imgui.BeginDock("Hierarchy") then 77 | editor.hierarchy:Render() 78 | end 79 | imgui.EndDock() 80 | 81 | imgui.SetNextDock("Right") 82 | 83 | if imgui.BeginDock("Scene") then 84 | nogame.env.love.store.window.focus = imgui.IsWindowFocused() 85 | local x, y = imgui.GetWindowPos() 86 | local w, h = imgui.GetWindowSize() 87 | 88 | nogame.env.love.window.setPosition(x, y) 89 | nogame.env.love.window.setMode(w, h) 90 | UIHelper:PushPostRender(function() 91 | nogame:draw() 92 | end) 93 | end 94 | imgui.EndDock() 95 | 96 | if imgui.BeginDock("Game") then 97 | -- pong game goes here when active 98 | end 99 | imgui.EndDock() 100 | 101 | imgui.EndDockspace() 102 | end 103 | UIHelper:End() 104 | 105 | love.graphics.clear(100, 100, 100, 255) 106 | 107 | local mx, my = love.mouse.getPosition() 108 | if imgui.Begin(pong.env.love.window.getTitle() .. " | " .. mx .. "\\" .. my .. "###GameWindow") then 109 | pong.env.love.store.window.focus = imgui.IsWindowFocused() 110 | local x, y = imgui.GetWindowPos() 111 | local w, h = imgui.GetWindowSize() 112 | local titleHeight = 19 113 | y = y + titleHeight 114 | h = h - titleHeight 115 | 116 | pong.env.love.window.setPosition(x, y) 117 | pong.env.love.window.setMode(w, h) 118 | UIHelper:PushPostRender(function() 119 | pong:draw() 120 | end) 121 | end 122 | imgui.End() 123 | 124 | imgui.Render() 125 | 126 | UIHelper:PostRender() 127 | end 128 | 129 | function love.quit() 130 | imgui.ShutDown() 131 | end 132 | 133 | -- 134 | -- User inputs 135 | -- 136 | function love.textinput(t) 137 | imgui.TextInput(t) 138 | if not imgui.GetWantCaptureKeyboard() then 139 | 140 | end 141 | end 142 | 143 | function love.keypressed(key, scan, isRepeat) 144 | imgui.KeyPressed(key) 145 | if not imgui.GetWantCaptureKeyboard() then 146 | if key == "escape" then 147 | love.event.quit() 148 | elseif key == "y" then 149 | editor.console:Log('error', "too many dannies") 150 | else 151 | pong.env.love.keypressed(key, scan, isRepeat) 152 | end 153 | end 154 | end 155 | 156 | function love.keyreleased(key) 157 | imgui.KeyReleased(key) 158 | if not imgui.GetWantCaptureKeyboard() then 159 | 160 | end 161 | end 162 | 163 | function love.mousemoved(x, y, dx, dy, istouch) 164 | imgui.MouseMoved(x, y) 165 | pong:mousemoved(x, y, dx, dy, istouch) 166 | if not imgui.GetWantCaptureMouse() then 167 | 168 | end 169 | end 170 | 171 | function love.mousepressed(x, y, button) 172 | imgui.MousePressed(button) 173 | if not imgui.GetWantCaptureMouse() then 174 | 175 | end 176 | end 177 | 178 | function love.mousereleased(x, y, button) 179 | imgui.MouseReleased(button) 180 | if not imgui.GetWantCaptureMouse() then 181 | 182 | end 183 | end 184 | 185 | function love.mousefocus( focus ) 186 | -- don't send mousefocus to the game because 187 | -- they don't actually receive focus when we do 188 | end 189 | 190 | function love.wheelmoved(x, y) 191 | imgui.WheelMoved(y) 192 | if not imgui.GetWantCaptureMouse() then 193 | 194 | end 195 | end -------------------------------------------------------------------------------- /project/nogame/main.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2006-2016 LOVE Development Team 3 | Modified by EntranceJew 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. 20 | --]] 21 | 22 | --love = require("love") 23 | 24 | function love.conf(t) 25 | t.title = "L\195\150VE " .. love._version .. " (" .. love._version_codename .. ")" 26 | t.gammacorrect = true 27 | t.modules.audio = false 28 | t.modules.sound = false 29 | t.modules.physics = false 30 | t.modules.joystick = false 31 | t.window.resizable = true 32 | t.window.highdpi = true 33 | 34 | if love._os == "iOS" then 35 | t.window.borderless = true 36 | end 37 | end 38 | 39 | local math = math 40 | 41 | -- 30log.lua begins 42 | local function require_30log() 43 | local assert, ipairs, pairs, type, tostring, setmetatable = assert, ipairs, pairs, type, tostring, setmetatable 44 | local baseMt, _instances, _classes, _class = {}, setmetatable({},{__mode='k'}), setmetatable({},{__mode='k'}) 45 | local function assert_class(class, method) assert(_classes[class], ('Wrong method call. Expected class:%s.'):format(method)) end 46 | local function deep_copy(t, dest, aType) t = t or {}; local r = dest or {} 47 | for k,v in pairs(t) do 48 | if aType and type(v)==aType then r[k] = v elseif not aType then 49 | if type(v) == 'table' and k ~= "__index" then r[k] = deep_copy(v) else r[k] = v end 50 | end 51 | end; return r 52 | end 53 | local function instantiate(self,...) 54 | assert_class(self, 'new(...) or class(...)'); local instance = {class = self}; _instances[instance] = tostring(instance); setmetatable(instance,self) 55 | if self.init then if type(self.init) == 'table' then deep_copy(self.init, instance) else self.init(instance, ...) end; end; return instance 56 | end 57 | local function extend(self, name, extra_params) 58 | assert_class(self, 'extend(...)'); local heir = {}; _classes[heir] = tostring(heir); deep_copy(extra_params, deep_copy(self, heir)); 59 | heir.name, heir.__index, heir.super = extra_params and extra_params.name or name, heir, self; return setmetatable(heir,self) 60 | end 61 | baseMt = { __call = function (self,...) return self:new(...) end, __tostring = function(self,...) 62 | if _instances[self] then return ("instance of '%s' (%s)"):format(rawget(self.class,'name') or '?', _instances[self]) end 63 | return _classes[self] and ("class '%s' (%s)"):format(rawget(self,'name') or '?',_classes[self]) or self 64 | end}; _classes[baseMt] = tostring(baseMt); setmetatable(baseMt, {__tostring = baseMt.__tostring}) 65 | local class = {isClass = function(class, ofsuper) local isclass = not not _classes[class]; if ofsuper then return isclass and (class.super == ofsuper) end; return isclass end, isInstance = function(instance, ofclass) 66 | local isinstance = not not _instances[instance]; if ofclass then return isinstance and (instance.class == ofclass) end; return isinstance end}; _class = function(name, attr) 67 | local c = deep_copy(attr); c.mixins=setmetatable({},{__mode='k'}); _classes[c] = tostring(c); c.name, c.__tostring, c.__call = name or c.name, baseMt.__tostring, baseMt.__call 68 | c.include = function(self,mixin) assert_class(self, 'include(mixin)'); self.mixins[mixin] = true; return deep_copy(mixin, self, 'function') end 69 | c.new, c.extend, c.__index, c.includes = instantiate, extend, c, function(self,mixin) assert_class(self,'includes(mixin)') return not not (self.mixins[mixin] or (self.super and self.super:includes(mixin))) end 70 | c.extends = function(self, class) assert_class(self, 'extends(class)') local super = self; repeat super = super.super until (super == class or super == nil); return class and (super == class) end 71 | return setmetatable(c, baseMt) end; class._DESCRIPTION = '30 lines library for object orientation in Lua'; class._VERSION = '30log v1.0.0'; class._URL = 'http://github.com/Yonaba/30log'; class._LICENSE = 'MIT LICENSE ' 72 | return setmetatable(class,{__call = function(_,...) return _class(...) end }) 73 | end 74 | -- 30log.lua ends 75 | 76 | local mosaic_png = 77 | "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAGXRFWHRTb2Z0d2FyZQBBZG9i\ 78 | ZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tl\ 79 | dCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1l\ 80 | dGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUu\ 81 | My1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpS\ 82 | REYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgt\ 83 | bnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6\ 84 | Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRv\ 85 | YmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9u\ 86 | cy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRp\ 87 | ZDo2NDY3ODU2OTk0NkJFNTExQTg3RkQ3MTNCOTc2N0UzNyIgeG1wTU06RG9jdW1lbnRJRD0i\ 88 | eG1wLmRpZDo5ODkwN0RFRTg3RUExMUU1QTRBMTk2NDJEQUYyRkUwNyIgeG1wTU06SW5zdGFu\ 89 | Y2VJRD0ieG1wLmlpZDo5ODkwN0RFRDg3RUExMUU1QTRBMTk2NDJEQUYyRkUwNyIgeG1wOkNy\ 90 | ZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJp\ 91 | dmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjBGQTIzQUI1RTc4N0U1MTFBNDUy\ 92 | ODc2NjFDRjM3OTIwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjY0Njc4NTY5OTQ2QkU1\ 93 | MTFBODdGRDcxM0I5NzY3RTM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwv\ 94 | eDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+GCWHRwAAGTRJREFUeNrsXQecFdXVv+wu\ 95 | TXqVIrAKwQKKQRAkqIANBZUYRRPDF8EaItiCyaeCSOwJfERRhBhUYqxEMSoCSijSQSCIICqw\ 96 | KKDAwtLLLji5f995YZi95dwpDz955/c7v4U3c8vcOXPv6aeM53kiC0cv5GSXIEsAWTiKIc9w\ 97 | 7QmJP5NYSWI1Sz97JJZILPb9e7fh73Xf4zUpK7G3xAsl1pS4WuLzEmc69oO290m8WmIDiZsl\ 98 | jpI4WOJBh7l0lthRYiuJzSXWD7yP7RLXS/xC4nyJ0yXOlvgtawTwABpc6iUDewxjHmmsLHGO\ 99 | Zt4DHPqpL3GVpp9HGO2rSnxW4r6Qa/y1xD9IrGcby3RxXUIEsCHEi8GCDpG4UOJ+iWsknqC4\ 100 | r63E2yX2ktg8xDiDDfM+ILExs5+xhn62ScwztG0gsSCmtd5JhJsThgD2JEQAnzi+lP4S9yr6\ 101 | mRG4r73E4sA9IJgLHMaaaZn7z5n9bLT009LQ9m8JrPm7Eo9RjadjAitIrBj47cuYztgih3sH\ 102 | SfwzzScIZ0vs5vv/UDoz/XCGxMkSBzLHq2G5fjCmNfix4Vr3BPiaSyS+reL5cgwMjB8elthE\ 103 | 4j0ZJIBWRAAm6Ed/T5PYwXDfEIkXM8asZLn+FXPun1iutzRcq54Qc9tF4r1cAggu5ib6uyaD\ 104 | BNBfYq7lngsk1pX4U0Z/dzPuqWC5/jVz7h9Zrp90hCSc39N6acXAXNoug1vm47TdnpVBAric\ 105 | qcfoSSKbDTpkkAAWW643jbB++2mH2UJiNUTC0xm7V/r5+kh8VCUG1pQ4xUseHmAyUlyYRRw6\ 106 | B2xjFhvaFjowk60t89hkaLvX0na4ok0uMbufMtZgqo4JnEznRNKwlXnfBuZ9HRhHBWAjU/Gi\ 107 | g28cntF2bx3DtR2WtpU1zOn7Es9hrO8pOh7gLtIoJQ3cI2B6zOMujdjeZW0ORBjHdsw0NFwD\ 108 | r/a6pX1dHQFMJ+oY/T0hgFdiHnduxPZ7He491qaAjbB7NJKYTyrhfJIowJtdQbzapS6ibJ5i\ 109 | +7mZqPD+hAigkHnfOxJXSjwxpnGnRGxfxuHejhGOCNvR1yKiNLaOIwZuS3AH4BIAjBmPxTTm\ 110 | ZqYxZ7vhWj3mWFjTvpZ71sQgaYSFGRwCqJbgBDY63Ps3hlKFA68ytXimL7O54JnPB1kUPYAF\ 111 | MSibwsJIDgGUSWjwEgaXG2Smboth3LHM+z6zaOguskgQQ5lH5z+PEAHg5c/hEMCOhCZQGKIN\ 112 | zu6XI579C5j3LrJcH0Wilh9gM+lFUsadjDHA10w1XP8yobXHR9BPdV7pzt8kYFPIdreFJB7A\ 113 | EId7JzM4cEhLa+nvApK7sbhc9e7vLFJA3DsAmL5fEZY6BvMiKmsyRQBg4m6U+KZju5eCTI8F\ 114 | sD3CA+gEy32NCV0BPM1blnvAgO8TdrW0TdSeQjqB8SLlqaUEHQEUfM8IQNCDPCPxFub9YOju\ 115 | cBwDXyYsn88m8OzY9m9yYJSbWNZxjo+n2ko7x2riYz637DK+J1bro6tJ3JWAHeChiC5bFSQu\ 116 | YowDnf45IceAXv3DmJ/7HzR37hyWWPqbEJcbXJ5BHsYWB4eK40nrBO1WVRIR24qU/n0X/b8a\ 117 | U0SKurNga4Sj6kJR2mfBz7/0ctz6g5qya+iMbxpxvvhC4Rj6J0e+arvleqW4tiWTVzC2lfc1\ 118 | 156ns+p2+j/+wnwLu3wVOr8qEcGk/4KAPohhzmtonImitNdSCb28NyKOsZ64/dck/iRkHyCg\ 119 | /iKcDcJGAGUzQQAmqEiccBqq0KS3JaxF9GuzLqIXXdsnul4m4jMiQSXbic7tgQ6aQHw0wyVO\ 120 | iDD2PGHW6S+NayHDBobUDegKKtH2nEn4UOKpEl8kpucsEb8FEYqop+k47EFjrfKJUweI6RpP\ 121 | MjaOywsjvnzAkyLl56/TpTx2pHeA4wI7QJMEFRg2Tr9XBsYpIfHtrQw9Fz6u9iIVRHIVHZ87\ 122 | Jb4nUn59a440ATxH21QayhkoNgvhYAvtKv2SHKRMNjr46IZscGiWAH7QAGeS0dnXfPQRQA7p\ 123 | JhZH4HP+Pz1rXtjn5DSC0gFWMChfzhUpv8GG9Ptu4v5hRoVi5l8ipUCCeHSkmAuIYmNIhgd8\ 124 | 9AN50RVpRztTpEK9oAcJGoxg9Pm3SIWHv0Gisvk9GPTE5Sng8h0HnfcBuv88CrUuk8HQbox1\ 125 | k8KG0TamvttT9PBkipwuof6/pWDQaRSSfVrMz9WAoo0PhLBBfCHxxjDRwQ0ljoxoAIFB5VxH\ 126 | I0hOyEXCfCcp5lBChBx28ctKvEXiSsdnf0lipxg+gPMlbo3BGDVXYlMuAZxCYdVxwaMU3895\ 127 | 4Mm0g9xNX1weo82vKOZeBYsiLH57ZqSNCZZJ7BmSEDpZIpVcoVC1GwYHBZUsT8AMPFFiM8ZD\ 128 | 9wi02y3xA4mDaEH8uwmyX4y3jPtMyJd/fcgtVwczdV+gBhGm900C72G9iQCQluTNBGMCcSQc\ 129 | xzgCllvs/DMpPq6QMeaNIV5+n4Sef7vErsw5DE3wPZTVEUDfDASGvqDLVOHDa2Mcr7Xjyz+d\ 130 | UtAkBSDgiy1zwLFXlOAcyvnHS6uCa5Auv2YGxBmEc79uEU0/FdGdMWCdrEqGHC7MFvGEwJsA\ 131 | Rp02Qu+C3pohuqLtMLqvkETEfJHKmgLfydqadhCPr1eJgVd5mYPljF3glpg4X5evv6PjWYpc\ 132 | R/OJUXRl1qYY5nE5o/39Bsa6rsS3A/fvkDiQ3N2UPMBYL7PQyfIyylE2sSjwlCMBPMnoExJK\ 133 | C0XbihIvlPga6QU40MEg+nHhcxpzIDHQTX0Sx/F03HSg+Rn1ACszTACcl3N7xDFmqSjegDbR\ 134 | 9zmmOHcGKWBsMFLTvrYDEakAeoP3aP2sklfSKeF0sNDjJW0sjDjOYAcC2GXh4Cs79FVL4grL\ 135 | 3FYY2r8d41pPMu24OT49cyaBw+DB43h4xHHgy9eRea/J03Y6zcfFmeOXlnvyDdcGiFTK3TgA\ 136 | LmpTySpaTmcNLMkwAVRh3jdCRItTxPP9Xdjz/wFMbtvlQowNDn2+4bop8gdS0JUxEoEg6QBJ\ 137 | N8qoCGBjhgmAa4aGh/GoiGM1ZvaxznANruFh8vetMFyzfXTw/2tLFr24ABbdvqoX8XGGCWBH\ 138 | AsRiAjhW3mC5xxQZjMRMfwgxbvOQBJeG5SIVn4BjDAGoRTGsxSD/LpBe3BkZJgCXCNjzYxrz\ 139 | z8KcbuZdS/tbhZuDZnuLUmmJQ1+zRCq6tw7tCr+hMx27g2tGEbj0Vwsqgs7IsBTwCpObrhNR\ 140 | JArCYoN5GPGQOxl9jGAosk7y7NnW+1r6yCebxxKSGMZ4+mzlkPNbkkIPBrCDlrFPDoqBlTx7\ 141 | hus44XYmAVydwNjDDOM9wuxjtcQbFMRUj0RPW7LHYtLYmfwAVGIp6gAca1mzMz17nYGzgwSQ\ 142 | S5STKeDm8n82ofF1VrkqEr9y6GcPqXXHk+/BQWa75z1zdPJmQ9snNO1akLGNY8Y+L2gMAlPQ\ 143 | IkPM4ELiqosZ9xYIc5z8f08ykQrP2isOJW+A36Iugyji60/TSD9nk9ycm9DzY45IIrXawHDu\ 144 | NLQ/QPqNCaS/aSfx58RzuPhNFnz34r1DgSF5pHj5TcIEAPn2H4z7molUzJ8JEKOHeL0HRenI\ 145 | JLzABkQMjYiQGvtwOS2cCnqT5SwJQLqbJyw6h/0Jrj/yL16qcwptSIxSUjCZtlnO9v9ryxk6\ 146 | mullFBb7OGzpLv4QnLG3JbT+WyQ28Sw+gT9mGjNcYbNnLpUSxHGaF/+Mx6/dExW7xOia9aKD\ 147 | cWpQQh5J7TymV/DJZFGKC9YZzJ861zC/N+xeEonqe5mvJFaLLIFhxVEQ7T2em2MopIt5Ma4/\ 148 | PuhWnmPRqBpeqvrW5xEHfylgp+ZgG2oLUeiPR+jFB7El+U24uIyN1/gPcBBm4ZkR1x4fzuMm\ 149 | S6YtOjiXtEY9SYVYn8loQDuFlG4II/9EuGXaFqRxAxf/JxE+P2BSAMMSCjudR65d+T5LIvIV\ 150 | LJM4TaTS00YNmS9LlsG7hVv6Xqz/C8TUG+083PDwHBJPoJe+gvzWjvc9eBE9LEQ8pEGdSy/9\ 151 | gMhCHFCZDDmokYTEXSf4rIl7SCWMJJcLyJK4mdtxNj/AUQ7Z/ABZAshClgCykCWALGQJwA+1\ 152 | RKqKGJIm3yfMlap0AAkBPn1IGOGRXv/mGOeOOb1P9oD9xP3C/w+eP/ViHAcp8eBP91eR8vPb\ 153 | SM/j0dibSeqBg8bVItlqKxyoS9Ia5ozoIRiNVtNcES2FGsIn6mwBaWeGDYrIkr6OigxdoOkt\ 154 | MSlmTIkrYBJ9S+JZIfvOI1vA3BDKl91kp2iSkEIKzijdvFTENUzXm8g/YYXHT/C9LW2SV4mB\ 155 | cA87W0NdiOm7Tti9VfF16nzekOa0lSNVtyLlC0yf8LND4kSukyYsj7cKfuFH7Hzw/4vqKr+P\ 156 | ds9hwj1dDnYwWEObBv5C6VQnpp3ize90OoqQLJsFbB65atmibHVQ5EDt3SUuiEEXXkiGHVs4\ 157 | 2nsJGGFeCUbkGvT/Qy3OIHHCXpUtoDqz8ccWIjjdsv1wtrmXY35gGGUuM4z5dIKL/RzjmUdl\ 158 | 2C+zxAvUDnYBeLSMF8lEFKFP+MRfE3O/0KuPE/r0770TZMxwbF5muadXhpnFSSopoIpDByja\ 159 | jBo4riXmbK5Ww0XpylxxEgEqkFVVXDuY8IIPt4jd5TL48uFm/l35miATCCbDNRP1IxLvCfyG\ 160 | evaLDW10RIMchMtEcnUL0zCMmD0/PCXsFT8h6iEP3yaaIxgyGMa6MkVluGK9o7kGP8ROMT9n\ 161 | CYnfKFUHFzgkllhI/xYqAmhON7vCFeLwil4nklzuSgDQG+h8Ej2S+xFvh+DLYnoBsIy1EYGy\ 162 | 6AwOvaE4vDraMTTnRoZ2sMQt0uxqV9NXbuLSEaKmK3qFo2maiJbZFGbgIST3AwuEzSIbYETa\ 163 | hWQodpL+wB/UYAIdI2TKU9DdwkTBTewxB4eNXyv6uNnSphfTkUUHcyztuykCSkrIKWcig1Fc\ 164 | 6UUsGlU9JOXBXv0Gyek7I1Bwvub3tYatMw1IWYuijPDm/SV90fsUx1MakGp1ZOC3xZYxmlm0\ 165 | qlcwGFwTIDytCR2h0KSup2f3f8UIEStv0B+4QYAirrEkNLCVbBtHrl9hdwAdLPHCp3hdremz\ 166 | QOMG51k0jJ9JvIs8kht5qdxC9zIdacfHoAlcaxnDJZFFKa7UtAOgiFI3YQ7sREm3eyPsALqC\ 167 | Uw1C9ueRSKnT8QehSJiLXuGs/5FIuap9TrsOvHEeFLykF5NiYOzWWq6f4NJZjsIIpINCYjK6\ 168 | W1TBUKP+IuTD6UrFg7E6OWSfKxzF0dUJSR6FJDZHBdv8mkchgNqGe7f4dPm2EqgPhXy4fxmu\ 169 | hVXUHIx5gcNCH+GWZkYHn1munx6FAOoyCAAAs+sTCSzSa4ZrEA/DFGw+VfO7znEyiWwp0C+8\ 170 | HVNftrwCraIQgOkICC7Mb0W86UsEKYF01UXB1b/oqDGDZrOn5lqB5vdNMT4P9Aw9FNJGFLAV\ 171 | jTwtCgGYUsUWKbRMPYV7hgobDDJcg5l6nODZIHJJJNQR9QLHncEVNpGCKu5ag+sCu3EQGluO\ 172 | ciMBmLxZVPVsvyEKdwn8sOUHQln0FyzqVHjgdDDc05q0aleG4Mi3xPSicJxOJ51E3Krtf1uu\ 173 | d8gUAQhSzV4r+NWxOSnp+lmYMWxzMGjMJtVnX1ICPUnHCFy3Olo48omaazZGzSVSqRFx/vOF\ 174 | 3skmDMyzXOcXvA4oBkypRWy1cPoxVbAFTCXFqRTRmukUNZ0sbW/zUqluw8DfGfmFOHiJZZxZ\ 175 | YRVB5ZlSgArw9Q1g0NxuJm1+HOJ44QB2jqci7FBlSCLpYmAkdQD9yPAYnmGu5XprwfT4zlFw\ 176 | rbqXz/Gpg4YMumpT+pf5Dg8KE2nnGDlzyNBXWV6yjUAr+uZ2KnH4Lj5/cTh+bBXmJJQVmIxy\ 177 | KQIYprlvoINCZSyddwWa8/XREOddG2LqogDSpLZnSC22ZIx7A8/Tl5iupcx5xJV7yKQ0W8He\ 178 | aRXGkxvIbFlAKV1+GvKcglHiUXLIhBFlhudewiU4NxRz+tLR8RHnrmvtQF3dom8NWU7gSv5b\ 179 | cqE3wYSY3MNP1pi+iynNHKufI510IWwtvx6UQm6Zd3iqexDbfLKbI8dgtZBjtFV45+LlD2C0\ 180 | rUsOpgc0aVpaxrgWP/FSqfcPErFPcCX2bHi4HqqTouskOnPfEm5p9OChdL9IGc/K0xF2p8NR\ 181 | kRHIEsBRDtng0CwBZCFLAFnIEkAWsgSQhaMQXIMQ4Ixxpki5f8MiB9sznCvhjlyZ7oFVEO7V\ 182 | sM5NiWGOcOpA1ZCLSCSDUyZ8BMvSWHBWRTQTTK9wAJ39A3o/eEaEyXUSKbUz3NJhZoa9v4xC\ 183 | Q1lM2km4kcPsDu0tHHngZDNSqR1kpm3tRmHOuxy0cMURNX8I9Pgro/hBED6KoL38viC0qEO8\ 184 | eEvIw6W/kosmsAzFCURJHD025AJc50hsuhgFV00gFuhWUlvv9GnvppAauryhbXXKKjKKxkZl\ 185 | 0J+RithVC/mVlwzcyyWAY8kOEBUWhnj5d8X4wFAVH8cc90RDEEkacL2zYoc02QBWksqWM4dW\ 186 | DFtCFJjKIYBmjOgTLrzr+PI7ePHn6F/G2AkqSFzF7A/zu8NnABrHPA6vtcwBqeSXJpwUYrot\ 187 | NhBOoZNEOPdrFbgEQoCpGZGAZIJSOEhabYrbu17wI2owv2Gk388XqWgoDjM3lhg0nZPopULv\ 188 | wh4XTLcxgXGVkQfjdp/j198xYer/hWHsqRlKy1Lk6YtdjEl47OWqndBvDIKItVzwPFghdsG1\ 189 | CtYxxMfBW2gniR3wtikQfNevNKCwY3/GfYj4XULiDUSiMwQvVgCOIE2F2sUMjp61MiTaYQfo\ 190 | ofgduQlMhS130I4KZ9mtAVEeojIcemsQIgNKJdp5sF4f0u663bQD2Grmwb49wpcHoDw5btb1\ 191 | MUP5ZKsfTLnyUCblYYrtL2/ZATgVMsBh1/RKF5cczfwKbnOMSk4KVDZ7m9RzSRIip38HQKBE\ 192 | Gw314ezqSl894GKixvRXg6+xujA7lSKgAfbw1zXXNwtzQAN88FCkQWe/RoSuLTJ5lUgFTwZd\ 193 | 2HV94kt7XLi5seGLQ0CKKd0MMrDeyJxDGhDytV4cyuOEtU77/VX2KfWq+3aGyr7d4z3l7ufx\ 194 | KlXd6buvAWXDDAv9Q36Ftuwc2IE4mT1V+QJ1JWM30PX7HZ5vILV51ZIqLy/Du9ACVb5CP8dt\ 195 | CgrxR9EgF84xEc7APwpzpg0dfGXhzHHucVyuVdKALua+Fn1JDwheiBe+sKd8O5IOqpFKPciH\ 196 | JAnY3c8Kawyq7Pt3k4gTAcPWR/G7zev4IToG5hHzWUBb9D5qC2/elxnjd1f8ttQw1+Y+UdEW\ 197 | OTzZx6B9LMzuX+0Vx1PSsM9EAFsNDbv6/h1H/HwnxW823/8O1A5fTksixBoWvkMFTUTpXESm\ 198 | WLsu9BexEbda+g6Gm5mMYUGZP+myvS8JRUyGXxH0hWJbSkNv+gJhZUKI9n0iWtJiVbYPfNH1\ 199 | RWbgTHF43MIiC/GPoH+PE4eCVVQQ9NU3RfA0VShpbrDM+2lxqJwuLH/pTC17xKFgnB3E5B4U\ 200 | hxJ2bRS6eAgfQzDMwkT4GcFTPH1Nuy/JeDLLMVHUmAyKYQ8qchMXG3Lq+kXPFgFX9DS8qnim\ 201 | dpakW0F38hLLvCd5/OqjzmJgR2FO+AAFz+Xi8KRL9UmB5NERUiAOhX9DXXqHRfXrB2yvT1q+\ 202 | gF101u8h3EG/7ab/bydG7HcWhRZSyAdDx6dJPFdzPwpdjA4wVL8XqRg8HF2v0i4RDDk73nBk\ 203 | bhSl07ohFV43yxogwmmA0Kfj90NtEgurEiIq6RPhD/MLmH+XM5RBDzAiXGtabNmrFG1aM77c\ 204 | YUzKXmfpZ76izUAXKxoTaxr63K+4v6vDLgYz/YeUeg74gcTZXiqT+9eawJR0gMtf0iJhMC7g\ 205 | SoOixg9FRPUzSITaQ8xYQ2JuelukBSiR/ifwWy6JQqZUdZvJA6nYMj94JJmSJa1VMIJthD5r\ 206 | iEfjbnDkNaoLc6yhapearRLXEgCIqQNVVDs+A2fwBZov5nVG256ML2+apY+tGgcY067VN8QO\ 207 | UN2RD0rXKN6fgXewydPUC+jt4zSTAPAZ7xvOZhtwCk/ts1yvqvnKTYkcO2dIQlkmoiXb5EId\ 208 | nSKoiGTfJBQTOzRKID8TZHt5XYQ9NbvNEplrsDfooFHI59U9jymn8lCJzyRMAAUmTeA6Ooem\ 209 | xfzyLxPmitrg6N9l9PUjy3VbMkWd6tf0vJ+GeOZvyTCkgjGGdh4Zkx5KkACGB6UAnYGlv8VQ\ 210 | xHXL4oZFd7f0BS62iaUPJHHeYujjJkPbORrjTbOQkgC47ceJ70jzHw97vEJSwHO8eApn+Z11\ 211 | BhPPw44OrkFy+s3CrYjkBtrORjA4dz9n/CbpHFQA0+z/Mvo5ifQK5wd2NuQhfM7Q7ji63pnk\ 212 | +okk86+M4aurKuxp8nQAnUMP0mLm05F0TOBI2UaSVFrzt5Fk/iIadzPxGP91DHEND88hnTwW\ 213 | tR2pdOv59PFbSfExn7byycJWsUINUFHfRarRZvQiUOrk/5hialAUy6cFWCPca/j9oCGbH+Ao\ 214 | h2xs4FEO/xFgABM8Re+PY7oUAAAAAElFTkSuQmCC\ 215 | " 216 | 217 | local mosaic_2x_png = 218 | "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAAGXRFWHRTb2Z0d2FyZQBBZG9i\ 219 | ZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tl\ 220 | dCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1l\ 221 | dGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUu\ 222 | My1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpS\ 223 | REYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgt\ 224 | bnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6\ 225 | Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRv\ 226 | YmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9u\ 227 | cy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRp\ 228 | ZDo2NDY3ODU2OTk0NkJFNTExQTg3RkQ3MTNCOTc2N0UzNyIgeG1wTU06RG9jdW1lbnRJRD0i\ 229 | eG1wLmRpZDo2MUE5QkRBODg3RkMxMUU1OTRCREE0MUFFRjY2QUZCQyIgeG1wTU06SW5zdGFu\ 230 | Y2VJRD0ieG1wLmlpZDo2MUE5QkRBNzg3RkMxMUU1OTRCREE0MUFFRjY2QUZCQyIgeG1wOkNy\ 231 | ZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJp\ 232 | dmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkIyQjY0ODJDRkM4N0U1MTFBNDUy\ 233 | ODc2NjFDRjM3OTIwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjY0Njc4NTY5OTQ2QkU1\ 234 | MTFBODdGRDcxM0I5NzY3RTM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwv\ 235 | eDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+4JpC3wAAOblJREFUeNrsXQe4FcX1P48q\ 236 | IDxFBEQURERiRbFXxB6VKLGXBI0VWywxKsaSqEFj7y22WCMR9W+DqHnYACuKBRQUASmCVOnI\ 237 | +8/v23Pj9eaW3Zkzs7P3ze/7zsfjvd3ZnZ2ZM+ecOaWmvr6eAgICGiYahU8QEBAYQEBAQGAA\ 238 | AQEBgQEEBAQEBhAQEFDdaKJxzyaKTlXUQ9HaitoqaqOosaLaBO38qOiHgt/NV4RjiSWKlila\ 239 | rmixolWKFhRcs0jRCkVLmVZye7n/L+T7F/L/R4XhNkYzRb0Vbc7jju/9jaK3FX2fwubVT9Hh\ 240 | irZX1I1//62ikYoeUPSi43fqoKiXoo15fXRXtBb/vp2iFgXXL+D5PEfRFEVfKZqo6BNF7xRZ\ 241 | H+Ko0TgGvELRpRmbuPVB2jECGPsgRScqWrPI38Gg/0/RZYo+cvA+uyu6U9EvKlz3nKKjeBOx\ 242 | ATDBgxX1VbSTog0E217FjOANRUMVvc4bXuoM4HZFAzM2gefyYAUkxw6K/qWoU4xrIYX9QdFN\ 243 | Ft/nXEV/S8DQX1B0EG8CUthM0Y2K9mDJ19UcfkbRfSxxpWYDaJfBSTwvrGMtYIcdFnPx51RK\ 244 | LIzfWnof7ObXJ5y3B7CaIAGoPiMUjVW0l8PFTyx5Ha/oLUXvKToEG3gaDGCtwAAqYkPeqf6p\ 245 | 6Au2Z0Df+2PGvts9FNl3kuJmSmYPijvvbtO8V0JiPUvRh4p282BcYId5miK71nYmDekYAbMo\ 246 | Ss938Ax8y6N5ovQu8ncY0AbzdVfFaG89Rdco2obv/Y6ZCYxDL/PPNrG1ol0MbAZHMAORwnEG\ 247 | c29nigxwSzTvP56Zmm/YjpkApK6LeaOxLgFkkQHYlgB2VfSxoodKLP58/DkG14bIPZJF3o0U\ 248 | dVG0raJjeCKOV/Qu/9+WGLqX4f27CL/PAQb34httoXlvR08Xfw41LG1CNegcGIB7BnCBojqq\ 249 | bJHO/+a3V9DfYOhZt0I7kAweYbF0ewv96mJ4f0fh9+lqeP9Wmvfh5KN1BuZ4b5YGetpkAI0z\ 250 | 8jEKMddSu1eymJ70O2LxHlTib/so2j9BWzBMvanoFOG+tTK8f5nw+5hKOr1SkDxcA5sGjJQb\ 251 | 2mIAlXb/FWyceJWic0xfMMdCm7B0DzK4//wyEoWO/eEuRWcL9q+l4f3fCX/vySlJAN0zttm1\ 252 | p8gBqo0NBrBmmb/Bsw/nor9m/RHW0kVVKgFA17pVwG6wdcHv4Eiyp0Gb1wvq3s0M758l/M0/\ 253 | MLx/k5QkoTQAL8R7XDOAYWyIyOEt3pWqkQH8RUgVOrWIVGEqJt9BMl6PqxveP0n4m78v0J/1\ 254 | NO5rQdkETmH6uWQAxY7aZlchA4Bx61ihtuCgslre/w8TaHNztiOYoonh/ZM9YwDAxtSw8LdK\ 255 | 45iEAXTnna8U9qUo6CF/NzqsChnAUQKLI4faPC7d1UBMLfaOpmhjeL+0BAC/hwUpqQFZBVSB\ 256 | QyUYAERTHDdtU+YaGAjho3wy2wFeKaLjpgVJI6C0VTg3QPsJtilxLGiqRkhLAKsE7ADdqOHh\ 257 | NBMxrwXr8b9J8IHv9vAjSEoA2wm/2y/5O/cRbLOLQBtrGDJcG6Gsnxp+p3U07llaoKbpABvj\ 258 | lxSF/EJVhl8KTsxqWArE8R0CjHY0/O7FAGMzHMumJWUAWMyIAutF2YckA5D2g2jFk1rSx3w1\ 259 | gTZMzt0nWxrHLw3v12EASwS+5wEUzxkN6xGnQOewSi2BGm7rgSRiHo6S3q2SxY+daKVge0ss\ 260 | vOOJmpOzFCSccEwY3VRLY2ka/9BB454FAu8d9yQB83QYq4Ow4ksdo++URM/rx/p7tcTPSzsB\ 261 | TbPwjv2F25NYgCZ+ADMtjaVp1iEd92SJRahzpIpI0kOFvtsmSRjADYqaV5ERRJoBvJeBPo93\ 262 | uGsVw2xPx7JNSn3RzaGBqM/HBJ7fPQkDQHqjasqfJ+0E9HoG+vxBys+3xQBWptAXCWmmg8G9\ 263 | 9wk8f80kDAAGHBikrqbIvTcwgJ8Due98r6f2YcrPt5UgNI28jhLMrL3BvaMFnt806QfFEQUC\ 264 | XXYWEierSQXAUc6bnvd5dMrPtxUDkkY6upkpvjdO4i6yueE0iTGRtqQo28iFZB4gUg0MALif\ 265 | ovNVX/X/b1N+h+WW2u1peL9OduAZAu+NSMT8EzX4AsAw2JjXFGwTazHBJwDu3Ehg0kXouy3U\ 266 | ZQAAjpSQ7vkJihwa1ggMgB5XdC1FdRF8Q51QOyvKiY4pYSfD+6dr3CPh03AoyVn0daVWY53q\ 267 | c3KTWy8L+iiY4m2e9ne4UDsmufRbWugXdssDDdvQ2c2nUvYxVoIBZBWzLLV7i4cMcakgA1ho\ 268 | cK+NzNH9qHKatDjqUUNkAHUNmQHYOpKCa+dNnvUVDlxSPvgmum8X4X5BFfmzzZ2wDHCKtCjD\ 269 | 8x9BVM9JMYAs5gO0WbPuOpIxEknaJnyQnKTDbmGD2kygHd2KOlMyzACepTLeqzpJQQMD+AnY\ 270 | bf/kST/hsz5UsL1vDO6FsU4qZwIiUQcJfZ8PUvgWaQJ+PFeUu6AhqAC2q9biSPB9D/qJ3V8y\ 271 | UGmcwb0Icd1b4B1+TyWi2DQAMVjXkzCrEgAKhnzUkBnAbLKfnRjtn07pewdKn0p8bni/SRk0\ 272 | HDU/yhNYao4+aXBvFhkAqgpfXOmipB93YQYZgAvAYerOFPsJy/8nwm2auhOjhHfSFOdQGwZQ\ 273 | lPjjaMG+fEVRquyGwgBQPg51J1ZIM4CsxQa4NND9MUVd8a8W2pzFC9EE1/BOXikZ5/r8/cax\ 274 | yN9JuC83GEqCkzM051GXA0lFYh1RJzXULMkYA5jp8FkwCCIf4jDHfcTz6iy1/R9Fmxq2cTTT\ 275 | GBZLp/DOhHBjlFNDSSubyTqRRMS0SGkWJIB5LHHdl0QdTcoAlmWMAcxy/LzhPAAnOnoeBvoi\ 276 | i+0PUXSGUFu9KJ0MU2fHEYUzLAFgU76Zpa3ENTCTqgBZc4iYnsIzz3OoCtxLdkN/kftgEmUX\ 277 | sMu8LNDOUrJfYTopYPM5i9Wli3TfLykDmJGxCZBGYZIFLPLatpdAvbnQgYTxYEYXPxjjuYLt\ 278 | fZdyfxayhIk+waaCiMFbTRlTUhVgesYmwbSUnguPMzhg/NniM1ANeK6DvqCU+R8oWzXyvqYo\ 279 | 3fpSYXWyh6NNC+HcOLnAUSziF5CGDgZS8SPtpAzg84wxgDSNN8ioBGcYG3kDINo+61CKupYq\ 280 | eJR5BCyefSxIqxKbH4yRI/PUChCs9d8zM4eU4dTOlpQBvJ0xBpCmCydUgCMpcj/tINjuO8Ki\ 281 | bRxcy2qN77X1vmCma8NoJ2EDGOmbSpXUBgC9anRGFv9CSt9wM42ZgJQ9ABP7EGHRNg7wvOPI\ 282 | XqYfCdRRVFnHlsU+rfTgXjEAAIEeyBUIP22UDXuBd7mZnvVtskcTU8JYN5t3t7TsGigUc6qn\ 283 | i/96FvvnWHyGRO6Hlr59OJ2IrVWsChRTB5qyuLsm0xr8L4qMwOnjJL4OTAMli1rxNavzz0ix\ 284 | JRVxONGzCboV6bu3YvHvR+aVcUwBLz0k+/ibJ991Gs+pFx08a7FAG62qgQGUAxwuplLxLCq9\ 285 | 8hjACVT8WAW131DEAFbn+5g5wGMMtdlqmYPi5zb8MVvwz2342jbMcDrlGVt8AI7TfqdoQ0pe\ 286 | uXcG725jPenLdbzT3kXp5gx8gOfJ946eJ5FopbbaGUA55CcTLaVL5ibUEtbffXO+MNWjD+Ld\ 287 | apsEUsy+nkkzAEKgP6PIz991yW0UrTk3BQYv4Qcw17dJ6TIcuCaGPtVS0ODiI3CWvJeit2Jc\ 288 | +zHbWiZ62hcsxC1ZHVjh4HmwQRxMkaEvDenuHYE2PmvIDKA2z4ZQKlihmaC+5SvA/PryLloK\ 289 | yO2H6kwzPe8LxGIEoMBB5m6SP8OGpPgUReXTtyN3vg/FgCNlk7Jw2NSGNWQGsGYMMahVBRWh\ 290 | WrCcbQKHFREtsZD2p2ylYJ9E0QlBJxbPTY6Kl/FCgacjsgAfrmiEJ/2EzUE3qxCCdRb4NnA1\ 291 | 9fXOEtkg3nswRS6NvyjBIHLHOPCee5MaBtrwosECupwi41o1AIt3Dx5LVPTZiMd4Nf57PUs4\ 292 | MBjD+PsRM45R5N7PIQmOp8hAnWTzRD1J+G/82JAZwF3M1SHeFssXh0mSczWGx9kXFBDgJ8DY\ 293 | 4NbbPYakdxVFbuErfeyIy1OAnLX4yxJ/3yjv5ylhjgV4DCRKQRKTfqzGwTC5DkWnWDi5wpEt\ 294 | Ivdg55nmc0dcMoBcMMWYEn/PFZOASLgkzLEAz4GTj38xZRYujYBwAtqe9adiQGpt+O//O8yt\ 295 | gIDqswEEBAQ0YAkgICAgMICAgIDAAAICAgIDCDAGAouuC58hIDCAhgWcOV9Jkefc4vA5AnTQ\ 296 | JHyCTAJReA/xv8A74ZMEBAbQMMYLMRWX0k+Rk8B74dNkDjUsgTflsWxOUZwEfo+AKCQ6WRkY\ 297 | QEAOPXnX367g93A1nRE+j7dAjouOFLkOY+yQD2LHGPchcAgu8QiSgpMc6ioiDZ9opGxwBPIf\ 298 | 2CWQgPVq3iUK8ZyiX4XP5A2woyN/46EUpXLrKNj2Eh7vf1IUYWiciCUNCQAiTleKkkjg47RT\ 299 | 1Jr/Bm6EfAGzmPshg8qcBjyZuvGuv0uZa3zW/zG+SGyyGVPXvDHPzyc4mwljjkAaZENC8o2v\ 300 | MzRWSHmHkO7zuH82gByYRzBB6ruDDMuDuZIAkBUYeeWR3GEr7khcTGHRB0UeXyD3FX/T0g8H\ 301 | UpREolIm2f1JpgCm1Hsj3uNI3gVNC4kgchRZgJAkZYLH43WMohsUtU/h2UgcM5ifv9wnBgDD\ 302 | BpJg/p3ksqHCKPIic75hVbr416coYGrvmNe394ApYnxPVnSaog0stP8jT3JUKPIpqw4k1/tZ\ 303 | 3E8byKWBbNuj0mYA6/IC7We5wwgrPl/Rq5bab0LukzgMoKjWe5uY10+ytODiAu+Joidnkpuq\ 304 | N8gUNJSi8lpILLMqxb53Yslrc48YEhjlJSw51rtmAFuzuNbZcaefVHQ6yeeHz00w6KKwwI4m\ 305 | e6mqOvKuf0DC+4ZQlJAiDVEfu83VKYm9wHiWCB4h9zkkMV5IWbehp1LkYxSlLlvuggF05wdu\ 306 | m2KHcRSGnGuSBrF+9PMstDibRWrqETz4IIliEdCXb2c7SVJcxKKxS7RnsfcATyb7FN71/hF3\ 307 | 1zNEC54D25LfeIXHaLlNBnAWRXnhm3nQ4SW8mJ4T3OU+KiPiQdz6kCfDG0xJTizWZlXJRH/c\ 308 | y6IKVAw7sQje3sMJP4olwQ8sPwfMeiBlA7fyGhVnAFjwMO4d61mHV7Ak8IJQe2Aoj8e8Fh8S\ 309 | x5Z1LB3g31IOOihwgSSppmXDcfTkKn04JKKnPGH25XRgqCVXkJ0MvH0oygeYFWBONqcy/gI6\ 310 | DACeTXBC6Otpp1GAAZ5WErX0GrOuqavrTcxjCJAU4ONwC0VHoqbAsdhGDhc/7A1NMzLx8c1x\ 311 | 5Cx5OgKJEC7XW1O2sC6VSUyalAFgAjxPkYeTz8DiQO0BCSs+CnjcJ/ReeB8p56snFB3l4Ftu\ 312 | xwuqRcYm/lcU1VWU8h84gOd+1rA1q6pFkTQc+M4MLH4Ahsk/CbUF45JUmnJJz8t3HXzHtrzz\ 313 | t8jgxO/GjKu7UHsnUzZRVmpLwgCO490wKxgkpK/Cinq9h/1zEQF4m6L1KLuA+Atr+DqG7UDt\ 314 | 3T+D/V/FkhCZqgAdWRduk7EPgCCamwXawQ44mez5eCcFBg3edwstPgM2nlepOoBqwn1I318A\ 315 | qoSUu/U3LJkgwu9b+unkCJvx2systqDIjtXT8FlipwAPUOSlljXUs9gt4TEGj7e/etIvnDZs\ 316 | avkZo+l/Q4+zjKtZKtTBORT52psALswnskoV1/CGMT6DoiCjpACTQTzGMlMVoDvJWK3TACy3\ 317 | Ug4bsH/4UrH3fcvt962yxQ8gkUovzXslvFuPpugYNYnV/VOK4itwbLwowX0PsMpSsVx7HKMU\ 318 | RIjGGR54GPF6CLQzn3XiQR70ybb+f5JwexC9YbTEMSgctlAV+Huef2vwAtuEN5saS31qzLt4\ 319 | X817TQGXdZzcIJYf7uVJ3MrhkQqHNLg+9y+xcdezyvYXbj/eDllBBcAAwZllrYxz/1qSiSKD\ 320 | DWASVQ7RtY2dWK+1Adg7Zgn1EYscGYvvpXixGjh1QOVdxDf0IzunDzql59GH8wTfYRkzcTBF\ 321 | 5D4Yy2pdnOSusBMgUjRXbh1M9Qte9N8mFpErMIBdk3ATjwEx6i6htm5SdHaKfYGHW2uyV0AV\ 322 | 7sUS9RmHsiShG6QFZoCjt/OFNyCI4YcnvOdsHnebgJ0KjmOf5DEF/DuBLMY4VGIAf+MByDq+\ 323 | I3O32xw6sRSQllcc3Dp7k4ynYzHAjfZSwzbAbAcKTdy2LPpKHUEv5100iUTYh9JzAcYOjyC3\ 324 | 0Sz1vZHQHqDNAHD2OZXFjGoAds0fhNrCBD8lxb7AOLStJSngGTLLMTiC9WzpWH2oBQ8JqQUI\ 325 | wEpS1rspSzKtPZjHK1gqH8JjZZQQttwpQL8qWvzEuqUUriM7wSZxgeMhW85JJmm8VvHObyNR\ 326 | B0T3fYSY3u4ai+4fnsxjMKM9KTqVmsKMbHcbDKA/VRd6CbYFvexJD+waNrIBdzK4F7aDzyz2\ 327 | Gca7Y1OaCzeS+wxRldCE12kd05ZSDKCR8I7pA7oIt3cVuUlAUQ5IzLGucJsm3p4vOOjz0xSd\ 328 | c5ugmybTv8bj+Q0pAP4hf6UEMSeNynygdlXGANYWbg873XMp9wkGskfInxqPEx09B74YJmnA\ 329 | dA3CqMX4rsdzHP4K8FhF/EOtCQPYnKoPtRbavMqDfvXhQfcBzR09Zzr9PF2bjuisAzjv9HPI\ 330 | 6EykgRFxNvFGFvTAhgTsBsM9eA8c3e0g1NZcg3s3dtjnV1L61rC6wxHrA8/nJuwBT1AFL8ZG\ 331 | jsRlH2DLau9DgBB2tEdJJlrzW4N7XSYKHWdwr2lMx3fMBJDdyefaejgtuFiHAVQjZltqt46i\ 332 | 5KFpA3abOwXaGW9wL0qY9XLUX5MU7RL+IHDnhYfgdixu+wowgA0CA4i4tg3UeKQyIeLMNHLT\ 333 | NNDoRrIX0JMPk0w/kwTfA9+rDxNOKHw7KkTJ8UFJGcDsKmQA4y21u4VnKhNSjZsUrHjN8PlY\ 334 | COc56KdJhp5xFt4HUsCvKTqWhY/GcIoRjutwY2iZhAGMr0IG8LGldvfyrJ8o0YVU5rqxCjBs\ 335 | TjN8BxQrOchiHyFxmdRTsOmsBEkTruLIIoRj2r4shuPUYmZKc6IFlagwXSoWANxiURUtfrhy\ 336 | Iu58sYW2XyQ/88XBaUX3ePAO3sVMIF2oJR+INDzY4P5tyH5SlVJAZCNcuZH/YGOW1kDdWFy3\ 337 | hXuoSPxKuWCg76h6TgMQPLG7hXaRdHRuKfEqZWBgETeuk9dva6EFgpiA89kuIIXLFV1mcD+C\ 338 | etqTebwCxn43ijxmN+TFO50lKATq6OSf6JTHDHowo0LQl0RMztdUzAMSDKAEHVtfPTizTD9N\ 339 | aHfP+z1NUTvNvtUJvsezijoafuvGim4QeJfHBN7jbEXflnnGAkXnKqoRmGONFB3MbZpgRbH2\ 340 | y50CPF0luz9cRm0F7uzled+RYRbxAjpW+WsF36MfG97O0RRzcbT4Nt9viocM7oVEjICkm6j8\ 341 | yQ/ChhGtKZGRehXbD74ybKdJsXlQjgFAX36nChjAY2TvCHCvDPQfxjidYpawbfxb8D3gin0D\ 342 | i6JI0FnJH7+GRWzk0IPXnUSS0skGfarlxZ/E4/JMQ1sFsSERodBbCvS/PokNgNhIMS7Dix/e\ 343 | f5tZ6gMmxBzKhi/FMtYlk2YRQnm1j0m2olH+zvY6L6rP2GiIk4vObIPYk8wLehTiQtKP6HuY\ 344 | 9HwskOJrC0ruMdiCjXZ/Ir3S8YWYXkxqiVMXYAH5kQlFB3eTXk71OABnH5qhb6GbRQhZZi+p\ 345 | AkkQPvxIpKnjBQjj2QTSd3CCFBa3rmB7nrMDSS6NHTCMojoBsVWAHLKaGARn2RdbbH/PjH0P\ 346 | 3SxCf64SVXAw6bsAb0Fm3o33UPlAKUg++7O6gzR8VwgvfqBoroY4EkANizGbZGiw0SkEprxk\ 347 | 8Rmfk3nppkIgSAUGJiS8wHnx+kzr8b9d+N+OBhMSkkvSUFo8/z3enbKIMSz9rDT4ZqbS3hIe\ 348 | 1xE8zq1ZskBQEY6o17DY/xU8d6brMABiPfpDS7qgDVzEHN8WoKdOEWwvlz//tpi7VDN+h/UL\ 349 | mESXvP+Xyus/h3e0pFF/21OUGTdrlYJX8uIfY9AG+j4qw9IPGM8JuhJADrBo3pKBzsIN8zTL\ 350 | zxhA5mmpcnopLON3klzG4hzalmEQSGihY9CCDvkcpZcSXQdQA01DtvHtJmd08S9i9e8bUwaQ\ 351 | 02VO8nzxS+WjLwek4TrGcOHDGn032SvwYQu/pMjTLQuSADLmHiYwHyBxLSU3UY7SOJMlS5Jg\ 352 | AI3ZUOGjYXAwc3vbi7+GdSkdIw3UBlSphXPOcsoudmY7gs8l4yCyw1ArFf8BK/4BGRsnZAQ6\ 353 | utyaSMoAiMW/B7lhH4Az7tMV/d3R82APSXqeDpH7Wv5uWV74+YA68RTJVV+WBE4tUENAspoz\ 354 | ouneyND4wF5zYCUGqOPEAovib8iPFMk4m93R4eIH9kq48I+n6LTgnipa/MQ6JWpH3u7Ze71M\ 355 | UQiudCl3OCw9Wk2LX5cBAPCwg1fVERY+dBzAi+xmNm586PjZcRgAjgiPoiiiC7v+SqpOQPo6\ 356 | g6KIuC89eJ/reeLbCmVHLMJUz8cEHov7xVV9dFSAQsBCCmOWq5h4BCmdSGbZa3XRlJ9b6oht\ 357 | DNsiniI75bF8BoJ8LqAo/Ne15ygs9Kfw7m8bvVkV8M0IupgZ1D1JbpLwY4dhC5bh/rzz2QJ8\ 358 | opHt5tcpLX5ghxKLHwv/EIp82J9sgIsfgJUcXoNwbrmO7CRfKaaO3siS4MuO+ok8CTAGLvDo\ 359 | 2yNF+lZJF78UA8gBnlIwkKH2+utCbX5BkRMGTh+upPSzFBWK/6NZ8sHHR6XWegpAPsk/UJQb\ 360 | 7xxLqgGYDXwnkBj0XJL3oYijY/ch8xBdifWB9bY3/5wYEipAKUD//RVzS1iK42TNWckcFg5H\ 361 | z5J/acneosh1cwSL+i+H9V55jvH4H8JkUjwEOQEeYSlrjgd9q2W7wwnk1kfgY37uY2RoX7LJ\ 362 | APKBHRyWcBwddWZm0Iz/hoFEskSE7E70WHxuzfYHlAOrC+taG51YlQIh3Hg9pvyQ14UUnTJA\ 363 | t0eo8JtM33vaJ6TuuoJVYVuAlAPHplw8gQx3dsQAAgIaArDJDaDI+7CbQHtggkhgAiekYWRW\ 364 | DCUwgIAAh4CqAz+J3izpgCEgmrKwgCoWIBKJwm72AUs8CGWfSw6OjwMDCAhwi0Z5Cz/1xdck\ 365 | jEdAgFN4ZeNqFMYjICCIIwEBAYEBBAQEBAYQEBAQGEBAQEBgAAEBAYEBBAQEBAYQEBBQNdB1\ 366 | BEJgDNwbEdADf+UpKbw7nn0UEwpHooY6MtSMp6ggyJ1UIhWyJ4CLKMJmEWKM5JrL+X0nUZRX\ 367 | AXnt3uP+ZAlwd92W+7cBRcFfCPZBElUkVMnlU0BWqTlMCPJBaC1CWj/gvs8Ky7MiEHKNgj2b\ 368 | MuFnhEi3o8iNGIVYEbF6N5XI1ZHUFRiTFanA9iiQHtA4cgQiHZEL90Z09HGKClyUAgInzlN0\ 369 | h4cDh5TiD1C8/PpTmKEhH/9wipJg+AQkaUE8OkqAIxff+gJtYg4h5BUViocwU2ioQJQk4ggQ\ 370 | W4Dahj345x787eMAzBb1Jy7inxMzAITz3kqVC27UKTqZ7OaHA9dDHsC1Y16PpBQ3eTSgkJzG\ 371 | 0E/h0EmAMudITHkbpZuMohkv+ON5U2hm+XkoTXcXM83FGV/QWLT7sZSEuYw8AsguNIMlofV5\ 372 | weeoVvDZSNpz2M+YABhADLq7Pj4WKzojZrs69ER9MuB9ulh8n6R0T705Vip6XNFmjt99DUWX\ 373 | KJpRnw5mKzpXUTOPxrMSNVLUWVFfRc8oWlqfLv6S/35xJIC9WfRMChQQ+R3JpmuCODRTw3Zx\ 374 | KUVlrl0B9gikCduaxbacDgxdfx3BHROc/F6KynfbTJaBBJjIwvxbB7t9HHzJkmadJ7s6dukN\ 375 | mbqz7aMrUxdPvlkOsDX1ZPtALAbwEhWpKx4TsA0cKCiu7kd6FX+HGfQhiWiP/Gy/ZPHOZYoo\ 376 | LH5kxf2XhbYhMj5E/mXBxcRFsZVBhXqtJazFzHwjFs2751G7jKkh17AtLxYDWGw4+N/xonhf\ 377 | 4MV/w5MxKT7k3VgaMIQeSlH9tV08GNiH2U4joSe3YOniGM8n8zBmUgstjO1hTLtncJGXAypb\ 378 | bZHrZDmsIcD527Oo1ldoUHTQ3MJHhGoE6/STniz+HINEZplOhu3AwPp2BhY/sC9FabEljWWd\ 379 | uf+orffrKlv8wCamC0rH8vm8EBPQQSvBttpQZImHXWRLDwcX5+/vsGiqyyyhZvXK0ISGH8iz\ 380 | Qrp2C5YqtqfqRWPXDCD3YdNkAhKA/gfnnKM9f891WerSYQIDmYlkDRDTJY57T8vfIasU36TB\ 381 | AHJM4BkDfTzNdEqbs1i4UUYGeV3eyZKW8D4+wxMbi/cgwzaOperH8LQYANCaRUyd3WlBiotp\ 382 | eAZ1QVircTLQNME9PTM+ue8yVPk2q/LFj2PAG+IyAFtHPzAMws2zbQY+GHwOUBCkY0YHHKLx\ 383 | oATX/5jxCQ4D6PkG9zet4sW/iiW8cXEZQHOLLwNReoijD27yjIspMjJlGWAAcY16H1XBRD+b\ 384 | 4vvJF+LbKl38OApHDM9jhbtbObSy/FJ4Ifi1n2L5Obr9gOh/URUMfhMWjXekysFa8LeXsoAj\ 385 | cAkRfpMo8nX/gTcdnKR0oJ8caqRVUXhi4gjzbo17UXbr6IyPN3wiPmPCwn+Vf/4fVHIE6sUN\ 386 | 2MaJiv4e47qDKQpoSIr5FPk0JAXcX8+qol0AxTmficEscNJhesSJsYpTzgpqJgquwlkMnpSd\ 387 | hfo6kttNCgQ3/Tsj44lIUbhFj2dGO44X+uS4DVRiALuRYCHCCoYJONO8W+E6WHifc8QAMDER\ 388 | d9DasG8/8kAhvj3nodeGddUOjifMfz3AKgD+6/AlaG/wrP2YASSVVOB4cyXp+zHkUM/fVyev\ 389 | AAy+e3uwwDF3JlDkSl9IE0jA47OSCtDGUUfhwAFrde8KA7bQ4cff12Dx4z3vZxsHmNqyMraJ\ 390 | TtxvEGIIUE9uNUt92px3xbcrXPcNqwGYaDUGz0rKAJDE4kmW8q6mKJ+DLmp4A9OJjxjAUtA6\ 391 | KTMAqMe/t/mAOK7ArrAeD35jT8SrvTTvw8TZjAfuzTKLP6cjY7HhlAGGun0oOhk5lPQiMOPg\ 392 | pJjXTWKJwYTZmEiEsORfZdjXTTXvQ3HOvpRuzgWg1vYDKjGANR13GEbBwZYkjKTQ8YbDDnZY\ 393 | Eh2sCJbwrrUvT8JJwt+iH8U3un1i8JweAu/6mOH9JvYE6NMI6UbEYSX/k6kUeV7CjnURM/Ct\ 394 | BFQ860fPlVSANcg9zmf98ynBNnX8GXTqu38kvGD/wzspcgeeSD8F+UC/fZ/7lXSXa8uqxuiY\ 395 | NgNdbCzQ/yNSYPz5wML/o6LLeHMCU8OJ0nxe9BOYykl5OPnQPZJslzYDWJPSAY6iPqUSRxeO\ 396 | oGMA+8LCe/zAE/Ay3lGw6Gfz73XzI/SJyQA+NXjvNXkCz9ZcuFCJLjX8dlLpw5byd9b51tNJ\ 397 | 3328k+1JbssGgHPH7wzeC1wWhqBCI6TvXmqzLbc/kyWMXJYlGNl08i/GlRpMMxKPZ5vINTEX\ 398 | AaST0ylKJHOpwPea6sGcMMlujE2ocZoMQFcCQJz8/mRmtYe4heQf+VbohZ4zANepn6AKDNG4\ 399 | L66/+9eGTLct21Iu4EX9AM8LqAdd+W9wurmCNw0wuNs01a9S8zBtTDOU0NfLogQwjz8+cvab\ 400 | RPDBmeTiDO3m66Xwni9aFC1zpxQSwE42gN93HDMXSAeP8m7fl/TrVJQS20d4wABMv1+3NBnA\ 401 | WprtzuV/X2DubwI4hRySwsDpWPLTiCN/X4PJJvHv+IKyCUhGSzx4D9OjxA2yyADyd8/rWfQz\ 402 | wSMUHau4hI4FvGsKUgAm+YSE9yQ5Ffkmg4sfqtENnrzL14b3b5QmA2gnwACAgWSWFLQlSxNd\ 403 | HA7cKAO1xTVspgTPIgP4B7mJYYmDiYb3b5IWA8DZpa5Ra1YRfQw+3nMM3hVumQ85HLg6zfvS\ 404 | yKiTtFzY8ipmAIg6PNej95lEZobUzdJiAGsbtDu7xEQ6gsyMgrUOBw6GKp0jNqgqezqeZElV\ 405 | tSS68ZQMLX4wwkMtS0RJAWZrYkfpyhJwphhAqQFA+uZBGZpQuq6ofyV36daaaeiJSU44ZmZk\ 406 | rLCxHKfoLQ/fzcShCsfgm6bBAHQNgAsqiKRwChmakUn1oKb4Blfbkxy94y4aqtqkBNdmIUPO\ 407 | cpYun/T0/T41vL93lhhAJc8nWGh/S3l5yTwGFopuTAJOP3o4eMcTNO5JcsS5iPw4TisFONr0\ 408 | IT2HqMAAyvxNNxlEHPESHn39SbZwqC3oRifCnRnJS2wmPkWg0FGa9o2ki8xHIGoSWatGej6H\ 409 | xmSRAehO3LgGmM9ZEnCBZQb3IsLvGc174fKKABIbxsuWbKPQsTW8nfD6uZ4tKJyt47j1UDLz\ 410 | tXeFCWSW0h6MvrlrBqA7aecnuPZptgnYxlLD+881YCLIKAw/93WFFz+qLOkcEcE+817GGQCs\ 411 | 6mMpO8iFb+uiiS0pIG0GAOBU4GXLA2AaRYgd51pDEQ6xERIlylGGHE5Ke2jeP0qDIc7xbEHt\ 412 | y3r15eRf2fJSMK2OvUu1MgAsziPJrlFQIooQR3sm+QnaszrwOOklvFyfotp3H5FZuq2nNe6Z\ 413 | 6+GCQt5E5EgYz/OnpsoZwM7VygBy9yAttK14+nqBNpbwRFtm2E6O2T3NOmy7MmIfnIoQH4+4\ 414 | fwSVoOCFSZGTFaTn27DI44W1HjPVNyiqe+Ar3ja8f0fXDMAkFFhXzAYTWGyhn/OF2hlLMm6m\ 415 | CI1FhCOOGGHEwrEcsgfXUeTDPpnFdKgNiI/fh2QSQ/yT9BK1mJzWwAN0uYMFtjMvMpwMbOwh\ 416 | A8CYmpymwDGvRxYYgMliwyL4NSX3bbc5gQtxB8nHJGAX24aiOn69+P/SmWCgal2dwvfDhrA9\ 417 | uSs51p/tA3eQW9fxOHjTNzXAFxUgHzAIwqVTshS4tAh7MvmRbCIJ7jGwYZg4AiH3AM7B4R2J\ 418 | zD8rHfQVzBOlwnHasaFHY2DqprxTQ2AAAFw6fyPIBKQZAETaX1GUvTgLgPh5UUrfr1Ge/eFy\ 419 | RVuTuUEsLmBsRRh5S0/GYWSWGICuVXW60LshVdQRQvqjDWeR+aybv+v54sf3O1LQDiJhR9lB\ 420 | 0SWObAOwBwz0pO+w75jYuHpIq4blGMBszck2WfD9hvAiW2DYzhhLA4pFhRpydZ4ufkhQvxXY\ 421 | eUxOUYqVV4MagKo/vR1JA0d5Mh7o92iD+3Ey1M4VA9DRV54leQPeCBZ9dDOr4H1sRh+CCcAx\ 422 | 5SHPFj+YMUpkPyHURxN9vBQ+YWngPLJz+pPDph6Ni+lx4FxXDOAWDe52paWPBqsurOQ6lYHR\ 423 | jxkOFtsARWeQuZ+ABHD0trvQ4je1ASyKMW9u4EU6zNL3aOIRAzA5CRgrrTaVYwCvKbo1QVuY\ 424 | /B9b/HA4TkIAyDkU3yo9inVNV7idomO80SlNLkg78BvYgvRzGhbDJIN7Jyd4xn4srk8X/i4+\ 425 | ZTWCRKt7rPqg9MtUiiQ7m8Wzclz8axaB73bw8aCLwh12yxh697P8XksdDzC8/HBeixqHrgqZ\ 426 | gCHimA8JJM8UsJkUAqnRdKsEJa1yDKmlJ4+zVCWo4R4xAIzVvRr3fc7MXRQ19fWx7DuoEHQ4\ 427 | T+wOLIZ8yVLCS5ROyS6cUsBp6CS2EazOuipcQu8kvYIZ0oD/P+oinEpRfgBpQIdGyvT7yH4e\ 428 | vDM11EKoQz1I3zC8BT9zd8N335zMKh1LA74R8FGIm8oN3283spCgNS4DCDBDLdsIIN5ub9AO\ 429 | GC18D/5N0QmJy5BYGPOep2QRjXDGuUvg2WD011GUIDMpsBkM9HBOdOYxrDQf4Np8ii0GHxiA\ 430 | e0CCQhmsbXl3RJRfF/p5tR6oXDBczmbReyzTSAvifRIg9PZmqpzvEO9/hrDOuhpLISgVF9dN\ 431 | HeXVDyB/U5pBBT+aCXkjkIZvJauRdfz9rB6TBgYQoANM1tPZxtIh7/efsu0Ffvi2komuyUzg\ 432 | DGYKpXA/M4zFYbgCAwiwh1Ysvcwht0eg6+YxIRwhImUWou1eYwY0MgxNYAABAQEVdJCAgIDA\ 433 | AAICAgIDCAgICAwgICAgMICAgIDAAAICAgIDCAgICAwgICAgMICAgIDAAAICAgIDCAgIyBxc\ 434 | 50oDw0Gu9o35324UBXV0pKj0ESK9EHJamMcdab1RWQb1Ah4kN8UldNGWotz3yFqEDD2o8oOQ\ 435 | X2RzRdBMfm0/JDBBqa6pFIWAIqUakrEiqm5VmJ6ZwzoUVW/uyXM8N7fbMWG9tS6x8SLXA1KF\ 436 | IaAK4cuIYkTinYX8N8yV+oJ/8fuZFCUKeYU0MnnbDgZCIoxdFfWhKPsrFsXqhm0iLh7lnyZ4\ 437 | NPBIb41Cn3vz4jetVAumgLBaFPIcQTLFTQPszG/kG+jLczzNKkTYMJA0FxmovkyTAWC3O4yi\ 438 | BJ47WVIzsGMi+eb3KX5whMEi5/6ZzPFt4QtFN1IU3748rDkvsCuPO6pDNfPs3X7g93rNJQOA\ 439 | WItqt0i9tLujjj7MC9A1kBoLGXEup58nw7ANiHmoTPx0WH+pAZvOTQ7nuC4WsCQ60TYDQEaW\ 440 | U1nsWMdxJ39kaWOaw2dCr0MBkO1THFyIeSeSnXJnpQD99SAWdWHX2Ih+qviDRCBIW4ZS5qjD\ 441 | hzRckpmYoUN347FezrovbCQzHPYfTP8yijIRNaZs4HkeMysMADru8bwLrpdiJ/EODzp61oGs\ 442 | k7f2YHAx+ZEo823Lz+nGY3xEAlEXhiikiL+FbRm6NhWM7X5l9OqxLAUiI/I8i98ABul/UbJk\ 443 | qL4A4/d12SvAABLShoper/cDgzXeX4eOUfRjvV9YpuhIi30+S9FSg/ebr+g8RU0SPLO3olcT\ 444 | Pmeeot8ramzhGzRT9J/67OLESn1MaqCDpfsjNoL4gBaOdv6HyT+fCezIj7I6IA2U6kL23+YG\ 445 | beDIE6m836TK1nHYkFAs9B1WM5Kglo2kr7OaIAlIMn0ybLPYoNIFSSY1Smw9RXYKXOjC9ikA\ 446 | fBUeIX8dphrxJD1csE1Yt88RbG97tg/sW4ZRPM/6tcl3xokTSrVvI/TeOGoekHGjZWMpBjBY\ 447 | 0V887KDNGnywc9zLO4zPwBj+Q9GOQjrjtRbeEYsclZoK6wlgM0GRk32EnoNKTK+SuZEWks/1\ 448 | lH1MkWAAOHr6o4edQ975/1hs/8gMiX9QB57mBWCCS6l8rn1TRoX6hafl7U545+0sMBvUAtzC\ 449 | oA2UVe9aBQxgRMVdrsIpwB4UuRj6KAIfx+K5rck6nlWALOFFtlnoHO2g2s5McuPYAgs/jlQv\ 450 | tPiMyawO6ByXSqoSaQE1MnczYQA47kJF0nU97ByOmM622P4BrJdmEb+jyGswKY5lVcIFUMa8\ 451 | CZm7TFcCJIH9EjJEGBK/yfjiX842kYplxcrt7Jd5uPjhANLf8uLPSRdZxd8octxJit0cvmNT\ 452 | B4uf2LZwWsJ79s344l/BElasmoKlJAB49X1lUR8EUEByEnNbEJxG4MK4kGlpwbUI/vnawQdE\ 453 | n+HM0irDk+B2imrnJcEoStfD0Rbms7oxM+b1MPxKHa2u5DkONWQuL84VPLdzDmUt+ecc1bId\ 454 | Q4dB1ik6j6JTl1goFQ48UHjxr2KOBAvtSP75W08nzA4WFv8Yis6pEfKbc2GFwW5T3qU2Fn4e\ 455 | 3LNvomQRk12pOoEF9VdFJ8S8/hcCz4T6CB+It0g/dB3vjdDyNZlyzKEZz89WLEkt5rUEg9/U\ 456 | pA8pJgGA80wREv8n8m6EOP5pQoOJo6o1mHPO5nanCE4YnEdfJdTWSxSdoIytcB0cqwaz3iYF\ 457 | xCwMSHD9j1S9CWLQt54xGeJUw7k/TNH+lJEQ7mIMAHXr3zFsF+IWjg+foOKJLVAHHVZWePJ9\ 458 | zxJBYRnnzhRFX23N/+L6UnEHEK/gFz+UaY7Buz9O0RGgKa6k6Fgt7kTA4ruQ75PQj1fw94or\ 459 | +lZ7zoG7WTKqhMVk5mGKuIl/ZuarFPEPvtDQ//glRWuV8D3G7x9UtLLgnhWKPlVUp+gTRQsM\ 460 | nr+YYwTW0vT/Hi3gg/20gf/5cYK+4IMSPFcH09jffLkDv/bXDO9fqKiVpe+Qj20dxaeIUDEJ\ 461 | AJFP/TX5CXwGcIRWLHFFJ96luzjibVAPkC/gxYT3QUc3ifPHB+1BZhmLrmJVxBRQjTZgETiO\ 462 | wSppqGvurLmfoiH083Rnknibn3OyojsM2sFR56OWJSGcInzBPzejn9LbtaSffCzasMTXiH8G\ 463 | mudJHq3yvmUtS4Sw1+Wyaa2WZ6Nbnf8Gw/m7bMScbqICfMLGqaRAJpLuJUTORjyIrq3Mq9ii\ 464 | +0CCe5YYGkBHCujyTVkt2lzgG8Ax6IUY12HSdNQwduVizg9lW4+0HQHMa6s8OwqMm7rHwC+z\ 465 | fl7NqtB8Zsivx9U7C6Gb2OPhMvrmEZTOERP6h3jxJL7mpqcf7wu894qY+mocxLVnzNRoOz+/\ 466 | 4xCy49n3eIER9WLSTwayB/1vwtlCLMo4A6jlsVhDlwG01Xzwq2X+dnyKHwR9hGecq0QeUl5k\ 467 | kJiGCbRzUEzRXGdRFcYewAnpGeHveX0RI92tmm01p8oOT7Mp+0CG7cN0GYAu5pR5xs4pfxAc\ 468 | 65zh6FkmKcubMOeGOyqOrYYK7Qh7xrhuvEbb3YvMIbgiS/l4wKFlTJHf32/wnSt9i2lUHdgs\ 469 | 7oSTwmplJmBLDz4IQlEHx9DxFuQZZnSA1OcDWOJAv1txe6vz/1vyIm+V9/da+rnhRxoHsv5b\ 470 | Dp9rtNuMGdVnBRsBounqBN57aBlpZURMxlaIStGH8DbdsQoYwFJdBrBSkzFsXmKSrebJB9mA\ 471 | uWIlp5wlhgxgAPmXSKJPjGvGaba9QwEDIF6csNYPNHzv5yowBx0GAJ+SmjIbwWdVIgFoGwGn\ 472 | aj6wVNLE6R4ZVuKkm6oWETAfm7JeWEnc1rGA713i9/CAnGzwzlAjPi7z95c1221D5VNlfVwF\ 473 | 4w3p6yVdBqDrVrt7mUk2zJMP07uBMoDc+FRSfXSkAJywFPMfwLGwyUnGGxX+PtFgs9qozN9G\ 474 | Z3iMceyNzNUHU8zScsVEfVixdZJ+5gpmXF3kb0gw2d+DDxTHv+HbKmUAOIYdUuEauIAnDYbB\ 475 | qRHcx0cV+dtLvFPrpNSOI8LCQNhZo+1ySUoRlfplBSYRBwtYnV7I/8/V/Msxx5wRc16e6v1D\ 476 | nhq6jH/O1QbM1QME4GiXc53P1RDE3+DDkygBSpMSk+BYzU6fyYt9UZHBvJf+Nyeca8TJGju+\ 477 | ShnAljGuQQZfnWpLB5dgAMC5rCYk9TKM48GJCX+gxvt2rfD3lwUYAKIB/+L7pGhUYhLooiOV\ 478 | zh+IxAx/T7m/7WJOqobKAOo02y535ozThdsStvckxfOn+MZgnpoyn0oA4+uQRQbwcZ6ooYOL\ 479 | KIrgKwTEmBN5NxgeU0eBR9xYnhCX8USDuDnS4jf5tEoZQPsYE3+CpgqEEO1yx2uXUHzj2icU\ 480 | P4uPrr2qkkEUTm1zDb83jnofIM9LiTUpsVChK/7OoE0c0eB4qFhQwitMOCeHgxCcSTrl6Taz\ 481 | eBKO4wlZ6PBRQ3qxCnGBZyNEea0qZAIIUpoRY/L/RqPtI6h0GDl0W7jh3lVGWoCO+yDvnHFL\ 482 | fc21xACw8TxBydOJFQJxB0hcOyBPp/cKpVKC9SHzlNs4T0Vk1FThd4Z0oetvPyfmwv4/Td0y\ 483 | HzACfc2TfzHTPLaPLOZ/5+f9/APfk7s2lx4NPy9hcde08g30+4crXHMM6WVbBrPvHEOyQ7ru\ 484 | /szEW/Ei/pA3nUkJn7k56R3bIVqvUhamXvxeEviEpaAXyMxTtFCdbcfzuS3/m0uU05w34jks\ 485 | 0Y6gnwyQFSUAAEY7HLNsaPCCm/COcLihXaEQJnEFcc+l3xJgAAuZWa0S6vdUAQbQNcY1w3k3\ 486 | TpqUBEFkfVm6K4ePSe6sfaHmfXFSvo3hebuLwHvCAe0Z3gA+4Hk4gymnbi9hlbw5E94xlw6s\ 487 | LS/2DqzKtaNkznrYYJCZ6/JCRlCqEUzaKylZGG2pSQFmgoIQV1CCOOUS2NFQLPsoAQMwBeIP\ 488 | dqUYxRli4juBNuIwgFk8SXtrtH9MDAbgA1aPed01Qgwg3y7QN4X+gplcoGgvpv+qTuWCgR5h\ 489 | KcBYzVB0CkVZhh/kD5DU5x1cbxBPLhOjSlxJ5N1SIlNCSBbunCvQRlyrtK4VvD+ZFRT1DRDZ\ 490 | R1dRfyCRPhTHBpADDDY28ptB3x3FouAk3t1yRpKmLOKswztWL9YXTSMX61lHjevpBycW05rw\ 491 | 6FMnMstRmAMq9poW7cRk3iHGdbhG96SlH9tQXKAd6VX+WZlgE9qVYvrVZwiY18Mohh7xFO8G\ 492 | v7QgguXEEVf4NyVz85VgANgNYVG/SeD95wm0EfdkA7Yb3ZOQwx0yAF2DWhL9GS7JD5PeyYiv\ 493 | OCHHAOLsqjgOnFkFnb4l4fVS8QsnC7WzwCEDgA1IN9hmP3JT9Qdo5ug5KLYxvYoYwG5xbAA5\ 494 | wFJ5NMkdX6SBdzT02vEk4xb8C9KLrbCBJLaXlzSfAbF8C0f9cZVnAlmCjqfqSZ3eIQkDAF4j\ 495 | uRx1roHd7AzNwRsi9A4SUoCECrB6gmuHG0x4VwxvucN5BIlwUJUwgBVJGQAAP/4LMthZZAF6\ 496 | V/PeoULvAGNqbca+G4xrH2ne29nRO35Hen4WCw3m0gNVwAC+1mEAAJI+DiQ55xbbgOHvUoP7\ 497 | 4XH4lcB7NCdzg2IaSVV0vUFdicpQS8do3DfGoF+IaH0s4wzgZV0GANxJ0anAPM87+Q7vvD8a\ 498 | tvOo0PuYxi98IfAOSTP0vK35nHEOx/l+jXtMdnHMJ5SPvzmjix/vf5sJA8jpQ9uTTA58G8DE\ 499 | Raaa+QJtPSL0TqYWa0RFmhZBTWrZf11jN4fvw3MOxxo1/0YluB7Heg8ZPhMS8O8pMgwuzhgD\ 500 | gEfuBFMGkNuR4DCCIIflHnUQO0JfocWf6+e7Qu2YTrrBhuLyjRo6dlKHILzjXIfjjX4hbmN4\ 501 | jGtxEnSQoAr7IEXeda9kZPFjbfw8Y5dQkcGeiobWp4tZivpbKqJ4jOG7LVHUTuA9agy+81ma\ 502 | z9w7wTOeUdQ4pUKX+DYHK3qhoLjsfEXPKzrI8vPR/hv1fuIzRUfGLQ5qgp1YInDpDLKC7RJI\ 503 | v2SrqgskpTrSP95CyaxrBFUJGGPPjPmN4XaNGPt7DZ55FkWuyI3LjAFSYF1K/viL5E5d5jt+\ 504 | LlzXUY7tEIryL9gEJG846U3nfi7II6wF5LZ4j8qkwpdmADkgnxrO3o+iyskXdLGQRTC42X7l\ 505 | YGDbs068ccL7UMbqbJK3jG/DeigCcIrVs8cEeJi/zxSB56FA53msXq3DCx3GPgTM3Edm1ZCr\ 506 | FfhOiGBFOrbuTGvxmmhTglnP48U8jwnjOCOPcgt+poSqZYsB5IAdow9PUkycnobtLWFd71mK\ 507 | 4hR+cDygbXlBHRtj98Wig9/EE5bfCX7t8LxbnycVAo8m8uKsD2swIE0GUAhwPqQB24R3UlAu\ 508 | s0ltgUg5jUUY7O4fsigjFaZrCrw/gkP2Ya7emg1LeFfE0sOB6GnPjKMBAakzgICAAI/QKHyC\ 509 | gIDAAAICAgIDCAgIaEj4fwEGAMmhp9jyY6E0AAAAAElFTkSuQmCC\ 510 | " 511 | 512 | local toast_back_png = 513 | "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAGWElEQVR4nO2dW4hVVRjHf6Oi\ 514 | I5mmRopdVEzJjLTbQ0WQJUGQDyIUQgX2li9FEAQ99thLTxW9FUS9SBD01kNQCBk9VGqWo+Ml\ 515 | nVHHMW8z5e30sM7kdjxz9nHW962z96z/Dw7jbX97yfqdb61vrX3paTQaiHyZ1u0GiO4iATJH\ 516 | AmSOBMgcCZA5EiBzJEDmSIDMkQCZIwEyRwJkjgTIHAmQOTO63QArtm9b3e0mtGXzh793uwkt\ 517 | 6anTdnAHndwLLAEWN38uAuYD84DbWnx6gVsLx99C6y/FZeBC89cXgZHmn51qfoaBk4XfnwIG\ 518 | gCPAUeDSRA3uthiVFKCko28H7gNWASuB5cCy5meRc9MmQ4MgQ1/zsw/YA/xMkOMGUkpRCQEm\ 519 | 6PBpwBrgEWAtsA54EFiQrmXuDBFE+AH4DthJyDD/4y1D1wRo0ekzgSeAp4AngceBuYmb1W1G\ 520 | gR3At8DnhCEE8BMhqQAtOn0psBF4HlgPzE7WmOpzCfgIeBc4Dz4SuAvQotPvAV4GXiSkdtGe\ 521 | PmATsAvsJXATYFzH9wKbga3AM0CPy0mnLmeB54AfwVYC83WAcR0/B3gdeItQmonJMRf4hpAx\ 522 | W1YOk8U0AxQ6fxbwNvAmsNDsBOJLYAvYZQGzpeBC5y8EvgfeQ51vzUvA/ZYBrfcCZgBfAY8Z\ 523 | xxWBHkJmNVv6NhGg0Jg3CHW88GMLYTXUBMsMMB14xzCeaM0sQhltgqUAazA0U7TlFbAZBiwF\ 524 | uNswlmjPw8AKi0CWAiwzjCXKecEiiKUAJkaKjtkA8cOApQDLDWOJcp7GYCXXUoAqXowxlZkD\ 525 | PBobxFIAVQDpeSg2gKUA8wxjic5YFxvAUoDcrt6pAtHXU1gK0GsYS3TGSoirBHRjSL1ZQLi8\ 526 | fdJIgPoTtf4iAerPXTEHS4D6syTmYAlQf+6MOVgC1J+oy+4kQP2JWoCTAPVHZWDmKANkTmUy\ 527 | wIXyfyIcqEwGuGwYS3TOnJiDNQRkjqUAfxvGEp0zK+ZgSwH+NYwlOidqG95SgFHDWCIRlgKc\ 528 | NYwlEmEpwJBhLJEISwGGDWOJREiAzLEU4JRhLJEIZYDMkQD1ZyTmYFUB9WfCJ5F3gqUAJwxj\ 529 | ic45E3OwpQDHDWOJzonahbWeA0SlIzEpzsUcbCJA4amVJy3iiZsiahPO+nqAv4zjiXKi9mCs\ 530 | BThOeEWKSEf3h4ACA+hR8KmpVAYYMI4nyqlMGQiFd9yIZFQqAxw2jifKiVqClwD1J2oJXgLU\ 531 | n0plgFHgtHFM0Z5qCFBYDTxoFVN0RDUEKHAILQalpHIC9KPFoJREXYrnIcABh5iiNaeBqzEB\ 532 | PATY7xBTtGYI4t4h6CFAn0NM0Zroy/C8JoFRaUl0TPRVWB4CXASOOcQVNxJ9HaapAIWx6E9U\ 533 | CqagkkMAhHmASkF/orffvQTY6xRXXE/0NZheAuxziiuup7IC2LzcXpRRySoAwoaQHhvnT2Uz\ 534 | wBW0IORNgypWAYVScC8qBT0ZwmDBzfNBkXtRKehJ9D4A+AqwyzG2MLob21OA3Y6xhdHd2N5D\ 535 | gDaF/DB5IIenAP8QdgaFD9XNAIWJya+oEvDC5FZ878fF70aVgBfVzQAF9jjHzxmTG3G9BdCe\ 536 | gB+1yADaFfSjFhngHHqZlAdnMXo/g5sAhUpAzw2y5yDELwNDmpdGHUSloDW/WAVKIcARVApa\ 537 | s8MqUAoBtBpoyxXga6tgKQT4LcE5cuIDDO+7mGEVqA0/JTjHVOcKsBP4BPgUbCaAkEaAY8Bn\ 538 | wKsJzlUnThAWc04S7vEfJjzybYTwEs4Rwo7fIeAP4LxHI3oaDd8J+vZtqwFmAh8DWwkVwVSf\ 539 | FF4mTH4PEKqgsZ/9wFFgkHAL3U1h9a0vkiIDQPjPvgZ8AbwPrE10Xk/OEVY6+wqffkJHHyGk\ 540 | 7bZ4dOjN4p4BxmhmAgjf/g3ANmAjMD1JAybHIOHbe4Brnbyf0PGlT+aoQgeXkUwAuE6CMRYA\ 541 | m4D1wLPA4nF/7zlcXCXMT/oJ4+whwrd37HOYkjRdhw4uI6kARVrIALACeAC4F1gFLAUWAncA\ 542 | 84HZTDxsnSeMvWe4NqkaJkyyBpo/BwmTr0HCEnXpCy6mQie3o2sCjGcCIdyZ6h1cRmUEEN0h\ 543 | xUqgqDASIHMkQOZIgMyRAJkjATJHAmSOBMgcCZA5EiBzJEDmSIDM+Q8AomrrsCk4HAAAAABJ\ 544 | RU5ErkJggg==\ 545 | " 546 | local toast_eyes_closed_png = 547 | "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAB3UlEQVR4nO3cMU4VURhA4X/A\ 548 | 2oIFyILA1sZlsABwAWyEVihYjpaWtiY8mqfEECnnmpzvSyaZ5Db/zD2ZyTSzHQ6Hoetk9QCs\ 549 | JYA4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA\ 550 | 4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiHu3eoCSm8vzt9fvv+00yYvdA9i2\ 551 | 7c/59cWHv9a+PHzfe5xdHa/348xczczPmXk/M79m5sfMfF4x064B3Fyev9r0mTmdmcPMPO05\ 552 | y0Jfj8d/YcUr4G5mzo7nvzf9dmYeF8yyq7eecKv+2LoigE//WvDb2v1tbnqbz8A4AcQJIE4A\ 553 | cQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJ\ 554 | IE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4\ 555 | AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQ\ 556 | J4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSDuGds2Io/iw+DXAAAAAElFTkSuQmCC\ 557 | " 558 | local toast_eyes_open_png = 559 | "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAACHUlEQVR4nO3cMU5UYRhG4TPG\ 560 | DegGVCq3YFgACSHU9nYuAkJs6AyFBS0FHTRQuBCtjImbIHQMBSROCMZuPsI5TzU3U8xbnPvP\ 561 | zRSzWC6XxOvF9IDMKgC5ApArALkCkCsAuQKQKwC5ApArALkCkCsAuQKQKwC5ApArALkCkCsA\ 562 | uQKQKwC5ApArALkCkCsAuQKQKwC5ApArALkCkCsAuQKQKwC5ApArALkCkCsAuZfTA0z2d97+\ 563 | 872Dyz9r27GqE2D9XgHfgRvgN/BhcsxaT4CneAcM2AO27l9vAKfAu6kxEyfAa+ArcAF8BhYD\ 564 | GyZtPLh+M7Li3sQzwCl/74Ad7gL4NrBjyhmwu3J9PjUEZgLYeuTaFMAJcAVsA7+Ao8kxE18B\ 565 | P/9z/WytPOecAZ+AQ+B6ag/MnAAfgWPgPXAJfBnYMOapPexOBPAD2Bz43Dxi0b+Fu/VDkFwB\ 566 | yBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBc\ 567 | AcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWA\ 568 | XAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgV\ 569 | gFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWA3C2ytyc6Pw+oOAAAAABJRU5E\ 570 | rkJggg==\ 571 | " 572 | local toast_front_png = 573 | "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAFy0lEQVR4nO2dzW8VVRiHnwJW\ 574 | EZRWo2IBgwooC/xAF1ajCenCoDEmJkYjIa4krtzoxqUbNu5cuTJB/wkTdWvcoAtF1JZWpKVW\ 575 | kbZCW2vVujj3Ovfaj0vb9507576/J5ncljS/M+E893zNzJmuxcVFRFw2tfsERHuRAMGRAMGR\ 576 | AMGRAMGRAMGRAMGRAMGRAMGRAMGRAMGRAMGRAMGRAMGRAMGRAMGRAMGRAMHZ0u4TMGHkVLvP\ 577 | YGXufrXdZ7AqXdncE9i6kjcBtwN9wE5gN3Ab0Fs7ehp+3gZ0A9tJX4IdLbKvAP8AM8ACMA1M\ 578 | NRyTwAQwXjvGgIvA5RUTKyJGNQVYubJvAA4C+4F9wL3APbXPPmBzGae3BqaA74FBYAg4A3wJ\ 579 | DC/7122QohoCLF/htwKPAYeBQ7VjP9Wr5PUwCZwGPgc+Bb4gtSyJEkVojwDLV/heYAB4AugH\ 580 | 7i/xjNrNDPAZ8F7tM1GCCOUK0Fzx3cBTwDPAUWJV+Gp8BLwOzALuEvjPApZ+2/uB48BLwC3u\ 581 | 5efHceAA8DRpsOmKXwvQXPE7gNeAE6R+XLTmQyB9/R1bAZ+FoKLye4CTwCjwLqr8tXCMNNNx\ 582 | xV6AovL7gbPA26T5tlgbm4G3ANeFLq+l4IdI05udTvlROAbc7FmArQCFqR8AN5pmx2Q78IJn\ 583 | AR4twF7gYYfcqLwMuHUDHgIccciMzBHgJq9wDwEGHDIj043j/6mdAEUT9ahZpqjzpFewdQvQ\ 584 | TQlz14A8DriMA6wFOEhnXK2rGo+QLoWb4yGAsOc6kgTmWAtwl3GeKHjQI9RaAK38+eFyudxa\ 585 | gF3GeaLApXu1FuBO4zxRcB9gPhOwFsD1wkVw9gBbrUOtBWh1e7XYGLutA60F0HV/X/qsAyVA\ 586 | XlReAJfVKvEfe6wD9XBoXtxhHSgB8qLHOlAC5IUECE6vdaAEyIvKtwAVeNS4o6m8AL8b54lm\ 587 | zFtsdQF5Yb7Ubi3AjHGeaKbLOtBagKvGeaIZ8+cDrAW4Ypwnmqn8GEAtQGZYCzBlnCec0TQw\ 588 | ONYC/GqcJ5yxFmDlnTFFJbEW4DfjPOGMWoDgWAswbpwnmllo/SdrQwLkxax1oLUAY8Z5whlr\ 589 | AeYoYXvTwMxbB9oJUGxnqm7AjznrQI/7AS44ZIqE+bUWDwF+dsgUib+sAz0EuOiQKRLmF9sk\ 590 | QF78aR3oIYCmgn6YX231EGDEIVMkJEBwshBgEi0GeVHxQWCxGLT8ixHFRsmiBQB1A15IgOBk\ 591 | I4C6AB+yEUAtgA/ZCKAWwIesBPjbKTsyk9aBXgIsoG7AA/Obbu0FKNYCvjPPjs08mdwQUucH\ 592 | x+yIpObf+EXSngKcdcyOiMszF54CDDpmR8R8AAi+AmgMYEt2Akygq4KWZCSAZgIeuDx4671N\ 593 | 3LfO+ZHIqAUo+MY5PxLZzQIAvnbOj0SWLYC6ADuyFGAMzQSsyKwLKGYCZ9zKiEWWLQBoKmhF\ 594 | tgKcL6GMCGTWBRTocfGNs4jDk8FQjgCjJZTR6aRbwYwvBUM5AvxUQhmdjvnuYHXKEODHEsro\ 595 | dIa8gssQYB7tGbBRPvYK9hWg6LO+ci2nM1kATgNvAie9CtniFfw/PgGeLamsKjNN2kXtAvAL\ 596 | cIk0vZskvW3lau3fJkjNvlvfX6csAd4HBoDnSiqvXcyTnokYJt0W3/g5zHp2+XIY+TdSlgDz\ 597 | wPPACeAdHN6CXSJ/kL6dg6Q7nwdrvw9xLdvjOFfoWulaXCzhZZ8jpxp/2wq8ArwBPOBf+LqY\ 598 | Bs6RvrXnake9kkdZ7Q2pFavgVpQjQJ1mEQAOAS8CR4HDlPciyznS+sR5Un88QlHRw1zL7VeZ\ 599 | VfRKlCtAI0tl2EaS4ACwr3bsIr0vt7d2XL9M0ixpsDRNqtj6oOoyxYBqnLSB5ThpSnqp5fl1\ 600 | SAW3on0CNLJUBn+CVHArqiGAaBt6eXRwJEBwJEBwJEBwJEBwJEBwJEBwJEBwJEBwJEBwJEBw\ 601 | JEBwJEBw/gUUgiKM2bt1ygAAAABJRU5ErkJggg==\ 602 | " 603 | local toast_mouth_png = 604 | "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAACO0lEQVR4nO3cP2oVURhA8RNJ\ 605 | o+ICAiJWVoqFm7BIkTaF4DYsRNyKYGUrpBBSZweChYSkTBqx8w/PIgmYKnmPebzhnvMrh7nw\ 606 | DXNmGG4xW4vFgnjd2fQA2awCkCsAuQKQKwC5ApArALkCkCsAuQKQKwC5ApArALkCkCsAuQKQ\ 607 | KwC5ApArALkCkCsAuQKQKwC5ApArALkCkCsAuQKQKwC5ApArALkCkCsAuQKQKwC5ApArALkC\ 608 | kCsAuQKQKwC57U0PsC7vdh+vvPb95+PJ5pi7rRF/FfvfzX8APAEeAveBu5fHriyAH8AZcAJ8\ 609 | Bf6AJ4Jh3wDAPvCB5a7xBHgBnK9lohka+RvgDcsH/gh4Pf0o8zVyAM9WXPd00ilmbuQAfq64\ 610 | 7tekU8zcyAEcrLjucNIpZm7kAN6y/FvgCPi0hllma+QAvgEvgdNbnv8F2AX+rm2iGRpyHwCu\ 611 | 7QXcA14Be8BzYOfy+G/gOxdP/UcuAgA8ewAwcACw/G6g6cZfGTqA3Gzkb4DcQgHIFYBcAcgV\ 612 | gFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHI\ 613 | FYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwB\ 614 | yBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBcAcgVgFwByBWAXAHIFYBc\ 615 | AcgVgFwByBWAXAHI/QPwhzWTvXo9agAAAABJRU5ErkJggg==\ 616 | " 617 | 618 | local class = require_30log() 619 | 620 | local Toast = class("Toast") 621 | 622 | function Toast:init() 623 | self:center() 624 | 625 | self.eyes = {} 626 | self.eyes.closed_t = 0 627 | self.eyes.blink_t = 2 628 | 629 | self.look = {} 630 | self.look.target = { x = 0.2, y = 0.2 } 631 | self.look.current = { x = 0.2, y = 0.2 } 632 | self.look.DURATION = 0.5 633 | self.look.POINTS = { 634 | { x = 0.8, y = 0.8 }, 635 | { x = 0.1, y = 0.1 }, 636 | { x = 0.8, y = 0.1 }, 637 | { x = 0.1, y = 0.8 }, 638 | } 639 | self.look.point = 0 640 | self.look.point_t = 1 641 | self.look.t = 0 642 | end 643 | 644 | local function easeOut(t, b, c, d) 645 | t = t / d - 1 646 | return c * (math.pow(t, 3) + 1) + b 647 | end 648 | 649 | function Toast:center() 650 | local ww, wh = love.window.fromPixels(love.graphics.getDimensions()) 651 | self.x = math.floor(ww / 2 / 32) * 32 + 16 652 | self.y = math.floor(wh / 2 / 32) * 32 + 16 653 | end 654 | 655 | function Toast:get_look_coordinates() 656 | local t = self.look.t 657 | 658 | local src = self.look.current 659 | local dst = self.look.target 660 | 661 | local look_x = easeOut(t, src.x, dst.x - src.x, self.look.DURATION) 662 | local look_y = easeOut(t, src.y, dst.y - src.y, self.look.DURATION) 663 | 664 | return look_x, look_y 665 | end 666 | 667 | function Toast:update(dt) 668 | self.look.t = math.min(self.look.t + dt, self.look.DURATION) 669 | self.eyes.closed_t = math.max(self.eyes.closed_t - dt, 0) 670 | self.eyes.blink_t = math.max(self.eyes.blink_t - dt, 0) 671 | self.look.point_t = math.max(self.look.point_t - dt, 0) 672 | 673 | if self.eyes.blink_t == 0 then 674 | self:blink() 675 | end 676 | 677 | if self.look.point_t == 0 then 678 | self:look_at_next_point() 679 | end 680 | 681 | local look_x, look_y = self:get_look_coordinates() 682 | 683 | self.offset_x = look_x * 4 684 | self.offset_y = (1 - look_y) * -4 685 | end 686 | 687 | function Toast:draw() 688 | local x = self.x 689 | local y = self.y 690 | 691 | local look_x, look_y = self:get_look_coordinates() 692 | 693 | love.graphics.draw(g_images.toast.back, x, y, self.r, 1, 1, 64, 64) 694 | love.graphics.draw(g_images.toast.front, x + self.offset_x, y + self.offset_y, self.r, 1, 1, 64, 64) 695 | love.graphics.draw(self:get_eyes_image(), x + self.offset_x * 2.5, y + self.offset_y * 2.5, self.r, 1, 1, 64, 64) 696 | love.graphics.draw(g_images.toast.mouth, x + self.offset_x * 2, y + self.offset_y * 2, self.r, 1, 1, 64, 64) 697 | end 698 | 699 | function Toast:get_eyes_image() 700 | if self.eyes.closed_t > 0 then 701 | return g_images.toast.eyes.closed 702 | end 703 | return g_images.toast.eyes.open 704 | end 705 | 706 | function Toast:blink() 707 | if self.eyes.closed_t > 0 then 708 | return 709 | end 710 | self.eyes.closed_t = 0.1 711 | self.eyes.blink_t = self.next_blink() 712 | end 713 | 714 | function Toast:next_blink() 715 | return 5 + love.math.random(0, 3) 716 | end 717 | 718 | function Toast:look_at(tx, ty) 719 | local look_x, look_y = self:get_look_coordinates() 720 | self.look.current.x = look_x 721 | self.look.current.y = look_y 722 | 723 | self.look.t = 0 724 | self.look.point_t = 3 + love.math.random(0, 1) 725 | 726 | self.look.target.x = tx 727 | self.look.target.y = ty 728 | end 729 | 730 | function Toast:look_at_next_point() 731 | self.look.point = self.look.point + 1 732 | if self.look.point > #self.look.POINTS then 733 | self.look.point = 1 734 | end 735 | local point = self.look.POINTS[self.look.point] 736 | self:look_at(point.x, point.y) 737 | end 738 | 739 | local Mosaic = class("Mosaic") 740 | 741 | function Mosaic:init() 742 | local mosaic_image = g_images.mosaic[1] 743 | 744 | local sw, sh = mosaic_image:getDimensions() 745 | local ww, wh = love.window.fromPixels(love.graphics.getDimensions()) 746 | 747 | if love.window.getPixelScale() > 1 then 748 | mosaic_image = g_images.mosaic[2] 749 | end 750 | 751 | local SIZE_X = math.floor(ww / 32 + 2) 752 | local SIZE_Y = math.floor(wh / 32 + 2) 753 | local SIZE = SIZE_X * SIZE_Y 754 | 755 | self.batch = love.graphics.newSpriteBatch(mosaic_image, SIZE, "stream") 756 | self.pieces = {} 757 | self.color_t = 1 758 | self.generation = 1 759 | 760 | local COLORS = {} 761 | 762 | local xcolors = { 763 | { 240, 240, 240 }, -- WHITE (ish) 764 | { 232, 104, 162}, -- PINK 765 | { 69, 155, 168 }, -- BLUE 766 | { 67, 93, 119 }, -- DARK BLUE 767 | } 768 | 769 | for _,color in pairs(xcolors) do 770 | table.insert(COLORS, color) 771 | table.insert(COLORS, color) 772 | end 773 | 774 | -- Insert only once. This way it appears half as often. 775 | table.insert(COLORS, { 220, 239, 113 }) -- LIME 776 | 777 | -- When using the higher-res mosaic sprite sheet, we want to draw its 778 | -- sprites at the same scale as the regular-resolution one, because 779 | -- we'll globally love.graphics.scale *everything* by the screen's 780 | -- pixel density ratio. 781 | -- We can avoid a lot of Quad scaling by taking advantage of the fact 782 | -- that Quads use normalized texture coordinates internally - if we use 783 | -- the 'source image size' and quad size of the @1x image for the Quads 784 | -- even when rendering them using the @2x image, it will automatically 785 | -- scale as expected. 786 | local QUADS = { 787 | love.graphics.newQuad(0, 0, 32, 32, sw, sh), 788 | love.graphics.newQuad(0, 32, 32, 32, sw, sh), 789 | love.graphics.newQuad(32, 32, 32, 32, sw, sh), 790 | love.graphics.newQuad(32, 0, 32, 32, sw, sh), 791 | } 792 | 793 | local exclude_left = math.floor(ww / 2 / 32) 794 | local exclude_right = exclude_left + 3 795 | local exclude_top = math.floor(wh / 2 / 32) 796 | local exclude_bottom = exclude_top + 3 797 | local exclude_width = exclude_right - exclude_left + 1 798 | local exclude_height = exclude_bottom - exclude_top + 1 799 | local exclude_area = exclude_width * exclude_height 800 | 801 | local exclude_center_x = exclude_left + 1.5 802 | local exclude_center_y = exclude_top + 1.5 803 | 804 | self.generators = { 805 | function(piece, generation) 806 | return COLORS[love.math.random(1, #COLORS)] 807 | end, 808 | function(piece, generation) 809 | return COLORS[1 + (generation + piece.grid_x - piece.grid_y) % #COLORS] 810 | end, 811 | function(piece, generation) 812 | return COLORS[1 + (piece.grid_x + generation) % #COLORS] 813 | end, 814 | function(piece, generation) 815 | local len = generation + math.sqrt(piece.grid_x ^ 2 + piece.grid_y ^ 2) 816 | return COLORS[1 + math.floor(len) % #COLORS] 817 | end, 818 | function(piece, generation) 819 | local dx = piece.grid_x - exclude_center_x 820 | local dy = piece.grid_y - exclude_center_y 821 | local len = generation - math.sqrt(dx ^ 2 + dy ^ 2) 822 | return COLORS[1 + math.floor(len) % #COLORS] 823 | end, 824 | function(piece, generation) 825 | local dx = math.abs(piece.grid_x - exclude_center_x) - generation 826 | local dy = math.abs(piece.grid_y - exclude_center_y) - generation 827 | return COLORS[1 + math.floor(math.max(dx, dy)) % #COLORS] 828 | end, 829 | } 830 | 831 | self.generator = self.generators[1] 832 | 833 | local EXCLUDE = {} 834 | for y = exclude_top,exclude_bottom do 835 | EXCLUDE[y] = {} 836 | for x = exclude_left,exclude_right do 837 | EXCLUDE[y][x] = true 838 | end 839 | end 840 | 841 | for y = 1,SIZE_Y do 842 | for x = 1,SIZE_X do 843 | if not EXCLUDE[y] or not EXCLUDE[y][x] then 844 | local piece = { 845 | grid_x = x, 846 | grid_y = y, 847 | x = (x - 1) * 32, 848 | y = (y - 1) * 32, 849 | r = love.math.random(0, 100) / 100 * math.pi, 850 | rv = 1, 851 | color = {}, 852 | quad = QUADS[(x + y) % 4 + 1] 853 | } 854 | 855 | piece.color.prev = self.generator(piece, self.generation) 856 | piece.color.next = piece.color.prev 857 | table.insert(self.pieces, piece) 858 | end 859 | end 860 | end 861 | 862 | local GLYPHS = { 863 | N = love.graphics.newQuad(0, 64, 32, 32, sw, sh), 864 | O = love.graphics.newQuad(32, 64, 32, 32, sw, sh), 865 | G = love.graphics.newQuad(0, 96, 32, 32, sw, sh), 866 | A = love.graphics.newQuad(32, 96, 32, 32, sw, sh), 867 | M = love.graphics.newQuad(64, 96, 32, 32, sw, sh), 868 | E = love.graphics.newQuad(96, 96, 32, 32, sw, sh), 869 | 870 | U = love.graphics.newQuad(64, 0, 32, 32, sw, sh), 871 | P = love.graphics.newQuad(96, 0, 32, 32, sw, sh), 872 | o = love.graphics.newQuad(64, 32, 32, 32, sw, sh), 873 | S = love.graphics.newQuad(96, 32, 32, 32, sw, sh), 874 | R = love.graphics.newQuad(64, 64, 32, 32, sw, sh), 875 | T = love.graphics.newQuad(96, 64, 32, 32, sw, sh), 876 | } 877 | 878 | local INITIAL_TEXT_COLOR = { 240, 240, 240 } 879 | 880 | local put_text = function(str, offset, x, y) 881 | local idx = offset + SIZE_X * y + x 882 | for i = 1, #str do 883 | local c = str:sub(i, i) 884 | if c ~= " " then 885 | local piece = self.pieces[idx + i] 886 | if piece then 887 | piece.quad = GLYPHS[c] 888 | piece.r = 0 889 | piece.rv = 0 890 | piece.color.prev = INITIAL_TEXT_COLOR 891 | piece.color.next = INITIAL_TEXT_COLOR 892 | end 893 | end 894 | end 895 | end 896 | 897 | local text_center_x = math.floor(ww / 2 / 32) 898 | 899 | local no_game_text_offset = SIZE_X * exclude_bottom - exclude_area 900 | put_text("No TOAT", no_game_text_offset, text_center_x - 2, 1) 901 | 902 | put_text("ERTOA SUPUR", 0, text_center_x - 4, exclude_top - 3) 903 | end 904 | 905 | function Mosaic:addGeneration() 906 | self.generation = self.generation + 1 907 | if self.generation % 5 == 0 then 908 | if love.math.random(0, 100) < 60 then 909 | self.generator = self.generators[love.math.random(2, #self.generators)] 910 | else 911 | self.generator = self.generators[1] 912 | end 913 | end 914 | end 915 | 916 | function Mosaic:update(dt) 917 | self.color_t = math.max(self.color_t - dt, 0) 918 | local change_color = self.color_t == 0 919 | if change_color then 920 | self.color_t = 1 921 | self:addGeneration() 922 | end 923 | local gen = self.generator 924 | for idx,piece in ipairs(self.pieces) do 925 | piece.r = piece.r + piece.rv * dt 926 | if change_color then 927 | piece.color.prev = piece.color.next 928 | piece.color.next = gen(piece, self.generation) 929 | end 930 | end 931 | end 932 | 933 | function Mosaic:draw() 934 | self.batch:clear() 935 | love.graphics.setColor(255, 255, 255, 64) 936 | for idx,piece in ipairs(self.pieces) do 937 | local ct = 1 - self.color_t 938 | local c0 = piece.color.prev 939 | local c1 = piece.color.next 940 | local r = easeOut(ct, c0[1], c1[1] - c0[1], 1) 941 | local g = easeOut(ct, c0[2], c1[2] - c0[2], 1) 942 | local b = easeOut(ct, c0[3], c1[3] - c0[3], 1) 943 | 944 | self.batch:setColor(r, g, b) 945 | self.batch:add(piece.quad, piece.x, piece.y, piece.r, 1, 1, 16, 16) 946 | end 947 | love.graphics.setColor(255, 255, 255) 948 | love.graphics.draw(self.batch, 0, 0) 949 | end 950 | 951 | function love.load() 952 | love.graphics.setBackgroundColor(136, 193, 206) 953 | 954 | local function load_image(file, name) 955 | return love.graphics.newImage(love.filesystem.newFileData(file, name:gsub("_", "."), "base64")) 956 | end 957 | 958 | g_images = {} 959 | g_images.toast = {} 960 | g_images.toast.back = load_image(toast_back_png, "toast_back.png") 961 | g_images.toast.front = load_image(toast_front_png, "toast_front.png") 962 | g_images.toast.eyes = {} 963 | g_images.toast.eyes.open = load_image(toast_eyes_open_png, "toast_eyes_open.png") 964 | g_images.toast.eyes.closed = load_image(toast_eyes_closed_png, "toast_eyes_closed.png") 965 | g_images.toast.mouth = load_image(toast_mouth_png, "toast_mouth.png") 966 | 967 | g_images.mosaic = {} 968 | g_images.mosaic[1] = load_image(mosaic_png, "mosaic.png") 969 | g_images.mosaic[2] = load_image(mosaic_2x_png, "mosaic@2x.png") 970 | 971 | g_entities = {} 972 | g_entities.toast = Toast() 973 | g_entities.mosaic = Mosaic() 974 | end 975 | 976 | function love.update(dt) 977 | dt = math.min(dt, 1/10) 978 | g_entities.toast:update(dt) 979 | g_entities.mosaic:update(dt) 980 | if love.keyboard.isDown("z") then 981 | g_entities.mosaic = Mosaic() 982 | g_entities.toast:center() 983 | end 984 | end 985 | 986 | function love.draw() 987 | love.graphics.setColor(255, 255, 255) 988 | love.graphics.push() 989 | love.graphics.scale(love.window.getPixelScale()) 990 | g_entities.mosaic:draw() 991 | g_entities.toast:draw() 992 | love.graphics.pop() 993 | end 994 | 995 | function love.resize(w, h) 996 | g_entities.mosaic = Mosaic() 997 | g_entities.toast:center() 998 | end 999 | 1000 | function love.keypressed(key) 1001 | if key == "escape" then 1002 | love.event.quit() 1003 | end 1004 | end 1005 | 1006 | function love.keyreleased(key) 1007 | if key == "f" then 1008 | local is_fs = love.window.getFullscreen() 1009 | love.window.setFullscreen(not is_fs) 1010 | end 1011 | end 1012 | 1013 | function love.mousepressed(x, y, b) 1014 | local tx = x / love.graphics.getWidth() 1015 | local ty = y / love.graphics.getHeight() 1016 | g_entities.toast:look_at(tx, ty) 1017 | end 1018 | 1019 | function love.mousemoved(x, y) 1020 | if love and love.mouse.isDown(1) then 1021 | local tx = x / love.graphics.getWidth() 1022 | local ty = y / love.graphics.getHeight() 1023 | g_entities.toast:look_at(tx, ty) 1024 | end 1025 | end 1026 | 1027 | local last_touch = {time=0, x=0, y=0} 1028 | 1029 | function love.touchpressed(id, x, y, pressure) 1030 | -- Double-tap the screen (when using a touch screen) to exit. 1031 | if #love.touch.getTouches() == 1 then 1032 | local dist = math.sqrt((x-last_touch.x)^2 + (y-last_touch.y)^2) 1033 | local difftime = love.timer.getTime() - last_touch.time 1034 | if difftime < 0.3 and dist < 50 then 1035 | if love.window.showMessageBox("Exit No-Game Screen", "", {"OK", "Cancel"}) == 1 then 1036 | love.event.quit() 1037 | end 1038 | end 1039 | 1040 | last_touch.time = love.timer.getTime() 1041 | last_touch.x = x 1042 | last_touch.y = y 1043 | end 1044 | end -------------------------------------------------------------------------------- /project/pong/main.lua: -------------------------------------------------------------------------------- 1 | local editor = sandsmas.editor 2 | local imgui = sandsmas.imgui 3 | 4 | local class = require('libs.middleclass.middleclass') 5 | objects = { Pad = {}, Ball = {} } 6 | local scale = function(valueIn, baseMin, baseMax, limitMin, limitMax) 7 | return ((limitMax - limitMin) * (valueIn - baseMin) / (baseMax - baseMin)) + limitMin 8 | end 9 | 10 | --[[ 11 | Pad 12 | ]] 13 | 14 | local Pad = class('Pad') 15 | local Ball = class('Ball') 16 | local Transform = class('Transform') 17 | 18 | function Transform:initialize(x, y, sx, sy, rx, ry) 19 | self.x = x or 0 20 | self.y = y or 0 21 | self.sx = sx or 1 22 | self.sy = sy or 1 23 | self.rx = rx or 0 24 | self.ry = ry or 0 25 | end 26 | 27 | function Transform:setPosition(x, y) 28 | self.x = x or self.x 29 | self.y = y or self.y 30 | end 31 | 32 | function Transform:getPosition() 33 | return self.x, self.y 34 | end 35 | 36 | function Transform:setScale(sx, sy) 37 | self.sx = sx or self.sx 38 | self.sy = sy or self.sy 39 | end 40 | 41 | function Transform:getScale() 42 | return self.sx, self.sy 43 | end 44 | 45 | function Transform:setRotation(rx, ry) 46 | self.rx = rx or self.rx 47 | self.ry = ry or self.ry 48 | end 49 | 50 | function Transform:getRotation() 51 | return self.rx, self.ry 52 | end 53 | 54 | Pad.static.score_to_win = 6 55 | Pad.static.color = {1.0,1.0,1.0,1.0} 56 | 57 | function Pad:initialize(upKey, downKey) 58 | self.upKey = upKey or "w" 59 | self.downKey = downKey or "s" 60 | 61 | self.honk = true 62 | 63 | self.playerNo = #objects['Pad'] + 1 64 | 65 | self.width = 60 66 | 67 | self.moveSpeed = 7 68 | 69 | self.extent = 250 70 | 71 | self.score = 0 72 | 73 | self.color = Pad.color 74 | 75 | self.sign = -1 76 | if self.playerNo > 1 then 77 | self.sign = self.sign * -1 78 | end 79 | 80 | self.transform = Transform:new(0, 0, 20, 100) 81 | 82 | table.insert(objects['Pad'], self) 83 | end 84 | 85 | function Pad:update(dt) 86 | local moveValue 87 | if love.keyboard.isDown(self.upKey) and love.keyboard.isDown(self.downKey) then 88 | -- what do you expect? 89 | elseif love.keyboard.isDown(self.upKey) then 90 | self.transform:setPosition(nil, self.transform.y - self.moveSpeed) 91 | elseif love.keyboard.isDown(self.downKey) then 92 | self.transform:setPosition(nil, self.transform.y + self.moveSpeed) 93 | end 94 | 95 | if self.transform.y < -self.extent then 96 | self.transform:setPosition(nil, -self.extent) 97 | end 98 | if self.transform.y > self.extent then 99 | self.transform:setPosition(nil, self.extent) 100 | end 101 | 102 | if self.score > Pad.score_to_win and self.color == Pad.color then 103 | self.color = {1.0,0.0,0.0,1.0} 104 | end 105 | end 106 | 107 | function Pad:draw() 108 | if self.color ~= Pad.color then 109 | love.graphics.setColor(self.color[1]*255, self.color[2]*255, self.color[3]*255, self.color[4]*255) 110 | end 111 | love.graphics.rectangle("fill", (Ball.outer_limit_x*self.sign)-10, self.transform.y - 50, 20, 100) 112 | end 113 | 114 | --[[ 115 | Ball 116 | ]] 117 | 118 | Ball.static.inner_limit_x = 325 119 | Ball.static.outer_limit_x = 350 120 | 121 | function Ball:initialize() 122 | self.x = 0 123 | self.y = 0 124 | self.speed = 3 125 | self.vx = -self.speed 126 | self.vy = self.speed 127 | 128 | self.limit_y = 285 129 | self.inner_limit_x = 325 130 | self.outer_limit_x = 350 131 | self.goal_limit = 415 132 | 133 | table.insert(objects['Ball'], self) 134 | end 135 | 136 | function Ball:update(dt) 137 | self.x = self.x + self.vx 138 | self.y = self.y + self.vy 139 | 140 | if math.abs(self.y) >= self.limit_y then 141 | self.vy = -self.vy 142 | end 143 | 144 | self:check_goal(objects['Pad'][1]) 145 | self:check_goal(objects['Pad'][2]) 146 | end 147 | 148 | function Ball:check_goal(pad) 149 | local otherPlayerNo = (3-pad.playerNo) 150 | 151 | local boundaryCheck = false 152 | if pad.playerNo == 1 then 153 | boundaryCheck = self.x < self.inner_limit_x*pad.sign and self.x > self.outer_limit_x*pad.sign 154 | elseif pad.playerNo == 2 then 155 | boundaryCheck = self.x > self.inner_limit_x*pad.sign and self.x < self.outer_limit_x*pad.sign 156 | end 157 | 158 | if boundaryCheck and math.abs(pad.transform.y - self.y) < pad.width then 159 | self.speed = self.speed + scale(pad.playerNo, 1, 2, 0.2, 0.5) 160 | self.x = self.inner_limit_x*pad.sign 161 | self.vx = self.speed*pad.sign*-1 162 | self.vy = self.vy * scale(pad.playerNo, 1, 2, 0.5, 0.8) + math.random(-10, 10) / (otherPlayerNo*10) * self.speed 163 | end 164 | 165 | if self.x > self.goal_limit or self.x < -self.goal_limit then 166 | local scoringPlayer = 1 167 | local logStatus = 'yay' 168 | local logBanter = 'Good for you!' 169 | if self.x < -self.goal_limit then 170 | scoringPlayer = 2 171 | logStatus = 'nay' 172 | logBanter = 'Idiot.' 173 | end 174 | self.x = 0 175 | self.vx = -self.vx 176 | objects['Pad'][scoringPlayer].score = objects['Pad'][scoringPlayer].score + 1 177 | local logLine = "Player #" .. scoringPlayer .. " scored a goal! " .. logBanter 178 | editor.console:Log(logStatus, logLine) 179 | end 180 | end 181 | 182 | function Ball:draw() 183 | love.graphics.circle("fill", self.x, self.y, 15) 184 | end 185 | 186 | --[[ 187 | love 188 | ]] 189 | 190 | function love.load(args) 191 | local root = editor.hierarchy:NewObject("Root") 192 | local balls = editor.hierarchy:NewObject("Balls", root) 193 | local paddles = editor.hierarchy:NewObject("Pads", root) 194 | 195 | Ball:new() 196 | Pad:new("up", "down") 197 | Pad:new("left", "right") 198 | 199 | editor.inspector:SetTypeGUI( 200 | "instance of class Transform", 201 | function(name, value) 202 | imgui.Columns(2, "visualizer", false) 203 | imgui.Text(name) 204 | imgui.NextColumn() 205 | 206 | imgui.Columns(2, "labels_and_vis") 207 | imgui.Text("Position"); imgui.NextColumn() 208 | local changed_x, new_x = imgui.InputFloat("x", value.x); imgui.SameLine() 209 | if changed_x then value.x = new_x end 210 | local changed_y, new_y = imgui.InputFloat("y", value.y) 211 | if changed_y then value.y = new_y end 212 | imgui.NextColumn() 213 | 214 | imgui.Text("Scale"); imgui.NextColumn() 215 | local changed_sx, new_sx = imgui.InputFloat("sx", value.sx); imgui.SameLine() 216 | if changed_sx then value.sx = new_sx end 217 | local changed_sy, new_sy = imgui.InputFloat("sy", value.sy) 218 | if changed_sy then value.sy = new_sy end 219 | imgui.NextColumn() 220 | 221 | imgui.Text("Rotation"); imgui.NextColumn() 222 | local changed_rx, new_rx = imgui.InputFloat("rx", value.rx); imgui.SameLine() 223 | if changed_rx then value.rx = new_rx end 224 | local changed_ry, new_ry = imgui.InputFloat("ry", value.ry) 225 | if changed_ry then value.ry = new_ry end 226 | imgui.NextColumn() 227 | imgui.Columns(1) 228 | 229 | imgui.Columns(1) 230 | 231 | return (changed_x or changed_y or 232 | changed_sx or changed_sy or 233 | changed_rx or changed_ry), 234 | value 235 | end 236 | ) 237 | 238 | editor.inspector:SetTypeIdentifier( 239 | "instance of class Transform", 240 | function(name, value) 241 | return type(value) == 'table' and value.class and value.class == Transform 242 | end 243 | ) 244 | 245 | -- register all the objects we made 246 | -- collect them for a selection 247 | local selection = {} 248 | for k1,v1 in pairs(objects) do 249 | for k2,v2 in pairs(v1) do 250 | editor.hierarchy:Register( v2 ) 251 | local parent 252 | if k1 == 'Ball' then 253 | parent = balls 254 | elseif k1 == 'Pad' then 255 | parent = paddles 256 | end 257 | 258 | editor.hierarchy:SetObjectParent( v2, parent ) 259 | end 260 | end 261 | end 262 | 263 | function love.update(dt) 264 | for k1,v1 in pairs(objects) do 265 | for k2,v2 in pairs(v1) do 266 | v2:update(dt) 267 | end 268 | end 269 | local x, y = love.window.getPosition() 270 | local w, h = love.window.getMode() 271 | local mx, my = love.mouse.getPosition() 272 | local focus 273 | if love.window.hasMouseFocus() then 274 | focus = " (!)" 275 | else 276 | focus = " (?)" 277 | end 278 | local newTitle = x .. "x" .. y .. "; " .. w .. "/" .. h .. "; " .. mx .. ":" .. my .. focus 279 | love.window.setTitle(newTitle) 280 | end 281 | 282 | function love.draw() 283 | love.graphics.translate(400, 300) 284 | love.graphics.print(objects['Pad'][1].score, -380, -280) 285 | love.graphics.print(objects['Pad'][2].score, 370, -280) 286 | 287 | for k1,v1 in pairs(objects) do 288 | for k2,v2 in pairs(v1) do 289 | love.graphics.setColor(255,255,255) 290 | v2:draw() 291 | end 292 | end 293 | end 294 | 295 | function love.keypressed(key, scan) 296 | if key == "i" then 297 | love.graphics.setColor(0,0,255) 298 | elseif key == "j" then 299 | break_everything() 300 | end 301 | end -------------------------------------------------------------------------------- /project/pong/middleclass.lua: -------------------------------------------------------------------------------- 1 | local middleclass = { 2 | _VERSION = 'middleclass v4.1.0', 3 | _DESCRIPTION = 'Object Orientation for Lua', 4 | _URL = 'https://github.com/kikito/middleclass', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2011 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | local function _createIndexWrapper(aClass, f) 32 | if f == nil then 33 | return aClass.__instanceDict 34 | else 35 | return function(self, name) 36 | local value = aClass.__instanceDict[name] 37 | 38 | if value ~= nil then 39 | return value 40 | elseif type(f) == "function" then 41 | return (f(self, name)) 42 | else 43 | return f[name] 44 | end 45 | end 46 | end 47 | end 48 | 49 | local function _propagateInstanceMethod(aClass, name, f) 50 | f = name == "__index" and _createIndexWrapper(aClass, f) or f 51 | aClass.__instanceDict[name] = f 52 | 53 | for subclass in pairs(aClass.subclasses) do 54 | if rawget(subclass.__declaredMethods, name) == nil then 55 | _propagateInstanceMethod(subclass, name, f) 56 | end 57 | end 58 | end 59 | 60 | local function _declareInstanceMethod(aClass, name, f) 61 | aClass.__declaredMethods[name] = f 62 | 63 | if f == nil and aClass.super then 64 | f = aClass.super.__instanceDict[name] 65 | end 66 | 67 | _propagateInstanceMethod(aClass, name, f) 68 | end 69 | 70 | local function _tostring(self) return "class " .. self.name end 71 | local function _call(self, ...) return self:new(...) end 72 | 73 | local function _createClass(name, super) 74 | local dict = {} 75 | dict.__index = dict 76 | 77 | local aClass = { name = name, super = super, static = {}, 78 | __instanceDict = dict, __declaredMethods = {}, 79 | subclasses = setmetatable({}, {__mode='k'}) } 80 | 81 | if super then 82 | setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) or super.static[k] end }) 83 | else 84 | setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) end }) 85 | end 86 | 87 | setmetatable(aClass, { __index = aClass.static, __tostring = _tostring, 88 | __call = _call, __newindex = _declareInstanceMethod }) 89 | 90 | return aClass 91 | end 92 | 93 | local function _includeMixin(aClass, mixin) 94 | assert(type(mixin) == 'table', "mixin must be a table") 95 | 96 | for name,method in pairs(mixin) do 97 | if name ~= "included" and name ~= "static" then aClass[name] = method end 98 | end 99 | 100 | for name,method in pairs(mixin.static or {}) do 101 | aClass.static[name] = method 102 | end 103 | 104 | if type(mixin.included)=="function" then mixin:included(aClass) end 105 | return aClass 106 | end 107 | 108 | local DefaultMixin = { 109 | __tostring = function(self) return "instance of " .. tostring(self.class) end, 110 | 111 | initialize = function(self, ...) end, 112 | 113 | isInstanceOf = function(self, aClass) 114 | return type(aClass) == 'table' and (aClass == self.class or self.class:isSubclassOf(aClass)) 115 | end, 116 | 117 | static = { 118 | allocate = function(self) 119 | assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'") 120 | return setmetatable({ class = self }, self.__instanceDict) 121 | end, 122 | 123 | new = function(self, ...) 124 | assert(type(self) == 'table', "Make sure that you are using 'Class:new' instead of 'Class.new'") 125 | local instance = self:allocate() 126 | instance:initialize(...) 127 | return instance 128 | end, 129 | 130 | subclass = function(self, name) 131 | assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'") 132 | assert(type(name) == "string", "You must provide a name(string) for your class") 133 | 134 | local subclass = _createClass(name, self) 135 | 136 | for methodName, f in pairs(self.__instanceDict) do 137 | _propagateInstanceMethod(subclass, methodName, f) 138 | end 139 | subclass.initialize = function(instance, ...) return self.initialize(instance, ...) end 140 | 141 | self.subclasses[subclass] = true 142 | self:subclassed(subclass) 143 | 144 | return subclass 145 | end, 146 | 147 | subclassed = function(self, other) end, 148 | 149 | isSubclassOf = function(self, other) 150 | return type(other) == 'table' and 151 | type(self.super) == 'table' and 152 | ( self.super == other or self.super:isSubclassOf(other) ) 153 | end, 154 | 155 | include = function(self, ...) 156 | assert(type(self) == 'table', "Make sure you that you are using 'Class:include' instead of 'Class.include'") 157 | for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end 158 | return self 159 | end 160 | } 161 | } 162 | 163 | function middleclass.class(name, super) 164 | assert(type(name) == 'string', "A name (string) is needed for the new class") 165 | return super and super:subclass(name) or _includeMixin(_createClass(name), DefaultMixin) 166 | end 167 | 168 | setmetatable(middleclass, { __call = function(_, ...) return middleclass.class(...) end }) 169 | 170 | return middleclass 171 | -------------------------------------------------------------------------------- /src/classes/Console.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | @class Console Puts colored text in a scrollable window. 3 | ]] 4 | local Console = require('libs.middleclass.middleclass')('Console') 5 | 6 | local imgui = require('imgui') 7 | 8 | Console.static.default_color = { 1.0, 1.0, 1.0, 1.0 } 9 | Console.static.status_colors = { 10 | nay = { 1.0, 0.4, 0.4, 1.0 }, 11 | warn = { 1.0, 0.78, 0.58, 1.0 }, 12 | yay = { 0.4, 1.0, 0.4, 1.0 } 13 | } 14 | 15 | function Console:initialize(args) 16 | self.console_history = {} 17 | -- entries in the form of {status=status, text=text} 18 | end 19 | 20 | --[[ 21 | @method Console:Log Add text to the console. 22 | @param string status The color of the text. 23 | @param string text The text to log. 24 | ]] 25 | function Console:Log(status, text) 26 | table.insert(self.console_history, { 27 | status = status, 28 | text = text 29 | }) 30 | end 31 | 32 | --[[ 33 | @method Console:Render Draws the console's contents. 34 | ]] 35 | function Console:Render() 36 | imgui.PushStyleVar("ItemSpacing", 4, 1) 37 | for i,item in ipairs(self.console_history) do 38 | -- get the color 39 | local color = Console.status_colors[item.status] or Console.default_color 40 | 41 | imgui.PushStyleColor("Text", unpack(color)) 42 | 43 | imgui.TextUnformatted("[" .. item.status .. "] " .. item.text) 44 | 45 | imgui.PopStyleColor() 46 | end 47 | imgui.PopStyleVar() 48 | end 49 | 50 | return Console -------------------------------------------------------------------------------- /src/classes/Editor.lua: -------------------------------------------------------------------------------- 1 | local Editor = require('libs.middleclass.middleclass')('Editor') 2 | 3 | --[[ 4 | @class Editor An editor instance, contains references to other classes as internal components. 5 | ]] 6 | function Editor:initialize(args) 7 | -- components 8 | self.console = require('src.classes.Console')() 9 | self.hierarchy = require('src.classes.Hierarchy')() 10 | self.inspector = require('src.classes.Inspector')() 11 | end 12 | 13 | return Editor -------------------------------------------------------------------------------- /src/classes/Hierarchy.lua: -------------------------------------------------------------------------------- 1 | local Hierarchy = require('libs.middleclass.middleclass')('Hierarchy') 2 | local lume = require('libs.lume.lume') 3 | 4 | local imgui = require('imgui') 5 | 6 | -- Hierarchy.static.property = 'great' 7 | Hierarchy.static.GetObjectName = function(obj) 8 | return tostring(obj) .. " [" .. obj.id .. "]" 9 | end 10 | 11 | --[[ 12 | Hierarchy 13 | ]] 14 | function Hierarchy:initialize(args) 15 | -- keyed by UIDs, contains object table references 16 | self.objects = {} 17 | 18 | -- keyed by UIDs, contains UIDs for an object's parent 19 | self.parents = {} 20 | 21 | -- keyed by UIDs, contains if a object is selected in the hierarchy 22 | self.selection = {} 23 | 24 | -- keyed by index id, contains ipairs of objects that were selected, in order 25 | self.chrono_selection = {} 26 | 27 | self.open_stack = {} 28 | end 29 | 30 | function Hierarchy:RenderObject(object) 31 | local name = Hierarchy.GetObjectName( object ) 32 | local node_clicked, node_open 33 | local node_flags = {"OpenOnArrow", "OpenOnDoubleClick"} 34 | local leaf_flags = {"Leaf", "NoTreePushOnOpen"} 35 | local use_flags 36 | 37 | -- Increase spacing to differentiate leaves from expanded contents. 38 | imgui.PushStyleVar("IndentSpacing", imgui.GetFontSize()*0.5) 39 | 40 | local has_children = not self:GetObjectHasNoChildren(object) 41 | if has_children then 42 | use_flags = node_flags 43 | else 44 | use_flags = leaf_flags 45 | end 46 | 47 | if self.selection[object.id] then 48 | table.insert(use_flags, "Selected") 49 | end 50 | 51 | node_open = imgui.TreeNodeEx(name, use_flags) 52 | node_clicked = imgui.IsItemClicked() 53 | 54 | self.open_stack[object.id] = node_open 55 | if node_open and has_children then 56 | for _, child_object in pairs(self.objects) do 57 | if self.parents[child_object.id] == object.id then 58 | self:RenderObject(child_object) 59 | end 60 | end 61 | imgui.TreePop() 62 | end 63 | 64 | if node_clicked then 65 | -- Update selection state. Process outside of tree loop to avoid visual inconsistencies during the clicking-frame. 66 | if love.keyboard.isDown("lctrl", "rctrl") then 67 | -- CTRL+click to toggle 68 | 69 | if self.selection[object.id] then 70 | local found_id 71 | for i, v in ipairs(self.chrono_selection) do 72 | if v == object.id then 73 | found_id = i 74 | break 75 | end 76 | end 77 | if found_id then 78 | table.remove(self.chrono_selection, found_id) 79 | end 80 | else 81 | table.insert(self.chrono_selection, object.id) 82 | end 83 | 84 | self.selection[object.id] = not self.selection[object.id] 85 | else 86 | -- Click to single-select 87 | self.selection = { [object.id] = true } 88 | self.chrono_selection = { [1] = object.id } 89 | end 90 | end 91 | 92 | imgui.PopStyleVar() 93 | end 94 | 95 | function Hierarchy:Render() 96 | for _, object in pairs(self.objects) do 97 | if self.parents[object.id] == '' then 98 | self:RenderObject(object) 99 | end 100 | end 101 | end 102 | 103 | function Hierarchy:Register(obj) 104 | if obj.id and self.objects[id] == obj then 105 | -- already registered, soft warning 106 | assert(false, "Object '" .. tostring(obj) .. "' [" .. obj.id .. "] already has a UUID.") 107 | elseif obj.id and self.objects[id] ~= obj then 108 | assert(false, "Object '" .. tostring(obj) .. "' [" .. obj.id .. "] had an ID but was not registered.\nCollision with entity properties.") 109 | end 110 | 111 | local id = lume.uuid() 112 | if self.objects[id] then 113 | assert(false, "Generated UUID collided with existing registered object '" .. tostring(self.objects[id]) .. "' [" .. id .. "].\nAttempted to register: Object '" .. tostring(obj) .. "'.") 114 | end 115 | 116 | obj.id = id 117 | self.objects[id] = obj 118 | 119 | self.parents[id] = '' 120 | return id 121 | end 122 | 123 | function Hierarchy:NewObject(name, parent) 124 | local obj = { 125 | name = name 126 | } 127 | local omet = { 128 | __tostring = function(oself) 129 | return oself.name 130 | end, 131 | } 132 | setmetatable(obj, omet) 133 | 134 | self:Register(obj) 135 | self:SetObjectParent(obj, parent) 136 | 137 | return obj 138 | end 139 | 140 | -- Set an object's parent with a reference to both. 141 | function Hierarchy:SetObjectParent(object, parent) 142 | local toset 143 | --local oldparent = self.parents[object.id] 144 | if parent then 145 | toset = parent.id 146 | else 147 | toset = '' 148 | end 149 | self.parents[object.id] = toset 150 | end 151 | 152 | function Hierarchy:GetObjectHasNoChildren(object) 153 | for k, v in pairs(self.parents) do 154 | if object.id == v then 155 | return false 156 | end 157 | end 158 | return true 159 | end 160 | 161 | return Hierarchy -------------------------------------------------------------------------------- /src/classes/Inspector.lua: -------------------------------------------------------------------------------- 1 | local Inspector = require('libs.middleclass.middleclass')('Inspector') 2 | 3 | local imgui = require('imgui') 4 | 5 | Inspector.static.type_guis = { 6 | default = function(name, value) 7 | return imgui.InputText(name, tostring(value), 40, {"ReadOnly"}) 8 | end, 9 | string = function(name, value) 10 | return imgui.InputText(name, tostring(value), 40) 11 | end, 12 | int = imgui.InputInt, 13 | float = imgui.InputFloat, 14 | color4 = function(name, value) 15 | return imgui.ColorEdit4(name, unpack(value)) 16 | end, 17 | } 18 | Inspector.static.type_identifiers = { 19 | float = function(name, value) 20 | local num, dec 21 | if type(value) == "number" then 22 | num, dec = math.modf(value) 23 | return ( 24 | math.abs(dec) ~= 0 25 | ) 26 | end 27 | return false 28 | end, 29 | int = function(name, value) 30 | local num, dec 31 | if type(value) == "number" then 32 | num, dec = math.modf(value) 33 | return ( 34 | math.abs(dec) == 0 35 | ) 36 | end 37 | return false 38 | end, 39 | color4 = function(name, value) 40 | return ( 41 | type(value) == "table" and 42 | #value == 4 and 43 | type(value[1]) == "number" and 44 | type(value[2]) == "number" and 45 | type(value[3]) == "number" and 46 | type(value[4]) == "number" and 47 | value[1] >= 0 and value[1] <= 1 and 48 | value[2] >= 0 and value[2] <= 1 and 49 | value[3] >= 0 and value[3] <= 1 and 50 | value[4] >= 0 and value[4] <= 1 51 | ) 52 | end, 53 | color3 = function(name, value) 54 | return ( 55 | type(value) == "table" and 56 | #value == 3 and 57 | type(value[1]) == "number" and 58 | type(value[2]) == "number" and 59 | type(value[3]) == "number" and 60 | value[1] >= 0 and value[1] <= 1 and 61 | value[2] >= 0 and value[2] <= 1 and 62 | value[3] >= 0 and value[3] <= 1 63 | ) 64 | end, 65 | } 66 | 67 | --[[ 68 | Inspector 69 | ]] 70 | function Inspector:initialize(args) 71 | self.selection = {} 72 | 73 | self.type_guis = {} 74 | for k, v in pairs(Inspector.type_guis) do 75 | self.type_guis[k] = v 76 | end 77 | 78 | self.type_identifiers = {} 79 | for k, v in pairs(Inspector.type_identifiers) do 80 | self.type_identifiers[k] = v 81 | end 82 | end 83 | 84 | -- TypeGUI = function(name, value) 85 | -- return didChange, newValue1, newValue2, ... 86 | -- end 87 | function Inspector:SetTypeGUI(typeName, func) 88 | assert(not self.type_guis[typeName], "A type GUI already exists for " .. tostring(typeName)) 89 | self.type_guis[typeName] = func 90 | end 91 | 92 | -- TypeIdentifier = function(name, value) 93 | -- return isTypeForKey 94 | -- end 95 | function Inspector:SetTypeIdentifier(typeName, func) 96 | assert(not self.type_identifiers[typeName], "A type identifier already exists for " .. tostring(typeName)) 97 | self.type_identifiers[typeName] = func 98 | end 99 | 100 | function Inspector:AddSelection(...) 101 | for k,v in pairs({...}) do 102 | table.insert(self.selection, v) 103 | end 104 | end 105 | 106 | function Inspector:SetSelection(...) 107 | self.selection = {...} 108 | end 109 | 110 | function Inspector:ClearSelection() 111 | self.selection = {} 112 | end 113 | 114 | function Inspector:Render() 115 | for k,v in ipairs(self.selection) do 116 | self:RenderInspect(v) 117 | end 118 | end 119 | 120 | function Inspector:RenderInspect(var) 121 | local itsname = tostring(var) 122 | if var.id then 123 | itsname = itsname .. "\t[" .. var.id .. "]" 124 | end 125 | 126 | if imgui.CollapsingHeader(tostring(itsname)) then 127 | local value_changed, out 128 | for k, v in pairs(var) do 129 | local typ = type(v) 130 | local atype = typ 131 | 132 | -- attempt to obtain the type from a sequence of functions 133 | -- if any function returns true, it is that type 134 | for k0, v0 in pairs(self.type_identifiers) do 135 | local result = v0(k, v) 136 | if result then 137 | atype = k0 138 | end 139 | end 140 | 141 | if not self.type_guis[atype] then 142 | atype = 'default' 143 | end 144 | 145 | if self.type_guis[atype] then 146 | out = {self.type_guis[atype](k, v)} 147 | 148 | -- get the status alone 149 | value_changed = out[1] 150 | table.remove(out, 1) 151 | end 152 | 153 | -- value changed 154 | if value_changed then 155 | if #out > 1 then 156 | var[k] = out 157 | else 158 | var[k] = out[1] 159 | end 160 | end 161 | end 162 | end 163 | end 164 | 165 | return Inspector -------------------------------------------------------------------------------- /src/classes/Template.lua: -------------------------------------------------------------------------------- 1 | local Template = require('libs.middleclass.middleclass')('Template') 2 | 3 | local imgui = require('imgui') 4 | 5 | -- Template.static.property = 'great' 6 | 7 | --[[ 8 | @class Template Example class. 9 | ]] 10 | function Template:initialize(args) 11 | -- self.instance_value = {} 12 | end 13 | 14 | --[[ 15 | @method Template:ExampleMethod A method that exemplifies an instance method. 16 | @param table a_table A table that stuff happens to. 17 | @param string a_string A string that things happen to. 18 | @param number a_number A number that things happen to. 19 | @return bool The status of the operation. 20 | ]] 21 | function Template:ExampleMethod(a_table, a_string, a_number) 22 | return true 23 | end 24 | 25 | return Template -------------------------------------------------------------------------------- /src/classes/UIHelper.lua: -------------------------------------------------------------------------------- 1 | local UIHelper = require('libs.middleclass.middleclass')('UIHelper') 2 | local lume = require('libs.lume.lume') 3 | local imgui = require('imgui') 4 | 5 | function UIHelper:initialize(args) 6 | self.postRenderStack = {} 7 | 8 | -- go by keys, call final callback 9 | self.menu = {} 10 | 11 | self:Reset() 12 | end 13 | 14 | function UIHelper:RenderMenuLevel(name, value) 15 | if type(value) == "function" then 16 | if name == "" then 17 | imgui.MenuItem("(Empty)", nil, false, false) 18 | elseif imgui.MenuItem(name) then 19 | value(name) 20 | end 21 | elseif type(value) == "table" then 22 | if imgui.BeginMenu(name) then 23 | local i = 0 24 | for k, v in pairs(value) do 25 | self:RenderMenuLevel(k, v) 26 | i = i + 1 27 | end 28 | if i == 0 then 29 | imgui.MenuItem("(Empty)", nil, false, false) 30 | end 31 | imgui.EndMenu() 32 | end 33 | end 34 | end 35 | 36 | function UIHelper:RenderMenu() 37 | if imgui.BeginMainMenuBar() then 38 | for k, v in pairs(self.menu) do 39 | if type(v) == 'table' then 40 | self:RenderMenuLevel(k, v) 41 | elseif type(v) == 'function' then 42 | if imgui.BeginMenu(k) then 43 | self:RenderMenuLevel(k, v) 44 | imgui.EndMenu() 45 | end 46 | end 47 | end 48 | imgui.EndMainMenuBar() 49 | end 50 | end 51 | 52 | function UIHelper:AddMenu(path, callback, shortcut, selected, enabled) 53 | local parts = lume.split(path, "/") 54 | local ctable = self.menu 55 | local depth = #parts 56 | 57 | for k,v in pairs(parts) do 58 | if k == depth then 59 | ctable[v] = callback 60 | else 61 | if not ctable[v] then 62 | ctable[v] = {} 63 | end 64 | ctable = ctable[v] 65 | end 66 | end 67 | end 68 | 69 | function UIHelper:Dock(name, layout) 70 | local fullname = "dock_" + self.numDocks 71 | if layout ~= nil then 72 | fullname = fullname .. "_" .. layout 73 | imgui.SetNextDock(layout) 74 | end 75 | 76 | if name ~= nil then 77 | fullname = name 78 | end 79 | 80 | if imgui.BeginDock(fullname) then 81 | imgui.Text(fullname) 82 | end 83 | imgui.EndDock() 84 | 85 | self.numDocks = self.numDocks + 1 86 | end 87 | 88 | function UIHelper:Reset() 89 | self.numDocks = 0 90 | end 91 | 92 | function UIHelper:PostRender() 93 | for k,v in pairs(self.postRenderStack) do 94 | v() 95 | end 96 | self.postRenderStack = {} 97 | end 98 | 99 | function UIHelper:PushPostRender(func) 100 | table.insert(self.postRenderStack, func) 101 | end 102 | 103 | function UIHelper:Begin(...) 104 | return imgui.Begin(...) 105 | end 106 | 107 | function UIHelper:End() 108 | imgui.End() 109 | self:Reset() 110 | end 111 | 112 | return UIHelper -------------------------------------------------------------------------------- /tools/generate_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | nowpath=$( dirname "${BASH_SOURCE[0]}" ); 3 | nowpath="${nowpath#./}"; 4 | rm -rf "docs"; 5 | mkdir -p "docs"; 6 | echo "" >> "docs/index.html"; --------------------------------------------------------------------------------