1
0

Compare commits

..

10 Commits

58 changed files with 1641 additions and 6813 deletions

View File

@ -4,6 +4,9 @@
"param-type-mismatch",
"cast-local-type"
],
"Lua.diagnostics.globals": [
"pprint"
],
"Lua.runtime.version": "LuaJIT",
"Lua.workspace.checkThirdParty": false,
"Lua.workspace.library": [

View File

@ -1,6 +1,6 @@
local map = vim.api.nvim_set_keymap
map('n', '<leader>l', ":execute 'silent !kitty -d src love . &' | redraw!<cr>", {
map('n', '<leader><leader>l', ":execute 'silent !kitty -d src love . &' | redraw!<cr>", {
silent = true,
noremap = true
})

View File

@ -10,7 +10,11 @@ all: win32 win64 linux
love: clean_love
mkdir -p build
cd src && zip -9 -r ../build/$(name).love .
cd src && zip -9 -r ../build/$(name).love . \
-x *.tiled-project \
-x *.tiled-session \
-x *.tsx \
-x *.tmx \
win32: clean_windows download-love-win32 love
mkdir -p build/win32/$(name)

9
src/bolt-mods/bouncy.lua Normal file
View File

@ -0,0 +1,9 @@
local BouncyMod = {}
BouncyMod.dont_die_on_touch_obstacle = true
function BouncyMod.on_bolt_shoot(bolt)
bolt.collider:setRestitution(1)
end
return BouncyMod

24
src/bolt-mods/grow.lua Normal file
View File

@ -0,0 +1,24 @@
local GrowthMod = {}
local GROW_SPEED = 1
local BOLT_SIZE = 4
function GrowthMod.on_bolt_create(bolt, player)
end
function GrowthMod.on_bolt_shoot(bolt, player)
local attr = bolt.attributes
attr.growth = attr.growth or 0
bolt.scale = 1 + attr.growth
end
function GrowthMod.on_bolt_update(bolt, dt)
if bolt.dead then return end
local attr = bolt.attributes
attr.growth = attr.growth + GROW_SPEED * dt
bolt.scale = 1 + attr.growth
bolt.collider:setRadius(BOLT_SIZE * bolt.scale)
end
return GrowthMod

21
src/bolt-mods/init.lua Normal file
View File

@ -0,0 +1,21 @@
local mods = {
bouncy = require(... .. ".bouncy"),
speed_bounce = require(... .. ".speed-bounce"),
retain_speed = require(... .. ".retain-speed"),
grow = require(... .. ".grow"),
}
local order = {
mods.bouncy,
mods.speed_bounce,
mods.retain_speed,
mods.grow
}
local priority = {}
for i, mod in ipairs(order) do
priority[mod] = i
end
return { mods = mods, priority = priority }

View File

@ -0,0 +1,26 @@
local RetainSpeedMod = {}
local MIN_LIFETIME = 0.1
function RetainSpeedMod.on_bolt_shoot(bolt, player)
local attr = bolt.attributes
if not attr.max_vel then return end
local velx, vely = bolt.collider:getLinearVelocity()
local old_velocity = (velx^2 + vely^2)^0.5
local new_velocity = attr.max_vel
bolt.max_lifetime = math.max(bolt.max_lifetime / (new_velocity / old_velocity)^2, MIN_LIFETIME)
local aim_dir = player:get_aim_dir()
bolt.collider:setLinearVelocity(aim_dir.x * new_velocity, aim_dir.y * new_velocity)
end
function RetainSpeedMod.on_bolt_update(bolt)
if bolt.dead then return end
local attr = bolt.attributes
local velx, vely = bolt.collider:getLinearVelocity()
attr.max_vel = math.max(attr.max_vel or 0, (velx^2 + vely^2)^0.5)
end
return RetainSpeedMod

View File

@ -0,0 +1,10 @@
local SpeedBounceMod = {}
local SPEEDUP_AMOUNT = 0.2
function SpeedBounceMod.on_bolt_shoot(bolt)
bolt.max_lifetime = bolt.max_lifetime / 2
bolt.collider:setRestitution(1 + SPEEDUP_AMOUNT)
end
return SpeedBounceMod

113
src/bolt.lua Normal file
View File

@ -0,0 +1,113 @@
local Bolt = {}
local Vec = require("lib.brinevector")
local rgb = require("helpers.rgb")
Bolt.__index = Bolt
Bolt.DEFAULT_RADIUS = 4
Bolt.DEFAULT_DURATION = 2.5
local ACTIVE_BOLT_COLOR = rgb(240, 20, 20)
local INACTIVE_BOLT_COLOR = rgb(200, 100, 100)
function Bolt.new(bf_world)
local bolt = setmetatable({}, Bolt)
bolt.pos = Vec()
bolt.vel = Vec()
bolt.restitution = 0.1
bolt.scale = 1
bolt.max_lifetime = Bolt.DEFAULT_DURATION
bolt.active = false
bolt.owner = nil
bolt.pickupable = true
bolt.bf_world = bf_world
return bolt
end
function Bolt:update_restitution(restitution)
self.restitution = restitution
if self.collider then
self.collider:setRestitution(restitution)
end
end
function Bolt:get_size()
return Bolt.DEFAULT_RADIUS * self.scale
end
function Bolt:update_scale(scale)
self.scale = scale
if self.collider then
self.collider:setRadius(self:get_size())
end
end
function Bolt:set_collisions(enabled)
if enabled and not self.collider then
self.collider = self.bf_world:newCollider("Circle", {
self.pos.x, self.pos.y, self:get_size()
})
self.collider:setLinearVelocity(self.vel.x, self.vel.y)
self.collider:setRestitution(self.restitution)
self.collider.fixture:setUserData("bolt")
end
if self.collider then
self.collider:setActive(enabled)
end
end
function Bolt:draw_active()
love.graphics.setColor(ACTIVE_BOLT_COLOR)
love.graphics.circle("line", self.pos.x, self.pos.y, self:get_size())
end
function Bolt:update_velocity(x, y)
self.vel.x = x
self.vel.y = y
if self.collider then
self.collider:setLinearVelocity(x, y)
end
end
function Bolt:update_position(x, y)
self.pos.x = x
self.pos.y = y
if self.collider then
self.collider:setX(x)
self.collider:setY(y)
end
end
function Bolt:update_physics(dt)
if self.collider then
self.pos.x = self.collider:getX()
self.pos.y = self.collider:getY()
local velx, vely = self.collider:getLinearVelocity()
self.vel.x = velx
self.vel.y = vely
else
self.pos.x = self.pos.x + self.vel.x * dt
self.pos.y = self.pos.y + self.vel.y * dt
end
end
function Bolt:draw_inactive()
love.graphics.setColor(INACTIVE_BOLT_COLOR)
love.graphics.circle("line", self.pos.x, self.pos.y, self:get_size())
end
function Bolt:draw()
if self.active then
self:draw_active()
else
self:draw_inactive()
end
end
return Bolt

View File

@ -1,11 +1,9 @@
function love.conf(t)
t.title = "Love2D project"
t.title = "Dodge Bolt"
t.console = true
t.window.width = 854
t.window.height = 480
t.window.resizable = true
t.modules.joystick = false
t.window.width = 640 * 2
t.window.height = 360 * 2
end

93
src/controls-manager.lua Normal file
View File

@ -0,0 +1,93 @@
local ControlsManager = {}
local Vec = require("lib.brinevector")
local JOYSTICK_DEADZONE = 0.1
local joystick_aim_dirs = {}
local function boolToNum(bool)
return bool and 1 or 0
end
local function getDirectionKey(positive, negative)
return boolToNum(love.keyboard.isDown(positive)) - boolToNum(love.keyboard.isDown(negative))
end
-- TODO: Is asserting here necessary? It could be a good idead to remove this, because
-- it gets called multiple times per frame
local function assert_controller(ctrl)
local is_controller = type(ctrl) == "userdata" and
ctrl.typeOf and
ctrl:typeOf("Joystick")
assert(is_controller, "Expected joystick object")
end
function ControlsManager.get_move_dir(player)
local ctrl = player.controls
if not ctrl then return Vec() end
if ctrl == "keyboard" then
local dirx = getDirectionKey("d", "a")
local diry = getDirectionKey("s", "w")
return Vec(dirx, diry).normalized
else
assert_controller(ctrl)
local dirx = ctrl:getGamepadAxis("leftx")
local diry = ctrl:getGamepadAxis("lefty")
local size = dirx^2 + diry^2
if size > JOYSTICK_DEADZONE then
return Vec(dirx, diry).normalized
else
return Vec(0, 0)
end
end
end
function ControlsManager.get_aim_dir(player)
local ctrl = player.controls
if not ctrl then return Vec() end
if ctrl == "keyboard" then
return (Vec(love.mouse.getPosition()) - player.pos).normalized
else
assert_controller(ctrl)
local dirx = ctrl:getGamepadAxis("rightx")
local diry = ctrl:getGamepadAxis("righty")
local size = dirx^2 + diry^2
local dir
local id = ctrl:getID()
if size < JOYSTICK_DEADZONE and joystick_aim_dirs[id] then
dir = joystick_aim_dirs[id]
else
dir = Vec(dirx, diry).normalized
joystick_aim_dirs[id] = dir
end
return dir
end
end
function ControlsManager.is_shoot_down(player)
local ctrl = player.controls
if not ctrl then return false end
if ctrl == "keyboard" then
return love.keyboard.isDown("space")
else
assert_controller(ctrl)
return ctrl:isGamepadDown("leftshoulder")
end
end
function ControlsManager.is_reload_down(player)
local ctrl = player.controls
if not ctrl then return false end
if ctrl == "keyboard" then
return love.keyboard.isDown("r")
else
assert_controller(ctrl)
return ctrl:isGamepadDown("rightshoulder")
end
end
return ControlsManager

View File

@ -1,14 +0,0 @@
return {
move_up = "w",
move_down = "s",
move_left = "a",
move_right = "d",
aim_up = "up",
aim_down = "down",
aim_left = "left",
aim_right = "right",
shoot = "space"
}

View File

@ -1,88 +0,0 @@
local cargo = require("lib.cargo")
local aseLoader = require("lib.ase-loader")
local pprint = require("lib.pprint")
-- 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)
local function loadAsepriteSprite(filename)
local sprite = {}
local ase = aseLoader(filename)
sprite.width = ase.header.width
sprite.height = ase.header.height
sprite.variants = {}
local LAYER_CHUNK = 0x2004
local CEL_CHUNK = 0x2005
local TAG_CHUNK = 0x2018
local frames = {}
local tags = {}
local layers = {}
local first_tag
for i, ase_frame in ipairs(ase.header.frames) do
for _, chunk in ipairs(ase_frame.chunks) do
if chunk.type == CEL_CHUNK then
local cel = chunk.data
local buffer = love.data.decompress("data", "zlib", cel.data)
local data = love.image.newImageData(cel.width, cel.height, "rgba8", buffer)
local image = love.graphics.newImage(data)
local frame = frames[i]
if not frame then
frame = {
image = love.graphics.newCanvas(sprite.width, sprite.height),
duration = ase_frame.frame_duration / 1000
}
frame.image:setFilter("nearest", "nearest")
frames[i] = frame
end
-- you need to draw in a canvas before.
-- frame images can be of different sizes
-- but never bigger than the header width and height
love.graphics.setCanvas(frame.image)
love.graphics.draw(image, cel.x, cel.y)
love.graphics.setCanvas()
elseif chunk.type == TAG_CHUNK then
for j, tag in ipairs(chunk.data.tags) do
-- first tag as default
if j == 1 then
first_tag = tag.name
end
-- aseprite use 0 notation to begin
-- but in lua, everthing starts in 1
tag.to = tag.to + 1
tag.from = tag.from + 1
tag.frames = tag.to - tag.from
tags[tag.name] = tag
end
elseif chunk.type == LAYER_CHUNK then
table.insert(layers, chunk.data)
end
end
end
for _, tag in pairs(tags) do
local variant = {}
sprite.variants[tag.name] = variant
for i=tag.from,tag.to do
table.insert(variant, frames[i])
end
end
if not sprite.variants.default then
sprite.variants.default = {frames[0]}
end
return sprite
end
return cargo.init{
dir = "data",
loaders = {
aseprite = loadAsepriteSprite,
ase = loadAsepriteSprite,
}
}

View File

@ -1,11 +0,0 @@
{
"automappingRulesFile": "",
"commands": [
],
"extensionsPath": "extensions",
"folders": [
"."
],
"propertyTypes": [
]
}

View File

@ -1,187 +0,0 @@
return {
version = "1.9",
luaversion = "5.1",
tiledversion = "1.9.0",
class = "",
orientation = "orthogonal",
renderorder = "right-down",
width = 30,
height = 20,
tilewidth = 16,
tileheight = 16,
nextlayerid = 5,
nextobjectid = 4,
properties = {},
tilesets = {
{
name = "roguelike-dungeon",
firstgid = 1,
filename = "../tilesets/roguelike-dungeon.tsx",
exportfilename = "../tilesets/roguelike-dungeon.lua"
}
},
layers = {
{
type = "tilelayer",
x = 0,
y = 0,
width = 30,
height = 20,
id = 3,
name = "ground",
class = "",
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
parallaxx = 1,
parallaxy = 1,
properties = {},
encoding = "lua",
data = {
484, 481, 485, 481, 485, 484, 484, 483, 484, 483, 481, 484, 481, 484, 485, 485, 482, 483, 483, 481, 485, 485, 483, 482, 481, 484, 484, 483, 485, 481,
483, 481, 482, 481, 484, 484, 483, 481, 483, 485, 485, 482, 483, 482, 482, 484, 481, 481, 485, 485, 484, 481, 485, 483, 481, 485, 483, 481, 484, 482,
482, 485, 485, 483, 483, 485, 481, 481, 484, 481, 483, 485, 482, 485, 484, 482, 485, 482, 482, 482, 484, 481, 485, 484, 484, 481, 482, 485, 482, 482,
482, 483, 484, 482, 481, 485, 482, 481, 485, 483, 483, 483, 481, 481, 482, 483, 485, 481, 481, 485, 481, 484, 485, 484, 484, 484, 481, 482, 482, 481,
485, 484, 484, 483, 483, 484, 485, 483, 482, 482, 484, 484, 484, 482, 482, 485, 485, 483, 483, 485, 484, 481, 484, 485, 481, 482, 485, 482, 482, 485,
484, 483, 485, 481, 483, 483, 483, 484, 481, 485, 483, 482, 483, 483, 483, 483, 484, 481, 483, 485, 482, 485, 484, 482, 481, 482, 484, 481, 483, 481,
483, 482, 483, 484, 481, 481, 483, 484, 485, 481, 482, 483, 483, 481, 483, 483, 482, 482, 484, 485, 484, 482, 482, 483, 484, 484, 483, 483, 483, 481,
485, 482, 484, 482, 484, 482, 484, 484, 483, 481, 483, 484, 483, 481, 482, 481, 482, 485, 483, 482, 483, 481, 482, 481, 481, 484, 481, 482, 481, 481,
481, 483, 482, 483, 481, 484, 481, 483, 484, 482, 483, 485, 483, 482, 485, 484, 481, 482, 481, 484, 483, 481, 484, 482, 482, 483, 483, 485, 481, 481,
485, 483, 485, 482, 485, 483, 485, 481, 485, 482, 485, 483, 483, 482, 484, 483, 484, 483, 482, 484, 482, 481, 484, 483, 481, 484, 481, 485, 484, 485,
483, 485, 483, 482, 483, 484, 484, 482, 484, 483, 483, 483, 484, 481, 483, 483, 483, 483, 481, 482, 482, 482, 481, 481, 483, 483, 481, 481, 484, 482,
485, 485, 485, 481, 484, 484, 483, 482, 484, 482, 483, 485, 484, 482, 481, 484, 483, 482, 483, 483, 485, 485, 485, 484, 484, 484, 481, 485, 482, 484,
484, 482, 482, 485, 483, 484, 485, 482, 481, 484, 485, 482, 484, 482, 481, 481, 481, 481, 484, 481, 485, 485, 483, 481, 483, 482, 482, 485, 484, 482,
482, 481, 483, 485, 485, 485, 481, 483, 484, 481, 482, 483, 485, 483, 482, 485, 481, 485, 485, 483, 482, 484, 482, 483, 482, 485, 483, 482, 485, 481,
481, 482, 485, 483, 482, 481, 481, 485, 481, 484, 485, 483, 485, 485, 484, 485, 482, 482, 484, 481, 484, 483, 483, 485, 485, 482, 485, 481, 483, 484,
483, 483, 483, 482, 484, 484, 483, 485, 482, 484, 485, 482, 484, 483, 482, 485, 485, 484, 482, 485, 481, 481, 484, 485, 481, 482, 484, 485, 484, 483,
483, 484, 481, 482, 482, 483, 485, 483, 483, 482, 481, 483, 483, 485, 484, 483, 484, 482, 485, 482, 485, 481, 485, 483, 485, 485, 481, 484, 483, 484,
483, 484, 483, 481, 484, 484, 482, 481, 481, 482, 481, 482, 485, 484, 482, 484, 484, 483, 484, 484, 483, 483, 484, 485, 485, 485, 484, 485, 481, 482,
481, 484, 484, 482, 483, 484, 482, 485, 485, 485, 481, 485, 485, 484, 481, 481, 483, 484, 484, 483, 481, 485, 481, 481, 484, 482, 485, 481, 482, 483,
483, 485, 482, 481, 485, 483, 483, 482, 485, 483, 481, 484, 484, 485, 484, 484, 485, 484, 483, 483, 481, 483, 484, 484, 484, 485, 482, 483, 481, 482
}
},
{
type = "tilelayer",
x = 0,
y = 0,
width = 30,
height = 20,
id = 1,
name = "walls",
class = "",
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
parallaxx = 1,
parallaxy = 1,
properties = {},
encoding = "lua",
data = {
13, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 45, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 14,
41, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 10, 11, 43, 0, 0, 0, 0, 0, 0, 0, 0, 42, 11, 11, 11, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 11, 12, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 10, 11, 14, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 13, 43, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 42, 14, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 41, 0, 0, 0, 9, 0, 0, 0, 42, 14, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 13, 43, 0, 0, 0, 0, 38, 0, 0, 0, 41, 0, 0, 0, 0, 42, 11, 12, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 10, 11, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 11, 11, 12, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 41,
41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 41,
42, 11, 11, 11, 11, 11, 11, 11, 11, 11, 16, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 16, 11, 11, 11, 11, 11, 43
}
},
{
type = "tilelayer",
x = 0,
y = 0,
width = 30,
height = 20,
id = 4,
name = "decorations",
class = "",
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
parallaxx = 1,
parallaxy = 1,
properties = {},
encoding = "lua",
data = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 60, 61, 0, 0, 0, 66, 65, 0, 0, 0, 509, 0, 62, 0, 0, 478, 0, 0, 0, 0, 479, 0, 478, 0, 478, 0,
0, 0, 480, 60, 0, 0, 0, 0, 0, 65, 65, 65, 480, 0, 0, 0, 505, 505, 505, 505, 505, 505, 505, 0, 0, 480, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 65, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 480, 0, 0, 65, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 480, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 66, 66, 0, 0, 0, 0, 0, 478, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 66, 0, 66, 0, 0, 479, 478, 0, 0, 0, 0, 0, 0,
0, 0, 480, 0, 0, 0, 0, 422, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 478, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 65, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 478, 478, 0, 0, 0, 0, 0,
0, 65, 65, 65, 0, 0, 0, 0, 0, 0, 0, 480, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 65, 65, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 65, 0, 0, 0, 0, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 483, 0, 0, 66, 66, 0, 0, 0, 0, 0, 65, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 478, 479, 505, 505, 505, 505, 505, 505, 0, 0, 0, 0, 0, 0, 0, 65, 65, 65, 480, 0, 0, 0, 0, 451, 0, 0, 0, 0, 0,
0, 478, 478, 478, 0, 0, 0, 0, 0, 0, 0, 479, 479, 478, 0, 0, 0, 0, 0, 0, 0, 0, 509, 0, 0, 0, 0, 478, 478, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
}
},
{
type = "objectgroup",
draworder = "topdown",
id = 2,
name = "spawnpoints",
class = "",
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
parallaxx = 1,
parallaxy = 1,
properties = {},
objects = {
{
id = 1,
name = "",
class = "",
shape = "point",
x = 67.3333,
y = 173.333,
width = 0,
height = 0,
rotation = 0,
visible = true,
properties = {}
},
{
id = 2,
name = "",
class = "",
shape = "point",
x = 409.333,
y = 163.333,
width = 0,
height = 0,
rotation = 0,
visible = true,
properties = {}
}
}
}
}
}

View File

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.9" tiledversion="1.9.0" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="4">
<editorsettings>
<export target="playground.lua" format="lua"/>
</editorsettings>
<tileset firstgid="1" source="../tilesets/roguelike-dungeon.tsx"/>
<layer id="3" name="ground" width="30" height="20">
<data encoding="csv">
484,481,485,481,485,484,484,483,484,483,481,484,481,484,485,485,482,483,483,481,485,485,483,482,481,484,484,483,485,481,
483,481,482,481,484,484,483,481,483,485,485,482,483,482,482,484,481,481,485,485,484,481,485,483,481,485,483,481,484,482,
482,485,485,483,483,485,481,481,484,481,483,485,482,485,484,482,485,482,482,482,484,481,485,484,484,481,482,485,482,482,
482,483,484,482,481,485,482,481,485,483,483,483,481,481,482,483,485,481,481,485,481,484,485,484,484,484,481,482,482,481,
485,484,484,483,483,484,485,483,482,482,484,484,484,482,482,485,485,483,483,485,484,481,484,485,481,482,485,482,482,485,
484,483,485,481,483,483,483,484,481,485,483,482,483,483,483,483,484,481,483,485,482,485,484,482,481,482,484,481,483,481,
483,482,483,484,481,481,483,484,485,481,482,483,483,481,483,483,482,482,484,485,484,482,482,483,484,484,483,483,483,481,
485,482,484,482,484,482,484,484,483,481,483,484,483,481,482,481,482,485,483,482,483,481,482,481,481,484,481,482,481,481,
481,483,482,483,481,484,481,483,484,482,483,485,483,482,485,484,481,482,481,484,483,481,484,482,482,483,483,485,481,481,
485,483,485,482,485,483,485,481,485,482,485,483,483,482,484,483,484,483,482,484,482,481,484,483,481,484,481,485,484,485,
483,485,483,482,483,484,484,482,484,483,483,483,484,481,483,483,483,483,481,482,482,482,481,481,483,483,481,481,484,482,
485,485,485,481,484,484,483,482,484,482,483,485,484,482,481,484,483,482,483,483,485,485,485,484,484,484,481,485,482,484,
484,482,482,485,483,484,485,482,481,484,485,482,484,482,481,481,481,481,484,481,485,485,483,481,483,482,482,485,484,482,
482,481,483,485,485,485,481,483,484,481,482,483,485,483,482,485,481,485,485,483,482,484,482,483,482,485,483,482,485,481,
481,482,485,483,482,481,481,485,481,484,485,483,485,485,484,485,482,482,484,481,484,483,483,485,485,482,485,481,483,484,
483,483,483,482,484,484,483,485,482,484,485,482,484,483,482,485,485,484,482,485,481,481,484,485,481,482,484,485,484,483,
483,484,481,482,482,483,485,483,483,482,481,483,483,485,484,483,484,482,485,482,485,481,485,483,485,485,481,484,483,484,
483,484,483,481,484,484,482,481,481,482,481,482,485,484,482,484,484,483,484,484,483,483,484,485,485,485,484,485,481,482,
481,484,484,482,483,484,482,485,485,485,481,485,485,484,481,481,483,484,484,483,481,485,481,481,484,482,485,481,482,483,
483,485,482,481,485,483,483,482,485,483,481,484,484,485,484,484,485,484,483,483,481,483,484,484,484,485,482,483,481,482
</data>
</layer>
<layer id="1" name="walls" width="30" height="20">
<data encoding="csv">
13,11,11,11,11,11,11,11,11,11,11,11,11,11,11,45,11,11,11,11,11,11,11,11,11,11,11,11,11,14,
41,0,0,0,0,0,41,0,0,0,0,0,0,0,0,41,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,41,0,0,0,0,0,0,0,0,41,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,10,11,43,0,0,0,0,0,0,0,0,42,11,11,11,12,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,11,12,0,0,0,0,41,
41,0,0,0,0,10,11,14,0,0,0,0,0,9,0,0,0,0,0,0,0,13,43,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,42,14,0,0,0,0,41,0,0,0,0,0,0,0,41,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,41,0,0,0,0,41,0,0,0,0,0,0,0,41,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,41,0,0,0,0,41,0,0,0,0,0,0,0,41,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,41,0,0,0,0,41,0,0,0,0,0,0,0,41,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,41,0,0,0,0,41,0,0,0,9,0,0,0,42,14,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,13,43,0,0,0,0,38,0,0,0,41,0,0,0,0,42,11,12,0,0,0,0,41,
41,0,0,0,0,10,11,43,0,0,0,0,0,0,0,0,0,41,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,38,0,0,0,0,0,0,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,11,11,12,0,0,41,
41,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,41,0,0,0,0,0,41,
41,0,0,0,0,0,0,0,0,0,41,0,0,0,0,0,0,0,0,0,0,0,0,41,0,0,0,0,0,41,
42,11,11,11,11,11,11,11,11,11,16,11,11,11,11,11,11,11,11,11,11,11,11,16,11,11,11,11,11,43
</data>
</layer>
<layer id="4" name="decorations" width="30" height="20">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,60,61,0,0,0,66,65,0,0,0,509,0,62,0,0,478,0,0,0,0,479,0,478,0,478,0,
0,0,480,60,0,0,0,0,0,65,65,65,480,0,0,0,505,505,505,505,505,505,505,0,0,480,0,0,0,0,
0,0,0,0,0,0,0,0,0,65,65,65,0,0,0,0,0,0,0,0,0,0,480,0,0,65,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,66,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,480,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,66,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,66,66,0,0,0,0,0,478,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,66,0,66,0,0,479,478,0,0,0,0,0,0,
0,0,480,0,0,0,0,422,0,0,0,0,0,0,0,0,0,0,0,0,0,0,478,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,65,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,478,478,0,0,0,0,0,
0,65,65,65,0,0,0,0,0,0,0,480,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,65,65,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,65,0,0,0,0,66,66,66,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,483,0,0,66,66,0,0,0,0,0,65,65,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,478,479,505,505,505,505,505,505,0,0,0,0,0,0,0,65,65,65,480,0,0,0,0,451,0,0,0,0,0,
0,478,478,478,0,0,0,0,0,0,0,479,479,478,0,0,0,0,0,0,0,0,509,0,0,0,0,478,478,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer>
<objectgroup id="2" name="spawnpoints">
<object id="1" x="67.3333" y="173.333">
<point/>
</object>
<object id="2" x="409.333" y="163.333">
<point/>
</object>
</objectgroup>
</map>

Binary file not shown.

Binary file not shown.

View File

@ -1,128 +0,0 @@
return {
version = "1.9",
luaversion = "5.1",
tiledversion = "1.9.0",
name = "roguelike-dungeon",
class = "",
tilewidth = 16,
tileheight = 16,
spacing = 1,
margin = 0,
columns = 29,
image = "roguelike-dungeon.png",
imagewidth = 492,
imageheight = 305,
objectalignment = "unspecified",
tilerendersize = "tile",
fillmode = "stretch",
tileoffset = {
x = 0,
y = 0
},
grid = {
orientation = "orthogonal",
width = 16,
height = 16
},
properties = {},
wangsets = {},
tilecount = 522,
tiles = {
{
id = 8,
properties = {
["collidable"] = true
}
},
{
id = 9,
properties = {
["collidable"] = true
}
},
{
id = 10,
properties = {
["collidable"] = true
}
},
{
id = 11,
properties = {
["collidable"] = true
}
},
{
id = 12,
properties = {
["collidable"] = true
}
},
{
id = 13,
properties = {
["collidable"] = true
}
},
{
id = 14,
properties = {
["collidable"] = true
}
},
{
id = 15,
properties = {
["collidable"] = true
}
},
{
id = 37,
properties = {
["collidable"] = true
}
},
{
id = 38,
properties = {
["collidable"] = true
}
},
{
id = 39,
properties = {
["collidable"] = true
}
},
{
id = 40,
properties = {
["collidable"] = true
}
},
{
id = 41,
properties = {
["collidable"] = true
}
},
{
id = 42,
properties = {
["collidable"] = true
}
},
{
id = 43,
properties = {
["collidable"] = true
}
},
{
id = 44,
properties = {
["collidable"] = true
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.9" tiledversion="1.9.0" name="roguelike-dungeon" tilewidth="16" tileheight="16" spacing="1" tilecount="522" columns="29">
<editorsettings>
<export target="roguelike-dungeon.lua" format="lua"/>
</editorsettings>
<image source="roguelike-dungeon.png" width="492" height="305"/>
<tile id="8">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="9">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="10">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="11">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="12">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="13">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="14">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="15">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="37">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="38">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="39">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="40">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="41">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="42">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="43">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
<tile id="44">
<properties>
<property name="collidable" type="bool" value="true"/>
</properties>
</tile>
</tileset>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.9" tiledversion="1.9.0" name="roguelike-indoors" tilewidth="16" tileheight="16" spacing="1" tilecount="486" columns="27">
<image source="roguelike-indoors.png" width="458" height="305"/>
</tileset>

View File

@ -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

View File

@ -1,371 +0,0 @@
--[[
MIT License
Copyright (c) 2021 Pedro Lucas (github.com/elloramir)
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.
]]--
local File = {}
File.__index = File
function File.open(filename)
local data, err = love.filesystem.read(filename)
if not data then return nil, err end
local self = setmetatable({}, File)
self.data = data
self.cursor = 1
return self
end
function File:read(size)
local bytes = self.data:sub(self.cursor, self.cursor+size-1)
self.cursor = self.cursor + size
return bytes
end
-- sizes in bytes
local BYTE = 1
local WORD = 2
local SHORT = 2
local DWORD = 4
local LONG = 4
local FIXED = 4
-- parse data/text to number
local function read_num(data, size)
local bytes = data:read(size)
local hex = ""
for i = size, 1, -1 do
local char = string.sub(bytes, i, i)
hex = hex .. string.format("%02X", string.byte(char))
end
return tonumber(hex, 16)
end
-- return a string by it size
local function read_string(data)
local length = read_num(data, WORD)
return data:read(length)
end
local function grab_header(data)
local header = {}
header.file_size = read_num(data, DWORD)
header.magic_number = read_num(data, WORD)
if header.magic_number ~= 0xA5E0 then
error("Not a valid aseprite file")
end
header.frames_number = read_num(data, WORD)
header.width = read_num(data, WORD)
header.height = read_num(data, WORD)
header.color_depth = read_num(data, WORD)
header.opacity = read_num(data, DWORD)
header.speed = read_num(data, WORD)
-- skip
read_num(data, DWORD * 2)
header.palette_entry = read_num(data, BYTE)
-- skip
read_num(data, BYTE * 3)
header.number_color = read_num(data, WORD)
header.pixel_width = read_num(data, BYTE)
header.pixel_height = read_num(data, BYTE)
header.grid_x = read_num(data, SHORT)
header.grid_y = read_num(data, SHORT)
header.grid_width = read_num(data, WORD)
header.grid_height = read_num(data, WORD)
-- skip
read_num(data, BYTE * 84)
-- to the future
header.frames = {}
return header
end
local function grab_frame_header(data)
local frame_header = {}
frame_header.bytes_size = read_num(data, DWORD)
frame_header.magic_number = read_num(data, WORD)
if frame_header.magic_number ~= 0xF1FA then
error("Corrupted file")
end
local old_chunks = read_num(data, WORD)
frame_header.frame_duration = read_num(data, WORD)
-- skip
read_num(data, BYTE * 2)
-- if 0, use old chunks as chunks
local new_chunks = read_num(data, DWORD)
if new_chunks == 0 then
frame_header.chunks_number = old_chunks
else
frame_header.chunks_number = new_chunks
end
-- to the future
frame_header.chunks = {}
return frame_header
end
local function grab_color_profile(data)
local color_profile = {}
color_profile.type = read_num(data, WORD)
color_profile.uses_fixed_gama = read_num(data, WORD)
color_profile.fixed_game = read_num(data, FIXED)
-- skip
read_num(data, BYTE * 8)
if color_profile.type ~= 1 then
error("No suported color profile, use sRGB")
end
return color_profile
end
local function grab_palette(data)
local palette = {}
palette.entry_size = read_num(data, DWORD)
palette.first_color = read_num(data, DWORD)
palette.last_color = read_num(data, DWORD)
palette.colors = {}
-- skip
read_num(data, BYTE * 8)
for i = 1, palette.entry_size do
local has_name = read_num(data, WORD)
palette.colors[i] = {
color = {
read_num(data, BYTE),
read_num(data, BYTE),
read_num(data, BYTE),
read_num(data, BYTE)}}
if has_name == 1 then
palette.colors[i].name = read_string(data)
end
end
return palette
end
local function grab_old_palette(data)
local palette = {}
palette.packets = read_num(data, WORD)
palette.colors_packet = {}
for i = 1, palette.packets do
palette.colors_packet[i] = {
entries = read_num(data, BYTE),
number = read_num(data, BYTE),
colors = {}}
for j = 1, palette.colors_packet[i].number do
palette.colors_packet[i][j] = {
read_num(data, BYTE),
read_num(data, BYTE),
read_num(data, BYTE)}
end
end
return palette
end
local function grab_layer(data)
local layer = {}
layer.flags = read_num(data, WORD)
layer.type = read_num(data, WORD)
layer.child_level = read_num(data, WORD)
layer.width = read_num(data, WORD)
layer.height = read_num(data, WORD)
layer.blend = read_num(data, WORD)
layer.opacity = read_num(data, BYTE)
-- skip
read_num(data, BYTE * 3)
layer.name = read_string(data)
return layer
end
local function grab_cel(data, size)
local cel = {}
cel.layer_index = read_num(data, WORD)
cel.x = read_num(data, WORD)
cel.y = read_num(data, WORD)
cel.opacity_level = read_num(data, BYTE)
cel.type = read_num(data, WORD)
read_num(data, BYTE * 7)
if cel.type == 2 then
cel.width = read_num(data, WORD)
cel.height = read_num(data, WORD)
cel.data = data:read(size - 26)
end
return cel
end
local function grab_tags(data)
local tags = {}
tags.number = read_num(data, WORD)
tags.tags = {}
-- skip
read_num(data, BYTE * 8)
for i = 1, tags.number do
tags.tags[i] = {
from = read_num(data, WORD),
to = read_num(data, WORD),
direction = read_num(data, BYTE),
extra_byte = read_num(data, BYTE),
color = read_num(data, BYTE * 3),
skip_holder = read_num(data, BYTE * 8),
name = read_string(data)}
end
return tags
end
local function grab_slice(data)
local slice = {}
slice.key_numbers = read_num(data, DWORD)
slice.keys = {}
slice.flags = read_num(data, DWORD)
-- reserved?
read_num(data, DWORD)
slice.name = read_string(data)
for i = 1, slice.key_numbers do
slice.keys[i] = {
frame = read_num(data, DWORD),
x = read_num(data, DWORD),
y = read_num(data, DWORD),
width = read_num(data, DWORD),
height = read_num(data, DWORD)}
if slice.flags == 1 then
slice.keys[i].center_x = read_num(data, DWORD)
slice.keys[i].center_y = read_num(data, DWORD)
slice.keys[i].center_width = read_num(data, DWORD)
slice.keys[i].center_height = read_num(data, DWORD)
elseif slice.flags == 2 then
slice.keys[i].pivot_x = read_num(data, DWORD)
slice.keys[i].pivot_y = read_num(data, DWORD)
end
end
return slice
end
local function grab_user_data(data)
local user_data = {}
user_data.flags = read_num(data, DWORD)
if user_data.flags == 1 then
user_data.text = read_string(data)
elseif user_data.flags == 2 then
user_data.colors = read_num(data, BYTE * 4)
end
return user_data
end
local function grab_chunk(data)
local chunk = {}
chunk.size = read_num(data, DWORD)
chunk.type = read_num(data, WORD)
if chunk.type == 0x2007 then
chunk.data = grab_color_profile(data)
elseif chunk.type == 0x2019 then
chunk.data = grab_palette(data)
elseif chunk.type == 0x0004 then
chunk.data = grab_old_palette(data)
elseif chunk.type == 0x2004 then
chunk.data = grab_layer(data)
elseif chunk.type == 0x2005 then
chunk.data = grab_cel(data, chunk.size)
elseif chunk.type == 0x2018 then
chunk.data = grab_tags(data)
elseif chunk.type == 0x2022 then
chunk.data = grab_slice(data)
elseif chunk.type == 0x2020 then
chunk.data = grab_user_data(data)
end
return chunk
end
local function ase_loader(src)
local data = File.open(src)
assert(data, "can't open " .. src)
local ase = {}
-- parse header
ase.header = grab_header(data)
-- parse frames
for i = 1, ase.header.frames_number do
ase.header.frames[i] = grab_frame_header(data)
-- parse frames chunks
for j = 1, ase.header.frames[i].chunks_number do
ase.header.frames[i].chunks[j] = grab_chunk(data)
end
end
return ase
end
return ase_loader

View File

@ -1,782 +0,0 @@
-- binser.lua
--[[
Copyright (c) 2016-2019 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.
]]
local assert = assert
local error = error
local select = select
local pairs = pairs
local getmetatable = getmetatable
local setmetatable = setmetatable
local type = type
local loadstring = loadstring or load
local concat = table.concat
local char = string.char
local byte = string.byte
local format = string.format
local sub = string.sub
local dump = string.dump
local floor = math.floor
local frexp = math.frexp
local unpack = unpack or table.unpack
local ffi = require("ffi")
-- Lua 5.3 frexp polyfill
-- From https://github.com/excessive/cpml/blob/master/modules/utils.lua
if not frexp then
local log, abs, floor = math.log, math.abs, math.floor
local log2 = log(2)
frexp = function(x)
if x == 0 then return 0, 0 end
local e = floor(log(abs(x)) / log2 + 1)
return x / 2 ^ e, e
end
end
local function pack(...)
return {...}, select("#", ...)
end
local function not_array_index(x, len)
return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x)
end
local function type_check(x, tp, name)
assert(type(x) == tp,
format("Expected parameter %q to be of type %q.", name, tp))
end
local bigIntSupport = false
local isInteger
if math.type then -- Detect Lua 5.3
local mtype = math.type
bigIntSupport = loadstring[[
local char = string.char
return function(n)
local nn = n < 0 and -(n + 1) or n
local b1 = nn // 0x100000000000000
local b2 = nn // 0x1000000000000 % 0x100
local b3 = nn // 0x10000000000 % 0x100
local b4 = nn // 0x100000000 % 0x100
local b5 = nn // 0x1000000 % 0x100
local b6 = nn // 0x10000 % 0x100
local b7 = nn // 0x100 % 0x100
local b8 = nn % 0x100
if n < 0 then
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
end
return char(212, b1, b2, b3, b4, b5, b6, b7, b8)
end]]()
isInteger = function(x)
return mtype(x) == 'integer'
end
else
isInteger = function(x)
return floor(x) == x
end
end
-- Copyright (C) 2012-2015 Francois Perrad.
-- number serialization code modified from https://github.com/fperrad/lua-MessagePack
-- Encode a number as a big-endian ieee-754 double, big-endian signed 64 bit integer, or a small integer
local function number_to_str(n)
if isInteger(n) then -- int
if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data
return char(n + 27)
elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data
n = n + 8192
return char(128 + (floor(n / 0x100) % 0x100), n % 0x100)
elseif bigIntSupport then
return bigIntSupport(n)
end
end
local sign = 0
if n < 0.0 then
sign = 0x80
n = -n
end
local m, e = frexp(n) -- mantissa, exponent
if m ~= m then
return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
elseif m == 1/0 then
if sign == 0 then
return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
else
return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
end
end
e = e + 0x3FE
if e < 1 then -- denormalized numbers
m = m * 2 ^ (52 + e)
e = 0
else
m = (m * 2 - 1) * 2 ^ 52
end
return char(203,
sign + floor(e / 0x10),
(e % 0x10) * 0x10 + floor(m / 0x1000000000000),
floor(m / 0x10000000000) % 0x100,
floor(m / 0x100000000) % 0x100,
floor(m / 0x1000000) % 0x100,
floor(m / 0x10000) % 0x100,
floor(m / 0x100) % 0x100,
m % 0x100)
end
-- Copyright (C) 2012-2015 Francois Perrad.
-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack
local function number_from_str(str, index)
local b = byte(str, index)
if not b then error("Expected more bytes of input.") end
if b < 128 then
return b - 27, index + 1
elseif b < 192 then
local b2 = byte(str, index + 1)
if not b2 then error("Expected more bytes of input.") end
return b2 + 0x100 * (b - 128) - 8192, index + 2
end
local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8)
if (not b1) or (not b2) or (not b3) or (not b4) or
(not b5) or (not b6) or (not b7) or (not b8) then
error("Expected more bytes of input.")
end
if b == 212 then
local flip = b1 >= 128
if flip then -- negative
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
end
local n = ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) *
0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
if flip then
return (-n) - 1, index + 9
else
return n, index + 9
end
end
if b ~= 203 then
error("Expected number")
end
local sign = b1 > 0x7F and -1 or 1
local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10)
local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
local n
if e == 0 then
if m == 0 then
n = sign * 0.0
else
n = sign * (m / 2 ^ 52) * 2 ^ -1022
end
elseif e == 0x7FF then
if m == 0 then
n = sign * (1/0)
else
n = 0.0/0.0
end
else
n = sign * (1.0 + m / 2 ^ 52) * 2 ^ (e - 0x3FF)
end
return n, index + 9
end
local function newbinser()
-- unique table key for getting next value
local NEXT = {}
local CTORSTACK = {}
-- NIL = 202
-- FLOAT = 203
-- TRUE = 204
-- FALSE = 205
-- STRING = 206
-- TABLE = 207
-- REFERENCE = 208
-- CONSTRUCTOR = 209
-- FUNCTION = 210
-- RESOURCE = 211
-- INT64 = 212
-- TABLE WITH META = 213
local mts = {}
local ids = {}
local serializers = {}
local deserializers = {}
local resources = {}
local resources_by_name = {}
local types = {}
types["nil"] = function(x, visited, accum)
accum[#accum + 1] = "\202"
end
function types.number(x, visited, accum)
accum[#accum + 1] = number_to_str(x)
end
function types.boolean(x, visited, accum)
accum[#accum + 1] = x and "\204" or "\205"
end
function types.string(x, visited, accum)
local alen = #accum
if visited[x] then
accum[alen + 1] = "\208"
accum[alen + 2] = number_to_str(visited[x])
else
visited[x] = visited[NEXT]
visited[NEXT] = visited[NEXT] + 1
accum[alen + 1] = "\206"
accum[alen + 2] = number_to_str(#x)
accum[alen + 3] = x
end
end
local function check_custom_type(x, visited, accum)
local res = resources[x]
if res then
accum[#accum + 1] = "\211"
types[type(res)](res, visited, accum)
return true
end
local mt = getmetatable(x)
local id = (mt and ids[mt]) or (ffi and type(x) == "cdata" and ids[tostring(ffi.typeof(x))])
if id then
local constructing = visited[CTORSTACK]
if constructing[x] then
error("Infinite loop in constructor.")
end
constructing[x] = true
accum[#accum + 1] = "\209"
types[type(id)](id, visited, accum)
local args, len = pack(serializers[id](x))
accum[#accum + 1] = number_to_str(len)
for i = 1, len do
local arg = args[i]
types[type(arg)](arg, visited, accum)
end
visited[x] = visited[NEXT]
visited[NEXT] = visited[NEXT] + 1
-- We finished constructing
constructing[x] = nil
return true
end
end
function types.userdata(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
error("Cannot serialize this userdata.")
end
end
function types.table(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
visited[x] = visited[NEXT]
visited[NEXT] = visited[NEXT] + 1
local xlen = #x
local mt = getmetatable(x)
if mt then
accum[#accum + 1] = "\213"
types.table(mt, visited, accum)
else
accum[#accum + 1] = "\207"
end
accum[#accum + 1] = number_to_str(xlen)
for i = 1, xlen do
local v = x[i]
types[type(v)](v, visited, accum)
end
local key_count = 0
for k in pairs(x) do
if not_array_index(k, xlen) then
key_count = key_count + 1
end
end
accum[#accum + 1] = number_to_str(key_count)
for k, v in pairs(x) do
if not_array_index(k, xlen) then
types[type(k)](k, visited, accum)
types[type(v)](v, visited, accum)
end
end
end
end
types["function"] = function(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
visited[x] = visited[NEXT]
visited[NEXT] = visited[NEXT] + 1
local str = dump(x)
accum[#accum + 1] = "\210"
accum[#accum + 1] = number_to_str(#str)
accum[#accum + 1] = str
end
end
types.cdata = function(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
error("Cannot serialize this cdata.")
end
end
types.thread = function() error("Cannot serialize threads.") end
local function deserialize_value(str, index, visited)
local t = byte(str, index)
if not t then return nil, index end
if t < 128 then
return t - 27, index + 1
elseif t < 192 then
local b2 = byte(str, index + 1)
if not b2 then error("Expected more bytes of input.") end
return b2 + 0x100 * (t - 128) - 8192, index + 2
elseif t == 202 then
return nil, index + 1
elseif t == 203 or t == 212 then
return number_from_str(str, index)
elseif t == 204 then
return true, index + 1
elseif t == 205 then
return false, index + 1
elseif t == 206 then
local length, dataindex = number_from_str(str, index + 1)
local nextindex = dataindex + length
if not (length >= 0) then error("Bad string length") end
if #str < nextindex - 1 then error("Expected more bytes of string") end
local substr = sub(str, dataindex, nextindex - 1)
visited[#visited + 1] = substr
return substr, nextindex
elseif t == 207 or t == 213 then
local mt, count, nextindex
local ret = {}
visited[#visited + 1] = ret
nextindex = index + 1
if t == 213 then
mt, nextindex = deserialize_value(str, nextindex, visited)
if type(mt) ~= "table" then error("Expected table metatable") end
end
count, nextindex = number_from_str(str, nextindex)
for i = 1, count do
local oldindex = nextindex
ret[i], nextindex = deserialize_value(str, nextindex, visited)
if nextindex == oldindex then error("Expected more bytes of input.") end
end
count, nextindex = number_from_str(str, nextindex)
for i = 1, count do
local k, v
local oldindex = nextindex
k, nextindex = deserialize_value(str, nextindex, visited)
if nextindex == oldindex then error("Expected more bytes of input.") end
oldindex = nextindex
v, nextindex = deserialize_value(str, nextindex, visited)
if nextindex == oldindex then error("Expected more bytes of input.") end
if k == nil then error("Can't have nil table keys") end
ret[k] = v
end
if mt then setmetatable(ret, mt) end
return ret, nextindex
elseif t == 208 then
local ref, nextindex = number_from_str(str, index + 1)
return visited[ref], nextindex
elseif t == 209 then
local count
local name, nextindex = deserialize_value(str, index + 1, visited)
count, nextindex = number_from_str(str, nextindex)
local args = {}
for i = 1, count do
local oldindex = nextindex
args[i], nextindex = deserialize_value(str, nextindex, visited)
if nextindex == oldindex then error("Expected more bytes of input.") end
end
if not name or not deserializers[name] then
error(("Cannot deserialize class '%s'"):format(tostring(name)))
end
local ret = deserializers[name](unpack(args))
visited[#visited + 1] = ret
return ret, nextindex
elseif t == 210 then
local length, dataindex = number_from_str(str, index + 1)
local nextindex = dataindex + length
if not (length >= 0) then error("Bad string length") end
if #str < nextindex - 1 then error("Expected more bytes of string") end
local ret = loadstring(sub(str, dataindex, nextindex - 1))
visited[#visited + 1] = ret
return ret, nextindex
elseif t == 211 then
local resname, nextindex = deserialize_value(str, index + 1, visited)
if resname == nil then error("Got nil resource name") end
local res = resources_by_name[resname]
if res == nil then
error(("No resources found for name '%s'"):format(tostring(resname)))
end
return res, nextindex
else
error("Could not deserialize type byte " .. t .. ".")
end
end
local function serialize(...)
local visited = {[NEXT] = 1, [CTORSTACK] = {}}
local accum = {}
for i = 1, select("#", ...) do
local x = select(i, ...)
types[type(x)](x, visited, accum)
end
return concat(accum)
end
local function make_file_writer(file)
return setmetatable({}, {
__newindex = function(_, _, v)
file:write(v)
end
})
end
local function serialize_to_file(path, mode, ...)
local file, err = io.open(path, mode)
assert(file, err)
local visited = {[NEXT] = 1, [CTORSTACK] = {}}
local accum = make_file_writer(file)
for i = 1, select("#", ...) do
local x = select(i, ...)
types[type(x)](x, visited, accum)
end
-- flush the writer
file:flush()
file:close()
end
local function writeFile(path, ...)
return serialize_to_file(path, "wb", ...)
end
local function appendFile(path, ...)
return serialize_to_file(path, "ab", ...)
end
local function deserialize(str, index)
assert(type(str) == "string", "Expected string to deserialize.")
local vals = {}
index = index or 1
local visited = {}
local len = 0
local val
while true do
local nextindex
val, nextindex = deserialize_value(str, index, visited)
if nextindex > index then
len = len + 1
vals[len] = val
index = nextindex
else
break
end
end
return vals, len
end
local function deserializeN(str, n, index)
assert(type(str) == "string", "Expected string to deserialize.")
n = n or 1
assert(type(n) == "number", "Expected a number for parameter n.")
assert(n > 0 and floor(n) == n, "N must be a poitive integer.")
local vals = {}
index = index or 1
local visited = {}
local len = 0
local val
while len < n do
local nextindex
val, nextindex = deserialize_value(str, index, visited)
if nextindex > index then
len = len + 1
vals[len] = val
index = nextindex
else
break
end
end
vals[len + 1] = index
return unpack(vals, 1, n + 1)
end
local function readFile(path)
local file, err = io.open(path, "rb")
assert(file, err)
local str = file:read("*all")
file:close()
return deserialize(str)
end
-- Resources
local function registerResource(resource, name)
type_check(name, "string", "name")
assert(not resources[resource],
"Resource already registered.")
assert(not resources_by_name[name],
format("Resource %q already exists.", name))
resources_by_name[name] = resource
resources[resource] = name
return resource
end
local function unregisterResource(name)
type_check(name, "string", "name")
assert(resources_by_name[name], format("Resource %q does not exist.", name))
local resource = resources_by_name[name]
resources_by_name[name] = nil
resources[resource] = nil
return resource
end
-- Templating
local function normalize_template(template)
local ret = {}
for i = 1, #template do
ret[i] = template[i]
end
local non_array_part = {}
-- The non-array part of the template (nested templates) have to be deterministic, so they are sorted.
-- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used
-- in templates. Looking for way around this.
for k in pairs(template) do
if not_array_index(k, #template) then
non_array_part[#non_array_part + 1] = k
end
end
table.sort(non_array_part)
for i = 1, #non_array_part do
local name = non_array_part[i]
ret[#ret + 1] = {name, normalize_template(template[name])}
end
return ret
end
local function templatepart_serialize(part, argaccum, x, len)
local extras = {}
local extracount = 0
for k, v in pairs(x) do
extras[k] = v
extracount = extracount + 1
end
for i = 1, #part do
local name
if type(part[i]) == "table" then
name = part[i][1]
len = templatepart_serialize(part[i][2], argaccum, x[name], len)
else
name = part[i]
len = len + 1
argaccum[len] = x[part[i]]
end
if extras[name] ~= nil then
extracount = extracount - 1
extras[name] = nil
end
end
if extracount > 0 then
argaccum[len + 1] = extras
else
argaccum[len + 1] = nil
end
return len + 1
end
local function templatepart_deserialize(ret, part, values, vindex)
for i = 1, #part do
local name = part[i]
if type(name) == "table" then
local newret = {}
ret[name[1]] = newret
vindex = templatepart_deserialize(newret, name[2], values, vindex)
else
ret[name] = values[vindex]
vindex = vindex + 1
end
end
local extras = values[vindex]
if extras then
for k, v in pairs(extras) do
ret[k] = v
end
end
return vindex + 1
end
local function template_serializer_and_deserializer(metatable, template)
return function(x)
local argaccum = {}
local len = templatepart_serialize(template, argaccum, x, 0)
return unpack(argaccum, 1, len)
end, function(...)
local ret = {}
local args = {...}
templatepart_deserialize(ret, template, args, 1)
return setmetatable(ret, metatable)
end
end
-- Used to serialize classes withh custom serializers and deserializers.
-- If no _serialize or _deserialize (or no _template) value is found in the
-- metatable, then the metatable is registered as a resources.
local function register(metatable, name, serialize, deserialize)
if type(metatable) == "table" then
name = name or metatable.name
serialize = serialize or metatable._serialize
deserialize = deserialize or metatable._deserialize
if (not serialize) or (not deserialize) then
if metatable._template then
-- Register as template
local t = normalize_template(metatable._template)
serialize, deserialize = template_serializer_and_deserializer(metatable, t)
else
-- Register the metatable as a resource. This is semantically
-- similar and more flexible (handles cycles).
registerResource(metatable, name)
return
end
end
elseif type(metatable) == "string" then
name = name or metatable
end
type_check(name, "string", "name")
type_check(serialize, "function", "serialize")
type_check(deserialize, "function", "deserialize")
assert((not ids[metatable]) and (not resources[metatable]),
"Metatable already registered.")
assert((not mts[name]) and (not resources_by_name[name]),
("Name %q already registered."):format(name))
mts[name] = metatable
ids[metatable] = name
serializers[name] = serialize
deserializers[name] = deserialize
return metatable
end
local function unregister(item)
local name, metatable
if type(item) == "string" then -- assume name
name, metatable = item, mts[item]
else -- assume metatable
name, metatable = ids[item], item
end
type_check(name, "string", "name")
mts[name] = nil
if (metatable) then
resources[metatable] = nil
ids[metatable] = nil
end
serializers[name] = nil
deserializers[name] = nil
resources_by_name[name] = nil;
return metatable
end
local function registerClass(class, name)
name = name or class.name
if class.__instanceDict then -- middleclass
register(class.__instanceDict, name)
else -- assume 30log or similar library
register(class, name)
end
return class
end
local registerStruct
local unregisterStruct
if ffi then
function registerStruct(name, serialize, deserialize)
type_check(name, "string", "name")
type_check(serialize, "function", "serialize")
type_check(deserialize, "function", "deserialize")
assert((not mts[name]) and (not resources_by_name[name]),
("Name %q already registered."):format(name))
local ctype_str = tostring(ffi.typeof(name))
ids[ctype_str] = name
mts[name] = ctype_str
serializers[name] = serialize
deserializers[name] = deserialize
return name
end
function unregisterStruct(name)
type_check(name, "string", "name")
local ctype_str = tostring(ffi.typeof(name))
ids[ctype_str] = nil
mts[name] = nil
serializers[name] = nil
deserializers[name] = nil
return name
end
end
return {
VERSION = "0.0-8",
-- aliases
s = serialize,
d = deserialize,
dn = deserializeN,
r = readFile,
w = writeFile,
a = appendFile,
serialize = serialize,
deserialize = deserialize,
deserializeN = deserializeN,
readFile = readFile,
writeFile = writeFile,
appendFile = appendFile,
register = register,
unregister = unregister,
registerResource = registerResource,
unregisterResource = unregisterResource,
registerClass = registerClass,
registerStruct = registerStruct,
unregisterStruct = unregisterStruct,
newbinser = newbinser
}
end
return newbinser()

View File

@ -0,0 +1,69 @@
-- a Collider object, wrapping shape, body, and fixtue
local set_funcs, lp, lg, COLLIDER_TYPES = unpack(
require((...):gsub('collider', '') .. '/utils'))
local Collider = {}
Collider.__index = Collider
function Collider.new(world, collider_type, ...)
-- deprecated
return world:newCollider(collider_type, {...})
end
function Collider:draw_type()
if self.collider_type == 'Edge' or self.collider_type == 'Chain' then
return 'line'
end
return self.collider_type:lower()
end
function Collider:__draw__()
self._draw_type = self._draw_type or self:draw_type()
local args
if self._draw_type == 'line' then
args = {self:getSpatialIdentity()}
else
args = {'line', self:getSpatialIdentity()}
end
love.graphics[self:draw_type()](unpack(args))
end
function Collider:draw()
self:__draw__()
end
function Collider:destroy()
self._world.colliders[self] = nil
self.fixture:setUserData(nil)
self.fixture:destroy()
self.body:destroy()
end
function Collider:getSpatialIdentity()
if self.collider_type == 'Circle' then
return self:getX(), self:getY(), self:getRadius()
else
return self:getWorldPoints(self:getPoints())
end
end
function Collider:collider_contacts()
local contacts = self:getContacts()
local colliders = {}
for i, contact in ipairs(contacts) do
if contact:isTouching() then
local f1, f2 = contact:getFixtures()
if f1 == self.fixture then
colliders[#colliders+1] = f2:getUserData()
else
colliders[#colliders+1] = f1:getUserData()
end
end
end
return colliders
end
return Collider

View File

@ -0,0 +1,24 @@
-- breezefield: init.lua
--[[
implements Collider and World objects
Collider wraps the basic functionality of shape, fixture, and body
World wraps world, and provides automatic drawing simplified collisions
]]--
local bf = {}
local Collider = require(... ..'/collider')
local World = require(... ..'/world')
function bf.newWorld(...)
return bf.World:new(...)
end
bf.Collider = Collider
bf.World = World
return bf

View File

@ -0,0 +1,32 @@
-- function used for both
local function set_funcs(mainobject, subobject)
-- this function assigns functions of a subobject to a primary object
--[[
mainobject: the table to which to assign the functions
subobject: the table whose functions to assign
no output
--]]
for k, v in pairs(subobject.__index) do
if k ~= '__gc' and k ~= '__eq' and k ~= '__index'
and k ~= '__tostring' and k ~= 'destroy' and k ~= 'type'
and k ~= 'typeOf'and k ~= 'getUserData' and k ~= 'setUserData' then
mainobject[k] = function(mainobject, ...)
return v(subobject, ...)
end
end
end
end
local COLLIDER_TYPES = {
CIRCLE = "Circle",
CIRC = "Circle",
RECTANGLE = "Rectangle",
RECT = "Rectangle",
POLYGON = "Polygon",
POLY = "Polygon",
EDGE = 'Edge',
CHAIN = 'Chain'
}
return {set_funcs, love.physics, love.graphics, COLLIDER_TYPES}

View File

@ -0,0 +1,255 @@
-- breezefield: World.lua
--[[
World: has access to all the functions of love.physics.world
additionally stores all Collider objects assigned to it in
self.colliders (as key-value pairs)
can draw all its Colliders
by default, calls :collide on any colliders in it for postSolve
or for beginContact if the colliders are sensors
--]]
-- TODO make updating work from here too
-- TODO: update test and tutorial
local Collider = require((...):gsub('world', '') .. 'collider')
local set_funcs, lp, lg, COLLIDER_TYPES = unpack(
require((...):gsub('world', '') .. '/utils'))
local World = {}
World.__index = World
function World:new(...)
-- create a new physics world
--[[
inputs: (same as love.physics.newWorld)
xg: float, gravity in x direction
yg: float, gravity in y direction
sleep: boolean, whether bodies can sleep
outputs:
w: bf.World, the created world
]]--
local w = {}
setmetatable(w, self)
w._world = lp.newWorld(...)
set_funcs(w, w._world)
w.update = nil -- to use our custom update
w.colliders = {}
-- some functions defined here to use w without being passed it
function w.collide(obja, objb, coll_type, ...)
-- collision event for two Colliders
local function run_coll(obj1, obj2, ...)
if obj1[coll_type] ~= nil then
local e = obj1[coll_type](obj1, obj2, ...)
if type(e) == 'function' then
w.collide_events[#w.collide_events+1] = e
end
end
end
if obja ~= nil and objb ~= nil then
run_coll(obja, objb, ...)
run_coll(objb, obja, ...)
end
end
function w.enter(a, b, ...)
return w.collision(a, b, 'enter', ...)
end
function w.exit(a, b, ...)
return w.collision(a, b, 'exit', ...)
end
function w.preSolve(a, b, ...)
return w.collision(a, b, 'preSolve', ...)
end
function w.postSolve(a, b, ...)
return w.collision(a, b, 'postSolve', ...)
end
function w.collision(a, b, ...)
-- objects that hit one another can have collide methods
-- by default used as postSolve callback
local obja = a:getUserData(a)
local objb = b:getUserData(b)
w.collide(obja, objb, ...)
end
w:setCallbacks(w.enter, w.exit, w.preSolve, w.postSolve)
w.collide_events = {}
return w
end
function World:draw(alpha, draw_over)
-- draw the world
--[[
alpha: sets the alpha of the drawing, defaults to 1
draw_over: draws the collision objects shapes even if their
.draw method is overwritten
--]]
for _, c in pairs(self.colliders) do
c:draw(alpha)
if draw_over then
c:__draw__()
end
end
end
function World:queryRectangleArea(x1, y1, x2, y2)
-- query a bounding-box aligned area for colliders
--[[
inputs:
x1, y1, x2, y2: floats, the x and y coordinates of two points
outputs:
colls: table, all colliders in bounding box
--]]
local colls = {}
local callback = function(fixture)
table.insert(colls, fixture:getUserData())
return true
end
self:queryBoundingBox(x1, y1, x2, y2, callback)
return colls
end
local function check_vertices(vertices)
if #vertices % 2 ~= 0 then
error('vertices must be a multiple of 2')
elseif #vertices < 4 then
error('must have at least 2 vertices with x and y each')
end
end
local function query_region(world, coll_type, args)
local collider = world:newCollider(coll_type, args)
collider:setSensor(true)
world:_disable_callbacks()
world:update(0)
local colls = collider:collider_contacts(collider)
collider:destroy()
world:_enable_callbacks()
return colls
end
function World:_disable_callbacks()
self._callbacks = {self._world:getCallbacks()}
self._world:setCallbacks()
end
function World:_enable_callbacks()
self._world:setCallbacks(unpack(self._callbacks))
end
function World:queryPolygonArea(...)
-- query an area enclosed by the lines connecting a series of points
--[[
inputs:
x1, y1, x2, y2, ... floats, the x and y positions defining polygon
outputs:
colls: table, all Colliders intersecting the area
--]]
local vertices = {...}
if type(vertices[1]) == 'table' then
vertices = vertices[1]
end
check_vertices(vertices)
return query_region(self, 'Polygon', vertices)
end
function World:queryCircleArea(x, y, r)
-- get all colliders in a circle are
--[[
inputs:
x, y, r: floats, x, y and radius of circle
outputs:
colls: table: colliders in area
]]--
return query_region(self, 'Circle', {x, y, r})
end
function World:queryEdgeArea(...)
-- get all colliders along a (series of) line(s)
--[[
inputs:
x1, y1, x2, y2, ... floats, the x and y positions defining lines
outpts:
colls: table: colliders intersecting these lines
--]]
local vertices = {...}
if type(vertices[1]) == 'table' then
vertices = vertices[1]
end
check_vertices(vertices)
return query_region(self, 'Edge', vertices)
end
function World:update(dt)
-- update physics world
self._world:update(dt)
for i, v in pairs(self.collide_events) do
v()
self.collide_events[i] = nil
end
end
--[[
create a new collider in this world
args:
collider_type (string): the type of the collider (not case seinsitive). any of:
circle, rectangle, polygon, edge, chain.
shape_arguments (table): arguments required to instantiate shape.
circle: {x, y, radius}
rectangle: {x, y, width height}
polygon/edge/chain: {x1, y1, x2, y2, ...}
table_to_use (optional, table): table to generate as the collider
]]--
function World:newCollider(collider_type, shape_arguments, table_to_use)
local o = table_to_use or {}
setmetatable(o, Collider)
-- note that you will need to set static vs dynamic later
local _collider_type = COLLIDER_TYPES[collider_type:upper()]
assert(_collider_type ~= nil, "unknown collider type: "..collider_type)
collider_type = _collider_type
local shape
if collider_type == 'Circle' then
local x, y, r = unpack(shape_arguments)
o.body = lp.newBody(self._world, x, y, "dynamic")
shape = lp.newCircleShape(r)
elseif collider_type == "Rectangle" then
local x, y, w, h = unpack(shape_arguments)
o.body = lp.newBody(self._world, x, y, "dynamic")
shape = lp.newRectangleShape(w, h)
collider_type = "Polygon"
else
o.body = lp.newBody(self._world, 0, 0, "dynamic")
shape = lp['new'..collider_type..'Shape'](unpack(shape_arguments))
end
o.collider_type = collider_type
o.fixture = lp.newFixture(o.body, shape, 1)
o.shape = o.fixture:getShape()
o.fixture:setUserData(o)
set_funcs(o, o.body)
set_funcs(o, o.shape)
set_funcs(o, o.fixture)
-- index by self for now
o._world = self
self.colliders[o] = o
return o
end
function World:removeCollider(o)
o._world = nil
o.body:destroy()
self.colliders[o] = nil
end
return World

View File

@ -1,781 +0,0 @@
local bump = {
_VERSION = 'bump v3.1.7',
_URL = 'https://github.com/kikito/bump.lua',
_DESCRIPTION = 'A collision detection library for Lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 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 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.
]]
}
------------------------------------------
-- Auxiliary functions
------------------------------------------
local DELTA = 1e-10 -- floating-point margin of error
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
local function sign(x)
if x > 0 then return 1 end
if x == 0 then return 0 end
return -1
end
local function nearest(x, a, b)
if abs(a - x) < abs(b - x) then return a else return b end
end
local function assertType(desiredType, value, name)
if type(value) ~= desiredType then
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
end
end
local function assertIsPositiveNumber(value, name)
if type(value) ~= 'number' or value <= 0 then
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
end
end
local function assertIsRect(x,y,w,h)
assertType('number', x, 'x')
assertType('number', y, 'y')
assertIsPositiveNumber(w, 'w')
assertIsPositiveNumber(h, 'h')
end
local defaultFilter = function()
return 'slide'
end
------------------------------------------
-- Rectangle functions
------------------------------------------
local function rect_getNearestCorner(x,y,w,h, px, py)
return nearest(px, x, x+w), nearest(py, y, y+h)
end
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
-- the normals of the sides where the segment intersects.
-- Returns nil if the segment never touches the rect
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2)
ti1, ti2 = ti1 or 0, ti2 or 1
local dx, dy = x2-x1, y2-y1
local nx, ny
local nx1, ny1, nx2, ny2 = 0,0,0,0
local p, q, r
for side = 1,4 do
if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left
elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right
elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top
else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom
end
if p == 0 then
if q <= 0 then return nil end
else
r = q / p
if p < 0 then
if r > ti2 then return nil
elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny
end
else -- p > 0
if r < ti1 then return nil
elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny
end
end
end
end
return ti1,ti2, nx1,ny1, nx2,ny2
end
-- Calculates the minkowsky difference between 2 rects, which is another rect
local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
return x2 - x1 - w1,
y2 - y1 - h1,
w1 + w2,
h1 + h2
end
local function rect_containsPoint(x,y,w,h, px,py)
return px - x > DELTA and py - y > DELTA and
x + w - px > DELTA and y + h - py > DELTA
end
local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2)
return x1 < x2+w2 and x2 < x1+w1 and
y1 < y2+h2 and y2 < y1+h1
end
local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2)
local dx = x1 - x2 + (w1 - w2)/2
local dy = y1 - y2 + (h1 - h2)/2
return dx*dx + dy*dy
end
local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY)
goalX = goalX or x1
goalY = goalY or y1
local dx, dy = goalX - x1, goalY - y1
local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
local overlaps, ti, nx, ny
if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other
local px, py = rect_getNearestCorner(x,y,w,h, 0, 0)
local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection
ti = -wi * hi -- ti is the negative area of intersection
overlaps = true
else
local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge)
-- item tunnels into other
if ti1
and ti1 < 1
and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner
and (0 < ti1 + DELTA
or 0 == ti1 and ti2 > 0)
then
ti, nx, ny = ti1, nx1, ny1
overlaps = false
end
end
if not ti then return end
local tx, ty
if overlaps then
if dx == 0 and dy == 0 then
-- intersecting and not moving - use minimum displacement vector
local px, py = rect_getNearestCorner(x,y,w,h, 0,0)
if abs(px) < abs(py) then py = 0 else px = 0 end
nx, ny = sign(px), sign(py)
tx, ty = x1 + px, y1 + py
else
-- intersecting and moving - move in the opposite direction
local ti1, _
ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1)
if not ti1 then return end
tx, ty = x1 + dx * ti1, y1 + dy * ti1
end
else -- tunnel
tx, ty = x1 + dx * ti, y1 + dy * ti
end
return {
overlaps = overlaps,
ti = ti,
move = {x = dx, y = dy},
normal = {x = nx, y = ny},
touch = {x = tx, y = ty},
itemRect = {x = x1, y = y1, w = w1, h = h1},
otherRect = {x = x2, y = y2, w = w2, h = h2}
}
end
------------------------------------------
-- Grid functions
------------------------------------------
local function grid_toWorld(cellSize, cx, cy)
return (cx - 1)*cellSize, (cy-1)*cellSize
end
local function grid_toCell(cellSize, x, y)
return floor(x / cellSize) + 1, floor(y / cellSize) + 1
end
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
-- It has been modified to include both cells when the ray "touches a grid corner",
-- and with a different exit condition
local function grid_traverse_initStep(cellSize, ct, t1, t2)
local v = t2 - t1
if v > 0 then
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
elseif v < 0 then
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
else
return 0, math.huge, math.huge
end
end
local function grid_traverse(cellSize, x1,y1,x2,y2, f)
local cx1,cy1 = grid_toCell(cellSize, x1,y1)
local cx2,cy2 = grid_toCell(cellSize, x2,y2)
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
local cx,cy = cx1,cy1
f(cx, cy)
-- The default implementation had an infinite loop problem when
-- approaching the last cell in some occassions. We finish iterating
-- when we are *next* to the last cell
while abs(cx - cx2) + abs(cy - cy2) > 1 do
if tx < ty then
tx, cx = tx + dx, cx + stepX
f(cx, cy)
else
-- Addition: include both cells when going through corners
if tx == ty then f(cx + stepX, cy) end
ty, cy = ty + dy, cy + stepY
f(cx, cy)
end
end
-- If we have not arrived to the last cell, use it
if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end
end
local function grid_toCellRect(cellSize, x,y,w,h)
local cx,cy = grid_toCell(cellSize, x, y)
local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize)
return cx, cy, cr - cx + 1, cb - cy + 1
end
------------------------------------------
-- Responses
------------------------------------------
local touch = function(world, col, x,y,w,h, goalX, goalY, filter)
return col.touch.x, col.touch.y, {}, 0
end
local cross = function(world, col, x,y,w,h, goalX, goalY, filter)
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local slide = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
if move.x ~= 0 or move.y ~= 0 then
if col.normal.x ~= 0 then
goalX = tch.x
else
goalY = tch.y
end
end
col.slide = {x = goalX, y = goalY}
x,y = tch.x, tch.y
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local bounce = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
local tx, ty = tch.x, tch.y
local bx, by = tx, ty
if move.x ~= 0 or move.y ~= 0 then
local bnx, bny = goalX - tx, goalY - ty
if col.normal.x == 0 then bny = -bny else bnx = -bnx end
bx, by = tx + bnx, ty + bny
end
col.bounce = {x = bx, y = by}
x,y = tch.x, tch.y
goalX, goalY = bx, by
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
------------------------------------------
-- World
------------------------------------------
local World = {}
local World_mt = {__index = World}
-- Private functions and methods
local function sortByWeight(a,b) return a.weight < b.weight end
local function sortByTiAndDistance(a,b)
if a.ti == b.ti then
local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h)
local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h)
return ad < bd
end
return a.ti < b.ti
end
local function addItemToCell(self, item, cx, cy)
self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'})
local row = self.rows[cy]
row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})}
local cell = row[cx]
self.nonEmptyCells[cell] = true
if not cell.items[item] then
cell.items[item] = true
cell.itemCount = cell.itemCount + 1
end
end
local function removeItemFromCell(self, item, cx, cy)
local row = self.rows[cy]
if not row or not row[cx] or not row[cx].items[item] then return false end
local cell = row[cx]
cell.items[item] = nil
cell.itemCount = cell.itemCount - 1
if cell.itemCount == 0 then
self.nonEmptyCells[cell] = nil
end
return true
end
local function getDictItemsInCellRect(self, cl,ct,cw,ch)
local items_dict = {}
for cy=ct,ct+ch-1 do
local row = self.rows[cy]
if row then
for cx=cl,cl+cw-1 do
local cell = row[cx]
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
for item,_ in pairs(cell.items) do
items_dict[item] = true
end
end
end
end
end
return items_dict
end
local function getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cells, cellsLen, visited = {}, 0, {}
grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy)
local row = self.rows[cy]
if not row then return end
local cell = row[cx]
if not cell or visited[cell] then return end
visited[cell] = true
cellsLen = cellsLen + 1
cells[cellsLen] = cell
end)
return cells, cellsLen
end
local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter)
local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1
local nx1,ny1
local visited, itemInfo, itemInfoLen = {},{},0
for i=1,len do
cell = cells[i]
for item in pairs(cell.items) do
if not visited[item] then
visited[item] = true
if (not filter or filter(item)) then
rect = self.rects[item]
l,t,w,h = rect.x,rect.y,rect.w,rect.h
ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1)
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
-- the sorting is according to the t of an infinite line, not the segment
tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge)
itemInfoLen = itemInfoLen + 1
itemInfo[itemInfoLen] = {
item = item,
ti1 = ti1,
ti2 = ti2,
nx = nx1,
ny = ny1,
weight = min(tii0,tii1)
}
end
end
end
end
end
table.sort(itemInfo, sortByWeight)
return itemInfo, itemInfoLen
end
local function getResponseByName(self, name)
local response = self.responses[name]
if not response then
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
end
return response
end
-- Misc Public Methods
function World:addResponse(name, response)
self.responses[name] = response
end
function World:project(item, x,y,w,h, goalX, goalY, filter)
assertIsRect(x,y,w,h)
goalX = goalX or x
goalY = goalY or y
filter = filter or defaultFilter
local collisions, len = {}, 0
local visited = {}
if item ~= nil then visited[item] = true end
-- This could probably be done with less cells using a polygon raster over the cells instead of a
-- bounding rect of the whole movement. Conditional to building a queryPolygon method
local tl, tt = min(goalX, x), min(goalY, y)
local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h)
local tw, th = tr-tl, tb-tt
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
for other,_ in pairs(dictItemsInCellRect) do
if not visited[other] then
visited[other] = true
local responseName = filter(item, other)
if responseName then
local ox,oy,ow,oh = self:getRect(other)
local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY)
if col then
col.other = other
col.item = item
col.type = responseName
len = len + 1
collisions[len] = col
end
end
end
end
table.sort(collisions, sortByTiAndDistance)
return collisions, len
end
function World:countCells()
local count = 0
for _,row in pairs(self.rows) do
for _,_ in pairs(row) do
count = count + 1
end
end
return count
end
function World:hasItem(item)
return not not self.rects[item]
end
function World:getItems()
local items, len = {}, 0
for item,_ in pairs(self.rects) do
len = len + 1
items[len] = item
end
return items, len
end
function World:countItems()
local len = 0
for _ in pairs(self.rects) do len = len + 1 end
return len
end
function World:getRect(item)
local rect = self.rects[item]
if not rect then
error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.')
end
return rect.x, rect.y, rect.w, rect.h
end
function World:toWorld(cx, cy)
return grid_toWorld(self.cellSize, cx, cy)
end
function World:toCell(x,y)
return grid_toCell(self.cellSize, x, y)
end
--- Query methods
function World:queryRect(x,y,w,h, filter)
assertIsRect(x,y,w,h)
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:queryPoint(x,y, filter)
local cx,cy = self:toCell(x,y)
local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:querySegment(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local items = {}
for i=1, len do
items[i] = itemInfo[i].item
end
return items, len
end
function World:querySegmentWithCoords(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local dx, dy = x2-x1, y2-y1
local info, ti1, ti2
for i=1, len do
info = itemInfo[i]
ti1 = info.ti1
ti2 = info.ti2
info.weight = nil
info.x1 = x1 + dx * ti1
info.y1 = y1 + dy * ti1
info.x2 = x1 + dx * ti2
info.y2 = y1 + dy * ti2
end
return itemInfo, len
end
--- Main methods
function World:add(item, x,y,w,h)
local rect = self.rects[item]
if rect then
error('Item ' .. tostring(item) .. ' added to the world twice.')
end
assertIsRect(x,y,w,h)
self.rects[item] = {x=x,y=y,w=w,h=h}
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
addItemToCell(self, item, cx, cy)
end
end
return item
end
function World:remove(item)
local x,y,w,h = self:getRect(item)
self.rects[item] = nil
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
removeItemFromCell(self, item, cx, cy)
end
end
end
function World:update(item, x2,y2,w2,h2)
local x1,y1,w1,h1 = self:getRect(item)
w2,h2 = w2 or w1, h2 or h1
assertIsRect(x2,y2,w2,h2)
if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then
local cellSize = self.cellSize
local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1)
local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2)
if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then
local cr1, cb1 = cl1+cw1-1, ct1+ch1-1
local cr2, cb2 = cl2+cw2-1, ct2+ch2-1
local cyOut
for cy = ct1, cb1 do
cyOut = cy < ct2 or cy > cb2
for cx = cl1, cr1 do
if cyOut or cx < cl2 or cx > cr2 then
removeItemFromCell(self, item, cx, cy)
end
end
end
for cy = ct2, cb2 do
cyOut = cy < ct1 or cy > cb1
for cx = cl2, cr2 do
if cyOut or cx < cl1 or cx > cr1 then
addItemToCell(self, item, cx, cy)
end
end
end
end
local rect = self.rects[item]
rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2
end
end
function World:move(item, goalX, goalY, filter)
local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter)
self:update(item, actualX, actualY)
return actualX, actualY, cols, len
end
function World:check(item, goalX, goalY, filter)
filter = filter or defaultFilter
local visited = {[item] = true}
local visitedFilter = function(itm, other)
if visited[other] then return false end
return filter(itm, other)
end
local cols, len = {}, 0
local x,y,w,h = self:getRect(item)
local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter)
while projected_len > 0 do
local col = projected_cols[1]
len = len + 1
cols[len] = col
visited[col.other] = true
local response = getResponseByName(self, col.type)
goalX, goalY, projected_cols, projected_len = response(
self,
col,
x, y, w, h,
goalX, goalY,
visitedFilter
)
end
return goalX, goalY, cols, len
end
-- Public library functions
bump.newWorld = function(cellSize)
cellSize = cellSize or 64
assertIsPositiveNumber(cellSize, 'cellSize')
local world = setmetatable({
cellSize = cellSize,
rects = {},
rows = {},
nonEmptyCells = {},
responses = {}
}, World_mt)
world:addResponse('touch', touch)
world:addResponse('cross', cross)
world:addResponse('slide', slide)
world:addResponse('bounce', bounce)
return world
end
bump.rect = {
getNearestCorner = rect_getNearestCorner,
getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices,
getDiff = rect_getDiff,
containsPoint = rect_containsPoint,
isIntersecting = rect_isIntersecting,
getSquareDistance = rect_getSquareDistance,
detectCollision = rect_detectCollision
}
bump.responses = {
touch = touch,
cross = cross,
slide = slide,
bounce = bounce
}
return bump

View File

@ -1,104 +0,0 @@
-- cargo v0.1.1
-- https://github.com/bjornbytes/cargo
-- MIT License
local cargo = {}
local function merge(target, source, ...)
if not target or not source then return target end
for k, v in pairs(source) do target[k] = v end
return merge(target, ...)
end
local la, lf, lg = love.audio, love.filesystem, love.graphics
local function makeSound(path)
local info = lf.getInfo(path, 'file')
return la.newSource(path, (info and info.size and info.size < 5e5) and 'static' or 'stream')
end
local function makeFont(path)
return function(size)
return lg.newFont(path, size)
end
end
local function loadFile(path)
return lf.load(path)()
end
cargo.loaders = {
lua = lf and loadFile,
png = lg and lg.newImage,
jpg = lg and lg.newImage,
dds = lg and lg.newImage,
ogv = lg and lg.newVideo,
glsl = lg and lg.newShader,
mp3 = la and makeSound,
ogg = la and makeSound,
wav = la and makeSound,
flac = la and makeSound,
txt = lf and lf.read,
ttf = lg and makeFont,
otf = lg and makeFont,
fnt = lg and lg.newFont
}
cargo.processors = {}
function cargo.init(config)
if type(config) == 'string' then
config = { dir = config }
end
local loaders = merge({}, cargo.loaders, config.loaders)
local processors = merge({}, cargo.processors, config.processors)
local init
local function halp(t, k)
local path = (t._path .. '/' .. k):gsub('^/+', '')
if lf.getInfo(path, 'directory') then
rawset(t, k, init(path))
return t[k]
else
for extension, loader in pairs(loaders) do
local file = path .. '.' .. extension
local fileInfo = lf.getInfo(file)
if loader and fileInfo then
local asset = loader(file)
rawset(t, k, asset)
for pattern, processor in pairs(processors) do
if file:match(pattern) then
processor(asset, file, t)
end
end
return asset
end
end
end
return rawget(t, k)
end
local function __call(t, recurse)
for i, f in ipairs(love.filesystem.getDirectoryItems(t._path)) do
local key = f:gsub('%..-$', '')
halp(t, key)
if recurse and love.filesystem.getInfo(t._path .. '/' .. f, 'directory') then
t[key](recurse)
end
end
return t
end
init = function(path)
return setmetatable({ _path = path }, { __index = halp, __call = __call })
end
return init(config.dir)
end
return cargo

138
src/lib/center.lua Normal file
View File

@ -0,0 +1,138 @@
--[[
MIT License
Copyright (c) 2019 Semyon Entsov <swalrus@yandex.ru>
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.
]]--
local center = {}
function center:setupScreen(width, height)
self._WIDTH = width
self._HEIGHT = height
self._MAX_WIDTH = 0
self._MAX_HEIGHT = 0
self._MAX_RELATIVE_WIDTH = 0
self._MAX_RELATIVE_HEIGHT = 0
self._SCREEN_WIDTH = love.graphics.getWidth()
self._SCREEN_HEIGHT = love.graphics.getHeight()
self._BORDERS = {
['t'] = 0,
['r'] = 0,
['b'] = 0,
['l'] = 0
}
self:apply()
return self
end
function center:setBorders(top, right, bottom, left)
self._BORDERS.t = top
self._BORDERS.r = right
self._BORDERS.b = bottom
self._BORDERS.l = left
end
function center:getScale()
return self._SCALE
end
function center:getOffsetX()
return self._OFFSET_X
end
function center:getOffsetY()
return self._OFFSET_Y
end
function center:setMaxWidth(width)
self._MAX_WIDTH = width
end
function center:setMaxHeight(height)
self._MAX_HEIGHT = height
end
function center:setMaxRelativeWidth(width)
self._MAX_RELATIVE_WIDTH = width
end
function center:setMaxRelativeHeight(height)
self._MAX_RELATIVE_HEIGHT = height
end
function center:resize(width, height)
self._SCREEN_WIDTH = width
self._SCREEN_HEIGHT = height
self:apply()
end
function center:apply()
local available_width = self._SCREEN_WIDTH - self._BORDERS.l - self._BORDERS.r
local available_height = self._SCREEN_HEIGHT - self._BORDERS.t - self._BORDERS.b
local max_width = available_width
local max_height = available_height
if self._MAX_RELATIVE_WIDTH > 0 and available_width * self._MAX_RELATIVE_WIDTH < max_width then
max_width = available_width * self._MAX_RELATIVE_WIDTH
end
if self._MAX_RELATIVE_HEIGHT > 0 and available_height * self._MAX_RELATIVE_HEIGHT < max_height then
max_height = available_height * self._MAX_RELATIVE_HEIGHT
end
if self._MAX_WIDTH > 0 and self._MAX_WIDTH < max_width then
max_width = self._MAX_WIDTH
end
if self._MAX_HEIGHT > 0 and self._MAX_HEIGHT < max_height then
max_height = self._MAX_HEIGHT
end
if max_height / max_width > self._HEIGHT / self._WIDTH then
self._CANVAS_WIDTH = max_width
self._CANVAS_HEIGHT = self._CANVAS_WIDTH * (self._HEIGHT / self._WIDTH)
else
self._CANVAS_HEIGHT = max_height
self._CANVAS_WIDTH = self._CANVAS_HEIGHT * (self._WIDTH / self._HEIGHT)
end
self._SCALE = self._CANVAS_HEIGHT / self._HEIGHT
self._OFFSET_X = self._BORDERS.l + (available_width - self._CANVAS_WIDTH) / 2
self._OFFSET_Y = self._BORDERS.t + (available_height - self._CANVAS_HEIGHT) / 2
end
function center:start()
love.graphics.push()
love.graphics.translate(self._OFFSET_X, self._OFFSET_Y)
love.graphics.scale(self._SCALE, self._SCALE)
end
function center:finish()
love.graphics.pop()
end
function center:toGame(x, y, w, h)
if not (self._OFFSET_X and self._OFFSET_Y and self._SCALE) then
return x, y, w, h
end
return (x - self._OFFSET_X) / self._SCALE,
(y - self._OFFSET_Y) / self._SCALE,
w and w / self._SCALE,
h and h / self._SCALE
end
return center

View File

@ -1,388 +0,0 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- 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.
--
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json

View File

@ -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,

View File

@ -1,132 +0,0 @@
local lg = _G.love.graphics
local graphics = { isCreated = lg and true or false }
function graphics.newSpriteBatch(...)
if graphics.isCreated then
return lg.newSpriteBatch(...)
end
end
function graphics.newCanvas(...)
if graphics.isCreated then
return lg.newCanvas(...)
end
end
function graphics.newImage(...)
if graphics.isCreated then
return lg.newImage(...)
end
end
function graphics.newQuad(...)
if graphics.isCreated then
return lg.newQuad(...)
end
end
function graphics.getCanvas(...)
if graphics.isCreated then
return lg.getCanvas(...)
end
end
function graphics.setCanvas(...)
if graphics.isCreated then
return lg.setCanvas(...)
end
end
function graphics.clear(...)
if graphics.isCreated then
return lg.clear(...)
end
end
function graphics.push(...)
if graphics.isCreated then
return lg.push(...)
end
end
function graphics.origin(...)
if graphics.isCreated then
return lg.origin(...)
end
end
function graphics.scale(...)
if graphics.isCreated then
return lg.scale(...)
end
end
function graphics.translate(...)
if graphics.isCreated then
return lg.translate(...)
end
end
function graphics.pop(...)
if graphics.isCreated then
return lg.pop(...)
end
end
function graphics.draw(...)
if graphics.isCreated then
return lg.draw(...)
end
end
function graphics.rectangle(...)
if graphics.isCreated then
return lg.rectangle(...)
end
end
function graphics.getColor(...)
if graphics.isCreated then
return lg.getColor(...)
end
end
function graphics.setColor(...)
if graphics.isCreated then
return lg.setColor(...)
end
end
function graphics.line(...)
if graphics.isCreated then
return lg.line(...)
end
end
function graphics.polygon(...)
if graphics.isCreated then
return lg.polygon(...)
end
end
function graphics.points(...)
if graphics.isCreated then
return lg.points(...)
end
end
function graphics.getWidth()
if graphics.isCreated then
return lg.getWidth()
end
return 0
end
function graphics.getHeight()
if graphics.isCreated then
return lg.getHeight()
end
return 0
end
return graphics

File diff suppressed because it is too large Load Diff

View File

@ -1,323 +0,0 @@
--- Box2D plugin for STI
-- @module box2d
-- @author Landon Manning
-- @copyright 2019
-- @license MIT/X11
local love = _G.love
local utils = require((...):gsub('plugins.box2d', 'utils'))
local lg = require((...):gsub('plugins.box2d', 'graphics'))
return {
box2d_LICENSE = "MIT/X11",
box2d_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
box2d_VERSION = "2.3.2.7",
box2d_DESCRIPTION = "Box2D hooks for STI.",
--- Initialize Box2D physics world.
-- @param world The Box2D world to add objects to.
box2d_init = function(map, world)
assert(love.physics, "To use the Box2D plugin, please enable the love.physics module.")
local body = love.physics.newBody(world, map.offsetx, map.offsety)
local collision = {
body = body,
}
local function addObjectToWorld(objshape, vertices, userdata, object)
local shape
if objshape == "polyline" then
if #vertices == 4 then
shape = love.physics.newEdgeShape(unpack(vertices))
else
shape = love.physics.newChainShape(false, unpack(vertices))
end
else
shape = love.physics.newPolygonShape(unpack(vertices))
end
local currentBody = body
--dynamic are objects/players etc.
if userdata.properties.dynamic == true then
currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'dynamic')
-- static means it shouldn't move. Things like walls/ground.
elseif userdata.properties.static == true then
currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'static')
-- kinematic means that the object is static in the game world but effects other bodies
elseif userdata.properties.kinematic == true then
currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'kinematic')
end
local fixture = love.physics.newFixture(currentBody, shape)
fixture:setUserData(userdata)
-- Set some custom properties from userdata (or use default set by box2d)
fixture:setFriction(userdata.properties.friction or 0.2)
fixture:setRestitution(userdata.properties.restitution or 0.0)
fixture:setSensor(userdata.properties.sensor or false)
fixture:setFilterData(
userdata.properties.categories or 1,
userdata.properties.mask or 65535,
userdata.properties.group or 0
)
local obj = {
object = object,
body = currentBody,
shape = shape,
fixture = fixture,
}
table.insert(collision, obj)
end
local function getPolygonVertices(object)
local vertices = {}
for _, vertex in ipairs(object.polygon) do
table.insert(vertices, vertex.x)
table.insert(vertices, vertex.y)
end
return vertices
end
local function calculateObjectPosition(object, tile)
local o = {
shape = object.shape,
x = (object.dx or object.x) + map.offsetx,
y = (object.dy or object.y) + map.offsety,
w = object.width,
h = object.height,
polygon = object.polygon or object.polyline or object.ellipse or object.rectangle
}
local userdata = {
object = o,
properties = object.properties
}
o.r = object.rotation or 0
if o.shape == "rectangle" then
local cos = math.cos(math.rad(o.r))
local sin = math.sin(math.rad(o.r))
local oy = 0
if object.gid then
local tileset = map.tilesets[map.tiles[object.gid].tileset]
local lid = object.gid - tileset.firstgid
local t = {}
-- This fixes a height issue
o.y = o.y + map.tiles[object.gid].offset.y
oy = o.h
for _, tt in ipairs(tileset.tiles) do
if tt.id == lid then
t = tt
break
end
end
if t.objectGroup then
for _, obj in ipairs(t.objectGroup.objects) do
-- Every object in the tile
calculateObjectPosition(obj, object)
end
return
else
o.w = map.tiles[object.gid].width
o.h = map.tiles[object.gid].height
end
end
o.polygon = {
{ x=o.x+0, y=o.y+0 },
{ x=o.x+o.w, y=o.y+0 },
{ x=o.x+o.w, y=o.y+o.h },
{ x=o.x+0, y=o.y+o.h }
}
for _, vertex in ipairs(o.polygon) do
vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin, oy)
end
local vertices = getPolygonVertices(o)
addObjectToWorld(o.shape, vertices, userdata, tile or object)
elseif o.shape == "ellipse" then
if not o.polygon then
o.polygon = utils.convert_ellipse_to_polygon(o.x, o.y, o.w, o.h)
end
local vertices = getPolygonVertices(o)
local triangles = love.math.triangulate(vertices)
for _, triangle in ipairs(triangles) do
addObjectToWorld(o.shape, triangle, userdata, tile or object)
end
elseif o.shape == "polygon" then
-- Recalculate collision polygons inside tiles
if tile then
local cos = math.cos(math.rad(o.r))
local sin = math.sin(math.rad(o.r))
for _, vertex in ipairs(o.polygon) do
vertex.x = vertex.x + o.x
vertex.y = vertex.y + o.y
vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin)
end
end
local vertices = getPolygonVertices(o)
local triangles = love.math.triangulate(vertices)
for _, triangle in ipairs(triangles) do
addObjectToWorld(o.shape, triangle, userdata, tile or object)
end
elseif o.shape == "polyline" then
local vertices = getPolygonVertices(o)
addObjectToWorld(o.shape, vertices, userdata, tile or object)
end
end
for _, tile in pairs(map.tiles) do
if map.tileInstances[tile.gid] then
for _, instance in ipairs(map.tileInstances[tile.gid]) do
-- Every object in every instance of a tile
if tile.objectGroup then
for _, object in ipairs(tile.objectGroup.objects) do
if object.properties.collidable == true then
object = utils.deepCopy(object)
object.dx = instance.x + object.x
object.dy = instance.y + object.y
calculateObjectPosition(object, instance)
end
end
end
-- Every instance of a tile
if tile.properties.collidable == true then
local object = {
shape = "rectangle",
x = instance.x,
y = instance.y,
width = map.tilewidth,
height = map.tileheight,
properties = tile.properties
}
calculateObjectPosition(object, instance)
end
end
end
end
for _, layer in ipairs(map.layers) do
-- Entire layer
if layer.properties.collidable == true then
if layer.type == "tilelayer" then
for gid, tiles in pairs(map.tileInstances) do
local tile = map.tiles[gid]
local tileset = map.tilesets[tile.tileset]
for _, instance in ipairs(tiles) do
if instance.layer == layer then
local object = {
shape = "rectangle",
x = instance.x,
y = instance.y,
width = tileset.tilewidth,
height = tileset.tileheight,
properties = tile.properties
}
calculateObjectPosition(object, instance)
end
end
end
elseif layer.type == "objectgroup" then
for _, object in ipairs(layer.objects) do
calculateObjectPosition(object)
end
elseif layer.type == "imagelayer" then
local object = {
shape = "rectangle",
x = layer.x or 0,
y = layer.y or 0,
width = layer.width,
height = layer.height,
properties = layer.properties
}
calculateObjectPosition(object)
end
end
-- Individual objects
if layer.type == "objectgroup" then
for _, object in ipairs(layer.objects) do
if object.properties.collidable == true then
calculateObjectPosition(object)
end
end
end
end
map.box2d_collision = collision
end,
--- Remove Box2D fixtures and shapes from world.
-- @param index The index or name of the layer being removed
box2d_removeLayer = function(map, index)
local layer = assert(map.layers[index], "Layer not found: " .. index)
local collision = map.box2d_collision
-- Remove collision objects
for i = #collision, 1, -1 do
local obj = collision[i]
if obj.object.layer == layer then
obj.fixture:destroy()
table.remove(collision, i)
end
end
end,
--- Draw Box2D physics world.
-- @param tx Translate on X
-- @param ty Translate on Y
-- @param sx Scale on X
-- @param sy Scale on Y
box2d_draw = function(map, tx, ty, sx, sy)
local collision = map.box2d_collision
lg.push()
lg.scale(sx or 1, sy or sx or 1)
lg.translate(math.floor(tx or 0), math.floor(ty or 0))
for _, obj in ipairs(collision) do
local points = {obj.body:getWorldPoints(obj.shape:getPoints())}
local shape_type = obj.shape:getType()
if shape_type == "edge" or shape_type == "chain" then
love.graphics.line(points)
elseif shape_type == "polygon" then
love.graphics.polygon("line", points)
else
error("sti box2d plugin does not support "..shape_type.." shapes")
end
end
lg.pop()
end
}
--- Custom Properties in Tiled are used to tell this plugin what to do.
-- @table Properties
-- @field collidable set to true, can be used on any Layer, Tile, or Object
-- @field sensor set to true, can be used on any Tile or Object that is also collidable
-- @field dynamic set to true, can be used on any Tile or Object
-- @field friction can be used to define the friction of any Object
-- @field restitution can be used to define the restitution of any Object
-- @field categories can be used to set the filter Category of any Object
-- @field mask can be used to set the filter Mask of any Object
-- @field group can be used to set the filter Group of any Object

View File

@ -1,235 +0,0 @@
--- Bump.lua plugin for STI
-- @module bump.lua
-- @author David Serrano (BobbyJones|FrenchFryLord)
-- @copyright 2019
-- @license MIT/X11
local lg = require((...):gsub('plugins.bump', 'graphics'))
local function getKeys(t)
local keys = {}
for k in pairs(t) do
table.insert(keys, k)
end
return keys
end
local bit = require("bit")
local FLIPPED_HORIZONTALLY_FLAG = 0x80000000;
local FLIPPED_VERTICALLY_FLAG = 0x40000000;
local FLIPPED_DIAGONALLY_FLAG = 0x20000000;
local ROTATED_HEXAGONAL_120_FLAG = 0x10000000;
local GID_MASK = bit.bnot(bit.bor(FLIPPED_DIAGONALLY_FLAG, FLIPPED_VERTICALLY_FLAG, FLIPPED_HORIZONTALLY_FLAG, ROTATED_HEXAGONAL_120_FLAG))
local function findTileFromTilesets(tilesets, id)
for _, tileset in ipairs(tilesets) do
if tileset.firstgid <= id then
for _, tile in ipairs(tileset.tiles) do
if tileset.firstgid + tile.id == id then
return tile
end
end
end
end
end
return {
bump_LICENSE = "MIT/X11",
bump_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
bump_VERSION = "3.1.7.1",
bump_DESCRIPTION = "Bump hooks for STI.",
--- Adds each collidable tile to the Bump world.
-- @param world The Bump world to add objects to.
-- @return collidables table containing the handles to the objects in the Bump world.
bump_init = function(map, world)
local collidables = {}
for _, gid in ipairs(getKeys(map.tileInstances)) do
local id = bit.band(gid, GID_MASK)
local tile = findTileFromTilesets(map.tilesets, id)
if tile then
for _, instance in ipairs(map.tileInstances[gid]) do
-- Every object in every instance of a tile
if tile.objectGroup then
for _, object in ipairs(tile.objectGroup.objects) do
if object.properties.collidable == true then
local t = {
name = object.name,
type = object.type,
x = instance.x + map.offsetx + object.x,
y = instance.y + map.offsety + object.y,
width = object.width,
height = object.height,
layer = instance.layer,
properties = object.properties
}
world:add(t, t.x, t.y, t.width, t.height)
table.insert(collidables, t)
end
end
end
-- Every instance of a tile
if tile.properties and tile.properties.collidable == true then
local tileProperties = map.tiles[gid]
local x = instance.x + map.offsetx
local y = instance.y + map.offsety
local sx = tileProperties.sx
local sy = tileProperties.sy
-- Width and height can only be positive in bump, to get around this
-- For negative scaling just move the position back instead
if sx < 1 then
sx = -sx
x = x - map.tilewidth * sx
end
if sy < 1 then
sy = -sy
x = x - map.tileheight * sy
end
local t = {
x = x,
y = y,
width = map.tilewidth * sx,
height = map.tileheight * sy,
layer = instance.layer,
type = tile.type,
properties = tile.properties
}
world:add(t, t.x, t.y, t.width, t.height)
table.insert(collidables, t)
end
end
end
end
for _, layer in ipairs(map.layers) do
-- Entire layer
if layer.properties.collidable == true then
if layer.type == "tilelayer" then
for y, tiles in ipairs(layer.data) do
for x, tile in pairs(tiles) do
if tile.objectGroup then
for _, object in ipairs(tile.objectGroup.objects) do
if object.properties.collidable == true then
local t = {
name = object.name,
type = object.type,
x = ((x-1) * map.tilewidth + tile.offset.x + map.offsetx) + object.x,
y = ((y-1) * map.tileheight + tile.offset.y + map.offsety) + object.y,
width = object.width,
height = object.height,
layer = layer,
properties = object.properties
}
world:add(t, t.x, t.y, t.width, t.height)
table.insert(collidables, t)
end
end
end
local t = {
x = (x-1) * map.tilewidth + tile.offset.x + map.offsetx,
y = (y-1) * map.tileheight + tile.offset.y + map.offsety,
width = tile.width,
height = tile.height,
layer = layer,
type = tile.type,
properties = tile.properties
}
world:add(t, t.x, t.y, t.width, t.height)
table.insert(collidables, t)
end
end
elseif layer.type == "imagelayer" then
world:add(layer, layer.x, layer.y, layer.width, layer.height)
table.insert(collidables, layer)
end
end
-- individual collidable objects in a layer that is not "collidable"
-- or whole collidable objects layer
if layer.type == "objectgroup" then
for _, obj in ipairs(layer.objects) do
if layer.properties.collidable == true or obj.properties.collidable == true then
if obj.shape == "rectangle" then
local t = {
name = obj.name,
type = obj.type,
x = obj.x + map.offsetx,
y = obj.y + map.offsety,
width = obj.width,
height = obj.height,
layer = layer,
properties = obj.properties
}
if obj.gid then
t.y = t.y - obj.height
end
world:add(t, t.x, t.y, t.width, t.height)
table.insert(collidables, t)
end -- TODO implement other object shapes?
end
end
end
end
map.bump_world = world
map.bump_collidables = collidables
end,
--- Remove layer
-- @param index to layer to be removed
bump_removeLayer = function(map, index)
local layer = assert(map.layers[index], "Layer not found: " .. index)
local collidables = map.bump_collidables
-- Remove collision objects
for i = #collidables, 1, -1 do
local obj = collidables[i]
if obj.layer == layer
and (
layer.properties.collidable == true
or obj.properties.collidable == true
) then
map.bump_world:remove(obj)
table.remove(collidables, i)
end
end
end,
--- Draw bump collisions world.
-- @param world bump world holding the tiles geometry
-- @param tx Translate on X
-- @param ty Translate on Y
-- @param sx Scale on X
-- @param sy Scale on Y
bump_draw = function(map, tx, ty, sx, sy)
lg.push()
lg.scale(sx or 1, sy or sx or 1)
lg.translate(math.floor(tx or 0), math.floor(ty or 0))
local items = map.bump_world:getItems()
for _, item in ipairs(items) do
lg.rectangle("line", map.bump_world:getRect(item))
end
lg.pop()
end
}
--- Custom Properties in Tiled are used to tell this plugin what to do.
-- @table Properties
-- @field collidable set to true, can be used on any Layer, Tile, or Object

View File

@ -1,217 +0,0 @@
-- Some utility functions that shouldn't be exposed.
local utils = {}
-- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/path.lua#L286
function utils.format_path(path)
local np_gen1,np_gen2 = '[^SEP]+SEP%.%.SEP?','SEP+%.?SEP'
local np_pat1, np_pat2 = np_gen1:gsub('SEP','/'), np_gen2:gsub('SEP','/')
local k
repeat -- /./ -> /
path,k = path:gsub(np_pat2,'/',1)
until k == 0
repeat -- A/../ -> (empty)
path,k = path:gsub(np_pat1,'',1)
until k == 0
if path == '' then path = '.' end
return path
end
-- Compensation for scale/rotation shift
function utils.compensate(tile, tileX, tileY, tileW, tileH)
local compx = 0
local compy = 0
if tile.sx < 0 then compx = tileW end
if tile.sy < 0 then compy = tileH end
if tile.r > 0 then
tileX = tileX + tileH - compy
tileY = tileY + tileH + compx - tileW
elseif tile.r < 0 then
tileX = tileX + compy
tileY = tileY - compx + tileH
else
tileX = tileX + compx
tileY = tileY + compy
end
return tileX, tileY
end
-- Cache images in main STI module
function utils.cache_image(sti, path, image)
image = image or love.graphics.newImage(path)
image:setFilter("nearest", "nearest")
sti.cache[path] = image
end
-- We just don't know.
function utils.get_tiles(imageW, tileW, margin, spacing)
imageW = imageW - margin
local n = 0
while imageW >= tileW do
imageW = imageW - tileW
if n ~= 0 then imageW = imageW - spacing end
if imageW >= 0 then n = n + 1 end
end
return n
end
-- Decompress tile layer data
function utils.get_decompressed_data(data)
local ffi = require "ffi"
local d = {}
local decoded = ffi.cast("uint32_t*", data)
for i = 0, data:len() / ffi.sizeof("uint32_t") do
table.insert(d, tonumber(decoded[i]))
end
return d
end
-- Convert a Tiled ellipse object to a LOVE polygon
function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments)
local ceil = math.ceil
local cos = math.cos
local sin = math.sin
local function calc_segments(segments)
local function vdist(a, b)
local c = {
x = a.x - b.x,
y = a.y - b.y,
}
return c.x * c.x + c.y * c.y
end
segments = segments or 64
local vertices = {}
local v = { 1, 2, ceil(segments/4-1), ceil(segments/4) }
local m
if love and love.physics then
m = love.physics.getMeter()
else
m = 32
end
for _, i in ipairs(v) do
local angle = (i / segments) * math.pi * 2
local px = x + w / 2 + cos(angle) * w / 2
local py = y + h / 2 + sin(angle) * h / 2
table.insert(vertices, { x = px / m, y = py / m })
end
local dist1 = vdist(vertices[1], vertices[2])
local dist2 = vdist(vertices[3], vertices[4])
-- Box2D threshold
if dist1 < 0.0025 or dist2 < 0.0025 then
return calc_segments(segments-2)
end
return segments
end
local segments = calc_segments(max_segments)
local vertices = {}
table.insert(vertices, { x = x + w / 2, y = y + h / 2 })
for i = 0, segments do
local angle = (i / segments) * math.pi * 2
local px = x + w / 2 + cos(angle) * w / 2
local py = y + h / 2 + sin(angle) * h / 2
table.insert(vertices, { x = px, y = py })
end
return vertices
end
function utils.rotate_vertex(map, vertex, x, y, cos, sin, oy)
if map.orientation == "isometric" then
x, y = utils.convert_isometric_to_screen(map, x, y)
vertex.x, vertex.y = utils.convert_isometric_to_screen(map, vertex.x, vertex.y)
end
vertex.x = vertex.x - x
vertex.y = vertex.y - y
return
x + cos * vertex.x - sin * vertex.y,
y + sin * vertex.x + cos * vertex.y - (oy or 0)
end
--- Project isometric position to cartesian position
function utils.convert_isometric_to_screen(map, x, y)
local mapW = map.width
local tileW = map.tilewidth
local tileH = map.tileheight
local tileX = x / tileH
local tileY = y / tileH
local offsetX = mapW * tileW / 2
return
(tileX - tileY) * tileW / 2 + offsetX,
(tileX + tileY) * tileH / 2
end
function utils.hex_to_color(hex)
if hex:sub(1, 1) == "#" then
hex = hex:sub(2)
end
return {
r = tonumber(hex:sub(1, 2), 16) / 255,
g = tonumber(hex:sub(3, 4), 16) / 255,
b = tonumber(hex:sub(5, 6), 16) / 255
}
end
function utils.pixel_function(_, _, r, g, b, a)
local mask = utils._TC
if r == mask.r and
g == mask.g and
b == mask.b then
return r, g, b, 0
end
return r, g, b, a
end
function utils.fix_transparent_color(tileset, path)
local image_data = love.image.newImageData(path)
tileset.image = love.graphics.newImage(image_data)
if tileset.transparentcolor then
utils._TC = utils.hex_to_color(tileset.transparentcolor)
image_data:mapPixel(utils.pixel_function)
tileset.image = love.graphics.newImage(image_data)
end
end
function utils.deepCopy(t)
local copy = {}
for k,v in pairs(t) do
if type(v) == "table" then
v = utils.deepCopy(v)
end
copy[k] = v
end
return copy
end
return utils

