-
Notifications
You must be signed in to change notification settings - Fork 0
/
level_sequence.lua
1297 lines (1184 loc) · 49.9 KB
/
level_sequence.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
local custom_levels = require("CustomLevels/custom_levels")
local BORDER_THEME, GROWABLE_SPAWN_TYPE = custom_levels.BORDER_THEME, custom_levels.GROWABLE_SPAWN_TYPE
local button_prompts = require("ButtonPrompts/button_prompts")
local level_sequence = {}
local sequence_state = {
levels = {},
-- Stores the desired levels if changed while not in the camp. Will set levels with
-- these upon entering camp.
buffered_levels = {},
-- Whether each level acts as a checkpoint. If false, the run will reset on the first level
-- upon each death/reset.
keep_progress = true,
-- If set, these types of procedural spawns will be allowed to spawn in. See custom_levels.ALLOW_SPAWN_TYPE.
allowed_spawn_types = nil,
-- If set, the selected level will be next in sequence. The sequence will continue from that level after
-- completion.
force_next_level = nil,
-- If true, the win state will be reached when entering the next door, even if it is not the final level.
force_win = false,
-- If true, the fix for custom theme transition tiles will be disabled, and there may be edge decorations
-- between tiles for levels of the same theme.
disable_transition_decoration_fix = false,
}
--------------------------------------
---- CALLBACKS
--------------------------------------
local sequence_callbacks = {
-- Called during level gen just before unloading the previous level.
on_level_will_unload = nil,
-- Called during level gen just before loading the current level.
on_level_will_load = nil,
-- Called during post level generation, after the level state has been configured.
on_post_level_generation = nil,
-- Called when resetting the run if keep progress is not enabled.
on_reset_run = nil,
-- Called when resetting and going back to the last checkpoint.
on_reset_to_checkpoint = nil,
-- Called in the transition after a level has been completed.
on_completed_level = nil,
-- Called in the transition after the last level has been completed.
on_win = nil,
-- Called in the base camp when the initial level is updated.
on_prepare_initial_level = nil,
-- Called when a level starts.
on_level_start = nil,
}
-- Set the callback that will be called just before unloading a level.
--
-- Callback signature:
-- level: Level that will be unloaded
level_sequence.set_on_level_will_unload = function(callback)
sequence_callbacks.on_level_will_unload = callback
end
-- Set the callback that will be called just before loading a level.
--
-- Callback signature:
-- level: Level that will be loaded
level_sequence.set_on_level_will_load = function(callback)
sequence_callbacks.on_level_will_load = callback
end
-- Set the callback that will be called on POST_LEVEL_GENERATION just after the level and
-- doors have been configured.
--
-- Callback signature:
-- level: Level that is currently loaded.
level_sequence.set_on_post_level_generation = function(on_post_level_generation)
sequence_callbacks.on_post_level_generation = on_post_level_generation
end
-- Set the callback that will be called when the run is reset back to the first level.
-- This callback will never be called if keep_progress is true.
level_sequence.set_on_reset_run = function(on_reset_run)
sequence_callbacks.on_reset_run = on_reset_run
end
-- Called when resetting and going back to the last checkpoint.
--
-- Callback signature:
-- checkpoint_level: Level that has been configured as a checkpoint and is being loaded.
-- previous_level: Level that was being played before this reset.
level_sequence.set_on_reset_to_checkpoint = function(on_reset_to_checkpoint)
sequence_callbacks.on_reset_to_checkpoint = on_reset_to_checkpoint
end
-- Set the callback that will be called when a level is completed.
--
-- Callback signature:
-- completed_level: The level that was just completed.
-- next_level: The level that will be loaded next.
level_sequence.set_on_completed_level = function(on_completed_level)
sequence_callbacks.on_completed_level = on_completed_level
end
-- Set the callback that will be called when the final level is completed.
--
-- Callback signature:
-- attempts: Number of attempts that it took to complete the sequence. Each reset/game exit counts
-- towards the attempt counter.
-- time: Total amount of time it took to complete all levels.
level_sequence.set_on_win = function(on_win)
sequence_callbacks.on_win = on_win
end
-- Set the callback that will be called in the camp to change the initial level when
-- walking near a shortcut door or the continue run door.
--
-- Callback signature:
-- level: Initial level that will be loaded when going through an entrance door.
-- continuing_run: True if going through the continue door. Otherwise, false.
level_sequence.set_on_prepare_initial_level = function(on_prepare_initial_level)
sequence_callbacks.on_prepare_initial_level = on_prepare_initial_level
end
-- Set the callback that will be called each time a level starts. This includes both the first
-- time the level is encountered and on every reset.
--
-- Callback signature:
-- level: The level that is being started.
level_sequence.set_on_level_start = function(on_level_start)
sequence_callbacks.on_level_start = on_level_start
end
--------------------------------------
---- /CALLBACKS
--------------------------------------
--------------------------------------
---- RUN STATE
--------------------------------------
local run_state = {
initial_level = nil,
previous_level = nil,
current_level = nil,
checkpoint_level = nil,
attempts = 0,
total_time = 0,
run_started = false,
}
-- Gets the state of the current run that is in progress.
--
-- Return:
-- initial_level: Level the run started on.
-- current_level: Level the player is currently on.
-- attempts: Number of attempts the player currently has in the run. A new attempt is added
-- when starting or continuing a run from the base camp, or on a reset.
-- total_time: The total amount of time the player has spent in the run.
level_sequence.get_run_state = function()
return {
initial_level = run_state.initial_level,
current_level = run_state.current_level,
checkpoint_level = run_state.checkpoint_level,
attempts = run_state.attempts,
total_time = state.time_total,
}
end
-- Whether a run is currently in progress. If false, we are probably in the base camp or the main
-- menu.
--
-- Return: Whether a run is in progress.
level_sequence.run_in_progress = function()
return run_state.run_started
end
-- Set whether or not to consider each level as a checkpoint. If set to false, will reset the run
-- from the initial_level on resets.
--
-- keep_progress: Whether or not to keep progress on resets.
level_sequence.set_keep_progress = function(keep_progress)
sequence_state.keep_progress = keep_progress
end
-- Set a level that the player should be reset to upon death instead of the current level (if keep progress
-- is on) or the first level (keep progress not on).
--
-- Set to `nil` to clear and return to the default behavior.
--
-- level: level to reset to on death/instant restart.
function level_sequence.set_checkpoint_level(level)
run_state.checkpoint_level = level
end
-- Compares two levels to see if they are the same level. Compares identifiers if the objects are not
-- identical, so that two levels with the same identifier are considered to be equal.
--
-- level_1: First of two levels to compare.
-- level_2: Second of two levels to compare.
-- Return: True if the two levels are considered equal, otherwise false.
local function equal_levels(level_1, level_2)
if not level_1 or not level_2 then return end
return level_1 == level_2 or (level_1.identifier ~= nil and level_1.identifier == level_2.identifier)
end
-- Whether a shortcut was taken to get into the current run.
level_sequence.took_shortcut = function()
return run_state.initial_level and #sequence_state.levels > 0 and not equal_levels(run_state.initial_level, sequence_state.levels[1])
end
-- Attempts to get the index of a level within the current levels. Returns nil if no level is
-- passed in or if the level cannot be found in the current levels.
--
-- level: Level to index.
-- Return: Index of the level in the levels list.
local function index_of_level(level)
if not level then return nil end
for index, level_at in pairs(sequence_state.levels) do
if equal_levels(level, level_at) then
return index
end
end
return nil
end
level_sequence.index_of_level = index_of_level
-- Attempts to get the next level in the levels list after the input level.
--
-- level: Level to find the next level of. If omitted, uses the current_level in the run_state.
-- Return: Next level in the levels list.
local function next_level(level)
if sequence_state.force_next_level then
return sequence_state.force_next_level
end
level = level or run_state.current_level
local index = index_of_level(level)
if not index then return nil end
return sequence_state.levels[index+1]
end
--------------------------------------
---- /RUN STATE
--------------------------------------
--------------------------------------
---- THEMES
--------------------------------------
local function theme_for_co_subtheme(co_subtheme)
if co_subtheme == COSUBTHEME.DWELLING then
return THEME.DWELLING
elseif co_subtheme == COSUBTHEME.JUNGLE then
return THEME.JUNGLE
elseif co_subtheme == COSUBTHEME.VOLCANA then
return THEME.VOLCANA
elseif co_subtheme == COSUBTHEME.TIDE_POOL then
return THEME.TIDE_POOL
elseif co_subtheme == COSUBTHEME.TEMPLE then
return THEME.TEMPLE
elseif co_subtheme == COSUBTHEME.ICE_CAVES then
return THEME.ICE_CAVES
elseif co_subtheme == COSUBTHEME.NEO_BABYLON then
return THEME.NEO_BABYLON
elseif co_subtheme == COSUBTHEME.SUNKEN_CITY then
return THEME.SUNKEN_CITY
end
end
local function theme_for_level(level)
if not level or not level.theme then return THEME.DWELLING end
return level.theme
end
local function floor_theme_for_level(level)
if not level then return THEME.DWELLING end
if level.floor_theme then return level.floor_theme end
if level.theme then return level.theme end
return THEME.DWELLING
end
local function background_theme_for_level(level)
if not level then return THEME.DWELLING end
if level.background_theme then return level.background_theme end
if level.theme then return level.theme end
return THEME.DWELLING
end
local function co_subtheme_for_level(level)
if not level or not level.co_subtheme then return COSUBTHEME.RESET end
return level.co_subtheme
end
local function subtheme_for_level(level)
if not level then return nil end
if level.subtheme then return level.subtheme end
if level.co_subtheme then return theme_for_co_subtheme(level.co_subtheme) end
return nil
end
-- Gets the level that doors will lead to for a particular theme.
--
-- theme: Theme that the door leads to.
-- Return: Level number that the door will be set to.
local function level_for_theme(theme)
-- We return 5 to reduce the chances of conflict with special rooms such as black market,
-- challenges, Vlad's, and also with setrooms in stages such as 1-4.
return 5
end
-- Gets the level that doors will lead to for a particular level.
--
-- level: Level that the door leads to.
-- Return: Level number that the door will be set to.
local function level_for_level(level)
return level_for_theme(theme_for_level(level))
end
-- Gets the world that doors will lead to for a particular theme.
--
-- theme: Theme that the door leads to.
-- Return: World number that the door will be set to.
local function world_for_theme(theme)
if theme == THEME.DWELLING then
return 1
elseif theme == THEME.VOLCANA then
return 2
elseif theme == THEME.JUNGLE then
return 2
elseif theme == THEME.OLMEC then
return 3
elseif theme == THEME.TIDE_POOL then
return 4
elseif theme == THEME.TEMPLE then
return 4
elseif theme == THEME.ICE_CAVES then
return 5
elseif theme == THEME.NEO_BABYLON then
return 6
elseif theme == THEME.SUNKEN_CITY then
return 7
elseif theme == THEME.CITY_OF_GOLD then
return 4
elseif theme == THEME.DUAT then
return 4
elseif theme == THEME.ABZU then
return 4
elseif theme == THEME.TIAMAT then
return 6
elseif theme == THEME.EGGPLANT_WORLD then
return 7
elseif theme == THEME.HUNDUN then
return 7
elseif theme == THEME.BASE_CAMP then
return 1
elseif theme == THEME.ARENA then
return 1
elseif theme == THEME.COSMIC_OCEAN then
return 8
end
return 1
end
-- Gets the world that doors will lead to for a particular level.
--
-- level: Level that the door leads to.
-- Return: World number that the door will be set to.
local function world_for_level(level)
return world_for_theme(theme_for_level(level))
end
-- Gets the texture that should be used to texture doors for a particular theme.
--
-- theme: Theme that the door leads to.
-- co_subtheme: Theme that the door leads to within the cosmic ocean.
-- Return: Texture to use for doors leading to the theme.
local function texture_for_theme(theme, subtheme)
if theme == THEME.DWELLING then
return TEXTURE.DATA_TEXTURES_FLOOR_CAVE_2
elseif theme == THEME.VOLCANA then
return TEXTURE.DATA_TEXTURES_FLOOR_VOLCANO_2
elseif theme == THEME.JUNGLE then
return TEXTURE.DATA_TEXTURES_FLOOR_JUNGLE_1
elseif theme == THEME.OLMEC then
return TEXTURE.DATA_TEXTURES_DECO_JUNGLE_2
elseif theme == THEME.TIDE_POOL then
return TEXTURE.DATA_TEXTURES_FLOOR_TIDEPOOL_3
elseif theme == THEME.TEMPLE then
return TEXTURE.DATA_TEXTURES_FLOOR_TEMPLE_1
elseif theme == THEME.ICE_CAVES then
return TEXTURE.DATA_TEXTURES_FLOOR_ICE_1
elseif theme == THEME.NEO_BABYLON then
return TEXTURE.DATA_TEXTURES_FLOOR_BABYLON_1
elseif theme == THEME.SUNKEN_CITY then
return TEXTURE.DATA_TEXTURES_FLOOR_SUNKEN_3
elseif theme == THEME.CITY_OF_GOLD then
return TEXTURE.DATA_TEXTURES_FLOOR_TEMPLE_4
elseif theme == THEME.DUAT then
return TEXTURE.DATA_TEXTURES_FLOOR_TEMPLE_1
elseif theme == THEME.ABZU then
return TEXTURE.DATA_TEXTURES_FLOOR_TIDEPOOL_3
elseif theme == THEME.TIAMAT then
return TEXTURE.DATA_TEXTURES_FLOOR_TIDEPOOL_3
elseif theme == THEME.EGGPLANT_WORLD then
return TEXTURE.DATA_TEXTURES_FLOOR_EGGPLANT_2
elseif theme == THEME.HUNDUN then
return TEXTURE.DATA_TEXTURES_FLOOR_SUNKEN_3
elseif theme == THEME.BASE_CAMP then
return TEXTURE.DATA_TEXTURES_FLOOR_CAVE_2
elseif theme == THEME.ARENA then
return TEXTURE.DATA_TEXTURES_FLOOR_CAVE_2
elseif theme == THEME.COSMIC_OCEAN then
if subtheme == THEME.COSMIC_OCEAN then
return TEXTURE.DATA_TEXTURES_FLOOR_CAVE_2
end
return texture_for_theme(subtheme)
end
return TEXTURE.DATA_TEXTURES_FLOOR_CAVE_2
end
-- Gets the texture that should be used to texture floors for a particular theme.
--
-- theme: Theme for floor texture.
-- co_subtheme: Subtheme if theme is cosmic ocean.
-- Return: Texture to use for floors in the theme.
local function floor_texture_for_theme(theme, subtheme)
if theme == THEME.DWELLING or theme == THEME.ARENA then
return TEXTURE.DATA_TEXTURES_FLOOR_CAVE_0
elseif theme == THEME.VOLCANA then
return TEXTURE.DATA_TEXTURES_FLOOR_VOLCANO_0
elseif theme == THEME.JUNGLE or theme == THEME.OLMEC then
return TEXTURE.DATA_TEXTURES_FLOOR_JUNGLE_0
elseif theme == THEME.TIDE_POOL or theme == THEME.ABZU or theme == THEME.TIAMAT then
return TEXTURE.DATA_TEXTURES_FLOOR_TIDEPOOL_0
elseif theme == THEME.TEMPLE or theme == THEME.CITY_OF_GOLD or theme == THEME.DUAT then
return TEXTURE.DATA_TEXTURES_FLOOR_TEMPLE_0
elseif theme == THEME.ICE_CAVES then
return TEXTURE.DATA_TEXTURES_FLOOR_ICE_0
elseif theme == THEME.NEO_BABYLON then
return TEXTURE.DATA_TEXTURES_FLOOR_BABYLON_0
elseif theme == THEME.SUNKEN_CITY or theme == THEME.HUNDUN then
return TEXTURE.DATA_TEXTURES_FLOOR_SUNKEN_0
elseif theme == THEME.EGGPLANT_WORLD then
return TEXTURE.DATA_TEXTURES_FLOOR_EGGPLANT_0
elseif theme == THEME.BASE_CAMP then
return TEXTURE.DATA_TEXTURES_FLOOR_SURFACE_0
elseif theme == THEME.COSMIC_OCEAN then
if subtheme == THEME.COSMIC_OCEAN then
return TEXTURE.DATA_TEXTURES_FLOOR_CAVE_0
end
return texture_for_theme(subtheme)
end
return TEXTURE.DATA_TEXTURES_FLOOR_CAVE_0
end
local function background_texture_for_theme(theme, subtheme)
if theme == THEME.DWELLING or theme == THEME.ARENA then
return TEXTURE.DATA_TEXTURES_BG_CAVE_0
elseif theme == THEME.VOLCANA then
return TEXTURE.DATA_TEXTURES_BG_VOLCANO_0
elseif theme == THEME.JUNGLE then
return TEXTURE.DATA_TEXTURES_BG_JUNGLE_0
elseif theme == THEME.OLMEC then
return TEXTURE.DATA_TEXTURES_BG_STONE_0
elseif theme == THEME.TIDE_POOL or theme == THEME.ABZU or theme == THEME.TIAMAT then
return TEXTURE.DATA_TEXTURES_BG_TIDEPOOL_0
elseif theme == THEME.TEMPLE then
return TEXTURE.DATA_TEXTURES_BG_TEMPLE_0
elseif theme == THEME.CITY_OF_GOLD then
return TEXTURE.DATA_TEXTURES_BG_GOLD_0
elseif theme == THEME.DUAT then
return TEXTURE.DATA_TEXTURES_BG_DUAT_0
elseif theme == THEME.ICE_CAVES then
return TEXTURE.DATA_TEXTURES_BG_ICE_0
elseif theme == THEME.NEO_BABYLON then
return TEXTURE.DATA_TEXTURES_BG_BABYLON_0
elseif theme == THEME.SUNKEN_CITY or theme == THEME.HUNDUN then
return TEXTURE.DATA_TEXTURES_BG_SUNKEN_0
elseif theme == THEME.EGGPLANT_WORLD then
return TEXTURE.DATA_TEXTURES_BG_EGGPLANT_0
elseif theme == THEME.BASE_CAMP then
return TEXTURE.DATA_TEXTURES_BG_CAVE_0
elseif theme == THEME.COSMIC_OCEAN then
if subtheme == THEME.COSMIC_OCEAN then
return TEXTURE.DATA_TEXTURES_BG_CAVE_0
end
return texture_for_theme(subtheme)
end
return TEXTURE.DATA_TEXTURES_BG_CAVE_0
end
-- Gets the texture that should be used to texture doors for a particular level.
--
-- level: Level that the door leads to.
-- Return: Texture to use for doors leading to the level.
local function texture_for_level(level)
return texture_for_theme(theme_for_level(level), subtheme_for_level(level))
end
-- Gets the texture that should be used to texture floors for a particular level.
--
-- level: Level to texture.
-- Return: Texture to use for floors in the level.
local function floor_texture_for_level(level)
return floor_texture_for_theme(floor_theme_for_level(level))
end
-- Gets the texture that should be used to texture backgrounds for a particular level.
--
-- level: Level to texture.
-- Return: Texture to use for backgrounds in the level.
local function background_texture_for_level(level)
return background_texture_for_theme(background_theme_for_level(level))
end
--------------------------------------
---- /THEMES
--------------------------------------
--------------------------------------
---- LEVEL GENERATION
--------------------------------------
local loaded_level = nil
local has_reset = false
-- Load a level. Loads the tile codes and callbacks of the level, then uses custom_levels to load
-- the level file and replace any existing level files.
--
-- level: Level to load.
-- ctx: Context to load the level into.
local function load_level(level, ctx)
local theme_properties
if level then
theme_properties = {
theme = level.theme,
width = level.width,
height = level.height,
subtheme = level.subtheme or theme_for_co_subtheme(level.co_subtheme),
border = level.border_type or level.border,
border_theme = level.border_theme,
border_entity_theme = level.border_entity_theme,
dont_spawn_effects = level.dont_spawn_effects,
dont_init = level.dont_init,
dont_loop = level.dont_loop,
dont_spawn_growables = level.dont_spawn_growables,
growables = level.growables or level.enabled_growables or level.growable_spawn_types,
dont_adjust_camera_focus = level.dont_adjust_camera_focus,
camera_bounds = level.camera_bounds,
dont_adjust_camera_bounds = level.dont_adjust_camera_bounds,
background_theme = level.background_theme,
background_texture_theme = level.background_texture_theme,
background_texture = level.background_texture,
floor_theme = level.floor_theme,
floor_texture_theme = level.floor_texture_theme,
floor_texture = level.floor_texture,
post_configure = level.post_configure,
}
end
if loaded_level ~= nil and equal_levels(loaded_level, level) and not has_reset then
level.load_next_room()
custom_levels.unload_level()
custom_levels.load_level_custom_theme(ctx, level.file_name, theme_properties)
return
end
has_reset = false
if loaded_level then
if sequence_callbacks.on_level_will_unload then
sequence_callbacks.on_level_will_unload(loaded_level)
end
loaded_level.unload_level()
custom_levels.unload_level()
end
loaded_level = level
if not loaded_level then return end
if sequence_callbacks.on_level_will_load then
sequence_callbacks.on_level_will_load(loaded_level)
end
loaded_level.load_level()
custom_levels.load_level_custom_theme(ctx, level.file_name, theme_properties)
end
local function on_reset_callback()
has_reset = true
end
-- Called just before the level files are loaded. It is here that we load the level files
-- for the current level and activate the callbacks of the level.
--
-- ctx: Context to load levels into.
local function pre_load_level_files_callback(ctx)
-- Unload any loaded level when entering the base camp or the title screen.
if state.screen == SCREEN.CAMP or state.screen == SCREEN.TITLE or state.theme == 0 then
load_level(nil)
return
end
local level = run_state.current_level
load_level(level, ctx)
end
--------------------------------------
---- /LEVEL GENERATION
--------------------------------------
--------------------------------------
---- SHORTCUT LOADING
--------------------------------------
-- Load in an on-going run from a continue state.
--
-- level: The level that the player is currently on in the run.
-- attempts: The number of attempts the player has on the run.
-- time: The amount of time the player has spent in the run.
local function load_run(level, attempts, time)
run_state.initial_level = sequence_state.levels[1]
run_state.previous_level = nil
run_state.current_level = level
run_state.attempts = attempts
run_state.total_time = time
end
-- Load in a shortcut to a level. Sets the initial_level to that level so that resets in hardcore
-- reset at the level.
--
-- level: The level that the shortcut leads to.
local function load_shortcut(level)
run_state.previous_level = nil
run_state.current_level = level
run_state.initial_level = level
run_state.attempts = 0
run_state.total_time = 0
end
--------------------------------------
---- /SHORTCUT LOADING
--------------------------------------
--------------------------------------
---- LEVEL TRANSITIONS
--------------------------------------
-- Load the next level on transitions. If we were on the last level, call the on_win callback.
local function transition_increment_level_callback()
if state.loading ~= 1 then return end
if state.screen_next ~= SCREEN.TRANSITION then return end
local previous_level = run_state.current_level
local current_level = previous_level
if previous_level.is_multi_room and not previous_level.should_complete then
return
end
current_level = next_level()
run_state.previous_level = run_state.current_level
run_state.current_level = current_level
sequence_state.force_next_level = nil
if sequence_callbacks.on_completed_level then
sequence_callbacks.on_completed_level(previous_level, current_level)
end
if sequence_state.force_win or not current_level then
sequence_state.force_win = false
run_state.run_started = false
if sequence_callbacks.on_win then
sequence_callbacks.on_win(run_state.attempts, state.time_total)
end
end
end
-- Reset the run state if the game is reset and keep progress is not enabled.
local function reset_run_if_hardcore()
if run_state.checkpoint_level then
local prev_current_level = run_state.current_level
run_state.previous_level = nil
run_state.current_level = run_state.checkpoint_level
if sequence_callbacks.on_reset_to_checkpoint then
sequence_callbacks.on_reset_to_checkpoint(run_state.checkpoint_level, prev_current_level)
end
elseif not sequence_state.keep_progress then
run_state.previous_level = nil
run_state.current_level = run_state.initial_level
run_state.attempts = 0
if sequence_callbacks.on_reset_run then
sequence_callbacks.on_reset_run()
end
end
end
-- Update the display of the world-level to the desired display instead of using the
-- world-level we set for the theme to load properly.
local function update_state_and_doors()
if state.screen ~= SCREEN.LEVEL then return end
local current_level = run_state.current_level
if not current_level then return end
-- This doesn't affect anything except what is displayed in the UI.
state.world = current_level.world or index_of_level(current_level)
state.level = current_level.level or 1
if current_level.music then
state.theme = current_level.music
elseif current_level.music_theme then
state.theme = current_level.music_theme
else
state.theme = current_level.theme
end
if run_state.checkpoint_level then
-- Setting the _start properties of the state will ensure that Instant Restarts will take
-- the player back to the checkpoint level, instead of going to the starting level.
state.world_start = world_for_level(run_state.checkpoint_level)
state.level_start = level_for_level(run_state.checkpoint_level)
state.theme_start = theme_for_level(run_state.checkpoint_level)
elseif sequence_state.keep_progress then
-- Setting the _start properties of the state will ensure that Instant Restarts will take
-- the player back to the current level, instead of going to the starting level.
state.world_start = world_for_level(current_level)
state.level_start = level_for_level(current_level)
state.theme_start = theme_for_level(current_level)
end
if sequence_callbacks.on_post_level_generation then
sequence_callbacks.on_post_level_generation(current_level)
end
end
-- Fix borders of transition tiles.
set_post_entity_spawn(function(entity)
if sequence_state.disable_transition_decoration_fix then return end
if not run_state.run_started then return end
if entity.type.id == ENT_TYPE.FLOOR_TUNNEL_NEXT then
entity:set_texture(floor_texture_for_level(run_state.current_level))
else
entity:set_texture(floor_texture_for_level(run_state.previous_level))
end
entity:set_post_floor_update(function(floor)
local sides = {
{deco = floor.deco_top, side = FLOOR_SIDE.TOP, x = 0, y = 1},
{deco = floor.deco_bottom, side = FLOOR_SIDE.BOTTOM, x = 0, y = -1},
{deco = floor.deco_right, side = FLOOR_SIDE.RIGHT, x = 1, y = 0},
{deco = floor.deco_left, side = FLOOR_SIDE.LEFT, x = -1, y = 0},
}
local x, y, layer = get_position(floor.uid)
for _, side in pairs(sides) do
if side.deco ~= -1 then
if floor.type.id == ENT_TYPE.FLOOR_TUNNEL_NEXT then
get_entity(side.deco):set_texture(floor_texture_for_level(run_state.current_level))
else
get_entity(side.deco):set_texture(floor_texture_for_level(run_state.previous_level))
end
local neighbor_uid = get_grid_entity_at(x + side.x, y + side.y, layer)
if neighbor_uid ~= -1 then
local neighbor = get_entity(neighbor_uid)
if neighbor:get_texture() == floor:get_texture() then
floor:remove_decoration(side.side)
end
end
end
end
return true
end)
end, SPAWN_TYPE.ANY, MASK.FLOOR, {ENT_TYPE.FLOOR_TUNNEL_NEXT, ENT_TYPE.FLOOR_TUNNEL_CURRENT})
--------------------------------------
---- /LEVEL TRANSITIONS
--------------------------------------
--------------------------------------
---- TIME SYNCHRONIZATION
--------------------------------------
-- Since we are keeping track of time for the entire run even through deaths and resets, we must track
-- what the time was on resets and level transitions.
local function save_time_on_reset_callback()
if state.screen ~= SCREEN.LEVEL or not run_state.run_started then return end
if sequence_state.keep_progress or run_state.checkpoint_level then
-- Save the time on reset so we can keep the timer going.
run_state.total_time = state.time_total
else
-- Reset the time when keep progress is disabled; the run is going to be reset.
run_state.total_time = 0
end
end
-- Save the time of the run on transitions so that the run state is correct on starting
-- the next level.
local function save_time_on_transition_callback()
if state.screen_last ~= SCREEN.LEVEL or not run_state.run_started then return end
run_state.total_time = state.time_total
end
-- Set the time in the state so it shows up in the player's HUD.
local function load_time_after_level_generation_callback()
if state.screen ~= SCREEN.LEVEL then return end
state.time_total = run_state.total_time
end
-- Increase the attempts on level start, and mark the run as started on the first level start so
-- we can begin keeping track of the time.
local function start_level_callback()
if state.screen ~= SCREEN.LEVEL then return end
run_state.run_started = true
run_state.attempts = run_state.attempts + 1
if sequence_callbacks.on_level_start then
sequence_callbacks.on_level_start(run_state.current_level)
end
end
local function reset_on_camp_callback()
run_state.run_started = false
run_state.attempts = 0
run_state.previous_level = nil
run_state.current_level = nil
run_state.total_time = 0
end
--------------------------------------
---- /TIME SYNCHRONIZATION
--------------------------------------
--------------------------------------
---- CAMP
--------------------------------------
local main_exits = {}
local shortcuts = {}
local continue_doors = {}
-- Replace the main entrance door with a door that leads to the first level to begin the run.
local function replace_main_entrance()
local entrance_uids = get_entities_by_type(ENT_TYPE.FLOOR_DOOR_MAIN_EXIT)
main_exits = {}
if #entrance_uids > 0 then
local entrance_uid = entrance_uids[1]
local first_level = sequence_state.levels[1]
local x, y, layer = get_position(entrance_uid)
local entrance = get_entity(entrance_uid)
entrance.flags = clr_flag(entrance.flags, ENT_FLAG.ENABLE_BUTTON_PROMPT)
local door = spawn_door(
x,
y,
layer,
world_for_level(first_level),
level_for_level(first_level),
theme_for_level(first_level))
main_exits[#main_exits+1] = get_entity(door)
end
end
-- Clear the tracked doors in the camp for different shortcut and main entrances.
local function reset_camp_doors()
main_exits = {}
shortcuts = {}
continue_doors = {}
end
-- Updates the main entrance to lead to the current first level in the state. This is
-- called when the levels are updated.
local function update_main_exits()
local first_level = sequence_state.levels[1]
for _, main_exit in pairs(main_exits) do
main_exit.world = world_for_level(first_level)
main_exit.level = level_for_level(first_level)
main_exit.theme = theme_for_level(first_level)
end
end
-- Where to spawn a sign in relation to shortcut doors.
--
-- NONE: Do not spawn any sign at all.
-- LEFT: Spawn a sign two tiles to the left of the door.
-- RIGHT: Spawn a sign two tiles to the right of the door.
local SIGN_TYPE = {
NONE = 0,
LEFT = 1,
RIGHT = 2,
}
level_sequence.SIGN_TYPE = SIGN_TYPE
-- Spawn a door that will act as a shortcut to a specific level.
--
-- x: x position that the door will spawn at.
-- y: y position that the door will spawn at.
-- layer: Layer that the door will spawn at.
-- level: Level that the door will lead to when entered.
-- include_sign: (optional) SIGN_TYPE enum. SIGN_TYPE.NONE to not include any sign. SIGN_TYPE.LEFT
-- to include a sign to the left of the door. SIGN_TYPE.RIGHT to include a sign to the
-- right of the door. The sign will pop up a toast with either the level name or sign_text.
-- Defaults to SIGN_TYPE.NONE if not set and does not display a sign.
-- sign_text: (optional) Text displayed when the interact button is pressed. If not set, will default
-- to displaying "Shortcut to level.title".
-- Return: A shortcut object with data for the shortcut that was spawned:
-- level: The level the shorcut leads to.
-- door: The door that was spawned to start the shortcut.
-- sign: The sign that was spawned to display information about the shortcut.
-- sign_text: The text that will be displayed when interacting with the sign.
-- destroy(): Method that can be called to remove the shortcut.
level_sequence.spawn_shortcut = function(x, y, layer, level, include_sign, sign_text)
include_sign = include_sign or SIGN_TYPE.NONE
local background_uid = spawn_entity(ENT_TYPE.BG_DOOR, x, y+.25, layer, 0, 0)
local door_uid = spawn_door(x, y, layer, world_for_level(level), level_for_level(level), theme_for_level(level))
local door = get_entity(door_uid)
local background = get_entity(background_uid)
background:set_texture(texture_for_level(level))
background.animation_frame = set_flag(background.animation_frame, 1)
local sign
local tv
if include_sign ~= SIGN_TYPE.NONE then
local sign_position_x = x
if include_sign == SIGN_TYPE.LEFT then
sign_position_x = x - 2
elseif include_sign == SIGN_TYPE.RIGHT then
sign_position_x = x + 2
end
local sign_uid = spawn_entity(ENT_TYPE.ITEM_SPEEDRUN_SIGN, sign_position_x, y, layer, 0, 0)
sign = get_entity(sign_uid)
-- This stops the sign from displaying its default toast text when pressing the door button.
sign.flags = clr_flag(sign.flags, ENT_FLAG.ENABLE_BUTTON_PROMPT)
local tv_uid = button_prompts.spawn_button_prompt(button_prompts.PROMPT_TYPE.VIEW, sign_position_x, y, layer)
tv = get_entity(tv_uid)
end
local shortcut = {
level = level,
door = door,
sign = sign,
sign_text = sign_text or f'Shortcut to {level.title}',
}
local destroyed = false
shortcut.destroy = function()
if destroyed then return end
destroyed = true
door:destroy()
background:destroy()
if sign then
sign:destroy()
end
if tv then
tv:destroy()
end
local new_shortcuts = {}
for _, new_shortcut in pairs(shortcuts) do
if new_shortcut ~= shortcut then
new_shortcuts[#new_shortcuts+1] = new_shortcut
end
end
shortcuts = new_shortcuts
end
shortcuts[#shortcuts+1] = shortcut
return shortcut
end
-- Spawn a door that can be entered to continue an ongoing run.
--
-- x: x position that the door will spawn at.
-- y: y position that the door will spawn at.
-- layer: Layer that the door will spawn at.
-- level: Level that the door will lead to when entered.
-- attempts: Number of attempts in the run that will be continued.
-- time: Total amount of time spent on the continued run.
-- include_sign: (optional) SIGN_TYPE enum. SIGN_TYPE.NONE to not include any sign. SIGN_TYPE.LEFT
-- to include a sign to the left of the door. SIGN_TYPE.RIGHT to include a sign to the
-- right of the door. The sign will pop up a toast with either the level name or sign_text.
-- Defaults to SIGN_TYPE.NONE if not set and does not display a sign.
-- sign_text: (optional) Text displayed when the interact button is pressed. If not set, will default
-- to displaying "Continue run from level.title".
-- disabled_sign_text: (optional) Text displayed when the interact button is pressed if continuing runs
-- is disbled due to keep_progress being disabled. If not set, will default to
-- displaying "Cannot continue in hardcore mode".
-- no_run_sign_text: (optional) Text displayed when the interact button is pressed if continuing runs
-- is enabled, but there is no saved run to load from. If not set, will default to
-- displaying "No run to continue"
-- Return: A shortcut object with data for the shortcut that was spawned:
-- level: The level the shorcut leads to.
-- attempts: Number of attempts that the run is on if entering the door.
-- time: Total time the run will be set to when continuing through the door.
-- door: The door that was spawned to continue the run.
-- sign: The sign that was spawned to display information about the run.
-- sign_text: The text that will be displayed when interacting with the sign.
-- disabled_sign_text: The text that will be displayed if continuing is disabled.
-- no_run_sign_text: The text that will be displayed if there is no run to continue.
-- destroy(): Method that can be called to remove the door.
-- update_door(level, attempts, time, sign_text, disabled_sign_text, no_run_sign_text): Method
-- that can be called to update the state of the run that the door will continue to.
level_sequence.spawn_continue_door = function(
x,
y,
layer,
level,
attempts,
time,
include_sign,
sign_text,
disabled_sign_text,
no_run_sign_text)
include_sign = include_sign or SIGN_TYPE.NONE
local background_uid = spawn_entity(ENT_TYPE.BG_DOOR, x, y+.25, layer, 0, 0)
local door_uid = spawn_door(x, y, layer, world_for_level(level), level_for_level(level), theme_for_level(level))
local door = get_entity(door_uid)
local background = get_entity(background_uid)
background.animation_frame = set_flag(background.animation_frame, 1)
local function update_door_for_level(level)