fix: another PNG CRC error
[poca-love.git] / rubens.lua
blobf3c33d3e4e7a85dfdcefa3cf52ef4276c1855c4f
1 --[[
2 Ruben's Pyramid - a minigame is a part of POCA - a puzzle game
4 POCA is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
9 This software is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <https://www.gnu.org/licenses/>.
18 require('special')
20 rp = {
23 hijacked_love_functions = {
24 'draw', 'resize',
25 'mousepressed',
26 'keypressed'
30 draw = function() -- hijacked function
31 -- pyramid
32 love.graphics.setColor(1,1,1, 1)
33 if not rp.meshes then
34 love.graphics.draw(rp.rich_canvas, rp.bigx, rp. bigy)
35 else -- draw rotating triangles over canvas with these traingles missing
36 love.graphics.draw(rp.poor_canvas, rp.bigx, rp. bigy)
37 for a = 1, 3 do
38 love.graphics.draw(rp.meshes[a], rp.bigx, rp.bigy)
39 end
40 -- restore selected triangle to hide weird shapes that appear when rotating
41 local p = rp.pyramid
42 local x = rp.bigx + p[p.selectedr][p.selectedt].x
43 local y = rp.bigy + p[p.selectedr][p.selectedt].y
44 rp.draw_triangle(x, y, p[p.selectedr][p.selectedt].color)
45 end
46 --cursor
47 rp:draw_pyramid_cursor()
48 -- buttons and label
49 rp.buttons:draw()
50 end,
53 -- height of equilateral triangle
54 h = function(a)
55 return a * 1.732 / 2
56 end,
59 draw_button_image = function(name, selected, disabled)
60 local images = {
61 more = function()
62 love.graphics.line(.5,1, .5,0)
63 love.graphics.line(0,.5, 1,.5)
64 end,
65 less = function()
66 love.graphics.line(0,.5, 1,.5)
67 end,
68 x = function()
69 love.graphics.line(0,0, 1,1)
70 love.graphics.line(1,0, 0,1)
71 end,
72 shuffle = function()
73 love.graphics.polygon('line', 0,.1, .5,rp.h(1)+.1, 1,.1)
74 end,
75 fast = function()
76 if rp.no_anim then love.graphics.setColor(1,1,1, .4) end
77 love.graphics.rectangle('line', 0,0, .2,.2)
78 love.graphics.rectangle('line', 0,.4, .2,.2)
79 love.graphics.rectangle('line', 0,.8, .2,.2)
80 end,
83 -- draw button background
84 love.graphics.setColor(.4,.4,.4, 1)
85 if selected then
86 love.graphics.rectangle('fill', 0, 0, 1, 1, .1)
87 else
88 love.graphics.rectangle('line', 0, 0, 1, 1, .1)
89 end
90 love.graphics.setLineWidth(.03)
91 love.graphics.rectangle('line', 0, 0, 1, 1, .1)
92 -- draw button image
93 love.graphics.push()
94 love.graphics.translate(.1, .1)
95 love.graphics.scale(.8)
96 if disabled then
97 love.graphics.setColor(1,1,1, .4)
98 else
99 love.graphics.setColor(1,1,1, 1)
101 images[name]()
102 love.graphics.pop()
103 end,
106 buttons = {
108 x, y, image = 'less',
109 onclick = function()
110 rp.size = math.max(rp.min_size, rp.size - 1)
111 rp:init()
115 x, y, image = 'more',
116 onclick = function()
117 rp.size = math.min(rp.max_size, rp.size + 1)
118 rp:init()
122 x, y, image = 'fast',
123 onclick = function()
124 rp.no_anim = not rp.no_anim
128 x, y, image = 'shuffle',
129 onclick = function()
130 rp:shuffle()
134 x, y, image = 'x',
135 onclick = function()
136 rp:quit()
139 draw = function(self)
140 for a = 1, #self do
141 love.graphics.push()
142 love.graphics.translate(self[a].x, self[a].y)
143 love.graphics.scale(rp.buttonsize)
144 rp.draw_button_image(self[a].image, self.selected == a, self[a].disabled)
145 love.graphics.pop()
147 end,
148 selected = nil,
152 -- 10 random clicks
153 shuffle = function()
154 local function last(psize)
155 return psize * (psize + 1) / 2
157 for a = 1, 10 do
158 local rnd = math.random(last(rp.size))
159 for row = 1, rp.size do
160 if last(row) >= rnd then
161 rp:pyramid_click(row, rnd - last(row - 1), true) -- no animation
162 break
166 -- remove last click indicator
167 rp.pyramid.selectedr, rp.pyramid.selectedt = nil, nil
168 -- remove any meshes
169 rp.meshes = false
170 end,
173 resize = function() -- hijacked function
174 if current.phase == 'rubens_anim' then return end
175 resize_common()
176 if screen.h < rp.h(screen.w) then
177 rp.margin = screen.h / 10
178 rp.bigh = screen.h - rp.margin
179 rp.bigw = rp.bigh * 1.1547
180 else
181 rp.margin = screen.w / 10
182 rp.bigw = screen.w - rp.margin
183 rp.bigh = rp.h(screen.w - rp.margin)
185 rp.bigx = (screen.w - rp.bigw) / 2
186 rp.bigy = (screen.h - rp.bigh) / 2
187 rp.buttonsize = rp.bigw / 7
188 local buttony = rp.bigy / 2
189 local buttonx = {}
190 buttonx[2] = rp.bigx / 2
191 buttonx[3] = buttonx[2] + rp.buttonsize + rp.margin / 3
192 buttonx[5] = screen.w - rp.buttonsize - rp.bigx / 2
193 buttonx[4] = buttonx[5] - rp.buttonsize - rp.margin / 3
194 for a = 2, #buttonx do
195 rp.buttons[a].x, rp.buttons[a].y = buttonx[a], buttony
197 rp.buttons[1].x = buttonx[2]
198 rp.buttons[1].y = buttony + rp.buttonsize + rp.margin / 3
199 rp.labelwidth = rp.buttonsize
200 rp.smallw = rp.bigw / rp.size
201 rp.smallh = rp.h(rp.smallw)
202 rp.inpyramid(rp.pyramid, function(p, row, tri)
203 p[row][tri].x = rp.bigw / 2 + (tri - 1 - row / 2) * rp.smallw
204 p[row][tri].y = (row - 1) * rp.smallh
205 end)
206 rp:update_canvases()
207 end,
210 update_canvases = function(self)
211 if not self.poor_canvas then
212 self.poor_canvas = love.graphics.newCanvas(self.bigw, self.bigh)
213 -- without mirrored triangles
215 if not self.rich_canvas then
216 self.rich_canvas = love.graphics.newCanvas(self.bigw, self.bigh)
217 -- with mirrored triangles
219 love.graphics.setCanvas(self.poor_canvas)
220 self:draw_pyramid(true) -- exclude mirrored triangles
221 love.graphics.setCanvas(self.rich_canvas)
222 self:draw_pyramid()
223 love.graphics.setCanvas()
224 end,
227 pyramid_click = function(self, r, t, no_anim)
228 local function mirror(p, r, t)
229 for row = r + 1, rp.size do -- skip first
230 for tri = t, t + math.floor((row - r) / 2) do
231 -- (row - r - 1) to exclude central triangles which go nowhere
232 -- but they need to be marked as mirrored nevertheless
233 local tempcolor = p[row][tri].color
234 p[row][tri].color = p[row][2 * t + row - r - tri].color
235 p[row][2 * t + row - r - tri].color = tempcolor
236 p[row][tri].mirrored = true
237 p[row][2 * t + row - r - tri].mirrored = true
241 self:pyramid_clear_mirrored()
242 self.pyramid.selectedr = r
243 self.pyramid.selectedt = t
244 self:init_meshes()
245 for a = 1, 3 do
246 mirror(self.pyramid, self.pyramid.selectedr, self.pyramid.selectedt)
247 self.pyramid = rp.rotate_pyramid(self.pyramid)
249 self:update_canvases()
250 if not (no_anim or self.no_anim) then animate_pyramid() end
251 end,
254 pyramid_clear_mirrored = function(self)
255 self.inpyramid(self.pyramid, function(p, row, tri)
256 p[row][tri].mirrored = nil
257 end)
258 end,
261 mousepressed = function(mx, my, button) -- hijacked function
262 if current.phase == 'rubens_anim' then return end
263 rp.pyramid.selectedr = nil
264 rp.pyramid.selectedt = nil
265 rp.buttons.selected = nil
266 -- check for button clicks
267 for a = 1, #rp.buttons do
269 isin(mx, my, rp.buttons[a].x, rp.buttons[a].y, rp.buttonsize, rp.buttonsize) and
270 not rp.buttons[a].disabled
271 then
272 rp.buttons[a].onclick()
273 return
276 -- check for pyramid clicks
277 rp.inpyramid(rp.pyramid, function(p, r, t)
278 if isin(mx - rp.bigx, my - rp.bigy, p[r][t].x, p[r][t].y, rp.smallw, rp.smallh) then
279 rp:pyramid_click(r,t)
280 return
282 end)
284 end,
287 cursor_move = function(dir)
288 -- default values
289 if not rp.pyramid.selectedr then
290 rp.pyramid.selectedr = rp.pyramid.selectedr or 1
291 rp.pyramid.selectedt = rp.pyramid.selectedt or 1
292 elseif dir == 'up' then
293 -- jump to third button from the tip of the pyramid
294 if rp.pyramid.selectedr == 1 then
295 rp.pyramid.selectedr = 0
296 rp.pyramid.selectedt = 3
297 -- special case: move to another arrow button
298 elseif rp.pyramid.selectedr == 0 and rp.pyramid.selectedt == 1 then
299 rp.pyramid.selectedt = 2
300 -- shound't do anything at 0th row (doesn't move to -1 and left)
301 else
302 rp.pyramid.selectedr = rp.pyramid.selectedr - 1
304 (rp.pyramid.selectedr % 2) == 1 and
305 rp.pyramid.selectedr > 1
306 then
307 rp.pyramid.selectedt = rp.pyramid.selectedt - 1
310 elseif dir == 'down' then
311 if rp.pyramid.selectedr == 0 and rp.pyramid.selectedt == 2 then
312 -- special case: move to another arrow button
313 rp.pyramid.selectedt = 1
314 else
315 -- shound't do anything at last row
316 rp.pyramid.selectedr = rp.pyramid.selectedr + 1
318 (rp.pyramid.selectedr % 2) == 0 and
319 rp.pyramid.selectedr <= rp.size
320 then
321 rp.pyramid.selectedt = rp.pyramid.selectedt + 1
324 elseif dir == 'left' then
325 --jump to buttons from 1
326 if rp.pyramid.selectedr == 1 then
327 rp.pyramid.selectedr = 0
328 rp.pyramid.selectedt = 3
329 else
330 rp.pyramid.selectedt = rp.pyramid.selectedt - 1
332 elseif dir == 'right' then
333 --jump to buttons from 1
334 if rp.pyramid.selectedr == 1 then
335 rp.pyramid.selectedr = 0
336 rp.pyramid.selectedt = 4
337 else
338 rp.pyramid.selectedt = rp.pyramid.selectedt + 1
341 rp.pyramid.selectedr = math.max(0, rp.pyramid.selectedr)
342 rp.pyramid.selectedr = math.min(rp.size, rp.pyramid.selectedr)
343 rp.pyramid.selectedt = math.max(1, rp.pyramid.selectedt)
344 if rp.pyramid.selectedr > 0 then
345 rp.pyramid.selectedt = math.min(rp.pyramid.selectedr, rp.pyramid.selectedt)
346 rp.buttons.selected = nil
347 else -- buttons
348 rp.pyramid.selectedt = math.min(#rp.buttons, rp.pyramid.selectedt)
349 rp.buttons.selected = rp.pyramid.selectedt
351 end,
354 keypressed = function(k) -- hijacked function
355 if current.phase == 'rubens_anim' then return end
356 if k == 'q' then
357 love.event.push('quit')
358 elseif k == 'escape' then
359 rp:quit()
360 elseif k == 'up' or k == 'down' or k == 'left' or k == 'right' then
361 rp.cursor_move(k)
362 elseif k == 'return' and rp.pyramid.selectedr then
363 if rp.pyramid.selectedr > 0 then
364 rp:pyramid_click(rp.pyramid.selectedr, rp.pyramid.selectedt)
365 else
366 local clicked_button = rp.pyramid.selectedt
367 if not rp.buttons[rp.pyramid.selectedt].disabled then
368 rp.buttons[rp.pyramid.selectedt].onclick()
370 -- preserve selection
371 rp.pyramid.selectedr = 0
372 rp.pyramid.selectedt = clicked_button
375 end,
378 -- init or go through pyramid
379 inpyramid = function(p, f, lastrow)
380 for row = 1, lastrow or p.size do
381 p[row] = p[row] or {}
382 for triangle = 1, row do
383 p[row][triangle] = p[row][triangle] or {color = 0}
384 if f then f(p, row, triangle) end
387 end,
390 rotate_pyramid = function(p)
391 local newp = {size = p.size}
392 rp.inpyramid(newp, function(np, r, t)
393 np[r][t].x, np[r][t].y = p[r][t].x, p[r][t].y
394 end) -- just init and copy coordinates
395 rp.inpyramid(newp, function(np, r, t)
396 np[np.size - t + 1][r - t + 1].color = p[r][t].color
397 np[np.size - t + 1][r - t + 1].mirrored = p[r][t].mirrored
398 -- preserve selection
399 if p.selectedr == r and p.selectedt == t then
400 np.selectedr = np.size - t + 1
401 np.selectedt = r - t + 1
403 end)
404 return newp
405 end,
408 init_pyramid = function(size)
409 -- init white and paint pyramid top red, then rotate...
410 local p = {size = size}
411 rp.inpyramid(p) --just init
412 local function paint_top(color, height)
413 rp.inpyramid(p, function(p, row, triangle)
414 p[row][triangle].color = color
415 end, height) -- paint until here
417 for a = 1, 3 do
418 paint_top(a, size / 2)
419 p = rp.rotate_pyramid(p)
421 return p
422 end,
425 -- initialize big triangles for animation
426 init_meshes = function(self)
427 if self.no_anim then return end
428 local p = self.pyramid
429 if not (p.selectedr and p.selectedr > 0) then
430 self.meshes = nil
431 return
433 local v = {}
434 local sr = p.selectedr
435 local st = p.selectedt
436 -- bottom triangle
437 local tx1 = p[sr][st].x + self.smallw / 2
438 local ty1 = p[sr][st].y
439 local bottomy = p[self.size][st].y + self.smallh
440 local dx1 = tx1 - p[self.size][st].x
441 v[1] = {
442 tx1, ty1, tx1 - dx1, bottomy, tx1 + dx1, bottomy
444 -- left triangle
445 local tx2 = p[sr][st].x + self.smallw
446 local ty2 = p[sr][st].y + self.smallh
447 local dx2 = tx2 - p[sr][1].x
448 v[2] = {
449 tx2, ty2, p[sr][1].x, ty2, tx2 - dx2 / 2, ty2 - self.h(dx2)
451 -- right triangle
452 local tx3 = p[sr][st].x
453 local dx3 = p[sr][sr].x - p[sr][st].x + self.smallw
454 v[3] = {
455 tx3, ty2, tx3 + dx3, ty2, tx3 + dx3 / 2, ty2 - self.h(dx3)
457 self.meshes = {}
458 for a = 1, 3 do
459 self.meshes[a] = love.graphics.newMesh({
460 { v[a][1], v[a][2], v[a][1] / self.bigw, v[a][2] / self.bigh },
461 { v[a][3], v[a][4], v[a][3] / self.bigw, v[a][4] / self.bigh },
462 { v[a][5], v[a][6], v[a][5] / self.bigw, v[a][6] / self.bigh }
464 self.meshes[a]:setTexture(self.rich_canvas) -- sample triangles from the whole picture
466 self.vertices = v
467 end,
470 -- exchange places of two last vertices
471 anim_triangles = function(self, phase)
472 -- sometimes meshes get unset at the last step of animation and this
473 -- needs to be checked to prevent crashes
474 if not self.meshes then return end
475 local v = self.vertices
476 for a = 1, 3 do
477 local dx = (v[a][5] - v[a][3]) * phase
478 local dy = (v[a][6] - v[a][4]) * phase
479 self.meshes[a]:setVertices({
480 { v[a][3] + dx, v[a][4] + dy, v[a][3] / self.bigw, v[a][4] / self.bigh },
481 { v[a][5] - dx, v[a][6] - dy, v[a][5] / self.bigw, v[a][6] / self.bigh }
482 }, 2)
484 end,
487 draw_triangle = function(x, y, color) -- upper left
488 if color == 0 then
489 love.graphics.setColor(.9,.9,.9, 1)
490 elseif color == 1 then
491 love.graphics.setColor(.9,.1,.1, 1)
492 elseif color == 2 then
493 love.graphics.setColor(.1,.9,.1, 1)
494 elseif color == 3 then
495 love.graphics.setColor(.1,.1,.9, 1)
497 love.graphics.polygon('fill',
498 x, y + rp.smallh,
499 x + rp.smallw / 2, y,
500 x + rp.smallw, y + rp.smallh
502 love.graphics.setColor(1,1,1, 1)
503 end,
506 -- the pyramid without some triangles will be drawn on poor_canvas.
507 -- although the poor_canvas will be cut out before animation,
508 -- to remove background, it's still better to exclude these triangles,
509 -- because canvas-to meshes mapping is not perfect
510 draw_pyramid = function(self, exclude_mirrored)
511 -- pyramid background
512 love.graphics.setColor(.2,.2,.2, 1)
513 love.graphics.polygon('fill',
514 0, 0 + rp.bigh,
515 0 + rp.bigw, 0 + rp.bigh,
516 0 + rp.bigw / 2, 0
518 local p = self.pyramid
519 rp.inpyramid(p, function(p, r, t)
520 if not (exclude_mirrored and p[r][t].mirrored) then
521 rp.draw_triangle(p[r][t].x, p[r][t].y, p[r][t].color)
523 end)
524 end,
527 draw_pyramid_cursor = function(self)
528 local p = self.pyramid
529 local r = p.selectedr
530 local t = p.selectedt
531 if r and r > 0 then
532 love.graphics.setColor(0,0,0, .5)
533 love.graphics.circle('fill',
534 rp.bigx + p[r][t].x + rp.smallw / 2, rp.bigy + p[r][t].y + rp.smallh * 2 / 3,
535 rp.smallh / 3
538 end,
541 -- called when changing the height
542 init = function(self)
543 self.pyramid = self.init_pyramid(self.size)
544 self.buttons[2].disabled = self.size >= self.max_size
545 self.buttons[1].disabled = self.size <= self.min_size
546 self.resize()
547 end,
550 main = function(self)
551 self.old_speed = settings.speed
552 if settings.speed == 2 then
553 settings.speed = 0
554 self.no_anim = true
556 hijack(self)
557 self.max_size = 20
558 self.min_size = 4
559 self.size = 4
560 self:init()
561 end,
564 quit = function(self)
565 release(self)
566 love.resize()
567 change_screen('menu_objects')
568 settings.speed = self.old_speed
569 end,
572 } -- end of rp