View File

@ -1,11 +1,9 @@
local Gamestate = require("lib.hump.gamestate")
local binser = require("lib.binser")
local Vec = require("lib.brinevector")
local ScreenScaler = require("screen-scaler")
pprint = require("lib.pprint")
binser.registerStruct("brinevector",
function(v) return v.x, v.y end,
function(x, y) return Vec(x, y) end
)
-- 640 x 360
ScreenScaler.setVirtualDimensions(16 * 40, 9 * 40)
function love.load()
love.math.setRandomSeed(love.timer.getTime())

25
src/player.lua Normal file
View File

@ -0,0 +1,25 @@
local Vec = require("lib.brinevector")
local Player = {}
Player.__index = Player
Player.MAX_HEALTH = 3
Player.PLAYER_SIZE = 10
function Player.new(ctrl, collision_world, pos)
local player = setmetatable({}, Player)
player.controls = ctrl
player.pos = pos
player.vel = Vec()
player.shoot_cooldown = nil
player.health = Player.MAX_HEALTH
player.held_bolt = true
player.bolt_mods = {}
player.collider = collision_world:newCollider("Circle", { pos.x, pos.y, Player.PLAYER_SIZE })
player.collider.fixture:setUserData("player")
return player
end
return Player

232
src/screen-scaler.lua Normal file
View File

