-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.lua
1348 lines (1162 loc) · 45.5 KB
/
main.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- main game
TileType = {
Empty = 1,
FloorEntry = 2,
FloorExit = 3,
Trap = 4,
Fall = 5,
BombItem = 6,
PageItem = 7,
Altar = 8,
StoneFloor = 9,
}
ItemType = {
Bomb = 1,
Page = 2,
Book = 3,
}
HintArrow = {
Up = 1,
UpRight = 2,
Right = 3,
DownRight = 4,
Down = 5,
DownLeft = 6,
Left = 7,
UpLeft = 8,
}
BookState = {
NotFound = 1,
FoundButLost = 2,
Holding = 3,
}
-- constants
function ISO_TILE_WIDTH()
return 32
end
function ISO_TILE_HEIGHT()
return 16
end
function MAX_TILE_LINE()
return 9
end
function MAX_CAMERA_DISTANCE_FROM_PLAYER()
return 20
end
function TOTAL_PAGE_COUNT()
return 10
end
function MAINGAME_TIME_LIMIT()
return 5 * 60 * 30 -- five minutes worth of ticks
end
function _init_main_game()
g_banner = nil
g_maingame_tick_count = 0
g_game_over_state = nil
g_player = {
book_state = BookState.NotFound,
pos = vec(0, 0),
sprite_offset = vec(-8, -14),
collider = { radius = 3 },
bomb_count = 0,
collected_pages = {},
}
g_camera_player_offset = vec(0, 0)
g_game_timer_ui = make_ui_timer(on_ui_timer_shake, MAINGAME_TIME_LIMIT())
g_maps = gen_maps(10)
move_to_level(1, TileType.FloorEntry)
g_detector = { cursor_val = 0, cursor_target = 0, next_scan = 0 }
music(0, 1000, 7)
end
function _init_game_over()
-- noop
end
function _update_main_game(input)
g_maingame_tick_count += 1
-- update our in-game accelerated timer UI
g_game_timer_ui.update(g_maingame_tick_count)
-- update the metal detector ui
update_detector(g_detector)
-- handle input blocking animation states
local block_input = false
if g_player.dig_state != nil then
-- update the dig animation
update_anim(g_player, g_player.dig_state.anim)
-- fire the on_dig_up callback exactly once when we start the 'dig up' animation
if (g_player.anim_state.a_st == 2) and g_player.dig_state.on_dig_up != nil then
g_player.dig_state.on_dig_up()
g_player.dig_state.on_dig_up = nil
end
-- check if we're done digging yet
if g_player.anim_state.loop > 0 then
-- reset our animation state to what is was before we started digging
reset_anim(g_player)
update_anim(g_player, g_player.dig_state.previous_anim)
g_player.dig_state = nil
end
block_input = true
elseif g_player.collect_item_state != nil then
-- update the collect item animation
update_anim(g_player, g_player.collect_item_state.anim)
g_player.collect_item_state.anim_timer.update()
if g_player.collect_item_state.anim_timer.done() then
g_player.collect_item_state = nil
end
block_input = true
elseif g_player_move_to_floor_state != nil then
g_player_move_to_floor_state.timer.update()
if g_player_move_to_floor_state.timer.done() then
local next_level = g_player_move_to_floor_state.next_level
local next_start_tile = g_player_move_to_floor_state.start_tile
g_player_move_to_floor_state = nil
move_to_level(next_level, next_start_tile)
end
block_input = true
elseif g_player.die_state != nil then
update_anim(g_player, g_player.die_state.anim)
g_player.die_state.respawn_timer.update()
if g_player.die_state.respawn_timer.done() then
g_player.die_state = nil
move_to_level(1, TileType.FloorEntry)
end
block_input = true
end
-- if we aren't blocking new input, process it
if not block_input then
handle_new_input(input)
end
-- update bomb states. We update these regardless of input blocks because bomb updates
-- are passive and don't depend on user input
local finished_bombs = {}
for bomb in all(g_bombs) do
bomb.update()
if bomb.done() then
add(finished_bombs, bomb)
end
for e in all(bomb.get_explosions()) do
local explosion_cells = get_cells_under_actor(g_map, e)
for cell in all(explosion_cells) do
cell.tile.visible = true
end
-- check for player-explosion collisions if the player isn't already dead
if (g_player.die_state == nil) and (circ_colliders_overlap(g_player, e)) then
kill_player(g_player, g_map)
end
end
end
-- clean up any bombs which have finished
for bomb in all(finished_bombs) do
del(g_bombs, bomb)
end
if g_maingame_tick_count >= MAINGAME_TIME_LIMIT() then
handle_game_over(false)
end
end
function collect_item(player, item_type, item_args)
player.collect_item_state = {
anim = g_anims.CollectItem,
item = item_type,
anim_timer = make_ingame_timer(45),
}
for k,v in pairs(item_args or {}) do
player.collect_item_state[k] = v
end
local item_text = nil
if item_type == ItemType.Bomb then
item_text = "bomb. \151 to use"
elseif item_type == ItemType.Page then
item_text = "page fragment"
elseif item_type == ItemType.Book then
item_text = "granddaddy's book"
else
assert(false)
end
set_banner("got: "..item_text, "item", 60)
sfx(Sfxs.GetItem)
end
function start_move_to_floor(next_level, start_tile_type, stair_sfx)
g_player_move_to_floor_state = {
timer = make_ingame_timer(15),
next_level = next_level,
start_tile = start_tile_type,
}
sfx(stair_sfx)
end
function handle_new_input(input)
move_player(input)
animate_player(input)
local is_digging = input.btn_o and input.btn_o_change
if is_digging then
g_player.dig_state = {
anim = get_dig_anim_for_player(g_player),
previous_anim = g_player.anim_state.last_anim }
-- reset the anim state so the digging animation always starts on frame 0
reset_anim(g_player)
end
local player_tile_idx = get_actor_tile_idx(g_map, g_player)
if player_tile_idx != nil then
local player_cell = g_map.cells[player_tile_idx]
-- If we've just dug up a tile, we'll set a callback so that after
-- the digging animation reveals the tile, we'll automatically
-- interact with it.
if is_digging then
local reveal_tile_callback = function()
sfx(Sfxs.Dig)
player_cell.tile.visible = true
interact_with_tile(player_cell.tile)
end
g_player.dig_state.on_dig_up = reveal_tile_callback
elseif player_cell.tile.visible then
interact_with_tile(player_cell.tile)
g_player.last_interacted_cell = player_cell
elseif player_cell.tile.has_book then
-- HACK: special case to support picking up books from unrevealed tiles
g_player.book_state = BookState.Holding
player_cell.tile.has_book = false
collect_item(g_player, ItemType.Book)
g_player.last_interacted_cell = player_cell
end
g_player.last_visited_cell = player_cell
end
-- Handle placing new bombs
local try_place_bomb = input.btn_x and input.btn_x_change
if try_place_bomb then
if g_player.bomb_count > 0 then
g_player.bomb_count -= 1
add(g_bombs, new_bomb(g_player.pos, on_explosion_start))
end
end
end
function on_explosion_start()
sfx(Sfxs.Explosion)
end
function kill_player(player, map)
local died_text = "died."
if player.book_state == BookState.Holding then
-- drop the book in a random safe cell
local safe_tile_idx = select_random_empty_tile_idx_from_map(map)
map.cells[safe_tile_idx].tile.has_book = true
g_player.book_state = BookState.FoundButLost
died_text = died_text.." dropped book."
end
player.die_state = {
anim = get_die_anim_for_player(player),
respawn_timer = make_ingame_timer(60),
}
set_banner(died_text, "death", 60)
sfx(Sfxs.Death)
end
function interact_with_tile(tile)
-- only interact with a tile the first time you step on it.
if tile == g_player.last_interacted_cell.tile then
return
end
if tile.type == TileType.FloorExit then
start_move_to_floor(g_map.level_id + 1, TileType.FloorEntry, Sfxs.DownStairs)
elseif tile.type == TileType.FloorEntry then
-- if we haven't found the book and we try to leave, warn the player
if g_player.book_state == BookState.NotFound then
set_banner("can't leave until i find it.", "warning", 90)
elseif g_player.book_state == BookState.FoundButLost then
-- if we've found the book but lost it we can freely traverse all floors... EXCEPT we can't leave
if g_map.level_id != 1 then
start_move_to_floor(g_map.level_id - 1, TileType.FloorExit, Sfxs.UpStairs)
else
set_banner("can't leave. i lost the book.", "warning", 90)
end
elseif g_player.book_state == BookState.Holding then
-- we have the book. we can traverse up any floor AND win the game by leaving the last floor
if g_map.level_id != 1 then
start_move_to_floor(g_map.level_id - 1, TileType.FloorExit, Sfxs.UpStairs)
else
handle_game_over(true)
end
else
assert(false)
end
elseif tile.type == TileType.Trap then
kill_player(g_player, g_map)
elseif tile.type == TileType.BombItem then
g_player.bomb_count += 1
collect_item(g_player, ItemType.Bomb)
-- after we've picked up the bomb, turn the cell into a hintless empty cell
tile.type = TileType.Empty
elseif tile.type == TileType.PageItem then
add(g_player.collected_pages, tile.page_frag)
collect_item(g_player, ItemType.Page, { page_frag = tile.page_frag })
-- after we've picked up the page, turn the cell into a hintless empty cell
tile.type = TileType.Empty
tile.page_frag = nil
elseif tile.has_book then
g_player.book_state = BookState.Holding
tile.has_book = false
collect_item(g_player, ItemType.Book)
end
end
function on_ui_timer_shake()
sfx(Sfxs.ClockBeep)
end
function is_player_facing_left(player)
local last_anim = player.anim_state.last_anim
return (last_anim == g_anims.IdleLeft or
last_anim == g_anims.WalkLeft or
last_anim == g_anims.IdleUpLeft or
last_anim == g_anims.WalkUpLeft or
last_anim == g_anims.IdleDownLeft or
last_anim == g_anims.WalkDownLeft)
end
function get_dig_anim_for_player(player)
if is_player_facing_left(player) then
return g_anims.DigLeft
else
return g_anims.DigRight
end
end
function get_die_anim_for_player(player)
if is_player_facing_left(player) then
return g_anims.DieLeft
else
return g_anims.DieRight
end
end
function get_centered_camera_on_player(player)
return vec(player.pos.x - 64, player.pos.y - 64)
end
function camera_follow_player(player, camera_ofs)
-- If the camera is already within the max allowed distance from the player don't move the camera
local player_camera_center_ofs = get_centered_camera_on_player(player)
local dist_squared = sqr_dist(player_camera_center_ofs, camera_ofs)
if dist_squared <= sqr(MAX_CAMERA_DISTANCE_FROM_PLAYER()) then
return
end
-- If the camera is outside of the max allowed distance, move it closer
local dir_vec = vec_sub(camera_ofs, player_camera_center_ofs)
-- get a unit vector for the direction to move the camera
local dist = sqrt(dist_squared)
dir_vec.x /= dist
dir_vec.y /= dist
-- multiply the max distance to get our max distance from player
dir_vec.x *= MAX_CAMERA_DISTANCE_FROM_PLAYER()
dir_vec.y *= MAX_CAMERA_DISTANCE_FROM_PLAYER()
new_ofs = vec_add(player.pos, dir_vec)
camera_ofs.x = new_ofs.x - 64
camera_ofs.y = new_ofs.y - 64
end
function _update_game_over(input)
g_game_timer_ui.update(g_maingame_tick_count)
if g_game_over_state.substate == "scroll_timer" then
g_game_over_state.timer_scroll += 1
g_game_timer_ui.move(0, 0.5)
if g_game_over_state.timer_scroll == 120 then
g_game_over_state.substate = "brief_blink"
g_game_over_state.final_blink = make_ingame_timer(120)
end
elseif g_game_over_state.substate == "brief_blink" then
g_game_over_state.final_blink.update()
if g_game_over_state.final_blink.done() then
g_game_over_state.substate = "display_game_over_text"
g_game_over_state.game_over_text_roll_spd = 0.8 -- chars per frame
g_game_over_state.game_over_text_frame_cnt = 0
g_game_over_state.game_over_text_final_frame_cnt = (#g_game_over_state.game_over_text) / g_game_over_state.game_over_text_roll_spd
end
elseif g_game_over_state.substate == "display_game_over_text" then
g_game_over_state.game_over_text_frame_cnt += 1
local text_finished = g_game_over_state.game_over_text_frame_cnt >= g_game_over_state.game_over_text_final_frame_cnt
if text_finished and (input.btn_x_change or input.btn_o_change) then
set_phase(GamePhase.PreGame)
end
end
end
function _draw_main_game()
cls(Colors.BLACK)
-- Set the camera view so that the world is draw relative to its position
camera_follow_player(g_player, g_camera_player_offset)
camera(g_camera_player_offset.x, g_camera_player_offset.y);
-- draw the tiles
for cell in all(g_map.cells) do
local frame_idx = get_tile_sprite_frame(cell.tile)
if frame_idx != nil then
-- DRAW A THIN BORDER
spr(128, cell.pos.x - ISO_TILE_WIDTH()/2, cell.pos.y, 4, 2, false)
spr(frame_idx, cell.pos.x - ISO_TILE_WIDTH()/2, cell.pos.y - ISO_TILE_HEIGHT()/2, 4, 2, false)
if cell.tile.visible then
-- draw the hint arrow if the empty hint cell is visible
if (cell.tile.type == TileType.Empty) and (cell.tile.hint != nil) then
draw_hint_arrow(cell.pos, cell.tile.hint)
-- If it's an unretrieved bomb cell, display an inactive bomb sprite in the middle of the cell.
-- This basically only happens if you use a bomb to reveal another bomb.
elseif cell.tile.type == TileType.BombItem then
draw_bomb_item(cell.pos)
elseif cell.tile.type == TileType.PageItem then
draw_page_item(cell.pos, cell.tile.page_frag)
elseif cell.tile.type == TileType.Altar then
-- draw the altar
spr_centered(88, cell.pos.x, cell.pos.y, 2, 1)
end
end
if cell.tile.has_book then
draw_book(cell.pos, cell.tile.type == TileType.Altar)
end
end
end
-- if the player is standing on their last visited isotile, highlight it
if circ_colliders_overlap(g_player, g_player.last_visited_cell) then
highlight_cell(g_player.last_visited_cell)
end
-- draw the player
draw_anim(g_player, sprite_pos(g_player))
-- if the player is collecting an item, draw the item above their collect animation
if g_player.collect_item_state != nil then
local item_pos = vec_sub(g_player.pos, vec(0, 16))
if g_player.collect_item_state.item == ItemType.Bomb then
draw_bomb_item(item_pos)
elseif g_player.collect_item_state.item == ItemType.Page then
draw_page_item(item_pos, g_player.collect_item_state.page_frag)
elseif g_player.collect_item_state.item == ItemType.Book then
draw_book(item_pos, false)
end
end
-- draw any active bombs
for bomb in all(g_bombs) do
bomb.draw()
end
--
-- Draw all UI unaffected by the camera
--
-- reset the camera to 0 keep the UI fixed on screen
camera(0, 0)
-- draw the level UI
print("Level: "..g_map.level_id, 0, 120, Colors.White)
-- draw any banners if set
if g_banner != nil then
draw_banner(g_banner.text, g_banner.fg_color, g_banner.bg_color)
g_banner.timer.update()
if g_banner.timer.done() then
g_banner = nil
end
end
-- draw the metal detector
draw_detector_ui(g_detector)
-- draw the bomb counter UI
draw_bomb_item(vec(4, 111))
print(":"..g_player.bomb_count, 8, 110, Colors.White)
-- draw the book UI
draw_book_ui(g_player)
-- draw the in-game timer UI
g_game_timer_ui.draw(g_maingame_tick_count)
end
function _draw_game_over()
cls(Colors.BLACK)
if g_game_over_state.substate != "display_game_over_text" then
-- Set the camera view so that the world is draw relative to its position
camera_follow_player(g_player, g_camera_player_offset)
camera(g_camera_player_offset.x, g_camera_player_offset.y);
-- draw the player frozen at the game_over state
local player_sprite_pos = sprite_pos(g_player)
draw_anim(g_player, player_sprite_pos)
-- draw the book UI
draw_book_ui(g_player)
-- reset the camera to 0 keep the UI fixed on screen
camera(0, 0)
-- draw the level UI
print("Level: "..g_map.level_id, 0, 120, Colors.White)
-- draw the in-game timer UI
g_game_timer_ui.draw(g_maingame_tick_count)
else
local rolled_text_ratio = (g_game_over_state.game_over_text_roll_spd * g_game_over_state.game_over_text_frame_cnt) / g_game_over_state.game_over_text_final_frame_cnt
draw_text_roll(g_game_over_state.game_over_text, rolled_text_ratio, 10, 10, nil, 17)
-- draw the book UI
draw_book_ui(g_player)
end
end
function update_detector(detector)
g_cursor_speed = 0.04
local do_proximity_scan = false
detector.next_scan -= 1
if detector.next_scan <= 0 then
do_proximity_scan = true
detector.next_scan = 15
end
-- if we need to do a proximity scan, calculate the
if do_proximity_scan then
local max_interference = 0
for cell in all(g_map.cells) do
local ttype = cell.tile.type
if ttype == TileType.BombItem or ttype == TileType.PageItem then
local item_dist = sqrt(sqr_dist(g_player.pos, cell.pos));
-- detector values move between 0 and 1
local item_interference = clamp(0, 1 - (item_dist/48), 1)
max_interference = max(max_interference, item_interference)
end
end
detector.cursor_target = max_interference
end
if detector.cursor_val > detector.cursor_target then
detector.cursor_val = max(detector.cursor_val - g_cursor_speed, detector.cursor_target)
else
detector.cursor_val = min(detector.cursor_val + g_cursor_speed, detector.cursor_target)
end
end
function draw_detector_ui(detector)
g_dui = { x = 2, y = 20, w = 8, h = 50 }
rect(g_dui.x, g_dui.y, g_dui.x + g_dui.w, g_dui.y + g_dui.h, Colors.White)
-- add some shake on ~30% of frames
local cursor_shake = 0
if rnd(1) > 0.7 then
cursor_shake = (rnd(2) - 1) / g_dui.h
end
-- cursor_val is a ratio between 0->1 of how far up the detector bar the cursor should be
-- invert the ratio since y values grow downwards
-- clamp between 0.05 and 0.95 so our cursor is always within the detector UI
local adj_cursor_val = clamp(0.05, 1 - detector.cursor_val + cursor_shake, 0.95)
local cursor_y = g_dui.y + (g_dui.h * adj_cursor_val)
line(g_dui.x, cursor_y, g_dui.x + (g_dui.w * .60), cursor_y)
-- draw markers on the meter to make this look more like a measuring device
g_num_markers = 5
for i=1,g_num_markers do
local marker_ofs = flr(g_dui.h/(g_num_markers+1)) * i
pset(g_dui.x+g_dui.w-1, g_dui.y + marker_ofs, Colors.White)
end
end
function draw_book_ui(player)
-- Book UI is drawn in screenspace. Temporarily reset the camera.
cam_state = save_cam_state()
camera(0, 0)
local tile_px_width = 8
for i=1,#g_player.collected_pages do
local x_ofs = i*tile_px_width
draw_page_item(vec(120-x_ofs, 120), g_player.collected_pages[i])
end
if player.book_state == BookState.Holding then
draw_book(vec(120, 120), false)
end
restore_cam_state(cam_state)
end
function center_text(rect, text)
local text_len = #text
local text_width = 4 * text_len
local text_height = 6
local text_x = rect.x + (rect.width/2) - (text_width/2)
local text_y = rect.y + (rect.height/2) - (text_height/2)
-- y + 1 to account for the extra pixel buffer below the font
return vec(text_x, text_y + 1)
end
function draw_hint_arrow(pos, hint)
local frame = nil
local flip_x = false
local flip_y = false
if hint == HintArrow.Right then
frame = 134
elseif hint == HintArrow.DownRight then
frame = 133
flip_y = true
elseif hint == HintArrow.Down then
frame = 132
flip_y = true
elseif hint == HintArrow.DownLeft then
frame = 133
flip_x = true
flip_y = true
elseif hint == HintArrow.Left then
frame = 134
flip_x = true
elseif hint == HintArrow.UpLeft then
frame = 133
flip_x = true
elseif hint == HintArrow.Up then
frame = 132
else -- hint == HintArrow.UpRight
frame = 133
end
spr_centered(frame, pos.x, pos.y, 1, 1, flip_x, flip_y)
end
function draw_bomb_item(pos)
spr_centered(74, pos.x, pos.y, 1, 1)
end
function draw_page_item(pos, sprite)
spr_centered(sprite, pos.x, pos.y, 1, 1)
end
function draw_book(pos, on_altar)
local y_ofs = 0 -- book on floor
if on_altar then
y_ofs = 4 -- book on altar
end
spr_centered(72, pos.x, pos.y - y_ofs, 1, 1)
end
function set_banner(text, banner_type, banner_time)
local fg_color, bg_color = nil, nil
if banner_type == "warning" then
fg_color, bg_color = Colors.Tan, Colors.BlueGray
elseif banner_type == "item" then
fg_color, bg_color = Colors.DarkGreen, Colors.Tan
elseif banner_type == "death" then
fg_color, bg_color = Colors.Red, Colors.Navy
end
g_banner = {
text = text,
fg_color = fg_color,
bg_color = bg_color,
timer = make_ingame_timer(banner_time),
}
end
function draw_banner(text, fg_color, bg_color)
-- Banners are drawn in screenspace. Temporarily reset the camera.
cam_state = save_cam_state()
camera(0, 0)
local bg_rect = { x = 0, y = 98, width = 128, height = 10 }
rectfill(bg_rect.x, bg_rect.y, bg_rect.x + bg_rect.width, bg_rect.y + bg_rect.height, bg_color)
rect(bg_rect.x + 1, bg_rect.y + 1, bg_rect.x + bg_rect.width - 2, bg_rect.y + bg_rect.height - 1, fg_color)
local text_pos = center_text(bg_rect, text)
print(text, text_pos.x, text_pos.y, fg_color)
restore_cam_state(cam_state)
end
function gen_maps(num_maps)
local maps = {}
for i=1,(num_maps - 1) do
-- the levels increase in size from 2 -> MAX_TILE_LINE()
local map_size = min((i + 1), MAX_TILE_LINE())
add(maps, gen_empty_level(i, map_size))
end
-- Place the start and end tile on each map FIRST (we have to have these tiles. The rest aren't guaranteed to be
-- present on every layer)
for map in all(maps) do
-- set the start cell on this map.
local player_start_iso_idx = select_random_empty_tile_idx_from_map(map)
map.cells[player_start_iso_idx].tile = make_tile(true, TileType.FloorEntry)
-- set the finish cell on this map.
map.finish_cell_idx = select_random_empty_tile_idx_from_map(map)
map.cells[map.finish_cell_idx].tile = make_tile(false, TileType.FloorExit)
end
-- set the trap cells in each map
-- On each map, %30 of the tiles rounded down have traps
for map in all(maps) do
local trap_cell_cnt = flr(map.iso_width * map.iso_width * 0.30)
local trap_cells = select_random_empty_tiles({map}, trap_cell_cnt)
for trap_cell in all(trap_cells) do
map.cells[trap_cell.idx].tile = make_tile(false, TileType.Trap)
end
end
-- set the bomb item cells
-- There are a total of 10 bombs across the whole game
local bomb_cell_cnt = 10
local bomb_cells = select_random_empty_tiles(maps, bomb_cell_cnt)
for bomb_cell in all(bomb_cells) do
bomb_cell.map.cells[bomb_cell.idx].tile = make_tile(false, TileType.BombItem)
end
-- set the page item cells
local page_fragments = { 172, 173, 188, 189 }
local next_page_frag_idx = 0
local page_cell_cnt = TOTAL_PAGE_COUNT()
local page_cells = select_random_empty_tiles(maps, page_cell_cnt)
for page_cell in all(page_cells) do
page_cell.map.cells[page_cell.idx].tile = make_tile(false, TileType.PageItem)
page_cell.map.cells[page_cell.idx].tile.page_frag = page_fragments[next_page_frag_idx + 1]
next_page_frag_idx = ((next_page_frag_idx + 1) % #page_fragments)
end
-- Now that all interesting cells have been placed, fill in the remaining empty cells with hint arrows
for map in all(maps) do
local iso_finish_cell_pos = map.cells[map.finish_cell_idx].pos
for cell in all(map.cells) do
if cell.tile.type == TileType.Empty then
local dir_vec = vec_sub(iso_finish_cell_pos, cell.pos)
local unit_circle_ratio = atan2(dir_vec.x, -1 * dir_vec.y)
local angle_to_finish_point_deg = unit_circle_ratio * 360
local hint = nil
if angle_to_finish_point_deg < 22.5 then
hint = HintArrow.Right
elseif angle_to_finish_point_deg < 67.5 then
hint = HintArrow.DownRight
elseif angle_to_finish_point_deg < 112.5 then
hint = HintArrow.Down
elseif angle_to_finish_point_deg < 157.5 then
hint = HintArrow.DownLeft
elseif angle_to_finish_point_deg < 202.5 then
hint = HintArrow.Left
elseif angle_to_finish_point_deg < 247.5 then
hint = HintArrow.UpLeft
elseif angle_to_finish_point_deg < 292.5 then
hint = HintArrow.Up
elseif angle_to_finish_point_deg < 337.5 then
hint = HintArrow.UpRight
else
hint = HintArrow.Right
end
cell.tile.hint = hint
end
end
end
-- Create the last level which has a unique structure:
-- It's just the single goal item in the middle of an empty layer.
local final_map = gen_empty_level(num_maps, MAX_TILE_LINE())
-- set the altar point in middle
local altar_cell_idx = flr(#final_map.cells / 2) + 1
final_map.cells[altar_cell_idx].tile = make_tile(true, TileType.Altar)
final_map.cells[altar_cell_idx].tile.has_book = true
-- set the final level's entry
local final_map_start_iso_idx = select_random_empty_tile_idx_from_map(final_map)
final_map.cells[final_map_start_iso_idx].tile = make_tile(true, TileType.FloorEntry)
-- turn every other cell into a stone floor cell
for cell in all(final_map.cells) do
if cell.tile.type == TileType.Empty then
cell.tile = make_tile(true, TileType.StoneFloor)
end
end
add(maps, final_map)
return maps
end
function gen_empty_level(level_id, map_iso_width)
-- create a new map
local next_map = {}
next_map.level_id = level_id
next_map.iso_width = map_iso_width
next_map.cells = {}
-- wrap the map with a ring of invisible fall tiles. adds 2 rows to each side
local visible_row_cnt = next_map.iso_width * 2 - 1
local row_cnt = visible_row_cnt + 4
-- initialize all the border cells as fall tiles and all the interior cells as empty
local idx = 1
for row = 1,row_cnt do
local midpoint_row = flr((row_cnt + 1) / 2)
local row_offset = (row - midpoint_row) * ISO_TILE_HEIGHT() / 2
local col_cnt = nil
if row <= midpoint_row then
col_cnt = row
else
col_cnt = 2 * midpoint_row - row
end
for col = 1,col_cnt do
local tile = nil
local is_edge_tile = (col == 1) or (col == col_cnt)
if is_edge_tile then
tile = make_tile(true, TileType.Fall)
else
tile = make_tile(false, TileType.Empty)
end
local col_offset =
-1 * ((col_cnt/2) * ISO_TILE_WIDTH()) -- shift half the board width to the left
+ (ISO_TILE_WIDTH()/2) -- offset by half a tile width to move back into the center of the first tile
+ ((col - 1)*ISO_TILE_WIDTH()) -- add a tile width for each subsequent tile
local cell = {
idx = idx,
tile = tile,
pos = vec(
SCREEN_SIZE()/2 + col_offset,
SCREEN_SIZE()/2 + row_offset),
collider = { radius = 4 }
}
add(next_map.cells, cell)
idx += 1
end
end
return next_map
end
function make_tile(visible, tile_type)
return { visible = visible, type = tile_type }
end
function select_random_empty_tile_idx_from_map(map)
return select_random_empty_tiles({map}, 1)[1].idx
end
function select_random_empty_tiles(maps, select_count)
local empty_cell_cnt = 0
local empty_cells = {}
for map in all(maps) do
for cell in all(map.cells) do
if cell.tile.type == TileType.Empty then
add(empty_cells, { map = map, idx = cell.idx })
empty_cell_cnt += 1
end
end
end
local selected_cells = {}
for i=1,select_count do
local next_selected_empty_cell_idx = rnd_incrange(1, empty_cell_cnt)
-- take the selected cell and update our collection so we have one less cell to select from
local next_selected_empty_cell = empty_cells[next_selected_empty_cell_idx]
empty_cells[next_selected_empty_cell_idx] = empty_cells[empty_cell_cnt]
empty_cells[empty_cell_cnt] = nil
add(selected_cells, next_selected_empty_cell)
end
return selected_cells
end
function get_tile_sprite_frame(tile)
if tile.visible then
local ttype = tile.type
if ttype == TileType.Empty then
return 160
elseif ttype == TileType.FloorEntry then
return 108
elseif ttype == TileType.FloorExit then
return 104
elseif ttype == TileType.Trap then
return 76
elseif ttype == TileType.Fall then
return nil -- return nil so we don't draw any sprites
elseif ttype == TileType.BombItem then
return 160
elseif ttype == TileType.PageItem then
return 160
elseif ttype == TileType.Altar then
return 140
elseif ttype == TileType.StoneFloor then
return 136
else
return nil
end
else
-- tiles on the bottom floor are never be covered
assert(ttype != TileType.Altar and ttype != TileType.StoneFloor)
return 96
end
end
function move_to_level(next_level, start_tile_type)
-- update the current map
g_map = g_maps[next_level]
-- place the player on the center of the iso tile
local player_start_cell = nil
for cell in all(g_map.cells) do
if cell.tile.type == start_tile_type then
player_start_cell = cell
break
end
end
assert(player_start_cell != nil)
g_player.pos = vec_copy(player_start_cell.pos)
g_player.last_visited_cell = player_start_cell
g_player.last_interacted_cell = player_start_cell
-- place the camera on top of the player
g_camera_player_offset = get_centered_camera_on_player(g_player)
-- reset the player's animation
update_anim(g_player, g_anims.IdleDown)
-- clear away any bombs on the board
g_bombs = {}
end
function move_player(input)
-- when traveling diagnonally, multiply by the factor sqrt(0.5) to avoid traveling further by going diagonally
local sqrt_half = 0.70710678118 -- sqrt(0.5); hardcode to avoid doing an expensive squareroot every frame
local dx = 0
local dy = 0
if input.btn_left then
if input.btn_up then
dx = -2 * sqrt_half
dy = -1 * sqrt_half
elseif input.btn_down then
dx = -2 * sqrt_half
dy = sqrt_half
else
dx = -2
dy = 0
end
elseif input.btn_right then
if input.btn_up then
dx = 2 * sqrt_half
dy = -1 * sqrt_half