diff --git a/BaseClasses.py b/BaseClasses.py index 24dc074b63..cd8749e11d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -160,14 +160,6 @@ class MultiWorld(): self.local_early_items = {player: {} for player in self.player_ids} self.indirect_connections = {} self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {} - self.fix_trock_doors = self.AttributeProxy( - lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') - self.fix_skullwoods_exit = self.AttributeProxy( - lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']) - self.fix_palaceofdarkness_exit = self.AttributeProxy( - lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']) - self.fix_trock_exit = self.AttributeProxy( - lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']) for player in range(1, players + 1): def set_player_attr(attr, val): @@ -445,7 +437,7 @@ class MultiWorld(): location.item = item item.location = location if collect: - self.state.collect(item, location.event, location) + self.state.collect(item, location.advancement, location) logging.debug('Placed %s at %s', item, location) @@ -592,8 +584,7 @@ class MultiWorld(): def location_relevant(location: Location): """Determine if this location is relevant to sweep.""" if location.progress_type != LocationProgressType.EXCLUDED \ - and (location.player in players["locations"] or location.event - or (location.item and location.item.advancement)): + and (location.player in players["locations"] or location.advancement): return True return False @@ -738,7 +729,7 @@ class CollectionState(): locations = self.multiworld.get_filled_locations() reachable_events = True # since the loop has a good chance to run more than once, only filter the events once - locations = {location for location in locations if location.event and location not in self.events and + locations = {location for location in locations if location.advancement and location not in self.events and not key_only or getattr(location.item, "locked_dungeon_item", False)} while reachable_events: reachable_events = {location for location in locations if location.can_reach(self)} @@ -1028,7 +1019,6 @@ class Location: name: str address: Optional[int] parent_region: Optional[Region] - event: bool = False locked: bool = False show_in_spoiler: bool = True progress_type: LocationProgressType = LocationProgressType.DEFAULT @@ -1059,7 +1049,6 @@ class Location: raise Exception(f"Location {self} already filled.") self.item = item item.location = self - self.event = item.advancement self.locked = True def __repr__(self): @@ -1075,6 +1064,15 @@ class Location: def __lt__(self, other: Location): return (self.player, self.name) < (other.player, other.name) + @property + def advancement(self) -> bool: + return self.item is not None and self.item.advancement + + @property + def is_event(self) -> bool: + """Returns True if the address of this location is None, denoting it is an Event Location.""" + return self.address is None + @property def native_item(self) -> bool: """Returns True if the item in this location matches game.""" @@ -1352,12 +1350,15 @@ class Spoiler: get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player)) def to_file(self, filename: str) -> None: + from itertools import chain from worlds import AutoWorld + from Options import Visibility def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None: res = getattr(self.multiworld.worlds[player].options, option_key) - display_name = getattr(option_obj, "display_name", option_key) - outfile.write(f"{display_name + ':':33}{res.current_option_name}\n") + if res.visibility & Visibility.spoiler: + display_name = getattr(option_obj, "display_name", option_key) + outfile.write(f"{display_name + ':':33}{res.current_option_name}\n") with open(filename, 'w', encoding="utf-8-sig") as outfile: outfile.write( @@ -1388,6 +1389,14 @@ class Spoiler: AutoWorld.call_all(self.multiworld, "write_spoiler", outfile) + precollected_items = [f"{item.name} ({self.multiworld.get_player_name(item.player)})" + if self.multiworld.players > 1 + else item.name + for item in chain.from_iterable(self.multiworld.precollected_items.values())] + if precollected_items: + outfile.write("\n\nStarting Items:\n\n") + outfile.write("\n".join([item for item in precollected_items])) + locations = [(str(location), str(location.item) if location.item is not None else "Nothing") for location in self.multiworld.get_locations() if location.show_in_spoiler] outfile.write('\n\nLocations:\n\n') diff --git a/CommonClient.py b/CommonClient.py index 085a48a4b7..88a2c512d5 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -193,6 +193,7 @@ class CommonContext: server_version: Version = Version(0, 0, 0) generator_version: Version = Version(0, 0, 0) current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server + max_size: int = 16*1024*1024 # 16 MB of max incoming packet size last_death_link: float = time.time() # last send/received death link on AP layer @@ -651,7 +652,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None) try: port = server_url.port or 38281 # raises ValueError if invalid socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None, - ssl=get_ssl_context() if address.startswith("wss://") else None) + ssl=get_ssl_context() if address.startswith("wss://") else None, + max_size=ctx.max_size) if ctx.ui is not None: ctx.ui.update_address_bar(server_url.netloc) ctx.server = Endpoint(socket) diff --git a/Fill.py b/Fill.py index 291ea7e882..cb143c408e 100644 --- a/Fill.py +++ b/Fill.py @@ -159,7 +159,6 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati multiworld.push_item(spot_to_fill, item_to_place, False) spot_to_fill.locked = lock placements.append(spot_to_fill) - spot_to_fill.event = item_to_place.advancement placed += 1 if not placed % 1000: _log_fill_progress(name, placed, total) @@ -310,7 +309,6 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo pool.append(location.item) state.remove(location.item) location.item = None - location.event = False if location in state.events: state.events.remove(location) locations.append(location) @@ -659,7 +657,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None: while True: # Check locations in the current sphere and gather progression items to swap earlier for location in balancing_sphere: - if location.event: + if location.advancement: balancing_state.collect(location.item, True, location) player = location.item.player # only replace items that end up in another player's world @@ -716,7 +714,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None: # sort then shuffle to maintain deterministic behaviour, # while allowing use of set for better algorithm growth behaviour elsewhere - replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked) + replacement_locations = sorted(l for l in checked_locations if not l.advancement and not l.locked) multiworld.random.shuffle(replacement_locations) items_to_replace.sort() multiworld.random.shuffle(items_to_replace) @@ -747,7 +745,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None: sphere_locations.add(location) for location in sphere_locations: - if location.event: + if location.advancement: state.collect(location.item, True, location) checked_locations |= sphere_locations @@ -768,7 +766,6 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked: location_2.item, location_1.item = location_1.item, location_2.item location_1.item.location = location_1 location_2.item.location = location_2 - location_1.event, location_2.event = location_2.event, location_1.event def distribute_planned(multiworld: MultiWorld) -> None: @@ -965,7 +962,6 @@ def distribute_planned(multiworld: MultiWorld) -> None: placement['force']) for (item, location) in successful_pairs: multiworld.push_item(location, item, collect=False) - location.event = True # flag location to be checked during fill location.locked = True logging.debug(f"Plando placed {item} at {location}") if from_pool: diff --git a/MultiServer.py b/MultiServer.py index 395577b663..e1f524ced7 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -586,7 +586,7 @@ class Context: self.location_check_points = savedata["game_options"]["location_check_points"] self.server_password = savedata["game_options"]["server_password"] self.password = savedata["game_options"]["password"] - self.release_mode = savedata["game_options"].get("release_mode", savedata["game_options"].get("forfeit_mode", "goal")) + self.release_mode = savedata["game_options"]["release_mode"] self.remaining_mode = savedata["game_options"]["remaining_mode"] self.collect_mode = savedata["game_options"]["collect_mode"] self.item_cheat = savedata["game_options"]["item_cheat"] @@ -631,8 +631,6 @@ class Context: def _set_options(self, server_options: dict): for key, value in server_options.items(): - if key == "forfeit_mode": - key = "release_mode" data_type = self.simple_options.get(key, None) if data_type is not None: if value not in {False, True, None}: # some can be boolean OR text, such as password @@ -1347,6 +1345,7 @@ class ClientMessageProcessor(CommonCommandProcessor): "Sorry, !remaining requires you to have beaten the game on this server") return False + @mark_raw def _cmd_missing(self, filter_text="") -> bool: """List all missing location checks from the server's perspective. Can be given text, which will be used as filter.""" @@ -1356,7 +1355,11 @@ class ClientMessageProcessor(CommonCommandProcessor): if locations: names = [self.ctx.location_names[location] for location in locations] if filter_text: - names = [name for name in names if filter_text in name] + location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] + if filter_text in location_groups: # location group name + names = [name for name in names if name in location_groups[filter_text]] + else: + names = [name for name in names if filter_text in name] texts = [f'Missing: {name}' for name in names] if filter_text: texts.append(f"Found {len(locations)} missing location checks, displaying {len(names)} of them.") @@ -1367,6 +1370,7 @@ class ClientMessageProcessor(CommonCommandProcessor): self.output("No missing location checks found.") return True + @mark_raw def _cmd_checked(self, filter_text="") -> bool: """List all done location checks from the server's perspective. Can be given text, which will be used as filter.""" @@ -1376,7 +1380,11 @@ class ClientMessageProcessor(CommonCommandProcessor): if locations: names = [self.ctx.location_names[location] for location in locations] if filter_text: - names = [name for name in names if filter_text in name] + location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] + if filter_text in location_groups: # location group name + names = [name for name in names if name in location_groups[filter_text]] + else: + names = [name for name in names if filter_text in name] texts = [f'Checked: {name}' for name in names] if filter_text: texts.append(f"Found {len(locations)} done location checks, displaying {len(names)} of them.") @@ -1839,6 +1847,11 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus) if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion if new_status == ClientStatus.CLIENT_GOAL: ctx.on_goal_achieved(client) + # if player has yet to ever connect to the server, they will not be in client_game_state + if all(player in ctx.client_game_state and ctx.client_game_state[player] == ClientStatus.CLIENT_GOAL + for player in ctx.player_names + if player[0] == client.team and player[1] != client.slot): + ctx.broadcast_text_all(f"Team #{client.team + 1} has completed all of their games! Congratulations!") ctx.client_game_state[client.team, client.slot] = new_status ctx.on_client_status_change(client.team, client.slot) @@ -2092,8 +2105,8 @@ class ServerCommandProcessor(CommonCommandProcessor): if full_name.isnumeric(): location, usable, response = int(full_name), True, None - elif self.ctx.location_names_for_game(game) is not None: - location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game)) + elif game in self.ctx.all_location_and_group_names: + location, usable, response = get_intended_text(full_name, self.ctx.all_location_and_group_names[game]) else: self.output("Can't look up location for unknown game. Hint for ID instead.") return False @@ -2101,6 +2114,11 @@ class ServerCommandProcessor(CommonCommandProcessor): if usable: if isinstance(location, int): hints = collect_hint_location_id(self.ctx, team, slot, location) + elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]: + hints = [] + for loc_name_from_group in self.ctx.location_name_groups[game][location]: + if loc_name_from_group in self.ctx.location_names_for_game(game): + hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group)) else: hints = collect_hint_location_name(self.ctx, team, slot, location) if hints: diff --git a/Options.py b/Options.py index e1ae339143..3b1cdc6e2b 100644 --- a/Options.py +++ b/Options.py @@ -7,6 +7,7 @@ import math import numbers import random import typing +import enum from copy import deepcopy from dataclasses import dataclass @@ -20,6 +21,15 @@ if typing.TYPE_CHECKING: import pathlib +class Visibility(enum.IntFlag): + none = 0b0000 + template = 0b0001 + simple_ui = 0b0010 # show option in simple menus, such as player-options + complex_ui = 0b0100 # show option in complex menus, such as weighted-options + spoiler = 0b1000 + all = 0b1111 + + class AssembleOptions(abc.ABCMeta): def __new__(mcs, name, bases, attrs): options = attrs["options"] = {} @@ -102,6 +112,7 @@ T = typing.TypeVar('T') class Option(typing.Generic[T], metaclass=AssembleOptions): value: T default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type + visibility = Visibility.all # convert option_name_long into Name Long as display_name, otherwise name_long is the result. # Handled in get_option_name() @@ -1115,6 +1126,17 @@ class ItemLinks(OptionList): link.setdefault("link_replacement", None) +class Removed(FreeText): + """This Option has been Removed.""" + default = "" + visibility = Visibility.none + + def __init__(self, value: str): + if value: + raise Exception("Option removed, please update your options file.") + super().__init__(value) + + @dataclass class PerGameCommonOptions(CommonOptions): local_items: LocalItems @@ -1170,7 +1192,10 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge for game_name, world in AutoWorldRegister.world_types.items(): if not world.hidden or generate_hidden: - all_options: typing.Dict[str, AssembleOptions] = world.options_dataclass.type_hints + all_options: typing.Dict[str, AssembleOptions] = { + option_name: option for option_name, option in world.options_dataclass.type_hints.items() + if option.visibility & Visibility.template + } with open(local_path("data", "options.yaml")) as f: file_data = f.read() diff --git a/SNIClient.py b/SNIClient.py index 062d7a7cbe..1804ab3cc0 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -564,16 +564,12 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int, PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'} try: for address, data in write_list: - while data: - # Divide the write into packets of 256 bytes. - PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]] - if ctx.snes_socket is not None: - await ctx.snes_socket.send(dumps(PutAddress_Request)) - await ctx.snes_socket.send(data[:256]) - address += 256 - data = data[256:] - else: - snes_logger.warning(f"Could not send data to SNES: {data}") + PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]] + if ctx.snes_socket is not None: + await ctx.snes_socket.send(dumps(PutAddress_Request)) + await ctx.snes_socket.send(data) + else: + snes_logger.warning(f"Could not send data to SNES: {data}") except ConnectionClosed: return False diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index 102c3a49f6..cfdbe25ff2 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -2,8 +2,9 @@ from typing import List, Tuple from uuid import UUID -from flask import Blueprint, abort +from flask import Blueprint, abort, url_for +import worlds.Files from .. import cache from ..models import Room, Seed @@ -21,12 +22,30 @@ def room_info(room: UUID): room = Room.get(id=room) if room is None: return abort(404) + + def supports_apdeltapatch(game: str): + return game in worlds.Files.AutoPatchRegister.patch_types + downloads = [] + for slot in sorted(room.seed.slots): + if slot.data and not supports_apdeltapatch(slot.game): + slot_download = { + "slot": slot.player_id, + "download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id) + } + downloads.append(slot_download) + elif slot.data: + slot_download = { + "slot": slot.player_id, + "download": url_for("download_patch", patch_id=slot.id, room_id=room.id) + } + downloads.append(slot_download) return { "tracker": room.tracker, "players": get_players(room.seed), "last_port": room.last_port, "last_activity": room.last_activity, - "timeout": room.timeout + "timeout": room.timeout, + "downloads": downloads, } diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 0158de7e24..b3fd8d612a 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -45,7 +45,15 @@ def create(): } game_options = {} + visible: typing.Set[str] = set() + visible_weighted: typing.Set[str] = set() + for option_name, option in all_options.items(): + if option.visibility & Options.Visibility.simple_ui: + visible.add(option_name) + if option.visibility & Options.Visibility.complex_ui: + visible_weighted.add(option_name) + if option_name in handled_in_js: pass @@ -116,8 +124,6 @@ def create(): else: logging.debug(f"{option} not exported to Web Options.") - player_options["gameOptions"] = game_options - player_options["presetOptions"] = {} for preset_name, preset in world.web.options_presets.items(): player_options["presetOptions"][preset_name] = {} @@ -156,12 +162,23 @@ def create(): os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True) + filtered_player_options = player_options + filtered_player_options["gameOptions"] = { + option_name: option_data for option_name, option_data in game_options.items() + if option_name in visible + } + with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f: - json.dump(player_options, f, indent=2, separators=(',', ': ')) + json.dump(filtered_player_options, f, indent=2, separators=(',', ': ')) + + filtered_player_options["gameOptions"] = { + option_name: option_data for option_name, option_data in game_options.items() + if option_name in visible_weighted + } if not world.hidden and world.web.options_page is True: # Add the random option to Choice, TextChoice, and Toggle options - for option in game_options.values(): + for option in filtered_player_options["gameOptions"].values(): if option["type"] == "select": option["options"].append({"name": "Random", "value": "random"}) @@ -170,7 +187,7 @@ def create(): weighted_options["baseOptions"]["game"][game_name] = 0 weighted_options["games"][game_name] = { - "gameSettings": game_options, + "gameSettings": filtered_player_options["gameOptions"], "gameItems": tuple(world.item_names), "gameItemGroups": [ group for group in world.item_name_groups.keys() if group != "Everything" diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 9cb48009a4..7bbb894de0 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -47,9 +47,6 @@ {% elif patch.game | supports_apdeltapatch %} Download Patch File... - {% elif patch.game == "Dark Souls III" %} - - Download JSON File... {% elif patch.game == "Final Fantasy Mystic Quest" %} Download APMQ File... diff --git a/WebHostLib/templates/startPlaying.html b/WebHostLib/templates/startPlaying.html index 436af3df07..ab2f021d61 100644 --- a/WebHostLib/templates/startPlaying.html +++ b/WebHostLib/templates/startPlaying.html @@ -18,7 +18,7 @@

To start playing a game, you'll first need to generate a randomized game. - You'll need to upload either a config file or a zip file containing one more config files. + You'll need to upload one or more config files (YAMLs) or a zip file containing one or more config files.

