/zoeplat

To get this branch, use:
bzr branch http://9ix.org/bzr/zoeplat
1 by Josh C
zoetrope 1.3.1
1
-- Class: View
2
-- A view is a group that packages several useful objects with it.
3
-- It's helpful to use, but not required. When a view is created, it
4
-- automatically sets the.view for itself. the.view should be considered
5
-- a read-only reference. If you want to switch views, you *must* set
6
-- the app's view property instead.
7
--
8
-- Extends:
9
--		<Group>
10
11
View = Group:extend{
12
	-- Property: timer
13
	-- A built-in <Timer> object for use as needed.
14
15
	-- Property: tween
16
	-- A built-in <Tween> object for use as needed.
17
18
	-- Property: factory
19
	-- A built-in <Factory> object for use as needed.
20
21
	-- Property: focus
22
	-- A <Sprite> to keep centered onscreen.
23
24
	-- Property: focusOffset
25
	-- This shifts the view of the focus, if one is set. If both
26
	-- x and y properties are set to 0, then the view keeps the focus
27
	-- centered onscreen.
28
	focusOffset = { x = 0, y = 0 },
29
30
	-- Property: minVisible
31
	-- The view clamps its scrolling so that nothing above or to the left
32
	-- of these x and y coordinates is visible.
33
	minVisible = { x = -math.huge, y = -math.huge },
34
35
	-- Property: maxVisible
36
	-- This view clamps its scrolling so that nothing below or to the right
37
	-- of these x and y coordinates is visible.
38
	maxVisible = { x = math.huge, y = math.huge },
39
40
	-- private property: _tint
41
	-- used to implement tints.
42
43
	-- private property: _fx
44
	-- used to perform fades and flashes.
45
46
	new = function (self, obj)
47
		obj = self:extend(obj)
48
49
		obj.timer = Timer:new()
50
		obj:add(obj.timer)
51
		obj.tween = Tween:new()
52
		obj:add(obj.tween)
53
		obj.factory = Factory:new()
54
55
		-- set the.view briefly, so that during the onNew() handler
56
		-- we appear to be the current view
57
	
58
		local oldView = the.view
59
60
		the.view = obj
61
		if obj.onNew then obj:onNew() end
62
63
		-- then reset it so that nothing breaks for the remainder
64
		-- of the frame for the old, outgoing view members.
65
		-- our parent app will restore us into the.view at the top of the next frame
66
		-- exception: there was no old view.
67
68
		if oldView then the.view = oldView end
69
		return obj
70
	end,
71
72
	-- Method: loadLayers
73
	-- Loads layers from a Lua source file (as generated by Tiled -- http://mapeditor.org).
74
	-- Each layer is created as a <Group> and added to preserve its ordering. Tile layers
75
	-- are created as <Map> instances; object layers will try to create instances of a class
76
	-- named by the object's name property. If no class exists by this name, or the object
77
	-- has no name property, a gray fill will be created instead, as a placeholder. If the
78
	-- object has a property named _the, then this will set the.[whatever] to it.
79
	--
80
	-- Arguments:
81
	--		file - filename to load
82
	--
83
	-- Returns:
84
	--		nothing
85
86
	loadLayers = function (self, file)
87
		local ok, data = pcall(loadstring(Cached:text(file)))
88
		local _, _, directory = string.find(file, '^(.*[/\\])')
89
		directory = directory or ''
90
91
		if ok then
92
			-- store tile properties by gid
93
			
94
			local tileProtos = {}
95
96
			for _, tileset in pairs(data.tilesets) do
97
				for _, tile in pairs(tileset.tiles) do
98
					local id = tileset.firstgid + tile.id
99
					
100
					for key, value in pairs(tile.properties) do
101
						tile.properties[key] = tovalue(value)
102
					end
103
104
					tileProtos[id] = tile
105
					tileProtos[id].width = tileset.tilewidth
106
					tileProtos[id].height = tileset.tileheight
107
				end
108
			end
109
110
			for _, layer in pairs(data.layers) do
111
				if View[layer.name] then
112
					error('the View class reserves the ' .. layer.name .. ' property for its own use; you cannot load a layer with that name')
113
				end
114
115
				if STRICT and self[layer.name] then
116
					local info = debug.getinfo(2, 'Sl')
117
					print('Warning: a property named ' .. layer.name .. ' already exists in the current view (' ..
118
						  info.short_src .. ', line ' .. info.currentline .. ')')
119
				end
120
121
				if layer.type == 'tilelayer' then
122
					local map = Map:new{ spriteWidth = data.tilewidth, spriteHeight = data.tileheight }
123
					map:empty(layer.width, layer.height)
124
125
					-- load tiles
126
127
					for _, tiles in pairs(data.tilesets) do
128
						map:loadTiles(directory .. tiles.image, Tile, tiles.firstgid)
129
130
						-- and mix in properties where applicable
131
132
						for id, tile in pairs(tileProtos) do
133
							if map.sprites[id] then
134
								map.sprites[id]:mixin(tile.properties)
135
							end
136
						end
137
					end
138
139
					-- load tile data
140
141
					local x = 1
142
					local y = 1
143
144
					for _, val in ipairs(layer.data) do
145
						map.map[x][y] = val
146
						x = x + 1
147
148
						if x > layer.width then
149
							x = 1
150
							y = y + 1
151
						end
152
					end
153
154
					self[layer.name] = map
155
					self:add(map)
156
				elseif layer.type == 'objectgroup' then
157
					local group = Group:new()
158
159
					for _, obj in pairs(layer.objects) do
160
						-- roll in tile properties if based on a tile
161
162
						if obj.gid and tileProtos[obj.gid] then
163
							local tile = tileProtos[obj.gid]
164
165
							obj.name = tile.properties.name
166
							obj.width = tile.width
167
							obj.height = tile.height
168
169
							for key, value in pairs(tile.properties) do
170
								obj.properties[key] = tovalue(value)
171
							end
172
173
							-- Tiled tile-based objects measure their y
174
							-- position at their lower-left corner, instead
175
							-- of their upper-left corner as usual
176
177
							obj.y = obj.y - obj.height
178
						end
179
180
						-- create a new object if the class does exist
181
182
						local spr
183
184
						if _G[obj.name] then
185
							obj.properties.x = obj.x
186
							obj.properties.y = obj.y
187
							obj.properties.width = obj.width
188
							obj.properties.height = obj.height
189
190
							spr = _G[obj.name]:new(obj.properties)
191
						else
192
							spr = Fill:new{ x = obj.x, y = obj.y, width = obj.width, height = obj.height, fill = { 128, 128, 128 } }
193
						end
194
195
						if obj.properties._the then
196
							the[obj.properties._the] = spr
197
						end
198
199
						group:add(spr)
200
					end
201
202
					self[layer.name] = group
203
					self:add(group)
204
				else
205
					error("don't know how to create a " .. layer.type .. " layer from file data")
206
				end
207
			end
208
		else
209
			error('could not load view data from file: ' .. data)
210
		end
211
	end,
212
213
	-- Method: clampTo
214
	-- Clamps the view so that it never scrolls past a sprite's boundaries.
215
	-- This only looks at the sprite's position at this instant in time,
216
	-- not afterwards.
217
	--
218
	-- Arguments:
219
	--		sprite - sprite to clamp to
220
	--
221
	-- Returns:
222
	--		nothing
223
224
	clampTo = function (self, sprite)
225
		self.minVisible.x = sprite.x
226
		
227
		if sprite.x + sprite.width > the.app.width then
228
			self.maxVisible.x = sprite.x + sprite.width
229
		else
230
			self.maxVisible.x = the.app.width
231
		end
232
		
233
		self.minVisible.y = sprite.y
234
		
235
		if sprite.y + sprite.height > the.app.height then
236
			self.maxVisible.y = sprite.y + sprite.height
237
		else
238
			self.maxVisible.y = the.app.height
239
		end
240
	end,
241
242
	-- Method: panTo
243
	-- Pans the view so that the target sprite or position is centered
244
	-- onscreen. This sets the view's focus to nil.
245
	--
246
	-- Arguments:
247
	--		target - sprite or coordinate pair to pan to
248
	--		duration - how long the pan will take, in seconds
249
	--		ease - what easing to apply, see <Tween> for details, defaults to 'quadInOut'
250
	--
251
	-- Returns:
252
	--		A <Promise> that is fulfilled when the pan completes.
253
254
	panTo = function (self, target, duration, ease)
255
		ease = ease or 'quadInOut'
256
		local targetX, targetY
257
258
		if STRICT then
259
			assert((target.x and target.y and target.width and target.height) or (#target == 2),
260
				   'pan target does not appear to be a sprite or coordinate pair')
261
			assert(type(duration) == 'number', 'pan duration is not a number')
262
			assert(self.tween.easers[ease], 'pan easing method ' .. ease .. ' is not defined')
263
		end
264
265
		if target.x and target.y and target.width and target.height then
266
			targetX = target.x + target.width / 2
267
			targetY = target.y + target.height / 2
268
		else
269
			targetX = target[1]
270
			targetY = target[2]
271
		end
272
273
		-- calculate translation to center these coordinates
274
275
		local tranX = math.floor(-targetX + the.app.width / 2)
276
		local tranY = math.floor(-targetY + the.app.height / 2)
277
		
278
		-- clamp translation to min and max visible
279
		
280
		if tranX > - self.minVisible.x then tranX = - self.minVisible.x end
281
		if tranY > - self.minVisible.y then tranY = - self.minVisible.y end
282
		
283
		if tranX < the.app.width - self.maxVisible.x then
284
			tranX = the.app.width - self.maxVisible.x
285
		end
286
		
287
		if tranY < the.app.height - self.maxVisible.y then
288
			tranY = the.app.height - self.maxVisible.y
289
		end
290
291
		-- tween the appropriate properties
292
		-- some care has to be taken to avoid fulfilling the promise twice
293
294
		self.focus = nil
295
		local promise = Promise:new()
296
297
		if tranX ~= self.translate.x then
298
			self.tween:start(self.translate, 'x', tranX, duration, ease)
299
				:andThen(function() promise:fulfill() end)
300
301
			if tranY ~= self.translate.y then
302
				self.tween:start(self.translate, 'y', tranY, duration, ease)
303
			end
304
		elseif tranY ~= self.translate.y then
305
			self.tween:start(self.translate, 'y', tranY, duration, ease)
306
				:andThen(function() promise:fulfill() end)
307
		else
308
			promise:fulfill()
309
		end
310
311
		return promise
312
	end,
313
314
	-- Method: fade
315
	-- Fades out to a specified color over a period of time.
316
	--
317
	-- Arguments:
318
	--		color - color table to fade to, e.g. { 0, 0, 0 }
319
	--		duration - how long to fade out in seconds, default 1
320
	--
321
	-- Returns:
322
	--		A <Promise> that is fulfilled when the effect completes.
323
324
	fade = function (self, color, duration)
325
		assert(type(color) == 'table', 'color to fade to is ' .. type(color) .. ', not a table')
326
		local alpha = color[4] or 255
327
		self._fx = color
328
		self._fx[4] = 0
329
		return self.tween:start(self._fx, 4, alpha, duration or 1, 'quadOut')
330
	end,
331
332
	-- Method: flash
333
	-- Immediately flashes the screen to a specific color, then fades out.
334
	--
335
	-- Arguments:
336
	--		color - color table to flash, e.g. { 0, 0, 0 }
337
	--		duration - how long to restore normal view in seconds, default 1
338
	--
339
	-- Returns:
340
	--		A <Promise> that is fulfilled when the effect completes.
341
342
	flash = function (self, color, duration)
343
		assert(type(color) == 'table', 'color to flash is ' .. type(color) .. ', not a table')
344
		color[4] = color[4] or 255
345
		self._fx = color
346
		return self.tween:start(self._fx, 4, 0, duration or 1, 'quadOut')
347
	end,
348
349
	-- Method: tint
350
	-- Immediately tints the screen a color. To restore normal viewing,
351
	-- call this method again with no arguments.
352
	--
353
	-- Arguments:
354
	--		red - red component, 0-255
355
	--		green - green component, 0-255
356
	--		blue - blue component, 0-255
357
	--		alpha - alpha, 0-255, default 255
358
	--
359
	-- Returns:
360
	--		nothing
361
362
	tint = function (self, red, green, blue, alpha)
363
		alpha = alpha or 255
364
365
		if red and green and blue and alpha > 0 then
366
			self._tint = { red, green, blue, alpha }
367
		else
368
			self._tint = nil
369
		end
370
	end,
371
372
	update = function (self, elapsed)
373
		local screenWidth = the.app.width
374
		local screenHeight = the.app.height
375
376
		-- follow the focused sprite
377
		
378
		if self.focus and self.focus.width < screenWidth
379
		   and self.focus.height < screenHeight then
380
			self.translate.x = math.floor(- (self.focus.x + self.focusOffset.x) +
381
							   (screenWidth - self.focus.width) / 2)
382
			self.translate.y = math.floor(- (self.focus.y + self.focusOffset.y) +
383
							   (screenHeight - self.focus.height) / 2)
384
		end
385
		
386
		-- clamp translation to min and max visible
387
		
388
		if self.translate.x > - self.minVisible.x then
389
			self.translate.x = - self.minVisible.x
390
		end
391
392
		if self.translate.y > - self.minVisible.y then
393
			self.translate.y = - self.minVisible.y
394
		end
395
		
396
		if self.translate.x < screenWidth - self.maxVisible.x then
397
			self.translate.x = screenWidth - self.maxVisible.x
398
		end
399
		
400
		if self.translate.y < screenHeight - self.maxVisible.y then
401
			self.translate.y = screenHeight - self.maxVisible.y
402
		end
403
404
		Group.update(self, elapsed)
405
	end,
406
407
	draw = function (self, x, y)
408
		Group.draw(self, x, y)
409
410
		-- draw our fx and tint on top of everything
411
412
		if self._tint then
413
			love.graphics.setColor(self._tint)
414
			love.graphics.rectangle('fill', 0, 0, the.app.width, the.app.height)
415
			love.graphics.setColor(255, 255, 255, 255)
416
		end
417
418
		if self._fx then
419
			love.graphics.setColor(self._fx)
420
			love.graphics.rectangle('fill', 0, 0, the.app.width, the.app.height)
421
			love.graphics.setColor(255, 255, 255, 255)
422
		end
423
	end,
424
425
	__tostring = function (self)
426
		local result = 'View ('
427
428
		if self.active then
429
			result = result .. 'active'
430
		else
431
			result = result .. 'inactive'
432
		end
433
434
		if self.visible then
435
			result = result .. ', visible'
436
		else
437
			result = result .. ', invisible'
438
		end
439
440
		if self.solid then
441
			result = result .. ', solid'
442
		else
443
			result = result .. ', not solid'
444
		end
445
446
		return result .. ', ' .. self:count(true) .. ' sprites)'
447
	end
448
}