diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index f1464a1ffd..23cf1a4cbd 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -39,10 +39,9 @@ def create_task_log_exception(awaitable: typing.Awaitable) -> asyncio.Task: class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): ctx: "JakAndDaxterContext" - # The command processor is not async and cannot use async tasks, so long-running operations - # like the /repl connect command (which takes 10-15 seconds to compile the game) have to be requested - # with user-initiated flags. The text client will hang while the operation runs, but at least we can - # inform the user to wait. The flags are checked by the agents every main_tick. + # The command processor is not async so long-running operations like the /repl connect command + # (which takes 10-15 seconds to compile the game) have to be requested with user-initiated flags. + # The flags are checked by the agents every main_tick. def _cmd_repl(self, *arguments: str): """Sends a command to the OpenGOAL REPL. Arguments: - connect : connect the client to the REPL (goalc). @@ -52,7 +51,7 @@ class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): logger.info("This may take a bit... Wait for the success audio cue before continuing!") self.ctx.repl.initiated_connect = True if arguments[0] == "status": - self.ctx.repl.print_status() + create_task_log_exception(self.ctx.repl.print_status()) def _cmd_memr(self, *arguments: str): """Sends a command to the Memory Reader. Arguments: @@ -119,41 +118,41 @@ class JakAndDaxterContext(CommonContext): else: orbsanity_bundle = 1 - # Keep compatibility with 0.0.8 at least for now + # Keep compatibility with 0.0.8 at least for now - TODO: Remove this. if "completion_condition" in slot_data: goal_id = slot_data["completion_condition"] else: goal_id = slot_data["jak_completion_condition"] - self.repl.setup_options(orbsanity_option, - orbsanity_bundle, - slot_data["fire_canyon_cell_count"], - slot_data["mountain_pass_cell_count"], - slot_data["lava_tube_cell_count"], - goal_id) + create_task_log_exception( + self.repl.setup_options(orbsanity_option, + orbsanity_bundle, + slot_data["fire_canyon_cell_count"], + slot_data["mountain_pass_cell_count"], + slot_data["lava_tube_cell_count"], + goal_id)) # Because Orbsanity and the orb traders in the game are intrinsically linked, we need the server # to track our trades at all times to support async play. "Retrieved" will tell us the orbs we lost, # while "ReceivedItems" will tell us the orbs we gained. This will give us the correct balance. if orbsanity_option in [EnableOrbsanity.option_per_level, EnableOrbsanity.option_global]: async def get_orb_balance(): - await self.send_msgs([{"cmd": "Get", - "keys": [f"jakanddaxter_{self.auth}_orbs_paid"] - }]) + await self.send_msgs([{"cmd": "Get", "keys": [f"jakanddaxter_{self.auth}_orbs_paid"]}]) create_task_log_exception(get_orb_balance()) if cmd == "Retrieved": if f"jakanddaxter_{self.auth}_orbs_paid" in args["keys"]: orbs_traded = args["keys"][f"jakanddaxter_{self.auth}_orbs_paid"] - self.repl.subtract_traded_orbs(orbs_traded if orbs_traded is not None else 0) + orbs_traded = orbs_traded if orbs_traded is not None else 0 + create_task_log_exception(self.repl.subtract_traded_orbs(orbs_traded)) if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): logger.debug(f"index: {str(index)}, item: {str(item)}") self.repl.item_inbox[index] = item - def on_print_json(self, args: dict) -> None: + async def json_to_game_text(self, args: dict): if "type" in args and args["type"] in {"ItemSend"}: item = args["item"] recipient = args["receiving"] @@ -179,8 +178,14 @@ class JakAndDaxterContext(CommonContext): self.repl.their_item_owner = self.player_names[recipient] # Write to game display. - self.repl.write_game_text() + await self.repl.write_game_text() + def on_print_json(self, args: dict) -> None: + + # Even though N items come in as 1 ReceivedItems packet, there are still N PrintJson packets to process, + # and they all arrive before the ReceivedItems packet does. Defer processing of these packets as + # async tasks to speed up large releases of items. + create_task_log_exception(self.json_to_game_text(args)) super(JakAndDaxterContext, self).on_print_json(args) def on_deathlink(self, data: dict): @@ -214,7 +219,7 @@ class JakAndDaxterContext(CommonContext): # Reset all flags. self.memr.send_deathlink = False self.memr.cause_of_death = "" - self.repl.reset_deathlink() + await self.repl.reset_deathlink() def on_deathlink_check(self): create_task_log_exception(self.ap_inform_deathlink()) @@ -225,14 +230,6 @@ class JakAndDaxterContext(CommonContext): def on_deathlink_toggle(self): create_task_log_exception(self.ap_inform_deathlink_toggle()) - async def repl_reset_orbsanity(self): - if self.memr.orbsanity_enabled: - self.memr.reset_orbsanity = False - self.repl.reset_orbsanity() - - def on_orbsanity_check(self): - create_task_log_exception(self.repl_reset_orbsanity()) - async def ap_inform_orb_trade(self, orbs_changed: int): if self.memr.orbsanity_enabled: await self.send_msgs([{"cmd": "Set", @@ -256,7 +253,6 @@ class JakAndDaxterContext(CommonContext): self.on_finish_check, self.on_deathlink_check, self.on_deathlink_toggle, - self.on_orbsanity_check, self.on_orb_trade) await asyncio.sleep(0.1) diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index 718fe6ff45..57f6c1ca74 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -1,18 +1,21 @@ from dataclasses import dataclass -from Options import Toggle, PerGameCommonOptions, Choice, Range +from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range class EnableMoveRandomizer(Toggle): - """Enable to include movement options as items in the randomizer. Jak is only able to run, swim, and single jump, - until you find his other moves. This adds 11 items to the pool.""" + """Enable to include movement options as items in the randomizer. Jak is only able to run, swim, and single jump + until you find his other moves. + + This adds 11 items to the pool.""" display_name = "Enable Move Randomizer" class EnableOrbsanity(Choice): - """Enable to include bundles of Precursor Orbs as an ordered list of progressive checks. Every time you collect - the chosen number of orbs, you will trigger the next release in the list. "Per Level" means these lists are - generated and populated for each level in the game (Geyser Rock, Sandover Village, etc.). "Global" means there is - only one list for the entire game. + """Enable to include bundles of Precursor Orbs as an ordered list of progressive checks. Every time you collect the + chosen number of orbs, you will trigger the next release in the list. + + "Per Level" means these lists are generated and populated for each level in the game. "Global" means there + is only one list for the entire game. This adds a number of Items and Locations to the pool inversely proportional to the size of the bundle. For example, if your bundle size is 20 orbs, you will add 100 items to the pool. If your bundle size is 250 orbs, @@ -25,8 +28,7 @@ class EnableOrbsanity(Choice): class GlobalOrbsanityBundleSize(Choice): - """Set the size of the bundle for Global Orbsanity. - This only applies if "Enable Orbsanity" is set to "Global." + """Set the orb bundle size for Global Orbsanity. This only applies if "Enable Orbsanity" is set to "Global." There are 2000 orbs in the game, so your bundle size must be a factor of 2000.""" display_name = "Global Orbsanity Bundle Size" option_1_orb = 1 @@ -53,8 +55,7 @@ class GlobalOrbsanityBundleSize(Choice): class PerLevelOrbsanityBundleSize(Choice): - """Set the size of the bundle for Per Level Orbsanity. - This only applies if "Enable Orbsanity" is set to "Per Level." + """Set the orb bundle size for Per Level Orbsanity. This only applies if "Enable Orbsanity" is set to "Per Level." There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50.""" display_name = "Per Level Orbsanity Bundle Size" option_1_orb = 1 @@ -113,3 +114,4 @@ class JakAndDaxterOptions(PerGameCommonOptions): mountain_pass_cell_count: MountainPassCellCount lava_tube_cell_count: LavaTubeCellCount jak_completion_condition: CompletionCondition + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 95d0779951..f165bb406d 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -100,13 +100,8 @@ def can_trade_orbsanity(state: CollectionState, def can_free_scout_flies(state: CollectionState, player: int) -> bool: - return (state.has("Jump Dive", player) - or (state.has("Crouch", player) - and state.has("Crouch Uppercut", player))) + return state.has("Jump Dive", player) or state.has_all({"Crouch", "Crouch Uppercut"}, player) def can_fight(state: CollectionState, player: int) -> bool: - return (state.has("Jump Dive", player) - or state.has("Jump Kick", player) - or state.has("Punch", player) - or state.has("Kick", player)) + return state.has_any({"Jump Dive", "Jump Kick", "Punch", "Kick"}, player) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 14cd63c10f..049fe1eb7c 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -70,8 +70,7 @@ moverando_enabled_offset = offsets.define(sizeof_uint8) # Orbsanity information. orbsanity_option_offset = offsets.define(sizeof_uint8) orbsanity_bundle_offset = offsets.define(sizeof_uint32) -collected_bundle_level_offset = offsets.define(sizeof_uint8) -collected_bundle_count_offset = offsets.define(sizeof_uint32) +collected_bundle_offset = offsets.define(sizeof_uint32, 17) # Progression and Completion information. fire_canyon_unlock_offset = offsets.define(sizeof_float) @@ -151,7 +150,6 @@ class JakAndDaxterMemoryReader: # Orbsanity handling orbsanity_enabled: bool = False - reset_orbsanity: bool = False orbs_paid: int = 0 def __init__(self, marker: ByteString = b'UnLiStEdStRaTs_JaK1\x00'): @@ -163,7 +161,6 @@ class JakAndDaxterMemoryReader: finish_callback: Callable, deathlink_callback: Callable, deathlink_toggle: Callable, - orbsanity_callback: Callable, paid_orbs_callback: Callable): if self.initiated_connect: await self.connect() @@ -200,9 +197,6 @@ class JakAndDaxterMemoryReader: if self.send_deathlink: deathlink_callback() - if self.reset_orbsanity: - orbsanity_callback() - if self.orbs_paid > 0: paid_orbs_callback(self.orbs_paid) self.orbs_paid = 0 @@ -303,26 +297,40 @@ class JakAndDaxterMemoryReader: # self.moverando_enabled = bool(moverando_flag) orbsanity_option = self.read_goal_address(orbsanity_option_offset, sizeof_uint8) - orbsanity_bundle = self.read_goal_address(orbsanity_bundle_offset, sizeof_uint32) + bundle_size = self.read_goal_address(orbsanity_bundle_offset, sizeof_uint32) self.orbsanity_enabled = orbsanity_option > 0 - # Treat these values like the Deathlink flag. They need to be reset once they are checked. - collected_bundle_level = self.read_goal_address(collected_bundle_level_offset, sizeof_uint8) - collected_bundle_count = self.read_goal_address(collected_bundle_count_offset, sizeof_uint32) + # Per Level Orbsanity option. Only need to do this loop if we chose this setting. + if orbsanity_option == 1: + for level in range(0, 16): + collected_bundles = self.read_goal_address(collected_bundle_offset + (level * sizeof_uint32), + sizeof_uint32) - if orbsanity_option > 0 and collected_bundle_count > 0: - # Count up from the first bundle, by bundle size, until you reach the latest collected bundle. - # e.g. {25, 50, 75, 100, 125...} - for k in range(orbsanity_bundle, - orbsanity_bundle + collected_bundle_count, # Range max is non-inclusive. - orbsanity_bundle): + # Count up from the first bundle, by bundle size, until you reach the latest collected bundle. + # e.g. {25, 50, 75, 100, 125...} + if collected_bundles > 0: + for bundle in range(bundle_size, + bundle_size + collected_bundles, # Range max is non-inclusive. + bundle_size): - bundle_ap_id = Orbs.to_ap_id(Orbs.find_address(collected_bundle_level, k, orbsanity_bundle)) - if bundle_ap_id not in self.location_outbox: - self.location_outbox.append(bundle_ap_id) - logger.debug("Checked orb bundle: " + str(bundle_ap_id)) + bundle_ap_id = Orbs.to_ap_id(Orbs.find_address(level, bundle, bundle_size)) + if bundle_ap_id not in self.location_outbox: + self.location_outbox.append(bundle_ap_id) + logger.debug("Checked orb bundle: " + str(bundle_ap_id)) - # self.reset_orbsanity = True + # Global Orbsanity option. Index 16 refers to all orbs found regardless of level. + if orbsanity_option == 2: + collected_bundles = self.read_goal_address(collected_bundle_offset + (16 * sizeof_uint32), + sizeof_uint32) + if collected_bundles > 0: + for bundle in range(bundle_size, + bundle_size + collected_bundles, # Range max is non-inclusive. + bundle_size): + + bundle_ap_id = Orbs.to_ap_id(Orbs.find_address(16, bundle, bundle_size)) + if bundle_ap_id not in self.location_outbox: + self.location_outbox.append(bundle_ap_id) + logger.debug("Checked orb bundle: " + str(bundle_ap_id)) completed = self.read_goal_address(completed_offset, sizeof_uint8) if completed > 0 and not self.finished_game: diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 712c268c6f..09b488d174 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -1,13 +1,15 @@ import json import time import struct -from typing import Dict, Callable import random -from socket import socket, AF_INET, SOCK_STREAM +from typing import Dict, Callable import pymem from pymem.exception import ProcessNotFound, ProcessError +import asyncio +from asyncio import StreamReader, StreamWriter, Lock + from CommonClient import logger from NetUtils import NetworkItem from ..GameID import jak1_id, jak1_max @@ -23,10 +25,13 @@ from ..locs import ( class JakAndDaxterReplClient: ip: str port: int - sock: socket + reader: StreamReader + writer: StreamWriter + lock: Lock connected: bool = False initiated_connect: bool = False # Signals when user tells us to try reconnecting. received_deathlink: bool = False + balanced_orbs: bool = False # The REPL client needs the REPL/compiler process running, but that process # also needs the game running. Therefore, the REPL client needs both running. @@ -44,6 +49,7 @@ class JakAndDaxterReplClient: def __init__(self, ip: str = "127.0.0.1", port: int = 8181): self.ip = ip self.port = port + self.lock = asyncio.Lock() self.connect() async def main_tick(self): @@ -67,33 +73,36 @@ class JakAndDaxterReplClient: # Receive Items from AP. Handle 1 item per tick. if len(self.item_inbox) > self.inbox_index: - self.receive_item() - self.save_data() + await self.receive_item() + await self.save_data() self.inbox_index += 1 if self.received_deathlink: - self.receive_deathlink() + await self.receive_deathlink() # Reset all flags. # As a precaution, we should reset our own deathlink flag as well. - self.reset_deathlink() + await self.reset_deathlink() self.received_deathlink = False # This helper function formats and sends `form` as a command to the REPL. # ALL commands to the REPL should be sent using this function. - # TODO - this blocks on receiving an acknowledgement from the REPL server. But it doesn't print - # any log info in the meantime. Is that a problem? - def send_form(self, form: str, print_ok: bool = True) -> bool: + async def send_form(self, form: str, print_ok: bool = True) -> bool: header = struct.pack(" *ap-info-jak1* my-item-name) " - f"{self.sanitize_game_text(self.my_item_name)})", - print_ok=False) - self.send_form(f"(charp<-string (-> *ap-info-jak1* my-item-finder) " - f"{self.sanitize_game_text(self.my_item_finder)})", - print_ok=False) - self.send_form(f"(charp<-string (-> *ap-info-jak1* their-item-name) " - f"{self.sanitize_game_text(self.their_item_name)})", - print_ok=False) - self.send_form(f"(charp<-string (-> *ap-info-jak1* their-item-owner) " - f"{self.sanitize_game_text(self.their_item_owner)})", - print_ok=False) + await self.send_form(f"(begin " + f" (charp<-string (-> *ap-info-jak1* my-item-name) " + f" {self.sanitize_game_text(self.my_item_name)}) " + f" (charp<-string (-> *ap-info-jak1* my-item-finder) " + f" {self.sanitize_game_text(self.my_item_finder)}) " + f" (charp<-string (-> *ap-info-jak1* their-item-name) " + f" {self.sanitize_game_text(self.their_item_name)}) " + f" (charp<-string (-> *ap-info-jak1* their-item-owner) " + f" {self.sanitize_game_text(self.their_item_owner)}) " + f" (none))", print_ok=False) - def receive_item(self): + async def receive_item(self): ap_id = getattr(self.item_inbox[self.inbox_index], "item") # Determine the type of item to receive. if ap_id in range(jak1_id, jak1_id + Flies.fly_offset): - self.receive_power_cell(ap_id) + await self.receive_power_cell(ap_id) elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Specials.special_offset): - self.receive_scout_fly(ap_id) + await self.receive_scout_fly(ap_id) elif ap_id in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset): - self.receive_special(ap_id) + await self.receive_special(ap_id) elif ap_id in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset): - self.receive_move(ap_id) + await self.receive_move(ap_id) elif ap_id in range(jak1_id + Orbs.orb_offset, jak1_max): - self.receive_precursor_orb(ap_id) # Ponder the Orbs. + await self.receive_precursor_orb(ap_id) # Ponder the Orbs. elif ap_id == jak1_max: - self.receive_green_eco() # Ponder why I chose to do ID's this way. + await self.receive_green_eco() # Ponder why I chose to do ID's this way. else: raise KeyError(f"Tried to receive item with unknown AP ID {ap_id}.") - def receive_power_cell(self, ap_id: int) -> bool: + async def receive_power_cell(self, ap_id: int) -> bool: cell_id = Cells.to_game_id(ap_id) - ok = self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type fuel-cell) " - "(the float " + str(cell_id) + "))") + ok = await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type fuel-cell) " + "(the float " + str(cell_id) + "))") if ok: logger.debug(f"Received a Power Cell!") else: logger.error(f"Unable to receive a Power Cell!") return ok - def receive_scout_fly(self, ap_id: int) -> bool: + async def receive_scout_fly(self, ap_id: int) -> bool: fly_id = Flies.to_game_id(ap_id) - ok = self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type buzzer) " - "(the float " + str(fly_id) + "))") + ok = await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type buzzer) " + "(the float " + str(fly_id) + "))") if ok: logger.debug(f"Received a {item_table[ap_id]}!") else: logger.error(f"Unable to receive a {item_table[ap_id]}!") return ok - def receive_special(self, ap_id: int) -> bool: + async def receive_special(self, ap_id: int) -> bool: special_id = Specials.to_game_id(ap_id) - ok = self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type ap-special) " - "(the float " + str(special_id) + "))") + ok = await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type ap-special) " + "(the float " + str(special_id) + "))") if ok: logger.debug(f"Received special unlock {item_table[ap_id]}!") else: logger.error(f"Unable to receive special unlock {item_table[ap_id]}!") return ok - def receive_move(self, ap_id: int) -> bool: + async def receive_move(self, ap_id: int) -> bool: move_id = Caches.to_game_id(ap_id) - ok = self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type ap-move) " - "(the float " + str(move_id) + "))") + ok = await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type ap-move) " + "(the float " + str(move_id) + "))") if ok: logger.debug(f"Received the ability to {item_table[ap_id]}!") else: logger.error(f"Unable to receive the ability to {item_table[ap_id]}!") return ok - def receive_precursor_orb(self, ap_id: int) -> bool: + async def receive_precursor_orb(self, ap_id: int) -> bool: orb_amount = Orbs.to_game_id(ap_id) - ok = self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type money) " - "(the float " + str(orb_amount) + "))") + ok = await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type money) " + "(the float " + str(orb_amount) + "))") if ok: logger.debug(f"Received {orb_amount} Precursor Orbs!") else: @@ -304,18 +303,15 @@ class JakAndDaxterReplClient: return ok # Green eco pills are our filler item. Use the get-pickup event instead to handle being full health. - def receive_green_eco(self) -> bool: - ok = self.send_form("(send-event " - "*target* \'get-pickup " - "(pickup-type eco-pill) " - "(the float 1))") + async def receive_green_eco(self) -> bool: + ok = await self.send_form("(send-event *target* \'get-pickup (pickup-type eco-pill) (the float 1))") if ok: logger.debug(f"Received a green eco pill!") else: logger.error(f"Unable to receive a green eco pill!") return ok - def receive_deathlink(self) -> bool: + async def receive_deathlink(self) -> bool: # Because it should at least be funny sometimes. death_types = ["\'death", @@ -328,51 +324,43 @@ class JakAndDaxterReplClient: "\'dark-eco-pool"] chosen_death = random.choice(death_types) - ok = self.send_form("(ap-deathlink-received! " + chosen_death + ")") + ok = await self.send_form("(ap-deathlink-received! " + chosen_death + ")") if ok: logger.debug(f"Received deathlink signal!") else: logger.error(f"Unable to receive deathlink signal!") return ok - def reset_deathlink(self) -> bool: - ok = self.send_form("(set! (-> *ap-info-jak1* died) 0)") + async def reset_deathlink(self) -> bool: + ok = await self.send_form("(set! (-> *ap-info-jak1* died) 0)") if ok: logger.debug(f"Reset deathlink flag!") else: logger.error(f"Unable to reset deathlink flag!") return ok - def subtract_traded_orbs(self, orb_count: int) -> bool: - ok = self.send_form(f"(-! (-> *game-info* money) (the float {orb_count}))") - if ok: - logger.debug(f"Subtracting {orb_count} traded orbs!") - else: - logger.error(f"Unable to subtract {orb_count} traded orbs!") - return ok + async def subtract_traded_orbs(self, orb_count: int) -> bool: - def reset_orbsanity(self) -> bool: - ok = self.send_form(f"(set! (-> *ap-info-jak1* collected-bundle-level) 0)") - if ok: - logger.debug(f"Reset level ID for collected orbsanity bundle!") - else: - logger.error(f"Unable to reset level ID for collected orbsanity bundle!") + # To protect against momentary server disconnects, + # this should only be done once per client session. + if not self.balanced_orbs: + self.balanced_orbs = True - ok = self.send_form(f"(set! (-> *ap-info-jak1* collected-bundle-count) 0)") - if ok: - logger.debug(f"Reset orb count for collected orbsanity bundle!") - else: - logger.error(f"Unable to reset orb count for collected orbsanity bundle!") - return ok + ok = await self.send_form(f"(-! (-> *game-info* money) (the float {orb_count}))") + if ok: + logger.debug(f"Subtracting {orb_count} traded orbs!") + else: + logger.error(f"Unable to subtract {orb_count} traded orbs!") + return ok - def setup_options(self, - os_option: int, os_bundle: int, - fc_count: int, mp_count: int, - lt_count: int, goal_id: int) -> bool: - ok = self.send_form(f"(ap-setup-options! " - f"(the uint {os_option}) (the uint {os_bundle}) " - f"(the float {fc_count}) (the float {mp_count}) " - f"(the float {lt_count}) (the uint {goal_id}))") + async def setup_options(self, + os_option: int, os_bundle: int, + fc_count: int, mp_count: int, + lt_count: int, goal_id: int) -> bool: + ok = await self.send_form(f"(ap-setup-options! " + f"(the uint {os_option}) (the uint {os_bundle}) " + f"(the float {fc_count}) (the float {mp_count}) " + f"(the float {lt_count}) (the uint {goal_id}))") message = (f"Setting options: \n" f" Orbsanity Option {os_option}, Orbsanity Bundle {os_bundle}, \n" f" FC Cell Count {fc_count}, MP Cell Count {mp_count}, \n" @@ -383,7 +371,7 @@ class JakAndDaxterReplClient: logger.error(message + "Failed!") return ok - def save_data(self): + async def save_data(self): with open("jakanddaxter_item_inbox.json", "w+") as f: dump = { "inbox_index": self.inbox_index, diff --git a/worlds/jakanddaxter/regs/BoggySwampRegions.py b/worlds/jakanddaxter/regs/BoggySwampRegions.py index 8b18a5d3f6..fe5ad5766c 100644 --- a/worlds/jakanddaxter/regs/BoggySwampRegions.py +++ b/worlds/jakanddaxter/regs/BoggySwampRegions.py @@ -10,15 +10,13 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # This level is full of short-medium gaps that cannot be crossed by single jump alone. # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...) def can_jump_farther(state: CollectionState, p: int) -> bool: - return (state.has("Double Jump", p) - or state.has("Jump Kick", p) - or (state.has("Punch", p) and state.has("Punch Uppercut", p))) + return state.has_any({"Double Jump", "Jump Kick"}, p) or state.has_all({"Punch", "Punch Uppercut"}, p) def can_jump_higher(state: CollectionState, p: int) -> bool: return (state.has("Double Jump", p) - or (state.has("Crouch", p) and state.has("Crouch Jump", p)) - or (state.has("Crouch", p) and state.has("Crouch Uppercut", p)) - or (state.has("Punch", p) and state.has("Punch Uppercut", p))) + or state.has_all({"Crouch", "Crouch Jump"}, p) + or state.has_all({"Crouch", "Crouch Uppercut"}, p) + or state.has_all({"Punch", "Punch Uppercut"}, p)) # Orb crates and fly box in this area can be gotten with yellow eco and goggles. # Start with the first yellow eco cluster near first_bats and work your way backward toward the entrance. @@ -93,9 +91,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter first_tether.connect(first_bats) first_tether.connect(first_tether_rat_colony, rule=lambda state: - (state.has("Roll", player) and state.has("Roll Jump", player)) - or (state.has("Double Jump", player) - and state.has("Jump Kick", player))) + (state.has_all({"Roll", "Roll Jump"}, player) + or state.has_all({"Double Jump", "Jump Kick"}, player))) first_tether.connect(second_jump_pad) first_tether.connect(first_pole_course) diff --git a/worlds/jakanddaxter/regs/GeyserRockRegions.py b/worlds/jakanddaxter/regs/GeyserRockRegions.py index 0a2646082f..b78ef7588e 100644 --- a/worlds/jakanddaxter/regs/GeyserRockRegions.py +++ b/worlds/jakanddaxter/regs/GeyserRockRegions.py @@ -16,9 +16,9 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter cliff.add_cell_locations([94]) main_area.connect(cliff, rule=lambda state: - ((state.has("Crouch", player) and state.has("Crouch Jump", player)) - or (state.has("Crouch", player) and state.has("Crouch Uppercut", player)) - or state.has("Double Jump", player))) + state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player) + or state.has_all({"Crouch", "Crouch Uppercut"}, player)) cliff.connect(main_area) # Jump down or ride blue eco elevator. diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py index 59fc7c8a4c..2104358ad9 100644 --- a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -13,31 +13,29 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter def can_jump_farther(state: CollectionState, p: int) -> bool: return (state.has("Double Jump", p) or state.has("Jump Kick", p) - or (state.has("Punch", p) and state.has("Punch Uppercut", p))) + or state.has_all({"Punch", "Punch Uppercut"}, p)) def can_triple_jump(state: CollectionState, p: int) -> bool: - return state.has("Double Jump", p) and state.has("Jump Kick", p) + return state.has_all({"Double Jump", "Jump Kick"}, p) def can_jump_stairs(state: CollectionState, p: int) -> bool: return (state.has("Double Jump", p) - or (state.has("Crouch", p) and state.has("Crouch Jump", p)) - or (state.has("Crouch", p) and state.has("Crouch Uppercut", p)) - or state.has("Jump Dive", p)) + or state.has("Jump Dive", p) + or state.has_all({"Crouch", "Crouch Jump"}, p) + or state.has_all({"Crouch", "Crouch Uppercut"}, p)) main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) main_area.add_fly_locations([91], access_rule=lambda state: can_free_scout_flies(state, player)) robot_scaffolding = JakAndDaxterRegion("Scaffolding Around Robot", player, multiworld, level_name, 8) - robot_scaffolding.add_fly_locations([196699], access_rule=lambda state: - can_free_scout_flies(state, player)) + robot_scaffolding.add_fly_locations([196699], access_rule=lambda state: can_free_scout_flies(state, player)) jump_pad_room = JakAndDaxterRegion("Jump Pad Chamber", player, multiworld, level_name, 88) jump_pad_room.add_cell_locations([73], access_rule=lambda state: can_fight(state, player)) jump_pad_room.add_special_locations([73], access_rule=lambda state: can_fight(state, player)) jump_pad_room.add_fly_locations([131163]) # Blue eco vent is right next to it. jump_pad_room.add_fly_locations([65627], access_rule=lambda state: - can_free_scout_flies(state, player) - and can_jump_farther(state, player)) + can_free_scout_flies(state, player) and can_jump_farther(state, player)) blast_furnace = JakAndDaxterRegion("Blast Furnace", player, multiworld, level_name, 39) blast_furnace.add_cell_locations([71], access_rule=lambda state: can_fight(state, player)) @@ -47,14 +45,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter bunny_room = JakAndDaxterRegion("Bunny Chamber", player, multiworld, level_name, 45) bunny_room.add_cell_locations([72], access_rule=lambda state: can_fight(state, player)) bunny_room.add_special_locations([72], access_rule=lambda state: can_fight(state, player)) - bunny_room.add_fly_locations([262235], access_rule=lambda state: - can_free_scout_flies(state, player)) + bunny_room.add_fly_locations([262235], access_rule=lambda state: can_free_scout_flies(state, player)) rotating_tower = JakAndDaxterRegion("Rotating Tower", player, multiworld, level_name, 20) rotating_tower.add_cell_locations([70], access_rule=lambda state: can_fight(state, player)) rotating_tower.add_special_locations([70], access_rule=lambda state: can_fight(state, player)) - rotating_tower.add_fly_locations([327771], access_rule=lambda state: - can_free_scout_flies(state, player)) + rotating_tower.add_fly_locations([327771], access_rule=lambda state: can_free_scout_flies(state, player)) final_boss = JakAndDaxterRegion("Final Boss", player, multiworld, level_name, 0) @@ -62,48 +58,43 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Jump Dive required for a lot of buttons, prepare yourself. main_area.connect(robot_scaffolding, rule=lambda state: - state.has("Jump Dive", player) - or (state.has("Roll", player) and state.has("Roll Jump", player))) + state.has("Jump Dive", player) or state.has_all({"Roll", "Roll Jump"}, player)) main_area.connect(jump_pad_room) robot_scaffolding.connect(main_area, rule=lambda state: state.has("Jump Dive", player)) robot_scaffolding.connect(blast_furnace, rule=lambda state: state.has("Jump Dive", player) and can_jump_farther(state, player) - and ((state.has("Roll", player) and state.has("Roll Jump", player)) - or can_triple_jump(state, player))) + and (can_triple_jump(state, player) or state.has_all({"Roll", "Roll Jump"}, player))) robot_scaffolding.connect(bunny_room, rule=lambda state: state.has("Jump Dive", player) and can_jump_farther(state, player) - and ((state.has("Roll", player) and state.has("Roll Jump", player)) - or can_triple_jump(state, player))) + and (can_triple_jump(state, player) or state.has_all({"Roll", "Roll Jump"}, player))) jump_pad_room.connect(main_area) jump_pad_room.connect(robot_scaffolding, rule=lambda state: state.has("Jump Dive", player) - and ((state.has("Roll", player) and state.has("Roll Jump", player)) - or can_triple_jump(state, player))) + and (can_triple_jump(state, player) or state.has_all({"Roll", "Roll Jump"}, player))) blast_furnace.connect(robot_scaffolding) # Blue eco elevator takes you right back. bunny_room.connect(robot_scaffolding, rule=lambda state: state.has("Jump Dive", player) - and ((state.has("Roll", player) and state.has("Roll Jump", player)) - or can_jump_farther(state, player))) + and (can_jump_farther(state, player) or state.has_all({"Roll", "Roll Jump"}, player))) # Final climb. robot_scaffolding.connect(rotating_tower, rule=lambda state: - state.has("Freed The Blue Sage", player) - and state.has("Freed The Red Sage", player) - and state.has("Freed The Yellow Sage", player) - and can_jump_stairs(state, player)) + can_jump_stairs(state, player) + and state.has_all({"Freed The Blue Sage", + "Freed The Red Sage", + "Freed The Yellow Sage"}, player)) rotating_tower.connect(main_area) # Take stairs back down. # Final elevator. Need to break boxes at summit to get blue eco for platform. rotating_tower.connect(final_boss, rule=lambda state: - state.has("Freed The Green Sage", player) - and can_fight(state, player)) + can_fight(state, player) + and state.has("Freed The Green Sage", player)) final_boss.connect(rotating_tower) # Take elevator back down. diff --git a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py index 716f0fd46b..2d1f712f57 100644 --- a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py +++ b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py @@ -19,8 +19,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Need jump dive to activate button, double jump to reach blue eco to unlock cache. first_room_orb_cache.add_cache_locations([14507], access_rule=lambda state: - state.has("Jump Dive", player) - and state.has("Double Jump", player)) + state.has_all({"Jump Dive", "Double Jump"}, player)) first_hallway = JakAndDaxterRegion("First Hallway", player, multiworld, level_name, 10) first_hallway.add_fly_locations([131121], access_rule=lambda state: can_free_scout_flies(state, player)) @@ -59,19 +58,16 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Use jump dive to activate button inside the capsule. Blue eco vent can ready the chamber and get the scout fly. capsule_room.add_cell_locations([47], access_rule=lambda state: state.has("Jump Dive", player) - and (state.has("Double Jump", player) - or state.has("Jump Kick", player) - or (state.has("Punch", player) - and state.has("Punch Uppercut", player)))) + and (state.has_any({"Double Jump", "Jump Kick"}, player) + or state.has_all({"Punch", "Punch Uppercut"}, player))) capsule_room.add_fly_locations([327729]) second_slide = JakAndDaxterRegion("Second Slide", player, multiworld, level_name, 31) helix_room = JakAndDaxterRegion("Helix Chamber", player, multiworld, level_name, 30) helix_room.add_cell_locations([46], access_rule=lambda state: - state.has("Double Jump", player) - or state.has("Jump Kick", player) - or (state.has("Punch", player) and state.has("Punch Uppercut", player))) + state.has_any({"Double Jump", "Jump Kick"}, player) + or state.has_all({"Punch", "Punch Uppercut"}, player)) helix_room.add_cell_locations([50], access_rule=lambda state: state.has("Double Jump", player) or can_fight(state, player)) @@ -86,11 +82,9 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Needs some movement to reach these orbs and orb cache. first_room_lower.connect(first_room_orb_cache, rule=lambda state: - state.has("Jump Dive", player) - and state.has("Double Jump", player)) + state.has_all({"Jump Dive", "Double Jump"}, player)) first_room_orb_cache.connect(first_room_lower, rule=lambda state: - state.has("Jump Dive", player) - and state.has("Double Jump", player)) + state.has_all({"Jump Dive", "Double Jump"}, player)) first_hallway.connect(first_room_upper) # Run and jump down. first_hallway.connect(second_room) # Run and jump (floating platforms). diff --git a/worlds/jakanddaxter/regs/MistyIslandRegions.py b/worlds/jakanddaxter/regs/MistyIslandRegions.py index 1a70095ce5..16a8790de5 100644 --- a/worlds/jakanddaxter/regs/MistyIslandRegions.py +++ b/worlds/jakanddaxter/regs/MistyIslandRegions.py @@ -59,9 +59,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter muse_course.connect(main_area) # Run and jump down. # The zoomer pad is low enough that it requires Crouch Jump specifically. - zoomer.connect(main_area, rule=lambda state: - (state.has("Crouch", player) - and state.has("Crouch Jump", player))) + zoomer.connect(main_area, rule=lambda state: state.has_all({"Crouch", "Crouch Jump"}, player)) ship.connect(main_area) # Run and jump down. ship.connect(far_side) # Run and jump down. @@ -72,9 +70,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Only if you can use the seesaw or Crouch Jump from the seesaw's edge. far_side.connect(far_side_cliff, rule=lambda state: - (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or state.has("Jump Dive", player)) + state.has("Jump Dive", player) + or state.has_all({"Crouch", "Crouch Jump"}, player)) # Only if you can break the bone bridges to carry blue eco over the mud pit. far_side.connect(far_side_cache, rule=lambda state: can_fight(state, player)) @@ -91,9 +88,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter upper_approach.connect(arena) # Jump down. # One cliff is accessible, but only via Crouch Jump. - lower_approach.connect(upper_approach, rule=lambda state: - (state.has("Crouch", player) - and state.has("Crouch Jump", player))) + lower_approach.connect(upper_approach, rule=lambda state: state.has_all({"Crouch", "Crouch Jump"}, player)) # Requires breaking bone bridges. lower_approach.connect(arena, rule=lambda state: can_fight(state, player)) diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py index 213dd0ae87..f59e63101a 100644 --- a/worlds/jakanddaxter/regs/RockVillageRegions.py +++ b/worlds/jakanddaxter/regs/RockVillageRegions.py @@ -31,8 +31,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter orb_cache = JakAndDaxterRegion("Orb Cache", player, multiworld, level_name, 20) # You need roll jump to be able to reach this before the blue eco runs out. - orb_cache.add_cache_locations([10945], access_rule=lambda state: - (state.has("Roll", player) and state.has("Roll Jump", player))) + orb_cache.add_cache_locations([10945], access_rule=lambda state: state.has_all({"Roll", "Roll Jump"}, player)) # Fly here can be gotten with Yellow Eco from Boggy, goggles, and no extra movement options (see fly ID 43). pontoon_bridge = JakAndDaxterRegion("Pontoon Bridge", player, multiworld, level_name, 7) @@ -40,7 +39,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter klaww_cliff = JakAndDaxterRegion("Klaww's Cliff", player, multiworld, level_name, 0) - main_area.connect(orb_cache, rule=lambda state: (state.has("Roll", player) and state.has("Roll Jump", player))) + main_area.connect(orb_cache, rule=lambda state: state.has_all({"Roll", "Roll Jump"}, player)) main_area.connect(pontoon_bridge, rule=lambda state: state.has("Warrior's Pontoons", player)) orb_cache.connect(main_area) @@ -48,11 +47,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter pontoon_bridge.connect(main_area, rule=lambda state: state.has("Warrior's Pontoons", player)) pontoon_bridge.connect(klaww_cliff, rule=lambda state: state.has("Double Jump", player) - or (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or (state.has("Crouch", player) - and state.has("Crouch Uppercut", player) - and state.has("Jump Kick", player))) + or state.has_all({"Crouch", "Crouch Jump"}, player) + or state.has_all({"Crouch", "Crouch Uppercut", "Jump Kick"}, player)) klaww_cliff.connect(pontoon_bridge) # Just jump back down. diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py index c872d32ebb..f4ee264a74 100644 --- a/worlds/jakanddaxter/regs/SandoverVillageRegions.py +++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py @@ -21,8 +21,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # The farmer's scout fly. You can either get the Orb Cache Cliff blue eco, or break it normally. main_area.add_fly_locations([196683], access_rule=lambda state: - (state.has("Crouch", player) and state.has("Crouch Jump", player)) - or state.has("Double Jump", player) + state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player) or can_free_scout_flies(state, player)) orb_cache_cliff = JakAndDaxterRegion("Orb Cache Cliff", player, multiworld, level_name, 15) @@ -41,23 +41,17 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter main_area.connect(orb_cache_cliff, rule=lambda state: state.has("Double Jump", player) - or (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or (state.has("Crouch", player) - and state.has("Crouch Uppercut", player) - and state.has("Jump Kick", player))) + or state.has_all({"Crouch", "Crouch Jump"}, player) + or state.has_all({"Crouch", "Crouch Uppercut", "Jump Kick"}, player)) main_area.connect(yakow_cliff, rule=lambda state: state.has("Double Jump", player) - or (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or (state.has("Crouch", player) - and state.has("Crouch Uppercut", player) - and state.has("Jump Kick", player))) + or state.has_all({"Crouch", "Crouch Jump"}, player) + or state.has_all({"Crouch", "Crouch Uppercut", "Jump Kick"}, player)) main_area.connect(oracle_platforms, rule=lambda state: - (state.has("Roll", player) and state.has("Roll Jump", player)) - or (state.has("Double Jump", player) and state.has("Jump Kick", player))) + state.has_all({"Roll", "Roll Jump"}, player) + or state.has_all({"Double Jump", "Jump Kick"}, player)) # All these can go back to main_area immediately. orb_cache_cliff.connect(main_area) diff --git a/worlds/jakanddaxter/regs/SentinelBeachRegions.py b/worlds/jakanddaxter/regs/SentinelBeachRegions.py index e66cb3b884..97c7a58772 100644 --- a/worlds/jakanddaxter/regs/SentinelBeachRegions.py +++ b/worlds/jakanddaxter/regs/SentinelBeachRegions.py @@ -27,13 +27,9 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Only these specific attacks can push the flut flut egg off the cliff. flut_flut_egg = JakAndDaxterRegion("Flut Flut Egg", player, multiworld, level_name, 0) flut_flut_egg.add_cell_locations([17], access_rule=lambda state: - state.has("Punch", player) - or state.has("Kick", player) - or state.has("Jump Kick", player)) + state.has_any({"Punch", "Kick", "Jump Kick"}, player)) flut_flut_egg.add_special_locations([17], access_rule=lambda state: - state.has("Punch", player) - or state.has("Kick", player) - or state.has("Jump Kick", player)) + state.has_any({"Punch", "Kick", "Jump Kick"}, player)) eco_harvesters = JakAndDaxterRegion("Eco Harvesters", player, multiworld, level_name, 0) eco_harvesters.add_cell_locations([15], access_rule=lambda state: can_fight(state, player)) @@ -55,15 +51,15 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # You don't need any kind of uppercut to reach this place, just a high jump from a convenient nearby ledge. main_area.connect(green_ridge, rule=lambda state: - (state.has("Crouch", player) and state.has("Crouch Jump", player)) - or state.has("Double Jump", player)) + state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player)) # Can either uppercut the log and jump from it, or use the blue eco jump pad. main_area.connect(blue_ridge, rule=lambda state: state.has("Blue Eco Switch", player) or (state.has("Double Jump", player) - and ((state.has("Crouch", player) and state.has("Crouch Uppercut", player)) - or (state.has("Punch", player) and state.has("Punch Uppercut", player))))) + and (state.has_all({"Crouch", "Crouch Uppercut"}, player) + or state.has_all({"Punch", "Punch Uppercut"}, player)))) main_area.connect(cannon_tower, rule=lambda state: state.has("Blue Eco Switch", player)) diff --git a/worlds/jakanddaxter/regs/SnowyMountainRegions.py b/worlds/jakanddaxter/regs/SnowyMountainRegions.py index 07bd33ca24..f6ee58b738 100644 --- a/worlds/jakanddaxter/regs/SnowyMountainRegions.py +++ b/worlds/jakanddaxter/regs/SnowyMountainRegions.py @@ -10,22 +10,20 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # We need a few helper functions. def can_cross_main_gap(state: CollectionState, p: int) -> bool: - return ((state.has("Roll", player) - and state.has("Roll Jump", player)) - or (state.has("Double Jump", player) - and state.has("Jump Kick", player))) + return (state.has_all({"Roll", "Roll Jump"}, p) + or state.has_all({"Double Jump", "Jump Kick"}, p)) def can_cross_frozen_cave(state: CollectionState, p: int) -> bool: return (state.has("Jump Kick", p) and (state.has("Double Jump", p) - or (state.has("Roll", p) and state.has("Roll Jump", p)))) + or state.has_all({"Roll", "Roll Jump"}, p))) def can_jump_blockers(state: CollectionState, p: int) -> bool: return (state.has("Double Jump", p) - or (state.has("Crouch", p) and state.has("Crouch Jump", p)) - or (state.has("Crouch", p) and state.has("Crouch Uppercut", p)) - or (state.has("Punch", p) and state.has("Punch Uppercut", p)) - or state.has("Jump Dive", p)) + or state.has("Jump Dive", p) + or state.has_all({"Crouch", "Crouch Jump"}, p) + or state.has_all({"Crouch", "Crouch Uppercut"}, p) + or state.has_all({"Punch", "Punch Uppercut"}, p)) main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) main_area.add_fly_locations([65], access_rule=lambda state: can_free_scout_flies(state, player)) @@ -145,25 +143,22 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter can_jump_blockers(state, player)) fort_interior.connect(fort_interior_caches, rule=lambda state: # Just need a little height. - (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or state.has("Double Jump", player)) + state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player)) fort_interior.connect(fort_interior_base, rule=lambda state: # Just need a little height. - (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or state.has("Double Jump", player)) + state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player)) fort_interior.connect(fort_interior_course_end, rule=lambda state: # Just need a little distance. - (state.has("Punch", player) - and state.has("Punch Uppercut", player)) - or state.has("Double Jump", player)) + state.has("Double Jump", player) + or state.has_all({"Punch", "Punch Uppercut"}, player)) flut_flut_course.connect(fort_exterior) # Ride the elevator. # Must fight way through cave, but there is also a grab-less ledge we must jump over. bunny_cave_start.connect(bunny_cave_end, rule=lambda state: can_fight(state, player) - and ((state.has("Crouch", player) and state.has("Crouch Jump", player)) - or state.has("Double Jump", player))) + and (state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player))) # All jump down. fort_interior_caches.connect(fort_interior) diff --git a/worlds/jakanddaxter/regs/SpiderCaveRegions.py b/worlds/jakanddaxter/regs/SpiderCaveRegions.py index 7124732d09..c773f8c064 100644 --- a/worlds/jakanddaxter/regs/SpiderCaveRegions.py +++ b/worlds/jakanddaxter/regs/SpiderCaveRegions.py @@ -22,18 +22,18 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # The rest of the crystals can be destroyed with yellow eco in main_area. dark_crystals.add_cell_locations([79], access_rule=lambda state: can_fight(state, player) - and (state.has("Roll", player) and state.has("Roll Jump", player))) + and state.has_all({"Roll", "Roll Jump"}, player)) dark_cave = JakAndDaxterRegion("Dark Cave", player, multiworld, level_name, 5) dark_cave.add_cell_locations([80], access_rule=lambda state: can_fight(state, player) - and ((state.has("Crouch", player) and state.has("Crouch Jump", player)) - or state.has("Double Jump", player))) + and (state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player))) dark_cave.add_fly_locations([262229], access_rule=lambda state: can_fight(state, player) and can_free_scout_flies(state, player) - and ((state.has("Crouch", player) and state.has("Crouch Jump", player)) - or state.has("Double Jump", player))) + and (state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player))) robot_cave = JakAndDaxterRegion("Robot Cave", player, multiworld, level_name, 0)