|
@@ -1,176 +1,196 @@
|
|
Pigeon = class()
|
|
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()
|
|
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)
|
|
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
|
|
end)
|
|
end)
|
|
|
|
|
|
- self.animation.spine.skeleton:setToSetupPose()
|
|
|
|
-
|
|
|
|
- ctx.view.target = self
|
|
|
|
-
|
|
|
|
ctx.event:emit('view.register', {object = self})
|
|
ctx.event:emit('view.register', {object = self})
|
|
end
|
|
end
|
|
|
|
|
|
function Pigeon:update()
|
|
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
|
|
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
|
|
end
|
|
|
|
|
|
|
|
+----------------
|
|
|
|
+-- Actions
|
|
|
|
+----------------
|
|
function Pigeon:move()
|
|
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
|
|
end
|
|
|
|
+
|
|
|
|
+ local vx, vy = self.body:getLinearVelocity()
|
|
|
|
+ self.body:setLinearVelocity(math.min(math.abs(vx), self.maxSpeed) * lume.sign(vx), vy)
|
|
end
|
|
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
|
|
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
|
|
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
|
|
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
|
|
end
|
|
end
|
|
end
|