-
Notifications
You must be signed in to change notification settings - Fork 38
/
Copy pathrandomize
executable file
·1461 lines (1445 loc) · 52.7 KB
/
randomize
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
#!/usr/bin/env node
'use strict'
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
const os = require('os')
const Worker = require('worker_threads').Worker
const constants = require('./src/constants')
const errors = require('./src/errors')
const extension = require('./src/extension')
const randomizeStats = require('./src/randomize_stats')
const randomizeItems = require('./src/randomize_items')
const randomizeMusic = require('./src/randomize_music')
const applyAccessibilityPatches = require('./src/accessibility_patches')
const relics = require('./src/relics')
const util = require('./src/util')
let version = require('./package').version
// Suppress all deprecation warnings
process.emitWarning = (warning, type) => {
if (type !== 'DeprecationWarning') {
console.warn(warning);
}
};
const optionsHelp = [
'The options string may contain any of the following:',
' "p" for preset (`--help preset`)',
' "d" for enemy drops (`--help drops`)',
' "e" for starting equipment (`--help equipment`)',
' "i" for item locations (`--help items`)',
' "b" for prologue rewards (`--help rewards`)',
' "r" for relic locations (`--help relics`)',
' "s" for stats',
' "m" for music',
' "w" for writes (`--help writes`)',
' "k" for turkey mode',
' "t" for tournament mode (`--help tournament`)',
'',
'The default randomization mode is "'
+ constants.defaultOptions
+ '", which randomizes everything.',
'',
'Examples:',
' $0 --opt d # Only randomize enemy drops.',
' $0 --opt di # Randomize drops and item locations.',
' $0 # Randomize everything (default mode).',
].join('\n')
const dropsHelp = [
'Enemy drop randomization can be toggled with the "d" switch. Drops may',
'also be specified using argument syntax.',
'',
'Drops format:',
' d[:<enemy>[-<level>][:[<item>][-[<item>]]][:...]',
'',
'Enemies and items are specified by removing any non-alphanumeric',
'characters from their name. Enemies with the same name can be dis-',
'ambiguated by specifying their level.',
'',
'A wildcard character ("*") can be used to replace items for all enemies.',
'',
'The global drop table can be edited by specifying "Global" as the enemy',
'name. Please note that there are 32 items in the global drop table.',
'',
'Examples:',
' d:Zombie:Cutlass-Bandanna Zombie drops Cutlass and Bandanna',
' d:Slinger:-Orange Replace Slinger rare drop with orange',
' d:MedusaHead-8: Medusa Head level 8 drops nothing',
' d:*:Grapes-Potion Every enemy drops Grapes and Potion',
' d:Global:Apple-Orange-Tart Replace first 3 items in global drops table',
' with Apple, Orange, and Tart',
'',
'Items can be prevented from being randomized to enemy\'s drop table by',
'prefixing the enemy\'s name with a hyphen ("-"):',
' d:-AxeKnight:Stopwatch Axe Knight never drops Stopwatch',
' d:-StoneRose:Medal-Opal Stone Rose never drops Medal or Opal',
'',
'If other randomization options follow a drop, they must also be',
'separated from the drop with a comma:',
' $0 --opt d:Slinger:-Orange,ipt',
].join('\n')
const equipmentHelp = [
'Starting equipment randomization can be toggled with the "e" switch.',
'Equipment may also be specified using argument syntax.',
'',
'Equipment format:',
' e[:<slot>[:<item>]][:...]',
'',
'Items are specified by removing any non-alphanumeric characters from',
'their name.',
'',
'Slot is one of:',
' "r" for right hand',
' "l" for left hand',
' "h" for head',
' "b" for body',
' "c" for cloak',
' "o" for other',
' "a" for Axe Lord armor (Axe Armor mode only)',
' "x" for Lapis lazuli (Luck mode only)',
'',
'Examples:',
' e:l:Marsil:Fireshield Marsil in left hand, Fire shield in right',
' e:o:Duplicator Duplicator in other slot',
' e:c: No cloak',
'',
'Equipment can be prevented from being starting equipment by prefixing',
'an equipment slot with a hyphen ("-"):',
' e:-r:Crissaegrim Never start with Crissaegrim',
' e:-l:IronShield:DarkShield Never start with Iron shield or Dark shield',
'',
'If other randomization options follow an equip, they must also be',
'separated from the equip with a comma:',
' $0 --opt e:o:Duplicator,dpt',
].join('\n')
const itemsHelp = [
'Item location randomization can be toggled using the "i" switch. Items',
'may be placed in specific locations using argument syntax.',
'',
'Items format:',
' i[:<zone>:<item>[-<index>]:<replacement>][:...]',
'',
'Items are specified by removing any non-alphanumeric characters from',
'their name. If a zone contains multiple occurences of the same item,',
'it can be disambuated by specifying its index.',
'',
'A wildcard character ("*") can be used for the zone and/or the item. When',
'used as the zone, the replacement will occur in every zone. When used as',
'the item, every item will be replaced.',
'',
'Zone is one of:',
' ST0 (Final Stage: Bloodlines)',
' ARE (Colosseum)',
' CAT (Catacombs)',
' CHI (Abandoned Mine)',
' DAI (Royal Chapel)',
' LIB (Long Library)',
' NO0 (Marble Gallery)',
' NO1 (Outer Wall)',
' NO2 (Olrox\'s Quarters)',
' NO3 (Castle Entrance)',
' NO4 (Underground Caverns)',
' NZ0 (Alchemy Laboratory)',
' NZ1 (Clock Tower)',
' TOP (Castle Keep)',
' RARE (Reverse Colosseum)',
' RCAT (Floating Catacombs)',
' RCHI (Cave)',
' RDAI (Anti-Chapel)',
' RLIB (Forbidden Library)',
' RNO0 (Black Marble Gallery)',
' RNO1 (Reverse Outer Wall)',
' RNO2 (Death Wing\'s Lair)',
' RNO3 (Reverse Entrance)',
' RNO4 (Reverse Caverns)',
' RNZ0 (Necromancy Laboratory)',
' RNZ1 (Reverse Clock Tower)',
' RTOP (Reverse Castle Keep)',
'',
'Examples:',
' i:ARE:BloodCloak:Banana Replace Blood Cloak with Banana',
' i:NO3:PotRoast:LibraryCard Replace Pot Roast with Library Card',
' i:TOP:Turkey-2:Peanuts Replace 2nd Turkey with Peanuts',
' i:CAT:*:Orange Replace every item in Catacombs with Orange',
' i:*:MannaPrism:Potion Replace every Manna Prism with Potion',
' i:*:*:Grapes Replace every item with Grapes',
'',
'Items can be prevented from being randomized to a map location by',
'prefixing the zone with a hyphen ("-"):',
' i:-RCHI:LifeApple:Mace Never replace the Cave Life Apple with Mace',
' i:-*:*:HeartRefresh-Uncurse Never replace any tile with a Heart Refresh',
' or Uncurse',
'',
'If other randomization options follow an item, they must also be',
'separated from the item with a comma:',
' $0 --opt i:TOP:Turkey-2:Peanuts,dpt',
].join('\n')
const rewardsHelp = [
'Prologue reward randomization can be toggled with the "b" switch.',
'Rewards may be specified using argument syntax.',
'',
'Rewards format:',
' b[:<reward>[:<item>]][:...]',
'',
'Reward is one of:',
' "h" for Heart Refresh',
' "n" for Neutron bomb',
' "p" for Potion',
'',
'Items are specified by removing any non-alphanumeric characters from',
'their name.',
'',
'Examples:',
' b:h:MannaPrism Replace Heart Refresh with Manna Prism',
' b:n:PowerofSire Replace Neutron bomb with Power of Sire',
' b:p:BuffaloStar Replace Potion with Buffalo Star',
'',
'Items can be prevented from replacing prologue rewards by prefixing the',
'reward with a hyphen ("-"):',
' b:-h:Uncurse Never replace Heart Refresh with Uncurse',
' b:-n:Turkey-TNT Never replace Neutron bomb with Turkey or TNT',
'',
'If other randomization options follow an item, they must also be',
'separated from the item with a comma:',
' $0 --opt b:h:MannaPrism,dt',
].join('\n')
const relicsHelp = [
'Relic location randomization can be toggled with the "r" switch, and',
'custom relic location locks may be specified using argument syntax.',
'',
'A relic location lock sets the abilities required to access a relic',
'location. Each relic location may be guarded by multiple locks, and the',
'location will be open to the player once they have all abilities',
'comprising any single lock.',
'',
'A location can also specify escape requirements. These are combinations of',
'abilities, any one of which must be satisified by all progression routes',
'granting access to the location. This is intended to prevent the player',
'from accessing an area that they might not have the ability to escape',
'from. Note that is is possible for the location itself to grant one of the',
'abilities required to escape from it.',
'',
'Relics format:',
' r[:[@]<location>[:<ability>[-<ability>...]]'
+ '[+<ability>[-<ability>...]]][:...]',
'',
'Relic locations and the abilities they provide are identified by one',
'letter:',
' (' + constants.RELIC.SOUL_OF_BAT + ') Soul of Bat',
' (' + constants.RELIC.FIRE_OF_BAT + ') Fire of Bat',
' (' + constants.RELIC.ECHO_OF_BAT + ') Echo of Echo',
' (' + constants.RELIC.FORCE_OF_ECHO + ') Force of Echo',
' (' + constants.RELIC.SOUL_OF_WOLF + ') Soul of Wolf',
' (' + constants.RELIC.POWER_OF_WOLF + ') Power of Wolf',
' (' + constants.RELIC.SKILL_OF_WOLF + ') Skill of Wolf',
' (' + constants.RELIC.FORM_OF_MIST + ') Form of Mist',
' (' + constants.RELIC.POWER_OF_MIST + ') Power of Mist',
' (' + constants.RELIC.GAS_CLOUD + ') Gas Cloud',
' (' + constants.RELIC.CUBE_OF_ZOE + ') Cube of Zoe',
' (' + constants.RELIC.SPIRIT_ORB + ') Spirit Orb',
' (' + constants.RELIC.GRAVITY_BOOTS + ') Gravity Boots',
' (' + constants.RELIC.LEAP_STONE + ') Leap Stone',
' (' + constants.RELIC.HOLY_SYMBOL + ') Holy Symbol',
' (' + constants.RELIC.FAERIE_SCROLL + ') Faerie Scroll',
' (' + constants.RELIC.JEWEL_OF_OPEN + ') Jewel of Open',
' (' + constants.RELIC.MERMAN_STATUE + ') Merman Statue',
' (' + constants.RELIC.BAT_CARD + ') Bat Card',
' (' + constants.RELIC.GHOST_CARD + ') Ghost Card',
' (' + constants.RELIC.FAERIE_CARD + ') Faerie Card',
' (' + constants.RELIC.DEMON_CARD + ') Demon Card',
' (' + constants.RELIC.SWORD_CARD + ') Sword Card',
' (' + constants.RELIC.SPRITE_CARD + ') Sprite Card',
' (' + constants.RELIC.NOSEDEVIL_CARD + ') Nosedevil Card',
' (' + constants.RELIC.HEART_OF_VLAD + ') Heart of Vlad',
' (' + constants.RELIC.TOOTH_OF_VLAD + ') Tooth of Vlad',
' (' + constants.RELIC.RIB_OF_VLAD + ') Rib of Vlad',
' (' + constants.RELIC.RING_OF_VLAD + ') Ring of Vlad',
' (' + constants.RELIC.EYE_OF_VLAD + ') Eye of Vlad',
' (' + constants.RELIC.SPIKE_BREAKER + ') Spike Breaker',
' (' + constants.RELIC.SILVER_RING + ') Silver ring',
' (' + constants.RELIC.GOLD_RING + ') Gold ring',
' (' + constants.RELIC.HOLY_GLASSES + ') Holy glasses',
'',
'Examples:',
' r:B:L Soul of Bat relic location requires only Leap Stone',
' r:y:LV-MP Holy Symbol relic location requires Leap Stone + Gravity',
' Boots OR Form of Mist + Power of Mist',
'',
'Note that relic location extensions use the name of the item being',
'replaced as their identifier:',
' r:Mormegil:JL-JV Mormegil location requires Jewel of Open + Leap Stone',
' OR Jewel of Open + Gravity Boots',
'',
'Escape requirements follow the ability locks and are separated by a "+":',
' r:H:GS+B-LV-MP Holy Glasses location requires Gold + Silver Rings for',
' access and Soul of Bat, Leap Stone + Gravity Boots, or',
' Mist + Power of Mist for escape.',
'',
'Locks for different locations can be specified by separating each',
'location by a colon:',
' r:B:L:y:LV-MP',
'',
'Relic locations extension can be specified with the letter "x". Extension',
'will allow progression to be placed in locations that do not contain',
'progression in the vanilla game.',
'',
'There are three extension modes:',
' guarded Adds Crystal cloak, Mormegil, Dark Blade, and Ring of Arcana',
' to the location pool. This is the default extension mode when',
' when enabled without an argument.',
' guardedplus Based on guarded, and adds Forbidden Library Opal and Badelaire',
' to the location pool.',
' equipment Adds remaining equipment tiles to the location pool.',
' scenic Adds many scenic locationss to the location pool.',
' extended a compromise between guardedPlus, equipment, and scenic creating',
' creating a "unique locations" variant with a moderate number of',
' checks.',
'',
'Extension format:',
' x:<mode>',
'',
'Examples:',
' r:x:guarded Enables guarded extension mode',
' r:x:equipment Enables equipment extension mode',
'',
'Additionally there are items to provide abilities that do not have a',
'dedicated vanilla location.',
' (' + constants.RELIC.THRUST_SWORD + ') Thrust sword',
'',
'These ability items can be added to the relic placement logic by',
'specifying their ability letter:',
' r:D:M:D-L Enable Thrust sword placement and have Form of Mist location',
' require a Thrust sword or Leap Stone',
'',
'An optional complexity target can specify a set of abilities that are',
'considered win conditions. A minimum and maximum complexity depth specify',
'how many relics must be obtained in series to unlock a win condition:',
' r:3:LV-MP Leap Stone + Gravity Boots OR Form of Mist + Power of Mist',
' required to complete seed with a minimum depth of .',
' r:3-5:SG Silver + Gold ring required to complete seed with a minimum',
' depth of 3 and a maximum depth of 5',
'',
'Relics can be placed in an explicit location by prefixing a location with',
'the "@" symbol. Multiple relics may be specified, however, only one will',
'be selected for that location at the time of seed generation. Ability',
'locks and placed relics may be freely mixed in together:',
' r:B:L:@B:fe Soul of Bat location requires Leap Stone and may contain',
' either Fire of Bat or Force of Echo',
'',
'A placed relic location may also be "empty". To specify an empty location,',
'include a "0" in the relic list for that location. Note that relic',
'locations must be extended when allowing a location to be empty:',
' r:x:guarded:@B:0 Soul of Bat location is empty',
' r:x:guarded:@y:fe0 Holy Symbol location may be empty or contain Fire of',
' Bat or Force of Echo',
'',
'Relics may be blocked from being randomized to a location by prefxing it',
'with a hypen ("-"):',
' r:-J:0BLG Never let Jewel of Open location be empty, or have Soul of',
' Bat, Leap Stone, or Gravity Boots',
'',
'A relic can be replaced with an arbitrary item by prefixing its ability',
'with the "=" symbol:',
' r:=z:Duplicator Replace Cube of Zoe with Duplicator',
'',
'Placing progression items in vanilla relic locations requires an item in',
'that zone to be removed. A player may notice a removed item in a zone',
'and correctly assume that a progression item has been randomized to a',
'location in that zone. To prevent this leakage of information, the default',
'behavior of the relic randomizer is to remove at most 3 random items from',
'every zone containing relics. This behavior can be turned off by including',
'the string "~r" as an argument:',
' r:~r Disable leak prevention',
'',
'If other randomization options follow a lock, they must also be',
'separated from the lock with a comma:',
' $0 --opt r:B:L:y:LG-MP,dpt',
].join('\n')
const writesHelp = [
'Arbitrary data can be written using the "w" option with argument syntax.',
'',
'Write format:',
' w:address:value[:address:value[:...]]',
'',
'Addresses should be either decimal numbers or "0x"-prefixed hex strings.',
'',
'Values are either "0x"-prefixed hex strings or unprefixed hex strings.',
'Use an unprefixed hex string to specify a string of bytes.',
'Use a prefixed hex string to specify a number written as little-endian.',
'The width of the number written is determined by the length of the hex.',
'To write a character, the hex must be 2 characters. To write a short, the',
'hex must be 4 characters. To write a word, the hex must be 8 characters.',
'To write a long, the hex must be 16 characters. A prefixed hex string of',
'any other character length is erroneous.',
'',
'Additionally, random data can be written by specifying a value of "rc" for',
'a random character (1 byte), "rs" for a random short (2 bytes), "rw" for a',
'random word (4 bytes), or "rl" for a random long (8 bytes).',
'',
'Examples:',
' w:0x04c590:0x00 Write the character 0x00 to the address',
' 0x04c590',
' w:0x043930c4:0x78b4 Write the value 0x78b4 as a little-endian',
' short integer to the address 0x043930c4',
' w:0x032b08:0x08075180 Write the value 0x08075180 as a little-',
' endian integer to the address 0x032b08',
' w:0x0abb28:0x00:0x0abb2a:0x01 Write the characters 0x00 and 0x01 to the',
' addresses 0x0abb28 and 0x0abb2a',
' respectively',
' w:0x04389c6c:74657374ff Write the string 74657374ff to the',
' address 0x04389c6c',
' w:0x04937fb4:rc Write a random byte to the address',
' 0x04937fb4',
' w:0x0456a274:rs Write 2 random bytes to the address',
' 0x0456a274',
' w:0x0456b888:rw Write 4 random bytes to the address',
' 0x0456b888',
' w:0x049f4a98:rl Write 8 random bytes to the address',
' 0x049f4a98',
'',
'If other randomization options follow a write, they must also be separated',
'from the write with a comma:',
' $0 --opt w:0x0abb28:0x00:0x0abb2a:0x01,dpt',
].join('\n')
const tournamentHelp = [
'Tournament mode applies the following:',
'- Spoiler log verbosity maximum is 2 (seed and starting equipment).',
'- The library shop relic is free.',
'- The clock room statue is always open.',
].join('\n')
function presetMetaHelp(preset) {
const options = preset.options()
let locations = relics.filter(function(relic) {
return !relic.extension && relic.ability !== constants.RELIC.THRUST_SWORD
})
const extensions = []
if (typeof(options.relicLocations) === 'object'
&& 'extension' in options.relicLocations) {
switch (options.relicLocations.extension) {
case constants.EXTENSION.EXTENDED:
extensions.push(constants.EXTENSION.EXTENDED)
break
case constants.EXTENSION.SCENIC:
extensions.push(constants.EXTENSION.SCENIC)
case constants.EXTENSION.EQUIPMENT:
extensions.push(constants.EXTENSION.EQUIPMENT)
case constants.EXTENSION.GUARDEDPLUS:
extensions.push(constants.EXTENSION.GUARDEDPLUS)
case constants.EXTENSION.GUARDED:
extensions.push(constants.EXTENSION.GUARDED)
}
}
const extendedLocations = extension.filter(function(location) {
return extensions.indexOf(location.extension) !== -1
})
locations = locations.concat(extendedLocations)
locations = locations.map(function(location) {
let id
if ('ability' in location) {
id = location.ability
} else {
id = location.name
}
return {
id: id,
name: location.name,
ability: location.ability,
}
})
let info = [
preset.name + ' by ' + preset.author,
preset.description,
'',
].concat(locations.map(function(location) {
let label
if (location.ability) {
label = ' (' + location.ability + ') ' + location.name.slice(0, 21)
} else {
label = ' ' + location.name.slice(0, 21)
}
label += Array(28).fill(' ').join('')
let locks
let escapes
if (options.relicLocations[location.id]) {
locks = options.relicLocations[location.id].filter(function(lock) {
return lock[0] !== '+'
})
escapes = options.relicLocations[location.id].filter(function(lock) {
return lock[0] === '+'
}).map(function(lock) {
return lock.slice(1)
})
}
return label.slice(0, 28) + location.id.replace(/[^a-zA-Z0-9]/g, '') + ':'
+ (locks ? locks.join('-') : '')
+ (escapes && escapes.length ? '+' + escapes.join('-') : '')
}))
const keys = Object.getOwnPropertyNames(options.relicLocations)
const target = keys.filter(function(key) {
return /^[0-9]+(-[0-9]+)?$/.test(key)
}).pop()
if (target) {
const parts = target.split('-')
info.push('')
info.push(' Complexity target: '
+ parts[0] + ' <= depth'
+ (parts.length === 2 ? ' <= ' + parts[1] : ''))
info.push(' Goals: ' + options.relicLocations[target].join('-'))
}
return info.join('\n')
}
let eccEdcCalc
const yargs = require('yargs')
.strict()
.usage('$0 [options] [url]')
.option('in-bin', {
alias: 'i',
describe: 'Path to vanilla .bin file',
conflicts: ['no-seed'],
type: 'string',
requiresArg: true,
})
.option('out', {
alias: 'o',
describe: [
'If used with `in-bin` option, path to write randomized .bin file, ',
'otherwise, path to write PPF file',
].join(''),
type: 'string',
requiresArg: true,
})
.option('seed', {
alias: 's',
describe: 'Randomization seed',
type: 'string',
requiresArg: true,
})
.option('options', {
alias: 'opt',
describe: 'Randomizations (`--help options`)',
type: 'string',
requiresArg: true,
})
.option('expect-checksum', {
alias: 'e',
describe: 'Verify checksum',
conflicts: ['no-seed'],
type: 'string',
requiresArg: true,
})
.option('url', {
alias: 'u',
description: 'Print seed url using optional base',
type: 'string',
})
.option('race', {
alias: 'r',
describe: 'Same as -uvv',
type: 'boolean',
})
.option('preset', {
alias: 'p',
describe: 'Use preset',
type: 'string',
requiresArg: true,
})
.option('preset-file', {
alias: 'f',
describe: 'Use preset file',
type: 'string',
requiresArg: true,
conflicts: ['preset'],
})
.option('complexity', {
alias: 'c',
describe: 'Shortcut to adjust seed complexity',
type: 'number',
requiresArg: true,
})
.option('tournament', {
alias: 't',
describe: 'Enable tournament mode (`--help tournament`)',
type: 'boolean',
})
.option('colorrando', {
alias: 'l',
describe: 'Enable color palette randomizing for various things',
type: 'boolean',
})
.option('magicmax', {
alias: 'x',
describe: 'Enable replace Heart Max with Magic Max Vessels',
type: 'boolean',
})
.option('antifreeze', {
alias: 'z',
describe: 'Enable Anti-Freeze Mode, removes screen freezes from level-up & more.',
type: 'boolean',
})
.option('mypurse', {
alias: 'y',
describe: 'Prevents Death from stealing your belongings.',
type: 'boolean',
})
.option('iws', {
alias: 'b',
describe: 'Makes wing smash essentially infinite.',
type: 'boolean',
})
.option('fastwarp', {
alias: '9',
describe: 'Quickens warp animation when using teleporters.',
type: 'boolean',
})
.option('noprologue', {
alias: 'R',
describe: 'Quickens warp animation when using teleporters.',
type: 'boolean',
})
.option('unlocked', {
alias: 'U',
describe: 'Unlocks all shortcuts.',
type: 'boolean',
})
.option('surprise', {
alias: 'S',
describe: 'Hides relics behind the same sprite and palette forcing the player to collect the relic to find out what it is.',
type: 'boolean',
})
.option('enemyStatRando', {
alias: 'E',
describe: 'Enemy stats are randomized ranging from 25% to 200% of their original value and their attack and defense types are randomized to include random elements.',
type: 'boolean',
})
.option('shopPriceRando', {
alias: 'sh',
describe: 'Shop prices are randomized ranging from 50% to 150% of their original value and they are shuffled between each other.',
type: 'boolean',
})
.option('startRoomRando', {
alias: 'ori',
describe: 'Start in the entrance as usual but after the first Warg, you are teleported to a random zone to start the rest of your run.',
type: 'boolean',
})
.option('debug', {
alias: 'D',
describe: 'Debug mode.',
type: 'boolean',
})
.option('mapcolor', {
alias: 'm',
describe: 'Change map color',
type: 'string',
requiresArg: true,
})
.option('excludesongs', {
alias: 'eds',
describe: 'Excludes the songs given by parameters. Example: --eds LOST_PAINTING,CURSE_ZONE',
type: 'string',
requiresArg: true,
})
.option('disable-accessibility-patches', {
alias: 'a',
describe: 'Disable accessibility patches',
type: 'boolean',
})
.option('no-seed', {
alias: 'n',
describe: 'Disable seed generation',
conflicts: ['in-bin', 'expect-checksum'],
type: 'boolean',
})
.option('verbose', {
alias: 'v',
describe: 'Verbosity level',
type: 'count',
default: undefined,
})
.option('quiet', {
alias: 'q',
describe: 'Suppress output',
conflicts: 'verbose',
type: 'boolean',
})
.option('compat', {
type: 'string',
requiresArg: true,
})
.hide('compat')
.help(false)
.option('help', {
alias: 'h',
describe: 'Show help',
type: 'string',
})
.demandCommand(0, 1)
const argv = yargs.argv
let options
let seed
let baseUrl
let expectChecksum
let haveChecksum
// Require at least one argument.
if (process.argv.length < 3) {
yargs.showHelp()
console.error('\nAt least 1 argument or option required')
process.exit(1)
}
// check for debug
if ('debug' in argv) {
if ('tournament' in argv) {
console.error('\nYou cannot run debug mode at the same time as tournament mode.')
process.exit(1)
} else {
console.log('Begin Debug output:')
}
}
if ('debug' in argv) {console.log('randomize | Checking for help flags')}
// Check for help.
if ('help' in argv) {
if (!argv.help) {
yargs.showHelp()
process.exit()
}
const presets = require('./build/presets')
const presetHelp = [
'Presets specify collection of randomization options. A preset is enabled',
'by using argument syntax.',
'',
'Preset format:',
' p:<preset>',
'',
'This randomizer has several built-in presets:',
].concat(presets.filter(function(preset) {
return !preset.hidden
}).map(function(preset) {
return ' ' + preset.id + (preset.id === 'safe' ? ' (default)' : '')
})).concat([
'',
'Use `--help <preset>` for information on a specific preset.',
'',
'Examples:',
' p:safe Use Safe preset',
' p:empty-hand Use Empty Hand preset',
'',
'When using the `$0` utility, you can use the `--preset` shorthand to',
'specify a preset:',
' $0 -p adventure # Use adventure preset',
'',
'Preset options may be overridden by specifying an options string:',
' $0 -p adventure --opt d:*:Peanuts- # Adventure with only Peanut drops',
'',
'A special negation syntax can be used in the options string to disable',
'randomizations that a preset would otherwise enable. To negate a',
'randomization, precede its letter with a tilde ("~"):',
' $0 -p adventure --opt ~m # Adventure but without music randomization',
]).join('\n')
const topics = {
options: optionsHelp,
drops: dropsHelp,
equipment: equipmentHelp,
items: itemsHelp,
rewards: rewardsHelp,
relics: relicsHelp,
writes: writesHelp,
tournament: tournamentHelp,
preset: presetHelp,
}
const script = path.basename(process.argv[1])
Object.getOwnPropertyNames(topics).forEach(function(topic) {
topics[topic] = topics[topic].replace(/\$0/g, script)
}, {})
presets.forEach(function(preset) {
if (!preset.hidden) {
topics[preset.id] = presetMetaHelp(preset)
}
})
if (argv.help in topics) {
console.log(topics[argv.help])
process.exit()
} else {
yargs.showHelp()
console.error('\nUnknown help topic: ' + argv.help)
process.exit(1)
}
}
if (argv.compat) {
version = argv.compat
}
if ('debug' in argv) {console.log('randomize | Checking for seed string')}
// Check for seed string.
if ('seed' in argv) {
if ('noSeed' in argv) {
yargs.showHelp()
console.error('\nCannot specify seed if seed generation is disabled')
process.exit(1)
}
seed = argv.seed.toString()
}
if ('debug' in argv) {console.log('randomize | Checking for base URL')}
// Check for base url.
if (argv.url) {
baseUrl = argv.url
}
// If seed generation is disabled, assume url output.
if (argv.noSeed) {
argv.url = ''
}
if ('debug' in argv) {console.log('randomize | Checking for expected checksum')}
// Check for expected checksum.
if ('expectChecksum' in argv) {
if (!('seed' in argv) && !argv._[0]) {
yargs.showHelp()
console.error('\nCannot specify checksum if not providing seed')
process.exit(1)
}
if (!argv.expectChecksum.match(/^[0-9a-f]{1,3}$/)) {
yargs.showHelp()
console.error('\nInvalid checksum string')
process.exit(1)
}
expectChecksum = parseInt(argv.expectChecksum, 16)
haveChecksum = true
}
if ('debug' in argv) {console.log('randomize | Checking for randomization string from CLI')}
// Check for randomization string.
if ('options' in argv) {
try {
options = util.optionsFromString(argv.options)
} catch (e) {
yargs.showHelp()
console.error('\n' + e.message)
process.exit(1)
}
}
if ('debug' in argv) {console.log('randomize | Checking for preset name')}
// Check for preset.
if ('preset' in argv) {
try {
if (options && 'preset' in options && options.preset !== argv.preset) {
throw new Error('Command line option preset conflits with options '
+ 'string preset')
}
options = Object.assign(
options || {},
util.optionsFromString('p:' + argv.preset)
)
process.env.chosenPreset = argv.preset
} catch (e) {
yargs.showHelp()
console.error('\n' + e.message)
process.exit(1)
}
}
// Check for preset file.
if ('presetFile' in argv) {
if (options && 'preset' in options) {
yargs.showHelp()
console.error('\nCannot specify options string preset when using a preset '
+ 'file')
process.exit(1)
}
const relative = path.relative(path.dirname(__filename), argv.presetFile)
const preset = require('./' + relative)
options = Object.assign(
options || {},
util.PresetBuilder.fromJSON(preset).build().options()
)
}
// If a preset and an options string are specified, determine if the options
// are just duplicate options of the preset.
if (options && 'preset' in options
&& Object.getOwnPropertyNames(options).length > 1) {
try {
const applied = util.Preset.options(options)
const preset = util.presetFromName(options.preset)
if (util.optionsToString(preset.options())
=== util.optionsToString(applied)) {
// Options string has duplicative values, so just make the options
// specifying the preset name.
options = {preset: preset.id}
} else {
// Options string overrides the preset, so use the applied options.
options = applied
}
} catch (err) {
yargs.showHelp()
console.error('\n' + err.message)
process.exit(1)
}
}
if ('debug' in argv) {console.log('randomize | Assume safe if no preset')}
// Assume safe if negations are specified without a preset.
if (options) {
const copy = Object.assign({}, options)
Object.getOwnPropertyNames(copy).forEach(function(opt) {
if (copy[opt] === false) {
delete copy[opt]
}
})
if (Object.getOwnPropertyNames(copy).length === 0) {
options.preset = 'safe'
}
}
if ('debug' in argv) {console.log('randomize | Checking for URL seed')}
// Check for seed url.
if (argv._[0]) {
if ('noSeed' in argv) {
yargs.showHelp()
console.error('\nCannot specify url if seed generation is disabled')
process.exit(1)
}
if ('presetFile' in argv) {
yargs.showHelp()
console.error('\nCannot specify url if using a preset file')
process.exit(1)
}
let url
try {
url = util.optionsFromUrl(argv._[0])
argv.race = true
options = url.options
seed = url.seed
expectChecksum = url.checksum
if (expectChecksum) {
haveChecksum = true
}
} catch (e) {
yargs.showHelp()
console.error('\nInvalid url')
process.exit(1)
}
if (seed === null) {
yargs.showHelp()
console.error('\nUrl does not contain seed')
process.exit(1)
}
// Ensure seeds match if given using --seed.
if ('seed' in argv && argv.seed.toString() !== seed) {
yargs.showHelp()
console.error('\nArgument seed is not url seed')
process.exit(1)
}
if ('debug' in argv) {console.log('randomize | Checking for options matching arguments')}
// Ensure randomizations match if given using --options.
const optionStr = util.optionsToString(options)
if (('options' in argv && argv.options !== optionStr)
|| ('preset' in argv && 'p:' + argv.preset !== optionStr)) {
yargs.showHelp()
console.error('\nArgument randomizations are not url randomizations')
process.exit(1)
}
if ('debug' in argv) {console.log('randomize | Checking for expected checksum matching checksum')}
// Ensure checksum match if given using --expect-checksum.
if ('expectChecksum' in argv && url.checksum != expectChecksum) {
yargs.showHelp()
console.error('\nArgument checksum is not url checksum')
process.exit(1)
}
}
if ('debug' in argv) {console.log('randomize | Enable race options')}
// Set options for --race.
if (argv.race) {
argv.url = ''
if (argv.verbose === undefined) {
argv.verbose = 2
}
}
if ('debug' in argv) {console.log('randomize | Enable quiet option')}
// Suppress output if quiet argument specified.
if (argv.quiet) {
argv.verbose = 0
}
if ('debug' in argv) {console.log('randomize | Enable option defaults if non specified')}
// Create default options if none provided.
if (typeof(seed) === 'undefined' && !argv.noSeed) {
seed = (new Date()).getTime().toString()
}
if (!options) {
options = util.optionsFromString(constants.defaultOptions)
}
if ('debug' in argv) {console.log('randomize | Set complexity')}
// Check for complexity setting.
if ('complexity' in argv) {
let applied = Object.assign({}, options)
// Check for preset.
if ('preset' in applied) {
applied = util.Preset.options(applied)
} else if (!('relicLocations' in options)) {
appield = util.Preset.options(Object.assign({preset: 'safe'}, options))
}
if (typeof applied.relicLocations !== 'object') {
if (applied.relicLocations) {
// Inherit safe relic locations.
const logic = util.presetFromName('safe').options().relicLocations
applied.relicLocations = logic
} else {
yargs.showHelp()
console.error('\nRelic location randomization must be enabled to set ' +
'complexity')
process.exit(1)
}
}
if ('debug' in argv) {console.log('randomize | Set seed goals')}
// Get seed goals.
let complexity = Object.getOwnPropertyNames(applied.relicLocations).filter(
function(key) {
return /^[0-9]+$/.test(key)
}
)
if (complexity.length) {