-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathminions.lua
More file actions
411 lines (372 loc) · 13.6 KB
/
minions.lua
File metadata and controls
411 lines (372 loc) · 13.6 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
require("game.state")
require("game.conf")
require("game.tilemap")
game.minions = {}
local MINION_SIZE = game.conf.minions.size
local state = game.state.minions
local idleState = 1
local idleUpdateTimer = 1
--- generate a new minion id that is unique for the current level
local function gen_id()
state.minion_id_seq = state.minion_id_seq + 1
return state.minion_id_seq
end
--- trigger other game components when a minion enters a new tile
local function triggerMinionStepOn(minion, mapX, mapY)
print(minion.name .. " " .. tostring(minion.id) .. " stepped on new tile " .. mapX .. "x" .. mapY)
game.tilemap.stepOn(mapX, mapY, minion)
end
--- trigger other game components when a minion leaves a tile
local function triggerMinionStepOff(minion, mapX, mapY)
print(minion.name .. " " .. tostring(minion.id) .. " stepped off old tile " .. mapX .. "x" .. mapY)
game.tilemap.stepOff(mapX, mapY, minion)
end
--- handle the movement behavior of a minion
local function moveMinion(minion, dt)
local x, y = minion.position.x, minion.position.y
local mapX, mapY = game.tilemap.screenToWorldPos(minion.position.x, minion.position.y)
local hasMoved = false
--- checks whether the screen position x,y is accessible by the current minion
local function canMoveTo(x, y)
local mapX, mapY = game.tilemap.screenToWorldPos(x, y)
if minion.movementType == "walking" then
return game.tilemap.getValue(mapX, mapY, "walkable")
elseif minion.movementType == "flying" then
return game.tilemap.getValue(mapX, mapY, "overFlyable")
else
error("minion " .. minion.name .. " uses unknown movementType " .. minion.movementType)
end
end
-- handle movement in all four directions
local isUp = love.keyboard.isScancodeDown("w") or love.keyboard.isScancodeDown("up")
local isRight = love.keyboard.isScancodeDown("d") or love.keyboard.isScancodeDown("right")
local isDown = love.keyboard.isScancodeDown("s") or love.keyboard.isScancodeDown("down")
local isLeft = love.keyboard.isScancodeDown("a") or love.keyboard.isScancodeDown("left")
if isUp and not isDown and canMoveTo(x, y - minion.moveSpeed * dt) then
minion.position.y = y - minion.moveSpeed * dt
hasMoved = true
end
if isRight and not isLeft and canMoveTo(x + minion.moveSpeed * dt, y) then
minion.position.x = x + minion.moveSpeed * dt
hasMoved = true
end
if isDown and not isUp and canMoveTo(x, y + minion.moveSpeed * dt) then
minion.position.y = y + minion.moveSpeed * dt
hasMoved = true
end
if isLeft and not isRight and canMoveTo(x - minion.moveSpeed * dt, y) then
minion.position.x = x - minion.moveSpeed * dt
hasMoved = true
end
-- trigger events if stepped on a new tile
local newMapX, newMapY = game.tilemap.screenToWorldPos(minion.position.x, minion.position.y)
if (mapX ~= newMapX or mapY ~= newMapY) and minion.canStepOn then
triggerMinionStepOff(minion, mapX, mapY)
triggerMinionStepOn(minion, newMapX, newMapY)
end
end
--- handle the rotation behavior of a minion
local function rotateMinion(minion)
local isUp = love.keyboard.isScancodeDown("w") or love.keyboard.isScancodeDown("up")
local isRight = love.keyboard.isScancodeDown("d") or love.keyboard.isScancodeDown("right")
local isDown = love.keyboard.isScancodeDown("s") or love.keyboard.isScancodeDown("down")
local isLeft = love.keyboard.isScancodeDown("a") or love.keyboard.isScancodeDown("left")
-- combinations that are supposed to do nothing
if not isUp and not isRight and not isDown and not isLeft then
-- combinations that dont make any sense
elseif isUp and isDown and not isRight and not isLeft then
elseif isLeft and isRight and not isUp and not isDown then
elseif isUp and isDown and isRight and isLeft then
-- single button combinations
elseif isUp and not isDown and not isRight and not isLeft then
minion.angle = 0
elseif isRight and not isUp and not isDown and not isLeft then
minion.angle = 90
elseif isDown and not isUp and not isRight and not isLeft then
minion.angle = 180
elseif isLeft and not isUp and not isRight and not isDown then
minion.angle = 270
-- diagonals
elseif isUp and isRight and not isDown and not isLeft then
minion.angle = 45
elseif isRight and isDown and not isUp and not isLeft then
minion.angle = 90 + 45
elseif isDown and isLeft and not isUp and not isRight then
minion.angle = 180 + 45
elseif isLeft and isUp and not isRight and not isDown then
minion.angle = 270 + 45
-- combinations that only partially make sense
elseif isUp and isLeft and isRight and not isDown then
minion.angle = 0
elseif isRight and isUp and isDown and not isLeft then
minion.angle = 90
elseif isDown and isLeft and isRight and not isUp then
minion.angle = 180
elseif isLeft and isUp and isDown and not isRight then
minion.angle = 270
else
error(
"unhandled input combination for minion: "
.. tostring(isUp)
.. " "
.. tostring(isRight)
.. " "
.. tostring(isDown)
.. " "
.. tostring(isLeft)
)
end
end
--- handle the interaction behavior of a minion
local function interactMinion(minion)
if minion.canInteract == true then
local mapX, mapY = game.tilemap.screenToWorldPos(minion.position.x, minion.position.y)
print(minion.name .. " " .. tostring(minion.id) .. " is interacting with tile " .. mapX .. "x" .. mapY)
game.tilemap.interact(mapX, mapY, minion)
end
end
--- handle the unstuck behavior of a potentially stuck minion by unstucking them
local function unstuckMinion(minion)
local mapX, mapY = game.tilemap.screenToWorldPos(minion.position.x, minion.position.y)
-- the property inside the tilemap which determines whether the minion can be on a tile
local mapProp
if minion.movementType == "walking" then
mapProp = "walkable"
elseif minion.movementType == "flying" then
mapProp = "overFlyable"
else
error("unknown minion movement type " .. tostring(minion.movementType))
end
-- if the minion is stuck, try to unstuck them
if game.tilemap.getValue(mapX, mapY, mapProp) == false then
print(minion.name .. " " .. minion.id .. " is stuck")
local tileScreenX, tileScreenY = game.tilemap.tilemapToScreen(mapX, mapY)
-- calculate the distance a minion has already traveled in each potential fix direction
local fixes = {
{ "right", minion.position.x - tileScreenX },
{ "left", tileScreenX - minion.position.x },
{ "up", tileScreenY - minion.position.y },
{ "down", minion.position.y - tileScreenY },
}
-- iterate over fixes with *most-distance-already-travelled* first and move them further in that direction but only if the tile in that direction can be moved to
table.sort(fixes, function(a, b)
return a[2] > b[2]
end)
for _, fix in ipairs(fixes) do
if fix[1] == "right" then
if game.tilemap.getValue(mapX + 1, mapY, mapProp) == true then
print(" moving them to the right")
minion.position.x = minion.position.x + game.conf.minions.unstuckMoveBy
break
else
print(" would like to move them right but moving there is not possible")
end
elseif fix[1] == "left" then
if game.tilemap.getValue(mapX - 1, mapY, mapProp) == true then
print(" moving them to the left")
minion.position.x = minion.position.x - game.conf.minions.unstuckMoveBy
break
else
print(" would like to move them left but moving there is not possible")
end
elseif fix[1] == "up" then
if game.tilemap.getValue(mapX, mapY - 1, mapProp) == true then
print(" moving them up")
minion.position.y = minion.position.y - game.conf.minions.unstuckMoveBy
break
else
print(" would like to move them up but moving there is not possible")
end
elseif fix[1] == "down" then
if game.tilemap.getValue(mapX, mapY + 1, mapProp) == true then
print(" moving them down")
minion.position.y = minion.position.y + game.conf.minions.unstuckMoveBy
break
else
print(" would like to move them down but moving there is not possible")
end
end
end
-- trigger step-on and step-off events if the minion is now unstuck
local newMapX, newMapY = game.tilemap.screenToWorldPos(minion.position.x, minion.position.y)
if newMapX ~= mapX or newMapY ~= mapY then
triggerMinionStepOff(minion, mapX, mapY)
triggerMinionStepOn(minion, newMapX, newMapY)
end
end
end
function game.minions.kill(minion)
print("killing " .. tostring(minion.name) .. " " .. tostring(minion.id))
local _, i = game.minions.get(minion.id)
table.remove(state.activeMinions, i)
game.summoning.refreshSummon(minion.presetId)
local randomDeathSoundIndex = math.random(1, #minion.deathSounds)
game.minions.audio[minion.deathSounds[randomDeathSoundIndex]]:play()
end
--- callback when the game loads
function game.minions.load()
-- nothing to do; initialization happens on level load
-- load assets
game.minions.assets = {}
-- load audio
game.minions.audio = {}
for _, preset in pairs(game.conf.minions.presets) do
for _, assetPath in pairs(preset.assets) do
if not game.minions.assets[assetPath] then
game.minions.assets[assetPath] = love.graphics.newImage(assetPath)
end
end
for _, audioPath in pairs(preset.deathSounds) do
if not game.minions.audio[audioPath] then
game.minions.audio[audioPath] = love.audio.newSource(audioPath, "static")
game.minions.audio[audioPath]:setVolume(game.conf.volume.voices)
end
end
end
end
--- callback for game updates
function game.minions.update(dt)
local currentLvl = game.state.level.current
for _, minion in ipairs(state.activeMinions) do
moveMinion(minion, dt)
-- check if minion finished level during movement, if so aboard update as all minions will be cleared
if currentLvl ~= game.state.level.current then
return
end
rotateMinion(minion)
unstuckMinion(minion)
end
-- update the idle state of minions
idleUpdateTimer = idleUpdateTimer - dt
if idleUpdateTimer <= 0 then
idleUpdateTimer = game.conf.minions.idleTime
if idleState == 1 then
idleState = 2
else
idleState = 1
end
end
end
--- callback for rendering
function game.minions.draw()
for _, minion in ipairs(state.activeMinions) do
local asset = game.minions.assets[minion.assets[idleState]]
--using sprite
love.graphics.draw(
asset,
minion.position.x,
minion.position.y - game.conf.level.tileSize / 2.5,
math.rad(0),
2,
2,
asset:getWidth() / 2,
asset:getHeight() / 2
)
---- body
--love.graphics.setColor(love.math.colorFromBytes(unpack(minion.color)))
--love.graphics.circle("fill", minion.position.x, minion.position.y, MINION_SIZE)
--
---- eye white
--local eyesSize = MINION_SIZE / 4
--local eyesDistance = MINION_SIZE / 2
--love.graphics.setColor(1, 1, 1)
--love.graphics.circle(
-- "fill",
-- minion.position.x + math.cos(math.rad(minion.angle - 90 - 35)) * eyesDistance,
-- minion.position.y + math.sin(math.rad(minion.angle - 90 - 35)) * eyesDistance,
-- eyesSize
--)
--love.graphics.circle(
-- "fill",
-- minion.position.x + math.cos(math.rad(minion.angle - 90 + 35)) * eyesDistance,
-- minion.position.y + math.sin(math.rad(minion.angle - 90 + 35)) * eyesDistance,
-- eyesSize
--)
---- eye outline
--love.graphics.setColor(0, 0, 0)
--love.graphics.circle(
-- "line",
-- minion.position.x + math.cos(math.rad(minion.angle - 90 - 35)) * eyesDistance,
-- minion.position.y + math.sin(math.rad(minion.angle - 90 - 35)) * eyesDistance,
-- eyesSize
--)
--love.graphics.circle(
-- "line",
-- minion.position.x + math.cos(math.rad(minion.angle - 90 + 35)) * eyesDistance,
-- minion.position.y + math.sin(math.rad(minion.angle - 90 + 35)) * eyesDistance,
-- eyesSize
--)
---- pupils
--love.graphics.setColor(0, 0, 0)
--love.graphics.circle(
-- "fill",
-- minion.position.x + math.cos(math.rad(minion.angle - 90 - 35)) * eyesDistance,
-- minion.position.y + math.sin(math.rad(minion.angle - 90 - 35)) * eyesDistance,
-- eyesSize / 3
--)
--love.graphics.circle(
-- "fill",
-- minion.position.x + math.cos(math.rad(minion.angle - 90 + 35)) * eyesDistance,
-- minion.position.y + math.sin(math.rad(minion.angle - 90 + 35)) * eyesDistance,
-- eyesSize / 3
--)
-- carried object
if minion.carrying == nil then
elseif minion.carrying == "key" then
error("rendering a minion carrying a key is not yet implemented")
else
error("cannot render carried item " .. tostring(minion.carrying))
end
end
end
--- callback for key presses
function game.minions.keypressed(key, scancode, isrepeat)
if scancode == "e" and not isrepeat then
for _, minion in ipairs(state.activeMinions) do
interactMinion(minion)
end
end
end
--- summon a minion of the specified type at a given position
---
--- Parameters:
--- presetId: The ID of a minion preset from game.conf.minions.preset
--- x, y: Map coordinates at which the minion should be summoned
function game.minions.summon(presetId, x, y)
print("summoning " .. presetId .. " at " .. x .. "x" .. y)
local preset = game.conf.minions.presets[presetId]
local screenX, screenY = game.tilemap.tilemapToScreen(x, y)
-- instantiate by copying all properties
local minion = {}
for k, v in pairs(preset) do
minion[k] = v
end
-- set runtime properties
minion.id = gen_id()
minion.position = {
x = screenX,
y = screenY,
}
minion.angle = 0
minion.carrying = nil
-- actually spawn by inserting into game state
table.insert(state.activeMinions, minion)
end
--- get the minion with the given id
---
--- Returns:
--- the minion table
--- the index into the minion state at which that minion is stored
function game.minions.get(id)
for i, minion in pairs(state.activeMinions) do
if minion.id == id then
return minion, i
end
end
error("No minion with id " .. tostring(id) .. " exists")
end
--- callback when the level with the given index is loaded
function game.minions.loadLevel(id)
state.activeMinions = {}
state.minion_id_seq = 0
end