/ld26

To get this branch, use:
bzr branch /bzr/ld26
1 by Josh C
zoetrope 1.4
1
-- Class: Group
2
-- A group is a set of sprites. Groups can be used to
3
-- implement layers or keep categories of sprites together.
4
--
5
-- Extends:
6
--		<Class>
7
--
8
-- Event: onUpdate
9
-- Called once each frame, with the elapsed time since the last frame in seconds.
10
--
11
-- Event: onBeginFrame
12
-- Called once each frame like onUpdate, but guaranteed to fire before any others' onUpdate handlers.
13
--
14
-- Event: onEndFrame
15
-- Called once each frame like onUpdate, but guaranteed to fire after all others' onUpdate handlers.
16
17
Group = Class:extend
18
{
19
	-- Property: active
20
	-- If false, none of its member sprites will receive update-related events.
21
	active = true,
22
23
	-- Property: visible
24
	-- If false, none of its member sprites will be drawn.
25
	visible = true,
26
27
	-- Property: solid
28
	-- If false, nothing will collide against this group, nor will this group
29
	-- displace any other sprite. This does not prevent collision checking
30
	-- against individual sprites in this group, however.
31
	solid = true,
32
33
	-- Property: sprites
34
	-- A table of member sprites, in drawing order.
35
	sprites = {},
36
37
	-- Property: timeScale
38
	-- Multiplier for elapsed time; 1.0 is normal, 0 is completely frozen.
39
	timeScale = 1,
40
41
	-- Property: translate
42
	-- This table's x and y properties shift member sprites' positions when drawn.
43
	-- To draw sprites at their normal position, set both x and y to 0.
44
	translate = { x = 0, y = 0 },
45
	
46
	-- Property: translateScale
47
	-- This table's x and y properties multiply member sprites'
48
	-- positions, which you can use to simulate parallax scrolling. To draw
49
	-- sprites at their normal position, set both x and y to 1.
50
	translateScale = { x = 1, y = 1 },
51
52
	-- Method: add
53
	-- Adds a sprite to the group.
54
	--
55
	-- Arguments:
56
	--		sprite - <Sprite> to add
57
	--
58
	-- Returns:
59
	--		nothing
60
61
	add = function (self, sprite)
62
		assert(sprite, 'asked to add nil to a group')
63
		assert(sprite ~= self, "can't add a group to itself")
64
	
65
		if STRICT and self:contains(sprite) then
66
			local info = debug.getinfo(2, 'Sl')
67
			print('Warning: adding a sprite to a group it already belongs to (' ..
68
				  info.short_src .. ' line ' .. info.currentline .. ')')
69
		end
70
71
		table.insert(self.sprites, sprite)
72
	end,
73
74
	-- Method: remove
75
	-- Removes a sprite from the group. If the sprite is
76
	-- not in the group, this does nothing.
77
	-- 
78
	-- Arguments:
79
	-- 		sprite - <Sprite> to remove
80
	-- 
81
	-- Returns:
82
	-- 		nothing
83
84
	remove = function (self, sprite)
85
		for i, spr in ipairs(self.sprites) do
86
			if spr == sprite then
87
				table.remove(self.sprites, i)
88
				return
89
			end
90
		end
91
		
92
		if STRICT then
93
			local info = debug.getinfo(2, 'Sl')
94
			print('Warning: asked to remove a sprite from a group it was not a member of (' ..
95
				  info.short_src .. ' line ' .. info.currentline .. ')')
96
		end
97
	end,
98
99
	-- Method: moveToFront
100
	-- Moves a sprite in the group so that it is drawn on top
101
	-- of all other sprites in the group.
102
	--
103
	-- Arguments:
104
	--		sprite - <Sprite> to move, should already be a member of the group
105
	--
106
	-- Returns:
107
	--		nothing
108
109
	moveToFront = function (self, sprite)
110
		for i, spr in ipairs(self.sprites) do
111
			if spr == sprite then
112
				table.remove(self.sprites, i)
113
				table.insert(self.sprites, sprite)
114
				return
115
			end
116
		end
117
118
		if STRICT then
119
			print('Warning: asked to move sprite to front of group, but is not a member: ' .. sprite)
120
		end
121
	end,
122
123
	-- Method: moveToBack
124
	-- Moves a sprite in the group so that it is drawn below
125
	-- all other sprites in the group.
126
	--
127
	-- Arguments:
128
	--		sprite - <Sprite> to move, should already be a member of the group
129
	--
130
	-- Returns:
131
	--		nothing
132
	
133
	moveToBack = function (self, sprite)
134
		for i, spr in ipairs(self.sprites) do
135
			if spr == sprite then
136
				table.remove(self.sprites, i)
137
				table.insert(self.sprites, 1, sprite)
138
				return
139
			end
140
		end
141
142
		if STRICT then
143
			print('Asked to move sprite to back of group, but is not a member: ' .. sprite)
144
		end
145
	end,
146
147
	-- Method: sort
148
	-- Sorts members into a new draw sequence.
149
	--
150
	-- Arguments:
151
	--		func - function to perform the sort. This will receive two <Sprite>s as arguments;
152
	--			   the function must return whether the first should be drawn below the second.
153
	--
154
	-- Returns:
155
	--		nothing
156
157
	sort = function (self, func)
158
		table.sort(self.sprites, func)
159
	end,
160
161
	-- Method: collide
162
	-- Collides all solid sprites in the group with another sprite or group.
163
	-- This calls the <Sprite.onCollide> event handlers on all sprites that
164
	-- collide with the same arguments <Sprite.collide> does.
165
	--
166
	-- It's often useful to collide a group with itself, e.g. myGroup:collide().
167
	-- This checks for collisions between the sprites that make up the group.
168
	--
169
	-- Arguments:
170
	-- 		... - any number of <Sprite>s or <Group>s to collide with. If none
171
	--			  are specified, the group collides with itself.
172
	-- 
173
	-- Returns:
174
	--		nothing
175
	--
176
	-- See Also:
177
	--		<Sprite.collide>
178
179
	collide = function (self, ...)
180
		local list = {...}
181
182
		if #list > 0 then
183
			if STRICT then
184
				for _, other in pairs(list) do
185
					assert(other:instanceOf(Group) or other:instanceOf(Sprite), 'asked to collide non-group/sprite ' ..
186
						   type(other))
187
				end
188
			end
189
190
			Collision:check(self, ...)
191
		else
192
			Collision:check(self, self)
193
		end
194
	end,
195
196
	-- Method: setEffect
197
	-- Sets a pixel effect to use while drawing sprites in this group.
198
	-- See https://love2d.org/wiki/PixelEffect for details on how pixel
199
	-- effects work. After this call, the group's effect property will be
200
	-- set up so you can send variables to it. Only one pixel effect can
201
	-- be active on a group at a time.
202
	--
203
	-- Arguments:
204
	--		filename - filename of effect source code; if nil, this
205
	--				   clears any existing pixel effect.
206
	--		effectType - either 'screen' (applies the effect to the entire
207
	--					 group once, via an offscreen canvas), or 'sprite'
208
	--					 (applies to the effect to each individual draw operation).
209
	--					 Screen effects use more resources, but certain effects
210
	--					 need to work on the entire screen to be effective.
211
	--
212
	-- Returns:
213
	--		whether the effect was successfully created
214
215
	setEffect = function (self, filename, effectType)
216
		effectType = effectType or 'screen'
217
218
		if love.graphics.isSupported('pixeleffect') and
219
		   (effectType == 'sprite' or love.graphics.isSupported('canvas'))then
220
			if filename then
221
				self.effect = love.graphics.newPixelEffect(Cached:text(filename))
222
				self.effectType = effectType
223
			else
224
				self.effect = nil
225
			end
226
227
			return true
228
		else
229
			return false
230
		end
231
	end,
232
233
	-- Method: count
234
	-- Counts how many sprites are in this group.
235
	-- 
236
	-- Arguments:
237
	--		subgroups - include subgroups?
238
	-- 
239
	-- Returns:
240
	--		integer count
241
242
	count = function (self, subgroups)
243
		if subgroups then
244
			local count = 0
245
246
			for _, spr in pairs(self.sprites) do
247
				if spr:instanceOf(Group) then
248
					count = count + spr:count(true)
249
				else
250
					count = count + 1
251
				end
252
			end
253
254
			return count
255
		else
256
			return #self.sprites
257
		end
258
	end,
259
260
	-- Method: die
261
	-- Makes the group totally inert. It will not receive
262
	-- update events, draw anything, or be collided.
263
	--
264
	-- Arguments:
265
	--		none
266
	--
267
	-- Returns:
268
	-- 		nothing
269
270
	die = function (self)
271
		self.active = false
272
		self.visible = false
273
		self.solid = false
274
	end,
275
276
	-- Method: revive
277
	-- Makes this group completely active. It will receive
278
	-- update events, draw itself, and be collided.
279
	--
280
	-- Arguments:
281
	--		none
282
	--
283
	-- Returns:
284
	-- 		nothing
285
286
	revive = function (self)
287
		self.active = true
288
		self.visible = true
289
		self.solid = true
290
	end,
291
292
	-- Method: contains
293
	-- Returns whether this group contains a sprite.
294
	--
295
	-- Arguments:
296
	--		sprite - sprite to look for
297
	--		recurse - check subgroups? defaults to true
298
	--
299
	-- Returns:
300
	--		boolean
301
302
	contains = function (self, sprite, recurse)
303
		if recurse ~= false then recurse = true end
304
305
		for _, spr in pairs(self.sprites) do
306
			if spr == sprite then return true end
307
308
			if recurse and spr:instanceOf(Group) and spr:contains(sprite) then
309
				return true
310
			end
311
		end
312
313
		return false
314
	end,
315
316
	-- Method: loadLayers
317
	-- Loads layers from a Lua source file (as generated by Tiled -- http://mapeditor.org).
318
	-- Each layer is created as a <Group> belonging to this one and added to preserve its
319
	-- ordering. Tile layers are created as <Map> instances; object layers will try to create
320
	-- instances of a class named by the object's name property. If no class exists by
321
	-- this name, or the object has no name property, a gray fill will be created instead,
322
	-- as a placeholder. If the object has a property named _the, then this will set
323
	-- the.[whatever] to it.
324
	--
325
	-- Arguments:
326
	--		file - filename to load
327
	--		tileClass - class to create tiles in tile layers with; constructor
328
	--				    will be called with properties: image, width,
329
	--			 	    height, imageOffset (with x and y sub-properties)
330
	--
331
	-- Returns:
332
	--		nothing
333
334
	loadLayers = function (self, file, tileClass)
335
		local ok, data = pcall(loadstring(Cached:text(file)))
336
		local _, _, directory = string.find(file, '^(.*[/\\])')
337
		directory = directory or ''
338
339
		if ok then
340
			-- store tile properties by gid
341
			
342
			local tileProtos = {}
343
344
			for _, tileset in pairs(data.tilesets) do
345
				for _, tile in pairs(tileset.tiles) do
346
					local id = tileset.firstgid + tile.id
347
					
348
					for key, value in pairs(tile.properties) do
349
						tile.properties[key] = tovalue(value)
350
					end
351
352
					tileProtos[id] = tile
353
					tileProtos[id].width = tileset.tilewidth
354
					tileProtos[id].height = tileset.tileheight
355
				end
356
			end
357
358
			for _, layer in pairs(data.layers) do
359
				if self.prototype[layer.name] then
360
					error('The class you are loading layers into reserves the ' .. layer.name .. ' property for its own use; you cannot load a layer with that name')
361
				end
362
363
				if STRICT and self[layer.name] then
364
					local info = debug.getinfo(2, 'Sl')
365
					print('Warning: a property named ' .. layer.name .. ' already exists in this group (' ..
366
						  info.short_src .. ', line ' .. info.currentline .. ')')
367
				end
368
369
				if layer.type == 'tilelayer' then
370
					local map = Map:new{ spriteWidth = data.tilewidth, spriteHeight = data.tileheight }
371
					map:empty(layer.width, layer.height)
372
373
					-- load tiles
374
375
					for _, tiles in pairs(data.tilesets) do
376
						map:loadTiles(directory .. tiles.image, tileClass or Tile, tiles.firstgid)
377
378
						-- and mix in properties where applicable
379
380
						for id, tile in pairs(tileProtos) do
381
							if map.sprites[id] then
382
								map.sprites[id]:mixin(tile.properties)
383
							end
384
						end
385
					end
386
387
					-- load tile data
388
389
					local x = 1
390
					local y = 1
391
392
					for _, val in ipairs(layer.data) do
393
						map.map[x][y] = val
394
						x = x + 1
395
396
						if x > layer.width then
397
							x = 1
398
							y = y + 1
399
						end
400
					end
401
402
					self[layer.name] = map
403
					self:add(map)
404
				elseif layer.type == 'objectgroup' then
405
					local group = Group:new()
406
407
					for _, obj in pairs(layer.objects) do
408
						-- roll in tile properties if based on a tile
409
410
						if obj.gid and tileProtos[obj.gid] then
411
							local tile = tileProtos[obj.gid]
412
413
							obj.name = tile.properties.name
414
							obj.width = tile.width
415
							obj.height = tile.height
416
417
							for key, value in pairs(tile.properties) do
418
								obj.properties[key] = tovalue(value)
419
							end
420
421
							-- Tiled tile-based objects measure their y
422
							-- position at their lower-left corner, instead
423
							-- of their upper-left corner as usual
424
425
							obj.y = obj.y - obj.height
426
						end
427
428
						-- create a new object if the class does exist
429
430
						local spr
431
432
						if _G[obj.name] then
433
							obj.properties.x = obj.x
434
							obj.properties.y = obj.y
435
							obj.properties.width = obj.width
436
							obj.properties.height = obj.height
437
438
							spr = _G[obj.name]:new(obj.properties)
439
						else
440
							spr = Fill:new{ x = obj.x, y = obj.y, width = obj.width, height = obj.height, fill = { 128, 128, 128 } }
441
						end
442
443
						if obj.properties._the then
444
							the[obj.properties._the] = spr
445
						end
446
447
						group:add(spr)
448
					end
449
450
					self[layer.name] = group
451
					self:add(group)
452
				else
453
					error("don't know how to create a " .. layer.type .. " layer from file data")
454
				end
455
			end
456
		else
457
			error('could not load layers from file: ' .. data)
458
		end
459
	end,
460
461
	-- passes startFrame events to member sprites
462
463
	startFrame = function (self, elapsed)
464
		if not self.active then return end
465
		elapsed = elapsed * self.timeScale
466
		
467
		for _, spr in pairs(self.sprites) do
468
			if spr.active then spr:startFrame(elapsed) end
469
		end
470
471
		if self.onStartFrame then self:onStartFrame(elapsed) end
472
	end,
473
474
	-- passes update events to member sprites
475
476
	update = function (self, elapsed)
477
		if not self.active then return end
478
		elapsed = elapsed * self.timeScale
479
480
		for _, spr in pairs(self.sprites) do
481
			if spr.active then spr:update(elapsed) end
482
		end
483
484
		if self.onUpdate then self:onUpdate(elapsed) end
485
	end,
486
487
	-- passes endFrame events to member sprites
488
489
	endFrame = function (self, elapsed)
490
		if not self.active then return end
491
		elapsed = elapsed * self.timeScale
492
493
		for _, spr in pairs(self.sprites) do
494
			if spr.active then spr:endFrame(elapsed) end
495
		end
496
497
		if self.onEndFrame then self:onEndFrame(elapsed) end
498
	end,
499
500
	-- Method: draw
501
	-- Draws all visible member sprites onscreen.
502
	--
503
	-- Arguments:
504
	--		x - x offset in pixels
505
	--		y - y offset in pixels
506
507
	draw = function (self, x, y)
508
		if not self.visible then return end
509
		x = x or self.translate.x
510
		y = y or self.translate.y
511
		
512
		local scrollX = x * self.translateScale.x
513
		local scrollY = y * self.translateScale.y
514
		local appWidth = the.app.width
515
		local appHeight = the.app.height
516
517
		if self.effect then
518
			if self.effectType == 'screen' then
519
				if not self._canvas then self._canvas = love.graphics.newCanvas() end
520
				self._canvas:clear()
521
				love.graphics.setCanvas(self._canvas)
522
			elseif self.effectType == 'sprite' then
523
				love.graphics.setPixelEffect(self.effect)
524
			end
525
		end
526
		
527
		for _, spr in pairs(self.sprites) do	
528
			if spr.visible then
529
				if spr.translate then
530
					spr:draw(spr.translate.x + scrollX, spr.translate.y + scrollY)
531
				elseif spr.x and spr.y and spr.width and spr.height then
532
					local sprX = spr.x + scrollX
533
					local sprY = spr.y + scrollY
534
535
					if sprX < appWidth and sprX + spr.width > 0 and
536
					   sprY < appHeight and sprY + spr.height > 0 then
537
						spr:draw(sprX, sprY)
538
					end
539
				else
540
					spr:draw(scrollX, scrollY)
541
				end
542
			end
543
		end
544
			
545
		if self.effect then
546
			if self.effectType == 'screen' then
547
				love.graphics.setPixelEffect(self.effect)
548
				love.graphics.setCanvas()
549
				love.graphics.draw(self._canvas)
550
			end
551
552
			love.graphics.setPixelEffect()
553
		end
554
	end,
555
556
	__tostring = function (self)
557
		local result = 'Group ('
558
559
		if self.active then
560
			result = result .. 'active'
561
		else
562
			result = result .. 'inactive'
563
		end
564
565
		if self.visible then
566
			result = result .. ', visible'
567
		else
568
			result = result .. ', invisible'
569
		end
570
571
		if self.solid then
572
			result = result .. ', solid'
573
		else
574
			result = result .. ', not solid'
575
		end
576
577
		return result .. ', ' .. self:count(true) .. ' sprites)'
578
	end
579
}