Browse Source

Merge branch 'physics'

bjorn 10 years ago
parent
commit
231ff4981d
46 changed files with 1622 additions and 87 deletions
  1. 3 3
      .gitmodules
  2. 38 0
      building.lua
  3. 2 2
      context.lua
  4. 17 0
      data/animation/pigeon.lua
  5. 120 0
      deps/animation.lua
  6. 791 0
      deps/dkjson.lua
  7. 27 0
      deps/event.lua
  8. 1 0
      deps/spine
  9. 29 2
      deps/util.lua
  10. 256 0
      deps/view.lua
  11. 23 18
      game.lua
  12. 2 1
      hud.lua
  13. 116 0
      loader.lua
  14. 1 0
      main.lua
  15. 25 0
      map.lua
  16. BIN
      media/skeletons/pigeon/beakbottom.png
  17. BIN
      media/skeletons/pigeon/beakclosed.png
  18. BIN
      media/skeletons/pigeon/beakopen.png
  19. BIN
      media/skeletons/pigeon/beaktop.png
  20. BIN
      media/skeletons/pigeon/body1.png
  21. BIN
      media/skeletons/pigeon/body2.png
  22. BIN
      media/skeletons/pigeon/body3.png
  23. BIN
      media/skeletons/pigeon/body4.png
  24. BIN
      media/skeletons/pigeon/head.png
  25. BIN
      media/skeletons/pigeon/leftclaw1.png
  26. BIN
      media/skeletons/pigeon/leftclaw2.png
  27. BIN
      media/skeletons/pigeon/leftclaw3.png
  28. BIN
      media/skeletons/pigeon/leftfoot.png
  29. BIN
      media/skeletons/pigeon/leftshin.png
  30. BIN
      media/skeletons/pigeon/leftthigh.png
  31. BIN
      media/skeletons/pigeon/leftwing.png
  32. BIN
      media/skeletons/pigeon/neck1.png
  33. BIN
      media/skeletons/pigeon/neck2.png
  34. BIN
      media/skeletons/pigeon/neck3.png
  35. BIN
      media/skeletons/pigeon/neck4.png
  36. BIN
      media/skeletons/pigeon/pupil.png
  37. BIN
      media/skeletons/pigeon/rightclaw1.png
  38. BIN
      media/skeletons/pigeon/rightclaw2.png
  39. BIN
      media/skeletons/pigeon/rightclaw3.png
  40. BIN
      media/skeletons/pigeon/rightfoot.png
  41. BIN
      media/skeletons/pigeon/rightshin.png
  42. BIN
      media/skeletons/pigeon/rightthigh.png
  43. BIN
      media/skeletons/pigeon/rightwing.png
  44. 53 7
      person.lua
  45. 110 54
      pigeon.lua
  46. 8 0
      require.lua

+ 3 - 3
.gitmodules

@@ -1,6 +1,6 @@
-[submodule "deps/spine-runtimes"]
-	path = deps/spine-runtimes
-	url = https://github.com/bjornswenson/spine-runtimes
 [submodule "deps/lovestep"]
 [submodule "deps/lovestep"]
 	path = deps/lovestep
 	path = deps/lovestep
 	url = https://github.com/bjornswenson/lovestep
 	url = https://github.com/bjornswenson/lovestep
+[submodule "deps/spine"]
+	path = deps/spine
+	url = [email protected]:bjornswenson/spine-runtimes

+ 38 - 0
building.lua

@@ -0,0 +1,38 @@
+Building = class()
+
+Building.category = 4
+
+function Building:init(x, w, h)
+  self.x = x
+  self.y = ctx.map.height - ctx.map.ground.height
+  self.w = w
+  self.h = h
+
+  self.body = love.physics.newBody(ctx.world, self.x - self.w / 2, self.y - self.h / 2, 'kinematic')
+  self.shape = love.physics.newRectangleShape(self.w, self.h)
+  self.fixture = love.physics.newFixture(self.body, self.shape)
+
+  self.body:setUserData(self)
+
+  self.body:setMass(1000)
+  self.fixture:setCategory(self.category)
+  self.fixture:setMask(self.category, Person.category)
+
+  ctx.event:emit('view.register', {object = self})
+end
+
+function Building:draw()
+  local g = love.graphics
+
+  g.setColor(128, 128, 128, 35)
+  physics.draw('fill', self)
+
+  g.setColor(255, 255, 255)
+  physics.draw('line', self)
+end
+
+function Building:die()
+  self.body:destroy()
+  ctx.event:emit('view.unregister', {object = self})
+end
+

+ 2 - 2
context.lua

@@ -9,7 +9,7 @@ function Context:add(obj, ...)
 
 
   local tmp = ctx
   local tmp = ctx
   ctx = c
   ctx = c
-  f.exe(ctx.load, ctx, ...)
+  lume.call(ctx.load, ctx, ...)
   ctx = tmp
   ctx = tmp
 
 
   return c
   return c
@@ -31,7 +31,7 @@ end
 function Context:remove(ctx, ...)
 function Context:remove(ctx, ...)
   for i = 1, #self.list do
   for i = 1, #self.list do
     if self.list[i] == ctx then
     if self.list[i] == ctx then
-      f.exe(ctx.unload, ctx, ...)
+      lume.call(ctx.unload, ctx, ...)
       table.remove(self.list, i)
       table.remove(self.list, i)
       collectgarbage()
       collectgarbage()
       return
       return

+ 17 - 0
data/animation/pigeon.lua

@@ -0,0 +1,17 @@
+local Pigeon = extend(Animation)
+
+Pigeon.scale = .15
+Pigeon.default = nil
+Pigeon.states = {}
+
+Pigeon.states.idle = {
+  loop = true,
+  priority = 1
+}
+
+Pigeon.states.peck = {
+  loop = false,
+  priority = 2
+}
+
+return Pigeon

+ 120 - 0
deps/animation.lua

@@ -0,0 +1,120 @@
+Animation = class()
+
+Animation.defaultMix = .2
+
+function Animation:init(vars)
+  --table.merge(vars, self, true)
+
+  self:initSpine(self.code)
+
+  self.event = Event()
+  self.spine.animationState.onComplete = function() self.event:emit('complete', {state = self.state}) end
+  self.spine.animationState.onEvent = function(_, data) self.event:emit('event', data) end
+
+  self:set(self.default)
+  self.speed = 1
+  self.flipped = false
+end
+
+function Animation:draw(x, y, options)
+  options = options or {}
+  local skeleton, animationState = self.spine.skeleton, self.spine.animationState
+  self:setPosition(x, y)
+  skeleton.flipX = self.flipped
+  if self.backwards then skeleton.flipX = not skeleton.flipX end
+  if not options.noupdate then self:tick(ls.dt) end
+  animationState:apply(skeleton)
+  local root = skeleton:getRootBone()
+  if root then
+    root.scaleX = self.scale
+    root.scaleY = self.scale
+  end
+  skeleton:updateWorldTransform()
+  skeleton:draw()
+
+  if options.debug then
+    table.each(self.spine.skeleton.slots, function(slot)
+      slot:setAttachment(self.spine.skeleton:getAttachment(slot.data.name, slot.data.name .. '_bb'))
+      self.spine.skeleton.flipY = true
+    end)
+    skeleton:updateWorldTransform()
+    self.spine.skeletonBounds:update(self.spine.skeleton)
+    love.graphics.setColor(255, 255, 255)
+    for i = 1, #self.spine.skeletonBounds.polygons do
+      love.graphics.polygon('line', self.spine.skeletonBounds.polygons[i])
+    end
+    table.each(self.spine.skeleton.slots, function(slot)
+      slot:setAttachment(self.spine.skeleton:getAttachment(slot.data.name, slot.data.name))
+      self.spine.skeleton.flipY = false
+    end)
+  end
+end
+
+function Animation:tick(delta)
+  self.spine.animationState:update(delta * (--[[self.state.speed]]1 or 1) * self.speed)
+  self.spine.animationState:apply(self.spine.skeleton)
+end
+
+function Animation:setPosition(x, y)
+  local skeleton = self.spine.skeleton
+  skeleton.x = x + (self.offsetx or 0)
+  skeleton.y = y + (self.offsety or 0)
+end
+
+function Animation:set(name, options)
+  if type(name) == 'number' and self.states[name] then name = self.states[name].name end
+  if not name or not self.states[name] then return end
+  options = options or {}
+
+  local target = self.states[name]
+
+  if self.state and self.state.name == target.name then return end
+  if not options.force and self.state and self.state.priority > target.priority then return end
+
+  self.state = target
+  if self.spine.skeletonData:findAnimation(self.state.name) then
+    self.spine.animationState:setAnimationByName(0, self.state.name, self.state.loop)
+  end
+end
+
+function Animation:contains(x, y)
+  do return false end
+
+  table.each(self.spine.skeleton.slots, function(slot)
+    slot:setAttachment(self.spine.skeleton:getAttachment(slot.data.name, slot.data.name .. '_bb'))
+  end)
+
+  self.spine.skeleton.flipY = true
+  self.spine.skeleton:updateWorldTransform()
+  self.spine.skeletonBounds:update(self.spine.skeleton)
+  self.spine.skeleton.flipY = false
+  local contains = self.spine.skeletonBounds:containsPoint(x, y)
+
+  table.each(self.spine.skeleton.slots, function(slot)
+    slot:setAttachment(self.spine.skeleton:getAttachment(slot.data.name, slot.data.name))
+  end)
+
+  return contains
+end
+
+function Animation:initSpine()
+  local s = setmetatable({}, data.animation[self.code].spine)
+
+  s.skeleton = spine.Skeleton.new(s.skeletonData)
+  s.skeletonBounds = spine.SkeletonBounds.new()
+  s.animationState = spine.AnimationState.new(s.animationStateData)
+
+	s.skeleton:setToSetupPose()
+  s.skeleton.createImage = function(_, attachment)
+    return self.graphics[attachment.name]
+  end
+  s.skeleton.createAtlasImage = function(_, page)
+    return self.graphics[page.name:gsub('.png', ''):gsub('.dds', '')]
+  end
+
+  self.spine = s
+end
+
+function Animation:on(...)
+  return self.event:on(...)
+end

