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:
Anna Rose 2023-10-03 16:16:19 -04:00
parent 2a2b106df0
commit 001ed4cfa4
8 changed files with 253 additions and 49 deletions

4
src/enemies.lua Normal file
View File

@ -0,0 +1,4 @@
-- Convenience import for all enemy types
import "enemy/ebi"
import "enemy/ika"

View File

@ -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
dir = -1
end 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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
View 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