diff --git a/worlds/ladx/Common.py b/worlds/ladx/Common.py index e85e9767b91d..a3cc4540f6ee 100644 --- a/worlds/ladx/Common.py +++ b/worlds/ladx/Common.py @@ -1,2 +1,2 @@ LINKS_AWAKENING = "Links Awakening DX" -BASE_ID = 10000000 \ No newline at end of file +BASE_ID = 10000000 diff --git a/worlds/ladx/LADXR/itempool.py b/worlds/ladx/LADXR/itempool.py index 50314883378a..ebd000e2f3d0 100644 --- a/worlds/ladx/LADXR/itempool.py +++ b/worlds/ladx/LADXR/itempool.py @@ -1,4 +1,5 @@ from .locations.items import * +from typing import Dict DEFAULT_ITEM_POOL = { @@ -165,12 +166,12 @@ def __setup(self, logic, settings): for n in range(9): self.remove("MAP%d" % (n + 1)) self.remove("COMPASS%d" % (n + 1)) - self.add("KEY%d" % (n +1)) - self.add("NIGHTMARE_KEY%d" % (n +1)) + self.add("KEY%d" % (n + 1)) + self.add("NIGHTMARE_KEY%d" % (n + 1)) if settings.owlstatues in ("none", "overworld"): for n in range(9): self.remove("STONE_BEAK%d" % (n + 1)) - self.add("KEY%d" % (n +1)) + self.add("KEY%d" % (n + 1)) # if settings.dungeon_items == 'keysy': # for n in range(9): @@ -274,5 +275,5 @@ def __randomizeRupees(self, options, rnd): self.add(new_item) self.remove(remove_item) - def toDict(self): + def toDict(self) -> Dict[str, int]: return self.__pool.copy() diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py index f3caa5be6160..1b022393a2cd 100644 --- a/worlds/ladx/LADXR/logic/overworld.py +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -619,20 +619,20 @@ def __init__(self, options, r): self.overworld_entrance = {} self.indoor_location = {} - start_house = Location("Start House").add(StartItem()) - Location().add(ShopItem(0)).connect(start_house, OR(COUNT("RUPEES", 200), SWORD)) - Location().add(ShopItem(1)).connect(start_house, OR(COUNT("RUPEES", 980), SWORD)) - Location().add(Song(0x0B1)).connect(start_house, OCARINA) # Marins song + start_house = IndoorLocation("Start House").add(StartItem()) + VirtualLocation().add(ShopItem(0)).connect(start_house, OR(COUNT("RUPEES", 200), SWORD)) + VirtualLocation().add(ShopItem(1)).connect(start_house, OR(COUNT("RUPEES", 980), SWORD)) + VirtualLocation().add(Song(0x0B1)).connect(start_house, OCARINA) # Marins song start_house.add(DroppedKey(0xB2)) # Sword on the beach - egg = Location().connect(start_house, AND(r.bush, BOMB)) - Location().add(MadBatter(0x1E1)).connect(start_house, MAGIC_POWDER) + egg = VirtualLocation().connect(start_house, AND(r.bush, BOMB)) + VirtualLocation().add(MadBatter(0x1E1)).connect(start_house, MAGIC_POWDER) if options.boomerang == 'trade': - Location().add(BoomerangGuy()).connect(start_house, AND(BOMB, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL))) + VirtualLocation().add(BoomerangGuy()).connect(start_house, AND(BOMB, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL))) elif options.boomerang == 'gift': - Location().add(BoomerangGuy()).connect(start_house, BOMB) + VirtualLocation().add(BoomerangGuy()).connect(start_house, BOMB) - nightmare = Location("Nightmare") - windfish = Location("Windfish").connect(nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW))) + nightmare = VirtualLocation("Nightmare") + windfish = VirtualLocation("Windfish").connect(nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW))) self.start = start_house self.overworld_entrance = { diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index 98016a18c08b..d1da151c13ab 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -1,5 +1,5 @@ from BaseClasses import Region, Entrance, Location, CollectionState - +from typing import List from .LADXR.checkMetadata import checkMetadataTable from .Common import * @@ -171,7 +171,7 @@ def ladxr_region_to_name(n): return name -def create_regions_from_ladxr(player, multiworld, logic): +def create_regions_from_ladxr(player, multiworld, logic) -> List[LinksAwakeningRegion]: tmp = set() def print_items(n): diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index b0d3e2ccc2c5..9757068c5399 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -73,18 +73,21 @@ class Boomerang(Choice): gift = 1 default = gift + class BossShuffle(Choice): none = 0 shuffle = 1 random = 2 default = none + class EntranceShuffle(Choice): option_vanilla = 0 option_simple = 1 option_mixed = 2 alias_false = option_vanilla + class StartShufflePool(OptionList): """ Shuffle Start Location @@ -121,6 +124,7 @@ class StartShufflePool(OptionList): # option_connector = 6 # option_dungeon = 7 + class SingleEntranceShuffle(EntranceShuffle): """ Shuffle Single Entrances (non connectors with checks inside) @@ -128,7 +132,8 @@ class SingleEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ - entrance_type=["single", "trade"] + entrance_type = ["single", "trade"] + class DummyEntranceShuffle(EntranceShuffle): """ @@ -137,7 +142,8 @@ class DummyEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ - entrance_type=["dummy"] + entrance_type = ["dummy"] + class AnnoyingEntranceShuffle(EntranceShuffle): """ @@ -146,7 +152,8 @@ class AnnoyingEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ - entrance_type=["insanity"] + entrance_type = ["insanity"] + class WaterEntranceShuffle(EntranceShuffle): """ @@ -155,7 +162,8 @@ class WaterEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ - entrance_type=["water"] + entrance_type = ["water"] + class ConnectorEntranceShuffle(EntranceShuffle): """ @@ -164,7 +172,8 @@ class ConnectorEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ - entrance_type=["connector"] + entrance_type = ["connector"] + class DungeonEntranceShuffle(EntranceShuffle): """ @@ -173,7 +182,8 @@ class DungeonEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among other entrances """ - entrance_type=["dungeon"] + entrance_type = ["dungeon"] + class APTitleScreen(DefaultOnToggle): """ @@ -181,6 +191,7 @@ class APTitleScreen(DefaultOnToggle): """ display_name = "AP Title Screen" + class OwnItemOnTarin(DefaultOnToggle): """ Forces one of your own items to be on Tarin. @@ -563,6 +574,7 @@ class AdditionalWarpPoints(DefaultOffToggle): [Off] No change """ + ladx_option_groups = [ OptionGroup("Goal Options", [ Goal, @@ -587,10 +599,6 @@ class AdditionalWarpPoints(DefaultOffToggle): NagMessages, BootsControls ]), - OptionGroup("Experimental", [ - DungeonShuffle, - EntranceShuffle - ]), OptionGroup("Visuals & Sound", [ LinkPalette, Palette, @@ -602,6 +610,7 @@ class AdditionalWarpPoints(DefaultOffToggle): ]) ] + @dataclass class LinksAwakeningOptions(PerGameCommonOptions): logic: Logic diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index b0eb8c6438fe..fcb976cacd4c 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,5 +1,8 @@ import binascii +import dataclasses import os +import copy +import itertools import pkgutil import tempfile import typing @@ -14,6 +17,7 @@ from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData, ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name) from .LADXR import generator +from .LADXR.entranceInfo import ENTRANCE_INFO, entrances_by_type from .LADXR.itempool import ItemPool as LADXRItemPool from .LADXR.locations.constants import CHEST_ITEMS from .LADXR.locations.instrument import Instrument @@ -22,8 +26,8 @@ from .LADXR.main import get_parser from .LADXR.settings import Settings as LADXRSettings from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup -from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, - create_regions_from_ladxr, get_locations_to_id) +from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, create_regions_from_ladxr, get_locations_to_id, + connector_info) from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups, EntranceShuffle from .Rom import LADXDeltaPatch, get_base_rom_path @@ -56,6 +60,7 @@ class DisplayMsgs(settings.Bool): rom_file: RomFile = RomFile(RomFile.copy_to) rom_start: typing.Union[RomStart, bool] = True + class LinksAwakeningWebWorld(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", @@ -68,6 +73,7 @@ class LinksAwakeningWebWorld(WebWorld): theme = "dirt" option_groups = ladx_option_groups + class LinksAwakeningWorld(World): """ After a previous adventure, Link is stranded on Koholint Island, full of mystery and familiar faces. @@ -90,7 +96,7 @@ class LinksAwakeningWorld(World): # items exist. They could be generated from json or something else. They can # include events, but don't have to since events will be placed manually. item_name_to_id = { - item.item_name : BASE_ID + item.item_id for item in links_awakening_items + item.item_name: BASE_ID + item.item_id for item in links_awakening_items } item_name_to_data = links_awakening_items_by_name @@ -99,9 +105,9 @@ class LinksAwakeningWorld(World): # Items can be grouped using their names to allow easy checking if any item # from that group has been collected. Group names can also be used for !hint - #item_name_groups = { + # item_name_groups = { # "weapons": {"sword", "lance"} - #} + # } prefill_dungeon_items = None @@ -119,17 +125,20 @@ class LinksAwakeningWorld(World): ItemName.RUPEES_500: 500, } + world_setup = None + prefill_original_dungeon = [[], [], [], [], [], [], [], [], []] + prefill_own_dungeons = [] + pre_fill_items = [] + dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] + def convert_ap_options_to_ladxr_logic(self): self.ladxr_settings = LADXRSettings(dataclasses.asdict(self.options)) - - self.ladxr_options = LADXRSettings(self.player_options) - self.ladxr_settings.validate() - world_setup = LADXRWorldSetup() - world_setup.randomize(self.ladxr_settings, self.random) + self.world_setup = LADXRWorldSetup() + self.world_setup.randomize(self.ladxr_settings, self.random) self.randomize_entrances() - self.ladxr_logic = LADXRLogic(configuration_options=self.ladxr_settings, world_setup=world_setup) - self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random).toDict() + self.ladxr_logic = LADXRLogic(configuration_options=self.ladxr_settings, world_setup=self.world_setup) + self.ladxr_itempool: typing.Dict[str, int] = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random).toDict() def randomize_entrances(self): banned_starts = [ @@ -166,16 +175,16 @@ def randomize_entrances(self): entrance_pool_mapping = {} random = self.multiworld.per_slot_randoms[self.player] - world = World(self.laxdr_options, self.world_setup, RequirementsSettings(self.laxdr_options)) + world = World(self.ladxr_settings, self.world_setup, RequirementsSettings(self.ladxr_settings)) # First shuffle the start location, if needed start = world.start start_entrance = "start_house" - start_shuffle = self.player_options["start_shuffle"] + start_shuffle = self.options.start_shuffle start_type_mappings = {} - for option_name, option in self.player_options.items(): + for option_name, option in self.options.as_dict(): if isinstance(option, EntranceShuffle): for cat in option.entrance_type: start_type_mappings[option_name] = cat @@ -228,15 +237,12 @@ def randomize_entrances(self): # This entrance wasn't shuffled, just map back self.world_setup.entrance_mapping["start_house"] = start_entrance - for pool in itertools.chain(entrance_pools.values(), indoor_pools.values()): # Sort first so that we get the same result every time pool.sort() has_castle_button = False - - # NOTE: this code uses LADXR terms for things where: # Region -> Location # Location -> ItemInfo @@ -263,9 +269,6 @@ def walk_locations(callback, current_location, filter=lambda _: True, walked=Non if check_castle_button(current_location, o): walk_locations(callback, o, filter, walked) - - - # First shuffle connectors, as they will fail if shuffled randomly if "connector" in entrance_pool_mapping: # Get the list of unshuffled connectors @@ -276,8 +279,8 @@ def walk_locations(callback, current_location, filter=lambda _: True, walked=Non unseen_entrances = copy.copy(entrance_pools[entrance_pool_mapping["connector"]]) location_to_entrances = {} - for k,v in world.overworld_entrance.items(): - location_to_entrances.setdefault(v.location,[]).append(k) + for k, v in world.overworld_entrance.items(): + location_to_entrances.setdefault(v.location, []).append(k) unshuffled_entrances = entrance_pools[entrance_pool_mapping["connector"]] seen_locations = set() @@ -356,7 +359,6 @@ def mark_location(l): seen_keys.add(k) seen_values.add(v) - def create_regions(self) -> None: # Initialize self.convert_ap_options_to_ladxr_logic() @@ -370,7 +372,7 @@ def create_regions(self) -> None: start = region break - assert(start) + assert start menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld) menu_region.exits = [Entrance(self.player, "Start Game", menu_region)] @@ -393,13 +395,19 @@ def create_regions(self) -> None: self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player) - - def create_item(self, item_name: str): + def create_item(self, item_name: str) -> LinksAwakeningItem: return LinksAwakeningItem(self.item_name_to_data[item_name], self, self.player) def create_event(self, event: str): return Item(event, ItemClassification.progression, None, self.player) + def get_regions(self, player: int) -> typing.Collection[LinksAwakeningRegion]: + regions = self.multiworld.get_regions(player) + for region in regions: + typing.cast(LinksAwakeningRegion, region) + regions = typing.cast(typing.Collection[LinksAwakeningRegion], regions) + return regions + def create_items(self) -> None: itempool = [] @@ -408,10 +416,6 @@ def create_items(self) -> None: dungeon_item_types = { } - - self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ] - self.prefill_own_dungeons = [] - self.pre_fill_items = [] # For any and different world, set item rule instead for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]: @@ -464,7 +468,7 @@ def create_items(self) -> None: # Find instrument, lock # TODO: we should be able to pinpoint the region we want, save a lookup table please found = False - for r in self.multiworld.get_regions(self.player): + for r in self.get_regions(self.player): if r.dungeon_index != item.item_data.dungeon_index: continue for loc in r.locations: @@ -496,9 +500,8 @@ def create_items(self) -> None: event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region) trendy_region.locations.insert(0, event_location) event_location.place_locked_item(self.create_event("Can Play Trendy Game")) - - self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] - for r in self.multiworld.get_regions(self.player): + + for r in self.get_regions(self.player): # Set aside dungeon locations if r.dungeon_index: self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations @@ -510,7 +513,7 @@ def create_items(self) -> None: self.dungeon_locations_by_dungeon[r.dungeon_index - 1].remove(location) # Properly fill locations within dungeon location.dungeon = r.dungeon_index - if self.multiworld.tarin_gifts_your_item[self.player]: + if self.options.tarin_gifts_your_item: self.force_start_item(itempool) self.multiworld.itempool += itempool @@ -578,9 +581,9 @@ def pre_fill(self) -> None: # 2. Either # 2a. it's not a restricted dungeon item # 2b. it's a restricted dungeon item and this location is specified as allowed - location.item_rule = lambda item, location=location, orig_rule=orig_rule: \ - (item not in allowed_locations_by_item or location in allowed_locations_by_item[item]) and orig_rule(item) - + # the weird names were to get pycharm to shut up + location.item_rule = lambda itm, loc=location, orig=orig_rule: \ + ((itm not in allowed_locations_by_item or loc in allowed_locations_by_item[itm]) and orig(itm)) # Now set up the allow-list for any-dungeon items for item in self.prefill_own_dungeons: # They of course get to go in any spot @@ -616,9 +619,11 @@ def priority(item): all_state.remove(item) # Finally, fill! - fill_restrictive(self.multiworld, all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False) + fill_restrictive(self.multiworld, all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, + single_player_placement=True, allow_partial=False) name_cache = {} + # Tries to associate an icon from another game with an icon we have def guess_icon_for_other_world(self, other): if not self.name_cache: @@ -671,8 +676,7 @@ def guess_icon_for_other_world(self, other): assert name in self.name_cache, name assert name in CHEST_ITEMS, name self.name_cache.update(others) - - + uppered = other.upper() if "BIG KEY" in uppered: return 'NIGHTMARE_KEY' @@ -697,7 +701,7 @@ def generate_output(self, output_directory: str): for r in self.multiworld.get_regions(self.player): for loc in r.locations: if isinstance(loc, LinksAwakeningLocation): - assert(loc.item) + assert loc.item # If we're a links awakening item, just use the item if isinstance(loc.item, LinksAwakeningItem):