From 2c47b088ba1aabf2498fb54bc173d3a8cd1d6e66 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 25 Mar 2023 21:08:05 -0700 Subject: [PATCH 01/32] wip --- worlds/ladx/LADXR/logic/location.py | 32 ++- worlds/ladx/LADXR/logic/overworld.py | 380 +++++++++++++-------------- worlds/ladx/__init__.py | 2 +- 3 files changed, 222 insertions(+), 192 deletions(-) diff --git a/worlds/ladx/LADXR/logic/location.py b/worlds/ladx/LADXR/logic/location.py index 18615a11647f..e166df44f666 100644 --- a/worlds/ladx/LADXR/logic/location.py +++ b/worlds/ladx/LADXR/logic/location.py @@ -2,17 +2,33 @@ from .requirements import hasConsumableRequirement, OR from ..locations.itemInfo import ItemInfo +from enum import Enum + +class LocationType(Enum): + Unknown = 0 + Overworld = 1 + Dungeon = 2 + Indoor = 3 class Location: - def __init__(self, name=None, dungeon=None): + def __init__(self, name=None, location_type=None, dungeon=None): self.name = name self.items = [] # type: typing.List[ItemInfo] self.dungeon = dungeon + if self.dungeon != None: + assert location_type == None or location_type == LocationType.Dungeon + location_type = LocationType.Dungeon + if location_type is not None: + self.location_type = location_type self.__connected_to = set() self.simple_connections = [] self.gated_connections = [] def add(self, *item_infos): + if not self.name: + meta = item_infos[0].metadata + self.name = f"{meta.name} ({meta.area})" + for ii in item_infos: assert isinstance(ii, ItemInfo) ii.setLocation(self) @@ -55,3 +71,17 @@ def connect(self, other, req, *, one_way=False): def __repr__(self): return "<%s:%s:%d:%d:%d>" % (self.__class__.__name__, self.dungeon, len(self.items), len(self.simple_connections), len(self.gated_connections)) + +class OverworldLocation(Location): + def __init__(self, name=None): + assert(name) + Location.__init__(self, name, location_type=LocationType.Overworld) + + +class IndoorLocation(Location): + def __init__(self, name): + assert(name) + Location.__init__(self, name, location_type=LocationType.Indoor) + +class VirtualLocation(Location): + pass \ No newline at end of file diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py index 551cf8353f4a..ac47c2ff131e 100644 --- a/worlds/ladx/LADXR/logic/overworld.py +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -1,5 +1,5 @@ from .requirements import * -from .location import Location +from .location import OverworldLocation, IndoorLocation, VirtualLocation from ..locations.all import * from ..worldSetup import ENTRANCE_INFO @@ -9,20 +9,19 @@ def __init__(self, options, world_setup, r): self.overworld_entrance = {} self.indoor_location = {} - mabe_village = Location("Mabe Village") - Location().add(HeartPiece(0x2A4)).connect(mabe_village, r.bush) # well - Location().add(FishingMinigame()).connect(mabe_village, AND(r.bush, COUNT("RUPEES", 20))) # fishing game, heart piece is directly done by the minigame. - Location().add(Seashell(0x0A3)).connect(mabe_village, r.bush) # bushes below the shop - Location().add(Seashell(0x0D2)).connect(mabe_village, PEGASUS_BOOTS) # smash into tree next to lv1 - Location().add(Song(0x092)).connect(mabe_village, OCARINA) # Marins song - rooster_cave = Location("Rooster Cave") - Location().add(DroppedKey(0x1E4)).connect(rooster_cave, AND(OCARINA, SONG3)) + mabe_village = OverworldLocation("Mabe Village") + IndoorLocation("Mabe Village Well").add(HeartPiece(0x2A4)).connect(mabe_village, r.bush) # well + OverworldLocation("Fishing Game").add(FishingMinigame()).connect(mabe_village, AND(r.bush, COUNT("RUPEES", 20))) # fishing game, heart piece is directly done by the minigame. + VirtualLocation().add(Seashell(0x0A3)).connect(mabe_village, r.bush) # bushes below the shop + VirtualLocation().add(Seashell(0x0D2)).connect(mabe_village, PEGASUS_BOOTS) # smash into tree next to lv1 + VirtualLocation().add(Song(0x092)).connect(mabe_village, OCARINA) # Marins song + rooster_cave = IndoorLocation("Rooster Cave") + VirtualLocation().add(DroppedKey(0x1E4)).connect(rooster_cave, AND(OCARINA, SONG3)) - papahl_house = Location("Papahl House") - papahl_house.connect(Location().add(TradeSequenceItem(0x2A6, TRADING_ITEM_RIBBON)), TRADING_ITEM_YOSHI_DOLL) + papahl_house = IndoorLocation("Papahl House") + papahl_house.connect(VirtualLocation().add(TradeSequenceItem(0x2A6, TRADING_ITEM_RIBBON)), TRADING_ITEM_YOSHI_DOLL) - trendy_shop = Location("Trendy Shop").add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)) - #trendy_shop.connect(Location()) + trendy_shop = IndoorLocation("Trendy Shop").add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)) self._addEntrance("papahl_house_left", mabe_village, papahl_house, None) self._addEntrance("papahl_house_right", mabe_village, papahl_house, None) @@ -36,232 +35,233 @@ def __init__(self, options, world_setup, r): self._addEntrance("d1", mabe_village, None, TAIL_KEY) self._addEntranceRequirementExit("d1", None) # if exiting, you do not need the key - start_house = Location("Start House").add(StartItem()) + start_house = IndoorLocation("Tarin's House").add(StartItem()) self._addEntrance("start_house", mabe_village, start_house, None) - shop = Location("Shop") - Location().add(ShopItem(0)).connect(shop, OR(COUNT("RUPEES", 500), SWORD)) - Location().add(ShopItem(1)).connect(shop, OR(COUNT("RUPEES", 1480), SWORD)) + shop = IndoorLocation("Shop") + VirtualLocation().add(ShopItem(0)).connect(shop, OR(COUNT("RUPEES", 500), SWORD)) + VirtualLocation().add(ShopItem(1)).connect(shop, OR(COUNT("RUPEES", 1480), SWORD)) self._addEntrance("shop", mabe_village, shop, None) - dream_hut = Location("Dream Hut") - dream_hut_right = Location().add(Chest(0x2BF)).connect(dream_hut, SWORD) + dream_hut = IndoorLocation("Dream Hut") + dream_hut_right = VirtualLocation().add(Chest(0x2BF)).connect(dream_hut, SWORD) if options.logic != "casual": dream_hut_right.connect(dream_hut, OR(BOOMERANG, HOOKSHOT, FEATHER)) - dream_hut_left = Location().add(Chest(0x2BE)).connect(dream_hut_right, PEGASUS_BOOTS) + dream_hut_left = VirtualLocation().add(Chest(0x2BE)).connect(dream_hut_right, PEGASUS_BOOTS) self._addEntrance("dream_hut", mabe_village, dream_hut, POWER_BRACELET) - kennel = Location("Kennel").connect(Location().add(Seashell(0x2B2)), SHOVEL) # in the kennel - kennel.connect(Location().add(TradeSequenceItem(0x2B2, TRADING_ITEM_DOG_FOOD)), TRADING_ITEM_RIBBON) + kennel = IndoorLocation("Kennel").connect(VirtualLocation().add(Seashell(0x2B2)), SHOVEL) # in the kennel + kennel.connect(VirtualLocation().add(TradeSequenceItem(0x2B2, TRADING_ITEM_DOG_FOOD)), TRADING_ITEM_RIBBON) self._addEntrance("kennel", mabe_village, kennel, None) - sword_beach = Location("Sword Beach").add(BeachSword()).connect(mabe_village, OR(r.bush, SHIELD, r.attack_hookshot)) - banana_seller = Location("Banana Seller") - banana_seller.connect(Location().add(TradeSequenceItem(0x2FE, TRADING_ITEM_BANANAS)), TRADING_ITEM_DOG_FOOD) + sword_beach = OverworldLocation("Sword Beach").add(BeachSword()).connect(mabe_village, OR(r.bush, SHIELD, r.attack_hookshot)) + banana_seller = IndoorLocation("Banana Seller") + banana_seller.connect(VirtualLocation().add(TradeSequenceItem(0x2FE, TRADING_ITEM_BANANAS)), TRADING_ITEM_DOG_FOOD) self._addEntrance("banana_seller", sword_beach, banana_seller, r.bush) - boomerang_cave = Location("Boomerang Cave") + boomerang_cave = IndoorLocation("Boomerang Cave") if options.boomerang == 'trade': - Location().add(BoomerangGuy()).connect(boomerang_cave, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL)) + VirtualLocation().add(BoomerangGuy()).connect(boomerang_cave, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL)) elif options.boomerang == 'gift': - Location().add(BoomerangGuy()).connect(boomerang_cave, None) + VirtualLocation().add(BoomerangGuy()).connect(boomerang_cave, None) self._addEntrance("boomerang_cave", sword_beach, boomerang_cave, BOMB) self._addEntranceRequirementExit("boomerang_cave", None) # if exiting, you do not need bombs - sword_beach_to_ghost_hut = Location("Sword Beach to Ghost House").add(Chest(0x0E5)).connect(sword_beach, POWER_BRACELET) - ghost_hut_outside = Location("Outside Ghost House").connect(sword_beach_to_ghost_hut, POWER_BRACELET) - ghost_hut_inside = Location("Ghost House").connect(Location().add(Seashell(0x1E3)), POWER_BRACELET) + sword_beach_to_ghost_hut = OverworldLocation("Sword Beach to Ghost House").add(Chest(0x0E5)).connect(sword_beach, POWER_BRACELET) + ghost_hut_outside = OverworldLocation("Outside Ghost House").connect(sword_beach_to_ghost_hut, POWER_BRACELET) + ghost_hut_inside = IndoorLocation("Ghost House").connect(VirtualLocation().add(Seashell(0x1E3)), POWER_BRACELET) self._addEntrance("ghost_house", ghost_hut_outside, ghost_hut_inside, None) ## Forest area - forest = Location("Forest").connect(mabe_village, r.bush) # forest stretches all the way from the start town to the witch hut - Location().add(Chest(0x071)).connect(forest, POWER_BRACELET) # chest at start forest with 2 zols - forest_heartpiece = Location("Forest Heart Piece").add(HeartPiece(0x044)) # next to the forest, surrounded by pits + forest = OverworldLocation("Forest").connect(mabe_village, r.bush) # forest stretches all the way from the start town to the witch hut + VirtualLocation().add(Chest(0x071)).connect(forest, POWER_BRACELET) # chest at start forest with 2 zols + forest_heartpiece = OverworldLocation("Forest Heart Piece").add(HeartPiece(0x044)) # next to the forest, surrounded by pits forest.connect(forest_heartpiece, OR(BOOMERANG, FEATHER, HOOKSHOT, ROOSTER), one_way=True) - witch_hut = Location().connect(Location().add(Witch()), TOADSTOOL) + witch_hut = IndoorLocation("Witch's Hut").connect(VirtualLocation().add(Witch()), TOADSTOOL) self._addEntrance("witch", forest, witch_hut, None) - crazy_tracy_hut = Location("Outside Crazy Tracy's House").connect(forest, POWER_BRACELET) - crazy_tracy_hut_inside = Location("Crazy Tracy's House") - Location().add(KeyLocation("MEDICINE2")).connect(crazy_tracy_hut_inside, FOUND("RUPEES", 50)) + crazy_tracy_hut = OverworldLocation("Outside Crazy Tracy's House").connect(forest, POWER_BRACELET) + crazy_tracy_hut_inside = IndoorLocation("Crazy Tracy's House") + VirtualLocation().add(KeyLocation("MEDICINE2")).connect(crazy_tracy_hut_inside, FOUND("RUPEES", 50)) self._addEntrance("crazy_tracy", crazy_tracy_hut, crazy_tracy_hut_inside, None) start_house.connect(crazy_tracy_hut, SONG2, one_way=True) # Manbo's Mambo into the pond outside Tracy - forest_madbatter = Location("Forest Mad Batter") - Location().add(MadBatter(0x1E1)).connect(forest_madbatter, MAGIC_POWDER) + forest_madbatter = IndoorLocation("Forest Mad Batter") + VirtualLocation().add(MadBatter(0x1E1)).connect(forest_madbatter, MAGIC_POWDER) self._addEntrance("forest_madbatter", forest, forest_madbatter, POWER_BRACELET) self._addEntranceRequirementExit("forest_madbatter", None) # if exiting, you do not need bracelet - forest_cave = Location("Forest Cave") - Location().add(Chest(0x2BD)).connect(forest_cave, SWORD) # chest in forest cave on route to mushroom - log_cave_heartpiece = Location().add(HeartPiece(0x2AB)).connect(forest_cave, POWER_BRACELET) # piece of heart in the forest cave on route to the mushroom - forest_toadstool = Location().add(Toadstool()) + forest_cave = IndoorLocation("Forest Cave") + VirtualLocation().add(Chest(0x2BD)).connect(forest_cave, SWORD) # chest in forest cave on route to mushroom + log_cave_heartpiece = VirtualLocation().add(HeartPiece(0x2AB)).connect(forest_cave, POWER_BRACELET) # piece of heart in the forest cave on route to the mushroom + forest_toadstool = OverworldLocation("Mysterious Woods Toadstool").add(Toadstool()) self._addEntrance("toadstool_entrance", forest, forest_cave, None) self._addEntrance("toadstool_exit", forest_toadstool, forest_cave, None) - hookshot_cave = Location("Hookshot Cave") - hookshot_cave_chest = Location().add(Chest(0x2B3)).connect(hookshot_cave, OR(HOOKSHOT, ROOSTER)) + hookshot_cave = IndoorLocation("Hookshot Cave") + hookshot_cave_chest = VirtualLocation().add(Chest(0x2B3)).connect(hookshot_cave, OR(HOOKSHOT, ROOSTER)) self._addEntrance("hookshot_cave", forest, hookshot_cave, POWER_BRACELET) - swamp = Location("Swamp").connect(forest, AND(OR(MAGIC_POWDER, FEATHER, ROOSTER), r.bush)) + swamp = OverworldLocation("Swamp").connect(forest, AND(OR(MAGIC_POWDER, FEATHER, ROOSTER), r.bush)) swamp.connect(forest, r.bush, one_way=True) # can go backwards past Tarin swamp.connect(forest_toadstool, OR(FEATHER, ROOSTER)) - swamp_chest = Location("Swamp Chest").add(Chest(0x034)).connect(swamp, OR(BOWWOW, HOOKSHOT, MAGIC_ROD, BOOMERANG)) + swamp_chest = OverworldLocation("Swamp Chest").add(Chest(0x034)).connect(swamp, OR(BOWWOW, HOOKSHOT, MAGIC_ROD, BOOMERANG)) self._addEntrance("d2", swamp, None, OR(BOWWOW, HOOKSHOT, MAGIC_ROD, BOOMERANG)) - forest_rear_chest = Location().add(Chest(0x041)).connect(swamp, r.bush) # tail key + forest_rear_chest = OverworldLocation("Forest Rear").add(Chest(0x041)).connect(swamp, r.bush) # tail key self._addEntrance("writes_phone", swamp, None, None) - writes_hut_outside = Location("Outside Write's House").connect(swamp, OR(FEATHER, ROOSTER)) # includes the cave behind the hut - writes_house = Location("Write's House") - writes_house.connect(Location().add(TradeSequenceItem(0x2a8, TRADING_ITEM_BROOM)), TRADING_ITEM_LETTER) + writes_hut_outside = OverworldLocation("Outside Write's House").connect(swamp, OR(FEATHER, ROOSTER)) # includes the cave behind the hut + writes_house = IndoorLocation("Write's House") + writes_house.connect(VirtualLocation().add(TradeSequenceItem(0x2a8, TRADING_ITEM_BROOM)), TRADING_ITEM_LETTER) self._addEntrance("writes_house", writes_hut_outside, writes_house, None) if options.owlstatues == "both" or options.owlstatues == "overworld": writes_hut_outside.add(OwlStatue(0x11)) - writes_cave = Location("Write's Cave") - writes_cave_left_chest = Location().add(Chest(0x2AE)).connect(writes_cave, OR(FEATHER, ROOSTER, HOOKSHOT)) # 1st chest in the cave behind the hut - Location().add(Chest(0x2AF)).connect(writes_cave, POWER_BRACELET) # 2nd chest in the cave behind the hut. + writes_cave = IndoorLocation("Write's Cave") + writes_cave_left_chest = VirtualLocation().add(Chest(0x2AE)).connect(writes_cave, OR(FEATHER, ROOSTER, HOOKSHOT)) # 1st chest in the cave behind the hut + VirtualLocation().add(Chest(0x2AF)).connect(writes_cave, POWER_BRACELET) # 2nd chest in the cave behind the hut. self._addEntrance("writes_cave_left", writes_hut_outside, writes_cave, None) self._addEntrance("writes_cave_right", writes_hut_outside, writes_cave, None) - graveyard = Location("Graveyard").connect(forest, OR(FEATHER, ROOSTER, POWER_BRACELET)) # whole area from the graveyard up to the moblin cave + graveyard = OverworldLocation("Graveyard").connect(forest, OR(FEATHER, ROOSTER, POWER_BRACELET)) # whole area from the graveyard up to the moblin cave if options.owlstatues == "both" or options.owlstatues == "overworld": graveyard.add(OwlStatue(0x035)) # Moblin cave owl self._addEntrance("photo_house", graveyard, None, None) self._addEntrance("d0", graveyard, None, POWER_BRACELET) self._addEntranceRequirementExit("d0", None) # if exiting, you do not need bracelet - ghost_grave = Location().connect(forest, POWER_BRACELET) - Location().add(Seashell(0x074)).connect(ghost_grave, AND(r.bush, SHOVEL)) # next to grave cave, digging spot + ghost_grave = OverworldLocation("Ghost Grave").connect(forest, POWER_BRACELET) + VirtualLocation().add(Seashell(0x074)).connect(ghost_grave, AND(r.bush, SHOVEL)) # next to grave cave, digging spot - graveyard_cave_left = Location() - graveyard_cave_right = Location().connect(graveyard_cave_left, OR(FEATHER, ROOSTER)) - graveyard_heartpiece = Location().add(HeartPiece(0x2DF)).connect(graveyard_cave_right, OR(AND(BOMB, OR(HOOKSHOT, PEGASUS_BOOTS), FEATHER), ROOSTER)) # grave cave + graveyard_cave_left = IndoorLocation("Graveyard Cave Left") + graveyard_cave_right = IndoorLocation("Graveyard Cave Right").connect(graveyard_cave_left, OR(FEATHER, ROOSTER)) + graveyard_heartpiece = IndoorLocation("Graveyard Cave Heartpiece").add(HeartPiece(0x2DF)).connect(graveyard_cave_right, OR(AND(BOMB, OR(HOOKSHOT, PEGASUS_BOOTS), FEATHER), ROOSTER)) # grave cave self._addEntrance("graveyard_cave_left", ghost_grave, graveyard_cave_left, POWER_BRACELET) self._addEntrance("graveyard_cave_right", graveyard, graveyard_cave_right, None) - moblin_cave = Location().connect(Location().add(Chest(0x2E2)), AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping["moblin_cave"]])) + moblin_cave = IndoorLocation("Moblin Cave").connect(VirtualLocation().add(Chest(0x2E2)), AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping["moblin_cave"]])) self._addEntrance("moblin_cave", graveyard, moblin_cave, None) # "Ukuku Prairie" - ukuku_prairie = Location().connect(mabe_village, POWER_BRACELET).connect(graveyard, POWER_BRACELET) - ukuku_prairie.connect(Location().add(TradeSequenceItem(0x07B, TRADING_ITEM_STICK)), TRADING_ITEM_BANANAS) - ukuku_prairie.connect(Location().add(TradeSequenceItem(0x087, TRADING_ITEM_HONEYCOMB)), TRADING_ITEM_STICK) + ukuku_prairie = OverworldLocation("Ukuku Prairie").connect(mabe_village, POWER_BRACELET).connect(graveyard, POWER_BRACELET) + ukuku_prairie.connect(VirtualLocation().add(TradeSequenceItem(0x07B, TRADING_ITEM_STICK)), TRADING_ITEM_BANANAS) + ukuku_prairie.connect(VirtualLocation().add(TradeSequenceItem(0x087, TRADING_ITEM_HONEYCOMB)), TRADING_ITEM_STICK) self._addEntrance("prairie_left_phone", ukuku_prairie, None, None) self._addEntrance("prairie_right_phone", ukuku_prairie, None, None) - self._addEntrance("prairie_left_cave1", ukuku_prairie, Location().add(Chest(0x2CD)), None) # cave next to town + self._addEntrance("prairie_left_cave1", ukuku_prairie, IndoorLocation("Cave East of Mabe").add(Chest(0x2CD)), None) # cave next to town self._addEntrance("prairie_left_fairy", ukuku_prairie, None, BOMB) self._addEntranceRequirementExit("prairie_left_fairy", None) # if exiting, you do not need bombs - prairie_left_cave2 = Location() # Bomb cave - Location().add(Chest(0x2F4)).connect(prairie_left_cave2, PEGASUS_BOOTS) - Location().add(HeartPiece(0x2E5)).connect(prairie_left_cave2, AND(BOMB, PEGASUS_BOOTS)) + prairie_left_cave2 = IndoorLocation("Boots 'n' Bomb Cave Chest") # Bomb cave + VirtualLocation().add(Chest(0x2F4)).connect(prairie_left_cave2, PEGASUS_BOOTS) + VirtualLocation().add(HeartPiece(0x2E5)).connect(prairie_left_cave2, AND(BOMB, PEGASUS_BOOTS)) self._addEntrance("prairie_left_cave2", ukuku_prairie, prairie_left_cave2, BOMB) self._addEntranceRequirementExit("prairie_left_cave2", None) # if exiting, you do not need bombs - mamu = Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 1480))) + mamu = IndoorLocation("Mamu").connect(VirtualLocation().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 1480))) self._addEntrance("mamu", ukuku_prairie, mamu, AND(OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER), OR(HOOKSHOT, ROOSTER), POWER_BRACELET)) - dungeon3_entrance = Location().connect(ukuku_prairie, OR(FEATHER, ROOSTER, FLIPPERS)) + dungeon3_entrance = OverworldLocation("Key Cavern Entrance").connect(ukuku_prairie, OR(FEATHER, ROOSTER, FLIPPERS)) self._addEntrance("d3", dungeon3_entrance, None, SLIME_KEY) self._addEntranceRequirementExit("d3", None) # if exiting, you do not need to open the door - Location().add(Seashell(0x0A5)).connect(dungeon3_entrance, SHOVEL) # above lv3 + OverworldLocation("Above Key Cavern").add(Seashell(0x0A5)).connect(dungeon3_entrance, SHOVEL) # above lv3 dungeon3_entrance.connect(ukuku_prairie, None, one_way=True) # jump down ledge back to ukuku_prairie - prairie_island_seashell = Location().add(Seashell(0x0A6)).connect(ukuku_prairie, AND(FLIPPERS, r.bush)) # next to lv3 - Location().add(Seashell(0x08B)).connect(ukuku_prairie, r.bush) # next to seashell house - Location().add(Seashell(0x0A4)).connect(ukuku_prairie, PEGASUS_BOOTS) # smash into tree next to phonehouse - self._addEntrance("castle_jump_cave", ukuku_prairie, Location().add(Chest(0x1FD)), OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER)) # left of the castle, 5 holes turned into 3 - Location().add(Seashell(0x0B9)).connect(ukuku_prairie, POWER_BRACELET) # under the rock + prairie_island_seashell = VirtualLocation().add(Seashell(0x0A6)).connect(ukuku_prairie, AND(FLIPPERS, r.bush)) # next to lv3 + VirtualLocation().add(Seashell(0x08B)).connect(ukuku_prairie, r.bush) # next to seashell house + VirtualLocation().add(Seashell(0x0A4)).connect(ukuku_prairie, PEGASUS_BOOTS) # smash into tree next to phonehouse + self._addEntrance("castle_jump_cave", ukuku_prairie, VirtualLocation().add(Chest(0x1FD)), OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER)) # left of the castle, 5 holes turned into 3 + VirtualLocation().add(Seashell(0x0B9)).connect(ukuku_prairie, POWER_BRACELET) # under the rock - left_bay_area = Location() + left_bay_area = OverworldLocation("Western Martha's Bay") left_bay_area.connect(ghost_hut_outside, OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER)) self._addEntrance("prairie_low_phone", left_bay_area, None, None) - Location().add(Seashell(0x0E9)).connect(left_bay_area, r.bush) # same screen as mermaid statue - tiny_island = Location().add(Seashell(0x0F8)).connect(left_bay_area, AND(OR(FLIPPERS, ROOSTER), r.bush)) # tiny island + OverworldLocation("Lone Bush (Martha's Bay)").add(Seashell(0x0E9)).connect(left_bay_area, r.bush) # same screen as mermaid statue + tiny_island = OverworldLocation("Tiny Island").add(Seashell(0x0F8)).connect(left_bay_area, AND(OR(FLIPPERS, ROOSTER), r.bush)) # tiny island - prairie_plateau = Location() # prairie plateau at the owl statue + prairie_plateau = OverworldLocation("Donut Plains Plateau") # prairie plateau at the owl statue if options.owlstatues == "both" or options.owlstatues == "overworld": prairie_plateau.add(OwlStatue(0x0A8)) - Location().add(Seashell(0x0A8)).connect(prairie_plateau, SHOVEL) # at the owl statue + VirtualLocation().add(Seashell(0x0A8)).connect(prairie_plateau, SHOVEL) # at the owl statue - prairie_cave = Location() - prairie_cave_secret_exit = Location().connect(prairie_cave, AND(BOMB, OR(FEATHER, ROOSTER))) + prairie_cave = IndoorLocation("Prairie Cave") + prairie_cave_secret_exit = VirtualLocation().connect(prairie_cave, AND(BOMB, OR(FEATHER, ROOSTER))) self._addEntrance("prairie_right_cave_top", ukuku_prairie, prairie_cave, None) self._addEntrance("prairie_right_cave_bottom", left_bay_area, prairie_cave, None) self._addEntrance("prairie_right_cave_high", prairie_plateau, prairie_cave_secret_exit, None) - bay_madbatter_connector_entrance = Location() - bay_madbatter_connector_exit = Location().connect(bay_madbatter_connector_entrance, FLIPPERS) - bay_madbatter_connector_outside = Location() - bay_madbatter = Location().connect(Location().add(MadBatter(0x1E0)), MAGIC_POWDER) + bay_madbatter_connector_entrance = IndoorLocation("Martha's Bay Mad Batter Connector Entrance") + bay_madbatter_connector_exit = IndoorLocation("Martha's Bay Mad Batter Connector Exit").connect(bay_madbatter_connector_entrance, FLIPPERS) + bay_madbatter_connector_outside = OverworldLocation("Outside Martha's Bay Mad Batter Connector") + bay_madbatter = IndoorLocation("Martha's Bay Batter").connect(VirtualLocation().add(MadBatter(0x1E0)), MAGIC_POWDER) self._addEntrance("prairie_madbatter_connector_entrance", left_bay_area, bay_madbatter_connector_entrance, AND(OR(FEATHER, ROOSTER), OR(SWORD, MAGIC_ROD, BOOMERANG))) self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), r.bush)) # if exiting, you can pick up the bushes by normal means self._addEntrance("prairie_madbatter_connector_exit", bay_madbatter_connector_outside, bay_madbatter_connector_exit, None) self._addEntrance("prairie_madbatter", bay_madbatter_connector_outside, bay_madbatter, None) - seashell_mansion = Location() + seashell_mansion = IndoorLocation("Seashell Mansion") if options.goal != "seashells": - Location().add(SeashellMansion(0x2E9)).connect(seashell_mansion, COUNT(SEASHELL, 20)) + VirtualLocation().add(SeashellMansion(0x2E9)).connect(seashell_mansion, COUNT(SEASHELL, 20)) else: seashell_mansion.add(DroppedKey(0x2E9)) self._addEntrance("seashell_mansion", ukuku_prairie, seashell_mansion, None) - bay_water = Location() + bay_water = OverworldLocation("Martha's Bay") bay_water.connect(ukuku_prairie, FLIPPERS) bay_water.connect(left_bay_area, FLIPPERS) - fisher_under_bridge = Location().add(TradeSequenceItem(0x2F5, TRADING_ITEM_NECKLACE)) + # Technically kinda "indoors" but has no flagged connector + fisher_under_bridge = OverworldLocation("Fisherman Under the Bridge").add(TradeSequenceItem(0x2F5, TRADING_ITEM_NECKLACE)) fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FEATHER, FLIPPERS)) - bay_water.connect(Location().add(TradeSequenceItem(0x0C9, TRADING_ITEM_SCALE)), AND(TRADING_ITEM_NECKLACE, FLIPPERS)) - d5_entrance = Location().connect(bay_water, FLIPPERS) + bay_water.connect(VirtualLocation().add(TradeSequenceItem(0x0C9, TRADING_ITEM_SCALE)), AND(TRADING_ITEM_NECKLACE, FLIPPERS)) + d5_entrance = OverworldLocation("Catfish's Maw Entrance").connect(bay_water, FLIPPERS) self._addEntrance("d5", d5_entrance, None, None) # Richard - richard_house = Location() - richard_cave = Location().connect(richard_house, COUNT(GOLD_LEAF, 5)) + richard_house = IndoorLocation("Richard's House") + richard_cave = IndoorLocation("Richard's Cave").connect(richard_house, COUNT(GOLD_LEAF, 5)) richard_cave.connect(richard_house, None, one_way=True) # can exit richard's cave even without leaves - richard_cave_chest = Location().add(Chest(0x2C8)).connect(richard_cave, OR(FEATHER, HOOKSHOT, ROOSTER)) - richard_maze = Location() + richard_cave_chest = VirtualLocation().add(Chest(0x2C8)).connect(richard_cave, OR(FEATHER, HOOKSHOT, ROOSTER)) + richard_maze = OverworldLocation("Richard's Maze") self._addEntrance("richard_house", ukuku_prairie, richard_house, None) self._addEntrance("richard_maze", richard_maze, richard_cave, None) if options.owlstatues == "both" or options.owlstatues == "overworld": - Location().add(OwlStatue(0x0C6)).connect(richard_maze, r.bush) - Location().add(SlimeKey()).connect(richard_maze, AND(r.bush, SHOVEL)) + VirtualLocation().add(OwlStatue(0x0C6)).connect(richard_maze, r.bush) + VirtualLocation().add(SlimeKey()).connect(richard_maze, AND(r.bush, SHOVEL)) - next_to_castle = Location() + next_to_castle = OverworldLocation("Next to Kanalet Castle") if options.tradequest: ukuku_prairie.connect(next_to_castle, TRADING_ITEM_BANANAS, one_way=True) # can only give bananas from ukuku prairie side else: next_to_castle.connect(ukuku_prairie, None) next_to_castle.connect(ukuku_prairie, FLIPPERS) self._addEntrance("castle_phone", next_to_castle, None, None) - castle_secret_entrance_left = Location() - castle_secret_entrance_right = Location().connect(castle_secret_entrance_left, FEATHER) - castle_courtyard = Location() - castle_frontdoor = Location().connect(castle_courtyard, r.bush) + castle_secret_entrance_left = IndoorLocation("Castle Secret Entrance Left") + castle_secret_entrance_right = IndoorLocation("Castle Secret Entrance Right").connect(castle_secret_entrance_left, FEATHER) + castle_courtyard = OverworldLocation("Kanalet Castle Courtyard") + castle_frontdoor = OverworldLocation("Kanalet Castle Front Door").connect(castle_courtyard, r.bush) castle_frontdoor.connect(ukuku_prairie, "CASTLE_BUTTON") # the button in the castle connector allows access to the castle grounds in ER self._addEntrance("castle_secret_entrance", next_to_castle, castle_secret_entrance_right, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) self._addEntrance("castle_secret_exit", castle_courtyard, castle_secret_entrance_left, None) - Location().add(HeartPiece(0x078)).connect(bay_water, FLIPPERS) # in the moat of the castle - castle_inside = Location() - Location().add(KeyLocation("CASTLE_BUTTON")).connect(castle_inside, None) - castle_top_outside = Location() - castle_top_inside = Location() + VirtualLocation().add(HeartPiece(0x078)).connect(bay_water, FLIPPERS) # in the moat of the castle + castle_inside = IndoorLocation("Kanalet Castle") + OverworldLocation("In the Castle Moat").add(KeyLocation("CASTLE_BUTTON")).connect(castle_inside, None) + castle_top_outside = OverworldLocation("Atop Kanalet Castle") + castle_top_inside = IndoorLocation("Kanalet Castle Spire") self._addEntrance("castle_main_entrance", castle_frontdoor, castle_inside, r.bush) self._addEntrance("castle_upper_left", castle_top_outside, castle_inside, None) self._addEntrance("castle_upper_right", castle_top_outside, castle_top_inside, None) - Location().add(GoldLeaf(0x05A)).connect(castle_courtyard, OR(SWORD, BOW, MAGIC_ROD)) # mad bomber, enemy hiding in the 6 holes - crow_gold_leaf = Location().add(GoldLeaf(0x058)).connect(castle_courtyard, AND(POWER_BRACELET, r.attack_hookshot_no_bomb)) # bird on tree, can't kill with bomb cause it flies off. immune to magic_powder - Location().add(GoldLeaf(0x2D2)).connect(castle_inside, r.attack_hookshot_powder) # in the castle, kill enemies - Location().add(GoldLeaf(0x2C5)).connect(castle_inside, AND(BOMB, r.attack_hookshot_powder)) # in the castle, bomb wall to show enemy - kanalet_chain_trooper = Location().add(GoldLeaf(0x2C6)) # in the castle, spinning spikeball enemy + VirtualLocation().add(GoldLeaf(0x05A)).connect(castle_courtyard, OR(SWORD, BOW, MAGIC_ROD)) # mad bomber, enemy hiding in the 6 holes + crow_gold_leaf = VirtualLocation().add(GoldLeaf(0x058)).connect(castle_courtyard, AND(POWER_BRACELET, r.attack_hookshot_no_bomb)) # bird on tree, can't kill with bomb cause it flies off. immune to magic_powder + VirtualLocation().add(GoldLeaf(0x2D2)).connect(castle_inside, r.attack_hookshot_powder) # in the castle, kill enemies + VirtualLocation().add(GoldLeaf(0x2C5)).connect(castle_inside, AND(BOMB, r.attack_hookshot_powder)) # in the castle, bomb wall to show enemy + kanalet_chain_trooper = VirtualLocation().add(GoldLeaf(0x2C6)) # in the castle, spinning spikeball enemy castle_top_inside.connect(kanalet_chain_trooper, AND(POWER_BRACELET, r.attack_hookshot), one_way=True) - animal_village = Location() - animal_village.connect(Location().add(TradeSequenceItem(0x0CD, TRADING_ITEM_FISHING_HOOK)), TRADING_ITEM_BROOM) - cookhouse = Location() - cookhouse.connect(Location().add(TradeSequenceItem(0x2D7, TRADING_ITEM_PINEAPPLE)), TRADING_ITEM_HONEYCOMB) - goathouse = Location() - goathouse.connect(Location().add(TradeSequenceItem(0x2D9, TRADING_ITEM_LETTER)), TRADING_ITEM_HIBISCUS) - mermaid_statue = Location() + animal_village = OverworldLocation("Animal Village") + animal_village.connect(VirtualLocation().add(TradeSequenceItem(0x0CD, TRADING_ITEM_FISHING_HOOK)), TRADING_ITEM_BROOM) + cookhouse = IndoorLocation("Cook's House") + cookhouse.connect(VirtualLocation().add(TradeSequenceItem(0x2D7, TRADING_ITEM_PINEAPPLE)), TRADING_ITEM_HONEYCOMB) + goathouse = IndoorLocation("Goat's House") + goathouse.connect(VirtualLocation().add(TradeSequenceItem(0x2D9, TRADING_ITEM_LETTER)), TRADING_ITEM_HIBISCUS) + mermaid_statue = VirtualLocation() mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, HOOKSHOT)) mermaid_statue.add(TradeSequenceItem(0x297, TRADING_ITEM_MAGNIFYING_GLASS)) self._addEntrance("animal_phone", animal_village, None, None) @@ -272,99 +272,99 @@ def __init__(self, options, world_setup, r): self._addEntrance("animal_house5", animal_village, cookhouse, None) animal_village.connect(bay_water, FLIPPERS) animal_village.connect(ukuku_prairie, OR(HOOKSHOT, ROOSTER)) - animal_village_connector_left = Location() - animal_village_connector_right = Location().connect(animal_village_connector_left, PEGASUS_BOOTS) + animal_village_connector_left = IndoorLocation("Animal Village Connector Left") + animal_village_connector_right = IndoorLocation("Animal Village Connector Right").connect(animal_village_connector_left, PEGASUS_BOOTS) self._addEntrance("prairie_to_animal_connector", ukuku_prairie, animal_village_connector_left, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) # passage under river blocked by bush self._addEntrance("animal_to_prairie_connector", animal_village, animal_village_connector_right, None) if options.owlstatues == "both" or options.owlstatues == "overworld": animal_village.add(OwlStatue(0x0DA)) - Location().add(Seashell(0x0DA)).connect(animal_village, SHOVEL) # owl statue at the water - desert = Location().connect(animal_village, r.bush) # Note: We moved the walrus blocking the desert. + VirtualLocation().add(Seashell(0x0DA)).connect(animal_village, SHOVEL) # owl statue at the water + desert = OverworldLocation("Yarna Desert").connect(animal_village, r.bush) # Note: We moved the walrus blocking the desert. if options.owlstatues == "both" or options.owlstatues == "overworld": desert.add(OwlStatue(0x0CF)) - desert_lanmola = Location().add(AnglerKey()).connect(desert, OR(BOW, SWORD, HOOKSHOT, MAGIC_ROD, BOOMERANG)) + desert_lanmola = VirtualLocation().add(AnglerKey()).connect(desert, OR(BOW, SWORD, HOOKSHOT, MAGIC_ROD, BOOMERANG)) - animal_village_bombcave = Location() + animal_village_bombcave = IndoorLocation("Animal Village Bomb Cave") self._addEntrance("animal_cave", desert, animal_village_bombcave, BOMB) self._addEntranceRequirementExit("animal_cave", None) # if exiting, you do not need bombs - animal_village_bombcave_heartpiece = Location().add(HeartPiece(0x2E6)).connect(animal_village_bombcave, OR(AND(BOMB, FEATHER, HOOKSHOT), ROOSTER)) # cave in the upper right of animal town + animal_village_bombcave_heartpiece = VirtualLocation().add(HeartPiece(0x2E6)).connect(animal_village_bombcave, OR(AND(BOMB, FEATHER, HOOKSHOT), ROOSTER)) # cave in the upper right of animal town - desert_cave = Location() + desert_cave = IndoorLocation("Lanmolas' Lair") self._addEntrance("desert_cave", desert, desert_cave, None) desert.connect(desert_cave, None, one_way=True) # Drop down the sinkhole - Location().add(HeartPiece(0x1E8)).connect(desert_cave, BOMB) # above the quicksand cave - Location().add(Seashell(0x0FF)).connect(desert, POWER_BRACELET) # bottom right corner of the map + VirtualLocation().add(HeartPiece(0x1E8)).connect(desert_cave, BOMB) # above the quicksand cave + VirtualLocation().add(Seashell(0x0FF)).connect(desert, POWER_BRACELET) # bottom right corner of the map - armos_maze = Location().connect(animal_village, POWER_BRACELET) - armos_temple = Location() - Location().add(FaceKey()).connect(armos_temple, r.miniboss_requirements[world_setup.miniboss_mapping["armos_temple"]]) + armos_maze = OverworldLocation("Armos Maze").connect(animal_village, POWER_BRACELET) + armos_temple = IndoorLocation("Armos Temple") + VirtualLocation().add(FaceKey()).connect(armos_temple, r.miniboss_requirements[world_setup.miniboss_mapping["armos_temple"]]) if options.owlstatues == "both" or options.owlstatues == "overworld": armos_maze.add(OwlStatue(0x08F)) - self._addEntrance("armos_maze_cave", armos_maze, Location().add(Chest(0x2FC)), None) + self._addEntrance("armos_maze_cave", armos_maze, IndoorLocation("Under Armos Cave").add(Chest(0x2FC)), None) self._addEntrance("armos_temple", armos_maze, armos_temple, None) - armos_fairy_entrance = Location().connect(bay_water, FLIPPERS).connect(animal_village, POWER_BRACELET) + armos_fairy_entrance = OverworldLocation("Armos Fairy Entrance").connect(bay_water, FLIPPERS).connect(animal_village, POWER_BRACELET) self._addEntrance("armos_fairy", armos_fairy_entrance, None, BOMB) self._addEntranceRequirementExit("armos_fairy", None) # if exiting, you do not need bombs - d6_connector_left = Location() - d6_connector_right = Location().connect(d6_connector_left, OR(AND(HOOKSHOT, OR(FLIPPERS, AND(FEATHER, PEGASUS_BOOTS))), ROOSTER)) - d6_entrance = Location() + d6_connector_left = IndoorLocation("Face Shrine Connector Left") + d6_connector_right = IndoorLocation("Face Shrine Connector Right").connect(d6_connector_left, OR(AND(HOOKSHOT, OR(FLIPPERS, AND(FEATHER, PEGASUS_BOOTS))), ROOSTER)) + d6_entrance = OverworldLocation("Face Shrine Entrance") d6_entrance.connect(bay_water, FLIPPERS, one_way=True) - d6_armos_island = Location().connect(bay_water, FLIPPERS) + d6_armos_island = OverworldLocation("Armos Island").connect(bay_water, FLIPPERS) self._addEntrance("d6_connector_entrance", d6_armos_island, d6_connector_right, None) self._addEntrance("d6_connector_exit", d6_entrance, d6_connector_left, None) self._addEntrance("d6", d6_entrance, None, FACE_KEY) self._addEntranceRequirementExit("d6", None) # if exiting, you do not need to open the dungeon - windfish_egg = Location().connect(swamp, POWER_BRACELET).connect(graveyard, POWER_BRACELET) + windfish_egg = OverworldLocation("Windfish's Egg").connect(swamp, POWER_BRACELET).connect(graveyard, POWER_BRACELET) windfish_egg.connect(graveyard, None, one_way=True) # Ledge jump - obstacle_cave_entrance = Location() - obstacle_cave_inside = Location().connect(obstacle_cave_entrance, SWORD) + obstacle_cave_entrance = OverworldLocation("Obstacle Cave Entrance") + obstacle_cave_inside = IndoorLocation("Obstacle Cave").connect(obstacle_cave_entrance, SWORD) obstacle_cave_inside.connect(obstacle_cave_entrance, FEATHER, one_way=True) # can get past the rock room from right to left pushing blocks and jumping over the pit - obstacle_cave_inside_chest = Location().add(Chest(0x2BB)).connect(obstacle_cave_inside, OR(HOOKSHOT, ROOSTER)) # chest at obstacles - obstacle_cave_exit = Location().connect(obstacle_cave_inside, OR(PEGASUS_BOOTS, ROOSTER)) + obstacle_cave_inside_chest = VirtualLocation().add(Chest(0x2BB)).connect(obstacle_cave_inside, OR(HOOKSHOT, ROOSTER)) # chest at obstacles + obstacle_cave_exit = IndoorLocation("Obstacle Cave Exit").connect(obstacle_cave_inside, OR(PEGASUS_BOOTS, ROOSTER)) - lower_right_taltal = Location() + lower_right_taltal = OverworldLocation("Lower Right Tal Tal") self._addEntrance("obstacle_cave_entrance", windfish_egg, obstacle_cave_entrance, POWER_BRACELET) - self._addEntrance("obstacle_cave_outside_chest", Location().add(Chest(0x018)), obstacle_cave_inside, None) + self._addEntrance("obstacle_cave_outside_chest", VirtualLocation().add(Chest(0x018)), obstacle_cave_inside, None) self._addEntrance("obstacle_cave_exit", lower_right_taltal, obstacle_cave_exit, None) - papahl_cave = Location().add(Chest(0x28A)) - papahl = Location().connect(lower_right_taltal, None, one_way=True) - hibiscus_item = Location().add(TradeSequenceItem(0x019, TRADING_ITEM_HIBISCUS)) + papahl_cave = IndoorLocation("Papahl Cave").add(Chest(0x28A)) + papahl = OverworldLocation("Papahl").connect(lower_right_taltal, None, one_way=True) + hibiscus_item = VirtualLocation().add(TradeSequenceItem(0x019, TRADING_ITEM_HIBISCUS)) papahl.connect(hibiscus_item, TRADING_ITEM_PINEAPPLE, one_way=True) self._addEntrance("papahl_entrance", lower_right_taltal, papahl_cave, None) self._addEntrance("papahl_exit", papahl, papahl_cave, None) # D4 entrance and related things - below_right_taltal = Location().connect(windfish_egg, POWER_BRACELET) + below_right_taltal = OverworldLocation("Tal Tal Heights").connect(windfish_egg, POWER_BRACELET) below_right_taltal.add(KeyLocation("ANGLER_KEYHOLE")) below_right_taltal.connect(bay_water, FLIPPERS) below_right_taltal.connect(next_to_castle, ROOSTER) # fly from staircase to staircase on the north side of the moat lower_right_taltal.connect(below_right_taltal, FLIPPERS, one_way=True) - heartpiece_swim_cave = Location().connect(Location().add(HeartPiece(0x1F2)), FLIPPERS) + heartpiece_swim_cave = IndoorLocation("Damp Cave").connect(VirtualLocation().add(HeartPiece(0x1F2)), FLIPPERS) self._addEntrance("heartpiece_swim_cave", below_right_taltal, heartpiece_swim_cave, FLIPPERS) # cave next to level 4 - d4_entrance = Location().connect(below_right_taltal, FLIPPERS) + d4_entrance = OverworldLocation("Angler's Tunnel Entrance").connect(below_right_taltal, FLIPPERS) lower_right_taltal.connect(d4_entrance, AND(ANGLER_KEY, "ANGLER_KEYHOLE"), one_way=True) self._addEntrance("d4", d4_entrance, None, ANGLER_KEY) self._addEntranceRequirementExit("d4", FLIPPERS) # if exiting, you can leave with flippers without opening the dungeon - mambo = Location().connect(Location().add(Song(0x2FD)), AND(OCARINA, FLIPPERS)) # Manbo's Mambo + mambo = IndoorLocation("Mambo's Cave").connect(VirtualLocation().add(Song(0x2FD)), AND(OCARINA, FLIPPERS)) # Manbo's Mambo self._addEntrance("mambo", d4_entrance, mambo, FLIPPERS) # Raft game. - raft_house = Location("Raft House") - Location().add(KeyLocation("RAFT")).connect(raft_house, COUNT("RUPEES", 100)) - raft_return_upper = Location() - raft_return_lower = Location().connect(raft_return_upper, None, one_way=True) - outside_raft_house = Location().connect(below_right_taltal, HOOKSHOT).connect(below_right_taltal, FLIPPERS, one_way=True) - raft_game = Location() + raft_house = IndoorLocation("Raft House") + VirtualLocation().add(KeyLocation("RAFT")).connect(raft_house, COUNT("RUPEES", 100)) + raft_return_upper = IndoorLocation("Raft Return Upper") + raft_return_lower = IndoorLocation("Raft Return Lower").connect(raft_return_upper, None, one_way=True) + outside_raft_house = OverworldLocation("Outside Raft House").connect(below_right_taltal, HOOKSHOT).connect(below_right_taltal, FLIPPERS, one_way=True) + raft_game = OverworldLocation("Raft Game") raft_game.connect(outside_raft_house, "RAFT") raft_game.add(Chest(0x05C), Chest(0x05D)) # Chests in the rafting game - raft_exit = Location() + raft_exit = OverworldLocation("Raft Game Exit") if options.logic != "casual": # use raft to reach north armos maze entrances without flippers raft_game.connect(raft_exit, None, one_way=True) raft_game.connect(armos_fairy_entrance, None, one_way=True) @@ -375,37 +375,37 @@ def __init__(self, options, world_setup, r): if options.owlstatues == "both" or options.owlstatues == "overworld": raft_game.add(OwlStatue(0x5D)) - outside_rooster_house = Location().connect(lower_right_taltal, OR(FLIPPERS, ROOSTER)) + outside_rooster_house = OverworldLocation("Outside Rooster House").connect(lower_right_taltal, OR(FLIPPERS, ROOSTER)) self._addEntrance("rooster_house", outside_rooster_house, None, None) - bird_cave = Location() - bird_key = Location().add(BirdKey()) + bird_cave = IndoorLocation("Bird Cave") + bird_key = VirtualLocation().add(BirdKey()) bird_cave.connect(bird_key, OR(AND(FEATHER, COUNT(POWER_BRACELET, 2)), ROOSTER)) if options.logic != "casual": bird_cave.connect(lower_right_taltal, None, one_way=True) # Drop in a hole at bird cave self._addEntrance("bird_cave", outside_rooster_house, bird_cave, None) - bridge_seashell = Location().add(Seashell(0x00C)).connect(outside_rooster_house, AND(OR(FEATHER, ROOSTER), POWER_BRACELET)) # seashell right of rooster house, there is a hole in the bridge + bridge_seashell = VirtualLocation().add(Seashell(0x00C)).connect(outside_rooster_house, AND(OR(FEATHER, ROOSTER), POWER_BRACELET)) # seashell right of rooster house, there is a hole in the bridge - multichest_cave = Location() - multichest_cave_secret = Location().connect(multichest_cave, BOMB) - water_cave_hole = Location() # Location with the hole that drops you onto the hearth piece under water + multichest_cave = IndoorLocation("Five Chest Cave") + multichest_cave_secret = IndoorLocation("Five Chest Cave Secret").connect(multichest_cave, BOMB) + water_cave_hole = OverworldLocation("Damp Cave Hole") # Location with the hole that drops you onto the hearth piece under water if options.logic != "casual": water_cave_hole.connect(heartpiece_swim_cave, FLIPPERS, one_way=True) - multichest_outside = Location().add(Chest(0x01D)) # chest after multichest puzzle outside + multichest_outside = OverworldLocation("Outside Five Chest Game").add(Chest(0x01D)) # chest after multichest puzzle outside self._addEntrance("multichest_left", lower_right_taltal, multichest_cave, OR(FLIPPERS, ROOSTER)) self._addEntrance("multichest_right", water_cave_hole, multichest_cave, None) self._addEntrance("multichest_top", multichest_outside, multichest_cave_secret, None) if options.owlstatues == "both" or options.owlstatues == "overworld": water_cave_hole.add(OwlStatue(0x1E)) # owl statue below d7 - right_taltal_connector1 = Location() - right_taltal_connector_outside1 = Location() - right_taltal_connector2 = Location() - right_taltal_connector3 = Location() + right_taltal_connector1 = IndoorLocation("Eastern Tal Tal Connector 1") + right_taltal_connector_outside1 = OverworldLocation("Eastern Tal Tal Connector Outside 1") + right_taltal_connector2 = IndoorLocation("Eastern Tal Tal Connector 2") + right_taltal_connector3 = IndoorLocation("Eastern Tal Tal Connector 3") right_taltal_connector2.connect(right_taltal_connector3, AND(OR(FEATHER, ROOSTER), HOOKSHOT), one_way=True) - right_taltal_connector_outside2 = Location() - right_taltal_connector4 = Location() - d7_platau = Location() - d7_tower = Location() + right_taltal_connector_outside2 = OverworldLocation("Eastern Tal Tal Outside 2") + right_taltal_connector4 = IndoorLocation("Eastern Tal Tal Connector 4") + d7_platau = OverworldLocation("Eagle's Tower Plateau") + d7_tower = OverworldLocation("Eagle's Tower Entrance") d7_platau.connect(d7_tower, AND(POWER_BRACELET, BIRD_KEY), one_way=True) self._addEntrance("right_taltal_connector1", water_cave_hole, right_taltal_connector1, None) self._addEntrance("right_taltal_connector2", right_taltal_connector_outside1, right_taltal_connector1, None) @@ -420,34 +420,34 @@ def __init__(self, options, world_setup, r): d7_platau.connect(heartpiece_swim_cave, FLIPPERS, one_way=True) d7_platau.connect(right_taltal_connector_outside1, None, one_way=True) - mountain_bridge_staircase = Location().connect(outside_rooster_house, OR(HOOKSHOT, ROOSTER)) # cross bridges to staircase + mountain_bridge_staircase = OverworldLocation("Tal Tal Mountain Bridge Staircase").connect(outside_rooster_house, OR(HOOKSHOT, ROOSTER)) # cross bridges to staircase if options.logic != "casual": # ledge drop mountain_bridge_staircase.connect(windfish_egg, None, one_way=True) - left_right_connector_cave_entrance = Location() - left_right_connector_cave_exit = Location() + left_right_connector_cave_entrance = IndoorLocation("Tal Tal Left Right Connector Cave Entrance") + left_right_connector_cave_exit = IndoorLocation("Tal Tal Left Right Connector Cave Entrance") left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, OR(HOOKSHOT, ROOSTER), one_way=True) # pass through the underground passage to left side - taltal_boulder_zone = Location() + taltal_boulder_zone = OverworldLocation("Tal Tal Cabbage Zone") self._addEntrance("left_to_right_taltalentrance", mountain_bridge_staircase, left_right_connector_cave_entrance, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) self._addEntrance("left_taltal_entrance", taltal_boulder_zone, left_right_connector_cave_exit, None) - mountain_heartpiece = Location().add(HeartPiece(0x2BA)) # heartpiece in connecting cave + mountain_heartpiece = VirtualLocation().add(HeartPiece(0x2BA)) # heartpiece in connecting cave left_right_connector_cave_entrance.connect(mountain_heartpiece, BOMB, one_way=True) # in the connecting cave from right to left. one_way to prevent access to left_side_mountain via glitched logic taltal_boulder_zone.add(Chest(0x004)) # top of falling rocks hill - taltal_madbatter = Location().connect(Location().add(MadBatter(0x1E2)), MAGIC_POWDER) + taltal_madbatter = IndoorLocation("Tal Tal Mad Batter").connect(VirtualLocation().add(MadBatter(0x1E2)), MAGIC_POWDER) self._addEntrance("madbatter_taltal", taltal_boulder_zone, taltal_madbatter, POWER_BRACELET) self._addEntranceRequirementExit("madbatter_taltal", None) # if exiting, you do not need bracelet - outside_fire_cave = Location() + outside_fire_cave = OverworldLocation("Outside Fire Cave") if options.logic != "casual": outside_fire_cave.connect(writes_hut_outside, None, one_way=True) # Jump down the ledge taltal_boulder_zone.connect(outside_fire_cave, None, one_way=True) - fire_cave_bottom = Location() - fire_cave_top = Location().connect(fire_cave_bottom, COUNT(SHIELD, 2)) + fire_cave_bottom = IndoorLocation("Fire Cave Bottom") + fire_cave_top = IndoorLocation("Fire Cave Top").connect(fire_cave_bottom, COUNT(SHIELD, 2)) self._addEntrance("fire_cave_entrance", outside_fire_cave, fire_cave_bottom, BOMB) self._addEntranceRequirementExit("fire_cave_entrance", None) # if exiting, you do not need bombs - d8_entrance = Location() + d8_entrance = OverworldLocation("Turtle Rock Entrance") if options.logic != "casual": d8_entrance.connect(writes_hut_outside, None, one_way=True) # Jump down the ledge d8_entrance.connect(outside_fire_cave, None, one_way=True) # Jump down the other ledge @@ -456,8 +456,8 @@ def __init__(self, options, world_setup, r): self._addEntrance("d8", d8_entrance, None, AND(OCARINA, SONG3, SWORD)) self._addEntranceRequirementExit("d8", None) # if exiting, you do not need to wake the turtle - 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))) if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': hookshot_cave.connect(hookshot_cave_chest, AND(FEATHER, PEGASUS_BOOTS)) # boots jump the gap to the chest diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 47c601a1f71c..15666a77c9bc 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -101,7 +101,7 @@ def create_regions(self) -> None: # Connect Menu -> Start start = None for region in regions: - if region.name == "Start House": + if region.name == "Tarin's House": start = region break From 356b65a181bffcc72ec1e00d245c5a04b6f60c36 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 25 Mar 2023 21:10:52 -0700 Subject: [PATCH 02/32] Outside the hole, technically --- worlds/ladx/LADXR/logic/overworld.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py index ac47c2ff131e..f2334abb45a5 100644 --- a/worlds/ladx/LADXR/logic/overworld.py +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -387,7 +387,7 @@ def __init__(self, options, world_setup, r): multichest_cave = IndoorLocation("Five Chest Cave") multichest_cave_secret = IndoorLocation("Five Chest Cave Secret").connect(multichest_cave, BOMB) - water_cave_hole = OverworldLocation("Damp Cave Hole") # Location with the hole that drops you onto the hearth piece under water + water_cave_hole = OverworldLocation("Outside Wet Cave Hole") # Location with the hole that drops you onto the hearth piece under water if options.logic != "casual": water_cave_hole.connect(heartpiece_swim_cave, FLIPPERS, one_way=True) multichest_outside = OverworldLocation("Outside Five Chest Game").add(Chest(0x01D)) # chest after multichest puzzle outside From 1674d401d3c651c74e97f06b7a738556d0d4da21 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Mon, 27 Mar 2023 14:33:17 -0700 Subject: [PATCH 03/32] gen --- worlds/ladx/LADXR/entranceInfo.py | 2 +- worlds/ladx/LADXR/logic/location.py | 8 +- .../LADXR/mapgen/locations/entrance_info.py | 2 +- worlds/ladx/Locations.py | 44 +++-- worlds/ladx/__init__.py | 185 +++++++++++++++++- 5 files changed, 219 insertions(+), 22 deletions(-) diff --git a/worlds/ladx/LADXR/entranceInfo.py b/worlds/ladx/LADXR/entranceInfo.py index de1a247355e3..40c7101b3952 100644 --- a/worlds/ladx/LADXR/entranceInfo.py +++ b/worlds/ladx/LADXR/entranceInfo.py @@ -71,7 +71,7 @@ def __init__(self, room, alt_room=None, *, type=None, dungeon=None, index=None, "castle_jump_cave": EntranceInfo(0x78, target=0x1fd, type="single"), "castle_main_entrance": EntranceInfo(0x69, target=0x2d3, type="connector"), "castle_upper_left": EntranceInfo(0x59, target=0x2d5, type="connector", index=0), - "castle_upper_right": EntranceInfo(0x59, target=0x2d6, type="single", index=1), + "castle_upper_right": EntranceInfo(0x59, target=0x2d6, type="connector", index=1), "castle_secret_exit": EntranceInfo(0x49, target=0x1eb, type="connector"), "castle_secret_entrance": EntranceInfo(0x4A, target=0x1ec, type="connector"), "castle_phone": EntranceInfo(0x4B, target=0x2cc, type="dummy"), diff --git a/worlds/ladx/LADXR/logic/location.py b/worlds/ladx/LADXR/logic/location.py index e166df44f666..a9fb52985e95 100644 --- a/worlds/ladx/LADXR/logic/location.py +++ b/worlds/ladx/LADXR/logic/location.py @@ -11,15 +11,15 @@ class LocationType(Enum): Indoor = 3 class Location: - def __init__(self, name=None, location_type=None, dungeon=None): + def __init__(self, name=None, location_type=LocationType.Unknown, dungeon=None): self.name = name self.items = [] # type: typing.List[ItemInfo] self.dungeon = dungeon if self.dungeon != None: - assert location_type == None or location_type == LocationType.Dungeon + assert location_type == location_type.Unknown or location_type == LocationType.Dungeon location_type = LocationType.Dungeon - if location_type is not None: - self.location_type = location_type + + self.location_type = location_type self.__connected_to = set() self.simple_connections = [] self.gated_connections = [] diff --git a/worlds/ladx/LADXR/mapgen/locations/entrance_info.py b/worlds/ladx/LADXR/mapgen/locations/entrance_info.py index 9de2b8610170..c7b548b2187a 100644 --- a/worlds/ladx/LADXR/mapgen/locations/entrance_info.py +++ b/worlds/ladx/LADXR/mapgen/locations/entrance_info.py @@ -212,7 +212,7 @@ def __init__(self, *, items=None, logic=None, exits=None): items={None: 2}, logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BD)), SWORD).connect( # chest in forest cave on route to mushroom Location().add(HeartPiece(0x2AB), POWER_BRACELET)), # piece of heart in the forest cave on route to the mushroom - exits=[("right_taltal_connector6", lambda loc: loc)], + exits=[("toadstool_exit", lambda loc: loc)], ), "toadstool_exit": EntranceInfo(), diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index 69eb78dd88a9..27e489c9b32d 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -84,6 +84,8 @@ def has_free_weapon(state: "CollectionState", player: int) -> bool: return state.has("Progressive Sword", player) or state.has("Magic Rod", player) or state.has("Boomerang", player) or state.has("Hookshot", player) # If the player has access to farm enough rupees to afford a game, we assume that they can keep beating the game + + def can_farm_rupees(state: "CollectionState", player: int) -> bool: return has_free_weapon(state, player) and (state.has("Can Play Trendy Game", player=player) or state.has("RAFT", player=player)) @@ -171,19 +173,6 @@ def access_rule(self, state): return self.condition.test(GameStateAdapater(state, self.player)) -# Helper to apply function to every ladxr region -def walk_ladxdr(f, n, walked=set()): - if n in walked: - return - f(n) - walked.add(n) - - for o, req in n.simple_connections: - walk_ladxdr(f, o, walked) - for o, req in n.gated_connections: - walk_ladxdr(f, o, walked) - - def ladxr_region_to_name(n): name = n.name if not name: @@ -245,3 +234,32 @@ def print_items(n): entrance.connect(region_b) return list(regions.values()) + + +class ConnectorInfo: + def __init__(self, entrances, oneway=False) -> None: + self.entrances = entrances + self.oneway = oneway + +connector_info = [ + ConnectorInfo(("fire_cave_entrance", "fire_cave_exit")), + ConnectorInfo(("left_to_right_taltalentrance", "left_taltal_entrance"), True), + ConnectorInfo(("obstacle_cave_entrance", "obstacle_cave_outside_chest", "obstacle_cave_exit")), + ConnectorInfo(("papahl_entrance", "papahl_exit")), + ConnectorInfo(("multichest_left", "multichest_right", "multichest_top")), + ConnectorInfo(("right_taltal_connector1", "right_taltal_connector2")), + ConnectorInfo(("right_taltal_connector3", "right_taltal_connector4"), True), + ConnectorInfo(("right_taltal_connector5", "right_taltal_connector6")), + ConnectorInfo(("writes_cave_left", "writes_cave_right")), + ConnectorInfo(("raft_return_enter", "raft_return_exit"), True), + ConnectorInfo(("toadstool_entrance", "toadstool_exit")), + ConnectorInfo(("graveyard_cave_left", "graveyard_cave_right")), + ConnectorInfo(("castle_main_entrance", "castle_upper_left", "castle_upper_right")), + ConnectorInfo(("castle_secret_entrance", "castle_secret_exit")), + ConnectorInfo(("papahl_house_left", "papahl_house_right")), + ConnectorInfo(("prairie_right_cave_top", "prairie_right_cave_bottom", "prairie_right_cave_high")), + ConnectorInfo(("prairie_to_animal_connector", "animal_to_prairie_connector")), + ConnectorInfo(("d6_connector_entrance", "d6_connector_exit")), + ConnectorInfo(("richard_house", "richard_maze")), + ConnectorInfo(("prairie_madbatter_connector_entrance", "prairie_madbatter_connector_exit")) + ] diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 15666a77c9bc..a59d442926f4 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -4,6 +4,7 @@ from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial from Fill import fill_restrictive from worlds.AutoWorld import WebWorld, World +from .LADXR.logic.requirements import RequirementsSettings from .Common import * from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, @@ -23,6 +24,8 @@ from .Options import links_awakening_options from .Rom import LADXDeltaPatch +from .LADXR.logic.location import LocationType + DEVELOPER_MODE = False class LinksAwakeningWebWorld(WebWorld): @@ -86,12 +89,188 @@ def convert_ap_options_to_ladxr_logic(self): self.laxdr_options = LADXRSettings(self.player_options) self.laxdr_options.validate() - world_setup = LADXRWorldSetup() - world_setup.randomize(self.laxdr_options, self.multiworld.random) - self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup) + self.world_setup = LADXRWorldSetup() + self.world_setup.randomize(self.laxdr_options, self.multiworld.random) + self.randomize_entrances() + self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=self.world_setup) self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() + + def randomize_entrances(self): + from .LADXR.logic.overworld import World + + random = self.multiworld.per_slot_randoms[self.player] + world = World(self.laxdr_options, self.world_setup, RequirementsSettings(self.laxdr_options)) + + # NOTE: this code uses LADXR terms for things where: + # Region -> Location + # Location -> ItemInfo + # Item -> ...also Item? but not used here + # Helper to apply function to every ladxr region + def walk_locations(callback, n, filter=lambda _: True, walked=None): + walked = walked or set() + if n in walked: + return + if not filter(n): + return + callback(n) + walked.add(n) + + for o, req in n.simple_connections: + walk_locations(callback, o, filter, walked) + for o, req in n.gated_connections: + walk_locations(callback, o, filter, walked) + + + from .Locations import connector_info + + from .LADXR.entranceInfo import ENTRANCE_INFO + + ################################################################################################ + # First shuffle the start location, if needed + start = world.start + import copy + + # Get the list of all unseen locations + def shuffleable(entrance_name, location): + return ENTRANCE_INFO[entrance_name].type == "connector" + + unshuffled_connectors = copy.copy(connector_info) + random.shuffle(unshuffled_connectors) + + unseen_entrances = set(k for k,v in world.overworld_entrance.items() if shuffleable(k, v.location)) + + location_to_entrances = {} + for k,v in world.overworld_entrance.items(): + location_to_entrances.setdefault(v.location,[]).append(k) + + unshuffled_entrances = copy.copy(unseen_entrances) + seen_locations = set() + def mark_location(l): + if l in location_to_entrances: + for entrance in location_to_entrances[l]: + seen_locations.add(entrance) + if entrance in unseen_entrances: + unseen_entrances.remove(entrance) + + # TODO: we can reuse our walked location cache + walk_locations(callback=mark_location, n=start) + + while unseen_entrances: + # Find the places we haven't yet seen + # Pick one + unseen_entrance_to_connect = random.choice(list(unseen_entrances)) + + # Pick an unshuffled seen entrance + seen_entrance_to_connect = random.choice(list(seen_locations.intersection(unshuffled_entrances))) + + # Pick a connector + connector = unshuffled_connectors.pop() + + # Pick the connector direction + entrances = connector.entrances + if not connector.oneway: + entrances = list(entrances) + random.shuffle(entrances) + else: + assert len(connector.entrances) == 2 + A = connector.entrances[0] + B = connector.entrances[1] + C = len(connector.entrances) > 2 and connector.entrances[2] or None + + # Flag the two doors as connected + self.world_setup.entrance_mapping[seen_entrance_to_connect] = A + self.world_setup.entrance_mapping[unseen_entrance_to_connect] = B + # Walk the new locations + walk_locations(callback=mark_location, n=world.overworld_entrance[unseen_entrance_to_connect].location) + assert unseen_entrance_to_connect not in unseen_entrances + unshuffled_entrances.remove(seen_entrance_to_connect) + unshuffled_entrances.remove(unseen_entrance_to_connect) + if C: + third_entrance_to_connect = random.choice(list(unshuffled_entrances)) + self.world_setup.entrance_mapping[third_entrance_to_connect] = C + walk_locations(callback=mark_location, n=world.overworld_entrance[third_entrance_to_connect].location) + unshuffled_entrances.remove(third_entrance_to_connect) + + # Shuffle the remainder + unshuffled_entrances = list(unshuffled_entrances) + random.shuffle(unshuffled_entrances) + random.shuffle(unshuffled_connectors) + while unshuffled_entrances: + connector = unshuffled_connectors.pop() + for entrance in connector.entrances: + self.world_setup.entrance_mapping[unshuffled_entrances.pop()] = entrance + + x = set() + y = set() + for k, v in self.world_setup.entrance_mapping.items(): + assert k not in x, k + assert v not in y, v + x.add(k) + y.add(v) + ################################################################################################ + # Next build the reachable regions from start + + ################################################################################################ + # Next pick a region that we haven't found yet that has an entrance with a connector + + ################################################################################################ + # ...and build up the region list some more + + ################################################################################################ + # loop :) + + ################################################################################################ + # Once we've done that, shuffle the rest of the connectors + + + + + + + return + + + + + ################################################################################################ + # First generate the list of overworld regions + + + island_map = {} + all_islands = [] + + while ow_locations: + candidate = next(iter(ow_locations)) + current_island = set([candidate]) + found_existing_island = False + def build_island(l): + # If this location already has an island, merge them + if l in island_map: + # I've never seen this code get hit before, but a oneway dropdown or similar could hit it + assert False + found_island = island_map[l] + found_island |= current_island + for new_location in current_island: + island_map[new_location] = found_island + found_existing_island = True + else: + if l not in ow_locations: + print(l.name) + ow_locations.remove(l) + current_island.add(l) + + walk_locations(build_island, candidate, lambda l: l.location_type == LocationType.Overworld and not found_existing_island) + + if not found_existing_island: + for l in current_island: + island_map[l] = current_island + all_islands.append(current_island) + + ################################################################################################ + # Next shuffle the start location + def create_regions(self) -> None: # Initialize self.convert_ap_options_to_ladxr_logic() From 7c46a8c0202d8f5ba6fedbfc24e0c53e863d58a7 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Mon, 27 Mar 2023 14:45:43 -0700 Subject: [PATCH 04/32] cleanup --- worlds/ladx/__init__.py | 86 +++++++---------------------------------- 1 file changed, 13 insertions(+), 73 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index a59d442926f4..9e0afe7c7a52 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,6 +1,9 @@ import binascii import os +import copy +from .Locations import connector_info +from .LADXR.entranceInfo import ENTRANCE_INFO from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial from Fill import fill_restrictive from worlds.AutoWorld import WebWorld, World @@ -94,10 +97,13 @@ def convert_ap_options_to_ladxr_logic(self): self.randomize_entrances() self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=self.world_setup) self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() - + + # Failing seeds - + # Generating for 1 player, 61797097351729839299 Seed 3526789157814043126 with plando: bosses + # Generating for 1 player, 08916103583371570033 Seed 34316645283856452042 with plando: bosses def randomize_entrances(self): from .LADXR.logic.overworld import World - + random = self.multiworld.per_slot_randoms[self.player] world = World(self.laxdr_options, self.world_setup, RequirementsSettings(self.laxdr_options)) @@ -121,14 +127,9 @@ def walk_locations(callback, n, filter=lambda _: True, walked=None): walk_locations(callback, o, filter, walked) - from .Locations import connector_info - from .LADXR.entranceInfo import ENTRANCE_INFO - - ################################################################################################ # First shuffle the start location, if needed start = world.start - import copy # Get the list of all unseen locations def shuffleable(entrance_name, location): @@ -137,7 +138,7 @@ def shuffleable(entrance_name, location): unshuffled_connectors = copy.copy(connector_info) random.shuffle(unshuffled_connectors) - unseen_entrances = set(k for k,v in world.overworld_entrance.items() if shuffleable(k, v.location)) + unseen_entrances = [k for k,v in world.overworld_entrance.items() if shuffleable(k, v.location)] location_to_entrances = {} for k,v in world.overworld_entrance.items(): @@ -158,10 +159,12 @@ def mark_location(l): while unseen_entrances: # Find the places we haven't yet seen # Pick one - unseen_entrance_to_connect = random.choice(list(unseen_entrances)) + unseen_entrance_to_connect = random.choice(unseen_entrances) # Pick an unshuffled seen entrance - seen_entrance_to_connect = random.choice(list(seen_locations.intersection(unshuffled_entrances))) + l = list(seen_locations.intersection(unshuffled_entrances)) + l.sort() + seen_entrance_to_connect = random.choice(l) # Pick a connector connector = unshuffled_connectors.pop() @@ -207,70 +210,7 @@ def mark_location(l): assert v not in y, v x.add(k) y.add(v) - ################################################################################################ - # Next build the reachable regions from start - - ################################################################################################ - # Next pick a region that we haven't found yet that has an entrance with a connector - - ################################################################################################ - # ...and build up the region list some more - ################################################################################################ - # loop :) - - ################################################################################################ - # Once we've done that, shuffle the rest of the connectors - - - - - - - - - return - - - - - ################################################################################################ - # First generate the list of overworld regions - - - island_map = {} - all_islands = [] - - while ow_locations: - candidate = next(iter(ow_locations)) - current_island = set([candidate]) - found_existing_island = False - def build_island(l): - # If this location already has an island, merge them - if l in island_map: - # I've never seen this code get hit before, but a oneway dropdown or similar could hit it - assert False - found_island = island_map[l] - found_island |= current_island - for new_location in current_island: - island_map[new_location] = found_island - found_existing_island = True - else: - if l not in ow_locations: - print(l.name) - ow_locations.remove(l) - current_island.add(l) - - walk_locations(build_island, candidate, lambda l: l.location_type == LocationType.Overworld and not found_existing_island) - - if not found_existing_island: - for l in current_island: - island_map[l] = current_island - all_islands.append(current_island) - - ################################################################################################ - # Next shuffle the start location - def create_regions(self) -> None: # Initialize self.convert_ap_options_to_ladxr_logic() From ac1f0641bd577cd45b37289fa38fe1c4dcc6ef89 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Mon, 27 Mar 2023 16:48:53 -0700 Subject: [PATCH 05/32] fix castle button --- worlds/ladx/Locations.py | 5 +++-- worlds/ladx/__init__.py | 27 ++++++++++++++++++++------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index 27e489c9b32d..2aa2afeba1a3 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -237,9 +237,10 @@ def print_items(n): class ConnectorInfo: - def __init__(self, entrances, oneway=False) -> None: + def __init__(self, entrances, oneway=False, castle_button=False) -> None: self.entrances = entrances self.oneway = oneway + self.castle_button = castle_button connector_info = [ ConnectorInfo(("fire_cave_entrance", "fire_cave_exit")), @@ -254,7 +255,7 @@ def __init__(self, entrances, oneway=False) -> None: ConnectorInfo(("raft_return_enter", "raft_return_exit"), True), ConnectorInfo(("toadstool_entrance", "toadstool_exit")), ConnectorInfo(("graveyard_cave_left", "graveyard_cave_right")), - ConnectorInfo(("castle_main_entrance", "castle_upper_left", "castle_upper_right")), + ConnectorInfo(("castle_main_entrance", "castle_upper_left", "castle_upper_right"), castle_button=True), ConnectorInfo(("castle_secret_entrance", "castle_secret_exit")), ConnectorInfo(("papahl_house_left", "papahl_house_right")), ConnectorInfo(("prairie_right_cave_top", "prairie_right_cave_bottom", "prairie_right_cave_high")), diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 9e0afe7c7a52..bbec64290010 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,7 +1,7 @@ import binascii import os import copy - +import itertools from .Locations import connector_info from .LADXR.entranceInfo import ENTRANCE_INFO from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial @@ -27,7 +27,6 @@ from .Options import links_awakening_options from .Rom import LADXDeltaPatch -from .LADXR.logic.location import LocationType DEVELOPER_MODE = False @@ -101,9 +100,13 @@ def convert_ap_options_to_ladxr_logic(self): # Failing seeds - # Generating for 1 player, 61797097351729839299 Seed 3526789157814043126 with plando: bosses # Generating for 1 player, 08916103583371570033 Seed 34316645283856452042 with plando: bosses + + # TODO: this needs to handle castle button - don't allow walking through gate unless you've found the castle connector def randomize_entrances(self): from .LADXR.logic.overworld import World + has_castle_button = False + random = self.multiworld.per_slot_randoms[self.player] world = World(self.laxdr_options, self.world_setup, RequirementsSettings(self.laxdr_options)) @@ -112,6 +115,15 @@ def randomize_entrances(self): # Location -> ItemInfo # Item -> ...also Item? but not used here # Helper to apply function to every ladxr region + + # If we haven't found the castle button, don't allow going back and forth over the gate + def check_castle_button(a, b): + if has_castle_button: + return True + gate_names = ("Kanalet Castle Front Door", "Ukuku Prairie") + return a.name not in gate_names or b.name not in gate_names + + walked_cache = set() def walk_locations(callback, n, filter=lambda _: True, walked=None): walked = walked or set() if n in walked: @@ -121,10 +133,10 @@ def walk_locations(callback, n, filter=lambda _: True, walked=None): callback(n) walked.add(n) - for o, req in n.simple_connections: - walk_locations(callback, o, filter, walked) - for o, req in n.gated_connections: - walk_locations(callback, o, filter, walked) + for o, req in itertools.chain(n.simple_connections, n.gated_connections): + if check_castle_button(n, o): + walk_locations(callback, o, filter, walked) + @@ -168,7 +180,8 @@ def mark_location(l): # Pick a connector connector = unshuffled_connectors.pop() - + if connector.castle_button: + has_castle_button = True # Pick the connector direction entrances = connector.entrances if not connector.oneway: From 3e1ca6a72fd859722cfda00b86bf46013b363757 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Tue, 28 Mar 2023 08:41:21 -0700 Subject: [PATCH 06/32] note some bad seeds --- worlds/ladx/__init__.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index bbec64290010..52fbc4bf321d 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -28,7 +28,7 @@ from .Rom import LADXDeltaPatch -DEVELOPER_MODE = False +DEVELOPER_MODE = True class LinksAwakeningWebWorld(WebWorld): tutorials = [Tutorial( @@ -98,10 +98,8 @@ def convert_ap_options_to_ladxr_logic(self): self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() # Failing seeds - - # Generating for 1 player, 61797097351729839299 Seed 3526789157814043126 with plando: bosses - # Generating for 1 player, 08916103583371570033 Seed 34316645283856452042 with plando: bosses - - # TODO: this needs to handle castle button - don't allow walking through gate unless you've found the castle connector + # Generating for 1 player, 45461688514297641536 Seed 17354083837832261298 with plando: bosses + # Generating for 1 player, 60252350886745909164 Seed 97069388582882178805 with plando: bosses def randomize_entrances(self): from .LADXR.logic.overworld import World @@ -215,14 +213,6 @@ def mark_location(l): connector = unshuffled_connectors.pop() for entrance in connector.entrances: self.world_setup.entrance_mapping[unshuffled_entrances.pop()] = entrance - - x = set() - y = set() - for k, v in self.world_setup.entrance_mapping.items(): - assert k not in x, k - assert v not in y, v - x.add(k) - y.add(v) def create_regions(self) -> None: # Initialize From e29c02ecb418c8f16b76f1210b6f44b20d90fd31 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Fri, 31 Mar 2023 11:33:37 -0700 Subject: [PATCH 07/32] castle upper right isn't a connector lol --- worlds/ladx/LADXR/entranceInfo.py | 2 +- worlds/ladx/Locations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ladx/LADXR/entranceInfo.py b/worlds/ladx/LADXR/entranceInfo.py index 40c7101b3952..de1a247355e3 100644 --- a/worlds/ladx/LADXR/entranceInfo.py +++ b/worlds/ladx/LADXR/entranceInfo.py @@ -71,7 +71,7 @@ def __init__(self, room, alt_room=None, *, type=None, dungeon=None, index=None, "castle_jump_cave": EntranceInfo(0x78, target=0x1fd, type="single"), "castle_main_entrance": EntranceInfo(0x69, target=0x2d3, type="connector"), "castle_upper_left": EntranceInfo(0x59, target=0x2d5, type="connector", index=0), - "castle_upper_right": EntranceInfo(0x59, target=0x2d6, type="connector", index=1), + "castle_upper_right": EntranceInfo(0x59, target=0x2d6, type="single", index=1), "castle_secret_exit": EntranceInfo(0x49, target=0x1eb, type="connector"), "castle_secret_entrance": EntranceInfo(0x4A, target=0x1ec, type="connector"), "castle_phone": EntranceInfo(0x4B, target=0x2cc, type="dummy"), diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index 2aa2afeba1a3..767bf8bbe6e6 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -255,7 +255,7 @@ def __init__(self, entrances, oneway=False, castle_button=False) -> None: ConnectorInfo(("raft_return_enter", "raft_return_exit"), True), ConnectorInfo(("toadstool_entrance", "toadstool_exit")), ConnectorInfo(("graveyard_cave_left", "graveyard_cave_right")), - ConnectorInfo(("castle_main_entrance", "castle_upper_left", "castle_upper_right"), castle_button=True), + ConnectorInfo(("castle_main_entrance", "castle_upper_left"), castle_button=True), ConnectorInfo(("castle_secret_entrance", "castle_secret_exit")), ConnectorInfo(("papahl_house_left", "papahl_house_right")), ConnectorInfo(("prairie_right_cave_top", "prairie_right_cave_bottom", "prairie_right_cave_high")), From 8f8ac7965379c3277e26542ac58719f4178ac388 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 1 Apr 2023 16:25:53 -0700 Subject: [PATCH 08/32] foobar --- worlds/ladx/Options.py | 93 ++++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 27 deletions(-) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 8d30186670fe..711a201008d2 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -52,39 +52,73 @@ class Boomerang(Choice): gift = 1 default = gift -class EntranceShuffle(Choice, LADXROption): +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 StartShuffle(EntranceShuffle): + """ + Shuffle Start Location + [Vanilla] No changes + [Simple] Your start location will be mixed with the single pool, or swapped with a "single" entrance (if there is no single shuffle) + [Mixed] Your start location will be shuffled among all other non-connector entrances, or swapped with a random non-connector (if there are no other shuffles enabled) """ - [WARNING] Experimental, may fail to fill - Randomizes where overworld entrances lead to. - [Simple] Single-entrance caves/houses that have items are shuffled amongst each other. - If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool. - Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this.""" - #[Advanced] Simple, but two-way connector caves are shuffled in their own pool as well. - #[Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool. - #[Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool. +class SingleEntranceShuffle(EntranceShuffle): + """ + Shuffle Single Entrances (non connectors with checks inside) + [Vanilla] No changes + [Simple] The entrances will be shuffled amongst themselves + [Mixed] The entrances will be shuffled among all other entrances + """ - option_none = 0 - option_simple = 1 - #option_advanced = 2 - #option_expert = 3 - #option_insanity = 4 - default = option_none - ladxr_name = "entranceshuffle" +class DummyEntranceShuffle(EntranceShuffle): + """ + Shuffle Dummy Entrances (non connectors with no checks inside) + [Vanilla] No changes + [Simple] The entrances will be shuffled amongst themselves + [Mixed] The entrances will be shuffled among all other entrances + """ -class DungeonShuffle(DefaultOffToggle, LADXROption): +class InsanityEntranceShuffle(EntranceShuffle): """ - [WARNING] Experimental, may fail to fill - Randomizes dungeon entrances within eachother + Shuffle Insanity Entrances (entrances that will be really annoying if moved - mamu or raft house) + [Vanilla] No changes + [Simple] The entrances will be shuffled amongst themselves + [Mixed] The entrances will be shuffled among all other entrances """ - ladxr_name = "dungeonshuffle" -class BossShuffle(Choice): - none = 0 - shuffle = 1 - random = 2 - default = none +class InsanityEntranceShuffle(EntranceShuffle): + """ + Shuffle Insanity Entrances (entrances that will be really annoying if moved - mamu or raft house) + [Vanilla] No changes + [Simple] The entrances will be shuffled amongst themselves + [Mixed] The entrances will be shuffled among all other entrances + """ + +class ConnectorEntranceShuffle(EntranceShuffle): + """ + Shuffle Connector Entrances + [Vanilla] No changes + [Simple] The entrances will be shuffled amongst themselves + [Mixed] The entrances will be shuffled among all other entrances + """ +class DungeonEntranceShuffle(EntranceShuffle): + """ + Shuffle Dungeon Entrances + [Vanilla] No changes + [Simple] The entrances will be shuffled amongst themselves + [Mixed] The entrances will be shuffled among other entrances + """ class DungeonItemShuffle(Choice): option_original_dungeon = 0 @@ -377,8 +411,13 @@ class Palette(Choice): # 'rooster': DefaultOnToggle, # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), # 'boomerang': Boomerang, # 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'), - 'experimental_dungeon_shuffle': DungeonShuffle, # 'Randomizes the dungeon that each dungeon entrance leads to'), - 'experimental_entrance_shuffle': EntranceShuffle, + 'start_shuffle': StartShuffle, + 'single_entrance_shuffle': SingleEntranceShuffle, + 'dummy_entrance_shuffle': DummyEntranceShuffle, + 'insanity_entrance_shuffle': InsanityEntranceShuffle, + 'insanity_entrance_shuffle': InsanityEntranceShuffle, + 'connector_entrance_shuffle': ConnectorEntranceShuffle, + 'dungeon_entrance_shuffle': DungeonEntranceShuffle, # 'bossshuffle': BossShuffle, # 'minibossshuffle': BossShuffle, 'goal': Goal, From fb8eaf6f8203d3a8455182a0e8d122c3135f25fb Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Mon, 3 Apr 2023 13:56:31 -0700 Subject: [PATCH 09/32] it's working --- worlds/ladx/LADXR/entranceInfo.py | 4 +- worlds/ladx/Options.py | 22 ++-- worlds/ladx/__init__.py | 199 ++++++++++++++++++------------ 3 files changed, 137 insertions(+), 88 deletions(-) diff --git a/worlds/ladx/LADXR/entranceInfo.py b/worlds/ladx/LADXR/entranceInfo.py index de1a247355e3..a6c8dcc163e5 100644 --- a/worlds/ladx/LADXR/entranceInfo.py +++ b/worlds/ladx/LADXR/entranceInfo.py @@ -48,12 +48,12 @@ def __init__(self, room, alt_room=None, *, type=None, dungeon=None, index=None, "d2": EntranceInfo(0x24, target=0x136, dungeon=2, instrument_room=0x12A), "moblin_cave": EntranceInfo(0x35, target=0x2f0, type="single"), "photo_house": EntranceInfo(0x37, target=0x2b5, type="dummy"), - "mambo": EntranceInfo(0x2A, target=0x2fd, type="single"), + "mambo": EntranceInfo(0x2A, target=0x2fd, type="water"), "d4": EntranceInfo(0x2B, "Alt2B", target=0x17a, dungeon=4, index=0, instrument_room=0x162), # TODO # "d4_connector": EntranceInfo(0x2B, "Alt2B", index=1), # "d4_connector_exit": EntranceInfo(0x2D), - "heartpiece_swim_cave": EntranceInfo(0x2E, target=0x1f2, type="single"), + "heartpiece_swim_cave": EntranceInfo(0x2E, target=0x1f2, type="water"), "raft_return_exit": EntranceInfo(0x2F, target=0x1e7, type="connector"), "raft_house": EntranceInfo(0x3F, target=0x2b0, type="insanity"), "raft_return_enter": EntranceInfo(0x8F, target=0x1f7, type="connector"), diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 711a201008d2..1474d0320d9a 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -68,9 +68,10 @@ class StartShuffle(EntranceShuffle): """ Shuffle Start Location [Vanilla] No changes - [Simple] Your start location will be mixed with the single pool, or swapped with a "single" entrance (if there is no single shuffle) + [Simple] Your start location will be mixed with the dummy pool, or swapped with a "dummy" entrance (if there is no dummy shuffle) [Mixed] Your start location will be shuffled among all other non-connector entrances, or swapped with a random non-connector (if there are no other shuffles enabled) """ + entrance_type="start" class SingleEntranceShuffle(EntranceShuffle): """ @@ -79,6 +80,7 @@ class SingleEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ + entrance_type="single" class DummyEntranceShuffle(EntranceShuffle): """ @@ -87,22 +89,25 @@ class DummyEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ + entrance_type="dummy" -class InsanityEntranceShuffle(EntranceShuffle): +class AnnoyingEntranceShuffle(EntranceShuffle): """ - Shuffle Insanity Entrances (entrances that will be really annoying if moved - mamu or raft house) + Shuffle Annoying Entrances (entrances that will be really annoying if moved - mamu or raft house) [Vanilla] No changes [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ + entrance_type="insanity" -class InsanityEntranceShuffle(EntranceShuffle): +class WaterEntranceShuffle(EntranceShuffle): """ - Shuffle Insanity Entrances (entrances that will be really annoying if moved - mamu or raft house) + Shuffle Water Entrances (entrances that drop you into water) [Vanilla] No changes [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ + entrance_type="water" class ConnectorEntranceShuffle(EntranceShuffle): """ @@ -111,6 +116,7 @@ class ConnectorEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ + entrance_type="connector" class DungeonEntranceShuffle(EntranceShuffle): """ @@ -119,6 +125,8 @@ class DungeonEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among other entrances """ + entrance_type="dungeon" + class DungeonItemShuffle(Choice): option_original_dungeon = 0 @@ -414,8 +422,8 @@ class Palette(Choice): 'start_shuffle': StartShuffle, 'single_entrance_shuffle': SingleEntranceShuffle, 'dummy_entrance_shuffle': DummyEntranceShuffle, - 'insanity_entrance_shuffle': InsanityEntranceShuffle, - 'insanity_entrance_shuffle': InsanityEntranceShuffle, + 'annoying_entrance_shuffle': AnnoyingEntranceShuffle, + 'water_entrance_shuffle': WaterEntranceShuffle, 'connector_entrance_shuffle': ConnectorEntranceShuffle, 'dungeon_entrance_shuffle': DungeonEntranceShuffle, # 'bossshuffle': BossShuffle, diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 52fbc4bf321d..453be0b8a926 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -24,7 +24,7 @@ from .LADXR.locations.constants import CHEST_ITEMS from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, create_regions_from_ladxr, get_locations_to_id) -from .Options import links_awakening_options +from .Options import links_awakening_options, EntranceShuffle from .Rom import LADXDeltaPatch @@ -97,10 +97,32 @@ def convert_ap_options_to_ladxr_logic(self): self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=self.world_setup) self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() - # Failing seeds - - # Generating for 1 player, 45461688514297641536 Seed 17354083837832261298 with plando: bosses - # Generating for 1 player, 60252350886745909164 Seed 97069388582882178805 with plando: bosses def randomize_entrances(self): + entrance_pools = {} + indoor_pools = {} + entrance_pool_mapping = {} + for option in self.player_options.values(): + if isinstance(option, EntranceShuffle): + print(option) + if option.value == EntranceShuffle.option_simple: + entrance_pool_mapping[option.entrance_type] = option.entrance_type + elif option.value == EntranceShuffle.option_mixed: + entrance_pool_mapping[option.entrance_type] = "mixed" + else: + continue + pool = entrance_pools.setdefault(entrance_pool_mapping[option.entrance_type], []) + indoor_pool = indoor_pools.setdefault(entrance_pool_mapping[option.entrance_type], []) + for entrance_name, entrance in ENTRANCE_INFO.items(): + if entrance.type == option.entrance_type: + pool.append(entrance_name) + # Connectors have special handling + if entrance.type != "connector": + indoor_pool.append(entrance_name) + + for pool in itertools.chain(entrance_pools.values(), indoor_pools.values()): + # Sort first so that we get the same result every time + pool.sort() + from .LADXR.logic.overworld import World has_castle_button = False @@ -136,84 +158,103 @@ def walk_locations(callback, n, filter=lambda _: True, walked=None): walk_locations(callback, o, filter, walked) - - # First shuffle the start location, if needed - start = world.start - - # Get the list of all unseen locations - def shuffleable(entrance_name, location): - return ENTRANCE_INFO[entrance_name].type == "connector" - - unshuffled_connectors = copy.copy(connector_info) - random.shuffle(unshuffled_connectors) - - unseen_entrances = [k for k,v in world.overworld_entrance.items() if shuffleable(k, v.location)] - - location_to_entrances = {} - for k,v in world.overworld_entrance.items(): - location_to_entrances.setdefault(v.location,[]).append(k) - - unshuffled_entrances = copy.copy(unseen_entrances) - seen_locations = set() - def mark_location(l): - if l in location_to_entrances: - for entrance in location_to_entrances[l]: - seen_locations.add(entrance) - if entrance in unseen_entrances: - unseen_entrances.remove(entrance) - - # TODO: we can reuse our walked location cache - walk_locations(callback=mark_location, n=start) - - while unseen_entrances: - # Find the places we haven't yet seen - # Pick one - unseen_entrance_to_connect = random.choice(unseen_entrances) - - # Pick an unshuffled seen entrance - l = list(seen_locations.intersection(unshuffled_entrances)) - l.sort() - seen_entrance_to_connect = random.choice(l) + + if "start" in entrance_pool_mapping: + # if simple, swap with a random dummy entrance - # Pick a connector - connector = unshuffled_connectors.pop() - if connector.castle_button: - has_castle_button = True - # Pick the connector direction - entrances = connector.entrances - if not connector.oneway: - entrances = list(entrances) - random.shuffle(entrances) - else: - assert len(connector.entrances) == 2 - A = connector.entrances[0] - B = connector.entrances[1] - C = len(connector.entrances) > 2 and connector.entrances[2] or None - - # Flag the two doors as connected - self.world_setup.entrance_mapping[seen_entrance_to_connect] = A - self.world_setup.entrance_mapping[unseen_entrance_to_connect] = B - # Walk the new locations - walk_locations(callback=mark_location, n=world.overworld_entrance[unseen_entrance_to_connect].location) - assert unseen_entrance_to_connect not in unseen_entrances - unshuffled_entrances.remove(seen_entrance_to_connect) - unshuffled_entrances.remove(unseen_entrance_to_connect) - if C: - third_entrance_to_connect = random.choice(list(unshuffled_entrances)) - self.world_setup.entrance_mapping[third_entrance_to_connect] = C - walk_locations(callback=mark_location, n=world.overworld_entrance[third_entrance_to_connect].location) - unshuffled_entrances.remove(third_entrance_to_connect) - - # Shuffle the remainder - unshuffled_entrances = list(unshuffled_entrances) - random.shuffle(unshuffled_entrances) - random.shuffle(unshuffled_connectors) - while unshuffled_entrances: - connector = unshuffled_connectors.pop() - for entrance in connector.entrances: - self.world_setup.entrance_mapping[unshuffled_entrances.pop()] = entrance + # if mixed, swap with any random entrance + pass + else: + start = world.start + + # First shuffle connectors, as they will fail if shuffled randomly + if "connector" in entrance_pool_mapping: + # Get the list of unshuffled connectors + unshuffled_connectors = copy.copy(connector_info) + random.shuffle(unshuffled_connectors) + + # Get the list of unshuffled candidates connector entrances + 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) + + unshuffled_entrances = entrance_pools[entrance_pool_mapping["connector"]] + seen_locations = set() + def mark_location(l): + if l in location_to_entrances: + for entrance in location_to_entrances[l]: + seen_locations.add(entrance) + if entrance in unseen_entrances: + unseen_entrances.remove(entrance) + + # TODO: we can reuse our walked location cache + walk_locations(callback=mark_location, n=start) + + while unseen_entrances: + # Find the places we haven't yet seen + # Pick one + unseen_entrance_to_connect = random.choice(unseen_entrances) + + # Pick an unshuffled seen entrance + l = list(seen_locations.intersection(unshuffled_entrances)) + l.sort() + seen_entrance_to_connect = random.choice(l) + + # Pick a connector + connector = unshuffled_connectors.pop() + if connector.castle_button: + has_castle_button = True + # Pick the connector direction + entrances = connector.entrances + if not connector.oneway: + entrances = list(entrances) + random.shuffle(entrances) + else: + assert len(connector.entrances) == 2 + A = connector.entrances[0] + B = connector.entrances[1] + C = len(connector.entrances) > 2 and connector.entrances[2] or None + + # Flag the two doors as connected + self.world_setup.entrance_mapping[seen_entrance_to_connect] = A + self.world_setup.entrance_mapping[unseen_entrance_to_connect] = B + # Walk the new locations + walk_locations(callback=mark_location, n=world.overworld_entrance[unseen_entrance_to_connect].location) + assert unseen_entrance_to_connect not in unseen_entrances + unshuffled_entrances.remove(seen_entrance_to_connect) + unshuffled_entrances.remove(unseen_entrance_to_connect) + if C: + third_entrance_to_connect = random.choice(list(unshuffled_entrances)) + self.world_setup.entrance_mapping[third_entrance_to_connect] = C + walk_locations(callback=mark_location, n=world.overworld_entrance[third_entrance_to_connect].location) + unshuffled_entrances.remove(third_entrance_to_connect) + + # Shuffle the remainder + random.shuffle(unshuffled_entrances) + random.shuffle(unshuffled_connectors) + while unshuffled_connectors: + connector = unshuffled_connectors.pop() + for entrance in connector.entrances: + self.world_setup.entrance_mapping[unshuffled_entrances.pop()] = entrance + + # Now for each pool of entrances, shuffle + for pool_name, pool in entrance_pools.items(): + random.shuffle(pool) + indoor_pool = indoor_pools[pool_name] + random.shuffle(indoor_pool) + for a, b in zip(pool, indoor_pool): + self.world_setup.entrance_mapping[a] = b + seen_keys = set() + seen_values = set() + for k, v in self.world_setup.entrance_mapping.items(): + assert k not in seen_keys + assert v not in seen_values, v + seen_keys.add(k) + seen_values.add(v) def create_regions(self) -> None: # Initialize self.convert_ap_options_to_ladxr_logic() From 5b09d3584dedd32515bd6bba597f9d0bcd844cc6 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Mon, 3 Apr 2023 13:57:24 -0700 Subject: [PATCH 10/32] fix D7 --- worlds/ladx/LADXR/generator.py | 1 + worlds/ladx/LADXR/patches/core.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 28966ab763d3..438ad05c827f 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -108,6 +108,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m assembler.const("HARD_MODE", 1 if settings.hardmode != "none" else 0) patches.core.cleanup(rom) + patches.core.fixD7exit(rom) patches.save.singleSaveSlot(rom) patches.phone.patchPhone(rom) patches.photographer.fixPhotographer(rom) diff --git a/worlds/ladx/LADXR/patches/core.py b/worlds/ladx/LADXR/patches/core.py index a202e661f945..78a602e98eaa 100644 --- a/worlds/ladx/LADXR/patches/core.py +++ b/worlds/ladx/LADXR/patches/core.py @@ -1,6 +1,6 @@ from ..assembler import ASM from ..entranceInfo import ENTRANCE_INFO -from ..roomEditor import RoomEditor, ObjectWarp, ObjectHorizontal +from ..roomEditor import RoomEditor, Object, ObjectWarp, ObjectHorizontal from ..backgroundEditor import BackgroundEditor from .. import utils @@ -537,3 +537,13 @@ def addFrameCounter(rom, check_count): gfx_low = "\n".join([line.split(" ")[n] for line in tile_graphics.split("\n")[8:]]) rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high) rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low) + +# For the D7 exit, make it so that we can exit it properly in ER +def fixD7exit(rom): + re = RoomEditor(rom, 0x0E) + for x in [0, 1, 2, 8, 9]: + for y in range(3): + re.removeObject(x, y) + re.objects.append(Object(5, 2, 0xE1)) + re.objects.append(Object(5, 3, 0x4A)) + re.store(rom) \ No newline at end of file From ef3bcf987b1e612f8fedd43d8d4a7e6429800fb5 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Mon, 3 Apr 2023 13:58:27 -0700 Subject: [PATCH 11/32] connect d7 --- worlds/ladx/LADXR/logic/overworld.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py index f2334abb45a5..f22a74907262 100644 --- a/worlds/ladx/LADXR/logic/overworld.py +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -405,6 +405,7 @@ def __init__(self, options, world_setup, r): right_taltal_connector_outside2 = OverworldLocation("Eastern Tal Tal Outside 2") right_taltal_connector4 = IndoorLocation("Eastern Tal Tal Connector 4") d7_platau = OverworldLocation("Eagle's Tower Plateau") + d7_tower.connect(d7_platau, None, one_way=True) d7_tower = OverworldLocation("Eagle's Tower Entrance") d7_platau.connect(d7_tower, AND(POWER_BRACELET, BIRD_KEY), one_way=True) self._addEntrance("right_taltal_connector1", water_cave_hole, right_taltal_connector1, None) From ba96120fccd14989af12c1cc6f497dc7f0cc1e9f Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Mon, 3 Apr 2023 17:57:31 -0700 Subject: [PATCH 12/32] allow secondary entrance type --- worlds/ladx/LADXR/logic/overworld.py | 2 +- worlds/ladx/Options.py | 14 +++++++------- worlds/ladx/__init__.py | 13 +++++++------ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py index f22a74907262..0a5a75715cab 100644 --- a/worlds/ladx/LADXR/logic/overworld.py +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -405,8 +405,8 @@ def __init__(self, options, world_setup, r): right_taltal_connector_outside2 = OverworldLocation("Eastern Tal Tal Outside 2") right_taltal_connector4 = IndoorLocation("Eastern Tal Tal Connector 4") d7_platau = OverworldLocation("Eagle's Tower Plateau") - d7_tower.connect(d7_platau, None, one_way=True) d7_tower = OverworldLocation("Eagle's Tower Entrance") + d7_tower.connect(d7_platau, None, one_way=True) d7_platau.connect(d7_tower, AND(POWER_BRACELET, BIRD_KEY), one_way=True) self._addEntrance("right_taltal_connector1", water_cave_hole, right_taltal_connector1, None) self._addEntrance("right_taltal_connector2", right_taltal_connector_outside1, right_taltal_connector1, None) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 1474d0320d9a..48bb72e4023f 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -71,7 +71,7 @@ class StartShuffle(EntranceShuffle): [Simple] Your start location will be mixed with the dummy pool, or swapped with a "dummy" entrance (if there is no dummy shuffle) [Mixed] Your start location will be shuffled among all other non-connector entrances, or swapped with a random non-connector (if there are no other shuffles enabled) """ - entrance_type="start" + entrance_type=["start"] class SingleEntranceShuffle(EntranceShuffle): """ @@ -80,7 +80,7 @@ class SingleEntranceShuffle(EntranceShuffle): [Simple] The entrances will be shuffled amongst themselves [Mixed] The entrances will be shuffled among all other entrances """ - entrance_type="single" + entrance_type=["single", "trade"] class DummyEntranceShuffle(EntranceShuffle): """ @@ -89,7 +89,7 @@ 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): """ @@ -98,7 +98,7 @@ 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): """ @@ -107,7 +107,7 @@ 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): """ @@ -116,7 +116,7 @@ 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): """ @@ -125,7 +125,7 @@ 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 DungeonItemShuffle(Choice): diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 453be0b8a926..4bd805d5c214 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -105,15 +105,15 @@ def randomize_entrances(self): if isinstance(option, EntranceShuffle): print(option) if option.value == EntranceShuffle.option_simple: - entrance_pool_mapping[option.entrance_type] = option.entrance_type + entrance_pool_mapping[option.entrance_type[0]] = option.entrance_type[0] elif option.value == EntranceShuffle.option_mixed: - entrance_pool_mapping[option.entrance_type] = "mixed" + entrance_pool_mapping[option.entrance_type[0]] = "mixed" else: continue - pool = entrance_pools.setdefault(entrance_pool_mapping[option.entrance_type], []) - indoor_pool = indoor_pools.setdefault(entrance_pool_mapping[option.entrance_type], []) + pool = entrance_pools.setdefault(entrance_pool_mapping[option.entrance_type[0]], []) + indoor_pool = indoor_pools.setdefault(entrance_pool_mapping[option.entrance_type[0]], []) for entrance_name, entrance in ENTRANCE_INFO.items(): - if entrance.type == option.entrance_type: + if entrance.type in option.entrance_type: pool.append(entrance_name) # Connectors have special handling if entrance.type != "connector": @@ -247,7 +247,7 @@ def mark_location(l): random.shuffle(indoor_pool) for a, b in zip(pool, indoor_pool): self.world_setup.entrance_mapping[a] = b - + print(f"{a} -> {b}") seen_keys = set() seen_values = set() for k, v in self.world_setup.entrance_mapping.items(): @@ -255,6 +255,7 @@ def mark_location(l): assert v not in seen_values, v seen_keys.add(k) seen_values.add(v) + def create_regions(self) -> None: # Initialize self.convert_ap_options_to_ladxr_logic() From 06357fe59cba79344c14a7fb2bc96da71c1f93f7 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Fri, 28 Jul 2023 19:31:50 -0700 Subject: [PATCH 13/32] fix --- worlds/ladx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index c2c4755d38b6..a9684328d022 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -30,7 +30,7 @@ from .LADXR.locations.constants import CHEST_ITEMS from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, create_regions_from_ladxr, get_locations_to_id) -from .Options import links_awakening_options, EntranceShuffle +from .Options import links_awakening_options, DungeonItemShuffle, EntranceShuffle from .Rom import LADXDeltaPatch From 3f5c4dc323046681bb41d1c20c4e9c5197030783 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sun, 13 Aug 2023 09:50:09 -0700 Subject: [PATCH 14/32] hooray --- worlds/ladx/LADXR/entranceInfo.py | 6 +++ worlds/ladx/Options.py | 34 ++++++++++++---- worlds/ladx/__init__.py | 68 ++++++++++++++++++++++--------- 3 files changed, 81 insertions(+), 27 deletions(-) diff --git a/worlds/ladx/LADXR/entranceInfo.py b/worlds/ladx/LADXR/entranceInfo.py index a6c8dcc163e5..9b7daa5f0fdf 100644 --- a/worlds/ladx/LADXR/entranceInfo.py +++ b/worlds/ladx/LADXR/entranceInfo.py @@ -1,3 +1,4 @@ +from collections import defaultdict class EntranceInfo: def __init__(self, room, alt_room=None, *, type=None, dungeon=None, index=None, instrument_room=None, target=None): @@ -134,3 +135,8 @@ def __init__(self, room, alt_room=None, *, type=None, dungeon=None, index=None, "animal_cave": EntranceInfo(0xCD, target=0x2f7, type="single", index=0), "desert_cave": EntranceInfo(0xCF, target=0x1f9, type="single"), } + +entrances_by_type = defaultdict(list) + +for name, entrance in ENTRANCE_INFO.items(): + entrances_by_type[entrance.type or "dungeon"].append(name) \ No newline at end of file diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index b41933fb7e15..a15f2c395544 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -1,7 +1,7 @@ import os.path import typing import logging -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText +from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText, OptionList from collections import defaultdict import Utils @@ -71,14 +71,32 @@ class EntranceShuffle(Choice): option_mixed = 2 alias_false = option_vanilla -class StartShuffle(EntranceShuffle): +class StartShufflePool(OptionList): """ Shuffle Start Location - [Vanilla] No changes - [Simple] Your start location will be mixed with the dummy pool, or swapped with a "dummy" entrance (if there is no dummy shuffle) - [Mixed] Your start location will be shuffled among all other non-connector entrances, or swapped with a random non-connector (if there are no other shuffles enabled) - """ - entrance_type=["start"] + + Decides which entrance pool(s) the start location is allowed to pull from. + + If the chosen entrance isn't shuffled, a swap will be performed instead. + + Connectors aren't allowed for now, due to having to work out which connectors are legal or not. + """ + valid_keys = [ + "single", + "dummy", + "trade", + "annoying", + "water", + # "connector", + "dungeon" + ] + # option_single = 1 + # option_dummy = 2 + # option_trade = 3 + # option_annoying = 4 + # option_water = 5 + # option_connector = 6 + # option_dungeon = 7 class SingleEntranceShuffle(EntranceShuffle): """ @@ -431,7 +449,7 @@ class Palette(Choice): 'rooster': Rooster, # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), # 'boomerang': Boomerang, # 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'), - 'start_shuffle': StartShuffle, + 'start_shuffle': StartShufflePool, 'single_entrance_shuffle': SingleEntranceShuffle, 'dummy_entrance_shuffle': DummyEntranceShuffle, 'annoying_entrance_shuffle': AnnoyingEntranceShuffle, diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index a9684328d022..a6dc38c98f10 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -4,7 +4,7 @@ import copy import itertools from .Locations import connector_info -from .LADXR.entranceInfo import ENTRANCE_INFO +from .LADXR.entranceInfo import ENTRANCE_INFO, entrances_by_type import pkgutil import settings import typing @@ -140,12 +140,27 @@ def convert_ap_options_to_ladxr_logic(self): self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() def randomize_entrances(self): + + + from .LADXR.logic.overworld import World entrance_pools = {} indoor_pools = {} entrance_pool_mapping = {} - for option in self.player_options.values(): + + random = self.multiworld.per_slot_randoms[self.player] + world = World(self.laxdr_options, self.world_setup, RequirementsSettings(self.laxdr_options)) + # First shuffle the start location, if needed + start = world.start + start_entrance = "start_house" + start_shuffle = self.player_options["start_shuffle"] + + start_type_mappings = {} + + for option_name, option in self.player_options.items(): if isinstance(option, EntranceShuffle): - print(option) + print(option_name, option) + for cat in option.entrance_type: + start_type_mappings[option_name] = cat if option.value == EntranceShuffle.option_simple: entrance_pool_mapping[option.entrance_type[0]] = option.entrance_type[0] elif option.value == EntranceShuffle.option_mixed: @@ -154,6 +169,8 @@ def randomize_entrances(self): continue pool = entrance_pools.setdefault(entrance_pool_mapping[option.entrance_type[0]], []) indoor_pool = indoor_pools.setdefault(entrance_pool_mapping[option.entrance_type[0]], []) + + # Gross N*M behavior, we can just iterate the entrance info once or twice for entrance_name, entrance in ENTRANCE_INFO.items(): if entrance.type in option.entrance_type: pool.append(entrance_name) @@ -161,16 +178,39 @@ def randomize_entrances(self): if entrance.type != "connector": indoor_pool.append(entrance_name) + + if start_shuffle.value: + start_candidates = [] + + for category in start_shuffle.value: + start_candidates += entrances_by_type[category] + if category == "single": + start_candidates += entrances_by_type["trade"] + + start_candidates.sort() + + # Ugh this is wrong + start_entrance = random.choice(start_candidates) + self.world_setup.entrance_mapping[start_entrance] = "start_house" + print("Picked " + start_entrance) + for pool in entrance_pools: + if start_entrance in pool: + pool.remove(start_entrance) + pool.add("start_house") + assert False + break + else: + # 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() - - from .LADXR.logic.overworld import World has_castle_button = False - random = self.multiworld.per_slot_randoms[self.player] - world = World(self.laxdr_options, self.world_setup, RequirementsSettings(self.laxdr_options)) + # NOTE: this code uses LADXR terms for things where: # Region -> Location @@ -185,8 +225,7 @@ def check_castle_button(a, b): gate_names = ("Kanalet Castle Front Door", "Ukuku Prairie") return a.name not in gate_names or b.name not in gate_names - walked_cache = set() - def walk_locations(callback, n, filter=lambda _: True, walked=None): + def walk_locations(callback, n, filter=lambda _: True, walked=None) -> None: walked = walked or set() if n in walked: return @@ -200,15 +239,6 @@ def walk_locations(callback, n, filter=lambda _: True, walked=None): walk_locations(callback, o, filter, walked) - # First shuffle the start location, if needed - - if "start" in entrance_pool_mapping: - # if simple, swap with a random dummy entrance - - # if mixed, swap with any random entrance - pass - else: - start = world.start # First shuffle connectors, as they will fail if shuffled randomly if "connector" in entrance_pool_mapping: @@ -468,7 +498,7 @@ def pre_fill(self) -> None: allowed_locations_by_item = {} # For now, special case first item - FORCE_START_ITEM = True + FORCE_START_ITEM = self.multiworld.players > 1 if FORCE_START_ITEM: self.force_start_item() From b839812c192bebe49d216af011f56f33fcf07fb2 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sun, 13 Aug 2023 23:03:16 -0700 Subject: [PATCH 15/32] foobar --- worlds/ladx/Options.py | 2 ++ worlds/ladx/__init__.py | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index a15f2c395544..24354e9b107d 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -80,6 +80,8 @@ class StartShufflePool(OptionList): If the chosen entrance isn't shuffled, a swap will be performed instead. Connectors aren't allowed for now, due to having to work out which connectors are legal or not. + + TODO: also allow specifying a specific entrance or list of entrances """ valid_keys = [ "single", diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index a6dc38c98f10..16791ce72cf8 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -140,7 +140,9 @@ def convert_ap_options_to_ladxr_logic(self): self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() def randomize_entrances(self): - + banned_starts = [ + "prarie_madbatter" + ] from .LADXR.logic.overworld import World entrance_pools = {} @@ -193,11 +195,11 @@ def randomize_entrances(self): start_entrance = random.choice(start_candidates) self.world_setup.entrance_mapping[start_entrance] = "start_house" print("Picked " + start_entrance) - for pool in entrance_pools: + for pool in entrance_pools.values(): + print(pool) if start_entrance in pool: pool.remove(start_entrance) - pool.add("start_house") - assert False + pool.append("start_house") break else: # This entrance wasn't shuffled, just map back From 62e8a1e1e87948b740e74e26ef0e36311e84daca Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Thu, 17 Aug 2023 23:14:34 -0700 Subject: [PATCH 16/32] fixed start item placer --- Fill.py | 5 +- worlds/ladx/LADXR/locations/startItem.py | 17 +-- worlds/ladx/LADXR/logic/__init__.py | 149 ----------------------- worlds/ladx/__init__.py | 97 +++++++++++---- 4 files changed, 83 insertions(+), 185 deletions(-) diff --git a/Fill.py b/Fill.py index 3e0342f42cd3..6ee2e85566f9 100644 --- a/Fill.py +++ b/Fill.py @@ -184,8 +184,9 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: logging.warning( f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})') else: - raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. ' - f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') + placements = [place for place in placements if place.item] + raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. \n' + f'Already placed {len(placements)}: ' + '\n' + ",\n".join(str(place) + " = " + str(place.item) for place in placements)) item_pool.extend(unplaced_items) diff --git a/worlds/ladx/LADXR/locations/startItem.py b/worlds/ladx/LADXR/locations/startItem.py index 95dd6ba54abd..4a4bbae5e2d3 100644 --- a/worlds/ladx/LADXR/locations/startItem.py +++ b/worlds/ladx/LADXR/locations/startItem.py @@ -7,23 +7,18 @@ class StartItem(DroppedKey): - # We need to give something here that we can use to progress. - # FEATHER - OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB] - MULTIWORLD = False def __init__(self): super().__init__(0x2A3) self.give_bowwow = False - def configure(self, options): - if options.bowwow != 'normal': - # When we have bowwow mode, we pretend to be a sword for logic reasons - self.OPTIONS = [SWORD] - self.give_bowwow = True - if options.randomstartlocation and options.entranceshuffle != 'none': - self.OPTIONS.append(FLIPPERS) + # def configure(self, options): + # if options.bowwow != 'normal': + # # When we have bowwow mode, we pretend to be a sword for logic reasons + # self.OPTIONS = [SWORD] + # self.give_bowwow = True + def patch(self, rom, option, *, multiworld=None): assert multiworld is None diff --git a/worlds/ladx/LADXR/logic/__init__.py b/worlds/ladx/LADXR/logic/__init__.py index 11a0acfd01f8..67a3a57200fb 100644 --- a/worlds/ladx/LADXR/logic/__init__.py +++ b/worlds/ladx/LADXR/logic/__init__.py @@ -133,152 +133,3 @@ def __recursiveFindAll(self, location): self.__recursiveFindAll(connection) for connection, requirement in location.gated_connections: self.__recursiveFindAll(connection) - - -class MultiworldLogic: - def __init__(self, settings, rnd=None, *, world_setups=None): - assert rnd or world_setups - self.worlds = [] - self.start = Location() - self.location_list = [self.start] - self.iteminfo_list = [] - - for n in range(settings.multiworld): - options = settings.multiworld_settings[n] - world = None - if world_setups: - world = Logic(options, world_setup=world_setups[n]) - else: - for cnt in range(1000): # Try the world setup in case entrance randomization generates unsolvable logic - world_setup = WorldSetup() - world_setup.randomize(options, rnd) - world = Logic(options, world_setup=world_setup) - if options.entranceshuffle not in ("advanced", "expert", "insanity") or len(world.iteminfo_list) == sum(itempool.ItemPool(options, rnd).toDict().values()): - break - - for ii in world.iteminfo_list: - ii.world = n - - req_done_set = set() - for loc in world.location_list: - loc.simple_connections = [(target, addWorldIdToRequirements(req_done_set, n, req)) for target, req in loc.simple_connections] - loc.gated_connections = [(target, addWorldIdToRequirements(req_done_set, n, req)) for target, req in loc.gated_connections] - loc.items = [MultiworldItemInfoWrapper(n, options, ii) for ii in loc.items] - self.iteminfo_list += loc.items - - self.worlds.append(world) - self.start.simple_connections += world.start.simple_connections - self.start.gated_connections += world.start.gated_connections - self.start.items += world.start.items - world.start.items.clear() - self.location_list += world.location_list - - self.entranceMapping = None - - -class MultiworldMetadataWrapper: - def __init__(self, world, metadata): - self.world = world - self.metadata = metadata - - @property - def name(self): - return self.metadata.name - - @property - def area(self): - return "P%d %s" % (self.world + 1, self.metadata.area) - - -class MultiworldItemInfoWrapper: - def __init__(self, world, configuration_options, target): - self.world = world - self.world_count = configuration_options.multiworld - self.target = target - self.dungeon_items = configuration_options.dungeon_items - self.MULTIWORLD_OPTIONS = None - self.item = None - - @property - def nameId(self): - return self.target.nameId - - @property - def forced_item(self): - if self.target.forced_item is None: - return None - if "_W" in self.target.forced_item: - return self.target.forced_item - return "%s_W%d" % (self.target.forced_item, self.world) - - @property - def room(self): - return self.target.room - - @property - def metadata(self): - return MultiworldMetadataWrapper(self.world, self.target.metadata) - - @property - def MULTIWORLD(self): - return self.target.MULTIWORLD - - def read(self, rom): - world = rom.banks[0x3E][0x3300 + self.target.room] if self.target.MULTIWORLD else self.world - return "%s_W%d" % (self.target.read(rom), world) - - def getOptions(self): - if self.MULTIWORLD_OPTIONS is None: - options = self.target.getOptions() - if self.target.MULTIWORLD and len(options) > 1: - self.MULTIWORLD_OPTIONS = [] - for n in range(self.world_count): - self.MULTIWORLD_OPTIONS += ["%s_W%d" % (t, n) for t in options if n == self.world or self.canMultiworld(t)] - else: - self.MULTIWORLD_OPTIONS = ["%s_W%d" % (t, self.world) for t in options] - return self.MULTIWORLD_OPTIONS - - def patch(self, rom, option): - idx = option.rfind("_W") - world = int(option[idx+2:]) - option = option[:idx] - if not self.target.MULTIWORLD: - assert self.world == world - self.target.patch(rom, option) - else: - self.target.patch(rom, option, multiworld=world) - - # Return true if the item is allowed to be placed in any world, or false if it is - # world specific for this check. - def canMultiworld(self, option): - if self.dungeon_items in {'', 'smallkeys'}: - if option.startswith("MAP"): - return False - if option.startswith("COMPASS"): - return False - if option.startswith("STONE_BEAK"): - return False - if self.dungeon_items in {'', 'localkeys'}: - if option.startswith("KEY"): - return False - if self.dungeon_items in {'', 'localkeys', 'localnightmarekey', 'smallkeys'}: - if option.startswith("NIGHTMARE_KEY"): - return False - return True - - @property - def location(self): - return self.target.location - - def __repr__(self): - return "W%d:%s" % (self.world, repr(self.target)) - - -def addWorldIdToRequirements(req_done_set, world, req): - if req is None: - return None - if isinstance(req, str): - return "%s_W%d" % (req, world) - if req in req_done_set: - return req - return req.copyWithModifiedItemNames(lambda item: "%s_W%d" % (item, world)) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 16791ce72cf8..a515ff4a3262 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -11,7 +11,7 @@ import tempfile -from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial +from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial, CollectionState from Fill import fill_restrictive from worlds.AutoWorld import WebWorld, World from .LADXR.logic.requirements import RequirementsSettings @@ -113,8 +113,6 @@ class LinksAwakeningWorld(World): # "weapons": {"sword", "lance"} #} - prefill_dungeon_items = None - player_options = None rupees = { @@ -141,9 +139,33 @@ def convert_ap_options_to_ladxr_logic(self): def randomize_entrances(self): banned_starts = [ - "prarie_madbatter" + # All of these lead directly to nothing or another connector + "obstacle_cave_outside_chest", + "multichest_top", + "right_taltal_connector2", + "right_taltal_connector3", + "right_taltal_connector4", + "right_taltal_connector5", + "right_fairy", + "castle_upper_left", + "castle_upper_right", + "prairie_madbatter_connector_exit", + "prairie_madbatter", + # Risky, two exits + "prairie_right_cave_high", + "richard_maze", + # Three + "multichest_right", + "right_taltal_connector1", + # Inside castle - funny but only if we force the castle button to be available (or open) + "castle_main_entrance", + "castle_secret_exit", + # Five exits + # This drops you down into the water, required flippers start + # "right_taltal_connector6", + # "d7" ] - + from .LADXR.logic.overworld import World entrance_pools = {} indoor_pools = {} @@ -154,6 +176,7 @@ def randomize_entrances(self): # First shuffle the start location, if needed start = world.start start_entrance = "start_house" + start_shuffle = self.player_options["start_shuffle"] start_type_mappings = {} @@ -172,7 +195,7 @@ def randomize_entrances(self): pool = entrance_pools.setdefault(entrance_pool_mapping[option.entrance_type[0]], []) indoor_pool = indoor_pools.setdefault(entrance_pool_mapping[option.entrance_type[0]], []) - # Gross N*M behavior, we can just iterate the entrance info once or twice + # TODO: Gross N*M behavior, we can just iterate the entrance info once or twice for entrance_name, entrance in ENTRANCE_INFO.items(): if entrance.type in option.entrance_type: pool.append(entrance_name) @@ -180,21 +203,30 @@ def randomize_entrances(self): if entrance.type != "connector": indoor_pool.append(entrance_name) - + # Shuffle starting location if start_shuffle.value: start_candidates = [] + # Find all possible start locations for category in start_shuffle.value: start_candidates += entrances_by_type[category] + # TODO: cleaner plz if category == "single": start_candidates += entrances_by_type["trade"] + # Some start locations result in either + # (A) requiring certain arrangements of connectors + # (B) immediately deadending + # these could be fixed with chaos entrance rando or smarter connector shuffle + # but for now we aren't gonna handle it + + start_candidates = [c for c in start_candidates if c not in banned_starts] + start_candidates.sort() - # Ugh this is wrong start_entrance = random.choice(start_candidates) self.world_setup.entrance_mapping[start_entrance] = "start_house" - print("Picked " + start_entrance) + start = world.overworld_entrance[start_entrance].location for pool in entrance_pools.values(): print(pool) if start_entrance in pool: @@ -227,21 +259,22 @@ def check_castle_button(a, b): gate_names = ("Kanalet Castle Front Door", "Ukuku Prairie") return a.name not in gate_names or b.name not in gate_names - def walk_locations(callback, n, filter=lambda _: True, walked=None) -> None: + def walk_locations(callback, current_location, filter=lambda _: True, walked=None) -> None: walked = walked or set() - if n in walked: + if current_location in walked: return - if not filter(n): + if not filter(current_location): return - callback(n) - walked.add(n) + callback(current_location) + walked.add(current_location) - for o, req in itertools.chain(n.simple_connections, n.gated_connections): - if check_castle_button(n, o): + for o, req in itertools.chain(current_location.simple_connections, current_location.gated_connections): + 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 @@ -257,6 +290,7 @@ def walk_locations(callback, n, filter=lambda _: True, walked=None) -> None: unshuffled_entrances = entrance_pools[entrance_pool_mapping["connector"]] seen_locations = set() + def mark_location(l): if l in location_to_entrances: for entrance in location_to_entrances[l]: @@ -265,7 +299,8 @@ def mark_location(l): unseen_entrances.remove(entrance) # TODO: we can reuse our walked location cache - walk_locations(callback=mark_location, n=start) + walked = set() + walk_locations(callback=mark_location, current_location=start, walked=walked) while unseen_entrances: # Find the places we haven't yet seen @@ -296,14 +331,14 @@ def mark_location(l): self.world_setup.entrance_mapping[seen_entrance_to_connect] = A self.world_setup.entrance_mapping[unseen_entrance_to_connect] = B # Walk the new locations - walk_locations(callback=mark_location, n=world.overworld_entrance[unseen_entrance_to_connect].location) + walk_locations(callback=mark_location, current_location=world.overworld_entrance[unseen_entrance_to_connect].location) assert unseen_entrance_to_connect not in unseen_entrances unshuffled_entrances.remove(seen_entrance_to_connect) unshuffled_entrances.remove(unseen_entrance_to_connect) if C: third_entrance_to_connect = random.choice(list(unshuffled_entrances)) self.world_setup.entrance_mapping[third_entrance_to_connect] = C - walk_locations(callback=mark_location, n=world.overworld_entrance[third_entrance_to_connect].location) + walk_locations(callback=mark_location, current_location=world.overworld_entrance[third_entrance_to_connect].location) unshuffled_entrances.remove(third_entrance_to_connect) # Shuffle the remainder @@ -330,6 +365,7 @@ 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() @@ -365,6 +401,7 @@ def create_regions(self) -> None: l.place_locked_item(self.create_event("An Alarm Clock")) self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player) + def create_item(self, item_name: str): return LinksAwakeningItem(self.item_name_to_data[item_name], self, self.player) @@ -481,12 +518,26 @@ def create_items(self) -> None: # Properly fill locations within dungeon location.dungeon = r.dungeon_index - def force_start_item(self): + def force_start_item(self): start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) if not start_loc.item: + """ + Find an item that forces progression for the player + """ + base_collection_state = CollectionState(self.multiworld) + base_collection_state.update_reachable_regions(self.player) + reachable_count = len(base_collection_state.reachable_regions[self.player]) + + def gives_progression(item): + collection_state = base_collection_state.copy() + collection_state.collect(item) + # Why isn't this needed? + # collection_state.update_reachable_regions(self.player) + return len(collection_state.reachable_regions[self.player]) > reachable_count + possible_start_items = [index for index, item in enumerate(self.multiworld.itempool) - if item.player == self.player - and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location] + if item.player == self.player and not item.location and item.advancement and gives_progression(item)] + if possible_start_items: index = self.multiworld.random.choice(possible_start_items) start_item = self.multiworld.itempool.pop(index) @@ -500,7 +551,7 @@ def pre_fill(self) -> None: allowed_locations_by_item = {} # For now, special case first item - FORCE_START_ITEM = self.multiworld.players > 1 + FORCE_START_ITEM = True # self.multiworld.players > 1 if FORCE_START_ITEM: self.force_start_item() From 9dde58e31f53486f469f231b87bc9ed8886cf238 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Thu, 17 Aug 2023 23:42:14 -0700 Subject: [PATCH 17/32] fixed code --- worlds/ladx/__init__.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index a515ff4a3262..520c4b96c06a 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -410,6 +410,8 @@ def create_event(self, event: str): return Item(event, ItemClassification.progression, None, self.player) def create_items(self) -> None: + itempool = [] + exclude = [item.name for item in self.multiworld.precollected_items[self.player]] dungeon_item_types = { @@ -450,7 +452,7 @@ def create_items(self) -> None: for _ in range(count): if item_name in exclude: exclude.remove(item_name) # this is destructive. create unique list above - self.multiworld.itempool.append(self.create_item("Master Stalfos' Message")) + itempool.append(self.create_item("Master Stalfos' Message")) else: item = self.create_item(item_name) @@ -490,9 +492,9 @@ def create_items(self) -> None: self.prefill_own_dungeons.append(item) self.pre_fill_items.append(item) else: - self.multiworld.itempool.append(item) + itempool.append(item) else: - self.multiworld.itempool.append(item) + itempool.append(item) self.multi_key = self.generate_multi_key() @@ -517,8 +519,13 @@ 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 + FORCE_START_ITEM = True # self.multiworld.players > 1 + if FORCE_START_ITEM: + self.force_start_item(itempool) + + self.multiworld.itempool += itempool - def force_start_item(self): + def force_start_item(self, itempool): start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) if not start_loc.item: """ @@ -535,14 +542,14 @@ def gives_progression(item): # collection_state.update_reachable_regions(self.player) return len(collection_state.reachable_regions[self.player]) > reachable_count - possible_start_items = [index for index, item in enumerate(self.multiworld.itempool) - if item.player == self.player and not item.location and item.advancement and gives_progression(item)] - - if possible_start_items: - index = self.multiworld.random.choice(possible_start_items) - start_item = self.multiworld.itempool.pop(index) - start_loc.place_locked_item(start_item) + possible_start_items = [item for item in itempool if item.advancement] + self.random.shuffle(possible_start_items) + for item in possible_start_items: + if gives_progression(item): + itempool.remove(item) + start_loc.place_locked_item(item) + return def get_pre_fill_items(self): return self.pre_fill_items @@ -550,11 +557,6 @@ def get_pre_fill_items(self): def pre_fill(self) -> None: allowed_locations_by_item = {} - # For now, special case first item - FORCE_START_ITEM = True # self.multiworld.players > 1 - if FORCE_START_ITEM: - self.force_start_item() - # Set up filter rules # The list of items we will pass to fill_restrictive, contains at first the items that go to all dungeons From 2db6455291ee0e558c259bdc83bfc1e42b5dd8da Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 19 Aug 2023 18:57:12 -0700 Subject: [PATCH 18/32] add new ER logic, add ability to turn off tarin gift --- worlds/ladx/LADXR/logic/overworld.py | 4 ++-- worlds/ladx/Options.py | 8 ++++++++ worlds/ladx/__init__.py | 3 +-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py index 0a5a75715cab..f3caa5be6160 100644 --- a/worlds/ladx/LADXR/logic/overworld.py +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -129,6 +129,7 @@ def __init__(self, options, world_setup, r): self._addEntranceRequirementExit("d0", None) # if exiting, you do not need bracelet ghost_grave = OverworldLocation("Ghost Grave").connect(forest, POWER_BRACELET) VirtualLocation().add(Seashell(0x074)).connect(ghost_grave, AND(r.bush, SHOVEL)) # next to grave cave, digging spot + graveyard.connect(forest_heartpiece, OR(BOOMERANG, HOOKSHOT), one_way=True) # grab the heart piece surrounded by pits from the north graveyard_cave_left = IndoorLocation("Graveyard Cave Left") graveyard_cave_right = IndoorLocation("Graveyard Cave Right").connect(graveyard_cave_left, OR(FEATHER, ROOSTER)) @@ -504,8 +505,7 @@ def __init__(self, options, world_setup, r): bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, FEATHER, one_way=True) # jesus jump (3 screen) through the underground passage leading to martha's bay mad batter self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(FEATHER, POWER_BRACELET)) # villa buffer into the top side of the bush, then pick it up - ukuku_prairie.connect(richard_maze, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD), one_way=True) # break bushes on north side of the maze, and 1 pit buffer into the maze - fisher_under_bridge.connect(bay_water, AND(BOMB, FLIPPERS)) # can bomb trigger the item without having the hook + ukuku_prairie.connect(richard_maze, OR(SWORD, AND(MAGIC_POWDER, MAX_POWDER_UPGRADE), MAGIC_ROD, BOOMERANG, BOMB)) # break bushes on north side of the maze, and 1 pit buffer into the maze. Do the same in one of the two northern screens of the maze to escape fisher_under_bridge.connect(bay_water, AND(BOMB, FLIPPERS)) # can bomb trigger the item without having the hook animal_village.connect(ukuku_prairie, FEATHER) # jesus jump below_right_taltal.connect(next_to_castle, FEATHER) # jesus jump (north of kanalet castle phonebooth) animal_village_connector_right.connect(animal_village_connector_left, FEATHER) # text clip past the obstacles (can go both ways), feather to wall clip the obstacle without triggering text or shaq jump in bottom right corner if text is off diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 24354e9b107d..981486970ef4 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -159,6 +159,13 @@ class APTitleScreen(DefaultOnToggle): Enables AP specific title screen and disables the intro cutscene """ +class OwnItemOnTarin(DefaultOnToggle): + """ + Forces one of your own items to be on Tarin. + This is to prevent being insta-bk'd at the start of the game. + This has no effect in single player games, and isn't always neccessary in ER. + """ + class DungeonItemShuffle(Choice): option_original_dungeon = 0 @@ -477,4 +484,5 @@ class Palette(Choice): 'music_change_condition': MusicChangeCondition, 'nag_messages': NagMessages, 'ap_title_screen': APTitleScreen, + 'tarin_gifts_your_item': OwnItemOnTarin, } diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 520c4b96c06a..ef358a00908b 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -519,8 +519,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 - FORCE_START_ITEM = True # self.multiworld.players > 1 - if FORCE_START_ITEM: + if self.multiworld.players > 1 and self.multiworld.tarin_gifts_your_item[self.player]: self.force_start_item(itempool) self.multiworld.itempool += itempool From bf0240c5e57152d6947d69fbae3e110b4e8e4118 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 19 Aug 2023 19:19:14 -0700 Subject: [PATCH 19/32] hmm --- worlds/ladx/LADXR/locations/startItem.py | 1 + worlds/ladx/Options.py | 10 +++++++++- worlds/ladx/__init__.py | 11 ++++++----- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/worlds/ladx/LADXR/locations/startItem.py b/worlds/ladx/LADXR/locations/startItem.py index 4a4bbae5e2d3..915c78cea272 100644 --- a/worlds/ladx/LADXR/locations/startItem.py +++ b/worlds/ladx/LADXR/locations/startItem.py @@ -7,6 +7,7 @@ class StartItem(DroppedKey): + OPTIONS = [SHIELD] MULTIWORLD = False def __init__(self): diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 981486970ef4..a019cbaa778a 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -81,6 +81,14 @@ class StartShufflePool(OptionList): Connectors aren't allowed for now, due to having to work out which connectors are legal or not. + Valid options: + - single + - dummy + - trade + - annoying + - water + - dungeon + TODO: also allow specifying a specific entrance or list of entrances """ valid_keys = [ @@ -163,7 +171,7 @@ class OwnItemOnTarin(DefaultOnToggle): """ Forces one of your own items to be on Tarin. This is to prevent being insta-bk'd at the start of the game. - This has no effect in single player games, and isn't always neccessary in ER. + This has little effect in single player games, and isn't always neccessary in ER. """ diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index ef358a00908b..0fb6a3151903 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -204,7 +204,7 @@ def randomize_entrances(self): indoor_pool.append(entrance_name) # Shuffle starting location - if start_shuffle.value: + if start_shuffle.value or True: start_candidates = [] # Find all possible start locations @@ -223,7 +223,7 @@ def randomize_entrances(self): start_candidates = [c for c in start_candidates if c not in banned_starts] start_candidates.sort() - + start_candidates = ["armos_temple"] start_entrance = random.choice(start_candidates) self.world_setup.entrance_mapping[start_entrance] = "start_house" start = world.overworld_entrance[start_entrance].location @@ -519,10 +519,12 @@ 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.players > 1 and self.multiworld.tarin_gifts_your_item[self.player]: + if self.multiworld.tarin_gifts_your_item[self.player]: self.force_start_item(itempool) + start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) + possible_start_items = [item for item in itempool if item.advancement and "shell" in item.name] - self.multiworld.itempool += itempool + start_loc.place_locked_item(possible_start_items[0]) def force_start_item(self, itempool): start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) @@ -540,7 +542,6 @@ def gives_progression(item): # Why isn't this needed? # collection_state.update_reachable_regions(self.player) return len(collection_state.reachable_regions[self.player]) > reachable_count - possible_start_items = [item for item in itempool if item.advancement] self.random.shuffle(possible_start_items) From 82675e1a8059d5ea531c0bf617c216304f1992a1 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 19 Aug 2023 19:19:44 -0700 Subject: [PATCH 20/32] undo --- worlds/ladx/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 0fb6a3151903..d5d23a0bae08 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -204,7 +204,7 @@ def randomize_entrances(self): indoor_pool.append(entrance_name) # Shuffle starting location - if start_shuffle.value or True: + if start_shuffle.value: start_candidates = [] # Find all possible start locations @@ -223,7 +223,6 @@ def randomize_entrances(self): start_candidates = [c for c in start_candidates if c not in banned_starts] start_candidates.sort() - start_candidates = ["armos_temple"] start_entrance = random.choice(start_candidates) self.world_setup.entrance_mapping[start_entrance] = "start_house" start = world.overworld_entrance[start_entrance].location From 73bae2b4ae11e2d193f1f31f6240fe4f5e461a72 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 19 Aug 2023 19:27:30 -0700 Subject: [PATCH 21/32] wip --- worlds/ladx/LADXR/locations/startItem.py | 1 - worlds/ladx/__init__.py | 32 +++++++++++------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/worlds/ladx/LADXR/locations/startItem.py b/worlds/ladx/LADXR/locations/startItem.py index 915c78cea272..4a4bbae5e2d3 100644 --- a/worlds/ladx/LADXR/locations/startItem.py +++ b/worlds/ladx/LADXR/locations/startItem.py @@ -7,7 +7,6 @@ class StartItem(DroppedKey): - OPTIONS = [SHIELD] MULTIWORLD = False def __init__(self): diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index d5d23a0bae08..e08d3572e9d0 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -221,20 +221,20 @@ def randomize_entrances(self): # but for now we aren't gonna handle it start_candidates = [c for c in start_candidates if c not in banned_starts] - - start_candidates.sort() - start_entrance = random.choice(start_candidates) - self.world_setup.entrance_mapping[start_entrance] = "start_house" - start = world.overworld_entrance[start_entrance].location - for pool in entrance_pools.values(): - print(pool) - if start_entrance in pool: - pool.remove(start_entrance) - pool.append("start_house") - break - else: - # This entrance wasn't shuffled, just map back - self.world_setup.entrance_mapping["start_house"] = start_entrance + if start_candidates: + start_candidates.sort() + start_entrance = random.choice(start_candidates) + self.world_setup.entrance_mapping[start_entrance] = "start_house" + start = world.overworld_entrance[start_entrance].location + for pool in entrance_pools.values(): + print(pool) + if start_entrance in pool: + pool.remove(start_entrance) + pool.append("start_house") + break + else: + # 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()): @@ -520,10 +520,6 @@ def create_items(self) -> None: location.dungeon = r.dungeon_index if self.multiworld.tarin_gifts_your_item[self.player]: self.force_start_item(itempool) - start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) - possible_start_items = [item for item in itempool if item.advancement and "shell" in item.name] - - start_loc.place_locked_item(possible_start_items[0]) def force_start_item(self, itempool): start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) From cb092eee6c3b61603c3f9ff07743d0cad0cb6b60 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 19 Aug 2023 19:40:36 -0700 Subject: [PATCH 22/32] fixes --- worlds/ladx/LADXR/locations/itemInfo.py | 3 --- worlds/ladx/LADXR/locations/startItem.py | 7 ------- worlds/ladx/__init__.py | 2 ++ 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/worlds/ladx/LADXR/locations/itemInfo.py b/worlds/ladx/LADXR/locations/itemInfo.py index dcd4205f4cd9..cd23eb69eeaa 100644 --- a/worlds/ladx/LADXR/locations/itemInfo.py +++ b/worlds/ladx/LADXR/locations/itemInfo.py @@ -23,9 +23,6 @@ def location(self): def setLocation(self, location): self._location = location - def getOptions(self): - return self.OPTIONS - def configure(self, options): pass diff --git a/worlds/ladx/LADXR/locations/startItem.py b/worlds/ladx/LADXR/locations/startItem.py index 4a4bbae5e2d3..6cd9651671ac 100644 --- a/worlds/ladx/LADXR/locations/startItem.py +++ b/worlds/ladx/LADXR/locations/startItem.py @@ -13,13 +13,6 @@ def __init__(self): super().__init__(0x2A3) self.give_bowwow = False - # def configure(self, options): - # if options.bowwow != 'normal': - # # When we have bowwow mode, we pretend to be a sword for logic reasons - # self.OPTIONS = [SWORD] - # self.give_bowwow = True - - def patch(self, rom, option, *, multiworld=None): assert multiworld is None diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index e08d3572e9d0..64798e67c7c9 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -521,6 +521,8 @@ def create_items(self) -> None: if self.multiworld.tarin_gifts_your_item[self.player]: self.force_start_item(itempool) + self.multiworld.itempool += itempool + def force_start_item(self, itempool): start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) if not start_loc.item: From 47ae93215f1b57c2e926e887a7a34ee0f22f68c0 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 19 Aug 2023 19:40:52 -0700 Subject: [PATCH 23/32] turn off dev mode --- worlds/ladx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 64798e67c7c9..dd3e1fc6151a 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -34,7 +34,7 @@ from .Rom import LADXDeltaPatch -DEVELOPER_MODE = True +DEVELOPER_MODE = False class LinksAwakeningSettings(settings.Group): From b6b7438ee0e62ee7bc8cbe1817f92057273790e1 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 19 Aug 2023 19:47:25 -0700 Subject: [PATCH 24/32] comment --- worlds/ladx/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index dd3e1fc6151a..816c464a53f2 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -512,6 +512,7 @@ def create_items(self) -> None: if r.dungeon_index: self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations for location in r.locations: + # This probably isn't needed any more, but we'll see # Don't place dungeon items on pit button chest, to reduce chance of the filler blowing up # TODO: no need for this if small key shuffle if location.name == "Pit Button Chest (Tail Cave)" or location.item: From 0c54ad3cdd98768a16ce8124d8e0a8d7524c7af2 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sun, 27 Aug 2023 11:39:02 -0700 Subject: [PATCH 25/32] remove prints --- worlds/ladx/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 816c464a53f2..7d8799667706 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -183,7 +183,6 @@ def randomize_entrances(self): for option_name, option in self.player_options.items(): if isinstance(option, EntranceShuffle): - print(option_name, option) for cat in option.entrance_type: start_type_mappings[option_name] = cat if option.value == EntranceShuffle.option_simple: @@ -227,7 +226,6 @@ def randomize_entrances(self): self.world_setup.entrance_mapping[start_entrance] = "start_house" start = world.overworld_entrance[start_entrance].location for pool in entrance_pools.values(): - print(pool) if start_entrance in pool: pool.remove(start_entrance) pool.append("start_house") @@ -355,7 +353,7 @@ def mark_location(l): random.shuffle(indoor_pool) for a, b in zip(pool, indoor_pool): self.world_setup.entrance_mapping[a] = b - print(f"{a} -> {b}") + seen_keys = set() seen_values = set() for k, v in self.world_setup.entrance_mapping.items(): From 3dc307e4cc2a7d7bc9b5a0b520383f97a273cd11 Mon Sep 17 00:00:00 2001 From: zig-for Date: Sun, 12 Nov 2023 16:53:55 -0800 Subject: [PATCH 26/32] Update Fill.py --- Fill.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index 689364c40d47..9fdbcc384392 100644 --- a/Fill.py +++ b/Fill.py @@ -200,9 +200,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: logging.warning( f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})') else: - placements = [place for place in placements if place.item] - raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. \n' - f'Already placed {len(placements)}: ' + '\n' + ",\n".join(str(place) + " = " + str(place.item) for place in placements)) + raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. ' + f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') item_pool.extend(unplaced_items) From 3cd804b4c2f9041bd481d5d88a282c8f4c4b33d3 Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Sat, 2 Dec 2023 16:32:41 -0800 Subject: [PATCH 27/32] ER --- worlds/ladx/Options.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index a0977f63b39b..3935022c8a8e 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -96,9 +96,8 @@ class StartShufflePool(OptionList): - annoying - water - dungeon - - TODO: also allow specifying a specific entrance or list of entrances """ + # TODO: also allow specifying a specific entrance or list of entrances valid_keys = [ "single", "dummy", From db5b6f102544cffbb68aa258696f43b03483a6cd Mon Sep 17 00:00:00 2001 From: Kyle Franz Date: Mon, 5 Feb 2024 21:25:07 -0800 Subject: [PATCH 28/32] fix warp code --- worlds/ladx/LADXR/generator.py | 2 ++ worlds/ladx/LADXR/patches/core.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index d3ac14c04f4f..66116a72b9a9 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -115,6 +115,8 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m assembler.const("wDropBombSpawnCount", 0xDE12) assembler.const("wLinkSpawnDelay", 0xDE13) + assembler.const("wOverworldRoomStatus", 0xD800) + #assembler.const("HARDWARE_LINK", 1) assembler.const("HARD_MODE", 1 if settings.hardmode != "none" else 0) diff --git a/worlds/ladx/LADXR/patches/core.py b/worlds/ladx/LADXR/patches/core.py index cafae82caec6..757ca6212bdc 100644 --- a/worlds/ladx/LADXR/patches/core.py +++ b/worlds/ladx/LADXR/patches/core.py @@ -633,9 +633,7 @@ def addWarpImprovements(rom, extra_warps): # Allow cursor to move over black squares # This allows warping to undiscovered areas - a fine cheat, but needs a check for wOverworldRoomStatus in the warp code - CHEAT_WARP_ANYWHERE = False - if CHEAT_WARP_ANYWHERE: - rom.patch(0x01, 0x1AE8, None, ASM("jp $5AF5")) + rom.patch(0x01, 0x1AE8, None, ASM("jp $5AF5")) # This disables the arrows around the selection bubble #rom.patch(0x01, 0x1B6F, None, ASM("ret"), fill_nop=True) @@ -714,8 +712,15 @@ def addWarpImprovements(rom, extra_warps): TeleportHandler: ld a, [$DBB4] ; Load the current selected tile - ; TODO: check if actually revealed so we can have free movement - ; Check cursor against different tiles to see if we are selecting a warp + ld hl, wOverworldRoomStatus + ld e, a ; $5D38: $5F + ld d, $00 ; $5D39: $16 $00 + add hl, de ; $5D3B: $19 + ld a, [hl] + and $80 + jr z, exit + ld a, [$DBB4] ; Load the current selected tile + {warp_jump} jr exit From 49a930182cef186ac796e5abef722140f2cc4d15 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 22 Jun 2024 14:03:53 -0400 Subject: [PATCH 29/32] Mostly works, just gotta deal with memory leak issue --- worlds/ladx/Common.py | 2 +- worlds/ladx/LADXR/itempool.py | 9 +-- worlds/ladx/LADXR/logic/overworld.py | 20 +++---- worlds/ladx/Locations.py | 4 +- worlds/ladx/Options.py | 29 +++++---- worlds/ladx/__init__.py | 90 +++++++++++++++------------- 6 files changed, 84 insertions(+), 70 deletions(-) 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): From e358d3e19833a98f1e0f5c75194887776ce663d3 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 22 Jun 2024 14:21:37 -0400 Subject: [PATCH 30/32] Make suggested change for loop in randomize entrances --- worlds/ladx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index fcb976cacd4c..6495f575de9c 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -184,7 +184,7 @@ def randomize_entrances(self): start_type_mappings = {} - for option_name, option in self.options.as_dict(): + for option_name, option in dataclasses.asdict(self.options).items(): if isinstance(option, EntranceShuffle): for cat in option.entrance_type: start_type_mappings[option_name] = cat From d3bd9853b84fdc39f842d3c5217a62e8cadab66d Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 26 Jun 2024 11:36:01 -0400 Subject: [PATCH 31/32] newline at end of file --- worlds/ladx/LADXR/entranceInfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ladx/LADXR/entranceInfo.py b/worlds/ladx/LADXR/entranceInfo.py index 9b7daa5f0fdf..fbb2e222ac3d 100644 --- a/worlds/ladx/LADXR/entranceInfo.py +++ b/worlds/ladx/LADXR/entranceInfo.py @@ -139,4 +139,4 @@ def __init__(self, room, alt_room=None, *, type=None, dungeon=None, index=None, entrances_by_type = defaultdict(list) for name, entrance in ENTRANCE_INFO.items(): - entrances_by_type[entrance.type or "dungeon"].append(name) \ No newline at end of file + entrances_by_type[entrance.type or "dungeon"].append(name) From c544420500247a0bec91bcd2ee1e7058113c2530 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 26 Jun 2024 11:36:37 -0400 Subject: [PATCH 32/32] Newline at end of file --- worlds/ladx/LADXR/logic/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ladx/LADXR/logic/location.py b/worlds/ladx/LADXR/logic/location.py index a9fb52985e95..2c95772b8fb6 100644 --- a/worlds/ladx/LADXR/logic/location.py +++ b/worlds/ladx/LADXR/logic/location.py @@ -84,4 +84,4 @@ def __init__(self, name): Location.__init__(self, name, location_type=LocationType.Indoor) class VirtualLocation(Location): - pass \ No newline at end of file + pass