Repository navigation
Expand file tree
/
Copy pathenemy.lua
More file actions
474 lines (423 loc) · 16.9 KB
/
Copy pathenemy.lua
File metadata and controls
474 lines (423 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
local ENUMS = require('enums')
local IMAGES = require('lib.images')
local SOUNDS = require('lib.sounds')
local UTIL = require('level.util')
local SETTINGS = require('settings')
--[[
*ENEMY PATHING NOTES
pathing only updates on turret buy/sell
enemies will have different paths based on position and final
--]]
---enemy class
---@class ENEMY : EnemyData inherit properties of EnemyData
---@field position {x:number,y:number} absolute coordinates
---@field coords {x:number,y:number} tile coordinates
---@field protected moveAnimation {frames:table[],currentFrame:number,frameTime:number,timer:number,loop:boolean,death:boolean,dead:boolean}
---@field protected deathAnimation {frames:table[],currentFrame:number,frameTime:number,timer:number,origin:{x:number,y:number}}
---@field index string uuid key for game.gameState.enemies
---@field orientation ENUMS.FLOWFIELD.TILE orientation of current sprite
---@field slow_rate number `Range: 0-1` slow % (0.21) caused by turret with slow attribute
---@field protected lastDirection ENUMS.FLOWFIELD.TILE
---@field flowField FLOWFIELD which flowField for the enemy to follow
---@field selected boolean determine if enemy is selected
---@field protected splitter {amount: integer, type:ENUMS.ENEMY_TYPE, x:integer,y:integer} enemies that split into more enemies after dying
---@field dying boolean mark enemy is dying or not
---@field fullHealth number enemy's health when full
---@field protected healthBar {width:number,height:number,x:number,y:number,value:number}
---@field protected origin {x:number,y:number} origin x,y for rotation
---@field protected stunned number stunned timer, if 0, entity is not stunned
---@field private scale number how large the enemy is
---@overload fun(gameState: GAME.GAMESTATE, enemyType: number, flowField: FLOWFIELD): ENEMY
local lib = setmetatable({},
{
__call = function(class, gameState, enemyType, flowField)
---@cast class ENEMY
return class:new(gameState, enemyType, flowField)
end
}
)
local enemies_sprite_sheet = IMAGES.library["enemies"]
---creates array of sprites for the enemy
---@private
---@param enemyType ENUMS.ENEMY_TYPE
---@return love.Quad[]
local function enemyMoveFrames(enemyType)
--480x128 -> 32x32
local frames = {}
local spriteSize = 32
--some enemies have 3 and some have 4 move frames
local frameCounter = 4
if enemyType == "BOSS1" or enemyType == "SCORPION" or enemyType == "BOSS2" then
frameCounter = 3
end
local index = 0
for i=1,#ENUMS.ENEMY_TYPE do
if ENUMS.ENEMY_TYPE[i] == enemyType then
index = i
break
end
end
for i=1,frameCounter do
frames[i] = love.graphics.newQuad(
(index-1)*spriteSize, --start x
(i-1)*spriteSize, --start y
spriteSize,--width
spriteSize,--height
enemies_sprite_sheet:getDimensions()
)
end
return frames
end
---@param gameState GAME.GAMESTATE from game.gameState, contains array of enemy objects
---@param enemyTypeName ENUMS.ENEMY_TYPE enemy type
---@param flowField FLOWFIELD pointer to flowField for the enemy to follow
---@param health? integer scale the enemy health based on the wave
---@param value? integer enemy kill value
---@param speed? number scale enemy speed
---@param scale? number scale enemy size
---@param splitter? {amount: integer, type:ENUMS.ENEMY_TYPE} how many enemies to split into
---@param coords? {x:number,y:number} position of splitter
function lib:new(gameState, enemyTypeName, flowField, health, value, speed, scale, splitter, coords)
--check if valid flowField
if not flowField then return end
--copy turret
local o = {}
for k, v in pairs(ENUMS.ENEMY[enemyTypeName]) do o[k] = v end
setmetatable(o, self)
self.__index = self
--overwrite speed/health for scaling
if health then o.health = health end
if speed then o.speed = speed end
if value then o.value = value end
local rand = 0
if flowField.direction == ENUMS.FLOWFIELD.LONGITUDE then
rand = love.math.random(12, 19)
o.position = {x=0, y=SETTINGS.TILE_SIZE*rand - SETTINGS.TILE_SIZE/2}
o.coords = {x=1, y=rand}
o.lastDirection = ENUMS.FLOWFIELD.TILE.RIGHT
elseif flowField.direction == ENUMS.FLOWFIELD.LATITUDE then
rand = love.math.random(13, 21)
o.position = {x=SETTINGS.TILE_SIZE*rand - SETTINGS.TILE_SIZE/2, y=0}
o.coords = {x=rand, y=1}
o.lastDirection = ENUMS.FLOWFIELD.TILE.DOWN
end
if coords then
o.coords = {x=coords.x,y=coords.y}
o.position = {x=SETTINGS.TILE_SIZE*coords.x - SETTINGS.TILE_SIZE/2, y=SETTINGS.TILE_SIZE*coords.y - SETTINGS.TILE_SIZE/2}
end
o.moveAnimation = {
frames = enemyMoveFrames(enemyTypeName),
currentFrame = 1,
frameTime = 0.1,
timer = 0,
loop = true,
}
o.deathAnimation = {
frames = ENUMS.AlienDeathFrames,
currentFrame = 1,
frameTime = 0.03,
timer = 0,
}
local frameW, frameH = o.deathAnimation.frames[o.deathAnimation.currentFrame]:getDimensions()
o.deathAnimation.origin = {x = frameW/2, y = frameH/2}
o.dying = false
o.index = UTIL.uuid()
o.flowField = flowField
o.orientation = 0
o.slow_rate = 1
local _,_,w,h = o.moveAnimation.frames[o.moveAnimation.currentFrame]:getViewport()
o.origin = { x = w/2, y = h/2 }
o.turning = false
o.goal = false
o.stunned = 0 --stun timer
o.fullHealth = o.health
o.healthBar = {
width = 2*w/3,
height = h/5,
x = 0,
y = 0,
value = o.fullHealth
}
if scale then
o.scale = scale*SETTINGS.scale
else
o.scale = SETTINGS.scale
end
if splitter then
o.splitter = {
amount = splitter.amount or 0,
type = splitter.type or nil,
}
end
gameState.enemies[o.index] = o
--return o
end
---draw the enemy frame
function lib:draw()
if self.dying then
love.graphics.draw(
self.deathAnimation.frames[self.deathAnimation.currentFrame],
self.position.x,
self.position.y,
0, --orientation (radians)
self.scale/3, --x scale
self.scale/3, --y scale
self.deathAnimation.origin.x,
self.deathAnimation.origin.y
)
local text = "+"..self.value.."$"
local font = love.graphics.getFont()
local textW = font:getWidth(text)
local textH = font:getHeight()
love.graphics.setColor(ENUMS.UPGRADE_COLORS.YELLOW)
love.graphics.print(text, self.position.x - textW/2, self.position.y - textH - SETTINGS.TILE_SIZE/3)
love.graphics.setColor{1,1,1}
else
--draw selected
if self.selected then
love.graphics.setColor(ENUMS.UPGRADE_COLORS.YELLOW)
love.graphics.circle("line",
self.position.x,
self.position.y,
SETTINGS.TILE_SIZE, --radius, should come from turret?
20
)
love.graphics.setColor{1,1,1}
--TODO change to 8 lines in a rectangle?
end
if self.stunned > 0 then
love.graphics.setColor(ENUMS.UPGRADE_COLORS.YELLOW)
elseif self.slow_rate ~= 1 then
love.graphics.setColor(ENUMS.UPGRADE_COLORS.CYAN)
else
love.graphics.setColor{1,1,1}
end
love.graphics.draw(
enemies_sprite_sheet,
self.moveAnimation.frames[self.moveAnimation.currentFrame],
self.position.x,
self.position.y,
self.orientation, --orientation (radians)
self.scale, --x scale
self.scale, --y scale
self.origin.x, --origin x for rotation
self.origin.y --origin y for rotation
)
--%
love.graphics.setColor{0.2, 0.8, 0.2}
love.graphics.rectangle("fill", self.healthBar.x, self.healthBar.y, self.healthBar.width*self.healthBar.value, self.healthBar.height)
--bar border
love.graphics.setColor{0,0,0}
love.graphics.rectangle("line", self.healthBar.x, self.healthBar.y, self.healthBar.width, self.healthBar.height)
--reset color
love.graphics.setColor{1,1,1}
end
end
---logic to turn enemy from one direction to another
---@private
---@param dt number delta time between last frame and current frame
---@param newDirection ENUMS.FLOWFIELD.TILE
function lib:turn(dt, newDirection)
--snap to center of current tile
self.position.x = (self.coords.x - 0.5)*SETTINGS.TILE_SIZE
self.position.y = (self.coords.y - 0.5)*SETTINGS.TILE_SIZE
--get radians of new direction
local radians = 0
if newDirection == ENUMS.FLOWFIELD.TILE.UP then
radians = ENUMS.ORIENTATION.UP
elseif newDirection == ENUMS.FLOWFIELD.TILE.LEFT then
radians = ENUMS.ORIENTATION.LEFT
elseif newDirection == ENUMS.FLOWFIELD.TILE.DOWN then
radians = ENUMS.ORIENTATION.DOWN
elseif newDirection == ENUMS.FLOWFIELD.TILE.RIGHT then
radians = ENUMS.ORIENTATION.RIGHT
else
--push back to original tile by adding half of tile size to original direction
local push = SETTINGS.TILE_SIZE / 2
if self.lastDirection == ENUMS.FLOWFIELD.TILE.UP then
self.position.y = self.position.y + push
elseif self.lastDirection == ENUMS.FLOWFIELD.TILE.DOWN then
self.position.y = self.position.y - push
elseif self.lastDirection == ENUMS.FLOWFIELD.TILE.LEFT then
self.position.x = self.position.x + push
elseif self.lastDirection == ENUMS.FLOWFIELD.TILE.RIGHT then
self.position.x = self.position.x - push
end
end
-- +/- diff of final - current
local diff = radians - self.orientation
--Normalize the difference to be between -pi and pi
while diff > math.pi do diff = diff - 2 * math.pi end
while diff < -math.pi do diff = diff + 2 * math.pi end
local turnAmount = math.pi * dt
--snap to direction if next frame gets us to right direction
if math.abs(diff) <= turnAmount then
self.orientation = radians
self.turning = false
self.lastDirection = newDirection
else
self.orientation = self.orientation + turnAmount * (diff > 0 and 1 or -1)
end
end
---update the enemy state
---@param dt number delta time between last frame and current frame
---@param gameState GAME.GAMESTATE
function lib:update(dt, gameState)
--check health
if self.health <= 0 and not self.dying then
self.dying = true
end
--update healthBar position & health
self.healthBar.x = self.position.x
self.healthBar.y = self.position.y
self.healthBar.value = self.health / self.fullHealth
--calc tile position and direction of tile
self.coords.x = math.floor(self.position.x / SETTINGS.TILE_SIZE)+1
self.coords.y = math.floor(self.position.y / SETTINGS.TILE_SIZE)+1
local direction = self.flowField:getDirection(self.coords.x, self.coords.y)
--debug bounds check
if direction == ENUMS.FLOWFIELD.TILE.BLOCKED and not self.dying and not self.air then
print(string.format("Enemy out of bounds! Coords: %d, %d Position: %.2f,%.2f",
self.coords.x, self.coords.y, self.position.x, self.position.y))
--push to nearest tile with a valid direction
local offsets = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1}} --orthogonal then diagonal
for _,offset in ipairs(offsets) do
local newX = self.coords.x + offset[1]
local newY = self.coords.y + offset[2]
if self.flowField:getDirection(newX, newY) ~= ENUMS.FLOWFIELD.TILE.BLOCKED then
--get new tile coords
self.coords.x = newX
self.coords.y = newY
--recenter
self.position.x = (self.coords.x - 0.5)*SETTINGS.TILE_SIZE
self.position.y = (self.coords.y - 0.5)*SETTINGS.TILE_SIZE
--set last direction
self.lastDirection = self.flowField:getDirection(newX, newY)
break
end
end
end
--check if at goal
if direction == ENUMS.FLOWFIELD.TILE.GOAL then
return self:finished(gameState)
end
--get to center of current tile before new direction logic
local center = {
x = (self.coords.x - 0.5) * SETTINGS.TILE_SIZE,
y = (self.coords.y - 0.5) * SETTINGS.TILE_SIZE
}
--check if we're close to center of tile
local threshold = (self.slow_rate*(self.speed / 4)) * 3 -- Make threshold larger to catch the center
local centerCheck = {
x = math.abs(self.position.x - center.x) <= threshold,
y = math.abs(self.position.y - center.y) <= threshold
}
local atCenter = centerCheck.x and centerCheck.y
--reduce slow rate
if self.slow_rate < 1 then
self.slow_rate = self.slow_rate + dt/2
if self.slow_rate > 1 then self.slow_rate = 1 end
end
--calc stun
if self.stunned > 0 then
self.stunned = self.stunned - dt
if self.stunned < 0 then self.stunned = 0 end
end
--turning state machine
if (not self.turning and not self.dying and self.stunned == 0) or self.air then
--move enemy
if self.lastDirection == ENUMS.FLOWFIELD.TILE.UP then
self.position.y = self.position.y - self.slow_rate*self.speed
self.orientation = ENUMS.ORIENTATION.UP
elseif self.lastDirection == ENUMS.FLOWFIELD.TILE.DOWN then
self.position.y = self.position.y + self.slow_rate*self.speed
self.orientation = ENUMS.ORIENTATION.DOWN
elseif self.lastDirection == ENUMS.FLOWFIELD.TILE.LEFT then
self.position.x = self.position.x - self.slow_rate*self.speed
self.orientation = ENUMS.ORIENTATION.LEFT
elseif self.lastDirection == ENUMS.FLOWFIELD.TILE.RIGHT then
self.position.x = self.position.x + self.slow_rate*self.speed
self.orientation = ENUMS.ORIENTATION.RIGHT
end
if (atCenter) and (direction ~= self.lastDirection) and (not self.air) then
self.position.x = center.x
self.position.y = center.y
self.turning = true
end
else
self:turn(dt, direction)
end
--sprite moving animation
local animate = self.moveAnimation
if not self.dying then
animate.timer = animate.timer + dt
if animate.timer >= animate.frameTime then
animate.timer = animate.timer - animate.frameTime
animate.currentFrame = animate.currentFrame + 1
if animate.currentFrame > #animate.frames then
if animate.loop then
animate.currentFrame = 1
else
animate.currentFrame = #animate.frames
end
end
end
else --death animation
self.deathAnimation.timer = self.deathAnimation.timer + dt
--reset animation timers
if self.deathAnimation.timer >= self.deathAnimation.frameTime then
self.deathAnimation.timer = self.deathAnimation.timer - self.deathAnimation.frameTime
self.deathAnimation.currentFrame = self.deathAnimation.currentFrame + 1
if self.deathAnimation.currentFrame > #self.deathAnimation.frames then
self.deathAnimation.currentFrame = #self.deathAnimation.frames
return self:kill(gameState)
end
end
end
end
---Remove entity from game state and award money
---@param gameState GAME.GAMESTATE
function lib:kill(gameState)
local sound = SOUNDS.library["enemy_kill"]:clone()
sound:play()
--if splitter spawn more enemies
if self.splitter then
for i=1,self.splitter.amount do
lib:new(gameState, self.splitter.type, self.flowField, self.fullHealth/self.splitter.amount, math.ceil(self.value/self.splitter.amount), nil, nil, nil, {x=self.coords.x,y=self.coords.y})
end
end
gameState.money = gameState.money + self.value
gameState.enemies[self.index] = nil
end
---Remove entity from game state and lose life
---@param gameState GAME.GAMESTATE
function lib:finished(gameState)
SOUNDS.library["lose_life"]:play()
gameState.lives = gameState.lives - 1
gameState.enemies[self.index] = nil
end
---slow down movement for a time period
---@param percent integer
function lib:slow(percent)
self.slow_rate = 1 - percent/100
end
---stun enemy for a short period
---@param t number timer for how long to stun
function lib:stun(t)
self.stunned = t
end
---turret clicked on check
---@param x number x mouse coordinate
---@param y number y mouse coordinate
---@return boolean
function lib:select(x,y)
local size = SETTINGS.TILE_SIZE
if (x >= self.position.x - size/2) and (x <= self.position.x + size/2) and
(y >= self.position.y - size/2) and (y <= self.position.y + size/2) then
self.selected = true
return true
end
self.selected = false
return false
end
return lib