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 |
||
4
by Josh C
fix jitter caused by focus shift happening in the wrong order. Looks |
376 |
-- let sprites update their physics before adjusting focus |
377 |
Group.update(self, elapsed) |
|
378 |
||
1
by Josh C
zoetrope 1.3.1 |
379 |
-- follow the focused sprite |
380 |
||
381 |
if self.focus and self.focus.width < screenWidth |
|
382 |
and self.focus.height < screenHeight then |
|
383 |
self.translate.x = math.floor(- (self.focus.x + self.focusOffset.x) + |
|
384 |
(screenWidth - self.focus.width) / 2) |
|
385 |
self.translate.y = math.floor(- (self.focus.y + self.focusOffset.y) + |
|
386 |
(screenHeight - self.focus.height) / 2) |
|
387 |
end |
|
388 |
||
389 |
-- clamp translation to min and max visible |
|
390 |
||
391 |
if self.translate.x > - self.minVisible.x then |
|
392 |
self.translate.x = - self.minVisible.x |
|
393 |
end |
|
394 |
||
395 |
if self.translate.y > - self.minVisible.y then |
|
396 |
self.translate.y = - self.minVisible.y |
|
397 |
end |
|
398 |
||
399 |
if self.translate.x < screenWidth - self.maxVisible.x then |
|
400 |
self.translate.x = screenWidth - self.maxVisible.x |
|
401 |
end |
|
402 |
||
403 |
if self.translate.y < screenHeight - self.maxVisible.y then |
|
404 |
self.translate.y = screenHeight - self.maxVisible.y |
|
405 |
end |
|
406 |
||
4
by Josh C
fix jitter caused by focus shift happening in the wrong order. Looks |
407 |
--Group.update(self, elapsed) |
1
by Josh C
zoetrope 1.3.1 |
408 |
end, |
409 |
||
410 |
draw = function (self, x, y) |
|
411 |
Group.draw(self, x, y) |
|
412 |
||
413 |
-- draw our fx and tint on top of everything |
|
414 |
||
415 |
if self._tint then |
|
416 |
love.graphics.setColor(self._tint) |
|
417 |
love.graphics.rectangle('fill', 0, 0, the.app.width, the.app.height) |
|
418 |
love.graphics.setColor(255, 255, 255, 255) |
|
419 |
end |
|
420 |
||
421 |
if self._fx then |
|
422 |
love.graphics.setColor(self._fx) |
|
423 |
love.graphics.rectangle('fill', 0, 0, the.app.width, the.app.height) |
|
424 |
love.graphics.setColor(255, 255, 255, 255) |
|
425 |
end |
|
426 |
end, |
|
427 |
||
428 |
__tostring = function (self) |
|
429 |
local result = 'View (' |
|
430 |
||
431 |
if self.active then |
|
432 |
result = result .. 'active' |
|
433 |
else |
|
434 |
result = result .. 'inactive' |
|
435 |
end |
|
436 |
||
437 |
if self.visible then |
|
438 |
result = result .. ', visible' |
|
439 |
else |
|
440 |
result = result .. ', invisible' |
|
441 |
end |
|
442 |
||
443 |
if self.solid then |
|
444 |
result = result .. ', solid' |
|
445 |
else |
|
446 |
result = result .. ', not solid' |
|
447 |
end |
|
448 |
||
449 |
return result .. ', ' .. self:count(true) .. ' sprites)' |
|
450 |
end |
|
451 |
} |