Ocarina of Time: long-awaited bugfixes (#2344)
- Added location name groups, so you can make your entire Water Temple priority to annoy everyone else - Significant improvement to ER generation success rate (~80% to >99%) - Changed `adult_trade_start` option to a choice option instead of a list (this shouldn't actually break any YAMLs though, due to the lesser-known property of lists parsing as a uniformly-weighted choice) - Major improvements to the option tooltips where needed. (Possibly too much text now) - Changed default hint distribution to `async` to help people's generation times. The tooltip explains that it removes WOTH hints so people hopefully don't get tripped up. - Makes stick and nut capacity upgrades useful items - Added shop prices and required trials to spoiler log - Added Cojiro to adult trade item group, because it had been forgotten previously - Fixed size-modified chests not being moved properly due to trap appearance changing the size - Fixed Thieves Hideout keyring not being allowed in start inventory - Fixed hint generation not accurately flagging barren locations on certain dungeon item shuffle settings - Fixed bug where you could plando arbitrarily-named items into the world, breaking everything
This commit is contained in:
		
							parent
							
								
									50244342d9
								
							
						
					
					
						commit
						724999fc43
					
				|  | @ -350,7 +350,7 @@ def generate_itempool(ootworld): | ||||||
|     ootworld.itempool = [ootworld.create_item(item) for item in pool] |     ootworld.itempool = [ootworld.create_item(item) for item in pool] | ||||||
|     for (location_name, item) in placed_items.items(): |     for (location_name, item) in placed_items.items(): | ||||||
|         location = world.get_location(location_name, player) |         location = world.get_location(location_name, player) | ||||||
|         location.place_locked_item(ootworld.create_item(item)) |         location.place_locked_item(ootworld.create_item(item, allow_arbitrary_name=True)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_pool_core(world): | def get_pool_core(world): | ||||||
|  | @ -675,7 +675,7 @@ def get_pool_core(world): | ||||||
|         world.remove_from_start_inventory.append('Scarecrow Song') |         world.remove_from_start_inventory.append('Scarecrow Song') | ||||||
|      |      | ||||||
|     if world.no_epona_race: |     if world.no_epona_race: | ||||||
|         world.multiworld.push_precollected(world.create_item('Epona')) |         world.multiworld.push_precollected(world.create_item('Epona', allow_arbitrary_name=True)) | ||||||
|         world.remove_from_start_inventory.append('Epona') |         world.remove_from_start_inventory.append('Epona') | ||||||
| 
 | 
 | ||||||
|     if world.shuffle_smallkeys == 'vanilla': |     if world.shuffle_smallkeys == 'vanilla': | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ from enum import Enum | ||||||
| from .LocationList import location_table | from .LocationList import location_table | ||||||
| from BaseClasses import Location | from BaseClasses import Location | ||||||
| 
 | 
 | ||||||
|  | non_indexed_location_types = {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'} | ||||||
|  | 
 | ||||||
| location_id_offset = 67000 | location_id_offset = 67000 | ||||||
| locnames_pre_70 = { | locnames_pre_70 = { | ||||||
|     "Gift from Sages", |     "Gift from Sages", | ||||||
|  | @ -18,7 +20,7 @@ new_name_order = sorted(location_table.keys(), | ||||||
|                 else 0) |                 else 0) | ||||||
| 
 | 
 | ||||||
| location_name_to_id = {name: (location_id_offset + index) for (index, name) in enumerate(new_name_order)  | location_name_to_id = {name: (location_id_offset + index) for (index, name) in enumerate(new_name_order)  | ||||||
|     if location_table[name][0] not in {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'}} |     if location_table[name][0] not in non_indexed_location_types} | ||||||
| 
 | 
 | ||||||
| class DisableType(Enum): | class DisableType(Enum): | ||||||
|     ENABLED  = 0 |     ENABLED  = 0 | ||||||
|  | @ -83,3 +85,57 @@ def LocationFactory(locations, player: int): | ||||||
|     return ret |     return ret | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def build_location_name_groups() -> dict: | ||||||
|  | 
 | ||||||
|  |     def fix_sing(t) -> tuple: | ||||||
|  |         if isinstance(t, str): | ||||||
|  |             return (t,) | ||||||
|  |         return t | ||||||
|  | 
 | ||||||
|  |     def rename(d, k1, k2) -> None: | ||||||
|  |         d[k2] = d[k1] | ||||||
|  |         del d[k1] | ||||||
|  | 
 | ||||||
|  |     # whoever wrote the location table didn't realize they need to add a comma to mark a singleton as a tuple | ||||||
|  |     # so we have to check types unfortunately | ||||||
|  |     tags = set() | ||||||
|  |     for v in location_table.values(): | ||||||
|  |         if v[5] is not None: | ||||||
|  |             tags.update(fix_sing(v[5])) | ||||||
|  | 
 | ||||||
|  |     sorted_tags = sorted(list(tags)) | ||||||
|  | 
 | ||||||
|  |     ret = { | ||||||
|  |         tag: {k for k, v in location_table.items() | ||||||
|  |         if v[5] is not None | ||||||
|  |             and tag in fix_sing(v[5]) | ||||||
|  |             and v[0] not in non_indexed_location_types} | ||||||
|  |         for tag in sorted_tags | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     # Delete tags which are a combination of other tags | ||||||
|  |     del ret['Death Mountain'] | ||||||
|  |     del ret['Forest'] | ||||||
|  |     del ret['Gerudo'] | ||||||
|  |     del ret['Kakariko'] | ||||||
|  |     del ret['Market'] | ||||||
|  | 
 | ||||||
|  |     # Delete Vanilla and MQ tags because they are just way too broad | ||||||
|  |     del ret['Vanilla'] | ||||||
|  |     del ret['Master Quest'] | ||||||
|  | 
 | ||||||
|  |     rename(ret, 'Beehive', 'Beehives') | ||||||
|  |     rename(ret, 'Cow', 'Cows') | ||||||
|  |     rename(ret, 'Crate', 'Crates') | ||||||
|  |     rename(ret, 'Deku Scrub', 'Deku Scrubs') | ||||||
|  |     rename(ret, 'FlyingPot', 'Flying Pots') | ||||||
|  |     rename(ret, 'Freestanding', 'Freestanding Items') | ||||||
|  |     rename(ret, 'Pot', 'Pots') | ||||||
|  |     rename(ret, 'RupeeTower', 'Rupee Groups') | ||||||
|  |     rename(ret, 'SmallCrate', 'Small Crates') | ||||||
|  |     rename(ret, 'the Market', 'Market') | ||||||
|  |     rename(ret, 'the Graveyard', 'Graveyard') | ||||||
|  |     rename(ret, 'the Lost Woods', 'Lost Woods') | ||||||
|  | 
 | ||||||
|  |     return ret | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -238,7 +238,7 @@ location_table = OrderedDict([ | ||||||
|     ("Market Night Green Rupee Crate 1",                             ("Crate",        0x21,  (0,0,24), None,                        'Rupee (1)',                             ("the Market", "Market", "Crate"))), |     ("Market Night Green Rupee Crate 1",                             ("Crate",        0x21,  (0,0,24), None,                        'Rupee (1)',                             ("the Market", "Market", "Crate"))), | ||||||
|     ("Market Night Green Rupee Crate 2",                             ("Crate",        0x21,  (0,0,25), None,                        'Rupee (1)',                             ("the Market", "Market", "Crate"))), |     ("Market Night Green Rupee Crate 2",                             ("Crate",        0x21,  (0,0,25), None,                        'Rupee (1)',                             ("the Market", "Market", "Crate"))), | ||||||
|     ("Market Night Green Rupee Crate 3",                             ("Crate",        0x21,  (0,0,26), None,                        'Rupee (1)',                             ("the Market", "Market", "Crate"))), |     ("Market Night Green Rupee Crate 3",                             ("Crate",        0x21,  (0,0,26), None,                        'Rupee (1)',                             ("the Market", "Market", "Crate"))), | ||||||
|     ("Market Dog Lady House Crate",                                  ("Crate",        0x35,  (0,0,3), None,                         'Rupees (5)',                            ("Market", "Market", "Crate"))), |     ("Market Dog Lady House Crate",                                  ("Crate",        0x35,  (0,0,3), None,                         'Rupees (5)',                            ("the Market", "Market", "Crate"))), | ||||||
|     ("Market Guard House Child Crate",                               ("Crate",        0x4D,  (0,0,6), None,                         'Rupee (1)',                             ("the Market", "Market", "Crate"))), |     ("Market Guard House Child Crate",                               ("Crate",        0x4D,  (0,0,6), None,                         'Rupee (1)',                             ("the Market", "Market", "Crate"))), | ||||||
|     ("Market Guard House Child Pot 1",                               ("Pot",          0x4D,  (0,0,9), None,                         'Rupee (1)',                             ("the Market", "Market", "Pot"))), |     ("Market Guard House Child Pot 1",                               ("Pot",          0x4D,  (0,0,9), None,                         'Rupee (1)',                             ("the Market", "Market", "Pot"))), | ||||||
|     ("Market Guard House Child Pot 2",                               ("Pot",          0x4D,  (0,0,10), None,                        'Rupee (1)',                             ("the Market", "Market", "Pot"))), |     ("Market Guard House Child Pot 2",                               ("Pot",          0x4D,  (0,0,10), None,                        'Rupee (1)',                             ("the Market", "Market", "Pot"))), | ||||||
|  |  | ||||||
|  | @ -30,7 +30,17 @@ class TrackRandomRange(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Logic(Choice):  | class Logic(Choice):  | ||||||
|     """Set the logic used for the generator.""" |     """Set the logic used for the generator. | ||||||
|  |     Glitchless: Normal gameplay. Can enable more difficult logical paths using the Logic Tricks option. | ||||||
|  |     Glitched: Many powerful glitches expected, such as bomb hovering and clipping. | ||||||
|  |     Glitched is incompatible with the following settings: | ||||||
|  |     - All forms of entrance randomizer | ||||||
|  |     - MQ dungeons | ||||||
|  |     - Pot shuffle | ||||||
|  |     - Freestanding item shuffle | ||||||
|  |     - Crate shuffle | ||||||
|  |     - Beehive shuffle | ||||||
|  |     No Logic: No logic is used when placing items. Not recommended for most players.""" | ||||||
|     display_name = "Logic Rules" |     display_name = "Logic Rules" | ||||||
|     option_glitchless = 0 |     option_glitchless = 0 | ||||||
|     option_glitched = 1 |     option_glitched = 1 | ||||||
|  | @ -38,12 +48,16 @@ class Logic(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class NightTokens(Toggle): | class NightTokens(Toggle): | ||||||
|     """Nighttime skulltulas will logically require Sun's Song.""" |     """When enabled, nighttime skulltulas logically require Sun's Song.""" | ||||||
|     display_name = "Nighttime Skulltulas Expect Sun's Song" |     display_name = "Nighttime Skulltulas Expect Sun's Song" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Forest(Choice):  | class Forest(Choice):  | ||||||
|     """Set the state of Kokiri Forest and the path to Deku Tree.""" |     """Set the state of Kokiri Forest and the path to Deku Tree. | ||||||
|  |     Open: Neither the forest exit nor the path to Deku Tree is blocked. | ||||||
|  |     Closed Deku: The forest exit is not blocked; the path to Deku Tree requires Kokiri Sword and Deku Shield. | ||||||
|  |     Closed: Path to Deku Tree requires sword and shield. The forest exit is blocked until Deku Tree is beaten. | ||||||
|  |     Closed forest will force child start, and becomes Closed Deku if interior entrances, overworld entrances, warp songs, or random spawn positions are enabled.""" | ||||||
|     display_name = "Forest" |     display_name = "Forest" | ||||||
|     option_open = 0 |     option_open = 0 | ||||||
|     option_closed_deku = 1 |     option_closed_deku = 1 | ||||||
|  | @ -53,7 +67,10 @@ class Forest(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Gate(Choice):  | class Gate(Choice):  | ||||||
|     """Set the state of the Kakariko Village gate.""" |     """Set the state of the Kakariko Village gate for child. The gate is always open as adult. | ||||||
|  |     Open: The gate starts open. Happy Mask Shop opens upon receiving Zelda's Letter. | ||||||
|  |     Zelda: The gate and Mask Shop open upon receiving Zelda's Letter, without needing to show it to the guard. | ||||||
|  |     Closed: Vanilla behavior; the gate and Mask Shop open upon showing Zelda's Letter to the gate guard.""" | ||||||
|     display_name = "Kakariko Gate" |     display_name = "Kakariko Gate" | ||||||
|     option_open = 0 |     option_open = 0 | ||||||
|     option_zelda = 1 |     option_zelda = 1 | ||||||
|  | @ -61,12 +78,15 @@ class Gate(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DoorOfTime(DefaultOnToggle): | class DoorOfTime(DefaultOnToggle): | ||||||
|     """Open the Door of Time by default, without the Song of Time.""" |     """When enabled, the Door of Time starts opened, without needing Song of Time.""" | ||||||
|     display_name = "Open Door of Time" |     display_name = "Open Door of Time" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Fountain(Choice):  | class Fountain(Choice):  | ||||||
|     """Set the state of King Zora, blocking the way to Zora's Fountain.""" |     """Set the state of King Zora, blocking the way to Zora's Fountain. | ||||||
|  |     Open: King Zora starts moved as both ages. Ruto's Letter is removed. | ||||||
|  |     Adult: King Zora must be moved as child, but is always moved for adult. | ||||||
|  |     Closed: Vanilla behavior; King Zora must be shown Ruto's Letter as child to move him as both ages.""" | ||||||
|     display_name = "Zora's Fountain" |     display_name = "Zora's Fountain" | ||||||
|     option_open = 0 |     option_open = 0 | ||||||
|     option_adult = 1 |     option_adult = 1 | ||||||
|  | @ -75,7 +95,10 @@ class Fountain(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Fortress(Choice):  | class Fortress(Choice):  | ||||||
|     """Set the requirements for access to Gerudo Fortress.""" |     """Set the requirements for access to Gerudo Fortress. | ||||||
|  |     Normal: Vanilla behavior; all four carpenters must be rescued. | ||||||
|  |     Fast: Only one carpenter must be rescued, which is the one in the bottom-left of the fortress. | ||||||
|  |     Open: The Gerudo Valley bridge starts repaired. Gerudo Membership Card is given to start if not shuffled.""" | ||||||
|     display_name = "Gerudo Fortress" |     display_name = "Gerudo Fortress" | ||||||
|     option_normal = 0 |     option_normal = 0 | ||||||
|     option_fast = 1 |     option_fast = 1 | ||||||
|  | @ -84,7 +107,14 @@ class Fortress(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Bridge(Choice):  | class Bridge(Choice):  | ||||||
|     """Set the requirements for the Rainbow Bridge.""" |     """Set the requirements for the Rainbow Bridge. | ||||||
|  |     Open: The bridge is always present. | ||||||
|  |     Vanilla: Bridge requires Shadow Medallion, Spirit Medallion, and Light Arrows. | ||||||
|  |     Stones: Bridge requires a configurable amount of Spiritual Stones. | ||||||
|  |     Medallions: Bridge requires a configurable amount of medallions. | ||||||
|  |     Dungeons: Bridge requires a configurable amount of rewards (stones + medallions). | ||||||
|  |     Tokens: Bridge requires a configurable amount of gold skulltula tokens. | ||||||
|  |     Hearts: Bridge requires a configurable amount of hearts.""" | ||||||
|     display_name = "Rainbow Bridge Requirement" |     display_name = "Rainbow Bridge Requirement" | ||||||
|     option_open = 0 |     option_open = 0 | ||||||
|     option_vanilla = 1 |     option_vanilla = 1 | ||||||
|  | @ -122,8 +152,9 @@ class StartingAge(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class InteriorEntrances(Choice):  | class InteriorEntrances(Choice):  | ||||||
|     """Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House, |     """Shuffles interior entrances. | ||||||
|     Temple of Time, and Kak potion shop.""" |     Simple: Houses and Great Fairies are shuffled. | ||||||
|  |     All: In addition to Simple, includes Windmill, Link's House, Temple of Time, and the Kakariko potion shop.""" | ||||||
|     display_name = "Shuffle Interior Entrances" |     display_name = "Shuffle Interior Entrances" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_simple = 1 |     option_simple = 1 | ||||||
|  | @ -137,7 +168,9 @@ class GrottoEntrances(Toggle): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DungeonEntrances(Choice): | class DungeonEntrances(Choice): | ||||||
|     """Shuffles dungeon entrances. Opens Deku, Fire and BotW to both ages. "All" includes Ganon's Castle.""" |     """Shuffles dungeon entrances. When enabled, both ages will have access to Fire Temple, Bottom of the Well, and Deku Tree. | ||||||
|  |     Simple: Shuffle dungeon entrances except for Ganon's Castle. | ||||||
|  |     All: Include Ganon's Castle as well.""" | ||||||
|     display_name = "Shuffle Dungeon Entrances" |     display_name = "Shuffle Dungeon Entrances" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_simple = 1 |     option_simple = 1 | ||||||
|  | @ -146,7 +179,9 @@ class DungeonEntrances(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class BossEntrances(Choice): | class BossEntrances(Choice): | ||||||
|     """Shuffles boss entrances. "Limited" prevents age-mixing of bosses.""" |     """Shuffles boss entrances. | ||||||
|  |     Limited: Bosses will be limited to the ages that typically fight them. | ||||||
|  |     Full: Bosses may be fought as different ages than usual. Child can defeat Phantom Ganon and Bongo Bongo.""" | ||||||
|     display_name = "Shuffle Boss Entrances" |     display_name = "Shuffle Boss Entrances" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_limited = 1 |     option_limited = 1 | ||||||
|  | @ -178,19 +213,19 @@ class SpawnPositions(Choice): | ||||||
|     alias_true = 3 |     alias_true = 3 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MixEntrancePools(Choice): | # class MixEntrancePools(Choice): | ||||||
|     """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all" | #     """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all" | ||||||
|      mixes them in.""" | #      mixes them in.""" | ||||||
|     display_name = "Mix Entrance Pools" | #     display_name = "Mix Entrance Pools" | ||||||
|     option_off = 0 | #     option_off = 0 | ||||||
|     option_indoor = 1 | #     option_indoor = 1 | ||||||
|     option_all = 2 | #     option_all = 2 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DecoupleEntrances(Toggle): | # class DecoupleEntrances(Toggle): | ||||||
|     """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if | #     """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if | ||||||
|     overworld is shuffled.""" | #     overworld is shuffled.""" | ||||||
|     display_name = "Decouple Entrances" | #     display_name = "Decouple Entrances" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TriforceHunt(Toggle): | class TriforceHunt(Toggle): | ||||||
|  | @ -216,13 +251,17 @@ class ExtraTriforces(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LogicalChus(Toggle): | class LogicalChus(Toggle): | ||||||
|     """Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell |     """Bombchus are properly considered in logic. | ||||||
|     refills; bombchus open Bombchu Bowling.""" |     The first found pack will always have 20 chus.  | ||||||
|  |     Kokiri Shop and Bazaar will sell refills at reduced cost. | ||||||
|  |     Bombchus open Bombchu Bowling.""" | ||||||
|     display_name = "Bombchus Considered in Logic" |     display_name = "Bombchus Considered in Logic" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DungeonShortcuts(Choice): | class DungeonShortcuts(Choice): | ||||||
|     """Shortcuts to dungeon bosses are available without any requirements.""" |     """Shortcuts to dungeon bosses are available without any requirements. | ||||||
|  |     If enabled, this will impact the logic of dungeons where shortcuts are available. | ||||||
|  |     Choice: Use the option "dungeon_shortcuts_list" to choose shortcuts.""" | ||||||
|     display_name = "Dungeon Boss Shortcuts Mode" |     display_name = "Dungeon Boss Shortcuts Mode" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_choice = 1 |     option_choice = 1 | ||||||
|  | @ -246,7 +285,11 @@ class DungeonShortcutsList(OptionSet): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MQDungeons(Choice): | class MQDungeons(Choice): | ||||||
|     """Choose between vanilla and Master Quest dungeon layouts.""" |     """Choose between vanilla and Master Quest dungeon layouts. | ||||||
|  |     Vanilla: All layouts are vanilla. | ||||||
|  |     MQ: All layouts are Master Quest. | ||||||
|  |     Specific: Use the option "mq_dungeons_list" to choose which dungeons are MQ. | ||||||
|  |     Count: Use the option "mq_dungeons_count" to choose a number of random dungeons as MQ.""" | ||||||
|     display_name = "MQ Dungeon Mode" |     display_name = "MQ Dungeon Mode" | ||||||
|     option_vanilla = 0 |     option_vanilla = 0 | ||||||
|     option_mq = 1 |     option_mq = 1 | ||||||
|  | @ -255,7 +298,7 @@ class MQDungeons(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MQDungeonList(OptionSet): | class MQDungeonList(OptionSet): | ||||||
|     """Chosen dungeons to be MQ layout.""" |     """With MQ dungeons as Specific: chosen dungeons to be MQ layout.""" | ||||||
|     display_name = "MQ Dungeon List" |     display_name = "MQ Dungeon List" | ||||||
|     valid_keys = { |     valid_keys = { | ||||||
|         "Deku Tree", |         "Deku Tree", | ||||||
|  | @ -274,41 +317,41 @@ class MQDungeonList(OptionSet): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MQDungeonCount(TrackRandomRange): | class MQDungeonCount(TrackRandomRange): | ||||||
|     """Number of MQ dungeons, chosen randomly.""" |     """With MQ dungeons as Count: number of randomly-selected dungeons to be MQ layout.""" | ||||||
|     display_name = "MQ Dungeon Count" |     display_name = "MQ Dungeon Count" | ||||||
|     range_start = 0 |     range_start = 0 | ||||||
|     range_end = 12 |     range_end = 12 | ||||||
|     default = 0 |     default = 0 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class EmptyDungeons(Choice): | # class EmptyDungeons(Choice): | ||||||
|     """Pre-completed dungeons are barren and rewards are given for free.""" | #     """Pre-completed dungeons are barren and rewards are given for free.""" | ||||||
|     display_name = "Pre-completed Dungeons Mode" | #     display_name = "Pre-completed Dungeons Mode" | ||||||
|     option_none = 0 | #     option_none = 0 | ||||||
|     option_specific = 1 | #     option_specific = 1 | ||||||
|     option_count = 2 | #     option_count = 2 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class EmptyDungeonList(OptionSet): | # class EmptyDungeonList(OptionSet): | ||||||
|     """Chosen dungeons to be pre-completed.""" | #     """Chosen dungeons to be pre-completed.""" | ||||||
|     display_name = "Pre-completed Dungeon List" | #     display_name = "Pre-completed Dungeon List" | ||||||
|     valid_keys = { | #     valid_keys = { | ||||||
|         "Deku Tree", | #         "Deku Tree", | ||||||
|         "Dodongo's Cavern", | #         "Dodongo's Cavern", | ||||||
|         "Jabu Jabu's Belly", | #         "Jabu Jabu's Belly", | ||||||
|         "Forest Temple", | #         "Forest Temple", | ||||||
|         "Fire Temple", | #         "Fire Temple", | ||||||
|         "Water Temple", | #         "Water Temple", | ||||||
|         "Shadow Temple", | #         "Shadow Temple", | ||||||
|         "Spirit Temple", | #         "Spirit Temple", | ||||||
|     } | #     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class EmptyDungeonCount(Range): | # class EmptyDungeonCount(Range): | ||||||
|     display_name = "Pre-completed Dungeon Count" | #     display_name = "Pre-completed Dungeon Count" | ||||||
|     range_start = 1 | #     range_start = 1 | ||||||
|     range_end = 8 | #     range_end = 8 | ||||||
|     default = 2 | #     default = 2 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| world_options: typing.Dict[str, type(Option)] = { | world_options: typing.Dict[str, type(Option)] = { | ||||||
|  | @ -341,59 +384,8 @@ world_options: typing.Dict[str, type(Option)] = { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # class LacsCondition(Choice):  |  | ||||||
| #     """Set the requirements for the Light Arrow Cutscene in the Temple of Time.""" |  | ||||||
| #     display_name = "Light Arrow Cutscene Requirement" |  | ||||||
| #     option_vanilla = 0 |  | ||||||
| #     option_stones = 1 |  | ||||||
| #     option_medallions = 2 |  | ||||||
| #     option_dungeons = 3 |  | ||||||
| #     option_tokens = 4 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # class LacsStones(Range): |  | ||||||
| #     """Set the number of Spiritual Stones required for LACS.""" |  | ||||||
| #     display_name = "Spiritual Stones Required for LACS" |  | ||||||
| #     range_start = 0 |  | ||||||
| #     range_end = 3 |  | ||||||
| #     default = 3 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # class LacsMedallions(Range): |  | ||||||
| #     """Set the number of medallions required for LACS.""" |  | ||||||
| #     display_name = "Medallions Required for LACS" |  | ||||||
| #     range_start = 0 |  | ||||||
| #     range_end = 6 |  | ||||||
| #     default = 6 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # class LacsRewards(Range): |  | ||||||
| #     """Set the number of dungeon rewards required for LACS.""" |  | ||||||
| #     display_name = "Dungeon Rewards Required for LACS" |  | ||||||
| #     range_start = 0 |  | ||||||
| #     range_end = 9 |  | ||||||
| #     default = 9 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # class LacsTokens(Range): |  | ||||||
| #     """Set the number of Gold Skulltula Tokens required for LACS.""" |  | ||||||
| #     display_name = "Tokens Required for LACS" |  | ||||||
| #     range_start = 0 |  | ||||||
| #     range_end = 100 |  | ||||||
| #     default = 40 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # lacs_options: typing.Dict[str, type(Option)] = { |  | ||||||
| #     "lacs_condition": LacsCondition, |  | ||||||
| #     "lacs_stones": LacsStones,  |  | ||||||
| #     "lacs_medallions": LacsMedallions,  |  | ||||||
| #     "lacs_rewards": LacsRewards,  |  | ||||||
| #     "lacs_tokens": LacsTokens, |  | ||||||
| # } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class BridgeStones(Range): | class BridgeStones(Range): | ||||||
|     """Set the number of Spiritual Stones required for the rainbow bridge.""" |     """With Stones bridge: set the number of Spiritual Stones required.""" | ||||||
|     display_name = "Spiritual Stones Required for Bridge" |     display_name = "Spiritual Stones Required for Bridge" | ||||||
|     range_start = 0 |     range_start = 0 | ||||||
|     range_end = 3 |     range_end = 3 | ||||||
|  | @ -401,7 +393,7 @@ class BridgeStones(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class BridgeMedallions(Range): | class BridgeMedallions(Range): | ||||||
|     """Set the number of medallions required for the rainbow bridge.""" |     """With Medallions bridge: set the number of medallions required.""" | ||||||
|     display_name = "Medallions Required for Bridge" |     display_name = "Medallions Required for Bridge" | ||||||
|     range_start = 0 |     range_start = 0 | ||||||
|     range_end = 6 |     range_end = 6 | ||||||
|  | @ -409,7 +401,7 @@ class BridgeMedallions(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class BridgeRewards(Range): | class BridgeRewards(Range): | ||||||
|     """Set the number of dungeon rewards required for the rainbow bridge.""" |     """With Dungeons bridge: set the number of dungeon rewards required.""" | ||||||
|     display_name = "Dungeon Rewards Required for Bridge" |     display_name = "Dungeon Rewards Required for Bridge" | ||||||
|     range_start = 0 |     range_start = 0 | ||||||
|     range_end = 9 |     range_end = 9 | ||||||
|  | @ -417,7 +409,7 @@ class BridgeRewards(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class BridgeTokens(Range): | class BridgeTokens(Range): | ||||||
|     """Set the number of Gold Skulltula Tokens required for the rainbow bridge.""" |     """With Tokens bridge: set the number of Gold Skulltula Tokens required.""" | ||||||
|     display_name = "Tokens Required for Bridge" |     display_name = "Tokens Required for Bridge" | ||||||
|     range_start = 0 |     range_start = 0 | ||||||
|     range_end = 100 |     range_end = 100 | ||||||
|  | @ -425,7 +417,7 @@ class BridgeTokens(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class BridgeHearts(Range): | class BridgeHearts(Range): | ||||||
|     """Set the number of hearts required for the rainbow bridge.""" |     """With Hearts bridge: set the number of hearts required.""" | ||||||
|     display_name = "Hearts Required for Bridge" |     display_name = "Hearts Required for Bridge" | ||||||
|     range_start = 4 |     range_start = 4 | ||||||
|     range_end = 20 |     range_end = 20 | ||||||
|  | @ -442,7 +434,15 @@ bridge_options: typing.Dict[str, type(Option)] = { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SongShuffle(Choice):  | class SongShuffle(Choice):  | ||||||
|     """Set where songs can appear.""" |     """Set where songs can appear. | ||||||
|  |     Song: Songs are shuffled into other song locations. | ||||||
|  |     Dungeon: Songs are placed into end-of-dungeon locations: | ||||||
|  |     - The 8 boss heart containers | ||||||
|  |     - Sheik in Ice Cavern | ||||||
|  |     - Lens of Truth chest in Bottom of the Well | ||||||
|  |     - Ice Arrows chest in Gerudo Training Ground | ||||||
|  |     - Impa at Hyrule Castle | ||||||
|  |     Any: Songs can appear anywhere in the multiworld.""" | ||||||
|     display_name = "Shuffle Songs" |     display_name = "Shuffle Songs" | ||||||
|     option_song = 0 |     option_song = 0 | ||||||
|     option_dungeon = 1 |     option_dungeon = 1 | ||||||
|  | @ -450,8 +450,10 @@ class SongShuffle(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShopShuffle(Choice):  | class ShopShuffle(Choice):  | ||||||
|     """Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop;  |     """Randomizes shop contents. | ||||||
|     "random_number" randomizes the value for each shop. """ |     Off: Shops are not randomized at all. | ||||||
|  |     Fixed Number: Shop contents are shuffled, and a specific number of multiworld locations exist in each shop, controlled by the "shop_slots" option. | ||||||
|  |     Random Number: Same as Fixed Number, but the number of locations per shop is random and may differ between shops.""" | ||||||
|     display_name = "Shopsanity" |     display_name = "Shopsanity" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_fixed_number = 1 |     option_fixed_number = 1 | ||||||
|  | @ -459,15 +461,20 @@ class ShopShuffle(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShopSlots(Range): | class ShopSlots(Range): | ||||||
|     """Number of items per shop to be randomized into the main itempool.  |     """With Shopsanity fixed number: quantity of multiworld locations per shop to be randomized.""" | ||||||
|     Only active if Shopsanity is set to "fixed_number." """ |  | ||||||
|     display_name = "Shuffled Shop Slots" |     display_name = "Shuffled Shop Slots" | ||||||
|     range_start = 0 |     range_start = 0 | ||||||
|     range_end = 4 |     range_end = 4 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShopPrices(Choice): | class ShopPrices(Choice): | ||||||
|     """Controls prices of shop items. "Normal" is a distribution from 0 to 300. "X Wallet" requires that wallet at max. "Affordable" is always 10 rupees.""" |     """Controls prices of shop locations. | ||||||
|  |     Normal: Balanced distribution from 0 to 300. | ||||||
|  |     Affordable: Every shop location costs 10 rupees. | ||||||
|  |     Starting Wallet: Prices capped at 99 rupees. | ||||||
|  |     Adult's Wallet: Prices capped at 200 rupees. | ||||||
|  |     Giant's Wallet: Prices capped at 500 rupees. | ||||||
|  |     Tycoon's Wallet: Prices capped at 999 rupees.""" | ||||||
|     display_name = "Shopsanity Prices" |     display_name = "Shopsanity Prices" | ||||||
|     option_normal = 0 |     option_normal = 0 | ||||||
|     option_affordable = 1 |     option_affordable = 1 | ||||||
|  | @ -478,7 +485,10 @@ class ShopPrices(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TokenShuffle(Choice):  | class TokenShuffle(Choice):  | ||||||
|     """Token rewards from Gold Skulltulas are shuffled into the pool.""" |     """Token rewards from Gold Skulltulas can be shuffled into the pool. | ||||||
|  |     Dungeons: Only skulltulas in dungeons are shuffled. | ||||||
|  |     Overworld: Only skulltulas on the overworld (all skulltulas not in dungeons) are shuffled. | ||||||
|  |     All: Every skulltula is shuffled.""" | ||||||
|     display_name = "Tokensanity" |     display_name = "Tokensanity" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_dungeons = 1 |     option_dungeons = 1 | ||||||
|  | @ -487,7 +497,11 @@ class TokenShuffle(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ScrubShuffle(Choice):  | class ScrubShuffle(Choice):  | ||||||
|     """Shuffle the items sold by Business Scrubs, and set the prices.""" |     """Shuffle the items sold by Business Scrubs, and set the prices. | ||||||
|  |     Off: Only the three business scrubs that sell one-time upgrades in vanilla will have items at their vanilla prices. | ||||||
|  |     Low/"Affordable": All scrub prices are 10 rupees. | ||||||
|  |     Regular/"Expensive": All scrub prices are vanilla. | ||||||
|  |     Random Prices: All scrub prices are randomized between 0 and 99 rupees.""" | ||||||
|     display_name = "Scrub Shuffle" |     display_name = "Scrub Shuffle" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_low = 1 |     option_low = 1 | ||||||
|  | @ -513,7 +527,11 @@ class ShuffleOcarinas(Toggle): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShuffleChildTrade(Choice): | class ShuffleChildTrade(Choice): | ||||||
|     """Controls the behavior of the start of the child trade quest.""" |     """Controls the behavior of the start of the child trade quest. | ||||||
|  |     Vanilla: Malon will give you the Weird Egg at Hyrule Castle. | ||||||
|  |     Shuffle: Malon will give you a random item, and the Weird Egg is shuffled. | ||||||
|  |     Skip Child Zelda: The game starts with Zelda already met, Zelda's Letter obtained, and the item from Impa obtained. | ||||||
|  |     """ | ||||||
|     display_name = "Shuffle Child Trade Item" |     display_name = "Shuffle Child Trade Item" | ||||||
|     option_vanilla = 0 |     option_vanilla = 0 | ||||||
|     option_shuffle = 1 |     option_shuffle = 1 | ||||||
|  | @ -538,30 +556,39 @@ class ShuffleMedigoronCarpet(Toggle): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShuffleFreestanding(Choice): | class ShuffleFreestanding(Choice): | ||||||
|     """Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot.""" |     """Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot drops. | ||||||
|  |     Dungeons: Only freestanding items in dungeons are shuffled. | ||||||
|  |     Overworld: Only freestanding items in the overworld are shuffled. | ||||||
|  |     All: All freestanding items are shuffled.""" | ||||||
|     display_name = "Shuffle Rupees & Hearts" |     display_name = "Shuffle Rupees & Hearts" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_all = 1 |     option_dungeons = 1 | ||||||
|     option_overworld = 2 |     option_overworld = 2 | ||||||
|     option_dungeons = 3 |     option_all = 3 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShufflePots(Choice): | class ShufflePots(Choice): | ||||||
|     """Shuffles pots and flying pots which normally contain an item.""" |     """Shuffles pots and flying pots which normally contain an item. | ||||||
|  |     Dungeons: Only pots in dungeons are shuffled. | ||||||
|  |     Overworld: Only pots in the overworld are shuffled. | ||||||
|  |     All: All pots are shuffled.""" | ||||||
|     display_name = "Shuffle Pots" |     display_name = "Shuffle Pots" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_all = 1 |     option_dungeons = 1 | ||||||
|     option_overworld = 2 |     option_overworld = 2 | ||||||
|     option_dungeons = 3 |     option_all = 3 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShuffleCrates(Choice): | class ShuffleCrates(Choice): | ||||||
|     """Shuffles large and small crates containing an item.""" |     """Shuffles large and small crates containing an item. | ||||||
|  |     Dungeons: Only crates in dungeons are shuffled. | ||||||
|  |     Overworld: Only crates in the overworld are shuffled. | ||||||
|  |     All: All crates are shuffled.""" | ||||||
|     display_name = "Shuffle Crates" |     display_name = "Shuffle Crates" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_all = 1 |     option_dungeons = 1 | ||||||
|     option_overworld = 2 |     option_overworld = 2 | ||||||
|     option_dungeons = 3 |     option_all = 3 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShuffleBeehives(Toggle): | class ShuffleBeehives(Toggle): | ||||||
|  | @ -597,72 +624,113 @@ shuffle_options: typing.Dict[str, type(Option)] = { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShuffleMapCompass(Choice):  | class ShuffleMapCompass(Choice):  | ||||||
|     """Control where to shuffle dungeon maps and compasses.""" |     """Control where to shuffle dungeon maps and compasses. | ||||||
|  |     Remove: There will be no maps or compasses in the itempool. | ||||||
|  |     Startwith: You start with all maps and compasses. | ||||||
|  |     Vanilla: Maps and compasses remain vanilla. | ||||||
|  |     Dungeon: Maps and compasses are shuffled within their original dungeon. | ||||||
|  |     Regional: Maps and compasses are shuffled only in regions near the original dungeon. | ||||||
|  |     Overworld: Maps and compasses are shuffled locally outside of dungeons. | ||||||
|  |     Any Dungeon: Maps and compasses are shuffled locally in any dungeon. | ||||||
|  |     Keysanity: Maps and compasses can be anywhere in the multiworld.""" | ||||||
|     display_name = "Maps & Compasses" |     display_name = "Maps & Compasses" | ||||||
|     option_remove = 0 |     option_remove = 0 | ||||||
|     option_startwith = 1 |     option_startwith = 1 | ||||||
|     option_vanilla = 2 |     option_vanilla = 2 | ||||||
|     option_dungeon = 3 |     option_dungeon = 3 | ||||||
|     option_overworld = 4 |     option_regional = 4 | ||||||
|     option_any_dungeon = 5 |     option_overworld = 5 | ||||||
|     option_keysanity = 6 |     option_any_dungeon = 6 | ||||||
|     option_regional = 7 |     option_keysanity = 7 | ||||||
|     default = 1 |     default = 1 | ||||||
|     alias_anywhere = 6 |     alias_anywhere = 7 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShuffleKeys(Choice):  | class ShuffleKeys(Choice):  | ||||||
|     """Control where to shuffle dungeon small keys.""" |     """Control where to shuffle dungeon small keys. | ||||||
|  |     Remove/"Keysy": There will be no small keys in the itempool. All small key doors are automatically unlocked. | ||||||
|  |     Vanilla: Small keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks. | ||||||
|  |     Dungeon: Small keys are shuffled within their original dungeon. | ||||||
|  |     Regional: Small keys are shuffled only in regions near the original dungeon. | ||||||
|  |     Overworld: Small keys are shuffled locally outside of dungeons. | ||||||
|  |     Any Dungeon: Small keys are shuffled locally in any dungeon. | ||||||
|  |     Keysanity: Small keys can be anywhere in the multiworld.""" | ||||||
|     display_name = "Small Keys" |     display_name = "Small Keys" | ||||||
|     option_remove = 0 |     option_remove = 0 | ||||||
|     option_vanilla = 2 |     option_vanilla = 2 | ||||||
|     option_dungeon = 3 |     option_dungeon = 3 | ||||||
|     option_overworld = 4 |     option_regional = 4 | ||||||
|     option_any_dungeon = 5 |     option_overworld = 5 | ||||||
|     option_keysanity = 6 |     option_any_dungeon = 6 | ||||||
|     option_regional = 7 |     option_keysanity = 7 | ||||||
|     default = 3 |     default = 3 | ||||||
|     alias_keysy = 0 |     alias_keysy = 0 | ||||||
|     alias_anywhere = 6 |     alias_anywhere = 7 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShuffleGerudoKeys(Choice):  | class ShuffleGerudoKeys(Choice):  | ||||||
|     """Control where to shuffle the Thieves' Hideout small keys.""" |     """Control where to shuffle the Thieves' Hideout small keys. | ||||||
|  |     Vanilla: Hideout keys remain vanilla. | ||||||
|  |     Regional: Hideout keys are shuffled only in the Gerudo Valley/Desert Colossus area. | ||||||
|  |     Overworld: Hideout keys are shuffled locally outside of dungeons. | ||||||
|  |     Any Dungeon: Hideout keys are shuffled locally in any dungeon. | ||||||
|  |     Keysanity: Hideout keys can be anywhere in the multiworld.""" | ||||||
|     display_name = "Thieves' Hideout Keys" |     display_name = "Thieves' Hideout Keys" | ||||||
|     option_vanilla = 0 |     option_vanilla = 0 | ||||||
|     option_overworld = 1 |     option_regional = 1 | ||||||
|     option_any_dungeon = 2 |     option_overworld = 2 | ||||||
|     option_keysanity = 3 |     option_any_dungeon = 3 | ||||||
|     option_regional = 4 |     option_keysanity = 4 | ||||||
|     alias_anywhere = 3 |     alias_anywhere = 4 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShuffleBossKeys(Choice):  | class ShuffleBossKeys(Choice):  | ||||||
|     """Control where to shuffle boss keys, except the Ganon's Castle Boss Key.""" |     """Control where to shuffle boss keys, except the Ganon's Castle Boss Key. | ||||||
|  |     Remove/"Keysy": There will be no boss keys in the itempool. All boss key doors are automatically unlocked. | ||||||
|  |     Vanilla: Boss keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks. | ||||||
|  |     Dungeon: Boss keys are shuffled within their original dungeon. | ||||||
|  |     Regional: Boss keys are shuffled only in regions near the original dungeon. | ||||||
|  |     Overworld: Boss keys are shuffled locally outside of dungeons. | ||||||
|  |     Any Dungeon: Boss keys are shuffled locally in any dungeon. | ||||||
|  |     Keysanity: Boss keys can be anywhere in the multiworld.""" | ||||||
|     display_name = "Boss Keys" |     display_name = "Boss Keys" | ||||||
|     option_remove = 0 |     option_remove = 0 | ||||||
|     option_vanilla = 2 |     option_vanilla = 2 | ||||||
|     option_dungeon = 3 |     option_dungeon = 3 | ||||||
|     option_overworld = 4 |     option_regional = 4 | ||||||
|     option_any_dungeon = 5 |     option_overworld = 5 | ||||||
|     option_keysanity = 6 |     option_any_dungeon = 6 | ||||||
|     option_regional = 7 |     option_keysanity = 7 | ||||||
|     default = 3 |     default = 3 | ||||||
|     alias_keysy = 0 |     alias_keysy = 0 | ||||||
|     alias_anywhere = 6 |     alias_anywhere = 7 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ShuffleGanonBK(Choice): | class ShuffleGanonBK(Choice): | ||||||
|     """Control how to shuffle the Ganon's Castle Boss Key.""" |     """Control how to shuffle the Ganon's Castle Boss Key (GCBK). | ||||||
|  |     Remove: GCBK is removed, and the boss key door is automatically unlocked. | ||||||
|  |     Vanilla: GCBK remains vanilla. | ||||||
|  |     Dungeon: GCBK is shuffled within its original dungeon. | ||||||
|  |     Regional: GCBK is shuffled only in Hyrule Field, Market, and Hyrule Castle areas. | ||||||
|  |     Overworld: GCBK is shuffled locally outside of dungeons. | ||||||
|  |     Any Dungeon: GCBK is shuffled locally in any dungeon. | ||||||
|  |     Keysanity: GCBK can be anywhere in the multiworld. | ||||||
|  |     On LACS: GCBK is on the Light Arrow Cutscene, which requires Shadow and Spirit Medallions. | ||||||
|  |     Stones: GCBK will be awarded when reaching the target number of Spiritual Stones. | ||||||
|  |     Medallions: GCBK will be awarded when reaching the target number of medallions. | ||||||
|  |     Dungeons: GCBK will be awarded when reaching the target number of dungeon rewards. | ||||||
|  |     Tokens: GCBK will be awarded when reaching the target number of Gold Skulltula Tokens. | ||||||
|  |     Hearts: GCBK will be awarded when reaching the target number of hearts. | ||||||
|  |     """ | ||||||
|     display_name = "Ganon's Boss Key" |     display_name = "Ganon's Boss Key" | ||||||
|     option_remove = 0 |     option_remove = 0 | ||||||
|     option_vanilla = 2 |     option_vanilla = 2 | ||||||
|     option_dungeon = 3 |     option_dungeon = 3 | ||||||
|     option_overworld = 4 |     option_regional = 4 | ||||||
|     option_any_dungeon = 5 |     option_overworld = 5 | ||||||
|     option_keysanity = 6 |     option_any_dungeon = 6 | ||||||
|     option_on_lacs = 7 |     option_keysanity = 7 | ||||||
|     option_regional = 8 |     option_on_lacs = 8 | ||||||
|     option_stones = 9 |     option_stones = 9 | ||||||
|     option_medallions = 10 |     option_medallions = 10 | ||||||
|     option_dungeons = 11 |     option_dungeons = 11 | ||||||
|  | @ -670,7 +738,7 @@ class ShuffleGanonBK(Choice): | ||||||
|     option_hearts = 13 |     option_hearts = 13 | ||||||
|     default = 0 |     default = 0 | ||||||
|     alias_keysy = 0 |     alias_keysy = 0 | ||||||
|     alias_anywhere = 6 |     alias_anywhere = 7 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class EnhanceMC(Toggle): | class EnhanceMC(Toggle): | ||||||
|  | @ -679,7 +747,7 @@ class EnhanceMC(Toggle): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class GanonBKMedallions(Range): | class GanonBKMedallions(Range): | ||||||
|     """Set how many medallions are required to receive Ganon BK.""" |     """With medallions GCBK: set how many medallions are required to receive GCBK.""" | ||||||
|     display_name = "Medallions Required for Ganon's BK" |     display_name = "Medallions Required for Ganon's BK" | ||||||
|     range_start = 1 |     range_start = 1 | ||||||
|     range_end = 6 |     range_end = 6 | ||||||
|  | @ -687,7 +755,7 @@ class GanonBKMedallions(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class GanonBKStones(Range): | class GanonBKStones(Range): | ||||||
|     """Set how many Spiritual Stones are required to receive Ganon BK.""" |     """With stones GCBK: set how many Spiritual Stones are required to receive GCBK.""" | ||||||
|     display_name = "Spiritual Stones Required for Ganon's BK" |     display_name = "Spiritual Stones Required for Ganon's BK" | ||||||
|     range_start = 1 |     range_start = 1 | ||||||
|     range_end = 3 |     range_end = 3 | ||||||
|  | @ -695,7 +763,7 @@ class GanonBKStones(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class GanonBKRewards(Range): | class GanonBKRewards(Range): | ||||||
|     """Set how many dungeon rewards are required to receive Ganon BK.""" |     """With dungeons GCBK: set how many dungeon rewards are required to receive GCBK.""" | ||||||
|     display_name = "Dungeon Rewards Required for Ganon's BK" |     display_name = "Dungeon Rewards Required for Ganon's BK" | ||||||
|     range_start = 1 |     range_start = 1 | ||||||
|     range_end = 9 |     range_end = 9 | ||||||
|  | @ -703,7 +771,7 @@ class GanonBKRewards(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class GanonBKTokens(Range): | class GanonBKTokens(Range): | ||||||
|     """Set how many Gold Skulltula Tokens are required to receive Ganon BK.""" |     """With tokens GCBK: set how many Gold Skulltula Tokens are required to receive GCBK.""" | ||||||
|     display_name = "Tokens Required for Ganon's BK" |     display_name = "Tokens Required for Ganon's BK" | ||||||
|     range_start = 1 |     range_start = 1 | ||||||
|     range_end = 100 |     range_end = 100 | ||||||
|  | @ -711,7 +779,7 @@ class GanonBKTokens(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class GanonBKHearts(Range): | class GanonBKHearts(Range): | ||||||
|     """Set how many hearts are required to receive Ganon BK.""" |     """With hearts GCBK: set how many hearts are required to receive GCBK.""" | ||||||
|     display_name = "Hearts Required for Ganon's BK" |     display_name = "Hearts Required for Ganon's BK" | ||||||
|     range_start = 4 |     range_start = 4 | ||||||
|     range_end = 20 |     range_end = 20 | ||||||
|  | @ -719,7 +787,9 @@ class GanonBKHearts(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class KeyRings(Choice): | class KeyRings(Choice): | ||||||
|     """Dungeons have all small keys found at once, rather than individually.""" |     """A key ring grants all dungeon small keys at once, rather than individually. | ||||||
|  |     Choose: Use the option "key_rings_list" to choose which dungeons have key rings. | ||||||
|  |     All: All dungeons have key rings instead of small keys.""" | ||||||
|     display_name = "Key Rings Mode" |     display_name = "Key Rings Mode" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_choose = 1 |     option_choose = 1 | ||||||
|  | @ -728,7 +798,7 @@ class KeyRings(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class KeyRingList(OptionSet): | class KeyRingList(OptionSet): | ||||||
|     """Select areas with keyrings rather than individual small keys.""" |     """With key rings as Choose: select areas with key rings rather than individual small keys.""" | ||||||
|     display_name = "Key Ring Areas" |     display_name = "Key Ring Areas" | ||||||
|     valid_keys = { |     valid_keys = { | ||||||
|         "Thieves' Hideout", |         "Thieves' Hideout", | ||||||
|  | @ -828,7 +898,8 @@ class BigPoeCount(Range): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FAETorchCount(Range): | class FAETorchCount(Range): | ||||||
|     """Number of lit torches required to open Shadow Temple.""" |     """Number of lit torches required to open Shadow Temple. | ||||||
|  |     Does not affect logic; use the trick Shadow Temple Entry with Fire Arrows if desired.""" | ||||||
|     display_name = "Fire Arrow Entry Torch Count" |     display_name = "Fire Arrow Entry Torch Count" | ||||||
|     range_start = 1 |     range_start = 1 | ||||||
|     range_end = 24 |     range_end = 24 | ||||||
|  | @ -853,7 +924,11 @@ timesavers_options: typing.Dict[str, type(Option)] = { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class CorrectChestAppearance(Choice): | class CorrectChestAppearance(Choice): | ||||||
|     """Changes chest textures and/or sizes to match their contents. "Classic" is the old behavior of CSMC.""" |     """Changes chest textures and/or sizes to match their contents. | ||||||
|  |     Off: All chests have their vanilla size/appearance. | ||||||
|  |     Textures: Chest textures reflect their contents. | ||||||
|  |     Both: Like Textures, but progression items and boss keys get big chests, and other items get small chests. | ||||||
|  |     Classic: Old behavior of CSMC; textures distinguish keys from non-keys, and size distinguishes importance.""" | ||||||
|     display_name = "Chest Appearance Matches Contents" |     display_name = "Chest Appearance Matches Contents" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_textures = 1 |     option_textures = 1 | ||||||
|  | @ -872,15 +947,24 @@ class InvisibleChests(Toggle): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class CorrectPotCrateAppearance(Choice): | class CorrectPotCrateAppearance(Choice): | ||||||
|     """Unchecked pots and crates have a different texture; unchecked beehives will wiggle. With textures_content, pots and crates have an appearance based on their contents; with textures_unchecked, all unchecked pots/crates have the same appearance.""" |     """Changes the appearance of pots, crates, and beehives that contain items. | ||||||
|  |     Off: Vanilla appearance for all containers. | ||||||
|  |     Textures (Content): Unchecked pots and crates have a texture reflecting their contents. Unchecked beehives with progression items will wiggle. | ||||||
|  |     Textures (Unchecked): Unchecked pots and crates are golden. Unchecked beehives will wiggle. | ||||||
|  |     """ | ||||||
|     display_name = "Pot, Crate, and Beehive Appearance" |     display_name = "Pot, Crate, and Beehive Appearance" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_textures_content = 1 |     option_textures_content = 1 | ||||||
|     option_textures_unchecked = 2 |     option_textures_unchecked = 2 | ||||||
|  |     default = 2 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Hints(Choice):  | class Hints(Choice):  | ||||||
|     """Gossip Stones can give hints about item locations.""" |     """Gossip Stones can give hints about item locations. | ||||||
|  |     None: Gossip Stones do not give hints. | ||||||
|  |     Mask: Gossip Stones give hints with Mask of Truth. | ||||||
|  |     Agony: Gossip Stones give hints wtih Stone of Agony. | ||||||
|  |     Always: Gossip Stones always give hints.""" | ||||||
|     display_name = "Gossip Stones" |     display_name = "Gossip Stones" | ||||||
|     option_none = 0 |     option_none = 0 | ||||||
|     option_mask = 1 |     option_mask = 1 | ||||||
|  | @ -895,7 +979,9 @@ class MiscHints(DefaultOnToggle): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class HintDistribution(Choice): | class HintDistribution(Choice): | ||||||
|     """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.""" |     """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc. | ||||||
|  |     Detailed documentation on hint distributions can be found on the Archipelago GitHub or OoTRandomizer.com. | ||||||
|  |     The Async hint distribution is intended for async multiworlds. It removes Way of the Hero hints to improve generation times, since they are not very useful in asyncs.""" | ||||||
|     display_name = "Hint Distribution" |     display_name = "Hint Distribution" | ||||||
|     option_balanced = 0 |     option_balanced = 0 | ||||||
|     option_ddr = 1 |     option_ddr = 1 | ||||||
|  | @ -907,10 +993,13 @@ class HintDistribution(Choice): | ||||||
|     option_useless = 7 |     option_useless = 7 | ||||||
|     option_very_strong = 8 |     option_very_strong = 8 | ||||||
|     option_async = 9 |     option_async = 9 | ||||||
|  |     default = 9 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TextShuffle(Choice):  | class TextShuffle(Choice):  | ||||||
|     """Randomizes text in the game for comedic effect.""" |     """Randomizes text in the game for comedic effect. | ||||||
|  |     Except Hints: does not randomize important text such as hints, small/boss key information, and item prices. | ||||||
|  |     Complete: randomizes every textbox, including the useful ones.""" | ||||||
|     display_name = "Text Shuffle" |     display_name = "Text Shuffle" | ||||||
|     option_none = 0 |     option_none = 0 | ||||||
|     option_except_hints = 1 |     option_except_hints = 1 | ||||||
|  | @ -946,7 +1035,8 @@ class HeroMode(Toggle): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class StartingToD(Choice): | class StartingToD(Choice): | ||||||
|     """Change the starting time of day.""" |     """Change the starting time of day. | ||||||
|  |     Daytime starts at Sunrise and ends at Sunset. Default is between Morning and Noon.""" | ||||||
|     display_name = "Starting Time of Day" |     display_name = "Starting Time of Day" | ||||||
|     option_default = 0 |     option_default = 0 | ||||||
|     option_sunrise = 1 |     option_sunrise = 1 | ||||||
|  | @ -999,7 +1089,11 @@ misc_options: typing.Dict[str, type(Option)] = { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class ItemPoolValue(Choice):  | class ItemPoolValue(Choice):  | ||||||
|     """Changes the number of items available in the game.""" |     """Changes the number of items available in the game. | ||||||
|  |     Plentiful: One extra copy of every major item. | ||||||
|  |     Balanced: Original item pool. | ||||||
|  |     Scarce: Extra copies of major items are removed. Heart containers are removed. | ||||||
|  |     Minimal: All major item upgrades not used for locations are removed. All health is removed.""" | ||||||
|     display_name = "Item Pool" |     display_name = "Item Pool" | ||||||
|     option_plentiful = 0 |     option_plentiful = 0 | ||||||
|     option_balanced = 1 |     option_balanced = 1 | ||||||
|  | @ -1009,7 +1103,12 @@ class ItemPoolValue(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class IceTraps(Choice):  | class IceTraps(Choice):  | ||||||
|     """Adds ice traps to the item pool.""" |     """Adds ice traps to the item pool. | ||||||
|  |     Off: All ice traps are removed. | ||||||
|  |     Normal: The vanilla quantity of ice traps are placed. | ||||||
|  |     On/"Extra": There is a chance for some extra ice traps to be placed. | ||||||
|  |     Mayhem: All added junk items are ice traps. | ||||||
|  |     Onslaught: All junk items are replaced by ice traps, even those in the base pool.""" | ||||||
|     display_name = "Ice Traps" |     display_name = "Ice Traps" | ||||||
|     option_off = 0 |     option_off = 0 | ||||||
|     option_normal = 1 |     option_normal = 1 | ||||||
|  | @ -1021,34 +1120,27 @@ class IceTraps(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class IceTrapVisual(Choice):  | class IceTrapVisual(Choice):  | ||||||
|     """Changes the appearance of ice traps as freestanding items.""" |     """Changes the appearance of traps, including other games' traps, as freestanding items.""" | ||||||
|     display_name = "Ice Trap Appearance" |     display_name = "Trap Appearance" | ||||||
|     option_major_only = 0 |     option_major_only = 0 | ||||||
|     option_junk_only = 1 |     option_junk_only = 1 | ||||||
|     option_anything = 2 |     option_anything = 2 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class AdultTradeStart(OptionSet): | class AdultTradeStart(Choice): | ||||||
|     """Choose the items that can appear to start the adult trade sequence. By default it is Claim Check only.""" |     """Choose the item that starts the adult trade sequence.""" | ||||||
|     display_name = "Adult Trade Sequence Items" |     display_name = "Adult Trade Sequence Start" | ||||||
|     default = {"Claim Check"} |     option_pocket_egg = 0 | ||||||
|     valid_keys = { |     option_pocket_cucco = 1 | ||||||
|         "Pocket Egg", |     option_cojiro = 2 | ||||||
|         "Pocket Cucco", |     option_odd_mushroom = 3 | ||||||
|         "Cojiro", |     option_poachers_saw = 4 | ||||||
|         "Odd Mushroom", |     option_broken_sword = 5 | ||||||
|         "Poachers Saw", |     option_prescription = 6 | ||||||
|         "Broken Sword", |     option_eyeball_frog = 7 | ||||||
|         "Prescription", |     option_eyedrops = 8 | ||||||
|         "Eyeball Frog", |     option_claim_check = 9 | ||||||
|         "Eyedrops", |     default = 9 | ||||||
|         "Claim Check", |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     def __init__(self, value: typing.Iterable[str]): |  | ||||||
|         if not value: |  | ||||||
|             value = self.default |  | ||||||
|         super().__init__(value) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| itempool_options: typing.Dict[str, type(Option)] = { | itempool_options: typing.Dict[str, type(Option)] = { | ||||||
|  | @ -1068,7 +1160,7 @@ class Targeting(Choice): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DisplayDpad(DefaultOnToggle): | class DisplayDpad(DefaultOnToggle): | ||||||
|     """Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots).""" |     """Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots, mask).""" | ||||||
|     display_name = "Display D-Pad HUD" |     display_name = "Display D-Pad HUD" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -1191,7 +1283,6 @@ oot_options: typing.Dict[str, type(Option)] = { | ||||||
|     **world_options,  |     **world_options,  | ||||||
|     **bridge_options, |     **bridge_options, | ||||||
|     **dungeon_items_options, |     **dungeon_items_options, | ||||||
|     # **lacs_options, |  | ||||||
|     **shuffle_options, |     **shuffle_options, | ||||||
|     **timesavers_options, |     **timesavers_options, | ||||||
|     **misc_options,  |     **misc_options,  | ||||||
|  |  | ||||||
|  | @ -2094,10 +2094,14 @@ def patch_rom(world, rom): | ||||||
|         if not world.dungeon_mq['Ganons Castle']: |         if not world.dungeon_mq['Ganons Castle']: | ||||||
|             chest_name = 'Ganons Castle Light Trial Lullaby Chest' |             chest_name = 'Ganons Castle Light Trial Lullaby Chest' | ||||||
|             location = world.get_location(chest_name) |             location = world.get_location(chest_name) | ||||||
|  |             if not location.item.trap: | ||||||
|                 if location.item.game == 'Ocarina of Time': |                 if location.item.game == 'Ocarina of Time': | ||||||
|                     item = read_rom_item(rom, location.item.index) |                     item = read_rom_item(rom, location.item.index) | ||||||
|                 else: |                 else: | ||||||
|                     item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) |                     item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) | ||||||
|  |             else: | ||||||
|  |                 looks_like_index = get_override_entry(world, location)[5] | ||||||
|  |                 item = read_rom_item(rom, looks_like_index) | ||||||
|             if item['chest_type'] in (GOLD_CHEST, GILDED_CHEST, SKULL_CHEST_BIG): |             if item['chest_type'] in (GOLD_CHEST, GILDED_CHEST, SKULL_CHEST_BIG): | ||||||
|                 rom.write_int16(0x321B176, 0xFC40) # original 0xFC48 |                 rom.write_int16(0x321B176, 0xFC40) # original 0xFC48 | ||||||
| 
 | 
 | ||||||
|  | @ -2106,10 +2110,14 @@ def patch_rom(world, rom): | ||||||
|             chest_name = 'Spirit Temple Compass Chest' |             chest_name = 'Spirit Temple Compass Chest' | ||||||
|             chest_address = 0x2B6B07C |             chest_address = 0x2B6B07C | ||||||
|             location = world.get_location(chest_name) |             location = world.get_location(chest_name) | ||||||
|  |             if not location.item.trap: | ||||||
|                 if location.item.game == 'Ocarina of Time': |                 if location.item.game == 'Ocarina of Time': | ||||||
|                     item = read_rom_item(rom, location.item.index) |                     item = read_rom_item(rom, location.item.index) | ||||||
|                 else: |                 else: | ||||||
|                     item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) |                     item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) | ||||||
|  |             else: | ||||||
|  |                 looks_like_index = get_override_entry(world, location)[5] | ||||||
|  |                 item = read_rom_item(rom, looks_like_index) | ||||||
|             if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL): |             if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL): | ||||||
|                 rom.write_int16(chest_address + 2, 0x0190) # X pos |                 rom.write_int16(chest_address + 2, 0x0190) # X pos | ||||||
|                 rom.write_int16(chest_address + 6, 0xFABC) # Z pos |                 rom.write_int16(chest_address + 6, 0xFABC) # Z pos | ||||||
|  | @ -2120,10 +2128,14 @@ def patch_rom(world, rom): | ||||||
|             chest_address_0 = 0x21A02D0  # Address in setup 0 |             chest_address_0 = 0x21A02D0  # Address in setup 0 | ||||||
|             chest_address_2 = 0x21A06E4  # Address in setup 2 |             chest_address_2 = 0x21A06E4  # Address in setup 2 | ||||||
|             location = world.get_location(chest_name) |             location = world.get_location(chest_name) | ||||||
|  |             if not location.item.trap: | ||||||
|                 if location.item.game == 'Ocarina of Time': |                 if location.item.game == 'Ocarina of Time': | ||||||
|                     item = read_rom_item(rom, location.item.index) |                     item = read_rom_item(rom, location.item.index) | ||||||
|                 else: |                 else: | ||||||
|                     item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) |                     item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) | ||||||
|  |             else: | ||||||
|  |                 looks_like_index = get_override_entry(world, location)[5] | ||||||
|  |                 item = read_rom_item(rom, looks_like_index) | ||||||
|             if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL): |             if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL): | ||||||
|                 rom.write_int16(chest_address_0 + 6, 0x0172)  # Z pos |                 rom.write_int16(chest_address_0 + 6, 0x0172)  # Z pos | ||||||
|                 rom.write_int16(chest_address_2 + 6, 0x0172)  # Z pos |                 rom.write_int16(chest_address_2 + 6, 0x0172)  # Z pos | ||||||
|  |  | ||||||
|  | @ -223,9 +223,6 @@ def set_shop_rules(ootworld): | ||||||
| # The goal is to automatically set item rules based on age requirements in case entrances were shuffled | # The goal is to automatically set item rules based on age requirements in case entrances were shuffled | ||||||
| def set_entrances_based_rules(ootworld): | def set_entrances_based_rules(ootworld): | ||||||
| 
 | 
 | ||||||
|     if ootworld.multiworld.accessibility == 'beatable':  |  | ||||||
|         return |  | ||||||
| 
 |  | ||||||
|     all_state = ootworld.multiworld.get_all_state(False) |     all_state = ootworld.multiworld.get_all_state(False) | ||||||
| 
 | 
 | ||||||
|     for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()): |     for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()): | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ from string import printable | ||||||
| 
 | 
 | ||||||
| logger = logging.getLogger("Ocarina of Time") | logger = logging.getLogger("Ocarina of Time") | ||||||
| 
 | 
 | ||||||
| from .Location import OOTLocation, LocationFactory, location_name_to_id | from .Location import OOTLocation, LocationFactory, location_name_to_id, build_location_name_groups | ||||||
| from .Entrance import OOTEntrance | from .Entrance import OOTEntrance | ||||||
| from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError | from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError | ||||||
| from .HintList import getRequiredHints | from .HintList import getRequiredHints | ||||||
|  | @ -163,11 +163,13 @@ class OOTWorld(World): | ||||||
|             "Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion", |             "Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion", | ||||||
|             "Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", |             "Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", | ||||||
|             "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"}, |             "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"}, | ||||||
|         "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Odd Mushroom", |         "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Cojiro", "Odd Mushroom", | ||||||
|             "Odd Potion", "Poachers Saw", "Broken Sword", "Prescription", |             "Odd Potion", "Poachers Saw", "Broken Sword", "Prescription", | ||||||
|             "Eyeball Frog", "Eyedrops", "Claim Check"} |             "Eyeball Frog", "Eyedrops", "Claim Check"}, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     location_name_groups = build_location_name_groups() | ||||||
|  | 
 | ||||||
|     def __init__(self, world, player): |     def __init__(self, world, player): | ||||||
|         self.hint_data_available = threading.Event() |         self.hint_data_available = threading.Event() | ||||||
|         self.collectible_flags_available = threading.Event() |         self.collectible_flags_available = threading.Event() | ||||||
|  | @ -384,6 +386,7 @@ class OOTWorld(World): | ||||||
|             self.mq_dungeons_mode = 'count' |             self.mq_dungeons_mode = 'count' | ||||||
|             self.mq_dungeons_count = 0 |             self.mq_dungeons_count = 0 | ||||||
|         self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table} |         self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table} | ||||||
|  |         self.dungeon_mq['Thieves Hideout'] = False  # fix for bug in SaveContext:287 | ||||||
| 
 | 
 | ||||||
|         # Empty dungeon placeholder for the moment |         # Empty dungeon placeholder for the moment | ||||||
|         self.empty_dungeons = {name: False for name in self.dungeon_mq} |         self.empty_dungeons = {name: False for name in self.dungeon_mq} | ||||||
|  | @ -409,6 +412,9 @@ class OOTWorld(World): | ||||||
|         self.starting_tod = self.starting_tod.replace('_', '-') |         self.starting_tod = self.starting_tod.replace('_', '-') | ||||||
|         self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '') |         self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '') | ||||||
| 
 | 
 | ||||||
|  |         # Convert adult trade option to expected Set | ||||||
|  |         self.adult_trade_start = {self.adult_trade_start.title().replace('_', ' ')} | ||||||
|  | 
 | ||||||
|         # Get hint distribution |         # Get hint distribution | ||||||
|         self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json')) |         self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json')) | ||||||
| 
 | 
 | ||||||
|  | @ -446,7 +452,7 @@ class OOTWorld(World): | ||||||
|         self.always_hints = [hint.name for hint in getRequiredHints(self)] |         self.always_hints = [hint.name for hint in getRequiredHints(self)] | ||||||
| 
 | 
 | ||||||
|         # Determine items which are not considered advancement based on settings. They will never be excluded. |         # Determine items which are not considered advancement based on settings. They will never be excluded. | ||||||
|         self.nonadvancement_items = {'Double Defense'} |         self.nonadvancement_items = {'Double Defense', 'Deku Stick Capacity', 'Deku Nut Capacity'} | ||||||
|         if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and |         if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and | ||||||
|                 self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances): |                 self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances): | ||||||
|             # nayru's love may be required to prevent forced damage |             # nayru's love may be required to prevent forced damage | ||||||
|  | @ -633,16 +639,18 @@ class OOTWorld(World): | ||||||
|             self.multiworld.itempool.remove(item) |             self.multiworld.itempool.remove(item) | ||||||
|             self.hinted_dungeon_reward_locations[item.name] = loc |             self.hinted_dungeon_reward_locations[item.name] = loc | ||||||
| 
 | 
 | ||||||
|     def create_item(self, name: str): |     def create_item(self, name: str, allow_arbitrary_name: bool = False): | ||||||
|         if name in item_table: |         if name in item_table: | ||||||
|             return OOTItem(name, self.player, item_table[name], False, |             return OOTItem(name, self.player, item_table[name], False, | ||||||
|                            (name in self.nonadvancement_items if getattr(self, 'nonadvancement_items', |                            (name in self.nonadvancement_items if getattr(self, 'nonadvancement_items', | ||||||
|                                                                          None) else False)) |                                                                          None) else False)) | ||||||
|  |         if allow_arbitrary_name: | ||||||
|             return OOTItem(name, self.player, ('Event', True, None, None), True, False) |             return OOTItem(name, self.player, ('Event', True, None, None), True, False) | ||||||
|  |         raise Exception(f"Invalid item name: {name}") | ||||||
| 
 | 
 | ||||||
|     def make_event_item(self, name, location, item=None): |     def make_event_item(self, name, location, item=None): | ||||||
|         if item is None: |         if item is None: | ||||||
|             item = self.create_item(name) |             item = self.create_item(name, allow_arbitrary_name=True) | ||||||
|         self.multiworld.push_item(location, item, collect=False) |         self.multiworld.push_item(location, item, collect=False) | ||||||
|         location.locked = True |         location.locked = True | ||||||
|         location.event = True |         location.event = True | ||||||
|  | @ -800,23 +808,25 @@ class OOTWorld(World): | ||||||
|                         self.multiworld.itempool.remove(item) |                         self.multiworld.itempool.remove(item) | ||||||
|                     self.multiworld.random.shuffle(locations) |                     self.multiworld.random.shuffle(locations) | ||||||
|                     fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items, |                     fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items, | ||||||
|                         single_player_placement=True, lock=True) |                         single_player_placement=True, lock=True, allow_excluded=True) | ||||||
|             else: |             else: | ||||||
|                 for dungeon_info in dungeon_table: |                 for dungeon_info in dungeon_table: | ||||||
|                     dungeon_name = dungeon_info['name'] |                     dungeon_name = dungeon_info['name'] | ||||||
|                     locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name) |                     locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name) | ||||||
|                     if isinstance(locations, list): |                     if isinstance(locations, list): | ||||||
|                         dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) |                         dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) | ||||||
|  |                         if not dungeon_items: | ||||||
|  |                             continue | ||||||
|                         for item in dungeon_items: |                         for item in dungeon_items: | ||||||
|                             self.multiworld.itempool.remove(item) |                             self.multiworld.itempool.remove(item) | ||||||
|                         self.multiworld.random.shuffle(locations) |                         self.multiworld.random.shuffle(locations) | ||||||
|                         fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, |                         fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, | ||||||
|                             single_player_placement=True, lock=True) |                             single_player_placement=True, lock=True, allow_excluded=True) | ||||||
| 
 | 
 | ||||||
|         # Place songs |         # Place songs | ||||||
|         # 5 built-in retries because this section can fail sometimes |         # 5 built-in retries because this section can fail sometimes | ||||||
|         if self.shuffle_song_items != 'any': |         if self.shuffle_song_items != 'any': | ||||||
|             tries = 5 |             tries = 10 | ||||||
|             if self.shuffle_song_items == 'song': |             if self.shuffle_song_items == 'song': | ||||||
|                 song_locations = list(filter(lambda location: location.type == 'Song', |                 song_locations = list(filter(lambda location: location.type == 'Song', | ||||||
|                                              self.multiworld.get_unfilled_locations(player=self.player))) |                                              self.multiworld.get_unfilled_locations(player=self.player))) | ||||||
|  | @ -852,7 +862,7 @@ class OOTWorld(World): | ||||||
|                 try: |                 try: | ||||||
|                     self.multiworld.random.shuffle(song_locations) |                     self.multiworld.random.shuffle(song_locations) | ||||||
|                     fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:], |                     fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:], | ||||||
|                                      True, True) |                                      single_player_placement=True, lock=True, allow_excluded=True) | ||||||
|                     logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") |                     logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") | ||||||
|                 except FillError as e: |                 except FillError as e: | ||||||
|                     tries -= 1 |                     tries -= 1 | ||||||
|  | @ -888,7 +898,8 @@ class OOTWorld(World): | ||||||
|             self.multiworld.random.shuffle(shop_locations) |             self.multiworld.random.shuffle(shop_locations) | ||||||
|             for item in shop_prog + shop_junk: |             for item in shop_prog + shop_junk: | ||||||
|                 self.multiworld.itempool.remove(item) |                 self.multiworld.itempool.remove(item) | ||||||
|             fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, True, True) |             fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, | ||||||
|  |                 single_player_placement=True, lock=True, allow_excluded=True) | ||||||
|             fast_fill(self.multiworld, shop_junk, shop_locations) |             fast_fill(self.multiworld, shop_junk, shop_locations) | ||||||
|             for loc in shop_locations: |             for loc in shop_locations: | ||||||
|                 loc.locked = True |                 loc.locked = True | ||||||
|  | @ -963,7 +974,7 @@ class OOTWorld(World): | ||||||
|                             multiworld.itempool.remove(item) |                             multiworld.itempool.remove(item) | ||||||
|                         multiworld.random.shuffle(locations) |                         multiworld.random.shuffle(locations) | ||||||
|                         fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items, |                         fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items, | ||||||
|                             single_player_placement=False, lock=True) |                             single_player_placement=False, lock=True, allow_excluded=True) | ||||||
|                         if fill_stage == 'Song': |                         if fill_stage == 'Song': | ||||||
|                             # We don't want song locations to contain progression unless it's a song |                             # We don't want song locations to contain progression unless it's a song | ||||||
|                             # or it was marked as priority. |                             # or it was marked as priority. | ||||||
|  | @ -984,7 +995,7 @@ class OOTWorld(World): | ||||||
|                                 multiworld.itempool.remove(item) |                                 multiworld.itempool.remove(item) | ||||||
|                             multiworld.random.shuffle(locations) |                             multiworld.random.shuffle(locations) | ||||||
|                             fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, |                             fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, | ||||||
|                                 single_player_placement=False, lock=True) |                                 single_player_placement=False, lock=True, allow_excluded=True) | ||||||
| 
 | 
 | ||||||
|     def generate_output(self, output_directory: str): |     def generate_output(self, output_directory: str): | ||||||
|         if self.hints != 'none': |         if self.hints != 'none': | ||||||
|  | @ -1051,7 +1062,10 @@ class OOTWorld(World): | ||||||
|     def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str): |     def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str): | ||||||
|         def hint_type_players(hint_type: str) -> set: |         def hint_type_players(hint_type: str) -> set: | ||||||
|             return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time") |             return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time") | ||||||
|                     if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0} |                     if autoworld.hints != 'none'  | ||||||
|  |                     and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0 | ||||||
|  |                     and (autoworld.hint_dist_user['distribution'][hint_type]['fixed'] > 0  | ||||||
|  |                       or autoworld.hint_dist_user['distribution'][hint_type]['weight'] > 0)} | ||||||
| 
 | 
 | ||||||
|         try: |         try: | ||||||
|             item_hint_players = hint_type_players('item') |             item_hint_players = hint_type_players('item') | ||||||
|  | @ -1078,10 +1092,10 @@ class OOTWorld(World): | ||||||
| 
 | 
 | ||||||
|                     if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or |                     if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or | ||||||
|                         (oot_is_item_of_type(loc.item, 'Song') or |                         (oot_is_item_of_type(loc.item, 'Song') or | ||||||
|                             (oot_is_item_of_type(loc.item, 'SmallKey')         and multiworld.worlds[loc.player].shuffle_smallkeys     == 'any_dungeon') or |                             (oot_is_item_of_type(loc.item, 'SmallKey')         and multiworld.worlds[loc.player].shuffle_smallkeys     in ('overworld', 'any_dungeon', 'regional')) or | ||||||
|                             (oot_is_item_of_type(loc.item, 'HideoutSmallKey')  and multiworld.worlds[loc.player].shuffle_hideoutkeys  == 'any_dungeon') or |                             (oot_is_item_of_type(loc.item, 'HideoutSmallKey')  and multiworld.worlds[loc.player].shuffle_hideoutkeys   in ('overworld', 'any_dungeon', 'regional')) or | ||||||
|                             (oot_is_item_of_type(loc.item, 'BossKey')          and multiworld.worlds[loc.player].shuffle_bosskeys      == 'any_dungeon') or |                             (oot_is_item_of_type(loc.item, 'BossKey')          and multiworld.worlds[loc.player].shuffle_bosskeys      in ('overworld', 'any_dungeon', 'regional')) or | ||||||
|                             (oot_is_item_of_type(loc.item, 'GanonBossKey')     and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): |                             (oot_is_item_of_type(loc.item, 'GanonBossKey')     and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))): | ||||||
|                         if loc.player in barren_hint_players: |                         if loc.player in barren_hint_players: | ||||||
|                             hint_area = get_hint_area(loc) |                             hint_area = get_hint_area(loc) | ||||||
|                             items_by_region[loc.player][hint_area]['weight'] += 1 |                             items_by_region[loc.player][hint_area]['weight'] += 1 | ||||||
|  | @ -1096,7 +1110,12 @@ class OOTWorld(World): | ||||||
|             elif barren_hint_players or woth_hint_players:  # Check only relevant oot locations for barren/woth |             elif barren_hint_players or woth_hint_players:  # Check only relevant oot locations for barren/woth | ||||||
|                 for player in (barren_hint_players | woth_hint_players): |                 for player in (barren_hint_players | woth_hint_players): | ||||||
|                     for loc in multiworld.worlds[player].get_locations(): |                     for loc in multiworld.worlds[player].get_locations(): | ||||||
|                         if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')): |                         if loc.item.code and (not loc.locked or | ||||||
|  |                             (oot_is_item_of_type(loc.item, 'Song') or | ||||||
|  |                                 (oot_is_item_of_type(loc.item, 'SmallKey')         and multiworld.worlds[loc.player].shuffle_smallkeys     in ('overworld', 'any_dungeon', 'regional')) or | ||||||
|  |                                 (oot_is_item_of_type(loc.item, 'HideoutSmallKey')  and multiworld.worlds[loc.player].shuffle_hideoutkeys   in ('overworld', 'any_dungeon', 'regional')) or | ||||||
|  |                                 (oot_is_item_of_type(loc.item, 'BossKey')          and multiworld.worlds[loc.player].shuffle_bosskeys      in ('overworld', 'any_dungeon', 'regional')) or | ||||||
|  |                                 (oot_is_item_of_type(loc.item, 'GanonBossKey')     and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))): | ||||||
|                             if player in barren_hint_players: |                             if player in barren_hint_players: | ||||||
|                                 hint_area = get_hint_area(loc) |                                 hint_area = get_hint_area(loc) | ||||||
|                                 items_by_region[player][hint_area]['weight'] += 1 |                                 items_by_region[player][hint_area]['weight'] += 1 | ||||||
|  | @ -1183,6 +1202,15 @@ class OOTWorld(World): | ||||||
|                             er_hint_data[self.player][location.address] = main_entrance.name |                             er_hint_data[self.player][location.address] = main_entrance.name | ||||||
|                             logger.debug(f"Set {location.name} hint data to {main_entrance.name}") |                             logger.debug(f"Set {location.name} hint data to {main_entrance.name}") | ||||||
| 
 | 
 | ||||||
|  |     def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: | ||||||
|  |         required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t]) | ||||||
|  |         spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n") | ||||||
|  | 
 | ||||||
