mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-24 05:53:19 -07:00
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:
committed by
GitHub
parent
22b43a8e3d
commit
ea82846a2b
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user