@ -0,0 +1,232 @@
--- Module resposible for scaling screen and centering contents
-- while respecting ratios.
-- @module ScreenScaler
local ScreenScaler = {}
-- This "Center" library will do most of the heavy lifting
-- TODO: Removed this depedency, embed needed functionality
local Center = require("lib.center")
local getRealDimensions = love.graphics.getDimensions
--- Set the "ideal" dimensions from which everything else will be scaled.
-- @tparam number w virtual width
-- @tparam number h virtual height
function ScreenScaler.setVirtualDimensions(w, h)
assert(type(w) == "number", "Expected width to be number")
assert(type(h) == "number", "Expected height to be number")
ScreenScaler.width, ScreenScaler.height = nil, nil
-- Setup library resposible for scaling the screen
Center:setupScreen(w, h)
ScreenScaler.width, ScreenScaler.height = w, h
end
--- Unsets virtual dimensions. Effectively disables scaler.
function ScreenScaler.unsetVirtualDimensions()
ScreenScaler.width = nil
ScreenScaler.height = nil
end
--- Get virtual dimensions.
-- @return width, height
function ScreenScaler.getVirtualDimensions()
return ScreenScaler.width, ScreenScaler.height
end
--- Returns true if scaler is enabled.
-- @treturn boolean
function ScreenScaler.isEnabled()
return ScreenScaler.width ~= nil
end
-- Overwrite default love.mouse functions to return position
-- relative to scaled window
do
local getX = love.mouse.getX
function love.mouse.getX()
local x = getX()
if not ScreenScaler.isEnabled() then
return x
end
return (x - Center:getOffsetX()) / Center:getScale()
end
local getY = love.mouse.getY
function love.mouse.getY()
local y = getY()
if not ScreenScaler.isEnabled() then
return y
end
return (y - Center:getOffsetY()) / Center:getScale()
end
local getPosition = love.mouse.getPosition
function love.mouse.getPosition()
if ScreenScaler.isEnabled() then
return Center:toGame(getPosition())
else
return getPosition()
end
end
-- TODO: add replacements for setX, setY, setPosition
end
-- Overwrite default getDimensions, getWidth, getHeight
-- to return virtual width and height if scaler is enabled
do
local getWidth = love.graphics.getWidth
function love.graphics.getWidth()
return ScreenScaler.width or getWidth()
end
local getHeight = love.graphics.getHeight
function love.graphics.getHeight()
return ScreenScaler.height or getHeight()
end
function love.graphics.getDimensions()
return love.graphics.getWidth(), love.graphics.getHeight()
end
end
-- Adjust setScissor and intersectScissor function, so that they are relative
-- to the scaled screen. By default these functions are unaffected by transformations
do
local setScissor = love.graphics.setScissor
function love.graphics.setScissor(x, y, w, h)
if x and ScreenScaler.isEnabled() then
setScissor(Center:toGame(x, y, w, h))
else
setScissor(x, y, w, h)
end
end
local intersectScissor = love.graphics.intersectScissor
function love.graphics.intersectScissor(x, y, w, h)
if x and ScreenScaler.isEnabled() then
intersectScissor(Center:toGame(x, y, w, h))
else
intersectScissor(x, y, w, h)
end
end
end
local function isInBounds(x, y)
if not ScreenScaler.isEnabled() then return true end
local w, h = ScreenScaler.getVirtualDimensions()
return x >= 0 and x < w and y >= 0 and y < h
end
-- Create event proccessors for converting normal screen coordinates
-- to scaled screen coordinates
-- If the user clicked out of bounds, it will not handled
local eventPreProccessor = {}
function eventPreProccessor.mousepressed(x, y, button, istouch, presses)
x, y = Center:toGame(x, y)
return isInBounds(x, y), x, y, button, istouch, presses
end
function eventPreProccessor.mousereleased(x, y, button, istouch, presses)
x, y = Center:toGame(x, y)
return isInBounds(x, y), x, y, button, istouch, presses
end
function eventPreProccessor.mousemoved(x, y, dx, dy, istouch)
local scale = Center:getScale()
x, y = Center:toGame(x, y)
dx, dy = dx / scale, dy / scale
return isInBounds(x, y), x, y, dx, dy, istouch, istouch
end
function eventPreProccessor.wheelmoved(x, y)
return isInBounds(love.mouse.getPosition()), x, y
end
local function hideOutOfBounds()
local r, g, b, a = love.graphics.getColor()
love.graphics.setColor(0, 0, 0, 1)
local w, h = getRealDimensions()
if Center._OFFSET_X ~= 0 then
love.graphics.rectangle("fill", 0, 0, Center._OFFSET_X, h)
love.graphics.rectangle("fill", Center._WIDTH*Center._SCALE+Center._OFFSET_X, 0, Center._OFFSET_X, h)
end
if Center._OFFSET_Y ~= 0 then
love.graphics.rectangle("fill", 0, 0, w, Center._OFFSET_Y)
love.graphics.rectangle("fill", 0, Center._HEIGHT*Center._SCALE+Center._OFFSET_Y, w, Center._OFFSET_Y)
end
love.graphics.setColor(r, g, b, a)
end
-- Modify core game loop so that if scaler is enabled:
-- * resize events are not handled
-- * out of bounds mouse events are not handled
-- * all drawing operations are centered
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
-- We don't want the first frame's dt to include time taken by love.load.
if love.timer then love.timer.step() end
local dt = 0
-- Main loop time.
return function()
-- Process events.
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
return a or 0
end
end
if ScreenScaler.isEnabled() then
if name == "resize" then
Center:resize(a, b)
goto continue
elseif eventPreProccessor[name] then
local success
success, a, b, c, d, e, f = eventPreProccessor[name](a, b, c, d, e, f)
if not success then goto continue end
end
end
love.handlers[name](a, b, c, d, e, f)
::continue::
end
end
-- Update dt, as we'll be passing it to update
if love.timer then dt = love.timer.step() end
-- Call update and draw
if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled
if love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then
if ScreenScaler.isEnabled() then
Center:start()
love.draw()
Center:finish()
else
love.draw()
end
end
love.graphics.present()
end
if love.timer then love.timer.sleep(0.001) end
end
end
return ScreenScaler

