forked from github/game-off-2016
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMGame.lua
700 lines (614 loc) · 19.6 KB
/
MGame.lua
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
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
----
-- MGame.lua
--
-- The primary gamestate which the game runs in. This gamestate handles all
-- entities, room loading, rendering, updating, etc. This is where most of
-- the core game code lies.
----
local Scene = require "xl.Scene"
local STI = require "libs.sti"
local Camera = require "hump.camera"
local Keymap = require "xl.Keymap"
-- local Lights = require "xl.Lights"
local Gamestate = require "hump.gamestate"
local Background = require "xl.Background"
local MGame = {}
local function __NULL__() end
-- Metatable used for TileLayers to give them a draw method and the lessThan
-- comparators needed for the TileLayer to go into the scenes.
local TileLayermt = {
__lt = Scene.lessThan,
__le = Scene.lessThan,
draw = function ( self )
MGame.map:drawLayer( self )
end,
tick = __NULL__,
create = __NULL__,
destroy = __NULL__,
}
TileLayermt.__index = TileLayermt
-- MGamestate callback
function MGame:init()
self.locked = false -- is the game state locked or isn't it
self.ticks = 0 -- how many ticks have we run through so far?
self.scene = Scene.new(40) -- the main drawing scene. Scene = everything that is on screen in the Game world. Very important and very common in every game
self.hud = Scene.new(20) -- the HUD scene.
self.backgrounds = Scene.new(10) -- the level backgrounds.
self.cam = Camera() -- a camera!
self.listeners = { -- Listeners are simple, they simply sit around and wait for something to happen. If it does happen it will call a function.
mousepressed = {},
mousereleased = {},
keypressed = {}, -- These are events. You use this one the most.
keyreleased = {},
pretick = {},
posttick = {},
roombegin = {},
roomend = {},
}
self.savedata = {} -- misc save data. THis is just game save data, represented in table format.
self.permdata = {} -- stores permanent object state (i.e. switches, etc.)
self.scissor = {}
self.entities = {} -- list of entities. This is a list of everything currently in the world. It is importante because.....
self.toDestroy = {}
self.world = love.physics.newWorld()
-- self.lights = Lights.newLightScene(GAME_SIZE.w, GAME_SIZE.h)
self.wrapPreSolve = lume.fn(self.preSolve, self)
self.wrapPostSolve = lume.fn(self.postSolve, self)
self.wrapOnContactBegin = lume.fn(self.onContactBegin, self)
self.wrapOnContactEnd = lume.fn(self.onContactEnd, self)
self.round =1
self.bgTheme = love.audio.newSource("/assets/sounds/WeirdElectro.mp3")
self.bgTheme:setLooping(true);
self.bgTheme:play()
self:resize( love.graphics.getDimensions() )
-----Initialize Canvases for drawing
self.ActiveCanvas = love.graphics.newCanvas( GAME_SIZE.w, GAME_SIZE.h) --this canvas will hold all active objects (sprites,tiles)
love.graphics.setBackgroundColor(0,0,0,0)
------------------------
end
-- MGamestate callback:. THis is called constantly.
function MGame:update( dt )
-- after-tick operations occur before the next tick so the objects will still be updated
-- before being draw. This preserves the correct function call order.
if self.next_room then
local nroom = self.next_room
self.next_room = nil
self:loadRoom(nroom)
end
self.ticks = self.ticks + 1
if self.ticks == 1/0 then self.ticks = 0 end
-- lock the game and update stuff
self.locked = true
local baseTime = love.timer.getTime()
self:emitEvent( "pretick" )
xl.DScreen.print("Num Objs: ", "(%f)", util.tablelength(self.entities))
xl.DScreen.print("Num Enemies: ", "(%f)", util.tablelength(self:findObjectsWithModule("ModEnemy")))
--
-- self.player:tick( dt, true ) -- The player object is ALWAYS updated first. Game logic.
-- xl.DScreen.print("Character DT: ", "(%f)", (love.timer.getTime() - baseTime))
baseTime = love.timer.getTime()
--The game loops through all the entities in the entity list and calls the tick function for everyone of them.
-- lume.trace(self.tagag)
-- lume.trace(self.entities[self.tagag])
for _, v in pairs(self.entities) do
if self.entities[v] then
v["tick"](v, dt)
end
end
xl.DScreen.print("Entities DT: ", "(%f)", (love.timer.getTime() - baseTime))
self:emitEvent( "posttick" )
self.scene:update(dt) -- This updates the sprites on screen to reflect what happened to their game objects.
xl.DScreen.print("Scene DT: ", "(%f)", (love.timer.getTime() - baseTime))
baseTime = love.timer.getTime()
self.hud:update(dt) -- HUD update
xl.DScreen.print("HUD DT: ", "(%f)", (love.timer.getTime() - baseTime))
baseTime = love.timer.getTime()
self.backgrounds:update(dt) -- Background update.
xl.DScreen.print("BKG DT: ", "(%f)", (love.timer.getTime() - baseTime))
baseTime = love.timer.getTime()
self._contacts = {}
self.world:update(dt) -- update physics. Understand the difference between physics update and game update.
xl.DScreen.print("Physics: ", "(%f)", (love.timer.getTime() - baseTime))
baseTime = love.timer.getTime()
for k,v in pairs(self.toDestroy) do
self:mDel(v)
end
self.locked = false
end
----
-- Gamestate callback
----
function MGame:draw()
local loveGraphics = love.graphics
love.graphics.clear() -- clear screen
self:setScissor( true )
self.cam:attach() -- < do camera movements
loveGraphics.setShader(Game.currentShader)
-- draw backgrounds
love.graphics.setCanvas(self.ActiveCanvas)
-- self.ActiveCanvas:clear(0,0,0,0)
love.graphics.clear()
-- draw tilelayers
for k,v in pairs(self.tilelayers) do
self.map:drawLayer(v)
end
self.scene:draw() -- < draw the scene
loveGraphics.setShader()
-- self.lights:process() -- < draw lights to light canvas
self.cam:detach() -- < undo the camera adjustment
-- self.lights:overlay() -- < overlay the light canvas
love.graphics.setCanvas()
self.cam:attach() -- < do camera movements
self.backgrounds:draw()
self.cam:detach() -- < undo the camera adjustment
love.graphics.draw(self.ActiveCanvas,0,0)
-- draw the HUD
self:nocamera( true )
self.hud:draw()
self:nocamera( false )
-- draw the simplistic HUD (this is gross but it works!)
-- possibly change color if this becomes hard to read
local currFont = love.graphics.getFont()
love.graphics.setFont(love.graphics.newFont(15))
if not Game.playerIsDead then
love.graphics.print("Round: " .. Game.round, 15, 10)
love.graphics.rectangle("line", 5, 5, 145, 75)
if Game.player then
love.graphics.print("Health: " .. Game.player.health .. "/" .. Game.player.max_health, 15, 30)
love.graphics.print("Defeated: " .. Game.player.killCount, 15, 50)
end
end
love.graphics.setFont(currFont)
-- reset the scissor
self:setScissor( false )
end
----
-- Gamestate callback
----
function MGame:resize(w,h)
-- self.lights:resize( w,h )
local scale, sciz = 1, self.scissor
scale, sciz[1], sciz[2], sciz[3], sciz[4], sciz.ox, sciz.oy = xl.calculateViewport(GAME_SIZE, w, h, 1, 16)
self.cam:zoomTo(scale * 2)
end
----
-- Gamestate callback
---
function MGame:mousepressed()
self:emitEvent( "mousepressed" )
end
----
-- Gamestate callback
---
function MGame:mousepressed()
self:emitEvent( "mousereleased" )
end
----
-- Gamestate callback
----
function MGame:keypressed( key, isrepeat )
if key == "escape" and not self.DialogActive then
love.event.quit()
end
self:emitEvent( "keypressed", key, isrepeat )
end
----
-- Gamestate callback
----
function MGame:keyreleased( key, isrepeat )
self:emitEvent( "keyreleased", key, isrepeat )
end
----
-- Gamestate callback
----
function MGame:quit()
Gamestate.push( require "state.QuitMenu" )
-- return true
end
----
-- Change the current overlay. The overlay can be and object with a draw()
-- function which will be the final thing to be drawn each frame. Alternately
-- the overlay can be a color table in the format {r,g,b,a}. In this case
-- a rectangle covering the screen will be drawn using this color.
-- @param {table} overlay - The new overlay or nil to diable it.
----
function MGame:setOverlay( overlay )
self.overlay = overlay
end
----
-- Public accessor for the overlay.
-- @return the current overlay
----
function MGame:getOverlay( )
return self.overlay
end
function MGame:emitEvent( name, ... )
for _,entity in pairs(self.listeners[name]) do
entity[name](entity, ...)
end
end
----
-- Adds an entity to the game.
-- @param {Entity} entity - The entity to be added to the game.
-- @return entity
----
function MGame:add(entity)
-- add entity to the entity list
-- lume.trace(entity)
-- if entity.type == "EnShooter" then
-- lume.trace(entity)
-- self.tagag = entity
-- end
self.entities[entity] = entity
-- try to call load on the entity
if entity.permanentid and self.permdata[entity.permanentid] then
entity:load( self.permdata[entity.permanentid] )
end
-- call create on the entity
entity:create()
-- add entity to listeners
for k,v in pairs(self.listeners) do
if entity[k] then
v[entity] = entity
end
end
return entity
end
----
-- Removes an entity from the game.
-- @param {Entity} entity - The entity to be removed.
-- @return entity
----
function MGame:del(entity)
self.entities[entity].destroyed = true
self.entities[entity] = nil
self.toDestroy[entity] = entity
end
function MGame:mDel( entity )
local permid = entity.permanentid
self.toDestroy[entity] = nil
if permid then
self.permdata[permid] = self.permdata[permid] or {}
entity:save( self.permdata[permid] )
end
entity:destroy()
self.entities[entity] = nil
-- remove entity from listeners
for k,v in pairs(self.listeners) do
v[entity] = nil
end
return entity
end
-- Check if an entity exists
function MGame:exists( entity )
return self.entities[entity]
end
----
-- Recreates entities from a list of previously stored entities.
-- Each entity will be recreated using util.recreateObject then it will be
-- loaded and added in to the game.
-- @param {table<?,entity>} entities - The table of entity definitions to
-- recreate.
----
function MGame:recreateEntities( entities )
for _,entity in pairs(entities) do
local inst = util.recreateObject( entity )
self:add(inst)
end
end
----
-- Loads the named room. If the game is locked then this will be delayed until
-- the game state is again unlocked.
-- @param {string} name - Path of the room to load (like in a require statement)
----
function MGame:loadRoom( name )
if (type(name) == "table") then
-- lume.trace("load pre-loaded Table")
self.curMapTable = util.deepcopy(name)
self.currentRoom = self.curMapTable.name
else
self.currentRoom = name
assert(love.filesystem.isFile(name..".lua"), "Room resource '" .. name .. "' doesn't exist")
end
if self.locked then
lume.trace()
self.next_room = name
else
lume.trace()
self:emitEvent( "roomend" )
-- save references to persistent entities
local persistentEntities = {}
for k,v in pairs(self.entities) do
if v.persistent then
table.insert(persistentEntities, v)
end
end
lume.trace()
self:i_loadRoom( name, true )
for k,v in pairs(persistentEntities) do
self:add(v)
end
lume.trace()
end
Background("assets/bkgs/space.png", true, 0, 9, 0, 1, 1)
end
----
-- Callback for World
-- @param {Body} a - the first body
-- @param {Body} b - the second body
-- @param {Contact} contact - the contact between them
----
function MGame:onContactBegin(a, b, contact)
local ct = self._contacts
if ct[contact] then return end
ct[contact] = true
local udA, udB = a:getBody():getUserData(), b:getBody():getUserData()
if contact:isTouching() and udA and udB then
local onCollide = __NULL__
;(udA.onCollide or onCollide)( udA, udB, contact )
;(udB.onCollide or onCollide)( udB, udA, contact )
end
end
----
-- Callback for World
-- @param {Body} a - the first body
-- @param {Body} b - the second body
-- @param {Contact} contact - the contact between them
----
function MGame:onContactEnd(a, b, contact)
local ct = self._contacts
if ct[contact] then return end
ct[contact] = true
local udA, udB = a:getBody():getUserData(), b:getBody():getUserData()
if not contact:isTouching() and udA and udB then
local postCollide = __NULL__
;(udA.postCollide or postCollide)( udA, udB, contact )
;(udB.postCollide or postCollide)( udB, udA, contact )
end
end
----
-- Callback for World
-- @param {Body} a - the first body
-- @param {Body} b - the second body
-- @param {Contact} contact - the contact between them
----
function MGame:preSolve( a, b, contact )
-- body
local udA, udB = a:getBody():getUserData(), b:getBody():getUserData()
if contact:isTouching() and udA and udB then
local preSolve = __NULL__
;(udA.preSolve or preSolve)( udA, udB, contact )
;(udB.preSolve or preSolve)( udB, udA, contact )
end
end
----
-- Callback for World
-- @param {Body} a - the first body
-- @param {Body} b - the second body
-- @param {Contact} contact - the contact between them
-- @param {...} ... yeah that stuff...
----
function MGame:postSolve( a, b, contact, normalimpulse1, tangentimpulse1, normalimpulse2, tangentimpulse2)
-- body
local udA, udB = a:getBody():getUserData(), b:getBody():getUserData()
if contact:isTouching() and udA and udB then
local postSolve = __NULL__
;(udA.postSolve or postSolve)( udA, udB, contact )
;(udB.postSolve or postSolve)( udB, udA, contact )
end
end
----
-- Load a new room replacing the old one.
-- @param {string} name - room name
-- @param {boolean} loadData - load objects from the room
----
-- This function loads a room from a table containing the room's information.
function MGame:i_loadRoom( name, loadData )
if type(name) == "table" then
self.roomname = name.name
else
self.roomname = name
end
lume.trace()
-- clear all entities
-- Before we can create a new room, we must clear our entities list to remove objects in the old room.
-- Otherwise, we would be updating objects that are not currently relevant, and wastes time.
-- So we delete everything else.
while next( self.entities ) do
for key,value in pairs(self.entities) do
self:mDel(value)
end
end
-- reset room
-- We clear the other things as well.
self.backgrounds:clear()
self.scene:clear()
-- self.lights:clear()
self.tilelayers = {}
lume.trace()
-- reset world
self.world:destroy() -- leave the old world behind
self.world = love.physics.newWorld(0,0, true) -- create a brand new world.
-- This is an external engine we use.
-- It is called Love2d, and handles most physics for us. We just need to add objects to the world and define their properties.
-- All love2d physics objects take place in a world.
-- When a room is loaded, a new world is created and the old world is destroyed. Any objects that persist between worlds,
-- like the character. Are copied back into the new world with the same parameters. tap me when done. Do that always.
self.world:setCallbacks(self.wrapOnContactBegin, self.wrapOnContactEnd, self.wrapPreSolve, self.wrapPostSolve)
lume.trace()
-- load the new room and various properties/options/settings
if type(name) == "table" then
self.map = STI.fromTable(name)
name = name.name
else
self.map = STI.new(name)
end
lume.trace()
local p = self.map.properties
-- Here we grab a few variables from the map we loaded as set the physics world's properties, such as gravity.
self.gravity = {(p.gravX or 0), (p.gravY or 480)}
-- self.lights.enabled = (p.use_lights == "true")
if p.camOffX then
Game.debugCam.offsetx = p.camOffX
end
if p.camOffY then
lume.trace()
Game.debugCam.offsety = p.camOffY
end
self.world:setGravity(unpack(self.gravity)) -- we do it here. tap. Do you want me to talk more about world create or physics. Okay.
-- New file then.
lume.trace()
for _,layer in ipairs(self.map.layers) do
if layer.type == "tilelayer" then
if layer.properties.depth then
local depth = tonumber( layer.properties.depth )
setmetatable(layer, TileLayermt)
layer.z = depth
self.scene:insert( Scene.makeNode( layer ) )
else
table.insert(self.tilelayers, layer)
end
end
if layer.type == "objectgroup" and loadData then
for _,object in pairs(layer.objects) do
Log.verbose("Loading object type %q at (%i,%i)", object.type, object.x, object.y)
--lume.trace(object.type)
local class = require("objects." .. object.type)
local inst = class()
inst.name, inst.x, inst.y, inst.width, inst.height = object.name, object.x, object.y - 16, object.width, object.height
if object.polyline then
local x,y,polyline = inst.x, inst.y, object.polyline
local pointlist = {}
for k=1,#polyline do
table.insert(pointlist, x + polyline[k].x)
table.insert(pointlist, y + polyline[k].y)
end
inst.pointlist = pointlist
end
for k,v in pairs(object.properties) do
inst[k] = v
end
self:add( inst )
end
end
end
lume.trace()
if loadData then
local roomname = string.match( name, "([%w_]-)$" )
local success,func = pcall( require, "scripts/room/" .. roomname )
-- this maintains support for the deprecated form of loading room scripts
-- this should be removed when all old rooms are converted
if not success then
print("name=",name,"roomname=",roomname)
local scriptname = self.map.properties["script"]
if scriptname then
require( "scripts/rooms/" .. scriptname )()
end
local backgroundScript = self.map.properties["backgroundScript"]
-- lume.trace("Loading room bkg: " , backgroundScript)
if backgroundScript then
require( "scripts/rooms/" .. backgroundScript)()
end
else
func()
end
self:emitEvent( "roombegin" )
end
lume.trace()
end
----
-- Finds objects based on their types.
-- @param {string...} - list of type names to search for
-- @return List of tables one for each argument containing the matching entities
----
function MGame:findAllObjects( ... )
local args = {...}
local results = {}
for k,_ in pairs( args ) do
results[k] = {}
end
for _,entity in pairs( self.entities ) do
for k,ty in pairs( args ) do
if Class.istype( entity, ty ) then
table.insert( results[k], entity )
end
end
end
return unpack( results )
end
function MGame:findObjects( ... )
local args = {...}
local results = {}
for _,entity in pairs( self.entities ) do
for _,ty in pairs( args ) do
if Class.istype( entity, ty ) then
table.insert( results, entity )
end
end
end
return results
end
function MGame:findObjectsWithModule( ... )
local args = {...}
local results = {}
for _,entity in pairs( self.entities ) do
for _,ty in pairs( args ) do
if Class.istype( entity, "ObjBase" ) and entity:hasModule(ty) then
table.insert( results, entity )
end
end
end
return results
end
function MGame:findObjectsAt( x, y )
local x1,y1,x2,y2 = x, y, x + 0.1, y + 0.1
return self:findObjectsIn( x1, y1, x2, y2 )
end
----
-- Uses Box2D collision detection to find all objects on the line path between
-- points 1 and 2. Point 1 is given by (x1,y1) and point 2 is given by (x2,y2).
-- @param {number} x1 - x-coordinate of first point
-- @param {number} y1 - y-coordinate of first point
-- @param {number} x2 - x-coordinate of second point
-- @param {number} y2 - y-coordinate of second point
-- @return A list of all the objects which have Box2D fixtures on the line
-- between (x1,y1) and (x2,y2)
----
function MGame:findObjectsIn( x1, y1, x2, y2 )
local duptracker = {}
local objects = {}
local function callback( fixture, x, y, xn, yn, fraction )
local ud = fixture:getBody():getUserData()
if not duptracker[ud] then
duptracker[ud] = true
table.insert( objects, ud )
end
return 1
end
self.world:rayCast( x1, y1, x2, y2, callback )
return objects
end
function MGame:getTicks()
return self.ticks
end
function MGame:nocamera(on)
local loveGraphics = love.graphics
if on then
loveGraphics.push()
loveGraphics.origin()
loveGraphics.translate(self.scissor.ox, self.scissor.oy)
else
loveGraphics.pop()
end
end
function MGame:setScissor( on )
if on then
love.graphics.setScissor( unpack( self.scissor ) )
else
love.graphics.setScissor()
end
end
function MGame:setPlayer( player, useFrame )
self.player = player
end
return MGame