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/>.
29 require('rainbowring')
31 require('pocketexplosions')
36 -- load optional debug things
37 local d
= package
.loaders
[2]('debug_things')
38 if type(d
) == 'function' then d() end
43 -- this table will be saved when quitting or saving game
50 searched_corpses
= {},
57 secrets_arrows_used
= {},
60 'pencil', 'pocketexplosions', 'frame',
61 'advice_yellow', 'advice_genie', 'advice_pill',
62 'advice_purple', 'advice_map', 'advice_boxes',
68 if current
and current
.music
then
70 current
.music
:release()
73 -- this table is reset on start or load
82 screen
= 'playfield', -- 'playfield', 'menu_objects', 'story_beginning' ...
83 phase
= 'play', -- see next_phase()
88 small_playfield
= {{}, {}, {}, {}, name
= 'small'}
89 big_playfield
= {{}, {}, {}, {}, name
= 'big'}
90 setup_big_playfield() -- unsets game.purple and current.temppurple
91 playfield
= small_playfield
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)
113 {1, 0}, {1, -1}, {0, -1}, {-1, -1},
114 {-1, 0}, {-1, 1}, {0, 1}, {1, 1}
116 math
.randomseed(os
.time())
127 -- load_game includes init() and setuplevel()
128 load_game('autosave')
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
143 if p
.kind
== 'memory' then -- remember
144 playfield
[playfield
.clickedx
][playfield
.clickedy
] = p
.memory
145 playfield
.remembered
= true
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
]
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
167 newtile
.color
= images
[p
.name
].color
168 newtile
.shape
= p
.shape
169 newtile
.gift
= p
.gift
170 newtile
.arrowgift
= p
.arrowgift
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
180 local discard
, distance
, pf
= check_direction(playfield
.clickedx
, playfield
.clickedy
, directions
[dir
][1], directions
[dir
][2], playfield
.clickedx
, playfield
.clickedy
)
182 if pf
and pf
.kind
== 'yellow' then
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
)
217 explode_arrow(x
, y
, direction
, distance
, true) -- kill=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
227 explode_arrow(x
, y
, direction
, distance
, false)
228 game
.wisepuzzle
= true
238 function influence_and_rotate()
239 -- devil influence on yellow arrows
242 local yellow_arrows
= {}
243 inplayfield(function(x
, y
, p
)
244 if p
.kind
== 'yellow' 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
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
})
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
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
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
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
289 -- wake arrows but preserve redirection
290 -- (only relevant for wise man battle)
291 playfield
[x
][y
] = recognizeink(p
.sleeping_name
)
297 inplayfield(function(x
, y
, p
)
298 if p
.kind
== 'red' or p
.kind
== 'loadred' then rotate_arrows(x
, y
) end
303 function next_phase()
304 local nextphases
= {'rotate', 'forget', 'die', 'level', 'play', 'explode'}
305 setmetatable(nextphases
, {__index
= function (table, key
)
307 if rawget(table, a
) == key
then return table[a
+ 1] or table[1] end
311 current
.phase
= nextphases
[current
.phase
] or 'play' -- nextphases['opengifts'] == nil , etc.
312 if current
.phase
== 'explode' then -- 1. explode clicked arrow
314 elseif current
.phase
== 'rotate' then -- 2.
315 influence_and_rotate()
316 elseif current
.phase
== 'forget' then -- 3. pass to memory if underpill
318 elseif current
.phase
== 'die' then -- 4. check for player death
320 elseif current
.phase
== 'level' then -- 5. check for level end
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
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()
359 screen
.pfquads
[x
] = {}
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
376 love
.graphics
.setCanvas()
380 function love
.update(dt
)
381 if current
.screen
~= 'playfield' then return end
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 })
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
])
409 if current
.screen
== 'playfield' then
418 function check_direction(x
, y
, dx
, dy
, targetx
, targety
)
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
432 function check_level()
433 local function update(list
)
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()
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
451 setuplevel(level_labels
['Remorse'])
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])
461 -- afterclick: get trinket, search corpse
464 playfield
[playfield
.clickedx
][playfield
.clickedy
].afterclick
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
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)
489 if not anyactivearrows
and not open_gifts() then
490 if playfield
.name
== 'small' then
493 play_sound('backwards')
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
)
505 setuplevel(game
.currentlevel
+ 1)
508 else -- big playfield
516 function open_gifts()
517 if game
.underpill
then return end
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
526 playfield
[x
][y
].ingift
= 'devil'
533 --playfield.clickedx, playfield.clickedy = nil, nil
534 --necessary to preserve clicked for opening arrowgifts
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
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
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
559 level_labels
[game
.currentlevel
] == 'Secrets'
561 if has_trinket('pencil') then paint_mode() end
562 elseif traversable(-1) then
563 setuplevel(game
.currentlevel
- 1)
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
)
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)
580 change_screen('menu_objects')
584 -- click on playfield
585 if playfield
.paint_mode
then
594 -- clickedx, clickedy from previous turn
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
601 local gift
= playfield
[playfield
.clickedx
][playfield
.clickedy
].gift
603 animate_arrow_gift(playfield
.clickedx
, playfield
.clickedy
)
605 playfield
[playfield
.clickedx
][playfield
.clickedy
].gift
= gift
607 -- new clickedx, clickedy
608 playfield
.remembered
= false
609 if game
.shuffled
then
611 local oldx
, oldy
= playfield
.clickedx
, playfield
.clickedy
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
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
632 playfield
.clickedx
, playfield
.clickedy
= mx
, my
634 playfield
[mx
][my
+ 1] and
635 playfield
[mx
][my
+ 1].kind
== 'click_hint'
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()
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
)
658 table.insert(r
, math
.random(#r
+ 1), a
)
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
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
688 elseif k
== 'right' then
690 elseif k
== 'up' then
692 elseif k
== 'down' then
694 elseif k
== 'return' and small_playfield
.selectedx
then
695 clicked(small_playfield
.selectedx
, small_playfield
.selectedy
)
697 change_screen('menu_main')
700 if k
== 'escape' or k
== 'm' then
701 change_screen('playfield')
702 elseif k
== 'left' then
704 elseif k
== 'right' then
706 elseif k
== 'up' then
708 elseif k
== 'down' then
710 elseif k
== 'return' 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
729 if (small_playfield
.selectedy
< 1) and
730 (small_playfield
.selectedx
+ x
> 1) and
731 (small_playfield
.selectedx
+ x
< 4)
736 small_playfield
.selectedx
= (small_playfield
.selectedx
+ x
- 1) % 4 + 1
737 small_playfield
.selectedy
= (small_playfield
.selectedy
+ y
) % 5
743 save(game
, 'autosave')
744 save(settings
, 'settings')