forked from obsidian-level-maker/Obsidian
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlevel.lua
More file actions
2863 lines (2065 loc) · 70.6 KB
/
Copy pathlevel.lua
File metadata and controls
2863 lines (2065 loc) · 70.6 KB
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
------------------------------------------------------------------------
-- LEVEL MANAGEMENT
------------------------------------------------------------------------
--
-- // Obsidian //
--
-- Copyright (C) 2006-2017 Andrew Apted
-- Copyright (C) 2020-2022 MsrSgtShooterPerson
-- Copyright (C) 2020 Armaetus
--
-- This program is free software; you can redistribute it and/or
-- modify it under the terms of the GNU General Public License
-- as published by the Free Software Foundation; either version 2,
-- of the License, or (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
------------------------------------------------------------------------
--class EPISODE
--[[
id : number -- index number (in GAME.episodes)
description -- a name generated for this episode
levels : list(LEVEL) -- all the levels to generate (may be empty)
used_keys : table -- for hubs, remember keys which have been used
-- on any level in the hub (cannot use them again)
--]]
--class LEVEL
--[[
id : number -- index number (in GAME.levels)
name : string -- engine name for this level, e.g. MAP01,
description : string -- level name or title (optional)
kind -- keyword: "NORMAL", "BOSS", "SECRET",
episode : EPISODE
hub : HUB_INFO -- used in hub-based games (like Hexen)
ep_along -- how far along in the episode: 0.0 --> 1.0,
game_along -- how far along in the whole game: 0.0 --> 1.0,
is_secret -- true if level is a secret level
prebuilt -- true if level will is prebuilt (not generated)
is_procedural_gotcha -- true if this level is a special Procedural Gotcha arena
is_linear -- true if this level is linear, as in no branching rooms
is_nature -- true if this level is entirely parks and caves
has_streets -- true if this level contains Street Mode streets
=== General planning ===
liquid : table -- the main liquid in the level (can be nil)
is_dark : bool -- true if outdoor rooms will be dark
special : keyword -- normally nil
-- [ not used at the moment ]
=== Monster planning ===
monster_level -- the maximum level of a monster usable here [ except bosses ]
new_monsters -- monsters which player has not encountered yet
global_pal -- global palette, can ONLY use these monsters [ except for bosses ]
boss_fights : list(BOSS_FIGHT) -- boss fights, from biggest to smallest
=== Weapon planning ===
weapon_quota -- number of weapons to add in this level [ except secrets ]
new_weapons -- weapons which player does not have yet [ may overlap with start_weapons ]
start_weapons -- a weapon or two for the start room
other_weapons -- the weapons for rest of map (#start + #other == quota)
secret_weapon -- an unseen weapon, for usage in a secret room
=== Item planning ===
usable_keys : prob table -- if present, can only use these keys
=== Other stuff ===
ids : table -- used for allocating tag numbers (etc)
rooms : list(ROOM)
areas : list(AREA)
conns : list(CONN)
quests : list(QUEST)
zones : list(ZONE)
locks : list(LOCK)
start_room : ROOM -- the starting room
exit_room : ROOM -- the exit room
junctions[id1 + id2] : JUNCTION
corners : array_2D(CORNER)
-- TODO: lots of other fields : document important ones
--]]
--class BOSS_FIGHT
--[[
mon -- name of monster
count -- number of them to use
boss_type -- keyword: guard / minor / nasty / tough
--]]
--class HUB_INFO
--[[
hub_key : name -- goal of this level must be this key
hub_weapon : name -- weapon to place on this level
hub_piece : name -- weapon PIECE for this level
--]]
-- Map size stuff
function Level_determine_map_size(LEV)
--
-- Determines size of map (Width x Height) in grid squares,
-- based on the user's settings and how far along in the
-- episode or game we are.
--
-- Named sizes --
-- Since we have other sizes and Auto-Detail, we can have these bigger sizes
-- now. -Armaetus, July 9th, 2019,
local ob_size = PARAM.float_size
local W, H
if LEV.custom_size then
ob_size = LEV.custom_size
W = ob_size
goto customsize
end
if LEV.custom_w then
if LEV.custom_h then
return LEV.custom_w, LEV.custom_h
else
ob_size = LEV.custom_size
W = ob_size
goto customsize
end
end
-- there is no real "progression" when making a single level.
-- hence use the average size instead.
if OB_CONFIG.length == "single" then
if ob_size == gui.gettext("Episodic") or
ob_size == gui.gettext("Progressive") then
ob_size = 36
end
end
-- Mix It Up --
-- Readjusted probabilities once again, added "Micro" size as suggested by activity
-- in the Discord server. -Armaetus, June 30th, 2019,
if ob_size == gui.gettext("Mix It Up") then
local result_skew = 1.0
local low = PARAM.float_level_lower_bound or 10
local high = PARAM.float_level_upper_bound or 75
if OB_CONFIG.level_size_bias then
if OB_CONFIG.level_size_bias == "small" then
result_skew = .80
elseif OB_CONFIG.level_size_bias == "large" then
result_skew = 1.20
end
end
ob_size = math.clamp(low, int(rand.irange(low, high) * result_skew), high)
end
if ob_size == gui.gettext("Episodic") or
ob_size == gui.gettext("Progressive") then
-- Progressive --
local ramp_factor = 0.66
if OB_CONFIG.level_size_ramp_factor then
ramp_factor = tonumber(OB_CONFIG.level_size_ramp_factor)
end
local along = LEV.game_along ^ ramp_factor
if ob_size == gui.gettext("Episodic") then along = LEV.ep_along end
along = math.clamp(0, along, 1)
-- Level Control fine tune for Prog/Epi
-- default when Level Control is off: ramp from "small" --> "large",
local def_small = PARAM.float_level_lower_bound or 30
local def_large = PARAM.float_level_upper_bound - def_small or 42
-- this basically ramps up
W = int(def_small + along * def_large)
else
-- Single Size --
W = ob_size
end
::customsize::
if not W then
error("Invalid value for size : " .. tostring(ob_size))
end
-- Try to prevent grower failures with Micro levels
if LEV.is_nature then
W = math.max(W, 16)
end
gui.printf("Initial size for " .. LEV.name .. ": " .. W .. "\n")
local H = 1 + int(W * 0.8)
return W, H
end
function Episode_determine_map_sizes()
for _,LEV in pairs(GAME.levels) do
local W, H = Level_determine_map_size(LEV)
if LEV.is_procedural_gotcha == true then
W = 26 -- defualt for proc gotchas
if PARAM.gotcha_map_size then
W = PROC_GOTCHA_MAP_SIZES[PARAM.gotcha_map_size]
end
if PARAM.bool_boss_gen == 1 then W = 16 end
H = W
end
assert(W + 4 <= SEED_W)
assert(H + 4 <= SEED_H)
LEV.map_W = W
LEV.map_H = H
-- part of the experimental size multiplier experiments
LEV.size_multiplier = 1
LEV.area_multiplier = 1
LEV.size_consistency = "normal"
local mix_type = "normal"
if PARAM.room_size_multiplier then
if PARAM.room_size_multiplier ~= "vanilla"
and PARAM.room_size_multiplier ~= "mixed" then
LEV.size_multiplier = tonumber(PARAM.room_size_multiplier)
end
end
if PARAM.room_area_multiplier then
if PARAM.room_area_multiplier ~= "vanilla"
and PARAM.room_area_multiplier ~= "mixed" then
LEV.area_multiplier = tonumber(PARAM.room_area_multiplier)
end
end
if PARAM.room_size_mix_type and PARAM.room_size_multiplier == "mixed" then
LEV.size_multiplier = rand.key_by_probs(ROOM_SIZE_MULTIPLIER_MIXED_PROBS[PARAM.room_size_mix_type])
end
if PARAM.room_area_mix_type and PARAM.room_area_multiplier == "mixed" then
LEV.area_multiplier = rand.key_by_probs(ROOM_AREA_MULTIPLIER_MIXED_PROBS[PARAM.room_area_mix_type])
end
if PARAM.room_size_consistency then
if PARAM.room_size_consistency == "mixed" then
LEV.size_consistency = rand.key_by_probs(SIZE_CONSISTENCY_MIXED_PROBS)
else
LEV.size_consistency = PARAM.room_size_consistency
end
end
gui.printf(
"size_multiplier: " .. LEV.size_multiplier .. "\n" ..
"area_multiplier: " .. LEV.area_multiplier .. "\n" ..
"size_consistency: " .. LEV.size_consistency .. "\n\n"
)
end
end
function Episode_pick_names()
--== Name Generator Test ==-- MSSP
-- game name (for title screen)
if not GAME.title then
GAME.title = Naming_grab_one("TITLE")
end
if not GAME.sub_title then
GAME.sub_title = Naming_grab_one("SUB_TITLE")
end
gui.printf("Game title: %s\n\n", GAME.title)
gui.printf("Game sub-title: %s\n\n", GAME.sub_title)
for index,EPI in pairs(GAME.episodes) do
-- only generate names for used episodes
if table.empty(EPI.levels) then goto continue end
EPI.description = Naming_grab_one("EPISODE")
gui.printf("Episode %d title: %s\n\n", index, EPI.description)
::continue::
end
end
function Episode_decide_specials()
---| Episode_decide_specials |---
for _,EPI in pairs(GAME.episodes) do
-- TODO
end
-- dump the results
local count = 0,
gui.printf("\nSpecial levels:\n")
for _,LEV in pairs(GAME.levels) do
if LEV.special then
gui.printf(" %s : %s\n", LEV.name, LEV.special)
count = count + 1
end
end
if count == 0 then
gui.printf(" none\n")
end
end
function Episode_plan_monsters()
--
-- Decides various monster stuff :
--
-- (1) monster palette for each level
-- (2) the end-of-level boss of each level
-- (3) guarding monsters (aka "mini bosses")
-- (4) one day: boss fights for special levels
--
local used_types = {}
local used_bosses = {}
local used_guards = {}
local BOSS_AHEAD = 2.2
local function default_level(info)
local hp = info.health
if hp < 45 then return 1 end
if hp < 130 then return 3 end
if hp < 450 then return 5 end
return 7
end
local function init_monsters()
for name,info in pairs(GAME.MONSTERS) do
if not info.id then
error(string.format("Monster '%s' lacks an id field", name))
end
-- default probability
if not info.prob and not info.replaces then -- Try to keep replacement-only monsters from being added to the palette - Dasho
info.prob = 50
end
-- default level
if not info.level then
info.level = default_level(info)
end
end
end
local function calc_monster_level(LEV)
local mon_strength = PARAM.float_strength
if mon_strength == 12 then
LEV.monster_level = mon_strength
return
end
local mon_along = LEV.game_along
local ramp_up = PARAM.float_ramp_up
-- this is for Doom 1 / Ultimate Doom / Heretic
if PARAM.episodic_monsters or ramp_up == gui.gettext("Episodic") then
mon_along = (LEV.ep_along + LEV.game_along) / 2
end
if LEV.is_secret then
-- secret levels are easier
mon_along = mon_along * 0.75
elseif OB_CONFIG.length == "single" then
-- for single level, use skew to occasionally make extremes
mon_along = rand.skew(0.6, 0.3)
elseif OB_CONFIG.length == "game" then
-- reach peak strength about 2/3rds along
mon_along = mon_along * 1.7
end
assert(mon_along >= 0)
-- apply the user Ramp-up setting
-- [ and some tweaks for the Strength setting ]
local factor
if ramp_up ~= gui.gettext("Episodic") then
factor = ramp_up
else
factor = 1.0
end
mon_along = mon_along * factor
-- New adjustments for Monster Strength slider...may need to tune
mon_along = mon_along + (mon_strength / 10)
mon_along = 1.0 + (PARAM.mon_along_factor or 8.0) * mon_along
-- add some randomness
mon_along = mon_along + 0.7 * (gui.random() ^ 2)
if LEV.is_procedural_gotcha then
local gotcha_strength = 2
if PARAM.bool_boss_gen == 1 then
if PARAM.boss_gen_reinforce == "weaker" then
gotcha_strength = math.max(8, mon_along * 0.9) * -1
elseif PARAM.boss_gen_reinforce == "default" then
gotcha_strength = math.max(4, mon_along * 0.75) * -1
elseif PARAM.boss_gen_reinforce == "harder" then
gotcha_strength = math.max(2, mon_along * 0.5) * -1
elseif PARAM.boss_gen_reinforce == "tougher" then
gotcha_strength = 2
elseif PARAM.boss_gen_reinforce == "nightmare" then
gotcha_strength = 16
end
elseif PARAM.float_gotcha_strength then
gotcha_strength = PARAM.float_gotcha_strength
end
LEV.monster_level = mon_along + gotcha_strength
if LEV.monster_level < 1 then
LEV.monster_level = 1
end
else
-- used by standard levels
LEV.monster_level = mon_along
end
end
local function check_theme(LEV, info)
-- if no theme specified, monster is usable in all themes
if not info.theme then return true end
-- anything goes in CRAZY mode
if PARAM.float_strength == 12 then return true end
return info.theme == LEV.theme_name
end
local function is_monster_usable(LEV, mon, info)
if not info.prob or info.prob <= 0 then return false end
if info.level > LEV.monster_level
+ (MONSTER_KIND_JUMPSTART_LEVELS[OB_CONFIG.mon_variety_jumpstart] or 0)
then return false end
if info.weap_min_damage and info.weap_min_damage > LEV.weap_max_damage then return false end
if not check_theme(LEV, info) then return false end
return true
end
local function mark_new_monsters()
-- for each level, determine what monsters can be used, and also
-- which ones are NEW for that level.
local seen_monsters = {}
for _,LEV in pairs(GAME.levels) do
LEV.new_monsters = {}
if not (LEV.prebuilt or LEV.is_secret) then
for mon,info in pairs(GAME.MONSTERS) do
if not seen_monsters[mon] and is_monster_usable(LEV, mon, info) then
table.insert(LEV.new_monsters, mon)
seen_monsters[mon] = true
end
end
end
LEV.seen_monsters = table.copy(seen_monsters)
end
end
local function pick_single_for_level(LEV)
local tab = {}
if not LEV.episode.single_mons then
LEV.episode.single_mons = {}
end
for name,_ in pairs(LEV.seen_monsters) do
local info = GAME.MONSTERS[name]
tab[name] = info.prob
-- prefer monsters which have not been used before
if LEV.episode.single_mons[name] then
tab[name] = tab[name] / 100
end
end
if table.empty(tab) then
return
end
local name = rand.key_by_probs(tab)
LEV.global_pal[name] = 1
-- mark it as used
LEV.episode.single_mons[name] = 1
end
local function pick_global_palette(LEV)
--
-- decides which monsters we will use on this level.
-- easiest way is to pick some monsters NOT to use.
--
-- Note: we exclude BOSS monsters here, except in CRAZY mode.
--
LEV.global_pal = {}
-- only one kind of monster in this level?
if STYLE.mon_variety == "none" then
pick_single_for_level(LEV)
return
end
for name,_ in pairs(LEV.seen_monsters) do
local info = GAME.MONSTERS[name]
if not info.boss_type or PARAM.float_strength == 12 or LEV.is_procedural_gotcha then
LEV.global_pal[name] = 1
elseif info.boss_type and OB_CONFIG.bossesnormal ~= "no" then
if info.boss_type == "minor" then
LEV.global_pal[name] = 1
elseif info.boss_type == "nasty" then
if OB_CONFIG.bossesnormal == "nasty" or OB_CONFIG.bossesnormal == "all" then
LEV.global_pal[name] = 1
end
elseif info.boss_type == "tough" and OB_CONFIG.bossesnormal == "all" then
LEV.global_pal[name] = 1
end
end
end
-- actually skip some monsters (esp. when # is high)
LEV.skip_monsters = {}
local skip_num = (table.size(LEV.global_pal) - 9) / 6
skip_num = rand.int(skip_num + LEV.game_along + 0.02)
-- MSSP: Sometimes, *don't* skip monsters later on in the map
-- because it's perfectly fine to have the whole kitchen sink
if LEV.game_along > 0.5 then
if rand.odds(50 * (LEV.game_along + 0.25)) then
skip_num = rand.irange(0, skip_num)
end
end
for i = 1, skip_num do
local mon = rand.key_by_probs(LEV.global_pal)
LEV.global_pal[mon] = nil
table.insert(LEV.skip_monsters, mon)
end
-- Another attempt to really truly respect 0 prob in a theme's monster prefs
if LEV.theme.monster_prefs then
if LEV.theme.monster_prefs[mon] and LEV.theme.monster_prefs[mon] == 0 then
LEV.global_pal[mon] = nil
end
end
end
local function is_boss_usable(LEV, mon, info)
if LEV.is_procedural_gotcha then return true end
if info.prob <= 0 then return false end
if info.boss_prob == 0 then return false end
if info.level > LEV.monster_level + BOSS_AHEAD then return false end
if info.weap_min_damage and info.weap_min_damage > LEV.weap_max_damage then return false end
return true
end
local function collect_usable_bosses(LEV, what)
assert(what)
local tab = {}
for name,info in pairs(GAME.MONSTERS) do
if LEV.theme.monster_prefs and LEV.theme.monster_prefs[name] and LEV.theme.monster_prefs[name] == 0 then goto skipboss end
if LEV.is_procedural_gotcha and PARAM.bool_boss_gen == 1 then
local bprob = 80
if PARAM.boss_gen_typelimit ~= "nolimit" then
local boss_diff = PARAM.boss_gen_diff
local lolevel
local hilevel
if boss_diff == "easier" then
lolevel = 1
hilevel = 4
elseif boss_diff == "default" then
lolevel = 2
hilevel = 6
elseif boss_diff == "harder" then
lolevel = 3
hilevel = 8
elseif boss_diff == "nightmare" then
lolevel = 7
hilevel = 9
end
if OB_CONFIG.length == "game" then
if LEV.game_along < 0.4 then
lolevel = math.max(1,lolevel-2)
hilevel = hilevel-2
elseif LEV.game_along < 0.7 then
hilevel = hilevel-1
else
lolevel = math.min(9,lolevel+4)
hilevel = math.min(9,hilevel+1)
end
end
if PARAM.boss_gen_typelimit == "softlimit" then
if info.level < lolevel then
bprob = bprob/(lolevel-info.level+1)
end
if info.level > hilevel then
bprob = bprob/(info.level-hilevel+1)
end
elseif PARAM.boss_gen_typelimit == "hardlimit" then
if info.level < lolevel then
bprob = 0
end
if info.level > hilevel then
bprob = 0
end
end
end
if info.attack == "hitscan" then
local hitred = PARAM.boss_gen_hitscan
if hitred == "less" then
bprob = bprob/2
elseif hitred == "muchless" then
bprob = bprob/5
elseif hitred == "none" then
bprob = 0
end
end
if PARAM.bool_boss_gen_types == 1 and info.prob == 0 then
bprob = 0
end
tab[name] = bprob
else
if info.boss_type == what and is_boss_usable(LEV, name, info) then
tab[name] = info.boss_prob or 50
end
end
::skipboss::
end
return tab
end
local function prob_for_guard(LEV, info)
if not info.prob or info.prob <= 0 then return 0 end
-- simply too weak
if info.health < 45 then return 0 end
if info.weap_min_damage and info.weap_min_damage > LEV.weap_max_damage then return 0 end
if info.level > LEV.monster_level + BOSS_AHEAD then return 0 end
-- ignore theme-specific monsters (SS NAZI)
if info.theme then return 0 end
-- already used on this map?
if LEV.seen_guards[info.name] then return 0 end
-- base probability : this value is designed to take into account
-- the settings of the monster control module
local prob = (info.damage or 1)
if OB_CONFIG.bosses == "easier" then
prob = prob ^ 0.3
elseif OB_CONFIG.bosses == "harder" then
prob = prob ^ 1.2
else
prob = prob ^ 0.6
end
if LEV.seen_monsters[info.name] then
prob = prob / 10
elseif not used_guards[info.name] then
prob = prob * 5
end
return prob
end
local function collect_usable_guards(LEV)
local tab = {}
for name,info in pairs(GAME.MONSTERS) do
-- skip the real boss monsters
if info.boss_type then
if OB_CONFIG.bossesnormal == "no" then goto continue
elseif info.boss_type == "nasty" and OB_CONFIG.bossesnormal == "minor" then goto continue
elseif info.boss_type == "tough" and OB_CONFIG.bossesnormal ~= "all" then goto continue end
end
if LEV.theme.monster_prefs and LEV.theme.monster_prefs[name] and LEV.theme.monster_prefs[name] == 0 then goto continue end
local prob = prob_for_guard(LEV, info)
if prob > 0 then
tab[name] = prob
end
::continue::
end
return tab
end
local function count_boss_type(LEV, what)
return table.size(collect_usable_bosses(LEV, what))
end
local function pick_boss_quotas(LEV)
local c_minor = count_boss_type(LEV, "minor")
local c_nasty = count_boss_type(LEV, "nasty")
local c_tough = count_boss_type(LEV, "tough")
local user_factor = BOSS_FACTORS[OB_CONFIG.bosses]
assert(user_factor)
-- Tough quota
local prob1 = 0
if LEV.dist_to_end then
local factor = sel(c_tough < 2, 40, 20)
prob1 = 100 - (LEV.dist_to_end - 1) * factor
prob1 = prob1 * user_factor
end
if c_tough > 0 and rand.odds(prob1) then
LEV.boss_quotas.tough = 1
prob1 = prob1 * LEV.game_along * user_factor
if rand.odds(prob1) and used_types["tough"] then
LEV.boss_quotas.tough = 2
end
end
if LEV.boss_quotas.tough > 0 then
used_types["tough"] = true
end
-- Nasty quota
local prob2 = sel(c_nasty < 2, 25, 40)
if LEV.dist_to_end == 2 then
prob2 = 90
end
prob2 = prob2 * user_factor
if c_nasty > 0 and rand.odds(prob2) then
LEV.boss_quotas.nasty = 1
prob2 = prob2 * LEV.game_along * user_factor
if rand.odds(prob2) and used_types["nasty"] then
LEV.boss_quotas.nasty = 2
end
end
if LEV.boss_quotas.nasty > 0 then
used_types["nasty"] = true
end
-- Minor quota
local prob3 = sel(c_minor < 2, 40, 70)
if LEV.dist_to_end == 3 then
prob3 = 99
end
prob3 = prob3 * user_factor
if c_minor > 0 and rand.odds(prob3) then
LEV.boss_quotas.minor = 1
prob3 = prob3 * LEV.game_along * user_factor
if rand.odds(prob3) and used_types["minor"] then
LEV.boss_quotas.minor = 2
end
end
if LEV.boss_quotas.minor > 0 then
used_types["minor"] = true
end
-- Guard quota
local total = LEV.boss_quotas.tough +
LEV.boss_quotas.nasty +
LEV.boss_quotas.minor
LEV.boss_quotas.guard = math.clamp(2, 4 - total, 4)
end
local function create_fight(LEV, boss_type, along)
local bosses = collect_usable_bosses(LEV, boss_type)
if table.empty(bosses) then return end
local mon = rand.key_by_probs(bosses)
local info = GAME.MONSTERS[mon]
-- select how many
local count = 1 + LEV.game_along
if boss_type ~= "tough" then count = count ^ 1.5 end
-- user quantity setting
local factor = MONSTER_QUANTITIES[OB_CONFIG.mons] or 1
if factor > 1 then factor = (factor + 1) / 2 end
count = count * factor
if OB_CONFIG.bosses == "easier" then count = count / 1.6 end
if OB_CONFIG.bosses == "harder" then count = count * 1.6 end
count = rand.int(count)
if boss_type == "tough" then
count = math.clamp(1, count, 2)
elseif boss_type == "nasty" then
count = math.clamp(1, count, 4)
else
count = math.clamp(1, count, 6)
end
-- secondary boss fights should be weaker than primary one
if along >= 3 then
count = 1
elseif along == 2 then
if count == 2 or count == 3 then
count = rand.sel(75, 1, 2)
elseif count > 1 then
count = rand.sel(50, 2, math.ceil(count / 2))
end
end
-- this is to prevent Masterminds infighting
if info.boss_limit then
count = math.min(count, info.boss_limit)
end
-- ensure first encounter with a boss only uses a single one
count = math.min(count, 1 + (used_bosses[mon] or 0))
if LEV.is_procedural_gotcha and PARAM.bool_boss_gen == 1 then
count = 1
end
-- stderrf(" count %1.2f for '%s'\n", count, mon)
local FIGHT =
{
mon = mon,
count = count,
boss_type = boss_type
}
table.insert(LEV.boss_fights, FIGHT)
used_bosses[mon] = math.max(used_bosses[mon] or 0, count)
return true -- ok
end
local function create_guard(LEV, along)
local guards = collect_usable_guards(LEV)
--- stderrf("%s Usable guards:\n%s\n", LEV.name, table.tostr(guards))
if table.empty(guards) then return end
local mon = rand.key_by_probs(guards)
local info = GAME.MONSTERS[mon]
-- select how many
local count = 2 * (1.5 + LEV.game_along)
-- user quantity setting
local factor = MONSTER_QUANTITIES[OB_CONFIG.mons] or 1
if factor > 1 then factor = (factor + 1) / 2 end