Compare commits

..

No commits in common. "f69f08f656269f47e62e32019d396a2186a51ae7" and "ffe9d8226d6b485ff74475a6d197b6172aa686f3" have entirely different histories.

15 changed files with 327 additions and 2052 deletions

View file

@ -101,4 +101,4 @@
(fs/write-lines "../levels.fnl" ["(local levels"
(str/replace (pr-str levels) #",+" "")
")\n\n"
"levels"])
"{ :levels levels }"])

View file

@ -1,9 +1,9 @@
{
"activeFile": "objects.tsx",
"activeFile": "tutorial.tmx",
"expandedProjectPaths": [
"/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game/assets",
"/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game",
".",
"/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game"
"/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game/assets"
],
"fileStates": {
"level_001.tmx": {
@ -60,8 +60,8 @@
"recentFiles": [
"level_001.tmx",
"walls.tsx",
"tutorial.tmx",
"objects.tsx",
"tutorial.tmx",
"map_tileset.tsx"
],
"tileset.lastUsedFormat": "tsx",

File diff suppressed because one or more lines are too long

View file

@ -1,175 +0,0 @@
-- beholder.lua - v2.1.1 (2011-11)
-- Copyright (c) 2011 Enrique García Cota
-- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN callback OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function copy(t)
local c = {}
for i = 1, #t do c[i] = t[i] end
return c
end
local function hash2array(t)
local arr, i = {}, 0
for _, v in pairs(t) do
i = i + 1
arr[i] = v
end
return arr, i
end
-- private Node class
local nodesById = nil
local root = nil
local function newNode()
return { callbacks = {}, children = setmetatable({}, { __mode = "k" }) }
end
local function findNodeById(id)
return nodesById[id]
end
local function findOrCreateChildNode(self, key)
self.children[key] = self.children[key] or newNode()
return self.children[key]
end
local function findOrCreateDescendantNode(self, keys)
local node = self
for i = 1, #keys do
node = findOrCreateChildNode(node, keys[i])
end
return node
end
local function invokeNodeCallbacks(self, params)
-- copy the hash into an array, for safety (self-erasures)
local callbacks, count = hash2array(self.callbacks)
for i = 1, #callbacks do
callbacks[i](unpack(params))
end
return count
end
local function invokeAllNodeCallbacksInSubTree(self, params)
local counter = invokeNodeCallbacks(self, params)
for _, child in pairs(self.children) do
counter = counter + invokeAllNodeCallbacksInSubTree(child, params)
end
return counter
end
local function invokeNodeCallbacksFromPath(self, path)
local node = self
local params = copy(path)
local counter = invokeNodeCallbacks(node, params)
for i = 1, #path do
node = node.children[path[i]]
if not node then break end
table.remove(params, 1)
counter = counter + invokeNodeCallbacks(node, params)
end
return counter
end
local function addCallbackToNode(self, callback)
local id = {}
self.callbacks[id] = callback
nodesById[id] = self
return id
end
local function removeCallbackFromNode(self, id)
self.callbacks[id] = nil
nodesById[id] = nil
end
------ beholder table
local beholder = {}
-- beholder private functions/vars
local groups = nil
local currentGroupId = nil
local function addIdToCurrentGroup(id)
if currentGroupId then
groups[currentGroupId] = groups[currentGroupId] or setmetatable({}, { __mode = "k" })
local group = groups[currentGroupId]
group[#group + 1] = id
end
return id
end
local function stopObservingGroup(group)
local count = #group
for i = 1, count do
beholder.stopObserving(group[i])
end
return count
end
local function falseIfZero(n)
return n > 0 and n
end
local function extractEventAndCallbackFromParams(params)
assert(#params > 0,
"beholder.observe requires at least one parameter - the callback. You usually want to use two, i.e.: beholder.observe('EVENT', callback)")
local callback = table.remove(params, #params)
return params, callback
end
------ Public interface
function beholder.observe(...)
local event, callback = extractEventAndCallbackFromParams({ ... })
local node = findOrCreateDescendantNode(root, event)
return addIdToCurrentGroup(addCallbackToNode(node, callback))
end
function beholder.stopObserving(id)
local node = findNodeById(id)
if node then removeCallbackFromNode(node, id) end
local group, count = groups[id], 0
if group then count = stopObservingGroup(group) end
return (node or count > 0) and true or false
end
function beholder.group(groupId, f)
assert(not currentGroupId, "beholder.group can not be nested!")
currentGroupId = groupId
f()
currentGroupId = nil
end
function beholder.trigger(...)
return falseIfZero(invokeNodeCallbacksFromPath(root, { ... }))
end
function beholder.triggerAll(...)
return falseIfZero(invokeAllNodeCallbacksInSubTree(root, { ... }))
end
function beholder.reset()
root = newNode()
nodesById = setmetatable({}, { __mode = "k" })
groups = {}
currentGroupId = nil
end
beholder.reset()
return beholder

View file

@ -1,546 +0,0 @@
--- 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

View file

@ -1,863 +0,0 @@
--[[
Copyright (c) 2016 Calvin Rose
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.
]]
--- @module tiny-ecs
-- @author Calvin Rose
-- @license MIT
-- @copyright 2016
local tiny = {}
-- Local versions of standard lua functions
local tinsert = table.insert
local tremove = table.remove
local tsort = table.sort
local setmetatable = setmetatable
local type = type
local select = select
-- Local versions of the library functions
local tiny_manageEntities
local tiny_manageSystems
local tiny_addEntity
local tiny_addSystem
local tiny_add
local tiny_removeEntity
local tiny_removeSystem
--- Filter functions.
-- A Filter is a function that selects which Entities apply to a System.
-- Filters take two parameters, the System and the Entity, and return a boolean
-- value indicating if the Entity should be processed by the System. A truthy
-- value includes the entity, while a falsey (nil or false) value excludes the
-- entity.
--
-- Filters must be added to Systems by setting the `filter` field of the System.
-- Filter's returned by tiny-ecs's Filter functions are immutable and can be
-- used by multiple Systems.
--
-- local f1 = tiny.requireAll("position", "velocity", "size")
-- local f2 = tiny.requireAny("position", "velocity", "size")
--
-- local e1 = {
-- position = {2, 3},
-- velocity = {3, 3},
-- size = {4, 4}
-- }
--
-- local entity2 = {
-- position = {4, 5},
-- size = {4, 4}
-- }
--
-- local e3 = {
-- position = {2, 3},
-- velocity = {3, 3}
-- }
--
-- print(f1(nil, e1), f1(nil, e2), f1(nil, e3)) -- prints true, false, false
-- print(f2(nil, e1), f2(nil, e2), f2(nil, e3)) -- prints true, true, true
--
-- Filters can also be passed as arguments to other Filter constructors. This is
-- a powerful way to create complex, custom Filters that select a very specific
-- set of Entities.
--
-- -- Selects Entities with an "image" Component, but not Entities with a
-- -- "Player" or "Enemy" Component.
-- filter = tiny.requireAll("image", tiny.rejectAny("Player", "Enemy"))
--
-- @section Filter
-- A helper function to compile filters.
local filterJoin
-- A helper function to filters from string
local filterBuildString
do
local loadstring = loadstring or load
local function getchr(c)
return "\\" .. c:byte()
end
local function make_safe(text)
return ("%q"):format(text):gsub('\n', 'n'):gsub("[\128-\255]", getchr)
end
local function filterJoinRaw(prefix, seperator, ...)
local accum = {}
local build = {}
for i = 1, select('#', ...) do
local item = select(i, ...)
if type(item) == 'string' then
accum[#accum + 1] = ("(e[%s] ~= nil)"):format(make_safe(item))
elseif type(item) == 'function' then
build[#build + 1] = ('local subfilter_%d_ = select(%d, ...)')
:format(i, i)
accum[#accum + 1] = ('(subfilter_%d_(system, e))'):format(i)
else
error 'Filter token must be a string or a filter function.'
end
end
local source = ('%s\nreturn function(system, e) return %s(%s) end')
:format(
table.concat(build, '\n'),
prefix,
table.concat(accum, seperator))
local loader, err = loadstring(source)
if err then error(err) end
return loader(...)
end
function filterJoin(...)
local state, value = pcall(filterJoinRaw, ...)
if state then return value else return nil, value end
end
local function buildPart(str)
local accum = {}
local subParts = {}
str = str:gsub('%b()', function(p)
subParts[#subParts + 1] = buildPart(p:sub(2, -2))
return ('\255%d'):format(#subParts)
end)
for invert, part, sep in str:gmatch('(%!?)([^%|%&%!]+)([%|%&]?)') do
if part:match('^\255%d+$') then
local partIndex = tonumber(part:match(part:sub(2)))
accum[#accum + 1] = ('%s(%s)')
:format(invert == '' and '' or 'not', subParts[partIndex])
else
accum[#accum + 1] = ("(e[%s] %s nil)")
:format(make_safe(part), invert == '' and '~=' or '==')
end
if sep ~= '' then
accum[#accum + 1] = (sep == '|' and ' or ' or ' and ')
end
end
return table.concat(accum)
end
function filterBuildString(str)
local source = ("return function(_, e) return %s end")
:format(buildPart(str))
local loader, err = loadstring(source)
if err then
error(err)
end
return loader()
end
end
--- Makes a Filter that selects Entities with all specified Components and
-- Filters.
function tiny.requireAll(...)
return filterJoin('', ' and ', ...)
end
--- Makes a Filter that selects Entities with at least one of the specified
-- Components and Filters.
function tiny.requireAny(...)
return filterJoin('', ' or ', ...)
end
--- Makes a Filter that rejects Entities with all specified Components and
-- Filters, and selects all other Entities.
function tiny.rejectAll(...)
return filterJoin('not', ' and ', ...)
end
--- Makes a Filter that rejects Entities with at least one of the specified
-- Components and Filters, and selects all other Entities.
function tiny.rejectAny(...)
return filterJoin('not', ' or ', ...)
end
--- Makes a Filter from a string. Syntax of `pattern` is as follows.
--
-- * Tokens are alphanumeric strings including underscores.
-- * Tokens can be separated by |, &, or surrounded by parentheses.
-- * Tokens can be prefixed with !, and are then inverted.
--
-- Examples are best:
-- 'a|b|c' - Matches entities with an 'a' OR 'b' OR 'c'.
-- 'a&!b&c' - Matches entities with an 'a' AND NOT 'b' AND 'c'.
-- 'a|(b&c&d)|e - Matches 'a' OR ('b' AND 'c' AND 'd') OR 'e'
-- @param pattern
function tiny.filter(pattern)
local state, value = pcall(filterBuildString, pattern)
if state then return value else return nil, value end
end
--- System functions.
-- A System is a wrapper around function callbacks for manipulating Entities.
-- Systems are implemented as tables that contain at least one method;
-- an update function that takes parameters like so:
--
-- * `function system:update(dt)`.
--
-- There are also a few other optional callbacks:
--
-- * `function system:filter(entity)` - Returns true if this System should
-- include this Entity, otherwise should return false. If this isn't specified,
-- no Entities are included in the System.
-- * `function system:onAdd(entity)` - Called when an Entity is added to the
-- System.
-- * `function system:onRemove(entity)` - Called when an Entity is removed
-- from the System.
-- * `function system:onModify(dt)` - Called when the System is modified by
-- adding or removing Entities from the System.
-- * `function system:onAddToWorld(world)` - Called when the System is added
-- to the World, before any entities are added to the system.
-- * `function system:onRemoveFromWorld(world)` - Called when the System is
-- removed from the world, after all Entities are removed from the System.
-- * `function system:preWrap(dt)` - Called on each system before update is
-- called on any system.
-- * `function system:postWrap(dt)` - Called on each system in reverse order
-- after update is called on each system. The idea behind `preWrap` and
-- `postWrap` is to allow for systems that modify the behavior of other systems.
-- Say there is a DrawingSystem, which draws sprites to the screen, and a
-- PostProcessingSystem, that adds some blur and bloom effects. In the preWrap
-- method of the PostProcessingSystem, the System could set the drawing target
-- for the DrawingSystem to a special buffer instead the screen. In the postWrap
-- method, the PostProcessingSystem could then modify the buffer and render it
-- to the screen. In this setup, the PostProcessingSystem would be added to the
-- World after the drawingSystem (A similar but less flexible behavior could
-- be accomplished with a single custom update function in the DrawingSystem).
--
-- For Filters, it is convenient to use `tiny.requireAll` or `tiny.requireAny`,
-- but one can write their own filters as well. Set the Filter of a System like
-- so:
-- system.filter = tiny.requireAll("a", "b", "c")
-- or
-- function system:filter(entity)
-- return entity.myRequiredComponentName ~= nil
-- end
--
-- All Systems also have a few important fields that are initialized when the
-- system is added to the World. A few are important, and few should be less
-- commonly used.
--
-- * The `world` field points to the World that the System belongs to. Useful
-- for adding and removing Entities from the world dynamically via the System.
-- * The `active` flag is whether or not the System is updated automatically.
-- Inactive Systems should be updated manually or not at all via
-- `system:update(dt)`. Defaults to true.
-- * The `entities` field is an ordered list of Entities in the System. This
-- list can be used to quickly iterate through all Entities in a System.
-- * The `interval` field is an optional field that makes Systems update at
-- certain intervals using buffered time, regardless of World update frequency.
-- For example, to make a System update once a second, set the System's interval
-- to 1.
-- * The `index` field is the System's index in the World. Lower indexed
-- Systems are processed before higher indices. The `index` is a read only
-- field; to set the `index`, use `tiny.setSystemIndex(world, system)`.
-- * The `indices` field is a table of Entity keys to their indices in the
-- `entities` list. Most Systems can ignore this.
-- * The `modified` flag is an indicator if the System has been modified in
-- the last update. If so, the `onModify` callback will be called on the System
-- in the next update, if it has one. This is usually managed by tiny-ecs, so
-- users should mostly ignore this, too.
--
-- There is another option to (hopefully) increase performance in systems that
-- have items added to or removed from them often, and have lots of entities in
-- them. Setting the `nocache` field of the system might improve performance.
-- It is still experimental. There are some restriction to systems without
-- caching, however.
--
-- * There is no `entities` table.
-- * Callbacks such onAdd, onRemove, and onModify will never be called
-- * Noncached systems cannot be sorted (There is no entities list to sort).
--
-- @section System
-- Use an empty table as a key for identifying Systems. Any table that contains
-- this key is considered a System rather than an Entity.
local systemTableKey = { "SYSTEM_TABLE_KEY" }
-- Checks if a table is a System.
local function isSystem(table)
return table[systemTableKey]
end
-- Update function for all Processing Systems.
local function processingSystemUpdate(system, dt)
local preProcess = system.preProcess
local process = system.process
local postProcess = system.postProcess
if preProcess then
preProcess(system, dt)
end
if process then
if system.nocache then
local entities = system.world.entities
local filter = system.filter
if filter then
for i = 1, #entities do
local entity = entities[i]
if filter(system, entity) then
process(system, entity, dt)
end
end
end
else
local entities = system.entities
for i = 1, #entities do
process(system, entities[i], dt)
end
end
end
if postProcess then
postProcess(system, dt)
end
end
-- Sorts Systems by a function system.sortDelegate(entity1, entity2) on modify.
local function sortedSystemOnModify(system)
local entities = system.entities
local indices = system.indices
local sortDelegate = system.sortDelegate
if not sortDelegate then
local compare = system.compare
sortDelegate = function(e1, e2)
return compare(system, e1, e2)
end
system.sortDelegate = sortDelegate
end
tsort(entities, sortDelegate)
for i = 1, #entities do
indices[entities[i]] = i
end
end
--- Creates a new System or System class from the supplied table. If `table` is
-- nil, creates a new table.
function tiny.system(table)
table = table or {}
table[systemTableKey] = true
return table
end
--- Creates a new Processing System or Processing System class. Processing
-- Systems process each entity individual, and are usually what is needed.
-- Processing Systems have three extra callbacks besides those inheritted from
-- vanilla Systems.
--
-- function system:preProcess(dt) -- Called before iteration.
-- function system:process(entity, dt) -- Process each entity.
-- function system:postProcess(dt) -- Called after iteration.
--
-- Processing Systems have their own `update` method, so don't implement a
-- a custom `update` callback for Processing Systems.
-- @see system
function tiny.processingSystem(table)
table = table or {}
table[systemTableKey] = true
table.update = processingSystemUpdate
return table
end
--- Creates a new Sorted System or Sorted System class. Sorted Systems sort
-- their Entities according to a user-defined method, `system:compare(e1, e2)`,
-- which should return true if `e1` should come before `e2` and false otherwise.
-- Sorted Systems also override the default System's `onModify` callback, so be
-- careful if defining a custom callback. However, for processing the sorted
-- entities, consider `tiny.sortedProcessingSystem(table)`.
-- @see system
function tiny.sortedSystem(table)
table = table or {}
table[systemTableKey] = true
table.onModify = sortedSystemOnModify
return table
end
--- Creates a new Sorted Processing System or Sorted Processing System class.
-- Sorted Processing Systems have both the aspects of Processing Systems and
-- Sorted Systems.
-- @see system
-- @see processingSystem
-- @see sortedSystem
function tiny.sortedProcessingSystem(table)
table = table or {}
table[systemTableKey] = true
table.update = processingSystemUpdate
table.onModify = sortedSystemOnModify
return table
end
--- World functions.
-- A World is a container that manages Entities and Systems. Typically, a
-- program uses one World at a time.
--
-- For all World functions except `tiny.world(...)`, object-oriented syntax can
-- be used instead of the documented syntax. For example,
-- `tiny.add(world, e1, e2, e3)` is the same as `world:add(e1, e2, e3)`.
-- @section World
-- Forward declaration
local worldMetaTable
--- Creates a new World.
-- Can optionally add default Systems and Entities. Returns the new World along
-- with default Entities and Systems.
function tiny.world(...)
local ret = setmetatable({
-- List of Entities to remove
entitiesToRemove = {},
-- List of Entities to change
entitiesToChange = {},
-- List of Entities to add
systemsToAdd = {},
-- List of Entities to remove
systemsToRemove = {},
-- Set of Entities
entities = {},
-- List of Systems
systems = {}
}, worldMetaTable)
tiny_add(ret, ...)
tiny_manageSystems(ret)
tiny_manageEntities(ret)
return ret, ...
end
--- Adds an Entity to the world.
-- Also call this on Entities that have changed Components such that they
-- match different Filters. Returns the Entity.
function tiny.addEntity(world, entity)
local e2c = world.entitiesToChange
e2c[#e2c + 1] = entity
return entity
end
tiny_addEntity = tiny.addEntity
--- Adds a System to the world. Returns the System.
function tiny.addSystem(world, system)
assert(system.world == nil, "System already belongs to a World.")
local s2a = world.systemsToAdd
s2a[#s2a + 1] = system
system.world = world
return system
end
tiny_addSystem = tiny.addSystem
--- Shortcut for adding multiple Entities and Systems to the World. Returns all
-- added Entities and Systems.
function tiny.add(world, ...)
for i = 1, select("#", ...) do
local obj = select(i, ...)
if obj then
if isSystem(obj) then
tiny_addSystem(world, obj)
else -- Assume obj is an Entity
tiny_addEntity(world, obj)
end
end
end
return ...
end
tiny_add = tiny.add
--- Removes an Entity from the World. Returns the Entity.
function tiny.removeEntity(world, entity)
local e2r = world.entitiesToRemove
e2r[#e2r + 1] = entity
return entity
end
tiny_removeEntity = tiny.removeEntity
--- Removes a System from the world. Returns the System.
function tiny.removeSystem(world, system)
assert(system.world == world, "System does not belong to this World.")
local s2r = world.systemsToRemove
s2r[#s2r + 1] = system
return system
end
tiny_removeSystem = tiny.removeSystem
--- Shortcut for removing multiple Entities and Systems from the World. Returns
-- all removed Systems and Entities
function tiny.remove(world, ...)
for i = 1, select("#", ...) do
local obj = select(i, ...)
if obj then
if isSystem(obj) then
tiny_removeSystem(world, obj)
else -- Assume obj is an Entity
tiny_removeEntity(world, obj)
end
end
end
return ...
end
-- Adds and removes Systems that have been marked from the World.
function tiny_manageSystems(world)
local s2a, s2r = world.systemsToAdd, world.systemsToRemove
-- Early exit
if #s2a == 0 and #s2r == 0 then
return
end
world.systemsToAdd = {}
world.systemsToRemove = {}
local worldEntityList = world.entities
local systems = world.systems
-- Remove Systems
for i = 1, #s2r do
local system = s2r[i]
local index = system.index
local onRemove = system.onRemove
if onRemove and not system.nocache then
local entityList = system.entities
for j = 1, #entityList do
onRemove(system, entityList[j])
end
end
tremove(systems, index)
for j = index, #systems do
systems[j].index = j
end
local onRemoveFromWorld = system.onRemoveFromWorld
if onRemoveFromWorld then
onRemoveFromWorld(system, world)
end
s2r[i] = nil
-- Clean up System
system.world = nil
system.entities = nil
system.indices = nil
system.index = nil
end
-- Add Systems
for i = 1, #s2a do
local system = s2a[i]
if systems[system.index or 0] ~= system then
if not system.nocache then
system.entities = {}
system.indices = {}
end
if system.active == nil then
system.active = true
end
system.modified = true
system.world = world
local index = #systems + 1
system.index = index
systems[index] = system
local onAddToWorld = system.onAddToWorld
if onAddToWorld then
onAddToWorld(system, world)
end
-- Try to add Entities
if not system.nocache then
local entityList = system.entities
local entityIndices = system.indices
local onAdd = system.onAdd
local filter = system.filter
if filter then
for j = 1, #worldEntityList do
local entity = worldEntityList[j]
if filter(system, entity) then
local entityIndex = #entityList + 1
entityList[entityIndex] = entity
entityIndices[entity] = entityIndex
if onAdd then
onAdd(system, entity)
end
end
end
end
end
end
s2a[i] = nil
end
end
-- Adds, removes, and changes Entities that have been marked.
function tiny_manageEntities(world)
local e2r = world.entitiesToRemove
local e2c = world.entitiesToChange
-- Early exit
if #e2r == 0 and #e2c == 0 then
return
end
world.entitiesToChange = {}
world.entitiesToRemove = {}
local entities = world.entities
local systems = world.systems
-- Change Entities
for i = 1, #e2c do
local entity = e2c[i]
-- Add if needed
if not entities[entity] then
local index = #entities + 1
entities[entity] = index
entities[index] = entity
end
for j = 1, #systems do
local system = systems[j]
if not system.nocache then
local ses = system.entities
local seis = system.indices
local index = seis[entity]
local filter = system.filter
if filter and filter(system, entity) then
if not index then
system.modified = true
index = #ses + 1
ses[index] = entity
seis[entity] = index
local onAdd = system.onAdd
if onAdd then
onAdd(system, entity)
end
end
elseif index then
system.modified = true
local tmpEntity = ses[#ses]
ses[index] = tmpEntity
seis[tmpEntity] = index
seis[entity] = nil
ses[#ses] = nil
local onRemove = system.onRemove
if onRemove then
onRemove(system, entity)
end
end
end
end
e2c[i] = nil
end
-- Remove Entities
for i = 1, #e2r do
local entity = e2r[i]
e2r[i] = nil
local listIndex = entities[entity]
if listIndex then
-- Remove Entity from world state
local lastEntity = entities[#entities]
entities[lastEntity] = listIndex
entities[entity] = nil
entities[listIndex] = lastEntity
entities[#entities] = nil
-- Remove from cached systems
for j = 1, #systems do
local system = systems[j]
if not system.nocache then
local ses = system.entities
local seis = system.indices
local index = seis[entity]
if index then
system.modified = true
local tmpEntity = ses[#ses]
ses[index] = tmpEntity
seis[tmpEntity] = index
seis[entity] = nil
ses[#ses] = nil
local onRemove = system.onRemove
if onRemove then
onRemove(system, entity)
end
end
end
end
end
end
end
--- Manages Entities and Systems marked for deletion or addition. Call this
-- before modifying Systems and Entities outside of a call to `tiny.update`.
-- Do not call this within a call to `tiny.update`.
function tiny.refresh(world)
tiny_manageSystems(world)
tiny_manageEntities(world)
local systems = world.systems
for i = #systems, 1, -1 do
local system = systems[i]
if system.active then
local onModify = system.onModify
if onModify and system.modified then
onModify(system, 0)
end
system.modified = false
end
end
end
--- Updates the World by dt (delta time). Takes an optional parameter, `filter`,
-- which is a Filter that selects Systems from the World, and updates only those
-- Systems. If `filter` is not supplied, all Systems are updated. Put this
-- function in your main loop.
function tiny.update(world, dt, filter)
tiny_manageSystems(world)
tiny_manageEntities(world)
local systems = world.systems
-- Iterate through Systems IN REVERSE ORDER
for i = #systems, 1, -1 do
local system = systems[i]
if system.active then
-- Call the modify callback on Systems that have been modified.
local onModify = system.onModify
if onModify and system.modified then
onModify(system, dt)
end
local preWrap = system.preWrap
if preWrap and
((not filter) or filter(world, system)) then
preWrap(system, dt)
end
end
end
-- Iterate through Systems IN ORDER
for i = 1, #systems do
local system = systems[i]
if system.active and ((not filter) or filter(world, system)) then
-- Update Systems that have an update method (most Systems)
local update = system.update
if update then
local interval = system.interval
if interval then
local bufferedTime = (system.bufferedTime or 0) + dt
while bufferedTime >= interval do
bufferedTime = bufferedTime - interval
update(system, interval)
end
system.bufferedTime = bufferedTime
else
update(system, dt)
end
end
system.modified = false
end
end
-- Iterate through Systems IN ORDER AGAIN
for i = 1, #systems do
local system = systems[i]
local postWrap = system.postWrap
if postWrap and system.active and
((not filter) or filter(world, system)) then
postWrap(system, dt)
end
end
end
--- Removes all Entities from the World.
function tiny.clearEntities(world)
local el = world.entities
for i = 1, #el do
tiny_removeEntity(world, el[i])
end
end
--- Removes all Systems from the World.
function tiny.clearSystems(world)
local systems = world.systems
for i = #systems, 1, -1 do
tiny_removeSystem(world, systems[i])
end
end
--- Gets number of Entities in the World.
function tiny.getEntityCount(world)
return #world.entities
end
--- Gets number of Systems in World.
function tiny.getSystemCount(world)
return #world.systems
end
--- Sets the index of a System in the World, and returns the old index. Changes
-- the order in which they Systems processed, because lower indexed Systems are
-- processed first. Returns the old system.index.
function tiny.setSystemIndex(world, system, index)
tiny_manageSystems(world)
local oldIndex = system.index
local systems = world.systems
if index < 0 then
index = tiny.getSystemCount(world) + 1 + index
end
tremove(systems, oldIndex)
tinsert(systems, index, system)
for i = oldIndex, index, index >= oldIndex and 1 or -1 do
systems[i].index = i
end
return oldIndex
end
-- Construct world metatable.
worldMetaTable = {
__index = {
add = tiny.add,
addEntity = tiny.addEntity,
addSystem = tiny.addSystem,
remove = tiny.remove,
removeEntity = tiny.removeEntity,
removeSystem = tiny.removeSystem,
refresh = tiny.refresh,
update = tiny.update,
clearEntities = tiny.clearEntities,
clearSystems = tiny.clearSystems,
getEntityCount = tiny.getEntityCount,
getSystemCount = tiny.getSystemCount,
setSystemIndex = tiny.setSystemIndex
},
__tostring = function()
return "<tiny-ecs_World>"
end
}
return tiny

View file

@ -1,32 +1,48 @@
;; deps
(local levels (. (require "levels.fnl") :levels))
(local bump (require "libs/bump"))
(local tiny (require "libs/tiny"))
(local assets (require "src/assets.fnl"))
(local utils (require "src/utils.fnl"))
(local colors (require "src/colors.fnl"))
(local tutorial (require "src/levels/tutorial.fnl"))
(local bump (require "bump"))
(local network (require "network.fnl"))
; (utils.debug-print assets)
; (local network (require "network.fnl"))
;; colors
(lambda color [full-r full-g full-b]
(let [(r g b) (love.math.colorFromBytes full-r full-g full-b)]
[r g b]
))
(local color-pallet {
:cream (color 255 238 204)
:dark-purple (color 70 66 94)
:dark-pink (color 255 105 115)
:dark-blue (color 21 120 140)
:light-blue (color 0 185 190)
:light-pink (color 255 176 163)
:black [0 0 0]
:black-half-tone [0 0 0 0.25]
})
(var pool nil)
(fn reset-color []
"reset color to white (no tinting)"
(love.graphics.setColor 1 1 1))
(lambda set-color [color]
"set color to the given color from the color pallet"
(love.graphics.setColor (unpack (. color-pallet color))))
;; global vars
; (var level-key "tutorial")
; (var player-art nil) ; 25x50 pixels each player is 25x25
; (var dust-sprite nil) ; 35x35 pixels
; (var battery-bar-sprite nil)
; (local walls {
; :sprite nil
; :quads []
; :batch nil
; })
; (local objects {
; :sprite nil
; :quads {}
; :list []
; })
(var level-key "tutorial")
(var player-art nil) ; 25x50 pixels each player is 25x25
(var dust-sprite nil) ; 35x35 pixels
(var battery-bar-sprite nil)
(local walls {
:sprite nil
:quads []
:batch nil
})
(local objects {
:sprite nil
:quads {}
:list []
})
(local screen
(let [scale 2 canvas-w 800 canvas-h 450]
@ -34,156 +50,146 @@
:screen-h (* canvas-h scale)
:canvas-w canvas-w
:canvas-h canvas-h
:scale scale}))
:scale scale
:canvas nil}))
(local camera {:x 0 :y 0})
(local debug true)
(lambda debug-print [obj]
(print (fennel.view obj)))
; (local collider-debug-boxes [])
; (lambda add-collider-debug-box [x y w h]
; (table.insert collider-debug-boxes {: x : y :width w :height h}))
(local collider-debug-boxes [])
(lambda add-collider-debug-box [x y w h]
(table.insert collider-debug-boxes {: x : y :width w :height h}))
; (var bump-world nil)
; (local player { :x 50 :y 50 :w 25 :h 25 :speed 80 :battery 100 :rot 0 })
(var bump-world nil)
(local player { :x 50 :y 50 :w 25 :h 25 :speed 80 :battery 100 :rot 0 })
; (lambda mirror-collider [collider]
; "Mirror a collider box horizontally within a 25-unit tile (center is at 12.5)
; Transforms collider positions so they're reflected across the vertical center line."
; {
; :x (- 25 collider.x collider.width)
; :y collider.y
; :width collider.width
; :height collider.height
; })
(lambda mirror-collider [collider]
"Mirror a collider box horizontally within a 25-unit tile (center is at 12.5)
Transforms collider positions so they're reflected across the vertical center line."
{
:x (- 25 collider.x collider.width)
:y collider.y
:width collider.width
:height collider.height
})
; (fn load-walls []
; (set walls.batch (love.graphics.newSpriteBatch walls.sprite 2500))
; ;; load quads
; (let [(w h) (walls.sprite:getDimensions)]
; (for [i 0 19 1]
; (table.insert walls.quads (love.graphics.newQuad (* i 25) 0 25 25 w h))))
; ;; fill batch
; (each [_ row (pairs (. levels :levels level-key :tiles))]
; (each [_ tile (pairs row)]
; (let [
; x tile.x
; y tile.y
; id tile.tile-id
; colliders tile.colliders]
; (if (and (> id 0) (< id 21)) ;; 1-20 are wall tiles
; (do
; ; (print (fennel.view {:quad (. walls.quads id) : x : y : id}))
; (walls.batch:add (. walls.quads id) (if tile.h-flip (+ x 25) x) y 0 (if tile.h-flip -1 1) 1)
; (each [_ collider (pairs colliders)]
; (let [
; mirrored-collider (if tile.h-flip (mirror-collider collider) collider)
; collider-x (+ x mirrored-collider.x)
; collider-y (+ y mirrored-collider.y)]
; (bump-world:add {: x : y :name :wall :behavior :block} collider-x collider-y mirrored-collider.width mirrored-collider.height)
; (table.insert
; collider-debug-boxes
; {:x collider-x :y collider-y :width mirrored-collider.width :height mirrored-collider.height})))))))))
(fn load-walls []
(set walls.batch (love.graphics.newSpriteBatch walls.sprite 2500))
;; load quads
(let [(w h) (walls.sprite:getDimensions)]
(for [i 0 19 1]
(table.insert walls.quads (love.graphics.newQuad (* i 25) 0 25 25 w h))))
;; fill batch
(each [_ row (pairs (. levels :levels level-key :tiles))]
(each [_ tile (pairs row)]
(let [
x tile.x
y tile.y
id tile.tile-id
colliders tile.colliders]
(if (and (> id 0) (< id 21)) ;; 1-20 are wall tiles
(do
; (print (fennel.view {:quad (. walls.quads id) : x : y : id}))
(walls.batch:add (. walls.quads id) (if tile.h-flip (+ x 25) x) y 0 (if tile.h-flip -1 1) 1)
(each [_ collider (pairs colliders)]
(let [
mirrored-collider (if tile.h-flip (mirror-collider collider) collider)
collider-x (+ x mirrored-collider.x)
collider-y (+ y mirrored-collider.y)]
(bump-world:add {: x : y :name :wall :behavior :block} collider-x collider-y mirrored-collider.width mirrored-collider.height)
(table.insert
collider-debug-boxes
{:x collider-x :y collider-y :width mirrored-collider.width :height mirrored-collider.height})))))))))
; (lambda create-charging-station [x y]
; (let [
; station {: x : y :type :charging-station :active-pads {1 false 2 false 3 false}}
; pads {
; 1 {:x (- x 25) :y y :width 25 :height 25 :light-x (+ x 5)}
; 3 {:x (+ x 25) :y y :width 25 :height 25 :light-x (+ x 5 6 6)}
; 2 {:x x :y (+ y 25) :width 25 :height 25 :light-x (+ x 5 6)}
; }]
; (bump-world:add {: x : y :name :charging-station :behavior :block} x y 25 25)
; (add-collider-debug-box x y 25 25)
; (fn station.draw [self]
; (reset-color)
; (love.graphics.draw objects.sprite objects.quads.charging-station self.x self.y)
; (for [i 1 3 1]
; (let [pad (. pads i)]
; (if (. self.active-pads i)
; (do (love.graphics.draw objects.sprite objects.quads.charging-pad-active pad.x pad.y)
; (set-color :light-pink)
; (love.graphics.rectangle "fill" pad.light-x (+ y 18) 2 2)
; (reset-color))
; (love.graphics.draw objects.sprite objects.quads.charging-pad pad.x pad.y))))
; )
; (lambda pad-hover-cb [self dt]
; (tset station.active-pads self.id true)
; (when (< player.battery 100) (set player.battery (+ player.battery (* dt 6)))))
; (lambda station.update [self _dt]
; (set self.active-pads {1 false 2 false 3 false}))
; (for [i 1 3 1]
; (let [pad (. pads i)]
; (bump-world:add {:name :charging-pad :behavior :hover :hover-cb pad-hover-cb :id i} pad.x pad.y 25 25)
; (add-collider-debug-box pad.x pad.y 25 25)))
; (table.insert objects.list station)))
(lambda create-charging-station [x y]
(let [
station {: x : y :type :charging-station :active-pads {1 false 2 false 3 false}}
pads {
1 {:x (- x 25) :y y :width 25 :height 25 :light-x (+ x 5)}
3 {:x (+ x 25) :y y :width 25 :height 25 :light-x (+ x 5 6 6)}
2 {:x x :y (+ y 25) :width 25 :height 25 :light-x (+ x 5 6)}
}]
(bump-world:add {: x : y :name :charging-station :behavior :block} x y 25 25)
(add-collider-debug-box x y 25 25)
(fn station.draw [self]
(reset-color)
(love.graphics.draw objects.sprite objects.quads.charging-station self.x self.y)
(for [i 1 3 1]
(let [pad (. pads i)]
(if (. self.active-pads i)
(do (love.graphics.draw objects.sprite objects.quads.charging-pad-active pad.x pad.y)
(set-color :light-pink)
(love.graphics.rectangle "fill" pad.light-x (+ y 18) 2 2)
(reset-color))
(love.graphics.draw objects.sprite objects.quads.charging-pad pad.x pad.y))))
)
(lambda pad-hover-cb [self dt]
(tset station.active-pads self.id true)
(when (< player.battery 100) (set player.battery (+ player.battery (* dt 6)))))
(lambda station.update [self _dt]
(set self.active-pads {1 false 2 false 3 false}))
(for [i 1 3 1]
(let [pad (. pads i)]
(bump-world:add {:name :charging-pad :behavior :hover :hover-cb pad-hover-cb :id i} pad.x pad.y 25 25)
(add-collider-debug-box pad.x pad.y 25 25)))
(table.insert objects.list station)))
; (lambda load-h-door [obj]
; (debug-print obj))
(fn load-objects []
(let [(w h) (objects.sprite:getDimensions)]
(set objects.quads.charging-pad (love.graphics.newQuad 0 0 25 25 w h))
(set objects.quads.charging-pad-active (love.graphics.newQuad 50 0 25 25 w h))
(set objects.quads.charging-station (love.graphics.newQuad 25 0 25 25 w h))))
; (fn load-objects []
; (let [(w h) (objects.sprite:getDimensions)]
; (set objects.quads.charging-pad (love.graphics.newQuad 0 0 25 25 w h))
; (set objects.quads.charging-pad-active (love.graphics.newQuad 50 0 25 25 w h))
; (set objects.quads.charging-station (love.graphics.newQuad 25 0 25 25 w h))
; (set objects.quads.door (love.graphics.newQuad 25 25 25 25 w h)))
; (each [_ object (pairs (. levels :levels level-key :objects))]
; ; (debug-print object)
; (case object.type
; "h_door" (load-h-door object))
; ))
(fn load-player []
(set player.battery 100)
(set player.x (. levels :levels level-key :spawns :player-1 :x))
(set player.y (. levels :levels level-key :spawns :player-1 :y))
(bump-world:add player player.x player.y player.w player.h))
; (fn load-player []
; (set player.battery 100)
; (set player.x (. levels :levels level-key :spawns :player-1 :x))
; (set player.y (. levels :levels level-key :spawns :player-1 :y))
; (bump-world:add player player.x player.y player.w player.h))
(lambda load-level [lvl-name]
(set bump-world (bump.newWorld 25))
(set level-key :tutorial)
;; set player 1 location
(load-player)
(load-walls)
(load-objects))
; (lambda load-level [lvl-name]
; (set bump-world (bump.newWorld 25))
; (set level-key :tutorial)
; ;; set player 1 location
; (load-player)
; (load-walls)
; (load-objects))
; (fn load-assets []
; (set objects.sprite (love.graphics.newImage "assets/objects.png"))
; (set walls.sprite (love.graphics.newImage "assets/walls.png"))
; (set battery-bar-sprite (love.graphics.newImage "assets/battery_bar.png"))
; (set player-art
; (let [
; player-sprite (love.graphics.newImage "assets/player.png")
; (w h) (player-sprite:getDimensions)]
; {
; :player-sprite player-sprite
; :player1 {
; :quads {
; :n (love.graphics.newQuad 0 0 25 25 w h)
; :s (love.graphics.newQuad 25 0 25 25 w h)
; :ne (love.graphics.newQuad 50 0 25 25 w h)
; :e (love.graphics.newQuad 75 0 25 25 w h)
; :se (love.graphics.newQuad 100 0 25 25 w h)
; :sw (love.graphics.newQuad 125 0 25 25 w h)
; :w (love.graphics.newQuad 150 0 25 25 w h)
; :nw (love.graphics.newQuad 175 0 25 25 w h)
; }
; }
; }
; ))
; )
(fn load-assets []
(set objects.sprite (love.graphics.newImage "assets/objects.png"))
(set walls.sprite (love.graphics.newImage "assets/walls.png"))
(set battery-bar-sprite (love.graphics.newImage "assets/battery_bar.png"))
(set player-art
(let [
player-sprite (love.graphics.newImage "assets/player.png")
(w h) (player-sprite:getDimensions)]
{
:player-sprite player-sprite
:player1 {
:quads {
:n (love.graphics.newQuad 0 0 25 25 w h)
:s (love.graphics.newQuad 25 0 25 25 w h)
:ne (love.graphics.newQuad 50 0 25 25 w h)
:e (love.graphics.newQuad 75 0 25 25 w h)
:se (love.graphics.newQuad 100 0 25 25 w h)
:sw (love.graphics.newQuad 125 0 25 25 w h)
:w (love.graphics.newQuad 150 0 25 25 w h)
:nw (love.graphics.newQuad 175 0 25 25 w h)
}
}
}
))
)
(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 pool (tutorial.load screen))
(pool:flush)
(pool:emit :load :tutorial)
; (utils.debug-print pool)
; (utils.debug-print world)
; (load-assets)
; (load-level :tutorial)
(tset screen :canvas (love.graphics.newCanvas screen.canvas-w screen.canvas-h))
(load-assets)
(load-level :tutorial)
;; Initialize network
; (network.init)
; (network.send-msg "REGISTER")
(network.init)
(network.send-msg "REGISTER")
;; start a thread listening on stdin
(: (love.thread.newThread "require('love.event')
@ -195,137 +201,146 @@ while 1 do love.event.push('stdin', io.read('*line')) end") :start))
(print (if ok (fennel.view val) val))))
;; drawing
; (fn draw-world []
; (reset-color)
; (love.graphics.draw walls.batch)
; ;; draw collider debug boxes
; (when debug
; (set-color :black)
; (each [_ collider (pairs collider-debug-boxes)]
; (love.graphics.rectangle "line" collider.x collider.y collider.width collider.height))))
(fn draw-world []
(reset-color)
(love.graphics.draw walls.batch)
;; draw collider debug boxes
(when debug
(set-color :black)
(each [_ collider (pairs collider-debug-boxes)]
(love.graphics.rectangle "line" collider.x collider.y collider.width collider.height))))
(fn love.update [dt]
(pool:flush)
(pool:emit :update dt))
(each [_ obj (pairs objects.list)]
(obj:update dt))
; move / rotate player
(let [
d-key (love.keyboard.isDown :d)
a-key (love.keyboard.isDown :a)
e-key (love.keyboard.isDown :e)
q-key (love.keyboard.isDown :q)]
; (print (fennel.view {: d-key : a-key : e-key : q-key}))
(match {:d-key d-key :a-key a-key :e-key e-key :q-key q-key}
{:d-key true :a-key false :e-key false :q-key false} (tset player :rot (+ player.rot (* dt 2)))
{:d-key false :a-key true :e-key false :q-key false} (tset player :rot (- player.rot (* dt 2)))
{:d-key false :a-key false :e-key true :q-key false} (tset player :rot (- player.rot (* dt 2)))
{:d-key false :a-key false :e-key false :q-key true} (tset player :rot (+ player.rot (* dt 2)))
)
(when (and (> player.battery 0) (or d-key a-key e-key q-key))
(let [
dir-fn (if (or d-key a-key) #(+ $1 $2) #(- $1 $2))
new-x (dir-fn player.x (* player.speed dt (math.cos player.rot)))
new-y (dir-fn player.y (* player.speed dt (math.sin player.rot)))
col-filter-fn (lambda [item other]
(if (= other.behavior "block") :slide :cross))
(x y cols len) (bump-world:move player new-x new-y col-filter-fn)]
(tset player :x x)
(tset player :y y))
(if (> player.battery 0)
(set player.battery (- player.battery (* dt 2))))))
;; Update camera to follow player (keep player centered on screen)
(tset camera :x (- player.x (/ screen.canvas-w 2)))
(tset camera :y (- player.y (/ screen.canvas-h 2)))
; (fn love.update [dt]
; (each [_ obj (pairs objects.list)]
; (obj:update dt))
; ; move / rotate player
; (let [
; d-key (love.keyboard.isDown :d)
; a-key (love.keyboard.isDown :a)
; e-key (love.keyboard.isDown :e)
; q-key (love.keyboard.isDown :q)]
; ; (print (fennel.view {: d-key : a-key : e-key : q-key}))
; (match {:d-key d-key :a-key a-key :e-key e-key :q-key q-key}
; {:d-key true :a-key false :e-key false :q-key false} (tset player :rot (+ player.rot (* dt 2)))
; {:d-key false :a-key true :e-key false :q-key false} (tset player :rot (- player.rot (* dt 2)))
; {:d-key false :a-key false :e-key true :q-key false} (tset player :rot (- player.rot (* dt 2)))
; {:d-key false :a-key false :e-key false :q-key true} (tset player :rot (+ player.rot (* dt 2)))
; )
; (when (and (> player.battery 0) (or d-key a-key e-key q-key))
; (let [
; dir-fn (if (or d-key a-key) #(+ $1 $2) #(- $1 $2))
; new-x (dir-fn player.x (* player.speed dt (math.cos player.rot)))
; new-y (dir-fn player.y (* player.speed dt (math.sin player.rot)))
; col-filter-fn (lambda [item other]
; (if (= other.behavior "block") :slide :cross))
; (x y cols len) (bump-world:move player new-x new-y col-filter-fn)]
; (tset player :x x)
; (tset player :y y))
; (if (> player.battery 0)
; (set player.battery (- player.battery (* dt 2))))))
; ;; Update camera to follow player (keep player centered on screen)
; (tset camera :x (- player.x (/ screen.canvas-w 2)))
; (tset camera :y (- player.y (/ screen.canvas-h 2)))
(let
[(items len) (bump-world:queryRect player.x player.y 25 25 #(= $1.behavior "hover"))]
(each [_ item (pairs items)]
(item:hover-cb dt)))
; (let
; [(items len) (bump-world:queryRect player.x player.y 25 25 #(= $1.behavior "hover"))]
; (each [_ item (pairs items)]
; (item:hover-cb dt)))
; ;; Network updates
; (network.update dt))
;; Network updates
(network.update dt))
; (set net-state.net-update-timer (+ net-state.net-update-timer dt))
; (when (>= net-state.net-update-timer net-state.net-update-interval)
; (when net-state.connected
; (network.send-update player.x player.y player.rot player.battery))
; (set net-state.net-update-timer 0)))
; (fn draw-ui []
; (love.graphics.push)
; (love.graphics.translate 0 400)
; (let [font (love.graphics.newFont 10) font-small (love.graphics.newFont 6)]
; (set-color :cream)
; (love.graphics.rectangle "fill" 0 0 screen.canvas-w 100)
; (reset-color)
; (love.graphics.draw battery-bar-sprite 78 8)
; (set-color :dark-purple)
; (love.graphics.setFont font)
; (love.graphics.print (.. "Battery: " (string.format "%d" player.battery) "%") 6 7)
; (set-color :light-blue)
; (love.graphics.rectangle "fill" 80 12 (* 152 (/ player.battery 100)) 4)
; (love.graphics.setFont font-small)
; (set-color :dark-purple)
; (love.graphics.print "100%" 229 2)
; (set-color :light-pink)
; (love.graphics.print "125%" 267 2)
; (love.graphics.print "125%" 306 2)
; (love.graphics.print "150%" 345 2)
; (love.graphics.print "200%" 382 2)
; (set-color :dark-purple)
; (love.graphics.line 0 0 screen.canvas-w 0)
; )
; (love.graphics.pop)
; )
(fn draw-ui []
(love.graphics.push)
(love.graphics.translate 0 400)
(let [font (love.graphics.newFont 10) font-small (love.graphics.newFont 6)]
(set-color :cream)
(love.graphics.rectangle "fill" 0 0 screen.canvas-w 100)
(reset-color)
(love.graphics.draw battery-bar-sprite 78 8)
(set-color :dark-purple)
(love.graphics.setFont font)
(love.graphics.print (.. "Battery: " (string.format "%d" player.battery) "%") 6 7)
(set-color :light-blue)
(love.graphics.rectangle "fill" 80 12 (* 152 (/ player.battery 100)) 4)
(love.graphics.setFont font-small)
(set-color :dark-purple)
(love.graphics.print "100%" 229 2)
(set-color :light-pink)
(love.graphics.print "125%" 267 2)
(love.graphics.print "125%" 306 2)
(love.graphics.print "150%" 345 2)
(love.graphics.print "200%" 382 2)
(set-color :dark-purple)
(love.graphics.line 0 0 screen.canvas-w 0)
)
(love.graphics.pop)
)
; (lambda angle-to-direction [angle]
; "Convert angle (radians) to compass direction keyword"
; (local tau (* 2 math.pi))
; ; Normalize angle to 0-2π range
; (local normalized (% (+ angle tau) tau))
; ; Offset by 22.5° (π/8) so section boundaries align with directions
; (local offset (+ normalized (/ math.pi 8)))
; ; Find which 45° section (π/4) it falls into
; (local section (math.floor (/ offset (/ math.pi 4))))
; ; Map to compass directions
; (local directions [:e :se :s :sw :w :nw :n :ne])
; (. directions (+ (% section 8) 1)))
(lambda angle-to-direction [angle]
"Convert angle (radians) to compass direction keyword"
(local tau (* 2 math.pi))
; Normalize angle to 0-2π range
(local normalized (% (+ angle tau) tau))
; Offset by 22.5° (π/8) so section boundaries align with directions
(local offset (+ normalized (/ math.pi 8)))
; Find which 45° section (π/4) it falls into
(local section (math.floor (/ offset (/ math.pi 4))))
; Map to compass directions
(local directions [:e :se :s :sw :w :nw :n :ne])
(. directions (+ (% section 8) 1)))
; (fn draw-objects []
; (each [_ obj (pairs objects.list)]
; ; (print (fennel.view obj))
; (obj:draw)))
(fn draw-player []
"draw player sprite and hitbox"
(reset-color)
(love.graphics.draw
player-art.player-sprite
(. player-art.player1.quads (angle-to-direction player.rot))
player.x player.y)
;; draw player hitbox and direction line
(when debug
(set-color :black)
(love.graphics.rectangle "line" player.x player.y player.w player.h)
(love.graphics.push)
(let [ox (+ player.x (/ 25 2)) oy (+ player.y (/ 25 2))]
(love.graphics.translate ox oy)
(love.graphics.rotate player.rot)
(love.graphics.line 0 0 35 0))
(love.graphics.pop)
))
(fn draw-objects []
(each [_ obj (pairs objects.list)]
; (print (fennel.view obj))
(obj:draw)))
(fn love.draw []
(for [i 1 99 1]
(let [draw-trigger (.. "draw" i)]
(pool:emit draw-trigger)
(when (and (= i 98) debug) (pool:emit "draw-debug")))))
; (fn love.draw []
; (love.graphics.setCanvas screen.canvas)
; (love.graphics.clear)
; (set-color :cream)
; (love.graphics.rectangle "fill" 0 0 screen.canvas-w screen.canvas-h)
; (love.graphics.push) ; stores the default coordinate system
; (love.graphics.translate (* -1 camera.x) (* -1 camera.y))
; (draw-world)
; (draw-objects)
; (draw-player)
; (love.graphics.pop)
; (draw-ui)
; (love.graphics.setCanvas)
; (reset-color)
; (love.graphics.draw screen.canvas 0 0 0 screen.scale screen.scale))
(love.graphics.setCanvas screen.canvas)
(love.graphics.clear)
(set-color :cream)
(love.graphics.rectangle "fill" 0 0 screen.canvas-w screen.canvas-h)
(love.graphics.push) ; stores the default coordinate system
(love.graphics.translate (* -1 camera.x) (* -1 camera.y))
(draw-world)
(draw-objects)
(draw-player)
(love.graphics.pop)
(draw-ui)
(love.graphics.setCanvas)
(reset-color)
(love.graphics.draw screen.canvas 0 0 0 screen.scale screen.scale))
; (love.graphics.print "Hello from Fennel!\nPress any key to quit" 10 10))
(fn love.keypressed [key] nil )
(fn love.quit []
"Clean up before game closes"
; (network.close)
(network.close)
false)

View file

@ -0,0 +1,36 @@
(local name "travis")
(local levels [
{:image-path "assets/level_002.png" :player-start [10 19] :walls []}
])
; "20 x 20 array with the outter edge set to 1 (walls) and the inner edge set to 0 (floor)"
(local empty-level
(let [level []]
(for [y 0 19 1] (table.insert level [])) ; empty rows
(each [y row (ipairs level)]
(for [x 0 19 1] (table.insert row 0)))
level
)
)
(lambda load-walls [level]
(let [image-data (love.image.newImageData (. level :image-path))]
(each [y row (ipairs (. level :walls))]
(each [x wall (ipairs row)]
(let [(r g b) (image-data:getPixel (- x 1) (- y 1))]
; (print (.. x "," y " -> " r "," g "," b))
(tset row x (if (= (.. r g b) "111") 0 1)))))))
(fn load []
"load levels"
(each [_ level (pairs levels)]
(tset level :walls empty-level)
(load-walls level))
; (print (fennel.view levels))
)
{
:load load
:levels levels
}

View file

@ -1,9 +0,0 @@
(local assets {})
(love.graphics.setDefaultFilter "nearest" "nearest")
(set assets.objects-sprite (love.graphics.newImage "assets/objects.png"))
(set assets.walls-sprite (love.graphics.newImage "assets/walls.png"))
(set assets.battery-bar-sprite (love.graphics.newImage "assets/battery_bar.png"))
(set assets.player-sprite (love.graphics.newImage "assets/player.png"))
assets

View file

@ -1,32 +0,0 @@
(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)]
[r g b]
))
(local colors {
:cream (color 255 238 204)
:dark-purple (color 70 66 94)
:dark-pink (color 255 105 115)
:dark-blue (color 21 120 140)
:light-blue (color 0 185 190)
:light-pink (color 255 176 163)
:black [0 0 0]
:black-half-tone [0 0 0 0.25]
})
(fn reset-color []
"reset color to white (no tinting)"
(love.graphics.setColor 1 1 1))
(lambda set-color [color-name]
"set color to the given color from the color pallet"
(love.graphics.setColor (unpack (. colors color-name))))
{
: reset-color
: set-color
: colors
}

View file

@ -1,87 +0,0 @@
(local beholder (require "libs/beholder"))
(local color (require "src/colors.fnl"))
(local levels (require "levels.fnl"))
(local assets (require "src/assets.fnl"))
(lambda angle-to-direction [angle]
"Convert angle (radians) to compass direction keyword"
(local tau (* 2 math.pi))
; Normalize angle to 0-2π range
(local normalized (% (+ angle tau) tau))
; Offset by 22.5° (π/8) so section boundaries align with directions
(local offset (+ normalized (/ math.pi 8)))
; Find which 45° section (π/4) it falls into
(local section (math.floor (/ offset (/ math.pi 4))))
; Map to compass directions
(local directions [:e :se :s :sw :w :nw :n :ne])
(. directions (+ (% section 8) 1)))
(local player { :x 50 :y 50 :w 25 :h 25 :speed 80 :battery 100 :rot 0 })
(fn player.update [self dt]
(let [
d-key (love.keyboard.isDown :d)
a-key (love.keyboard.isDown :a)
e-key (love.keyboard.isDown :e)
q-key (love.keyboard.isDown :q)]
(match {:d-key d-key :a-key a-key :e-key e-key :q-key q-key}
{:d-key true :a-key false :e-key false :q-key false} (set self.rot (+ self.rot (* dt 2)))
{:d-key false :a-key true :e-key false :q-key false} (set self.rot (- self.rot (* dt 2)))
{:d-key false :a-key false :e-key true :q-key false} (set self.rot (- self.rot (* dt 2)))
{:d-key false :a-key false :e-key false :q-key true} (set self.rot (+ self.rot (* dt 2)))
))
; (when (and (> self.battery 0) (or d-key a-key e-key q-key))
; (let [
; dir-fn (if (or d-key a-key) #(+ $1 $2) #(- $1 $2))
; new-x (dir-fn player.x (* player.speed dt (math.cos player.rot)))
; new-y (dir-fn player.y (* player.speed dt (math.sin player.rot)))
; col-filter-fn (lambda [item other]
; (if (= other.behavior "block") :slide :cross))
; (x y cols len) (bump-world:move player new-x new-y col-filter-fn)]
; (tset player :x x)
; (tset player :y y))
; (if (> player.battery 0)
; (set player.battery (- player.battery (* dt 2))))))
(beholder.trigger "PLAYER.POS" self.x self.y))
(fn player.load [self level-key]
(set self.battery 100)
(set self.x (. levels :levels level-key :spawns :player-1 :x))
(set self.y (. levels :levels level-key :spawns :player-1 :y))
; (bump-world:add player player.x player.y player.w player.h))
(set self.quads
(let [
(w h) (assets.player-sprite:getDimensions)]
{
:n (love.graphics.newQuad 0 0 25 25 w h)
:s (love.graphics.newQuad 25 0 25 25 w h)
:ne (love.graphics.newQuad 50 0 25 25 w h)
:e (love.graphics.newQuad 75 0 25 25 w h)
:se (love.graphics.newQuad 100 0 25 25 w h)
:sw (love.graphics.newQuad 125 0 25 25 w h)
:w (love.graphics.newQuad 150 0 25 25 w h)
:nw (love.graphics.newQuad 175 0 25 25 w h)
}))
)
(fn player.draw50 [self]
"draw player sprite and hitbox"
(color:reset-color)
(love.graphics.draw
assets.player-sprite
(. self.quads (angle-to-direction player.rot))
self.x self.y))
(fn player.draw-debug [self]
"draw player hitbox and direction line"
(color.set-color :black)
(love.graphics.rectangle "line" self.x self.y self.w self.h)
(love.graphics.push)
(let [ox (+ self.x (/ 25 2)) oy (+ self.y (/ 25 2))]
(love.graphics.translate ox oy)
(love.graphics.rotate self.rot)
(love.graphics.line 0 0 35 0))
(love.graphics.pop))
player

View file

@ -1,59 +0,0 @@
(local color (require "src/colors.fnl"))
(local nata (require "libs/nata"))
(local utils (require "src/utils.fnl"))
(local beholder (require "libs/beholder"))
(local player (require "src/entities/player.fnl"))
(lambda gen-renderer [screen canvas]
(let [renderer {}]
(fn renderer.draw99 [self]
; (print "draw99 drawing canvas to screen")
(love.graphics.pop) ; undo camera translation
(love.graphics.setCanvas) ; reset to root canvas
(color:reset-color)
(love.graphics.draw canvas 0 0 0 screen.scale screen.scale))
(fn renderer.draw1 [self]
; use canvas
(love.graphics.setCanvas canvas)
; clear the screen
(love.graphics.clear)
(color.set-color :cream)
(love.graphics.rectangle "fill" 0 0 screen.canvas-w screen.canvas-h))
renderer))
(lambda gen-camera [screen]
(let [camera {:x 0 :y 0}]
(fn camera.draw2 [self]
(love.graphics.push)
(love.graphics.translate (* -1 self.x) (* -1 self.y)))
(fn camera.load [self]
(beholder.observe "PLAYER.POS" (lambda [x y]
;; Update camera to follow player (keep player centered on screen)
(set self.x (- x (/ screen.canvas-w 2)))
(set self.y (- y (/ screen.canvas-h 2))))))
camera
))
; (local debug true)
(fn load [screen]
(let [
canvas (love.graphics.newCanvas screen.canvas-w screen.canvas-h)
pool (nata.new {
; :groups {
; gravity = {filter = {'gravity'}},
; :post-draw {:filter [:post-draw]}
; }
; :systems [
; (gen-camera screen)
; ]
})]
(pool:queue (gen-renderer screen canvas))
(pool:queue player)
(pool:queue (gen-camera screen))
pool))
{
: load
}

View file

@ -1,6 +0,0 @@
(lambda debug-print [obj]
(print (fennel.view obj)))
{
: debug-print
}