+ 791 - 0
deps/dkjson.lua

@@ -0,0 +1,791 @@
+    -- Module options:
+    local always_try_using_lpeg = true
+
+    --[==[
+
+David Kolf's JSON module for Lua 5.1/5.2
+========================================
+
+*Version 2.2*
+
+This module writes no global values, not even the module table.
+Import it using
+
+    json = require ("dkjson")
+
+Exported functions and values:
+
+`json.encode (object [, state])`
+--------------------------------
+
+Create a string representing the object. `Object` can be a table,
+a string, a number, a boolean, `nil`, `json.null` or any object with
+a function `__tojson` in its metatable. A table can only use strings
+and numbers as keys and its values have to be valid objects as
+well. It raises an error for any invalid data types or reference
+cycles.
+
+`state` is an optional table with the following fields:
+
+  - `indent`
+    When `indent` (a boolean) is set, the created string will contain
+    newlines and indentations. Otherwise it will be one long line.
+  - `keyorder`
+    `keyorder` is an array to specify the ordering of keys in the
+    encoded output. If an object has keys which are not in this array
+    they are written after the sorted keys.
+  - `level`
+    This is the initial level of indentation used when `indent` is
+    set. For each level two spaces are added. When absent it is set
+    to 0.
+  - `buffer`
+    `buffer` is an array to store the strings for the result so they
+    can be concatenated at once. When it isn't given, the encode
+    function will create it temporary and will return the
+    concatenated result.
+  - `bufferlen`
+    When `bufferlen` is set, it has to be the index of the last
+    element of `buffer`.
+  - `tables`
+    `tables` is a set to detect reference cycles. It is created
+    temporary when absent. Every table that is currently processed
+    is used as key, the value is `true`.
+
+When `state.buffer` was set, the return value will be `true` on
+success. Without `state.buffer` the return value will be a string.
+
+`json.decode (string [, position [, null]])`
+--------------------------------------------
+
+Decode `string` starting at `position` or at 1 if `position` was
+omitted.
+
+`null` is an optional value to be returned for null values. The
+default is `nil`, but you could set it to `json.null` or any other
+value.
+
+The return values are the object or `nil`, the position of the next
+character that doesn't belong to the object, and in case of errors
+an error message.
+
+Two metatables are created. Every array or object that is decoded gets
+a metatable with the `__jsontype` field set to either `array` or
+`object`. If you want to provide your own metatables use the syntax
+
+    json.decode (string, position, null, objectmeta, arraymeta)
+
+To prevent the assigning of metatables pass `nil`:
+
+    json.decode (string, position, null, nil)
+
+`<metatable>.__jsonorder`
+-------------------------
+
+`__jsonorder` can overwrite the `keyorder` for a specific table.
+
+`<metatable>.__jsontype`
+------------------------
+
+`__jsontype` can be either `"array"` or `"object"`. This value is only
+checked for empty tables. (The default for empty tables is `"array"`).
+
+`<metatable>.__tojson (self, state)`
+------------------------------------
+
+You can provide your own `__tojson` function in a metatable. In this
+function you can either add directly to the buffer and return true,
+or you can return a string. On errors nil and a message should be
+returned.
+
+`json.null`
+-----------
+
+You can use this value for setting explicit `null` values.
+
+`json.version`
+--------------
+
+Set to `"dkjson 2.2"`.
+
+`json.quotestring (string)`
+---------------------------
+
+Quote a UTF-8 string and escape critical characters using JSON
+escape sequences. This function is only necessary when you build
+your own `__tojson` functions.
+
+`json.addnewline (state)`
+-------------------------
+
+When `state.indent` is set, add a newline to `state.buffer` and spaces
+according to `state.level`.
+
+LPeg support
+------------
+
+When the local configuration variable `always_try_using_lpeg` is set,
+this module tries to load LPeg to replace the `decode` function. The
+speed increase is significant. You can get the LPeg module at
+  <http://www.inf.puc-rio.br/~roberto/lpeg/>.
+When LPeg couldn't be loaded, the pure Lua functions stay active.
+
+In case you don't want this module to require LPeg on its own,
+disable the option `always_try_using_lpeg` in the options section at
+the top of the module.
+
+In this case you can later load LPeg support using
+
+### `json.use_lpeg ()`
+
+Require the LPeg module and replace the functions `quotestring` and
+and `decode` with functions that use LPeg patterns.
+This function returns the module table, so you can load the module
+using:
+
+    json = require "dkjson".use_lpeg()
+
+Alternatively you can use `pcall` so the JSON module still works when
+LPeg isn't found.
+
+    json = require "dkjson"
+    pcall (json.use_lpeg)
+
+### `json.using_lpeg`
+
+This variable is set to `true` when LPeg was loaded successfully.
+
+---------------------------------------------------------------------
+
+Contact
+-------
+
+You can contact the author by sending an e-mail to 'kolf' at the
+e-mail provider 'gmx.de'.
+
+---------------------------------------------------------------------
+
+*Copyright (C) 2010, 2011, 2012 David Heiko Kolf*
+
+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.
+
+<!-- This documentation can be parsed using Markdown to generate HTML.
+     The source code is enclosed in a HTML comment so it won't be displayed
+     by browsers, but it should be removed from the final HTML file as
+     it isn't a valid HTML comment (and wastes space).
+  -->
+
+  <!--]==]
+
+-- global dependencies:
+local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =
+      pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset
+local error, require, pcall, select = error, require, pcall, select
+local floor, huge = math.floor, math.huge
+local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
+      string.rep, string.gsub, string.sub, string.byte, string.char,
+      string.find, string.len, string.format
+local concat = table.concat
+
+local _ENV = nil -- blocking globals in Lua 5.2
+
+local json = { version = "dkjson 2.2" }
+
+pcall (function()
+  -- Enable access to blocked metatables.
+  -- Don't worry, this module doesn't change anything in them.
+  local debmeta = require "debug".getmetatable
+  if debmeta then getmetatable = debmeta end
+end)
+
+json.null = setmetatable ({}, {
+  __tojson = function () return "null" end
+})
+
+local function isarray (tbl)
+  local max, n, arraylen = 0, 0, 0
+  for k,v in pairs (tbl) do
+    if k == 'n' and type(v) == 'number' then
+      arraylen = v
+      if v > max then
+        max = v
+      end
+    else
+      if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
+        return false
+      end
+      if k > max then
+        max = k
+      end
+      n = n + 1
+    end
+  end
+  if max > 10 and max > arraylen and max > n * 2 then
+    return false -- don't create an array with too many holes
+  end
+  return true, max
+end
+
+local escapecodes = {
+  ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
+  ["\n"] = "\\n",  ["\r"] = "\\r",  ["\t"] = "\\t"
+}
+
+local function escapeutf8 (uchar)
+  local value = escapecodes[uchar]
+  if value then
+    return value
+  end
+  local a, b, c, d = strbyte (uchar, 1, 4)
+  a, b, c, d = a or 0, b or 0, c or 0, d or 0
+  if a <= 0x7f then
+    value = a
+  elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
+    value = (a - 0xc0) * 0x40 + b - 0x80
+  elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
+    value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
+  elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
+    value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
+  else
+    return ""
+  end
+  if value <= 0xffff then
+    return strformat ("\\u%.4x", value)
+  elseif value <= 0x10ffff then
+    -- encode as UTF-16 surrogate pair
+    value = value - 0x10000
+    local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
+    return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
+  else
+    return ""
+  end
+end
+
+local function fsub (str, pattern, repl)
+  -- gsub always builds a new string in a buffer, even when no match
+  -- exists. First using find should be more efficient when most strings
+  -- don't contain the pattern.
+  if strfind (str, pattern) then
+    return gsub (str, pattern, repl)
+  else
+    return str
+  end
+end
+
+local function quotestring (value)
+  -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
+  value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
+  if strfind (value, "[\194\216\220\225\226\239]") then
+    value = fsub (value, "\194[\128-\159\173]", escapeutf8)
+    value = fsub (value, "\216[\128-\132]", escapeutf8)
+    value = fsub (value, "\220\143", escapeutf8)
+    value = fsub (value, "\225\158[\180\181]", escapeutf8)
+    value = fsub (value, "\226\128[\140-\143\168\175]", escapeutf8)
+    value = fsub (value, "\226\129[\160-\175]", escapeutf8)
+    value = fsub (value, "\239\187\191", escapeutf8)
+    value = fsub (value, "\239\191[\176\191]", escapeutf8)
+  end
+  return "\"" .. value .. "\""
+end
+json.quotestring = quotestring
+
+local function addnewline2 (level, buffer, buflen)
+  buffer[buflen+1] = "\n"
+  buffer[buflen+2] = strrep ("  ", level)
+  buflen = buflen + 2
+  return buflen
+end
+
+function json.addnewline (state)
+  if state.indent then
+    state.bufferlen = addnewline2 (state.level or 0,
+                           state.buffer, state.bufferlen or #(state.buffer))
+  end
+end
+
+local encode2 -- forward declaration
+
+local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder)
+  local kt = type (key)
+  if kt ~= 'string' and kt ~= 'number' then
+    return nil, "type '" .. kt .. "' is not supported as a key by JSON."
+  end
+  if prev then
+    buflen = buflen + 1
+    buffer[buflen] = ","
+  end
+  if indent then
+    buflen = addnewline2 (level, buffer, buflen)
+  end
+  buffer[buflen+1] = quotestring (key)
+  buffer[buflen+2] = ":"
+  return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder)
+end
+
+encode2 = function (value, indent, level, buffer, buflen, tables, globalorder)
+  local valtype = type (value)
+  local valmeta = getmetatable (value)
+  valmeta = type (valmeta) == 'table' and valmeta -- only tables
+  local valtojson = valmeta and valmeta.__tojson
+  if valtojson then
+    if tables[value] then
+      return nil, "reference cycle"
+    end
+    tables[value] = true
+    local state = {
+        indent = indent, level = level, buffer = buffer,
+        bufferlen = buflen, tables = tables, keyorder = globalorder
+    }
+    local ret, msg = valtojson (value, state)
+    if not ret then return nil, msg end
+    tables[value] = nil
+    buflen = state.bufferlen
+    if type (ret) == 'string' then
+      buflen = buflen + 1
+      buffer[buflen] = ret
+    end
+  elseif value == nil then
+    buflen = buflen + 1
+    buffer[buflen] = "null"
+  elseif valtype == 'number' then
+    local s
+    if value ~= value or value >= huge or -value >= huge then
+      -- This is the behaviour of the original JSON implementation.
+      s = "null"
+    else
+      s = tostring (value)
+    end
+    buflen = buflen + 1
+    buffer[buflen] = s
+  elseif valtype == 'boolean' then
+    buflen = buflen + 1
+    buffer[buflen] = value and "true" or "false"
+  elseif valtype == 'string' then
+    buflen = buflen + 1
+    buffer[buflen] = quotestring (value)
+  elseif valtype == 'table' then
+    if tables[value] then
+      return nil, "reference cycle"
+    end
+    tables[value] = true
+    level = level + 1
+    local isa, n = isarray (value)
+    if n == 0 and valmeta and valmeta.__jsontype == 'object' then
+      isa = false
+    end
+    local msg
+    if isa then -- JSON array
+      buflen = buflen + 1
+      buffer[buflen] = "["
+      for i = 1, n do
+        buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder)
+        if not buflen then return nil, msg end
+        if i < n then
+          buflen = buflen + 1
+          buffer[buflen] = ","
+        end
+      end
+      buflen = buflen + 1
+      buffer[buflen] = "]"
+    else -- JSON object
+      local prev = false
+      buflen = buflen + 1
+      buffer[buflen] = "{"
+      local order = valmeta and valmeta.__jsonorder or globalorder
+      if order then
+        local used = {}
+        n = #order
+        for i = 1, n do
+          local k = order[i]
+          local v = value[k]
+          if v then
+            used[k] = true
+            buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder)
+            prev = true -- add a seperator before the next element
+          end
+        end
+        for k,v in pairs (value) do
+          if not used[k] then
+            buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder)
+            if not buflen then return nil, msg end
+            prev = true -- add a seperator before the next element
+          end
+        end
+      else -- unordered
+        for k,v in pairs (value) do
+          buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder)
+          if not buflen then return nil, msg end
+          prev = true -- add a seperator before the next element
+        end
+      end
+      if indent then
+        buflen = addnewline2 (level - 1, buffer, buflen)
+      end
+      buflen = buflen + 1
+      buffer[buflen] = "}"
+    end
+    tables[value] = nil
+  else
+    return nil, "type '" .. valtype .. "' is not supported by JSON."
+  end
+  return buflen
+end
+
+function json.encode (value, state)
+  state = state or {}
+  local oldbuffer = state.buffer
+  local buffer = oldbuffer or {}
+  local ret, msg = encode2 (value, state.indent, state.level or 0,
+                   buffer, state.bufferlen or 0, state.tables or {}, state.keyorder)
+  if not ret then
+    error (msg, 2)
+  elseif oldbuffer then
+    state.bufferlen = ret
+    return true
+  else
+    return concat (buffer)
+  end
+end
+
+local function loc (str, where)
+  local line, pos, linepos = 1, 1, 0
+  while true do
+    pos = strfind (str, "\n", pos, true)
+    if pos and pos < where then
+      line = line + 1
+      linepos = pos
+      pos = pos + 1
+    else
+      break
+    end
+  end
+  return "line " .. line .. ", column " .. (where - linepos)
+end
+
+local function unterminated (str, what, where)
+  return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where)
+end
+
+local function scanwhite (str, pos)
+  while true do
+    pos = strfind (str, "%S", pos)
+    if not pos then return nil end
+    if strsub (str, pos, pos + 2) == "\239\187\191" then
+      -- UTF-8 Byte Order Mark
+      pos = pos + 3
+    else
+      return pos
+    end
+  end
+end
+
+local escapechars = {
+  ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f",
+  ["n"] = "\n", ["r"] = "\r", ["t"] = "\t"
+}
+
+local function unichar (value)
+  if value < 0 then
+    return nil
+  elseif value <= 0x007f then
+    return strchar (value)
+  elseif value <= 0x07ff then
+    return strchar (0xc0 + floor(value/0x40),
+                    0x80 + (floor(value) % 0x40))
+  elseif value <= 0xffff then
+    return strchar (0xe0 + floor(value/0x1000),
+                    0x80 + (floor(value/0x40) % 0x40),
+                    0x80 + (floor(value) % 0x40))
+  elseif value <= 0x10ffff then
+    return strchar (0xf0 + floor(value/0x40000),
+                    0x80 + (floor(value/0x1000) % 0x40),
+                    0x80 + (floor(value/0x40) % 0x40),
+                    0x80 + (floor(value) % 0x40))
+  else
+    return nil
+  end
+end
+
+local function scanstring (str, pos)
+  local lastpos = pos + 1
+  local buffer, n = {}, 0
+  while true do
+    local nextpos = strfind (str, "[\"\\]", lastpos)
+    if not nextpos then
+      return unterminated (str, "string", pos)
+    end
+    if nextpos > lastpos then
+      n = n + 1
+      buffer[n] = strsub (str, lastpos, nextpos - 1)
+    end
+    if strsub (str, nextpos, nextpos) == "\"" then
+      lastpos = nextpos + 1
+      break
+    else
+      local escchar = strsub (str, nextpos + 1, nextpos + 1)
+      local value
+      if escchar == "u" then
+        value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)
+        if value then
+          local value2
+          if 0xD800 <= value and value <= 0xDBff then
+            -- we have the high surrogate of UTF-16. Check if there is a
+            -- low surrogate escaped nearby to combine them.
+            if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then
+              value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)
+              if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
+                value = (value - 0xD800)  * 0x400 + (value2 - 0xDC00) + 0x10000
+              else
+                value2 = nil -- in case it was out of range for a low surrogate
+              end
+            end
+          end
+          value = value and unichar (value)
+          if value then
+            if value2 then
+              lastpos = nextpos + 12
+            else
+              lastpos = nextpos + 6
+            end
+          end
+        end
+      end
+      if not value then
+        value = escapechars[escchar] or escchar
+        lastpos = nextpos + 2
+      end
+      n = n + 1
+      buffer[n] = value
+    end
+  end
+  if n == 1 then
+    return buffer[1], lastpos
+  elseif n > 1 then
+    return concat (buffer), lastpos
+  else
+    return "", lastpos
+  end
+end
+
+local scanvalue -- forward declaration
+
+local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)
+  local len = strlen (str)
+  local tbl, n = {}, 0
+  local pos = startpos + 1
+  if what == 'object' then
+    setmetatable (tbl, objectmeta)
+  else
+    setmetatable (tbl, arraymeta)
+  end
+  while true do
+    pos = scanwhite (str, pos)
+    if not pos then return unterminated (str, what, startpos) end
+    local char = strsub (str, pos, pos)
+    if char == closechar then
+      return tbl, pos + 1
+    end
+    local val1, err
+    val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
+    if err then return nil, pos, err end
+    pos = scanwhite (str, pos)
+    if not pos then return unterminated (str, what, startpos) end
+    char = strsub (str, pos, pos)
+    if char == ":" then
+      if val1 == nil then
+        return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")"
+      end
+      pos = scanwhite (str, pos + 1)
+      if not pos then return unterminated (str, what, startpos) end
+      local val2
+      val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
+      if err then return nil, pos, err end
+      tbl[val1] = val2
+      pos = scanwhite (str, pos)
+      if not pos then return unterminated (str, what, startpos) end
+      char = strsub (str, pos, pos)
+    else
+      n = n + 1
+      tbl[n] = val1
+    end
+    if char == "," then
+      pos = pos + 1
+    end
+  end
+end
+
+scanvalue = function (str, pos, nullval, objectmeta, arraymeta)
+  pos = pos or 1
+  pos = scanwhite (str, pos)
+  if not pos then
+    return nil, strlen (str) + 1, "no valid JSON value (reached the end)"
+  end
+  local char = strsub (str, pos, pos)
+  if char == "{" then
+    return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta)
+  elseif char == "[" then
+    return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta)
+  elseif char == "\"" then
+    return scanstring (str, pos)
+  else
+    local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos)
+    if pstart then
+      local number = tonumber (strsub (str, pstart, pend))
+      if number then
+        return number, pend + 1
+      end
+    end
+    pstart, pend = strfind (str, "^%a%w*", pos)
+    if pstart then
+      local name = strsub (str, pstart, pend)
+      if name == "true" then
+        return true, pend + 1
+      elseif name == "false" then
+        return false, pend + 1
+      elseif name == "null" then
+        return nullval, pend + 1
+      end
+    end
+    return nil, pos, "no valid JSON value at " .. loc (str, pos)
+  end
+end
+
+local function optionalmetatables(...)
+  if select("#", ...) > 0 then
+    return ...
+  else
+    return {__jsontype = 'object'}, {__jsontype = 'array'}
+  end
+end
+
+function json.decode (str, pos, nullval, ...)
+  local objectmeta, arraymeta = optionalmetatables(...)
+  return scanvalue (str, pos, nullval, objectmeta, arraymeta)
+end
+
+function json.use_lpeg ()
+  local g = require ("lpeg")
+  local pegmatch = g.match
+  local P, S, R, V = g.P, g.S, g.R, g.V
+
+  local function ErrorCall (str, pos, msg, state)
+    if not state.msg then
+      state.msg = msg .. " at " .. loc (str, pos)
+      state.pos = pos
+    end
+    return false
+  end
+
+  local function Err (msg)
+    return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall)
+  end
+
+  local Space = (S" \n\r\t" + P"\239\187\191")^0
+
+  local PlainChar = 1 - S"\"\\\n\r"
+  local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars
+  local HexDigit = R("09", "af", "AF")
+  local function UTF16Surrogate (match, pos, high, low)
+    high, low = tonumber (high, 16), tonumber (low, 16)
+    if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then
+      return true, unichar ((high - 0xD800)  * 0x400 + (low - 0xDC00) + 0x10000)
+    else
+      return false
+    end
+  end
+  local function UTF16BMP (hex)
+    return unichar (tonumber (hex, 16))
+  end
+  local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit))
+  local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP
+  local Char = UnicodeEscape + EscapeSequence + PlainChar
+  local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string")
+  local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0))
+  local Fractal = P"." * R"09"^0
+  local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1
+  local Number = (Integer * Fractal^(-1) * Exponent^(-1))/tonumber
+  local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1)
+  local SimpleValue = Number + String + Constant
+  local ArrayContent, ObjectContent
+
+  -- The functions parsearray and parseobject parse only a single value/pair
+  -- at a time and store them directly to avoid hitting the LPeg limits.
+  local function parsearray (str, pos, nullval, state)
+    local obj, cont
+    local npos
+    local t, nt = {}, 0
+    repeat
+      obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state)
+      if not npos then break end
+      pos = npos
+      nt = nt + 1
+      t[nt] = obj
+    until cont == 'last'
+    return pos, setmetatable (t, state.arraymeta)
+  end
+
+  local function parseobject (str, pos, nullval, state)
+    local obj, key, cont
+    local npos
+    local t = {}
+    repeat
+      key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state)
+      if not npos then break end
+      pos = npos
+      t[key] = obj
+    until cont == 'last'
+    return pos, setmetatable (t, state.objectmeta)
+  end
+
+  local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected")
+  local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected")
+  local Value = Space * (Array + Object + SimpleValue)
+  local ExpectedValue = Value + Space * Err "value expected"
+  ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
+  local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue)
+  ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
+  local DecodeValue = ExpectedValue * g.Cp ()
+
+  function json.decode (str, pos, nullval, ...)
+    local state = {}
+    state.objectmeta, state.arraymeta = optionalmetatables(...)
+    local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state)
+    if state.msg then
+      return nil, state.pos, state.msg
+    else
+      return obj, retpos
+    end
+  end
+
+  -- use this function only once:
+  json.use_lpeg = function () return json end
+
+  json.using_lpeg = true
+
+  return json -- so you can get the module using json = require "dkjson".use_lpeg()
+end
+
+if always_try_using_lpeg then
+  pcall (json.use_lpeg)
+end
+
+return json
+
+-->

