diff --git a/Options.py b/Options.py index 135a1dcb53..d9122d444c 100644 --- a/Options.py +++ b/Options.py @@ -689,9 +689,9 @@ class Range(NumericOption): @classmethod def weighted_range(cls, text) -> Range: if text == "random-low": - return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_start)) + return cls(cls.triangular(cls.range_start, cls.range_end, 0.0)) elif text == "random-high": - return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_end)) + return cls(cls.triangular(cls.range_start, cls.range_end, 1.0)) elif text == "random-middle": return cls(cls.triangular(cls.range_start, cls.range_end)) elif text.startswith("random-range-"): @@ -717,11 +717,11 @@ class Range(NumericOption): f"{random_range[0]}-{random_range[1]} is outside allowed range " f"{cls.range_start}-{cls.range_end} for option {cls.__name__}") if text.startswith("random-range-low"): - return cls(cls.triangular(random_range[0], random_range[1], random_range[0])) + return cls(cls.triangular(random_range[0], random_range[1], 0.0)) elif text.startswith("random-range-middle"): return cls(cls.triangular(random_range[0], random_range[1])) elif text.startswith("random-range-high"): - return cls(cls.triangular(random_range[0], random_range[1], random_range[1])) + return cls(cls.triangular(random_range[0], random_range[1], 1.0)) else: return cls(random.randint(random_range[0], random_range[1])) @@ -739,8 +739,16 @@ class Range(NumericOption): return str(self.value) @staticmethod - def triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int: - return int(round(random.triangular(lower, end, tri), 0)) + def triangular(lower: int, end: int, tri: float = 0.5) -> int: + """ + Integer triangular distribution for `lower` inclusive to `end` inclusive. + + Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined. + """ + # Use the continuous range [lower, end + 1) to produce an integer result in [lower, end]. + # random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even + # when a != b, so ensure the result is never more than `end`. + return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower)) class NamedRange(Range): diff --git a/Utils.py b/Utils.py index 8f5ba1a0f8..0aa81af150 100644 --- a/Utils.py +++ b/Utils.py @@ -521,8 +521,8 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, def filter(self, record: logging.LogRecord) -> bool: return self.condition(record) - file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False))) - file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.msg)) + file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False))) + file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.getMessage())) root_logger.addHandler(file_handler) if sys.stdout: formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') diff --git a/pytest.ini b/pytest.ini index 33e0bab8a9..cd8fd8dfce 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,3 +2,6 @@ python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported python_classes = Test python_functions = test +testpaths = + test + worlds diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 1432ef5c0d..76723d3931 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -141,9 +141,12 @@ def set_dw_rules(world: "HatInTimeWorld"): add_dw_rules(world, all_clear) add_rule(main_stamp, main_objective.access_rule) add_rule(all_clear, main_objective.access_rule) - # Only set bonus stamp rules if we don't auto complete bonuses + # Only set bonus stamp rules to require All Clear if we don't auto complete bonuses if not world.options.DWAutoCompleteBonuses and not world.is_bonus_excluded(all_clear.name): add_rule(bonus_stamps, all_clear.access_rule) + else: + # As soon as the Main Objective is completed, the bonuses auto-complete. + add_rule(bonus_stamps, main_objective.access_rule) if world.options.DWShuffle: for i in range(len(world.dw_shuffle)-1): @@ -343,6 +346,7 @@ def create_enemy_events(world: "HatInTimeWorld"): def set_enemy_rules(world: "HatInTimeWorld"): no_tourist = "Camera Tourist" in world.excluded_dws or "Camera Tourist" in world.excluded_bonuses + difficulty = get_difficulty(world) for enemy, regions in hit_list.items(): if no_tourist and enemy in bosses: @@ -372,6 +376,14 @@ def set_enemy_rules(world: "HatInTimeWorld"): or state.has("Zipline Unlock - The Lava Cake Path", world.player) or state.has("Zipline Unlock - The Windmill Path", world.player)) + elif enemy == "Toilet": + if area == "Toilet of Doom": + # The boss firewall is in the way and can only be skipped on Expert logic using a cherry hover. + add_rule(event, lambda state: has_paintings(state, world, 1, allow_skip=difficulty == Difficulty.EXPERT)) + if difficulty < Difficulty.HARD: + # Hard logic and above can cross the boss arena gap with a cherry bridge. + add_rule(event, lambda state: can_use_hookshot(state, world)) + elif enemy == "Director": if area == "Dead Bird Studio Basement": add_rule(event, lambda state: can_use_hookshot(state, world)) @@ -430,7 +442,7 @@ hit_list = { # Bosses "Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"], - "Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"], + "Director": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"], "Toilet": ["Toilet of Doom", "Boss Rush"], "Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush", @@ -454,7 +466,7 @@ triple_enemy_locations = [ bosses = [ "Mafia Boss", - "Conductor", + "Director", "Toilet", "Snatcher", "Toxic Flower", diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 9954514e8f..b34e6bb4a7 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -264,7 +264,6 @@ ahit_locations = { required_hats=[HatType.DWELLER], paintings=3), "Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area", - required_hats=[HatType.DWELLER], hookshot=True, paintings=3), @@ -323,7 +322,7 @@ ahit_locations = { "Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]), "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"), "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"), - "Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"), + "Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area (TIHS)", hookshot=True), "Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)", hookshot=True), "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"), "Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"), @@ -407,7 +406,7 @@ act_completions = { hit_type=HitType.umbrella_or_brewing, hookshot=True, paintings=1), "Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor", - hit_type=HitType.umbrella, paintings=1), + hit_type=HitType.dweller_bell, paintings=1), "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service", required_hats=[HatType.SPRINT]), @@ -878,7 +877,7 @@ snatcher_coins = { dlc_flags=HatDLC.death_wish), "Snatcher Coin - Top of HQ (DW: BTH)": LocData(0, "Beat the Heat", snatcher_coin="Snatcher Coin - Top of HQ", - dlc_flags=HatDLC.death_wish), + hit_type=HitType.umbrella, dlc_flags=HatDLC.death_wish), "Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", snatcher_coin="Snatcher Coin - Top of Tower", dlc_flags=HatDLC.death_wish), diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 183248a0e6..6753b8eb81 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -414,7 +414,7 @@ def set_moderate_rules(world: "HatInTimeWorld"): # Moderate: Mystifying Time Mesa time trial without hats set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), - lambda state: can_use_hookshot(state, world)) + lambda state: True) # Moderate: Goat Refinery from TIHS with Sprint only add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player), @@ -493,9 +493,6 @@ def set_hard_rules(world: "HatInTimeWorld"): lambda state: has_paintings(state, world, 3, True)) # SDJ - add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), - lambda state: can_use_hat(state, world, HatType.SPRINT) and has_paintings(state, world, 2), "or") - add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT), "or") @@ -533,7 +530,10 @@ def set_expert_rules(world: "HatInTimeWorld"): # Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) set_rule(world.multiworld.get_location("Mafia Town - Top of Lighthouse", world.player), lambda state: True) - set_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player), lambda state: True) + # There are not enough buckets/beach balls to bucket/ball hover in Heating Up Mafia Town, so any other Mafia Town + # act is required. + add_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player), + lambda state: state.can_reach_region("Mafia Town Area", world.player), "or") # Expert: Clear Dead Bird Studio with nothing for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: @@ -590,7 +590,7 @@ def set_expert_rules(world: "HatInTimeWorld"): if world.is_dlc2(): # Expert: clear Rush Hour with nothing - if not world.options.NoTicketSkips: + if world.options.NoTicketSkips != NoTicketSkips.option_true: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) else: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), @@ -739,7 +739,7 @@ def set_dlc1_rules(world: "HatInTimeWorld"): # This particular item isn't present in Act 3 for some reason, yes in vanilla too add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player), - lambda state: state.can_reach("Bon Voyage!", "Region", world.player) + lambda state: (state.can_reach("Bon Voyage!", "Region", world.player) and can_use_hookshot(state, world)) or state.can_reach("Ship Shape", "Region", world.player)) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index e2d2338b76..3ea47e40eb 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -110,6 +110,7 @@ class KH2Context(CommonContext): 18: TWTNW_Checks, # 255: {}, # starting screen } + self.last_world_int = -1 # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room # self.sveroom = 0x2A09C00 + 0x41 # 0 not in battle 1 in yellow battle 2 red battle #short @@ -345,33 +346,12 @@ class KH2Context(CommonContext): self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()} self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]] - if "keyblade_abilities" in self.kh2slotdata.keys(): - sora_ability_dict = self.kh2slotdata["KeybladeAbilities"] + if "KeybladeAbilities" in self.kh2slotdata.keys(): # sora ability to slot + self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"]) # itemid:[slots that are available for that item] - for k, v in sora_ability_dict.items(): - if v >= 1: - if k not in self.sora_ability_to_slot.keys(): - self.sora_ability_to_slot[k] = [] - for _ in range(sora_ability_dict[k]): - self.sora_ability_to_slot[k].append(self.kh2_seed_save_cache["SoraInvo"][0]) - self.kh2_seed_save_cache["SoraInvo"][0] -= 2 - donald_ability_dict = self.kh2slotdata["StaffAbilities"] - for k, v in donald_ability_dict.items(): - if v >= 1: - if k not in self.donald_ability_to_slot.keys(): - self.donald_ability_to_slot[k] = [] - for _ in range(donald_ability_dict[k]): - self.donald_ability_to_slot[k].append(self.kh2_seed_save_cache["DonaldInvo"][0]) - self.kh2_seed_save_cache["DonaldInvo"][0] -= 2 - goofy_ability_dict = self.kh2slotdata["ShieldAbilities"] - for k, v in goofy_ability_dict.items(): - if v >= 1: - if k not in self.goofy_ability_to_slot.keys(): - self.goofy_ability_to_slot[k] = [] - for _ in range(goofy_ability_dict[k]): - self.goofy_ability_to_slot[k].append(self.kh2_seed_save_cache["GoofyInvo"][0]) - self.kh2_seed_save_cache["GoofyInvo"][0] -= 2 + self.AbilityQuantityDict.update(self.kh2slotdata["StaffAbilities"]) + self.AbilityQuantityDict.update(self.kh2slotdata["ShieldAbilities"]) all_weapon_location_id = [] for weapon_location in all_weapon_slot: @@ -408,13 +388,15 @@ class KH2Context(CommonContext): async def checkWorldLocations(self): try: currentworldint = self.kh2_read_byte(self.Now) - await self.send_msgs([{ - "cmd": "Set", "key": "Slot: " + str(self.slot) + " :CurrentWorld", - "default": 0, "want_reply": True, "operations": [{ - "operation": "replace", - "value": currentworldint - }] - }]) + if self.last_world_int != currentworldint: + self.last_world_int = currentworldint + await self.send_msgs([{ + "cmd": "Set", "key": "Slot: " + str(self.slot) + " :CurrentWorld", + "default": 0, "want_reply": False, "operations": [{ + "operation": "replace", + "value": currentworldint + }] + }]) if currentworldint in self.worldid_to_locations: curworldid = self.worldid_to_locations[currentworldint] for location, data in curworldid.items(): @@ -525,27 +507,7 @@ class KH2Context(CommonContext): if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]: self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = [] # appending the slot that the ability should be in - # for non beta. remove after 4.3 - if "PoptrackerVersion" in self.kh2slotdata: - if self.kh2slotdata["PoptrackerVersionCheck"] < 4.3: - if (itemname in self.sora_ability_set - and len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < self.item_name_to_data[itemname].quantity) \ - and self.kh2_seed_save_cache["SoraInvo"][1] > 0x254C: - ability_slot = self.kh2_seed_save_cache["SoraInvo"][1] - self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) - self.kh2_seed_save_cache["SoraInvo"][1] -= 2 - elif itemname in self.donald_ability_set: - ability_slot = self.kh2_seed_save_cache["DonaldInvo"][1] - self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) - self.kh2_seed_save_cache["DonaldInvo"][1] -= 2 - else: - ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1] - self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) - self.kh2_seed_save_cache["GoofyInvo"][1] -= 2 - if ability_slot in self.front_ability_slots: - self.front_ability_slots.remove(ability_slot) - - elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ + if len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ self.AbilityQuantityDict[itemname]: if itemname in self.sora_ability_set: ability_slot = self.kh2_seed_save_cache["SoraInvo"][0] @@ -845,7 +807,7 @@ class KH2Context(CommonContext): logger.info("line 840") -def finishedGame(ctx: KH2Context, message): +def finishedGame(ctx: KH2Context): if ctx.kh2slotdata['FinalXemnas'] == 1: if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \ & 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0: @@ -877,8 +839,9 @@ def finishedGame(ctx: KH2Context, message): elif ctx.kh2slotdata['Goal'] == 2: # for backwards compat if "hitlist" in ctx.kh2slotdata: + locations = ctx.sending for boss in ctx.kh2slotdata["hitlist"]: - if boss in message[0]["locations"]: + if boss in locations: ctx.hitlist_bounties += 1 if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]: if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1: @@ -919,11 +882,12 @@ async def kh2_watcher(ctx: KH2Context): await asyncio.create_task(ctx.verifyChests()) await asyncio.create_task(ctx.verifyItems()) await asyncio.create_task(ctx.verifyLevel()) - message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] - if finishedGame(ctx, message) and not ctx.kh2_finished_game: + if finishedGame(ctx) and not ctx.kh2_finished_game: await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) ctx.kh2_finished_game = True - await ctx.send_msgs(message) + if ctx.sending: + message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] + await ctx.send_msgs(message) elif not ctx.kh2connected and ctx.serverconneced: logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.") ctx.kh2 = None diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index ff6cc06c39..413bf89c06 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -103,6 +103,7 @@ def generateRom(args, world: "LinksAwakeningWorld"): assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available (and boots) assembler.const("wCustomMessage", 0xC0A0) + assembler.const("wOverworldRoomStatus", 0xD800) # We store the link info in unused color dungeon flags, so it gets preserved in the savegame. assembler.const("wLinkSyncSequenceNumber", 0xDDF6) diff --git a/worlds/ladx/LADXR/locations/itemInfo.py b/worlds/ladx/LADXR/locations/itemInfo.py index dcd4205f4c..cd0f355149 100644 --- a/worlds/ladx/LADXR/locations/itemInfo.py +++ b/worlds/ladx/LADXR/locations/itemInfo.py @@ -2,6 +2,10 @@ import typing from ..checkMetadata import checkMetadataTable from .constants import * +custom_name_replacements = { + '"':"'", + '_':' ', +} class ItemInfo: MULTIWORLD = True @@ -23,6 +27,11 @@ class ItemInfo: def setLocation(self, location): self._location = location + def setCustomItemName(self, name): + for key, val in custom_name_replacements.items(): + name = name.replace(key, val) + self.custom_item_name = name + def getOptions(self): return self.OPTIONS diff --git a/worlds/ladx/LADXR/patches/core.py b/worlds/ladx/LADXR/patches/core.py index f4752c82e3..d9fcd62e30 100644 --- a/worlds/ladx/LADXR/patches/core.py +++ b/worlds/ladx/LADXR/patches/core.py @@ -716,9 +716,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) @@ -797,8 +795,14 @@ 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 diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index f20b7f8018..7b1a35666a 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -439,7 +439,7 @@ class LinksAwakeningWorld(World): # Otherwise, use a cute letter as the icon elif self.options.foreign_item_icons == 'guess_by_name': loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item) - loc.ladxr_item.custom_item_name = loc.item.name + loc.ladxr_item.setCustomItemName(loc.item.name) else: if loc.item.advancement: diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 4bcacb33c3..7d1bae0d6a 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,36 +1,37 @@ -pyevermizer==0.48.0 \ - --hash=sha256:069ce348e480e04fd6208cfd0f789c600b18d7c34b5272375b95823be191ed57 \ - --hash=sha256:58164dddaba2f340b0a8b4f39605e9dac46d8b0ffb16120e2e57bef2bfc1d683 \ - --hash=sha256:115dd09d38a10f11d4629b340dfd75e2ba4089a1ff9e9748a11619829e02c876 \ - --hash=sha256:b5e79cfe721e75cd7dec306b5eecd6385ce059e31ef7523ba7f677e22161ec6f \ - --hash=sha256:382882fa9d641b9969a6c3ed89449a814bdabcb6b17b558872d95008a6cc908b \ - --hash=sha256:92f67700e9132064a90858d391dd0b8fb111aff6dfd472befed57772d89ae567 \ - --hash=sha256:fe4c453b7dbd5aa834b81f9a7aedb949a605455650b938b8b304d8e5a7edcbf7 \ - --hash=sha256:c6bdbc45daf73818f763ed59ad079f16494593395d806f772dd62605c722b3e9 \ - --hash=sha256:bb09f45448fdfd28566ae6fcc38c35a6632f4c31a9de2483848f6ce17b2359b5 \ - --hash=sha256:00a8b9014744bd1528d0d39c33ede7c0d1713ad797a331cebb33d377a5bc1064 \ - --hash=sha256:64ee69edc0a7d3b3caded78f2e46975f9beaff1ff8feaf29b87da44c45f38d7d \ - --hash=sha256:9211bdb1313e9f4869ed5bdc61f3831d39679bd08bb4087f1c1e5475d9e3018b \ - --hash=sha256:4a57821e422a1d75fe3307931a78db7a65e76955f8e401c4b347db6570390d09 \ - --hash=sha256:04670cee0a0b913f24d2b9a1e771781560e2485bda31e6cd372a08421cf85cfa \ - --hash=sha256:971fe77d0a20a1db984020ad253b613d0983f5e23ff22cba60ee5ac00d8128de \ - --hash=sha256:127265fdb49f718f54706bf15604af1cec23590afd00d423089dea4331dcfc61 \ - --hash=sha256:d47576360337c1a23f424cd49944a8d68fc4f3338e00719c9f89972c84604bef \ - --hash=sha256:879659603e51130a0de8d9885d815a2fa1df8bd6cebe6d520d1c6002302adfdb \ - --hash=sha256:6a91bfc53dd130db6424adf8ac97a1133e97b4157ed00f889d8cbd26a2a4b340 \ - --hash=sha256:f3bf35fc5eef4cda49d2de77339fc201dd3206660a3dc15db005625b15bb806c \ - --hash=sha256:e7c8d5bf59a3c16db20411bc5d8e9c9087a30b6b4edf1b5ed9f4c013291427e4 \ - --hash=sha256:054a4d84ffe75448d41e88e1e0642ef719eb6111be5fe608e71e27a558c59069 \ - --hash=sha256:e6f141ca367469c69ba7fbf65836c479ec6672c598cfcb6b39e8098c60d346bc \ - --hash=sha256:6e65eb88f0c1ff4acde1c13b24ce649b0fe3d1d3916d02d96836c781a5022571 \ - --hash=sha256:e61e8f476b6da809cf38912755ed8bb009665f589e913eb8df877e9fa763024b \ - --hash=sha256:7e7c5484c0a2e3da6064de3f73d8d988d6703db58ab0be4730cbbf1a82319237 \ - --hash=sha256:9033b954e5f4878fd94af6d2056c78e3316115521fb1c24a4416d5cbf2ad66ad \ - --hash=sha256:824c623fff8ae4da176306c458ad63ad16a06a495a16db700665eca3c115924f \ - --hash=sha256:8e31031409a8386c6a63b79d480393481badb3ba29f32ff7a0db2b4abed20ac8 \ - --hash=sha256:7dbb7bb13e1e94f69f7ccdbcf4d35776424555fce5af1ca29d0256f91fdf087a \ - --hash=sha256:3a24e331b259407b6912d6e0738aa8a675831db3b7493fcf54dc17cb0cb80d37 \ - --hash=sha256:fdda06662a994271e96633cba100dd92b2fcd524acef8b2f664d1aaa14503cbd \ - --hash=sha256:0f0fc81bef3dbb78ba6a7622dd4296f23c59825968a0bb0448beb16eb3397cc2 \ - --hash=sha256:e07cbef776a7468669211546887357cc88e9afcf1578b23a4a4f2480517b15d9 \ - --hash=sha256:e442212695bdf60e455673b7b9dd83a5d4b830d714376477093d2c9054d92832 +pyevermizer==0.48.1 \ + --hash=sha256:db85cb4760abfde9d4b566d4613f2eddb8c2ff6f1c202ca0c2c5800bd62c9507 \ + --hash=sha256:1c67d0dff0a42b9a037cdb138c0c7b2c776d8d7425830e7fd32f7ebf8f35ac00 \ + --hash=sha256:d417f5b0407b063496aca43a65389e3308b6d0933c1d7907f7ecc8a00057903b \ + --hash=sha256:abf6560204128783239c8f0fb15059a7c2ff453812f85fb8567766706b7839cc \ + --hash=sha256:39e0cba1de1bc108c5b770ebe0fcbf3f6cb05575daf6bebe78c831c74848d101 \ + --hash=sha256:a16054ce0d904749ef27ede375c0ca8f420831e28c4e84c67361e8181207f00d \ + --hash=sha256:e6de509e4943bcde3e207a3640cad8efe3d8183740b63dc3cdbf5013db0f618b \ + --hash=sha256:e9269cf1290ab2967eaac0bc24e658336fb0e1f6612efce8d7ef0e76c1c26200 \ + --hash=sha256:f69e244229a110183d36b6a43ca557e716016d17e11265dca4070b8857afdb8d \ + --hash=sha256:118d059b8ccd246dafb0a51d0aa8e4543c172f9665378983b9f43c680487732e \ + --hash=sha256:185210c68b16351b3add4896ecfc26fe3867dadee9022f6a256e13093cca4a3b \ + --hash=sha256:10e281612c38bbec11d35f5c09f5a5174fb884cc60e6f16b6790d854e4346678 \ + --hash=sha256:9fc7d7e986243a96e96c1c05a386eb5d2ae4faef1ba810ab7e9e63dd83e86c2b \ + --hash=sha256:c26eafc2230dca9e91aaf925a346532586d0f448456437ea4ce5054e15653fd8 \ + --hash=sha256:8f96ffc5cfbe17b5c08818052be6f96906a1c9d3911e7bc4fbefee9b9ffa8f15 \ + --hash=sha256:e40948cbcaab27aa4febb58054752f83357e81f4a6f088da22a71c4ec9aa7ef2 \ + --hash=sha256:d59369cafa5df0fd2ce5cd5656c926e2fc0226a5a67a003d95497d56a0728dd3 \ + --hash=sha256:345a25675d92aada5d94bc3f3d3e2946efd940a7228628bf8c05d2853ddda86d \ + --hash=sha256:c0aa5054178c5e9900bfcf393c2bffdc69921d165521a3e9e5271528b01ef442 \ + --hash=sha256:719d417fc21778d5036c9d25b7ce55582ab6f49da63ab93ec17d75ea6042364c \ + --hash=sha256:28e220939850cfd8da16743365b28fa36d5bfc1dc58564789ae415e014ebc354 \ + --hash=sha256:770e582000abf64dc7f0c62672e4a1f64729bb20695664c59e29d238398cb865 \ + --hash=sha256:61d451b6f7d76fd435a5e9d2df111533e6e43da397a457f310151917318bd175 \ + --hash=sha256:1c8b596e246bb8437c7fc6c9bb8d9c2c70bd9942f09b06ada02d2fabe596fa0b \ + --hash=sha256:617f3eb0938e71a07b16477529f97fdf64487875462eb2edba6c9820b9686c0a \ + --hash=sha256:98d655a256040a3ae6305145a9692a5483ddcfb9b9bbdb78d43f5e93e002a3ae \ + --hash=sha256:d565bde7b1eb873badeedc2c9f327b4e226702b571aab2019778d46aa4509572 \ + --hash=sha256:e04b89d6edf6ffdbf5c725b0cbf7375c87003378da80e6666818a2b6d59d3fc9 \ + --hash=sha256:cc35e72f2a9e438786451f54532ce663ca63aedc3b4a43532f4ee97b45a71ed1 \ + --hash=sha256:2e4640a975bf324e75f15edd6450e63db8228e2046b893bbdc47d896d5aec890 \ + --hash=sha256:752716024255f13f96e40877b932694a517100a382a13f76c0bed3116b77f6d6 \ + --hash=sha256:d36518349132cf2f3f4e5a6b0294db0b40f395daa620b0938227c2c8f5b1213e \ + --hash=sha256:b5bca6e7fe5dcccd1e8757db4fb20d4bd998ed2b0f4b9ed26f7407c0a9b48d9f \ + --hash=sha256:4663b727d2637ce7713e3db7b68828ca7dc6f03482f4763a055156f3fd16e026 \ + --hash=sha256:7732bec7ffb29337418e62f15dc924e229faf09c55b079ad3f46f47eedc10c0d \ + --hash=sha256:b83a7a4df24800f82844f6acc6d43cd4673de0c24c9041ab56e57f518defa5a1 \ diff --git a/worlds/soe/test/test_oob.py b/worlds/soe/test/test_oob.py index 3c1a2829de..0878fd56e3 100644 --- a/worlds/soe/test/test_oob.py +++ b/worlds/soe/test/test_oob.py @@ -12,13 +12,13 @@ class OoBTest(SoETestBase): # some locations that just need a weapon + OoB oob_reachable = [ "Aquagoth", "Sons of Sth.", "Mad Monk", "Magmar", # OoB can use volcano shop to skip rock skip - "Levitate", "Fireball", "Drain", "Speed", + "Levitate", "Fireball", "Speed", "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57", ] # some locations that should still be unreachable oob_unreachable = [ "Tiny", "Rimsala", - "Barrier", "Call Up", "Reflect", "Force Field", "Stop", # Stop guy doesn't spawn for the other entrances + "Barrier", "Drain", "Call Up", "Reflect", "Force Field", "Stop", # Stop guy only spawns from one entrance "Pyramid bottom #118", "Tiny's hideout #160", "Tiny's hideout #161", "Greenhouse #275", ] # OoB + Diamond Eyes @@ -31,11 +31,42 @@ class OoBTest(SoETestBase): "Tiny's hideout #161", ] - self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=False) - self.collect_by_name("Gladiator Sword") - self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=in_logic) - self.collect_by_name("Diamond Eye") - self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic) + with self.subTest("No items", oob_logic=in_logic): + self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=False) + with self.subTest("Cutting Weapon", oob_logic=in_logic): + self.collect_by_name("Gladiator Sword") + self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=in_logic) + with self.subTest("Cutting Weapon + DEs", oob_logic=in_logic): + self.collect_by_name("Diamond Eye") + self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic) + + def test_real_axe(self) -> None: + in_logic = self.options["out_of_bounds"] == "logic" + + # needs real Bronze Axe+, regardless of OoB + real_axe_required = [ + "Drain", + "Drain Cave #180", + "Drain Cave #181", + ] + also_des_required = [ + "Double Drain", + ] + + with self.subTest("No Axe", oob_logic=in_logic): + self.collect_by_name("Gladiator Sword") + self.assertLocationReachability(reachable=real_axe_required, satisfied=False) + with self.subTest("Bronze Axe", oob_logic=in_logic): + self.collect_by_name("Bronze Axe") + self.assertLocationReachability(reachable=real_axe_required, satisfied=True) + with self.subTest("Knight Basher", oob_logic=in_logic): + self.remove_by_name("Bronze Axe") + self.collect_by_name("Knight Basher") + self.assertLocationReachability(reachable=real_axe_required, satisfied=True) + self.assertLocationReachability(reachable=also_des_required, satisfied=False) + with self.subTest("Knight Basher + DEs", oob_logic=in_logic): + self.collect_by_name("Diamond Eye") + self.assertLocationReachability(reachable=also_des_required, satisfied=True) def test_oob_goal(self) -> None: # still need Energy Core with OoB if sequence breaks are not in logic diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 09279dd1bd..388a44113a 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -95,7 +95,7 @@ class TunicWorld(World): # for the local_fill option fill_items: List[TunicItem] - fill_locations: List[TunicLocation] + fill_locations: List[Location] amount_to_local_fill: int # so we only loop the multiworld locations once @@ -394,8 +394,6 @@ class TunicWorld(World): self.multiworld.itempool += tunic_items def pre_fill(self) -> None: - self.fill_locations = [] - if self.options.local_fill > 0 and self.multiworld.players > 1: # we need to reserve a couple locations so that we don't fill up every sphere 1 location reserved_locations: Set[str] = set(self.random.sample(sphere_one, 2)) @@ -406,8 +404,8 @@ class TunicWorld(World): if len(viable_locations) < self.amount_to_local_fill: raise OptionError(f"TUNIC: Not enough locations for local_fill option for {self.player_name}. " f"This is likely due to excess plando or priority locations.") - - self.fill_locations += viable_locations + self.random.shuffle(viable_locations) + self.fill_locations = viable_locations[:self.amount_to_local_fill] @classmethod def stage_pre_fill(cls, multiworld: MultiWorld) -> None: