bjorn 10 år sedan
förälder
incheckning
2843dddd4b
15 ändrade filer med 441 tillägg och 217 borttagningar
  1. 3 0
      .gitmodules
  2. 59 21
      building.lua
  3. 82 0
      caveman.lua
  4. 19 5
      data/animation/pigeon.lua
  5. 0 1
      deps/animation.lua
  6. 1 0
      deps/lutil
  7. 3 1
      deps/manager.lua
  8. 36 0
      deps/physicsinterpolator.lua
  9. 4 3
      deps/view.lua
  10. 13 7
      game.lua
  11. 1 8
      hud.lua
  12. 1 0
      map.lua
  13. 60 35
      person.lua
  14. 156 136
      pigeon.lua
  15. 3 0
      require.lua

+ 3 - 0
.gitmodules

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

+ 59 - 21
building.lua

@@ -1,38 +1,76 @@
 Building = class()
 
-Building.category = 4
+Building.wallWidth = 16
 
-function Building:init(x, w, h)
-  self.x = x
+function Building:activate()
   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.pieces = {}
 
-  self.body:setUserData(self)
+  -- Roof
+  local piece = {}
+  piece.body = love.physics.newBody(ctx.world, self.x, self.y - self.height - self.wallWidth / 2, 'kinematic')
+  piece.body:setUserData(self)
+  piece.shape = love.physics.newRectangleShape(self.width, self.wallWidth)
+  piece.fixture = love.physics.newFixture(piece.body, piece.shape)
+  piece.fixture:setCategory(ctx.categories.building)
+  piece.fixture:setMask(ctx.categories.debris)
+  piece.phlerp = PhysicsInterpolator(piece.body)
+  table.insert(self.pieces, piece)
 
-  self.body:setMass(1000)
-  self.fixture:setCategory(self.category)
-  self.fixture:setMask(self.category, Person.category)
+  -- Left wall
+  piece = {}
+  piece.body = love.physics.newBody(ctx.world, self.x - self.width / 2 + self.wallWidth / 2, self.y - self.height / 2, 'kinematic')
+  piece.body:setUserData(self)
+  piece.shape = love.physics.newRectangleShape(self.wallWidth, self.height)
+  piece.fixture = love.physics.newFixture(piece.body, piece.shape)
+  piece.fixture:setCategory(ctx.categories.building)
+  piece.fixture:setMask(ctx.categories.debris)
+  piece.phlerp = PhysicsInterpolator(piece.body)
+  table.insert(self.pieces, piece)
+
+  -- Right wall
+  piece = {}
+  piece.body = love.physics.newBody(ctx.world, self.x + self.width / 2 - self.wallWidth / 2, self.y - self.height / 2, 'kinematic')
+  piece.body:setUserData(self)
+  piece.shape = love.physics.newRectangleShape(self.wallWidth, self.height)
+  piece.fixture = love.physics.newFixture(piece.body, piece.shape)
+  piece.fixture:setCategory(ctx.categories.building)
+  piece.fixture:setMask(ctx.categories.debris)
+  piece.phlerp = PhysicsInterpolator(piece.body)
+  table.insert(self.pieces, piece)
 
   ctx.event:emit('view.register', {object = self})
 end
 
+function Building:update()
+  table.each(self.pieces, function(piece)
+    piece.phlerp:update()
+  end)
+
+  if ctx.pigeon.body:getY() + ctx.pigeon.shapeSize / 2 > self.pieces[1].body:getY() - self.wallWidth / 2 then
+    self.pieces[1].fixture:setCategory(ctx.categories.oneWayPlatform)
+  else
+    self.pieces[1].fixture:setCategory(ctx.categories.building)
+  end
+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)
+  table.each(self.pieces, function(piece)
+    piece.phlerp:lerp()
+    local points = {piece.body:getWorldPoints(piece.shape:getPoints())}
+    g.setColor(255, 255, 255)
+    g.polygon('line', points)
+    piece.phlerp:delerp()
+  end)
 end
 
-function Building:die()
-  self.body:destroy()
-  ctx.event:emit('view.unregister', {object = self})
+function Building:destroy()
+  table.each(self.pieces, function(piece)
+    piece.body:setType('dynamic')
+    piece.body:applyTorque(lume.random(50000, 100000) * (love.math.random() > .5 and 1 or -1))
+    piece.fixture:setCategory(ctx.categories.debris)
+  end)
 end
-

+ 82 - 0
caveman.lua

@@ -0,0 +1,82 @@
+Caveman = extend(Person)
+
+Caveman.rangeRange = {100, 150}
+Caveman.reloadRange = {3, 4}
+
+----------------
+-- Core
+----------------
+function Caveman:activate()
+  self.gender = love.math.random() < .5 and 'female' or 'male'
+  self.image = data.media.graphics.dinoland[self.gender].normal
+  self.direction = 1
+
+  self.state = self.attack
+  self.hasSpear = true
+  self.walkTimer = 1
+  self.reloadTimer = 0
+
+  self.range = lume.random(unpack(self.rangeRange))
+
+  Person.activate(self)
+end
+
+function Caveman:update()
+  self.walkTimer = timer.rot(self.walkTimer)
+
+  Person.update(self)
+end
+
+----------------
+-- Helpers
+----------------
+function Caveman:reloadSpear()
+  self.reloadTimer = timer.rot(self.reloadTimer, function()
+    self.hasSpear = true
+  end)
+end
+
+function Caveman:inRange()
+  return self:distanceTo(ctx.pigeon) < self.range
+end
+
+----------------
+-- States
+----------------
+Caveman.panic = {}
+Caveman.panic.walkRate = {.28, .30}
+function Caveman.panic:update()
+  self.direction = -self:directionTo(ctx.pigeon)
+  self.image = data.media.graphics.dinoland[self.gender].panic
+
+  if self.walkTimer == 0 then
+    self:hop(self.direction)
+    self.walkTimer = lume.random(unpack(self.state.walkRate))
+  end
+
+  self:reloadSpear()
+
+  if self.hasSpear then
+    self:changeState('attack')
+  end
+end
+
+Caveman.attack = {}
+Caveman.attack.walkRate = {.4, .6}
+function Caveman.attack:update()
+  self.direction = self:directionTo(ctx.pigeon)
+  self.image = data.media.graphics.dinoland[self.gender].normal
+
+  if self:inRange() then
+    if self.walkTimer == 0 then
+      self.hasSpear = false
+      self.reloadTimer = lume.random(unpack(self.reloadRange))
+      self:changeState('panic')
+    end
+  else
+    if self.walkTimer == 0 then
+      self:hop(self.direction)
+      self.walkTimer = lume.random(unpack(self.state.walkRate))
+    end
+  end
+end

+ 19 - 5
data/animation/pigeon.lua

@@ -1,17 +1,31 @@
 local Pigeon = extend(Animation)
 
-Pigeon.scale = .3
+Pigeon.scale = .5
 Pigeon.default = 'idle'
 Pigeon.states = {}
 
 Pigeon.states.idle = {
-  loop = true,
-  priority = 1
+  loop = true
+}
+
+Pigeon.states.walk = {
+  loop = true
 }
 
 Pigeon.states.peck = {
-  loop = false,
-  priority = 2
+  loop = false
+}
+
+Pigeon.states.jump = {
+  loop = false
+}
+
+Pigeon.states.fly = {
+  loop = false
+}
+
+Pigeon.states.laser = {
+  loop = false
 }
 
 return Pigeon

+ 0 - 1
deps/animation.lua

@@ -69,7 +69,6 @@ function Animation:set(name, options)
   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

+ 1 - 0
deps/lutil

@@ -0,0 +1 @@
+Subproject commit 60db1f48073374a25baaef1b4e85830db72f4355

+ 3 - 1
deps/manager.lua

@@ -12,7 +12,9 @@ function Manager:paused()
   table.with(self.objects, 'paused')
 end
 
-function Manager:add(object)
+function Manager:add(kind, vars)
+  local object = kind()
+  table.merge(vars, object)
   lume.call(object.activate, object)
   self.objects[object] = object
 

+ 36 - 0
deps/physicsinterpolator.lua

@@ -0,0 +1,36 @@
+PhysicsInterpolator = class()
+
+function PhysicsInterpolator:init(body)
+  self.body = body
+
+  self.previous = {}
+  self.current = {}
+
+  self:update()
+end
+
+function PhysicsInterpolator:update()
+  self:updateState(self.previous)
+end
+
+function PhysicsInterpolator:lerp()
+  self:updateState(self.current)
+
+  local z = ls.accum / ls.tickrate
+  local x = lume.lerp(self.previous.x, self.current.x, z)
+  local y = lume.lerp(self.previous.y, self.current.y, z)
+  local angle = lume.lerp(self.previous.angle, self.current.angle, z)
+
+  self.body:setPosition(x, y)
+  self.body:setAngle(angle)
+end
+
+function PhysicsInterpolator:delerp()
+  self.body:setPosition(self.current.x, self.current.y)
+  self.body:setAngle(self.current.angle)
+end
+
+function PhysicsInterpolator:updateState(dest)
+  dest.x, dest.y = self.body:getPosition()
+  dest.angle = self.body:getAngle()
+end

+ 4 - 3
deps/view.lua

@@ -190,12 +190,13 @@ function View:threeDepth(x, y, z)
 end
 
 function View:follow()
-  if not self.target then return end
+  if not ctx.pigeon then return end
 
+  local x, y = ctx.pigeon.body:getPosition()
   local margin = 0.5
 
-  self.x = self.target.x - self.width * margin
-  self.y = self.target.y - self.height * margin
+  self.x = x - self.width * margin
+  self.y = y - self.height * margin
 end
 
 function View:contain()

+ 13 - 7
game.lua

@@ -1,5 +1,14 @@
 Game = class()
 
+Game.categories = {
+  ground = 1,
+  building = 2,
+  person = 3,
+  pigeon = 4,
+  oneWayPlatform = 5,
+  debris = 6
+}
+
 function Game:load()
   self.event = Event()
   self.world = love.physics.newWorld(0, 1000)
@@ -12,13 +21,8 @@ function Game:load()
   self.hud = Hud()
   self.goal = Goal()
 
-  for i = 1, 5 do
-    self.buildings:add(Building(300 + lume.random(self.map.width - 300), lume.random(20, 100), lume.random(60, 300)))
-  end
-
-  for i = 1, 50 do
-    self.people:add(Person(500 + lume.random(self.map.width - 500), 400, -1))
-  end
+  --self.people:add(Caveman, {x = 500, y = 300})
+  self.buildings:add(Building, {x = 600, width = 200, height = 80})
 end
 
 function Game:update()
@@ -28,6 +32,8 @@ function Game:update()
   self.projectiles:update()
   self.world:update(ls.tickrate)
   self.view:update()
+
+  lurker.update()
 end
 
 function Game:draw()

+ 1 - 8
hud.lua

@@ -8,13 +8,6 @@ end
 function Hud:gui()
   local g = love.graphics
   g.setColor(255, 255, 255)
-
-  local barWidth = 100
-  local barHeight = 10
-  local p = ctx.pigeon
-  g.rectangle('fill', 2, 2, barWidth * (p.health / p.maxHealth), barHeight)
-  g.rectangle('line', 2, 2, barWidth, barHeight)
-
   g.setFont(self.font)
-  g.print('Lives: ' .. p.lives, barWidth + 10, 2)
+  g.print(ctx.pigeon.fuel, 0, 0)
 end

+ 1 - 0
map.lua

@@ -9,6 +9,7 @@ function Map:init()
   self.ground.shape = love.physics.newRectangleShape(self.width, self.ground.height)
 
   self.ground.fixture = love.physics.newFixture(self.ground.body, self.ground.shape)
+  self.ground.fixture:setCategory(ctx.categories.ground)
 
   ctx.view.xmax = self.width
   ctx.view.ymax = self.height

+ 60 - 35
person.lua

@@ -1,15 +1,11 @@
 Person = class()
 
-Person.category = 3
-
-function Person:init(x, y, dir)
-  self.x = x
-  self.y = y
-  self.direction = dir
-
-  self.image = data.media.graphics.dinoland[love.math.random() < .5 and 'female' or 'male'].normal
+----------------
+-- Core
+----------------
+function Person:activate()
   local widthRatio = self.image:getWidth() / self.image:getHeight()
-  self.h = 40
+  self.h = 60
   self.w = self.h * widthRatio
   self.scale = self.h / self.image:getHeight()
 
@@ -18,45 +14,74 @@ function Person:init(x, y, dir)
   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.fixture:setFriction(1)
+  self.fixture:setCategory(ctx.categories.person)
+  self.fixture:setMask(ctx.categories.person, ctx.categories.building, ctx.categories.debris)
+
+  self.phlerp = PhysicsInterpolator(self.body)
 
   ctx.event:emit('view.register', {object = self})
 end
 
 function Person:update()
-  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)
-  else
-    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
-      ctx.people:remove(self)
-      self.body:destroy()
-      ctx.event:emit('view.unregister', {object = self})
-    end
+  self.phlerp:update()
+
+  if self.state then
+    self.state.update(self)
   end
 end
 
 function Person:draw()
   local g = love.graphics
-  g.draw(self.image, self.body:getX(), self.body:getY(), self.body:getAngle(), self.scale * self.direction, self.scale, self.image:getWidth() / 2, self.image:getHeight() / 2)
+
+  self.phlerp:lerp()
+
+  local x, y, angle = self.body:getX(), self.body:getY(), self.body:getAngle()
+  g.setColor(255, 255, 255)
+  g.draw(self.image, x, y, angle, self.scale * self.direction, self.scale, self.image:getWidth() / 2, self.image:getHeight() / 2)
+
+  self.phlerp:delerp()
+end
+
+----------------
+-- Helpers
+----------------
+function Person:hop(direction)
+  self.body:applyLinearImpulse(120 * direction, -180)
+end
+
+function Person:directionTo(object)
+  return lume.sign(object.body:getX() - self.body:getX())
+end
+
+function Person:distanceTo(object)
+  return math.abs(object.body:getX() - self.body:getX())
+end
+
+function Person:changeState(target)
+  lume.call(self.state.exit, self)
+  self.state = self[target]
+  lume.call(self.state.enter, self)
+  return self.state
+end
+
+----------------
+-- Base States
+----------------
+Person.dead = {}
+function Person.dead:enter()
+  self.body:applyLinearImpulse(-500 + love.math.random() * 1000, love.math.random() * -800)
+  self.body:applyTorque(-200 * love.math.random() * 400)
+  self.body:setFixedRotation(false)
 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)
+function Person.dead:update()
+  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
+    ctx.people:remove(self)
+    self.body:destroy()
+    ctx.event:emit('view.unregister', {object = self})
   end
 end

+ 156 - 136
pigeon.lua

@@ -1,176 +1,196 @@
 Pigeon = class()
 
+----------------
+-- Constants
+----------------
+Pigeon.walkForce = 600
+Pigeon.maxSpeed = 350
+Pigeon.jumpForce = 3000
+Pigeon.rocketForce = 500
+Pigeon.maxFlySpeed = 300
+Pigeon.maxFuel = 50
+
+----------------
+-- Core
+----------------
 function Pigeon:init()
-  self.x = 0
-  self.y = 500
-  self.prevx = self.x
-  self.prevy = self.y
-
-  self.scale = .3
-  self.speed = 200
-
-  self.laser = {
-    active = false,
-    length = 0,
-    angle = 0,
-    speed = .2,
-    tween = nil
-  }
-
-  self.lives = 3
-  self.health = 100
-  self.maxHealth = 100
+  self.shapeSize = 50
+  self.body = love.physics.newBody(ctx.world, self.shapeSize / 2, ctx.map.height - ctx.map.ground.height - self.shapeSize / 2, 'dynamic')
+  self.shape = love.physics.newRectangleShape(self.shapeSize, self.shapeSize)
+  self.fixture = love.physics.newFixture(self.body, self.shape)
 
-  self.animation = data.animation.pigeon()
-  self.animation:set('idle')
+  self.body:setFixedRotation(true)
+  self.body:setGravityScale(5)
+  self.fixture:setCategory(ctx.categories.pigeon)
+  self.fixture:setMask(ctx.categories.oneWayPlatform, ctx.categories.person, ctx.categories.debris)
+
+  self.phlerp = PhysicsInterpolator(self.body)
+
+  self.fuel = self.maxFuel
+  self.state = self.idle
 
+  self.animation = data.animation.pigeon()
   self.animation:on('complete', function(event)
-    if event.state.name == 'peck' then
-      self.animation:set('idle', {force = true})
+    if event.state.name == 'jump' then
+      self.animation:set('idle')
     end
   end)
 
-  self.animation.spine.skeleton:setToSetupPose()
-
-  ctx.view.target = self
-
   ctx.event:emit('view.register', {object = self})
 end
 
 function Pigeon:update()
-  self.prevx = self.x
-  self.prevy = self.y
+  self.phlerp:update()
 
-  -- Movement
-  self:move()
+  self.grounded = self:getGrounded()
+  self.state.update(self)
+  self:contain()
+end
 
-  -- Laser
-  self:updateLaser()
+function Pigeon:draw()
+  local g = love.graphics
+  self.phlerp:lerp()
 
-  -- Pecking
-  if not self.laser.active and love.keyboard.isDown('down') then self.animation:set('peck') end
-  if self.animation.state.name == 'peck' then
-    self:killThingsOnBeak()
-  end
+  local points = {self.body:getWorldPoints(self.shape:getPoints())}
+  g.setColor(255, 255, 255)
+  g.polygon('line', points)
 
-  -- Death
-  if self.health < 0 then
-    if self.lives == 0 then
-      print('you lose')
-      love.event.quit()
-      return
-    end
+  local x, y = self.body:getPosition()
+  self.animation:draw(x, y + self.shapeSize / 2)
 
-    self.health = self.maxHealth
-    self.lives = self.lives - 1
-    flux.to(self.animation, .6, {scale = self.animation.scale / 2}):ease('elasticin')
-  end
+  local x1, y1, x2, y2 = self:getGroundRaycastPoints()
+  g.setColor(self.grounded and {0, 255, 0} or {255, 0, 0})
+  g.line(x1, y1, x2, y2)
+
+  self.phlerp:delerp()
 end
 
-function Pigeon:draw()
-  local g = love.graphics
-  local x = lume.lerp(self.prevx, self.x, ls.accum / ls.tickrate)
-  local y = lume.lerp(self.prevy, self.y, ls.accum / ls.tickrate)
+----------------
+-- Helpers
+----------------
+function Pigeon:changeState(target)
+  lume.call(self.state.exit, self)
+  self.state = self[target]
+  lume.call(self.state.enter, self)
+  return self.state
+end
 
-  if self.laser.active then
-    local x2, y2 = x + math.cos(self.laser.angle) * self.laser.length, y + math.sin(self.laser.angle) * self.laser.length
+function Pigeon:getGroundRaycastPoints()
+  local x, y = self.body:getPosition()
+  local h = self.shapeSize / 2
+  local x1, y1, x2, y2 = x, y + h, x, y + h + 1
+  return x1, y1, x2, y2
+end
 
-    g.setColor(255, 0, 0)
-    g.line(x, y, x2, y2)
-  end
+function Pigeon:getGrounded()
+  local grounded = false
+  local x1, y1, x2, y2 = self:getGroundRaycastPoints()
+  ctx.world:rayCast(x1, y1, x2, y2, function(fixture)
+    local categories = {fixture:getCategory()}
+    if lume.find(categories, ctx.categories.ground) or lume.find(categories, ctx.categories.building) then
+      grounded = true
+    end
+    return 1
+  end)
 
-  self.animation:draw(x, y)
+  return grounded
 end
 
+----------------
+-- Actions
+----------------
 function Pigeon:move()
-  if not self.laser.active then
-    if self.animation.state.name ~= 'peck' then
-      if love.keyboard.isDown('left') then
-        self.x = self.x - self.speed * ls.tickrate
-        self.animation.flipped = true
-      elseif love.keyboard.isDown('right') then
-        self.x = self.x + self.speed * ls.tickrate
-        self.animation.flipped = false
-      end
-    end
+  local left, right = love.keyboard.isDown('left'), love.keyboard.isDown('right')
+
+  if left then
+    self.body:applyLinearImpulse(-self.walkForce, 0)
+    self.animation.flipped = true
+  elseif right then
+    self.body:applyLinearImpulse(self.walkForce, 0)
+    self.animation.flipped = false
   end
+
+  local vx, vy = self.body:getLinearVelocity()
+  self.body:setLinearVelocity(math.min(math.abs(vx), self.maxSpeed) * lume.sign(vx), vy)
 end
 
-function Pigeon:updateLaser()
-  self.laser.active = love.keyboard.isDown(' ')
-
-  if self.laser.active then
-    self.laser.tween = flux.to(self.laser, 1, {length = 1000}):ease('expoout')
-
-    if self.laser.length == 0 then
-      self.laser.angle = self.animation.flipped and math.pi or 0
-    else
-      local x1, y1 = self.x, self.y
-      local x2, y2 = self.x + math.cos(self.laser.angle) * self.laser.length, self.y + math.sin(self.laser.angle) * self.laser.length
-
-      ctx.world:rayCast(x1, y1, x2, y2, function(fixture)
-        local person = fixture:getBody():getUserData()
-        if person and not person.dead and person.die then
-          self:kill(person)
-          return 1
-        end
-        return -1
-      end)
-    end
+function Pigeon:jump()
+  self.body:applyLinearImpulse(0, -self.jumpForce)
+  self.animation:set('jump')
+end
 
-    local diff = self.laser.speed * ls.tickrate * lume.sign(math.pi / 2 - self.laser.angle)
-    if love.keyboard.isDown('up') then
-      self.laser.angle = self.laser.angle - diff
-    elseif love.keyboard.isDown('down') then
-      self.laser.angle = self.laser.angle + diff
-    end
+function Pigeon:recoverFuel()
+  self.fuel = math.min(self.fuel + 20 * ls.tickrate, self.maxFuel)
+end
+
+function Pigeon:contain()
+  if self.body:getX() < 0 then
+    self.body:setPosition(0, self.body:getY())
+  end
+end
+
+----------------
+-- States
+----------------
+Pigeon.idle = {}
+function Pigeon.idle:update()
+  self:recoverFuel()
+  self.animation:set('idle')
+
+  if love.keyboard.isDown('left', 'right') then
+    return self:changeState('walk').update(self)
+  end
+
+  if love.keyboard.isDown('up') then
+    self:jump()
+    self:changeState('air')
   else
-    if self.laser.tween then
-      self.laser.tween:stop()
-      self.laser.length = 0
-    end
+    local vx, vy = self.body:getLinearVelocity()
+    self.body:setLinearVelocity(vx / 1.2, vy)
   end
 end
 
-function Pigeon:kill(person)
-  if person and not person.dead then
-    person:die()
-    self.health = math.min(self.health + 15, self.maxHealth)
-    flux.to(self.animation, .6, {scale = self.animation.scale + .02}):ease('elasticout')
+Pigeon.walk = {}
+function Pigeon.walk:update()
+  local left, right = love.keyboard.isDown('left'), love.keyboard.isDown('right')
+  self.animation:set('walk')
+
+  self:recoverFuel()
+
+  if love.keyboard.isDown('up') then
+    self:jump()
+    return self:changeState('air')
+  end
+
+  if left or right then
+    self:move()
+  else
+    return self:changeState('idle').update(self)
   end
 end
 
--- 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')
-    if beakAttachment then
-      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
-            self:kill(person)
-            return true
-          end
-        end)
+Pigeon.air = {}
+function Pigeon.air:update()
+  local left, right = love.keyboard.isDown('left'), love.keyboard.isDown('right')
+  local vx, vy = self.body:getLinearVelocity()
+
+  if self.grounded then
+    return self:changeState('idle').update(self)
+  end
+
+  if left or right then
+    self:move()
+  else
+    self.body:setLinearVelocity(0, vy)
+  end
+
+  if love.keyboard.isDown(' ') then
+    if self.fuel > 0 then
+      if (vy > 0 or math.abs(vy) < self.maxFlySpeed) then
+        self.fuel = math.max(self.fuel - 33 * ls.tickrate, 0)
+
+        local bonusBoost = vy > 0 and vy / 2 or 0
+        self.body:applyLinearImpulse(0, -(self.rocketForce + bonusBoost))
       end
     end
   end

+ 3 - 0
require.lua

@@ -4,15 +4,18 @@ flux = require 'deps/flux'
 lume = require 'deps/lume'
 lurker = require 'deps/lurker'
 ls = require 'deps/lovestep/lovestep'
+require 'deps/lutil/util'
 require 'deps/util'
 require 'deps/animation'
 require 'deps/event'
 require 'deps/view'
 require 'deps/manager'
+require 'deps/physicsinterpolator'
 
 require 'context'
 require 'game'
 require 'person'
+require 'caveman'
 require 'building'
 require 'pigeon'
 require 'map'