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.
13
-- A built-in <Timer> object for use as needed.
16
-- A built-in <Tween> object for use as needed.
19
-- A built-in <Factory> object for use as needed.
22
-- A <Sprite> to keep centered onscreen.
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
28
focusOffset = { x = 0, y = 0 },
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 },
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 },
40
-- private property: _tint
41
-- used to implement tints.
43
-- private property: _fx
44
-- used to perform fades and flashes.
46
new = function (self, obj)
47
obj = self:extend(obj)
49
obj.timer = Timer:new()
51
obj.tween = Tween:new()
53
obj.factory = Factory:new()
55
-- set the.view briefly, so that during the onNew() handler
56
-- we appear to be the current view
58
local oldView = the.view
61
if obj.onNew then obj:onNew() end
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.
68
if oldView then the.view = oldView end
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.
81
-- file - filename to load
86
loadLayers = function (self, file)
87
local ok, data = pcall(loadstring(Cached:text(file)))
88
local _, _, directory = string.find(file, '^(.*[/\\])')
89
directory = directory or ''
92
-- store tile properties by gid
96
for _, tileset in pairs(data.tilesets) do
97
for _, tile in pairs(tileset.tiles) do
98
local id = tileset.firstgid + tile.id
100
for key, value in pairs(tile.properties) do
101
tile.properties[key] = tovalue(value)
104
tileProtos[id] = tile
105
tileProtos[id].width = tileset.tilewidth
106
tileProtos[id].height = tileset.tileheight
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')
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 .. ')')
121
if layer.type == 'tilelayer' then
122
local map = Map:new{ spriteWidth = data.tilewidth, spriteHeight = data.tileheight }
123
map:empty(layer.width, layer.height)
127
for _, tiles in pairs(data.tilesets) do
128
map:loadTiles(directory .. tiles.image, Tile, tiles.firstgid)
130
-- and mix in properties where applicable
132
for id, tile in pairs(tileProtos) do
133
if map.sprites[id] then
134
map.sprites[id]:mixin(tile.properties)
144
for _, val in ipairs(layer.data) do
148
if x > layer.width then
154
self[layer.name] = map
156
elseif layer.type == 'objectgroup' then
157
local group = Group:new()
159
for _, obj in pairs(layer.objects) do
160
-- roll in tile properties if based on a tile
162
if obj.gid and tileProtos[obj.gid] then
163
local tile = tileProtos[obj.gid]
165
obj.name = tile.properties.name
166
obj.width = tile.width
167
obj.height = tile.height
169
for key, value in pairs(tile.properties) do
170
obj.properties[key] = tovalue(value)
173
-- Tiled tile-based objects measure their y
174
-- position at their lower-left corner, instead
175
-- of their upper-left corner as usual
177
obj.y = obj.y - obj.height
180
-- create a new object if the class does exist
185
obj.properties.x = obj.x
186
obj.properties.y = obj.y
187
obj.properties.width = obj.width
188
obj.properties.height = obj.height
190
spr = _G[obj.name]:new(obj.properties)
192
spr = Fill:new{ x = obj.x, y = obj.y, width = obj.width, height = obj.height, fill = { 128, 128, 128 } }
195
if obj.properties._the then
196
the[obj.properties._the] = spr
202
self[layer.name] = group
205
error("don't know how to create a " .. layer.type .. " layer from file data")
209
error('could not load view data from file: ' .. data)
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,
219
-- sprite - sprite to clamp to
224
clampTo = function (self, sprite)
225
self.minVisible.x = sprite.x
227
if sprite.x + sprite.width > the.app.width then
228
self.maxVisible.x = sprite.x + sprite.width
230
self.maxVisible.x = the.app.width
233
self.minVisible.y = sprite.y
235
if sprite.y + sprite.height > the.app.height then
236
self.maxVisible.y = sprite.y + sprite.height
238
self.maxVisible.y = the.app.height
243
-- Pans the view so that the target sprite or position is centered
244
-- onscreen. This sets the view's focus to nil.
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'
252
-- A <Promise> that is fulfilled when the pan completes.
254
panTo = function (self, target, duration, ease)
255
ease = ease or 'quadInOut'
256
local targetX, targetY
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')
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
273
-- calculate translation to center these coordinates
275
local tranX = math.floor(-targetX + the.app.width / 2)
276
local tranY = math.floor(-targetY + the.app.height / 2)
278
-- clamp translation to min and max visible
280
if tranX > - self.minVisible.x then tranX = - self.minVisible.x end
281
if tranY > - self.minVisible.y then tranY = - self.minVisible.y end
283
if tranX < the.app.width - self.maxVisible.x then
284
tranX = the.app.width - self.maxVisible.x
287
if tranY < the.app.height - self.maxVisible.y then
288
tranY = the.app.height - self.maxVisible.y
291
-- tween the appropriate properties
292
-- some care has to be taken to avoid fulfilling the promise twice
295
local promise = Promise:new()
297
if tranX ~= self.translate.x then
298
self.tween:start(self.translate, 'x', tranX, duration, ease)
299
:andThen(function() promise:fulfill() end)
301
if tranY ~= self.translate.y then
302
self.tween:start(self.translate, 'y', tranY, duration, ease)
304
elseif tranY ~= self.translate.y then
305
self.tween:start(self.translate, 'y', tranY, duration, ease)
306
:andThen(function() promise:fulfill() end)
315
-- Fades out to a specified color over a period of time.
318
-- color - color table to fade to, e.g. { 0, 0, 0 }
319
-- duration - how long to fade out in seconds, default 1
322
-- A <Promise> that is fulfilled when the effect completes.
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
329
return self.tween:start(self._fx, 4, alpha, duration or 1, 'quadOut')
333
-- Immediately flashes the screen to a specific color, then fades out.
336
-- color - color table to flash, e.g. { 0, 0, 0 }
337
-- duration - how long to restore normal view in seconds, default 1
340
-- A <Promise> that is fulfilled when the effect completes.
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
346
return self.tween:start(self._fx, 4, 0, duration or 1, 'quadOut')
350
-- Immediately tints the screen a color. To restore normal viewing,
351
-- call this method again with no 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
362
tint = function (self, red, green, blue, alpha)
365
if red and green and blue and alpha > 0 then
366
self._tint = { red, green, blue, alpha }
372
update = function (self, elapsed)
373
local screenWidth = the.app.width
374
local screenHeight = the.app.height
376
-- follow the focused sprite
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)
386
-- clamp translation to min and max visible
388
if self.translate.x > - self.minVisible.x then
389
self.translate.x = - self.minVisible.x
392
if self.translate.y > - self.minVisible.y then
393
self.translate.y = - self.minVisible.y
396
if self.translate.x < screenWidth - self.maxVisible.x then
397
self.translate.x = screenWidth - self.maxVisible.x
400
if self.translate.y < screenHeight - self.maxVisible.y then
401
self.translate.y = screenHeight - self.maxVisible.y
404
Group.update(self, elapsed)
407
draw = function (self, x, y)
408
Group.draw(self, x, y)
410
-- draw our fx and tint on top of everything
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)
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)
425
__tostring = function (self)
426
local result = 'View ('
429
result = result .. 'active'
431
result = result .. 'inactive'
435
result = result .. ', visible'
437
result = result .. ', invisible'
441
result = result .. ', solid'
443
result = result .. ', not solid'
446
return result .. ', ' .. self:count(true) .. ' sprites)'