Compare commits
10 Commits
e40b9a959e
...
40834bfa87
Author | SHA1 | Date | |
---|---|---|---|
40834bfa87 | |||
96d4d1f7d3 | |||
6ae7e3b1a4 | |||
a709e4f1c4 | |||
e2134d76bd | |||
cc709ba84a | |||
0066f8ba0a | |||
6ab0b2eb4a | |||
6e754155dc | |||
7f26575942 |
@ -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": [
|
||||
|
@ -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
|
||||
})
|
||||
|
6
Makefile
6
Makefile
@ -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
9
src/bolt-mods/bouncy.lua
Normal 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
24
src/bolt-mods/grow.lua
Normal 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
21
src/bolt-mods/init.lua
Normal 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 }
|
26
src/bolt-mods/retain-speed.lua
Normal file
26
src/bolt-mods/retain-speed.lua
Normal 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
|
10
src/bolt-mods/speed-bounce.lua
Normal file
10
src/bolt-mods/speed-bounce.lua
Normal 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
113
src/bolt.lua
Normal 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
|
@ -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
93
src/controls-manager.lua
Normal 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
|
@ -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"
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"automappingRulesFile": "",
|
||||
"commands": [
|
||||
],
|
||||
"extensionsPath": "extensions",
|
||||
"folders": [
|
||||
"."
|
||||
],
|
||||
"propertyTypes": [
|
||||
]
|
||||
}
|
@ -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 = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
@ -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 |
@ -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 |
@ -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>
|
@ -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
|
||||
|
@ -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
|
@ -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()
|
69
src/lib/breezefield/collider.lua
Normal file
69
src/lib/breezefield/collider.lua
Normal 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
|
24
src/lib/breezefield/init.lua
Normal file
24
src/lib/breezefield/init.lua
Normal 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
|
32
src/lib/breezefield/utils.lua
Normal file
32
src/lib/breezefield/utils.lua
Normal 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}
|
255
src/lib/breezefield/world.lua
Normal file
255
src/lib/breezefield/world.lua
Normal 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
|
781
src/lib/bump.lua
781
src/lib/bump.lua
@ -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
|
@ -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
138
src/lib/center.lua
Normal 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
|
||||
|
388
src/lib/json.lua
388
src/lib/json.lua
@ -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
|
@ -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,
|
||||
|
@ -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
|
1627
src/lib/sti/init.lua
1627
src/lib/sti/init.lua
File diff suppressed because it is too large
Load Diff
@ -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
|
@ -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
|
@ -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
|
10
src/main.lua
10
src/main.lua
@ -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
25
src/player.lua
Normal 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
232
src/screen-scaler.lua
Normal 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
|
@ -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
|
@ -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
95
src/states/map-editor.lua
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
Loading…
Reference in New Issue
Block a user