+ 27 - 0
deps/event.lua

@@ -0,0 +1,27 @@
+Event = class()
+
+function Event:init()
+  self.handlers = {}
+end
+
+function Event:on(event, fn, context)
+  self.handlers[event] = self.handlers[event] or {}
+  if context then
+    self.handlers[event][context] = fn
+  else
+    table.insert(self.handlers[event], fn)
+  end
+end
+
+function Event:emit(event, data)
+  if not self.handlers[event] then return end
+  for _, fn in pairs(self.handlers[event]) do
+    fn(data)
+  end
+end
+
+function Event:remove(event, context)
+  if self.handlers[event] and self.handlers[event][context] then
+    self.handlers[event][context] = nil
+  end
+end

+ 1 - 0
deps/spine

@@ -0,0 +1 @@
+Subproject commit 8277caad74d0df8301dc0dda9f4a3af36258929b

+ 29 - 2
deps/util.lua

@@ -1,5 +1,13 @@
-f = {}
-f.exe = function(x, ...) if type(x) == 'function' then return x(...) end return x end
+function table.interpolate(t1, t2, x)
+  local res = {}
+  for k, v in pairs(t2) do
+    if not t1[k] then return t2[k] end
+    res[k] = lume.lerp(t1[k], t2[k], x)
+  end
+  return res
+end
+
+function math.clamp(x, l, h) return math.min(math.max(x, l), h) end
 
 
 function math.hlola(x1, y1, x2, y2, x3, y3, x4, y4) -- Hot line on line action (boolean).
 function math.hlola(x1, y1, x2, y2, x3, y3, x4, y4) -- Hot line on line action (boolean).
   local function s(x1, y1, x2, y2, x3, y3)
   local function s(x1, y1, x2, y2, x3, y3)
