From 724999fc43c377161c96dee553f362a551b58c22 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Sun, 22 Oct 2023 10:38:47 -0600 Subject: [PATCH] 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 --- worlds/oot/ItemPool.py | 4 +- worlds/oot/Location.py | 58 ++++- worlds/oot/LocationList.py | 2 +- worlds/oot/Options.py | 489 ++++++++++++++++++++++--------------- worlds/oot/Patches.py | 30 ++- worlds/oot/Rules.py | 3 - worlds/oot/__init__.py | 90 ++++--- 7 files changed, 424 insertions(+), 252 deletions(-) diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 94e1011d..6ca6bc92 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -350,7 +350,7 @@ def generate_itempool(ootworld): ootworld.itempool = [ootworld.create_item(item) for item in pool] for (location_name, item) in placed_items.items(): 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): @@ -675,7 +675,7 @@ def get_pool_core(world): world.remove_from_start_inventory.append('Scarecrow Song') 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') if world.shuffle_smallkeys == 'vanilla': diff --git a/worlds/oot/Location.py b/worlds/oot/Location.py index e2b0e52e..3f7d7551 100644 --- a/worlds/oot/Location.py +++ b/worlds/oot/Location.py @@ -2,6 +2,8 @@ from enum import Enum from .LocationList import location_table from BaseClasses import Location +non_indexed_location_types = {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'} + location_id_offset = 67000 locnames_pre_70 = { "Gift from Sages", @@ -18,7 +20,7 @@ new_name_order = sorted(location_table.keys(), else 0) 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): ENABLED = 0 @@ -83,3 +85,57 @@ def LocationFactory(locations, player: int): 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 + diff --git a/worlds/oot/LocationList.py b/worlds/oot/LocationList.py index 3f4602c4..27ad5756 100644 --- a/worlds/oot/LocationList.py +++ b/worlds/oot/LocationList.py @@ -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 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 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 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"))), diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 03f5346c..120027e2 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -30,7 +30,17 @@ class TrackRandomRange(Range): 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" option_glitchless = 0 option_glitched = 1 @@ -38,12 +48,16 @@ class Logic(Choice): 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" 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" option_open = 0 option_closed_deku = 1 @@ -53,7 +67,10 @@ class Forest(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" option_open = 0 option_zelda = 1 @@ -61,12 +78,15 @@ class Gate(Choice): 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" 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" option_open = 0 option_adult = 1 @@ -75,7 +95,10 @@ class Fountain(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" option_normal = 0 option_fast = 1 @@ -84,7 +107,14 @@ class Fortress(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" option_open = 0 option_vanilla = 1 @@ -122,8 +152,9 @@ class StartingAge(Choice): class InteriorEntrances(Choice): - """Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House, - Temple of Time, and Kak potion shop.""" + """Shuffles interior entrances. + 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" option_off = 0 option_simple = 1 @@ -137,7 +168,9 @@ class GrottoEntrances(Toggle): 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" option_off = 0 option_simple = 1 @@ -146,7 +179,9 @@ class DungeonEntrances(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" option_off = 0 option_limited = 1 @@ -178,19 +213,19 @@ class SpawnPositions(Choice): alias_true = 3 -class MixEntrancePools(Choice): - """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all" - mixes them in.""" - display_name = "Mix Entrance Pools" - option_off = 0 - option_indoor = 1 - option_all = 2 +# class MixEntrancePools(Choice): +# """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all" +# mixes them in.""" +# display_name = "Mix Entrance Pools" +# option_off = 0 +# option_indoor = 1 +# option_all = 2 -class DecoupleEntrances(Toggle): - """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if - overworld is shuffled.""" - display_name = "Decouple Entrances" +# class DecoupleEntrances(Toggle): +# """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if +# overworld is shuffled.""" +# display_name = "Decouple Entrances" class TriforceHunt(Toggle): @@ -216,13 +251,17 @@ class ExtraTriforces(Range): class LogicalChus(Toggle): - """Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell - refills; bombchus open Bombchu Bowling.""" + """Bombchus are properly considered in logic. + 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" 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" option_off = 0 option_choice = 1 @@ -246,7 +285,11 @@ class DungeonShortcutsList(OptionSet): 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" option_vanilla = 0 option_mq = 1 @@ -255,7 +298,7 @@ class MQDungeons(Choice): 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" valid_keys = { "Deku Tree", @@ -274,41 +317,41 @@ class MQDungeonList(OptionSet): 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" range_start = 0 range_end = 12 default = 0 -class EmptyDungeons(Choice): - """Pre-completed dungeons are barren and rewards are given for free.""" - display_name = "Pre-completed Dungeons Mode" - option_none = 0 - option_specific = 1 - option_count = 2 +# class EmptyDungeons(Choice): +# """Pre-completed dungeons are barren and rewards are given for free.""" +# display_name = "Pre-completed Dungeons Mode" +# option_none = 0 +# option_specific = 1 +# option_count = 2 -class EmptyDungeonList(OptionSet): - """Chosen dungeons to be pre-completed.""" - display_name = "Pre-completed Dungeon List" - valid_keys = { - "Deku Tree", - "Dodongo's Cavern", - "Jabu Jabu's Belly", - "Forest Temple", - "Fire Temple", - "Water Temple", - "Shadow Temple", - "Spirit Temple", - } +# class EmptyDungeonList(OptionSet): +# """Chosen dungeons to be pre-completed.""" +# display_name = "Pre-completed Dungeon List" +# valid_keys = { +# "Deku Tree", +# "Dodongo's Cavern", +# "Jabu Jabu's Belly", +# "Forest Temple", +# "Fire Temple", +# "Water Temple", +# "Shadow Temple", +# "Spirit Temple", +# } -class EmptyDungeonCount(Range): - display_name = "Pre-completed Dungeon Count" - range_start = 1 - range_end = 8 - default = 2 +# class EmptyDungeonCount(Range): +# display_name = "Pre-completed Dungeon Count" +# range_start = 1 +# range_end = 8 +# default = 2 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): - """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" range_start = 0 range_end = 3 @@ -401,7 +393,7 @@ class BridgeStones(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" range_start = 0 range_end = 6 @@ -409,7 +401,7 @@ class BridgeMedallions(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" range_start = 0 range_end = 9 @@ -417,7 +409,7 @@ class BridgeRewards(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" range_start = 0 range_end = 100 @@ -425,7 +417,7 @@ class BridgeTokens(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" range_start = 4 range_end = 20 @@ -442,7 +434,15 @@ bridge_options: typing.Dict[str, type(Option)] = { 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" option_song = 0 option_dungeon = 1 @@ -450,8 +450,10 @@ class SongShuffle(Choice): class ShopShuffle(Choice): - """Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop; - "random_number" randomizes the value for each shop. """ + """Randomizes shop contents. + 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" option_off = 0 option_fixed_number = 1 @@ -459,15 +461,20 @@ class ShopShuffle(Choice): class ShopSlots(Range): - """Number of items per shop to be randomized into the main itempool. - Only active if Shopsanity is set to "fixed_number." """ + """With Shopsanity fixed number: quantity of multiworld locations per shop to be randomized.""" display_name = "Shuffled Shop Slots" range_start = 0 range_end = 4 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" option_normal = 0 option_affordable = 1 @@ -478,7 +485,10 @@ class ShopPrices(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" option_off = 0 option_dungeons = 1 @@ -487,7 +497,11 @@ class TokenShuffle(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" option_off = 0 option_low = 1 @@ -513,7 +527,11 @@ class ShuffleOcarinas(Toggle): 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" option_vanilla = 0 option_shuffle = 1 @@ -538,30 +556,39 @@ class ShuffleMedigoronCarpet(Toggle): 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" option_off = 0 - option_all = 1 + option_dungeons = 1 option_overworld = 2 - option_dungeons = 3 + option_all = 3 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" option_off = 0 - option_all = 1 + option_dungeons = 1 option_overworld = 2 - option_dungeons = 3 + option_all = 3 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" option_off = 0 - option_all = 1 + option_dungeons = 1 option_overworld = 2 - option_dungeons = 3 + option_all = 3 class ShuffleBeehives(Toggle): @@ -597,72 +624,113 @@ shuffle_options: typing.Dict[str, type(Option)] = { 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" option_remove = 0 option_startwith = 1 option_vanilla = 2 option_dungeon = 3 - option_overworld = 4 - option_any_dungeon = 5 - option_keysanity = 6 - option_regional = 7 + option_regional = 4 + option_overworld = 5 + option_any_dungeon = 6 + option_keysanity = 7 default = 1 - alias_anywhere = 6 + alias_anywhere = 7 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" option_remove = 0 option_vanilla = 2 option_dungeon = 3 - option_overworld = 4 - option_any_dungeon = 5 - option_keysanity = 6 - option_regional = 7 + option_regional = 4 + option_overworld = 5 + option_any_dungeon = 6 + option_keysanity = 7 default = 3 alias_keysy = 0 - alias_anywhere = 6 + alias_anywhere = 7 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" option_vanilla = 0 - option_overworld = 1 - option_any_dungeon = 2 - option_keysanity = 3 - option_regional = 4 - alias_anywhere = 3 + option_regional = 1 + option_overworld = 2 + option_any_dungeon = 3 + option_keysanity = 4 + alias_anywhere = 4 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" option_remove = 0 option_vanilla = 2 option_dungeon = 3 - option_overworld = 4 - option_any_dungeon = 5 - option_keysanity = 6 - option_regional = 7 + option_regional = 4 + option_overworld = 5 + option_any_dungeon = 6 + option_keysanity = 7 default = 3 alias_keysy = 0 - alias_anywhere = 6 + alias_anywhere = 7 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" option_remove = 0 option_vanilla = 2 option_dungeon = 3 - option_overworld = 4 - option_any_dungeon = 5 - option_keysanity = 6 - option_on_lacs = 7 - option_regional = 8 + option_regional = 4 + option_overworld = 5 + option_any_dungeon = 6 + option_keysanity = 7 + option_on_lacs = 8 option_stones = 9 option_medallions = 10 option_dungeons = 11 @@ -670,7 +738,7 @@ class ShuffleGanonBK(Choice): option_hearts = 13 default = 0 alias_keysy = 0 - alias_anywhere = 6 + alias_anywhere = 7 class EnhanceMC(Toggle): @@ -679,7 +747,7 @@ class EnhanceMC(Toggle): 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" range_start = 1 range_end = 6 @@ -687,7 +755,7 @@ class GanonBKMedallions(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" range_start = 1 range_end = 3 @@ -695,7 +763,7 @@ class GanonBKStones(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" range_start = 1 range_end = 9 @@ -703,7 +771,7 @@ class GanonBKRewards(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" range_start = 1 range_end = 100 @@ -711,7 +779,7 @@ class GanonBKTokens(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" range_start = 4 range_end = 20 @@ -719,7 +787,9 @@ class GanonBKHearts(Range): 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" option_off = 0 option_choose = 1 @@ -728,7 +798,7 @@ class KeyRings(Choice): 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" valid_keys = { "Thieves' Hideout", @@ -828,7 +898,8 @@ class BigPoeCount(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" range_start = 1 range_end = 24 @@ -853,7 +924,11 @@ timesavers_options: typing.Dict[str, type(Option)] = { 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" option_off = 0 option_textures = 1 @@ -872,15 +947,24 @@ class InvisibleChests(Toggle): 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" option_off = 0 option_textures_content = 1 option_textures_unchecked = 2 + default = 2 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" option_none = 0 option_mask = 1 @@ -895,7 +979,9 @@ class MiscHints(DefaultOnToggle): 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" option_balanced = 0 option_ddr = 1 @@ -907,10 +993,13 @@ class HintDistribution(Choice): option_useless = 7 option_very_strong = 8 option_async = 9 + default = 9 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" option_none = 0 option_except_hints = 1 @@ -946,7 +1035,8 @@ class HeroMode(Toggle): 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" option_default = 0 option_sunrise = 1 @@ -999,7 +1089,11 @@ misc_options: typing.Dict[str, type(Option)] = { } 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" option_plentiful = 0 option_balanced = 1 @@ -1009,7 +1103,12 @@ class ItemPoolValue(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" option_off = 0 option_normal = 1 @@ -1021,34 +1120,27 @@ class IceTraps(Choice): class IceTrapVisual(Choice): - """Changes the appearance of ice traps as freestanding items.""" - display_name = "Ice Trap Appearance" + """Changes the appearance of traps, including other games' traps, as freestanding items.""" + display_name = "Trap Appearance" option_major_only = 0 option_junk_only = 1 option_anything = 2 -class AdultTradeStart(OptionSet): - """Choose the items that can appear to start the adult trade sequence. By default it is Claim Check only.""" - display_name = "Adult Trade Sequence Items" - default = {"Claim Check"} - valid_keys = { - "Pocket Egg", - "Pocket Cucco", - "Cojiro", - "Odd Mushroom", - "Poachers Saw", - "Broken Sword", - "Prescription", - "Eyeball Frog", - "Eyedrops", - "Claim Check", - } - - def __init__(self, value: typing.Iterable[str]): - if not value: - value = self.default - super().__init__(value) +class AdultTradeStart(Choice): + """Choose the item that starts the adult trade sequence.""" + display_name = "Adult Trade Sequence Start" + option_pocket_egg = 0 + option_pocket_cucco = 1 + option_cojiro = 2 + option_odd_mushroom = 3 + option_poachers_saw = 4 + option_broken_sword = 5 + option_prescription = 6 + option_eyeball_frog = 7 + option_eyedrops = 8 + option_claim_check = 9 + default = 9 itempool_options: typing.Dict[str, type(Option)] = { @@ -1068,7 +1160,7 @@ class Targeting(Choice): 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" @@ -1191,7 +1283,6 @@ oot_options: typing.Dict[str, type(Option)] = { **world_options, **bridge_options, **dungeon_items_options, - # **lacs_options, **shuffle_options, **timesavers_options, **misc_options, diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index ab1e75d1..f83b3418 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -2094,10 +2094,14 @@ def patch_rom(world, rom): if not world.dungeon_mq['Ganons Castle']: chest_name = 'Ganons Castle Light Trial Lullaby Chest' location = world.get_location(chest_name) - if location.item.game == 'Ocarina of Time': - item = read_rom_item(rom, location.item.index) + if not location.item.trap: + if location.item.game == 'Ocarina of Time': + item = read_rom_item(rom, location.item.index) + else: + item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) else: - item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) + 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): rom.write_int16(0x321B176, 0xFC40) # original 0xFC48 @@ -2106,10 +2110,14 @@ def patch_rom(world, rom): chest_name = 'Spirit Temple Compass Chest' chest_address = 0x2B6B07C location = world.get_location(chest_name) - if location.item.game == 'Ocarina of Time': - item = read_rom_item(rom, location.item.index) + if not location.item.trap: + if location.item.game == 'Ocarina of Time': + item = read_rom_item(rom, location.item.index) + else: + item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) else: - item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) + 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): rom.write_int16(chest_address + 2, 0x0190) # X 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_2 = 0x21A06E4 # Address in setup 2 location = world.get_location(chest_name) - if location.item.game == 'Ocarina of Time': - item = read_rom_item(rom, location.item.index) + if not location.item.trap: + if location.item.game == 'Ocarina of Time': + item = read_rom_item(rom, location.item.index) + else: + item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) else: - item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) + 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): rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 1f44cebd..fa198e0c 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -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 def set_entrances_based_rules(ootworld): - if ootworld.multiworld.accessibility == 'beatable': - return - all_state = ootworld.multiworld.get_all_state(False) for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()): diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 539abd96..6af19683 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -10,7 +10,7 @@ from string import printable 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 .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError 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 Blue Potion", "Bottle with Fairy", "Bottle with Fish", "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", - "Eyeball Frog", "Eyedrops", "Claim Check"} + "Eyeball Frog", "Eyedrops", "Claim Check"}, } + location_name_groups = build_location_name_groups() + def __init__(self, world, player): self.hint_data_available = threading.Event() self.collectible_flags_available = threading.Event() @@ -384,6 +386,7 @@ class OOTWorld(World): self.mq_dungeons_mode = 'count' self.mq_dungeons_count = 0 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 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.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 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)] # 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 self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances): # nayru's love may be required to prevent forced damage @@ -633,16 +639,18 @@ class OOTWorld(World): self.multiworld.itempool.remove(item) 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: return OOTItem(name, self.player, item_table[name], False, (name in self.nonadvancement_items if getattr(self, 'nonadvancement_items', None) else False)) - return OOTItem(name, self.player, ('Event', True, None, None), True, False) + if allow_arbitrary_name: + 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): 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) location.locked = True location.event = True @@ -800,23 +808,25 @@ class OOTWorld(World): self.multiworld.itempool.remove(item) self.multiworld.random.shuffle(locations) 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: for dungeon_info in dungeon_table: dungeon_name = dungeon_info['name'] locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name) if isinstance(locations, list): dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) + if not dungeon_items: + continue for item in dungeon_items: self.multiworld.itempool.remove(item) self.multiworld.random.shuffle(locations) 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 # 5 built-in retries because this section can fail sometimes if self.shuffle_song_items != 'any': - tries = 5 + tries = 10 if self.shuffle_song_items == 'song': song_locations = list(filter(lambda location: location.type == 'Song', self.multiworld.get_unfilled_locations(player=self.player))) @@ -852,7 +862,7 @@ class OOTWorld(World): try: self.multiworld.random.shuffle(song_locations) 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)") except FillError as e: tries -= 1 @@ -888,7 +898,8 @@ class OOTWorld(World): self.multiworld.random.shuffle(shop_locations) for item in shop_prog + shop_junk: 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) for loc in shop_locations: loc.locked = True @@ -963,7 +974,7 @@ class OOTWorld(World): multiworld.itempool.remove(item) multiworld.random.shuffle(locations) 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': # We don't want song locations to contain progression unless it's a song # or it was marked as priority. @@ -984,7 +995,7 @@ class OOTWorld(World): multiworld.itempool.remove(item) multiworld.random.shuffle(locations) 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): if self.hints != 'none': @@ -1051,7 +1062,10 @@ class OOTWorld(World): def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str): def hint_type_players(hint_type: str) -> set: 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: 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 (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, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys == 'any_dungeon') 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, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): + (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 loc.player in barren_hint_players: hint_area = get_hint_area(loc) 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 for player in (barren_hint_players | woth_hint_players): 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: hint_area = get_hint_area(loc) 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 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 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 @@ -1265,25 +1293,13 @@ class OOTWorld(World): # Specifically ensures that only real items are gotten, not any events. # In particular, ensures that Time Travel needs to be found. def get_state_with_complete_itempool(self): - all_state = self.multiworld.get_all_state(use_cache=False) - # Remove event progression items - for item, player in all_state.prog_items: - if player == self.player and (item not in item_table or item_table[item][2] is None): - 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} + all_state = CollectionState(self.multiworld) + for item in self.multiworld.itempool: + if item.player == self.player: + self.multiworld.worlds[item.player].collect(all_state, item) # If free_scarecrow give Scarecrow Song if self.free_scarecrow: 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 return all_state @@ -1349,7 +1365,7 @@ def gather_locations(multiworld: MultiWorld, condition = lambda location: location.name in dungeon_song_locations locations += filter(condition, multiworld.get_unfilled_locations(player=player)) else: - if any(map(lambda v: v in {'keysanity'}, fill_opts.values())): + if any(map(lambda v: v == 'keysanity', fill_opts.values())): return None for player, option in fill_opts.items(): condition = functools.partial(valid_dungeon_item_location,