Add code for repeating waves of enemies, adding intro animations so the enemies can 'fly in', and refactor entity to use a state machine system for more flexibility.
This commit is contained in:
parent
2a2b106df0
commit
001ed4cfa4
4
src/enemies.lua
Normal file
4
src/enemies.lua
Normal file
|
@ -0,0 +1,4 @@
|
|||
-- Convenience import for all enemy types
|
||||
|
||||
import "enemy/ebi"
|
||||
import "enemy/ika"
|
|
@ -11,15 +11,11 @@ class("Ebi").extends(Entity)
|
|||
|
||||
function Ebi:init()
|
||||
Ebi.super.init(self, gfx.image.new("images/ebi.png"), 5)
|
||||
|
||||
self:setCollidesWithGroupsMask(0x3)
|
||||
local dir = 1
|
||||
if math.random(2) == 1 then
|
||||
dir = -1
|
||||
end
|
||||
|
||||
self.vector = geom.vector2D.new(0, dir)
|
||||
self.type = 'ebi'
|
||||
end
|
||||
|
||||
function Ebi:onReady()
|
||||
self.weaponTimer = playdate.timer.new(2500,
|
||||
function()
|
||||
local b = Bullet(2, 1)
|
||||
|
@ -30,8 +26,9 @@ function Ebi:init()
|
|||
self.weaponTimer.repeats = true
|
||||
end
|
||||
|
||||
function Ebi:update()
|
||||
local collisions = Ebi.super.update(self)
|
||||
-- Ebi needs to bounce off the walls
|
||||
function Ebi:runReady()
|
||||
local collisions = Ebi.super.runReady(self)
|
||||
for i=1, #collisions, 1 do
|
||||
if collisions[i].other:getGroupMask() == 0x1 then
|
||||
self.vector.dy *= -1
|
||||
|
@ -39,7 +36,7 @@ function Ebi:update()
|
|||
end
|
||||
end
|
||||
|
||||
function Ebi:delete()
|
||||
Ebi.super.delete(self)
|
||||
self.weaponTimer:remove()
|
||||
function Ebi:remove()
|
||||
Ebi.super.remove(self)
|
||||
if self.weaponTimer then self.weaponTimer:remove() end
|
||||
end
|
||||
|
|
|
@ -12,12 +12,15 @@ class("Ika").extends(Entity)
|
|||
|
||||
function Ika:init(target)
|
||||
Ika.super.init(self, gfx.image.new("images/ika.png"), 25, 1)
|
||||
|
||||
self.target = target
|
||||
self:setCollidesWithGroupsMask(0x2)
|
||||
self.type = 'ika'
|
||||
end
|
||||
|
||||
function Ika:onReady()
|
||||
self.weaponTimer = playdate.timer.new(7000,
|
||||
function()
|
||||
local b = Bullet(9, 20, self:calculateVector(target))
|
||||
local b = Bullet(9, 20, self:calculateVector(self.target))
|
||||
b:moveTo(self.x - (self.width/2) - 1, self.y)
|
||||
b:add()
|
||||
end
|
||||
|
@ -26,9 +29,12 @@ function Ika:init(target)
|
|||
end
|
||||
|
||||
function Ika:calculateVector(target)
|
||||
local vec = geom.vector2D.new(0,0)
|
||||
vec.dx = target.x - self.x
|
||||
vec.dy = target.y - self.y
|
||||
|
||||
local vec = geom.point.new(target:getPosition()) -
|
||||
geom.point.new(self:getPosition())
|
||||
return vec:normalized() * 3
|
||||
end
|
||||
|
||||
function Ika:remove()
|
||||
Ika.super.remove(self)
|
||||
if self.weaponTimer then self.weaponTimer:remove() end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import "CoreLibs/object"
|
||||
import "CoreLibs/graphics"
|
||||
import "CoreLibs/sprites"
|
||||
import "statemachine"
|
||||
|
||||
local gfx <const> = playdate.graphics
|
||||
local geom <const> = playdate.geometry
|
||||
|
@ -10,19 +11,39 @@ class("Entity").extends(gfx.sprite)
|
|||
|
||||
function Entity:init(img, health, armor)
|
||||
Entity.super.init(self, img)
|
||||
self.type = "entity"
|
||||
self.health = health or 10
|
||||
self.armor = armor or 0
|
||||
self.introAnimator = nil
|
||||
|
||||
-- movement direction, every update() the entity will move along this vector and return
|
||||
-- collision data to the subclass
|
||||
self.vector = geom.vector2D.new(0, 0)
|
||||
|
||||
self:setCollideRect(0, 0, self:getSize())
|
||||
|
||||
-- most entities will be enemies, so we configure this mask by default
|
||||
-- We don't set a collider mask because collision is a bit too variable
|
||||
-- (but we should always include 0x2 and handle player collisions)
|
||||
self:setGroupMask(0x4)
|
||||
|
||||
-- state machine mapping
|
||||
-- this can be extended by subclasses if they need more states using
|
||||
-- StateMachine:addState()
|
||||
local states = {
|
||||
["INIT"] = {
|
||||
main=self.runInit,
|
||||
transition=self.onInit
|
||||
},
|
||||
["INTRO"] = {
|
||||
main=self.runIntro,
|
||||
transition=self.onIntro,
|
||||
},
|
||||
["READY"] = {
|
||||
main=self.runReady,
|
||||
transition=self.onReady,
|
||||
},
|
||||
}
|
||||
self.fsm = StateMachine.new({self}, states)
|
||||
end
|
||||
|
||||
function Entity:damage(amount)
|
||||
|
@ -31,16 +52,50 @@ function Entity:damage(amount)
|
|||
self.health = math.max(self.health - (amount - self.armor), 0)
|
||||
|
||||
if self.health == 0 then
|
||||
self:delete()
|
||||
self:remove()
|
||||
end
|
||||
end
|
||||
|
||||
function Entity:add()
|
||||
Entity.super.add(self)
|
||||
if self.introAnimator then
|
||||
self.fsm:changeState("INTRO")
|
||||
else
|
||||
self.fsm:changeState("READY")
|
||||
end
|
||||
end
|
||||
|
||||
function Entity:update()
|
||||
local collisions = select(3, self:moveWithCollisions(self.x + self.vector.dx, self.y + self.vector.dy))
|
||||
return collisions
|
||||
-- update state machine
|
||||
self.fsm:execute()
|
||||
end
|
||||
|
||||
-- override this if you create timers
|
||||
function Entity:delete()
|
||||
self:remove()
|
||||
-- State machine-controlled functions
|
||||
|
||||
function Entity:onInit()
|
||||
-- noop
|
||||
end
|
||||
|
||||
function Entity:runInit()
|
||||
-- noop
|
||||
end
|
||||
|
||||
function Entity:onIntro()
|
||||
-- noop
|
||||
end
|
||||
|
||||
function Entity:runIntro()
|
||||
if self.introAnimator and not self.introAnimator:ended() then
|
||||
self:moveTo(self.introAnimator:currentValue())
|
||||
else
|
||||
self.fsm:changeState("READY")
|
||||
end
|
||||
end
|
||||
|
||||
function Entity:onReady()
|
||||
-- noop
|
||||
end
|
||||
|
||||
function Entity:runReady()
|
||||
return select(3, self:moveWithCollisions(self.x + self.vector.dx, self.y + self.vector.dy))
|
||||
end
|
||||
|
|
|
@ -42,6 +42,7 @@ class("Kani").extends(Entity)
|
|||
function Kani:init(ui)
|
||||
Kani.super.init(self, gfx.image.new("images/kani.png"), 100)
|
||||
|
||||
self.type = 'kani'
|
||||
self:setGroupMask(0x2)
|
||||
self:setCollidesWithGroupsMask(0xd)
|
||||
|
||||
|
@ -76,6 +77,8 @@ function Kani:init(ui)
|
|||
end,
|
||||
AButtonUp = function() self:fire() end,
|
||||
}
|
||||
|
||||
self.fsm:changeState("READY")
|
||||
end
|
||||
|
||||
function Kani:chargeReserve(change)
|
||||
|
@ -141,8 +144,8 @@ function Kani:removeInputHandlers()
|
|||
end
|
||||
|
||||
-- move that crab!
|
||||
function Kani:update()
|
||||
local collisions = Kani.super.update(self)
|
||||
function Kani:runReady()
|
||||
local collisions = Kani.super.runReady(self)
|
||||
for i=0, #collisions, 1 do
|
||||
-- handle player-triggered collisions
|
||||
end
|
||||
|
|
72
src/main.lua
72
src/main.lua
|
@ -9,13 +9,15 @@ import "CoreLibs/graphics"
|
|||
import "CoreLibs/sprites"
|
||||
import "CoreLibs/timer"
|
||||
import "kani"
|
||||
import "enemy/ika"
|
||||
import "enemy/ebi"
|
||||
import "enemies"
|
||||
import "wave"
|
||||
|
||||
local gfx <const> = playdate.graphics
|
||||
local geom <const> = playdate.geometry
|
||||
|
||||
local player = nil
|
||||
local ui = nil
|
||||
local currentWave = nil
|
||||
|
||||
function setup()
|
||||
ui = UI()
|
||||
|
@ -24,26 +26,8 @@ function setup()
|
|||
player:addInputHandlers()
|
||||
player:add()
|
||||
ui:add()
|
||||
|
||||
local enemy = Ika(player)
|
||||
enemy:moveTo(350, 120)
|
||||
enemy:add()
|
||||
|
||||
-- enemy = Ebi()
|
||||
-- enemy:moveTo(270, 50)
|
||||
-- enemy:add()
|
||||
|
||||
-- enemy = Ebi()
|
||||
-- enemy:moveTo(280, 100)
|
||||
-- enemy:add()
|
||||
|
||||
-- enemy = Ebi()
|
||||
-- enemy:moveTo(290, 150)
|
||||
-- enemy:add()
|
||||
|
||||
-- enemy = Ebi()
|
||||
-- enemy:moveTo(300, 200)
|
||||
-- enemy:add()
|
||||
currentWave = newWave()
|
||||
currentWave:add()
|
||||
|
||||
makeWalls()
|
||||
drawBackground()
|
||||
|
@ -81,8 +65,52 @@ function drawBackground()
|
|||
)
|
||||
end
|
||||
|
||||
-- Right now we only have a single wave and we repeat it forever
|
||||
function newWave()
|
||||
wave = Wave.new()
|
||||
|
||||
local startPosition = geom.point.new(410,120)
|
||||
|
||||
local enemy = Ika(player)
|
||||
enemy.introAnimator = gfx.animator.new(
|
||||
10000,
|
||||
startPosition,
|
||||
geom.point.new(350,120)
|
||||
)
|
||||
|
||||
wave:addEntity(enemy)
|
||||
|
||||
local y = 50
|
||||
for x=270, 300, 10 do
|
||||
local dir = 1
|
||||
if math.random(2) == 1 then
|
||||
dir = -1
|
||||
end
|
||||
local vector = geom.vector2D.new(0, dir)
|
||||
|
||||
enemy = Ebi()
|
||||
enemy.vector = vector
|
||||
enemy.introAnimator = gfx.animator.new(
|
||||
5000,
|
||||
startPosition,
|
||||
geom.point.new(x,y)
|
||||
)
|
||||
|
||||
wave:addEntity(enemy)
|
||||
|
||||
y += 50
|
||||
end
|
||||
|
||||
return wave
|
||||
end
|
||||
|
||||
function playdate.update()
|
||||
gfx.sprite.update()
|
||||
if currentWave:update() then
|
||||
-- fight forever lol
|
||||
currentWave = newWave()
|
||||
currentWave:add()
|
||||
end
|
||||
playdate.timer.updateTimers()
|
||||
end
|
||||
|
||||
|
|
47
src/statemachine.lua
Normal file
47
src/statemachine.lua
Normal file
|
@ -0,0 +1,47 @@
|
|||
-- A state machine
|
||||
import "CoreLibs/object"
|
||||
|
||||
class("StateMachine").extends()
|
||||
|
||||
function StateMachine.new(params, stateTable)
|
||||
local fsm = StateMachine(params, stateTable)
|
||||
return fsm
|
||||
end
|
||||
|
||||
-- params is a table of parameters to always pass to the functions
|
||||
-- stateTable is a table of initial states. Entries should be of the form:
|
||||
-- name = {main=mainFunc, transition=transFunc}
|
||||
-- where mainFunc and transFunc are functions that should be executed in relation to the state.
|
||||
-- the State Machine will call mainFunc on execute()
|
||||
-- the State Machine will call transFunc when the state is entered
|
||||
function StateMachine:init(params, stateTable)
|
||||
self.currentState = nil
|
||||
self.parameters = params or {}
|
||||
self.states = stateTable or {}
|
||||
end
|
||||
|
||||
function StateMachine:addState(name, mainFunc, transFunc)
|
||||
self.states[name] = {main=mainFunc, transition=transFunc}
|
||||
end
|
||||
|
||||
-- execute the main function for the current state.
|
||||
-- returns true if the function was executed, false otherwise
|
||||
function StateMachine:execute()
|
||||
if self.currentState == nil or not self.states[self.currentState] then
|
||||
return false
|
||||
end
|
||||
|
||||
self.states[self.currentState].main(table.unpack(self.parameters))
|
||||
return true
|
||||
end
|
||||
|
||||
-- change to the specified state. returns true if the state change is successful
|
||||
function StateMachine:changeState(state)
|
||||
if not self.states[state] then return false end
|
||||
|
||||
self.currentState = state
|
||||
if self.states[state].transition then
|
||||
self.states[state].transition(table.unpack(self.parameters))
|
||||
end
|
||||
return true
|
||||
end
|
64
src/wave.lua
Normal file
64
src/wave.lua
Normal file
|
@ -0,0 +1,64 @@
|
|||
-- A set of enemies that spawn as a group
|
||||
-- TODO: initialize waves from a file so we can define levels
|
||||
|
||||
import "CoreLibs/object"
|
||||
import "entity"
|
||||
|
||||
local gfx <const> = playdate.graphics
|
||||
local geom <const> = playdate.geometry
|
||||
|
||||
class("Wave").extends()
|
||||
|
||||
function Wave.new()
|
||||
local w = Wave()
|
||||
return w
|
||||
end
|
||||
|
||||
function Wave:init()
|
||||
Wave.super.init(self)
|
||||
self.entities = {}
|
||||
end
|
||||
|
||||
-- Adds an entity to the wave. Positions are playdate.geometry.point objects
|
||||
-- - position is where the entity should end up at the start of the combat sequence
|
||||
-- - vec is the enemy's starting vector
|
||||
-- - prePosition is the entity's start location for "flying in" animation
|
||||
-- - introDuration is the number of milliseconds the animation should take. Use this to control the fly-in speed
|
||||
function Wave:addEntity(entity)
|
||||
table.insert(self.entities, entity)
|
||||
end
|
||||
|
||||
-- Add all the sprites in the Wave to the global table
|
||||
-- Also initializes their intro animations
|
||||
function Wave:add()
|
||||
for i=1, #self.entities, 1 do
|
||||
self.entities[i]:add()
|
||||
end
|
||||
end
|
||||
|
||||
-- Call this every frame for an active Wave
|
||||
-- It will crawl the Entity list and remove any "dead"
|
||||
-- entities, and will return true to signal the wave end
|
||||
-- when all enemies are defeated
|
||||
function Wave:update()
|
||||
local dead = {}
|
||||
for i=1, #self.entities, 1 do
|
||||
if self.entities[i].health == 0 then
|
||||
table.insert(dead, i)
|
||||
print(string.format("%d is dead", i))
|
||||
end
|
||||
end
|
||||
|
||||
-- Starting with the highest index to avoid renumbering errors,
|
||||
-- we remove dead entities from the list
|
||||
while #dead > 0 do
|
||||
table.remove(self.entities, table.remove(dead))
|
||||
end
|
||||
|
||||
if #self.entities == 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user