@@ -15,3 +23,22 @@ function math.hlora(x1, y1, x2, y2, rx, ry, rw, rh) -- Hot line on rectangle act
       or math.hlola(x1, y1, x2, y2, rxw, ry, rxw, ryh)
       or math.hlola(x1, y1, x2, y2, rxw, ry, rxw, ryh)
       or math.hlola(x1, y1, x2, y2, rx, ryh, rxw, ryh)
       or math.hlola(x1, y1, x2, y2, rx, ryh, rxw, ryh)
 end
 end
+
+physics = {}
+function physics.draw(how, obj)
+  if obj.shape:typeOf('PolygonShape') then
+    love.graphics.polygon(how, obj.body:getWorldPoints(obj.shape:getPoints()))
+  end
+end
+
+timer = {}
+function timer.rot(v, fn)
+ if v > 0 then
+   v = v - ls.tickrate
+   if v <= 0 then
+     v = 0
+     v = lume.call(fn) or 0
+   end
+ end
+ return v
+end

+ 256 - 0
deps/view.lua

@@ -0,0 +1,256 @@
+View = class()
+
+local g = love.graphics
+
+function View:init()
+  self.x = 0
+  self.y = 0
+  self.width = 800
+  self.height = 600
+  self.xmin = 0
+  self.ymin = 0
+  self.xmax = self.width
+  self.ymax = self.height
+
+  self.frame = {}
+  self.frame.x = 0
+  self.frame.y = 0
+  self.frame.width = love.graphics.getWidth()
+  self.frame.height = love.graphics.getHeight()
+
+  self.vx = 0
+  self.vy = 0
+
+  self.viewId = 0
+  self.draws = {}
+  self.guis = {}
+  self.effects = {}
+  self.toRemove = {}
+  self.target = nil
+
+  self:resize()
+
+  self.prevx = 0
+  self.prevy = 0
+  self.prevscale = self.scale
+
+  self.shake = 0
+
+  ctx.event:on('view.register', function(data)
+    self:register(data.object, data.mode)
+  end)
+
+  ctx.event:on('view.unregister', function(data)
+    self:unregister(data.object)
+  end)
+end
+
+function View:update()
+  self.prevx = self.x
+  self.prevy = self.y
+  self.prevscale = self.scale
+
+  self.x = self.x + self.vx * ls.tickrate
+  self.y = self.y + self.vy * ls.tickrate
+
+  self:follow()
+  self:contain()
+
+  self.shake = lume.lerp(self.shake, 0, 8 * ls.tickrate)
+
+  while #self.toRemove > 0 do
+    local x = self.toRemove[1]
+
+    if x.draw then
+      for i = 1, #self.draws do
+        if self.draws[i] == x then table.remove(self.draws, i) i = #self.draws + 1 end
+      end
+    end
+
+    if x.gui then
+      for i = 1, #self.guis do
+        if self.guis[i] == x then table.remove(self.guis, i) i = #self.guis + 1 end
+      end
+    end
+
+    for i = 1, #self.effects do
+      if self.effects[i] == x then table.remove(self.effects, i) i = #self.effects + 1 end
+    end
+
+    table.remove(self.toRemove, 1)
+  end
+
+  table.sort(self.draws, function(a, b)
+    return a.depth == b.depth and a.viewId < b.viewId or a.depth > b.depth
+  end)
+end
+
+function View:draw()
+  local w, h = g.getDimensions()
+  local source, target = self.sourceCanvas, self.targetCanvas
+
+  self:worldPush()
+
+  g.setCanvas(source)
+  for i = 1, #self.draws do self.draws[i]:draw() end
+  g.setCanvas()
+  g.pop()
+
+  for i = 1, #self.effects do
+    local effect = self.effects[i]
+    if effect.active then
+      g.setColor(255, 255, 255)
+      if effect.applyEffect then
+        effect:applyEffect(source, target)
+      else
+        g.setShader(effect.shader)
+        g.setCanvas(target)
+        g.draw(source)
+      end
+      g.setShader()
+      source, target = target, source
+    end
+  end
+
+  g.setCanvas()
+  g.setColor(255, 255, 255)
+  g.draw(source)
+
+  g.push()
+
+  local fr = self.frame
+  local fx, fy, fw, fh = fr.x, fr.y, fr.width, fr.height
+  g.translate(fx, fy)
+
+  for i = 1, #self.guis do self.guis[i]:gui() end
+
+  g.pop()
+
+  g.setColor(0, 0, 0)
+  g.rectangle('fill', 0, 0, w, fy)
+  g.rectangle('fill', 0, 0, fx, h)
+  g.rectangle('fill', 0, fy + fh, w, h - (fy + fh))
+  g.rectangle('fill', fx + fw, 0, w - (fx + fw), h)
+end
+
+function View:resize()
+  local w, h = love.graphics.getDimensions()
+  local ratio = w / h
+
+  self.frame.x, self.frame.y, self.frame.width, self.frame.height = 0, 0, self.width, self.height
+  if (self.width / self.height) > (w / h) then
+    self.scale = w / self.width
+    local margin = math.max(lume.round(((h - w * (self.height / self.width)) / 2)), 0)
+    self.frame.y = margin
+    self.frame.height = h - 2 * margin
+    self.frame.width = w
+  else
+    self.scale = h / self.height
+    local margin = math.max(lume.round(((w - h * (self.width / self.height)) / 2)), 0)
+    self.frame.x = margin
+    self.frame.width = w - 2 * margin
+    self.frame.height = h
+  end
+
+  self.sourceCanvas = love.graphics.newCanvas(w, h)
+  self.targetCanvas = love.graphics.newCanvas(w, h)
+end
+
+function View:register(x, action)
+  x.viewId = self.viewId
+  action = action or 'draw'
+  if action == 'draw' then
+    table.insert(self.draws, x)
+    x.depth = x.depth or 0
+  elseif action == 'gui' then
+    table.insert(self.guis, x)
+  elseif action == 'effect' then
+    table.insert(self.effects, x)
+  end
+
+  self.viewId = self.viewId + 1
+end
+
+function View:unregister(x)
+  table.insert(self.toRemove, x)
+end
+
+function View:convertZ(z)
+  return (.8 * z) ^ (1 + (.0008 * z))
+end
+
+function View:three(x, y, z)
+  local sx, sy = lume.lerp(self.prevx, self.x, ls.accum / ls.tickrate), lume.lerp(self.prevy, self.y, ls.accum / ls.tickrate)
+  z = self:convertZ(z)
+  return x - (z * ((sx + self.width / 2 - x) / 500)), y - (z * ((sy + self.height / 2 - y) / 500))
+end
+
+function View:threeDepth(x, y, z)
+  return math.clamp(lume.distance(x, y, self.x + self.width / 2, self.y + self.height / 2) * self.scale - 1000 - z, -4096, -16)
+end
+
+function View:follow()
+  if not self.target then return end
+
+  local margin = 0.5
+
+  self.x = self.target.x - self.width * margin
+  self.y = self.target.y - self.height * margin
+end
+
+function View:contain()
+  self.x = math.clamp(self.x, 0, self.xmax - self.width)
+  self.y = math.clamp(self.y, 0, self.ymax - self.height)
+end
+
+function View:worldPoint(x, y)
+  x = lume.round(((x - self.frame.x) / self.scale) + self.x)
+  if y then y = lume.round(((y - self.frame.y) / self.scale) + self.y) end
+  return x, y
+end
+
+function View:screenPoint(x, y)
+  local vx, vy = lume.lerp(self.prevx, self.x, ls.accum / ls.tickrate), lume.lerp(self.prevy, self.y, ls.accum / ls.tickrate)
+  x = (x - vx) * self.scale
+  if y then y = (y - vy) * self.scale end
+  return x, y
+end
+
+function View:worldMouseX()
+  return lume.round(((love.mouse.getX() - self.frame.x) / self.scale) + self.x)
+end
+
+function View:worldMouseY()
+  return lume.round(((love.mouse.getY() - self.frame.y) / self.scale) + self.y)
+end
+
+function View:frameMouseX()
+  return love.mouse.getX() - self.frame.x
+end
+
+function View:frameMouseY()
+  return love.mouse.getY() - self.frame.y
+end
+
+function View:screenshake(amount)
+  if self.shake > amount then self.shake = self.shake + (amount / 2) end
+  self.shake = amount
+end
+
+function View:worldPush()
+  local x, y, s = unpack(table.interpolate({self.prevx, self.prevy, self.prevscale}, {self.x, self.y, self.scale}, ls.accum / ls.tickrate))
+  local shakex = 1 - (2 * love.math.noise(self.shake + x + ls.accum))
+  local shakey = 1 - (2 * love.math.noise(self.shake + y + ls.accum))
+  x = x + (shakex * self.shake)
+  y = y + (shakey * self.shake)
+
+  g.push()
+  g.translate(self.frame.x, self.frame.y)
+  g.scale(s)
+  g.translate(-x, -y)
+end
+
+function View:guiPush()
+  g.push()
+  g.translate(self.frame.x, self.frame.y)
+end