View File

@ -1,70 +0,0 @@
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 function getPublicIP()
local ip = http.request("https://ipinfo.io/ip")
return ip
end
local function splitat(str, char)
local index = str:find(char)
return str:sub(1, index-1), str:sub(index+1)
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))},
}
self.addr_textbox = { text = "" }
self.host_socket = enet.host_create("*:0")
self.peer_socket = nil
self.hosting = false
self.connecting = false
end
function MainMenu:update()
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:textinput(...)
self.ui:textinput(...)
end
function MainMenu:draw()
local w, h = love.graphics.getDimensions()
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

View File

@ -1,123 +1,464 @@
local MainState = {}
local pprint = require("lib.pprint")
local nata = require("lib.nata")
local data = require("data")
local Player = require("player")
local Bolt = require("bolt")
local lerp = require("lib.lume").lerp
local rgb = require("helpers.rgb")
local Vec = require("lib.brinevector")
local bf = require("lib.breezefield")
local ControlsManager = require("controls-manager")
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"}}
}
-- TODO: Use hump.signal, for emitting events to bolt mods
local systems = {
require("systems.physics"),
require("systems.map"),
require("systems.sprite"),
require("systems.player"),
require("systems.bolt"),
require("systems.screen-scaler")
}
-- TODO: Mod idea: Experiment making bolts held by player interact with environment?
-- Like the hammer from "Getting Over It"
if not love.filesystem.isFused() then
table.insert(systems, require("systems.debug"))
end
-- TODO: Mod idea: Bolts have large recoil that you can use to move around quickly
if host_socket then
table.insert(systems, require("systems.multiplayer"))
end
-- TODO: Should bolts be allowed to be reloaded early?
self.ecs = nata.new{
groups = groups,
systems = systems,
data = {
host_socket = host_socket
}
}
-- TODO: Should a player be able to pickup enemy bolts?
self.downscaled_canvas = nil
self:refreshDownscaledCanvas()
self.ecs:on("onMapSwitch", function(map)
self:refreshDownscaledCanvas(map)
end)
love.graphics.setNewFont(48)
-- TODO: Somehow display bolt shoot cooldown
-- TODO: Can a bolt mod have multiple variants/tiers
-- TODO: If you have a multiples of the same mod, should they stack?
local DRAW_COLLIDERS = false
local SHOOT_COOLDOWN = 1
local FULL_HEALTH_COLOR = rgb(20, 200, 20)
local EMPTY_HEALTH_COLOR = rgb(180, 20, 20)
local function get_hold_distance(bolt)
return Player.PLAYER_SIZE + bolt:get_size() + 8
end
function MainState:refreshDownscaledCanvas(map)
if not map then
local Map = self.ecs:getSystem(require("systems.map"))
map = Map.map
if not map then
self.downscaled_canvas = nil
local function remove_by_value(array, value)
for i, v in ipairs(array) do
if v == value then
table.remove(array, i)
return
end
end
end
self.downscaled_canvas = love.graphics.newCanvas(
map.width * map.tilewidth,
map.height * map.tileheight
)
self.downscaled_canvas:setFilter("nearest", "nearest")
function MainState:stop_match()
while #self.bolts > 0 do
self:destroy_bolt(self.bolts[1])
end
for _, player in ipairs(self.players) do
self:destroy_player(player)
end
end
function MainState:start_match()
self:stop_match()
self:create_player("keyboard", Vec(60, 80))
self:create_player(nil, Vec(600, 300))
end
function MainState:destroy_player(player)
remove_by_value(self.players, player)
self.world:removeCollider(player.collider)
end
function MainState:destroy_bolt(bolt)
remove_by_value(self.bolts, bolt)
if bolt.collider then
self.world:removeCollider(bolt.collider)
end
end
local function sort_bolt_mods(bolt_mods)
table.sort(bolt_mods, function(a, b)
return BoltMods.priority[a] < BoltMods.priority[b]
end)
end
function MainState:create_player(controls, pos)
local player = Player.new(controls, self.world, pos)
player.bolt_mods = {
-- BoltMods.mods.growth,
-- BoltMods.mods.speed_bounce,
-- BoltMods.mods.bouncy,
-- BoltMods.mods.retain_speed,
}
sort_bolt_mods(player.bolt_mods)
player.held_bolt = self:create_bolt()
player.held_bolt.owner = player
table.insert(self.players, player)
end
local function lerp_color(a, b, t)
return lerp(a[1], b[1], t), lerp(a[2], b[2], t), lerp(a[3], b[3], t)
end
function MainState:create_bolt()
local bolt = Bolt.new(self.world)
table.insert(self.bolts, bolt)
return bolt
end
function MainState:find_bolt_by_player(player)
for _, bolt in ipairs(self.bolts) do
if bolt.owner == player then
return bolt
end
end
end
function MainState:player_shoot_bolt(player)
if not player.held_bolt then return end
local now = love.timer.getTime()
if player.last_shot_time then
if now - player.last_shot_time < SHOOT_COOLDOWN then return end
end
local bolt = player.held_bolt
local aim_dir = ControlsManager.get_aim_dir(player)
local DEFAULT_VELOCITY = 500
bolt.shot_at = now
bolt.active = true
bolt:set_collisions(true)
bolt:update_velocity(aim_dir.x * DEFAULT_VELOCITY, aim_dir.y * DEFAULT_VELOCITY)
player.last_shot_time = now
player.held_bolt = nil
-- TODO:
--[[
for _, mod in ipairs(player.bolt_mods) do
if mod.on_shoot then
mod.on_shoot(bolt, player)
end
end
if bolt.scale ~= 1 then
bolt.collider:setRadius(BOLT_SIZE * bolt.scale)
end
]]--
end
function MainState:player_reload_bolt(player)
if player.held_bolt then return end
local bolt = self:find_bolt_by_player(player)
if not bolt then return end
self:destroy_bolt(bolt)
player.held_bolt = self:create_bolt()
player.held_bolt.owner = player
end
function MainState:is_bolt_being_held(bolt)
for _, player in ipairs(self.players) do
if player.held_bolt == bolt then
return true
end
end
return false
end
function MainState:pickup_nearby_bolt(player)
local nearest_bolt, nearest_dist
do -- find nearest pickupable bolt
for _, bolt in ipairs(self.bolts) do
if not bolt.active and bolt.pickupable and not self:is_bolt_being_held(bolt) then
local dist = (bolt.pos - player.pos).length
if not nearest_bolt or dist < nearest_dist then
nearest_bolt = bolt
nearest_dist = dist
end
end
end
end
if not nearest_bolt then return end
if nearest_dist < get_hold_distance(nearest_bolt) then
if player.held_bolt then
self:destroy_bolt(player.held_bolt)
end
player.held_bolt = nearest_bolt
nearest_bolt:set_collisions(false)
end
end
function MainState:get_bolt_by_collider(collider)
for _, bolt in ipairs(self.bolts) do
if bolt.collider and bolt.collider.fixture == collider then
return bolt
end
end
end
function MainState:get_player_by_collider(collider)
for _, player in ipairs(self.players) do
if player.collider.fixture == collider then
return player
end
end
end
function MainState:on_bolt_touch_obstacle(bolt)
local should_stop = true
-- TODO:
--[[
local player = bolt.owner
for _, mod in ipairs(player.bolt_mods) do
-- if mod.on_touch_obstacle then
-- mod.on_touch_obstacle(bolt, player)
-- end
if mod.dont_stop_on_touch_obstacle then
should_stop = false
end
end
]]--
if should_stop then
bolt.active = false
end
end
function MainState:on_player_touch_bolt(player, bolt)
player.health = player.health - 1
self:destroy_bolt(bolt)
local shot_by = bolt.owner
shot_by.held_bolt = self:create_bolt()
shot_by.held_bolt.owner = shot_by
end
function MainState:on_begin_contact(a, b)
local a_data = a:getUserData()
local b_data = b:getUserData()
-- Make ordering consistent
if a_data < b_data then
local c = a
local c_data = a_data
a = b
a_data = b_data
b = c
b_data = c_data
end
if a_data == "player" and b_data == "bolt" then
self:on_player_touch_bolt(self:get_player_by_collider(a), self:get_bolt_by_collider(b))
elseif a_data == "obstacle" and b_data == "bolt" then
self:on_bolt_touch_obstacle(self:get_bolt_by_collider(b))
end
end
function MainState:enter()
love.graphics.setNewFont(72)
love.mouse.setVisible(false)
self.world = bf.newWorld(0, 0, true)
self.world:setCallbacks(function(...) self:on_begin_contact(...) end)
self.players = {}
self.bolts = {}
self.obstacles = {}
do
table.insert(self.obstacles, {{77, 2}, {6, 60}, {5, 5}})
table.insert(self.obstacles, {{576, 357}, {639, 314}, {639, 357}})
table.insert(self.obstacles, {{275, 101}, {395, 101}, {392, 55}, {281, 60}})
table.insert(self.obstacles, {{271, 280}, {392, 277}, {392, 322}, {252, 320}})
table.insert(self.obstacles, {{222, 115}, {248, 145}, {221, 176}, {192, 151}})
table.insert(self.obstacles, {{435, 112}, {401, 142}, {426, 174}, {457, 142}})
table.insert(self.obstacles, {{194, 229}, {246, 260}, {192, 262}})
table.insert(self.obstacles, {{417, 260}, {466, 258}, {465, 226}})
table.insert(self.obstacles, {{141, 148}, {136, 249}, {94, 246}, {105, 149}})
table.insert(self.obstacles, {{216, 318}, {216, 359}, {134, 358}})
table.insert(self.obstacles, {{435, 0}, {434, 52}, {541, 5}})
table.insert(self.obstacles, {{507, 142}, {505, 247}, {554, 246}, {553, 138}})
table.insert(self.obstacles, {{124, 104}, {219, 34}, {230, 64}, {166, 111}})
table.insert(self.obstacles, {{429, 337}, {527, 282}, {504, 274}, {432, 312}})
table.insert(self.obstacles, {{63, 292}, {78, 314}, {40, 315}})
table.insert(self.obstacles, {{588, 92}, {565, 65}, {616, 66}})
end
self.obstacle_colliders = {}
for _, obstacle in ipairs(self.obstacles) do
local points = {}
for _, p in ipairs(obstacle) do
table.insert(points, p[1])
table.insert(points, p[2])
end
local collider = self.world:newCollider("Polygon", points)
collider:setType("static")
collider.fixture:setUserData("obstacle")
table.insert(self.obstacle_colliders, collider)
end
do
local winw, winh = love.graphics.getDimensions()
local edges = {
{ 0, 0, winw, 0 }, -- top
{ 0, 0, 0, winh }, -- left
{ winw, 0, winw, winh }, -- right
{ 0, winh, winw, winh } -- bottom
}
for _, edge in ipairs(edges) do
local collider = self.world:newCollider("Edge", edge)
collider.fixture:setUserData("obstacle")
collider:setType("static")
end
end
self:start_match()
end
function MainState:update(dt)
self.ecs:flush()
self.ecs:emit("update", dt)
if #self.players == 1 then
-- TODO: Use hump.timer instead of manually keeping track of timers
self.victory_timer = (self.victory_timer or 0) + dt
if self.victory_timer > 1 then
self.victory_timer = nil
self:start_match()
end
return
end
if love.keyboard.isDown("escape") then
local now = love.timer.getTime()
for _, bolt in ipairs(self.bolts) do
-- TODO: use hump.signal
-- for _, mod in ipairs(bolt.shot_by.bolt_mods) do
-- if mod.on_update then
-- mod.on_update(bolt, dt)
-- end
-- end
if bolt.active then
local lifetime = now - bolt.shot_at
if lifetime > bolt.max_lifetime then
bolt.active = false
end
end
if not bolt.active then
local dampening = 0.75
bolt:update_velocity(bolt.vel.x * dampening, bolt.vel.y * dampening)
end
end
for _, player in ipairs(self.players) do
if ControlsManager.is_shoot_down(player) then
self:player_shoot_bolt(player)
end
if ControlsManager.is_reload_down(player) then
self:player_reload_bolt(player)
end
local move_dir = ControlsManager.get_move_dir(player)
local acc = move_dir * 1700
player.vel = player.vel + acc * dt
player.vel = player.vel * 0.8
player.collider:setLinearVelocity(player.vel.x, player.vel.y)
if not player.held_bolt then
self:pickup_nearby_bolt(player)
end
local was_dead = player.dead
if player.health <= 0 then
player.dead = true
end
if not was_dead and player.dead then
self:destroy_player(player)
end
end
do -- update objects based on physics
self.world:update(dt)
for _, bolt in ipairs(self.bolts) do
bolt:update_physics(dt)
end
for _, player in ipairs(self.players) do
local collider = player.collider
player.pos.x = collider:getX()
player.pos.y = collider:getY()
local velx, vely = collider:getLinearVelocity()
player.vel.x = velx
player.vel.y = vely
if player.held_bolt then
local bolt = player.held_bolt
local aim_dir = ControlsManager.get_aim_dir(player)
local hold_dist = get_hold_distance(bolt)
local pos = player.pos + aim_dir * hold_dist
bolt:update_position(pos.x, pos.y)
end
end
end
end
function MainState:keypressed(key)
if key == "escape" then
love.event.quit()
end
end
function MainState:cleanup()
local multiplayer = self.ecs:getSystem(require("systems.multiplayer"))
if multiplayer then
multiplayer:disconnectPeers()
function MainState:draw()
for _, player in ipairs(self.players) do
love.graphics.setLineWidth(4)
local health_percent = player.health / Player.MAX_HEALTH
love.graphics.setColor(lerp_color(EMPTY_HEALTH_COLOR, FULL_HEALTH_COLOR, health_percent))
love.graphics.circle("fill", player.pos.x, player.pos.y, Player.PLAYER_SIZE)
love.graphics.setColor(1, 1, 1)
love.graphics.circle("line", player.pos.x, player.pos.y, Player.PLAYER_SIZE)
end
love.graphics.setLineWidth(3)
for _, bolt in ipairs(self.bolts) do
if bolt.owner and bolt.owner.held_bolt == bolt then
bolt:draw_active()
else
bolt:draw()
end
end
love.graphics.setLineWidth(3)
love.graphics.setColor(0.33, 0.33, 0.33)
for _, obstacle in ipairs(self.obstacles) do
local n = #obstacle
love.graphics.line(obstacle[1][1], obstacle[1][2], obstacle[n][1], obstacle[n][2])
for i=1, n-1 do
love.graphics.line(obstacle[i][1], obstacle[i][2], obstacle[i+1][1], obstacle[i+1][2])
end
end
do -- cursor
local cursor_size = 10
local mx, my = love.mouse.getPosition()
love.graphics.setColor(1, 1, 1)
love.graphics.line(mx-cursor_size, my, mx+cursor_size, my)
love.graphics.line(mx, my-cursor_size, mx, my+cursor_size)
end
if DRAW_COLLIDERS then
love.graphics.setColor(0.8, 0.1, 0.8)
self.world:draw()
end
if #self.players == 1 then
love.graphics.setColor(0.2, 1, 0.2)
love.graphics.print("Victory!", 150, 100)
end
end
function MainState:quit()
self:cleanup()
end
function MainState:mousemoved(x, y, dx, dy, istouch)
local ScreenScaler = self.ecs:getSystem(require("systems.screen-scaler"))
if not ScreenScaler:isInBounds(x, y) then return end
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 MainState:keypressed(...)
self.ecs:emit("keypressed", ...)
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

