fix: another PNG CRC error
[poca-love.git] / main.lua
blobc26e534a40eb98424020e0e76bedf57ae475d5d8
1 --[[
2 POCA - a puzzle game
3 Copyright 2019, 2020 Pajo <xpio at tut dot by>
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>.
19 require('images')
20 require('special')
21 require('levels')
22 require('hints')
23 require('anim')
24 require('menu')
25 require('trinkets')
26 require('saves')
27 require('background')
28 -- minigames
29 require('rainbowring')
30 require('rubens')
31 require('pocketexplosions')
32 -- sequences
33 require('license')
34 require('gameglitch')
35 require('ending')
36 -- load optional debug things
37 local d = package.loaders[2]('debug_things')
38 if type(d) == 'function' then d() end
39 d = nil
41 function init()
43 -- this table will be saved when quitting or saving game
44 game = {
45 purple = false,
46 devil = false,
47 underpill = false,
48 shuffled = false,
49 wisepuzzle = false,
50 searched_corpses = {},
51 seen_levels = {},
52 solved_levels = {},
53 shown_screens = {},
54 painted_levels = {},
55 trinkets = {},
56 former_trinkets = {},
57 secrets_arrows_used = {},
58 currentlevel = 1,
59 wise_man_has = {
60 'pencil', 'pocketexplosions', 'frame',
61 'advice_yellow', 'advice_genie', 'advice_pill',
62 'advice_purple', 'advice_map', 'advice_boxes',
64 matches = 3,
65 cigarettes = 3,
68 if current and current.music then
69 current.music:stop()
70 current.music:release()
71 current.music = nil
72 end
73 -- this table is reset on start or load
74 current = {
75 symbolic = false,
76 music = nil,
77 played = nil,
78 temppurple = false,
79 died = false,
80 showkill = false,
81 timer = 0,
82 screen = 'playfield', -- 'playfield', 'menu_objects', 'story_beginning' ...
83 phase = 'play', -- see next_phase()
84 trinket = nil,
85 painting = false,
88 small_playfield = {{}, {}, {}, {}, name = 'small'}
89 big_playfield = {{}, {}, {}, {}, name = 'big'}
90 setup_big_playfield() -- unsets game.purple and current.temppurple
91 playfield = small_playfield
92 end
95 function love.load()
96 hatch = love.graphics.newShader [[
97 uniform float line_distance;
98 uniform float line_transparency;
99 vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
101 vec4 tc = Texel(tex, texture_coords) * color;
102 float thickness = line_distance / 4.0;
103 float m = mod(screen_coords.x, line_distance);
104 float n = mod(screen_coords.y, line_distance);
105 m = clamp((m + n) / 2.0 - thickness, line_transparency, 1.0);
106 return vec4(vec3(tc.r, tc.g, tc.b) * m, tc.a);
109 -- initialize uniforms separarely for compatibility
110 hatch:send('line_distance', 16.0)
111 hatch:send('line_transparency', .7)
112 directions = {
113 {1, 0}, {1, -1}, {0, -1}, {-1, -1},
114 {-1, 0}, {-1, 1}, {0, 1}, {1, 1}
116 math.randomseed(os.time())
117 settings = {
118 speed = 0, -- normal
119 sounds = true,
120 sounds_volume = 1,
121 music = true,
122 music_volume = 1,
123 vibrate = false,
125 load_settings()
126 load_sounds()
127 -- load_game includes init() and setuplevel()
128 load_game('autosave')
129 screen = {}
130 love.resize()
131 -- first time, enter game directly, afterwards, enter title screen
132 if not (current.screen == 'story_beginning') then change_screen('menu_main') end
136 function explode_clicked()
137 if not playfield.clickedx then return end
138 local p = playfield[playfield.clickedx][playfield.clickedy]
139 if p.kind == 'pill' then
140 events('pill on')
141 return
143 if p.kind == 'memory' then -- remember
144 playfield[playfield.clickedx][playfield.clickedy] = p.memory
145 playfield.remembered = true
146 return
148 -- mark all purple arrow tiles if a purple arrow is clicked
149 -- even if only a memory
150 if p.kind == 'purple' then
151 current.temppurple = not current.temppurple
152 inplayfield(function (x, y, p) if p.kind == 'purple' or (p.memory and p.memory.kind == 'purple') then
153 playfield[x][y].hit = true
154 local pf = playfield[x][y].memory or playfield[x][y]
155 pf.center = 'dot'
156 end end)
159 -- clicked arrows should explode, even if player dies
160 -- to explain turning of yellow arrows, and just for fun
161 -- first, copy them to new table
162 local newtile = recognizeink('0')
163 newtile.trinket = p.trinket -- preserve trinket information
164 newtile.afterclick = p.afterclick -- preserve ac function
165 if p.shape == 'x' or p.shape == '+' then -- perhaps needs more conditions later
166 newtile.arrows = {}
167 newtile.color = images[p.name].color
168 newtile.shape = p.shape
169 newtile.gift = p.gift
170 newtile.arrowgift = p.arrowgift
171 if p.sleep then
172 newtile.sleep = 4
173 newtile.sleeping_name = p.sleeping_name or p.name
176 for dir, arr in ipairs(p.arrows) do
177 -- arrows are false or integer: distance from tile center
178 newtile.arrows[dir] = arr
179 if arr then
180 local discard, distance, pf = check_direction(playfield.clickedx, playfield.clickedy, directions[dir][1], directions[dir][2], playfield.clickedx, playfield.clickedy)
181 local target
182 if pf and pf.kind == 'yellow' then
183 pf.hit = true
184 target = pf
185 end -- mark yellow arrows for rotation
186 explode_arrow(playfield.clickedx, playfield.clickedy, dir, distance, false, target)
190 if game.underpill then return end
191 playfield[playfield.clickedx][playfield.clickedy] = newtile
195 function forget_clicked()
196 --only when underpill, transform clicked (and exploded) arrow to the memory
197 if not (game.underpill and playfield.clickedx) then return end
198 if playfield.remembered then playfield.remembered = false ; return end
199 local p = playfield[playfield.clickedx][playfield.clickedy]
200 local p_d = p.devil -- to preserve devil after becoming a memory
201 playfield[playfield.clickedx][playfield.clickedy] = recognizeink(get_memory_name(p.name))
202 if playfield[playfield.clickedx][playfield.clickedy].memory then
203 playfield[playfield.clickedx][playfield.clickedy].memory.devil = p_d
208 function check_player_death()
209 inplayfield(function(x, y, p)
210 if not p.arrows then return end
211 for direction, arrow in ipairs(p.arrows) do
212 if not p.backgrounded and arrow then
213 local death, distance = check_direction(x, y,
214 directions[direction][1], directions[direction][2],
215 playfield.clickedx, playfield.clickedy)
216 if death then
217 explode_arrow(x, y, direction, distance, true) -- kill=true
218 current.died = true
219 if playfield.name == 'big' then setup_big_death() end
221 if not current.died and playfield.wisemanx then
222 local wisedeath, distance = check_direction(x, y,
223 directions[direction][1], directions[direction][2],
224 playfield.wisemanx, playfield.wisemany
226 if wisedeath then
227 explode_arrow(x, y, direction, distance, false)
228 game.wisepuzzle = true
229 play_sound('dead')
234 end)
238 function influence_and_rotate()
239 -- devil influence on yellow arrows
240 if game.devil then
241 local dx, dy
242 local yellow_arrows = {}
243 inplayfield(function(x, y, p)
244 if p.kind == 'yellow' then
245 if p.devil then
246 -- don't insert last influenced yellow arrow
247 playfield[x][y].devil = nil -- unset but remember in case there are no more arrows
248 dx, dy = x, y
249 elseif not (not playfield.remembered and
250 playfield.clickedx == x and playfield.clickedy == y) then
251 -- don't insert arrow just passing into memory
252 table.insert(yellow_arrows, {x, y})
255 end)
256 if #yellow_arrows > 0 then
257 -- choose among available yellow arrows
258 dx, dy = unpack(yellow_arrows[math.random(#yellow_arrows)])
259 playfield[dx][dy].center = 'dot'
260 if playfield[dx][dy].hit then
261 rotate_arrows(dx, dy, 0) -- will just remove dot
262 else
263 rotate_arrows(dx, dy, -1)
265 playfield[dx][dy].hit = false -- so won't be rotated later
267 if dx then --set devil on some of the arrows in foreground
268 playfield[dx][dy].devil = true
269 -- unset any devil in memories
270 inplayfield(function(x, y, p)
271 if p.memory and p.memory.devil then
272 playfield[x][y].memory.devil = nil
274 end)
275 end -- new or last dx, dy in case there was only 1
277 -- turn yellow and purple arrows
278 inplayfield(function(x, y, p)
279 if (p.kind == 'yellow' or p.kind == 'purple' or (p.kind == 'memory' and p.memory.kind == 'purple')) and p.hit then
280 rotate_arrows(x, y)
281 p.hit = false
283 end)
284 -- wake regenerating arrows up
285 inplayfield(function(x, y, p)
286 if p.sleep and p.sleep > 0 then
287 p.sleep = p.sleep - 1
288 if p.sleep == 0 then
289 -- wake arrows but preserve redirection
290 -- (only relevant for wise man battle)
291 playfield[x][y] = recognizeink(p.sleeping_name)
292 grow_arrows(x, y)
295 end)
296 -- rotate red arrows
297 inplayfield(function(x, y, p)
298 if p.kind == 'red' or p.kind == 'loadred' then rotate_arrows(x, y) end
299 end)
303 function next_phase()
304 local nextphases = {'rotate', 'forget', 'die', 'level', 'play', 'explode'}
305 setmetatable(nextphases, {__index = function (table, key)
306 for a = 1, #table do
307 if rawget(table, a) == key then return table[a + 1] or table[1] end
309 end})
311 current.phase = nextphases[current.phase] or 'play' -- nextphases['opengifts'] == nil , etc.
312 if current.phase == 'explode' then -- 1. explode clicked arrow
313 explode_clicked()
314 elseif current.phase == 'rotate' then -- 2.
315 influence_and_rotate()
316 elseif current.phase == 'forget' then -- 3. pass to memory if underpill
317 forget_clicked()
318 elseif current.phase == 'die' then -- 4. check for player death
319 check_player_death()
320 elseif current.phase == 'level' then -- 5. check for level end
321 check_level()
322 elseif current.phase == 'play' then -- 6. await next click
323 playfield = small_playfield
328 function remember_arrows()
329 if game.underpill then return end
330 playfield.clickedx, playfield.clickedy = nil, nil
331 inplayfield(function (x, y, p)
332 if p.memory then playfield[x][y] = p.memory end
333 end)
337 function resize_common()
338 -- variables from the main game, needed for menu, etc.
339 -- disregard screen sizes below 50x50 (crashes font otherwise)
340 screen.w = math.max(love.graphics.getWidth(), 50)
341 screen.h = math.max(love.graphics.getHeight(), 50)
342 screen.tilesize = math.min(screen.w * .9 / 4, screen.h * .9 / 5)
343 screen.playfieldsize = screen.tilesize * 4
344 screen.playfieldx = math.floor((screen.w - screen.playfieldsize) / 2)
345 screen.playfieldy = math.floor((screen.h - screen.playfieldsize) / 2) + screen.tilesize / 2
346 screen.pfcanvas = love.graphics.newCanvas(screen.playfieldsize, screen.playfieldsize)
347 screen.defaultfont = love.graphics.newFont(screen.tilesize / 5)
348 screen.fontheight = screen.defaultfont:getHeight()
349 love.graphics.setFont(screen.defaultfont)
350 local s = love.graphics.getPixelDimensions() / love.graphics.getDimensions()
351 hatch:send('line_distance', screen.tilesize * s / 10)
355 function love.resize()
356 resize_common()
357 screen.pfquads = {}
358 for x = 1, 4 do
359 screen.pfquads[x] = {}
360 for y = 1, 4 do
361 screen.pfquads[x][y] = love.graphics.newQuad(
362 (x - 1) * screen.tilesize, (y - 1) * screen.tilesize,
363 screen.tilesize, screen.tilesize,
364 screen.playfieldsize, screen.playfieldsize
368 screen.minimap = love.graphics.newCanvas(screen.tilesize * 4/5, screen.tilesize * 4/5)
369 screen.minimap:renderTo(draw_minimap)
370 love.graphics.setCanvas({ {screen.pfcanvas}, stencil = true })
371 if current.screen == 'menu_main' then
372 draw_title()
373 else
374 draw_playfield()
376 love.graphics.setCanvas()
380 function love.update(dt)
381 if current.screen ~= 'playfield' then return end
382 if dt < 1/50 then
383 love.timer.sleep(1/30 - dt)
385 current.timer = current.timer + dt
386 if not update_anim(dt) then next_phase() end
387 screen.changed = true --!! stub
388 if screen.changed then
389 love.graphics.setCanvas({ {screen.pfcanvas}, stencil = true })
390 draw_playfield()
391 love.graphics.setCanvas()
392 if playfield.name == 'big' then
393 screen.minimap:renderTo(draw_minimap)
395 screen.changed = false
400 function inplayfield(f)
401 for y = 1, 4 do for x = 1, 4 do
402 f(x, y, playfield[x][y])
403 end end
407 function love.draw()
408 draw_background()
409 if current.screen == 'playfield' then
410 draw_osd()
411 draw_foreground()
412 else
413 draw_menu()
418 function check_direction(x, y, dx, dy, targetx, targety)
419 local p, xx, yy
420 for a = 1, 4 do
421 xx, yy = x + dx * a, y + dy * a
422 if not playfield[xx] then return false, a end -- arrow hits playfield edge
423 p = playfield[xx][yy]
424 if not p then return false, a end --||--
425 if xx == targetx and yy == targety then return true, a end -- arrow hits player
426 if not p.passive then return false, a, p end -- arrow hits another object. return that object
428 return false, 3
432 function check_level()
433 local function update(list)
434 update_seen_levels(
435 list, game.currentlevel,
436 game.devil, game.underpill, game.purple,
437 levels[game.currentlevel].has_yellow,
438 levels[game.currentlevel].has_purple
441 -- things to do when not dead
442 if game.wisepuzzle then
443 if playfield.wisemanx then
444 start_wise_man_puzzle()
445 return
447 for x = 1, 4 do for y = 1, 4 do
448 if game.shuffled[x][y].x ~= x or game.shuffled[x][y].y ~= y then return end
449 end end
450 -- puzzle solved
451 setuplevel(level_labels['Remorse'])
452 return
454 -- teleport arrows in Secrets use this.
455 -- they work only if player survived
456 if not current.died and playfield.goto then
457 game.secrets_arrows_used[playfield.goto[2]] = true
458 fade_to_level(playfield.goto[1])
459 return
461 -- afterclick: get trinket, search corpse
463 not current.died and
464 playfield[playfield.clickedx][playfield.clickedy].afterclick
465 then
466 playfield[playfield.clickedx][playfield.clickedy]:afterclick()
467 --get_trinket(playfield[playfield.clickedx][playfield.clickedy].trinket)
470 -- checks for the level end (when all arrows are eliminated)
471 -- game should generally check for arrows not shapes!
472 local anymemories, anyactivearrows = false, false
473 inplayfield(function(x, y, p)
474 anymemories = anymemories or p.memory
475 if p.arrows then for direction, arrow in ipairs(p.arrows) do
476 anyactivearrows = anyactivearrows or arrow
477 end end
478 end)
479 if game.underpill then
480 if not anymemories and not current.died then
481 update(game.solved_levels)
482 -- consider it's seen too (for levels where states are changed)
483 update(game.seen_levels)
484 game.purple = current.temppurple
485 setuplevel(game.currentlevel - 1)
486 return
488 else
489 if not anyactivearrows and not open_gifts() then
490 if playfield.name == 'small' then
491 if anymemories then
492 remember_arrows()
493 play_sound('backwards')
494 else -- level solved
495 update(game.solved_levels)
496 -- consider it's seen too (for levels where states are changed)
497 update(game.seen_levels)
498 game.purple = current.temppurple
499 if current.symbolic then
500 -- press big playfield
501 current.phase = 'play'
502 clicked(big_playfield.selectedx, big_playfield.selectedy, big_playfield)
503 else
504 -- go to next level
505 setuplevel(game.currentlevel + 1)
508 else -- big playfield
509 ending:main()
516 function open_gifts()
517 if game.underpill then return end
518 local opened = false
519 inplayfield(function(x, y, p)
520 if p.name == 'J' or p.name == 'K' then
521 playfield[x][y] = recognizeink(p.gift)
522 playfield[x][y].gift = p.gift
523 playfield[x][y].ingift = true
524 if p.name == 'K' then
525 events('devil on')
526 playfield[x][y].ingift = 'devil'
528 opened = true
530 end)
531 if opened then
532 animate_gifts()
533 --playfield.clickedx, playfield.clickedy = nil, nil
534 --necessary to preserve clicked for opening arrowgifts
536 return opened
540 function clicked(mx, my, pf)
541 -- skip death animation and fast-forward any remaining flying arrows
542 if playfield.afterdeath_phase then
543 local current_speed = settings.speed
544 settings.speed = 2
545 if not update_anim(0) then current.phase = 'play' end
546 settings.speed = current_speed
548 if current.screen ~= 'playfield' or current.phase ~= 'play' then
549 return
551 playfield = pf or small_playfield
552 -- click above playfield
553 if mx < 1 or mx > 4 or my < 0 or my > 4 then return end
554 if my == 0 then -- levels panel
555 play_sound('tone')
556 if mx == 1 then
558 current.painting or
559 level_labels[game.currentlevel] == 'Secrets'
560 then
561 if has_trinket('pencil') then paint_mode() end
562 elseif traversable(-1) then
563 setuplevel(game.currentlevel - 1)
565 elseif mx == 4 then
566 if level_labels[game.currentlevel] == 'Secrets' then
567 -- do the big click if previously solved
568 if game.solved_levels[game.currentlevel] then
569 clicked(big_playfield.selectedx, big_playfield.selectedy, big_playfield)
570 return
572 elseif current.painting then
573 -- turn off painting when not in Secrets
574 current.painting = false
575 playfield.paint_mode = false
576 elseif traversable(1) then
577 setuplevel(game.currentlevel + 1)
579 else
580 change_screen('menu_objects')
582 return
584 -- click on playfield
585 if playfield.paint_mode then
586 paint_mode(mx, my)
587 return
589 -- click after death
590 if current.died then
591 setuplevel()
592 return
594 -- clickedx, clickedy from previous turn
595 if -- arrowgift
596 playfield.clickedx and
597 playfield[playfield.clickedx][playfield.clickedy].arrowgift and
598 not (mx == playfield.clickedx and my == playfield.clickedy) and
599 not game.underpill and not playfield.remembered
600 then
601 local gift = playfield[playfield.clickedx][playfield.clickedy].gift
602 -- open arrowgift
603 animate_arrow_gift(playfield.clickedx, playfield.clickedy)
604 -- preserve gift
605 playfield[playfield.clickedx][playfield.clickedy].gift = gift
607 -- new clickedx, clickedy
608 playfield.remembered = false
609 if game.shuffled then
610 -- wise man battle
611 local oldx, oldy = playfield.clickedx, playfield.clickedy
612 local oldsx, oldsy
613 for x = 1, 4 do for y = 1, 4 do
614 if game.shuffled[x][y].x == mx and game.shuffled[x][y].y == my then
615 playfield.clickedx = x
616 playfield.clickedy = y
618 end end
619 if oldx and game.wisepuzzle then
620 -- swap shuffled tiles
621 oldsx = game.shuffled[oldx][oldy].x
622 oldsy = game.shuffled[oldx][oldy].y
623 game.shuffled[oldx][oldy].x = game.shuffled[playfield.clickedx][playfield.clickedy].x
624 game.shuffled[oldx][oldy].y = game.shuffled[playfield.clickedx][playfield.clickedy].y
625 game.shuffled[playfield.clickedx][playfield.clickedy].x = oldsx
626 game.shuffled[playfield.clickedx][playfield.clickedy].y = oldsy
627 playfield.clickedx, playfield.clickedy = nil, nil
629 else
630 -- the usual click
631 play_sound('click')
632 playfield.clickedx, playfield.clickedy = mx, my
634 playfield[mx][my + 1] and
635 playfield[mx][my + 1].kind == 'click_hint'
636 then
637 playfield[mx][my + 1] = recognizeink()
640 if playfield.clickedx and playfield[playfield.clickedx][playfield.clickedy].onclick then
641 playfield[playfield.clickedx][playfield.clickedy].onclick()
644 next_phase()
648 -- checks if point is in rectangle
649 function isin(mx, my, x, y, w, h)
650 return mx >= x and mx <= x + w and my >= y and my <= y + h
654 -- returns a random list of given length
655 function random_list(length)
656 local r = {1}
657 for a = 2, length do
658 table.insert(r, math.random(#r + 1), a)
660 return r
664 function love.mousepressed(mx, my, button)
665 if current.screen == 'playfield' then
666 small_playfield.selectedx = nil -- remove keyboard cursor
667 local x = math.floor((mx - screen.playfieldx) / screen.tilesize) + 1
668 local y = math.floor((my - screen.playfieldy) / screen.tilesize) + 1
669 clicked(x, y)
670 else
671 menu_click(mx, my)
676 function love.mousereleased(mx, my, button)
677 love.mouse.setGrabbed(false)
678 menu_click(mx, my, true)
682 function love.keypressed(k)
683 if current.screen == 'playfield' then
684 if k == 'escape' then
685 love.event.push('quit')
686 elseif k == 'left' then
687 cursor_move(-1, 0)
688 elseif k == 'right' then
689 cursor_move(1, 0)
690 elseif k == 'up' then
691 cursor_move(0, -1)
692 elseif k == 'down' then
693 cursor_move(0, 1)
694 elseif k == 'return' and small_playfield.selectedx then
695 clicked(small_playfield.selectedx, small_playfield.selectedy)
696 elseif k == 'm' then
697 change_screen('menu_main')
699 else --menu
700 if k == 'escape' or k == 'm' then
701 change_screen('playfield')
702 elseif k == 'left' then
703 menu_select(-1, 0)
704 elseif k == 'right' then
705 menu_select(1, 0)
706 elseif k == 'up' then
707 menu_select(0, -1)
708 elseif k == 'down' then
709 menu_select(0, 1)
710 elseif k == 'return' then
711 menu_press()
714 -- global keys
715 if k == 'q' then
716 love.event.push('quit')
718 if debug_keys then debug_keys(k) end
722 function cursor_move(x, y)
723 -- default cursor position
724 if not small_playfield.selectedx then
725 small_playfield.selectedx, small_playfield.selectedy = 1, 1
726 return
728 -- skip wide cursor
729 if (small_playfield.selectedy < 1) and
730 (small_playfield.selectedx + x > 1) and
731 (small_playfield.selectedx + x < 4)
732 then
733 x = 2 * x
735 -- move cursor
736 small_playfield.selectedx = (small_playfield.selectedx + x - 1) % 4 + 1
737 small_playfield.selectedy = (small_playfield.selectedy + y) % 5
742 function love.quit()
743 save(game, 'autosave')
744 save(settings, 'settings')