+ 23 - 18
game.lua

@@ -1,9 +1,16 @@
 Game = class()
 Game = class()
 
 
 function Game:load()
 function Game:load()
+  self.event = Event()
+  self.world = love.physics.newWorld(0, 1000)
+  self.view = View()
+  self.map = Map(ctx)
   self.pigeon = Pigeon()
   self.pigeon = Pigeon()
-  self.people = {Person()}
+  self.people = {}
+  self.buildings = {}
   self.hud = Hud()
   self.hud = Hud()
+
+  lume.push(self.buildings, Building(650, 60, 100), Building(800, 60, 200))
 end
 end
 
 
 function Game:update()
 function Game:update()
@@ -12,37 +19,35 @@ function Game:update()
     person:update()
     person:update()
   end)
   end)
 
 
-  if love.math.random() < .5 * ls.tickrate then
-    local p = Person()
+  --[[lume.each(self.buildings, function(building)
+    building:update()
+  end)]]
+
+  if love.math.random() < .7 * ls.tickrate then
+    local x, dir
 
 
     if love.math.random() < .5 then
     if love.math.random() < .5 then
-      p.x = 0
-      p.direction = 1
+      x = 0
+      dir = 1
     else
     else
-      p.x = 800
-      p.direction = -1
+      x = 800
+      dir = -1
     end
     end
 
 
