First project-specific commit.

Hardly a game yet, but extremely basic ball movements work!
This commit is contained in:
Sage Vaillancourt 2025-03-18 00:08:44 -04:00
parent 4134634584
commit a2d796900f
27 changed files with 5539 additions and 4470 deletions

BIN
assets/images/Ball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

BIN
assets/images/Flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

BIN
assets/images/Grass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
assets/images/SandTrap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

View File

@ -5,4 +5,5 @@ require("../systems/decay")
require("../systems/draw")
require("../systems/gravity")
require("../systems/input")
require("../systems/rounds")
require("../systems/velocity")

View File

@ -1,11 +1,47 @@
-- GENERATED FILE - DO NOT EDIT
-- Instead, edit the source file directly: assets.lua2p.
-- luacheck: ignore
---@type love.Texture
Ball = love.graphics.newImage("assets/images/Ball.png")
-- luacheck: ignore
---@type love.Texture
Flag = love.graphics.newImage("assets/images/Flag.png")
-- luacheck: ignore
---@type love.Texture
GolferRightActivated = love.graphics.newImage("assets/images/GolferRightActivated.png")
-- luacheck: ignore
---@type love.Texture
GolferRight = love.graphics.newImage("assets/images/GolferRight.png")
-- luacheck: ignore
---@type love.Texture
Grass = love.graphics.newImage("assets/images/Grass.png")
-- luacheck: ignore
---@type love.Texture
SandTrap = love.graphics.newImage("assets/images/SandTrap.png")
-- luacheck: ignore
---@type love.Texture
SmallSandTrapActivated = love.graphics.newImage("assets/images/SmallSandTrapActivated.png")
-- luacheck: ignore
---@type love.Texture
SmallSandTrap = love.graphics.newImage("assets/images/SmallSandTrap.png")
-- luacheck: ignore
---@type love.Texture
StartButton = love.graphics.newImage("assets/images/StartButton.png")
-- luacheck: ignore
---@type FontData
---@type fun(fontSize: number | nil): love.Font
EtBt7001Z0xa = function(fontSize)
return love.graphics.newFont("assets/fonts/EtBt7001Z0xa.ttf", fontSize)
end

View File

