diff --git a/two_player_cleaning_game/libs/nata.lua b/two_player_cleaning_game/libs/nata.lua new file mode 100644 index 0000000..eec7193 --- /dev/null +++ b/two_player_cleaning_game/libs/nata.lua @@ -0,0 +1,546 @@ +--- Entity management for Lua. +-- @module nata + +local nata = { + _VERSION = 'Nata v0.3.3', + _DESCRIPTION = 'Entity management for Lua.', + _URL = 'https://github.com/tesselode/nata', + _LICENSE = [[ + MIT License + + Copyright (c) 2020 Andrew Minnich + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ]] +} + +-- gets the error level needed to make an error appear +-- in the user's code, not the library code +local function getUserErrorLevel() + local source = debug.getinfo(1).source + local level = 1 + while debug.getinfo(level).source == source do + level = level + 1 + end + --[[ + we return level - 1 here and not just level + because the level was calculated one function + deeper than the function that will actually + use this value. if we produced an error *inside* + this function, level would be correct, but + for the function calling this function, level - 1 + is correct. + ]] + return level - 1 +end + +-- gets the name of the function that the user called +-- that eventually caused an error +local function getUserCalledFunctionName() + return debug.getinfo(getUserErrorLevel() - 1).name +end + +local function checkCondition(condition, message) + if condition then return end + error(message, getUserErrorLevel()) +end + +-- changes a list of types into a human-readable phrase +-- i.e. string, table, number -> "string, table, or number" +local function getAllowedTypesText(...) + local numberOfArguments = select('#', ...) + if numberOfArguments >= 3 then + local text = '' + for i = 1, numberOfArguments - 1 do + text = text .. string.format('%s, ', select(i, ...)) + end + text = text .. string.format('or %s', select(numberOfArguments, ...)) + return text + elseif numberOfArguments == 2 then + return string.format('%s or %s', select(1, ...), select(2, ...)) + end + return select(1, ...) +end + +-- checks if an argument is of the correct type, and if not, +-- throws a "bad argument" error consistent with the ones +-- lua and love produce +local function checkArgument(argumentIndex, argument, ...) + for i = 1, select('#', ...) do + -- allow tables with the __call metamethod to be treated like functions + if select(i, ...) == 'function' then + if type(argument) == 'table' and getmetatable(argument).__call then + return + end + end + if type(argument) == select(i, ...) then return end + end + error( + string.format( + "bad argument #%i to '%s' (expected %s, got %s)", + argumentIndex, + getUserCalledFunctionName(), + getAllowedTypesText(...), + type(argument) + ), + getUserErrorLevel() + ) +end + +local function checkOptionalArgument(argumentIndex, argument, ...) + if argument == nil then return end + checkArgument(argumentIndex, argument, ...) +end + +local function removeByValue(t, v) + for i = #t, 1, -1 do + if t[i] == v then table.remove(t, i) end + end +end + +local function entityHasKeys(entity, keys) + for _, key in ipairs(keys) do + if not entity[key] then return false end + end + return true +end + +local function filterEntity(entity, filter) + if type(filter) == 'table' then + return entityHasKeys(entity, filter) + elseif type(filter) == 'function' then + return filter(entity) + end + return true +end + +--- Defines the behaviors of a system. +-- +-- There's no constructor for SystemDefinitions. Rather, you simply +-- define a table with functions that correspond to events. These +-- events can be named anything you like. Below are the built-in events +-- that the pool will automatically call. +-- @type SystemDefinition + +--- Called when the pool is first created. +-- @function SystemDefinition:init +-- @param ... additional arguments that were passed to `nata.new`. + +--- Called when an entity is added to the pool. +-- @function SystemDefinition:add +-- @tparam table e the entity that was added + +--- Called when an entity is removed from the pool. +-- @function SystemDefinition:remove +-- @tparam table e the entity that was removed + +--- Called when an entity is added to a group. +-- @function SystemDefinition:addToGroup +-- @string groupName the name of the group that the entity was added to +-- @tparam table e the entity that was added + +--- Called when an entity is removed from a group. +-- @function SystemDefinition:removeFromGroup +-- @string groupName the name of the group that the entity was removed from +-- @tparam table e the entity that was removed + +--- Responds to events in a pool. +-- +-- Systems are not created directly. They're created by the @{Pool} +-- according to the @{SystemDefinition}s passed to `nata.new`. +-- Each system instance inherits all of the functions of its +-- @{SystemDefinition}. +-- @type System + +--- The @{Pool} that this system is running on. +-- @tfield Pool pool + +--- Manages a subset of entities. +-- @type Group + +--- The filter that defines which entities are added to this group. +-- Can be either: +-- +-- - A list of required keys +-- - A function that takes the entity as the first argument +-- and returns true if the entity should be added to the group +-- @tfield[opt] table|function filter + +--- A function that specifies how the entities in this group should be sorted. +-- Has the same requirements as the function argument to Lua's built-in `table.sort`. +-- @tfield[opt] function sort + +--- A list of all the entities in the group. +-- @tfield table entities + +--- A set of all the entities in the group. +-- @tfield table hasEntity +-- @usage +-- print(pool.groups.physical.hasEntity[e]) -- prints "true" if the entity is in the "physical" group, or "nil" if not + +--- Manages entities in a game world. +-- @type Pool +local Pool = {} +Pool.__index = Pool + +--- A list of all the entities in the pool. +-- @tfield table entities + +--- A set of all the entities in the pool. +-- @tfield table hasEntity +-- @usage +-- print(pool.hasEntity[e]) -- prints "true" if the entity is in the pool, or "nil" if not + +--- A dictionary of the @{Group}s in the pool. +-- @tfield table groups + +--- A field containing any data you want. +-- @field data + +--- + +function Pool:_validateOptions(options) + checkOptionalArgument(1, options, 'table') + if not options then return end + if options.groups then + checkCondition(type(options.groups) == 'table', "groups must be a table") + for groupName, groupOptions in pairs(options.groups) do + checkCondition(type(groupOptions) == 'table', + string.format("options for group '$s' must be a table", groupName)) + local filter = groupOptions.filter + if filter ~= nil then + checkCondition(type(filter) == 'table' or type(filter) == 'function', + string.format("filter for group '%s' must be a table or function", groupName)) + end + local sort = groupOptions.sort + if sort ~= nil then + checkCondition(type(sort) == 'function', + string.format("sort for group '%s' must be a function", groupName)) + end + end + end + if options.systems then + checkCondition(type(options.systems) == 'table', "systems must be a table") + for _, system in ipairs(options.systems) do + checkCondition(type(system) == 'table', "all systems must be tables") + end + end +end + +function Pool:_init(options, ...) + self:_validateOptions(options) + options = options or {} + -- entities that will be added to the pool on the next flush + self._queue = {} + -- a temporary table for entities that will be added to the pool + -- on the current flush (see Pool.flush for more details) + self._entitiesToFlush = {} + self.entities = {} + self.hasEntity = {} + self.groups = {} + self._systems = {} + self._events = {} + self.data = options.data or {} + local groups = options.groups or {} + local systems = options.systems or { nata.oop() } + for groupName, groupOptions in pairs(groups) do + self.groups[groupName] = { + filter = groupOptions.filter, + sort = groupOptions.sort, + entities = {}, + hasEntity = {}, + } + end + for _, systemDefinition in ipairs(systems) do + local system = setmetatable({ + pool = self, + }, { __index = systemDefinition }) + table.insert(self._systems, system) + end + self:emit('init', ...) +end + +--- Queues an entity to be added to the pool. +-- @tparam table entity the entity to add +-- @treturn table the queued entity +function Pool:queue(entity) + table.insert(self._queue, entity) + return entity +end + +--- Adds the queued entities to the pool. Entities are added +-- in the order they were queued. +function Pool:flush() + --[[ + Move the currently queued entities to a temporary + table. This way, if an add/addToGroup/removeToGroup + event emission leads to another entity being queued, + it will be saved for the next flush, rather than + adding entities to the table we're in the middle + of iterating over, which would lead to an array with + holes and screw everything up. + ]] + for i = 1, #self._queue do + local entity = self._queue[i] + self._entitiesToFlush[i] = entity + self._queue[i] = nil + end + for i = 1, #self._entitiesToFlush do + local entity = self._entitiesToFlush[i] + -- check if the entity belongs in each group and + -- add it to/remove it from the group as needed + for groupName, group in pairs(self.groups) do + if filterEntity(entity, group.filter) then + if not group.hasEntity[entity] then + table.insert(group.entities, entity) + group.hasEntity[entity] = true + self:emit('addToGroup', groupName, entity) + end + if group.sort then group._needsResort = true end + elseif group.hasEntity[entity] then + removeByValue(group.entities, entity) + group.hasEntity[entity] = nil + self:emit('removeFromGroup', groupName, entity) + end + end + -- add the entity to the pool if it hasn't been added already + if not self.hasEntity[entity] then + table.insert(self.entities, entity) + self.hasEntity[entity] = true + self:emit('add', entity) + end + self._entitiesToFlush[i] = nil + end + -- re-sort groups + for _, group in pairs(self.groups) do + if group._needsResort then + table.sort(group.entities, group.sort) + group._needsResort = nil + end + end +end + +--- Removes entities from the pool. +-- @tparam function f the condition upon which an entity should be +-- removed. The function should take an entity as the first argument +-- and return `true` if the entity should be removed. +function Pool:remove(f) + checkArgument(1, f, 'function') + for groupName, group in pairs(self.groups) do + for i = #group.entities, 1, -1 do + local entity = group.entities[i] + if f(entity) then + self:emit('removeFromGroup', groupName, entity) + table.remove(group.entities, i) + group.hasEntity[entity] = nil + end + end + end + for i = #self.entities, 1, -1 do + local entity = self.entities[i] + if f(entity) then + self:emit('remove', entity) + table.remove(self.entities, i) + self.hasEntity[entity] = nil + end + end +end + +--- Registers a function to be called when an event is emitted. +-- @string event the event to listen for +-- @tparam function f the function to call +-- @treturn function the function that was registered +function Pool:on(event, f) + checkCondition(event ~= nil, "event cannot be nil") + checkArgument(2, f, 'function') + self._events[event] = self._events[event] or {} + table.insert(self._events[event], f) + return f +end + +--- Unregisters a function from an event. +-- @string event the event to unregister the function from +-- @tparam function f the function to unregister +function Pool:off(event, f) + checkCondition(event ~= nil, "event cannot be nil") + checkArgument(2, f, 'function') + if self._events[event] then + removeByValue(self._events[event], f) + end +end + +--- Emits an event. The `system[event]` function will be called +-- for each system that has it, and functions registered +-- to the event will be called as well. +-- @string event the event to emit +-- @param ... additional arguments to pass to the functions that are called +function Pool:emit(event, ...) + checkCondition(event ~= nil, "event cannot be nil") + for _, system in ipairs(self._systems) do + if type(system[event]) == 'function' then + system[event](system, ...) + end + end + if self._events[event] then + for _, f in ipairs(self._events[event]) do + f(...) + end + end +end + +--- Gets this pool's instance of a system. +-- @tparam SystemDefinition systemDefinition the system class to get the instance of +-- @treturn System the instance of the system running in this pool +function Pool:getSystem(systemDefinition) + checkArgument(1, systemDefinition, 'table') + for _, system in ipairs(self._systems) do + if getmetatable(system).__index == systemDefinition then + return system + end + end +end + +--- +-- @section end + +local function validateOopOptions(options) + checkOptionalArgument(1, options, 'table') + if not options then return end + if options.include then + checkCondition(type(options.include) == 'table', "include must be a table") + end + if options.exclude then + checkCondition(type(options.exclude) == 'table', "exclude must be a table") + end +end + +--- Defines the behavior of an OOP system. +-- @type OopOptions +-- @see nata.oop + +--- A list of events to forward to entities. If not defined, +-- the system will forward all events to entities (except +-- for the ones in the exclude list). +-- @tfield table include + +--- A list of events *not* to forward to entities. +-- @tfield table exclude + +--- The name of the group of entities to forward events to. +-- If not defined, the system will forward events to all entities. +-- @tfield string group + +--- +-- @section end + +--- Creates a new OOP system definition. +-- An OOP system, upon receiving an event, will call +-- the function of the same name on each entity it monitors +-- (if it exists). This facilitates a more traditional, OOP-style +-- entity management, where you loop over a table of entities and +-- call update and draw functions on them. +-- @tparam[opt] OopOptions options how to set up the OOP system +-- @treturn SystemDefinition the new OOP system definition +function nata.oop(options) + validateOopOptions(options) + local group = options and options.group + local include, exclude + if options and options.include then + include = {} + for _, event in ipairs(options.include) do + include[event] = true + end + end + if options and options.exclude then + exclude = {} + for _, event in ipairs(options.exclude) do + exclude[event] = true + end + end + return setmetatable({ _cache = {} }, { + __index = function(t, event) + t._cache[event] = t._cache[event] or function(self, ...) + local shouldCallEvent = true + if include and not include[event] then shouldCallEvent = false end + if exclude and exclude[event] then shouldCallEvent = false end + if shouldCallEvent then + local entities + -- not using ternary here because if the group doesn't exist, + -- i'd rather it cause an error than just silently falling back + -- to the main entity pool + if group then + entities = self.pool.groups[group].entities + else + entities = self.pool.entities + end + for _, entity in ipairs(entities) do + if type(entity[event]) == 'function' then + entity[event](entity, ...) + end + end + end + end + return t._cache[event] + end + }) +end + +--- Defines the filter and sort function for a @{Group}. +-- @type GroupOptions + +--- The filter that defines which entities are added to this group. +-- Can be either: +-- +-- - A list of required keys +-- - A function that takes the entity as the first argument +-- and returns true if the entity should be added to the group +-- @tfield[opt] table|function filter + +--- A function that specifies how the entities in this group should be sorted. +-- Has the same requirements as the function argument to Lua's built-in `table.sort`. +-- @tfield[opt] function sort + +--- Defines the groups and systems for a @{Pool}. +-- @type PoolOptions + +--- A dictionary of groups for the pool to have. +-- Each key is the name of the group, and each value +-- should be a @{GroupOptions} table. +-- @tfield[opt={}] table groups + +--- A list of @{SystemDefinition}s for the pool to use. +-- @tfield[opt={nata.oop()}] table systems + +--- An initial value to set @{Pool.data} to. +-- @field[opt={}] data + +--- +-- @section end + +--- Creates a new @{Pool}. +-- @tparam[opt] PoolOptions options how to set up the pool +-- @param[opt] ... additional arguments to pass to the pool's init event +-- @treturn Pool the new pool +function nata.new(options, ...) + local pool = setmetatable({}, Pool) + pool:_init(options, ...) + return pool +end + +return nata diff --git a/two_player_cleaning_game/main.fnl b/two_player_cleaning_game/main.fnl index 27ffc00..72299dd 100644 --- a/two_player_cleaning_game/main.fnl +++ b/two_player_cleaning_game/main.fnl @@ -10,7 +10,7 @@ ; (utils.debug-print assets) ; (local network (require "network.fnl")) -(var world nil) +(var pool nil) ;; global vars ; (var level-key "tutorial") @@ -174,8 +174,8 @@ (fn love.load [] (love.window.setMode screen.screen-w screen.screen-h) ; (tset screen :canvas (love.graphics.newCanvas screen.canvas-w screen.canvas-h)) - (set world (tutorial.load screen)) - (utils.debug-print world) + (set pool (tutorial.load screen)) + ; (utils.debug-print world) ; (load-assets) ; (load-level :tutorial) ;; Initialize network @@ -203,9 +203,8 @@ while 1 do love.event.push('stdin', io.read('*line')) end") :start)) (fn love.update [dt] - (let [filter (tiny.requireAll :update)] - ; (utils.debug-print filter) - (tiny.update world dt filter))) + (pool:flush) + (pool:emit :update dt)) ; (fn love.update [dt] ; (each [_ obj (pairs objects.list)] @@ -317,11 +316,10 @@ while 1 do love.event.push('stdin', io.read('*line')) end") :start)) ; ; (print (fennel.view obj)) ; (obj:draw))) -(fn love.draw [dt] - (tiny.update world dt (tiny.requireAll :pre-draw)) - (tiny.update world dt (tiny.requireAll :draw)) - ; (world:update dt (tiny.requireAll "post-draw")) - ) +(fn love.draw [] + (pool:emit :pre-draw) + (pool:emit :draw) + (pool:emit :post-draw)) ; (fn love.draw [] ; (love.graphics.setCanvas screen.canvas) ; (love.graphics.clear) diff --git a/two_player_cleaning_game/src/colors.fnl b/two_player_cleaning_game/src/colors.fnl index 049b680..9c6bd9b 100644 --- a/two_player_cleaning_game/src/colors.fnl +++ b/two_player_cleaning_game/src/colors.fnl @@ -1,3 +1,5 @@ +(local utils (require "src/utils.fnl")) + ;; colors (lambda color [full-r full-g full-b] (let [(r g b) (love.math.colorFromBytes full-r full-g full-b)] @@ -19,12 +21,12 @@ "reset color to white (no tinting)" (love.graphics.setColor 1 1 1)) -(lambda set-color [color] +(lambda set-color [color-name] "set color to the given color from the color pallet" - (love.graphics.setColor (unpack (. colors color)))) + (love.graphics.setColor (unpack (. colors color-name)))) { - :reset-color reset-color - :set-color set-color + : reset-color + : set-color : colors } diff --git a/two_player_cleaning_game/src/levels/tutorial.fnl b/two_player_cleaning_game/src/levels/tutorial.fnl index 4d659df..3f4bbb9 100644 --- a/two_player_cleaning_game/src/levels/tutorial.fnl +++ b/two_player_cleaning_game/src/levels/tutorial.fnl @@ -1,46 +1,37 @@ (local color (require "src/colors.fnl")) -(local tiny (require "libs/tiny")) +(local nata (require "libs/nata")) (local utils (require "src/utils.fnl")) -(local level {}) +(lambda gen-renderer [screen canvas] + (let [renderer {}] + (fn renderer.draw [self] + (color.set-color :cream) + (love.graphics.rectangle "fill" 0 0 screen.canvas-w screen.canvas-h)) + (fn renderer.post-draw [self] + (love.graphics.setCanvas) + (color:reset-color) + (love.graphics.draw canvas 0 0 0 screen.scale screen.scale)) + (fn renderer.pre-draw [self] + ; use canvas + (love.graphics.setCanvas canvas) + ; clear the screen + (love.graphics.clear)) + renderer)) -; function HudSystem:init(layerFlag) -; self.filter = tiny.requireAll("drawHud", layerFlag) -; end - -; function HudSystem:process(e, dt) -; e:drawHud(dt) -; end -(fn bg-render [screen] - (let [system (tiny.system {: screen})] - (fn system.draw [self] - (print "Drawing BG") - (color.set-color color.colors.cream) - (love.graphics.rectangle "fill" 0 0 self.screen.canvas-w self.screen.canvas-h)) - (utils.debug-print system) - system - )) - -(local root-render (tiny.system)) -(fn root-render.pre-draw [self] - (print "Pre-drawing root") - ; use canvas - (love.graphics.setCanvas level.canvas) - ; clear the screen - (love.graphics.clear)) - - -(local camera {:x 0 :y 0}) - -(local debug true) +; (local camera {:x 0 :y 0}) +; (local debug true) (fn load [screen] - (set level.canvas (love.graphics.newCanvas screen.canvas-w screen.canvas-h)) - (tiny.world - root-render - (bg-render screen))) - + (let [canvas (love.graphics.newCanvas screen.canvas-w screen.canvas-h)] + (nata.new { + ; :groups { + ; gravity = {filter = {'gravity'}}, + ; }, + :systems [ + (gen-renderer screen canvas) + ] + }))) { : load