-    lume.push(self.people, p)
+    lume.push(self.people, Person(x, 400, dir))
   end
   end
+
+  self.world:update(ls.tickrate)
+  self.view:update()
 end
 end
 
 
 function Game:draw()
 function Game:draw()
-  local g = love.graphics
   flux.update(ls.dt)
   flux.update(ls.dt)
-  g.setColor(0, 50, 0)
-  g.rectangle('fill', 0, 0, 800, 600)
-  self.pigeon:draw()
-  lume.each(self.people, function(person)
-    person:draw()
-  end)
-  self.hud:draw()
+  self.view:draw()
 end
 end
 
 
 function Game:keypressed(key)
 function Game:keypressed(key)
   if key == 'escape' then
   if key == 'escape' then
     love.event.quit()
     love.event.quit()
   end
   end
-
-  self.pigeon:keypressed(key)
 end
 end

+ 2 - 1
hud.lua

@@ -2,9 +2,10 @@ Hud = class()
 
 
 function Hud:init()
 function Hud:init()
   self.font = love.graphics.newFont(12)
   self.font = love.graphics.newFont(12)
+  ctx.event:emit('view.register', {object = self, mode = 'gui'})
 end
 end
 
 
-function Hud:draw()
+function Hud:gui()
   local g = love.graphics
   local g = love.graphics
   g.setColor(255, 255, 255)
   g.setColor(255, 255, 255)
 
 

+ 116 - 0
loader.lua

@@ -0,0 +1,116 @@
+data = {}
+data.load = function()
+
+  -- Media
+	local function lookup(ext, fn)
+		local function halp(s, k)
+			local base = s._path .. '/' .. k
+      local function extLoad(ext)
+        if love.filesystem.exists(base .. ext) then
+          s[k] = fn(base .. ext)
+        elseif love.filesystem.isDirectory(base) then
+          local t = {}
+          t._path = base
+          setmetatable(t, {__index = halp})
+          s[k] = t
+        else
+          return false
+        end
+
+        return true
+      end
+
+      if type(ext) == 'table' then
+        lume.each(ext, function(e) return extLoad(e) end)
+      else
+        extLoad(ext)
+      end
+
+			return rawget(s, k)
+		end
+
+		return halp
+	end
+
+  data.media = {}
+	data.media.graphics = setmetatable({_path = 'media/graphics'}, {__index = lookup({'.png', '.dds'}, love.graphics and love.graphics.newImage or f.empty)})
+
+  -- Data
+  local function load(dir, type, fn)
+    local id = 1
+    local function halp(dir, dst)
+      for _, file in ipairs(love.filesystem.getDirectoryItems(dir)) do
+        path = dir .. '/' .. file
+        if love.filesystem.isDirectory(path) then
+          dst[file] = {}
+          halp(path, dst[file])
+        elseif file:match('%.lua$') and not file:match('^%.') then
+          local obj = love.filesystem.load(path)()
+          assert(obj, path .. ' did not return a value')
+          obj.code = obj.code or file:gsub('%.lua', '')
+          obj.id = id
+          obj = lume.call(fn, obj) or obj
+          data[type][id] = obj
+          dst[obj.code] = obj
+          id = id + 1
+        end
+      end
+    end
+
+    data[type] = {}
+    halp(dir, data[type])
+  end
+
+  load('data/animation', 'animation', function(animation)
+
+    -- Set up lazy loading for images
+    local code = animation.code
+    animation.graphics = setmetatable({_path = 'media/skeletons/' .. code}, {
+      __index = lookup({'.png', '.dds'}, function(path)
+        local img = love.graphics.newImage(path)
+        if path:match('%.dds') then img:setMipmapFilter('nearest', 1) end
+        return img
+      end)
+    })
+
+    -- Set up static spine data structures
+    local s = {}
+    s.__index = s
+    if love.filesystem.exists('media/skeletons/' .. code .. '/' .. code .. '.atlas') then
+      s.atlas = spine.Atlas.new('media/skeletons/' .. code .. '/' .. code .. '.atlas')
+      s.atlasAttachmentLoader = spine.AtlasAttachmentLoader.new(s.atlas)
+    end
+    s.json = spine.SkeletonJson.new(s.atlasAttachmentLoader)
+    s.skeletonData = s.json:readSkeletonDataFile('media/skeletons/' .. code .. '/' .. code .. '.json')
+    s.animationStateData = spine.AnimationStateData.new(s.skeletonData)
+
+    -- Reverse-index keys (sorted for consistent order)
+    local keys = lume.keys(animation.states)
+    table.sort(keys)
+
+    for i = 1, #keys do
+      local state = animation.states[keys[i]]
+      animation.states[i] = state
+      state.index = i
+      state.name = keys[i]
+    end
+
+    -- Set mixes
+    for i = 1, #animation.states do
+      lume.each(animation.states, function(state)
+        if state.index ~= i then
+          s.animationStateData:setMix(animation.states[i].name, state.name, Animation.defaultMix)
+        end
+      end)
+
+      lume.each(animation.states[i].mix or {}, function(time, to)
+        s.animationStateData:setMix(animation.states[i].name, to, time)
+      end)
+    end
+
+    animation.spine = s
+
+    return animation
+  end)
+end
+

+ 1 - 0
main.lua

@@ -1,5 +1,6 @@
 require 'require'
 require 'require'
 
 
 function love.load()
 function love.load()
+  data.load()
   Context:bind(Game)
   Context:bind(Game)
 end
 end

+ 25 - 0
map.lua

@@ -0,0 +1,25 @@
+Map = class()
+Map.width = 1600
+Map.height = 600
+
+function Map:init()
+  self.ground = {}
+  self.ground.height = 100
+  self.ground.body = love.physics.newBody(ctx.world, self.width / 2, self.height - self.ground.height / 2, 'static')
+  self.ground.shape = love.physics.newRectangleShape(self.width, self.ground.height)
+
+  self.ground.fixture = love.physics.newFixture(self.ground.body, self.ground.shape)
+
+  ctx.view.xmax = self.width
+  ctx.view.ymax = self.height
+  ctx.event:emit('view.register', {object = self})
+end
+
+function Map:draw()
+  local g = love.graphics
+  g.setColor(0, 50, 0)
+  g.rectangle('fill', 0, 0, self.width, self.height)
+
+  g.setColor(100, 80, 0)
+  physics.draw('fill', self.ground)
+end

BIN
media/skeletons/pigeon/beakbottom.png


BIN
media/skeletons/pigeon/beakclosed.png


BIN
media/skeletons/pigeon/beakopen.png


BIN
media/skeletons/pigeon/beaktop.png


BIN
media/skeletons/pigeon/body1.png


BIN
media/skeletons/pigeon/body2.png


BIN
media/skeletons/pigeon/body3.png


BIN
media/skeletons/pigeon/body4.png


BIN
media/skeletons/pigeon/head.png


BIN
media/skeletons/pigeon/leftclaw1.png


BIN
media/skeletons/pigeon/leftclaw2.png


BIN
media/skeletons/pigeon/leftclaw3.png


BIN
media/skeletons/pigeon/leftfoot.png


BIN
media/skeletons/pigeon/leftshin.png


BIN
media/skeletons/pigeon/leftthigh.png


BIN
media/skeletons/pigeon/leftwing.png


BIN
media/skeletons/pigeon/neck1.png


BIN
media/skeletons/pigeon/neck2.png


BIN
media/skeletons/pigeon/neck3.png


BIN
media/skeletons/pigeon/neck4.png


BIN
media/skeletons/pigeon/pupil.png


BIN
media/skeletons/pigeon/rightclaw1.png


BIN
media/skeletons/pigeon/rightclaw2.png


BIN
media/skeletons/pigeon/rightclaw3.png


BIN
media/skeletons/pigeon/rightfoot.png


BIN
media/skeletons/pigeon/rightshin.png


BIN
media/skeletons/pigeon/rightthigh.png


BIN
media/skeletons/pigeon/rightwing.png


+ 53 - 7
person.lua

@@ -1,21 +1,67 @@
 Person = class()
 Person = class()
 
 
-function Person:init()
-  self.x = 800
-  self.direction = -1
-  self.y = 500
+Person.category = 3
+
+function Person:init(x, y, dir)
+  self.x = x
+  self.y = y
+  self.direction = dir
   self.w = 20
   self.w = 20
   self.h = 40
   self.h = 40
+
+  self.body = love.physics.newBody(ctx.world, self.x - self.w / 2, self.y - self.h / 2, 'dynamic')
+  self.shape = love.physics.newRectangleShape(self.w, self.h)
+  self.fixture = love.physics.newFixture(self.body, self.shape)
+
+  self.body:setUserData(self)
+
+  self.body:setMass(100)
+  self.body:setFixedRotation(true)
+  self.fixture:setFriction(.35)
+  self.fixture:setCategory(self.category)
+  self.fixture:setMask(self.category, Building.category)
+
+  self.dead = false
+  self.walkTimer = 1
+  self.deathTimer = 0
+
+  ctx.event:emit('view.register', {object = self})
 end
 end
 
 
 function Person:update()
 function Person:update()
-  self.x = self.x + 100 * self.direction * ls.tickrate
+  if not self.dead then
+    self.walkTimer = timer.rot(self.walkTimer, function()
+      self.body:applyLinearImpulse(self.direction * 50, -100)
+      return .6 + love.math.random() * .2
+    end)
+  end
+
+  if self.dead then
+    local x, y = self.body:getLinearVelocity()
+    if (math.abs(x) < 1 and math.abs(y) < 1) or (math.abs(x) > 5000 and math.abs(y) > 5000) then
+      lume.remove(ctx.people, self)
+      self.body:destroy()
+      ctx.event:emit('view.unregister', {object = self})
+    end
+  end
 end
 end
 
 
 function Person:draw()
 function Person:draw()
   local g = love.graphics
   local g = love.graphics
+
+  g.setColor(255, 255, 255, 35)
+  physics.draw('fill', self)
+
   g.setColor(255, 255, 255)
   g.setColor(255, 255, 255)
-  g.rectangle('line', self.x - self.w / 2, self.y - self.h / 2, self.w, self.h)
+  physics.draw('line', self)
 end
 end
 
 
-
+function Person:die()
+  if not self.dead then
+    self.dead = true
+    self.body:applyLinearImpulse(-500 + love.math.random() * 1000, love.math.random() * -800)
+    self.body:applyTorque(-200 * love.math.random() * 400)
+    self.body:setFixedRotation(false)
+    self.deathTimer = 1
+  end
+end

+ 110 - 54
pigeon.lua

@@ -13,73 +13,91 @@ function Pigeon:init()
   self.laser = false
   self.laser = false
   self.laserLength = 0
   self.laserLength = 0
 
 
-  self.gravity = 0--180
-  self.jumpspeed = 120
-  self.vy = 0
-
   self.lives = 3
   self.lives = 3
   self.health = 100
   self.health = 100
   self.maxHealth = 100
   self.maxHealth = 100
+
+  self.animation = data.animation.pigeon()
+  self.animation:set('idle')
+
+  self.animation:on('complete', function(event)
+    if event.state.name == 'peck' then
+      self.animation:set('idle', {force = true})
+    end
+  end)
+
+  ctx.view.target = self
+
+  ctx.event:emit('view.register', {object = self})
 end
 end
 
 
 function Pigeon:update()
 function Pigeon:update()
   self.prevx = self.x
   self.prevx = self.x
   self.prevy = self.y
   self.prevy = self.y
 
 
-  if love.keyboard.isDown('left') then
-    self.x = self.x - self.speed * ls.tickrate
-    self.targetDirection.x = -1
-  elseif love.keyboard.isDown('right') then
-    self.x = self.x + self.speed * ls.tickrate
-    self.targetDirection.x = 1
-  end
-
-  if love.keyboard.isDown('up') then
-    self.targetDirection.y = -1
-    self.y = self.y - self.speed * ls.tickrate
-  elseif love.keyboard.isDown('down') then
-    self.targetDirection.y = 1
-    self.y = self.y + self.speed * ls.tickrate
-  else
-    self.targetDirection.y = 0
-  end
-
-  flux.to(self.direction, .4, self.targetDirection):ease('expoout')
+  -- Movement
+  if self.animation.state.name ~= 'peck' then
+    if love.keyboard.isDown('left') then
+      self.x = self.x - self.speed * ls.tickrate
+      self.targetDirection.x = -1
+      self.animation.flipped = true
+    elseif love.keyboard.isDown('right') then
+      self.x = self.x + self.speed * ls.tickrate
+      self.targetDirection.x = 1
+      self.animation.flipped = false
+    end
 
 
-  self.vy = self.vy + self.gravity * ls.tickrate
-  self.y = self.y + self.vy * ls.tickrate
+    if love.keyboard.isDown('up') then
+      self.targetDirection.y = -1
+    elseif love.keyboard.isDown('down') then
+      self.targetDirection.y = 1
+    else
+      self.targetDirection.y = 0
+    end
 
 
-  if self.y + self.h / 2 > 600 then
-    self.vy = math.abs(self.vy) * -1
+    flux.to(self.direction, .4, self.targetDirection):ease('expoout')
   end
   end
 
 
-  self.laser = love.keyboard.isDown(' ')
-
-  if self.laser then
-    self.laserTween = flux.to(self, 1, {laserLength = 1000}):ease('expoout')
-    local kills = 0
-
-    local x1, y1 = self.x, self.y
-    local x2, y2 = self.x + self.direction.x * self.laserLength, self.y + self.direction.y * self.laserLength
-
-    for i, person in lume.ripairs(ctx.people) do
-      if math.hlora(x1, y1, x2, y2, person.x - person.w / 2, person.y - person.h / 2, person.w, person.h) then
-        kills = kills + 1
-        table.remove(ctx.people, i)
+  local kills = 0
+
+  -- Laser
+  do
+    self.laser = love.keyboard.isDown(' ')
+    self.laser = false
+
+    if self.laser then
+      self.laserTween = flux.to(self, 1, {laserLength = 1000}):ease('expoout')
+
+      if self.laserLength > 0 then
+        local x1, y1 = self.x, self.y - self.h / 2
+        local x2, y2 = self.x + self.direction.x * self.laserLength, self.y + self.direction.y * self.laserLength - self.h / 2
+
+        ctx.world:rayCast(x1, y1, x2, y2, function(fixture)
+          local person = fixture:getBody():getUserData()
+          if person and person.die then
+            person:die()
+            lume.remove(ctx.people, person)
+            kills = kills + 1
+            return 1
+          end
+          return -1
+        end)
+      end
+    else
+      if self.laserTween then
+        self.laserTween:stop()
+        self.laserLength = 0
       end
       end
     end
     end
+  end
 
 
-    if kills > 0 then
-      flux.to(self, .6, {w = self.w + 5 * kills, h = self.h + 10 * kills}):ease('elasticout')
-      self.health = math.min(self.health + 15 * kills, self.maxHealth)
-    end
-  else
-    if self.laserTween then
-      self.laserTween:stop()
-      self.laserLength = 0
-    end
+  -- Increase size and health on kill
+  if kills > 0 then
+    flux.to(self, .6, {w = self.w + 5 * kills, h = self.h + 10 * kills}):ease('elasticout')
+    self.health = math.min(self.health + 15 * kills, self.maxHealth)
   end
   end
 
 
+  -- Death
   if self.health < 0 then
   if self.health < 0 then
     if self.lives == 0 then
     if self.lives == 0 then
       print('you lose')
       print('you lose')
@@ -93,7 +111,14 @@ function Pigeon:update()
     self.h = self.h / 2
     self.h = self.h / 2
   end
   end
 
 
+  -- Health decay
   self.health = self.health - 10 * ls.tickrate
   self.health = self.health - 10 * ls.tickrate
+
+  if love.keyboard.isDown(' ') then self.animation:set('peck') end
+
+  if self.animation.state.name == 'peck' then
+    self:killThingsOnBeak()
+  end
 end
 end
 
 
 function Pigeon:draw()
 function Pigeon:draw()
@@ -101,20 +126,51 @@ function Pigeon:draw()
   local x = lume.lerp(self.prevx, self.x, ls.accum / ls.tickrate)
   local x = lume.lerp(self.prevx, self.x, ls.accum / ls.tickrate)
   local y = lume.lerp(self.prevy, self.y, ls.accum / ls.tickrate)
   local y = lume.lerp(self.prevy, self.y, ls.accum / ls.tickrate)
   g.setColor(255, 255, 255)
   g.setColor(255, 255, 255)
-  g.rectangle('line', x - self.w / 2, y - self.h / 2, self.w, self.h)
+  g.rectangle('line', x - self.w / 2, y - self.h, self.w, self.h)
 
 
   if self.laser then
   if self.laser then
     local x2, y2 = x + self.direction.x * self.laserLength, y + self.direction.y * self.laserLength
     local x2, y2 = x + self.direction.x * self.laserLength, y + self.direction.y * self.laserLength
 
 
     g.setColor(255, 0, 0)
     g.setColor(255, 0, 0)
     g.setLineWidth(self.w / 5)
     g.setLineWidth(self.w / 5)
-    g.line(x, y, x2, y2)
+    g.line(x, y - self.h / 2, x2, y2 - self.h / 2)
     g.setLineWidth(1)
     g.setLineWidth(1)
   end
   end
+
+  self.animation:draw(x, y)
 end
 end
 
 
-function Pigeon:keypressed(key)
-  if key == 'z' then
-    self.vy = -self.jumpspeed
+-- Kill everything near the beak. Uses very dumb AABB checking but can be improved.
+function Pigeon:killThingsOnBeak()
+  local spine = self.animation.spine
+
+  spine.skeleton.flipY = true
+  spine.skeleton:updateWorldTransform()
+  spine.skeletonBounds:update(spine.skeleton)
+  spine.skeleton.flipY = false
+
+  for _, slotName in pairs({'beakbottom', 'beaktop'}) do
+    local beakSlot = spine.skeleton:findSlot(slotName)
+    local beakAttachment = spine.skeleton:getAttachment(beakSlot.data.name, beakSlot.data.name .. '_bb')
+    local polygon = spine.skeletonBounds:getPolygon(beakAttachment)
+
+    if polygon then
+      local x1, y1, x2, y2
+      for i = 1, #polygon, 2 do
+        x1 = math.min(x1 or math.huge, polygon[i])
+        x2 = math.max(x2 or -math.huge, polygon[i])
+        y1 = math.min(y1 or math.huge, polygon[i + 1])
+        y2 = math.max(y2 or -math.huge, polygon[i + 1])
+      end
+
+      ctx.world:queryBoundingBox(x1, y1, x2, y2, function(fixture)
+        local person = fixture:getBody():getUserData()
+        if person and person.die then
+          person:die()
+
+          return true
+        end
+      end)
+    end
   end
   end
 end
 end

+ 8 - 0
require.lua

@@ -1,13 +1,21 @@
 require 'deps/class'
 require 'deps/class'
+require 'deps/spine/love/spine'
 flux = require 'deps/flux'
 flux = require 'deps/flux'
 lume = require 'deps/lume'
 lume = require 'deps/lume'
 lurker = require 'deps/lurker'
 lurker = require 'deps/lurker'
 ls = require 'deps/lovestep/lovestep'
 ls = require 'deps/lovestep/lovestep'
 require 'deps/util'
 require 'deps/util'
+require 'deps/animation'
+require 'deps/event'
+require 'deps/view'
 
 
 require 'context'
 require 'context'
 require 'game'
 require 'game'
 require 'person'
 require 'person'
+require 'building'
 require 'pigeon'
 require 'pigeon'
+require 'map'
 
 
 require 'hud'
 require 'hud'
+
+require 'loader'