If you have already generated a game and just need to host it, this site can
diff --git a/docs/world api.md b/docs/world api.md index f82ef40a98..4f9fc2b1dd 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -380,11 +380,6 @@ from BaseClasses import Location class MyGameLocation(Location): game: str = "My Game" - - # override constructor to automatically mark event locations as such - def __init__(self, player: int, name="", code=None, parent=None) -> None: - super(MyGameLocation, self).__init__(player, name, code, parent) - self.event = code is None ``` in your `__init__.py` or your `locations.py`. diff --git a/kvui.py b/kvui.py index fba3204929..a1663126cc 100644 --- a/kvui.py +++ b/kvui.py @@ -740,15 +740,17 @@ class KivyJSONtoTextParser(JSONtoTextParser): def _handle_item_name(self, node: JSONMessagePart): flags = node.get("flags", 0) + item_types = [] if flags & 0b001: # advancement - itemtype = "progression" - elif flags & 0b010: # useful - itemtype = "useful" - elif flags & 0b100: # trap - itemtype = "trap" - else: - itemtype = "normal" - node.setdefault("refs", []).append("Item Class: " + itemtype) + item_types.append("progression") + if flags & 0b010: # useful + item_types.append("useful") + if flags & 0b100: # trap + item_types.append("trap") + if not item_types: + item_types.append("normal") + + node.setdefault("refs", []).append("Item Class: " + ", ".join(item_types)) return super(KivyJSONtoTextParser, self)._handle_item_name(node) def _handle_player_id(self, node: JSONMessagePart): diff --git a/test/bases.py b/test/bases.py index 07a3e60086..ee9fbcb683 100644 --- a/test/bases.py +++ b/test/bases.py @@ -221,7 +221,7 @@ class WorldTestBase(unittest.TestCase): if isinstance(items, Item): items = (items,) for item in items: - if item.location and item.location.event and item.location in self.multiworld.state.events: + if item.location and item.advancement and item.location in self.multiworld.state.events: self.multiworld.state.events.remove(item.location) self.multiworld.state.remove(item) diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 70e9e822bf..7b004db61f 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -80,7 +80,6 @@ def fill_region(multiworld: MultiWorld, region: Region, items: List[Item]) -> Li return items item = items.pop(0) multiworld.push_item(location, item, False) - location.event = item.advancement return items @@ -489,7 +488,6 @@ class TestFillRestrictive(unittest.TestCase): player1 = generate_player_data(multiworld, 1, 1, 1) location = player1.locations[0] location.address = None - location.event = True item = player1.prog_items[0] item.code = None location.place_locked_item(item) @@ -527,13 +525,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase): distribute_items_restrictive(multiworld) self.assertEqual(locations[0].item, basic_items[1]) - self.assertFalse(locations[0].event) + self.assertFalse(locations[0].advancement) self.assertEqual(locations[1].item, prog_items[0]) - self.assertTrue(locations[1].event) + self.assertTrue(locations[1].advancement) self.assertEqual(locations[2].item, prog_items[1]) - self.assertTrue(locations[2].event) + self.assertTrue(locations[2].advancement) self.assertEqual(locations[3].item, basic_items[0]) - self.assertFalse(locations[3].event) + self.assertFalse(locations[3].advancement) def test_excluded_distribute(self): """Test that distribute_items_restrictive doesn't put advancement items on excluded locations""" @@ -746,7 +744,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): for item in multiworld.get_items(): self.assertEqual(item.player, item.location.player) - self.assertFalse(item.location.event, False) + self.assertFalse(item.location.advancement, False) def test_early_items(self) -> None: """Test that the early items API successfully places items early""" diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 85e2c99097..05bee23412 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -234,8 +234,11 @@ async def _run_game(rom: str): async def _patch_and_run_game(patch_file: str): - metadata, output_file = Patch.create_rom_file(patch_file) - Utils.async_start(_run_game(output_file)) + try: + metadata, output_file = Patch.create_rom_file(patch_file) + Utils.async_start(_run_game(output_file)) + except Exception as exc: + logger.exception(exc) def launch() -> None: diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index 988455ba3c..062f0588f6 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -2657,6 +2657,10 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'), ('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'), ('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'), + ('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'), + ('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'), + ('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'), + ('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'), ('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'), ('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'), ('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'), @@ -2815,6 +2819,10 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'), ('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'), ('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'), ('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'), + ('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'), + ('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'), + ('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'), + ('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'), ('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'), ('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'), ('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'), diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index 25d4314769..63a2d499e2 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -408,14 +408,16 @@ def create_inverted_regions(world, player): ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']), create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], - ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', - 'Turtle Rock Big Key Door']), + ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', + 'Turtle Rock Second Section Bomb Wall']), + create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']), create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), + create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']), create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], - ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']), + ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']), create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']), create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']), create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 438c6226bc..c26c32a7a3 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -253,10 +253,8 @@ def generate_itempool(world): region.locations.append(loc) multiworld.push_item(loc, item_factory('Triforce', world), False) - loc.event = True loc.locked = True - multiworld.get_location('Ganon', player).event = True multiworld.get_location('Ganon', player).locked = True event_pairs = [ ('Agahnim 1', 'Beat Agahnim 1'), @@ -273,7 +271,7 @@ def generate_itempool(world): location = multiworld.get_location(location_name, player) event = item_factory(event_name, world) multiworld.push_item(location, event, False) - location.event = location.locked = True + location.locked = True # set up item pool diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 2b23dc341c..8cb377b7a4 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -2,7 +2,7 @@ import typing from BaseClasses import MultiWorld from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\ - FreeText + FreeText, Removed class GlitchesRequired(Choice): @@ -716,9 +716,8 @@ class BeemizerTrapChance(BeemizerRange): display_name = "Beemizer Trap Chance" -class AllowCollect(Toggle): - """Allows for !collect / co-op to auto-open chests containing items for other players. - Off by default, because it currently crashes on real hardware.""" +class AllowCollect(DefaultOnToggle): + """Allows for !collect / co-op to auto-open chests containing items for other players.""" display_name = "Allow Collection of checks for other players" @@ -796,4 +795,9 @@ alttp_options: typing.Dict[str, type(Option)] = { "music": Music, "reduceflashing": ReduceFlashing, "triforcehud": TriforceHud, + + # removed: + "goals": Removed, + "smallkey_shuffle": Removed, + "bigkey_shuffle": Removed, } diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index dc3adb108a..4c2e7d509e 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -336,13 +336,15 @@ def create_regions(world, player): ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']), create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']), create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']), - create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']), + create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']), + create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']), create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), + create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']), create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], - ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']), + ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']), create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']), create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']), create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 747f61498e..80f7ab7fbe 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -868,11 +868,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}): # For exits that connot be reached from another, no need to apply offset fixes. rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else - elif room_id == 0x0059 and world.fix_skullwoods_exit[player]: + elif room_id == 0x0059 and local_world.fix_skullwoods_exit: rom.write_int16(0x15DB5 + 2 * offset, 0x00F8) - elif room_id == 0x004a and world.fix_palaceofdarkness_exit[player]: + elif room_id == 0x004a and local_world.fix_palaceofdarkness_exit: rom.write_int16(0x15DB5 + 2 * offset, 0x0640) - elif room_id == 0x00d6 and world.fix_trock_exit[player]: + elif room_id == 0x00d6 and local_world.fix_trock_exit: rom.write_int16(0x15DB5 + 2 * offset, 0x0134) elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point rom.write_int16(0x15DB5 + 2 * offset, 0x00A4) @@ -1674,14 +1674,14 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_byte(0x4E3BB, 0xEB) # fix trock doors for reverse entrances - if world.fix_trock_doors[player]: + if local_world.fix_trock_doors: rom.write_byte(0xFED31, 0x0E) # preopen bombable exit rom.write_byte(0xFEE41, 0x0E) # preopen bombable exit # included unconditionally in base2current # rom.write_byte(0xFE465, 0x1E) # remove small key door on backside of big key door else: - rom.write_byte(0xFED31, 0x2A) # preopen bombable exit - rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit + rom.write_byte(0xFED31, 0x2A) # bombable exit + rom.write_byte(0xFEE41, 0x2A) # bombable exit if world.tile_shuffle[player]: tile_set = TileSet.get_random_tile_set(world.per_slot_randoms[player]) @@ -2397,6 +2397,9 @@ def write_strings(rom, world, player): if hint_count: locations = world.find_items_in_locations(items_to_hint, player, True) local_random.shuffle(locations) + # make locked locations less likely to appear as hint, + # chances are the lock means the player already knows. + locations.sort(key=lambda sorting_location: not sorting_location.locked) for x in range(min(hint_count, len(locations))): this_location = locations.pop() this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.' diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 320f9fe6fd..6646aae1b9 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -279,6 +279,9 @@ def global_rules(world, player): (state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4)))))) ) + set_rule(world.get_entrance('Hookshot Cave Bomb Wall (North)', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Hookshot Cave Bomb Wall (South)', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_location('Hookshot Cave - Top Left', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_location('Hookshot Cave - Bottom Right', player), @@ -477,7 +480,6 @@ def global_rules(world, player): set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player))) set_rule(world.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10)) - set_rule(world.get_entrance('Turtle Rock Ledge Exit (West)', player), lambda state: can_use_bombs(state, player) and can_kill_most_things(state, player, 10)) set_rule(world.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player) or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player)) set_rule(world.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player)) @@ -487,6 +489,13 @@ def global_rules(world, player): set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10)) + + if not world.worlds[player].fix_trock_doors: + add_rule(world.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player)) if world.enemy_shuffle[player]: set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3)) @@ -1184,7 +1193,6 @@ def set_trock_key_rules(world, player): item = item_factory('Small Key (Turtle Rock)', world.worlds[player]) location = world.get_location('Turtle Rock - Big Key Chest', player) location.place_locked_item(item) - location.event = True toss_junk_item(world, player) if world.accessibility[player] != 'locations': diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 8baeeb6dc2..9abc15b75b 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -261,6 +261,10 @@ class ALTTPWorld(World): self.dungeons = {} self.waterfall_fairy_bottle_fill = "Bottle" self.pyramid_fairy_bottle_fill = "Bottle" + self.fix_trock_doors = None + self.fix_skullwoods_exit = None + self.fix_palaceofdarkness_exit = None + self.fix_trock_exit = None super(ALTTPWorld, self).__init__(*args, **kwargs) @classmethod @@ -280,6 +284,15 @@ class ALTTPWorld(World): player = self.player multiworld = self.multiworld + self.fix_trock_doors = (multiworld.entrance_shuffle[player] != 'vanilla' + or multiworld.mode[player] == 'inverted') + self.fix_skullwoods_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted', + 'dungeons_simple'] + self.fix_palaceofdarkness_exit = multiworld.entrance_shuffle[player] not in ['dungeons_simple', 'vanilla', + 'simple', 'restricted'] + self.fix_trock_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted', + 'dungeons_simple'] + # fairy bottle fills bottle_options = [ "Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)", diff --git a/worlds/alttp/test/inverted_owg/TestDeathMountain.py b/worlds/alttp/test/inverted_owg/TestDeathMountain.py index b509643d0c..5186ae9106 100644 --- a/worlds/alttp/test/inverted_owg/TestDeathMountain.py +++ b/worlds/alttp/test/inverted_owg/TestDeathMountain.py @@ -101,20 +101,20 @@ class TestDeathMountain(TestInvertedOWG): ["Hookshot Cave - Bottom Right", False, []], ["Hookshot Cave - Bottom Right", False, [], ['Hookshot', 'Pegasus Boots']], ["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']], - ["Hookshot Cave - Bottom Right", True, ['Pegasus Boots']], + ["Hookshot Cave - Bottom Right", True, ['Pegasus Boots', 'Bomb Upgrade (50)']], ["Hookshot Cave - Bottom Left", False, []], ["Hookshot Cave - Bottom Left", False, [], ['Hookshot']], ["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']], - ["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot']], + ["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']], ["Hookshot Cave - Top Left", False, []], ["Hookshot Cave - Top Left", False, [], ['Hookshot']], ["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']], - ["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot']], + ["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']], ["Hookshot Cave - Top Right", False, []], ["Hookshot Cave - Top Right", False, [], ['Hookshot']], ["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']], - ["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot']], + ["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']], ]) \ No newline at end of file diff --git a/worlds/alttp/test/owg/TestDeathMountain.py b/worlds/alttp/test/owg/TestDeathMountain.py index 0933b2881e..59308b65f0 100644 --- a/worlds/alttp/test/owg/TestDeathMountain.py +++ b/worlds/alttp/test/owg/TestDeathMountain.py @@ -177,7 +177,7 @@ class TestDeathMountain(TestVanillaOWG): ["Hookshot Cave - Bottom Right", False, []], ["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots']], ["Hookshot Cave - Bottom Right", False, [], ['Moon Pearl']], - ["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots']], + ["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots', 'Bomb Upgrade (50)']], ["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']], ["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']], @@ -185,7 +185,7 @@ class TestDeathMountain(TestVanillaOWG): ["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots']], ["Hookshot Cave - Bottom Left", False, [], ['Moon Pearl']], ["Hookshot Cave - Bottom Left", False, [], ['Hookshot']], - ["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']], + ["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']], ["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']], ["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']], @@ -193,7 +193,7 @@ class TestDeathMountain(TestVanillaOWG): ["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots']], ["Hookshot Cave - Top Left", False, [], ['Moon Pearl']], ["Hookshot Cave - Top Left", False, [], ['Hookshot']], - ["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']], + ["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']], ["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']], ["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']], @@ -201,7 +201,7 @@ class TestDeathMountain(TestVanillaOWG): ["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots']], ["Hookshot Cave - Top Right", False, [], ['Moon Pearl']], ["Hookshot Cave - Top Right", False, [], ['Hookshot']], - ["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']], + ["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']], ["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']], ["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']], ]) \ No newline at end of file diff --git a/worlds/archipidle/Items.py b/worlds/archipidle/Items.py index 2b5e6e9a81..94665631b7 100644 --- a/worlds/archipidle/Items.py +++ b/worlds/archipidle/Items.py @@ -1,4 +1,7 @@ item_table = ( + 'An Old GeoCities Profile', + 'Very Funny Joke', + 'Motivational Video', 'Staples Easy Button', 'One Million Dollars', 'Replica Master Sword', @@ -13,7 +16,7 @@ item_table = ( '2012 Magic the Gathering Core Set Starter Box', 'Poke\'mon Booster Pack', 'USB Speakers', - 'Plastic Spork', + 'Eco-Friendly Spork', 'Cheeseburger', 'Brand New Car', 'Hunting Knife', @@ -22,7 +25,7 @@ item_table = ( 'One-Up Mushroom', 'Nokia N-GAGE', '2-Liter of Sprite', - 'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward expansion up to level 60 with no restrictions on playtime!', + 'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward and Stormblood expansions up to level 70 with no restrictions on playtime!', 'Can of Compressed Air', 'Striped Kitten', 'USB Power Adapter', diff --git a/worlds/archipidle/Rules.py b/worlds/archipidle/Rules.py index 3bf4bad475..2cc6220c69 100644 --- a/worlds/archipidle/Rules.py +++ b/worlds/archipidle/Rules.py @@ -1,6 +1,5 @@ from BaseClasses import MultiWorld -from ..AutoWorld import LogicMixin -from ..generic.Rules import set_rule +from worlds.AutoWorld import LogicMixin class ArchipIDLELogic(LogicMixin): @@ -10,29 +9,20 @@ class ArchipIDLELogic(LogicMixin): def set_rules(world: MultiWorld, player: int): for i in range(16, 31): - set_rule( - world.get_location(f"IDLE item number {i}", player), - lambda state: state._archipidle_location_is_accessible(player, 4) - ) + world.get_location(f"IDLE item number {i}", player).access_rule = lambda \ + state: state._archipidle_location_is_accessible(player, 4) for i in range(31, 51): - set_rule( - world.get_location(f"IDLE item number {i}", player), - lambda state: state._archipidle_location_is_accessible(player, 10) - ) + world.get_location(f"IDLE item number {i}", player).access_rule = lambda \ + state: state._archipidle_location_is_accessible(player, 10) for i in range(51, 101): - set_rule( - world.get_location(f"IDLE item number {i}", player), - lambda state: state._archipidle_location_is_accessible(player, 20) - ) + world.get_location(f"IDLE item number {i}", player).access_rule = lambda \ + state: state._archipidle_location_is_accessible(player, 20) for i in range(101, 201): - set_rule( - world.get_location(f"IDLE item number {i}", player), - lambda state: state._archipidle_location_is_accessible(player, 40) - ) + world.get_location(f"IDLE item number {i}", player).access_rule = lambda \ + state: state._archipidle_location_is_accessible(player, 40) world.completion_condition[player] =\ - lambda state:\ - state.can_reach(world.get_location("IDLE item number 200", player), "Location", player) + lambda state: state.can_reach(world.get_location("IDLE item number 200", player), "Location", player) diff --git a/worlds/archipidle/__init__.py b/worlds/archipidle/__init__.py index 2d182f31dc..f4345444ef 100644 --- a/worlds/archipidle/__init__.py +++ b/worlds/archipidle/__init__.py @@ -1,8 +1,8 @@ from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification +from worlds.AutoWorld import World, WebWorld +from datetime import datetime from .Items import item_table from .Rules import set_rules -from ..AutoWorld import World, WebWorld -from datetime import datetime class ArchipIDLEWebWorld(WebWorld): @@ -29,11 +29,10 @@ class ArchipIDLEWebWorld(WebWorld): class ArchipIDLEWorld(World): """ - An idle game which sends a check every thirty seconds, up to two hundred checks. + An idle game which sends a check every thirty to sixty seconds, up to two hundred checks. """ game = "ArchipIDLE" topology_present = False - data_version = 5 hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April web = ArchipIDLEWebWorld() @@ -56,18 +55,40 @@ class ArchipIDLEWorld(World): return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player) def create_items(self): - item_table_copy = list(item_table) - self.multiworld.random.shuffle(item_table_copy) - - item_pool = [] - for i in range(200): - item = ArchipIDLEItem( - item_table_copy[i], - ItemClassification.progression if i < 40 else ItemClassification.filler, - self.item_name_to_id[item_table_copy[i]], + item_pool = [ + ArchipIDLEItem( + item_table[0], + ItemClassification.progression, + self.item_name_to_id[item_table[0]], self.player ) - item_pool.append(item) + ] + + for i in range(40): + item_pool.append(ArchipIDLEItem( + item_table[1], + ItemClassification.progression, + self.item_name_to_id[item_table[1]], + self.player + )) + + for i in range(40): + item_pool.append(ArchipIDLEItem( + item_table[2], + ItemClassification.filler, + self.item_name_to_id[item_table[2]], + self.player + )) + + item_table_copy = list(item_table[3:]) + self.random.shuffle(item_table_copy) + for i in range(119): + item_pool.append(ArchipIDLEItem( + item_table_copy[i], + ItemClassification.progression if i < 9 else ItemClassification.filler, + self.item_name_to_id[item_table_copy[i]], + self.player + )) self.multiworld.itempool += item_pool diff --git a/worlds/checksfinder/Locations.py b/worlds/checksfinder/Locations.py index 8a2ae07b27..59a96c83ea 100644 --- a/worlds/checksfinder/Locations.py +++ b/worlds/checksfinder/Locations.py @@ -10,10 +10,6 @@ class AdvData(typing.NamedTuple): class ChecksFinderAdvancement(Location): game: str = "ChecksFinder" - def __init__(self, player: int, name: str, address: typing.Optional[int], parent): - super().__init__(player, name, address, parent) - self.event = not address - advancement_table = { "Tile 1": AdvData(81000, 'Board'), diff --git a/worlds/dkc3/Names/LocationName.py b/worlds/dkc3/Names/LocationName.py index f79a25f143..dbd63623ab 100644 --- a/worlds/dkc3/Names/LocationName.py +++ b/worlds/dkc3/Names/LocationName.py @@ -294,7 +294,7 @@ barnacle_region = "Barnacle's Island Region" blue_region = "Blue's Beach Hut Region" blizzard_region = "Bizzard's Basecamp Region" -lake_orangatanga_region = "Lake_Orangatanga" +lake_orangatanga_region = "Lake Orangatanga" kremwood_forest_region = "Kremwood Forest" cotton_top_cove_region = "Cotton-Top Cove" mekanos_region = "Mekanos" diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index dfb42bd04c..b0e153dcd2 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -201,7 +201,12 @@ class DKC3World(World): er_hint_data = {} for world_index in range(len(world_names)): for level_index in range(5): - level_region = self.multiworld.get_region(self.active_level_list[world_index * 5 + level_index], self.player) + level_id: int = world_index * 5 + level_index + + if level_id >= len(self.active_level_list): + break + + level_region = self.multiworld.get_region(self.active_level_list[level_id], self.player) for location in level_region.locations: er_hint_data[location.address] = world_names[world_index] diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index 2200729a32..ca2862113f 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -61,7 +61,7 @@ class DLCqworld(World): self.precollect_coinsanity() locations_count = len([location for location in self.multiworld.get_locations(self.player) - if not location.event]) + if not location.advancement]) items_to_exclude = [excluded_items for excluded_items in self.multiworld.precollected_items[self.player]] diff --git a/worlds/dlcquest/test/checks/world_checks.py b/worlds/dlcquest/test/checks/world_checks.py index a97093d620..cc2fa7f51a 100644 --- a/worlds/dlcquest/test/checks/world_checks.py +++ b/worlds/dlcquest/test/checks/world_checks.py @@ -10,7 +10,7 @@ def get_all_item_names(multiworld: MultiWorld) -> List[str]: def get_all_location_names(multiworld: MultiWorld) -> List[str]: - return [location.name for location in multiworld.get_locations() if not location.event] + return [location.name for location in multiworld.get_locations() if not location.advancement] def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld): @@ -38,5 +38,5 @@ def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld): def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld): - non_event_locations = [location for location in multiworld.get_locations() if not location.event] + non_event_locations = [location for location in multiworld.get_locations() if not location.advancement] tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) \ No newline at end of file diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index c434351e94..f0eef22480 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -90,7 +90,7 @@ def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typi if loc_name not in multiworld.worlds[player].location_name_to_id: raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e else: - if not location.event: + if not location.advancement: location.progress_type = LocationProgressType.EXCLUDED else: logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.") diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/Regions.py index 794a565e0a..8909c58be3 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/Regions.py @@ -110,7 +110,11 @@ def generate_rooms(world: "KDL3World", level_regions: typing.Dict[int, Region]): else: world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage - 1]], world.player).parent_region.add_exits([first_rooms[proper_stage].name]) - level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name]) + if world.options.open_world: + level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name]) + else: + world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player)\ + .parent_region.add_exits([first_rooms[0x770200 + level - 1].name]) def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict: diff --git a/worlds/ladx/LADXR/assembler.py b/worlds/ladx/LADXR/assembler.py index 6c35fac4b3..c95d4dd991 100644 --- a/worlds/ladx/LADXR/assembler.py +++ b/worlds/ladx/LADXR/assembler.py @@ -757,7 +757,7 @@ class Assembler: def const(name: str, value: int) -> None: name = name.upper() - assert name not in CONST_MAP + assert name not in CONST_MAP or CONST_MAP[name] == value CONST_MAP[name] = value diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 0406ad51f8..e87459fb11 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -65,7 +65,7 @@ from .locations.keyLocation import KeyLocation from BaseClasses import ItemClassification from ..Locations import LinksAwakeningLocation -from ..Options import TrendyGame, Palette, MusicChangeCondition +from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls # Function to generate a final rom, this patches the rom with all required patches @@ -97,7 +97,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m assembler.const("wTradeSequenceItem2", 0xDB7F) # Normally used to store that we have exchanged the trade item, we use it to store flags of which trade items we have assembler.const("wSeashellsCount", 0xDB41) 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 + assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available (and boots) assembler.const("wCustomMessage", 0xC0A0) # We store the link info in unused color dungeon flags, so it gets preserved in the savegame. @@ -243,6 +243,9 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m patches.core.quickswap(rom, 1) elif settings.quickswap == 'b': patches.core.quickswap(rom, 0) + + patches.core.addBootsControls(rom, ap_settings['boots_controls']) + world_setup = logic.world_setup diff --git a/worlds/ladx/LADXR/locations/startItem.py b/worlds/ladx/LADXR/locations/startItem.py index 95dd6ba54a..0421c1d6d8 100644 --- a/worlds/ladx/LADXR/locations/startItem.py +++ b/worlds/ladx/LADXR/locations/startItem.py @@ -10,7 +10,6 @@ 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): diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm index b19e879dc3..57771c17b3 100644 --- a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm +++ b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm @@ -51,7 +51,7 @@ GiveItemFromChest: dw ChestBow ; CHEST_BOW dw ChestWithItem ; CHEST_HOOKSHOT dw ChestWithItem ; CHEST_MAGIC_ROD - dw ChestWithItem ; CHEST_PEGASUS_BOOTS + dw Boots ; CHEST_PEGASUS_BOOTS dw ChestWithItem ; CHEST_OCARINA dw ChestWithItem ; CHEST_FEATHER dw ChestWithItem ; CHEST_SHOVEL @@ -273,6 +273,13 @@ ChestMagicPowder: ld [$DB4C], a jp ChestWithItem +Boots: + ; We use DB6D to store which tunics we have available + ; ...and the boots + ld a, [wCollectedTunics] + or $04 + ld [wCollectedTunics], a + jp ChestWithItem Flippers: ld a, $01 diff --git a/worlds/ladx/LADXR/patches/core.py b/worlds/ladx/LADXR/patches/core.py index c9f3a7c34b..f4752c82e3 100644 --- a/worlds/ladx/LADXR/patches/core.py +++ b/worlds/ladx/LADXR/patches/core.py @@ -1,9 +1,11 @@ +from .. import assembler from ..assembler import ASM from ..entranceInfo import ENTRANCE_INFO from ..roomEditor import RoomEditor, ObjectWarp, ObjectHorizontal from ..backgroundEditor import BackgroundEditor from .. import utils +from ...Options import BootsControls def bugfixWrittingWrongRoomStatus(rom): # The normal rom contains a pretty nasty bug where door closing triggers in D7/D8 can effect doors in @@ -391,7 +393,7 @@ OAMData: db $20, $20, $20, $00 ;I db $20, $28, $28, $00 ;M db $20, $30, $18, $00 ;E - + db $20, $70, $16, $00 ;D db $20, $78, $18, $00 ;E db $20, $80, $10, $00 ;A @@ -408,7 +410,7 @@ OAMData: db $68, $38, $%02x, $00 ;0 db $68, $40, $%02x, $00 ;0 db $68, $48, $%02x, $00 ;0 - + """ % ((((check_count // 100) % 10) * 2) | 0x40, (((check_count // 10) % 10) * 2) | 0x40, ((check_count % 10) * 2) | 0x40), 0x469D), fill_nop=True) # Lower line of credits roll into XX XX XX rom.patch(0x17, 0x0784, 0x082D, ASM(""" @@ -425,7 +427,7 @@ OAMData: call updateOAM ld a, [$B001] ; seconds call updateOAM - + ld a, [$DB58] ; death count high call updateOAM ld a, [$DB57] ; death count low @@ -473,7 +475,7 @@ OAMData: db $68, $18, $40, $00 ;0 db $68, $20, $40, $00 ;0 db $68, $28, $40, $00 ;0 - + """, 0x4784), fill_nop=True) # Grab the "mostly" complete A-Z font @@ -539,6 +541,97 @@ OAMData: 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) +def addBootsControls(rom, boots_controls: BootsControls): + if boots_controls == BootsControls.option_vanilla: + return + consts = { + "INVENTORY_PEGASUS_BOOTS": 0x8, + "INVENTORY_POWER_BRACELET": 0x3, + "UsePegasusBoots": 0x1705, + "J_A": (1 << 4), + "J_B": (1 << 5), + "wAButtonSlot": 0xDB01, + "wBButtonSlot": 0xDB00, + "wPegasusBootsChargeMeter": 0xC14B, + "hPressedButtonsMask": 0xCB + } + for c,v in consts.items(): + assembler.const(c, v) + + BOOTS_START_ADDR = 0x11E8 + condition = { + BootsControls.option_bracelet: """ + ld a, [hl] + ; Check if we are using the bracelet + cp INVENTORY_POWER_BRACELET + jr z, .yesBoots + """, + BootsControls.option_press_a: """ + ; Check if we are using the A slot + cp J_A + jr z, .yesBoots + ld a, [hl] + """, + BootsControls.option_press_b: """ + ; Check if we are using the B slot + cp J_B + jr z, .yesBoots + ld a, [hl] + """ + }[boots_controls.value] + + # The new code fits exactly within Nintendo's poorly space optimzied code while having more features + boots_code = assembler.ASM(""" +CheckBoots: + ; check if we own boots + ld a, [wCollectedTunics] + and $04 + ; if not, move on to the next inventory item (shield) + jr z, .out + + ; Check the B button + ld hl, wBButtonSlot + ld d, J_B + call .maybeBoots + + ; Check the A button + inc l ; l = wAButtonSlot - done this way to save a byte or two + ld d, J_A + call .maybeBoots + + ; If neither, reset charge meter and bail + xor a + ld [wPegasusBootsChargeMeter], a + jr .out + +.maybeBoots: + ; Check if we are holding this button even + ldh a, [hPressedButtonsMask] + and d + ret z + """ + # Check the special condition (also loads the current item for button into a) + + condition + + """ + ; Check if we are just using boots regularly + cp INVENTORY_PEGASUS_BOOTS + ret nz +.yesBoots: + ; We're using boots! Do so. + call UsePegasusBoots + ; If we return now we will go back into CheckBoots, we don't want that + ; We instead want to move onto the next item + ; but if we don't cleanup, the next "ret" will take us back there again + ; So we pop the return address off of the stack + pop af +.out: + """, BOOTS_START_ADDR) + + + + original_code = 'fa00dbfe08200ff0cbe6202805cd05171804afea4bc1fa01dbfe08200ff0cbe6102805cd05171804afea4bc1' + rom.patch(0, BOOTS_START_ADDR, original_code, boots_code, fill_nop=True) + def addWarpImprovements(rom, extra_warps): # Patch in a warp icon tile = utils.createTileData( \ @@ -739,4 +832,3 @@ success: exit: ret """)) - diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index c7b127ef2b..f29355f2ba 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -60,13 +60,11 @@ class LinksAwakeningLocation(Location): def __init__(self, player: int, region, ladxr_item): name = meta_to_name(ladxr_item.metadata) - - self.event = ladxr_item.event is not None - if self.event: - name = ladxr_item.event - address = None - if not self.event: + + if ladxr_item.event is not None: + name = ladxr_item.event + else: address = locations_to_id[name] super().__init__(player, name, address) self.parent_region = region diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index ec45706407..f7bf632545 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -316,6 +316,21 @@ class Overworld(Choice, LADXROption): # [Disable] no music in the whole game""", # aesthetic=True), +class BootsControls(Choice): + """ + Adds additional button to activate Pegasus Boots (does nothing if you haven't picked up your boots!) + [Vanilla] Nothing changes, you have to equip the boots to use them + [Bracelet] Holding down the button for the bracelet also activates boots (somewhat like Link to the Past) + [Press A] Holding down A activates boots + [Press B] Holding down B activates boots + """ + display_name = "Boots Controls" + option_vanilla = 0 + option_bracelet = 1 + option_press_a = 2 + option_press_b = 3 + + class LinkPalette(Choice, LADXROption): """ Sets link's palette @@ -485,5 +500,5 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = { 'music_change_condition': MusicChangeCondition, 'nag_messages': NagMessages, 'ap_title_screen': APTitleScreen, - + 'boots_controls': BootsControls, } diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index d662b526bb..6c7517f359 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -154,7 +154,7 @@ class LinksAwakeningWorld(World): # Place RAFT, other access events for region in regions: for loc in region.locations: - if loc.event: + if loc.address is None: loc.place_locked_item(self.create_event(loc.ladxr_item.event)) # Connect Windfish -> Victory diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index b749418368..25be166991 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -63,7 +63,7 @@ class LingoWorld(World): self.player_logic = LingoPlayerLogic(self) def create_regions(self): - create_regions(self, self.player_logic) + create_regions(self) def create_items(self): pool = [self.create_item(name) for name in self.player_logic.real_items] diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index 464e9a149a..5fddabd689 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -4,7 +4,6 @@ from BaseClasses import Entrance, ItemClassification, Region from .datatypes import Room, RoomAndDoor from .items import LingoItem from .locations import LingoLocation -from .player_logic import LingoPlayerLogic from .rules import lingo_can_use_entrance, make_location_lambda from .static_logic import ALL_ROOMS, PAINTINGS @@ -12,14 +11,14 @@ if TYPE_CHECKING: from . import LingoWorld -def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region: +def create_region(room: Room, world: "LingoWorld") -> Region: new_region = Region(room.name, world.player, world.multiworld) - for location in player_logic.locations_by_room.get(room.name, {}): + for location in world.player_logic.locations_by_room.get(room.name, {}): new_location = LingoLocation(world.player, location.name, location.code, new_region) - new_location.access_rule = make_location_lambda(location, world, player_logic) + new_location.access_rule = make_location_lambda(location, world) new_region.locations.append(new_location) - if location.name in player_logic.event_loc_to_item: - event_name = player_logic.event_loc_to_item[location.name] + if location.name in world.player_logic.event_loc_to_item: + event_name = world.player_logic.event_loc_to_item[location.name] event_item = LingoItem(event_name, ItemClassification.progression, None, world.player) new_location.place_locked_item(event_item) @@ -27,22 +26,21 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str, - door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic): + door: Optional[RoomAndDoor], world: "LingoWorld"): connection = Entrance(world.player, description, source_region) - connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world, player_logic) + connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world) source_region.exits.append(connection) connection.connect(target_region) if door is not None: effective_room = target_region.name if door.room is None else door.room - if door.door not in player_logic.item_by_door.get(effective_room, {}): - for region in player_logic.calculate_door_requirements(effective_room, door.door, world).rooms: + if door.door not in world.player_logic.item_by_door.get(effective_room, {}): + for region in world.player_logic.calculate_door_requirements(effective_room, door.door, world).rooms: world.multiworld.register_indirect_condition(regions[region], connection) -def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld", - player_logic: LingoPlayerLogic) -> None: +def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld") -> None: source_painting = PAINTINGS[warp_enter] target_painting = PAINTINGS[warp_exit] @@ -50,11 +48,10 @@ def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str source_region = regions[source_painting.room] entrance_name = f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)" - connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world, - player_logic) + connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world) -def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: +def create_regions(world: "LingoWorld") -> None: regions = { "Menu": Region("Menu", world.player, world.multiworld) } @@ -64,7 +61,7 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: # Instantiate all rooms as regions with their locations first. for room in ALL_ROOMS: - regions[room.name] = create_region(room, world, player_logic) + regions[room.name] = create_region(room, world) # Connect all created regions now that they exist. for room in ALL_ROOMS: @@ -80,18 +77,17 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: else: entrance_name += f" (through {room.name} - {entrance.door.door})" - connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world, - player_logic) + connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world) # Add the fake pilgrimage. connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage", - RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world, player_logic) + RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world) if early_color_hallways: regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") if painting_shuffle: - for warp_enter, warp_exit in player_logic.painting_mapping.items(): - connect_painting(regions, warp_enter, warp_exit, world, player_logic) + for warp_enter, warp_exit in world.player_logic.painting_mapping.items(): + connect_painting(regions, warp_enter, warp_exit, world) world.multiworld.regions += regions.values() diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 054c330c45..4e12938afa 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -2,61 +2,58 @@ from typing import TYPE_CHECKING from BaseClasses import CollectionState from .datatypes import RoomAndDoor -from .player_logic import AccessRequirements, LingoPlayerLogic, PlayerLocation +from .player_logic import AccessRequirements, PlayerLocation from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS if TYPE_CHECKING: from . import LingoWorld -def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld", - player_logic: LingoPlayerLogic): +def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld"): if door is None: return True effective_room = room if door.room is None else door.room - return _lingo_can_open_door(state, effective_room, door.door, world, player_logic) + return _lingo_can_open_door(state, effective_room, door.door, world) -def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld", - player_logic: LingoPlayerLogic): - return _lingo_can_satisfy_requirements(state, location.access, world, player_logic) +def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld"): + return _lingo_can_satisfy_requirements(state, location.access, world) -def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): +def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): satisfied_count = 0 - for access_req in player_logic.mastery_reqs: - if _lingo_can_satisfy_requirements(state, access_req, world, player_logic): + for access_req in world.player_logic.mastery_reqs: + if _lingo_can_satisfy_requirements(state, access_req, world): satisfied_count += 1 return satisfied_count >= world.options.mastery_achievements.value -def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): +def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): counted_panels = 0 state.update_reachable_regions(world.player) for region in state.reachable_regions[world.player]: - for access_req, panel_count in player_logic.counting_panel_reqs.get(region.name, []): - if _lingo_can_satisfy_requirements(state, access_req, world, player_logic): + for access_req, panel_count in world.player_logic.counting_panel_reqs.get(region.name, []): + if _lingo_can_satisfy_requirements(state, access_req, world): counted_panels += panel_count if counted_panels >= world.options.level_2_requirement.value - 1: return True # THE MASTER has to be handled separately, because it has special access rules. if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\ - and lingo_can_use_mastery_location(state, world, player_logic): + and lingo_can_use_mastery_location(state, world): counted_panels += 1 if counted_panels >= world.options.level_2_requirement.value - 1: return True return False -def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld", - player_logic: LingoPlayerLogic): +def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld"): for req_room in access.rooms: if not state.can_reach(req_room, "Region", world.player): return False for req_door in access.doors: - if not _lingo_can_open_door(state, req_door.room, req_door.door, world, player_logic): + if not _lingo_can_open_door(state, req_door.room, req_door.door, world): return False if len(access.colors) > 0 and world.options.shuffle_colors: @@ -67,15 +64,14 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir return True -def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld", - player_logic: LingoPlayerLogic): +def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld"): """ Determines whether a door can be opened """ - if door not in player_logic.item_by_door.get(room, {}): - return _lingo_can_satisfy_requirements(state, player_logic.door_reqs[room][door], world, player_logic) + if door not in world.player_logic.item_by_door.get(room, {}): + return _lingo_can_satisfy_requirements(state, world.player_logic.door_reqs[room][door], world) - item_name = player_logic.item_by_door[room][door] + item_name = world.player_logic.item_by_door[room][door] if item_name in PROGRESSIVE_ITEMS: progression = PROGRESSION_BY_ROOM[room][door] return state.has(item_name, world.player, progression.index) @@ -83,12 +79,12 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "L return state.has(item_name, world.player) -def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): - if location.name == player_logic.mastery_location: - return lambda state: lingo_can_use_mastery_location(state, world, player_logic) +def make_location_lambda(location: PlayerLocation, world: "LingoWorld"): + if location.name == world.player_logic.mastery_location: + return lambda state: lingo_can_use_mastery_location(state, world) if world.options.level_2_requirement > 1\ - and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location): - return lambda state: lingo_can_use_level_2_location(state, world, player_logic) + and (location.name == "Second Room - ANOTHER TRY" or location.name == world.player_logic.level_2_location): + return lambda state: lingo_can_use_level_2_location(state, world) - return lambda state: lingo_can_use_location(state, location, world, player_logic) + return lambda state: lingo_can_use_location(state, location, world) diff --git a/worlds/meritous/Locations.py b/worlds/meritous/Locations.py index 1893b8520e..690c757eff 100644 --- a/worlds/meritous/Locations.py +++ b/worlds/meritous/Locations.py @@ -9,11 +9,6 @@ from BaseClasses import Location class MeritousLocation(Location): game: str = "Meritous" - def __init__(self, player: int, name: str = '', address: int = None, parent=None): - super(MeritousLocation, self).__init__(player, name, address, parent) - if "Wervyn Anixil" in name or "Defeat" in name: - self.event = True - offset = 593_000 diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 20bb8deceb..68e4ad5912 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -35,13 +35,14 @@ class MuseDashCollections: "Rush-Hour", "Find this Month's Featured Playlist", "PeroPero in the Universe", - "umpopoff" + "umpopoff", + "P E R O P E R O Brother Dance", ] REMOVED_SONGS = [ "CHAOS Glitch", "FM 17314 SUGAR RADIO", - "Yume Ou Mono Yo Secret" + "Yume Ou Mono Yo Secret", ] album_items: Dict[str, AlbumData] = {} @@ -57,6 +58,7 @@ class MuseDashCollections: "Chromatic Aberration Trap": STARTING_CODE + 5, "Background Freeze Trap": STARTING_CODE + 6, "Gray Scale Trap": STARTING_CODE + 7, + "Focus Line Trap": STARTING_CODE + 10, } sfx_trap_items: Dict[str, int] = { @@ -64,7 +66,19 @@ class MuseDashCollections: "Error SFX Trap": STARTING_CODE + 9, } - item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items) + filler_items: Dict[str, int] = { + "Great To Perfect (10 Pack)": STARTING_CODE + 30, + "Miss To Great (5 Pack)": STARTING_CODE + 31, + "Extra Life": STARTING_CODE + 32, + } + + filler_item_weights: Dict[str, int] = { + "Great To Perfect (10 Pack)": 10, + "Miss To Great (5 Pack)": 3, + "Extra Life": 1, + } + + item_names_to_id: ChainMap = ChainMap({}, filler_items, sfx_trap_items, vfx_trap_items) location_names_to_id: ChainMap = ChainMap(song_locations, album_locations) def __init__(self) -> None: diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 620c1968bd..0a8beba37b 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -518,7 +518,7 @@ Haunted Dance|43-48|MD Plus Project|False|6|9|11| Hey Vincent.|43-49|MD Plus Project|True|6|8|10| Meteor feat. TEA|43-50|MD Plus Project|True|3|6|9| Narcissism Angel|43-51|MD Plus Project|True|1|3|6| -AlterLuna|43-52|MD Plus Project|True|6|8|11| +AlterLuna|43-52|MD Plus Project|True|6|8|11|12 Niki Tousen|43-53|MD Plus Project|True|6|8|10|11 Rettou Joutou|70-0|Rin Len's Mirrorland|False|4|7|9| Telecaster B-Boy|70-1|Rin Len's Mirrorland|False|5|7|10| @@ -537,4 +537,11 @@ Ruler Of My Heart VIVINOS|71-1|Valentine Stage|False|2|4|6| Reality Show|71-2|Valentine Stage|False|5|7|10| SIG feat.Tobokegao|71-3|Valentine Stage|True|3|6|8| Rose Love|71-4|Valentine Stage|True|2|4|7| -Euphoria|71-5|Valentine Stage|True|1|3|6| \ No newline at end of file +Euphoria|71-5|Valentine Stage|True|1|3|6| +P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|False|0|?|0| +PA PPA PANIC|72-1|Legends of Muse Warriors|False|4|8|10| +How To Make Music Game Song!|72-2|Legends of Muse Warriors|False|6|8|10|11 +Re Re|72-3|Legends of Muse Warriors|False|7|9|11|12 +Marmalade Twins|72-4|Legends of Muse Warriors|False|5|8|10| +DOMINATOR|72-5|Legends of Muse Warriors|False|7|9|11| +Teshikani TESHiKANi|72-6|Legends of Muse Warriors|False|5|7|9| diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index 26ad5ff5d9..b695395135 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -4,11 +4,13 @@ from dataclasses import dataclass from .MuseDashCollection import MuseDashCollections + class AllowJustAsPlannedDLCSongs(Toggle): """Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs. Note: The [Just As Planned] DLC contains all [Muse Plus] songs.""" display_name = "Allow [Muse Plus] DLC Songs" + class DLCMusicPacks(OptionSet): """Which non-[Muse Plus] DLC packs can be chosen as randomised songs.""" display_name = "DLC Packs" @@ -101,20 +103,10 @@ class GradeNeeded(Choice): default = 0 -class AdditionalItemPercentage(Range): - """The percentage of songs that will have 2 items instead of 1 when completing them. - - Starting Songs will always have 2 items. - - Locations will be filled with duplicate songs if there are not enough items. - """ - display_name = "Additional Item %" - range_start = 50 - default = 80 - range_end = 100 - - class MusicSheetCountPercentage(Range): - """Collecting enough Music Sheets will unlock the goal song needed for completion. - This option controls how many are in the item pool, based on the total number of songs.""" + """Controls how many music sheets are added to the pool based on the number of songs, including starting songs. + Higher numbers leads to more consistent game lengths, but will cause individual music sheets to be less important. + """ range_start = 10 range_end = 40 default = 20 @@ -175,7 +167,6 @@ class MuseDashOptions(PerGameCommonOptions): streamer_mode_enabled: StreamerModeEnabled starting_song_count: StartingSongs additional_song_count: AdditionalSongs - additional_item_percentage: AdditionalItemPercentage song_difficulty_mode: DifficultyMode song_difficulty_min: DifficultyModeOverrideMin song_difficulty_max: DifficultyModeOverrideMax diff --git a/worlds/musedash/Presets.py b/worlds/musedash/Presets.py index 6459111802..8dd8507d9b 100644 --- a/worlds/musedash/Presets.py +++ b/worlds/musedash/Presets.py @@ -6,7 +6,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = { "allow_just_as_planned_dlc_songs": False, "starting_song_count": 5, "additional_song_count": 34, - "additional_item_percentage": 80, "music_sheet_count_percentage": 20, "music_sheet_win_count_percentage": 90, }, @@ -15,7 +14,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = { "allow_just_as_planned_dlc_songs": True, "starting_song_count": 5, "additional_song_count": 34, - "additional_item_percentage": 80, "music_sheet_count_percentage": 20, "music_sheet_win_count_percentage": 90, }, @@ -24,7 +22,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = { "allow_just_as_planned_dlc_songs": True, "starting_song_count": 8, "additional_song_count": 91, - "additional_item_percentage": 80, "music_sheet_count_percentage": 20, "music_sheet_win_count_percentage": 90, }, diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index af2d4cc207..1c009bfaee 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -57,6 +57,8 @@ class MuseDashWorld(World): # Necessary Data md_collection = MuseDashCollections() + filler_item_names = list(md_collection.filler_item_weights.keys()) + filler_item_weights = list(md_collection.filler_item_weights.values()) item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()} location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()} @@ -70,7 +72,7 @@ class MuseDashWorld(World): def generate_early(self): dlc_songs = {key for key in self.options.dlc_packs.value} - if (self.options.allow_just_as_planned_dlc_songs.value): + if self.options.allow_just_as_planned_dlc_songs.value: dlc_songs.add(self.md_collection.MUSE_PLUS_DLC) streamer_mode = self.options.streamer_mode_enabled @@ -84,7 +86,7 @@ class MuseDashWorld(World): while True: # In most cases this should only need to run once available_song_keys = self.md_collection.get_songs_with_settings( - dlc_songs, streamer_mode, lower_diff_threshold, higher_diff_threshold) + dlc_songs, bool(streamer_mode.value), lower_diff_threshold, higher_diff_threshold) available_song_keys = self.handle_plando(available_song_keys) @@ -161,19 +163,17 @@ class MuseDashWorld(World): break self.included_songs.append(available_song_keys.pop()) - self.location_count = len(self.starting_songs) + len(self.included_songs) - location_multiplier = 1 + (self.get_additional_item_percentage() / 100.0) - self.location_count = floor(self.location_count * location_multiplier) - - minimum_location_count = len(self.included_songs) + self.get_music_sheet_count() - if self.location_count < minimum_location_count: - self.location_count = minimum_location_count + self.location_count = 2 * (len(self.starting_songs) + len(self.included_songs)) def create_item(self, name: str) -> Item: if name == self.md_collection.MUSIC_SHEET_NAME: return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing, self.md_collection.MUSIC_SHEET_CODE, self.player) + filler = self.md_collection.filler_items.get(name) + if filler: + return MuseDashFixedItem(name, ItemClassification.filler, filler, self.player) + trap = self.md_collection.vfx_trap_items.get(name) if trap: return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player) @@ -189,6 +189,9 @@ class MuseDashWorld(World): song = self.md_collection.song_items.get(name) return MuseDashSongItem(name, self.player, song) + def get_filler_item_name(self) -> str: + return self.random.choices(self.filler_item_names, self.filler_item_weights)[0] + def create_items(self) -> None: song_keys_in_pool = self.included_songs.copy() @@ -199,8 +202,13 @@ class MuseDashWorld(World): for _ in range(0, item_count): self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME)) - # Then add all traps - trap_count = self.get_trap_count() + # Then add 1 copy of every song + item_count += len(self.included_songs) + for song in self.included_songs: + self.multiworld.itempool.append(self.create_item(song)) + + # Then add all traps, making sure we don't over fill + trap_count = min(self.location_count - item_count, self.get_trap_count()) trap_list = self.get_available_traps() if len(trap_list) > 0 and trap_count > 0: for _ in range(0, trap_count): @@ -209,23 +217,38 @@ class MuseDashWorld(World): item_count += trap_count - # Next fill all remaining slots with song items - needed_item_count = self.location_count - while item_count < needed_item_count: - # If we have more items needed than keys, just iterate the list and add them all - if len(song_keys_in_pool) <= needed_item_count - item_count: - for key in song_keys_in_pool: - self.multiworld.itempool.append(self.create_item(key)) + # At this point, if a player is using traps, it's possible that they have filled all locations + items_left = self.location_count - item_count + if items_left <= 0: + return - item_count += len(song_keys_in_pool) - continue + # When it comes to filling remaining spaces, we have 2 options. A useless filler or additional songs. + # First fill 50% with the filler. The rest is to be duplicate songs. + filler_count = floor(0.5 * items_left) + items_left -= filler_count - # Otherwise add a random assortment of songs - self.random.shuffle(song_keys_in_pool) - for i in range(0, needed_item_count - item_count): - self.multiworld.itempool.append(self.create_item(song_keys_in_pool[i])) + for _ in range(0, filler_count): + self.multiworld.itempool.append(self.create_item(self.get_filler_item_name())) - item_count = needed_item_count + # All remaining spots are filled with duplicate songs. Duplicates are set to useful instead of progression + # to cut down on the number of progression items that Muse Dash puts into the pool. + + # This is for the extraordinary case of needing to fill a lot of items. + while items_left > len(song_keys_in_pool): + for key in song_keys_in_pool: + item = self.create_item(key) + item.classification = ItemClassification.useful + self.multiworld.itempool.append(item) + + items_left -= len(song_keys_in_pool) + continue + + # Otherwise add a random assortment of songs + self.random.shuffle(song_keys_in_pool) + for i in range(0, items_left): + item = self.create_item(song_keys_in_pool[i]) + item.classification = ItemClassification.useful + self.multiworld.itempool.append(item) def create_regions(self) -> None: menu_region = Region("Menu", self.player, self.multiworld) @@ -245,8 +268,6 @@ class MuseDashWorld(World): self.random.shuffle(included_song_copy) all_selected_locations.extend(included_song_copy) - two_item_location_count = self.location_count - len(all_selected_locations) - # Make a region per song/album, then adds 1-2 item locations to them for i in range(0, len(all_selected_locations)): name = all_selected_locations[i] @@ -254,10 +275,11 @@ class MuseDashWorld(World): self.multiworld.regions.append(region) song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player)) - # Up to 2 Locations are defined per song - region.add_locations({name + "-0": self.md_collection.song_locations[name + "-0"]}, MuseDashLocation) - if i < two_item_location_count: - region.add_locations({name + "-1": self.md_collection.song_locations[name + "-1"]}, MuseDashLocation) + # Muse Dash requires 2 locations per song to be *interesting*. Balanced out by filler. + region.add_locations({ + name + "-0": self.md_collection.song_locations[name + "-0"], + name + "-1": self.md_collection.song_locations[name + "-1"] + }, MuseDashLocation) def set_rules(self) -> None: self.multiworld.completion_condition[self.player] = lambda state: \ @@ -276,19 +298,14 @@ class MuseDashWorld(World): return trap_list - def get_additional_item_percentage(self) -> int: - trap_count = self.options.trap_count_percentage.value - song_count = self.options.music_sheet_count_percentage.value - return max(trap_count + song_count, self.options.additional_item_percentage.value) - def get_trap_count(self) -> int: multiplier = self.options.trap_count_percentage.value / 100.0 - trap_count = (len(self.starting_songs) * 2) + len(self.included_songs) + trap_count = len(self.starting_songs) + len(self.included_songs) return max(0, floor(trap_count * multiplier)) def get_music_sheet_count(self) -> int: multiplier = self.options.music_sheet_count_percentage.value / 100.0 - song_count = (len(self.starting_songs) * 2) + len(self.included_songs) + song_count = len(self.starting_songs) + len(self.included_songs) return max(1, floor(song_count * multiplier)) def get_music_sheet_win_count(self) -> int: @@ -329,5 +346,4 @@ class MuseDashWorld(World): "deathLink": self.options.death_link.value, "musicSheetWinCount": self.get_music_sheet_win_count(), "gradeNeeded": self.options.grade_needed.value, - "hasFiller": True, } diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py index af3469aa08..89214d3f0f 100644 --- a/worlds/musedash/test/TestDifficultyRanges.py +++ b/worlds/musedash/test/TestDifficultyRanges.py @@ -5,9 +5,9 @@ class DifficultyRanges(MuseDashTestBase): def test_all_difficulty_ranges(self) -> None: muse_dash_world = self.multiworld.worlds[1] dlc_set = {x for x in muse_dash_world.md_collection.DLC} - difficulty_choice = self.multiworld.song_difficulty_mode[1] - difficulty_min = self.multiworld.song_difficulty_min[1] - difficulty_max = self.multiworld.song_difficulty_max[1] + difficulty_choice = muse_dash_world.options.song_difficulty_mode + difficulty_min = muse_dash_world.options.song_difficulty_min + difficulty_max = muse_dash_world.options.song_difficulty_max def test_range(inputRange, lower, upper): self.assertEqual(inputRange[0], lower) @@ -66,9 +66,9 @@ class DifficultyRanges(MuseDashTestBase): for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES: song = muse_dash_world.md_collection.song_items[song_name] - # umpopoff is a one time weird song. Its currently the only song in the game - # with non-standard difficulties and also doesn't have 3 or more difficulties. - if song_name == 'umpopoff': + # Some songs are weird and have less than the usual 3 difficulties. + # So this override is to avoid failing on these songs. + if song_name in ("umpopoff", "P E R O P E R O Brother Dance"): self.assertTrue(song.easy is None and song.hard is not None and song.master is None, f"Song '{song_name}' difficulty not set when it should be.") else: diff --git a/worlds/musedash/test/__init__.py b/worlds/musedash/test/__init__.py index 818fd357cd..c77f9f6a06 100644 --- a/worlds/musedash/test/__init__.py +++ b/worlds/musedash/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class MuseDashTestBase(WorldTestBase): diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py index b8f8e4ae83..43078c5e43 100644 --- a/worlds/noita/__init__.py +++ b/worlds/noita/__init__.py @@ -38,7 +38,7 @@ class NoitaWorld(World): web = NoitaWeb() - def generate_early(self): + def generate_early(self) -> None: if not self.multiworld.get_player_name(self.player).isascii(): raise Exception("Noita yaml's slot name has invalid character(s).") diff --git a/worlds/noita/locations.py b/worlds/noita/locations.py index afe16c54e4..cf91ba44fb 100644 --- a/worlds/noita/locations.py +++ b/worlds/noita/locations.py @@ -12,7 +12,7 @@ class NoitaLocation(Location): class LocationData(NamedTuple): id: int flag: int = 0 - ltype: Optional[str] = "shop" + ltype: str = "shop" class LocationFlag(IntEnum): diff --git a/worlds/noita/regions.py b/worlds/noita/regions.py index 6a9c867723..3b7fd3962c 100644 --- a/worlds/noita/regions.py +++ b/worlds/noita/regions.py @@ -41,7 +41,7 @@ def create_regions(world: "NoitaWorld") -> Dict[str, Region]: # An "Entrance" is really just a connection between two regions -def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]): +def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]) -> Entrance: entrance = Entrance(player, f"From {source} To {destination}", regions[source]) entrance.connect(regions[destination]) return entrance diff --git a/worlds/noita/rules.py b/worlds/noita/rules.py index 95039bee46..65871a804e 100644 --- a/worlds/noita/rules.py +++ b/worlds/noita/rules.py @@ -68,7 +68,7 @@ def has_orb_count(state: CollectionState, player: int, amount: int) -> bool: return state.count("Orb", player) >= amount -def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]): +def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]) -> None: for shop_location in shop_locations: location = world.multiworld.get_location(shop_location, world.player) GenericRules.forbid_items_for_player(location, forbidden_items, world.player) @@ -129,7 +129,7 @@ def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None: ) -def biome_unlock_conditions(world: "NoitaWorld"): +def biome_unlock_conditions(world: "NoitaWorld") -> None: lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances diff --git a/worlds/oot/Location.py b/worlds/oot/Location.py index 3f7d75517e..f924dd048d 100644 --- a/worlds/oot/Location.py +++ b/worlds/oot/Location.py @@ -44,14 +44,11 @@ class OOTLocation(Location): self.vanilla_item = vanilla_item if filter_tags is None: self.filter_tags = None - else: + else: self.filter_tags = list(filter_tags) self.never = False # no idea what this does self.disabled = DisableType.ENABLED - if type == 'Event': - self.event = True - @property def dungeon(self): return self.parent_region.dungeon diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 303529c945..d9ee63850e 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -717,7 +717,6 @@ class OOTWorld(World): item = self.create_item(name, allow_arbitrary_name=True) self.multiworld.push_item(location, item, collect=False) location.locked = True - location.event = True if name not in item_table: location.internal = True return item @@ -842,7 +841,7 @@ class OOTWorld(World): all_state.sweep_for_events(locations=all_locations) reachable = self.multiworld.get_reachable_locations(all_state, self.player) unreachable = [loc for loc in all_locations if - (loc.internal or loc.type == 'Drop') and loc.event and loc.locked and loc not in reachable] + (loc.internal or loc.type == 'Drop') and loc.address is None and loc.locked and loc not in reachable] for loc in unreachable: loc.parent_region.locations.remove(loc) # Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool. @@ -972,7 +971,6 @@ class OOTWorld(World): for location in song_locations: location.item = None location.locked = False - location.event = False else: break diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index da0e189089..633b624b84 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -115,8 +115,6 @@ class Overcooked2World(World): region, ) - location.event = is_event - if priority: location.progress_type = LocationProgressType.PRIORITY else: diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 3b9f90270d..0be48261cd 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -98,7 +98,7 @@ LEGENDARY_NAMES = { "Registeel": "REGISTEEL", "Mew": "MEW", "Deoxys": "DEOXYS", - "Ho-oh": "HO_OH", + "Ho-Oh": "HO_OH", "Lugia": "LUGIA", } diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py index 786740a9e4..aa923d7ef0 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -741,7 +741,7 @@ def _init() -> None: ("SPECIES_PUPITAR", "Pupitar", 247), ("SPECIES_TYRANITAR", "Tyranitar", 248), ("SPECIES_LUGIA", "Lugia", 249), - ("SPECIES_HO_OH", "Ho-oh", 250), + ("SPECIES_HO_OH", "Ho-Oh", 250), ("SPECIES_CELEBI", "Celebi", 251), ("SPECIES_TREECKO", "Treecko", 252), ("SPECIES_GROVYLE", "Grovyle", 253), diff --git a/worlds/pokemon_emerald/data/locations.json b/worlds/pokemon_emerald/data/locations.json index 6affdf4146..55ef15d871 100644 --- a/worlds/pokemon_emerald/data/locations.json +++ b/worlds/pokemon_emerald/data/locations.json @@ -2877,7 +2877,7 @@ "tags": ["Pokedex"] }, "POKEDEX_REWARD_250": { - "label": "Pokedex - Ho-oh", + "label": "Pokedex - Ho-Oh", "tags": ["Pokedex"] }, "POKEDEX_REWARD_251": { diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index 69ce47f207..978f9d3dcd 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -246,7 +246,7 @@ class AllowedLegendaryHuntEncounters(OptionSet): "Regirock" "Registeel" "Regice" - "Ho-oh" + "Ho-Oh" "Lugia" "Deoxys" "Mew" @@ -261,7 +261,7 @@ class AllowedLegendaryHuntEncounters(OptionSet): "Regirock", "Registeel", "Regice", - "Ho-oh", + "Ho-Oh", "Lugia", "Deoxys", "Mew", diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index 059e21b749..0b5c6b79c0 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -56,7 +56,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: "Registeel": "REGISTEEL", "Mew": "MEW", "Deoxys": "DEOXYS", - "Ho-oh": "HO_OH", + "Ho-Oh": "HO_OH", "Lugia": "LUGIA", }.items() if name in world.options.allowed_legendary_hunt_encounters.value diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index beb2010b58..79028a68b1 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -18,7 +18,7 @@ from .options import pokemon_rb_options from .rom_addresses import rom_addresses from .text import encode_text from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch -from .pokemon import process_pokemon_data, process_move_data +from .pokemon import process_pokemon_data, process_move_data, verify_hm_moves from .encounters import process_pokemon_locations, process_trainer_data from .rules import set_rules from .level_scaling import level_scaling @@ -265,7 +265,6 @@ class PokemonRedBlueWorld(World): state = sweep_from_pool(multiworld.state, progitempool + unplaced_items) if (not item.advancement) or state.can_reach(loc, "Location", loc.player): multiworld.push_item(loc, item, False) - loc.event = item.advancement fill_locations.remove(loc) break else: @@ -279,12 +278,12 @@ class PokemonRedBlueWorld(World): def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations): if not self.multiworld.badgesanity[self.player]: # Door Shuffle options besides Simple place badges during door shuffling - if not self.multiworld.door_shuffle[self.player] not in ("off", "simple"): + if self.multiworld.door_shuffle[self.player] in ("off", "simple"): badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player] for badge in badges: self.multiworld.itempool.remove(badge) progitempool.remove(badge) - for _ in range(5): + for attempt in range(6): badgelocs = [ self.multiworld.get_location(loc, self.player) for loc in [ "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", @@ -293,6 +292,12 @@ class PokemonRedBlueWorld(World): "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize" ] if self.multiworld.get_location(loc, self.player).item is None] state = self.multiworld.get_all_state(False) + # Give it two tries to place badges with wild Pokemon and learnsets as-is. + # If it can't, then try with all Pokemon collected, and we'll try to fix HM move availability after. + if attempt > 1: + for mon in poke_data.pokemon_data.keys(): + state.collect(self.create_item(mon), True) + state.sweep_for_events() self.multiworld.random.shuffle(badges) self.multiworld.random.shuffle(badgelocs) badgelocs_copy = badgelocs.copy() @@ -312,6 +317,7 @@ class PokemonRedBlueWorld(World): break else: raise FillError(f"Failed to place badges for player {self.player}") + verify_hm_moves(self.multiworld, self, self.player) if self.multiworld.key_items_only[self.player]: return @@ -355,97 +361,14 @@ class PokemonRedBlueWorld(World): for location in self.multiworld.get_locations(self.player): if location.name in locs: location.show_in_spoiler = False - - def intervene(move, test_state): - move_bit = pow(2, poke_data.hm_moves.index(move) + 2) - viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit] - if self.multiworld.randomize_wild_pokemon[self.player] and viable_mons: - accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if - loc.type == "Wild Encounter"] - - def number_of_zones(mon): - zones = set() - for loc in [slot for slot in accessible_slots if slot.item.name == mon]: - zones.add(loc.name.split(" - ")[0]) - return len(zones) - - placed_mons = [slot.item.name for slot in accessible_slots] - - if self.multiworld.area_1_to_1_mapping[self.player]: - placed_mons.sort(key=lambda i: number_of_zones(i)) - else: - # this sort method doesn't work if you reference the same list being sorted in the lambda - placed_mons_copy = placed_mons.copy() - placed_mons.sort(key=lambda i: placed_mons_copy.count(i)) - - placed_mon = placed_mons.pop() - replace_mon = self.multiworld.random.choice(viable_mons) - replace_slot = self.multiworld.random.choice([slot for slot in accessible_slots if slot.item.name - == placed_mon]) - if self.multiworld.area_1_to_1_mapping[self.player]: - zone = " - ".join(replace_slot.name.split(" - ")[:-1]) - replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name - == placed_mon] - for replace_slot in replace_slots: - replace_slot.item = self.create_item(replace_mon) - else: - replace_slot.item = self.create_item(replace_mon) - else: - tms_hms = self.local_tms + poke_data.hm_moves - flag = tms_hms.index(move) - mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, self.player)] - self.multiworld.random.shuffle(mon_list) - mon_list.sort(key=lambda mon: self.local_move_data[move]["type"] not in - [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]]) - for mon in mon_list: - if test_state.has(mon, self.player): - self.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8) - break - - last_intervene = None - while True: - intervene_move = None - test_state = self.multiworld.get_all_state(False) - if not logic.can_learn_hm(test_state, "Surf", self.player): - intervene_move = "Surf" - elif not logic.can_learn_hm(test_state, "Strength", self.player): - intervene_move = "Strength" - # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off, - # as you will require cut to access celadon gyn - elif ((not logic.can_learn_hm(test_state, "Cut", self.player)) and - (self.multiworld.accessibility[self.player] != "minimal" or ((not - self.multiworld.badgesanity[self.player]) and max( - self.multiworld.elite_four_badges_condition[self.player], - self.multiworld.route_22_gate_condition[self.player], - self.multiworld.victory_road_condition[self.player]) - > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))): - intervene_move = "Cut" - elif ((not logic.can_learn_hm(test_state, "Flash", self.player)) - and self.multiworld.dark_rock_tunnel_logic[self.player] - and (self.multiworld.accessibility[self.player] != "minimal" - or self.multiworld.door_shuffle[self.player])): - intervene_move = "Flash" - # If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps - # as reachable, and if on no door shuffle or simple, fly is simply never necessary. - # We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been - # considered in door shuffle. - elif ((not logic.can_learn_hm(test_state, "Fly", self.player)) - and self.multiworld.door_shuffle[self.player] not in - ("off", "simple") and [self.fly_map, self.town_map_fly_map] != ["Pallet Town", "Pallet Town"]): - intervene_move = "Fly" - if intervene_move: - if intervene_move == last_intervene: - raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {self.player}") - intervene(intervene_move, test_state) - last_intervene = intervene_move - else: - break + verify_hm_moves(self.multiworld, self, self.player) # Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not # fail. Re-use test_state from previous final loop. + all_state = self.multiworld.get_all_state(False) evolutions_region = self.multiworld.get_region("Evolution", self.player) for location in evolutions_region.locations.copy(): - if not test_state.can_reach(location, player=self.player): + if not all_state.can_reach(location, player=self.player): evolutions_region.locations.remove(location) if self.multiworld.old_man[self.player] == "early_parcel": diff --git a/worlds/pokemon_rb/client.py b/worlds/pokemon_rb/client.py index 8ed21443e0..97ca126476 100644 --- a/worlds/pokemon_rb/client.py +++ b/worlds/pokemon_rb/client.py @@ -31,7 +31,7 @@ DATA_LOCATIONS = { "CrashCheck2": (0x1617, 1), # Progressive keys, should never be above 10. Just before Dexsanity flags. "CrashCheck3": (0x1A70, 1), - # Route 18 script value. Should never be above 2. Just before Hidden items flags. + # Route 18 Gate script value. Should never be above 3. Just before Hidden items flags. "CrashCheck4": (0x16DD, 1), } @@ -116,7 +116,7 @@ class PokemonRBClient(BizHawkClient): or data["CrashCheck1"][0] & 0xF0 or data["CrashCheck1"][1] & 0xFF or data["CrashCheck2"][0] or data["CrashCheck3"][0] > 10 - or data["CrashCheck4"][0] > 2): + or data["CrashCheck4"][0] > 3): # Should mean game crashed logger.warning("Pokémon Red/Blue game may have crashed. Disconnecting from server.") self.game_state = False diff --git a/worlds/pokemon_rb/encounters.py b/worlds/pokemon_rb/encounters.py index a426374c2e..6d1762b0ca 100644 --- a/worlds/pokemon_rb/encounters.py +++ b/worlds/pokemon_rb/encounters.py @@ -197,7 +197,6 @@ def process_pokemon_locations(self): mon = randomize_pokemon(self, original_mon, mons_list, 2, self.multiworld.random) placed_mons[mon] += 1 location.item = self.create_item(mon) - location.event = True location.locked = True location.item.location = location locations.append(location) @@ -269,7 +268,6 @@ def process_pokemon_locations(self): for slot in encounter_slots: location = self.multiworld.get_location(slot.name, self.player) location.item = self.create_item(slot.original_item) - location.event = True location.locked = True location.item.location = location placed_mons[location.item.name] += 1 \ No newline at end of file diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index abaa58fcf9..b7b7e533a5 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -175,7 +175,7 @@ location_data = [ LocationData("Route 2-SE", "South Item", "Moon Stone", rom_addresses["Missable_Route_2_Item_1"], Missable(25)), LocationData("Route 2-SE", "North Item", "HP Up", rom_addresses["Missable_Route_2_Item_2"], Missable(26)), - LocationData("Route 4-E", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)), + LocationData("Route 4-C", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)), LocationData("Route 9", "Item", "TM30 Teleport", rom_addresses["Missable_Route_9_Item"], Missable(28)), LocationData("Route 12-N", "Island Item", "TM16 Pay Day", rom_addresses["Missable_Route_12_Item_1"], Missable(30)), LocationData("Route 12-Grass", "Item Behind Cuttable Tree", "Iron", rom_addresses["Missable_Route_12_Item_2"], Missable(31)), diff --git a/worlds/pokemon_rb/pokemon.py b/worlds/pokemon_rb/pokemon.py index 267f424ca8..28098a2c53 100644 --- a/worlds/pokemon_rb/pokemon.py +++ b/worlds/pokemon_rb/pokemon.py @@ -1,5 +1,5 @@ from copy import deepcopy -from . import poke_data +from . import poke_data, logic from .rom_addresses import rom_addresses @@ -135,7 +135,6 @@ def process_pokemon_data(self): learnsets = deepcopy(poke_data.learnsets) tms_hms = self.local_tms + poke_data.hm_moves - compat_hms = set() for mon, mon_data in local_poke_data.items(): @@ -323,19 +322,20 @@ def process_pokemon_data(self): mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) hm_verify = ["Surf", "Strength"] - if self.multiworld.accessibility[self.player] == "locations" or ((not + if self.multiworld.accessibility[self.player] != "minimal" or ((not self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_badges_condition[self.player], self.multiworld.route_22_gate_condition[self.player], self.multiworld.victory_road_condition[self.player]) > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")): hm_verify += ["Cut"] - if self.multiworld.accessibility[self.player] == "locations" or (not + if self.multiworld.accessibility[self.player] != "minimal" or (not self.multiworld.dark_rock_tunnel_logic[self.player]) and ((self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player]) or self.multiworld.door_shuffle[self.player]): hm_verify += ["Flash"] - # Fly does not need to be verified. Full/Insanity door shuffle connects reachable regions to unreachable regions, - # so if Fly is available and can be learned, the towns you can fly to would be reachable, but if no Pokémon can - # learn it this simply would not occur + # Fly does not need to be verified. Full/Insanity/Decoupled door shuffle connects reachable regions to unreachable + # regions, so if Fly is available and can be learned, the towns you can fly to would be considered reachable for + # door shuffle purposes, but if no Pokémon can learn it, that connection would just be out of logic and it would + # ensure connections to those towns. for hm_move in hm_verify: if hm_move not in compat_hms: @@ -346,3 +346,90 @@ def process_pokemon_data(self): self.local_poke_data = local_poke_data self.learnsets = learnsets + + +def verify_hm_moves(multiworld, world, player): + def intervene(move, test_state): + move_bit = pow(2, poke_data.hm_moves.index(move) + 2) + viable_mons = [mon for mon in world.local_poke_data if world.local_poke_data[mon]["tms"][6] & move_bit] + if multiworld.randomize_wild_pokemon[player] and viable_mons: + accessible_slots = [loc for loc in multiworld.get_reachable_locations(test_state, player) if + loc.type == "Wild Encounter"] + + def number_of_zones(mon): + zones = set() + for loc in [slot for slot in accessible_slots if slot.item.name == mon]: + zones.add(loc.name.split(" - ")[0]) + return len(zones) + + placed_mons = [slot.item.name for slot in accessible_slots] + + if multiworld.area_1_to_1_mapping[player]: + placed_mons.sort(key=lambda i: number_of_zones(i)) + else: + # this sort method doesn't work if you reference the same list being sorted in the lambda + placed_mons_copy = placed_mons.copy() + placed_mons.sort(key=lambda i: placed_mons_copy.count(i)) + + placed_mon = placed_mons.pop() + replace_mon = multiworld.random.choice(viable_mons) + replace_slot = multiworld.random.choice([slot for slot in accessible_slots if slot.item.name + == placed_mon]) + if multiworld.area_1_to_1_mapping[player]: + zone = " - ".join(replace_slot.name.split(" - ")[:-1]) + replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name + == placed_mon] + for replace_slot in replace_slots: + replace_slot.item = world.create_item(replace_mon) + else: + replace_slot.item = world.create_item(replace_mon) + else: + tms_hms = world.local_tms + poke_data.hm_moves + flag = tms_hms.index(move) + mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, player)] + multiworld.random.shuffle(mon_list) + mon_list.sort(key=lambda mon: world.local_move_data[move]["type"] not in + [world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]]) + for mon in mon_list: + if test_state.has(mon, player): + world.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8) + break + + last_intervene = None + while True: + intervene_move = None + test_state = multiworld.get_all_state(False) + if not logic.can_learn_hm(test_state, "Surf", player): + intervene_move = "Surf" + elif not logic.can_learn_hm(test_state, "Strength", player): + intervene_move = "Strength" + # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off, + # as you will require cut to access celadon gyn + elif ((not logic.can_learn_hm(test_state, "Cut", player)) and + (multiworld.accessibility[player] != "minimal" or ((not + multiworld.badgesanity[player]) and max( + multiworld.elite_four_badges_condition[player], + multiworld.route_22_gate_condition[player], + multiworld.victory_road_condition[player]) + > 7) or (multiworld.door_shuffle[player] not in ("off", "simple")))): + intervene_move = "Cut" + elif ((not logic.can_learn_hm(test_state, "Flash", player)) + and multiworld.dark_rock_tunnel_logic[player] + and (multiworld.accessibility[player] != "minimal" + or multiworld.door_shuffle[player])): + intervene_move = "Flash" + # If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps + # as reachable, and if on no door shuffle or simple, fly is simply never necessary. + # We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been + # considered in door shuffle. + elif ((not logic.can_learn_hm(test_state, "Fly", player)) + and multiworld.door_shuffle[player] not in + ("off", "simple") and [world.fly_map, world.town_map_fly_map] != ["Pallet Town", "Pallet Town"]): + intervene_move = "Fly" + if intervene_move: + if intervene_move == last_intervene: + raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {player}") + intervene(intervene_move, test_state) + last_intervene = intervene_move + else: + break \ No newline at end of file diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index afeb301c9b..4932f57935 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1540,7 +1540,6 @@ def create_regions(self): item = self.create_filler() elif location.original_item == "Pokedex": if self.multiworld.randomize_pokedex[self.player] == "vanilla": - location_object.event = True event = True item = self.create_item("Pokedex") elif location.original_item == "Moon Stone" and self.multiworld.stonesanity[self.player]: @@ -1948,7 +1947,7 @@ def create_regions(self): for entrance in reversed(region.exits): if isinstance(entrance, PokemonRBWarp): region.exits.remove(entrance) - multiworld.regions.entrance_cache[self.player] = cache + multiworld.regions.entrance_cache[self.player] = cache.copy() if badge_locs: for loc in badge_locs: loc.item = None diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index fe6efb9c30..96b3ddc66b 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -957,13 +957,13 @@ def caclulate_soa_options(ctx: SC2Context) -> int: return options -def kerrigan_primal(ctx: SC2Context, items: typing.Dict[SC2Race, typing.List[int]]) -> bool: +def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool: if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_zerg: return True elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_human: return False elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_level_35: - return items[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]] >= 35 + return kerrigan_level >= 35 elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_half_completion: total_missions = len(ctx.mission_id_to_location_ids) completed = len([(mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations @@ -1138,7 +1138,7 @@ class ArchipelagoBot(bot.bot_ai.BotAI): async def updateZergTech(self, current_items, kerrigan_level): zerg_items = current_items[SC2Race.ZERG] - kerrigan_primal_by_items = kerrigan_primal(self.ctx, current_items) + kerrigan_primal_by_items = kerrigan_primal(self.ctx, kerrigan_level) kerrigan_primal_bot_value = 1 if kerrigan_primal_by_items else 0 await self.chat_send("?GiveZergTech {} {} {} {} {} {} {} {} {} {} {} {}".format( kerrigan_level, kerrigan_primal_bot_value, zerg_items[0], zerg_items[1], zerg_items[2], diff --git a/worlds/sc2/Locations.py b/worlds/sc2/Locations.py index 22b400a238..bf9c06fa3f 100644 --- a/worlds/sc2/Locations.py +++ b/worlds/sc2/Locations.py @@ -1368,9 +1368,9 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]: lambda state: logic.templars_charge_requirement(state)), LocationData("Templar's Charge", "Templar's Charge: Southeast Power Core", SC2LOTV_LOC_ID_OFFSET + 1903, LocationType.EXTRA, lambda state: logic.templars_charge_requirement(state)), - LocationData("Templar's Charge", "Templar's Charge: West Hybrid Statis Chamber", SC2LOTV_LOC_ID_OFFSET + 1904, LocationType.VANILLA, + LocationData("Templar's Charge", "Templar's Charge: West Hybrid Stasis Chamber", SC2LOTV_LOC_ID_OFFSET + 1904, LocationType.VANILLA, lambda state: logic.templars_charge_requirement(state)), - LocationData("Templar's Charge", "Templar's Charge: Southeast Hybrid Statis Chamber", SC2LOTV_LOC_ID_OFFSET + 1905, LocationType.VANILLA, + LocationData("Templar's Charge", "Templar's Charge: Southeast Hybrid Stasis Chamber", SC2LOTV_LOC_ID_OFFSET + 1905, LocationType.VANILLA, lambda state: logic.protoss_fleet(state)), LocationData("Templar's Return", "Templar's Return: Victory", SC2LOTV_LOC_ID_OFFSET + 2000, LocationType.VICTORY, lambda state: logic.templars_return_requirement(state)), diff --git a/worlds/sc2/PoolFilter.py b/worlds/sc2/PoolFilter.py index 068c623149..e94dc4e214 100644 --- a/worlds/sc2/PoolFilter.py +++ b/worlds/sc2/PoolFilter.py @@ -58,7 +58,8 @@ def filter_missions(world: World) -> Dict[MissionPools, List[SC2Mission]]: # Vanilla uses the entire mission pool goal_priorities: Dict[SC2Campaign, SC2CampaignGoalPriority] = {campaign: get_campaign_goal_priority(campaign) for campaign in enabled_campaigns} goal_level = max(goal_priorities.values()) - candidate_campaigns = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level] + candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level] + candidate_campaigns.sort(key=lambda it: it.id) goal_campaign = world.random.choice(candidate_campaigns) if campaign_final_mission_locations[goal_campaign] is not None: mission_pools[MissionPools.FINAL] = [campaign_final_mission_locations[goal_campaign].mission] @@ -70,7 +71,8 @@ def filter_missions(world: World) -> Dict[MissionPools, List[SC2Mission]]: # Finding the goal map goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns} goal_level = max(goal_priorities.values()) - candidate_campaigns = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level] + candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level] + candidate_campaigns.sort(key=lambda it: it.id) goal_campaign = world.random.choice(candidate_campaigns) primary_goal = campaign_final_mission_locations[goal_campaign] if primary_goal is None or primary_goal.mission in excluded_missions: @@ -242,8 +244,8 @@ class ValidInventory: def generate_reduced_inventory(self, inventory_size: int, mission_requirements: List[Tuple[str, Callable]]) -> List[Item]: """Attempts to generate a reduced inventory that can fulfill the mission requirements.""" - inventory = list(self.item_pool) - locked_items = list(self.locked_items) + inventory: List[Item] = list(self.item_pool) + locked_items: List[Item] = list(self.locked_items) item_list = get_full_item_list() self.logical_inventory = [ item.name for item in inventory + locked_items + self.existing_items @@ -346,7 +348,7 @@ class ValidInventory: removable_generic_items.append(item) # Main cull process - unused_items = [] # Reusable items for the second pass + unused_items: List[str] = [] # Reusable items for the second pass while len(inventory) + len(locked_items) > inventory_size: if len(inventory) == 0: # There are more items than locations and all of them are already locked due to YAML or logic. @@ -394,18 +396,35 @@ class ValidInventory: if attempt_removal(item): unused_items.append(item.name) + pool_items: List[str] = [item.name for item in (inventory + locked_items + self.existing_items)] + unused_items = [ + unused_item for unused_item in unused_items + if item_list[unused_item].parent_item is None + or item_list[unused_item].parent_item in pool_items + ] + # Removing extra dependencies # WoL logical_inventory_set = set(self.logical_inventory) if not spider_mine_sources & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Spider Mine)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Spider Mine)")] if not BARRACKS_UNITS & logical_inventory_set: - inventory = [item for item in inventory if - not (item.name.startswith(ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX) or item.name == ItemNames.ORBITAL_STRIKE)] + inventory = [ + item for item in inventory + if not (item.name.startswith(ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX) + or item.name == ItemNames.ORBITAL_STRIKE)] + unused_items = [ + item_name for item_name in unused_items + if not (item_name.startswith( + ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX) + or item_name == ItemNames.ORBITAL_STRIKE)] if not FACTORY_UNITS & logical_inventory_set: inventory = [item for item in inventory if not item.name.startswith(ItemNames.TERRAN_VEHICLE_UPGRADE_PREFIX)] + unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.TERRAN_VEHICLE_UPGRADE_PREFIX)] if not STARPORT_UNITS & logical_inventory_set: inventory = [item for item in inventory if not item.name.startswith(ItemNames.TERRAN_SHIP_UPGRADE_PREFIX)] + unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.TERRAN_SHIP_UPGRADE_PREFIX)] # HotS # Baneling without sources => remove Baneling and upgrades if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory @@ -414,6 +433,8 @@ class ValidInventory: ): inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT] + unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT] + unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT] # Spawn Banelings without Zergling => remove Baneling unit, keep upgrades except macro ones if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory and ItemNames.ZERGLING not in self.logical_inventory @@ -421,9 +442,12 @@ class ValidInventory: ): inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT] inventory = [item for item in inventory if item.name != ItemNames.BANELING_RAPID_METAMORPH] + unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT] + unused_items = [item_name for item_name in unused_items if item_name != ItemNames.BANELING_RAPID_METAMORPH] if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR, ItemNames.SCOURGE} & logical_inventory_set: inventory = [item for item in inventory if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)] locked_items = [item for item in locked_items if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)] + unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)] # T3 items removal rules - remove morph and its upgrades if the basic unit isn't in if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Mutalisk/Corruptor)")] @@ -431,45 +455,69 @@ class ValidInventory: inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Mutalisk/Corruptor)")] + unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT] + unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT] + unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT] + unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT] if ItemNames.ROACH not in logical_inventory_set: inventory = [item for item in inventory if item.name != ItemNames.ROACH_RAVAGER_ASPECT] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT] + unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ROACH_RAVAGER_ASPECT] + unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT] if ItemNames.HYDRALISK not in logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Hydralisk)")] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT] inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_IMPALER_ASPECT] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Hydralisk)")] + unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT] + unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.HYDRALISK_IMPALER_ASPECT] # LotV # Shared unit upgrades between several units if not {ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Stalker/Instigator/Slayer)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Stalker/Instigator/Slayer)")] if not {ItemNames.PHOENIX, ItemNames.MIRAGE} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Phoenix/Mirage)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Phoenix/Mirage)")] if not {ItemNames.VOID_RAY, ItemNames.DESTROYER} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Void Ray/Destroyer)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Void Ray/Destroyer)")] if not {ItemNames.IMMORTAL, ItemNames.ANNIHILATOR} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Immortal/Annihilator)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Immortal/Annihilator)")] if not {ItemNames.DARK_TEMPLAR, ItemNames.AVENGER, ItemNames.BLOOD_HUNTER} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Dark Templar/Avenger/Blood Hunter)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Dark Templar/Avenger/Blood Hunter)")] if not {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ASCENDANT, ItemNames.DARK_TEMPLAR} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Archon)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Archon)")] logical_inventory_set.difference_update([item_name for item_name in logical_inventory_set if item_name.endswith("(Archon)")]) if not {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ARCHON_HIGH_ARCHON} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(High Templar/Signifier)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(High Templar/Signifier)")] if ItemNames.SUPPLICANT not in logical_inventory_set: inventory = [item for item in inventory if item.name != ItemNames.ASCENDANT_POWER_OVERWHELMING] + unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ASCENDANT_POWER_OVERWHELMING] if not {ItemNames.DARK_ARCHON, ItemNames.DARK_TEMPLAR_DARK_ARCHON_MELD} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Dark Archon)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Dark Archon)")] if not {ItemNames.SENTRY, ItemNames.ENERGIZER, ItemNames.HAVOC} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Sentry/Energizer/Havoc)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Sentry/Energizer/Havoc)")] if not {ItemNames.SENTRY, ItemNames.ENERGIZER, ItemNames.HAVOC, ItemNames.SHIELD_BATTERY} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Sentry/Energizer/Havoc/Shield Battery)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Sentry/Energizer/Havoc/Shield Battery)")] if not {ItemNames.ZEALOT, ItemNames.CENTURION, ItemNames.SENTINEL} & logical_inventory_set: inventory = [item for item in inventory if not item.name.endswith("(Zealot/Sentinel/Centurion)")] + unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Zealot/Sentinel/Centurion)")] # Static defense upgrades only if static defense present if not {ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH, ItemNames.NEXUS_OVERCHARGE, ItemNames.SHIELD_BATTERY} & logical_inventory_set: inventory = [item for item in inventory if item.name != ItemNames.ENHANCED_TARGETING] + unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ENHANCED_TARGETING] if not {ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH, ItemNames.NEXUS_OVERCHARGE} & logical_inventory_set: inventory = [item for item in inventory if item.name != ItemNames.OPTIMIZED_ORDNANCE] + unused_items = [item_name for item_name in unused_items if item_name != ItemNames.OPTIMIZED_ORDNANCE] # Cull finished, adding locked items back into inventory inventory += locked_items diff --git a/worlds/sc2/Regions.py b/worlds/sc2/Regions.py index e6c001b186..84830a9a32 100644 --- a/worlds/sc2/Regions.py +++ b/worlds/sc2/Regions.py @@ -180,7 +180,7 @@ def create_vanilla_regions( connect(world, names, "Menu", "Dark Whispers") connect(world, names, "Dark Whispers", "Ghosts in the Fog", lambda state: state.has("Beat Dark Whispers", player)) - connect(world, names, "Dark Whispers", "Evil Awoken", + connect(world, names, "Ghosts in the Fog", "Evil Awoken", lambda state: state.has("Beat Ghosts in the Fog", player)) if SC2Campaign.LOTV in enabled_campaigns: @@ -250,7 +250,7 @@ def create_vanilla_regions( connect(world, names, "Enemy Intelligence", "Trouble In Paradise", lambda state: state.has("Beat Enemy Intelligence", player)) connect(world, names, "Trouble In Paradise", "Night Terrors", - lambda state: state.has("Beat Evacuation", player)) + lambda state: state.has("Beat Trouble In Paradise", player)) connect(world, names, "Night Terrors", "Flashpoint", lambda state: state.has("Beat Night Terrors", player)) connect(world, names, "Flashpoint", "In the Enemy's Shadow", diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index fffa618d26..59c6fe9001 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -42,7 +42,6 @@ class SC2World(World): game = "Starcraft 2" web = Starcraft2WebWorld() - data_version = 6 item_name_to_id = {name: data.code for name, data in get_full_item_list().items()} location_name_to_id = {location.name: location.code for location in get_locations(None)} diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 39aa42c07a..04c376f3c8 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -527,7 +527,6 @@ class SMZ3World(World): if (loc.item.player == self.player and loc.always_allow(state, loc.item)): loc.item.classification = ItemClassification.filler loc.item.item.Progression = False - loc.item.location.event = False self.unreachable.append(loc) def get_filler_item_name(self) -> str: @@ -573,7 +572,6 @@ class SMZ3World(World): break assert itemFromPool is not None, "Can't find anymore item(s) to pre fill GT" self.multiworld.push_item(loc, itemFromPool, False) - loc.event = False toRemove.sort(reverse = True) for i in toRemove: self.multiworld.itempool.pop(i) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index dcca722ad1..061322650e 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -486,4 +486,3 @@ class SoELocation(Location): super().__init__(player, name, address, parent) # unconditional assignments favor a split dict, saving memory self.progress_type = LocationProgressType.EXCLUDED if exclude else LocationProgressType.DEFAULT - self.event = not address diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 35ef940906..d8a9322ab4 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -92,12 +92,6 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi class SpireLocation(Location): game: str = "Slay the Spire" - def __init__(self, player: int, name: str, address=None, parent=None): - super(SpireLocation, self).__init__(player, name, address, parent) - if address is None: - self.event = True - self.locked = True - class SpireItem(Item): game = "Slay the Spire" diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index e25fd8eb9a..6a82a2a26d 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -30,10 +30,6 @@ client_version = 0 class StardewLocation(Location): game: str = "Stardew Valley" - def __init__(self, player: int, name: str, address: Optional[int], parent=None): - super().__init__(player, name, address, parent) - self.event = not address - class StardewItem(Item): game: str = "Stardew Valley" @@ -144,7 +140,7 @@ class StardewValleyWorld(World): locations_count = len([location for location in self.multiworld.get_locations(self.player) - if not location.event]) + if location.address is not None]) created_items = create_items(self.create_item, self.delete_item, locations_count, items_to_exclude, self.options, self.random) diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index 055407d97d..c1bd7a25c2 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -268,7 +268,6 @@ class BuildingProgression(Choice): Vanilla: You can buy each building normally. Progressive: You will receive the buildings and will be able to build the first one of each type for free, once it is received. If you want more of the same building, it will cost the vanilla price. - Progressive early shipping bin: Same as Progressive, but the shipping bin will be placed early in the multiworld. Cheap: Buildings will cost half as much Very Cheap: Buildings will cost 1/5th as much """ diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 55ad4f0754..1b4d1476b9 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -371,8 +371,7 @@ class TestLocationGeneration(SVTestBase): def test_all_location_created_are_in_location_table(self): for location in self.get_real_locations(): - if not location.event: - self.assertIn(location.name, location_table) + self.assertIn(location.name, location_table) class TestMinLocationAndMaxItem(SVTestBase): @@ -771,11 +770,10 @@ class TestShipsanityNone(SVTestBase): } def test_no_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event: - with self.subTest(location.name): - self.assertFalse("Shipsanity" in location.name) - self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags) + for location in self.get_real_locations(): + with self.subTest(location.name): + self.assertFalse("Shipsanity" in location.name) + self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags) class TestShipsanityCrops(SVTestBase): @@ -785,8 +783,8 @@ class TestShipsanityCrops(SVTestBase): } def test_only_crop_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags) @@ -808,8 +806,8 @@ class TestShipsanityCropsExcludeIsland(SVTestBase): } def test_only_crop_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags) @@ -831,8 +829,8 @@ class TestShipsanityCropsNoQiCropWithoutSpecialOrders(SVTestBase): } def test_only_crop_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags) @@ -854,8 +852,8 @@ class TestShipsanityFish(SVTestBase): } def test_only_fish_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) @@ -878,8 +876,8 @@ class TestShipsanityFishExcludeIsland(SVTestBase): } def test_only_fish_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) @@ -902,8 +900,8 @@ class TestShipsanityFishExcludeQiOrders(SVTestBase): } def test_only_fish_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) @@ -926,8 +924,8 @@ class TestShipsanityFullShipment(SVTestBase): } def test_only_full_shipment_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags) self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) @@ -953,8 +951,8 @@ class TestShipsanityFullShipmentExcludeIsland(SVTestBase): } def test_only_full_shipment_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags) self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) @@ -979,8 +977,8 @@ class TestShipsanityFullShipmentExcludeQiBoard(SVTestBase): } def test_only_full_shipment_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags) self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) @@ -1006,8 +1004,8 @@ class TestShipsanityFullShipmentWithFish(SVTestBase): } def test_only_full_shipment_and_fish_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or LocationTags.SHIPSANITY_FISH in location_table[location.name].tags) @@ -1041,8 +1039,8 @@ class TestShipsanityFullShipmentWithFishExcludeIsland(SVTestBase): } def test_only_full_shipment_and_fish_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or LocationTags.SHIPSANITY_FISH in location_table[location.name].tags) @@ -1075,8 +1073,8 @@ class TestShipsanityFullShipmentWithFishExcludeQiBoard(SVTestBase): } def test_only_full_shipment_and_fish_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: with self.subTest(location.name): self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or LocationTags.SHIPSANITY_FISH in location_table[location.name].tags) diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py index 787e0ce39c..3ee921bd2b 100644 --- a/worlds/stardew_valley/test/TestRules.py +++ b/worlds/stardew_valley/test/TestRules.py @@ -557,8 +557,8 @@ class TestDonationLogicRandomized(SVTestBase): railroad_item = "Railroad Boulder Removed" swap_museum_and_bathhouse(self.multiworld, self.player) collect_all_except(self.multiworld, railroad_item) - donation_locations = [location for location in self.multiworld.get_locations() if - not location.event and LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags] + donation_locations = [location for location in self.get_real_locations() if + LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags] for donation in donation_locations: self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) @@ -713,10 +713,9 @@ class TestShipsanityNone(SVTestBase): } def test_no_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event: - self.assertFalse("Shipsanity" in location.name) - self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags) + for location in self.get_real_locations(): + self.assertFalse("Shipsanity" in location.name) + self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags) class TestShipsanityCrops(SVTestBase): @@ -725,8 +724,8 @@ class TestShipsanityCrops(SVTestBase): } def test_only_crop_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags) @@ -736,8 +735,8 @@ class TestShipsanityFish(SVTestBase): } def test_only_fish_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) @@ -747,8 +746,8 @@ class TestShipsanityFullShipment(SVTestBase): } def test_only_full_shipment_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags) self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) @@ -759,8 +758,8 @@ class TestShipsanityFullShipmentWithFish(SVTestBase): } def test_only_full_shipment_and_fish_shipsanity_locations(self): - for location in self.multiworld.get_locations(self.player): - if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags: + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or LocationTags.SHIPSANITY_FISH in location_table[location.name].tags) @@ -774,8 +773,8 @@ class TestShipsanityEverything(SVTestBase): def test_all_shipsanity_locations_require_shipping_bin(self): bin_name = "Shipping Bin" collect_all_except(self.multiworld, bin_name) - shipsanity_locations = [location for location in self.multiworld.get_locations() if - not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags] + shipsanity_locations = [location for location in self.get_real_locations() if + LocationTags.SHIPSANITY in location_table[location.name].tags] bin_item = self.world.create_item(bin_name) for location in shipsanity_locations: with self.subTest(location.name): diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 5eddb7e280..1a463d9fc2 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -277,10 +277,10 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): self.multiworld.state.collect(self.world.create_item("Stardrop"), event=False) def get_real_locations(self) -> List[Location]: - return [location for location in self.multiworld.get_locations(self.player) if not location.event] + return [location for location in self.multiworld.get_locations(self.player) if location.address is not None] def get_real_location_names(self) -> List[str]: - return [location.name for location in self.multiworld.get_locations(self.player) if not location.event] + return [location.name for location in self.get_real_locations()] pre_generated_worlds = {} diff --git a/worlds/stardew_valley/test/assertion/mod_assert.py b/worlds/stardew_valley/test/assertion/mod_assert.py index 4f72c9a397..eec7f805d2 100644 --- a/worlds/stardew_valley/test/assertion/mod_assert.py +++ b/worlds/stardew_valley/test/assertion/mod_assert.py @@ -20,7 +20,7 @@ class ModAssertMixin(TestCase): self.assertTrue(item.mod_name is None or item.mod_name in chosen_mods, f"Item {item.name} has is from mod {item.mod_name}. Allowed mods are {chosen_mods}.") for multiworld_location in multiworld.get_locations(): - if multiworld_location.event: + if multiworld_location.address is None: continue location = location_table[multiworld_location.name] self.assertTrue(location.mod_name is None or location.mod_name in chosen_mods) diff --git a/worlds/stardew_valley/test/assertion/world_assert.py b/worlds/stardew_valley/test/assertion/world_assert.py index 413517e1c9..1e5512682f 100644 --- a/worlds/stardew_valley/test/assertion/world_assert.py +++ b/worlds/stardew_valley/test/assertion/world_assert.py @@ -13,7 +13,7 @@ def get_all_item_names(multiworld: MultiWorld) -> List[str]: def get_all_location_names(multiworld: MultiWorld) -> List[str]: - return [location.name for location in multiworld.get_locations() if not location.event] + return [location.name for location in multiworld.get_locations() if location.address is not None] class WorldAssertMixin(RuleAssertMixin, TestCase): @@ -48,7 +48,7 @@ class WorldAssertMixin(RuleAssertMixin, TestCase): self.assert_can_reach_victory(multiworld) def assert_same_number_items_locations(self, multiworld: MultiWorld): - non_event_locations = [location for location in multiworld.get_locations() if not location.event] + non_event_locations = [location for location in multiworld.get_locations() if location.address is not None] self.assertEqual(len(multiworld.itempool), len(non_event_locations)) def assert_can_reach_everything(self, multiworld: MultiWorld): diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index e9341ec3b9..08df70d78b 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -4,7 +4,7 @@ import logging import itertools from typing import List, Dict, Any, cast -from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld from . import items from . import locations @@ -42,14 +42,16 @@ class SubnauticaWorld(World): item_name_to_id = {data.name: item_id for item_id, data in items.item_table.items()} location_name_to_id = all_locations - option_definitions = options.option_definitions - + options_dataclass = options.SubnauticaOptions + options: options.SubnauticaOptions data_version = 10 required_client_version = (0, 4, 1) creatures_to_scan: List[str] def generate_early(self) -> None: + if not self.options.filler_items_distribution.weights_pair[1][-1]: + raise Exception("Filler Items Distribution needs at least one positive weight.") if self.options.early_seaglide: self.multiworld.local_early_items[self.player]["Seaglide Fragment"] = 2 @@ -98,7 +100,7 @@ class SubnauticaWorld(World): planet_region ] - # refer to Rules.py + # refer to rules.py set_rules = set_rules def create_items(self): @@ -129,7 +131,7 @@ class SubnauticaWorld(World): extras -= group_amount for item_name in self.random.sample( - # list of high-count important fragments as priority filler + # list of high-count important fragments as priority filler [ "Cyclops Engine Fragment", "Cyclops Hull Fragment", @@ -140,7 +142,7 @@ class SubnauticaWorld(World): "Modification Station Fragment", "Moonpool Fragment", "Laser Cutter Fragment", - ], + ], k=min(extras, 9)): item = self.create_item(item_name) pool.append(item) @@ -176,7 +178,10 @@ class SubnauticaWorld(World): item_id, player=self.player) def get_filler_item_name(self) -> str: - return item_table[self.multiworld.random.choice(items_by_type[ItemType.resource])].name + item_names, cum_item_weights = self.options.filler_items_distribution.weights_pair + return self.random.choices(item_names, + cum_weights=cum_item_weights, + k=1)[0] class SubnauticaLocation(Location): diff --git a/worlds/subnautica/items.py b/worlds/subnautica/items.py index bffc843241..d5dcf6a6af 100644 --- a/worlds/subnautica/items.py +++ b/worlds/subnautica/items.py @@ -145,6 +145,9 @@ item_table: Dict[int, ItemData] = { items_by_type: Dict[ItemType, List[int]] = {item_type: [] for item_type in ItemType} for item_id, item_data in item_table.items(): items_by_type[item_data.type].append(item_id) +item_names_by_type: Dict[ItemType, List[str]] = { + item_type: sorted(item_table[item_id].name for item_id in item_ids) for item_type, item_ids in items_by_type.items() +} group_items: Dict[int, Set[int]] = { 35100: {35025, 35047, 35048, 35056, 35057, 35058, 35059, 35060, 35061, 35062, 35063, 35064, 35065, 35067, 35068, diff --git a/worlds/subnautica/options.py b/worlds/subnautica/options.py index d8d727a9e1..6554425dc7 100644 --- a/worlds/subnautica/options.py +++ b/worlds/subnautica/options.py @@ -1,7 +1,20 @@ import typing +from dataclasses import dataclass +from functools import cached_property + +from Options import ( + Choice, + Range, + DeathLink, + Toggle, + DefaultOnToggle, + StartInventoryPool, + ItemDict, + PerGameCommonOptions, +) -from Options import Choice, Range, DeathLink, Toggle, DefaultOnToggle, StartInventoryPool from .creatures import all_creatures, Definitions +from .items import ItemType, item_names_by_type class SwimRule(Choice): @@ -103,13 +116,28 @@ class SubnauticaDeathLink(DeathLink): Note: can be toggled via in-game console command "deathlink".""" -option_definitions = { - "swim_rule": SwimRule, - "early_seaglide": EarlySeaglide, - "free_samples": FreeSamples, - "goal": Goal, - "creature_scans": CreatureScans, - "creature_scan_logic": AggressiveScanLogic, - "death_link": SubnauticaDeathLink, - "start_inventory_from_pool": StartInventoryPool, -} +class FillerItemsDistribution(ItemDict): + """Random chance weights of various filler resources that can be obtained. + Available items: """ + __doc__ += ", ".join(f"\"{item_name}\"" for item_name in item_names_by_type[ItemType.resource]) + _valid_keys = frozenset(item_names_by_type[ItemType.resource]) + default = {item_name: 1 for item_name in item_names_by_type[ItemType.resource]} + display_name = "Filler Items Distribution" + + @cached_property + def weights_pair(self) -> typing.Tuple[typing.List[str], typing.List[int]]: + from itertools import accumulate + return list(self.value.keys()), list(accumulate(self.value.values())) + + +@dataclass +class SubnauticaOptions(PerGameCommonOptions): + swim_rule: SwimRule + early_seaglide: EarlySeaglide + free_samples: FreeSamples + goal: Goal + creature_scans: CreatureScans + creature_scan_logic: AggressiveScanLogic + death_link: SubnauticaDeathLink + start_inventory_from_pool: StartInventoryPool + filler_items_distribution: FillerItemsDistribution diff --git a/worlds/terraria/Checks.py b/worlds/terraria/Checks.py index b6be45258c..0630d6290b 100644 --- a/worlds/terraria/Checks.py +++ b/worlds/terraria/Checks.py @@ -177,6 +177,7 @@ def validate_conditions( if condition not in { "npc", "calamity", + "grindy", "pickaxe", "hammer", "mech_boss", @@ -221,62 +222,60 @@ def mark_progression( mark_progression(conditions, progression, rules, rule_indices, loc_to_item) -def read_data() -> ( - Tuple[ - # Goal to rule index that ends that goal's range and the locations required - List[Tuple[int, Set[str]]], - # Rules - List[ - Tuple[ - # Rule - str, - # Flag to flag arg - Dict[str, Union[str, int, None]], - # True = or, False = and, None = N/A - Union[bool, None], - # Conditions - List[ - Tuple[ - # True = positive, False = negative - bool, - # Condition type - int, - # Condition name or list (True = or, False = and, None = N/A) (list shares type with outer) - Union[str, Tuple[Union[bool, None], List]], - # Condition arg - Union[str, int, None], - ] - ], - ] - ], - # Rule to rule index - Dict[str, int], - # Label to rewards - Dict[str, List[str]], - # Reward to flags - Dict[str, Set[str]], - # Item name to ID - Dict[str, int], - # Location name to ID - Dict[str, int], - # NPCs - List[str], - # Pickaxe to pick power - Dict[str, int], - # Hammer to hammer power - Dict[str, int], - # Mechanical bosses - List[str], - # Calamity final bosses - List[str], - # Progression rules - Set[str], - # Armor to minion count, - Dict[str, int], - # Accessory to minion count, - Dict[str, int], - ] -): +def read_data() -> Tuple[ + # Goal to rule index that ends that goal's range and the locations required + List[Tuple[int, Set[str]]], + # Rules + List[ + Tuple[ + # Rule + str, + # Flag to flag arg + Dict[str, Union[str, int, None]], + # True = or, False = and, None = N/A + Union[bool, None], + # Conditions + List[ + Tuple[ + # True = positive, False = negative + bool, + # Condition type + int, + # Condition name or list (True = or, False = and, None = N/A) (list shares type with outer) + Union[str, Tuple[Union[bool, None], List]], + # Condition arg + Union[str, int, None], + ] + ], + ] + ], + # Rule to rule index + Dict[str, int], + # Label to rewards + Dict[str, List[str]], + # Reward to flags + Dict[str, Set[str]], + # Item name to ID + Dict[str, int], + # Location name to ID + Dict[str, int], + # NPCs + List[str], + # Pickaxe to pick power + Dict[str, int], + # Hammer to hammer power + Dict[str, int], + # Mechanical bosses + List[str], + # Calamity final bosses + List[str], + # Progression rules + Set[str], + # Armor to minion count, + Dict[str, int], + # Accessory to minion count, + Dict[str, int], +]: next_id = 0x7E0000 item_name_to_id = {} diff --git a/worlds/terraria/Rules.dsv b/worlds/terraria/Rules.dsv index 38ca4e575f..322bf9c5d3 100644 --- a/worlds/terraria/Rules.dsv +++ b/worlds/terraria/Rules.dsv @@ -234,9 +234,9 @@ Spider Armor; ArmorMinions(3); Cross Necklace; ; Wall of Flesh; Altar; ; Wall of Flesh & @hammer(80); Begone, Evil!; Achievement; Altar; -Cobalt Ore; ; ((~@calamity & Altar) | (@calamity & Wall of Flesh)) & @pickaxe(100); +Cobalt Ore; ; (((~@calamity & Altar) | (@calamity & Wall of Flesh)) & @pickaxe(100)) | Wall of Flesh; Extra Shiny!; Achievement; Cobalt Ore | Mythril Ore | Adamantite Ore | Chlorophyte Ore; -Cobalt Bar; ; Cobalt Ore; +Cobalt Bar; ; Cobalt Ore | Wall of Flesh; Cobalt Pickaxe; Pickaxe(110); Cobalt Bar; Soul of Night; ; Wall of Flesh | (@calamity & Altar); Hallow; ; Wall of Flesh; @@ -249,7 +249,7 @@ Blessed Apple; ; Rod of Discord; ; Hallow; Gelatin World Tour; Achievement | Grindy; Dungeon & Wall of Flesh & Hallow & #King Slime; Soul of Flight; ; Wall of Flesh; -Head in the Clouds; Achievement; (Soul of Flight & ((Hardmode Anvil & (Soul of Light | Soul of Night | Pixie Dust | Wall of Flesh | Solar Eclipse | @mech_boss(1) | Plantera | Spectre Bar | #Golem)) | (Shroomite Bar & Autohammer) | #Mourning Wood | #Pumpking)) | Steampunker | (Wall of Flesh & Witch Doctor) | (Solar Eclipse & Plantera) | #Everscream | #Old One's Army Tier 3 | #Empress of Light | #Duke Fishron | (Fragment & Luminite Bar & Ancient Manipulator); // Leaf Wings are Post-Plantera in 1.4.4 +Head in the Clouds; Achievement; @grindy | (Soul of Flight & ((Hardmode Anvil & (Soul of Light | Soul of Night | Pixie Dust | Wall of Flesh | Solar Eclipse | @mech_boss(1) | Plantera | Spectre Bar | #Golem)) | (Shroomite Bar & Autohammer) | #Mourning Wood | #Pumpking)) | Steampunker | (Wall of Flesh & Witch Doctor) | (Solar Eclipse & Plantera) | #Everscream | #Old One's Army Tier 3 | #Empress of Light | #Duke Fishron | (Fragment & Luminite Bar & Ancient Manipulator); // Leaf Wings are Post-Plantera in 1.4.4 Bunny; Npc; Zoologist & Wall of Flesh; // Extremely simplified Forbidden Fragment; ; Sandstorm & Wall of Flesh; Astral Infection; Calamity; Wall of Flesh; @@ -274,13 +274,13 @@ Pirate; Npc; Queen Slime; Location | Item; Hallow; // Aquatic Scourge -Mythril Ore; ; ((~@calamity & Altar) | (@calamity & @mech_boss(1))) & @pickaxe(110); -Mythril Bar; ; Mythril Ore; +Mythril Ore; ; (((~@calamity & Altar) | (@calamity & @mech_boss(1))) & @pickaxe(110)) | (Wall of Flesh & (~@calamity | @mech_boss(1))); +Mythril Bar; ; Mythril Ore | (Wall of Flesh & (~@calamity | @mech_boss(1))); Hardmode Anvil; ; Mythril Bar; Mythril Pickaxe; Pickaxe(150); Hardmode Anvil & Mythril Bar; -Adamantite Ore; ; ((~@calamity & Altar) | (@calamity & @mech_boss(2))) & @pickaxe(150); +Adamantite Ore; ; (((~@calamity & Altar) | (@calamity & @mech_boss(2))) & @pickaxe(150)) | (Wall of Flesh & (~@calamity | @mech_boss(2))); Hardmode Forge; ; Hardmode Anvil & Adamantite Ore & Hellforge; -Adamantite Bar; ; Hardmode Forge & Adamantite Ore; +Adamantite Bar; ; (Hardmode Forge & Adamantite Ore) | (Wall of Flesh & (~@calamity | @mech_boss(2))); Adamantite Pickaxe; Pickaxe(180); Hardmode Anvil & Adamantite Bar; Forbidden Armor; ArmorMinions(2); Hardmode Anvil & Adamantite Bar & Forbidden Fragment; Aquatic Scourge; Calamity | Location | Item; diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py index 306a65ef91..6ef281157f 100644 --- a/worlds/terraria/__init__.py +++ b/worlds/terraria/__init__.py @@ -240,6 +240,8 @@ class TerrariaWorld(World): return not sign elif condition == "calamity": return sign == self.calamity + elif condition == "grindy": + return sign == (self.multiworld.achievements[self.player].value >= 2) elif condition == "pickaxe": if type(arg) is not int: raise Exception("@pickaxe requires an integer argument") diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index f80babc0e6..4f53f75eff 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -206,7 +206,6 @@ def create_location(player: int, location_data: LocationData, region: Region) -> location.access_rule = location_data.rule if id is None: - location.event = True location.locked = True return location diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index b2f23ae2ca..d4bea783a7 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -116,7 +116,6 @@ class TLoZWorld(World): def create_location(self, name, id, parent, event=False): return_location = TLoZLocation(self.player, name, id, parent) - return_location.event = event return return_location def create_regions(self): diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 3220c6c934..77324b2047 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -1,5 +1,5 @@ from typing import Dict, List, Any - +from logging import warning from BaseClasses import Region, Location, Item, Tutorial, ItemClassification from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations @@ -123,9 +123,9 @@ class TunicWorld(World): # Filler items in the item pool available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and item_table[filler].classification == ItemClassification.filler] - + # Remove filler to make room for other items - def remove_filler(amount: int): + def remove_filler(amount: int) -> None: for _ in range(0, amount): if not available_filler: fill = "Fool Trap" @@ -150,7 +150,7 @@ class TunicWorld(World): hexagon_goal = self.options.hexagon_goal extra_hexagons = self.options.extra_hexagon_percentage items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP)) - + # Replace pages and normal hexagons with filler for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)): filler_name = self.get_filler_item_name() @@ -184,7 +184,7 @@ class TunicWorld(World): self.tunic_portal_pairs = {} self.er_portal_hints = {} self.ability_unlocks = randomize_ability_unlocks(self.random, self.options) - + # stuff for universal tracker support, can be ignored for standard gen if hasattr(self.multiworld, "re_gen_passthrough"): if "TUNIC" in self.multiworld.re_gen_passthrough: @@ -231,7 +231,7 @@ class TunicWorld(World): def get_filler_item_name(self) -> str: return self.random.choice(filler_items) - def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if self.options.entrance_rando: hint_data.update({self.player: {}}) # all state seems to have efficient paths @@ -245,17 +245,27 @@ class TunicWorld(World): continue path_to_loc = [] previous_name = "placeholder" - name, connection = paths[location.parent_region] - while connection != ("Menu", None): - name, connection = connection - # for LS entrances, we just want to give the portal name - if "(LS)" in name: - name, _ = name.split(" (LS) ") - # was getting some cases like Library Grave -> Library Grave -> other place - if name in portal_names and name != previous_name: - previous_name = name - path_to_loc.append(name) - hint_text = " -> ".join(reversed(path_to_loc)) + try: + name, connection = paths[location.parent_region] + except KeyError: + # logic bug, proceed with warning since it takes a long time to update AP + warning(f"{location.name} is not logically accessible for " + f"{self.multiworld.get_file_safe_player_name(self.player)}. " + "Creating entrance hint Inaccessible. " + "Please report this to the TUNIC rando devs.") + hint_text = "Inaccessible" + else: + while connection != ("Menu", None): + name, connection = connection + # for LS entrances, we just want to give the portal name + if "(LS)" in name: + name, _ = name.split(" (LS) ") + # was getting some cases like Library Grave -> Library Grave -> other place + if name in portal_names and name != previous_name: + previous_name = name + path_to_loc.append(name) + hint_text = " -> ".join(reversed(path_to_loc)) + if hint_text: hint_data[self.player][location.address] = hint_text diff --git a/worlds/tunic/docs/setup_en.md b/worlds/tunic/docs/setup_en.md index 5ec41e8d52..94a8a03841 100644 --- a/worlds/tunic/docs/setup_en.md +++ b/worlds/tunic/docs/setup_en.md @@ -54,7 +54,7 @@ Launch the game, and if everything was installed correctly you should see `Rando ### Configure Your YAML File -Visit the [TUNIC options page](/games/Tunic/player-options) to generate a YAML with your selected options. +Visit the [TUNIC options page](/games/TUNIC/player-options) to generate a YAML with your selected options. ### Configure Your Mod Settings Launch the game, and using the menu on the Title Screen select `Archipelago` under `Randomizer Mode`. @@ -65,4 +65,4 @@ Once you've input your information, click the `Close` button. If everything was An error message will display if the game fails to connect to the server. -Be sure to also look at the in-game options menu for a variety of additional settings, such as enemy randomization! \ No newline at end of file +Be sure to also look at the in-game options menu for a variety of additional settings, such as enemy randomization! diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 96a3c39ad2..c6f9e242df 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -991,7 +991,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # connecting the regions portals are in to other portals you can access via ladder storage # using has_stick instead of can_ladder_storage since it's already checking the logic rules if options.logic_rules == "unrestricted": - def get_portal_info(portal_sd: str) -> (str, str): + def get_portal_info(portal_sd: str) -> Tuple[str, str]: for portal1, portal2 in portal_pairs.items(): if portal1.scene_destination() == portal_sd: return portal1.name, portal2.region @@ -1226,12 +1226,12 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re and (has_ladder("Ladders in Swamp", state, player, options) or has_ice_grapple_logic(True, state, player, options, ability_unlocks) or not options.entrance_rando)) + # soft locked without this ladder elif portal_name == "West Garden Exit after Boss" and not options.entrance_rando: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, rule=lambda state: has_stick(state, player) - and state.has_any(ladders, player) and (state.has("Ladders to West Bell", player))) # soft locked unless you have either ladder. if you have laurels, you use the other Entrance elif portal_name in {"Furnace Exit towards West Garden", "Furnace Exit to Dark Tomb"} \ diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 5d08188ace..ffd3ae30de 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -22,13 +22,13 @@ class TunicERLocation(Location): def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: regions: Dict[str, Region] = {} if world.options.entrance_rando: - portal_pairs: Dict[Portal, Portal] = pair_portals(world) + portal_pairs = pair_portals(world) # output the entrances to the spoiler log here for convenience for portal1, portal2 in portal_pairs.items(): world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player) else: - portal_pairs: Dict[Portal, Portal] = vanilla_portals() + portal_pairs = vanilla_portals() for region_name, region_data in tunic_er_regions.items(): regions[region_name] = Region(region_name, world.player, world.multiworld) diff --git a/worlds/tunic/test/__init__.py b/worlds/tunic/test/__init__.py index d7ae47f7d7..d0b68955c5 100644 --- a/worlds/tunic/test/__init__.py +++ b/worlds/tunic/test/__init__.py @@ -3,4 +3,4 @@ from test.bases import WorldTestBase class TunicTestBase(WorldTestBase): game = "TUNIC" - player: int = 1 \ No newline at end of file + player = 1 diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py index 1c4f06d504..72d4a498d1 100644 --- a/worlds/tunic/test/test_access.py +++ b/worlds/tunic/test/test_access.py @@ -4,14 +4,14 @@ from .. import options class TestAccess(TunicTestBase): # test whether you can get into the temple without laurels - def test_temple_access(self): + def test_temple_access(self) -> None: self.collect_all_but(["Hero's Laurels", "Lantern"]) self.assertFalse(self.can_reach_location("Sealed Temple - Page Pickup")) self.collect_by_name(["Lantern"]) self.assertTrue(self.can_reach_location("Sealed Temple - Page Pickup")) # test that the wells function properly. Since fairies is written the same way, that should succeed too - def test_wells(self): + def test_wells(self) -> None: self.collect_all_but(["Golden Coin"]) self.assertFalse(self.can_reach_location("Coins in the Well - 3 Coins")) self.collect_by_name(["Golden Coin"]) @@ -22,7 +22,7 @@ class TestStandardShuffle(TunicTestBase): options = {options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true} # test that you need to get holy cross to open the hc door in overworld - def test_hc_door(self): + def test_hc_door(self) -> None: self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup")) self.collect_by_name("Pages 42-43 (Holy Cross)") self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) @@ -33,7 +33,7 @@ class TestHexQuestShuffle(TunicTestBase): options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true} # test that you need the gold questagons to open the hc door in overworld - def test_hc_door_hex_shuffle(self): + def test_hc_door_hex_shuffle(self) -> None: self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup")) self.collect_by_name("Gold Questagon") self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) @@ -44,7 +44,7 @@ class TestHexQuestNoShuffle(TunicTestBase): options.AbilityShuffling.internal_name: options.AbilityShuffling.option_false} # test that you can get the item behind the overworld hc door with nothing and no ability shuffle - def test_hc_door_no_shuffle(self): + def test_hc_door_no_shuffle(self) -> None: self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) @@ -52,7 +52,7 @@ class TestNormalGoal(TunicTestBase): options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_false} # test that you need the three colored hexes to reach the Heir in standard - def test_normal_goal(self): + def test_normal_goal(self) -> None: location = ["The Heir"] items = [["Red Questagon", "Blue Questagon", "Green Questagon"]] self.assertAccessDependency(location, items) @@ -63,7 +63,7 @@ class TestER(TunicTestBase): options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, options.HexagonQuest.internal_name: options.HexagonQuest.option_false} - def test_overworld_hc_chest(self): + def test_overworld_hc_chest(self) -> None: # test to see that static connections are working properly -- this chest requires holy cross and is in Overworld self.assertFalse(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross")) self.collect_by_name(["Pages 42-43 (Holy Cross)"]) diff --git a/worlds/undertale/Locations.py b/worlds/undertale/Locations.py index 2f7de44512..5b45af63a9 100644 --- a/worlds/undertale/Locations.py +++ b/worlds/undertale/Locations.py @@ -10,10 +10,6 @@ class AdvData(typing.NamedTuple): class UndertaleAdvancement(Location): game: str = "Undertale" - def __init__(self, player: int, name: str, address: typing.Optional[int], parent): - super().__init__(player, name, address, parent) - self.event = not address - advancement_table = { "Snowman": AdvData(79100, "Snowdin Forest"), diff --git a/worlds/wargroove/__init__.py b/worlds/wargroove/__init__.py index ab4a9364fa..abca210b2d 100644 --- a/worlds/wargroove/__init__.py +++ b/worlds/wargroove/__init__.py @@ -131,12 +131,6 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi class WargrooveLocation(Location): game: str = "Wargroove" - def __init__(self, player: int, name: str, address=None, parent=None): - super(WargrooveLocation, self).__init__(player, name, address, parent) - if address is None: - self.event = True - self.locked = True - class WargrooveItem(Item): game = "Wargroove" diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 88de0f3134..a9c611acbe 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -2,24 +2,26 @@ Archipelago init file for The Witness """ import dataclasses +from logging import error, warning +from typing import Any, Dict, List, Optional, cast + +from BaseClasses import CollectionState, Entrance, Location, Region, Tutorial -from typing import Dict, Optional, cast -from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial, CollectionState from Options import PerGameCommonOptions, Toggle -from .presets import witness_option_presets -from worlds.AutoWorld import World, WebWorld -from .player_logic import WitnessPlayerLogic -from .static_logic import StaticWitnessLogic, ItemCategory, DoorItemDefinition -from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \ - get_priority_hint_items, make_always_and_priority_hints, generate_joke_hints, make_area_hints, get_hintable_areas, \ - make_extra_location_hints, create_all_hints, make_laser_hints, make_compact_hint_data, CompactItemData -from .locations import WitnessPlayerLocations, StaticWitnessLocations -from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData -from .regions import WitnessRegions -from .rules import set_rules +from worlds.AutoWorld import WebWorld, World + +from .data import static_items as static_witness_items +from .data import static_logic as static_witness_logic +from .data.item_definition_classes import DoorItemDefinition, ItemData +from .data.utils import get_audio_logs +from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints +from .locations import WitnessPlayerLocations, static_witness_locations from .options import TheWitnessOptions -from .utils import get_audio_logs, get_laser_shuffle -from logging import warning, error +from .player_items import WitnessItem, WitnessPlayerItems +from .player_logic import WitnessPlayerLogic +from .presets import witness_option_presets +from .regions import WitnessPlayerRegions +from .rules import set_rules class WitnessWebWorld(WebWorld): @@ -50,46 +52,43 @@ class WitnessWorld(World): options: TheWitnessOptions item_name_to_id = { - name: data.ap_code for name, data in StaticWitnessItems.item_data.items() + name: data.ap_code for name, data in static_witness_items.ITEM_DATA.items() } - location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID - item_name_groups = StaticWitnessItems.item_groups - location_name_groups = StaticWitnessLocations.AREA_LOCATION_GROUPS + location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID + item_name_groups = static_witness_items.ITEM_GROUPS + location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS required_client_version = (0, 4, 5) - def __init__(self, multiworld: "MultiWorld", player: int): - super().__init__(multiworld, player) + player_logic: WitnessPlayerLogic + player_locations: WitnessPlayerLocations + player_items: WitnessPlayerItems + player_regions: WitnessPlayerRegions - self.player_logic = None - self.locat = None - self.items = None - self.regio = None + log_ids_to_hints: Dict[int, CompactItemData] + laser_ids_to_hints: Dict[int, CompactItemData] - self.log_ids_to_hints: Dict[int, CompactItemData] = dict() - self.laser_ids_to_hints: Dict[int, CompactItemData] = dict() + items_placed_early: List[str] + own_itempool: List[WitnessItem] - self.items_placed_early = [] - self.own_itempool = [] - - def _get_slot_data(self): + def _get_slot_data(self) -> Dict[str, Any]: return { - 'seed': self.random.randrange(0, 1000000), - 'victory_location': int(self.player_logic.VICTORY_LOCATION, 16), - 'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID, - 'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(), - 'door_hexes_in_the_pool': self.items.get_door_ids_in_pool(), - 'symbols_not_in_the_game': self.items.get_symbol_ids_not_in_pool(), - 'disabled_entities': [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES], - 'log_ids_to_hints': self.log_ids_to_hints, - 'laser_ids_to_hints': self.laser_ids_to_hints, - 'progressive_item_lists': self.items.get_progressive_item_ids_in_pool(), - 'obelisk_side_id_to_EPs': StaticWitnessLogic.OBELISK_SIDE_ID_TO_EP_HEXES, - 'precompleted_puzzles': [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS], - 'entity_to_name': StaticWitnessLogic.ENTITY_ID_TO_NAME, + "seed": self.random.randrange(0, 1000000), + "victory_location": int(self.player_logic.VICTORY_LOCATION, 16), + "panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID, + "item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(), + "door_hexes_in_the_pool": self.player_items.get_door_ids_in_pool(), + "symbols_not_in_the_game": self.player_items.get_symbol_ids_not_in_pool(), + "disabled_entities": [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES], + "log_ids_to_hints": self.log_ids_to_hints, + "laser_ids_to_hints": self.laser_ids_to_hints, + "progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(), + "obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES, + "precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS], + "entity_to_name": static_witness_logic.ENTITY_ID_TO_NAME, } - def determine_sufficient_progression(self): + def determine_sufficient_progression(self) -> None: """ Determine whether there are enough progression items in this world to consider it "interactive". In the case of singleplayer, this just outputs a warning. @@ -127,20 +126,20 @@ class WitnessWorld(World): elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1: raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough" f" progression items that can be placed in other players' worlds. Please turn on Symbol" - f" Shuffle, Door Shuffle or Obelisk Keys.") + f" Shuffle, Door Shuffle, or Obelisk Keys.") - def generate_early(self): + def generate_early(self) -> None: disabled_locations = self.options.exclude_locations.value self.player_logic = WitnessPlayerLogic( self, disabled_locations, self.options.start_inventory.value ) - self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic) - self.items: WitnessPlayerItems = WitnessPlayerItems( - self, self.player_logic, self.locat + self.player_locations: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic) + self.player_items: WitnessPlayerItems = WitnessPlayerItems( + self, self.player_logic, self.player_locations ) - self.regio: WitnessRegions = WitnessRegions(self.locat, self) + self.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self) self.log_ids_to_hints = dict() @@ -149,22 +148,27 @@ class WitnessWorld(World): if self.options.shuffle_lasers == "local": self.options.local_items.value |= self.item_name_groups["Lasers"] - def create_regions(self): - self.regio.create_regions(self, self.player_logic) + def create_regions(self) -> None: + self.player_regions.create_regions(self, self.player_logic) # Set rules early so extra locations can be created based on the results of exploring collection states set_rules(self) + # Start creating items + + self.items_placed_early = [] + self.own_itempool = [] + # Add event items and tie them to event locations (e.g. laser activations). event_locations = [] - for event_location in self.locat.EVENT_LOCATION_TABLE: + for event_location in self.player_locations.EVENT_LOCATION_TABLE: item_obj = self.create_item( self.player_logic.EVENT_ITEM_PAIRS[event_location] ) - location_obj = self.multiworld.get_location(event_location, self.player) + location_obj = self.get_location(event_location) location_obj.place_locked_item(item_obj) self.own_itempool.append(item_obj) @@ -172,14 +176,16 @@ class WitnessWorld(World): # Place other locked items dog_puzzle_skip = self.create_item("Puzzle Skip") - self.multiworld.get_location("Town Pet the Dog", self.player).place_locked_item(dog_puzzle_skip) + self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip) self.own_itempool.append(dog_puzzle_skip) self.items_placed_early.append("Puzzle Skip") # Pick an early item to place on the tutorial gate. - early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()] + early_items = [ + item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items() + ] if early_items: random_early_item = self.random.choice(early_items) if self.options.puzzle_randomization == "sigma_expert": @@ -188,7 +194,7 @@ class WitnessWorld(World): else: # Force the item onto the tutorial gate check and remove it from our random pool. gate_item = self.create_item(random_early_item) - self.multiworld.get_location("Tutorial Gate Open", self.player).place_locked_item(gate_item) + self.get_location("Tutorial Gate Open").place_locked_item(gate_item) self.own_itempool.append(gate_item) self.items_placed_early.append(random_early_item) @@ -223,19 +229,19 @@ class WitnessWorld(World): break region, loc = extra_checks.pop(0) - self.locat.add_location_late(loc) - self.multiworld.get_region(region, self.player).add_locations({loc: self.location_name_to_id[loc]}) + self.player_locations.add_location_late(loc) + self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}) player = self.multiworld.get_player_name(self.player) - + warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""") - def create_items(self): + def create_items(self) -> None: # Determine pool size. - pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) + pool_size = len(self.player_locations.CHECK_LOCATION_TABLE) - len(self.player_locations.EVENT_LOCATION_TABLE) # Fill mandatory items and remove precollected and/or starting items from the pool. - item_pool: Dict[str, int] = self.items.get_mandatory_items() + item_pool = self.player_items.get_mandatory_items() # Remove one copy of each item that was placed early for already_placed in self.items_placed_early: @@ -283,7 +289,7 @@ class WitnessWorld(World): # Add junk items. if remaining_item_slots > 0: - item_pool.update(self.items.get_filler_items(remaining_item_slots)) + item_pool.update(self.player_items.get_filler_items(remaining_item_slots)) # Generate the actual items. for item_name, quantity in sorted(item_pool.items()): @@ -291,32 +297,28 @@ class WitnessWorld(World): self.own_itempool += new_items self.multiworld.itempool += new_items - if self.items.item_data[item_name].local_only: + if self.player_items.item_data[item_name].local_only: self.options.local_items.value.add(item_name) def fill_slot_data(self) -> dict: + self.log_ids_to_hints: Dict[int, CompactItemData] = dict() + self.laser_ids_to_hints: Dict[int, CompactItemData] = dict() + already_hinted_locations = set() # Laser hints if self.options.laser_hints: - laser_hints = make_laser_hints(self, StaticWitnessItems.item_groups["Lasers"]) + laser_hints = make_laser_hints(self, static_witness_items.ITEM_GROUPS["Lasers"]) for item_name, hint in laser_hints.items(): - item_def = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]) + item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]) self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player) already_hinted_locations.add(hint.location) # Audio Log Hints hint_amount = self.options.hint_amount.value - - credits_hint = ( - "This Randomizer is brought to you by\n" - "NewSoupVi, Jarno, blastron,\n" - "jbzdarkid, sigma144, IHNN, oddGarrett, Exempt-Medic.", -1, -1 - ) - audio_logs = get_audio_logs().copy() if hint_amount: @@ -335,15 +337,8 @@ class WitnessWorld(World): audio_log = audio_logs.pop() self.log_ids_to_hints[int(audio_log, 16)] = compact_hint_data - if audio_logs: - audio_log = audio_logs.pop() - self.log_ids_to_hints[int(audio_log, 16)] = credits_hint - - joke_hints = generate_joke_hints(self, len(audio_logs)) - - while audio_logs: - audio_log = audio_logs.pop() - self.log_ids_to_hints[int(audio_log, 16)] = joke_hints.pop() + # Client will generate joke hints for these. + self.log_ids_to_hints.update({int(audio_log, 16): ("", -1, -1) for audio_log in audio_logs}) # Options for the client & auto-tracker @@ -356,18 +351,18 @@ class WitnessWorld(World): return slot_data - def create_item(self, item_name: str) -> Item: + def create_item(self, item_name: str) -> WitnessItem: # If the player's plando options are malformed, the item_name parameter could be a dictionary containing the # name of the item, rather than the item itself. This is a workaround to prevent a crash. - if type(item_name) is dict: - item_name = list(item_name.keys())[0] + if isinstance(item_name, dict): + item_name = next(iter(item_name)) # this conditional is purely for unit tests, which need to be able to create an item before generate_early item_data: ItemData - if hasattr(self, 'items') and self.items and item_name in self.items.item_data: - item_data = self.items.item_data[item_name] + if hasattr(self, "player_items") and self.player_items and item_name in self.player_items.item_data: + item_data = self.player_items.item_data[item_name] else: - item_data = StaticWitnessItems.item_data[item_name] + item_data = static_witness_items.ITEM_DATA[item_name] return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player) @@ -382,12 +377,13 @@ class WitnessLocation(Location): game: str = "The Witness" entity_hex: int = -1 - def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1): + def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1) -> None: super().__init__(player, name, address, parent) self.entity_hex = ch_hex -def create_region(world: WitnessWorld, name: str, locat: WitnessPlayerLocations, region_locations=None, exits=None): +def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations, + region_locations=None, exits=None) -> Region: """ Create an Archipelago Region for The Witness """ @@ -395,12 +391,12 @@ def create_region(world: WitnessWorld, name: str, locat: WitnessPlayerLocations, ret = Region(name, world.player, world.multiworld) if region_locations: for location in region_locations: - loc_id = locat.CHECK_LOCATION_TABLE[location] + loc_id = player_locations.CHECK_LOCATION_TABLE[location] entity_hex = -1 - if location in StaticWitnessLogic.ENTITIES_BY_NAME: + if location in static_witness_logic.ENTITIES_BY_NAME: entity_hex = int( - StaticWitnessLogic.ENTITIES_BY_NAME[location]["entity_hex"], 0 + static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0 ) location = WitnessLocation( world.player, location, loc_id, ret, entity_hex diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/data/WitnessItems.txt similarity index 99% rename from worlds/witness/WitnessItems.txt rename to worlds/witness/data/WitnessItems.txt index 28dc4a4d97..86567f29e2 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/data/WitnessItems.txt @@ -72,7 +72,7 @@ Doors: 1164 - Town RGB Control (Panel) - 0x334D8 1166 - Town Maze Stairs (Panel) - 0x28A79 1167 - Town Maze Rooftop Bridge (Panel) - 0x2896A -1169 - Town Windmill Entry (Panel) - 0x17F5F +1169 - Windmill Entry (Panel) - 0x17F5F 1172 - Town Cargo Box Entry (Panel) - 0x0A0C8 1173 - Town Desert Laser Redirect Control (Panel) - 0x09F98 1182 - Windmill Turn Control (Panel) - 0x17D02 @@ -159,7 +159,7 @@ Doors: 1723 - Town RGB House Entry (Door) - 0x28A61 1726 - Town Church Entry (Door) - 0x03BB0 1729 - Town Maze Stairs (Door) - 0x28AA2 -1732 - Town Windmill Entry (Door) - 0x1845B +1732 - Windmill Entry (Door) - 0x1845B 1735 - Town RGB House Stairs (Door) - 0x2897B 1738 - Town Tower Second (Door) - 0x27798 1741 - Town Tower First (Door) - 0x27799 diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt similarity index 100% rename from worlds/witness/WitnessLogic.txt rename to worlds/witness/data/WitnessLogic.txt diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt similarity index 100% rename from worlds/witness/WitnessLogicExpert.txt rename to worlds/witness/data/WitnessLogicExpert.txt diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt similarity index 100% rename from worlds/witness/WitnessLogicVanilla.txt rename to worlds/witness/data/WitnessLogicVanilla.txt diff --git a/worlds/witness/data/__init__.py b/worlds/witness/data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/witness/data/item_definition_classes.py b/worlds/witness/data/item_definition_classes.py new file mode 100644 index 0000000000..b095a83abe --- /dev/null +++ b/worlds/witness/data/item_definition_classes.py @@ -0,0 +1,59 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Dict, List, Optional + +from BaseClasses import ItemClassification + + +class ItemCategory(Enum): + SYMBOL = 0 + DOOR = 1 + LASER = 2 + USEFUL = 3 + FILLER = 4 + TRAP = 5 + JOKE = 6 + EVENT = 7 + + +CATEGORY_NAME_MAPPINGS: Dict[str, ItemCategory] = { + "Symbols:": ItemCategory.SYMBOL, + "Doors:": ItemCategory.DOOR, + "Lasers:": ItemCategory.LASER, + "Useful:": ItemCategory.USEFUL, + "Filler:": ItemCategory.FILLER, + "Traps:": ItemCategory.TRAP, + "Jokes:": ItemCategory.JOKE +} + + +@dataclass(frozen=True) +class ItemDefinition: + local_code: int + category: ItemCategory + + +@dataclass(frozen=True) +class ProgressiveItemDefinition(ItemDefinition): + child_item_names: List[str] + + +@dataclass(frozen=True) +class DoorItemDefinition(ItemDefinition): + panel_id_hexes: List[str] + + +@dataclass(frozen=True) +class WeightedItemDefinition(ItemDefinition): + weight: int + + +@dataclass() +class ItemData: + """ + ItemData for an item in The Witness + """ + ap_code: Optional[int] + definition: ItemDefinition + classification: ItemClassification + local_only: bool = False diff --git a/worlds/witness/settings/Audio_Logs.txt b/worlds/witness/data/settings/Audio_Logs.txt similarity index 100% rename from worlds/witness/settings/Audio_Logs.txt rename to worlds/witness/data/settings/Audio_Logs.txt diff --git a/worlds/witness/settings/Door_Shuffle/Boat.txt b/worlds/witness/data/settings/Door_Shuffle/Boat.txt similarity index 100% rename from worlds/witness/settings/Door_Shuffle/Boat.txt rename to worlds/witness/data/settings/Door_Shuffle/Boat.txt diff --git a/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Complex_Additional_Panels.txt similarity index 100% rename from worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt rename to worlds/witness/data/settings/Door_Shuffle/Complex_Additional_Panels.txt diff --git a/worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt similarity index 97% rename from worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt rename to worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt index 70223bd749..63d8a58d26 100644 --- a/worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt +++ b/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt @@ -19,7 +19,7 @@ Monastery Entry Right (Panel) Town RGB House Entry (Panel) Town Church Entry (Panel) Town Maze Stairs (Panel) -Town Windmill Entry (Panel) +Windmill Entry (Panel) Town Cargo Box Entry (Panel) Theater Entry (Panel) Theater Exit (Panel) diff --git a/worlds/witness/settings/Door_Shuffle/Complex_Doors.txt b/worlds/witness/data/settings/Door_Shuffle/Complex_Doors.txt similarity index 99% rename from worlds/witness/settings/Door_Shuffle/Complex_Doors.txt rename to worlds/witness/data/settings/Door_Shuffle/Complex_Doors.txt index 87ec69f59c..7c81fd3472 100644 --- a/worlds/witness/settings/Door_Shuffle/Complex_Doors.txt +++ b/worlds/witness/data/settings/Door_Shuffle/Complex_Doors.txt @@ -49,7 +49,7 @@ Town Wooden Roof Stairs (Door) Town RGB House Entry (Door) Town Church Entry (Door) Town Maze Stairs (Door) -Town Windmill Entry (Door) +Windmill Entry (Door) Town RGB House Stairs (Door) Town Tower Second (Door) Town Tower First (Door) diff --git a/worlds/witness/settings/Door_Shuffle/Elevators_Come_To_You.txt b/worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt similarity index 100% rename from worlds/witness/settings/Door_Shuffle/Elevators_Come_To_You.txt rename to worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt diff --git a/worlds/witness/settings/Door_Shuffle/Obelisk_Keys.txt b/worlds/witness/data/settings/Door_Shuffle/Obelisk_Keys.txt similarity index 100% rename from worlds/witness/settings/Door_Shuffle/Obelisk_Keys.txt rename to worlds/witness/data/settings/Door_Shuffle/Obelisk_Keys.txt diff --git a/worlds/witness/settings/Door_Shuffle/Simple_Additional_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Simple_Additional_Panels.txt similarity index 100% rename from worlds/witness/settings/Door_Shuffle/Simple_Additional_Panels.txt rename to worlds/witness/data/settings/Door_Shuffle/Simple_Additional_Panels.txt diff --git a/worlds/witness/settings/Door_Shuffle/Simple_Doors.txt b/worlds/witness/data/settings/Door_Shuffle/Simple_Doors.txt similarity index 100% rename from worlds/witness/settings/Door_Shuffle/Simple_Doors.txt rename to worlds/witness/data/settings/Door_Shuffle/Simple_Doors.txt diff --git a/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt similarity index 100% rename from worlds/witness/settings/Door_Shuffle/Simple_Panels.txt rename to worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt diff --git a/worlds/witness/settings/EP_Shuffle/EP_All.txt b/worlds/witness/data/settings/EP_Shuffle/EP_All.txt similarity index 100% rename from worlds/witness/settings/EP_Shuffle/EP_All.txt rename to worlds/witness/data/settings/EP_Shuffle/EP_All.txt diff --git a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt b/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt similarity index 100% rename from worlds/witness/settings/EP_Shuffle/EP_Easy.txt rename to worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt diff --git a/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt b/worlds/witness/data/settings/EP_Shuffle/EP_NoEclipse.txt similarity index 100% rename from worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt rename to worlds/witness/data/settings/EP_Shuffle/EP_NoEclipse.txt diff --git a/worlds/witness/settings/EP_Shuffle/EP_Sides.txt b/worlds/witness/data/settings/EP_Shuffle/EP_Sides.txt similarity index 100% rename from worlds/witness/settings/EP_Shuffle/EP_Sides.txt rename to worlds/witness/data/settings/EP_Shuffle/EP_Sides.txt diff --git a/worlds/witness/settings/Early_Caves.txt b/worlds/witness/data/settings/Early_Caves.txt similarity index 100% rename from worlds/witness/settings/Early_Caves.txt rename to worlds/witness/data/settings/Early_Caves.txt diff --git a/worlds/witness/settings/Early_Caves_Start.txt b/worlds/witness/data/settings/Early_Caves_Start.txt similarity index 100% rename from worlds/witness/settings/Early_Caves_Start.txt rename to worlds/witness/data/settings/Early_Caves_Start.txt diff --git a/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt b/worlds/witness/data/settings/Exclusions/Disable_Unrandomized.txt similarity index 100% rename from worlds/witness/settings/Exclusions/Disable_Unrandomized.txt rename to worlds/witness/data/settings/Exclusions/Disable_Unrandomized.txt diff --git a/worlds/witness/settings/Exclusions/Discards.txt b/worlds/witness/data/settings/Exclusions/Discards.txt similarity index 100% rename from worlds/witness/settings/Exclusions/Discards.txt rename to worlds/witness/data/settings/Exclusions/Discards.txt diff --git a/worlds/witness/settings/Exclusions/Vaults.txt b/worlds/witness/data/settings/Exclusions/Vaults.txt similarity index 100% rename from worlds/witness/settings/Exclusions/Vaults.txt rename to worlds/witness/data/settings/Exclusions/Vaults.txt diff --git a/worlds/witness/settings/Laser_Shuffle.txt b/worlds/witness/data/settings/Laser_Shuffle.txt similarity index 100% rename from worlds/witness/settings/Laser_Shuffle.txt rename to worlds/witness/data/settings/Laser_Shuffle.txt diff --git a/worlds/witness/settings/Postgame/Beyond_Challenge.txt b/worlds/witness/data/settings/Postgame/Beyond_Challenge.txt similarity index 100% rename from worlds/witness/settings/Postgame/Beyond_Challenge.txt rename to worlds/witness/data/settings/Postgame/Beyond_Challenge.txt diff --git a/worlds/witness/settings/Postgame/Bottom_Floor_Discard.txt b/worlds/witness/data/settings/Postgame/Bottom_Floor_Discard.txt similarity index 100% rename from worlds/witness/settings/Postgame/Bottom_Floor_Discard.txt rename to worlds/witness/data/settings/Postgame/Bottom_Floor_Discard.txt diff --git a/worlds/witness/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt b/worlds/witness/data/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt similarity index 100% rename from worlds/witness/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt rename to worlds/witness/data/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt diff --git a/worlds/witness/settings/Postgame/Caves.txt b/worlds/witness/data/settings/Postgame/Caves.txt similarity index 100% rename from worlds/witness/settings/Postgame/Caves.txt rename to worlds/witness/data/settings/Postgame/Caves.txt diff --git a/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt b/worlds/witness/data/settings/Postgame/Challenge_Vault_Box.txt similarity index 100% rename from worlds/witness/settings/Postgame/Challenge_Vault_Box.txt rename to worlds/witness/data/settings/Postgame/Challenge_Vault_Box.txt diff --git a/worlds/witness/settings/Postgame/Mountain_Lower.txt b/worlds/witness/data/settings/Postgame/Mountain_Lower.txt similarity index 100% rename from worlds/witness/settings/Postgame/Mountain_Lower.txt rename to worlds/witness/data/settings/Postgame/Mountain_Lower.txt diff --git a/worlds/witness/settings/Postgame/Mountain_Upper.txt b/worlds/witness/data/settings/Postgame/Mountain_Upper.txt similarity index 100% rename from worlds/witness/settings/Postgame/Mountain_Upper.txt rename to worlds/witness/data/settings/Postgame/Mountain_Upper.txt diff --git a/worlds/witness/settings/Postgame/Path_To_Challenge.txt b/worlds/witness/data/settings/Postgame/Path_To_Challenge.txt similarity index 100% rename from worlds/witness/settings/Postgame/Path_To_Challenge.txt rename to worlds/witness/data/settings/Postgame/Path_To_Challenge.txt diff --git a/worlds/witness/settings/Symbol_Shuffle.txt b/worlds/witness/data/settings/Symbol_Shuffle.txt similarity index 100% rename from worlds/witness/settings/Symbol_Shuffle.txt rename to worlds/witness/data/settings/Symbol_Shuffle.txt diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py new file mode 100644 index 0000000000..8eb889f820 --- /dev/null +++ b/worlds/witness/data/static_items.py @@ -0,0 +1,56 @@ +from typing import Dict, List + +from BaseClasses import ItemClassification + +from . import static_logic as static_witness_logic +from .item_definition_classes import DoorItemDefinition, ItemCategory, ItemData +from .static_locations import ID_START + +ITEM_DATA: Dict[str, ItemData] = {} +ITEM_GROUPS: Dict[str, List[str]] = {} + +# Useful items that are treated specially at generation time and should not be automatically added to the player's +# item list during get_progression_items. +_special_usefuls: List[str] = ["Puzzle Skip"] + + +def populate_items() -> None: + for item_name, definition in static_witness_logic.ALL_ITEMS.items(): + ap_item_code = definition.local_code + ID_START + classification: ItemClassification = ItemClassification.filler + local_only: bool = False + + if definition.category is ItemCategory.SYMBOL: + classification = ItemClassification.progression + ITEM_GROUPS.setdefault("Symbols", []).append(item_name) + elif definition.category is ItemCategory.DOOR: + classification = ItemClassification.progression + ITEM_GROUPS.setdefault("Doors", []).append(item_name) + elif definition.category is ItemCategory.LASER: + classification = ItemClassification.progression_skip_balancing + ITEM_GROUPS.setdefault("Lasers", []).append(item_name) + elif definition.category is ItemCategory.USEFUL: + classification = ItemClassification.useful + elif definition.category is ItemCategory.FILLER: + if item_name in ["Energy Fill (Small)"]: + local_only = True + classification = ItemClassification.filler + elif definition.category is ItemCategory.TRAP: + classification = ItemClassification.trap + elif definition.category is ItemCategory.JOKE: + classification = ItemClassification.filler + + ITEM_DATA[item_name] = ItemData(ap_item_code, definition, + classification, local_only) + + +def get_item_to_door_mappings() -> Dict[int, List[int]]: + output: Dict[int, List[int]] = {} + for item_name, item_data in ITEM_DATA.items(): + if not isinstance(item_data.definition, DoorItemDefinition): + continue + output[item_data.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] + return output + + +populate_items() diff --git a/worlds/witness/data/static_locations.py b/worlds/witness/data/static_locations.py new file mode 100644 index 0000000000..e11544235f --- /dev/null +++ b/worlds/witness/data/static_locations.py @@ -0,0 +1,482 @@ +from . import static_logic as static_witness_logic + +ID_START = 158000 + +GENERAL_LOCATIONS = { + "Tutorial Front Left", + "Tutorial Back Left", + "Tutorial Back Right", + "Tutorial Patio Floor", + "Tutorial Gate Open", + + "Outside Tutorial Vault Box", + "Outside Tutorial Discard", + "Outside Tutorial Shed Row 5", + "Outside Tutorial Tree Row 9", + "Outside Tutorial Outpost Entry Panel", + "Outside Tutorial Outpost Exit Panel", + + "Glass Factory Discard", + "Glass Factory Back Wall 5", + "Glass Factory Front 3", + "Glass Factory Melting 3", + + "Symmetry Island Lower Panel", + "Symmetry Island Right 5", + "Symmetry Island Back 6", + "Symmetry Island Left 7", + "Symmetry Island Upper Panel", + "Symmetry Island Scenery Outlines 5", + "Symmetry Island Laser Yellow 3", + "Symmetry Island Laser Blue 3", + "Symmetry Island Laser Panel", + + "Orchard Apple Tree 5", + + "Desert Vault Box", + "Desert Discard", + "Desert Surface 8", + "Desert Light Room 3", + "Desert Pond Room 5", + "Desert Flood Room 6", + "Desert Elevator Room Hexagonal", + "Desert Elevator Room Bent 3", + "Desert Laser Panel", + + "Quarry Entry 1 Panel", + "Quarry Entry 2 Panel", + "Quarry Stoneworks Entry Left Panel", + "Quarry Stoneworks Entry Right Panel", + "Quarry Stoneworks Lower Row 6", + "Quarry Stoneworks Upper Row 8", + "Quarry Stoneworks Control Room Left", + "Quarry Stoneworks Control Room Right", + "Quarry Stoneworks Stairs Panel", + "Quarry Boathouse Intro Right", + "Quarry Boathouse Intro Left", + "Quarry Boathouse Front Row 5", + "Quarry Boathouse Back First Row 9", + "Quarry Boathouse Back Second Row 3", + "Quarry Discard", + "Quarry Laser Panel", + + "Shadows Intro 8", + "Shadows Far 8", + "Shadows Near 5", + "Shadows Laser Panel", + + "Keep Hedge Maze 1", + "Keep Hedge Maze 2", + "Keep Hedge Maze 3", + "Keep Hedge Maze 4", + "Keep Pressure Plates 1", + "Keep Pressure Plates 2", + "Keep Pressure Plates 3", + "Keep Pressure Plates 4", + "Keep Discard", + "Keep Laser Panel Hedges", + "Keep Laser Panel Pressure Plates", + + "Shipwreck Vault Box", + "Shipwreck Discard", + + "Monastery Outside 3", + "Monastery Inside 4", + "Monastery Laser Panel", + + "Town Cargo Box Entry Panel", + "Town Cargo Box Discard", + "Town Tall Hexagonal", + "Town Church Entry Panel", + "Town Church Lattice", + "Town Maze Panel", + "Town Rooftop Discard", + "Town Red Rooftop 5", + "Town Wooden Roof Lower Row 5", + "Town Wooden Rooftop", + "Windmill Entry Panel", + "Town RGB House Entry Panel", + "Town Laser Panel", + + "Town RGB House Upstairs Left", + "Town RGB House Upstairs Right", + "Town RGB House Sound Room Right", + + "Windmill Theater Entry Panel", + "Theater Exit Left Panel", + "Theater Exit Right Panel", + "Theater Tutorial Video", + "Theater Desert Video", + "Theater Jungle Video", + "Theater Shipwreck Video", + "Theater Mountain Video", + "Theater Discard", + + "Jungle Discard", + "Jungle First Row 3", + "Jungle Second Row 4", + "Jungle Popup Wall 6", + "Jungle Laser Panel", + + "Jungle Vault Box", + "Jungle Monastery Garden Shortcut Panel", + + "Bunker Entry Panel", + "Bunker Intro Left 5", + "Bunker Intro Back 4", + "Bunker Glass Room 3", + "Bunker UV Room 2", + "Bunker Laser Panel", + + "Swamp Entry Panel", + "Swamp Intro Front 6", + "Swamp Intro Back 8", + "Swamp Between Bridges Near Row 4", + "Swamp Cyan Underwater 5", + "Swamp Platform Row 4", + "Swamp Platform Shortcut Right Panel", + "Swamp Between Bridges Far Row 4", + "Swamp Red Underwater 4", + "Swamp Purple Underwater", + "Swamp Beyond Rotating Bridge 4", + "Swamp Blue Underwater 5", + "Swamp Laser Panel", + "Swamp Laser Shortcut Right Panel", + + "Treehouse First Door Panel", + "Treehouse Second Door Panel", + "Treehouse Third Door Panel", + "Treehouse Yellow Bridge 9", + "Treehouse First Purple Bridge 5", + "Treehouse Second Purple Bridge 7", + "Treehouse Green Bridge 7", + "Treehouse Green Bridge Discard", + "Treehouse Left Orange Bridge 15", + "Treehouse Laser Discard", + "Treehouse Right Orange Bridge 12", + "Treehouse Laser Panel", + "Treehouse Drawbridge Panel", + + "Mountainside Discard", + "Mountainside Vault Box", + "Mountaintop River Shape", + + "Tutorial First Hallway EP", + "Tutorial Cloud EP", + "Tutorial Patio Flowers EP", + "Tutorial Gate EP", + "Outside Tutorial Garden EP", + "Outside Tutorial Town Sewer EP", + "Outside Tutorial Path EP", + "Outside Tutorial Tractor EP", + "Mountainside Thundercloud EP", + "Glass Factory Vase EP", + "Symmetry Island Glass Factory Black Line Reflection EP", + "Symmetry Island Glass Factory Black Line EP", + "Desert Sand Snake EP", + "Desert Facade Right EP", + "Desert Facade Left EP", + "Desert Stairs Left EP", + "Desert Stairs Right EP", + "Desert Broken Wall Straight EP", + "Desert Broken Wall Bend EP", + "Desert Shore EP", + "Desert Island EP", + "Desert Pond Room Near Reflection EP", + "Desert Pond Room Far Reflection EP", + "Desert Flood Room EP", + "Desert Elevator EP", + "Quarry Shore EP", + "Quarry Entrance Pipe EP", + "Quarry Sand Pile EP", + "Quarry Rock Line EP", + "Quarry Rock Line Reflection EP", + "Quarry Railroad EP", + "Quarry Stoneworks Ramp EP", + "Quarry Stoneworks Lift EP", + "Quarry Boathouse Moving Ramp EP", + "Quarry Boathouse Hook EP", + "Shadows Quarry Stoneworks Rooftop Vent EP", + "Treehouse Beach Rock Shadow EP", + "Treehouse Beach Sand Shadow EP", + "Treehouse Beach Both Orange Bridges EP", + "Keep Red Flowers EP", + "Keep Purple Flowers EP", + "Shipwreck Circle Near EP", + "Shipwreck Circle Left EP", + "Shipwreck Circle Far EP", + "Shipwreck Stern EP", + "Shipwreck Rope Inner EP", + "Shipwreck Rope Outer EP", + "Shipwreck Couch EP", + "Keep Pressure Plates 1 EP", + "Keep Pressure Plates 2 EP", + "Keep Pressure Plates 3 EP", + "Keep Pressure Plates 4 Left Exit EP", + "Keep Pressure Plates 4 Right Exit EP", + "Keep Path EP", + "Keep Hedges EP", + "Monastery Facade Left Near EP", + "Monastery Facade Left Far Short EP", + "Monastery Facade Left Far Long EP", + "Monastery Facade Right Near EP", + "Monastery Facade Left Stairs EP", + "Monastery Facade Right Stairs EP", + "Monastery Grass Stairs EP", + "Monastery Left Shutter EP", + "Monastery Middle Shutter EP", + "Monastery Right Shutter EP", + "Windmill First Blade EP", + "Windmill Second Blade EP", + "Windmill Third Blade EP", + "Town Tower Underside Third EP", + "Town Tower Underside Fourth EP", + "Town Tower Underside First EP", + "Town Tower Underside Second EP", + "Town RGB House Red EP", + "Town RGB House Green EP", + "Town Maze Bridge Underside EP", + "Town Black Line Redirect EP", + "Town Black Line Church EP", + "Town Brown Bridge EP", + "Town Black Line Tower EP", + "Theater Eclipse EP", + "Theater Window EP", + "Theater Door EP", + "Theater Church EP", + "Jungle Long Arch Moss EP", + "Jungle Straight Left Moss EP", + "Jungle Pop-up Wall Moss EP", + "Jungle Short Arch Moss EP", + "Jungle Entrance EP", + "Jungle Tree Halo EP", + "Jungle Bamboo CCW EP", + "Jungle Bamboo CW EP", + "Jungle Green Leaf Moss EP", + "Monastery Garden Left EP", + "Monastery Garden Right EP", + "Monastery Wall EP", + "Bunker Tinted Door EP", + "Bunker Green Room Flowers EP", + "Swamp Purple Sand Middle EP", + "Swamp Purple Sand Top EP", + "Swamp Purple Sand Bottom EP", + "Swamp Sliding Bridge Left EP", + "Swamp Sliding Bridge Right EP", + "Swamp Cyan Underwater Sliding Bridge EP", + "Swamp Rotating Bridge CCW EP", + "Swamp Rotating Bridge CW EP", + "Swamp Boat EP", + "Swamp Long Bridge Side EP", + "Swamp Purple Underwater Right EP", + "Swamp Purple Underwater Left EP", + "Treehouse Buoy EP", + "Treehouse Right Orange Bridge EP", + "Treehouse Burned House Beach EP", + "Mountainside Cloud Cycle EP", + "Mountainside Bush EP", + "Mountainside Apparent River EP", + "Mountaintop River Shape EP", + "Mountaintop Arch Black EP", + "Mountaintop Arch White Right EP", + "Mountaintop Arch White Left EP", + "Mountain Bottom Floor Yellow Bridge EP", + "Mountain Bottom Floor Blue Bridge EP", + "Mountain Floor 2 Pink Bridge EP", + "Caves Skylight EP", + "Challenge Water EP", + "Tunnels Theater Flowers EP", + "Boat Desert EP", + "Boat Shipwreck CCW Underside EP", + "Boat Shipwreck Green EP", + "Boat Shipwreck CW Underside EP", + "Boat Bunker Yellow Line EP", + "Boat Town Long Sewer EP", + "Boat Tutorial EP", + "Boat Tutorial Reflection EP", + "Boat Tutorial Moss EP", + "Boat Cargo Box EP", + + "Desert Obelisk Side 1", + "Desert Obelisk Side 2", + "Desert Obelisk Side 3", + "Desert Obelisk Side 4", + "Desert Obelisk Side 5", + "Monastery Obelisk Side 1", + "Monastery Obelisk Side 2", + "Monastery Obelisk Side 3", + "Monastery Obelisk Side 4", + "Monastery Obelisk Side 5", + "Monastery Obelisk Side 6", + "Treehouse Obelisk Side 1", + "Treehouse Obelisk Side 2", + "Treehouse Obelisk Side 3", + "Treehouse Obelisk Side 4", + "Treehouse Obelisk Side 5", + "Treehouse Obelisk Side 6", + "Mountainside Obelisk Side 1", + "Mountainside Obelisk Side 2", + "Mountainside Obelisk Side 3", + "Mountainside Obelisk Side 4", + "Mountainside Obelisk Side 5", + "Mountainside Obelisk Side 6", + "Quarry Obelisk Side 1", + "Quarry Obelisk Side 2", + "Quarry Obelisk Side 3", + "Quarry Obelisk Side 4", + "Quarry Obelisk Side 5", + "Town Obelisk Side 1", + "Town Obelisk Side 2", + "Town Obelisk Side 3", + "Town Obelisk Side 4", + "Town Obelisk Side 5", + "Town Obelisk Side 6", + + "Caves Mountain Shortcut Panel", + "Caves Swamp Shortcut Panel", + + "Caves Blue Tunnel Right First 4", + "Caves Blue Tunnel Left First 1", + "Caves Blue Tunnel Left Second 5", + "Caves Blue Tunnel Right Second 5", + "Caves Blue Tunnel Right Third 1", + "Caves Blue Tunnel Left Fourth 1", + "Caves Blue Tunnel Left Third 1", + + "Caves First Floor Middle", + "Caves First Floor Right", + "Caves First Floor Left", + "Caves First Floor Grounded", + "Caves Lone Pillar", + "Caves First Wooden Beam", + "Caves Second Wooden Beam", + "Caves Third Wooden Beam", + "Caves Fourth Wooden Beam", + "Caves Right Upstairs Left Row 8", + "Caves Right Upstairs Right Row 3", + "Caves Left Upstairs Single", + "Caves Left Upstairs Left Row 5", + + "Caves Challenge Entry Panel", + "Challenge Tunnels Entry Panel", + + "Tunnels Vault Box", + "Theater Challenge Video", + + "Tunnels Town Shortcut Panel", + + "Caves Skylight EP", + "Challenge Water EP", + "Tunnels Theater Flowers EP", + "Tutorial Gate EP", + + "Mountaintop Mountain Entry Panel", + + "Mountain Floor 1 Light Bridge Controller", + + "Mountain Floor 1 Right Row 5", + "Mountain Floor 1 Left Row 7", + "Mountain Floor 1 Back Row 3", + "Mountain Floor 1 Trash Pillar 2", + "Mountain Floor 2 Near Row 5", + "Mountain Floor 2 Far Row 6", + + "Mountain Floor 2 Light Bridge Controller Near", + "Mountain Floor 2 Light Bridge Controller Far", + + "Mountain Bottom Floor Yellow Bridge EP", + "Mountain Bottom Floor Blue Bridge EP", + "Mountain Floor 2 Pink Bridge EP", + + "Mountain Floor 2 Elevator Discard", + "Mountain Bottom Floor Giant Puzzle", + + "Mountain Bottom Floor Pillars Room Entry Left", + "Mountain Bottom Floor Pillars Room Entry Right", + + "Mountain Bottom Floor Caves Entry Panel", + + "Mountain Bottom Floor Left Pillar 4", + "Mountain Bottom Floor Right Pillar 4", + + "Challenge Vault Box", + "Theater Challenge Video", + "Mountain Bottom Floor Discard", +} + +OBELISK_SIDES = { + "Desert Obelisk Side 1", + "Desert Obelisk Side 2", + "Desert Obelisk Side 3", + "Desert Obelisk Side 4", + "Desert Obelisk Side 5", + "Monastery Obelisk Side 1", + "Monastery Obelisk Side 2", + "Monastery Obelisk Side 3", + "Monastery Obelisk Side 4", + "Monastery Obelisk Side 5", + "Monastery Obelisk Side 6", + "Treehouse Obelisk Side 1", + "Treehouse Obelisk Side 2", + "Treehouse Obelisk Side 3", + "Treehouse Obelisk Side 4", + "Treehouse Obelisk Side 5", + "Treehouse Obelisk Side 6", + "Mountainside Obelisk Side 1", + "Mountainside Obelisk Side 2", + "Mountainside Obelisk Side 3", + "Mountainside Obelisk Side 4", + "Mountainside Obelisk Side 5", + "Mountainside Obelisk Side 6", + "Quarry Obelisk Side 1", + "Quarry Obelisk Side 2", + "Quarry Obelisk Side 3", + "Quarry Obelisk Side 4", + "Quarry Obelisk Side 5", + "Town Obelisk Side 1", + "Town Obelisk Side 2", + "Town Obelisk Side 3", + "Town Obelisk Side 4", + "Town Obelisk Side 5", + "Town Obelisk Side 6", +} + +ALL_LOCATIONS_TO_ID = dict() + +AREA_LOCATION_GROUPS = dict() + + +def get_id(entity_hex: str) -> str: + """ + Calculates the location ID for any given location + """ + + return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"] + + +def get_event_name(entity_hex: str) -> str: + """ + Returns the event name of any given panel. + """ + + action = " Opened" if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved" + + return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] + action + + +ALL_LOCATIONS_TO_IDS = { + panel_obj["checkName"]: get_id(chex) + for chex, panel_obj in static_witness_logic.ENTITIES_BY_HEX.items() + if panel_obj["id"] +} + +ALL_LOCATIONS_TO_IDS = dict( + sorted(ALL_LOCATIONS_TO_IDS.items(), key=lambda loc: loc[1]) +) + +for key, item in ALL_LOCATIONS_TO_IDS.items(): + ALL_LOCATIONS_TO_ID[key] = item + +for loc in ALL_LOCATIONS_TO_IDS: + area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"] + AREA_LOCATION_GROUPS.setdefault(area, []).append(loc) diff --git a/worlds/witness/static_logic.py b/worlds/witness/data/static_logic.py similarity index 51% rename from worlds/witness/static_logic.py rename to worlds/witness/data/static_logic.py index 3efab4915e..94e6f7a3cc 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -1,56 +1,26 @@ -from dataclasses import dataclass -from enum import Enum +from functools import lru_cache from typing import Dict, List -from .utils import define_new_region, parse_lambda, lazy, get_items, get_sigma_normal_logic, get_sigma_expert_logic,\ - get_vanilla_logic - - -class ItemCategory(Enum): - SYMBOL = 0 - DOOR = 1 - LASER = 2 - USEFUL = 3 - FILLER = 4 - TRAP = 5 - JOKE = 6 - EVENT = 7 - - -CATEGORY_NAME_MAPPINGS: Dict[str, ItemCategory] = { - "Symbols:": ItemCategory.SYMBOL, - "Doors:": ItemCategory.DOOR, - "Lasers:": ItemCategory.LASER, - "Useful:": ItemCategory.USEFUL, - "Filler:": ItemCategory.FILLER, - "Traps:": ItemCategory.TRAP, - "Jokes:": ItemCategory.JOKE -} - - -@dataclass(frozen=True) -class ItemDefinition: - local_code: int - category: ItemCategory - - -@dataclass(frozen=True) -class ProgressiveItemDefinition(ItemDefinition): - child_item_names: List[str] - - -@dataclass(frozen=True) -class DoorItemDefinition(ItemDefinition): - panel_id_hexes: List[str] - - -@dataclass(frozen=True) -class WeightedItemDefinition(ItemDefinition): - weight: int +from .item_definition_classes import ( + CATEGORY_NAME_MAPPINGS, + DoorItemDefinition, + ItemCategory, + ItemDefinition, + ProgressiveItemDefinition, + WeightedItemDefinition, +) +from .utils import ( + define_new_region, + get_items, + get_sigma_expert_logic, + get_sigma_normal_logic, + get_vanilla_logic, + parse_lambda, +) class StaticWitnessLogicObj: - def read_logic_file(self, lines): + def read_logic_file(self, lines) -> None: """ Reads the logic file and does the initial population of data structures """ @@ -152,7 +122,7 @@ class StaticWitnessLogicObj: } if location_type == "Obelisk Side": - eps = set(list(required_panels)[0]) + eps = set(next(iter(required_panels))) eps -= {"Theater to Tunnels"} eps_ints = {int(h, 16) for h in eps} @@ -177,7 +147,7 @@ class StaticWitnessLogicObj: current_region["panels"].append(entity_hex) - def __init__(self, lines=None): + def __init__(self, lines=None) -> None: if lines is None: lines = get_sigma_normal_logic() @@ -199,102 +169,95 @@ class StaticWitnessLogicObj: self.read_logic_file(lines) -class StaticWitnessLogic: - # Item data parsed from WitnessItems.txt - all_items: Dict[str, ItemDefinition] = {} - _progressive_lookup: Dict[str, str] = {} - - ALL_REGIONS_BY_NAME = dict() - ALL_AREAS_BY_NAME = dict() - STATIC_CONNECTIONS_BY_REGION_NAME = dict() - - OBELISK_SIDE_ID_TO_EP_HEXES = dict() - - ENTITIES_BY_HEX = dict() - ENTITIES_BY_NAME = dict() - STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() - - EP_TO_OBELISK_SIDE = dict() - - ENTITY_ID_TO_NAME = dict() - - @staticmethod - def parse_items(): - """ - Parses currently defined items from WitnessItems.txt - """ - - lines: List[str] = get_items() - current_category: ItemCategory = ItemCategory.SYMBOL - - for line in lines: - # Skip empty lines and comments. - if line == "" or line[0] == "#": - continue - - # If this line is a category header, update our cached category. - if line in CATEGORY_NAME_MAPPINGS.keys(): - current_category = CATEGORY_NAME_MAPPINGS[line] - continue - - line_split = line.split(" - ") - - item_code = int(line_split[0]) - item_name = line_split[1] - arguments: List[str] = line_split[2].split(",") if len(line_split) >= 3 else [] - - if current_category in [ItemCategory.DOOR, ItemCategory.LASER]: - # Map doors to IDs. - StaticWitnessLogic.all_items[item_name] = DoorItemDefinition(item_code, current_category, - arguments) - elif current_category == ItemCategory.TRAP or current_category == ItemCategory.FILLER: - # Read filler weights. - weight = int(arguments[0]) if len(arguments) >= 1 else 1 - StaticWitnessLogic.all_items[item_name] = WeightedItemDefinition(item_code, current_category, weight) - elif arguments: - # Progressive items. - StaticWitnessLogic.all_items[item_name] = ProgressiveItemDefinition(item_code, current_category, - arguments) - for child_item in arguments: - StaticWitnessLogic._progressive_lookup[child_item] = item_name - else: - StaticWitnessLogic.all_items[item_name] = ItemDefinition(item_code, current_category) - - @staticmethod - def get_parent_progressive_item(item_name: str): - """ - Returns the name of the item's progressive parent, if there is one, or the item's name if not. - """ - return StaticWitnessLogic._progressive_lookup.get(item_name, item_name) - - @lazy - def sigma_expert(self) -> StaticWitnessLogicObj: - return StaticWitnessLogicObj(get_sigma_expert_logic()) - - @lazy - def sigma_normal(self) -> StaticWitnessLogicObj: - return StaticWitnessLogicObj(get_sigma_normal_logic()) - - @lazy - def vanilla(self) -> StaticWitnessLogicObj: - return StaticWitnessLogicObj(get_vanilla_logic()) - - def __init__(self): - self.parse_items() - - self.ALL_REGIONS_BY_NAME.update(self.sigma_normal.ALL_REGIONS_BY_NAME) - self.ALL_AREAS_BY_NAME.update(self.sigma_normal.ALL_AREAS_BY_NAME) - self.STATIC_CONNECTIONS_BY_REGION_NAME.update(self.sigma_normal.STATIC_CONNECTIONS_BY_REGION_NAME) - - self.ENTITIES_BY_HEX.update(self.sigma_normal.ENTITIES_BY_HEX) - self.ENTITIES_BY_NAME.update(self.sigma_normal.ENTITIES_BY_NAME) - self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX.update(self.sigma_normal.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) - - self.OBELISK_SIDE_ID_TO_EP_HEXES.update(self.sigma_normal.OBELISK_SIDE_ID_TO_EP_HEXES) - - self.EP_TO_OBELISK_SIDE.update(self.sigma_normal.EP_TO_OBELISK_SIDE) - - self.ENTITY_ID_TO_NAME.update(self.sigma_normal.ENTITY_ID_TO_NAME) +# Item data parsed from WitnessItems.txt +ALL_ITEMS: Dict[str, ItemDefinition] = {} +_progressive_lookup: Dict[str, str] = {} -StaticWitnessLogic() +def parse_items() -> None: + """ + Parses currently defined items from WitnessItems.txt + """ + + lines: List[str] = get_items() + current_category: ItemCategory = ItemCategory.SYMBOL + + for line in lines: + # Skip empty lines and comments. + if line == "" or line[0] == "#": + continue + + # If this line is a category header, update our cached category. + if line in CATEGORY_NAME_MAPPINGS.keys(): + current_category = CATEGORY_NAME_MAPPINGS[line] + continue + + line_split = line.split(" - ") + + item_code = int(line_split[0]) + item_name = line_split[1] + arguments: List[str] = line_split[2].split(",") if len(line_split) >= 3 else [] + + if current_category in [ItemCategory.DOOR, ItemCategory.LASER]: + # Map doors to IDs. + ALL_ITEMS[item_name] = DoorItemDefinition(item_code, current_category, arguments) + elif current_category == ItemCategory.TRAP or current_category == ItemCategory.FILLER: + # Read filler weights. + weight = int(arguments[0]) if len(arguments) >= 1 else 1 + ALL_ITEMS[item_name] = WeightedItemDefinition(item_code, current_category, weight) + elif arguments: + # Progressive items. + ALL_ITEMS[item_name] = ProgressiveItemDefinition(item_code, current_category, arguments) + for child_item in arguments: + _progressive_lookup[child_item] = item_name + else: + ALL_ITEMS[item_name] = ItemDefinition(item_code, current_category) + + +def get_parent_progressive_item(item_name: str) -> str: + """ + Returns the name of the item's progressive parent, if there is one, or the item's name if not. + """ + return _progressive_lookup.get(item_name, item_name) + + +@lru_cache +def get_vanilla() -> StaticWitnessLogicObj: + return StaticWitnessLogicObj(get_vanilla_logic()) + + +@lru_cache +def get_sigma_normal() -> StaticWitnessLogicObj: + return StaticWitnessLogicObj(get_sigma_normal_logic()) + + +@lru_cache +def get_sigma_expert() -> StaticWitnessLogicObj: + return StaticWitnessLogicObj(get_sigma_expert_logic()) + + +def __getattr__(name): + if name == "vanilla": + return get_vanilla() + elif name == "sigma_normal": + return get_sigma_normal() + elif name == "sigma_expert": + return get_sigma_expert() + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +parse_items() + +ALL_REGIONS_BY_NAME = get_sigma_normal().ALL_REGIONS_BY_NAME +ALL_AREAS_BY_NAME = get_sigma_normal().ALL_AREAS_BY_NAME +STATIC_CONNECTIONS_BY_REGION_NAME = get_sigma_normal().STATIC_CONNECTIONS_BY_REGION_NAME + +ENTITIES_BY_HEX = get_sigma_normal().ENTITIES_BY_HEX +ENTITIES_BY_NAME = get_sigma_normal().ENTITIES_BY_NAME +STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = get_sigma_normal().STATIC_DEPENDENT_REQUIREMENTS_BY_HEX + +OBELISK_SIDE_ID_TO_EP_HEXES = get_sigma_normal().OBELISK_SIDE_ID_TO_EP_HEXES + +EP_TO_OBELISK_SIDE = get_sigma_normal().EP_TO_OBELISK_SIDE + +ENTITY_ID_TO_NAME = get_sigma_normal().ENTITY_ID_TO_NAME diff --git a/worlds/witness/utils.py b/worlds/witness/data/utils.py similarity index 93% rename from worlds/witness/utils.py rename to worlds/witness/data/utils.py index 43e039475d..bb89227ca3 100644 --- a/worlds/witness/utils.py +++ b/worlds/witness/data/utils.py @@ -1,11 +1,11 @@ from functools import lru_cache from math import floor -from typing import List, Collection, FrozenSet, Tuple, Dict, Any, Set from pkgutil import get_data from random import random +from typing import Any, Collection, Dict, FrozenSet, List, Set, Tuple -def weighted_sample(world_random: random, population: List, weights: List[float], k: int): +def weighted_sample(world_random: random, population: List, weights: List[float], k: int) -> List: positions = range(len(population)) indices = [] while True: @@ -95,25 +95,9 @@ def parse_lambda(lambda_string) -> FrozenSet[FrozenSet[str]]: return lambda_set -class lazy(object): - def __init__(self, func, name=None): - self.func = func - self.name = name if name is not None else func.__name__ - self.__doc__ = func.__doc__ - - def __get__(self, instance, class_): - if instance is None: - res = self.func(class_) - setattr(class_, self.name, res) - return res - res = self.func(instance) - setattr(instance, self.name, res) - return res - - @lru_cache(maxsize=None) def get_adjustment_file(adjustment_file: str) -> List[str]: - data = get_data(__name__, adjustment_file).decode('utf-8') + data = get_data(__name__, adjustment_file).decode("utf-8") return [line.strip() for line in data.split("\n")] diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 6ebf8eeec0..2863143893 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -1,190 +1,17 @@ import logging from dataclasses import dataclass -from typing import Tuple, List, TYPE_CHECKING, Set, Dict, Optional, Union -from BaseClasses import Item, ItemClassification, Location, LocationProgressType, CollectionState -from . import StaticWitnessLogic -from .utils import weighted_sample +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union + +from BaseClasses import CollectionState, Item, Location, LocationProgressType + +from .data import static_logic as static_witness_logic +from .data.utils import weighted_sample if TYPE_CHECKING: from . import WitnessWorld CompactItemData = Tuple[str, Union[str, int], int] -joke_hints = [ - "Have you tried Adventure?\n...Holy crud, that game is 17 years older than me.", - "Have you tried A Link to the Past?\nThe Archipelago game that started it all!", - "Waiting to get your items?\nTry BK Sudoku! Make progress even while stuck.", - "Have you tried Blasphemous?\nYou haven't? Blasphemy!\n...Sorry. You should try it, though!", - "Have you tried Bumper Stickers?\nDecades after its inception, people are still inventing unique twists on the match-3 genre.", - "Have you tried Bumper Stickers?\nMaybe after spending so much time on this island, you are longing for a simpler puzzle game.", - "Have you tried Celeste 64?\nYou need smol low-poly Madeline in your life. TRUST ME.", - "Have you tried ChecksFinder?\nIf you like puzzles, you might enjoy it!", - "Have you tried Clique?\nIt's certainly a lot less complicated than this game!", - "Have you tried Dark Souls III?\nA tough game like this feels better when friends are helping you!", - "Have you tried Donkey Kong Country 3?\nA legendary game from a golden age of platformers!", - "Have you tried DLC Quest?\nI know you all like parody games.\nI got way too many requests to make a randomizer for \"The Looker\".", - "Have you tried Doom?\nI wonder if a smart fridge can connect to Archipelago.", - "Have you tried Doom II?\nGot a good game on your hands? Just make it bigger and better.", - "Have you tried Factorio?\nAlone in an unknown multiworld. Sound familiar?", - "Have you tried Final Fantasy?\nExperience a classic game improved to fit modern standards!", - "Have you tried Final Fantasy Mystic Quest?\nApparently, it was made in an attempt to simplify Final Fantasy for the western market.\nThey were right, I suck at RPGs.", - "Have you tried Heretic?\nWait, there is a Doom Engine game where you can look UP AND DOWN???", - "Have you tried Hollow Knight?\nAnother independent hit revolutionising a genre!", - "Have you tried Hylics 2?\nStop motion might just be the epitome of unique art styles.", - "Have you tried Kirby's Dream Land 3?\nAll good things must come to an end, including Nintendo's SNES library.\nWent out with a bang though!", - "Have you tried Kingdom Hearts II?\nI'll wait for you to name a more epic crossover.", - "Have you tried Link's Awakening DX?\nHopefully, Link won't be obsessed with circles when he wakes up.", - "Have you tried Landstalker?\nThe Witness player's greatest fear: A diagonal movement grid...\nWait, I guess we have the Monastery puzzles.", - "Have you tried Lingo?\nIt's an open world puzzle game. It features puzzle panels with non-verbally explained mechanics.\nIf you like this game, you'll like Lingo too.", - "(Middle Yellow)\nYOU AILED OVERNIGHT\nH--- --- ----- -----?", - "Have you tried Lufia II?\nRoguelites are not just a 2010s phenomenon, turns out.", - "Have you tried Meritous?\nYou should know that obscure games are often groundbreaking!", - "Have you tried The Messenger?\nOld ideas made new again. It's how all art is made.", - "Have you tried Minecraft?\nI have recently learned this is a question that needs to be asked.", - "Have you tried Mega Man Battle Network 3?\nIt's a Mega Man RPG. How could you not want to try that?", - "Have you tried Muse Dash?\nRhythm game with cute girls!\n(Maybe skip if you don't like the Jungle panels)", - "Have you tried Noita?\nIf you like punishing yourself, you will like it.", - "Have you tried Ocarina of Time?\nOne of the biggest randomizers, big inspiration for this one's features!", - "Have you tried Overcooked 2?\nWhen you're done relaxing with puzzles, use your energy to yell at your friends.", - "Have you tried Pokemon Emerald?\nI'm going to say it: 10/10, just the right amount of water.", - "Have you tried Pokemon Red&Blue?\nA cute pet collecting game that fascinated an entire generation.", - "Have you tried Raft?\nHaven't you always wanted to explore the ocean surrounding this island?", - "Have you tried Rogue Legacy?\nAfter solving so many puzzles it's the perfect way to rest your \"thinking\" brain.", - "Have you tried Risk of Rain 2?\nI haven't either. But I hear it's incredible!", - "Have you tried Sonic Adventure 2?\nIf the silence on this island is getting to you, there aren't many games more energetic.", - "Have you tried Starcraft 2?\nUse strategy and management to crush your enemies!", - "Have you tried Shivers?\nWitness 2 should totally feature a haunted museum.", - "Have you tried Super Metroid?\nA classic game, yet still one of the best in the genre.", - "Have you tried Super Mario 64?\n3-dimensional games like this owe everything to that game.", - "Have you tried Super Mario World?\nI don't think I need to tell you that it is beloved by many.", - "Have you tried SMZ3?\nWhy play one incredible game when you can play 2 at once?", - "Have you tried Secret of Evermore?\nI haven't either. But I hear it's great!", - "Have you tried Slay the Spire?\nExperience the thrill of combat without needing fast fingers!", - "Have you tried Stardew Valley?\nThe Farming game that gave a damn. It's so easy to lose hours and days to it...", - "Have you tried Subnautica?\nIf you like this game's lonely atmosphere, I would suggest you try it.", - "Have you tried Terraria?\nA prime example of a survival sandbox game that beats the \"Wide as an ocean, deep as a puddle\" allegations.", - "Have you tried Timespinner?\nEveryone who plays it ends up loving it!", - "Have you tried The Legend of Zelda?\nIn some sense, it was the starting point of \"adventure\" in video games.", - "Have you tried TUNC?\nWhat? No, I'm pretty sure I spelled that right.", - "Have you tried TUNIC?\nRemember what discovering your first Environmental Puzzle was like?\nTUNIC will make you feel like that at least 5 times over.", - "Have you tried Undertale?\nI hope I'm not the 10th person to ask you that. But it's, like, really good.", - "Have you tried VVVVVV?\nExperience the essence of gaming distilled into its purest form!", - "Have you tried Wargroove?\nI'm glad that for every abandoned series, enough people are yearning for its return that one of them will know how to code.", - "Have you tried The Witness?\nOh. I guess you already have. Thanks for playing!", - "Have you tried Zillion?\nMe neither. But it looks fun. So, let's try something new together?", - "Have you tried Zork: Grand Inquisitor?\nThis 1997 game uses Z-Vision technology to simulate 3D environments.\nCome on, I know you wanna find out what \"Z-Vision\" is.", - - "Quaternions break my brain", - "Eclipse has nothing, but you should do it anyway.", - "Beep", - "Putting in custom subtitles shouldn't have been as hard as it was...", - "BK mode is right around the corner.", - "You can do it!", - "I believe in you!", - "The person playing is cute. <3", - "dash dot, dash dash dash,\ndash, dot dot dot dot, dot dot,\ndash dot, dash dash dot", - "When you think about it, there are actually a lot of bubbles in a stream.", - "Never gonna give you up\nNever gonna let you down\nNever gonna run around and desert you", - "Thanks to the Archipelago developers for making this possible.", - "One day I was fascinated by the subject of generation of waves by wind.", - "I don't like sandwiches. Why would you think I like sandwiches? Have you ever seen me with a sandwich?", - "Where are you right now?\nI'm at soup!\nWhat do you mean you're at soup?", - "Remember to ask in the Archipelago Discord what the Functioning Brain does.", - "Don't use your puzzle skips, you might need them later.", - "For an extra challenge, try playing blindfolded.", - "Go to the top of the mountain and see if you can see your house.", - "Yellow = Red + Green\nCyan = Green + Blue\nMagenta = Red + Blue", - "Maybe that panel really is unsolvable.", - "Did you make sure it was plugged in?", - "Do not look into laser with remaining eye.", - "Try pressing Space to jump.", - "The Witness is a Doom clone.\nJust replace the demons with puzzles", - "Test Hint please ignore", - "Shapers can never be placed outside the panel boundaries, even if subtracted.", - "The Keep laser panels use the same trick on both sides!", - "Can't get past a door? Try going around. Can't go around? Try building a nether portal.", - "We've been trying to reach you about your car's extended warranty.", - "I hate this game. I hate this game. I hate this game.\n- Chess player Bobby Fischer", - "Dear Mario,\nPlease come to the castle. I've baked a cake for you!", - "Have you tried waking up?\nYeah, me neither.", - "Why do they call it The Witness, when wit game the player view play of with the game.", - "THE WIND FISH IN NAME ONLY, FOR IT IS NEITHER", - "Like this game?\nTry The Wit.nes, Understand, INSIGHT, Taiji What the Witness?, and Tametsi.", - "In a race, It's survival of the Witnesst.", - "This hint has been removed. We apologize for your inconvenience.", - "O-----------", - "Circle is draw\nSquare is separate\nLine is win", - "Circle is draw\nStar is pair\nLine is win", - "Circle is draw\nCircle is copy\nLine is win", - "Circle is draw\nDot is eat\nLine is win", - "Circle is start\nWalk is draw\nLine is win", - "Circle is start\nLine is win\nWitness is you", - "Can't find any items?\nConsider a relaxing boat trip around the island!", - "Don't forget to like, comment, and subscribe.", - "Ah crap, gimme a second.\n[papers rustling]\nSorry, nothing.", - "Trying to get a hint? Too bad.", - "Here's a hint: Get good at the game.", - "I'm still not entirely sure what we're witnessing here.", - "Have you found a red page yet? No? Then have you found a blue page?", - "And here we see the Witness player, seeking answers where there are none-\nDid someone turn on the loudspeaker?", - - "Be quiet. I can't hear the elevator.", - "Witness me.\n- The famous last words of John Witness.", - "It's okay, I always have to skip the Rotated Shaper puzzles too.", - "Alan please add hint.", - "Rumor has it there's an audio log with a hint nearby.", - "In the future, war will break out between obelisk_sides and individual EP players.\nWhich side are you on?", - "Droplets: Low, High, Mid.\nAmbience: Mid, Low, Mid, High.", - "Name a better game involving lines. I'll wait.", - "\"You have to draw a line in the sand.\"\n- Arin \"Egoraptor\" Hanson", - "Have you tried?\nThe puzzles tend to get easier if you do.", - "Sorry, I accidentally left my phone in the Jungle.\nAnd also all my fragile dishes.", - "Winner of the \"Most Irrelevant PR in AP History\" award!", - "I bet you wish this was a real hint :)", - "\"This hint is an impostor.\"- Junk hint submitted by T1mshady.\n...wait, I'm not supposed to say that part?", - "Wouldn't you like to know, weather buoy?", - "Give me a few minutes, I should have better material by then.", - "Just pet the doggy! You know you want to!!!", - "ceci n'est pas une metroidvania", - "HINT is MELT\nYOU is HOT", - "Who's that behind you?", - ":3", - "^v ^^v> >>^>v\n^^v>v ^v>> v>^> v>v^", - "Statement #0162601, regarding a strange island that--\nOh, wait, sorry. I'm not supposed to be here.", - "Hollow Bastion has 6 progression items.\nOr maybe it doesn't.\nI wouldn't know.", - "Set your hint count lower so I can tell you more jokes next time.", - "A non-edge start point is similar to a cat.\nIt must be either inside or outside, it can't be both.", - "What if we kissed on the Bunker Laser Platform?\nJk... unless?", - "You don't have Boat? Invisible boat time!\nYou do have boat? Boat clipping time!", - "Cet indice est en français. Nous nous excusons de tout inconvénients engendrés par cela.", - "How many of you have personally witnessed a total solar eclipse?", - "In the Treehouse area, you will find 69 progression items.\nNice.\n(Source: Just trust me)", - "Lingo\nLingoing\nLingone", - "The name of the captain was Albert Einstein.", - "Panel impossible Sigma plz fix", - "Welcome Back! (:", - "R R R U L L U L U R U R D R D R U U", - "Have you tried checking your tracker?", - "Lines are drawn on grids\nAll symbols must be obeyed\nIt's snowing on Mt. Fuji", - "If you're BK, you could try today's Wittle:\nhttps://www.fourisland.com/wittle/", - "They say that plundering Outside Ganon's Castle is a foolish choice.", - "You should try to BLJ. Maybe that'll get you through that door.", - "Error: Witness Randomizer disconnected from Archipelago.\n(lmao gottem)", - "You have found: One (1) Audio Log!\nSeries of 49! Collect them all!", - "In the Town area, you will find 1 good boi.\nGo pet him.", - "If you're ever stuck on a panel, feel free to ask Rever.\nSurely you'll understand his drawing!", - "[This hint has been removed as part of the Witness Protection Program]", - "Panel Diddle", - "Witness AP when", - "This game is my favorite walking simulator.", - "Did you hear that? It said --\n\nCosmic background radiation is a riot!", - "Well done solving those puzzles.\nPray return to the Waking Sands.", - "Having trouble finding your checks?\nTry the PopTracker pack!\nIt's got auto-tracking and a detailed map.", - - "Hints suggested by:\nIHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi, Jasper Bird, T1mshady, " - "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch, Rever, Ishigh, snolid.", -] - @dataclass class WitnessLocationHint: @@ -192,10 +19,10 @@ class WitnessLocationHint: hint_came_from_location: bool # If a hint gets added to a set twice, but once as an item hint and once as a location hint, those are the same - def __hash__(self): + def __hash__(self) -> int: return hash(self.location) - def __eq__(self, other): + def __eq__(self, other) -> bool: return self.location == other.location @@ -324,7 +151,7 @@ def get_priority_hint_locations(world: "WitnessWorld") -> List[str]: "Boat Shipwreck Green EP", "Quarry Stoneworks Control Room Left", ] - + # Add Obelisk Sides that contain EPs that are meant to be hinted, if they are necessary to complete the Obelisk Side if "0x33A20" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES: priority.append("Town Obelisk Side 6") # Theater Flowers EP @@ -338,7 +165,7 @@ def get_priority_hint_locations(world: "WitnessWorld") -> List[str]: return priority -def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint): +def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint: location_name = hint.location.name if hint.location.player != world.player: location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" @@ -373,8 +200,8 @@ def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Ite def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]: - location_obj = world.multiworld.get_location(location, world.player) - item_obj = world.multiworld.get_location(location, world.player).item + location_obj = world.get_location(location) + item_obj = location_obj.item item_name = item_obj.name if item_obj.player != world.player: item_name += " (" + world.multiworld.get_player_name(item_obj.player) + ")" @@ -382,7 +209,8 @@ def hint_from_location(world: "WitnessWorld", location: str) -> Optional[Witness return WitnessLocationHint(location_obj, True) -def get_items_and_locations_in_random_order(world: "WitnessWorld", own_itempool: List[Item]): +def get_items_and_locations_in_random_order(world: "WitnessWorld", + own_itempool: List[Item]) -> Tuple[List[str], List[str]]: prog_items_in_this_world = sorted( item.name for item in own_itempool if item.advancement and item.code and item.location @@ -455,7 +283,11 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp hints = [] # This is a way to reverse a Dict[a,List[b]] to a Dict[b,a] - area_reverse_lookup = {v: k for k, l in unhinted_locations_for_hinted_areas.items() for v in l} + area_reverse_lookup = { + unhinted_location: hinted_area + for hinted_area, unhinted_locations in unhinted_locations_for_hinted_areas.items() + for unhinted_location in unhinted_locations + } while len(hints) < hint_amount: if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first: @@ -495,10 +327,6 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp return hints -def generate_joke_hints(world: "WitnessWorld", amount: int) -> List[Tuple[str, int, int]]: - return [(x, -1, -1) for x in world.random.sample(joke_hints, amount)] - - def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[str, List[Location]], already_hinted_locations: Set[Location]) -> Tuple[List[str], Dict[str, Set[Location]]]: """ @@ -529,16 +357,16 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]: - potential_areas = list(StaticWitnessLogic.ALL_AREAS_BY_NAME.keys()) + potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys()) locations_per_area = dict() items_per_area = dict() for area in potential_areas: regions = [ - world.regio.created_regions[region] - for region in StaticWitnessLogic.ALL_AREAS_BY_NAME[area]["regions"] - if region in world.regio.created_regions + world.player_regions.created_regions[region] + for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"] + if region in world.player_regions.created_regions ] locations = [location for region in regions for location in region.get_locations() if location.address] @@ -596,7 +424,7 @@ def word_area_hint(world: "WitnessWorld", hinted_area: str, corresponding_items: if local_lasers == total_progression: sentence_end = (" for this world." if player_count > 1 else ".") - hint_string += f"\nAll of them are lasers" + sentence_end + hint_string += "\nAll of them are lasers" + sentence_end elif player_count > 1: if local_progression and non_local_progression: @@ -663,7 +491,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int, already_hinted_locations |= { loc for loc in world.multiworld.get_reachable_locations(state, world.player) - if loc.address and StaticWitnessLogic.ENTITIES_BY_NAME[loc.name]["area"]["name"] == "Tutorial (Inside)" + if loc.address and static_witness_logic.ENTITIES_BY_NAME[loc.name]["area"]["name"] == "Tutorial (Inside)" } intended_location_hints = hint_amount - area_hints diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index cd6d71f469..df8214ac92 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -3,511 +3,24 @@ Defines constants for different types of locations in the game """ from typing import TYPE_CHECKING +from .data import static_locations as static_witness_locations +from .data import static_logic as static_witness_logic from .player_logic import WitnessPlayerLogic -from .static_logic import StaticWitnessLogic if TYPE_CHECKING: from . import WitnessWorld -ID_START = 158000 - - -class StaticWitnessLocations: - """ - Witness Location Constants that stay consistent across worlds - """ - - GENERAL_LOCATIONS = { - "Tutorial Front Left", - "Tutorial Back Left", - "Tutorial Back Right", - "Tutorial Patio Floor", - "Tutorial Gate Open", - - "Outside Tutorial Vault Box", - "Outside Tutorial Discard", - "Outside Tutorial Shed Row 5", - "Outside Tutorial Tree Row 9", - "Outside Tutorial Outpost Entry Panel", - "Outside Tutorial Outpost Exit Panel", - - "Glass Factory Discard", - "Glass Factory Back Wall 5", - "Glass Factory Front 3", - "Glass Factory Melting 3", - - "Symmetry Island Lower Panel", - "Symmetry Island Right 5", - "Symmetry Island Back 6", - "Symmetry Island Left 7", - "Symmetry Island Upper Panel", - "Symmetry Island Scenery Outlines 5", - "Symmetry Island Laser Yellow 3", - "Symmetry Island Laser Blue 3", - "Symmetry Island Laser Panel", - - "Orchard Apple Tree 5", - - "Desert Vault Box", - "Desert Discard", - "Desert Surface 8", - "Desert Light Room 3", - "Desert Pond Room 5", - "Desert Flood Room 6", - "Desert Elevator Room Hexagonal", - "Desert Elevator Room Bent 3", - "Desert Laser Panel", - - "Quarry Entry 1 Panel", - "Quarry Entry 2 Panel", - "Quarry Stoneworks Entry Left Panel", - "Quarry Stoneworks Entry Right Panel", - "Quarry Stoneworks Lower Row 6", - "Quarry Stoneworks Upper Row 8", - "Quarry Stoneworks Control Room Left", - "Quarry Stoneworks Control Room Right", - "Quarry Stoneworks Stairs Panel", - "Quarry Boathouse Intro Right", - "Quarry Boathouse Intro Left", - "Quarry Boathouse Front Row 5", - "Quarry Boathouse Back First Row 9", - "Quarry Boathouse Back Second Row 3", - "Quarry Discard", - "Quarry Laser Panel", - - "Shadows Intro 8", - "Shadows Far 8", - "Shadows Near 5", - "Shadows Laser Panel", - - "Keep Hedge Maze 1", - "Keep Hedge Maze 2", - "Keep Hedge Maze 3", - "Keep Hedge Maze 4", - "Keep Pressure Plates 1", - "Keep Pressure Plates 2", - "Keep Pressure Plates 3", - "Keep Pressure Plates 4", - "Keep Discard", - "Keep Laser Panel Hedges", - "Keep Laser Panel Pressure Plates", - - "Shipwreck Vault Box", - "Shipwreck Discard", - - "Monastery Outside 3", - "Monastery Inside 4", - "Monastery Laser Panel", - - "Town Cargo Box Entry Panel", - "Town Cargo Box Discard", - "Town Tall Hexagonal", - "Town Church Entry Panel", - "Town Church Lattice", - "Town Maze Panel", - "Town Rooftop Discard", - "Town Red Rooftop 5", - "Town Wooden Roof Lower Row 5", - "Town Wooden Rooftop", - "Windmill Entry Panel", - "Town RGB House Entry Panel", - "Town Laser Panel", - - "Town RGB House Upstairs Left", - "Town RGB House Upstairs Right", - "Town RGB House Sound Room Right", - - "Windmill Theater Entry Panel", - "Theater Exit Left Panel", - "Theater Exit Right Panel", - "Theater Tutorial Video", - "Theater Desert Video", - "Theater Jungle Video", - "Theater Shipwreck Video", - "Theater Mountain Video", - "Theater Discard", - - "Jungle Discard", - "Jungle First Row 3", - "Jungle Second Row 4", - "Jungle Popup Wall 6", - "Jungle Laser Panel", - - "Jungle Vault Box", - "Jungle Monastery Garden Shortcut Panel", - - "Bunker Entry Panel", - "Bunker Intro Left 5", - "Bunker Intro Back 4", - "Bunker Glass Room 3", - "Bunker UV Room 2", - "Bunker Laser Panel", - - "Swamp Entry Panel", - "Swamp Intro Front 6", - "Swamp Intro Back 8", - "Swamp Between Bridges Near Row 4", - "Swamp Cyan Underwater 5", - "Swamp Platform Row 4", - "Swamp Platform Shortcut Right Panel", - "Swamp Between Bridges Far Row 4", - "Swamp Red Underwater 4", - "Swamp Purple Underwater", - "Swamp Beyond Rotating Bridge 4", - "Swamp Blue Underwater 5", - "Swamp Laser Panel", - "Swamp Laser Shortcut Right Panel", - - "Treehouse First Door Panel", - "Treehouse Second Door Panel", - "Treehouse Third Door Panel", - "Treehouse Yellow Bridge 9", - "Treehouse First Purple Bridge 5", - "Treehouse Second Purple Bridge 7", - "Treehouse Green Bridge 7", - "Treehouse Green Bridge Discard", - "Treehouse Left Orange Bridge 15", - "Treehouse Laser Discard", - "Treehouse Right Orange Bridge 12", - "Treehouse Laser Panel", - "Treehouse Drawbridge Panel", - - "Mountainside Discard", - "Mountainside Vault Box", - "Mountaintop River Shape", - - "Tutorial First Hallway EP", - "Tutorial Cloud EP", - "Tutorial Patio Flowers EP", - "Tutorial Gate EP", - "Outside Tutorial Garden EP", - "Outside Tutorial Town Sewer EP", - "Outside Tutorial Path EP", - "Outside Tutorial Tractor EP", - "Mountainside Thundercloud EP", - "Glass Factory Vase EP", - "Symmetry Island Glass Factory Black Line Reflection EP", - "Symmetry Island Glass Factory Black Line EP", - "Desert Sand Snake EP", - "Desert Facade Right EP", - "Desert Facade Left EP", - "Desert Stairs Left EP", - "Desert Stairs Right EP", - "Desert Broken Wall Straight EP", - "Desert Broken Wall Bend EP", - "Desert Shore EP", - "Desert Island EP", - "Desert Pond Room Near Reflection EP", - "Desert Pond Room Far Reflection EP", - "Desert Flood Room EP", - "Desert Elevator EP", - "Quarry Shore EP", - "Quarry Entrance Pipe EP", - "Quarry Sand Pile EP", - "Quarry Rock Line EP", - "Quarry Rock Line Reflection EP", - "Quarry Railroad EP", - "Quarry Stoneworks Ramp EP", - "Quarry Stoneworks Lift EP", - "Quarry Boathouse Moving Ramp EP", - "Quarry Boathouse Hook EP", - "Shadows Quarry Stoneworks Rooftop Vent EP", - "Treehouse Beach Rock Shadow EP", - "Treehouse Beach Sand Shadow EP", - "Treehouse Beach Both Orange Bridges EP", - "Keep Red Flowers EP", - "Keep Purple Flowers EP", - "Shipwreck Circle Near EP", - "Shipwreck Circle Left EP", - "Shipwreck Circle Far EP", - "Shipwreck Stern EP", - "Shipwreck Rope Inner EP", - "Shipwreck Rope Outer EP", - "Shipwreck Couch EP", - "Keep Pressure Plates 1 EP", - "Keep Pressure Plates 2 EP", - "Keep Pressure Plates 3 EP", - "Keep Pressure Plates 4 Left Exit EP", - "Keep Pressure Plates 4 Right Exit EP", - "Keep Path EP", - "Keep Hedges EP", - "Monastery Facade Left Near EP", - "Monastery Facade Left Far Short EP", - "Monastery Facade Left Far Long EP", - "Monastery Facade Right Near EP", - "Monastery Facade Left Stairs EP", - "Monastery Facade Right Stairs EP", - "Monastery Grass Stairs EP", - "Monastery Left Shutter EP", - "Monastery Middle Shutter EP", - "Monastery Right Shutter EP", - "Windmill First Blade EP", - "Windmill Second Blade EP", - "Windmill Third Blade EP", - "Town Tower Underside Third EP", - "Town Tower Underside Fourth EP", - "Town Tower Underside First EP", - "Town Tower Underside Second EP", - "Town RGB House Red EP", - "Town RGB House Green EP", - "Town Maze Bridge Underside EP", - "Town Black Line Redirect EP", - "Town Black Line Church EP", - "Town Brown Bridge EP", - "Town Black Line Tower EP", - "Theater Eclipse EP", - "Theater Window EP", - "Theater Door EP", - "Theater Church EP", - "Jungle Long Arch Moss EP", - "Jungle Straight Left Moss EP", - "Jungle Pop-up Wall Moss EP", - "Jungle Short Arch Moss EP", - "Jungle Entrance EP", - "Jungle Tree Halo EP", - "Jungle Bamboo CCW EP", - "Jungle Bamboo CW EP", - "Jungle Green Leaf Moss EP", - "Monastery Garden Left EP", - "Monastery Garden Right EP", - "Monastery Wall EP", - "Bunker Tinted Door EP", - "Bunker Green Room Flowers EP", - "Swamp Purple Sand Middle EP", - "Swamp Purple Sand Top EP", - "Swamp Purple Sand Bottom EP", - "Swamp Sliding Bridge Left EP", - "Swamp Sliding Bridge Right EP", - "Swamp Cyan Underwater Sliding Bridge EP", - "Swamp Rotating Bridge CCW EP", - "Swamp Rotating Bridge CW EP", - "Swamp Boat EP", - "Swamp Long Bridge Side EP", - "Swamp Purple Underwater Right EP", - "Swamp Purple Underwater Left EP", - "Treehouse Buoy EP", - "Treehouse Right Orange Bridge EP", - "Treehouse Burned House Beach EP", - "Mountainside Cloud Cycle EP", - "Mountainside Bush EP", - "Mountainside Apparent River EP", - "Mountaintop River Shape EP", - "Mountaintop Arch Black EP", - "Mountaintop Arch White Right EP", - "Mountaintop Arch White Left EP", - "Mountain Bottom Floor Yellow Bridge EP", - "Mountain Bottom Floor Blue Bridge EP", - "Mountain Floor 2 Pink Bridge EP", - "Caves Skylight EP", - "Challenge Water EP", - "Tunnels Theater Flowers EP", - "Boat Desert EP", - "Boat Shipwreck CCW Underside EP", - "Boat Shipwreck Green EP", - "Boat Shipwreck CW Underside EP", - "Boat Bunker Yellow Line EP", - "Boat Town Long Sewer EP", - "Boat Tutorial EP", - "Boat Tutorial Reflection EP", - "Boat Tutorial Moss EP", - "Boat Cargo Box EP", - - "Desert Obelisk Side 1", - "Desert Obelisk Side 2", - "Desert Obelisk Side 3", - "Desert Obelisk Side 4", - "Desert Obelisk Side 5", - "Monastery Obelisk Side 1", - "Monastery Obelisk Side 2", - "Monastery Obelisk Side 3", - "Monastery Obelisk Side 4", - "Monastery Obelisk Side 5", - "Monastery Obelisk Side 6", - "Treehouse Obelisk Side 1", - "Treehouse Obelisk Side 2", - "Treehouse Obelisk Side 3", - "Treehouse Obelisk Side 4", - "Treehouse Obelisk Side 5", - "Treehouse Obelisk Side 6", - "Mountainside Obelisk Side 1", - "Mountainside Obelisk Side 2", - "Mountainside Obelisk Side 3", - "Mountainside Obelisk Side 4", - "Mountainside Obelisk Side 5", - "Mountainside Obelisk Side 6", - "Quarry Obelisk Side 1", - "Quarry Obelisk Side 2", - "Quarry Obelisk Side 3", - "Quarry Obelisk Side 4", - "Quarry Obelisk Side 5", - "Town Obelisk Side 1", - "Town Obelisk Side 2", - "Town Obelisk Side 3", - "Town Obelisk Side 4", - "Town Obelisk Side 5", - "Town Obelisk Side 6", - - "Caves Mountain Shortcut Panel", - "Caves Swamp Shortcut Panel", - - "Caves Blue Tunnel Right First 4", - "Caves Blue Tunnel Left First 1", - "Caves Blue Tunnel Left Second 5", - "Caves Blue Tunnel Right Second 5", - "Caves Blue Tunnel Right Third 1", - "Caves Blue Tunnel Left Fourth 1", - "Caves Blue Tunnel Left Third 1", - - "Caves First Floor Middle", - "Caves First Floor Right", - "Caves First Floor Left", - "Caves First Floor Grounded", - "Caves Lone Pillar", - "Caves First Wooden Beam", - "Caves Second Wooden Beam", - "Caves Third Wooden Beam", - "Caves Fourth Wooden Beam", - "Caves Right Upstairs Left Row 8", - "Caves Right Upstairs Right Row 3", - "Caves Left Upstairs Single", - "Caves Left Upstairs Left Row 5", - - "Caves Challenge Entry Panel", - "Challenge Tunnels Entry Panel", - - "Tunnels Vault Box", - "Theater Challenge Video", - - "Tunnels Town Shortcut Panel", - - "Caves Skylight EP", - "Challenge Water EP", - "Tunnels Theater Flowers EP", - "Tutorial Gate EP", - - "Mountaintop Mountain Entry Panel", - - "Mountain Floor 1 Light Bridge Controller", - - "Mountain Floor 1 Right Row 5", - "Mountain Floor 1 Left Row 7", - "Mountain Floor 1 Back Row 3", - "Mountain Floor 1 Trash Pillar 2", - "Mountain Floor 2 Near Row 5", - "Mountain Floor 2 Far Row 6", - - "Mountain Floor 2 Light Bridge Controller Near", - "Mountain Floor 2 Light Bridge Controller Far", - - "Mountain Bottom Floor Yellow Bridge EP", - "Mountain Bottom Floor Blue Bridge EP", - "Mountain Floor 2 Pink Bridge EP", - - "Mountain Floor 2 Elevator Discard", - "Mountain Bottom Floor Giant Puzzle", - - "Mountain Bottom Floor Pillars Room Entry Left", - "Mountain Bottom Floor Pillars Room Entry Right", - - "Mountain Bottom Floor Caves Entry Panel", - - "Mountain Bottom Floor Left Pillar 4", - "Mountain Bottom Floor Right Pillar 4", - - "Challenge Vault Box", - "Theater Challenge Video", - "Mountain Bottom Floor Discard", - } - - OBELISK_SIDES = { - "Desert Obelisk Side 1", - "Desert Obelisk Side 2", - "Desert Obelisk Side 3", - "Desert Obelisk Side 4", - "Desert Obelisk Side 5", - "Monastery Obelisk Side 1", - "Monastery Obelisk Side 2", - "Monastery Obelisk Side 3", - "Monastery Obelisk Side 4", - "Monastery Obelisk Side 5", - "Monastery Obelisk Side 6", - "Treehouse Obelisk Side 1", - "Treehouse Obelisk Side 2", - "Treehouse Obelisk Side 3", - "Treehouse Obelisk Side 4", - "Treehouse Obelisk Side 5", - "Treehouse Obelisk Side 6", - "Mountainside Obelisk Side 1", - "Mountainside Obelisk Side 2", - "Mountainside Obelisk Side 3", - "Mountainside Obelisk Side 4", - "Mountainside Obelisk Side 5", - "Mountainside Obelisk Side 6", - "Quarry Obelisk Side 1", - "Quarry Obelisk Side 2", - "Quarry Obelisk Side 3", - "Quarry Obelisk Side 4", - "Quarry Obelisk Side 5", - "Town Obelisk Side 1", - "Town Obelisk Side 2", - "Town Obelisk Side 3", - "Town Obelisk Side 4", - "Town Obelisk Side 5", - "Town Obelisk Side 6", - } - - ALL_LOCATIONS_TO_ID = dict() - - AREA_LOCATION_GROUPS = dict() - - @staticmethod - def get_id(chex: str): - """ - Calculates the location ID for any given location - """ - - return StaticWitnessLogic.ENTITIES_BY_HEX[chex]["id"] - - @staticmethod - def get_event_name(panel_hex: str): - """ - Returns the event name of any given panel. - """ - - action = " Opened" if StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["entityType"] == "Door" else " Solved" - - return StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["checkName"] + action - - def __init__(self): - all_loc_to_id = { - panel_obj["checkName"]: self.get_id(chex) - for chex, panel_obj in StaticWitnessLogic.ENTITIES_BY_HEX.items() - if panel_obj["id"] - } - - all_loc_to_id = dict( - sorted(all_loc_to_id.items(), key=lambda loc: loc[1]) - ) - - for key, item in all_loc_to_id.items(): - self.ALL_LOCATIONS_TO_ID[key] = item - - for loc in all_loc_to_id: - area = StaticWitnessLogic.ENTITIES_BY_NAME[loc]["area"]["name"] - self.AREA_LOCATION_GROUPS.setdefault(area, []).append(loc) - - class WitnessPlayerLocations: """ Class that defines locations for a single player """ - def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic): + def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None: """Defines locations AFTER logic changes due to options""" self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"} - self.CHECK_LOCATIONS = StaticWitnessLocations.GENERAL_LOCATIONS.copy() + self.CHECK_LOCATIONS = static_witness_locations.GENERAL_LOCATIONS.copy() if world.options.shuffle_discarded_panels: self.PANEL_TYPES_TO_SHUFFLE.add("Discard") @@ -520,28 +33,28 @@ class WitnessPlayerLocations: elif world.options.shuffle_EPs == "obelisk_sides": self.PANEL_TYPES_TO_SHUFFLE.add("Obelisk Side") - for obelisk_loc in StaticWitnessLocations.OBELISK_SIDES: - obelisk_loc_hex = StaticWitnessLogic.ENTITIES_BY_NAME[obelisk_loc]["entity_hex"] + for obelisk_loc in static_witness_locations.OBELISK_SIDES: + obelisk_loc_hex = static_witness_logic.ENTITIES_BY_NAME[obelisk_loc]["entity_hex"] if player_logic.REQUIREMENTS_BY_HEX[obelisk_loc_hex] == frozenset({frozenset()}): self.CHECK_LOCATIONS.discard(obelisk_loc) self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS - self.CHECK_LOCATIONS.discard(StaticWitnessLogic.ENTITIES_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"]) + self.CHECK_LOCATIONS.discard(static_witness_logic.ENTITIES_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"]) self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - { - StaticWitnessLogic.ENTITIES_BY_HEX[entity_hex]["checkName"] + static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] for entity_hex in player_logic.COMPLETELY_DISABLED_ENTITIES | player_logic.PRECOMPLETED_LOCATIONS } self.CHECK_PANELHEX_TO_ID = { - StaticWitnessLogic.ENTITIES_BY_NAME[ch]["entity_hex"]: StaticWitnessLocations.ALL_LOCATIONS_TO_ID[ch] + static_witness_logic.ENTITIES_BY_NAME[ch]["entity_hex"]: static_witness_locations.ALL_LOCATIONS_TO_ID[ch] for ch in self.CHECK_LOCATIONS - if StaticWitnessLogic.ENTITIES_BY_NAME[ch]["entityType"] in self.PANEL_TYPES_TO_SHUFFLE + if static_witness_logic.ENTITIES_BY_NAME[ch]["entityType"] in self.PANEL_TYPES_TO_SHUFFLE } - dog_hex = StaticWitnessLogic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"] - dog_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"] + dog_hex = static_witness_logic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"] + dog_id = static_witness_locations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"] self.CHECK_PANELHEX_TO_ID[dog_hex] = dog_id self.CHECK_PANELHEX_TO_ID = dict( @@ -553,22 +66,19 @@ class WitnessPlayerLocations: } self.EVENT_LOCATION_TABLE = { - StaticWitnessLocations.get_event_name(panel_hex): None - for panel_hex in event_locations + static_witness_locations.get_event_name(entity_hex): None + for entity_hex in event_locations } check_dict = { - StaticWitnessLogic.ENTITIES_BY_HEX[location]["checkName"]: - StaticWitnessLocations.get_id(StaticWitnessLogic.ENTITIES_BY_HEX[location]["entity_hex"]) + static_witness_logic.ENTITIES_BY_HEX[location]["checkName"]: + static_witness_locations.get_id(static_witness_logic.ENTITIES_BY_HEX[location]["entity_hex"]) for location in self.CHECK_PANELHEX_TO_ID } self.CHECK_LOCATION_TABLE = {**self.EVENT_LOCATION_TABLE, **check_dict} - def add_location_late(self, entity_name: str): - entity_hex = StaticWitnessLogic.ENTITIES_BY_NAME[entity_name]["entity_hex"] + def add_location_late(self, entity_name: str) -> None: + entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"] self.CHECK_LOCATION_TABLE[entity_hex] = entity_name - self.CHECK_PANELHEX_TO_ID[entity_hex] = StaticWitnessLocations.get_id(entity_hex) - - -StaticWitnessLocations() + self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index b66308df43..63f98faea4 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -1,10 +1,11 @@ from dataclasses import dataclass -from schema import Schema, And, Optional +from schema import And, Schema -from Options import Toggle, DefaultOnToggle, Range, Choice, PerGameCommonOptions, OptionDict +from Options import Choice, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, Toggle -from .static_logic import WeightedItemDefinition, ItemCategory, StaticWitnessLogic +from .data import static_logic as static_witness_logic +from .data.item_definition_classes import ItemCategory, WeightedItemDefinition class DisableNonRandomizedPuzzles(Toggle): @@ -232,12 +233,12 @@ class TrapWeights(OptionDict): display_name = "Trap Weights" schema = Schema({ trap_name: And(int, lambda n: n >= 0) - for trap_name, item_definition in StaticWitnessLogic.all_items.items() + for trap_name, item_definition in static_witness_logic.ALL_ITEMS.items() if isinstance(item_definition, WeightedItemDefinition) and item_definition.category is ItemCategory.TRAP }) default = { trap_name: item_definition.weight - for trap_name, item_definition in StaticWitnessLogic.all_items.items() + for trap_name, item_definition in static_witness_logic.ALL_ITEMS.items() if isinstance(item_definition, WeightedItemDefinition) and item_definition.category is ItemCategory.TRAP } @@ -315,7 +316,7 @@ class TheWitnessOptions(PerGameCommonOptions): shuffle_discarded_panels: ShuffleDiscardedPanels shuffle_vault_boxes: ShuffleVaultBoxes obelisk_keys: ObeliskKeys - shuffle_EPs: ShuffleEnvironmentalPuzzles + shuffle_EPs: ShuffleEnvironmentalPuzzles # noqa: N815 EP_difficulty: EnvironmentalPuzzlesDifficulty shuffle_postgame: ShufflePostgame victory_condition: VictoryCondition diff --git a/worlds/witness/items.py b/worlds/witness/player_items.py similarity index 66% rename from worlds/witness/items.py rename to worlds/witness/player_items.py index 6802fd2a21..925b21ae6d 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/player_items.py @@ -2,16 +2,23 @@ Defines progression, junk and event items for The Witness """ import copy +from typing import TYPE_CHECKING, Dict, List, Set -from dataclasses import dataclass -from typing import Optional, Dict, List, Set, TYPE_CHECKING +from BaseClasses import Item, ItemClassification, MultiWorld -from BaseClasses import Item, MultiWorld, ItemClassification -from .locations import ID_START, WitnessPlayerLocations +from .data import static_items as static_witness_items +from .data import static_logic as static_witness_logic +from .data.item_definition_classes import ( + DoorItemDefinition, + ItemCategory, + ItemData, + ItemDefinition, + ProgressiveItemDefinition, + WeightedItemDefinition, +) +from .data.utils import build_weighted_int_list +from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic -from .static_logic import ItemDefinition, DoorItemDefinition, ProgressiveItemDefinition, ItemCategory, \ - StaticWitnessLogic, WeightedItemDefinition -from .utils import build_weighted_int_list if TYPE_CHECKING: from . import WitnessWorld @@ -19,17 +26,6 @@ if TYPE_CHECKING: NUM_ENERGY_UPGRADES = 4 -@dataclass() -class ItemData: - """ - ItemData for an item in The Witness - """ - ap_code: Optional[int] - definition: ItemDefinition - classification: ItemClassification - local_only: bool = False - - class WitnessItem(Item): """ Item from the game The Witness @@ -37,79 +33,30 @@ class WitnessItem(Item): game: str = "The Witness" -class StaticWitnessItems: - """ - Class that handles Witness items independent of world settings - """ - item_data: Dict[str, ItemData] = {} - item_groups: Dict[str, List[str]] = {} - - # Useful items that are treated specially at generation time and should not be automatically added to the player's - # item list during get_progression_items. - special_usefuls: List[str] = ["Puzzle Skip"] - - def __init__(self): - for item_name, definition in StaticWitnessLogic.all_items.items(): - ap_item_code = definition.local_code + ID_START - classification: ItemClassification = ItemClassification.filler - local_only: bool = False - - if definition.category is ItemCategory.SYMBOL: - classification = ItemClassification.progression - StaticWitnessItems.item_groups.setdefault("Symbols", []).append(item_name) - elif definition.category is ItemCategory.DOOR: - classification = ItemClassification.progression - StaticWitnessItems.item_groups.setdefault("Doors", []).append(item_name) - elif definition.category is ItemCategory.LASER: - classification = ItemClassification.progression_skip_balancing - StaticWitnessItems.item_groups.setdefault("Lasers", []).append(item_name) - elif definition.category is ItemCategory.USEFUL: - classification = ItemClassification.useful - elif definition.category is ItemCategory.FILLER: - if item_name in ["Energy Fill (Small)"]: - local_only = True - classification = ItemClassification.filler - elif definition.category is ItemCategory.TRAP: - classification = ItemClassification.trap - elif definition.category is ItemCategory.JOKE: - classification = ItemClassification.filler - - StaticWitnessItems.item_data[item_name] = ItemData(ap_item_code, definition, - classification, local_only) - - @staticmethod - def get_item_to_door_mappings() -> Dict[int, List[int]]: - output: Dict[int, List[int]] = {} - for item_name, item_data in {name: data for name, data in StaticWitnessItems.item_data.items() - if isinstance(data.definition, DoorItemDefinition)}.items(): - item = StaticWitnessItems.item_data[item_name] - output[item.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] - return output - - class WitnessPlayerItems: """ Class that defines Items for a single world """ - def __init__(self, world: "WitnessWorld", logic: WitnessPlayerLogic, locat: WitnessPlayerLocations): + def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, + player_locations: WitnessPlayerLocations) -> None: """Adds event items after logic changes due to options""" self._world: "WitnessWorld" = world self._multiworld: MultiWorld = world.multiworld self._player_id: int = world.player - self._logic: WitnessPlayerLogic = logic - self._locations: WitnessPlayerLocations = locat + self._logic: WitnessPlayerLogic = player_logic + self._locations: WitnessPlayerLocations = player_locations # Duplicate the static item data, then make any player-specific adjustments to classification. - self.item_data: Dict[str, ItemData] = copy.deepcopy(StaticWitnessItems.item_data) + self.item_data: Dict[str, ItemData] = copy.deepcopy(static_witness_items.ITEM_DATA) # Remove all progression items that aren't actually in the game. self.item_data = { name: data for (name, data) in self.item_data.items() if data.classification not in - {ItemClassification.progression, ItemClassification.progression_skip_balancing} - or name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME + {ItemClassification.progression, ItemClassification.progression_skip_balancing} + or name in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME } # Downgrade door items @@ -138,7 +85,7 @@ class WitnessPlayerItems: # Add setting-specific useful items to the mandatory item list. for item_name, item_data in {name: data for (name, data) in self.item_data.items() if data.classification == ItemClassification.useful}.items(): - if item_name in StaticWitnessItems.special_usefuls: + if item_name in static_witness_items._special_usefuls: continue elif item_name == "Energy Capacity": self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES @@ -149,7 +96,7 @@ class WitnessPlayerItems: # Add event items to the item definition list for later lookup. for event_location in self._locations.EVENT_LOCATION_TABLE: - location_name = logic.EVENT_ITEM_PAIRS[event_location] + location_name = player_logic.EVENT_ITEM_PAIRS[event_location] self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT), ItemClassification.progression, False) @@ -219,7 +166,7 @@ class WitnessPlayerItems: output.add("Triangles") # Replace progressive items with their parents. - output = {StaticWitnessLogic.get_parent_progressive_item(item) for item in output} + output = {static_witness_logic.get_parent_progressive_item(item) for item in output} # Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved # before create_items so that we'll be able to check placed items instead of just removing all items mentioned @@ -227,16 +174,16 @@ class WitnessPlayerItems: for plando_setting in self._multiworld.plando_items[self._player_id]: if plando_setting.get("from_pool", True): for item_setting_key in [key for key in ["item", "items"] if key in plando_setting]: - if type(plando_setting[item_setting_key]) is str: + if isinstance(plando_setting[item_setting_key], str): output -= {plando_setting[item_setting_key]} - elif type(plando_setting[item_setting_key]) is dict: + elif isinstance(plando_setting[item_setting_key], dict): output -= {item for item, weight in plando_setting[item_setting_key].items() if weight} else: # Assume this is some other kind of iterable. for inner_item in plando_setting[item_setting_key]: - if type(inner_item) is str: + if isinstance(inner_item, str): output -= {inner_item} - elif type(inner_item) is dict: + elif isinstance(inner_item, dict): output -= {item for item, weight in inner_item.items() if weight} # Sort the output for consistency across versions if the implementation changes but the logic does not. @@ -257,7 +204,7 @@ class WitnessPlayerItems: """ Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool. """ - return [data.ap_code for name, data in StaticWitnessItems.item_data.items() + return [data.ap_code for name, data in static_witness_items.ITEM_DATA.items() if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL] def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]: @@ -267,9 +214,8 @@ class WitnessPlayerItems: if isinstance(item.definition, ProgressiveItemDefinition): # Note: we need to reference the static table here rather than the player-specific one because the child # items were removed from the pool when we pruned out all progression items not in the settings. - output[item.ap_code] = [StaticWitnessItems.item_data[child_item].ap_code + output[item.ap_code] = [static_witness_items.ITEM_DATA[child_item].ap_code for child_item in item.definition.child_item_names] return output -StaticWitnessItems() diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 6bc263b9cc..01caee8951 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -17,11 +17,13 @@ When the world has parsed its options, a second function is called to finalize t import copy from collections import defaultdict -from typing import cast, TYPE_CHECKING +from functools import lru_cache from logging import warning +from typing import TYPE_CHECKING, Dict, FrozenSet, List, Set, Tuple, cast -from .static_logic import StaticWitnessLogic, DoorItemDefinition, ItemCategory, ProgressiveItemDefinition -from .utils import * +from .data import static_logic as static_witness_logic +from .data import utils +from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition if TYPE_CHECKING: from . import WitnessWorld @@ -31,7 +33,7 @@ class WitnessPlayerLogic: """WITNESS LOGIC CLASS""" @lru_cache(maxsize=None) - def reduce_req_within_region(self, panel_hex: str) -> FrozenSet[FrozenSet[str]]: + def reduce_req_within_region(self, entity_hex: str) -> FrozenSet[FrozenSet[str]]: """ Panels in this game often only turn on when other panels are solved. Those other panels may have different item requirements. @@ -40,15 +42,15 @@ class WitnessPlayerLogic: Panels outside of the same region will still be checked manually. """ - if panel_hex in self.COMPLETELY_DISABLED_ENTITIES or panel_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: + if entity_hex in self.COMPLETELY_DISABLED_ENTITIES or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: return frozenset() - entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel_hex] + entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex] these_items = frozenset({frozenset()}) if entity_obj["id"]: - these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"] + these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["items"] these_items = frozenset({ subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI) @@ -58,28 +60,28 @@ class WitnessPlayerLogic: for subset in these_items: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) - these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"] + these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["panels"] - if panel_hex in self.DOOR_ITEMS_BY_ID: - door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]}) + if entity_hex in self.DOOR_ITEMS_BY_ID: + door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]}) all_options: Set[FrozenSet[str]] = set() - for dependentItem in door_items: - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependentItem) + for dependent_item in door_items: + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item) for items_option in these_items: - all_options.add(items_option.union(dependentItem)) + all_options.add(items_option.union(dependent_item)) # If this entity is not an EP, and it has an associated door item, ignore the original power dependencies - if StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["entityType"] != "EP": + if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP": # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved, # except in Expert, where that dependency doesn't exist, but now there *is* a power dependency. # In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies. - if panel_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels): + if entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels): these_items = all_options # Another dependency that is not power-based: The Symmetry Island Upper Panel latches - elif panel_hex == "0x1C349": + elif entity_hex == "0x1C349": these_items = all_options else: @@ -107,9 +109,9 @@ class WitnessPlayerLogic: if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX: new_items = frozenset({frozenset([option_entity])}) - elif (panel_hex, option_entity) in self.CONDITIONAL_EVENTS: + elif (entity_hex, option_entity) in self.CONDITIONAL_EVENTS: new_items = frozenset({frozenset([option_entity])}) - self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(panel_hex, option_entity)] + self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(entity_hex, option_entity)] elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", "PP2 Weirdness", "Theater to Tunnels"}: new_items = frozenset({frozenset([option_entity])}) @@ -121,36 +123,36 @@ class WitnessPlayerLogic: for possibility in new_items ) - dependent_items_for_option = dnf_and([dependent_items_for_option, new_items]) + dependent_items_for_option = utils.dnf_and([dependent_items_for_option, new_items]) for items_option in these_items: - for dependentItem in dependent_items_for_option: - all_options.add(items_option.union(dependentItem)) + for dependent_item in dependent_items_for_option: + all_options.add(items_option.union(dependent_item)) - return dnf_remove_redundancies(frozenset(all_options)) + return utils.dnf_remove_redundancies(frozenset(all_options)) - def make_single_adjustment(self, adj_type: str, line: str): - from . import StaticWitnessItems + def make_single_adjustment(self, adj_type: str, line: str) -> None: + from .data import static_items as static_witness_items """Makes a single logic adjustment based on additional logic file""" if adj_type == "Items": line_split = line.split(" - ") item_name = line_split[0] - if item_name not in StaticWitnessItems.item_data: - raise RuntimeError("Item \"" + item_name + "\" does not exist.") + if item_name not in static_witness_items.ITEM_DATA: + raise RuntimeError(f'Item "{item_name}" does not exist.') self.THEORETICAL_ITEMS.add(item_name) - if isinstance(StaticWitnessLogic.all_items[item_name], ProgressiveItemDefinition): + if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition): self.THEORETICAL_ITEMS_NO_MULTI.update(cast(ProgressiveItemDefinition, - StaticWitnessLogic.all_items[item_name]).child_item_names) + static_witness_logic.ALL_ITEMS[item_name]).child_item_names) else: self.THEORETICAL_ITEMS_NO_MULTI.add(item_name) - if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: - panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes - for panel_hex in panel_hexes: - self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, []).append(item_name) + if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: + entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes + for entity_hex in entity_hexes: + self.DOOR_ITEMS_BY_ID.setdefault(entity_hex, []).append(item_name) return @@ -158,18 +160,18 @@ class WitnessPlayerLogic: item_name = line self.THEORETICAL_ITEMS.discard(item_name) - if isinstance(StaticWitnessLogic.all_items[item_name], ProgressiveItemDefinition): + if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition): self.THEORETICAL_ITEMS_NO_MULTI.difference_update( - cast(ProgressiveItemDefinition, StaticWitnessLogic.all_items[item_name]).child_item_names + cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).child_item_names ) else: self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name) - if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: - panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes - for panel_hex in panel_hexes: - if panel_hex in self.DOOR_ITEMS_BY_ID and item_name in self.DOOR_ITEMS_BY_ID[panel_hex]: - self.DOOR_ITEMS_BY_ID[panel_hex].remove(item_name) + if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: + entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes + for entity_hex in entity_hexes: + if entity_hex in self.DOOR_ITEMS_BY_ID and item_name in self.DOOR_ITEMS_BY_ID[entity_hex]: + self.DOOR_ITEMS_BY_ID[entity_hex].remove(item_name) if adj_type == "Starting Inventory": self.STARTING_INVENTORY.add(line) @@ -189,13 +191,13 @@ class WitnessPlayerLogic: line_split = line.split(" - ") requirement = { - "panels": parse_lambda(line_split[1]), + "panels": utils.parse_lambda(line_split[1]), } if len(line_split) > 2: - required_items = parse_lambda(line_split[2]) + required_items = utils.parse_lambda(line_split[2]) items_actually_in_the_game = [ - item_name for item_name, item_definition in StaticWitnessLogic.all_items.items() + item_name for item_name, item_definition in static_witness_logic.ALL_ITEMS.items() if item_definition.category is ItemCategory.SYMBOL ] required_items = frozenset( @@ -210,21 +212,21 @@ class WitnessPlayerLogic: return if adj_type == "Disabled Locations": - panel_hex = line[:7] + entity_hex = line[:7] - self.COMPLETELY_DISABLED_ENTITIES.add(panel_hex) + self.COMPLETELY_DISABLED_ENTITIES.add(entity_hex) return if adj_type == "Irrelevant Locations": - panel_hex = line[:7] + entity_hex = line[:7] - self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES.add(panel_hex) + self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES.add(entity_hex) return if adj_type == "Region Changes": - new_region_and_options = define_new_region(line + ":") + new_region_and_options = utils.define_new_region(line + ":") self.CONNECTIONS_BY_REGION_NAME[new_region_and_options[0]["name"]] = new_region_and_options[1] @@ -245,11 +247,11 @@ class WitnessPlayerLogic: (target_region, frozenset({frozenset(["TrueOneWay"])})) ) else: - new_lambda = connection[1] | parse_lambda(panel_set_string) + new_lambda = connection[1] | utils.parse_lambda(panel_set_string) self.CONNECTIONS_BY_REGION_NAME[source_region].add((target_region, new_lambda)) break else: # Execute if loop did not break. TIL this is a thing you can do! - new_conn = (target_region, parse_lambda(panel_set_string)) + new_conn = (target_region, utils.parse_lambda(panel_set_string)) self.CONNECTIONS_BY_REGION_NAME[source_region].add(new_conn) if adj_type == "Added Locations": @@ -258,7 +260,7 @@ class WitnessPlayerLogic: self.ADDED_CHECKS.add(line) @staticmethod - def handle_postgame(world: "WitnessWorld"): + def handle_postgame(world: "WitnessWorld") -> List[List[str]]: # In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled. # This has a lot of complicated considerations, which I'll try my best to explain. postgame_adjustments = [] @@ -285,29 +287,29 @@ class WitnessPlayerLogic: # Caves & Challenge should never have anything if doors are vanilla - definitionally "post-game" # This is technically imprecise, but it matches player expectations better. if not (early_caves or doors): - postgame_adjustments.append(get_caves_exclusion_list()) - postgame_adjustments.append(get_beyond_challenge_exclusion_list()) + postgame_adjustments.append(utils.get_caves_exclusion_list()) + postgame_adjustments.append(utils.get_beyond_challenge_exclusion_list()) # If Challenge is the goal, some panels on the way need to be left on, as well as Challenge Vault box itself if not victory == "challenge": - postgame_adjustments.append(get_path_to_challenge_exclusion_list()) - postgame_adjustments.append(get_challenge_vault_box_exclusion_list()) + postgame_adjustments.append(utils.get_path_to_challenge_exclusion_list()) + postgame_adjustments.append(utils.get_challenge_vault_box_exclusion_list()) # Challenge can only have something if the goal is not challenge or longbox itself. # In case of shortbox, it'd have to be a "reverse shortbox" situation where shortbox requires *more* lasers. # In that case, it'd also have to be a doors mode, but that's already covered by the previous block. if not (victory == "elevator" or reverse_shortbox_goal): - postgame_adjustments.append(get_beyond_challenge_exclusion_list()) + postgame_adjustments.append(utils.get_beyond_challenge_exclusion_list()) if not victory == "challenge": - postgame_adjustments.append(get_challenge_vault_box_exclusion_list()) + postgame_adjustments.append(utils.get_challenge_vault_box_exclusion_list()) # Mountain can't be reached if the goal is shortbox (or "reverse long box") if not mountain_enterable_from_top: - postgame_adjustments.append(get_mountain_upper_exclusion_list()) + postgame_adjustments.append(utils.get_mountain_upper_exclusion_list()) # Same goes for lower mountain, but that one *can* be reached in remote doors modes. if not doors: - postgame_adjustments.append(get_mountain_lower_exclusion_list()) + postgame_adjustments.append(utils.get_mountain_lower_exclusion_list()) # The Mountain Bottom Floor Discard is a bit complicated, so we handle it separately. ("it" == the Discard) # In Elevator Goal, it is definitionally in the post-game, unless remote doors is played. @@ -319,15 +321,15 @@ class WitnessPlayerLogic: # This has different consequences depending on whether remote doors is being played. # If doors are vanilla, Bottom Floor Discard locks a door to an area, which has to be disabled as well. if doors: - postgame_adjustments.append(get_bottom_floor_discard_exclusion_list()) + postgame_adjustments.append(utils.get_bottom_floor_discard_exclusion_list()) else: - postgame_adjustments.append(get_bottom_floor_discard_nondoors_exclusion_list()) + postgame_adjustments.append(utils.get_bottom_floor_discard_nondoors_exclusion_list()) # In Challenge goal + early_caves + vanilla doors, you could find something important on Bottom Floor Discard, # including the Caves Shortcuts themselves if playing "early_caves: start_inventory". # This is another thing that was deemed "unfun" more than fitting the actual definition of post-game. if victory == "challenge" and early_caves and not doors: - postgame_adjustments.append(get_bottom_floor_discard_nondoors_exclusion_list()) + postgame_adjustments.append(utils.get_bottom_floor_discard_nondoors_exclusion_list()) # If we have a proper short box goal, long box will never be activated first. if proper_shortbox_goal: @@ -335,7 +337,7 @@ class WitnessPlayerLogic: return postgame_adjustments - def make_options_adjustments(self, world: "WitnessWorld"): + def make_options_adjustments(self, world: "WitnessWorld") -> None: """Makes logic adjustments based on options""" adjustment_linesets_in_order = [] @@ -356,15 +358,15 @@ class WitnessPlayerLogic: # In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both # (remote) doors and lasers are shuffled. if not world.options.disable_non_randomized_puzzles or (doors and lasers): - adjustment_linesets_in_order.append(get_discard_exclusion_list()) + adjustment_linesets_in_order.append(utils.get_discard_exclusion_list()) if doors: - adjustment_linesets_in_order.append(get_bottom_floor_discard_exclusion_list()) + adjustment_linesets_in_order.append(utils.get_bottom_floor_discard_exclusion_list()) if not world.options.shuffle_vault_boxes: - adjustment_linesets_in_order.append(get_vault_exclusion_list()) + adjustment_linesets_in_order.append(utils.get_vault_exclusion_list()) if not victory == "challenge": - adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list()) + adjustment_linesets_in_order.append(utils.get_challenge_vault_box_exclusion_list()) # Victory Condition @@ -387,54 +389,54 @@ class WitnessPlayerLogic: ]) if world.options.disable_non_randomized_puzzles: - adjustment_linesets_in_order.append(get_disable_unrandomized_list()) + adjustment_linesets_in_order.append(utils.get_disable_unrandomized_list()) if world.options.shuffle_symbols: - adjustment_linesets_in_order.append(get_symbol_shuffle_list()) + adjustment_linesets_in_order.append(utils.get_symbol_shuffle_list()) if world.options.EP_difficulty == "normal": - adjustment_linesets_in_order.append(get_ep_easy()) + adjustment_linesets_in_order.append(utils.get_ep_easy()) elif world.options.EP_difficulty == "tedious": - adjustment_linesets_in_order.append(get_ep_no_eclipse()) + adjustment_linesets_in_order.append(utils.get_ep_no_eclipse()) if world.options.door_groupings == "regional": if world.options.shuffle_doors == "panels": - adjustment_linesets_in_order.append(get_simple_panels()) + adjustment_linesets_in_order.append(utils.get_simple_panels()) elif world.options.shuffle_doors == "doors": - adjustment_linesets_in_order.append(get_simple_doors()) + adjustment_linesets_in_order.append(utils.get_simple_doors()) elif world.options.shuffle_doors == "mixed": - adjustment_linesets_in_order.append(get_simple_doors()) - adjustment_linesets_in_order.append(get_simple_additional_panels()) + adjustment_linesets_in_order.append(utils.get_simple_doors()) + adjustment_linesets_in_order.append(utils.get_simple_additional_panels()) else: if world.options.shuffle_doors == "panels": - adjustment_linesets_in_order.append(get_complex_door_panels()) - adjustment_linesets_in_order.append(get_complex_additional_panels()) + adjustment_linesets_in_order.append(utils.get_complex_door_panels()) + adjustment_linesets_in_order.append(utils.get_complex_additional_panels()) elif world.options.shuffle_doors == "doors": - adjustment_linesets_in_order.append(get_complex_doors()) + adjustment_linesets_in_order.append(utils.get_complex_doors()) elif world.options.shuffle_doors == "mixed": - adjustment_linesets_in_order.append(get_complex_doors()) - adjustment_linesets_in_order.append(get_complex_additional_panels()) + adjustment_linesets_in_order.append(utils.get_complex_doors()) + adjustment_linesets_in_order.append(utils.get_complex_additional_panels()) if world.options.shuffle_boat: - adjustment_linesets_in_order.append(get_boat()) + adjustment_linesets_in_order.append(utils.get_boat()) if world.options.early_caves == "starting_inventory": - adjustment_linesets_in_order.append(get_early_caves_start_list()) + adjustment_linesets_in_order.append(utils.get_early_caves_start_list()) if world.options.early_caves == "add_to_pool" and not doors: - adjustment_linesets_in_order.append(get_early_caves_list()) + adjustment_linesets_in_order.append(utils.get_early_caves_list()) if world.options.elevators_come_to_you: - adjustment_linesets_in_order.append(get_elevators_come_to_you()) + adjustment_linesets_in_order.append(utils.get_elevators_come_to_you()) for item in self.YAML_ADDED_ITEMS: adjustment_linesets_in_order.append(["Items:", item]) if lasers: - adjustment_linesets_in_order.append(get_laser_shuffle()) + adjustment_linesets_in_order.append(utils.get_laser_shuffle()) if world.options.shuffle_EPs and world.options.obelisk_keys: - adjustment_linesets_in_order.append(get_obelisk_keys()) + adjustment_linesets_in_order.append(utils.get_obelisk_keys()) if world.options.shuffle_EPs == "obelisk_sides": ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items() @@ -446,10 +448,10 @@ class WitnessPlayerLogic: ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"] self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}" else: - adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:]) + adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.get_ep_obelisks()[1:]) if not world.options.shuffle_EPs: - adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_all_individual()[1:]) + adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.get_ep_all_individual()[1:]) for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS: if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME: @@ -480,7 +482,7 @@ class WitnessPlayerLogic: if entity_id in self.DOOR_ITEMS_BY_ID: del self.DOOR_ITEMS_BY_ID[entity_id] - def make_dependency_reduced_checklist(self): + def make_dependency_reduced_checklist(self) -> None: """ Turns dependent check set into semi-independent check set """ @@ -492,10 +494,10 @@ class WitnessPlayerLogic: for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: if item not in self.THEORETICAL_ITEMS: - progressive_item_name = StaticWitnessLogic.get_parent_progressive_item(item) + progressive_item_name = static_witness_logic.get_parent_progressive_item(item) self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name) child_items = cast(ProgressiveItemDefinition, - StaticWitnessLogic.all_items[progressive_item_name]).child_item_names + static_witness_logic.ALL_ITEMS[progressive_item_name]).child_item_names multi_list = [child_item for child_item in child_items if child_item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI] self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1 @@ -520,24 +522,24 @@ class WitnessPlayerLogic: if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]: region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"] - entity_req = dnf_and([entity_req, frozenset({frozenset({region_name})})]) + entity_req = utils.dnf_and([entity_req, frozenset({frozenset({region_name})})]) individual_entity_requirements.append(entity_req) - overall_requirement |= dnf_and(individual_entity_requirements) + overall_requirement |= utils.dnf_and(individual_entity_requirements) new_connections.append((connection[0], overall_requirement)) self.CONNECTIONS_BY_REGION_NAME[region] = new_connections - def solvability_guaranteed(self, entity_hex: str): + def solvability_guaranteed(self, entity_hex: str) -> bool: return not ( entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY or entity_hex in self.COMPLETELY_DISABLED_ENTITIES or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES ) - def determine_unrequired_entities(self, world: "WitnessWorld"): + def determine_unrequired_entities(self, world: "WitnessWorld") -> None: """Figure out which major items are actually useless in this world's settings""" # Gather quick references to relevant options @@ -596,7 +598,7 @@ class WitnessPlayerLogic: item_name for item_name, is_required in is_item_required_dict.items() if not is_required } - def make_event_item_pair(self, panel: str): + def make_event_item_pair(self, panel: str) -> Tuple[str, str]: """ Makes a pair of an event panel and its event item """ @@ -604,12 +606,12 @@ class WitnessPlayerLogic: name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action if panel not in self.USED_EVENT_NAMES_BY_HEX: - warning("Panel \"" + name + "\" does not have an associated event name.") + warning(f'Panel "{name}" does not have an associated event name.') self.USED_EVENT_NAMES_BY_HEX[panel] = name + " Event" pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel]) return pair - def make_event_panel_lists(self): + def make_event_panel_lists(self) -> None: self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX) @@ -623,7 +625,7 @@ class WitnessPlayerLogic: pair = self.make_event_item_pair(panel) self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] - def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]): + def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None: self.YAML_DISABLED_LOCATIONS = disabled_locations self.YAML_ADDED_ITEMS = start_inv @@ -646,11 +648,11 @@ class WitnessPlayerLogic: self.DIFFICULTY = world.options.puzzle_randomization if self.DIFFICULTY == "sigma_normal": - self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal + self.REFERENCE_LOGIC = static_witness_logic.sigma_normal elif self.DIFFICULTY == "sigma_expert": - self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_expert + self.REFERENCE_LOGIC = static_witness_logic.sigma_expert elif self.DIFFICULTY == "none": - self.REFERENCE_LOGIC = StaticWitnessLogic.vanilla + self.REFERENCE_LOGIC = static_witness_logic.vanilla self.CONNECTIONS_BY_REGION_NAME = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME) self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 350017c694..e1f0ddb216 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -2,26 +2,29 @@ Defines Region for The Witness, assigns locations to them, and connects them with the proper requirements """ -from typing import FrozenSet, TYPE_CHECKING, Dict, Tuple, List +from collections import defaultdict +from typing import TYPE_CHECKING, Dict, FrozenSet, List, Tuple from BaseClasses import Entrance, Region -from Utils import KeyedDefaultDict -from .static_logic import StaticWitnessLogic -from .locations import WitnessPlayerLocations, StaticWitnessLocations + +from worlds.generic.Rules import CollectionRule + +from .data import static_logic as static_witness_logic +from .locations import WitnessPlayerLocations, static_witness_locations from .player_logic import WitnessPlayerLogic if TYPE_CHECKING: from . import WitnessWorld -class WitnessRegions: +class WitnessPlayerRegions: """Class that defines Witness Regions""" - locat = None + player_locations = None logic = None @staticmethod - def make_lambda(item_requirement: FrozenSet[FrozenSet[str]], world: "WitnessWorld"): + def make_lambda(item_requirement: FrozenSet[FrozenSet[str]], world: "WitnessWorld") -> CollectionRule: from .rules import _meets_item_requirements """ @@ -82,7 +85,7 @@ class WitnessRegions: for dependent_region in mentioned_regions: world.multiworld.register_indirect_condition(regions_by_name[dependent_region], connection) - def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic): + def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None: """ Creates all the regions for The Witness """ @@ -94,16 +97,17 @@ class WitnessRegions: for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items(): locations_for_this_region = [ self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["panels"] - if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE + if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] + in self.player_locations.CHECK_LOCATION_TABLE ] locations_for_this_region += [ - StaticWitnessLocations.get_event_name(panel) for panel in region["panels"] - if StaticWitnessLocations.get_event_name(panel) in self.locat.EVENT_LOCATION_TABLE + static_witness_locations.get_event_name(panel) for panel in region["panels"] + if static_witness_locations.get_event_name(panel) in self.player_locations.EVENT_LOCATION_TABLE ] all_locations = all_locations | set(locations_for_this_region) - new_region = create_region(world, region_name, self.locat, locations_for_this_region) + new_region = create_region(world, region_name, self.player_locations, locations_for_this_region) regions_by_name[region_name] = new_region @@ -133,16 +137,16 @@ class WitnessRegions: world.multiworld.regions += self.created_regions.values() - def __init__(self, locat: WitnessPlayerLocations, world: "WitnessWorld"): + def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None: difficulty = world.options.puzzle_randomization if difficulty == "sigma_normal": - self.reference_logic = StaticWitnessLogic.sigma_normal + self.reference_logic = static_witness_logic.sigma_normal elif difficulty == "sigma_expert": - self.reference_logic = StaticWitnessLogic.sigma_expert + self.reference_logic = static_witness_logic.sigma_expert elif difficulty == "none": - self.reference_logic = StaticWitnessLogic.vanilla + self.reference_logic = static_witness_logic.vanilla - self.locat = locat - self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = KeyedDefaultDict(lambda _: []) + self.player_locations = player_locations + self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: []) self.created_regions: Dict[str, Region] = dict() diff --git a/worlds/witness/ruff.toml b/worlds/witness/ruff.toml new file mode 100644 index 0000000000..d42361a4aa --- /dev/null +++ b/worlds/witness/ruff.toml @@ -0,0 +1,11 @@ +line-length = 120 + +[lint] +select = ["E", "F", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"] +ignore = ["RUF012", "RUF100"] + +[per-file-ignores] +# The way options definitions work right now, I am forced to break line length requirements. +"options.py" = ["E501"] +# The import list would just be so big if I imported every option individually in presets.py +"presets.py" = ["F403", "F405"] diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 8636829a4e..6445545e9b 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -3,13 +3,16 @@ Defines the rules by which locations can be accessed, depending on the items received """ -from typing import TYPE_CHECKING, Callable, FrozenSet +from typing import TYPE_CHECKING, FrozenSet from BaseClasses import CollectionState -from .player_logic import WitnessPlayerLogic + +from worlds.generic.Rules import CollectionRule, set_rule + +from . import WitnessPlayerRegions +from .data import static_logic as static_witness_logic from .locations import WitnessPlayerLocations -from . import StaticWitnessLogic, WitnessRegions -from worlds.generic.Rules import set_rule +from .player_logic import WitnessPlayerLogic if TYPE_CHECKING: from . import WitnessWorld @@ -30,17 +33,17 @@ laser_hexes = [ def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, - redirect_required: bool) -> Callable[[CollectionState], bool]: + redirect_required: bool) -> CollectionRule: if laser_hex == "0x012FB" and redirect_required: return lambda state: ( - _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat)(state) + _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state) and state.has("Desert Laser Redirection", player) ) else: - return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat) + return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations) -def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> Callable[[CollectionState], bool]: +def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule: laser_lambdas = [] for laser_hex in laser_hexes: @@ -52,7 +55,7 @@ def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic, - locat: WitnessPlayerLocations) -> Callable[[CollectionState], bool]: + player_locations: WitnessPlayerLocations) -> CollectionRule: """ Determines whether a panel can be solved """ @@ -60,15 +63,16 @@ def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logi panel_obj = player_logic.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel] entity_name = panel_obj["checkName"] - if entity_name + " Solved" in locat.EVENT_LOCATION_TABLE: + if entity_name + " Solved" in player_locations.EVENT_LOCATION_TABLE: return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player) else: return make_lambda(panel, world) -def _can_move_either_direction(state: CollectionState, source: str, target: str, regio: WitnessRegions) -> bool: - entrance_forward = regio.created_entrances[source, target] - entrance_backward = regio.created_entrances[target, source] +def _can_move_either_direction(state: CollectionState, source: str, target: str, + player_regions: WitnessPlayerRegions) -> bool: + entrance_forward = player_regions.created_entrances[source, target] + entrance_backward = player_regions.created_entrances[target, source] return ( any(entrance.can_reach(state) for entrance in entrance_forward) @@ -81,49 +85,49 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: player = world.player hedge_2_access = ( - _can_move_either_direction(state, "Keep 2nd Maze", "Keep", world.regio) + _can_move_either_direction(state, "Keep 2nd Maze", "Keep", world.player_regions) ) hedge_3_access = ( - _can_move_either_direction(state, "Keep 3rd Maze", "Keep", world.regio) - or _can_move_either_direction(state, "Keep 3rd Maze", "Keep 2nd Maze", world.regio) - and hedge_2_access + _can_move_either_direction(state, "Keep 3rd Maze", "Keep", world.player_regions) + or _can_move_either_direction(state, "Keep 3rd Maze", "Keep 2nd Maze", world.player_regions) + and hedge_2_access ) hedge_4_access = ( - _can_move_either_direction(state, "Keep 4th Maze", "Keep", world.regio) - or _can_move_either_direction(state, "Keep 4th Maze", "Keep 3rd Maze", world.regio) - and hedge_3_access + _can_move_either_direction(state, "Keep 4th Maze", "Keep", world.player_regions) + or _can_move_either_direction(state, "Keep 4th Maze", "Keep 3rd Maze", world.player_regions) + and hedge_3_access ) hedge_access = ( - _can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.regio) - and state.can_reach("Keep", "Region", player) - and hedge_4_access + _can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.player_regions) + and state.can_reach("Keep", "Region", player) + and hedge_4_access ) backwards_to_fourth = ( - state.can_reach("Keep", "Region", player) - and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Keep Tower", world.regio) - and ( - _can_move_either_direction(state, "Keep", "Keep Tower", world.regio) - or hedge_access - ) + state.can_reach("Keep", "Region", player) + and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Keep Tower", world.player_regions) + and ( + _can_move_either_direction(state, "Keep", "Keep Tower", world.player_regions) + or hedge_access + ) ) shadows_shortcut = ( - state.can_reach("Main Island", "Region", player) - and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.regio) + state.can_reach("Main Island", "Region", player) + and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.player_regions) ) backwards_access = ( - _can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.regio) - and (backwards_to_fourth or shadows_shortcut) + _can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.player_regions) + and (backwards_to_fourth or shadows_shortcut) ) front_access = ( - _can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.regio) - and state.can_reach("Keep", "Region", player) + _can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.player_regions) + and state.can_reach("Keep", "Region", player) ) return front_access and backwards_access @@ -131,27 +135,27 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool: direct_access = ( - _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio) - and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio) + _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.player_regions) + and _can_move_either_direction(state, "Theater", "Windmill Interior", world.player_regions) ) theater_from_town = ( - _can_move_either_direction(state, "Town", "Windmill Interior", world.regio) - and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio) - or _can_move_either_direction(state, "Town", "Theater", world.regio) + _can_move_either_direction(state, "Town", "Windmill Interior", world.player_regions) + and _can_move_either_direction(state, "Theater", "Windmill Interior", world.player_regions) + or _can_move_either_direction(state, "Town", "Theater", world.player_regions) ) tunnels_from_town = ( - _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio) - and _can_move_either_direction(state, "Town", "Windmill Interior", world.regio) - or _can_move_either_direction(state, "Tunnels", "Town", world.regio) + _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.player_regions) + and _can_move_either_direction(state, "Town", "Windmill Interior", world.player_regions) + or _can_move_either_direction(state, "Tunnels", "Town", world.player_regions) ) return direct_access or theater_from_town and tunnels_from_town def _has_item(item: str, world: "WitnessWorld", player: int, - player_logic: WitnessPlayerLogic, locat: WitnessPlayerLocations) -> Callable[[CollectionState], bool]: + player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule: if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME: return lambda state: state.can_reach(item, "Region", player) if item == "7 Lasers": @@ -171,21 +175,21 @@ def _has_item(item: str, world: "WitnessWorld", player: int, elif item == "Theater to Tunnels": return lambda state: _can_do_theater_to_tunnels(state, world) if item in player_logic.USED_EVENT_NAMES_BY_HEX: - return _can_solve_panel(item, world, player, player_logic, locat) + return _can_solve_panel(item, world, player, player_logic, player_locations) - prog_item = StaticWitnessLogic.get_parent_progressive_item(item) + prog_item = static_witness_logic.get_parent_progressive_item(item) return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]) def _meets_item_requirements(requirements: FrozenSet[FrozenSet[str]], - world: "WitnessWorld") -> Callable[[CollectionState], bool]: + world: "WitnessWorld") -> CollectionRule: """ Checks whether item and panel requirements are met for a panel """ lambda_conversion = [ - [_has_item(item, world, world.player, world.player_logic, world.locat) for item in subset] + [_has_item(item, world, world.player, world.player_logic, world.player_locations) for item in subset] for subset in requirements ] @@ -195,7 +199,7 @@ def _meets_item_requirements(requirements: FrozenSet[FrozenSet[str]], ) -def make_lambda(entity_hex: str, world: "WitnessWorld") -> Callable[[CollectionState], bool]: +def make_lambda(entity_hex: str, world: "WitnessWorld") -> CollectionRule: """ Lambdas are created in a for loop so values need to be captured """ @@ -204,15 +208,15 @@ def make_lambda(entity_hex: str, world: "WitnessWorld") -> Callable[[CollectionS return _meets_item_requirements(entity_req, world) -def set_rules(world: "WitnessWorld"): +def set_rules(world: "WitnessWorld") -> None: """ Sets all rules for all locations """ - for location in world.locat.CHECK_LOCATION_TABLE: + for location in world.player_locations.CHECK_LOCATION_TABLE: real_location = location - if location in world.locat.EVENT_LOCATION_TABLE: + if location in world.player_locations.EVENT_LOCATION_TABLE: real_location = location[:-7] associated_entity = world.player_logic.REFERENCE_LOGIC.ENTITIES_BY_NAME[real_location] @@ -220,8 +224,8 @@ def set_rules(world: "WitnessWorld"): rule = make_lambda(entity_hex, world) - location = world.multiworld.get_location(location, world.player) + location = world.get_location(location) set_rule(location, rule) - world.multiworld.completion_condition[world.player] = lambda state: state.has('Victory', world.player) + world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player) diff --git a/worlds/yoshisisland/__init__.py b/worlds/yoshisisland/__init__.py index b5d7e137b5..f1aba3018b 100644 --- a/worlds/yoshisisland/__init__.py +++ b/worlds/yoshisisland/__init__.py @@ -32,8 +32,7 @@ class YoshisIslandWeb(WebWorld): setup_en = Tutorial( "Multiworld Setup Guide", - "A guide to setting up the Yoshi's Island randomizer" - "and connecting to an Archipelago server.", + "A guide to setting up the Yoshi's Island randomizer and connecting to an Archipelago server.", "English", "setup_en.md", "setup/en", diff --git a/worlds/yoshisisland/docs/setup_en.md b/worlds/yoshisisland/docs/setup_en.md index 4c8ffad704..d761446089 100644 --- a/worlds/yoshisisland/docs/setup_en.md +++ b/worlds/yoshisisland/docs/setup_en.md @@ -72,8 +72,7 @@ first time launching, you may be prompted to allow it to communicate through the 3. Click on **New Lua Script Window...** 4. In the new window, click **Browse...** 5. Select the connector lua file included with your client - - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the - emulator is 64-bit or 32-bit. + - Look in the Archipelago folder for `/SNI/lua/Connector.lua`. 6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 2dc634e47d..66f062631c 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -109,9 +109,7 @@ class ZorkGrandInquisitorWorld(World): region_mapping[data.region], ) - location.event = isinstance(location_enum_item, ZorkGrandInquisitorEvents) - - if location.event: + if isinstance(location_enum_item, ZorkGrandInquisitorEvents): location.place_locked_item( ZorkGrandInquisitorItem( data.event_item_name,