|  |         if self.shopsanity != 'off': | ||||||
|  |             spoiler_handle.write(f"\nShop Prices ({self.multiworld.get_player_name(self.player)}):\n") | ||||||
|  |             for k, v in self.shop_prices.items(): | ||||||
|  |                 spoiler_handle.write(f"{k}: {v} Rupees\n") | ||||||
|  | 
 | ||||||
|     # Key ring handling: |     # Key ring handling: | ||||||
|     # Key rings are multiple items glued together into one, so we need to give |     # Key rings are multiple items glued together into one, so we need to give | ||||||
|     # the appropriate number of keys in the collection state when they are |     # the appropriate number of keys in the collection state when they are | ||||||
|  | @ -1265,25 +1293,13 @@ class OOTWorld(World): | ||||||
|     # Specifically ensures that only real items are gotten, not any events. |     # Specifically ensures that only real items are gotten, not any events. | ||||||
|     # In particular, ensures that Time Travel needs to be found. |     # In particular, ensures that Time Travel needs to be found. | ||||||
|     def get_state_with_complete_itempool(self): |     def get_state_with_complete_itempool(self): | ||||||
|         all_state = self.multiworld.get_all_state(use_cache=False) |         all_state = CollectionState(self.multiworld) | ||||||
|         # Remove event progression items |         for item in self.multiworld.itempool: | ||||||
|         for item, player in all_state.prog_items: |             if item.player == self.player: | ||||||
|             if player == self.player and (item not in item_table or item_table[item][2] is None): |                 self.multiworld.worlds[item.player].collect(all_state, item) | ||||||
|                 all_state.prog_items[(item, player)] = 0 |  | ||||||
|         # Remove all events and checked locations |  | ||||||
|         all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player} |  | ||||||
|         all_state.events = {loc for loc in all_state.events if loc.player != self.player} |  | ||||||
|         # If free_scarecrow give Scarecrow Song |         # If free_scarecrow give Scarecrow Song | ||||||
|         if self.free_scarecrow: |         if self.free_scarecrow: | ||||||
|             all_state.collect(self.create_item("Scarecrow Song"), event=True) |             all_state.collect(self.create_item("Scarecrow Song"), event=True) | ||||||
| 
 |  | ||||||