95
src/states/map-editor.lua Normal file
View File

@ -0,0 +1,95 @@
local MapEditor = {}
function MapEditor:enter()
self.obstacles = {}
self.current_obstacle = {}
self:from_clipboard()
end
function MapEditor:keypressed(key)
if key == "escape" then
love.event.quit()
self:refresh_clipboard()
end
end
function MapEditor:mousepressed(mx, my)
mx = math.floor(mx)
my = math.floor(my)
if self.current_obstacle[1] then
local origin_x, origin_y = self.current_obstacle[1][1], self.current_obstacle[1][2]
local dist = ((origin_x-mx)^2 + (origin_y-my)^2)^0.5
if dist < 10 then
table.insert(self.obstacles, self.current_obstacle)
self.current_obstacle = {}
self:update_clipboard()
return
end
end
table.insert(self.current_obstacle, {mx, my})
end
function MapEditor:update_clipboard()
local lines = {}
for _, obstacle in ipairs(self.obstacles) do
local points = {}
for _, point in ipairs(obstacle) do
table.insert(points, ("{%d, %d}"):format(point[1], point[2]))
end
table.insert(lines, ("table.insert(self.obstacles, {%s})"):format(table.concat(points, ", ")))
end
local text = table.concat(lines, "\n")
love.system.setClipboardText(text)
end
function MapEditor:from_clipboard()
self.obstacles = {}
self.current_obstacle = {}
local text = love.system.getClipboardText()
for line in text:gmatch("%s*([^\n]+)%s*") do
local points = line:match("^table%.insert%(self%.obstacles, {(.+)}%)$")
if points then
local obstacle = {}
table.insert(self.obstacles, obstacle)
for x, y in points:gmatch("{(%d+),%s*(%d+)}") do
table.insert(obstacle, {tonumber(x), tonumber(y)})
end
end
end
end
function MapEditor:draw_obstacle(obstacle)
local n = #obstacle
if n >= 2 then
love.graphics.line(obstacle[1][1], obstacle[1][2], obstacle[n][1], obstacle[n][2])
for i=1, n-1 do
love.graphics.line(obstacle[i][1], obstacle[i][2], obstacle[i+1][1], obstacle[i+1][2])
end
end
end
function MapEditor:draw()
for _, obstacle in ipairs(self.obstacles) do
self:draw_obstacle(obstacle)
end
do
local obstacle = self.current_obstacle
local n = #self.current_obstacle
for i=1, n-1 do
love.graphics.line(obstacle[i][1], obstacle[i][2], obstacle[i+1][1], obstacle[i+1][2])
end
if n > 0 then
local mx, my = love.mouse.getPosition()
mx = math.floor(mx)
my = math.floor(my)
local last_point = self.current_obstacle[n]
love.graphics.line(last_point[1], last_point[2], mx, my)
end
end
end
return MapEditor

View File

@ -1,89 +0,0 @@
local Bolt = {}
local Vec = require("lib.brinevector")
-- BUG: trajectory projection doesn't take into account the bolt collider
local BOLT_COLLIDER = {-3, -3, 3, 3}
function Bolt:update(dt)
for _, bolt in ipairs(self.pool.groups.bolt.entities) do
if bolt.vel.length < 20 and not bolt.pickupable then
bolt.pickupable = true
bolt.sprite.variant = "idle"
bolt.vel.x = 0
bolt.vel.y = 0
end
end
end
function Bolt:projectTrajectory(pos, step, count)
local physics = self.pool:getSystem(require("systems.physics"))
local step_size = step.length
local distance = step_size * (count-1)
local key_points = physics:project(pos, step.normalized, distance)
local trajectory = {}
local current_segment = 1
local current_distance = -step_size
local segment_direction, segment_size
local last_point = key_points[1] - step
do
local diff = key_points[2] - key_points[1]
segment_size = diff.length
segment_direction = diff.normalized
end
-- For every point that we want to place along our projected line
for _=1, count do
-- First check if the point about to be placed would be outside current segment
while current_distance+step_size > segment_size do
-- Check if there are any unused segments left
if current_segment < (#key_points-1) then
-- Switch next segment
current_distance = current_distance - segment_size
current_segment = current_segment + 1
local diff = key_points[current_segment+1] - key_points[current_segment]
segment_size = diff.length
segment_direction = diff.normalized
-- Adjust where the next point is going to be placed, so it's correct
local to_key_point = (key_points[current_segment]-last_point).length
last_point = key_points[current_segment] - segment_direction * to_key_point
-- If there are not segments left, just treat the last segment as infinite
else
segment_size = math.huge
end
end
-- Place point along segment
local point = last_point + segment_direction * step_size
table.insert(trajectory, point)
current_distance = current_distance + step_size
last_point = point
end
return trajectory
end
function Bolt:boltShot(player, bolt)
bolt.pickupable = nil
bolt.collider = BOLT_COLLIDER
bolt.bolt = true
bolt.sprite.variant = "active"
bolt.hidden = nil
self.pool:queue(bolt)
end
function Bolt:storeBolt(player, bolt)
bolt.pickupable = nil
bolt.collider = nil
bolt.bolt = nil
bolt.sprite.variant = "idle"
bolt.hidden = true
self.pool:queue(bolt)
end
return Bolt

View File

@ -1,84 +0,0 @@
local Debug = {}
local rgb = require("helpers.rgb")
local DRAW_GRID = false
local GRID_COLOR = rgb(30, 30, 30)
local DRAW_COLLIDERS = false
local COLLIDER_COLOR = rgb(200, 20, 200)
local DRAW_PING = false
function Debug:init()
self.current_player = 1
end
function Debug:drawColliders()
local physics = self.pool:getSystem(require("systems.physics"))
love.graphics.setColor(COLLIDER_COLOR)
local bump = physics.bump
local items = bump:getItems()
for _, item in ipairs(items) do
love.graphics.rectangle("line", bump:getRect(item))
end
end
function Debug:drawGrid()
local map = self.pool:getSystem(require("systems.map"))
if not map.map then return end
local scaler = self.pool:getSystem(require("systems.screen-scaler"))
local w, h = scaler:getDimensions()
love.graphics.setColor(GRID_COLOR)
for x=0, w, map.map.tilewidth do
love.graphics.line(x, 0, x, h)
end
for y=0, h, map.map.tileheight do
love.graphics.line(0, y, w, y)
end
end
function Debug:keypressed(key)
if key == "e" and love.keyboard.isDown("lshift") then
local PlayerSystem = self.pool:getSystem(require("systems.player"))
local player = PlayerSystem:spawnPlayer()
for _, e in ipairs(self.pool.groups.controllable_player.entities) do
e.controllable = false
self.pool:queue(e)
end
player.controllable = true
self.pool:queue(player)
self.current_player = #self.pool.groups.player.entities+1
elseif key == "tab" then
local player_entities = self.pool.groups.player.entities
player_entities[self.current_player].controllable = false
self.pool:queue(player_entities[self.current_player])
self.current_player = self.current_player % #player_entities + 1
player_entities[self.current_player].controllable = true
self.pool:queue(player_entities[self.current_player])
end
end
function Debug:draw()
if DRAW_GRID then
self:drawGrid()
end
if DRAW_COLLIDERS then
self:drawColliders()
end
if DRAW_PING and self.pool.data.host_socket then
local host_socket = self.pool.data.host_socket
local height = love.graphics.getFont():getHeight()
for _, index in ipairs(self.connected_peers) do
local peer = host_socket:get_peer(index)
love.graphics.print(peer:round_trip_time(), 0, (index-1)*height)
end
end
end
return Debug

View File

@ -1,27 +0,0 @@
local Map = {}
local sti = require("lib.sti")
local Vector = require("lib.brinevector")
function Map:init()
self.map = sti("data/maps/playground.lua", { "bump" })
self.pool:emit("onMapSwitch", self.map)
end
function Map:update(dt)
self.map:update(dt)
end
function Map:listSpawnpoints()
local points = {}
for _, object in ipairs(self.map.layers.spawnpoints.objects) do
table.insert(points, Vector(object.x, object.y))
end
return points
end
function Map:draw()
love.graphics.setColor(1, 1, 1)
self.map:draw()
end
return Map

View File

@ -1,151 +0,0 @@
local Multiplayer = {}
local binser = require("lib.binser")
local pprint = require("lib.pprint")
local uid = require("helpers.uid")
local RATE_LIMIT = 20
local CMD = {
SPAWN_PLAYER = 1,
MOVE_PLAYER = 2,
AIM_PLAYER = 3,
PLAYER_SHOT = 4,
}
local function removeValue(t, v)
for i, vv in ipairs(t) do
if v == vv then
table.remove(t, i)
end
end
end
function Multiplayer:init()
self.connected_peers = {}
local host_socket = self.pool.data.host_socket
for i=1, host_socket:peer_count() do
local peer = host_socket:get_peer(i)
if peer:state() == "connected" then
self:onPeerConnect(peer)
end
end
end
function Multiplayer:onPeerConnect(peer)
table.insert(self.connected_peers, peer:index())
end
function Multiplayer:onPeerDisconnect(peer)
local peer_index = peer:index()
removeValue(self.connected_peers, peer_index)
for _, player in ipairs(self.pool.groups.player.entities) do
if player.peer_index == peer_index then
self.pool:removeEntity(player)
end
end
end
function Multiplayer:disconnectPeers()
local host_socket = self.pool.data.host_socket
for _, index in ipairs(self.connected_peers) do
local peer = host_socket:get_peer(index)
peer:disconnect_now()
self:onPeerDisconnect(peer)
end
host_socket:flush()
end
function Multiplayer:sendToPeer(peer, ...)
peer:send(binser.serialize(...), nil, "unreliable")
end
function Multiplayer:sendToAllPeers(...)
local payload = binser.serialize(...)
local host_socket = self.pool.data.host_socket
for _, index in ipairs(self.connected_peers) do
local peer = host_socket:get_peer(index)
peer:send(payload, nil, "unreliable")
end
end
function Multiplayer:update()
local host_socket = self.pool.data.host_socket
local event = host_socket:service()
if event then
if event.type == "connect" then
self:onPeerConnect(event.peer)
elseif event.type == "disconnect" then
self:onPeerDisconnect(event.peer)
elseif event.type == "receive" then
local parameters = binser.deserialize(event.data)
self:handlePeerMessage(event.peer, unpack(parameters))
end
end
end
function Multiplayer:getPlayerEntityById(id)
for _, player in ipairs(self.pool.groups.player.entities) do
if player.id == id then
return player
end
end
end
function Multiplayer:handlePeerMessage(peer, cmd, ...)
if cmd == CMD.SPAWN_PLAYER then
local id = ...
local PlayerSystem = self.pool:getSystem(require("systems.player"))
local player = PlayerSystem:spawnPlayer()
player.id = id
player.peer_index = peer:index()
elseif cmd == CMD.MOVE_PLAYER then
local id, move_dir, pos = ...
local player = self:getPlayerEntityById(id)
if player and player.peer_index == peer:index() then
player.move_dir = move_dir
player.pos = pos
end
elseif cmd == CMD.AIM_PLAYER then
local id, aim_dir = ...
local player = self:getPlayerEntityById(id)
if player and player.peer_index == peer:index() then
player.aim_dir = aim_dir
end
elseif cmd == CMD.PLAYER_SHOT then
local id = ...
local player = self:getPlayerEntityById(id)
if player and player.peer_index == peer:index() then
self.pool:emit("tryShootingBolt", player)
end
else
print("DEBUG INFORMATION:")
print("Message:")
pprint{cmd, ...}
error("Unhandled message from peer")
end
end
function Multiplayer:addToGroup(group, player)
if group == "player" and not player.peer_index then
player.id = uid()
self:sendToAllPeers(CMD.SPAWN_PLAYER, player.id)
self.pool:queue(player)
end
end
function Multiplayer:playerMoved(player)
self:sendToAllPeers(CMD.MOVE_PLAYER, player.id, player.move_dir, player.pos)
end
function Multiplayer:playerAimed(player)
self:sendToAllPeers(CMD.AIM_PLAYER, player.id, player.aim_dir)
end
function Multiplayer:playerShot(player)
self:sendToAllPeers(CMD.PLAYER_SHOT, player.id)
end
return Multiplayer

View File

@ -1,166 +0,0 @@
local Physics = {}
local bump = require("lib.bump")
local pprint = require("lib.pprint")
local Vec = require("lib.brinevector")
-- TODO: Tweak bump world `cellSize` at runtime, when the map switches
function Physics:init()
self.bump = bump.newWorld()
self.boundCollisionFilter = function(...)
return self:collisionFilter(...)
end
end
function Physics:onMapSwitch(map)
map:bump_init(self.bump)
end
local function getColliderBounds(e)
local x = e.pos.x + e.collider[1]
local y = e.pos.y + e.collider[2]
local w = math.abs(e.collider[3] - e.collider[1])
local h = math.abs(e.collider[4] - e.collider[2])
return x, y, w, h
end
function Physics:addToGroup(group, e)
if group == "collider" then
self.bump:add(e, getColliderBounds(e))
end
end
function Physics:removeFromGroup(group, e)
if group == "collider" then
self.bump:remove(e)
end
end
function Physics:collisionFilter(entity, other)
if entity.hidden then return end
if other.hidden then return end
local bolt = entity.bolt or other.bolt
local vel = entity.vel or other.vel
local player = entity.bolts or other.bolts
if bolt and player then
return "cross"
elseif bolt then
return "bounce"
elseif (vel or vel) and not (entity.bolts and other.bolts) then
return "slide"
end
end
function Physics:resolveCollisions(e, dt)
local targetPos = e.pos + e.vel * dt
local ox = e.collider[1]
local oy = e.collider[2]
self.bump:update(e, e.pos.x+ox, e.pos.y+oy)
local x, y, cols = self.bump:move(e, targetPos.x+ox, targetPos.y+oy, self.boundCollisionFilter)
local skip_moving = false
if #cols > 0 then
e.pos.x = x - ox
e.pos.y = y - oy
skip_moving = true
end
for _, col in ipairs(cols) do
if col.type == "cross" then
local player = col.other.bolts and col.other or e
local bolt = col.other.bolt and col.other or e
if player and bolt and bolt.owner ~= player then
self.pool:emit("hitPlayer", player, bolt)
end
elseif col.type == "bounce" then
-- sx and sy and just number for flipped the direction when something
-- bounces.
-- When `normal.x` is zero, sx = 1, otherwise sx = -1. Same with sy
local sx = 1 - 2*math.abs(col.normal.x)
local sy = 1 - 2*math.abs(col.normal.y)
e.vel.x = e.vel.x * sx
e.vel.y = e.vel.y * sy
if e.acc then
e.acc.x = e.acc.x * sx
e.acc.y = e.acc.y * sy
end
skip_moving = true
end
end
return skip_moving
end
function Physics:update(dt)
for _, e in ipairs(self.pool.groups.physical.entities) do
if e.acc then
e.vel = e.vel + e.acc * dt
end
if e.friction then
e.vel = e.vel * (1 - math.min(e.friction, 1)) ^ dt
end
local skip_moving = false
if self.pool.groups.collider.hasEntity[e] then
skip_moving = self:resolveCollisions(e, dt)
end
if not skip_moving then
if e.max_speed then
e.pos = e.pos + e.vel:trim(e.max_speed) * dt
else
e.pos = e.pos + e.vel * dt
end
end
end
end
function Physics:castRay(from, to)
local items = self.bump:querySegmentWithCoords(from.x, from.y, to.x, to.y, function(item)
return not item.vel
end)
for _, item in ipairs(items) do
if item.nx ~= 0 or item.ny ~= 0 then
local pos = Vec(item.x1, item.y1)
local normal = Vec(item.nx, item.ny)
return pos, normal
end
end
end
function Physics:project(from, direction, distance)
local key_points = {}
table.insert(key_points, from)
local distance_traveled = 0
local position = from
-- While the ray hasen't traveled the required amount
while math.abs(distance_traveled - distance) > 0.01 do
-- Check if it collides with anything
local destination = position + direction * (distance-distance_traveled)
local intersect, normal = self:castRay(position, destination)
local new_position = intersect or destination
-- Update how much the ray has moved in total
distance_traveled = distance_traveled + (position - new_position).length
position = new_position
-- If it collided change its travel direction
if normal then
-- sx and sy and just number for flipped the direction when something
-- bounces.
-- When `normal.x` is zero, sx = 1, otherwise sx = -1. Same with sy
direction.x = direction.x * (1 - 2*math.abs(normal.x))
direction.y = direction.y * (1 - 2*math.abs(normal.y))
end
-- Save this point where it stopped/collided
table.insert(key_points, new_position)
end
return key_points
end
return Physics

View File

@ -1,249 +0,0 @@
local Player = {}
local data = require("data")
local Vec = require("lib.brinevector")
local controls = data.controls
local AIMED_BOLT_DISTANCE = 15
local BOLT_AMOUNT = 1
local function getDirection(up_key, down_key, left_key, right_key)
local dx = (love.keyboard.isDown(right_key) and 1 or 0)
- (love.keyboard.isDown(left_key) and 1 or 0)
local dy = (love.keyboard.isDown(down_key) and 1 or 0)
- (love.keyboard.isDown(up_key) and 1 or 0)
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
function Player:getMoveDirection()
return getDirection(
controls.move_up,
controls.move_down,
controls.move_left,
controls.move_right
)
end
function Player:getAimDirection(player)
if self.use_mouse_aim then
local ScreenScaler = self.pool:getSystem(require("systems.screen-scaler"))
local dir = Vec(ScreenScaler:getMousePosition()) - player.pos
return dir.normalized
-- local MAX_DIRECTIONS = 8
-- local angle_segment = math.pi*2/MAX_DIRECTIONS
-- local new_angle = math.floor(dir.angle/angle_segment+0.5)*angle_segment
-- return Vec(math.cos(new_angle), math.sin(new_angle))
else
return getDirection(
controls.aim_up,
controls.aim_down,
controls.aim_left,
controls.aim_right
)
end
end
function Player:addToGroup(group, e)
if group == "player" then
e.bolt_cooldown_timer = e.bolt_cooldown_timer or 0
e.aim_dir = e.aim_dir or Vec()
e.move_dir = e.move_dir or Vec()
end
end
function Player:update(dt)
if love.keyboard.isDown(controls.aim_up, controls.aim_down, controls.aim_left, controls.aim_right) then
self.use_mouse_aim = false
end
-- Update controls for "my" players
local move_direction = self:getMoveDirection()
for _, e in ipairs(self.pool.groups.controllable_player.entities) do
-- Update where they are aiming/looking
local aim_direction = self:getAimDirection(e)
if e.aim_dir ~= aim_direction then
e.aim_dir = aim_direction
self.pool:emit("playerAimed", e)
end
-- Update acceleration to make the player move
if e.move_dir ~= move_direction then
e.move_dir = move_direction
self.pool:emit("playerMoved", e)
end
-- Check if the player tried to shoot a bolt
if love.keyboard.isDown(controls.shoot) then
if self:tryShootingBolt(e) then
self.pool:emit("playerShot", e)
end
end
end
-- Update each player
for _, e in ipairs(self.pool.groups.player.entities) do
e.acc = e.move_dir * e.speed
-- Decrease bolt cooldown timer
if e.bolt_cooldown_timer > 0 then
e.bolt_cooldown_timer = math.max(0, e.bolt_cooldown_timer - dt)
end
-- If the player is nearby a non-moving bolt, pick it up
for _, bolt in ipairs(self.pool.groups.bolt.entities) do
if (e.pos-bolt.pos).length < 20 and bolt.vel.length < 3 then
self.pool:emit("storeBolt", e, bolt)
end
end
if e.acc.x ~= 0 or e.acc.y ~= 0 then
e.sprite.variant = "walk"
else
e.sprite.variant = "idle"
end
if e.acc.x < 0 then
e.sprite.flip = true
elseif e.acc.x > 0 then
e.sprite.flip = false
end
if #e.bolts > 0 then
local bolt = e.bolts[1]
if e.aim_dir.x ~= 0 or e.aim_dir.y ~= 0 then
bolt.hidden = nil
bolt.pos = e.pos + e.aim_dir * AIMED_BOLT_DISTANCE
else
bolt.hidden = true
end
end
end
end
function Player:mousemoved()
self.use_mouse_aim = true
end
function Player:tryShootingBolt(player)
if (player.aim_dir.x ~= 0 or player.aim_dir.y ~= 0) and
player.bolt_cooldown_timer == 0 and
#player.bolts > 0
then
player.bolt_cooldown_timer = player.bolt_cooldown
self.pool:emit("shootBolt", player)
return true
end
end
function Player:shootBolt(player)
local bolt = table.remove(player.bolts, 1)
if not bolt then return end
bolt.pos = player.pos + player.aim_dir * AIMED_BOLT_DISTANCE
bolt.vel = player.aim_dir * player.bolt_speed
bolt.owner = player
self.pool:emit("boltShot", player, bolt)
end
function Player:storeBolt(player, bolt)
bolt.owner = nil
table.insert(player.bolts, bolt)
end
function Player:hitPlayer(player, bolt)
self:resetMap()
end
local function createBolt()
return {
pos = Vec(),
vel = Vec(),
acc = Vec(),
sprite = {
name = "bolt",
variant = "idle",
},
friction = 0.98,
max_speed = 400,
}
end
function Player:resetMap()
local map = self.pool:getSystem(require("systems.map"))
local spawnpoints = map:listSpawnpoints()
for _, bolt in ipairs(self.pool.groups.bolt.entities) do
self.pool:removeEntity(bolt)
end
for i, player in ipairs(self.pool.groups.player.entities) do
for _, bolt in ipairs(player.bolts) do
self.pool:removeEntity(bolt)
end
local spawnpoint = spawnpoints[(i - 1) % #spawnpoints + 1]
player.pos = spawnpoint
player.bolts = {}
for _=1, BOLT_AMOUNT do
local bolt = self.pool:queue(createBolt())
self.pool:emit("storeBolt", player, bolt)
end
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]
local player = self.pool:queue{
pos = spawnpoint,
vel = Vec(0, 0),
acc = Vec(),
sprite = {
name = "player",
variant = "walk"
},
friction = 0.998,
speed = 800,
bolt_speed = 2000,
bolts = {},
bolt_cooldown = 0.1,
collider = {-5, -5, 4, 8}
}
for _=1, BOLT_AMOUNT do
local bolt = self.pool:queue(createBolt())
self.pool:emit("storeBolt", player, bolt)
end
return player
end
function Player:draw()
for _, e in ipairs(self.pool.groups.controllable_player.entities) do
if e.aim_dir.x ~= 0 or e.aim_dir.y ~= 0 then
local boltSystem = self.pool:getSystem(require("systems.bolt"))
local pos = e.pos + e.aim_dir * AIMED_BOLT_DISTANCE*0
local vel = (e.aim_dir * e.bolt_speed).normalized * AIMED_BOLT_DISTANCE*1
local point_amount = 5
local points = boltSystem:projectTrajectory(pos, vel, point_amount+3)
for i=3, point_amount+3-1 do
love.graphics.circle("line", points[i].x, points[i].y, 5)
end
end
end
end
return Player

View File

@ -1,104 +0,0 @@
local ScreenScaler = {}
function ScreenScaler:hideBorders()
local r, g, b, a = love.graphics.getColor()
love.graphics.setColor(love.graphics.getBackgroundColor())
local w, h = love.graphics.getDimensions()
if self.offset_x ~= 0 then
love.graphics.rectangle("fill", 0, 0, self.offset_x, h)
love.graphics.rectangle("fill", w-self.offset_x, 0, self.offset_x, h)
end
if self.offset_y ~= 0 then
love.graphics.rectangle("fill", 0, 0, w, self.offset_y)
love.graphics.rectangle("fill", 0, h-self.offset_y, w, self.offset_y)
end
love.graphics.setColor(r, g, b, a)
end
function ScreenScaler:getPosition(x, y)
return (x - self.offset_x) / self.scale,
(y - self.offset_y) / self.scale
end
function ScreenScaler:isInBounds(x, y)
if self.scale then
local w, h = love.graphics.getDimensions()
return x >= self.offset_x and
x < w-self.offset_x and
y >= self.offset_y and
y < h-self.offset_y
else
return true
end
end
function ScreenScaler:getMousePosition()
if self.scale then
return self:getPosition(love.mouse.getPosition())
else
return love.mouse.getPosition()
end
end
function ScreenScaler:getDimensions()
if self.current_width and self.current_height then
return self.current_width, self.current_height
else
return love.graphics.getDimensions()
end
end
function ScreenScaler:overrideScaling(width, height)
local sw, sh = love.graphics.getDimensions()
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.current_width = width
self.current_height = height
end
function ScreenScaler:start(p1, p2)
local width, height
if type(p1) == "number" and type(p2) == "number" then
width, height = p1, p2
self.canvas = nil
elseif p1:typeOf("Canvas") then
self.canvas = p1
width, height = self.canvas:getDimensions()
else
return
end
local sw, sh = love.graphics.getDimensions()
self.current_width = width
self.current_height = height
self.scale = math.min(sw / width, sh / height)
self.offset_x = (sw - width * self.scale)/2
self.offset_y = (sh - height * self.scale)/2
love.graphics.push()
if self.canvas then
love.graphics.setCanvas(self.canvas)
love.graphics.clear()
else
love.graphics.translate(self.offset_x, self.offset_y)
love.graphics.scale(self.scale)
end
end
function ScreenScaler:finish()
love.graphics.pop()
if self.canvas then
love.graphics.setCanvas()
love.graphics.setColor(1, 1, 1)
love.graphics.draw(self.canvas, self.offset_x, self.offset_y, 0, self.scale)
else
self:hideBorders()
end
self.canvas = nil
end
return ScreenScaler

View File

@ -1,62 +0,0 @@
local data = require("data")
local pprint = require("lib.pprint")
local Sprite = {}
function Sprite:addToWorld(group, e)
if group ~= "sprite" then return end
local sprite = data.sprites[e.sprite.variant]
assert(sprite, ("Attempt to draw unknown sprite: %s"):format(e.sprite.name))
end
function Sprite:update(dt)
for _, e in ipairs(self.pool.groups.sprite.entities) do
local sprite = data.sprites[e.sprite.name]
if sprite then
local variant = sprite.variants[e.sprite.variant or "default"]
e.sprite.timer = (e.sprite.timer or 0) + dt
if variant then
local frame = variant[e.sprite.frame]
if not frame then
frame = variant[1]
end
if e.sprite.timer > frame.duration then
e.sprite.timer = e.sprite.timer % 0.1
e.sprite.frame = (e.sprite.frame or 1) % #variant + 1
end
end
end
end
end
function Sprite:draw()
love.graphics.setColor(1, 1, 1)
for _, e in ipairs(self.pool.groups.sprite.entities) do
local sprite = data.sprites[e.sprite.name]
assert(sprite, ("Attempt to draw unknown sprite: %s"):format(e.sprite.name))
if not e.hidden then
local variant = sprite.variants[e.sprite.variant or "default"]
if variant and e.sprite.frame then
local frame = variant[e.sprite.frame]
if not frame then
frame = variant[1]
end
local sx = 1
if e.sprite.flip then
sx = -1
end
love.graphics.draw(
frame.image,
e.pos.x,
e.pos.y,
0, sx, 1,
sprite.width/2, sprite.height/2
)
end
end
end
end
return Sprite

View File

@ -1,23 +0,0 @@
return function(ui, text, x, y, w, h)
local mx, my = love.mouse.getPosition()
local bg = ui.theme.bg_color
local result = false
if ui.inRect(mx, my, x, y, w, h) then
bg = ui.theme.bg_hover_color
if love.mouse.isDown(1) then
bg = ui.theme.bg_pressed_color
elseif ui:wasDown(1) then
result = true
end
end
love.graphics.setColor(bg)
love.graphics.rectangle("fill", x, y, w, h)
local font_height = ui.theme.font:getHeight()
love.graphics.setFont(ui.theme.font)
love.graphics.setColor(ui.theme.text_color)
love.graphics.printf(text, x, y+(h-font_height)/2, w, "center")
return result
end

View File

@ -1,51 +0,0 @@
local UI = {}
UI.__index = UI
local elements = {
label = require(... .. ".label"),
button = require(... .. ".button"),
textbox = require(... .. ".textbox"),
}
function UI.new(theme)
local self = setmetatable({}, UI)
self.theme = theme or {}
self.mouse_buttons = {}
return self
end
function UI:postDraw()
self.keys_pressed = {}
self.entered_text = nil
self.mouse_buttons[1] = love.mouse.isDown(1)
self.mouse_buttons[2] = love.mouse.isDown(2)
self.mouse_buttons[3] = love.mouse.isDown(3)
end
function UI:keypressed(key)
self.keys_pressed[key] = true
end
function UI:textinput(text)
self.entered_text = text
end
function UI.inRect(mx, my, x, y, w, h)
return (x <= mx and mx < x+w) and (y <= my and my < y+h)
end
function UI:wasDown(...)
for i=1, select("#", ...) do
local button = select(i, ...)
if self.mouse_buttons[button] then
return true
end
end
return false
end
function UI.__index(t, k)
return UI[k] or elements[k]
end
return UI

View File

@ -1,5 +0,0 @@
return function (ui, text, x, y)
love.graphics.setFont(ui.theme.font)
love.graphics.setColor(ui.theme.text_color)
love.graphics.print(text, x, y)
end

View File

@ -1,54 +0,0 @@
local padding = 10
return function(ui, textbox, x, y, w, h)
local mx, my = love.mouse.getPosition()
local bg = ui.theme.bg_color
if ui.inRect(mx, my, x, y, w, h) then
bg = ui.theme.bg_hover_color
if love.mouse.isDown(1) then
bg = ui.theme.bg_pressed_color
elseif ui:wasDown(1) then
textbox.is_editing = true
textbox.cursor = #textbox.text
end
else
if love.mouse.isDown(1) then
textbox.is_editing = false
end
end
love.graphics.setColor(bg)
love.graphics.rectangle("fill", x, y, w, h)
love.graphics.setColor(ui.theme.text_color)
if textbox.is_editing then
if ui.keys_pressed["escape"] then
textbox.is_editing = false
elseif ui.keys_pressed["left"] then
textbox.cursor = math.max(textbox.cursor - 1, 0)
elseif ui.keys_pressed["right"] then
textbox.cursor = math.min(textbox.cursor + 1, #textbox.text)
elseif ui.keys_pressed["backspace"] and textbox.cursor > 0 then
textbox.text = textbox.text:sub(0, textbox.cursor-1) .. textbox.text:sub(textbox.cursor+1)
textbox.cursor = textbox.cursor - 1
elseif ui.keys_pressed["v"] and love.keyboard.isDown("lctrl", "rctrl") then
local text = love.system.getClipboardText()
textbox.text = textbox.text:sub(1, textbox.cursor) .. text .. textbox.text:sub(textbox.cursor+1)
textbox.cursor = textbox.cursor + #text
end
if ui.entered_text then
textbox.text = textbox.text:sub(1, textbox.cursor) .. ui.entered_text .. textbox.text:sub(textbox.cursor+1)
textbox.cursor = textbox.cursor + #ui.entered_text
end
love.graphics.setLineWidth(3)
love.graphics.line(x, y+h-1.5, x+w, y+h-1.5)
local cursor_ox = ui.theme.font:getWidth(textbox.text:sub(1, textbox.cursor))+padding
love.graphics.line(x+cursor_ox, y+padding, x+cursor_ox, y+h-padding)
end
local font_height = ui.theme.font:getHeight()
love.graphics.setFont(ui.theme.font)
love.graphics.print(textbox.text or "", x+padding, y+(h-font_height)/2)
end