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()
|
function Ebi:init()
|
||||||
Ebi.super.init(self, gfx.image.new("images/ebi.png"), 5)
|
Ebi.super.init(self, gfx.image.new("images/ebi.png"), 5)
|
||||||
|
|
||||||
self:setCollidesWithGroupsMask(0x3)
|
self:setCollidesWithGroupsMask(0x3)
|
||||||
local dir = 1
|
self.type = 'ebi'
|
||||||
if math.random(2) == 1 then
|
end
|
||||||
dir = -1
|
|
||||||
end
|
|
||||||
|
|
||||||
self.vector = geom.vector2D.new(0, dir)
|
|
||||||
|
|
||||||
|
function Ebi:onReady()
|
||||||
self.weaponTimer = playdate.timer.new(2500,
|
self.weaponTimer = playdate.timer.new(2500,
|
||||||
function()
|
function()
|
||||||
local b = Bullet(2, 1)
|
local b = Bullet(2, 1)
|
||||||
|
@ -30,8 +26,9 @@ function Ebi:init()
|
||||||
self.weaponTimer.repeats = true
|
self.weaponTimer.repeats = true
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ebi:update()
|
-- Ebi needs to bounce off the walls
|
||||||
local collisions = Ebi.super.update(self)
|
function Ebi:runReady()
|
||||||
|
local collisions = Ebi.super.runReady(self)
|
||||||
for i=1, #collisions, 1 do
|
for i=1, #collisions, 1 do
|
||||||
if collisions[i].other:getGroupMask() == 0x1 then
|
if collisions[i].other:getGroupMask() == 0x1 then
|
||||||
self.vector.dy *= -1
|
self.vector.dy *= -1
|
||||||
|
@ -39,7 +36,7 @@ function Ebi:update()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ebi:delete()
|
function Ebi:remove()
|
||||||
Ebi.super.delete(self)
|
Ebi.super.remove(self)
|
||||||
self.weaponTimer:remove()
|
if self.weaponTimer then self.weaponTimer:remove() end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,12 +12,15 @@ class("Ika").extends(Entity)
|
||||||
|
|
||||||
function Ika:init(target)
|
function Ika:init(target)
|
||||||
Ika.super.init(self, gfx.image.new("images/ika.png"), 25, 1)
|
Ika.super.init(self, gfx.image.new("images/ika.png"), 25, 1)
|
||||||
|
self.target = target
|
||||||
self:setCollidesWithGroupsMask(0x2)
|
self:setCollidesWithGroupsMask(0x2)
|
||||||
|
self.type = 'ika'
|
||||||
|
end
|
||||||
|
|
||||||
|
function Ika:onReady()
|
||||||
self.weaponTimer = playdate.timer.new(7000,
|
self.weaponTimer = playdate.timer.new(7000,
|
||||||
function()
|
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:moveTo(self.x - (self.width/2) - 1, self.y)
|
||||||
b:add()
|
b:add()
|
||||||
end
|
end
|
||||||
|
@ -26,9 +29,12 @@ function Ika:init(target)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Ika:calculateVector(target)
|
function Ika:calculateVector(target)
|
||||||
local vec = geom.vector2D.new(0,0)
|
local vec = geom.point.new(target:getPosition()) -
|
||||||
vec.dx = target.x - self.x
|
geom.point.new(self:getPosition())
|
||||||
vec.dy = target.y - self.y
|
|
||||||
|
|
||||||
return vec:normalized() * 3
|
return vec:normalized() * 3
|
||||||
end
|
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/object"
|
||||||
import "CoreLibs/graphics"
|
import "CoreLibs/graphics"
|
||||||
import "CoreLibs/sprites"
|
import "CoreLibs/sprites"
|
||||||
|
import "statemachine"
|
||||||
|
|
||||||
local gfx <const> = playdate.graphics
|
local gfx <const> = playdate.graphics
|
||||||
local geom <const> = playdate.geometry
|
local geom <const> = playdate.geometry
|
||||||
|
@ -10,19 +11,39 @@ class("Entity").extends(gfx.sprite)
|
||||||
|
|
||||||
function Entity:init(img, health, armor)
|
function Entity:init(img, health, armor)
|
||||||
Entity.super.init(self, img)
|
Entity.super.init(self, img)
|
||||||
|
self.type = "entity"
|
||||||
self.health = health or 10
|
self.health = health or 10
|
||||||
self.armor = armor or 0
|
self.armor = armor or 0
|
||||||
|
self.introAnimator = nil
|
||||||
|
|
||||||
-- movement direction, every update() the entity will move along this vector and return
|
-- movement direction, every update() the entity will move along this vector and return
|
||||||
-- collision data to the subclass
|
-- collision data to the subclass
|
||||||
self.vector = geom.vector2D.new(0, 0)
|
self.vector = geom.vector2D.new(0, 0)
|
||||||
|
|
||||||
self:setCollideRect(0, 0, self:getSize())
|
self:setCollideRect(0, 0, self:getSize())
|
||||||
|
|
||||||
-- most entities will be enemies, so we configure this mask by default
|
-- 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
|
-- We don't set a collider mask because collision is a bit too variable
|
||||||
-- (but we should always include 0x2 and handle player collisions)
|
-- (but we should always include 0x2 and handle player collisions)
|
||||||
self:setGroupMask(0x4)
|
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
|
end
|
||||||
|
|
||||||
function Entity:damage(amount)
|
function Entity:damage(amount)
|
||||||
|
@ -31,16 +52,50 @@ function Entity:damage(amount)
|
||||||
self.health = math.max(self.health - (amount - self.armor), 0)
|
self.health = math.max(self.health - (amount - self.armor), 0)
|
||||||
|
|
||||||
if self.health == 0 then
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
function Entity:update()
|
function Entity:update()
|
||||||
local collisions = select(3, self:moveWithCollisions(self.x + self.vector.dx, self.y + self.vector.dy))
|
-- update state machine
|
||||||
return collisions
|
self.fsm:execute()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- override this if you create timers
|
-- State machine-controlled functions
|
||||||
function Entity:delete()
|
|
||||||
self:remove()
|
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
|
end
|
||||||
|
|
|
@ -42,6 +42,7 @@ class("Kani").extends(Entity)
|
||||||
function Kani:init(ui)
|
function Kani:init(ui)
|
||||||
Kani.super.init(self, gfx.image.new("images/kani.png"), 100)
|
Kani.super.init(self, gfx.image.new("images/kani.png"), 100)
|
||||||
|
|
||||||
|
self.type = 'kani'
|
||||||
self:setGroupMask(0x2)
|
self:setGroupMask(0x2)
|
||||||
self:setCollidesWithGroupsMask(0xd)
|
self:setCollidesWithGroupsMask(0xd)
|
||||||
|
|
||||||
|
@ -76,6 +77,8 @@ function Kani:init(ui)
|
||||||
end,
|
end,
|
||||||
AButtonUp = function() self:fire() end,
|
AButtonUp = function() self:fire() end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.fsm:changeState("READY")
|
||||||
end
|
end
|
||||||
|
|
||||||
function Kani:chargeReserve(change)
|
function Kani:chargeReserve(change)
|
||||||
|
@ -141,8 +144,8 @@ function Kani:removeInputHandlers()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- move that crab!
|
-- move that crab!
|
||||||
function Kani:update()
|
function Kani:runReady()
|
||||||
local collisions = Kani.super.update(self)
|
local collisions = Kani.super.runReady(self)
|
||||||
for i=0, #collisions, 1 do
|
for i=0, #collisions, 1 do
|
||||||
-- handle player-triggered collisions
|
-- handle player-triggered collisions
|
||||||
end
|
end
|
||||||
|
|
72
src/main.lua
72
src/main.lua
|
@ -9,13 +9,15 @@ import "CoreLibs/graphics"
|
||||||
import "CoreLibs/sprites"
|
import "CoreLibs/sprites"
|
||||||
import "CoreLibs/timer"
|
import "CoreLibs/timer"
|
||||||
import "kani"
|
import "kani"
|
||||||
import "enemy/ika"
|
import "enemies"
|
||||||
import "enemy/ebi"
|
import "wave"
|
||||||
|
|
||||||
local gfx <const> = playdate.graphics
|
local gfx <const> = playdate.graphics
|
||||||
|
local geom <const> = playdate.geometry
|
||||||
|
|
||||||
local player = nil
|
local player = nil
|
||||||
local ui = nil
|
local ui = nil
|
||||||
|
local currentWave = nil
|
||||||
|
|
||||||
function setup()
|
function setup()
|
||||||
ui = UI()
|
ui = UI()
|
||||||
|
@ -24,26 +26,8 @@ function setup()
|
||||||
player:addInputHandlers()
|
player:addInputHandlers()
|
||||||
player:add()
|
player:add()
|
||||||
ui:add()
|
ui:add()
|
||||||
|
currentWave = newWave()
|
||||||
local enemy = Ika(player)
|
currentWave:add()
|
||||||
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()
|
|
||||||
|
|
||||||
makeWalls()
|
makeWalls()
|
||||||
drawBackground()
|
drawBackground()
|
||||||
|
@ -81,8 +65,52 @@ function drawBackground()
|
||||||
)
|
)
|
||||||
end
|
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()
|
function playdate.update()
|
||||||
gfx.sprite.update()
|
gfx.sprite.update()
|
||||||
|
if currentWave:update() then
|
||||||
|
-- fight forever lol
|
||||||
|
currentWave = newWave()
|
||||||
|
currentWave:add()
|
||||||
|
end
|
||||||
playdate.timer.updateTimers()
|
playdate.timer.updateTimers()
|
||||||
end
|
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