The Afterparty (#42)

* Fixes to Jak client, rules, options, and more.

* Post-rebase fixes.

* Remove orbsanity reset code, optimize game text in client.

* More game text optimization.
This commit is contained in:
massimilianodelliubaldini
2024-08-03 14:19:06 -04:00
committed by GitHub
parent 22b43a8e3d
commit ea82846a2b
15 changed files with 270 additions and 323 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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("<II", len(form), 10)
self.sock.sendall(header + form.encode())
response = self.sock.recv(1024).decode()
if "OK!" in response:
if print_ok:
logger.debug(response)
return True
else:
logger.error(f"Unexpected response from REPL: {response}")
return False
async with self.lock:
self.writer.write(header + form.encode())
await self.writer.drain()
response_data = await self.reader.read(1024)
response = response_data.decode()
if "OK!" in response:
if print_ok:
logger.debug(response)
return True
else:
logger.error(f"Unexpected response from REPL: {response}")
return False
async def connect(self):
try:
@@ -111,10 +120,10 @@ class JakAndDaxterReplClient:
return
try:
self.sock = socket(AF_INET, SOCK_STREAM)
self.sock.connect((self.ip, self.port))
self.reader, self.writer = await asyncio.open_connection(self.ip, self.port)
time.sleep(1)
welcome_message = self.sock.recv(1024).decode()
connect_data = await self.reader.read(1024)
welcome_message = connect_data.decode()
# Should be the OpenGOAL welcome message (ignore version number).
if "Connected to OpenGOAL" and "nREPL!" in welcome_message:
@@ -126,50 +135,42 @@ class JakAndDaxterReplClient:
return
ok_count = 0
if self.sock:
if self.reader and self.writer:
# Have the REPL listen to the game's internal websocket.
if self.send_form("(lt)", print_ok=False):
if await self.send_form("(lt)", print_ok=False):
ok_count += 1
# Show this visual cue when compilation is started.
# It's the version number of the OpenGOAL Compiler.
if self.send_form("(set! *debug-segment* #t)", print_ok=False):
ok_count += 1
# Play this audio cue when compilation is started.
# It's the sound you hear when you press START + CIRCLE to open the Options menu.
if self.send_form("(dotimes (i 1) "
"(sound-play-by-name "
"(static-sound-name \"start-options\") "
"(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False):
if await self.send_form("(set! *debug-segment* #t)", print_ok=False):
ok_count += 1
# Start compilation. This is blocking, so nothing will happen until the REPL is done.
if self.send_form("(mi)", print_ok=False):
if await self.send_form("(mi)", print_ok=False):
ok_count += 1
# Play this audio cue when compilation is complete.
# It's the sound you hear when you press START + START to close the Options menu.
if self.send_form("(dotimes (i 1) "
"(sound-play-by-name "
"(static-sound-name \"menu-close\") "
"(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False):
if await self.send_form("(dotimes (i 1) "
"(sound-play-by-name "
"(static-sound-name \"menu-close\") "
"(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False):
ok_count += 1
# Disable cheat-mode and debug (close the visual cues).
if self.send_form("(set! *debug-segment* #f)", print_ok=False):
if await self.send_form("(set! *debug-segment* #f)", print_ok=False):
ok_count += 1
if self.send_form("(set! *cheat-mode* #f)", print_ok=False):
if await self.send_form("(set! *cheat-mode* #f)", print_ok=False):
ok_count += 1
# Run the retail game start sequence (while still in debug).
if self.send_form("(start \'play (get-continue-by-name *game-info* \"title-start\"))"):
if await self.send_form("(start \'play (get-continue-by-name *game-info* \"title-start\"))"):
ok_count += 1
# Now wait until we see the success message... 8 times.
if ok_count == 8:
# Now wait until we see the success message... 7 times.
if ok_count == 7:
self.connected = True
else:
self.connected = False
@@ -177,20 +178,20 @@ class JakAndDaxterReplClient:
if self.connected:
logger.info("The REPL is ready!")
def print_status(self):
async def print_status(self):
logger.info("REPL Status:")
logger.info(" REPL process ID: " + (str(self.goalc_process.process_id) if self.goalc_process else "None"))
logger.info(" Game process ID: " + (str(self.gk_process.process_id) if self.gk_process else "None"))
try:
if self.sock:
ip, port = self.sock.getpeername()
logger.info(" Game websocket: " + (str(ip) + ", " + str(port) if ip else "None"))
self.send_form("(dotimes (i 1) "
"(sound-play-by-name "
"(static-sound-name \"menu-close\") "
"(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False)
except:
logger.warn(" Game websocket not found!")
if self.reader and self.writer:
addr = self.writer.get_extra_info("peername")
logger.info(" Game websocket: " + (str(addr) if addr else "None"))
await self.send_form("(dotimes (i 1) "
"(sound-play-by-name "
"(static-sound-name \"menu-close\") "
"(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False)
except ConnectionResetError:
logger.warn(" Connection to the game was lost or reset!")
logger.info(" Did you hear the success audio cue?")
logger.info(" Last item received: " + (str(getattr(self.item_inbox[self.inbox_index], "item"))
if self.inbox_index else "None"))
@@ -209,94 +210,92 @@ class JakAndDaxterReplClient:
# OpenGOAL can handle both its own string datatype and C-like character pointers (charp).
# So for the game to constantly display this information in the HUD, we have to write it
# to a memory address as a char*.
def write_game_text(self):
async def write_game_text(self):
logger.debug(f"Sending info to in-game display!")
self.send_form(f"(charp<-string (-> *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,

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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).

View File

@@ -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))

View File

@@ -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.

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)