diff --git a/src/data/audio/switch-5.ogg b/src/data/audio/switch-5.ogg new file mode 100644 index 0000000..799e327 Binary files /dev/null and b/src/data/audio/switch-5.ogg differ diff --git a/src/data/audio/switch-7.ogg b/src/data/audio/switch-7.ogg new file mode 100644 index 0000000..856a60d Binary files /dev/null and b/src/data/audio/switch-7.ogg differ diff --git a/src/data/fonts/kenney-future.ttf b/src/data/fonts/kenney-future.ttf new file mode 100644 index 0000000..1dbb2dd Binary files /dev/null and b/src/data/fonts/kenney-future.ttf differ diff --git a/src/data/init.lua b/src/data/init.lua index 0ce676d..a2a8941 100644 --- a/src/data/init.lua +++ b/src/data/init.lua @@ -1,6 +1,10 @@ local cargo = require("lib.cargo") local aseLoader = require("lib.ase-loader") local pprint = require("lib.pprint") +local slicy = require("lib.slicy") +local ripple = require("lib.ripple") + +love.graphics.setDefaultFilter("nearest", "nearest") -- TODO: Maybe add a texture atlas library for packing frame data -- TODO: For production maybe use another type of loader? (https://github.com/elloramir/packer, https://github.com/EngineerSmith/Runtime-TextureAtlas) @@ -84,5 +88,11 @@ return cargo.init{ loaders = { aseprite = loadAsepriteSprite, ase = loadAsepriteSprite, + ["9.png"] = function(filename) + return slicy.load(filename) + end, + ogg = function(filename) + return ripple.newSound(love.audio.newSource(filename, 'static')) + end } } diff --git a/src/data/ui/panels/blue-pressed.9.png b/src/data/ui/panels/blue-pressed.9.png new file mode 100644 index 0000000..6ea7327 Binary files /dev/null and b/src/data/ui/panels/blue-pressed.9.png differ diff --git a/src/data/ui/panels/blue.9.png b/src/data/ui/panels/blue.9.png new file mode 100644 index 0000000..d199a46 Binary files /dev/null and b/src/data/ui/panels/blue.9.png differ diff --git a/src/groups.lua b/src/groups.lua new file mode 100644 index 0000000..6069bb5 --- /dev/null +++ b/src/groups.lua @@ -0,0 +1,13 @@ +return { + physical = {filter = {"pos", "vel"}}, + player = {filter = { + "pos", "acc", "speed", "bolts" + -- "bolt_count", + -- "bolt_cooldown", "bolt_speed", "bolt_friction" + }}, + controllable_player = {filter = {"controllable"}}, + sprite = {filter = {"pos", "sprite"}}, + bolt = {filter={"pos", "vel", "bolt"}}, + collider = {filter={"collider"}}, + ui_button = {filter={"pos", "size", "text"}} +} diff --git a/src/helpers/rgb.lua b/src/helpers/rgb.lua index ec9f6fc..d83b471 100644 --- a/src/helpers/rgb.lua +++ b/src/helpers/rgb.lua @@ -1,3 +1,3 @@ return function(r, g, b) - return {r/255, g/255, b/255} + return {r/255, g/255, b/255, 1} end diff --git a/src/lib/nata.lua b/src/lib/nata.lua index 17993ae..e16a713 100644 --- a/src/lib/nata.lua +++ b/src/lib/nata.lua @@ -259,6 +259,25 @@ function Pool:_init(options, ...) self.data = options.data or {} local groups = options.groups or {} local systems = options.systems or {nata.oop()} + + if options.available_groups then + for _, system in ipairs(systems) do + if type(system.required_groups) == "table" then + for _, group_name in ipairs(system.required_groups) do + local group = options.available_groups[group_name] + if group and not self.groups[group_name] then + self.groups[group_name] = { + filter = group.filter, + sort = group.sort, + entities = {}, + hasEntity = {}, + } + end + end + end + end + end + for groupName, groupOptions in pairs(groups) do self.groups[groupName] = { filter = groupOptions.filter, diff --git a/src/lib/slicy.lua b/src/lib/slicy.lua new file mode 100644 index 0000000..30f1bb1 --- /dev/null +++ b/src/lib/slicy.lua @@ -0,0 +1,497 @@ +local M = {} + +--[[ + file format is as follows: + * extension: *.9.png, same as patchy and (afaik) behaves the same + * original author never documented it anywhere + * actual image has a 1 pixel border on all sides + * pixels in the border can either be black with opacity > 0 ("set") or not + * set pixels serve as metadata for how to slice the image into 9 patches + * first and last set pixels on the top and left define the interval for the "edge" portions of the image + * image "edge" will be scaled in 1 dimension to accomodate variable size + * first and last set pixels on the bottom and right define the "content window" + * content window defines inner padding so that content doesn't touch the borders + * can be different from the "edge" definitions + * getContentRegion() returns the content region bounds for a given size +]] + +---@class PatchedImage +---@field patches table +---@field contentPadding table +---@field x number +---@field y number +---@field width number +---@field height number +local PatchedImage = {} + +local PatchedImageMt = {__index = PatchedImage} + +local debugDraw = false +local debugLog = false +local DEBUG_DRAW_SEP_WIDTH = 1 + +local function dbg(...) + if debugLog then + print(...) + end +end + +local function firstBlackPixel(imgData, axis, axisoffset, reversed) + local lim, getpixel + if axis == "row" then + lim = imgData:getWidth() - 1 + getpixel = function(idx) + return imgData:getPixel(idx, axisoffset) + end + elseif axis == "col" then + lim = imgData:getHeight() - 1 + getpixel = function(idx) + return imgData:getPixel(axisoffset, idx) + end + else + return nil, "argument 2: expected either 'row' or 'col', got " .. tostring(axis) + end + dbg("looking for black pixel in axis", axis, axisoffset) + + local startidx, endidx, step + if reversed then + -- start at last valid position, down to 0 + startidx, endidx, step = lim, 0, -1 + else + -- go upwards instead + startidx, endidx, step = 0, lim, 1 + end + + for idx = startidx, endidx, step do + local r, g, b, a = getpixel(idx) + -- black non-transparent pixel + if r + g + b == 0 and a > 0 then + dbg("black pixel found at idx", idx) + return idx + end + end + return nil, "no black pixel found" +end + +local function setCorners(p, rawdata, horizontalEdgeSegment, verticalEdgeSegment) + dbg("slicing corners") + + -- -1 from file format border, -1 from excluding the pixel itself + local brCornerWidth = rawdata:getWidth() - horizontalEdgeSegment[2] - 2 + local brCornerHeight = rawdata:getHeight() - verticalEdgeSegment[2] - 2 + + local tlCorner = love.image.newImageData(horizontalEdgeSegment[1] - 1, verticalEdgeSegment[1] - 1) + tlCorner:paste( + rawdata, + 0, 0, + -- skip metadata column and row + 1, 1, + tlCorner:getDimensions() + ) + p.patches[1][1] = love.graphics.newImage(tlCorner, {}) + tlCorner:release() + dbg("top left corner:", p.patches[1][1]:getDimensions()) + + local trCorner = love.image.newImageData(brCornerWidth, verticalEdgeSegment[1] - 1) + trCorner:paste( + rawdata, + 0, 0, + horizontalEdgeSegment[2] + 1, 1, + trCorner:getDimensions() + ) + p.patches[1][3] = love.graphics.newImage(trCorner, {}) + trCorner:release() + dbg("top right corner:", p.patches[1][3]:getDimensions()) + + local blCorner = love.image.newImageData(horizontalEdgeSegment[1] - 1, brCornerHeight) + blCorner:paste( + rawdata, + 0, 0, + 1, verticalEdgeSegment[2] + 1, + blCorner:getDimensions() + ) + p.patches[3][1] = love.graphics.newImage(blCorner, {}) + blCorner:release() + dbg("bottom left corner:", p.patches[3][1]:getDimensions()) + + local brCorner = love.image.newImageData(brCornerWidth, brCornerHeight) + brCorner:paste( + rawdata, + 0, 0, + horizontalEdgeSegment[2] + 1, verticalEdgeSegment[2] + 1, + brCorner:getDimensions() + ) + p.patches[3][3] = love.graphics.newImage(brCorner, {}) + brCorner:release() + dbg("bottom right corner:", p.patches[3][3]:getDimensions()) +end + +local function setMiddle(p, rawdata, horizontalEdgeSegment, verticalEdgeSegment) + dbg("slicing middle") + local w = horizontalEdgeSegment[2] - horizontalEdgeSegment[1] + 1 + local h = verticalEdgeSegment[2] - verticalEdgeSegment[1] + 1 + local middle = love.image.newImageData(w, h) + middle:paste( + rawdata, + 0, 0, + horizontalEdgeSegment[1], verticalEdgeSegment[1], + w, h + ) + p.patches[2][2] = love.graphics.newImage(middle, {}) + middle:release() + dbg("middle:", p.patches[2][2]:getDimensions()) +end + +local function setEdges(p, rawdata, horizontalEdgeSegment, verticalEdgeSegment) + dbg("slicing edges") + local hlen = horizontalEdgeSegment[2] - horizontalEdgeSegment[1] + 1 + local vlen = verticalEdgeSegment[2] - verticalEdgeSegment[1] + 1 + + local top = love.image.newImageData(hlen, verticalEdgeSegment[1] - 1) + top:paste( + rawdata, + 0, 0, + -- 1 to skip over metadata row + horizontalEdgeSegment[1], 1, + top:getDimensions() + ) + p.patches[1][2] = love.graphics.newImage(top, {}) + top:release() + dbg("top:", p.patches[1][2]:getDimensions()) + + -- -2 because of 2 distinct -1s, see comments in setCorners + local bottom = love.image.newImageData(hlen, rawdata:getHeight() - verticalEdgeSegment[2] - 2) + bottom:paste( + rawdata, + 0, 0, + horizontalEdgeSegment[1], verticalEdgeSegment[2] + 1, + bottom:getDimensions() + ) + p.patches[3][2] = love.graphics.newImage(bottom, {}) + bottom:release() + dbg("bottom:", p.patches[3][2]:getDimensions()) + + local left = love.image.newImageData(horizontalEdgeSegment[1] - 1, vlen) + left:paste( + rawdata, + 0, 0, + 1, verticalEdgeSegment[1], + left:getDimensions() + ) + p.patches[2][1] = love.graphics.newImage(left, {}) + left:release() + dbg("left:", p.patches[2][1]:getDimensions()) + + local right = love.image.newImageData(rawdata:getWidth() - horizontalEdgeSegment[2] - 2, vlen) + right:paste( + rawdata, + 0, 0, + horizontalEdgeSegment[2] + 1, verticalEdgeSegment[1], + right:getDimensions() + ) + p.patches[2][3] = love.graphics.newImage(right, {}) + dbg("right:", p.patches[2][3]:getDimensions()) +end + +---Load a 9-slice image +---@param arg string|love.ImageData filename or raw image data to use for 9-slicing +---@return nil +---@return string +function M.load(arg) + local rawdata + local release = false + local p = {} + + if type(arg) == "string" then + dbg("loading sliced image from:", arg) + + rawdata = love.image.newImageData(arg, {}) + release = true + elseif arg.type and arg:type() == "ImageData" then + rawdata = arg + else + return nil, "expected string or ImageData, got "..tostring(arg) + end + + local horizontalEdgeSegment = { + assert(firstBlackPixel(rawdata, "row", 0, false)), + assert(firstBlackPixel(rawdata, "row", 0, true)) + } + + local verticalEdgeSegment = { + assert(firstBlackPixel(rawdata, "col", 0, false)), + assert(firstBlackPixel(rawdata, "col", 0, true)) + } + + local horizontalContentPadding = { + assert(firstBlackPixel(rawdata, "row", rawdata:getHeight() - 1, false)) - 1, + rawdata:getWidth() - assert(firstBlackPixel(rawdata, "row", rawdata:getHeight() - 1, true)) - 1 + } + + local verticalContentPadding = { + assert(firstBlackPixel(rawdata, "col", rawdata:getWidth() - 1, false)) - 1, + rawdata:getHeight() - assert(firstBlackPixel(rawdata, "col", rawdata:getWidth() - 1, true)) - 1 + } + + -- TODO check for valid value ranges in content padding + + p.contentPadding = { + left = horizontalContentPadding[1], + right = horizontalContentPadding[2], + up = verticalContentPadding[1], + down = verticalContentPadding[2] + } + dbg("padding (u,d,l,r):", p.contentPadding.up, p.contentPadding.down, p.contentPadding.left, p.contentPadding.right) + + p.patches = {{}, {}, {}} + + setCorners(p, rawdata, horizontalEdgeSegment, verticalEdgeSegment) + setMiddle(p, rawdata, horizontalEdgeSegment, verticalEdgeSegment) + setEdges(p, rawdata, horizontalEdgeSegment, verticalEdgeSegment) + + p.x, p.y = 0, 0 + p.width = p.patches[1][1]:getWidth() + p.patches[1][3]:getWidth() + p.height = p.patches[1][1]:getHeight() + p.patches[3][1]:getHeight() + + if release then + rawdata:release() + end + + setmetatable(p, PatchedImageMt) + return p +end + +-- THIS REUSES THE IMAGE OBJECTS FROM THE ORIGINAL +function PatchedImage:clone() + local c = {} + + for i = 1, 3 do + c.patches[i] = {} + for j = 1, 3 do + c.patches[i][j] = self.patches[i][j] + end + end + + c.contentPadding = { + left = self.contentPadding.left, + right = self.contentPadding.right, + up = self.contentPadding.up, + down = self.contentPadding.down + } + c.x, c.y = self.x, self.y + c.width, c.height = self.width, self.height + + setmetatable(c, PatchedImageMt) + return c +end + +---Resize image to given dimensions +---@param w number new width +---@param h number new height +function PatchedImage:resize(w, h) + self.width = self:clampWidth(assert(w)) + self.height = self:clampHeight(assert(h)) +end + +---Move image to given position +---@param x number +---@param y number +function PatchedImage:move(x, y) + self.x = assert(x) + self.y = assert(y) +end + +function PatchedImage:getX() + return self.x +end + +function PatchedImage:getY() + return self.y +end + +function PatchedImage:getPosition() + return self.x, self.y +end + +function PatchedImage:getWidth() + return self.width +end + +function PatchedImage:getHeight() + return self.height +end + +function PatchedImage:getDimensions() + return self.width, self.height +end + +---Draws a patch with an optional background debug rect +---@param p love.Image +---@param x number +---@param y number +---@param sx number? +---@param sy number? +local function drawPatch(p, x, y, sx, sy) + sx = sx or 1 + sy = sy or 1 + if debugDraw then + love.graphics.rectangle("fill", x, y, p:getWidth() * sx, p:getHeight() * sy) + end + love.graphics.draw(p, x, y, 0, sx, sy) +end + +function PatchedImage:draw(x, y, w, h) + if x then + self.x = x + else + x = self.x + end + if y then + self.y = y + else + y = self.y + end + if w then + self.width = w + else + w = self.width + end + if h then + self.height = h + else + h = self.height + end + local debugSpacing = debugDraw and DEBUG_DRAW_SEP_WIDTH or 0 + local horizontalEdgeLen = w - self.patches[1][1]:getWidth() - self.patches[1][3]:getWidth() + local verticalEdgeLen = h - self.patches[1][1]:getHeight() - self.patches[3][1]:getHeight() + + horizontalEdgeLen = math.max(horizontalEdgeLen, 0) + verticalEdgeLen = math.max(verticalEdgeLen, 0) + + local horizontalEdgeScale = horizontalEdgeLen / self.patches[1][2]:getWidth() + local verticalEdgeScale = verticalEdgeLen / self.patches[2][1]:getHeight() + + -- middle + drawPatch( + self.patches[2][2], + x + debugSpacing + self.patches[1][1]:getWidth(), + y + debugSpacing + self.patches[1][1]:getHeight(), + horizontalEdgeScale, verticalEdgeScale + ) + + -- edges + -- top + drawPatch( + self.patches[1][2], + x + debugSpacing + self.patches[1][1]:getWidth(), + y, + horizontalEdgeScale, 1 + ) + -- left + drawPatch( + self.patches[2][1], + x, + y + debugSpacing + self.patches[1][1]:getHeight(), + 1, verticalEdgeScale + ) + -- right + drawPatch( + self.patches[2][3], + x + 2*debugSpacing + self.patches[1][1]:getWidth() + horizontalEdgeLen, + y + debugSpacing + self.patches[1][1]:getHeight(), + 1, verticalEdgeScale + ) + -- bottom + drawPatch( + self.patches[3][2], + x + debugSpacing + self.patches[1][1]:getWidth(), + y + 2*debugSpacing + self.patches[1][1]:getHeight() + verticalEdgeLen, + horizontalEdgeScale, 1 + ) + + -- corners + -- top left + drawPatch(self.patches[1][1], x, y) + -- top right + drawPatch( + self.patches[1][3], + x + 2*debugSpacing + self.patches[1][1]:getWidth() + horizontalEdgeLen, + y + ) + -- bottom left + drawPatch( + self.patches[3][1], + x, + y + 2*debugSpacing + self.patches[1][1]:getHeight() + verticalEdgeLen + ) + -- bottom right + drawPatch( + self.patches[3][3], + x + 2*debugSpacing + self.patches[1][1]:getWidth() + horizontalEdgeLen, + y + 2*debugSpacing + self.patches[1][1]:getHeight() + verticalEdgeLen + ) + + if debugDraw then + local rw = 2*debugSpacing + - self.contentPadding.left - self.contentPadding.right + + self.patches[1][1]:getWidth() + horizontalEdgeLen + self.patches[1][3]:getWidth() + local rh = 2*debugSpacing + - self.contentPadding.up - self.contentPadding.down + + self.patches[1][1]:getHeight() + verticalEdgeLen + self.patches[3][1]:getHeight() + + local r, g, b, a = love.graphics.getColor() + local lw = love.graphics.getLineWidth() + love.graphics.setColor(1, 0, 0) + love.graphics.setLineWidth(1) + love.graphics.rectangle( + "line", + x + self.contentPadding.left + 0.5, y + self.contentPadding.up + 0.5, + rw, rh + ) + love.graphics.setColor(r, g, b, a) + love.graphics.setLineWidth(lw) + end +end + +function PatchedImage:getContentWindow(x, y, w, h) + x = x or self.x + y = y or self.y + w = w or self.width + h = h or self.height + + x = x + self.contentPadding.left + y = y + self.contentPadding.up + + w = self:clampWidth(w) - self.contentPadding.left - self.contentPadding.right + h = self:clampHeight(h) - self.contentPadding.up - self.contentPadding.down + + if debugDraw then + w = w + 2*DEBUG_DRAW_SEP_WIDTH + h = h + 2*DEBUG_DRAW_SEP_WIDTH + end + + return x, y, w, h +end + +function PatchedImage:clampWidth(w) + return math.max(w, self.patches[1][1]:getWidth() + self.patches[1][3]:getWidth()) +end + +function PatchedImage:clampHeight(h) + return math.max(h, self.patches[1][1]:getHeight() + self.patches[3][1]:getHeight()) +end + +function M.setDebug(t) + debugDraw = t.draw + debugLog = t.log +end + +function M.isDebugLogging() + return debugLog +end + +function M.isDebugDrawing() + return debugDraw +end + +return M diff --git a/src/main.lua b/src/main.lua index 51fe1cc..257cc57 100644 --- a/src/main.lua +++ b/src/main.lua @@ -12,6 +12,6 @@ function love.load() love.keyboard.setKeyRepeat(true) math.randomseed(love.timer.getTime()) - Gamestate.switch(require("states.main")) + Gamestate.switch(require("states.main-menu")) Gamestate.registerEvents() end diff --git a/src/states/main-menu.lua b/src/states/main-menu.lua index db8829b..4458221 100644 --- a/src/states/main-menu.lua +++ b/src/states/main-menu.lua @@ -1,11 +1,10 @@ local MainMenu = {} -local UI = require("ui") -local rgb = require("helpers.rgb") -local darken = require("helpers.darken") -local lighten = require("helpers.lighten") local Gamestate = require("lib.hump.gamestate") local http = require("socket.http") local enet = require("enet") +local nata = require("lib.nata") +local Vec = require("lib.brinevector") +local data = require("data") local function getPublicIP() local ip = http.request("https://ipinfo.io/ip") @@ -18,53 +17,112 @@ local function splitat(str, char) end function MainMenu:init() - self.ui = UI.new{ - font = love.graphics.newFont(32), - text_color = rgb(255, 255, 255), - bg_color = rgb(60, 60, 60), - bg_hover_color = {lighten(rgb(60, 60, 60))}, - bg_pressed_color = {darken(rgb(60, 60, 60))}, + local systems = { + require("systems.physics"), + require("systems.bolt"), + require("systems.ui"), + require("systems.player"), + require("systems.sprite"), + require("systems.screen-scaler"), } + if not love.filesystem.isFused() then + table.insert(systems, require("systems.debug")) + end - self.addr_textbox = { text = "" } + self.ecs = nata.new{ + available_groups = require("groups"), + systems = systems + } self.host_socket = enet.host_create("*:0") self.peer_socket = nil self.hosting = false self.connecting = false + + do + local PlayerSystem = self.ecs:getSystem(require("systems.player")) + local player = PlayerSystem:spawnPlayer(Vec(192, 48)) + player.sprite.flip = true + player.controllable = true + end + + do + local tileSize = 16 + local screenW, screenH = love.graphics.getDimensions() + local w = 16 * tileSize + local h = 16 * screenH/screenW * tileSize + self.canvas = love.graphics.newCanvas(w, h) + + local border = 2.5 + self.ecs:queue{ collider = {0, 0, w, border} } + self.ecs:queue{ collider = {0, 0, border, h} } + self.ecs:queue{ collider = {w-border, 0, w, h} } + self.ecs:queue{ collider = {0, h-border, w, h} } + end + + do + local host_btn = self.ecs:queue{ + pos = Vec(16, 16), + size = Vec(96, 32), + text = "Host" + } + local join_btn = self.ecs:queue{ + pos = Vec(16, 48+8), + size = Vec(96, 32), + text = "Join" + } + local quit_btn = self.ecs:queue{ + pos = Vec(16, 96), + size = Vec(96, 32), + text = "Quit" + } + self.ecs:on("on_btn_pressed", function(btn) + if btn == quit_btn then + love.event.quit() + end + end) + end end -function MainMenu:update() +function MainMenu:update(dt) + self.ecs:flush() + self.ecs:emit("update", dt) local event = self.host_socket:service() if event and event.type == "connect" then Gamestate.switch(require("states.main"), self.host_socket) end end -function MainMenu:keypressed(...) - self.ui:keypressed(...) -end +function MainMenu:mousemoved(x, y, dx, dy, istouch) + local ScreenScaler = self.ecs:getSystem(require("systems.screen-scaler")) + if not ScreenScaler:isInBounds(x, y) then return end -function MainMenu:textinput(...) - self.ui:textinput(...) + x, y = ScreenScaler:getPosition(x, y) + dx = (ScreenScaler.scale or 1) * dx + dy = (ScreenScaler.scale or 1) * dy + self.ecs:emit("mousemoved", x, y, dx, dy, istouch) end function MainMenu:draw() - local w, h = love.graphics.getDimensions() + local ScreenScaler = self.ecs:getSystem(require("systems.screen-scaler")) - self.ui:textbox(self.addr_textbox, w/2-250, h/2-30, 280, 60) - if self.ui:button("Copy my address", w/2-50, h/2-100, 300, 60) then - local _, port = splitat(self.host_socket:get_socket_address(), ":") - local address = getPublicIP() .. ":" .. port - love.system.setClipboardText(address) - end - if self.ui:button("Connect", w/2+50, h/2-30, 200, 60) then - self.connecting = true - self.host_socket:connect(self.addr_textbox.text) - end + ScreenScaler:start(self.canvas) + self.ecs:emit("draw") + ScreenScaler:finish() - self.ui:postDraw() + -- self.ui:textbox(self.addr_textbox, w/2-250, h/2-30, 280, 60) + -- if self.ui:button("Copy my address", w/2-50, h/2-100, 300, 60) then + -- local _, port = splitat(self.host_socket:get_socket_address(), ":") + -- local address = getPublicIP() .. ":" .. port + -- love.system.setClipboardText(address) + -- end + -- if self.ui:button("Connect", w/2+50, h/2-30, 200, 60) then + -- self.connecting = true + -- self.host_socket:connect(self.addr_textbox.text) + -- end + + -- self.ui:postDraw() end return MainMenu diff --git a/src/states/main.lua b/src/states/main.lua index 6253e13..570af7f 100644 --- a/src/states/main.lua +++ b/src/states/main.lua @@ -4,19 +4,6 @@ local nata = require("lib.nata") local data = require("data") function MainState:enter(_, host_socket) - local groups = { - physical = {filter = {"pos", "vel"}}, - player = {filter = { - "pos", "acc", "speed", "bolts" - -- "bolt_count", - -- "bolt_cooldown", "bolt_speed", "bolt_friction" - }}, - controllable_player = {filter = {"controllable"}}, - sprite = {filter = {"pos", "sprite"}}, - bolt = {filter={"pos", "vel", "bolt"}}, - collider = {filter={"pos", "collider"}} - } - local systems = { require("systems.physics"), require("systems.map"), @@ -35,7 +22,7 @@ function MainState:enter(_, host_socket) end self.ecs = nata.new{ - groups = groups, + available_groups = require("groups"), systems = systems, data = { host_socket = host_socket @@ -47,7 +34,12 @@ function MainState:enter(_, host_socket) self.ecs:on("onMapSwitch", function(map) self:refreshDownscaledCanvas(map) end) - love.graphics.setNewFont(48) + + + -- Spawn in the entity that the local player will be using + local PlayerSystem = self.ecs:getSystem(require("systems.player")) + local player = PlayerSystem:spawnPlayer() + player.controllable = true end function MainState:refreshDownscaledCanvas(map) @@ -104,20 +96,9 @@ end function MainState:draw() local ScreenScaler = self.ecs:getSystem(require("systems.screen-scaler")) - -- Draw the game ScreenScaler:start(self.downscaled_canvas) self.ecs:emit("draw") ScreenScaler:finish() - - -- Draw UI on top - -- local w, h = self.downscaled_canvas:getDimensions() - -- ScreenScaler:start(1000, h/w * 1000) - -- love.graphics.setColor(1, 1, 1) - -- love.graphics.print("Hello World!") - -- ScreenScaler:finish() - - -- Override scaling factors from UI, with scalers for the game - ScreenScaler:overrideScaling(self.downscaled_canvas:getDimensions()) end return MainState diff --git a/src/systems/bolt.lua b/src/systems/bolt.lua index 1a4b165..2f85fcb 100644 --- a/src/systems/bolt.lua +++ b/src/systems/bolt.lua @@ -5,6 +5,8 @@ local Vec = require("lib.brinevector") local BOLT_COLLIDER = {-3, -3, 3, 3} +Bolt.required_groups = {"bolt"} + function Bolt:update(dt) for _, bolt in ipairs(self.pool.groups.bolt.entities) do if bolt.vel.length < 20 and not bolt.pickupable then diff --git a/src/systems/physics.lua b/src/systems/physics.lua index 55fbfd7..782e2c4 100644 --- a/src/systems/physics.lua +++ b/src/systems/physics.lua @@ -5,6 +5,8 @@ local Vec = require("lib.brinevector") -- TODO: Tweak bump world `cellSize` at runtime, when the map switches +Physics.required_groups = {"physical", "collider"} + function Physics:init() self.bump = bump.newWorld() self.boundCollisionFilter = function(...) @@ -17,10 +19,14 @@ function Physics:onMapSwitch(map) end local function getColliderBounds(e) - local x = e.pos.x + e.collider[1] - local y = e.pos.y + e.collider[2] + local x = e.collider[1] + local y = e.collider[2] local w = math.abs(e.collider[3] - e.collider[1]) local h = math.abs(e.collider[4] - e.collider[2]) + if e.pos then + x = x + e.pos.x + y = y + e.pos.y + end return x, y, w, h end @@ -163,4 +169,8 @@ function Physics:project(from, direction, distance) return key_points end +function Physics:queryRect(pos, size, filter) + return self.bump:queryRect(pos.x, pos.y, size.x, size.y, filter) +end + return Physics diff --git a/src/systems/player.lua b/src/systems/player.lua index 01811b3..7126fef 100644 --- a/src/systems/player.lua +++ b/src/systems/player.lua @@ -16,11 +16,7 @@ local function getDirection(up_key, down_key, left_key, right_key) return Vec(dx, dy).normalized end -function Player:init() - -- Spawn in the entity that the local player will be using - local player = self:spawnPlayer() - player.controllable = true -end +Player.required_groups = {"player", "controllable_player", "bolt"} function Player:getMoveDirection() return getDirection( @@ -201,14 +197,16 @@ function Player:resetMap() end end -function Player:spawnPlayer() - local map = self.pool:getSystem(require("systems.map")) - local spawnpoints = map:listSpawnpoints() - local players = self.pool.groups.player.entities - local spawnpoint = spawnpoints[#players % #spawnpoints + 1] +function Player:spawnPlayer(pos) + if not pos then + local map = self.pool:getSystem(require("systems.map")) + local spawnpoints = map:listSpawnpoints() + local players = self.pool.groups.player.entities + pos = spawnpoints[#players % #spawnpoints + 1] + end local player = self.pool:queue{ - pos = spawnpoint, + pos = pos, vel = Vec(0, 0), acc = Vec(), diff --git a/src/systems/screen-scaler.lua b/src/systems/screen-scaler.lua index 635b01a..9a8290c 100644 --- a/src/systems/screen-scaler.lua +++ b/src/systems/screen-scaler.lua @@ -78,6 +78,7 @@ function ScreenScaler:start(p1, p2) self.scale = math.min(sw / width, sh / height) self.offset_x = (sw - width * self.scale)/2 self.offset_y = (sh - height * self.scale)/2 + self.active = true love.graphics.push() if self.canvas then @@ -99,6 +100,7 @@ function ScreenScaler:finish() self:hideBorders() end self.canvas = nil + self.active = false end return ScreenScaler diff --git a/src/systems/sprite.lua b/src/systems/sprite.lua index 79686a2..f49cfc9 100644 --- a/src/systems/sprite.lua +++ b/src/systems/sprite.lua @@ -2,10 +2,12 @@ local data = require("data") local pprint = require("lib.pprint") local Sprite = {} -function Sprite:addToWorld(group, e) +Sprite.required_groups = {"sprite"} + +function Sprite:addToGroup(group, e) if group ~= "sprite" then return end - local sprite = data.sprites[e.sprite.variant] + local sprite = data.sprites[e.sprite.name] assert(sprite, ("Attempt to draw unknown sprite: %s"):format(e.sprite.name)) end diff --git a/src/systems/ui.lua b/src/systems/ui.lua new file mode 100644 index 0000000..5eaf4f2 --- /dev/null +++ b/src/systems/ui.lua @@ -0,0 +1,111 @@ +local UI = {} +local data = require("data") +local rgb = require("helpers.rgb") + +local font = data.fonts["kenney-future"](16) +local on_press_sound = data.audio["switch-5"] +local on_release_sound = data.audio["switch-7"] + +UI.required_groups = {"ui_button", "player"} + +local BUTTON_TIME = 1.2 + +function UI:addToGroup(group, e) + if group == "ui_button" then + e.pressed_timer = 0 + end +end + +function UI:update(dt) + local physics = self.pool:getSystem(require("systems.physics")) + + for _, btn in ipairs(self.pool.groups.ui_button.entities) do + local collisions = physics:queryRect(btn.pos, btn.size) + local pressed = #collisions > 0 + if btn.pressed then + btn.pressed_timer = math.min(btn.pressed_timer + dt, BUTTON_TIME) + elseif btn.pressed_timer and btn.pressed_timer > 0 then + btn.pressed_timer = math.max(0, btn.pressed_timer - dt * 3) + end + + if btn.pressed_timer == BUTTON_TIME and not btn.event_emitted then + btn.event_emitted = true + self.pool:emit("on_btn_pressed", btn) + elseif btn.pressed_timer < BUTTON_TIME and btn.event_emitted then + btn.event_emitted = nil + self.pool:emit("on_btn_released", btn) + end + + if not btn.pressed and pressed then + on_press_sound:play() + elseif btn.pressed and not pressed then + on_release_sound:play() + end + btn.pressed = pressed + end +end + +local mask_shader = love.graphics.newShader[[ + extern number offset_x; + extern number scale; + extern number threshold; + extern vec4 mask_color; + vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) + { + vec4 texturecolor = Texel(tex, texture_coords); + if (screen_coords.x > threshold * scale + offset_x) { + return texturecolor * color; + } else { + return texturecolor * mask_color; + } + } +]] + +function UI:draw() + love.graphics.setFont(font) + local ScreenScaler = self.pool:getSystem(require("systems.screen-scaler")) + + mask_shader:send("mask_color", rgb(10, 250, 20)) + if ScreenScaler.active and not ScreenScaler.canvas then + mask_shader:send("scale", ScreenScaler.scale) + mask_shader:send("offset_x", ScreenScaler.offset_x) + else + mask_shader:send("scale", 1) + mask_shader:send("offset_x", 0) + end + + for _, btn in ipairs(self.pool.groups.ui_button.entities) do + local panel = data.ui.panels["blue"] + local oy = 0 + local r = 0 + if btn.pressed then + panel = data.ui.panels["blue-pressed"] + oy = 2 + r = math.sin(love.timer.getTime()*20)*0.15 + end + + love.graphics.setColor(1, 1, 1) + panel:draw(btn.pos.x, btn.pos.y, btn.size.x, btn.size.y) + local cx, cy, cw, ch = panel:getContentWindow() + + local text_width = font:getWidth(btn.text) + local text_height = font:getHeight(btn.text) + local threshold = math.min(1, (btn.pressed_timer or 0) / BUTTON_TIME) + mask_shader:send("threshold", cx+threshold*text_width + (cw-text_width)/2) + love.graphics.setShader(mask_shader) + love.graphics.printf( + btn.text, + cx+cw/2, + cy+ch/2+oy, + cw, + "left", + r, + 1, 1, + text_width/2, + text_height/2 + ) + love.graphics.setShader() + end +end + +return UI