瀏覽代碼

Honestly the game is basically done now;

bjorn 10 年之前
父節點
當前提交
8d10bf1dc3
共有 13 個文件被更改,包括 1212 次插入0 次删除
  1. 3 0
      .gitmodules
  2. 62 0
      context.lua
  3. 15 0
      deps/class.lua
  4. 1 0
      deps/lovestep
  5. 747 0
      deps/lume.lua
  6. 249 0
      deps/lurker.lua
  7. 1 0
      deps/spine-runtimes
  8. 2 0
      deps/util.lua
  9. 30 0
      game.lua
  10. 5 0
      main.lua
  11. 20 0
      person.lua
  12. 67 0
      pigeon.lua
  13. 10 0
      require.lua

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "deps/spine-runtimes"]
+	path = deps/spine-runtimes
+	url = https://github.com/bjornswenson/spine-runtimes

+ 62 - 0
context.lua

@@ -0,0 +1,62 @@
+Context = {
+  list = {},
+  started = false
+}
+
+function Context:add(obj, ...)
+  local c = obj(...)
+  table.insert(self.list, c)
+
+  local tmp = ctx
+  ctx = c
+  f.exe(ctx.load, ctx, ...)
+  ctx = tmp
+
+  return c
+end
+
+function Context:run(key, ...)
+  for i = #self.list, 1, -1 do
+    ctx = self.list[i]
+    if key == 'update' then
+      ctx.tick = (ctx.tick or 0) + 1
+    end
+    tick = ctx.tick or 0
+    if ctx[key] then ctx[key](ctx, ...) end
+    ctx = nil
+    tick = nil
+  end
+end
+
+function Context:remove(ctx, ...)
+  for i = 1, #self.list do
+    if self.list[i] == ctx then
+      f.exe(ctx.unload, ctx, ...)
+      table.remove(self.list, i)
+      collectgarbage()
+      return
+    end
+  end
+end
+
+function Context:clear()
+  table.each(self.list, f.cur(self.remove, self))
+end
+
+function Context:bind(initial, ...)
+  love.update = Context.update
+  love.draw = Context.draw
+  love.quit = Context.quit
+
+  love.handlers = setmetatable({}, {__index = Context})
+
+  if initial then
+    Context:add(initial, ...)
+  end
+end
+
+setmetatable(Context, {
+  __index = function(t, k)
+    return function(...) return t:run(k, ...) end
+  end
+})

+ 15 - 0
deps/class.lua

@@ -0,0 +1,15 @@
+function new(x, ...)
+  local t = extend(x)
+  if t.init then
+    t:init(...)
+  end
+  return t
+end
+
+function extend(x)
+  local t = {}
+  setmetatable(t, {__index = x, __call = new})
+  return t
+end
+
+function class() return extend() end

+ 1 - 0
deps/lovestep

@@ -0,0 +1 @@
+Subproject commit 813c93e6a98618dfb4157cc42c701d59f3aafdc8

+ 747 - 0
deps/lume.lua

@@ -0,0 +1,747 @@
+--
+-- lume
+--
+-- Copyright (c) 2015 rxi
+--
+-- This library is free software; you can redistribute it and/or modify it
+-- under the terms of the MIT license. See LICENSE for details.
+--
+
+local lume = { _version = "2.1.0" }
+
+local pairs, ipairs = pairs, ipairs
+local type, assert, unpack = type, assert, unpack or table.unpack
+local tostring, tonumber = tostring, tonumber
+local math_floor = math.floor
+local math_ceil = math.ceil
+local math_random = math.random
+local math_cos = math.cos
+local math_atan2 = math.atan2 or math.atan
+local math_sqrt = math.sqrt
+local math_abs = math.abs
+local math_pi = math.pi
+
+local noop = function()
+end
+
+local identity = function(x)
+  return x
+end
+
+local patternescape = function(str)
+  return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1")
+end
+
+local absindex = function(len, i)
+  return i < 0 and (len + i + 1) or i
+end
+
+local iscallable = function(x)
+  if type(x) == "function" then return true end
+  local mt = getmetatable(x)
+  return mt and mt.__call ~= nil
+end
+
+local isarray = function(x)
+  return (type(x) == "table" and x[1] ~= nil) and true or false
+end
+
+local getiter = function(x)
+  if isarray(x) then
+    return ipairs
+  elseif type(x) == "table" then
+    return pairs
+  end
+  error("expected table", 3)
+end
+
+local iteratee = function(x)
+  if x == nil then return identity end
+  if iscallable(x) then return x end
+  if type(x) == "table" then
+    return function(z)
+      for k, v in pairs(x) do
+        if z[k] ~= v then return false end
+      end
+      return true
+    end
+  end
+  return function(z) return z[x] end
+end
+
+
+
+function lume.clamp(x, min, max)
+  return x < min and min or (x > max and max or x)
+end
+
+
+function lume.round(x, increment)
+  if increment then return lume.round(x / increment) * increment end
+  return x >= 0 and math_floor(x + .5) or math_ceil(x - .5)
+end
+
+
+function lume.sign(x)
+  return x < 0 and -1 or 1
+end
+
+
+function lume.lerp(a, b, amount)
+  return a + (b - a) * lume.clamp(amount, 0, 1)
+end
+
+
+function lume.smooth(a, b, amount)
+  local t = lume.clamp(amount, 0, 1)
+  local m = t * t * (3 - 2 * t)
+  return a + (b - a) * m
+end
+
+
+function lume.pingpong(x)
+  return 1 - math_abs(1 - x % 2)
+end
+
+
+function lume.distance(x1, y1, x2, y2, squared)
+  local dx = x1 - x2
+  local dy = y1 - y2
+  local s = dx * dx + dy * dy
+  return squared and s or math_sqrt(s)
+end
+
+
+function lume.angle(x1, y1, x2, y2)
+  return math_atan2(y2 - y1, x2 - x1)
+end
+
+
+function lume.random(a, b)
+  if not a then a, b = 0, 1 end
+  if not b then b = 0 end
+  return a + math_random() * (b - a)
+end
+
+
+function lume.randomchoice(t)
+  return t[math_random(#t)]
+end
+
+
+function lume.weightedchoice(t)
+  local sum = 0
+  for k, v in pairs(t) do
+    assert(v >= 0, "weight value less than zero")
+    sum = sum + v
+  end
+  assert(sum ~= 0, "all weights are zero")
+  local rnd = lume.random(sum)
+  for k, v in pairs(t) do
+    if rnd < v then return k end
+    rnd = rnd - v
+  end
+end
+
+
+function lume.push(t, ...)
+  local n = select("#", ...)
+  for i = 1, n do
+    t[#t + 1] = select(i, ...)
+  end
+  return ...
+end
+
+
+function lume.remove(t, x) 
+  local iter = getiter(t)
+  for i, v in iter(t) do
+    if v == x then
+      if isarray(t) then
+        table.remove(t, i)
+        break
+      else
+        t[i] = nil
+        break
+      end
+    end
+  end
+  return x
+end
+
+
+function lume.clear(t)
+  local iter = getiter(t)
+  for k, v in iter(t) do
+    t[k] = nil
+  end
+  return t
+end
+
+
+function lume.extend(t, ...)
+  for i = 1, select("#", ...) do
+    local x = select(i, ...)
+    if x then
+      for k, v in pairs(x) do
+        t[k] = v
+      end
+    end
+  end
+  return t
+end
+
+
+function lume.shuffle(t)
+  local rtn = {}
+  for i = 1, #t do
+    local r = math_random(i)
+    if r ~= i then
+      rtn[i] = rtn[r]
+    end
+    rtn[r] = t[i]
+  end
+  return rtn
+end
+
+
+function lume.sort(t, comp)
+  local rtn = lume.clone(t)
+  if comp then
+    if type(comp) == "string" then
+      table.sort(rtn, function(a, b) return a[comp] < b[comp] end)
+    else
+      table.sort(rtn, comp)
+    end
+  else
+    table.sort(rtn)
+  end
+  return rtn
+end
+
+
+function lume.array(...)
+  local t = {}
+  for x in ... do t[#t + 1] = x end
+  return t
+end
+
+
+function lume.each(t, fn, ...)
+  local iter = getiter(t)
+  if type(fn) == "string" then
+    for _, v in iter(t) do v[fn](v, ...) end
+  else
+    for _, v in iter(t) do fn(v, ...) end
+  end
+  return t
+end
+
+
+function lume.map(t, fn)
+  fn = iteratee(fn)
+  local iter = getiter(t)
+  local rtn = {}
+  for k, v in iter(t) do rtn[k] = fn(v) end
+  return rtn
+end
+
+
+function lume.all(t, fn)
+  fn = iteratee(fn)
+  local iter = getiter(t)
+  for k, v in iter(t) do
+    if not fn(v) then return false end
+  end
+  return true
+end
+
+
+function lume.any(t, fn)
+  fn = iteratee(fn)
+  local iter = getiter(t)
+  for k, v in iter(t) do
+    if fn(v) then return true end
+  end
+  return false
+end
+
+
+function lume.reduce(t, fn, first)
+  local acc = first
+  local started = first and true or false
+  local iter = getiter(t)
+  for k, v in iter(t) do
+    if started then
+      acc = fn(acc, v)
+    else
+      acc = v
+      started = true
+    end
+  end
+  assert(started, "reduce of an empty table with no first value")
+  return acc
+end
+
+
+function lume.set(t)
+  local rtn = {}
+  for k, v in pairs(lume.invert(t)) do
+    rtn[#rtn + 1] = k
+  end
+  return rtn
+end
+
+
+function lume.filter(t, fn, retainkeys)
+  fn = iteratee(fn)
+  local iter = getiter(t)
+  local rtn = {}
+  if retainkeys then
+    for k, v in iter(t) do
+      if fn(v) then rtn[k] = v end
+    end
+  else
+    for k, v in iter(t) do
+      if fn(v) then rtn[#rtn + 1] = v end
+    end
+  end
+  return rtn
+end
+
+
+function lume.reject(t, fn, retainkeys)
+  fn = iteratee(fn)
+  local iter = getiter(t)
+  local rtn = {}
+  if retainkeys then
+    for k, v in iter(t) do
+      if not fn(v) then rtn[k] = v end
+    end
+  else
+    for k, v in iter(t) do
+      if not fn(v) then rtn[#rtn + 1] = v end
+    end
+  end
+  return rtn
+end
+
+
+function lume.merge(...)
+  local rtn = {}
+  for i = 1, select("#", ...) do
+    local t = select(i, ...)
+    local iter = getiter(t)
+    for k, v in iter(t) do
+      rtn[k] = v
+    end
+  end
+  return rtn
+end
+
+
+function lume.concat(...)
+  local rtn = {}
+  for i = 1, select("#", ...) do
+    local t = select(i, ...)
+    if t ~= nil then
+      local iter = getiter(t)
+      for k, v in iter(t) do
+        rtn[#rtn + 1] = v
+      end
+    end
+  end
+  return rtn
+end
+
+
+function lume.find(t, value)
+  local iter = getiter(t)
+  for k, v in iter(t) do
+    if v == value then return k end
+  end
+  return nil
+end
+
+
+function lume.match(t, fn)
+  fn = iteratee(fn)
+  local iter = getiter(t)
+  for k, v in iter(t) do
+    if fn(v) then return v, k end
+  end
+  return nil
+end
+
+
+function lume.count(t, fn)
+  local count = 0
+  local iter = getiter(t)
+  if fn then
+    fn = iteratee(fn)
+    for k, v in iter(t) do
+      if fn(v) then count = count + 1 end
+    end
+  else
+    if isarray(t) then
+      return #t
+    end
+    for k in iter(t) do count = count + 1 end
+  end
+  return count
+end
+
+
+function lume.slice(t, i, j)
+  i = i and absindex(#t, i) or 1
+  j = j and absindex(#t, j) or #t
+  local rtn = {}
+  for x = i < 1 and 1 or i, j > #t and #t or j do
+    rtn[#rtn + 1] = t[x]
+  end
+  return rtn
+end
+
+
+function lume.first(t, n)
+  if not n then return t[1] end
+  return lume.slice(t, 1, n)
+end
+
+
+function lume.last(t, n)
+  if not n then return t[#t] end
+  return lume.slice(t, -n, -1)
+end
+
+
+function lume.invert(t)
+  local rtn = {}
+  for k, v in pairs(t) do rtn[v] = k end
+  return rtn
+end
+
+
+function lume.pick(t, ...)
+  local rtn = {}
+  for i = 1, select("#", ...) do
+    local k = select(i, ...)
+    rtn[k] = t[k]
+  end
+  return rtn
+end
+
+
+function lume.keys(t)
+  local rtn = {}
+  local iter = getiter(t)
+  for k, v in iter(t) do rtn[#rtn + 1] = k end
+  return rtn
+end
+
+
+function lume.clone(t)
+  local rtn = {}
+  for k, v in pairs(t) do rtn[k] = v end
+  return rtn
+end
+
+
+function lume.fn(fn, ...)
+  assert(iscallable(fn), "expected a function as the first argument")
+  local args = { ... }
+  return function(...)
+    local a = lume.concat(args, { ... })
+    return fn(unpack(a))
+  end
+end
+
+
+function lume.once(fn, ...)
+  local fn = lume.fn(fn, ...)
+  local done = false
+  return function(...)
+    if done then return end
+    done = true
+    return fn(...)
+  end
+end
+
+
+local memoize_fnkey = {}
+local memoize_nil = {}
+
+function lume.memoize(fn)
+  local cache = {}
+  return function(...)
+    local c = cache
+    for i = 1, select("#", ...) do
+      local a = select(i, ...) or memoize_nil
+      c[a] = c[a] or {}
+      c = c[a]
+    end
+    c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)}
+    return unpack(c[memoize_fnkey])
+  end
+end
+
+
+function lume.combine(...)
+  local n = select('#', ...)
+  if n == 0 then return noop end
+  if n == 1 then 
+    local fn = select(1, ...)
+    if not fn then return noop end
+    assert(iscallable(fn), "expected a function or nil")
+    return fn
+  end
+  local funcs = {}
+  for i = 1, n do
+    local fn = select(i, ...)
+    if fn ~= nil then
+      assert(iscallable(fn), "expected a function or nil")
+      funcs[#funcs + 1] = fn
+    end
+  end
+  return function(...)
+    for _, f in ipairs(funcs) do f(...) end
+  end
+end
+
+
+function lume.call(fn, ...)
+  if fn then
+    return fn(...)
+  end
+end
+
+
+function lume.time(fn, ...)
+  local start = os.clock()
+  local rtn = {fn(...)}
+  return (os.clock() - start), unpack(rtn)
+end
+
+
+local lambda_cache = {}
+
+function lume.lambda(str)
+  if not lambda_cache[str] then
+    local args, body = str:match([[^([%w,_ ]-)%->(.-)$]])
+    assert(args and body, "bad string lambda")
+    local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend"
+    lambda_cache[str] = lume.dostring(s)
+  end
+  return lambda_cache[str]
+end
+
+
+function lume.serialize(x)
+  local f = { string = function(v) return string.format("%q", v) end,
+              number = tostring, boolean = tostring }
+  f.table = function(t)
+    local rtn = {}
+    for k, v in pairs(t) do
+      rtn[#rtn + 1] = "[" .. f[type(k)](k) .. "]=" .. f[type(v)](v) .. ","
+    end
+    return "{" .. table.concat(rtn) .. "}"
+  end
+  local err = function(t,k) error("unsupported serialize type: " .. k) end
+  setmetatable(f, { __index = err })
+  return f[type(x)](x)
+end
+
+
+function lume.deserialize(str)
+  return lume.dostring("return " .. str)
+end
+
+
+function lume.split(str, sep)
+  if not sep then
+    return lume.array(str:gmatch("([%S]+)"))
+  else
+    assert(sep ~= "", "empty separator")
+    local psep = patternescape(sep)
+    return lume.array((str..sep):gmatch("(.-)("..psep..")"))
+  end
+end
+
+
+function lume.trim(str, chars)
+  if not chars then return str:match("^[%s]*(.-)[%s]*$") end
+  chars = patternescape(chars)
+  return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$")
+end
+
+
+function lume.wordwrap(str, limit)
+  limit = limit or 72
+  local check
+  if type(limit) == "number" then
+    check = function(str) return #str >= limit end
+  else
+    check = limit
+  end
+  local rtn = {}
+  local line = ""
+  for word, spaces in str:gmatch("(%S+)(%s*)") do
+    local str = line .. word
+    if check(str) then
+      table.insert(rtn, line .. "\n")
+      line = word
+    else
+      line = str
+    end
+    for c in spaces:gmatch(".") do
+      if c == "\n" then
+        table.insert(rtn, line .. "\n")
+        line = ""
+      else
+        line = line .. c
+      end
+    end
+  end
+  table.insert(rtn, line)
+  return table.concat(rtn)
+end
+
+
+function lume.format(str, vars)
+  if not vars then return str end
+  local f = function(x)
+    return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}")
+  end
+  return (str:gsub("{(.-)}", f))
+end
+
+
+function lume.trace(...)
+  local info = debug.getinfo(2, "Sl")
+  local t = { "[" .. info.short_src .. ":" .. info.currentline .. "]" }
+  for i = 1, select("#", ...) do
+    local x = select(i, ...)
+    if type(x) == "number" then
+      x = string.format("%g", lume.round(x, .01))
+    end
+    t[#t + 1] = tostring(x)
+  end
+  print(table.concat(t, " "))
+end
+
+
+function lume.dostring(str)
+  return assert((loadstring or load)(str))()
+end
+
+
+function lume.uuid()
+  local fn = function(x)
+    local r = math_random(16) - 1
+    r = (x == "x") and (r + 1) or (r % 4) + 9
+    return ("0123456789abcdef"):sub(r, r)
+  end
+  return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
+end
+
+
+function lume.hotswap(modname)
+  local oldglobal = lume.clone(_G)
+  local updated = {}
+  local function update(old, new)
+    if updated[old] then return end
+    updated[old] = true
+    local oldmt, newmt = getmetatable(old), getmetatable(new)
+    if oldmt and newmt then update(oldmt, newmt) end
+    for k, v in pairs(new) do
+      if type(v) == "table" then update(old[k], v) else old[k] = v end
+    end
+  end
+  local err = nil
+  local function onerror(e)
+    for k, v in pairs(_G) do _G[k] = oldglobal[k] end
+    err = lume.trim(e)
+  end
+  local ok, oldmod = pcall(require, modname)
+  oldmod = ok and oldmod or nil
+  xpcall(function()
+    package.loaded[modname] = nil
+    local newmod = require(modname)
+    if type(oldmod) == "table" then update(oldmod, newmod) end
+    for k, v in pairs(oldglobal) do
+      if v ~= _G[k] and type(v) == "table" then
+        update(v, _G[k])
+        _G[k] = v
+      end
+    end
+  end, onerror)
+  package.loaded[modname] = oldmod
+  if err then return nil, err end
+  return oldmod
+end
+
+
+local ripairs_iter = function(t, i)
+  i = i - 1
+  local v = t[i]
+  if v then return i, v end
+end
+
+function lume.ripairs(t)
+  return ripairs_iter, t, (#t + 1)
+end
+
+
+function lume.color(str, mul)
+  mul = mul or 1
+  local r, g, b, a
+  r, g, b = str:match("#(%x%x)(%x%x)(%x%x)")
+  if r then
+    r = tonumber(r, 16) / 0xff
+    g = tonumber(g, 16) / 0xff
+    b = tonumber(b, 16) / 0xff
+    a = 1
+  elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
+    local f = str:gmatch("[%d.]+")
+    r = (f() or 0) / 0xff
+    g = (f() or 0) / 0xff
+    b = (f() or 0) / 0xff
+    a = f() or 1
+  else
+    error(("bad color string '%s'"):format(str))
+  end
+  return r * mul, g * mul, b * mul, a * mul
+end
+
+
+function lume.rgba(color)
+  local a = math_floor((color / 16777216) % 256)
+  local r = math_floor((color /    65536) % 256)
+  local g = math_floor((color /      256) % 256)
+  local b = math_floor((color) % 256)
+  return r, g, b, a
+end
+
+
+local chain_mt = {}
+chain_mt.__index = lume.map(lume.filter(lume, iscallable, true),
+  function(fn)
+    return function(self, ...)
+      self._value = fn(self._value, ...)
+      return self
+    end
+  end)
+chain_mt.__index.result = function(x) return x._value end
+
+function lume.chain(value)
+  return setmetatable({ _value = value }, chain_mt)
+end
+
+setmetatable(lume,  {
+  __call = function(t, ...)
+    return lume.chain(...)
+  end
+})
+
+
+return lume

+ 249 - 0
deps/lurker.lua

@@ -0,0 +1,249 @@
+--
+-- lurker 
+--
+-- Copyright (c) 2015 rxi
+--
+-- This library is free software; you can redistribute it and/or modify it
+-- under the terms of the MIT license. See LICENSE for details.
+--
+
+-- Assumes lume is in the same directory as this file
+local lume = require((...):gsub("[^/.\\]+$", "lume"))
+
+local lurker = { _version = "1.0.1" }
+
+
+local dir = love.filesystem.enumerate or love.filesystem.getDirectoryItems
+local isdir = love.filesystem.isDirectory
+local time = love.timer.getTime or os.time
+local lastmodified = love.filesystem.getLastModified
+
+local lovecallbacknames = {
+  "update",
+  "load",
+  "draw",
+  "mousepressed",
+  "mousereleased",
+  "keypressed",
+  "keyreleased",
+  "focus",
+  "quit",
+}
+
+
+function lurker.init()
+  lurker.print("Initing lurker")
+  lurker.path = "."
+  lurker.preswap = function() end
+  lurker.postswap = function() end
+  lurker.interval = .5
+  lurker.protected = true
+  lurker.quiet = false
+  lurker.lastscan = 0
+  lurker.lasterrorfile = nil
+  lurker.files = {}
+  lurker.funcwrappers = {}
+  lurker.lovefuncs = {}
+  lurker.state = "init"
+  lume.each(lurker.getchanged(), lurker.resetfile)
+  return lurker
+end
+
+
+function lurker.print(...)
+  print("[lurker] " .. lume.format(...))
+end
+
+
+function lurker.listdir(path, recursive, skipdotfiles)
+  path = (path == ".") and "" or path
+  local function fullpath(x) return path .. "/" .. x end
+  local t = {}
+  for _, f in pairs(lume.map(dir(path), fullpath)) do
+    if not skipdotfiles or not f:match("/%.[^/]*$") then
+      if recursive and isdir(f) then
+        t = lume.concat(t, lurker.listdir(f, true, true))
+      else
+        table.insert(t, lume.trim(f, "/"))
+      end
+    end
+  end
+  return t
+end
+
+
+function lurker.initwrappers()
+  for _, v in pairs(lovecallbacknames) do
+    lurker.funcwrappers[v] = function(...)
+      local args = {...}
+      xpcall(function()
+        return lurker.lovefuncs[v] and lurker.lovefuncs[v](unpack(args))
+      end, lurker.onerror)
+    end
+    lurker.lovefuncs[v] = love[v]
+  end
+  lurker.updatewrappers()
+end
+
+
+function lurker.updatewrappers()
+  for _, v in pairs(lovecallbacknames) do
+    if love[v] ~= lurker.funcwrappers[v] then
+      lurker.lovefuncs[v] = love[v]
+      love[v] = lurker.funcwrappers[v]
+    end
+  end
+end
+
+
+function lurker.onerror(e, nostacktrace)
+  lurker.print("An error occurred; switching to error state")
+  lurker.state = "error"
+ 
+  -- Release mouse
+  local setgrab = love.mouse.setGrab or love.mouse.setGrabbed
+  setgrab(false)
+
+  -- Set up callbacks
+  for _, v in pairs(lovecallbacknames) do
+    love[v] = function() end
+  end
+
+  love.update = lurker.update
+
+  love.keypressed = function(k)
+    if k == "escape" then
+      lurker.print("Exiting...")
+      love.event.quit()
+    end
+  end
+
+  local stacktrace = nostacktrace and "" or
+                     lume.trim((debug.traceback("", 2):gsub("\t", "")))
+  local msg = lume.format("{1}\n\n{2}", {e, stacktrace})
+  local colors = { 0xFF1E1E2C, 0xFFF0A3A3, 0xFF92B5B0, 0xFF66666A, 0xFFCDCDCD }
+  love.graphics.reset()
+  love.graphics.setFont(love.graphics.newFont(12))
+
+  love.draw = function()
+    local pad = 25
+    local width = love.graphics.getWidth()
+    local function drawhr(pos, color1, color2)
+      local animpos = lume.smooth(pad, width - pad - 8, lume.pingpong(time()))
+      if color1 then love.graphics.setColor(lume.rgba(color1)) end
+      love.graphics.rectangle("fill", pad, pos, width - pad*2, 1)
+      if color2 then love.graphics.setColor(lume.rgba(color2)) end
+      love.graphics.rectangle("fill", animpos, pos, 8, 1)
+    end
+    local function drawtext(str, x, y, color, limit)
+      love.graphics.setColor(lume.rgba(color))
+      love.graphics[limit and "printf" or "print"](str, x, y, limit)
+    end
+    love.graphics.setBackgroundColor(lume.rgba(colors[1]))
+    love.graphics.clear()
+    drawtext("An error has occurred", pad, pad, colors[2])
+    drawtext("lurker", width - love.graphics.getFont():getWidth("lurker") - 
+             pad, pad, colors[4])
+    drawhr(pad + 32, colors[4], colors[5])
+    drawtext("If you fix the problem and update the file the program will " ..
+             "resume", pad, pad + 46, colors[3])
+    drawhr(pad + 72, colors[4], colors[5])
+    drawtext(msg, pad, pad + 90, colors[5], width - pad * 2)
+    love.graphics.reset()
+  end
+end
+
+
+function lurker.exitinitstate()
+  lurker.state = "normal"
+  if lurker.protected then
+    lurker.initwrappers()
+  end
+end
+
+
+function lurker.exiterrorstate()
+  lurker.state = "normal"
+  for _, v in pairs(lovecallbacknames) do
+    love[v] = lurker.funcwrappers[v]
+  end
+end
+
+
+function lurker.update() 
+  if lurker.state == "init" then
+    lurker.exitinitstate()
+  end
+  local diff = time() - lurker.lastscan
+  if diff > lurker.interval then
+    lurker.lastscan = lurker.lastscan + diff
+    local changed = lurker.scan()
+    if #changed > 0 and lurker.lasterrorfile then
+      local f = lurker.lasterrorfile
+      lurker.lasterrorfile = nil
+      lurker.hotswapfile(f)
+    end
+  end
+end
+
+
+function lurker.getchanged()
+  local function fn(f)
+    return f:match("%.lua$") and lurker.files[f] ~= lastmodified(f)
+  end
+  return lume.filter(lurker.listdir(lurker.path, true, true), fn)
+end
+
+
+function lurker.modname(f)
+  return (f:gsub("%.lua$", ""):gsub("[/\\]", "."))
+end
+
+
+function lurker.resetfile(f)
+  lurker.files[f] = lastmodified(f)
+end
+
+
+function lurker.hotswapfile(f)
+  lurker.print("Hotswapping '{1}'...", {f})
+  if lurker.state == "error" then 
+    lurker.exiterrorstate()
+  end
+  if lurker.preswap(f) then
+    lurker.print("Hotswap of '{1}' aborted by preswap", {f})
+    lurker.resetfile(f)
+    return
+  end
+  local modname = lurker.modname(f)
+  local t, ok, err = lume.time(lume.hotswap, modname)
+  if ok then
+    lurker.print("Swapped '{1}' in {2} secs", {f, t})
+  else 
+    lurker.print("Failed to swap '{1}' : {2}", {f, err})
+    if not lurker.quiet and lurker.protected then
+      lurker.lasterrorfile = f
+      lurker.onerror(err, true)
+      lurker.resetfile(f)
+      return
+    end
+  end
+  lurker.resetfile(f)
+  lurker.postswap(f)
+  if lurker.protected then
+    lurker.updatewrappers()
+  end
+end
+
+
+function lurker.scan()
+  if lurker.state == "init" then
+    lurker.exitinitstate()
+  end
+  local changed = lurker.getchanged()
+  lume.each(changed, lurker.hotswapfile)
+  return changed
+end
+
+
+return lurker.init()

+ 1 - 0
deps/spine-runtimes

@@ -0,0 +1 @@
+Subproject commit d7eabe6b70e502dac340da3e900adcdaa6e7709b

+ 2 - 0
deps/util.lua

@@ -0,0 +1,2 @@
+f = {}
+f.exe = function(x, ...) if type(x) == 'function' then return x(...) end return x end

+ 30 - 0
game.lua

@@ -0,0 +1,30 @@
+Game = class()
+
+function Game:load()
+  self.pigeon = Pigeon()
+  self.people = {Person()}
+end
+
+function Game:update()
+  self.pigeon:update()
+  lume.each(self.people, function(person)
+    person:update()
+  end)
+
+  if love.math.random() < .5 * ls.tickrate then
+    lume.push(self.people, Person())
+  end
+end
+
+function Game:draw()
+  self.pigeon:draw()
+  lume.each(self.people, function(person)
+    person:draw()
+  end)
+end
+
+function Game:keypressed(key)
+  if key == 'escape' then
+    love.event.quit()
+  end
+end

+ 5 - 0
main.lua

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

+ 20 - 0
person.lua

@@ -0,0 +1,20 @@
+Person = class()
+
+function Person:init()
+  self.x = 800
+  self.y = 500
+  self.w = 20
+  self.h = 40
+end
+
+function Person:update()
+  self.x = self.x - 100 * ls.tickrate
+end
+
+function Person:draw()
+  local g = love.graphics
+  g.setColor(255, 255, 255)
+  g.rectangle('line', self.x - self.w / 2, self.y - self.h / 2, self.w, self.h)
+end
+
+

+ 67 - 0
pigeon.lua

@@ -0,0 +1,67 @@
+Pigeon = class()
+
+function Pigeon:init()
+  self.x = 0
+  self.y = 500
+  self.w = 20
+  self.h = 40
+  self.speed = 100
+  self.direction = 0
+  self.laser = false
+
+  self.lives = 3
+  self.health = 100
+  self.maxHealth = 100
+end
+
+function Pigeon:update()
+  if love.keyboard.isDown('left') then
+    self.x = self.x - self.speed * ls.tickrate
+    self.direction = -1
+  elseif love.keyboard.isDown('right') then
+    self.x = self.x + self.speed * ls.tickrate
+    self.direction = 1
+  end
+
+  self.laser = love.keyboard.isDown(' ')
+
+  if self.laser then
+    local kills = 0
+    lume.each(ctx.people, function(person, i)
+      local x1, x2 = self.x, self.x + 1000 * self.direction
+      if person.x > math.min(x1, x2) and person.x < math.max(x1, x2) then
+        kills = kills + 1
+        table.remove(ctx.people, i)
+      end
+    end)
+
+    if kills > 0 then
+      self.w = self.w + 5 * kills
+      self.h = self.h + 10 * kills
+    end
+  end
+
+  if self.health < 0 then
+    if self.lives == 0 then
+      print('you lose')
+      love.event.quit()
+      return
+    end
+
+    self.health = self.maxHealth
+    self.lives = self.lives - 1
+  end
+
+  self.health = self.health - 10 * ls.tickrate
+end
+
+function Pigeon:draw()
+  local g = love.graphics
+  g.setColor(255, 255, 255)
+  g.rectangle('line', self.x - self.w / 2, self.y - self.h / 2, self.w, self.h)
+
+  if self.laser then
+    love.graphics.setColor(255, 0, 0)
+    love.graphics.line(self.x, self.y, self.x + 1000 * self.direction, self.y)
+  end
+end

+ 10 - 0
require.lua

@@ -0,0 +1,10 @@
+require 'deps/class'
+lume = require 'deps/lume'
+lurker = require 'deps/lurker'
+ls = require 'deps/lovestep/lovestep'
+require 'deps/util'
+
+require 'context'
+require 'game'
+require 'person'
+require 'pigeon'