@ -25,9 +25,11 @@ function generatedFileWarning()
return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p."
end)!!(generatedFileWarning())
!!(dirLookup('assets/images', 'png', 'love.graphics.newImage', 'Image'))
!!(dirLookup('assets/sounds', 'wav', 'love.sound.newSoundData', 'SoundData'))
!!(dirLookup('assets/music', 'wav', 'love.sound.newSoundData', 'SoundData'))
!!(dirLookup('assets/fonts', 'ttf', 'love.graphics.newFont', 'FontData', function(varName, newFunc, file)
!!(dirLookup('assets/images', 'png', 'love.graphics.newImage', 'love.Texture'))
!!(dirLookup('assets/sounds', 'ogg', 'love.audio.newSource', 'love.Source', function(varName, newFunc, file)
return varName .. ' = ' .. newFunc .. '("' .. file .. '", "static")'
end))
!!(dirLookup('assets/music', 'wav', 'love.sound.newSoundData', 'love.SoundData'))
!!(dirLookup('assets/fonts', 'ttf', 'love.graphics.newFont', 'fun(fontSize: number | nil): love.Font', function(varName, newFunc, file)
return varName .. ' = function(fontSize)\n return ' .. newFunc .. '("' .. file .. '", fontSize)\nend'
end))

File diff suppressed because it is too large Load Diff

View File

@ -175,14 +175,14 @@ Handler messages:
]]
--==============================================================
local startTime = os.time()
local startClock = os.clock()
local args = arg
if not args[0] then error("Expected to run from the Lua interpreter.") end
if not args[0] then
error("Expected to run from the Lua interpreter.")
end
local pp = dofile((args[0]:gsub("[^/\\]+$", "preprocess.lua")))
-- From args:
@ -217,7 +217,7 @@ local function formatBytes(n)
elseif n >= 1024 * 1024 then
return F("%.2f MiB", n / (1024 * 1024))
elseif n >= 1024 then
return F("%.2f KiB", n/(1024))
return F("%.2f KiB", n / 1024)
elseif n == 1 then
return F("1 byte", n)
else
@ -246,9 +246,13 @@ local loadLuaFile = (
end
or function(path, env)
local chunk, err = loadfile(path)
if not chunk then return nil, err end
if not chunk then
return nil, err
end
if env then setfenv(chunk, env) end
if env then
setfenv(chunk, env)
end
return chunk
end
@ -270,59 +274,46 @@ local pathsIn = {}
local pathsOut = {}
for _, arg in ipairs(args) do
if processOptions and (arg:find"^%-%-?help$" or arg == "/?" or arg:find"^/[Hh][Ee][Ll][Pp]$") then
if processOptions and (arg:find("^%-%-?help$") or arg == "/?" or arg:find("^/[Hh][Ee][Ll][Pp]$")) then
print("LuaPreprocess v" .. pp.VERSION)
print((help:gsub("\t", " ")))
os.exit()
elseif not (processOptions and arg:find"^%-.") then
elseif not (processOptions and arg:find("^%-.")) then
local paths = (hasOutputPaths and #pathsOut < #pathsIn) and pathsOut or pathsIn
table.insert(paths, arg)
if arg == "-" and (not hasOutputPaths or paths == pathsOut) then
silent = true
end
elseif arg == "--" then
processOptions = false
elseif arg:find"^%-%-data=" or arg:find"^%-d=" then
elseif arg:find("^%-%-data=") or arg:find("^%-d=") then
customData = arg:gsub("^.-=", "")
elseif arg == "--backtickstrings" then
allowBacktickStrings = true
elseif arg == "--debug" then
isDebug = true
outputMeta = outputMeta or true
elseif arg:find"^%-%-handler=" or arg:find"^%-h=" then
elseif arg:find("^%-%-handler=") or arg:find("^%-h=") then
messageHandlerPath = arg:gsub("^.-=", "")
elseif arg == "--jitsyntax" then
allowJitSyntax = true
elseif arg == "--linenumbers" then
addLineNumbers = true
elseif arg == "--meta" then
outputMeta = true
elseif arg:find"^%-%-meta=" then
elseif arg:find("^%-%-meta=") then
outputMeta = arg:gsub("^.-=", "")
elseif arg == "--nonil" then
canOutputNil = false
elseif arg == "--novalidate" then
validate = false
elseif arg:find"^%-%-outputextension=" then
elseif arg:find("^%-%-outputextension=") then
if hasOutputPaths then
errorLine("Cannot specify both --outputextension and --outputpaths")
end
hasOutputExtension = true
outputExtension = arg:gsub("^.-=", "")
elseif arg == "--outputpaths" or arg == "-o" then
if hasOutputExtension then
errorLine("Cannot specify both --outputpaths and --outputextension")
@ -330,38 +321,27 @@ for _, arg in ipairs(args) do
errorLine(arg .. " must appear before any input path.")
end
hasOutputPaths = true
elseif arg:find"^%-%-saveinfo=" or arg:find"^%-i=" then
elseif arg:find("^%-%-saveinfo=") or arg:find("^%-i=") then
processingInfoPath = arg:gsub("^.-=", "")
elseif arg == "--silent" then
silent = true
elseif arg == "--faststrings" then
fastStrings = true
elseif arg == "--nogc" then
collectgarbage("stop")
elseif arg:find"^%-%-macroprefix=" then
elseif arg:find("^%-%-macroprefix=") then
macroPrefix = arg:gsub("^.-=", "")
elseif arg:find"^%-%-macrosuffix=" then
elseif arg:find("^%-%-macrosuffix=") then
macroSuffix = arg:gsub("^.-=", "")
elseif arg == "--release" then
releaseMode = true
elseif arg:find"^%-%-loglevel=" then
elseif arg:find("^%-%-loglevel=") then
maxLogLevel = arg:gsub("^.-=", "")
elseif arg == "--version" then
io.stdout:write(pp.VERSION)
os.exit()
elseif arg == "--nostrictmacroarguments" then
strictMacroArguments = false
else
errorLine("Unknown option '" .. arg:gsub("=.*", "") .. "'.")
end
@ -380,26 +360,19 @@ if hasOutputPaths and #pathsOut < #pathsIn then
errorLine("Missing output path for " .. pathsIn[#pathsIn])
end
-- Prepare metaEnvironment.
pp.metaEnvironment.dataFromCommandLine = customData -- May be nil.
-- Load message handler.
local messageHandler = nil
local function hasMessageHandler(message)
if not messageHandler then
return false
elseif type(messageHandler) == "function" then
return true
elseif type(messageHandler) == "table" then
return messageHandler[message] ~= nil
else
assert(false)
end
@ -408,18 +381,17 @@ end
local function sendMessage(message, ...)
if not messageHandler then
return
elseif type(messageHandler) == "function" then
local returnValues = pp.pack(messageHandler(message, ...))
return pp.unpack(returnValues, 1, returnValues.n)
elseif type(messageHandler) == "table" then
local _messageHandler = messageHandler[message]
if not _messageHandler then return end
if not _messageHandler then
return
end
local returnValues = pp.pack(_messageHandler(...))
return pp.unpack(returnValues, 1, returnValues.n)
else
assert(false)
end
@ -450,8 +422,6 @@ if messageHandlerPath ~= "" then
end
end
-- Init stuff.
sendMessage("init", pathsIn, (hasOutputPaths and pathsOut or nil)) -- @Incomplete: Use pcall and format error message better?
@ -471,17 +441,23 @@ local pathsSetIn = {}
local pathsSetOut = {}
for i = 1, #pathsIn do
if pathsSetIn [pathsIn [i]] then errorLine("Duplicate input path: " ..pathsIn [i]) end
if pathsSetOut[pathsOut[i]] then errorLine("Duplicate output path: "..pathsOut[i]) end
if pathsSetIn[pathsIn[i]] then
errorLine("Duplicate input path: " .. pathsIn[i])
end
if pathsSetOut[pathsOut[i]] then
errorLine("Duplicate output path: " .. pathsOut[i])
end
pathsSetIn[pathsIn[i]] = true
pathsSetOut[pathsOut[i]] = true
if pathsIn [i] ~= "-" and pathsSetOut[pathsIn [i]] then errorLine("Path is both input and output: "..pathsIn [i]) end
if pathsOut[i] ~= "-" and pathsSetIn [pathsOut[i]] then errorLine("Path is both input and output: "..pathsOut[i]) end
if pathsIn[i] ~= "-" and pathsSetOut[pathsIn[i]] then
errorLine("Path is both input and output: " .. pathsIn[i])
end
if pathsOut[i] ~= "-" and pathsSetIn[pathsOut[i]] then
errorLine("Path is both input and output: " .. pathsOut[i])
end
end
-- Process files.
@ -507,7 +483,7 @@ for i, pathIn in ipairs(pathsIn) do
pathMeta = nil
end
local info, err = pp.processFile{
local info, err = pp.processFile({
pathIn = pathIn,
pathMeta = pathMeta,
pathOut = pathOut,
@ -549,17 +525,20 @@ for i, pathIn in ipairs(pathsIn) do
sendMessage("beforemeta", pathIn, lua)
end,
onAfterMeta = messageHandler and function(lua)
onAfterMeta = messageHandler
and function(lua)
local luaModified = sendMessage("aftermeta", pathIn, lua)
if type(luaModified) == "string" then
lua = luaModified
elseif luaModified ~= nil then
error(F(
error(
F(
"%s: Message handler did not return a string for 'aftermeta'. (Got %s)",
messageHandlerPath, type(luaModified)
))
messageHandlerPath,
type(luaModified)
)
)
end
return lua
@ -577,7 +556,7 @@ for i, pathIn in ipairs(pathsIn) do
end)
os.exit(1)
end,
}
})
assert(info, err) -- The onError() handler above should have been called and we should have exited already.
byteCount = byteCount + info.processedByteCount
@ -586,18 +565,14 @@ for i, pathIn in ipairs(pathsIn) do
tokenCount = tokenCount + info.tokenCount
if processingInfoPath ~= "" then
-- :SavedInfo
table.insert(processingInfo.files, info) -- See 'ProcessInfo' in preprocess.lua for what more 'info' contains.
end
printfNoise("Processing '%s' successful! (%.3fs)", pathIn, os.clock() - startClockForPath)
printfNoise(("-"):rep(#header))
end
-- Finalize stuff.
if processingInfoPath ~= "" then
printfNoise("Saving processing info to '%s'.", processingInfoPath)
@ -614,17 +589,18 @@ end
printfNoise(
"All done! (%.3fs, %.0f file%s, %.0f LOC, %.0f line%s, %.0f token%s, %s)",
os.clock() - startClock,
#pathsIn, (#pathsIn == 1) and "" or "s",
#pathsIn,
(#pathsIn == 1) and "" or "s",
lineCountCode,
lineCount, (lineCount == 1) and "" or "s",
tokenCount, (tokenCount == 1) and "" or "s",
lineCount,
(lineCount == 1) and "" or "s",
tokenCount,
(tokenCount == 1) and "" or "s",
formatBytes(byteCount)
)
sendMessage("alldone") -- @Incomplete: Use pcall and format error message better?
--[[!===========================================================
Copyright © 2018-2022 Marcus 'ReFreezed' Thunström
@ -648,4 +624,3 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
==============================================================]]

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---@field indices table<any, any> field is a table of Entity keys to their indices in the entities list. Most Systems can ignore this.
---@field modified boolean indicator for 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.
--- @module tiny-ecs
-- @author Calvin Rose
-- @license MIT
@ -103,14 +102,13 @@ local filterJoin
-- A helper function to filters from string
local filterBuildString
local function filterJoinRaw(invert, joining_op, ...)
local _args = { ... }
return function(system, e)
local acc
local args = _args
if joining_op == 'or' then
if joining_op == "or" then
acc = false
for i = 1, #args do
local v = args[i]
@ -119,7 +117,7 @@ local function filterJoinRaw(invert, joining_op, ...)
elseif type(v) == "function" then
acc = acc or v(system, e)
else
error 'Filter token must be a string or a filter function.'
error("Filter token must be a string or a filter function.")
end
end
else
@ -131,7 +129,7 @@ local function filterJoinRaw(invert, joining_op, ...)
elseif type(v) == "function" then
acc = acc and v(system, e)
else
error 'Filter token must be a string or a filter function.'
error("Filter token must be a string or a filter function.")
end
end
end
@ -146,69 +144,68 @@ local function filterJoinRaw(invert, joining_op, ...)
end
do
function filterJoin(...)
local state, value = pcall(filterJoinRaw, ...)
if state then return value else return nil, value end
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)
str = str:gsub("%b()", function(p)
subParts[#subParts + 1] = buildPart(p:sub(2, -2))
return ('\255%d'):format(#subParts)
return ("\255%d"):format(#subParts)
end)
for invert, part, sep in str:gmatch('(%!?)([^%|%&%!]+)([%|%&]?)') do
if part:match('^\255%d+$') then
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])
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 '==')
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 ')
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 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(false, 'and', ...)
return filterJoin(false, "and", ...)
end
--- Makes a Filter that selects Entities with at least one of the specified
-- Components and Filters.
function tiny.requireAny(...)
return filterJoin(false, 'or', ...)
return filterJoin(false, "or", ...)
end
--- Makes a Filter that rejects Entities with all specified Components and
-- Filters, and selects all other Entities.
function tiny.rejectAll(...)
return filterJoin(true, 'and', ...)
return filterJoin(true, "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(true, 'or', ...)
return filterJoin(true, "or", ...)
end
--- Makes a Filter from a string. Syntax of `pattern` is as follows.
@ -224,7 +221,11 @@ end
-- @param pattern
function tiny.filter(pattern)
local state, value = pcall(filterBuildString, pattern)
if state then return value else return nil, value end
if state then
return value
else
return nil, value
end
end
--- System functions.
@ -463,8 +464,7 @@ function tiny.world(...)
entities = {},
-- List of Systems
systems = {}
systems = {},
}, worldMetaTable)
tiny_add(ret, ...)
@ -673,7 +673,6 @@ end
-- Adds, removes, and changes Entities that have been marked.
function tiny_manageEntities(world)
local e2r = world.entitiesToRemove
local e2c = world.entitiesToChange
@ -793,7 +792,6 @@ end
-- 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)
@ -809,8 +807,7 @@ function tiny.update(world, dt, filter)
onModify(system, dt)
end
local preWrap = system.preWrap
if preWrap and
((not filter) or filter(world, system)) then
if preWrap and ((not filter) or filter(world, system)) then
preWrap(system, dt)
end
end
@ -853,12 +850,10 @@ function tiny.update(world, dt, filter)
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
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.
@ -924,11 +919,11 @@ worldMetaTable = {
clearSystems = tiny.clearSystems,
getEntityCount = tiny.getEntityCount,
getSystemCount = tiny.getSystemCount,
setSystemIndex = tiny.setSystemIndex
setSystemIndex = tiny.setSystemIndex,
},
__tostring = function()
return "<tiny-ecs_World>"
end
end,
}
_G.tiny = tiny

190
main.lua
View File

@ -9,25 +9,177 @@ require("generated/filter-types")
require("generated/assets")
require("generated/all-systems")
local scenarios = {
default = function()
-- TODO: Add default entities
end,
textTestScenario = function()
local width, height = love.graphics.getWidth(), love.graphics.getHeight()
local squareSize = 80
local tileSize = math.floor(squareSize * 1.2)
local marginSize = 10
CursorMask = 1
BallMask = 2
local function emptyTile(unhighlightSiblings, gridPosition)
local size = { x = squareSize, y = squareSize }
return {
unhighlightSiblings = unhighlightSiblings,
canBeCollidedBy = CursorMask,
highlightOnMouseOver = T.marker,
size = size,
gridPosition = gridPosition,
drawAsRectangle = { size = size },
}
end
local function replaceAt(grid, y, x, entity)
local current = grid[y][x]
grid[y][x] = entity
current.unhighlightSiblings[entity] = true
current.unhighlightSiblings[current] = nil
entity.unhighlightSiblings = current.unhighlightSiblings
-- entity.position = current.position
entity.gridPosition = current.gridPosition
entity.toLeft = current.toLeft
entity.toRight = current.toRight
entity.above = current.above
entity.below = current.below
current.toLeft.toRight = current.toRight
current.toRight.toLeft = current.toLeft
current.above.below = current.below
current.below.above = current.above
World:removeEntity(current)
World:addEntity(entity)
end
function PositionAtGridXy(x, y)
return {
x = 20 + ((x - 1) * tileSize),
y = 60 + ((y - 1) * tileSize),
}
end
Scenarios = {
---@return table xyGrid Where grid[y][x] is an Entity
emptyGrid = function()
local cursorSize = { x = 5, y = 5 }
World:addEntity({
position = { x = 0, y = 600 },
drawAsText = {
text = "Hello, world!",
style = TextStyle.Inverted,
size = cursorSize,
moveWithCursor = T.marker,
canCollideWith = bit.bor(CursorMask, BallMask),
position = { x = -999, y = -999 },
highlightsCollided = T.marker,
drawAsRectangle = {
size = cursorSize,
mode = "fill",
},
})
World:addEntity({
position = { x = 0, y = 0 },
drawAsSprite = Grass,
z = 0,
})
-- Temporary storages for connecting grid items
local previous
local unhighlightSiblings = {}
local yxGrid = {}
local xCount = 8
local yCount = 5
for y = 1, yCount do
yxGrid[y] = {}
for x = 1, xCount do
local current = World:addEntity(emptyTile(unhighlightSiblings, { x = x, y = y }))
yxGrid[y][x] = current
unhighlightSiblings[current] = true
current.toLeft = previous
if previous ~= nil then
if previous.toRight == nil then
previous.toRight = current
end
else
current.canReceiveButtons = T.marker
current.highlighted = T.marker
end
if yxGrid[y - 1] and yxGrid[y - 1][x] then
current.above = yxGrid[y - 1][x]
yxGrid[y - 1][x].below = current
end
if y == yCount then
-- Connect last row to first row
yxGrid[1][x].above = current
current.below = yxGrid[1][x]
end
if x == xCount then
-- Connect last entry to first Entry
current.toRight = yxGrid[y][1]
yxGrid[y][1].toLeft = current
end
previous = current
end
end
return yxGrid
end,
firstLevel = function()
local yxGrid = Scenarios.emptyGrid()
replaceAt(yxGrid, 4, 7, {
drawAsSprite = Flag,
z = 1,
effectsToApply = {
endOfRound = T.marker,
}
})
replaceAt(yxGrid, 2, 2, {
drawAsSprite = SmallSandTrap,
z = 1,
spriteAfterEffect = SmallSandTrapActivated,
effectsToApply = {
slowsBy = { x = 1, y = 1 },
movement = { x = 0, y = 0, },
},
})
replaceAt(yxGrid, 1, 1, {
pickedUpOnClick = T.marker,
drawAsSprite = GolferRight,
z = 1,
spriteAfterEffect = GolferRightActivated,
effectsToApply = {
movement = { x = 6, y = 0, },
},
})
replaceAt(yxGrid, 1, 7, {
pickedUpOnClick = T.marker,
drawAsSprite = GolferRight,
z = 1,
spriteAfterEffect = GolferRightActivated,
effectsToApply = {
movement = { x = 0, y = 3 },
},
})
World:addEntity({
ballEffects = {},
drawAsSprite = Ball,
gridPosition = { x = 1, y = 1 },
z = 2,
})
-- TODO: Make the start button start
World:addEntity({
drawAsSprite = StartButton,
z = 3,
position = {
x = width - 120,
y = height - 50,
},
velocity = { x = 240, y = -500 },
mass = 1,
decayAfterSeconds = 10,
})
end,
}
scenarios.textTestScenario()
Scenarios.firstLevel()
function love.load()
love.graphics.setBackgroundColor(1, 1, 1)
@ -35,5 +187,15 @@ function love.load()
end
function love.draw()
World:update(love.timer.getDelta())
local dt = love.timer.getDelta()
if love.keyboard.isDown("r") then
World:clearEntities()
Scenarios.firstLevel()
end
World:update(dt, function(_, system)
return not system.isDrawSystem
end)
World:update(dt, function(_, system)
return system.isDrawSystem
end)
end

View File

@ -1,11 +1,24 @@
collidingEntities = filteredSystem("collidingEntitites", {
velocity = T.XyPair,
position = T.XyPair,
size = T.XyPair,
canCollideWith = T.BitMask,
isSolid = Maybe(T.bool),
})
local function intersects(rect, rectOther)
local left = rect.position.x
local right = rect.position.x + rect.size.x
local top = rect.position.y
local bottom = rect.position.y + rect.size.y
local leftOther = rectOther.position.x
local rightOther = rectOther.position.x + rectOther.size.x
local topOther = rectOther.position.y
local bottomOther = rectOther.position.y + rectOther.size.y
return leftOther < right and left < rightOther and topOther < bottom and top < bottomOther
end
filteredSystem(
"collisionDetection",
{ position = T.XyPair, size = T.XyPair, canBeCollidedBy = T.BitMask, isSolid = Maybe(T.bool) },
@ -18,19 +31,7 @@ filteredSystem(
and e.canBeCollidedBy
and bit.band(collider.canCollideWith, e.canBeCollidedBy) ~= 0
then
local colliderTop = collider.position.y
local colliderBottom = collider.position.y + collider.size.y
local entityTop = e.position.y
local entityBottom = entityTop + e.size.y
local withinY = (entityTop > colliderTop and entityTop < colliderBottom)
or (entityBottom > colliderTop and entityBottom < colliderBottom)
if
withinY
and collider.position.x < e.position.x + e.size.x
and collider.position.x + collider.size.x > e.position.x
then
if intersects(e, collider) then
system.world:addEntity({ collisionBetween = { e, collider } })
end
end

View File

@ -1,4 +1,23 @@
filteredSystem("collisionResolution", { collisionBetween = T.Collision }, function(e, _, system)
Collisions = filteredSystem("collisionResolution", { collisionBetween = T.Collision }, function(e, _, system)
local collidedInto, collider = e.collisionBetween[1], e.collisionBetween[2]
if collider.highlightsCollided then
if collidedInto.unhighlightSiblings then
for sibling in pairs(collidedInto.unhighlightSiblings) do
sibling.highlighted = nil
sibling.canReceiveButtons = nil
system.world:addEntity(sibling)
end
end
if collidedInto.highlightOnMouseOver ~= nil then
collidedInto.highlighted = T.marker
collidedInto.canReceiveButtons = T.marker
system.world:addEntity(collidedInto)
end
if collidedInto.pickedUpOnClick ~= nil then -- and #HeldByCursor.entities == 0 and love.mouse.isDown(1) then
print("Click!")
collidedInto.moveWithCursor = T.marker
system.world:addEntity(collidedInto)
end
end
system.world:removeEntity(e)
end)

View File

@ -1,23 +1,52 @@
local floor = math.floor
local gfx = love.graphics
---
---@generic T
---@param shape T | fun()
---@param process fun(entity: T, dt: number, system: System) | nil
---@return System | { entities: T[] }
local function drawSystem(name, shape, process)
local system = filteredSystem(name, shape, process, function(_, a, b)
if a.z ~= nil and b.z ~= nil then
return a.z < b.z
end
if a.z ~= nil then
return true
end
return false
end)
system.isDrawSystem = true
return system
end
filteredSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _)
gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y)
filteredSystem("mapGridPositionToRealPosition", { gridPosition = T.XyPair }, function(e, _, system)
e.position = PositionAtGridXy(e.gridPosition.x, e.gridPosition.y)
system.world:addEntity(e)
end)
filteredSystem("drawSprites", { position = T.XyPair, drawAsSprite = T.pd_image }, function(e)
if e.position.y < Camera.pan.y - 240 or e.position.y > Camera.pan.y + 480 then
local spriteDrawSystem = drawSystem(
"drawSprites",
{ position = T.XyPair, drawAsSprite = T.Drawable, rotation = Maybe(T.number) },
function(e)
if not e.drawAsSprite then
return
end
e.drawAsSprite:draw(e.position.x, e.position.y)
end)
local width, height = e.drawAsSprite:getDimensions()
gfx.draw(e.drawAsSprite, e.position.x, e.position.y)
end
)
function spriteDrawSystem:preProcess()
gfx.setColor(1, 1, 1)
end
local margin = 8
filteredSystem(
drawSystem(
"drawText",
{ position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.pd_font) } },
function(e)
local font = gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold
local font = e.font or gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold
local textHeight = font:getHeight()
local textWidth = font:getWidth(e.drawAsText.text)
@ -40,3 +69,9 @@ filteredSystem(
gfx.print(e.drawAsText.text, bgLeftEdge + margin, bgTopEdge + margin)
end
)
drawSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _)
gfx.setColor(1, 1, 1, 0.5)
local mode = e.drawAsRectangle.mode or (e.highlighted and "fill" or "line")
gfx.rectangle(mode, e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y)
end)

View File

@ -1,3 +1,5 @@
local isDown = love.keyboard.isDown
---@type ButtonState
local buttonState = {}
@ -6,8 +8,105 @@ buttonInputSystem = filteredSystem("buttonInput", { canReceiveButtons = T.marker
system.world:addEntity(e)
end)
HeldByCursor = filteredSystem("HeldByCursor", { pickedUpOnClick = T.marker, moveWithCursor = T.marker })
function buttonInputSystem:preProcess()
if #self.entities == 0 then
return
end
end
function love.keypressed(key, _, _)
buttonState[key] = true
end
function ClearButtonState()
for key in pairs(buttonState) do
buttonState[key] = nil
end
end
---@type System
local cursorTracking
local mouseX, mouseY = -9999, -9999
local mouseInControl = false
function love.mousemoved(x, y)
mouseInControl = true
mouseX, mouseY = x, y
end
local keyDebounceSec = 0.1
local delay = 0
local menuSystem = filteredSystem(
"menu",
{ canReceiveButtons = T.marker, highlighted = T.marker },
function(e, dt, system)
if delay > 0 then
delay = delay - dt
return
end
local function tryShiftMenu(target, keys)
if target == nil then
return false
end
for _, key in ipairs(keys) do
if isDown(key) then
e.highlighted = nil
e.canReceiveButtons = nil
target.canReceiveButtons = T.marker
target.highlighted = T.marker
system.world:addEntity(e)
system.world:addEntity(target)
e = target
return true
end
end
return false
end
local pressed = tryShiftMenu(e.toRight, { "right", "d" })
pressed = tryShiftMenu(e.toLeft, { "left", "a" }) or pressed
pressed = tryShiftMenu(e.below, { "down", "s" }) or pressed
pressed = tryShiftMenu(e.above, { "up", "w" }) or pressed
if isDown("return") then
pressed = true
system.world:addEntity({
round = "start",
})
end
if pressed then
mouseInControl = false
mouseX, mouseY = -9999, -9999
for _, tracker in pairs(cursorTracking.entities) do
tracker.position.x = -9999
tracker.position.y = -9999
end
delay = keyDebounceSec
end
end
)
cursorTracking = filteredSystem("cursorTracking", { moveWithCursor = T.marker, position = T.XyPair }, function(e)
if mouseInControl then
e.position.x = mouseX
e.position.y = mouseY
end
end)
-- local allHighlighted = filteredSystem("allHighlighted", { highlighted = T.marker })
function cursorTracking:postProcess()
-- if mouseInControl and #Collisions.entities == 0 then
-- -- If the cursor is not colliding with anything, wipe all highlighted components
-- for _, highlighted in pairs(allHighlighted.entities) do
-- highlighted.highlighted = nil
-- self.world:addEntity(highlighted)
-- end
-- end
end

85
systems/rounds.lua Normal file
View File

@ -0,0 +1,85 @@
local gridElements = filteredSystem("gridElements", { gridPosition = T.XyPair, effectsToApply = T.AnyComponent })
local roundRunning = false
filteredSystem("timers", { timerSec = T.number, callback = T.SelfFunction }, function(e, dt, system)
e.timerSec = e.timerSec - dt
if e.timerSec < 0 then
e:callback()
system.world:removeEntity(e)
end
end)
local function sign(n)
return n > 0 and 1 or n < 0 and -1 or 0
end
local roundSystem = filteredSystem("rounds", { round = T.str })
local activeBallEffects = filteredSystem("activeBallEffects", { ballEffects = T.AnyComponent, gridPosition = T.XyPair }, function(e, dt, system)
local roundActive = false
for _, state in pairs(roundSystem.entities) do
if state.round == "start" then
roundActive = true
end
end
if not roundActive then
return
end
local gridPosition, effects = e.gridPosition, e.ballEffects
-- Search for new effects from the current tile
for _, gridElement in pairs(gridElements.entities) do
if
gridPosition.x == gridElement.gridPosition.x
and gridPosition.y == gridElement.gridPosition.y
then
-- More direct-mutation-y than I'd like,
-- but offers a simple way to overwrite existing effects.
-- We're "setting InRelations" :D
for key, value in pairs(gridElement.effectsToApply) do
effects[key] = value
end
if gridElement.spriteAfterEffect then
gridElement.drawAsSprite = gridElement.spriteAfterEffect
gridElement.spriteAfterEffect = nil
end
gridElement.effectsToApply = nil
system.world:addEntity(gridElement)
end
end
-- Apply any effects currently connected to this ball
if effects.movement ~= nil then
gridPosition.x = gridPosition.x + sign(effects.movement.x)
gridPosition.y = gridPosition.y + sign(effects.movement.y)
effects.movement.x = effects.movement.x - sign(effects.movement.x)
effects.movement.y = effects.movement.y - sign(effects.movement.y)
if effects.movement.x == 0 and effects.movement.y == 0 then
effects.movement = nil
end
end
-- TODO: Trigger the round end
-- for _, _ in pairs(effects) do
-- -- Return if there are any effects left
-- return
-- end
-- system.world:addEntity({ round = "end" })
end)
local stepTimeSec = 0.3
local stepTimer = stepTimeSec
function activeBallEffects:preProcess(dt)
stepTimer = stepTimer - dt
if stepTimer <= 0 then
stepTimer = stepTimeSec
return
end
return tiny.SKIP_PROCESS
end

View File

@ -1,13 +1,15 @@
---@generic T
---@param shape T | fun()
---@param process fun(entity: T, dt: number, system: System) | nil
---@param compare nil | fun(system: System, entityA: T, entityB: T): boolean
---@return System | { entities: T[] }
function filteredSystem(name, shape, process)
function filteredSystem(name, shape, process, compare)
assert(type(name) == "string")
assert(type(shape) == "table" or type(shape) == "function")
assert(process == nil or type(process) == "function")
local system = tiny.processingSystem()
local system = compare and tiny.sortedProcessingSystem() or tiny.processingSystem()
system.compare = compare
system.name = name
if type(shape) == "table" then
local keys = {}