|         # Invalidate caches |  | ||||||
|         all_state.child_reachable_regions[self.player] = set() |  | ||||||
|         all_state.adult_reachable_regions[self.player] = set() |  | ||||||
|         all_state.child_blocked_connections[self.player] = set() |  | ||||||
|         all_state.adult_blocked_connections[self.player] = set() |  | ||||||
|         all_state.day_reachable_regions[self.player] = set() |  | ||||||
|         all_state.dampe_reachable_regions[self.player] = set() |  | ||||||
|         all_state.stale[self.player] = True |         all_state.stale[self.player] = True | ||||||
| 
 | 
 | ||||||
|         return all_state |         return all_state | ||||||
|  | @ -1349,7 +1365,7 @@ def gather_locations(multiworld: MultiWorld, | ||||||
|                 condition = lambda location: location.name in dungeon_song_locations |                 condition = lambda location: location.name in dungeon_song_locations | ||||||
|             locations += filter(condition, multiworld.get_unfilled_locations(player=player)) |             locations += filter(condition, multiworld.get_unfilled_locations(player=player)) | ||||||
|     else: |     else: | ||||||
|         if any(map(lambda v: v in {'keysanity'}, fill_opts.values())): |         if any(map(lambda v: v == 'keysanity', fill_opts.values())): | ||||||
|             return None |             return None | ||||||
|         for player, option in fill_opts.items(): |         for player, option in fill_opts.items(): | ||||||
|             condition = functools.partial(valid_dungeon_item_location, |             condition = functools.partial(valid_dungeon_item_location, | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue