Merge branch 'ArchipelagoMW:main' into sc2wol

This commit is contained in:
Magnemania
2022-10-16 09:30:51 -04:00
committed by GitHub
106 changed files with 16556 additions and 524 deletions

3
.gitignore vendored
View File

@@ -13,6 +13,9 @@
*.z64
*.n64
*.nes
*.gb
*.gbc
*.gba
*.wixobj
*.lck
*.db3

View File

@@ -689,14 +689,14 @@ class CollectionState():
def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None:
if locations is None:
locations = self.world.get_filled_locations()
new_locations = True
reachable_events = True
# since the loop has a good chance to run more than once, only filter the events once
locations = {location for location in locations if location.event and
not key_only or getattr(location.item, "locked_dungeon_item", False)}
while new_locations:
while reachable_events:
reachable_events = {location for location in locations if location.can_reach(self)}
new_locations = reachable_events - self.events
for event in new_locations:
locations -= reachable_events
for event in reachable_events:
self.events.add(event)
assert isinstance(event.item, Item), "tried to collect Event with no Item"
self.collect(event.item, True, event)

View File

@@ -211,6 +211,8 @@ async def game_watcher(ctx: FactorioContext):
def stream_factorio_output(pipe, queue, process):
pipe.reconfigure(errors="replace")
def queuer():
while process.poll() is None:
text = pipe.readline().strip()

View File

@@ -233,9 +233,12 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio
def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations):
maximum_exploration_state = sweep_from_pool(state, [])
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
for location in unreachable_locations:
add_item_rule(location, lambda item: not ((item.classification & 0b0011) and
world.accessibility[item.player] != 'minimal'))
if unreachable_locations:
def forbid_important_item_rule(item: Item):
return not ((item.classification & 0b0011) and world.accessibility[item.player] != 'minimal')
for location in unreachable_locations:
add_item_rule(location, forbid_important_item_rule)
def distribute_items_restrictive(world: MultiWorld) -> None:

View File

@@ -145,6 +145,8 @@ components: Iterable[Component] = (
Component('OoT Adjuster', 'OoTAdjuster'),
# FF1
Component('FF1 Client', 'FF1Client'),
# Pokémon
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
# ChecksFinder
Component('ChecksFinder Client', 'ChecksFinderClient'),
# Starcraft 2

View File

@@ -82,7 +82,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
numlength = 8
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
if not cls.hidden:
if not cls.hidden and len(cls.item_names) > 0:
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} "
f"Items (IDs: {min(cls.item_id_to_name):{numlength}} - "
f"{max(cls.item_id_to_name):{numlength}}) | "

View File

@@ -427,7 +427,6 @@ class TextChoice(Choice):
assert isinstance(value, str) or isinstance(value, int), \
f"{value} is not a valid option for {self.__class__.__name__}"
self.value = value
super(TextChoice, self).__init__()
@property
def current_key(self) -> str:
@@ -467,6 +466,124 @@ class TextChoice(Choice):
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
class BossMeta(AssembleOptions):
def __new__(mcs, name, bases, attrs):
if name != "PlandoBosses":
assert "bosses" in attrs, f"Please define valid bosses for {name}"
attrs["bosses"] = frozenset((boss.lower() for boss in attrs["bosses"]))
assert "locations" in attrs, f"Please define valid locations for {name}"
attrs["locations"] = frozenset((location.lower() for location in attrs["locations"]))
cls = super().__new__(mcs, name, bases, attrs)
assert not cls.duplicate_bosses or "singularity" in cls.options, f"Please define option_singularity for {name}"
return cls
class PlandoBosses(TextChoice, metaclass=BossMeta):
"""Generic boss shuffle option that supports plando. Format expected is
'location1-boss1;location2-boss2;shuffle_mode'.
If shuffle_mode is not provided in the string, this will be the default shuffle mode. Must override can_place_boss,
which passes a plando boss and location. Check if the placement is valid for your game here."""
bosses: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]]
locations: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]]
duplicate_bosses: bool = False
@classmethod
def from_text(cls, text: str):
# set all of our text to lower case for name checking
text = text.lower()
if text == "random":
return cls(random.choice(list(cls.options.values())))
for option_name, value in cls.options.items():
if option_name == text:
return cls(value)
options = text.split(";")
# since plando exists in the option verify the plando values given are valid
cls.validate_plando_bosses(options)
return cls.get_shuffle_mode(options)
@classmethod
def get_shuffle_mode(cls, option_list: typing.List[str]):
# find out what mode of boss shuffle we should use for placing bosses after plando
# and add as a string to look nice in the spoiler
if "random" in option_list:
shuffle = random.choice(list(cls.options))
option_list.remove("random")
options = ";".join(option_list) + f";{shuffle}"
boss_class = cls(options)
else:
for option in option_list:
if option in cls.options:
options = ";".join(option_list)
break
else:
if cls.duplicate_bosses and len(option_list) == 1:
if cls.valid_boss_name(option_list[0]):
# this doesn't exist in this class but it's a forced option for classes where this is called
options = option_list[0] + ";singularity"
else:
options = option_list[0] + f";{cls.name_lookup[cls.default]}"
else:
options = ";".join(option_list) + f";{cls.name_lookup[cls.default]}"
boss_class = cls(options)
return boss_class
@classmethod
def validate_plando_bosses(cls, options: typing.List[str]) -> None:
used_locations = []
used_bosses = []
for option in options:
# check if a shuffle mode was provided in the incorrect location
if option == "random" or option in cls.options:
if option != options[-1]:
raise ValueError(f"{option} option must be at the end of the boss_shuffle options!")
elif "-" in option:
location, boss = option.split("-")
if location in used_locations:
raise ValueError(f"Duplicate Boss Location {location} not allowed.")
if not cls.duplicate_bosses and boss in used_bosses:
raise ValueError(f"Duplicate Boss {boss} not allowed.")
used_locations.append(location)
used_bosses.append(boss)
if not cls.valid_boss_name(boss):
raise ValueError(f"{boss.title()} is not a valid boss name.")
if not cls.valid_location_name(location):
raise ValueError(f"{location.title()} is not a valid boss location name.")
if not cls.can_place_boss(boss, location):
raise ValueError(f"{location.title()} is not a valid location for {boss.title()} to be placed.")
else:
if cls.duplicate_bosses:
if not cls.valid_boss_name(option):
raise ValueError(f"{option} is not a valid boss name.")
else:
raise ValueError(f"{option.title()} is not formatted correctly.")
@classmethod
def can_place_boss(cls, boss: str, location: str) -> bool:
raise NotImplementedError
@classmethod
def valid_boss_name(cls, value: str) -> bool:
return value in cls.bosses
@classmethod
def valid_location_name(cls, value: str) -> bool:
return value in cls.locations
def verify(self, world, player_name: str, plando_options) -> None:
if isinstance(self.value, int):
return
from Generate import PlandoSettings
if not(PlandoSettings.bosses & plando_options):
import logging
# plando is disabled but plando options were given so pull the option and change it to an int
option = self.value.split(";")[-1]
self.value = self.options[option]
logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} "
f"boss shuffle will be used for player {player_name}.")
class Range(NumericOption):
range_start = 0
range_end = 1

319
PokemonClient.py Normal file
View File

@@ -0,0 +1,319 @@
import asyncio
import json
import time
import os
import bsdiff4
import subprocess
import zipfile
import hashlib
from asyncio import StreamReader, StreamWriter
from typing import List
import Utils
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
from worlds.pokemon_rb.locations import location_data
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}}
location_bytes_bits = {}
for location in location_data:
if location.ram_address is not None:
if type(location.ram_address) == list:
location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address
location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit},
{'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}]
else:
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
DISPLAY_MSGS = True
class GBCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_gb(self):
"""Check Gameboy Connection State"""
if isinstance(self.ctx, GBContext):
logger.info(f"Gameboy Status: {self.ctx.gb_status}")
class GBContext(CommonContext):
command_processor = GBCommandProcessor
game = 'Pokemon Red and Blue'
items_handling = 0b101
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.gb_streams: (StreamReader, StreamWriter) = None
self.gb_sync_task = None
self.messages = {}
self.locations_array = None
self.gb_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.display_msgs = True
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(GBContext, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to Bizhawk to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[(time.time(), msg_id)] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.locations_array = None
elif cmd == "RoomInfo":
self.seed_name = args['seed_name']
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
def run_gui(self):
from kvui import GameManager
class GBManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Pokémon Client"
self.ui = GBManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def get_payload(ctx: GBContext):
current_time = time.time()
return json.dumps(
{
"items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10}
}
)
async def parse_locations(data: List, ctx: GBContext):
locations = []
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]}
# Check for clear problems
if len(flags['Rod']) > 1:
return
if flags["EventFlag"][1] + flags["EventFlag"][8] + flags["EventFlag"][9] + flags["EventFlag"][12] \
+ flags["EventFlag"][61] + flags["EventFlag"][62] + flags["EventFlag"][63] + flags["EventFlag"][64] \
+ flags["EventFlag"][65] + flags["EventFlag"][66] + flags["EventFlag"][67] + flags["EventFlag"][68] \
+ flags["EventFlag"][69] + flags["EventFlag"][70] != 0:
return
for flag_type, loc_map in location_map.items():
for flag, loc_id in loc_map.items():
if flag_type == "list":
if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit']
and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']):
locations.append(loc_id)
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
locations.append(loc_id)
if flags["EventFlag"][280] & 1 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": 30}
])
ctx.finished_game = True
if locations == ctx.locations_array:
return
ctx.locations_array = locations
if locations is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
async def gb_sync_task(ctx: GBContext):
logger.info("Starting GB connector. Use /gb for status information")
while not ctx.exit_event.is_set():
error_status = None
if ctx.gb_streams:
(reader, writer) = ctx.gb_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to two fields:
# 1. A keepalive response of the Players Name (always)
# 2. An array representing the memory values of the locations area (if in game)
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
#print(data_decoded)
if ctx.seed_name and ctx.seed_name != bytes(data_decoded['seedName']).decode():
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
ctx.seed_name = bytes(data_decoded['seedName']).decode()
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '':
logger.info("Invalid ROM detected. No player name built into the ROM.")
if ctx.awaiting_rom:
await ctx.server_auth(False)
if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \
and not error_status and ctx.auth:
# Not just a keep alive ping, parse
asyncio.create_task(parse_locations(data_decoded['locations'], ctx))
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
if ctx.gb_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to Gameboy")
ctx.gb_status = CONNECTION_CONNECTED_STATUS
else:
ctx.gb_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.gb_status = error_status
logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates")
else:
try:
logger.debug("Attempting to connect to Gameboy")
ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10)
ctx.gb_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.gb_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.gb_status = CONNECTION_REFUSED_STATUS
continue
async def run_game(romfile):
auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True)
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(game_version, patch_file, ctx):
base_name = os.path.splitext(patch_file)[0]
comp_path = base_name + '.gb'
with open(Utils.local_path(Utils.get_options()["pokemon_rb_options"][f"{game_version}_rom_file"]), "rb") as stream:
base_rom = bytes(stream.read())
try:
with open(Utils.local_path('lib', 'worlds', 'pokemon_rb', f'basepatch_{game_version}.bsdiff4'), 'rb') as stream:
base_patch = bytes(stream.read())
except FileNotFoundError:
with open(Utils.local_path('worlds', 'pokemon_rb', f'basepatch_{game_version}.bsdiff4'), 'rb') as stream:
base_patch = bytes(stream.read())
base_patched_rom_data = bsdiff4.patch(base_rom, base_patch)
basemd5 = hashlib.md5()
basemd5.update(base_patched_rom_data)
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
with patch_archive.open('delta.bsdiff4', 'r') as stream:
patch = stream.read()
patched_rom_data = bsdiff4.patch(base_patched_rom_data, patch)
written_hash = patched_rom_data[0xFFCC:0xFFDC]
if written_hash == basemd5.digest():
with open(comp_path, "wb") as patched_rom_file:
patched_rom_file.write(patched_rom_data)
asyncio.create_task(run_game(comp_path))
else:
msg = "Patch supplied was not generated with the same base patch version as this client. Patching failed."
logger.warning(msg)
ctx.gui_error('Error', msg)
if __name__ == '__main__':
Utils.init_logging("PokemonClient")
options = Utils.get_options()
async def main():
parser = get_base_parser()
parser.add_argument('patch_file', default="", type=str, nargs="?",
help='Path to an APRED or APBLUE patch file')
args = parser.parse_args()
ctx = GBContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync")
if args.patch_file:
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
if ext == "apred":
logger.info("APRED file supplied, beginning patching process...")
asyncio.create_task(patch_and_run_game("red", args.patch_file, ctx))
elif ext == "apblue":
logger.info("APBLUE file supplied, beginning patching process...")
asyncio.create_task(patch_and_run_game("blue", args.patch_file, ctx))
else:
logger.warning(f"Unknown patch file extension {ext}")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.gb_sync_task:
await ctx.gb_sync_task
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

View File

@@ -29,6 +29,9 @@ Currently, the following games are supported:
* Donkey Kong Country 3
* Dark Souls 3
* Super Mario World
* Pokémon Red and Blue
* Hylics 2
* Overcooked! 2
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -114,12 +114,40 @@ class StarcraftClientProcessor(ClientCommandProcessor):
"""Manually set the SC2 install directory (if the automatic detection fails)."""
if path:
os.environ["SC2PATH"] = path
check_mod_install()
is_mod_installed_correctly()
return True
else:
sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
return False
def _cmd_download_data(self, force: bool = False) -> bool:
"""Download the most recent release of the necessary files for playing SC2 with
Archipelago. force should be True or False. force=True will overwrite your files."""
if "SC2PATH" not in os.environ:
check_game_install_path()
if os.path.exists(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt"):
with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "r") as f:
current_ver = f.read()
else:
current_ver = None
tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData', current_version=current_ver, force_download=force)
if tempzip != '':
try:
import zipfile
zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
sc2_logger.info(f"Download complete. Version {version} installed.")
with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "w") as f:
f.write(version)
finally:
os.remove(tempzip)
else:
sc2_logger.warning("Download aborted/failed. Read the log for more information.")
return False
return True
class SC2Context(CommonContext):
command_processor = StarcraftClientProcessor
@@ -162,10 +190,13 @@ class SC2Context(CommonContext):
self.build_location_to_mission_mapping()
# Look for and set SC2PATH.
# check_game_install_path() returns True if and only if it finds + sets SC2PATH.
if "SC2PATH" not in os.environ and check_game_install_path():
check_mod_install()
# Looks for the required maps and mods for SC2. Runs check_game_install_path.
is_mod_installed_correctly()
if os.path.exists(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt"):
with open(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt", "r") as f:
current_ver = f.read()
if is_mod_update_available("TheCondor07", "Starcraft2ArchipelagoData", current_ver):
sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
def on_print_json(self, args: dict):
# goes to this world
@@ -857,18 +888,53 @@ def check_game_install_path() -> bool:
return False
def check_mod_install() -> bool:
# Pull up the SC2PATH if set. If not, encourage the user to manually run /set_path.
try:
# Check inside the Mods folder for Archipelago.SC2Mod. If found, tell user. If not, tell user.
if os.path.isfile(modfile := (os.environ["SC2PATH"] / Path("Mods") / Path("Archipelago.SC2Mod"))):
sc2_logger.info(f"Archipelago mod found at {modfile}.")
return True
else:
sc2_logger.warning(f"Archipelago mod could not be found at {modfile}. Please install the mod file there.")
except KeyError:
sc2_logger.warning(f"SC2PATH isn't set. Please run /set_path with the path to your SC2 install.")
return False
def is_mod_installed_correctly() -> bool:
"""Searches for all required files."""
if "SC2PATH" not in os.environ:
check_game_install_path()
mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign')
modfile = os.environ["SC2PATH"] / Path("Mods/Archipelago.SC2Mod")
wol_required_maps = [
"ap_thanson01.SC2Map", "ap_thanson02.SC2Map", "ap_thanson03a.SC2Map", "ap_thanson03b.SC2Map",
"ap_thorner01.SC2Map", "ap_thorner02.SC2Map", "ap_thorner03.SC2Map", "ap_thorner04.SC2Map", "ap_thorner05s.SC2Map",
"ap_traynor01.SC2Map", "ap_traynor02.SC2Map", "ap_traynor03.SC2Map",
"ap_ttosh01.SC2Map", "ap_ttosh02.SC2Map", "ap_ttosh03a.SC2Map", "ap_ttosh03b.SC2Map",
"ap_ttychus01.SC2Map", "ap_ttychus02.SC2Map", "ap_ttychus03.SC2Map", "ap_ttychus04.SC2Map", "ap_ttychus05.SC2Map",
"ap_tvalerian01.SC2Map", "ap_tvalerian02a.SC2Map", "ap_tvalerian02b.SC2Map", "ap_tvalerian03.SC2Map",
"ap_tzeratul01.SC2Map", "ap_tzeratul02.SC2Map", "ap_tzeratul03.SC2Map", "ap_tzeratul04.SC2Map"
]
needs_files = False
# Check for maps.
missing_maps = []
for mapfile in wol_required_maps:
if not os.path.isfile(mapdir / mapfile):
missing_maps.append(mapfile)
if len(missing_maps) >= 19:
sc2_logger.warning(f"All map files missing from {mapdir}.")
needs_files = True
elif len(missing_maps) > 0:
for map in missing_maps:
sc2_logger.debug(f"Missing {map} from {mapdir}.")
sc2_logger.warning(f"Missing {len(missing_maps)} map files.")
needs_files = True
else: # Must be no maps missing
sc2_logger.info(f"All maps found in {mapdir}.")
# Check for mods.
if os.path.isfile(modfile):
sc2_logger.info(f"Archipelago mod found at {modfile}.")
else:
sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.")
needs_files = True
# Final verdict.
if needs_files:
sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.")
return False
else:
return True
class DllDirectory:
@@ -907,6 +973,64 @@ class DllDirectory:
return False
def download_latest_release_zip(owner: str, repo: str, current_version: str = None, force_download=False) -> (str, str):
"""Downloads the latest release of a GitHub repo to the current directory as a .zip file."""
import requests
headers = {"Accept": 'application/vnd.github.v3+json'}
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
r1 = requests.get(url, headers=headers)
if r1.status_code == 200:
latest_version = r1.json()["tag_name"]
sc2_logger.info(f"Latest version: {latest_version}.")
else:
sc2_logger.warning(f"Status code: {r1.status_code}")
sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.")
sc2_logger.warning(f"text: {r1.text}")
return "", current_version
if (force_download is False) and (current_version == latest_version):
sc2_logger.info("Latest version already installed.")
return "", current_version
sc2_logger.info(f"Attempting to download version {latest_version} of {repo}.")
download_url = r1.json()["assets"][0]["browser_download_url"]
r2 = requests.get(download_url, headers=headers)
if r2.status_code == 200:
with open(f"{repo}.zip", "wb") as fh:
fh.write(r2.content)
sc2_logger.info(f"Successfully downloaded {repo}.zip.")
return f"{repo}.zip", latest_version
else:
sc2_logger.warning(f"Status code: {r2.status_code}")
sc2_logger.warning("Download failed.")
sc2_logger.warning(f"text: {r2.text}")
return "", current_version
def is_mod_update_available(owner: str, repo: str, current_version: str) -> bool:
import requests
headers = {"Accept": 'application/vnd.github.v3+json'}
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
r1 = requests.get(url, headers=headers)
if r1.status_code == 200:
latest_version = r1.json()["tag_name"]
if current_version != latest_version:
return True
else:
return False
else:
sc2_logger.warning(f"Failed to reach GitHub while checking for updates.")
sc2_logger.warning(f"Status code: {r1.status_code}")
sc2_logger.warning(f"text: {r1.text}")
return False
if __name__ == '__main__':
colorama.init()
asyncio.run(main())

View File

@@ -295,6 +295,11 @@ def get_default_options() -> OptionsType:
"sni": "SNI",
"rom_start": True,
},
"pokemon_rb_options": {
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
"rom_start": True
}
}
return options

View File

@@ -15,7 +15,13 @@ handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hin
def create():
target_folder = local_path("WebHostLib", "static", "generated")
os.makedirs(os.path.join(target_folder, "configs"), exist_ok=True)
yaml_folder = os.path.join(target_folder, "configs")
os.makedirs(yaml_folder, exist_ok=True)
for file in os.listdir(yaml_folder):
full_path: str = os.path.join(yaml_folder, file)
if os.path.isfile(full_path):
os.unlink(full_path)
def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]):
data = {}
@@ -25,9 +31,12 @@ def create():
data.update({
option.range_start: 0,
option.range_end: 0,
"random": 0, "random-low": 0, "random-high": 0,
option.default: 50
})
for sub_option in {"random", "random-low", "random-high"}:
if sub_option != option.default:
data[sub_option] = 0
notes = {
special: "minimum value without special meaning",
option.range_start: "minimum value",
@@ -43,11 +52,6 @@ def create():
return data, notes
def default_converter(default_value):
if isinstance(default_value, (set, frozenset)):
return list(default_value)
return default_value
def get_html_doc(option_type: type(Options.Option)) -> str:
if not option_type.__doc__:
return "Please document me!"
@@ -73,7 +77,7 @@ def create():
res = Template(file_data).render(
options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range, default_converter=default_converter,
dictify_range=dictify_range,
)
del file_data

View File

@@ -26,24 +26,22 @@ window.addEventListener('load', () => {
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
for (let i=0; i < headers.length; i++){
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
headers[i].setAttribute('id', headerId);
headers[i].addEventListener('click', () =>
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
if (scrollTargetIndex > -1) {
try{
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
} catch(error) {
console.error(error);
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
}
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =

View File

@@ -26,24 +26,22 @@ window.addEventListener('load', () => {
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
for (let i=0; i < headers.length; i++){
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
headers[i].setAttribute('id', headerId);
headers[i].addEventListener('click', () =>
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
if (scrollTargetIndex > -1) {
try{
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
} catch(error) {
console.error(error);
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
}
});
}).catch((error) => {
console.error(error);
gameInfo.innerHTML =

View File

@@ -26,24 +26,22 @@ window.addEventListener('load', () => {
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
for (let i=0; i < headers.length; i++){
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
headers[i].setAttribute('id', headerId);
headers[i].addEventListener('click', () =>
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
if (scrollTargetIndex > -1) {
try{
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
} catch(error) {
console.error(error);
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
}
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =

View File

@@ -27,25 +27,28 @@ window.addEventListener('load', () => {
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
const title = document.querySelector('h1')
if (title) {
document.title = title.textContent;
}
// Reset the id of all header divs to something nicer
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
for (let i=0; i < headers.length; i++){
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
headers[i].setAttribute('id', headerId);
headers[i].addEventListener('click', () =>
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
if (scrollTargetIndex > -1) {
try{
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
} catch(error) {
console.error(error);
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
}
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =

View File

@@ -55,4 +55,6 @@
border: 1px solid #2a6c2f;
border-radius: 6px;
color: #000000;
overflow-y: auto;
max-height: 400px;
}

View File

@@ -1,5 +1,7 @@
html{
padding-top: 110px;
scroll-padding-top: 100px;
scroll-behavior: smooth;
}
#base-header{

View File

@@ -1,7 +1,6 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{{ super() }}
<title>Mystery Check Result</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/check.css") }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/check.js") }}"></script>

View File

@@ -1,7 +1,6 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{{ super() }}
<title>Generate Game</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/generate.css") }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/generate.js") }}"></script>

View File

@@ -1,7 +1,6 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{{ super() }}
<title>Upload Multidata</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostGame.css") }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/hostGame.js") }}"></script>

View File

@@ -43,14 +43,18 @@ requires:
{%- if option.range_start is defined and option.range_start is number %}
{{- range_option(option) -}}
{%- elif option.options -%}
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
{{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
{%- endfor -%}
{% if option.default == "random" %}
random: 50
{%- endif -%}
{%- endfor -%}
{% if option.name_lookup[option.default] not in option.options %}
{{ option.default }}: 50
{%- endif -%}
{%- elif option.default is string %}
{{ option.default }}: 50
{%- elif option.default is iterable and option.default is not mapping %}
{{ option.default | list }}
{%- else %}
{{ yaml_dump(default_converter(option.default)) | indent(4, first=False) }}
{%- endif -%}
{{ yaml_dump(option.default) | indent(4, first=false) }}
{%- endif -%}
{%- endfor %}
{% if not options %}{}{% endif %}

View File

@@ -1,7 +1,6 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{{ super() }}
<title>Start Playing</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/startPlaying.css") }}" />
{% endblock %}

BIN
data/lua/PKMN_RB/core.dll Normal file

Binary file not shown.

389
data/lua/PKMN_RB/json.lua Normal file
View File

@@ -0,0 +1,389 @@
--
-- json.lua
--
-- Copyright (c) 2015 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--
local json = { _version = "0.1.0" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
function error(err)
print(err)
end
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if val[1] ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
print("invalid table: sparse array")
print(n)
print("VAL:")
print(val)
print("STACK:")
print(stack)
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
--local line_count = 1
--local col_count = 1
--for i = 1, idx - 1 do
-- col_count = col_count + 1
-- if str:sub(i, i) == "\n" then
-- line_count = line_count + 1
-- col_count = 1
-- end
-- end
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
return ( parse(str, next_char(str, 1, space_chars, true)) )
end
return json

View File

@@ -0,0 +1,238 @@
local socket = require("socket")
local json = require('json')
local math = require('math')
local STATE_OK = "Ok"
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
local STATE_UNINITIALIZED = "Uninitialized"
local APIndex = 0x1A6E
local APItemAddress = 0x00FF
local EventFlagAddress = 0x1735
local MissableAddress = 0x161A
local HiddenItemsAddress = 0x16DE
local RodAddress = 0x1716
local InGame = 0x1A71
local ItemsReceived = nil
local playerName = nil
local seedName = nil
local prevstate = ""
local curstate = STATE_UNINITIALIZED
local gbSocket = nil
local frame = 0
local u8 = nil
local wU8 = nil
local u16
--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded
local function defineMemoryFunctions()
local memDomain = {}
local domains = memory.getmemorydomainlist()
--if domains[1] == "System Bus" then
-- --NesHawk
-- isNesHawk = true
-- memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
-- memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end
-- memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
--elseif domains[1] == "WRAM" then
-- --QuickNES
-- memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
-- memDomain["saveram"] = function() memory.usememorydomain("WRAM") end
-- memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
--end
memDomain["rom"] = function() memory.usememorydomain("ROM") end
memDomain["wram"] = function() memory.usememorydomain("WRAM") end
return memDomain
end
local memDomain = defineMemoryFunctions()
u8 = memory.read_u8
wU8 = memory.write_u8
u16 = memory.read_u16_le
function uRange(address, bytes)
data = memory.readbyterange(address - 1, bytes + 1)
data[0] = nil
return data
end
function table.empty (self)
for _, _ in pairs(self) do
return false
end
return true
end
function slice (tbl, s, e)
local pos, new = 1, {}
for i = s + 1, e do
new[pos] = tbl[i]
pos = pos + 1
end
return new
end
function processBlock(block)
if block == nil then
return
end
local itemsBlock = block["items"]
memDomain.wram()
if itemsBlock ~= nil then-- and u8(0x116B) ~= 0x00 then
-- print(itemsBlock)
ItemsReceived = itemsBlock
end
end
function difference(a, b)
local aa = {}
for k,v in pairs(a) do aa[v]=true end
for k,v in pairs(b) do aa[v]=nil end
local ret = {}
local n = 0
for k,v in pairs(a) do
if aa[v] then n=n+1 ret[n]=v end
end
return ret
end
function generateLocationsChecked()
memDomain.wram()
events = uRange(EventFlagAddress, 0x140)
missables = uRange(MissableAddress, 0x20)
hiddenitems = uRange(HiddenItemsAddress, 0x0E)
rod = u8(RodAddress)
data = {}
table.foreach(events, function(k, v) table.insert(data, v) end)
table.foreach(missables, function(k, v) table.insert(data, v) end)
table.foreach(hiddenitems, function(k, v) table.insert(data, v) end)
table.insert(data, rod)
return data
end
function generateSerialData()
memDomain.wram()
status = u8(0x1A73)
if status == 0 then
return nil
end
return uRange(0x1A76, u8(0x1A74))
end
local function arrayEqual(a1, a2)
if #a1 ~= #a2 then
return false
end
for i, v in ipairs(a1) do
if v ~= a2[i] then
return false
end
end
return true
end
function receive()
l, e = gbSocket:receive()
if e == 'closed' then
if curstate == STATE_OK then
print("Connection closed")
end
curstate = STATE_UNINITIALIZED
return
elseif e == 'timeout' then
--print("timeout") -- this keeps happening for some reason? just hide it
return
elseif e ~= nil then
print(e)
curstate = STATE_UNINITIALIZED
return
end
if l ~= nil then
processBlock(json.decode(l))
end
-- Determine Message to send back
memDomain.rom()
newPlayerName = uRange(0xFFF0, 0x10)
newSeedName = uRange(0xFFDC, 20)
if (playerName ~= nil and not arrayEqual(playerName, newPlayerName)) or (seedName ~= nil and not arrayEqual(seedName, newSeedName)) then
print("ROM changed, quitting")
curstate = STATE_UNINITIALIZED
return
end
playerName = newPlayerName
seedName = newSeedName
local retTable = {}
retTable["playerName"] = playerName
retTable["seedName"] = seedName
memDomain.wram()
if u8(InGame) == 0xAC then
retTable["locations"] = generateLocationsChecked()
serialData = generateSerialData()
if serialData ~= nil then
retTable["serial"] = serialData
end
end
msg = json.encode(retTable).."\n"
local ret, error = gbSocket:send(msg)
if ret == nil then
print(error)
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
curstate = STATE_TENTATIVELY_CONNECTED
elseif curstate == STATE_TENTATIVELY_CONNECTED then
print("Connected!")
curstate = STATE_OK
end
end
function main()
if (is23Or24Or25 or is26To28) == false then
print("Must use a version of bizhawk 2.3.1 or higher")
return
end
server, error = socket.bind('localhost', 17242)
while true do
if not (curstate == prevstate) then
print("Current state: "..curstate)
prevstate = curstate
end
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
if (frame % 60 == 0) then
receive()
if u8(InGame) == 0xAC then
ItemIndex = u16(APIndex)
if ItemsReceived[ItemIndex + 1] ~= nil then
wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000)
end
end
end
elseif (curstate == STATE_UNINITIALIZED) then
if (frame % 60 == 0) then
print("Waiting for client.")
emu.frameadvance()
server:settimeout(2)
print("Attempting to connect")
local client, timeout = server:accept()
if timeout == nil then
-- print('Initial Connection Made')
curstate = STATE_INITIAL_CONNECTION_MADE
gbSocket = client
gbSocket:settimeout(0)
end
end
end
emu.frameadvance()
end
end
main()

132
data/lua/PKMN_RB/socket.lua Normal file
View File

@@ -0,0 +1,132 @@
-----------------------------------------------------------------------------
-- LuaSocket helper module
-- Author: Diego Nehab
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-----------------------------------------------------------------------------
local base = _G
local string = require("string")
local math = require("math")
local socket = require("socket.core")
module("socket")
-----------------------------------------------------------------------------
-- Exported auxiliar functions
-----------------------------------------------------------------------------
function connect(address, port, laddress, lport)
local sock, err = socket.tcp()
if not sock then return nil, err end
if laddress then
local res, err = sock:bind(laddress, lport, -1)
if not res then return nil, err end
end
local res, err = sock:connect(address, port)
if not res then return nil, err end
return sock
end
function bind(host, port, backlog)
local sock, err = socket.tcp()
if not sock then return nil, err end
sock:setoption("reuseaddr", true)
local res, err = sock:bind(host, port)
if not res then return nil, err end
res, err = sock:listen(backlog)
if not res then return nil, err end
return sock
end
try = newtry()
function choose(table)
return function(name, opt1, opt2)
if base.type(name) ~= "string" then
name, opt1, opt2 = "default", name, opt1
end
local f = table[name or "nil"]
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
else return f(opt1, opt2) end
end
end
-----------------------------------------------------------------------------
-- Socket sources and sinks, conforming to LTN12
-----------------------------------------------------------------------------
-- create namespaces inside LuaSocket namespace
sourcet = {}
sinkt = {}
BLOCKSIZE = 2048
sinkt["close-when-done"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if not chunk then
sock:close()
return 1
else return sock:send(chunk) end
end
})
end
sinkt["keep-open"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if chunk then return sock:send(chunk)
else return 1 end
end
})
end
sinkt["default"] = sinkt["keep-open"]
sink = choose(sinkt)
sourcet["by-length"] = function(sock, length)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if length <= 0 then return nil end
local size = math.min(socket.BLOCKSIZE, length)
local chunk, err = sock:receive(size)
if err then return nil, err end
length = length - string.len(chunk)
return chunk
end
})
end
sourcet["until-closed"] = function(sock)
local done
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if done then return nil end
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
if not err then return chunk
elseif err == "closed" then
sock:close()
done = 1
return partial
else return nil, err end
end
})
end
sourcet["default"] = sourcet["until-closed"]
source = choose(sourcet)

View File

@@ -234,6 +234,8 @@ Sent to clients as a response the a [Get](#Get) package.
| ---- | ---- | ----- |
| keys | dict\[str\, any] | A key-value collection containing all the values for the keys requested in the [Get](#Get) package. |
If a requested key was not present in the server's data, the associated value will be `null`.
Additional arguments added to the [Get](#Get) package that triggered this [Retrieved](#Retrieved) will also be passed along.
### SetReply

View File

@@ -19,6 +19,10 @@ After this, you should be able to run the programs.
* With yaml(s) in the `Players` folder, `Generate.py` will generate the multiworld archive.
* `MultiServer.py`, with the filename of the generated archive as a command line parameter, will host the multiworld locally.
* `--log_network` is a command line parameter useful for debugging.
* `WebHost.py` will host the website on your computer.
* You can copy `docs/webhost configuration sample.yaml` to `config.yaml`
to change WebHost options (like the web hosting port number).
* As a side effect, `WebHost.py` creates the template yamls for all the games in `WebHostLib/static/generated`.
## Windows

View File

@@ -138,6 +138,14 @@ dkc3_options:
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
pokemon_rb_options:
# File names of the Pokemon Red and Blue roms
red_rom_file: "Pokemon Red (UE) [S][!].gb"
blue_rom_file: "Pokemon Blue (UE) [S][!].gb"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .gb file with
rom_start: true
smw_options:
# File name of the SMW US rom
rom_file: "Super Mario World (USA).sfc"

View File

@@ -59,6 +59,8 @@ Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: ful
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
Name: "server"; Description: "Server"; Types: full hosting
Name: "client"; Description: "Clients"; Types: full playing
Name: "client/sni"; Description: "SNI Client"; Types: full playing
@@ -70,6 +72,9 @@ Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
Name: "client/pkmn"; Description: "Pokemon Client"
Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
@@ -84,6 +89,8 @@ Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Countr
Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
@@ -98,6 +105,7 @@ Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags
Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn
Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
@@ -111,6 +119,7 @@ Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactor
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn
Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2
@@ -121,6 +130,7 @@ Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\Archipela
Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot
Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1
Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn
Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2
@@ -179,6 +189,16 @@ Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archip
Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; Components: client/oot
Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/oot
Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn/red
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn/red
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn/red
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn/red
Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn/blue
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn/blue
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn/blue
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn/blue
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
@@ -234,6 +254,12 @@ var SoERomFilePage: TInputFileWizardPage;
var ootrom: string;
var OoTROMFilePage: TInputFileWizardPage;
var redrom: string;
var RedROMFilePage: TInputFileWizardPage;
var bluerom: string;
var BlueROMFilePage: TInputFileWizardPage;
function GetSNESMD5OfFile(const rom: string): string;
var data: AnsiString;
begin
@@ -281,6 +307,21 @@ begin
'.sfc');
end;
function AddGBRomPage(name: string): TInputFileWizardPage;
begin
Result :=
CreateInputFilePage(
wpSelectComponents,
'Select ROM File',
'Where is your ' + name + ' located?',
'Select the file, then click Next.');
Result.Add(
'Location of ROM file:',
'GB ROM files|*.gb;*.gbc|All files|*.*',
'.gb');
end;
procedure AddOoTRomPage();
begin
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
@@ -425,6 +466,38 @@ begin
Result := '';
end;
function GetRedROMPath(Param: string): string;
begin
if Length(redrom) > 0 then
Result := redrom
else if Assigned(RedRomFilePage) then
begin
R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc')
if R <> 0 then
MsgBox('Pokemon Red ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := RedROMFilePage.Values[0]
end
else
Result := '';
end;
function GetBlueROMPath(Param: string): string;
begin
if Length(bluerom) > 0 then
Result := bluerom
else if Assigned(BlueRomFilePage) then
begin
R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b')
if R <> 0 then
MsgBox('Pokemon Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := BlueROMFilePage.Values[0]
end
else
Result := '';
end;
procedure InitializeWizard();
begin
AddOoTRomPage();
@@ -448,6 +521,14 @@ begin
soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
if Length(soerom) = 0 then
SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
redrom := CheckRom('Pokemon Red (UE) [S][!].gb','3d45c1ee9abd5738df46d2bdda8b57dc');
if Length(redrom) = 0 then
RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb');
bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b');
if Length(bluerom) = 0 then
BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
end;
@@ -466,4 +547,8 @@ begin
Result := not (WizardIsComponentSelected('generator/soe'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot'));
end;
if (assigned(RedROMFilePage)) and (PageID = RedROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red'));
if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue'));
end;

View File

@@ -289,6 +289,7 @@ tmp="${{exe#*/}}"
if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then
exe="{default_exe.parent}/$exe"
fi
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib"
$APPDIR/$exe "$@"
""")
launcher_filename.chmod(0o755)

View File

@@ -8,7 +8,7 @@ class TestImplemented(unittest.TestCase):
def testCompletionCondition(self):
"""Ensure a completion condition is set that has requirements."""
for gamename, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden and gamename not in {"ArchipIDLE", "Final Fantasy"}:
if not world_type.hidden and gamename not in {"ArchipIDLE", "Final Fantasy", "Sudoku"}:
with self.subTest(gamename):
world = setup_default_world(world_type)
self.assertFalse(world.completion_condition[1](world.state))

View File

@@ -28,7 +28,7 @@ class TestBase(unittest.TestCase):
def testEmptyStateCanReachSomething(self):
for game_name, world_type in AutoWorldRegister.world_types.items():
# Final Fantasy logic is controlled by finalfantasyrandomizer.com
if game_name != "Archipelago" and game_name != "Final Fantasy":
if game_name not in {"Archipelago", "Final Fantasy", "Sudoku"}:
with self.subTest("Game", game=game_name):
world = setup_default_world(world_type)
state = CollectionState(world)

View File

@@ -0,0 +1,136 @@
import unittest
import Generate
from Options import PlandoBosses
class SingleBosses(PlandoBosses):
bosses = {"B1", "B2"}
locations = {"L1", "L2"}
option_vanilla = 0
option_shuffle = 1
@staticmethod
def can_place_boss(boss: str, location: str) -> bool:
if boss == "b1" and location == "l1":
return False
return True
class MultiBosses(SingleBosses):
bosses = SingleBosses.bosses # explicit copy required
locations = SingleBosses.locations
duplicate_bosses = True
option_singularity = 2 # required when duplicate_bosses = True
class TestPlandoBosses(unittest.TestCase):
def testCI(self):
"""Bosses, locations and modes are supposed to be case-insensitive"""
self.assertEqual(MultiBosses.from_any("L1-B2").value, "l1-b2;vanilla")
self.assertEqual(MultiBosses.from_any("ShUfFlE").value, SingleBosses.option_shuffle)
def testRandom(self):
"""Validate random is random"""
import random
random.seed(0)
value1 = MultiBosses.from_any("random")
random.seed(0)
value2 = MultiBosses.from_text("random")
self.assertEqual(value1, value2)
for n in range(0, 100):
if MultiBosses.from_text("random") != value1:
break
else:
raise Exception("random is not random")
def testShuffleMode(self):
"""Test that simple modes (no Plando) work"""
self.assertEqual(MultiBosses.from_any("shuffle"), MultiBosses.option_shuffle)
self.assertNotEqual(MultiBosses.from_any("vanilla"), MultiBosses.option_shuffle)
def testPlacement(self):
"""Test that valid placements work and invalid placements fail"""
with self.assertRaises(ValueError):
MultiBosses.from_any("l1-b1")
MultiBosses.from_any("l1-b2;l2-b1")
def testMixed(self):
"""Test that shuffle is applied for remaining locations"""
self.assertIn("shuffle", MultiBosses.from_any("l1-b2;l2-b1;shuffle").value)
self.assertIn("vanilla", MultiBosses.from_any("l1-b2;l2-b1").value)
def testUnknown(self):
"""Test that unknown values throw exceptions"""
# unknown boss
with self.assertRaises(ValueError):
MultiBosses.from_any("l1-b0")
# unknown location
with self.assertRaises(ValueError):
MultiBosses.from_any("l0-b1")
# swapped boss-location
with self.assertRaises(ValueError):
MultiBosses.from_any("b2-b2")
# boss name in place of mode (no singularity)
with self.assertRaises(ValueError):
SingleBosses.from_any("b1")
with self.assertRaises(ValueError):
SingleBosses.from_any("l2-b2;b1")
# location name in place of mode
with self.assertRaises(ValueError):
MultiBosses.from_any("l1")
with self.assertRaises(ValueError):
MultiBosses.from_any("l2-b2;l1")
# mode name in place of location
with self.assertRaises(ValueError):
MultiBosses.from_any("shuffle-b2;vanilla")
with self.assertRaises(ValueError):
MultiBosses.from_any("shuffle-b2;l2-b2")
# mode name in place of boss
with self.assertRaises(ValueError):
MultiBosses.from_any("l2-shuffle;vanilla")
with self.assertRaises(ValueError):
MultiBosses.from_any("l1-shuffle;l2-b2")
def testOrder(self):
"""Can't use mode in random places"""
with self.assertRaises(ValueError):
MultiBosses.from_any("shuffle;l2-b2")
def testDuplicateBoss(self):
"""Can place the same boss twice"""
MultiBosses.from_any("l1-b2;l2-b2")
with self.assertRaises(ValueError):
SingleBosses.from_any("l1-b2;l2-b2")
def testDuplicateLocation(self):
"""Can't use the same location twice"""
with self.assertRaises(ValueError):
MultiBosses.from_any("l1-b2;l1-b2")
def testSingularity(self):
"""Test automatic singularity mode"""
self.assertIn(";singularity", MultiBosses.from_any("b2").value)
def testPlandoSettings(self):
"""Test that plando settings verification works"""
plandoed_string = "l1-b2;l2-b1"
mixed_string = "l1-b2;shuffle"
regular_string = "shuffle"
plandoed = MultiBosses.from_any(plandoed_string)
mixed = MultiBosses.from_any(mixed_string)
regular = MultiBosses.from_any(regular_string)
# plando should work with boss plando
plandoed.verify(None, "Player", Generate.PlandoSettings.bosses)
self.assertTrue(plandoed.value.startswith(plandoed_string))
# plando should fall back to default without boss plando
plandoed.verify(None, "Player", Generate.PlandoSettings.items)
self.assertEqual(plandoed, MultiBosses.option_vanilla)
# mixed should fall back to mode
mixed.verify(None, "Player", Generate.PlandoSettings.items) # should produce a warning and still work
self.assertEqual(mixed, MultiBosses.option_shuffle)
# mode stuff should just work
regular.verify(None, "Player", Generate.PlandoSettings.items)
self.assertEqual(regular, MultiBosses.option_shuffle)

0
test/options/__init__.py Normal file
View File

View File

@@ -0,0 +1,142 @@
import unittest
import json
from random import Random
from worlds.overcooked2.Items import *
from worlds.overcooked2.Overcooked2Levels import Overcooked2Level, level_id_to_shortname
from worlds.overcooked2.Logic import level_logic, level_shuffle_factory
from worlds.overcooked2.Locations import oc2_location_name_to_id
class Overcooked2Test(unittest.TestCase):
def testItems(self):
self.assertEqual(len(item_name_to_id), len(item_id_to_name))
self.assertEqual(len(item_name_to_id), len(item_table))
previous_item = None
for item_name in item_table.keys():
item: Item = item_table[item_name]
self.assertGreaterEqual(item.code, oc2_base_id, "Overcooked Item ID out of range")
self.assertLessEqual(item.code, item_table["Calmer Unbread"].code, "Overcooked Item ID out of range")
if previous_item is not None:
self.assertEqual(item.code, previous_item + 1,
f"Overcooked Item ID noncontinguous: {item.code-oc2_base_id}")
previous_item = item.code
self.assertEqual(item_table["Ok Emote"].code - item_table["Cooking Emote"].code,
5, "Overcooked Emotes noncontigious")
for item_name in item_frequencies:
self.assertIn(item_name, item_table.keys(), "Unexpected Overcooked Item in item_frequencies")
for item_name in item_name_to_config_name.keys():
self.assertIn(item_name, item_table.keys(), "Unexpected Overcooked Item in config mapping")
for config_name in item_name_to_config_name.values():
self.assertIn(config_name, vanilla_values.keys(), "Unexpected Overcooked Item in default config mapping")
for config_name in vanilla_values.keys():
self.assertIn(config_name, item_name_to_config_name.values(),
"Unexpected Overcooked Item in default config mapping")
events = [
("Kevin-2", {"action": "UNLOCK_LEVEL", "payload": "38"}),
("Curse Emote", {"action": "UNLOCK_EMOTE", "payload": "1"}),
("Larger Tip Jar", {"action": "INC_TIP_COMBO", "payload": ""}),
("Order Lookahead", {"action": "INC_ORDERS_ON_SCREEN", "payload": ""}),
("Control Stick Batteries", {"action": "SET_VALUE", "payload": "DisableControlStick=False"}),
]
for (item_name, expected_event) in events:
expected_event["message"] = f"{item_name} Acquired!"
event = item_to_unlock_event(item_name)
self.assertEqual(event, expected_event)
self.assertFalse(is_progression("Preparing Emote"))
for item_name in item_table:
item_to_unlock_event(item_name)
def testOvercooked2Levels(self):
level_count = 0
for _ in Overcooked2Level():
level_count += 1
self.assertEqual(level_count, 44)
def testOvercooked2ShuffleFactory(self):
previous_runs = set()
for seed in range(0, 5):
levels = level_shuffle_factory(Random(seed), True, False)
self.assertEqual(len(levels), 44)
previous_level_id = None
for level_id in levels.keys():
if previous_level_id is not None:
self.assertEqual(previous_level_id+1, level_id)
previous_level_id = level_id
self.assertNotIn(levels[15], previous_runs)
previous_runs.add(levels[15])
levels = level_shuffle_factory(Random(123), False, True)
self.assertEqual(len(levels), 44)
def testLevelNameRepresentation(self):
shortnames = [level.as_generic_level.shortname for level in Overcooked2Level()]
for shortname in shortnames:
self.assertIn(shortname, level_logic.keys())
self.assertEqual(len(level_logic), len(level_id_to_shortname))
for level_name in level_logic.keys():
if level_name != "*":
self.assertIn(level_name, level_id_to_shortname.values())
for level_name in level_id_to_shortname.values():
if level_name != "Tutorial":
self.assertIn(level_name, level_logic.keys())
region_names = [level.level_name for level in Overcooked2Level()]
for location_name in oc2_location_name_to_id.keys():
level_name = location_name.split(" ")[0]
self.assertIn(level_name, region_names)
def testLogic(self):
for level_name in level_logic.keys():
logic = level_logic[level_name]
self.assertEqual(len(logic), 3, "Levels must provide logic for 1, 2, and 3 stars")
for l in logic:
self.assertEqual(len(l), 2)
(exclusive, additive) = l
for req in exclusive:
self.assertEqual(type(req), str)
self.assertIn(req, item_table.keys())
if len(additive) != 0:
self.assertGreater(len(additive), 1)
total_weight = 0.0
for req in additive:
self.assertEqual(len(req), 2)
(item_name, weight) = req
self.assertEqual(type(item_name), str)
self.assertEqual(type(weight), float)
total_weight += weight
self.assertIn(item_name, item_table.keys())
self.assertGreaterEqual(total_weight, 0.99, "Additive requirements must add to 1.0 or greater to have any effect")
def testItemLocationMapping(self):
number_of_items = 0
for item_name in item_frequencies:
freq = item_frequencies[item_name]
self.assertGreaterEqual(freq, 0)
number_of_items += freq
for item_name in item_table:
if item_name not in item_frequencies.keys():
number_of_items += 1
self.assertLessEqual(number_of_items, len(oc2_location_name_to_id), "Too many items (before fillers placed)")

View File

22
test/soe/TestAccess.py Normal file
View File

@@ -0,0 +1,22 @@
import typing
from . import SoETestBase
class AccessTest(SoETestBase):
@staticmethod
def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]):
return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers]
def testBronzeAxe(self):
gourds = {
"Pyramid bottom": (118, 121, 122, 123, 124, 125),
"Pyramid top": (140,)
}
locations = ["Rimsala"] + self._resolveGourds(gourds)
items = [["Bronze Axe"]]
self.assertAccessDependency(locations, items)
def testBronzeSpearPlus(self):
locations = ["Megataur"]
items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]]
self.assertAccessDependency(locations, items)

53
test/soe/TestGoal.py Normal file
View File

@@ -0,0 +1,53 @@
from . import SoETestBase
class TestFragmentGoal(SoETestBase):
options = {
"energy_core": "fragments",
"available_fragments": 21,
"required_fragments": 20,
}
def testFragments(self):
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
self.assertBeatable(False) # 0 fragments
fragments = self.get_items_by_name("Energy Core Fragment")
victory = self.get_item_by_name("Victory")
self.collect(fragments[:-2]) # 1 too few
self.assertEqual(self.count("Energy Core Fragment"), 19)
self.assertBeatable(False)
self.collect(fragments[-2:-1]) # exact
self.assertEqual(self.count("Energy Core Fragment"), 20)
self.assertBeatable(True)
self.remove([victory]) # reset
self.collect(fragments[-1:]) # 1 extra
self.assertEqual(self.count("Energy Core Fragment"), 21)
self.assertBeatable(True)
def testNoWeapon(self):
self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"])
self.assertBeatable(False)
def testNoRocket(self):
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"])
self.assertBeatable(False)
class TestShuffleGoal(SoETestBase):
options = {
"energy_core": "shuffle",
}
def testCore(self):
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
self.assertBeatable(False)
self.collect_by_name(["Energy Core"])
self.assertBeatable(True)
def testNoWeapon(self):
self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"])
self.assertBeatable(False)
def testNoRocket(self):
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"])
self.assertBeatable(False)

84
test/soe/__init__.py Normal file
View File

@@ -0,0 +1,84 @@
import typing
import unittest
from argparse import Namespace
from test.general import gen_steps
from BaseClasses import MultiWorld, Item
from worlds import AutoWorld
from worlds.AutoWorld import call_all
class SoETestBase(unittest.TestCase):
options: typing.Dict[str, typing.Any] = {}
world: MultiWorld
game = "Secret of Evermore"
def setUp(self):
self.world = MultiWorld(1)
self.world.game[1] = self.game
self.world.player_name = {1: "Tester"}
self.world.set_seed()
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items():
setattr(args, name, {1: option.from_any(self.options.get(name, option.default))})
self.world.set_options(args)
self.world.set_default_common_options()
for step in gen_steps:
call_all(self.world, step)
def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]):
if isinstance(item_names, str):
item_names = (item_names,)
for item in self.world.get_items():
if item.name not in item_names:
self.world.state.collect(item)
def get_item_by_name(self, item_name: str):
for item in self.world.get_items():
if item.name == item_name:
return item
raise ValueError("No such item")
def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]):
if isinstance(item_names, str):
item_names = (item_names,)
return [item for item in self.world.itempool if item.name in item_names]
def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]):
items = self.get_items_by_name(item_names)
self.collect(items)
return items
def collect(self, items: typing.Union[Item, typing.Iterable[Item]]):
if isinstance(items, Item):
items = (items,)
for item in items:
self.world.state.collect(item)
def remove(self, items: typing.Union[Item, typing.Iterable[Item]]):
if isinstance(items, Item):
items = (items,)
for item in items:
if item.location and item.location.event and item.location in self.world.state.events:
self.world.state.events.remove(item.location)
self.world.state.remove(item)
def can_reach_location(self, location):
return self.world.state.can_reach(location, "Location", 1)
def count(self, item_name):
return self.world.state.count(item_name, 1)
def assertAccessDependency(self, locations, possible_items):
all_items = [item_name for item_names in possible_items for item_name in item_names]
self.collect_all_but(all_items)
for location in self.world.get_locations():
self.assertEqual(self.world.state.can_reach(location), location.name not in locations)
for item_names in possible_items:
items = self.collect_by_name(item_names)
for location in locations:
self.assertTrue(self.can_reach_location(location))
self.remove(items)
def assertBeatable(self, beatable: bool):
self.assertEqual(self.world.can_beat_game(self.world.state), beatable)

View File

@@ -99,7 +99,7 @@ class APContainer:
"player_name": self.player_name,
"game": self.game,
# minimum version of patch system expected for patching to be successful
"compatible_version": 4,
"compatible_version": 5,
"version": current_patch_version,
}

View File

@@ -3,7 +3,7 @@ from typing import Optional, Union, List, Tuple, Callable, Dict
from BaseClasses import Boss
from Fill import FillError
from .Options import Bosses
from .Options import LTTPBosses as Bosses
def BossFactory(boss: str, player: int) -> Optional[Boss]:

View File

@@ -1,7 +1,7 @@
import typing
from BaseClasses import MultiWorld
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice, PlandoBosses
class Logic(Choice):
@@ -138,7 +138,7 @@ class WorldState(Choice):
option_inverted = 2
class Bosses(TextChoice):
class LTTPBosses(PlandoBosses):
"""Shuffles bosses around to different locations.
Basic will shuffle all bosses except Ganon and Agahnim anywhere they can be placed.
Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur.
@@ -152,7 +152,9 @@ class Bosses(TextChoice):
option_chaos = 3
option_singularity = 4
bosses: set = {
duplicate_bosses = True
bosses = {
"Armos Knights",
"Lanmolas",
"Moldorm",
@@ -165,7 +167,7 @@ class Bosses(TextChoice):
"Trinexx",
}
locations: set = {
locations = {
"Ganons Tower Top",
"Tower of Hera",
"Skull Woods",
@@ -181,99 +183,16 @@ class Bosses(TextChoice):
"Ganons Tower Bottom"
}
def __init__(self, value: typing.Union[str, int]):
assert isinstance(value, str) or isinstance(value, int), \
f"{value} is not a valid option for {self.__class__.__name__}"
self.value = value
@classmethod
def from_text(cls, text: str):
import random
# set all of our text to lower case for name checking
text = text.lower()
cls.bosses = {boss_name.lower() for boss_name in cls.bosses}
cls.locations = {boss_location.lower() for boss_location in cls.locations}
if text == "random":
return cls(random.choice(list(cls.options.values())))
for option_name, value in cls.options.items():
if option_name == text:
return cls(value)
options = text.split(";")
# since plando exists in the option verify the plando values given are valid
cls.validate_plando_bosses(options)
# find out what type of boss shuffle we should use for placing bosses after plando
# and add as a string to look nice in the spoiler
if "random" in options:
shuffle = random.choice(list(cls.options))
options.remove("random")
options = ";".join(options) + ";" + shuffle
boss_class = cls(options)
else:
for option in options:
if option in cls.options:
boss_class = cls(";".join(options))
break
else:
if len(options) == 1:
if cls.valid_boss_name(options[0]):
options = options[0] + ";singularity"
boss_class = cls(options)
else:
options = options[0] + ";none"
boss_class = cls(options)
else:
options = ";".join(options) + ";none"
boss_class = cls(options)
return boss_class
@classmethod
def validate_plando_bosses(cls, options: typing.List[str]) -> None:
from .Bosses import can_place_boss, format_boss_location
for option in options:
if option == "random" or option in cls.options:
if option != options[-1]:
raise ValueError(f"{option} option must be at the end of the boss_shuffle options!")
continue
if "-" in option:
location, boss = option.split("-")
level = ''
if not cls.valid_boss_name(boss):
raise ValueError(f"{boss} is not a valid boss name for location {location}.")
if not cls.valid_location_name(location):
raise ValueError(f"{location} is not a valid boss location name.")
if location.split(" ")[-1] in ("top", "middle", "bottom"):
location = location.split(" ")
level = location[-1]
location = " ".join(location[:-1])
location = location.title().replace("Of", "of")
if not can_place_boss(boss.title(), location, level):
raise ValueError(f"{format_boss_location(location, level)} "
f"is not a valid location for {boss.title()}.")
else:
if not cls.valid_boss_name(option):
raise ValueError(f"{option} is not a valid boss name.")
@classmethod
def valid_boss_name(cls, value: str) -> bool:
return value.lower() in cls.bosses
@classmethod
def valid_location_name(cls, value: str) -> bool:
return value in cls.locations
def verify(self, world, player_name: str, plando_options) -> None:
if isinstance(self.value, int):
return
from Generate import PlandoSettings
if not(PlandoSettings.bosses & plando_options):
import logging
# plando is disabled but plando options were given so pull the option and change it to an int
option = self.value.split(";")[-1]
self.value = self.options[option]
logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} "
f"boss shuffle will be used for player {player_name}.")
def can_place_boss(cls, boss: str, location: str) -> bool:
from worlds.alttp.Bosses import can_place_boss
level = ''
words = location.split(" ")
if words[-1] in ("top", "middle", "bottom"):
level = words[-1]
location = " ".join(words[:-1])
location = location.title().replace("Of", "of")
return can_place_boss(boss.title(), location, level)
class Enemies(Choice):
@@ -497,7 +416,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
"hints": Hints,
"scams": Scams,
"restrict_dungeon_item_on_boss": RestrictBossItem,
"boss_shuffle": Bosses,
"boss_shuffle": LTTPBosses,
"pot_shuffle": PotShuffle,
"enemy_shuffle": EnemyShuffle,
"killable_thieves": KillableThieves,

View File

@@ -0,0 +1,33 @@
from BaseClasses import Tutorial
from ..AutoWorld import World, WebWorld
from typing import Dict
class Bk_SudokuWebWorld(WebWorld):
settings_page = "games/Sudoku/info/en"
theme = 'partyTime'
tutorials = [
Tutorial(
tutorial_name='Setup Guide',
description='A guide to playing BK Sudoku',
language='English',
file_name='setup_en.md',
link='guide/en',
authors=['Jarno']
)
]
class Bk_SudokuWorld(World):
"""
Play a little Sudoku while you're in BK mode to maybe get some useful hints
"""
game = "Sudoku"
web = Bk_SudokuWebWorld()
item_name_to_id: Dict[str, int] = {}
location_name_to_id: Dict[str, int] = {}
@classmethod
def stage_assert_generate(cls, world):
raise Exception("BK Sudoku cannot be used for generating worlds, the client can instead connect to any other world")

View File

@@ -0,0 +1,13 @@
# Bk Sudoku
## What is this game?
BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku client that can connect to any existing multiworld. When connected, you can play Sudoku to unlock random hints for your game. While slow, it will give you something to do when you can't reach the checks in your game.
## What hints are unlocked?
After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to. It is possible to hint the same location repeatedly if that location is still unchecked.
## Where is the settings page?
There is no settings page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.

View File

@@ -0,0 +1,24 @@
# BK Sudoku Setup Guide
## Required Software
- [Bk Sudoku](https://github.com/Jarno458/sudoku)
- [.Net 6](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net60)
## General Concept
This is a client that can connect to any multiworld slot, and lets you play Sudoku to unlock random hints for that slot's locations.
Due to the fact that the Sudoku client may connect to any slot, it is not necessary to generate a YAML for this game as it does not generate any new slots in the multiworld session.
## Installation Procedures
Go to the latest release on [BK Sudoku Releases](https://github.com/Jarno458/sudoku/releases). Download and extract the `Bk_Sudoku.zip` file.
## Joining a MultiWorld Game
1. Run Bk_Sudoku.exe
2. Enter the name of the slot you wish to connect to
3. Enter the server url & port number
4. Press connect
5. Choose difficulty
6. Try to solve the Sudoku

View File

@@ -53,17 +53,25 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"],
spot.access_rule = rule
def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine='and'):
def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"):
old_rule = spot.access_rule
if combine == 'or':
spot.access_rule = lambda state: rule(state) or old_rule(state)
# empty rule, replace instead of add
if old_rule is spot.__class__.access_rule:
spot.access_rule = rule if combine == "and" else old_rule
else:
spot.access_rule = lambda state: rule(state) and old_rule(state)
if combine == "and":
spot.access_rule = lambda state: rule(state) and old_rule(state)
else:
spot.access_rule = lambda state: rule(state) or old_rule(state)
def forbid_item(location: "BaseClasses.Location", item: str, player: int):
old_rule = location.item_rule
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
# empty rule
if old_rule is location.__class__.item_rule:
location.item_rule = lambda i: i.name != item or i.player != player
else:
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
def forbid_items_for_player(location: "BaseClasses.Location", items: typing.Set[str], player: int):
@@ -77,9 +85,16 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]):
location.item_rule = lambda i: i.name not in items and old_rule(i)
def add_item_rule(location: "BaseClasses.Location", rule: ItemRule):
def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"):
old_rule = location.item_rule
location.item_rule = lambda item: rule(item) and old_rule(item)
# empty rule, replace instead of add
if old_rule is location.__class__.item_rule:
location.item_rule = rule if combine == "and" else old_rule
else:
if combine == "and":
location.item_rule = lambda item: rule(item) and old_rule(item)
else:
location.item_rule = lambda item: rule(item) or old_rule(item)
def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int,

View File

@@ -2,23 +2,24 @@
## What is Plando?
The purposes of randomizers is to randomize the items in a game to give a new experience. Plando takes this concept and
The purpose of randomizers is to randomize the items in a game to give a new experience. Plando takes this concept and
changes it up by allowing you to plan out certain aspects of the game by placing certain items in certain locations,
certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of
these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`,
and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported
by certain games. Currently, only LTTP supports text and boss plando. Support for connection plando may vary.
and `connection plando`. Every game in Archipelago supports item plando but the other plando options are only supported
by certain games. Currently, only A Link to the Past supports text and boss plando. Support for connection plando may
vary.
### Enabling Plando
On the website plando will already be enabled. If you will be generating the game locally plando features must be
On the website, plando will already be enabled. If you will be generating the game locally, plando features must be
enabled (opt-in).
* To opt-in go to the archipelago installation (default: `C:\ProgramData\Archipelago`), open the host.yaml with a text
* To opt-in go to the Archipelago installation (default: `C:\ProgramData\Archipelago`), open `host.yaml` with a text
editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such
as
`plando_options: bosses, items, texts, connections`.
* You can add the necessary plando modules for your settings to the `requires` section of your yaml. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like:
* You can add the necessary plando modules for your settings to the `requires` section of your YAML. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like:
```yaml
requires:
@@ -27,45 +28,45 @@ enabled (opt-in).
```
## Item Plando
Item plando allows a player to place an item in a specific location or specific locations, place multiple items into a
Item plando allows a player to place an item in a specific location or specific locations, or place multiple items into a
list of specific locations both in their own game or in another player's game.
* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either item and location, or items
and locations.
* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either `item` and
`location`, or `items` and `locations`.
* `from_pool` determines if the item should be taken *from* the item pool or *added* to it. This can be true or
false and defaults to true if omitted.
* `world` is the target world to place the item in.
* It gets ignored if only one world is generated.
* Can be a number, name, true, false, null, or a list. False is the default.
* If a number is used it targets that slot or player number in the multiworld.
* If a name is used it will target the world with that player name.
* If set to true it will be any player's world besides your own.
* If set to false it will target your own world.
* If set to null it will target a random world in the multiworld.
* If a number is used, it targets that slot or player number in the multiworld.
* If a name is used, it will target the world with that player name.
* If set to true, it will be any player's world besides your own.
* If set to false, it will target your own world.
* If set to null, it will target a random world in the multiworld.
* If a list of names is used, it will target the games with the player names specified.
* `force` determines whether the generator will fail if the item can't be placed in the location can be true, false,
* `force` determines whether the generator will fail if the item can't be placed in the location. Can be true, false,
or silent. Silent is the default.
* If set to true the item must be placed and the generator will throw an error if it is unable to do so.
* If set to false the generator will log a warning if the placement can't be done but will still generate.
* If set to silent and the placement fails it will be ignored entirely.
* If set to true, the item must be placed and the generator will throw an error if it is unable to do so.
* If set to false, the generator will log a warning if the placement can't be done but will still generate.
* If set to silent and the placement fails, it will be ignored entirely.
* `percentage` is the percentage chance for the relevant block to trigger. This can be any value from 0 to 100 and
if omitted will default to 100.
* Single Placement is when you use a plando block to place a single item at a single location.
* `item` is the item you would like to place and `location` is the location to place it.
* Multi Placement uses a plando block to place multiple items in multiple locations until either list is exhausted.
* `items` defines the items to use and a number letting you place multiple of it. You can use true instead of a number to have it use however many of that item are in your item pool.
* `items` defines the items to use, each with a number for the amount. Using `true` instead of a number uses however many of that item are in your item pool.
* `locations` is a list of possible locations those items can be placed in.
* Using the multi placement method, placements are picked randomly.
* Instead of a number, you can use true
* `count` can be used to set the maximum number of items placed from the block. The default is 1 if using `item` and False if using `items`
* If a number is used it will try to place this number of items.
* If set to false it will try to place as many items from the block as it can.
* If `min` and `max` are defined, it will try to place a number of items between these two numbers at random
* If a number is used, it will try to place this number of items.
* If set to false, it will try to place as many items from the block as it can.
* If `min` and `max` are defined, it will try to place a number of items between these two numbers at random.
### Available Items and Locations
A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is caps-sensitive.
A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is case-sensitive.
### Examples
@@ -142,43 +143,43 @@ plando_items:
min: 1
max: 4
```
1. This block has a 50% chance to occur, and if it does will place either the Empire Orb or Radiant Orb on another player's
Starter Chest 1 and removes the chosen item from the item pool.
1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another
player's Starter Chest 1 and removes the chosen item from the item pool.
2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots
in their own dungeon major item chests.
3. This block will always trigger and will lock boss relics on the bosses.
4. This block has an 80% chance of occurring and when it does will place all but 1 of the items randomly among the four
locations chosen here.
4. This block has an 80% chance of occurring, and when it does, it will place all but 1 of the items randomly among the
four locations chosen here.
5. This block will always trigger and will attempt to place a random 2 of Levitate, Revealer and Energize into
other players' Master Sword Pedestals or Boss Relic 1 locations.
6. This block will always trigger and will attempt to place a random number, between 1 and 4, of progressive swords
into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy
into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy.
## Boss Plando
As this is currently only supported by A Link to the Past instead of explaining here please refer to the
As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the
relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en)
## Text Plando
As this is currently only supported by A Link to the Past instead of explaining here please refer to the
As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the
relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en)
## Connections Plando
This is currently only supported by Minecraft and A Link to the Past. As the way that these games interact with their
connections is different I will only explain the basics here while more specifics for Link to the Past connection plando
can be found in its plando guide.
connections is different, I will only explain the basics here, while more specifics for A Link to the Past connection
plando can be found in its plando guide.
* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options support
* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options supports
subweights.
* `percentage` is the percentage chance for this connection from 0 to 100 and defaults to 100.
* Every connection has an `entrance` and an `exit`. These can be unlinked like in A Link to the Past insanity entrance
shuffle.
* `direction` can be `both`, `entrance`, or `exit` and determines in which direction this connection will operate.
[Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
[A Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
[Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Regions.py#L62)
@@ -186,7 +187,7 @@ can be found in its plando guide.
```yaml
plando_connections:
# example block 1 - Link to the Past
# example block 1 - A Link to the Past
- entrance: Cave Shop (Lake Hylia)
exit: Cave 45
direction: entrance
@@ -206,9 +207,9 @@ plando_connections:
direction: both
```
1. These connections are decoupled so going into the lake hylia cave shop will take you to the inside of cave 45 and
when you leave the interior you will exit to the cave 45 ledge. Going into the cave 45 entrance will then take you to
the lake hylia cave shop. Walking into the entrance for the old man cave and Agahnim's Tower entrance will both take
you to their locations as normal but leaving old man cave will exit at Agahnim's Tower.
2. This will force a nether fortress and a village to be the overworld structures for your game. Note that for the
1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and
when you leave the interior, you will exit to the Cave 45 ledge. Going into the Cave 45 entrance will then take you to
the Lake Hylia Cave Shop. Walking into the entrance for the Old Man Cave and Agahnim's Tower entrance will both take
you to their locations as normal, but leaving Old Man Cave will exit at Agahnim's Tower.
2. This will force a Nether fortress and a village to be the Overworld structures for your game. Note that for the
Minecraft connection plando to work structure shuffle must be enabled.

View File

@@ -8,36 +8,31 @@ about 5 minutes to read.
Triggers allow you to customize your game settings by allowing you to define one or many options which only occur under
specific conditions. These are essentially "if, then" statements for options in your game. A good example of what you
can do with triggers is the custom mercenary mode YAML that was created using entirely triggers and plando.
can do with triggers is the [custom mercenary mode YAML
](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml) that was
created using entirely triggers and plando.
Mercenary mode
YAML: [Mercenary Mode YAML on GitHub](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml)
For more information on plando you can reference the general plando guide or the Link to the Past plando guide.
General plando guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
Link to the Past plando guide: [LttP Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en)
For more information on plando, you can reference the [general plando guide](/tutorial/Archipelago/plando/en) or the
[A Link to the Past plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en).
## Trigger use
Triggers may be defined in either the root or in the relevant game sections. Generally, The best place to do this is the
bottom of the yaml for clear organization.
Triggers may be defined in either the root or in the relevant game sections. Generally, the best place to do this is the
bottom of the YAML for clear organization.
- Triggers comprise the trigger section and then each trigger must have an `option_category`, `option_name`, and
`option_result` from which it will react to and then an `options` section for the definition of what will happen.
- `option_category` is the defining section from which the option is defined in.
Each trigger consists of four parts:
- `option_category` specifies the section which the triggering option is defined in.
- Example: `A Link to the Past`
- This is the root category the option is located in. If the option you're triggering off of is in root then you
- This is the category the option is located in. If the option you're triggering off of is in root then you
would use `null`, otherwise this is the game for which you want this option trigger to activate.
- `option_name` is the option setting from which the triggered choice is going to react to.
- `option_name` specifies the name of the triggering option.
- Example: `shop_item_slots`
- This can be any option from any category defined in the yaml file in either root or a game section.
- `option_result` is the result of this option setting from which you would like to react.
- This can be any option from any category defined in the YAML file in either root or a game section.
- `option_result` specifies the value of the option that activates this trigger.
- Example: `15`
- Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple
results you would need multiple triggers for this.
- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring
results, you would need multiple triggers for this.
- `options` is where you define what will happen when the trigger activates. This can be something as simple as ensuring
another option also gets selected or placing an item in a certain location. It is possible to have multiple things
happen in this section.
- Example:
@@ -47,10 +42,10 @@ bottom of the yaml for clear organization.
Rupees (300): 2
```
This format must be:
The general format is:
```yaml
root option:
category:
option to change:
desired result
```
@@ -70,8 +65,8 @@ The above examples all together will end up looking like this:
Rupees(300): 2
```
For this example if the generator happens to roll 15 shuffled in shop item slots for your game you'll be granted 600
rupees at the beginning. These can also be used to change other options.
For this example, if the generator happens to roll 15 shuffled in shop item slots for your game, you'll be granted 600
rupees at the beginning. Triggers can also be used to change other options.
For example:
@@ -85,9 +80,9 @@ For example:
Inverted: true
```
In this example if your world happens to roll SpecificKeycards then your game will also start in inverted.
In this example, if your world happens to roll SpecificKeycards, then your game will also start in inverted.
It is also possible to use imaginary names in options to trigger specific settings. You can use these made up names in
It is also possible to use imaginary values in options to trigger specific settings. You can use these made-up values in
either your main options or to trigger from another trigger. Currently, this is the only way to trigger on "setting 1
AND setting 2".
@@ -97,33 +92,33 @@ For example:
triggers:
- option_category: Secret of Evermore
option_name: doggomizer
option_result: pupdunk
options:
Secret of Evermore:
difficulty:
normal: 50
pupdunk_hard: 25
pupdunk_mystery: 25
exp_modifier:
150: 50
200: 50
- option_category: Secret of Evermore
option_name: difficulty
option_result: pupdunk_hard
options:
Secret of Evermore:
fix_wings_glitch: false
difficulty: hard
- option_category: Secret of Evermore
option_name: difficulty
option_result: pupdunk_mystery
options:
Secret of Evermore:
fix_wings_glitch: false
difficulty: mystery
option_result: pupdunk
options:
Secret of Evermore:
difficulty:
normal: 50
pupdunk_hard: 25
pupdunk_mystery: 25
exp_modifier:
150: 50
200: 50
- option_category: Secret of Evermore
option_name: difficulty
option_result: pupdunk_hard
options:
Secret of Evermore:
fix_wings_glitch: false
difficulty: hard
- option_category: Secret of Evermore
option_name: difficulty
option_result: pupdunk_mystery
options:
Secret of Evermore:
fix_wings_glitch: false
difficulty: mystery
```
In this example (thanks to @Black-Sliver) if the `pupdunk` option is rolled then the difficulty values will be rolled
In this example (thanks to @Black-Sliver), if the `pupdunk` option is rolled, then the difficulty values will be rolled
again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using
new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard`
and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery".

View File

@@ -637,7 +637,7 @@ class HKItem(Item):
def __init__(self, name, advancement, code, type: str, player: int = None):
if name == "Mimic_Grub":
classification = ItemClassification.trap
elif type in ("Grub", "DreamWarrior", "Root", "Egg"):
elif type in ("Grub", "DreamWarrior", "Root", "Egg", "Dreamer"):
classification = ItemClassification.progression_skip_balancing
elif type == "Charm" and name not in progression_charms:
classification = ItemClassification.progression_skip_balancing

94
worlds/hylics2/Exits.py Normal file
View File

@@ -0,0 +1,94 @@
from typing import List, Dict
region_exit_table: Dict[int, List[str]] = {
0: ["New Game"],
1: ["To Waynehouse",
"To New Muldul",
"To Viewax",
"To TV Island",
"To Shield Facility",
"To Worm Pod",
"To Foglast",
"To Sage Labyrinth",
"To Hylemxylem"],
2: ["To World",
"To Afterlife",],
3: ["To Airship",
"To Waynehouse",
"To New Muldul",
"To Drill Castle",
"To Viewax",
"To Arcade Island",
"To TV Island",
"To Juice Ranch",
"To Shield Facility",
"To Worm Pod",
"To Foglast",
"To Sage Airship",
"To Hylemxylem"],
4: ["To World",
"To Afterlife",
"To New Muldul Vault"],
5: ["To New Muldul"],
6: ["To World",
"To Afterlife"],
7: ["To World"],
8: ["To World"],
9: ["To World",
"To Afterlife"],
10: ["To World"],
11: ["To World",
"To Afterlife",
"To Worm Pod"],
12: ["To Shield Facility",
"To Afterlife"],
13: ["To World",
"To Afterlife"],
14: ["To World",
"To Sage Labyrinth"],
15: ["To Drill Castle",
"To Afterlife"],
16: ["To World"],
17: ["To World",
"To Afterlife"]
}
exit_lookup_table: Dict[str, int] = {
"New Game": 2,
"To Waynehouse": 2,
"To Afterlife": 1,
"To World": 3,
"To New Muldul": 4,
"To New Muldul Vault": 5,
"To Viewax": 6,
"To Airship": 7,
"To Arcade Island": 8,
"To TV Island": 9,
"To Juice Ranch": 10,
"To Shield Facility": 11,
"To Worm Pod": 12,
"To Foglast": 13,
"To Drill Castle": 14,
"To Sage Labyrinth": 15,
"To Sage Airship": 16,
"To Hylemxylem": 17
}

243
worlds/hylics2/Items.py Normal file
View File

@@ -0,0 +1,243 @@
from BaseClasses import ItemClassification
from typing import TypedDict, Dict
class ItemDict(TypedDict):
classification: ItemClassification
count: int
name: str
item_table: Dict[int, ItemDict] = {
# Things
200622: {'classification': ItemClassification.filler,
'count': 1,
'name': 'DUBIOUS BERRY'},
200623: {'classification': ItemClassification.filler,
'count': 11,
'name': 'BURRITO'},
200624: {'classification': ItemClassification.filler,
'count': 1,
'name': 'COFFEE'},
200625: {'classification': ItemClassification.filler,
'count': 6,
'name': 'SOUL SPONGE'},
200626: {'classification': ItemClassification.useful,
'count': 6,
'name': 'MUSCLE APPLIQUE'},
200627: {'classification': ItemClassification.filler,
'count': 1,
'name': 'POOLWINE'},
200628: {'classification': ItemClassification.filler,
'count': 3,
'name': 'CUPCAKE'},
200629: {'classification': ItemClassification.filler,
'count': 3,
'name': 'COOKIE'},
200630: {'classification': ItemClassification.progression,
'count': 1,
'name': 'HOUSE KEY'},
200631: {'classification': ItemClassification.filler,
'count': 2,
'name': 'MEAT'},
200632: {'classification': ItemClassification.progression,
'count': 1,
'name': 'PNEUMATOPHORE'},
200633: {'classification': ItemClassification.progression,
'count': 1,
'name': 'CAVE KEY'},
200634: {'classification': ItemClassification.filler,
'count': 6,
'name': 'JUICE'},
200635: {'classification': ItemClassification.progression,
'count': 1,
'name': 'DOCK KEY'},
200636: {'classification': ItemClassification.filler,
'count': 14,
'name': 'BANANA'},
200637: {'classification': ItemClassification.progression,
'count': 3,
'name': 'PAPER CUP'},
200638: {'classification': ItemClassification.progression,
'count': 1,
'name': 'JAIL KEY'},
200639: {'classification': ItemClassification.progression,
'count': 1,
'name': 'PADDLE'},
200640: {'classification': ItemClassification.progression,
'count': 1,
'name': 'WORM ROOM KEY'},
200641: {'classification': ItemClassification.progression,
'count': 1,
'name': 'BRIDGE KEY'},
200642: {'classification': ItemClassification.filler,
'count': 2,
'name': 'STEM CELL'},
200643: {'classification': ItemClassification.progression,
'count': 1,
'name': 'UPPER CHAMBER KEY'},
200644: {'classification': ItemClassification.progression,
'count': 1,
'name': 'VESSEL ROOM KEY'},
200645: {'classification': ItemClassification.filler,
'count': 3,
'name': 'CLOUD GERM'},
200646: {'classification': ItemClassification.progression,
'count': 1,
'name': 'SKULL BOMB'},
200647: {'classification': ItemClassification.progression,
'count': 1,
'name': 'TOWER KEY'},
200648: {'classification': ItemClassification.progression,
'count': 1,
'name': 'DEEP KEY'},
200649: {'classification': ItemClassification.filler,
'count': 1,
'name': 'MULTI-COFFEE'},
200650: {'classification': ItemClassification.filler,
'count': 4,
'name': 'MULTI-JUICE'},
200651: {'classification': ItemClassification.filler,
'count': 1,
'name': 'MULTI STEM CELL'},
200652: {'classification': ItemClassification.filler,
'count': 6,
'name': 'MULTI SOUL SPONGE'},
#200653: {'classification': ItemClassification.filler,
# 'count': 1,
# 'name': 'ANTENNA'},
200654: {'classification': ItemClassification.progression,
'count': 1,
'name': 'UPPER HOUSE KEY'},
200655: {'classification': ItemClassification.useful,
'count': 1,
'name': 'BOTTOMLESS JUICE'},
200656: {'classification': ItemClassification.progression,
'count': 3,
'name': 'SAGE TOKEN'},
200657: {'classification': ItemClassification.progression,
'count': 1,
'name': 'CLICKER'},
# Garbs > Gloves
200658: {'classification': ItemClassification.useful,
'count': 1,
'name': 'CURSED GLOVES'},
200659: {'classification': ItemClassification.useful,
'count': 5,
'name': 'LONG GLOVES'},
200660: {'classification': ItemClassification.useful,
'count': 1,
'name': 'BRAIN DIGITS'},
200661: {'classification': ItemClassification.useful,
'count': 1,
'name': 'MATERIEL MITTS'},
200662: {'classification': ItemClassification.useful,
'count': 1,
'name': 'PLEATHER GAGE'},
200663: {'classification': ItemClassification.useful,
'count': 1,
'name': 'PEPTIDE BODKINS'},
200664: {'classification': ItemClassification.useful,
'count': 1,
'name': 'TELESCOPIC SLEEVE'},
200665: {'classification': ItemClassification.useful,
'count': 1,
'name': 'TENDRIL HAND'},
200666: {'classification': ItemClassification.useful,
'count': 1,
'name': 'PSYCHIC KNUCKLE'},
200667: {'classification': ItemClassification.useful,
'count': 1,
'name': 'SINGLE GLOVE'},
# Garbs > Accessories
200668: {'classification': ItemClassification.useful,
'count': 1,
'name': 'FADED PONCHO'},
200669: {'classification': ItemClassification.useful,
'count': 1,
'name': 'JUMPSUIT'},
200670: {'classification': ItemClassification.useful,
'count': 1,
'name': 'BOOTS'},
200671: {'classification': ItemClassification.useful,
'count': 1,
'name': 'CONVERTER WORM'},
200672: {'classification': ItemClassification.useful,
'count': 1,
'name': 'COFFEE CHIP'},
200673: {'classification': ItemClassification.useful,
'count': 1,
'name': 'RANCHER PONCHO'},
200674: {'classification': ItemClassification.useful,
'count': 1,
'name': 'ORGAN FORT'},
200675: {'classification': ItemClassification.useful,
'count': 2,
'name': 'LOOPED DOME'},
200676: {'classification': ItemClassification.useful,
'count': 1,
'name': 'DUCTILE HABIT'},
200677: {'classification': ItemClassification.useful,
'count': 2,
'name': 'TARP'},
# Bones
200686: {'classification': ItemClassification.filler,
'count': 1,
'name': '100 Bones'},
200687: {'classification': ItemClassification.filler,
'count': 1,
'name': '50 Bones'}
}
gesture_item_table: Dict[int, ItemDict] = {
200678: {'classification': ItemClassification.useful,
'count': 1,
'name': 'POROMER BLEB'},
200679: {'classification': ItemClassification.useful,
'count': 1,
'name': 'SOUL CRISPER'},
200680: {'classification': ItemClassification.useful,
'count': 1,
'name': 'TIME SIGIL'},
200681: {'classification': ItemClassification.progression,
'count': 1,
'name': 'CHARGE UP'},
200682: {'classification': ItemClassification.useful,
'count': 1,
'name': 'FATE SANDBOX'},
200683: {'classification': ItemClassification.useful,
'count': 1,
'name': 'TELEDENUDATE'},
200684: {'classification': ItemClassification.useful,
'count': 1,
'name': 'LINK MOLLUSC'},
200685: {'classification': ItemClassification.useful,
'count': 1,
'name': 'BOMBO - GENESIS'},
200688: {'classification': ItemClassification.useful,
'count': 1,
'name': 'NEMATODE INTERFACE'},
}
party_item_table: Dict[int, ItemDict] = {
200689: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Pongorma'},
200690: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Dedusmuln'},
200691: {'classification': ItemClassification.progression,
'count': 1,
'name': 'Somsnosa'}
}
medallion_item_table: Dict[int, ItemDict] = {
200692: {'classification': ItemClassification.filler,
'count': 30,
'name': '10 Bones'}
}

383
worlds/hylics2/Locations.py Normal file
View File

@@ -0,0 +1,383 @@
from typing import Dict, TypedDict
class LocationDict(TypedDict, total=False):
name: str
region: int
location_table: Dict[int, LocationDict] = {
# Waynehouse
200622: {'name': "Waynehouse: Toilet",
'region': 2},
200623: {'name': "Waynehouse: Basement Pot 1",
'region': 2},
200624: {'name': "Waynehouse: Basement Pot 2",
'region': 2},
200625: {'name': "Waynehouse: Basement Pot 3",
'region': 2},
200626: {'name': "Waynehouse: Sarcophagus",
'region': 2},
# Afterlife
200628: {'name': "Afterlife: Mangled Wayne",
'region': 1},
200629: {'name': "Afterlife: Jar near Mangled Wayne",
'region': 1},
200630: {'name': "Afterlife: Jar under Pool",
'region': 1},
# New Muldul
200632: {'name': "New Muldul: Shop Ceiling Pot 1",
'region': 4},
200633: {'name': "New Muldul: Shop Ceiling Pot 2",
'region': 4},
200634: {'name': "New Muldul: Flag Banana",
'region': 4},
200635: {'name': "New Muldul: Pot near Vault",
'region': 4},
200636: {'name': "New Muldul: Underground Pot",
'region': 4},
200637: {'name': "New Muldul: Underground Chest",
'region': 4},
200638: {'name': "New Muldul: Juice Trade",
'region': 4},
200639: {'name': "New Muldul: Basement Suitcase",
'region': 4},
200640: {'name': "New Muldul: Upper House Chest 1",
'region': 4},
200641: {'name': "New Muldul: Upper House Chest 2",
'region': 4},
# New Muldul Vault
200643: {'name': "New Muldul: Talk to Pongorma",
'region': 4},
200645: {'name': "New Muldul: Rescued Blerol 1",
'region': 4},
200646: {'name': "New Muldul: Rescued Blerol 2",
'region': 4},
200647: {'name': "New Muldul: Vault Left Chest",
'region': 5},
200648: {'name': "New Muldul: Vault Right Chest",
'region': 5},
200649: {'name': "New Muldul: Vault Bomb",
'region': 5},
# Viewax's Edifice
200650: {'name': "Viewax's Edifice: Fountain Banana",
'region': 6},
200651: {'name': "Viewax's Edifice: Dedusmuln's Suitcase",
'region': 6},
200652: {'name': "Viewax's Edifice: Dedusmuln's Campfire",
'region': 6},
200653: {'name': "Viewax's Edifice: Talk to Dedusmuln",
'region': 6},
200655: {'name': "Viewax's Edifice: Canopic Jar",
'region': 6},
200656: {'name': "Viewax's Edifice: Cave Sarcophagus",
'region': 6},
200657: {'name': "Viewax's Edifice: Shielded Key",
'region': 6},
200658: {'name': "Viewax's Edifice: Tower Pot",
'region': 6},
200659: {'name': "Viewax's Edifice: Tower Jar",
'region': 6},
200660: {'name': "Viewax's Edifice: Tower Chest",
'region': 6},
200661: {'name': "Viewax's Edifice: Sage Fridge",
'region': 6},
200662: {'name': "Viewax's Edifice: Sage Item 1",
'region': 6},
200663: {'name': "Viewax's Edifice: Sage Item 2",
'region': 6},
200664: {'name': "Viewax's Edifice: Viewax Pot",
'region': 6},
200665: {'name': "Viewax's Edifice: Defeat Viewax",
'region': 6},
# Viewax Arcade Minigame
200667: {'name': "Arcade 1: Key",
'region': 6},
200668: {'name': "Arcade 1: Coin Dash",
'region': 6},
200669: {'name': "Arcade 1: Burrito Alcove 1",
'region': 6},
200670: {'name': "Arcade 1: Burrito Alcove 2",
'region': 6},
200671: {'name': "Arcade 1: Behind Spikes Banana",
'region': 6},
200672: {'name': "Arcade 1: Pyramid Banana",
'region': 6},
200673: {'name': "Arcade 1: Moving Platforms Muscle Applique",
'region': 6},
200674: {'name': "Arcade 1: Bed Banana",
'region': 6},
# Airship
200675: {'name': "Airship: Talk to Somsnosa",
'region': 7},
# Arcade Island
200676: {'name': "Arcade Island: Shielded Key",
'region': 8},
200677: {'name': "Arcade 2: Flying Machine Banana",
'region': 8},
200678: {'name': "Arcade 2: Paper Cup Detour",
'region': 8},
200679: {'name': "Arcade 2: Peak Muscle Applique",
'region': 8},
200680: {'name': "Arcade 2: Double Banana 1",
'region': 8},
200681: {'name': "Arcade 2: Double Banana 2",
'region': 8},
200682: {'name': "Arcade 2: Cave Burrito",
'region': 8},
# Juice Ranch
200684: {'name': "Juice Ranch: Juice 1",
'region': 10},
200685: {'name': "Juice Ranch: Juice 2",
'region': 10},
200686: {'name': "Juice Ranch: Juice 3",
'region': 10},
200687: {'name': "Juice Ranch: Ledge Rancher",
'region': 10},
200688: {'name': "Juice Ranch: Battle with Somsnosa",
'region': 10},
200690: {'name': "Juice Ranch: Fridge",
'region': 10},
# Worm Pod
200692: {'name': "Worm Pod: Key",
'region': 12},
# Foglast
200693: {'name': "Foglast: West Sarcophagus",
'region': 13},
200694: {'name': "Foglast: Underground Sarcophagus",
'region': 13},
200695: {'name': "Foglast: Shielded Key",
'region': 13},
200696: {'name': "Foglast: Buy Clicker",
'region': 13},
200698: {'name': "Foglast: Shielded Chest",
'region': 13},
200699: {'name': "Foglast: Cave Fridge",
'region': 13},
200700: {'name': "Foglast: Roof Sarcophagus",
'region': 13},
200701: {'name': "Foglast: Under Lair Sarcophagus 1",
'region': 13},
200702: {'name': "Foglast: Under Lair Sarcophagus 2",
'region': 13},
200703: {'name': "Foglast: Under Lair Sarcophagus 3",
'region': 13},
200704: {'name': "Foglast: Sage Sarcophagus",
'region': 13},
200705: {'name': "Foglast: Sage Item 1",
'region': 13},
200706: {'name': "Foglast: Sage Item 2",
'region': 13},
# Drill Castle
200707: {'name': "Drill Castle: Ledge Banana",
'region': 14},
200708: {'name': "Drill Castle: Island Banana",
'region': 14},
200709: {'name': "Drill Castle: Island Pot",
'region': 14},
200710: {'name': "Drill Castle: Cave Sarcophagus",
'region': 14},
200711: {'name': "Drill Castle: Roof Banana",
'region': 14},
# Sage Labyrinth
200713: {'name': "Sage Labyrinth: 1F Chest Near Fountain",
'region': 15},
200714: {'name': "Sage Labyrinth: 1F Hidden Sarcophagus",
'region': 15},
200715: {'name': "Sage Labyrinth: 1F Four Statues Chest 1",
'region': 15},
200716: {'name': "Sage Labyrinth: 1F Four Statues Chest 2",
'region': 15},
200717: {'name': "Sage Labyrinth: B1 Double Chest 1",
'region': 15},
200718: {'name': "Sage Labyrinth: B1 Double Chest 2",
'region': 15},
200719: {'name': "Sage Labyrinth: B1 Single Chest",
'region': 15},
200720: {'name': "Sage Labyrinth: B1 Enemy Chest",
'region': 15},
200721: {'name': "Sage Labyrinth: B1 Hidden Sarcophagus",
'region': 15},
200722: {'name': "Sage Labyrinth: B1 Hole Chest",
'region': 15},
200723: {'name': "Sage Labyrinth: B2 Hidden Sarcophagus 1",
'region': 15},
200724: {'name': "Sage Labyrinth: B2 Hidden Sarcophagus 2",
'region': 15},
200754: {'name': "Sage Labyrinth: 2F Sarcophagus",
'region': 15},
200725: {'name': "Sage Labyrinth: Motor Hunter Sarcophagus",
'region': 15},
200726: {'name': "Sage Labyrinth: Sage Item 1",
'region': 15},
200727: {'name': "Sage Labyrinth: Sage Item 2",
'region': 15},
200728: {'name': "Sage Labyrinth: Sage Left Arm",
'region': 15},
200729: {'name': "Sage Labyrinth: Sage Right Arm",
'region': 15},
200730: {'name': "Sage Labyrinth: Sage Left Leg",
'region': 15},
200731: {'name': "Sage Labyrinth: Sage Right Leg",
'region': 15},
# Sage Airship
200732: {'name': "Sage Airship: Bottom Level Pot",
'region': 16},
200733: {'name': "Sage Airship: Flesh Pot",
'region': 16},
200734: {'name': "Sage Airship: Top Jar",
'region': 16},
# Hylemxylem
200736: {'name': "Hylemxylem: Jar",
'region': 17},
200737: {'name': "Hylemxylem: Lower Reservoir Key",
'region': 17},
200738: {'name': "Hylemxylem: Fountain Banana",
'region': 17},
200739: {'name': "Hylemxylem: East Island Banana",
'region': 17},
200740: {'name': "Hylemxylem: East Island Chest",
'region': 17},
200741: {'name': "Hylemxylem: Upper Chamber Banana",
'region': 17},
200742: {'name': "Hylemxylem: Across Upper Reservoir Chest",
'region': 17},
200743: {'name': "Hylemxylem: Drained Lower Reservoir Chest",
'region': 17},
200744: {'name': "Hylemxylem: Drained Lower Reservoir Burrito 1",
'region': 17},
200745: {'name': "Hylemxylem: Drained Lower Reservoir Burrito 2",
'region': 17},
200746: {'name': "Hylemxylem: Lower Reservoir Hole Pot 1",
'region': 17},
200747: {'name': "Hylemxylem: Lower Reservoir Hole Pot 2",
'region': 17},
200748: {'name': "Hylemxylem: Lower Reservoir Hole Pot 3",
'region': 17},
200749: {'name': "Hylemxylem: Lower Reservoir Hole Sarcophagus",
'region': 17},
200750: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 1",
'region': 17},
200751: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 2",
'region': 17},
200752: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 3",
'region': 17},
200753: {'name': "Hylemxylem: Upper Reservoir Hole Key",
'region': 17}
}
tv_location_table: Dict[int, LocationDict] = {
200627: {'name': "Waynehouse: TV",
'region': 2},
200631: {'name': "Afterlife: TV",
'region': 1},
200642: {'name': "New Muldul: TV",
'region': 4},
200666: {'name': "Viewax's Edifice: TV",
'region': 6},
200683: {'name': "TV Island: TV",
'region': 9},
200691: {'name': "Juice Ranch: TV",
'region': 10},
200697: {'name': "Foglast: TV",
'region': 13},
200712: {'name': "Drill Castle: TV",
'region': 14},
200735: {'name': "Sage Airship: TV",
'region': 16}
}
party_location_table: Dict[int, LocationDict] = {
200644: {'name': "New Muldul: Pongorma Joins",
'region': 4},
200654: {'name': "Viewax's Edifice: Dedusmuln Joins",
'region': 6},
200689: {'name': "Juice Ranch: Somsnosa Joins",
'region': 10}
}
medallion_location_table: Dict[int, LocationDict] = {
200755: {'name': "New Muldul: Upper House Medallion",
'region': 4},
200756: {'name': "New Muldul: Vault Rear Left Medallion",
'region': 5},
200757: {'name': "New Muldul: Vault Rear Right Medallion",
'region': 5},
200758: {'name': "New Muldul: Vault Center Medallion",
'region': 5},
200759: {'name': "New Muldul: Vault Front Left Medallion",
'region': 5},
200760: {'name': "New Muldul: Vault Front Right Medallion",
'region': 5},
200761: {'name': "Viewax's Edifice: Fort Wall Medallion",
'region': 6},
200762: {'name': "Viewax's Edifice: Jar Medallion",
'region': 6},
200763: {'name': "Viewax's Edifice: Sage Chair Medallion",
'region': 6},
200764: {'name': "Arcade 1: Lonely Medallion",
'region': 6},
200765: {'name': "Arcade 1: Alcove Medallion",
'region': 6},
200766: {'name': "Arcade 1: Lava Medallion",
'region': 6},
200767: {'name': "Arcade 2: Flying Machine Medallion",
'region': 8},
200768: {'name': "Arcade 2: Guarded Medallion",
'region': 8},
200769: {'name': "Arcade 2: Spinning Medallion",
'region': 8},
200770: {'name': "Arcade 2: Hook Medallion",
'region': 8},
200771: {'name': "Arcade 2: Flag Medallion",
'region': 8},
200772: {'name': "Foglast: Under Lair Medallion",
'region': 13},
200773: {'name': "Foglast: Mid-Air Medallion",
'region': 13},
200774: {'name': "Foglast: Top of Tower Medallion",
'region': 13},
200775: {'name': "Sage Airship: Walkway Medallion",
'region': 16},
200776: {'name': "Sage Airship: Flesh Medallion",
'region': 16},
200777: {'name': "Sage Airship: Top of Ship Medallion",
'region': 16},
200778: {'name': "Sage Airship: Hidden Medallion 1",
'region': 16},
200779: {'name': "Sage Airship: Hidden Medallion 2",
'region': 16},
200780: {'name': "Sage Airship: Hidden Medallion 3",
'region': 16},
200781: {'name': "Hylemxylem: Lower Reservoir Medallion",
'region': 17},
200782: {'name': "Hylemxylem: Lower Reservoir Hole Medallion",
'region': 17},
200783: {'name': "Hylemxylem: Drain Switch Medallion",
'region': 17},
200784: {'name': "Hylemxylem: Warpo Medallion",
'region': 17}
}

41
worlds/hylics2/Options.py Normal file
View File

@@ -0,0 +1,41 @@
from Options import Choice, Toggle, DefaultOnToggle, DeathLink
class PartyShuffle(Toggle):
"""Shuffles party members into the pool.
Note that enabling this can potentially increase both the difficulty and length of a run."""
display_name = "Shuffle Party Members"
class GestureShuffle(Choice):
"""Choose where gestures will appear in the item pool."""
display_name = "Shuffle Gestures"
option_anywhere = 0
option_tvs_only = 1
option_default_locations = 2
default = 0
class MedallionShuffle(Toggle):
"""Shuffles red medallions into the pool."""
display_name = "Shuffle Red Medallions"
class RandomStart(Toggle):
"""Start the randomizer in 1 of 4 positions.
(Waynehouse, Viewax's Edifice, TV Island, Shield Facility)"""
display_name = "Randomize Start Location"
class ExtraLogic(DefaultOnToggle):
"""Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult."""
display_name = "Extra Items in Logic"
class Hylics2DeathLink(DeathLink):
"""When you die, everyone dies. The reverse is also true.
Note that this also includes death by using the PERISH gesture.
Can be toggled via in-game console command "/deathlink"."""
hylics2_options = {
"party_shuffle": PartyShuffle,
"gesture_shuffle" : GestureShuffle,
"medallion_shuffle" : MedallionShuffle,
"random_start" : RandomStart,
"extra_items_in_logic": ExtraLogic,
"death_link": Hylics2DeathLink
}

433
worlds/hylics2/Rules.py Normal file
View File

@@ -0,0 +1,433 @@
from worlds.generic.Rules import add_rule
from ..AutoWorld import LogicMixin
class Hylics2Logic(LogicMixin):
def _hylics2_can_air_dash(self, player):
return self.has("PNEUMATOPHORE", player)
def _hylics2_has_airship(self, player):
return self.has("DOCK KEY", player)
def _hylics2_has_jail_key(self, player):
return self.has("JAIL KEY", player)
def _hylics2_has_paddle(self, player):
return self.has("PADDLE", player)
def _hylics2_has_worm_room_key(self, player):
return self.has("WORM ROOM KEY", player)
def _hylics2_has_bridge_key(self, player):
return self.has("BRIDGE KEY", player)
def _hylics2_has_upper_chamber_key(self, player):
return self.has("UPPER CHAMBER KEY", player)
def _hylics2_has_vessel_room_key(self, player):
return self.has("VESSEL ROOM KEY", player)
def _hylics2_has_house_key(self, player):
return self.has("HOUSE KEY", player)
def _hylics2_has_cave_key(self, player):
return self.has("CAVE KEY", player)
def _hylics2_has_skull_bomb(self, player):
return self.has("SKULL BOMB", player)
def _hylics2_has_tower_key(self, player):
return self.has("TOWER KEY", player)
def _hylics2_has_deep_key(self, player):
return self.has("DEEP KEY", player)
def _hylics2_has_upper_house_key(self, player):
return self.has("UPPER HOUSE KEY", player)
def _hylics2_has_clicker(self, player):
return self.has("CLICKER", player)
def _hylics2_has_tokens(self, player):
return self.has("SAGE TOKEN", player, 3)
def _hylics2_has_charge_up(self, player):
return self.has("CHARGE UP", player)
def _hylics2_has_cup(self, player):
return self.has("PAPER CUP", player, 1)
def _hylics2_has_1_member(self, player):
return self.has("Pongorma", player) or self.has("Dedusmuln", player) or self.has("Somsnosa", player)
def _hylics2_has_2_members(self, player):
return (self.has("Pongorma", player) and self.has("Dedusmuln", player)) or\
(self.has("Pongorma", player) and self.has("Somsnosa", player)) or\
(self.has("Dedusmuln", player) and self.has("Somsnosa", player))
def _hylics2_has_3_members(self, player):
return self.has("Pongorma", player) and self.has("Dedusmuln", player) and self.has("Somsnosa", player)
def _hylics2_enter_arcade2(self, player):
return self._hylics2_can_air_dash(player) and self._hylics2_has_airship(player)
def _hylics2_enter_wormpod(self, player):
return self._hylics2_has_airship(player) and self._hylics2_has_worm_room_key(player) and\
self._hylics2_has_paddle(player)
def _hylics2_enter_sageship(self, player):
return self._hylics2_has_skull_bomb(player) and self._hylics2_has_airship(player) and\
self._hylics2_has_paddle(player)
def _hylics2_enter_foglast(self, player):
return self._hylics2_enter_wormpod(player)
def _hylics2_enter_hylemxylem(self, player):
return self._hylics2_can_air_dash(player) and self._hylics2_enter_wormpod(player) and\
self._hylics2_has_bridge_key(player)
def set_rules(hylics2world):
world = hylics2world.world
player = hylics2world.player
# Afterlife
add_rule(world.get_location("Afterlife: TV", player),
lambda state: state._hylics2_has_cave_key(player))
# New Muldul
add_rule(world.get_location("New Muldul: Underground Chest", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("New Muldul: TV", player),
lambda state: state._hylics2_has_house_key(player))
add_rule(world.get_location("New Muldul: Upper House Chest 1", player),
lambda state: state._hylics2_has_upper_house_key(player))
add_rule(world.get_location("New Muldul: Upper House Chest 2", player),
lambda state: state._hylics2_has_upper_house_key(player))
# New Muldul Vault
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
lambda state: (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) and\
((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) or\
(state._hylics2_has_bridge_key(player) and state._hylics2_has_worm_room_key(player))))
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
lambda state: (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) and\
((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) or\
(state._hylics2_has_bridge_key(player) and state._hylics2_has_worm_room_key(player))))
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("New Muldul: Vault Bomb", player),
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
# Viewax's Edifice
add_rule(world.get_location("Viewax's Edifice: Canopic Jar", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Viewax's Edifice: Cave Sarcophagus", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Viewax's Edifice: Tower Pot", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Viewax's Edifice: Tower Jar", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Viewax's Edifice: Tower Chest", player),
lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_tower_key(player))
add_rule(world.get_location("Viewax's Edifice: Viewax Pot", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Viewax's Edifice: TV", player),
lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_jail_key(player))
add_rule(world.get_location("Viewax's Edifice: Sage Fridge", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Viewax's Edifice: Sage Item 1", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Viewax's Edifice: Sage Item 2", player),
lambda state: state._hylics2_can_air_dash(player))
# Arcade 1
add_rule(world.get_location("Arcade 1: Key", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Arcade 1: Coin Dash", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Arcade 1: Burrito Alcove 1", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Arcade 1: Burrito Alcove 2", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Arcade 1: Behind Spikes Banana", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Arcade 1: Pyramid Banana", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Arcade 1: Moving Platforms Muscle Applique", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Arcade 1: Bed Banana", player),
lambda state: state._hylics2_has_paddle(player))
# Airship
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
lambda state: state._hylics2_has_worm_room_key(player))
# Foglast
add_rule(world.get_location("Foglast: Underground Sarcophagus", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Foglast: Shielded Key", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Foglast: TV", player),
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_clicker(player))
add_rule(world.get_location("Foglast: Buy Clicker", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Foglast: Shielded Chest", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Foglast: Cave Fridge", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Foglast: Roof Sarcophagus", player),
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 1", player),
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 2", player),
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 3", player),
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("Foglast: Sage Sarcophagus", player),
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("Foglast: Sage Item 1", player),
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("Foglast: Sage Item 2", player),
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
# Drill Castle
add_rule(world.get_location("Drill Castle: Island Banana", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Drill Castle: Island Pot", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Drill Castle: Cave Sarcophagus", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Drill Castle: TV", player),
lambda state: state._hylics2_can_air_dash(player))
# Sage Labyrinth
add_rule(world.get_location("Sage Labyrinth: Sage Item 1", player),
lambda state: state._hylics2_has_deep_key(player))
add_rule(world.get_location("Sage Labyrinth: Sage Item 2", player),
lambda state: state._hylics2_has_deep_key(player))
add_rule(world.get_location("Sage Labyrinth: Sage Left Arm", player),
lambda state: state._hylics2_has_deep_key(player))
add_rule(world.get_location("Sage Labyrinth: Sage Right Arm", player),
lambda state: state._hylics2_has_deep_key(player))
add_rule(world.get_location("Sage Labyrinth: Sage Left Leg", player),
lambda state: state._hylics2_has_deep_key(player))
add_rule(world.get_location("Sage Labyrinth: Sage Right Leg", player),
lambda state: state._hylics2_has_deep_key(player))
# Sage Airship
add_rule(world.get_location("Sage Airship: TV", player),
lambda state: state._hylics2_has_tokens(player))
# Hylemxylem
add_rule(world.get_location("Hylemxylem: Upper Chamber Banana", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Across Upper Reservoir Chest", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Chest", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 1", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 2", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 1", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 2", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 3", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Sarcophagus", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 1", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 2", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 3", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
add_rule(world.get_location("Hylemxylem: Upper Reservoir Hole Key", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
# extra rules if Extra Items in Logic is enabled
if world.extra_items_in_logic[player]:
for i in world.get_region("Foglast", player).entrances:
add_rule(i, lambda state: state._hylics2_has_charge_up(player))
for i in world.get_region("Sage Airship", player).entrances:
add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player) and\
state._hylics2_has_worm_room_key(player))
for i in world.get_region("Hylemxylem", player).entrances:
add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
# extra rules if Shuffle Party Members is enabled
if world.party_shuffle[player]:
for i in world.get_region("Arcade Island", player).entrances:
add_rule(i, lambda state: state._hylics2_has_3_members(player))
for i in world.get_region("Foglast", player).entrances:
add_rule(i, lambda state: state._hylics2_has_3_members(player) or\
(state._hylics2_has_2_members(player) and state._hylics2_has_jail_key(player)))
for i in world.get_region("Sage Airship", player).entrances:
add_rule(i, lambda state: state._hylics2_has_3_members(player))
for i in world.get_region("Hylemxylem", player).entrances:
add_rule(i, lambda state: state._hylics2_has_3_members(player))
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
lambda state: state._hylics2_has_2_members(player))
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
lambda state: state._hylics2_has_2_members(player))
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
lambda state: state._hylics2_has_2_members(player))
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
lambda state: state._hylics2_has_3_members(player))
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
lambda state: state._hylics2_has_3_members(player))
add_rule(world.get_location("New Muldul: Vault Bomb", player),
lambda state: state._hylics2_has_2_members(player))
add_rule(world.get_location("Juice Ranch: Battle with Somsnosa", player),
lambda state: state._hylics2_has_2_members(player))
add_rule(world.get_location("Juice Ranch: Somsnosa Joins", player),
lambda state: state._hylics2_has_2_members(player))
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
lambda state: state._hylics2_has_3_members(player))
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
lambda state: state._hylics2_has_3_members(player))
# extra rules if Shuffle Red Medallions is enabled
if world.medallion_shuffle[player]:
add_rule(world.get_location("New Muldul: Upper House Medallion", player),
lambda state: state._hylics2_has_upper_house_key(player))
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Viewax's Edifice: Jar Medallion", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Viewax's Edifice: Sage Chair Medallion", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Arcade 1: Lonely Medallion", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Arcade 1: Alcove Medallion", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Foglast: Under Lair Medallion", player),
lambda state: state._hylics2_has_bridge_key(player))
add_rule(world.get_location("Foglast: Mid-Air Medallion", player),
lambda state: state._hylics2_can_air_dash(player))
add_rule(world.get_location("Foglast: Top of Tower Medallion", player),
lambda state: state._hylics2_has_paddle(player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Medallion", player),
lambda state: state._hylics2_has_upper_chamber_key(player))
# extra rules is Shuffle Red Medallions and Party Shuffle are enabled
if world.party_shuffle[player] and world.medallion_shuffle[player]:
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
lambda state: state._hylics2_has_jail_key(player))
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
lambda state: state._hylics2_has_jail_key(player))
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
lambda state: state._hylics2_has_jail_key(player))
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
lambda state: state._hylics2_has_jail_key(player))
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
lambda state: state._hylics2_has_jail_key(player))
# entrances
for i in world.get_region("Airship", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Arcade Island", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player) and state._hylics2_can_air_dash(player))
for i in world.get_region("Worm Pod", player).entrances:
add_rule(i, lambda state: state._hylics2_enter_wormpod(player))
for i in world.get_region("Foglast", player).entrances:
add_rule(i, lambda state: state._hylics2_enter_foglast(player))
for i in world.get_region("Sage Labyrinth", player).entrances:
add_rule(i, lambda state: state._hylics2_has_skull_bomb(player))
for i in world.get_region("Sage Airship", player).entrances:
add_rule(i, lambda state: state._hylics2_enter_sageship(player))
for i in world.get_region("Hylemxylem", player).entrances:
add_rule(i, lambda state: state._hylics2_enter_hylemxylem(player))
# random start logic (default)
if ((not world.random_start[player]) or \
(world.random_start[player] and hylics2world.start_location == "Waynehouse")):
# entrances
for i in world.get_region("Viewax", player).entrances:
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
for i in world.get_region("TV Island", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Shield Facility", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Juice Ranch", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
# random start logic (Viewax's Edifice)
elif (world.random_start[player] and hylics2world.start_location == "Viewax's Edifice"):
for i in world.get_region("Waynehouse", player).entrances:
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
for i in world.get_region("New Muldul", player).entrances:
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
for i in world.get_region("New Muldul Vault", player).entrances:
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
for i in world.get_region("Drill Castle", player).entrances:
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
for i in world.get_region("TV Island", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Shield Facility", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Juice Ranch", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Sage Labyrinth", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
# random start logic (TV Island)
elif (world.random_start[player] and hylics2world.start_location == "TV Island"):
for i in world.get_region("Waynehouse", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("New Muldul", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("New Muldul Vault", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Drill Castle", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Viewax", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Shield Facility", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Juice Ranch", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Sage Labyrinth", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
# random start logic (Shield Facility)
elif (world.random_start[player] and hylics2world.start_location == "Shield Facility"):
for i in world.get_region("Waynehouse", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("New Muldul", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("New Muldul Vault", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Drill Castle", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Viewax", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("TV Island", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))
for i in world.get_region("Sage Labyrinth", player).entrances:
add_rule(i, lambda state: state._hylics2_has_airship(player))

246
worlds/hylics2/__init__.py Normal file
View File

@@ -0,0 +1,246 @@
import random
from typing import Dict, Any
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, RegionType
from worlds.generic.Rules import set_rule
from ..AutoWorld import World, WebWorld
from . import Items, Locations, Options, Rules, Exits
class Hylics2Web(WebWorld):
theme = "ocean"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to settings up the Hylics 2 randomizer connected to an Archipelago Multiworld",
"English",
"setup_en.md",
"setup/en",
["TRPG"]
)]
class Hylics2World(World):
"""
Hylics 2 is a surreal and unusual RPG, with a bizarre yet unique visual style. Play as Wayne,
travel the world, and gather your allies to defeat the nefarious Gibby in his Hylemxylem!
"""
game: str = "Hylics 2"
web = Hylics2Web()
all_items = {**Items.item_table, **Items.gesture_item_table, **Items.party_item_table,
**Items.medallion_item_table}
all_locations = {**Locations.location_table, **Locations.tv_location_table, **Locations.party_location_table,
**Locations.medallion_location_table}
item_name_to_id = {data["name"]: item_id for item_id, data in all_items.items()}
location_name_to_id = {data["name"]: loc_id for loc_id, data in all_locations.items()}
option_definitions = Options.hylics2_options
topology_present: bool = True
remote_items: bool = True
remote_start_inventory: bool = True
data_version: 1
start_location = "Waynehouse"
def set_rules(self):
Rules.set_rules(self)
def create_item(self, name: str) -> "Hylics2Item":
item_id: int = self.item_name_to_id[name]
return Hylics2Item(name, self.all_items[item_id]["classification"], item_id, player=self.player)
def add_item(self, name: str, classification: ItemClassification, code: int) -> "Item":
return Hylics2Item(name, classification, code, self.player)
def create_event(self, event: str):
return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player)
# set random starting location if option is enabled
def generate_early(self):
if self.world.random_start[self.player]:
i = self.world.random.randint(0, 3)
if i == 0:
self.start_location = "Waynehouse"
elif i == 1:
self.start_location = "Viewax's Edifice"
elif i == 2:
self.start_location = "TV Island"
elif i == 3:
self.start_location = "Shield Facility"
def generate_basic(self):
# create location for beating the game and place Victory event there
loc = Location(self.player, "Defeat Gibby", None, self.world.get_region("Hylemxylem", self.player))
loc.place_locked_item(self.create_event("Victory"))
set_rule(loc, lambda state: state._hylics2_has_upper_chamber_key(self.player)
and state._hylics2_has_vessel_room_key(self.player))
self.world.get_region("Hylemxylem", self.player).locations.append(loc)
self.world.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
# create item pool
pool = []
# add regular items
for i, data in Items.item_table.items():
if data["count"] > 0:
for j in range(data["count"]):
pool.append(self.add_item(data["name"], data["classification"], i))
# add party members if option is enabled
if self.world.party_shuffle[self.player]:
for i, data in Items.party_item_table.items():
pool.append(self.add_item(data["name"], data["classification"], i))
# handle gesture shuffle options
if self.world.gesture_shuffle[self.player] == 2: # vanilla locations
gestures = Items.gesture_item_table
self.world.get_location("Waynehouse: TV", self.player)\
.place_locked_item(self.add_item(gestures[200678]["name"], gestures[200678]["classification"], 200678))
self.world.get_location("Afterlife: TV", self.player)\
.place_locked_item(self.add_item(gestures[200683]["name"], gestures[200683]["classification"], 200683))
self.world.get_location("New Muldul: TV", self.player)\
.place_locked_item(self.add_item(gestures[200679]["name"], gestures[200679]["classification"], 200679))
self.world.get_location("Viewax's Edifice: TV", self.player)\
.place_locked_item(self.add_item(gestures[200680]["name"], gestures[200680]["classification"], 200680))
self.world.get_location("TV Island: TV", self.player)\
.place_locked_item(self.add_item(gestures[200681]["name"], gestures[200681]["classification"], 200681))
self.world.get_location("Juice Ranch: TV", self.player)\
.place_locked_item(self.add_item(gestures[200682]["name"], gestures[200682]["classification"], 200682))
self.world.get_location("Foglast: TV", self.player)\
.place_locked_item(self.add_item(gestures[200684]["name"], gestures[200684]["classification"], 200684))
self.world.get_location("Drill Castle: TV", self.player)\
.place_locked_item(self.add_item(gestures[200688]["name"], gestures[200688]["classification"], 200688))
self.world.get_location("Sage Airship: TV", self.player)\
.place_locked_item(self.add_item(gestures[200685]["name"], gestures[200685]["classification"], 200685))
elif self.world.gesture_shuffle[self.player] == 1: # TVs only
gestures = list(Items.gesture_item_table.items())
tvs = list(Locations.tv_location_table.items())
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
# placed at Sage Airship: TV
if self.world.extra_items_in_logic[self.player]:
tv = self.world.random.choice(tvs)
gest = gestures.index((200681, Items.gesture_item_table[200681]))
while tv[1]["name"] == "Sage Airship: TV":
tv = self.world.random.choice(tvs)
self.world.get_location(tv[1]["name"], self.player)\
.place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"],
gestures[gest]))
gestures.remove(gestures[gest])
tvs.remove(tv)
for i in range(len(gestures)):
gest = self.world.random.choice(gestures)
tv = self.world.random.choice(tvs)
self.world.get_location(tv[1]["name"], self.player)\
.place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[1]))
gestures.remove(gest)
tvs.remove(tv)
else: # add gestures to pool like normal
for i, data in Items.gesture_item_table.items():
pool.append(self.add_item(data["name"], data["classification"], i))
# add '10 Bones' items if medallion shuffle is enabled
if self.world.medallion_shuffle[self.player]:
for i, data in Items.medallion_item_table.items():
for j in range(data["count"]):
pool.append(self.add_item(data["name"], data["classification"], i))
# add to world's pool
self.world.itempool += pool
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {
"party_shuffle": self.world.party_shuffle[self.player].value,
"medallion_shuffle": self.world.medallion_shuffle[self.player].value,
"random_start" : self.world.random_start[self.player].value,
"start_location" : self.start_location,
"death_link": self.world.death_link[self.player].value
}
return slot_data
def create_regions(self) -> None:
region_table: Dict[int, Region] = {
0: Region("Menu", RegionType.Generic, "Menu", self.player, self.world),
1: Region("Afterlife", RegionType.Generic, "Afterlife", self.player, self.world),
2: Region("Waynehouse", RegionType.Generic, "Waynehouse", self.player, self.world),
3: Region("World", RegionType.Generic, "World", self.player, self.world),
4: Region("New Muldul", RegionType.Generic, "New Muldul", self.player, self.world),
5: Region("New Muldul Vault", RegionType.Generic, "New Muldul Vault", self.player, self.world),
6: Region("Viewax", RegionType.Generic, "Viewax's Edifice", self.player, self.world),
7: Region("Airship", RegionType.Generic, "Airship", self.player, self.world),
8: Region("Arcade Island", RegionType.Generic, "Arcade Island", self.player, self.world),
9: Region("TV Island", RegionType.Generic, "TV Island", self.player, self.world),
10: Region("Juice Ranch", RegionType.Generic, "Juice Ranch", self.player, self.world),
11: Region("Shield Facility", RegionType.Generic, "Shield Facility", self.player, self.world),
12: Region("Worm Pod", RegionType.Generic, "Worm Pod", self.player, self.world),
13: Region("Foglast", RegionType.Generic, "Foglast", self.player, self.world),
14: Region("Drill Castle", RegionType.Generic, "Drill Castle", self.player, self.world),
15: Region("Sage Labyrinth", RegionType.Generic, "Sage Labyrinth", self.player, self.world),
16: Region("Sage Airship", RegionType.Generic, "Sage Airship", self.player, self.world),
17: Region("Hylemxylem", RegionType.Generic, "Hylemxylem", self.player, self.world)
}
# create regions from table
for i, reg in region_table.items():
self.world.regions.append(reg)
# get all exits per region
for j, exits in Exits.region_exit_table.items():
if j == i:
for k in exits:
# create entrance and connect it to parent and destination regions
ent = Entrance(self.player, k, reg)
reg.exits.append(ent)
if k == "New Game" and self.world.random_start[self.player]:
if self.start_location == "Waynehouse":
ent.connect(region_table[2])
elif self.start_location == "Viewax's Edifice":
ent.connect(region_table[6])
elif self.start_location == "TV Island":
ent.connect(region_table[9])
elif self.start_location == "Shield Facility":
ent.connect(region_table[11])
else:
for name, num in Exits.exit_lookup_table.items():
if k == name:
ent.connect(region_table[num])
# add regular locations
for i, data in Locations.location_table.items():
region_table[data["region"]].locations\
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
for i, data in Locations.tv_location_table.items():
region_table[data["region"]].locations\
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
# add party member locations if option is enabled
if self.world.party_shuffle[self.player]:
for i, data in Locations.party_location_table.items():
region_table[data["region"]].locations\
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
# add medallion locations if option is enabled
if self.world.medallion_shuffle[self.player]:
for i, data in Locations.medallion_location_table.items():
region_table[data["region"]].locations\
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
class Hylics2Location(Location):
game: str = "Hylics 2"
class Hylics2Item(Item):
game: str = "Hylics 2"

View File

@@ -0,0 +1,17 @@
# Hylics 2
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
In Hylics 2, all unique items, equipment and skills are randomized. This includes items in chests, items that are freely standing in the world, items recieved from talking to certain characters, gestures learned from TVs, and so on. Items recieved from completing battles are not randomized, with the exception of the Jail Key recieved from defeating Viewax.
## What Hylics 2 items can appear in other players' worlds?
Consumable items, key items, gloves, accessories, and gestures can appear in other players' worlds.
## What does another world's item look like in Hylics 2?
All items retain their original appearance. You won't know if an item belongs to another player until you collect it.

View File

@@ -0,0 +1,35 @@
# Hylics 2 Randomizer Setup Guide
## Required Software
- Hylics 2 from: [Steam](https://store.steampowered.com/app/1286710/Hylics_2/) or [itch.io](https://mason-lindroth.itch.io/hylics-2)
- BepInEx from: [GitHub](https://github.com/BepInEx/BepInEx/releases)
- Archipelago Mod for Hylics 2 from: [GitHub](https://github.com/TRPG0/ArchipelagoHylics2)
## Instructions (Windows)
1. Download and install BepInEx 5 (32-bit, version 5.4.20 or newer) to your Hylics 2 root folder. *Do not use any pre-release versions of BepInEx 6.*
2. Start Hylics 2 once so that BepInEx can create its required configuration files.
3. Download the latest version of ArchipelagoHylics2 from the [Releases](https://github.com/TRPG0/ArchipelagoHylics2/releases) page and extract the contents of the zip file into `BepInEx\plugins`.
4. Start Hylics 2 again. To verify that the mod is working, begin a new game or load a save file.
## Connecting
To connect to an Archipelago server, open the in-game console (default key: `/`) and use the command `/connect [address:port] [name] [password]`. The port and password are both optional - if no port is provided then the default port of 38281 is used.
**Make sure that you have connected to a server at least once before attempting to check any locations.**
## Other Commands
There are a few additional commands that can be used while playing Hylics 2 randomizer:
- `/disconnect` - Disconnect from an Archipelago server.
- `/popups` - Enables or disables in-game messages when an item is found or recieved.
- `/airship` - Resummons the airship at the dock above New Muldul and teleports Wayne to it, in case the player gets stuck. Player must have the DOCK KEY to use this command.
- `/respawn` - Moves Wayne back to the spawn position of the current area in case you get stuck. `/respawn home` will teleport Wayne back to his original starting position.
- `/checked [region]` - States how many locations have been checked in a given region. If no region is given, then the player's location will be used.
- `/deathlink` - Enables or disables DeathLink.
- `/help [command]` - Lists a command, it's description, and it's required arguments (if any). If no command is given, all commands will be displayed.
- `![command]` - Entering any command with an `!` at the beginning allows for remotely sending commands to the server.

View File

@@ -343,6 +343,27 @@ priority_entrance_table = {
}
# These hint texts have more than one entrance, so they are OK for impa's house and potion shop
multi_interior_regions = {
'Kokiri Forest',
'Lake Hylia',
'the Market',
'Kakariko Village',
'Lon Lon Ranch',
}
interior_entrance_bias = {
'Kakariko Village -> Kak Potion Shop Front': 4,
'Kak Backyard -> Kak Potion Shop Back': 4,
'Kakariko Village -> Kak Impas House': 3,
'Kak Impas Ledge -> Kak Impas House Back': 3,
'Goron City -> GC Shop': 2,
'Zoras Domain -> ZD Shop': 2,
'Market Entrance -> Market Guard House': 2,
'ToT Entrance -> Temple of Time': 1,
}
class EntranceShuffleError(Exception):
pass
@@ -500,7 +521,7 @@ def shuffle_random_entrances(ootworld):
delete_target_entrance(remaining_target)
for pool_type, entrance_pool in one_way_entrance_pools.items():
shuffle_entrance_pool(ootworld, entrance_pool, one_way_target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True, retry_count=5)
shuffle_entrance_pool(ootworld, pool_type, entrance_pool, one_way_target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True, retry_count=5)
replaced_entrances = [entrance.replaces for entrance in entrance_pool]
for remaining_target in chain.from_iterable(one_way_target_entrance_pools.values()):
if remaining_target.replaces in replaced_entrances:
@@ -510,7 +531,7 @@ def shuffle_random_entrances(ootworld):
# Shuffle all entrance pools, in order
for pool_type, entrance_pool in entrance_pools.items():
shuffle_entrance_pool(ootworld, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state)
shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True)
# Multiple checks after shuffling to ensure everything is OK
# Check that all entrances hook up correctly
@@ -596,7 +617,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}')
def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20):
def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20):
restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances)
@@ -604,11 +625,11 @@ def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_t
retry_count -= 1
rollbacks = []
try:
shuffle_entrances(ootworld, restrictive_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
shuffle_entrances(ootworld, pool_type+'Rest', restrictive_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
if check_all:
shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
shuffle_entrances(ootworld, pool_type+'Soft', soft_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
else:
shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, set(), all_state, none_state)
shuffle_entrances(ootworld, pool_type+'Soft', soft_entrances, target_entrances, rollbacks, set(), all_state, none_state)
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
for entrance, target in rollbacks:
@@ -621,12 +642,16 @@ def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_t
raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}')
def shuffle_entrances(ootworld, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
ootworld.world.random.shuffle(entrances)
for entrance in entrances:
if entrance.connected_region != None:
continue
ootworld.world.random.shuffle(target_entrances)
# Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems.
# success rate over randomization
if pool_type in {'InteriorSoft', 'MixedSoft'}:
target_entrances.sort(reverse=True, key=lambda entrance: interior_entrance_bias.get(entrance.replaces.name, 0))
for target in target_entrances:
if target.connected_region == None:
continue
@@ -715,25 +740,33 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
# Check if all locations are reachable if not beatable-only or game is not yet complete
if locations_to_ensure_reachable:
if world.accessibility[player].current_key != 'minimal' or not world.can_beat_game(all_state):
for loc in locations_to_ensure_reachable:
if not all_state.can_reach(loc, 'Location', player):
raise EntranceShuffleError(f'{loc} is unreachable')
for loc in locations_to_ensure_reachable:
if not all_state.can_reach(loc, 'Location', player):
raise EntranceShuffleError(f'{loc} is unreachable')
if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
# Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints
potion_front_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
potion_back_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
if potion_front_entrance is not None and potion_back_entrance is not None and not same_hint_area(potion_front_entrance, potion_back_entrance):
potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back):
raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area')
elif (potion_front and not potion_back) or (not potion_front and potion_back):
# Check the hint area and ensure it's one of the ones with more than one entrance
potion_placed_entrance = potion_front if potion_front else potion_back
if get_hint_area(potion_placed_entrance) not in multi_interior_regions:
raise EntranceShuffleError('Kak Potion Shop entrances can never be in the same hint area')
# When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides
if ootworld.shuffle_cows:
impas_front_entrance = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
impas_back_entrance = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
if impas_front_entrance is not None and impas_back_entrance is not None and not same_hint_area(impas_front_entrance, impas_back_entrance):
impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back):
raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area')
elif (impas_front and not impas_back) or (not impas_front and impas_back):
impas_placed_entrance = impas_front if impas_front else impas_back
if get_hint_area(impas_placed_entrance) not in multi_interior_regions:
raise EntranceShuffleError('Kak Impas House entrances can never be in the same hint area')
# Check basic refills, time passing, return to ToT
if (ootworld.shuffle_special_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions) and \
@@ -845,3 +878,4 @@ def delete_target_entrance(target):
if target.parent_region != None:
target.parent_region.exits.remove(target)
target.parent_region = None
del target

View File

@@ -1,7 +1,7 @@
import logging
import threading
import copy
from collections import Counter
from collections import Counter, deque
logger = logging.getLogger("Ocarina of Time")
@@ -412,17 +412,6 @@ class OOTWorld(World):
self.shop_prices[location.name] = int(self.world.random.betavariate(1.5, 2) * 60) * 5
def fill_bosses(self, bossCount=9):
rewardlist = (
'Kokiri Emerald',
'Goron Ruby',
'Zora Sapphire',
'Forest Medallion',
'Fire Medallion',
'Water Medallion',
'Spirit Medallion',
'Shadow Medallion',
'Light Medallion'
)
boss_location_names = (
'Queen Gohma',
'King Dodongo',
@@ -434,7 +423,7 @@ class OOTWorld(World):
'Twinrova',
'Links Pocket'
)
boss_rewards = [self.create_item(reward) for reward in rewardlist]
boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward']
boss_locations = [self.world.get_location(loc, self.player) for loc in boss_location_names]
placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None]
@@ -447,9 +436,8 @@ class OOTWorld(World):
self.world.random.shuffle(prize_locs)
item = prizepool.pop()
loc = prize_locs.pop()
self.world.push_item(loc, item, collect=False)
loc.locked = True
loc.event = True
loc.place_locked_item(item)
self.world.itempool.remove(item)
def create_item(self, name: str):
if name in item_table:
@@ -496,6 +484,10 @@ class OOTWorld(World):
# Generate itempool
generate_itempool(self)
add_dungeon_items(self)
# Add dungeon rewards
rewardlist = sorted(list(self.item_name_groups['rewards']))
self.itempool += map(self.create_item, rewardlist)
junk_pool = get_junk_pool(self)
removed_items = []
# Determine starting items
@@ -621,61 +613,64 @@ class OOTWorld(World):
"Gerudo Training Ground Maze Path Final Chest", "Gerudo Training Ground MQ Ice Arrows Chest",
]
def get_names(items):
for item in items:
yield item.name
# Place/set rules for dungeon items
itempools = {
'dungeon': [],
'overworld': [],
'any_dungeon': [],
'dungeon': set(),
'overworld': set(),
'any_dungeon': set(),
}
any_dungeon_locations = []
for dungeon in self.dungeons:
itempools['dungeon'] = []
itempools['dungeon'] = set()
# Put the dungeon items into their appropriate pools.
# Build in reverse order since we need to fill boss key first and pop() returns the last element
if self.shuffle_mapcompass in itempools:
itempools[self.shuffle_mapcompass].extend(dungeon.dungeon_items)
itempools[self.shuffle_mapcompass].update(get_names(dungeon.dungeon_items))
if self.shuffle_smallkeys in itempools:
itempools[self.shuffle_smallkeys].extend(dungeon.small_keys)
itempools[self.shuffle_smallkeys].update(get_names(dungeon.small_keys))
shufflebk = self.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else self.shuffle_ganon_bosskey
if shufflebk in itempools:
itempools[shufflebk].extend(dungeon.boss_key)
itempools[shufflebk].update(get_names(dungeon.boss_key))
# We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it.
dungeon_locations = [loc for region in dungeon.regions for loc in region.locations
if loc.item is None and (
self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)]
if itempools['dungeon']: # only do this if there's anything to shuffle
for item in itempools['dungeon']:
dungeon_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['dungeon']]
for item in dungeon_itempool:
self.world.itempool.remove(item)
self.world.random.shuffle(dungeon_locations)
fill_restrictive(self.world, self.world.get_all_state(False), dungeon_locations,
itempools['dungeon'], True, True)
dungeon_itempool, True, True)
any_dungeon_locations.extend(dungeon_locations) # adds only the unfilled locations
# Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary
if self.shuffle_fortresskeys == 'any_dungeon':
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'HideoutSmallKey',
self.world.itempool)
itempools['any_dungeon'].extend(fortresskeys)
itempools['any_dungeon'].add('Small Key (Thieves Hideout)')
if itempools['any_dungeon']:
for item in itempools['any_dungeon']:
any_dungeon_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['any_dungeon']]
for item in any_dungeon_itempool:
self.world.itempool.remove(item)
itempools['any_dungeon'].sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0))
any_dungeon_itempool.sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0))
self.world.random.shuffle(any_dungeon_locations)
fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations,
itempools['any_dungeon'], True, True)
any_dungeon_itempool, True, True)
# If anything is overworld-only, fill into local non-dungeon locations
if self.shuffle_fortresskeys == 'overworld':
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'HideoutSmallKey',
self.world.itempool)
itempools['overworld'].extend(fortresskeys)
itempools['overworld'].add('Small Key (Thieves Hideout)')
if itempools['overworld']:
for item in itempools['overworld']:
overworld_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['overworld']]
for item in overworld_itempool:
self.world.itempool.remove(item)
itempools['overworld'].sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0))
overworld_itempool.sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0))
non_dungeon_locations = [loc for loc in self.get_locations() if
not loc.item and loc not in any_dungeon_locations and
(loc.type != 'Shop' or loc.name in self.shop_prices) and
@@ -683,7 +678,7 @@ class OOTWorld(World):
(loc.name not in dungeon_song_locations or self.shuffle_song_items != 'dungeon')]
self.world.random.shuffle(non_dungeon_locations)
fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations,
itempools['overworld'], True, True)
overworld_itempool, True, True)
# Place songs
# 5 built-in retries because this section can fail sometimes
@@ -805,6 +800,10 @@ class OOTWorld(World):
or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
loc.address = None
# Handle item-linked dungeon items and songs
def stage_pre_fill(cls):
pass
def generate_output(self, output_directory: str):
if self.hints != 'none':
self.hint_data_available.wait()
@@ -831,18 +830,25 @@ class OOTWorld(World):
# Write entrances to spoiler log
all_entrances = self.get_shuffled_entrances()
all_entrances.sort(key=lambda x: x.name)
all_entrances.sort(key=lambda x: x.type)
all_entrances.sort(reverse=True, key=lambda x: x.name)
all_entrances.sort(reverse=True, key=lambda x: x.type)
if not self.decouple_entrances:
for loadzone in all_entrances:
if loadzone.primary:
entrance = loadzone
while all_entrances:
loadzone = all_entrances.pop()
if loadzone.type != 'Overworld':
if loadzone.primary:
entrance = loadzone
else:
entrance = loadzone.reverse
if entrance.reverse is not None:
self.world.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
else:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
else:
entrance = loadzone.reverse
if entrance.reverse is not None:
self.world.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
else:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
reverse = loadzone.replaces.reverse
if reverse in all_entrances:
all_entrances.remove(reverse)
self.world.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
else:
for entrance in all_entrances:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
@@ -1027,7 +1033,7 @@ class OOTWorld(World):
all_state = self.world.get_all_state(use_cache=False)
# Remove event progression items
for item, player in all_state.prog_items:
if (item not in item_table or item_table[item][2] is None) and player == self.player:
if player == self.player and (item not in item_table or (item_table[item][2] is None and item_table[item][0] != 'DungeonReward')):
all_state.prog_items[(item, player)] = 0
# Remove all events and checked locations
all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player}

152
worlds/overcooked2/Items.py Normal file
View File

@@ -0,0 +1,152 @@
from BaseClasses import Item
from typing import NamedTuple, Dict
class ItemData(NamedTuple):
code: int
class Overcooked2Item(Item):
game: str = "Overcooked! 2"
oc2_base_id = 213700
item_table: Dict[str, ItemData] = {
"Wood" : ItemData(oc2_base_id + 1 ),
"Coal Bucket" : ItemData(oc2_base_id + 2 ),
"Spare Plate" : ItemData(oc2_base_id + 3 ),
"Fire Extinguisher" : ItemData(oc2_base_id + 4 ),
"Bellows" : ItemData(oc2_base_id + 5 ),
"Clean Dishes" : ItemData(oc2_base_id + 6 ),
"Larger Tip Jar" : ItemData(oc2_base_id + 7 ),
"Progressive Dash" : ItemData(oc2_base_id + 8 ),
"Progressive Throw/Catch" : ItemData(oc2_base_id + 9 ),
"Coin Purse" : ItemData(oc2_base_id + 10),
"Control Stick Batteries" : ItemData(oc2_base_id + 11),
"Wok Wheels" : ItemData(oc2_base_id + 12),
"Dish Scrubber" : ItemData(oc2_base_id + 13),
"Burn Leniency" : ItemData(oc2_base_id + 14),
"Sharp Knife" : ItemData(oc2_base_id + 15),
"Order Lookahead" : ItemData(oc2_base_id + 16),
"Lightweight Backpack" : ItemData(oc2_base_id + 17),
"Faster Respawn Time" : ItemData(oc2_base_id + 18),
"Faster Condiment/Drink Switch" : ItemData(oc2_base_id + 19),
"Guest Patience" : ItemData(oc2_base_id + 20),
"Kevin-1" : ItemData(oc2_base_id + 21),
"Kevin-2" : ItemData(oc2_base_id + 22),
"Kevin-3" : ItemData(oc2_base_id + 23),
"Kevin-4" : ItemData(oc2_base_id + 24),
"Kevin-5" : ItemData(oc2_base_id + 25),
"Kevin-6" : ItemData(oc2_base_id + 26),
"Kevin-7" : ItemData(oc2_base_id + 27),
"Kevin-8" : ItemData(oc2_base_id + 28),
"Cooking Emote" : ItemData(oc2_base_id + 29),
"Curse Emote" : ItemData(oc2_base_id + 30),
"Serving Emote" : ItemData(oc2_base_id + 31),
"Preparing Emote" : ItemData(oc2_base_id + 32),
"Washing Up Emote" : ItemData(oc2_base_id + 33),
"Ok Emote" : ItemData(oc2_base_id + 34),
"Ramp Button" : ItemData(oc2_base_id + 35),
"Bonus Star" : ItemData(oc2_base_id + 36),
"Calmer Unbread" : ItemData(oc2_base_id + 37),
}
item_frequencies = {
"Progressive Throw/Catch": 2,
"Larger Tip Jar": 2,
"Order Lookahead": 2,
"Progressive Dash": 2,
"Bonus Star": 0, # Filler Item
# default: 1
}
item_name_to_config_name = {
"Wood" : "DisableWood" ,
"Coal Bucket" : "DisableCoal" ,
"Spare Plate" : "DisableOnePlate" ,
"Fire Extinguisher" : "DisableFireExtinguisher" ,
"Bellows" : "DisableBellows" ,
"Clean Dishes" : "PlatesStartDirty" ,
"Control Stick Batteries" : "DisableControlStick" ,
"Wok Wheels" : "DisableWokDrag" ,
"Dish Scrubber" : "WashTimeMultiplier" ,
"Burn Leniency" : "BurnSpeedMultiplier" ,
"Sharp Knife" : "ChoppingTimeScale" ,
"Lightweight Backpack" : "BackpackMovementScale" ,
"Faster Respawn Time" : "RespawnTime" ,
"Faster Condiment/Drink Switch": "CarnivalDispenserRefactoryTime",
"Guest Patience" : "CustomOrderLifetime" ,
"Ramp Button" : "DisableRampButton" ,
"Calmer Unbread" : "AggressiveHorde" ,
"Coin Purse" : "DisableEarnHordeMoney" ,
}
vanilla_values = {
"DisableWood": False,
"DisableCoal": False,
"DisableOnePlate": False,
"DisableFireExtinguisher": False,
"DisableBellows": False,
"PlatesStartDirty": False,
"DisableControlStick": False,
"DisableWokDrag": False,
"DisableRampButton": False,
"WashTimeMultiplier": 1.0,
"BurnSpeedMultiplier": 1.0,
"ChoppingTimeScale": 1.0,
"BackpackMovementScale": 1.0,
"RespawnTime": 5.0,
"CarnivalDispenserRefactoryTime": 0.0,
"CustomOrderLifetime": 100.0,
"AggressiveHorde": False,
"DisableEarnHordeMoney": False,
}
item_id_to_name: Dict[int, str] = {
data.code: item_name for item_name, data in item_table.items() if data.code
}
item_name_to_id: Dict[str, int] = {
item_name: data.code for item_name, data in item_table.items() if data.code
}
def is_progression(item_name: str) -> bool:
return not item_name.endswith("Emote")
def item_to_unlock_event(item_name: str) -> Dict[str, str]:
message = f"{item_name} Acquired!"
action = ""
payload = ""
if item_name.startswith("Kevin"):
kevin_num = int(item_name.split("-")[-1])
action = "UNLOCK_LEVEL"
payload = str(kevin_num + 36)
elif "Emote" in item_name:
action = "UNLOCK_EMOTE"
payload = str(item_table[item_name].code - item_table["Cooking Emote"].code)
elif item_name == "Larger Tip Jar":
action = "INC_TIP_COMBO"
elif item_name == "Order Lookahead":
action = "INC_ORDERS_ON_SCREEN"
elif item_name == "Bonus Star":
action = "INC_STAR_COUNT"
payload = "1"
elif item_name == "Progressive Dash":
action = "INC_DASH"
elif item_name == "Progressive Throw/Catch":
action = "INC_THROW"
else:
config_name = item_name_to_config_name[item_name]
vanilla_value = vanilla_values[config_name]
action = "SET_VALUE"
payload = f"{config_name}={vanilla_value}"
return {
"message": message,
"action": action,
"payload": payload,
}

View File

@@ -0,0 +1,15 @@
from BaseClasses import Location
from .Overcooked2Levels import Overcooked2Level
class Overcooked2Location(Location):
game: str = "Overcooked! 2"
oc2_location_name_to_id = dict()
oc2_location_id_to_name = dict()
for level in Overcooked2Level():
if level.level_id == 36:
continue # level 6-6 does not have an item location
oc2_location_name_to_id[level.location_name_item] = level.level_id
oc2_location_id_to_name[level.level_id] = level.location_name_item

3899
worlds/overcooked2/Logic.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
from typing import TypedDict
from Options import DefaultOnToggle, Range, Choice
class OC2OnToggle(DefaultOnToggle):
@property
def result(self) -> bool:
return bool(self.value)
class AlwaysServeOldestOrder(OC2OnToggle):
"""Modifies the game so that serving an expired order doesn't target the ticket with the highest tip. This helps players dig out of a broken tip combo faster."""
display_name = "Always Serve Oldest Order"
class AlwaysPreserveCookingProgress(OC2OnToggle):
"""Modifies the game to behave more like AYCE, where adding an item to an in-progress container doesn't reset the entire progress bar."""
display_name = "Preserve Cooking/Mixing Progress"
class DisplayLeaderboardScores(OC2OnToggle):
"""Modifies the Overworld map to fetch and display the current world records for each level. Press number keys 1-4 to view leaderboard scores for that number of players."""
display_name = "Display Leaderboard Scores"
class ShuffleLevelOrder(OC2OnToggle):
"""Shuffles the order of kitchens on the overworld map. Also draws from DLC maps."""
display_name = "Shuffle Level Order"
class IncludeHordeLevels(OC2OnToggle):
"""Includes "Horde Defence" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds two horde-specific items into the item pool."""
display_name = "Include Horde Levels"
class KevinLevels(OC2OnToggle):
"""Includes the 8 Kevin level locations on the map as unlockables. Turn off to make games shorter."""
display_name = "Kevin Level Checks"
class FixBugs(OC2OnToggle):
"""Fixes Bugs Present in the base game:
- Double Serving Exploit
- Sink Bug
- Control Stick Cancel/Throw Bug
- Can't Throw Near Empty Burner Bug"""
display_name = "Fix Bugs"
class ShorterLevelDuration(OC2OnToggle):
"""Modifies level duration to be about 1/3rd shorter than in the original game, thus bringing the item discovery pace in line with other popular Archipelago games.
Points required to earn stars are scaled accordingly. ("Boss Levels" which change scenery mid-game are not affected.)"""
display_name = "Shorter Level Duration"
class PrepLevels(Choice):
"""Choose How "Prep Levels" are handled (levels where the timer does not start until the first order is served):
- Original: Prep Levels may appear
- Excluded: Prep Levels are excluded from the pool during level shuffling
- All You Can Eat: Prep Levels may appear, but the timer automatically starts. The star score requirements are also adjusted to use the All You Can Eat World Record (if it exists)"""
auto_display_name = True
display_name = "Prep Level Behavior"
option_original = 0
option_excluded = 1
option_all_you_can_eat = 2
default = 1
class StarsToWin(Range):
"""Number of stars required to unlock 6-6.
Level purchase requirements between 1-1 and 6-6 will be spread between these two numbers. Using too high of a number may result in more frequent generation failures, especially when horde levels are enabled."""
display_name = "Stars to Win"
range_start = 0
range_end = 100
default = 66
class StarThresholdScale(Range):
"""How difficult should the third star for each level be on a scale of 1-100%, where 100% is the current world record score and 45% is the average vanilla 4-star score."""
display_name = "Star Difficulty %"
range_start = 1
range_end = 100
default = 45
overcooked_options = {
# randomization options
"shuffle_level_order": ShuffleLevelOrder,
"include_horde_levels": IncludeHordeLevels,
"prep_levels": PrepLevels,
"kevin_levels": KevinLevels,
# quality of life options
"fix_bugs": FixBugs,
"shorter_level_duration": ShorterLevelDuration,
"always_preserve_cooking_progress": AlwaysPreserveCookingProgress,
"always_serve_oldest_order": AlwaysServeOldestOrder,
"display_leaderboard_scores": DisplayLeaderboardScores,
# difficulty settings
"stars_to_win": StarsToWin,
"star_threshold_scale": StarThresholdScale,
}
OC2Options = TypedDict("OC2Options", {option.__name__: option for option in overcooked_options.values()})

View File

@@ -0,0 +1,349 @@
from enum import Enum
from typing import List
class Overcooked2Dlc(Enum):
STORY = "Story"
SURF_N_TURF = "Surf 'n' Turf"
CAMPFIRE_COOK_OFF = "Campfire Cook Off"
NIGHT_OF_THE_HANGRY_HORDE = "Night of the Hangry Horde"
CARNIVAL_OF_CHAOS = "Carnival of Chaos"
SEASONAL = "Seasonal"
# CHRISTMAS = "Christmas"
# CHINESE_NEW_YEAR = "Chinese New Year"
# WINTER_WONDERLAND = "Winter Wonderland"
# MOON_HARVEST = "Moon Harvest"
# SPRING_FRESTIVAL = "Spring Festival"
# SUNS_OUT_BUNS_OUT = "Sun's Out Buns Out"
def __int__(self) -> int:
if self == Overcooked2Dlc.STORY:
return 0
if self == Overcooked2Dlc.SURF_N_TURF:
return 1
if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF:
return 2
if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE:
return 3
if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS:
return 4
if self == Overcooked2Dlc.SEASONAL:
return 5
assert False
# inclusive
def start_level_id(self) -> int:
if self == Overcooked2Dlc.STORY:
return 1
return 0
# exclusive
def end_level_id(self) -> int:
id = None
if self == Overcooked2Dlc.STORY:
id = 6*6 + 8 # world_count*level_count + kevin count
if self == Overcooked2Dlc.SURF_N_TURF:
id = 3*4 + 1
if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF:
id = 3*4 + 3
if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE:
id = 3*3 + 3 + 8
if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS:
id = 3*4 + 3
if self == Overcooked2Dlc.SEASONAL:
id = 31
return self.start_level_id() + id
# Tutorial + Horde Levels + Endgame
def excluded_levels(self) -> List[int]:
if self == Overcooked2Dlc.STORY:
return [0, 36]
return []
def horde_levels(self) -> List[int]:
if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE:
return [12, 13, 14, 15, 16, 17, 18, 19]
if self == Overcooked2Dlc.SEASONAL:
return [13, 15]
return []
def prep_levels(self) -> List[int]:
if self == Overcooked2Dlc.STORY:
return [1, 2, 5, 10, 12, 13, 28, 31]
if self == Overcooked2Dlc.SURF_N_TURF:
return [0, 4]
if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF:
return [0, 2, 4, 9]
if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE:
return [0, 1, 4]
if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS:
return [0, 1, 3, 4, 5]
if self == Overcooked2Dlc.SEASONAL:
# moon 1-1 is a prep level for 1P only, but we can't make that assumption here
return [0, 1, 5, 6, 12, 14, 16, 17, 18, 22, 23, 24, 27, 29]
return []
class Overcooked2GameWorld(Enum):
ONE = 1
TWO = 2
THREE = 3
FOUR = 4
FIVE = 5
SIX = 6
KEVIN = 7
@property
def as_str(self) -> str:
if self == Overcooked2GameWorld.KEVIN:
return "Kevin"
return str(int(self.value))
@property
def sublevel_count(self) -> int:
if self == Overcooked2GameWorld.KEVIN:
return 8
return 6
@property
def base_id(self) -> int:
if self == Overcooked2GameWorld.ONE:
return 1
prev = Overcooked2GameWorld(self.value - 1)
return prev.base_id + prev.sublevel_count
@property
def name(self) -> str:
if self == Overcooked2GameWorld.KEVIN:
return "Kevin"
return "World " + self.as_str
class Overcooked2GenericLevel():
dlc: Overcooked2Dlc
level_id: int
def __init__(self, level_id: int, dlc: Overcooked2Dlc = Overcooked2Dlc("Story")):
self.dlc = dlc
self.level_id = level_id
def __str__(self) -> str:
return f"{self.dlc.value}|{self.level_id}"
def __repr__(self) -> str:
return f"{self}"
@property
def shortname(self) -> str:
return level_id_to_shortname[(self.dlc, self.level_id)]
@property
def is_horde(self) -> bool:
return self.level_id in self.dlc.horde_levels()
class Overcooked2Level:
"""
Abstraction for a playable levels in Overcooked 2. By default constructor
it can be used as an iterator for all locations in the Story map.
"""
world: Overcooked2GameWorld
sublevel: int
def __init__(self):
self.world = Overcooked2GameWorld.ONE
self.sublevel = 0
def __iter__(self):
return self
def __next__(self):
self.sublevel += 1
if self.sublevel > self.world.sublevel_count:
if self.world == Overcooked2GameWorld.KEVIN:
raise StopIteration
self.world = Overcooked2GameWorld(self.world.value + 1)
self.sublevel = 1
return self
@property
def level_id(self) -> int:
return self.world.base_id + (self.sublevel - 1)
@property
def level_name(self) -> str:
return self.world.as_str + "-" + str(self.sublevel)
@property
def location_name_item(self) -> str:
return self.level_name + " Completed"
@property
def location_name_level_complete(self) -> str:
return self.level_name + " Level Completed"
@property
def event_name_level_complete(self) -> str:
return self.level_name + " Level Complete"
def location_name_star_event(self, stars: int) -> str:
return "%s (%d-Star)" % (self.level_name, stars)
@property
def as_generic_level(self) -> Overcooked2GenericLevel:
return Overcooked2GenericLevel(self.level_id)
# Note that there are valid levels beyond what is listed here, but they are all
# Onion King Dialogs
level_id_to_shortname = {
(Overcooked2Dlc.STORY , 0 ): "Tutorial" ,
(Overcooked2Dlc.STORY , 1 ): "Story 1-1" ,
(Overcooked2Dlc.STORY , 2 ): "Story 1-2" ,
(Overcooked2Dlc.STORY , 3 ): "Story 1-3" ,
(Overcooked2Dlc.STORY , 4 ): "Story 1-4" ,
(Overcooked2Dlc.STORY , 5 ): "Story 1-5" ,
(Overcooked2Dlc.STORY , 6 ): "Story 1-6" ,
(Overcooked2Dlc.STORY , 7 ): "Story 2-1" ,
(Overcooked2Dlc.STORY , 8 ): "Story 2-2" ,
(Overcooked2Dlc.STORY , 9 ): "Story 2-3" ,
(Overcooked2Dlc.STORY , 10 ): "Story 2-4" ,
(Overcooked2Dlc.STORY , 11 ): "Story 2-5" ,
(Overcooked2Dlc.STORY , 12 ): "Story 2-6" ,
(Overcooked2Dlc.STORY , 13 ): "Story 3-1" ,
(Overcooked2Dlc.STORY , 14 ): "Story 3-2" ,
(Overcooked2Dlc.STORY , 15 ): "Story 3-3" ,
(Overcooked2Dlc.STORY , 16 ): "Story 3-4" ,
(Overcooked2Dlc.STORY , 17 ): "Story 3-5" ,
(Overcooked2Dlc.STORY , 18 ): "Story 3-6" ,
(Overcooked2Dlc.STORY , 19 ): "Story 4-1" ,
(Overcooked2Dlc.STORY , 20 ): "Story 4-2" ,
(Overcooked2Dlc.STORY , 21 ): "Story 4-3" ,
(Overcooked2Dlc.STORY , 22 ): "Story 4-4" ,
(Overcooked2Dlc.STORY , 23 ): "Story 4-5" ,
(Overcooked2Dlc.STORY , 24 ): "Story 4-6" ,
(Overcooked2Dlc.STORY , 25 ): "Story 5-1" ,
(Overcooked2Dlc.STORY , 26 ): "Story 5-2" ,
(Overcooked2Dlc.STORY , 27 ): "Story 5-3" ,
(Overcooked2Dlc.STORY , 28 ): "Story 5-4" ,
(Overcooked2Dlc.STORY , 29 ): "Story 5-5" ,
(Overcooked2Dlc.STORY , 30 ): "Story 5-6" ,
(Overcooked2Dlc.STORY , 31 ): "Story 6-1" ,
(Overcooked2Dlc.STORY , 32 ): "Story 6-2" ,
(Overcooked2Dlc.STORY , 33 ): "Story 6-3" ,
(Overcooked2Dlc.STORY , 34 ): "Story 6-4" ,
(Overcooked2Dlc.STORY , 35 ): "Story 6-5" ,
(Overcooked2Dlc.STORY , 36 ): "Story 6-6" ,
(Overcooked2Dlc.STORY , 37 ): "Story K-1" ,
(Overcooked2Dlc.STORY , 38 ): "Story K-2" ,
(Overcooked2Dlc.STORY , 39 ): "Story K-3" ,
(Overcooked2Dlc.STORY , 40 ): "Story K-4" ,
(Overcooked2Dlc.STORY , 41 ): "Story K-5" ,
(Overcooked2Dlc.STORY , 42 ): "Story K-6" ,
(Overcooked2Dlc.STORY , 43 ): "Story K-7" ,
(Overcooked2Dlc.STORY , 44 ): "Story K-8" ,
(Overcooked2Dlc.SURF_N_TURF , 0 ): "Surf 1-1" ,
(Overcooked2Dlc.SURF_N_TURF , 1 ): "Surf 1-2" ,
(Overcooked2Dlc.SURF_N_TURF , 2 ): "Surf 1-3" ,
(Overcooked2Dlc.SURF_N_TURF , 3 ): "Surf 1-4" ,
(Overcooked2Dlc.SURF_N_TURF , 4 ): "Surf 2-1" ,
(Overcooked2Dlc.SURF_N_TURF , 5 ): "Surf 2-2" ,
(Overcooked2Dlc.SURF_N_TURF , 6 ): "Surf 2-3" ,
(Overcooked2Dlc.SURF_N_TURF , 7 ): "Surf 2-4" ,
(Overcooked2Dlc.SURF_N_TURF , 8 ): "Surf 3-1" ,
(Overcooked2Dlc.SURF_N_TURF , 9 ): "Surf 3-2" ,
(Overcooked2Dlc.SURF_N_TURF , 10 ): "Surf 3-3" ,
(Overcooked2Dlc.SURF_N_TURF , 11 ): "Surf 3-4" ,
(Overcooked2Dlc.SURF_N_TURF , 12 ): "Surf K-1" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 0 ): "Campfire 1-1" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 1 ): "Campfire 1-2" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 2 ): "Campfire 1-3" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 3 ): "Campfire 1-4" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 4 ): "Campfire 2-1" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 5 ): "Campfire 2-2" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 6 ): "Campfire 2-3" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 7 ): "Campfire 2-4" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 8 ): "Campfire 3-1" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 9 ): "Campfire 3-2" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 10 ): "Campfire 3-3" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 11 ): "Campfire 3-4" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 12 ): "Campfire K-1" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 13 ): "Campfire K-2" ,
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 14 ): "Campfire K-3" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 0 ): "Carnival 1-1" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 1 ): "Carnival 1-2" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 2 ): "Carnival 1-3" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 3 ): "Carnival 1-4" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 4 ): "Carnival 2-1" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 5 ): "Carnival 2-2" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 6 ): "Carnival 2-3" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 7 ): "Carnival 2-4" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 8 ): "Carnival 3-1" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 9 ): "Carnival 3-2" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 10 ): "Carnival 3-3" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 11 ): "Carnival 3-4" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 12 ): "Carnival K-1" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 13 ): "Carnival K-2" ,
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 14 ): "Carnival K-3" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 0 ): "Horde 1-1" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 1 ): "Horde 1-2" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 2 ): "Horde 1-3" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 3 ): "Horde 2-1" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 4 ): "Horde 2-2" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 5 ): "Horde 2-3" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 6 ): "Horde 3-1" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 7 ): "Horde 3-2" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 8 ): "Horde 3-3" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 9 ): "Horde K-1" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 10 ): "Horde K-2" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 11 ): "Horde K-3" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 12 ): "Horde H-1" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 13 ): "Horde H-2" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 14 ): "Horde H-3" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 15 ): "Horde H-4" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 16 ): "Horde H-5" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 17 ): "Horde H-6" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 18 ): "Horde H-7" ,
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 19 ): "Horde H-8" ,
(Overcooked2Dlc.SEASONAL , 0 ): "Christmas 1-1" ,
(Overcooked2Dlc.SEASONAL , 1 ): "Christmas 1-2" ,
(Overcooked2Dlc.SEASONAL , 2 ): "Christmas 1-3" ,
(Overcooked2Dlc.SEASONAL , 3 ): "Christmas 1-4" ,
(Overcooked2Dlc.SEASONAL , 4 ): "Christmas 1-5" ,
(Overcooked2Dlc.SEASONAL , 5 ): "Chinese 1-1" ,
(Overcooked2Dlc.SEASONAL , 6 ): "Chinese 1-2" ,
(Overcooked2Dlc.SEASONAL , 7 ): "Chinese 1-3" ,
(Overcooked2Dlc.SEASONAL , 8 ): "Chinese 1-4" ,
(Overcooked2Dlc.SEASONAL , 9 ): "Chinese 1-5" ,
(Overcooked2Dlc.SEASONAL , 10 ): "Chinese 1-6" ,
(Overcooked2Dlc.SEASONAL , 11 ): "Chinese 1-7" ,
(Overcooked2Dlc.SEASONAL , 12 ): "Winter 1-1" ,
(Overcooked2Dlc.SEASONAL , 13 ): "Winter H-2" ,
(Overcooked2Dlc.SEASONAL , 14 ): "Winter 1-3" ,
(Overcooked2Dlc.SEASONAL , 15 ): "Winter H-4" ,
(Overcooked2Dlc.SEASONAL , 16 ): "Winter 1-5" ,
(Overcooked2Dlc.SEASONAL , 17 ): "Spring 1-1" ,
(Overcooked2Dlc.SEASONAL , 18 ): "Spring 1-2" ,
(Overcooked2Dlc.SEASONAL , 19 ): "Spring 1-3" ,
(Overcooked2Dlc.SEASONAL , 20 ): "Spring 1-4" ,
(Overcooked2Dlc.SEASONAL , 21 ): "Spring 1-5" ,
(Overcooked2Dlc.SEASONAL , 22 ): "SOBO 1-1" ,
(Overcooked2Dlc.SEASONAL , 23 ): "SOBO 1-2" ,
(Overcooked2Dlc.SEASONAL , 24 ): "SOBO 1-3" ,
(Overcooked2Dlc.SEASONAL , 25 ): "SOBO 1-4" ,
(Overcooked2Dlc.SEASONAL , 26 ): "SOBO 1-5" ,
(Overcooked2Dlc.SEASONAL , 27 ): "Moon 1-1" ,
(Overcooked2Dlc.SEASONAL , 28 ): "Moon 1-2" ,
(Overcooked2Dlc.SEASONAL , 29 ): "Moon 1-3" ,
(Overcooked2Dlc.SEASONAL , 30 ): "Moon 1-4" ,
(Overcooked2Dlc.SEASONAL , 31 ): "Moon 1-5" ,
}

View File

@@ -0,0 +1,510 @@
from enum import Enum
from typing import Callable, Dict, Any, List, Optional
from BaseClasses import ItemClassification, CollectionState, Region, Entrance, Location, RegionType, Tutorial
from worlds.AutoWorld import World, WebWorld
from .Overcooked2Levels import Overcooked2Level, Overcooked2GenericLevel
from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name
from .Options import overcooked_options, OC2Options, OC2OnToggle
from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies
from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful
class Overcooked2Web(WebWorld):
theme = "partyTime"
bug_report_page = "https://github.com/toasterparty/oc2-modding/issues"
setup_en = Tutorial(
"Multiworld Setup Tutorial",
"A guide to setting up the Overcooked! 2 randomizer on your computer.",
"English",
"setup_en.md",
"setup/en",
["toasterparty"]
)
tutorials = [setup_en]
class PrepLevelMode(Enum):
original = 0
excluded = 1
ayce = 2
class Overcooked2World(World):
"""
Overcooked! 2 is a franticly paced arcade cooking game where
players race against the clock to complete orders for points. Bring
peace to the Onion Kingdom once again by recovering lost items and abilities,
earning stars to unlock levels, and defeating the unbread horde. Levels are
randomized to increase gameplay variety. Play with up to 4 friends.
"""
# Autoworld API
game = "Overcooked! 2"
web = Overcooked2Web()
required_client_version = (0, 3, 4)
option_definitions = overcooked_options
topology_present: bool = False
remote_items: bool = True
remote_start_inventory: bool = False
data_version = 2
item_name_to_id = item_name_to_id
item_id_to_name = item_id_to_name
location_id_to_name = oc2_location_id_to_name
location_name_to_id = oc2_location_name_to_id
options: Dict[str, Any]
itempool: List[Overcooked2Item]
# Helper Functions
def is_level_horde(self, level_id: int) -> bool:
return self.options["IncludeHordeLevels"] and \
(self.level_mapping is not None) and \
level_id in self.level_mapping.keys() and \
self.level_mapping[level_id].is_horde
def create_item(self, item: str, classification: ItemClassification = ItemClassification.progression) -> Overcooked2Item:
return Overcooked2Item(item, classification, self.item_name_to_id[item], self.player)
def create_event(self, event: str, classification: ItemClassification) -> Overcooked2Item:
return Overcooked2Item(event, classification, None, self.player)
def place_event(self, location_name: str, item_name: str,
classification: ItemClassification = ItemClassification.progression_skip_balancing):
location: Location = self.world.get_location(location_name, self.player)
location.place_locked_item(self.create_event(item_name, classification))
def add_region(self, region_name: str):
region = Region(
region_name,
RegionType.Generic,
region_name,
self.player,
self.world,
)
self.world.regions.append(region)
def connect_regions(self, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None):
sourceRegion = self.world.get_region(source, self.player)
targetRegion = self.world.get_region(target, self.player)
connection = Entrance(self.player, '', sourceRegion)
if rule:
connection.access_rule = rule
sourceRegion.exits.append(connection)
connection.connect(targetRegion)
def add_level_location(
self,
region_name: str,
location_name: str,
level_id: int,
stars: int,
is_event: bool = False,
) -> None:
if is_event:
location_id = None
else:
location_id = level_id
region = self.world.get_region(region_name, self.player)
location = Overcooked2Location(
self.player,
location_name,
location_id,
region,
)
location.event = is_event
# if level_id is none, then it's the 6-6 edge case
if level_id is None:
level_id = 36
if self.level_mapping is not None and level_id in self.level_mapping:
level = self.level_mapping[level_id]
else:
level = Overcooked2GenericLevel(level_id)
completion_condition: Callable[[CollectionState], bool] = \
lambda state, level=level, stars=stars: \
has_requirements_for_level_star(state, level, stars, self.player)
location.access_rule = completion_condition
region.locations.append(
location
)
def get_options(self) -> Dict[str, Any]:
return OC2Options({option.__name__: getattr(self.world, name)[self.player].result
if issubclass(option, OC2OnToggle) else getattr(self.world, name)[self.player].value
for name, option in overcooked_options.items()})
# Helper Data
level_unlock_counts: Dict[int, int] # level_id, stars to purchase
level_mapping: Dict[int, Overcooked2GenericLevel] # level_id, level
# Autoworld Hooks
def generate_early(self):
self.options = self.get_options()
# 0.0 to 1.0 where 1.0 is World Record
self.star_threshold_scale = self.options["StarThresholdScale"] / 100.0
# Generate level unlock requirements such that the levels get harder to unlock
# the further the game has progressed, and levels progress radially rather than linearly
self.level_unlock_counts = level_unlock_requirement_factory(self.options["StarsToWin"])
# Assign new kitchens to each spot on the overworld using pure random chance and nothing else
if self.options["ShuffleLevelOrder"]:
self.level_mapping = \
level_shuffle_factory(
self.world.random,
self.options["PrepLevels"] != PrepLevelMode.excluded.value,
self.options["IncludeHordeLevels"],
)
else:
self.level_mapping = None
def create_regions(self) -> None:
# Menu -> Overworld
self.add_region("Menu")
self.add_region("Overworld")
self.connect_regions("Menu", "Overworld")
for level in Overcooked2Level():
if not self.options["KevinLevels"] and level.level_id > 36:
break
# Create Region (e.g. "1-1")
self.add_region(level.level_name)
# Add Location to house progression item (1-star)
if level.level_id == 36:
# 6-6 doesn't have progression, but it does have victory condition which is placed later
self.add_level_location(
level.level_name,
level.location_name_item,
None,
1,
)
else:
# Location to house progression item
self.add_level_location(
level.level_name,
level.location_name_item,
level.level_id,
1,
)
# Location to house level completed event
self.add_level_location(
level.level_name,
level.location_name_level_complete,
level.level_id,
1,
is_event=True,
)
# Add Locations to house star aquisition events, except for horde levels
if not self.is_level_horde(level.level_id):
for n in [1, 2, 3]:
self.add_level_location(
level.level_name,
level.location_name_star_event(n),
level.level_id,
n,
is_event=True,
)
# Overworld -> Level
required_star_count: int = self.level_unlock_counts[level.level_id]
if level.level_id % 6 != 1 and level.level_id <= 36:
previous_level_completed_event_name: str = Overcooked2GenericLevel(
level.level_id - 1).shortname.split(" ")[1] + " Level Complete"
else:
previous_level_completed_event_name = None
level_access_rule: Callable[[CollectionState], bool] = \
lambda state, level_name=level.level_name, previous_level_completed_event_name=previous_level_completed_event_name, required_star_count=required_star_count: \
has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.player)
self.connect_regions("Overworld", level.level_name, level_access_rule)
# Level --> Overworld
self.connect_regions(level.level_name, "Overworld")
completion_condition: Callable[[CollectionState], bool] = lambda state: \
state.has("Victory", self.player)
self.world.completion_condition[self.player] = completion_condition
def create_items(self):
self.itempool = []
# Make Items
# useful = list()
# filler = list()
# progression = list()
for item_name in item_table:
if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]:
# skip items which are irrelevant to the seed
continue
if not self.options["KevinLevels"] and item_name.startswith("Kevin"):
continue
if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]):
# print(f"{item_name} is progression")
# progression.append(item_name)
classification = ItemClassification.progression
else:
# print(f"{item_name} is filler")
if (is_useful(item_name)):
# useful.append(item_name)
classification = ItemClassification.useful
else:
# filler.append(item_name)
classification = ItemClassification.filler
if item_name in item_frequencies:
freq = item_frequencies[item_name]
while freq > 0:
self.itempool.append(self.create_item(item_name, classification))
classification = ItemClassification.useful # only the first progressive item can be progression
freq -= 1
else:
self.itempool.append(self.create_item(item_name, classification))
# print(f"progression: {progression}")
# print(f"useful: {useful}")
# print(f"filler: {filler}")
# Fill any free space with filler
pool_count = len(oc2_location_name_to_id)
if not self.options["KevinLevels"]:
pool_count -= 8
while len(self.itempool) < pool_count:
self.itempool.append(self.create_item("Bonus Star", ItemClassification.useful))
self.world.itempool += self.itempool
def set_rules(self):
pass
def generate_basic(self) -> None:
# Add Events (Star Acquisition)
for level in Overcooked2Level():
if not self.options["KevinLevels"] and level.level_id > 36:
break
if level.level_id != 36:
self.place_event(level.location_name_level_complete, level.event_name_level_complete)
if self.is_level_horde(level.level_id):
continue # horde levels don't have star rewards
for n in [1, 2, 3]:
self.place_event(level.location_name_star_event(n), "Star")
# Add Victory Condition
self.place_event("6-6 Completed", "Victory")
# Items get distributed to locations
def fill_json_data(self) -> Dict[str, Any]:
mod_name = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}"
# Serialize Level Order
story_level_order = dict()
if self.options["ShuffleLevelOrder"]:
for level_id in self.level_mapping:
level: Overcooked2GenericLevel = self.level_mapping[level_id]
story_level_order[str(level_id)] = {
"DLC": level.dlc.value,
"LevelID": level.level_id,
}
custom_level_order = dict()
custom_level_order["Story"] = story_level_order
# Serialize Unlock Requirements
level_purchase_requirements = dict()
for level_id in self.level_unlock_counts:
level_purchase_requirements[str(level_id)] = self.level_unlock_counts[level_id]
# Override Vanilla Unlock Chain Behavior
# (all worlds accessible from the start and progressible in any order)
level_unlock_requirements = dict()
level_force_reveal = [
1, # 1-1
7, # 2-1
13, # 3-1
19, # 4-1
25, # 5-1
31, # 6-1
]
for level_id in range(1, 37):
if (level_id not in level_force_reveal):
level_unlock_requirements[str(level_id)] = level_id - 1
# Set Kevin Unlock Requirements
if self.options["KevinLevels"]:
def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
location = self.world.find_item(f"Kevin-{level_id-36}", self.player)
if location.player != self.player:
return None # This kevin level will be unlocked by the server at runtime
level_id = oc2_location_name_to_id[location.name]
return level_id
for level_id in range(37, 45):
keyholder_level_id = kevin_level_to_keyholder_level_id(level_id)
if keyholder_level_id is not None:
level_unlock_requirements[str(level_id)] = keyholder_level_id
# Place Items at Level Completion Screens (local only)
on_level_completed: Dict[str, list[Dict[str, str]]] = dict()
regions = self.world.get_regions(self.player)
for region in regions:
for location in region.locations:
if location.item is None:
continue
if location.item.code is None:
continue # it's an event
if location.item.player != self.player:
continue # not for us
level_id = str(oc2_location_name_to_id[location.name])
on_level_completed[level_id] = [item_to_unlock_event(location.item.name)]
# Put it all together
star_threshold_scale = self.options["StarThresholdScale"] / 100
base_data = {
# Changes Inherent to rando
"DisableAllMods": False,
"UnlockAllChefs": True,
"UnlockAllDLC": True,
"DisplayFPS": True,
"SkipTutorial": True,
"SkipAllOnionKing": True,
"SkipTutorialPopups": True,
"RevealAllLevels": False,
"PurchaseAllLevels": False,
"CheatsEnabled": False,
"ImpossibleTutorial": True,
"ForbidDLC": True,
"ForceSingleSaveSlot": True,
"DisableNGP": True,
"LevelForceReveal": level_force_reveal,
"SaveFolderName": mod_name,
"CustomOrderTimeoutPenalty": 10,
"LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44],
# Game Modifications
"LevelPurchaseRequirements": level_purchase_requirements,
"Custom66TimerScale": max(0.4, (1.0 - star_threshold_scale)),
"CustomLevelOrder": custom_level_order,
# Items (Starting Inventory)
"CustomOrderLifetime": 70.0, # 100 is original
"DisableWood": True,
"DisableCoal": True,
"DisableOnePlate": True,
"DisableFireExtinguisher": True,
"DisableBellows": True,
"PlatesStartDirty": True,
"MaxTipCombo": 2,
"DisableDash": True,
"WeakDash": True,
"DisableThrow": True,
"DisableCatch": True,
"DisableControlStick": True,
"DisableWokDrag": True,
"DisableRampButton": True,
"WashTimeMultiplier": 1.4,
"BurnSpeedMultiplier": 1.43,
"MaxOrdersOnScreenOffset": -2,
"ChoppingTimeScale": 1.4,
"BackpackMovementScale": 0.75,
"RespawnTime": 10.0,
"CarnivalDispenserRefactoryTime": 4.0,
"LevelUnlockRequirements": level_unlock_requirements,
"LockedEmotes": [0, 1, 2, 3, 4, 5],
"StarOffset": 0,
"AggressiveHorde": True,
"DisableEarnHordeMoney": True,
# Item Unlocking
"OnLevelCompleted": on_level_completed,
}
# Set remaining data in the options dict
bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"]
for bug in bugs:
self.options[bug] = self.options["FixBugs"]
self.options["PreserveCookingProgress"] = self.options["AlwaysPreserveCookingProgress"]
self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce.value
self.options["LevelTimerScale"] = 0.666 if self.options["ShorterLevelDuration"] else 1.0
self.options["LeaderboardScoreScale"] = {
"FourStars": 1.0,
"ThreeStars": star_threshold_scale,
"TwoStars": star_threshold_scale * 0.75,
"OneStar": star_threshold_scale * 0.35,
}
base_data.update(self.options)
return base_data
def fill_slot_data(self) -> Dict[str, Any]:
return self.fill_json_data()
def level_unlock_requirement_factory(stars_to_win: int) -> Dict[int, int]:
level_unlock_counts = dict()
level = 1
sublevel = 1
for n in range(1, 37):
progress: float = float(n)/36.0
progress *= progress # x^2 curve
star_count = int(progress*float(stars_to_win))
min = (n-1)*3
if (star_count > min):
star_count = min
level_id = (level-1)*6 + sublevel
# print("%d-%d (%d) = %d" % (level, sublevel, level_id, star_count))
level_unlock_counts[level_id] = star_count
level += 1
if level > 6:
level = 1
sublevel += 1
# force sphere 1 to 0 stars to help keep our promises to the item fill algo
level_unlock_counts[1] = 0 # 1-1
level_unlock_counts[7] = 0 # 2-1
level_unlock_counts[19] = 0 # 4-1
# Force 5-1 into sphere 1 to help things out
level_unlock_counts[25] = 1 # 5-1
for n in range(37, 46):
level_unlock_counts[n] = 0
return level_unlock_counts

View File

@@ -0,0 +1,86 @@
# Overcooked! 2
## Quick Links
- [Setup Guide](../../../../tutorial/Overcooked!%202/setup/en)
- [Settings Page](../../../../games/Overcooked!%202/player-settings)
- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding)
## How Does Randomizer Work in the Kitchen?
The *Overcooked! 2* Randomizer completely transforms the game into a metroidvania with items and item locations. Many of the Chefs' inherent abilities have been temporarily removed such that your scoring potential is limited at the start of the game. The more your inventory grows, the easier it will be to earn 2 and 3 Stars on each level.
The game takes place entirely in the "Story" campaign on a fresh save file. The ultimate goal is to reach and complete level 6-6. In order to do this you must regain enough of your abilities to complete all levels in World 6 and obtain enough stars to purchase 6-6*.
Randomizer can be played alone (one player switches between controlling two chefs) or up to 4 local/online friends. Player count can be changed at any time during the Archipelago game.
**Note: 6-6 is excluded from "Shuffle Level Order", so it will always be the standard final boss stage.*
## Items
The first time a level is completed, a random item is given to the chef(s). If playing in a MultiWorld, completing a level may instead give another Archipelago user their item. The item found is displayed as text at the top of the results screen.
Once all items have been obtained, the game will play like the original experience.
The following items were invented for Randomizer:
### Player Abilities
- Dash/Dash Cooldown
- Throw/Catch
- Sharp Knife
- Dish Scrubber
- Control Stick Batteries
- Lightweight Backpack
- Faster Respawn Time
- Emote (x6)
### Objects
- Spare Plate
- Clean Dishes
- Wood
- Coal Bucket
- Bellows
- Fire Extinguisher
### Kitchen/Environment
- Larger Tip Jar
- Guest Patience
- Burn Leniency
- Faster Condiment & Drink Switch
- Wok Wheels
- Coin Purse
- Calmer Unbread
### Overworld
- Unlock Kevin Level (x8)
- Ramp Button
- Bonus Star (Filler Item*)
**Note: Bonus star count varies with settings*
## Other Game Modifications
In addition to shuffling items, the following changes are applied to the game:
### Quality of Life
- Tutorial is skipped
- Non-linear level order
- "Auto-Complete" feature to finish a level early when a target score is obtained
- Bugfixes for issues present in the base game (including "Sink Bug" and "Double Serving")
- All chef avatars automatically unlocked
- Optionally, level time can be reduced to make progression faster paced
### Randomization Options
- *Shuffle Level Order*
- Replaces each level on the overworld with a random level
- DLC levels can show up on the Story Overworld
- Optionally exclude "Horde" Levels
- Optionally exclude "Prep" Levels
### Difficulty Adjustments
- Stars required to unlock levels have been rebalanced
- Points required to earn stars have been rebalanced
- Based off of the current World Record on the game's [Leaderboard](https://overcooked.greeny.dev)
- 1-Star/2-Star scores are much closer to the 3-Star Score
- Significantly reduced the time allotted to beat the final level
- Reduced penalty for expired order

View File

@@ -0,0 +1,84 @@
# Overcooked! 2 Randomizer Setup Guide
## Quick Links
- [Main Page](../../../../games/Overcooked!%202/info/en)
- [Settings Page](../../../../games/Overcooked!%202/player-settings)
- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding)
## Required Software
- Windows 10+
- [Overcooked! 2](https://store.steampowered.com/bundle/13608/Overcooked_2___Gourmet_Edition/) for PC
- **Steam: Recommended**
- Steam (Beta Branch): Supported
- Epic Games: Supported
- GOG: Not officially supported - Adventurous users may choose to experiment at their own risk
- Windows Store (aka GamePass): Not Supported
- Xbox/PS/Switch: Not Supported
- [OC2-Modding Client](https://github.com/toasterparty/oc2-modding/releases) (instructions below)
## Overview
*OC2-Modding* is a general purpose modding framework which doubles as an Archipelago MultiWorld Client. It works by using Harmony to inject custom code into the game at runtime, so none of the orignal game files need to be modified in any way.
When connecting to an Archipelago session using the in-game login screen, a modfile containing all relevant game modifications is automatically downloaded and applied.
From this point, the game will communicate with the Archipelago service directly to manage sending/receiving items. Notifications of important events will appear through an in-game console at the top of the screen.
## Overcooked! 2 Modding Guide
### Install
1. Download and extract the contents of the latest [OC2-Modding Release](https://github.com/toasterparty/oc2-modding/releases) anywhere on your PC
2. Double-Click **oc2-modding-install.bat** follow the instructions.
Once *OC2-Modding* is installed, you have successfully installed everything you need to play/participate in Archipelago MultiWorld games.
### Disable
To temporarily turn off *OC2-Modding* and return to the original game, open **...\Overcooked! 2\BepInEx\config\OC2Modding.cfg** in a text editor like notepad and edit the following:
`DisableAllMods = true`
To re-enable, simply change the word **true** back to a **false**.
### Uninstall
To completely remove *OC2-Modding*, navigate to your game's installation folder and run **oc2-modding-uninstall.bat**.
## Generate a MultiWorld Game
1. Visit the [Player Settings](../../../../games/Overcooked!%202/player-settings) page and configure the game-specific settings to taste
2. Export your yaml file and use it to generate a new randomized game
- (For instructions on how to generate an Archipelago game, refer to the [Archipelago Web Guide](../../../../tutorial/Archipelago/using_website/en))
## Joining a MultiWorld Game
1. Launch the game
2. When attempting to enter the main menu from the title screen, the game will freeze and prompt you to sign in:
![Sign-In Screen](https://i.imgur.com/goMy7o2.png)
3. Sign-in with server address, username and password of the corresponding room you would like to join.
- Otherwise, if you just want to play the vanilla game without any modifications, you may press "Continue without Archipelago" button.
4. Upon successful connection to the Archipelago service, you will be granted access to the main menu. The game will act as though you are playing for the first time. ***DO NOT FEAR*** — your original save data has not been overwritten; the Overcooked Randomizer just uses a temporary directory for it's save game data.
## Playing Co-Op
- To play local multiplayer (or Parsec/"Steam Play Together"), simply add the additional player to your game session as you would in the base game
- To play online multiplayer, the guest *must* also have the same version of OC2-Modding installed. In order for the game to work, the guest must sign in using the same information the host used to connect to the Archipelago session. Once both host and client are both connected, they may join one another in-game and proceed as normal. It does not matter who hosts the game, and the game's hosts may be changed at any point. You may notice some things are different when playing this way:
- Guests will still receive Archipelago messages about sent/received items the same as the host
- When the host loads the campaign, any connected guests are forced to select "Don't Save" when prompted to pick which save slot to use. This is because randomizer uses the Archipelago service as a pseudo "cloud save", so progress will always be synchronized between all participants of that randomized *Overcooked! 2* instance.
## Auto-Complete
Since the goal of randomizer isn't necessarily to achieve new personal high scores, players may find themselves waiting for a level timer to expire once they've met their objective. A new feature called *Auto-Complete* has been added to automatically complete levels once a target star count has been achieved.
To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired setting.

21
worlds/pokemon_rb/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Alex "Alchav" Avery
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,252 @@
from typing import TextIO
import os
import logging
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from Fill import fill_restrictive, FillError, sweep_from_pool
from ..AutoWorld import World, WebWorld
from ..generic.Rules import add_item_rule
from .items import item_table, item_groups
from .locations import location_data, PokemonRBLocation
from .regions import create_regions
from .logic import PokemonLogic
from .options import pokemon_rb_options
from .rom_addresses import rom_addresses
from .text import encode_text
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, process_pokemon_data, process_wild_pokemon,\
process_static_pokemon
from .rules import set_rules
import worlds.pokemon_rb.poke_data as poke_data
class PokemonWebWorld(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to playing Pokemon Red and Blue with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["Alchav"]
)]
class PokemonRedBlueWorld(World):
"""Pokémon Red and Pokémon Blue are the original monster-collecting turn-based RPGs. Explore the Kanto region with
your Pokémon, catch more than 150 unique creatures, earn badges from the region's Gym Leaders, and challenge the
Elite Four to become the champion!"""
# -MuffinJets#4559
game = "Pokemon Red and Blue"
option_definitions = pokemon_rb_options
remote_items = False
data_version = 1
topology_present = False
item_name_to_id = {name: data.id for name, data in item_table.items()}
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"}
item_name_groups = item_groups
web = PokemonWebWorld()
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
self.fly_map = None
self.fly_map_code = None
self.extra_badges = {}
self.type_chart = None
self.local_poke_data = None
self.learnsets = None
self.trainer_name = None
self.rival_name = None
@classmethod
def stage_assert_generate(cls, world):
versions = set()
for player in world.player_ids:
if world.worlds[player].game == "Pokemon Red and Blue":
versions.add(world.game_version[player].current_key)
for version in versions:
if not os.path.exists(get_base_rom_path(version)):
raise FileNotFoundError(get_base_rom_path(version))
def generate_early(self):
def encode_name(name, t):
try:
if len(encode_text(name)) > 7:
raise IndexError(f"{t} name too long for player {self.world.player_name[self.player]}. Must be 7 characters or fewer.")
return encode_text(name, length=8, whitespace="@", safety=True)
except KeyError as e:
raise KeyError(f"Invalid character(s) in {t} name for player {self.world.player_name[self.player]}") from e
self.trainer_name = encode_name(self.world.trainer_name[self.player].value, "Player")
self.rival_name = encode_name(self.world.rival_name[self.player].value, "Rival")
if self.world.badges_needed_for_hm_moves[self.player].value >= 2:
badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"]
if self.world.badges_needed_for_hm_moves[self.player].value == 3:
badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge",
"Soul Badge", "Volcano Badge", "Earth Badge"]
self.world.random.shuffle(badges)
badges_to_add += [badges.pop(), badges.pop()]
hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"]
self.world.random.shuffle(hm_moves)
self.extra_badges = {}
for badge in badges_to_add:
self.extra_badges[hm_moves.pop()] = badge
process_pokemon_data(self)
def create_items(self) -> None:
locations = [location for location in location_data if location.type == "Item"]
item_pool = []
for location in locations:
if "Hidden" in location.name and not self.world.randomize_hidden_items[self.player].value:
continue
if "Rock Tunnel B1F" in location.region and not self.world.extra_key_items[self.player].value:
continue
if location.name == "Celadon City - Mansion Lady" and not self.world.tea[self.player].value:
continue
item = self.create_item(location.original_item)
if location.event:
self.world.get_location(location.name, self.player).place_locked_item(item)
elif ("Badge" not in item.name or self.world.badgesanity[self.player].value) and \
(item.name != "Oak's Parcel" or self.world.old_man[self.player].value != 1):
item_pool.append(item)
self.world.random.shuffle(item_pool)
self.world.itempool += item_pool
def pre_fill(self):
process_wild_pokemon(self)
process_static_pokemon(self)
if self.world.old_man[self.player].value == 1:
item = self.create_item("Oak's Parcel")
locations = []
for location in self.world.get_locations():
if location.player == self.player and location.item is None and location.can_reach(self.world.state) \
and location.item_rule(item):
locations.append(location)
self.world.random.choice(locations).place_locked_item(item)
if not self.world.badgesanity[self.player].value:
self.world.non_local_items[self.player].value -= self.item_name_groups["Badges"]
for i in range(5):
try:
badges = []
badgelocs = []
for badge in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge",
"Marsh Badge", "Volcano Badge", "Earth Badge"]:
badges.append(self.create_item(badge))
for loc in ["Pewter Gym - Brock 1", "Cerulean Gym - Misty 1", "Vermilion Gym - Lt. Surge 1",
"Celadon Gym - Erika 1", "Fuchsia Gym - Koga 1", "Saffron Gym - Sabrina 1",
"Cinnabar Gym - Blaine 1", "Viridian Gym - Giovanni 1"]:
badgelocs.append(self.world.get_location(loc, self.player))
state = self.world.get_all_state(False)
self.world.random.shuffle(badges)
self.world.random.shuffle(badgelocs)
fill_restrictive(self.world, state, badgelocs.copy(), badges, True, True)
except FillError:
for location in badgelocs:
location.item = None
continue
break
else:
raise FillError(f"Failed to place badges for player {self.player}")
locs = [self.world.get_location("Fossil - Choice A", self.player),
self.world.get_location("Fossil - Choice B", self.player)]
for loc in locs:
add_item_rule(loc, lambda i: i.advancement or i.name in self.item_name_groups["Unique"]
or i.name == "Master Ball")
loc = self.world.get_location("Pallet Town - Player's PC", self.player)
if loc.item is None:
locs.append(loc)
for loc in locs:
unplaced_items = []
if loc.name in self.world.priority_locations[self.player].value:
add_item_rule(loc, lambda i: i.advancement)
for item in self.world.itempool:
if item.player == self.player and loc.item_rule(item):
self.world.itempool.remove(item)
state = sweep_from_pool(self.world.state, self.world.itempool + unplaced_items)
if state.can_reach(loc, "Location", self.player):
loc.place_locked_item(item)
break
else:
unplaced_items.append(item)
self.world.itempool += unplaced_items
intervene = False
test_state = self.world.get_all_state(False)
if not test_state.pokemon_rb_can_surf(self.player) or not test_state.pokemon_rb_can_strength(self.player):
intervene = True
elif self.world.accessibility[self.player].current_key != "minimal":
if not test_state.pokemon_rb_can_cut(self.player) or not test_state.pokemon_rb_can_flash(self.player):
intervene = True
if intervene:
# the way this is handled will be improved significantly in the future when I add options to
# let you choose the exact weights for HM compatibility
logging.warning(
f"HM-compatible Pokémon possibly missing, placing Mew on Route 1 for player {self.player}")
loc = self.world.get_location("Route 1 - Wild Pokemon - 1", self.player)
loc.item = self.create_item("Mew")
def create_regions(self):
if self.world.free_fly_location[self.player].value:
fly_map_code = self.world.random.randint(5, 9)
if fly_map_code == 9:
fly_map_code = 10
if fly_map_code == 5:
fly_map_code = 4
else:
fly_map_code = 0
self.fly_map = ["Pallet Town", "Viridian City", "Pewter City", "Cerulean City", "Lavender Town",
"Vermilion City", "Celadon City", "Fuchsia City", "Cinnabar Island", "Indigo Plateau",
"Saffron City"][fly_map_code]
self.fly_map_code = fly_map_code
create_regions(self.world, self.player)
self.world.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player)
def set_rules(self):
set_rules(self.world, self.player)
def create_item(self, name: str) -> Item:
return PokemonRBItem(name, self.player)
def generate_output(self, output_directory: str):
generate_output(self, output_directory)
def write_spoiler_header(self, spoiler_handle: TextIO):
if self.world.free_fly_location[self.player].value:
spoiler_handle.write('Fly unlocks: %s\n' % self.fly_map)
if self.extra_badges:
for hm_move, badge in self.extra_badges.items():
spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n")
def write_spoiler(self, spoiler_handle):
if self.world.randomize_type_matchup_types[self.player].value or \
self.world.randomize_type_matchup_type_effectiveness[self.player].value:
spoiler_handle.write(f"\n\nType matchups ({self.world.player_name[self.player]}):\n\n")
for matchup in self.type_chart:
spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n")
def get_filler_item_name(self) -> str:
return self.world.random.choice([item for item in item_table if item_table[item].classification in
[ItemClassification.filler, ItemClassification.trap]])
class PokemonRBItem(Item):
game = "Pokemon Red and Blue"
type = None
def __init__(self, name, player: int = None):
item_data = item_table[name]
super(PokemonRBItem, self).__init__(
name,
item_data.classification,
item_data.id, player
)

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,55 @@
# Pokémon Red and Blue
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
always able to be completed, but because of the item shuffle the player may need to access certain areas before they
would in the vanilla game.
A great many things besides item placement can be randomized, such as the location of Pokémon, their stats, types, etc., depending on your yaml settings.
Many baseline changes are made to the game, including:
* Bag item space increased to 128 slots (up from 20)
* PC item storage increased to 64 slots (up from 50)
* You can hold B to run (or bike extra fast!).
* You can hold select while talking to a trainer to re-battle them.
* You can return to route 2 from Diglett's Cave without the use of Cut.
* Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your settings.
* The S.S. Anne will never depart.
* Seafoam Islands entrances are swapped. This means you need Strength to travel through from Cinnabar Island to Fuchsia
City
* After obtaining one of the fossil item checks in Mt Moon, the remaining item can be received from the Cinnabar Lab
fossil scientist. This may require reviving a number of fossils, depending on your settings.
* Obedience depends on the total number of badges you have obtained instead of depending on specific badges.
* Pokémon that evolve by trading can also evolve by reaching level 35.
* Evolution stones are reusable.
* Much of the dialogue throughout the game has been removed or shortened.
* If the Old Man is blocking your way through Viridian City, you do not have Oak's Parcel in your inventory, and you've
exhausted your money and Poké Balls, you can get a free Poké Ball from your mom.
## What items and locations get shuffled?
All items that go into your bags given by NPCs or found on the ground, as well as gym badges.
Optionally, hidden items (those located with the Item Finder) can be shuffled as well.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world.
By default, gym badges are shuffled across only the 8 gyms, but you can turn on Badgesanity in your yaml to shuffle them
into the general item pool.
## What does another world's item look like in Pokémon Red and Blue?
All items for other games will display simply as "AP ITEM," including those for other Pokémon Red and Blue games.
## When the player receives an item, what happens?
A "received item" sound effect will play. Currently, there is no in-game message informing you of what the item is.
If you are in battle, have menus or text boxes opened, or scripted events are occurring, the items will not be given to
you until these have ended.

View File

@@ -0,0 +1,84 @@
# Setup Guide for Pokémon Red and Blue: Archipelago
## Important
As we are using Bizhawk, this guide is only applicable to Windows and Linux systems.
## Required Software
- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
- Detailed installation instructions for Bizhawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
(select `Pokemon Client` during installation).
- Pokémon Red and/or Blue ROM files. The Archipelago community cannot provide these.
## Configuring Bizhawk
Once Bizhawk has been installed, open Bizhawk and change the following settings:
- Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button.
This reduces the possibility of losing save data in emulator crashes.
- Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to
continue playing in the background, even if another window is selected.
It is strongly recommended to associate GB rom extensions (\*.gb) to the Bizhawk we've just installed.
To do so, we simply have to search any Gameboy rom we happened to own, right click and select "Open with...", unfold
the list that appears and select the bottom option "Look for another application", then browse to the Bizhawk folder
and select EmuHawk.exe.
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how it should
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
an experience customized for their taste, and different players in the same multiworld can all have different options.
### Where do I get a YAML file?
You can generate a yaml or download a template by visiting the [Pokemon Red and Blue Player Settings Page](/games/Pokemon Red and Blue/player-settings)
It is important to note that the `game_version` option determines the ROM file that will be patched.
Both the player and the person generating (if they are generating locally) will need the corresponding ROM file.
For `trainer_name` and `rival_name` the following regular characters are allowed:
* `‘’“”·… ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyzé'-?!.♂$×/,♀0123456789`
And the following special characters (these each take up one character):
* `<'d>`
* `<'l>`
* `<'t>`
* `<'v>`
* `<'r>`
* `<'m>`
* `<PK>`
* `<MN>`
* `<MALE>` alias for `♂`
* `<FEMALE>` alias for `♀`
## Joining a MultiWorld Game
### Obtain your Pokémon patch file
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
files. Your data file should have a `.apred` or `.apblue` extension.
Double-click on your patch file to start your client and start the ROM patch process. Once the process is finished
(this can take a while), the client and the emulator will be started automatically (if you associated the extension
to the emulator as recommended).
### Connect to the Multiserver
Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
Navigate to your Archipelago install folder and open `data/lua/PKMN_RB/pkmr_rb.lua`.
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)
Now you are ready to start your adventure in Kanto.

176
worlds/pokemon_rb/items.py Normal file
View File

@@ -0,0 +1,176 @@
from BaseClasses import ItemClassification
from .poke_data import pokemon_data
class ItemData:
def __init__(self, id, classification, groups):
self.groups = groups
self.classification = classification
self.id = None if id is None else id + 172000000
item_table = {
"Master Ball": ItemData(1, ItemClassification.useful, ["Consumables", "Poke Balls"]),
"Ultra Ball": ItemData(2, ItemClassification.filler, ["Consumables", "Poke Balls"]),
"Great Ball": ItemData(3, ItemClassification.filler, ["Consumables", "Poke Balls"]),
"Poke Ball": ItemData(4, ItemClassification.filler, ["Consumables", "Poke Balls"]),
"Town Map": ItemData(5, ItemClassification.progression_skip_balancing, ["Unique", "Key Items"]),
"Bicycle": ItemData(6, ItemClassification.progression, ["Unique", "Key Items"]),
# "Flippers": ItemData(7, ItemClassification.progression),
#"Safari Ball": ItemData(8, ItemClassification.filler),
#"Pokedex": ItemData(9, ItemClassification.filler),
"Moon Stone": ItemData(10, ItemClassification.useful, ["Unique", "Evolution Stones"]),
"Antidote": ItemData(11, ItemClassification.filler, ["Consumables"]),
"Burn Heal": ItemData(12, ItemClassification.filler, ["Consumables"]),
"Ice Heal": ItemData(13, ItemClassification.filler, ["Consumables"]),
"Awakening": ItemData(14, ItemClassification.filler, ["Consumables"]),
"Paralyze Heal": ItemData(15, ItemClassification.filler, ["Consumables"]),
"Full Restore": ItemData(16, ItemClassification.filler, ["Consumables"]),
"Max Potion": ItemData(17, ItemClassification.filler, ["Consumables"]),
"Hyper Potion": ItemData(18, ItemClassification.filler, ["Consumables"]),
"Super Potion": ItemData(19, ItemClassification.filler, ["Consumables"]),
"Potion": ItemData(20, ItemClassification.filler, ["Consumables"]),
"Boulder Badge": ItemData(21, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Cascade Badge": ItemData(22, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Thunder Badge": ItemData(23, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Rainbow Badge": ItemData(24, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Soul Badge": ItemData(25, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Marsh Badge": ItemData(26, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Volcano Badge": ItemData(27, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Earth Badge": ItemData(28, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Escape Rope": ItemData(29, ItemClassification.filler, ["Consumables"]),
"Repel": ItemData(30, ItemClassification.filler, ["Consumables"]),
"Old Amber": ItemData(31, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]),
"Fire Stone": ItemData(32, ItemClassification.useful, ["Unique", "Evolution Stones"]),
"Thunder Stone": ItemData(33, ItemClassification.useful, ["Unique", "Evolution Stones"]),
"Water Stone": ItemData(34, ItemClassification.useful, ["Unique", "Evolution Stones"]),
"HP Up": ItemData(35, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Protein": ItemData(36, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Iron": ItemData(37, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Carbos": ItemData(38, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Calcium": ItemData(39, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Rare Candy": ItemData(40, ItemClassification.useful, ["Consumables"]),
"Dome Fossil": ItemData(41, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]),
"Helix Fossil": ItemData(42, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]),
"Secret Key": ItemData(43, ItemClassification.progression, ["Unique", "Key Items"]),
"Bike Voucher": ItemData(45, ItemClassification.progression, ["Unique", "Key Items"]),
"X Accuracy": ItemData(46, ItemClassification.filler, ["Consumables", "Battle Items"]),
"Leaf Stone": ItemData(47, ItemClassification.useful, ["Unique", "Evolution Stones"]),
"Card Key": ItemData(48, ItemClassification.progression, ["Unique", "Key Items"]),
"Nugget": ItemData(49, ItemClassification.filler, []),
#"Laptop": ItemData(50, ItemClassification.useful, ["Unique"]),
"Poke Doll": ItemData(51, ItemClassification.filler, ["Consumables"]),
"Full Heal": ItemData(52, ItemClassification.filler, ["Consumables"]),
"Revive": ItemData(53, ItemClassification.filler, ["Consumables"]),
"Max Revive": ItemData(54, ItemClassification.filler, ["Consumables"]),
"Guard Spec": ItemData(55, ItemClassification.filler, ["Consumables", "Battle Items"]),
"Super Repel": ItemData(56, ItemClassification.filler, ["Consumables"]),
"Max Repel": ItemData(57, ItemClassification.filler, ["Consumables"]),
"Dire Hit": ItemData(58, ItemClassification.filler, ["Consumables", "Battle Items"]),
#"Coin": ItemData(59, ItemClassification.filler),
"Fresh Water": ItemData(60, ItemClassification.filler, ["Consumables"]),
"Soda Pop": ItemData(61, ItemClassification.filler, ["Consumables"]),
"Lemonade": ItemData(62, ItemClassification.filler, ["Consumables"]),
"S.S. Ticket": ItemData(63, ItemClassification.progression, ["Unique", "Key Items"]),
"Gold Teeth": ItemData(64, ItemClassification.progression, ["Unique", "Key Items"]),
"X Attack": ItemData(65, ItemClassification.filler, ["Consumables", "Battle Items"]),
"X Defend": ItemData(66, ItemClassification.filler, ["Consumables", "Battle Items"]),
"X Speed": ItemData(67, ItemClassification.filler, ["Consumables", "Battle Items"]),
"X Special": ItemData(68, ItemClassification.filler, ["Consumables", "Battle Items"]),
"Coin Case": ItemData(69, ItemClassification.progression_skip_balancing, ["Unique", "Key Items"]),
"Oak's Parcel": ItemData(70, ItemClassification.progression, ["Unique", "Key Items"]),
"Item Finder": ItemData(71, ItemClassification.progression, ["Unique", "Key Items"]),
"Silph Scope": ItemData(72, ItemClassification.progression, ["Unique", "Key Items"]),
"Poke Flute": ItemData(73, ItemClassification.progression, ["Unique", "Key Items"]),
"Lift Key": ItemData(74, ItemClassification.progression, ["Unique", "Key Items"]),
"Exp. All": ItemData(75, ItemClassification.useful, ["Unique"]),
"Old Rod": ItemData(76, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]),
"Good Rod": ItemData(77, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]),
"Super Rod": ItemData(78, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]),
"PP Up": ItemData(79, ItemClassification.filler, ["Consumables"]),
"Ether": ItemData(80, ItemClassification.filler, ["Consumables"]),
"Max Ether": ItemData(81, ItemClassification.filler, ["Consumables"]),
"Elixir": ItemData(82, ItemClassification.filler, ["Consumables"]),
"Max Elixir": ItemData(83, ItemClassification.filler, ["Consumables"]),
"Tea": ItemData(84, ItemClassification.progression, ["Unique", "Key Items"]),
# "Master Sword": ItemData(85, ItemClassification.progression),
# "Flute": ItemData(86, ItemClassification.progression),
# "Titan's Mitt": ItemData(87, ItemClassification.progression),
# "Lamp": ItemData(88, ItemClassification.progression),
"Plant Key": ItemData(89, ItemClassification.progression, ["Unique", "Key Items"]),
"Mansion Key": ItemData(90, ItemClassification.progression, ["Unique", "Key Items"]),
"Hideout Key": ItemData(91, ItemClassification.progression, ["Unique", "Key Items"]),
"Safari Pass": ItemData(93, ItemClassification.progression, ["Unique", "Key Items"]),
"HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs"]),
"HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs"]),
"HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs"]),
"HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs"]),
"HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs"]),
"TM01 Mega Punch": ItemData(201, ItemClassification.useful, ["Unique", "TMs"]),
"TM02 Razor Wind": ItemData(202, ItemClassification.useful, ["Unique", "TMs"]),
"TM03 Swords Dance": ItemData(203, ItemClassification.useful, ["Unique", "TMs"]),
"TM04 Whirlwind": ItemData(204, ItemClassification.filler, ["Unique", "TMs"]),
"TM05 Mega Kick": ItemData(205, ItemClassification.useful, ["Unique", "TMs"]),
"TM06 Toxic": ItemData(206, ItemClassification.useful, ["Unique", "TMs"]),
"TM07 Horn Drill": ItemData(207, ItemClassification.useful, ["Unique", "TMs"]),
"TM08 Body Slam": ItemData(208, ItemClassification.useful, ["Unique", "TMs"]),
"TM09 Take Down": ItemData(209, ItemClassification.useful, ["Unique", "TMs"]),
"TM10 Double Edge": ItemData(210, ItemClassification.useful, ["Unique", "TMs"]),
"TM11 Bubble Beam": ItemData(211, ItemClassification.useful, ["Unique", "TMs"]),
"TM12 Water Gun": ItemData(212, ItemClassification.useful, ["Unique", "TMs"]),
"TM13 Ice Beam": ItemData(213, ItemClassification.useful, ["Unique", "TMs"]),
"TM14 Blizzard": ItemData(214, ItemClassification.useful, ["Unique", "TMs"]),
"TM15 Hyper Beam": ItemData(215, ItemClassification.useful, ["Unique", "TMs"]),
"TM16 Pay Day": ItemData(216, ItemClassification.useful, ["Unique", "TMs"]),
"TM17 Submission": ItemData(217, ItemClassification.useful, ["Unique", "TMs"]),
"TM18 Counter": ItemData(218, ItemClassification.filler, ["Unique", "TMs"]),
"TM19 Seismic Toss": ItemData(219, ItemClassification.useful, ["Unique", "TMs"]),
"TM20 Rage": ItemData(220, ItemClassification.useful, ["Unique", "TMs"]),
"TM21 Mega Drain": ItemData(221, ItemClassification.useful, ["Unique", "TMs"]),
"TM22 Solar Beam": ItemData(222, ItemClassification.useful, ["Unique", "TMs"]),
"TM23 Dragon Rage": ItemData(223, ItemClassification.useful, ["Unique", "TMs"]),
"TM24 Thunderbolt": ItemData(224, ItemClassification.useful, ["Unique", "TMs"]),
"TM25 Thunder": ItemData(225, ItemClassification.useful, ["Unique", "TMs"]),
"TM26 Earthquake": ItemData(226, ItemClassification.useful, ["Unique", "TMs"]),
"TM27 Fissure": ItemData(227, ItemClassification.useful, ["Unique", "TMs"]),
"TM28 Dig": ItemData(228, ItemClassification.useful, ["Unique", "TMs"]),
"TM29 Psychic": ItemData(229, ItemClassification.useful, ["Unique", "TMs"]),
"TM30 Teleport": ItemData(230, ItemClassification.filler, ["Unique", "TMs"]),
"TM31 Mimic": ItemData(231, ItemClassification.useful, ["Unique", "TMs"]),
"TM32 Double Team": ItemData(232, ItemClassification.useful, ["Unique", "TMs"]),
"TM33 Reflect": ItemData(233, ItemClassification.useful, ["Unique", "TMs"]),
"TM34 Bide": ItemData(234, ItemClassification.filler, ["Unique", "TMs"]),
"TM35 Metronome": ItemData(235, ItemClassification.useful, ["Unique", "TMs"]),
"TM36 Self Destruct": ItemData(236, ItemClassification.useful, ["Unique", "TMs"]),
"TM37 Egg Bomb": ItemData(237, ItemClassification.useful, ["Unique", "TMs"]),
"TM38 Fire Blast": ItemData(238, ItemClassification.useful, ["Unique", "TMs"]),
"TM39 Swift": ItemData(239, ItemClassification.useful, ["Unique", "TMs"]),
"TM40 Skull Bash": ItemData(240, ItemClassification.filler, ["Unique", "TMs"]),
"TM41 Soft Boiled": ItemData(241, ItemClassification.useful, ["Unique", "TMs"]),
"TM42 Dream Eater": ItemData(242, ItemClassification.useful, ["Unique", "TMs"]),
"TM43 Sky Attack": ItemData(243, ItemClassification.filler, ["Unique", "TMs"]),
"TM44 Rest": ItemData(244, ItemClassification.useful, ["Unique", "TMs"]),
"TM45 Thunder Wave": ItemData(245, ItemClassification.useful, ["Unique", "TMs"]),
"TM46 Psywave": ItemData(246, ItemClassification.filler, ["Unique", "TMs"]),
"TM47 Explosion": ItemData(247, ItemClassification.useful, ["Unique", "TMs"]),
"TM48 Rock Slide": ItemData(248, ItemClassification.useful, ["Unique", "TMs"]),
"TM49 Tri Attack": ItemData(249, ItemClassification.useful, ["Unique", "TMs"]),
"TM50 Substitute": ItemData(250, ItemClassification.useful, ["Unique", "TMs"]),
"Fuji Saved": ItemData(None, ItemClassification.progression, []),
"Silph Co Liberated": ItemData(None, ItemClassification.progression, []),
"Become Champion": ItemData(None, ItemClassification.progression, [])
}
item_table.update(
{pokemon: ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()}
)
item_table.update(
{f"Missable {pokemon}": ItemData(None, ItemClassification.useful, []) for pokemon in pokemon_data.keys()}
)
item_table.update(
{f"Static {pokemon}": ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()}
)
item_groups = {}
for item, data in item_table.items():
for group in data.groups:
item_groups[group] = item_groups.get(group, []) + [item]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
from ..AutoWorld import LogicMixin
import worlds.pokemon_rb.poke_data as poke_data
class PokemonLogic(LogicMixin):
def pokemon_rb_can_surf(self, player):
return (((self.has("HM03 Surf", player) and self.can_learn_hm("10000", player))
or self.has("Flippers", player)) and (self.has("Soul Badge", player) or
self.has(self.world.worlds[player].extra_badges.get("Surf"), player)
or self.world.badges_needed_for_hm_moves[player].value == 0))
def pokemon_rb_can_cut(self, player):
return ((self.has("HM01 Cut", player) and self.can_learn_hm("100", player) or self.has("Master Sword", player))
and (self.has("Cascade Badge", player) or
self.has(self.world.worlds[player].extra_badges.get("Cut"), player) or
self.world.badges_needed_for_hm_moves[player].value == 0))
def pokemon_rb_can_fly(self, player):
return (((self.has("HM02 Fly", player) and self.can_learn_hm("1000", player)) or self.has("Flute", player)) and
(self.has("Thunder Badge", player) or self.has(self.world.worlds[player].extra_badges.get("Fly"), player)
or self.world.badges_needed_for_hm_moves[player].value == 0))
def pokemon_rb_can_strength(self, player):
return ((self.has("HM04 Strength", player) and self.can_learn_hm("100000", player)) or
self.has("Titan's Mitt", player)) and (self.has("Rainbow Badge", player) or
self.has(self.world.worlds[player].extra_badges.get("Strength"), player)
or self.world.badges_needed_for_hm_moves[player].value == 0)
def pokemon_rb_can_flash(self, player):
return (((self.has("HM05 Flash", player) and self.can_learn_hm("1000000", player)) or self.has("Lamp", player))
and (self.has("Boulder Badge", player) or self.has(self.world.worlds[player].extra_badges.get("Flash"),
player) or self.world.badges_needed_for_hm_moves[player].value == 0))
def can_learn_hm(self, move, player):
for pokemon, data in self.world.worlds[player].local_poke_data.items():
if self.has(pokemon, player) and data["tms"][6] & int(move, 2):
return True
return False
def pokemon_rb_can_get_hidden_items(self, player):
return self.has("Item Finder", player) or not self.world.require_item_finder[player].value
def pokemon_rb_cerulean_cave(self, count, player):
return len([item for item in
["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", "Marsh Badge",
"Volcano Badge", "Earth Badge", "Bicycle", "Silph Scope", "Item Finder", "Super Rod", "Good Rod",
"Old Rod", "Lift Key", "Card Key", "Town Map", "Coin Case", "S.S. Ticket", "Secret Key",
"Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "HM01 Cut", "HM02 Fly", "HM03 Surf",
"HM04 Strength", "HM05 Flash"] if self.has(item, player)]) >= count
def pokemon_rb_can_pass_guards(self, player):
if self.world.tea[player].value:
return self.has("Tea", player)
else:
# this could just be "True", but you never know what weird options I might introduce later ;)
return self.can_reach("Celadon City - Counter Man", "Location", player)
def pokemon_rb_has_badges(self, count, player):
return len([item for item in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge",
"Soul Badge", "Volcano Badge", "Earth Badge"] if self.has(item, player)]) >= count
def pokemon_rb_has_pokemon(self, count, player):
obtained_pokemon = set()
for pokemon in poke_data.pokemon_data.keys():
if self.has(pokemon, player) or self.has(f"Static {pokemon}", player):
obtained_pokemon.add(pokemon)
return len(obtained_pokemon) >= count
def pokemon_rb_fossil_checks(self, count, player):
return (self.can_reach('Mt Moon 1F - Southwest Item', 'Location', player) and
self.can_reach('Cinnabar Island - Lab Scientist', 'Location', player) and len(
[item for item in ["Dome Fossil", "Helix Fossil", "Old Amber"] if self.has(item, player)]) >= count)

View File

@@ -0,0 +1,480 @@
from Options import Toggle, Choice, Range, SpecialRange, FreeText, TextChoice
class GameVersion(Choice):
"""Select Red or Blue version."""
display_name = "Game Version"
option_red = 1
option_blue = 0
default = "random"
class TrainerName(FreeText):
"""Your trainer name. Cannot exceed 7 characters.
See the setup guide on archipelago.gg for a list of allowed characters."""
display_name = "Trainer Name"
default = "ASH"
class RivalName(FreeText):
"""Your rival's name. Cannot exceed 7 characters.
See the setup guide on archipelago.gg for a list of allowed characters."""
display_name = "Rival's Name"
default = "GARY"
class Goal(Choice):
"""If Professor Oak is selected, your victory condition will require challenging and defeating Oak after becoming"""
"""Champion and defeating or capturing the Pokemon at the end of Cerulean Cave."""
display_name = "Goal"
option_pokemon_league = 0
option_professor_oak = 1
default = 0
class EliteFourCondition(Range):
"""Number of badges required to challenge the Elite Four once the Indigo Plateau has been reached.
Your rival will reveal the amount needed on the first Route 22 battle (after turning in Oak's Parcel)."""
display_name = "Elite Four Condition"
range_start = 0
range_end = 8
default = 8
class VictoryRoadCondition(Range):
"""Number of badges required to reach Victory Road."""
display_name = "Victory Road Condition"
range_start = 0
range_end = 8
default = 8
class ViridianGymCondition(Range):
"""Number of badges required to enter Viridian Gym."""
display_name = "Viridian Gym Condition"
range_start = 0
range_end = 7
default = 7
class CeruleanCaveCondition(Range):
"""Number of badges, HMs, and key items (not counting items you can lose) required to access Cerulean Cave."""
"""If extra_key_items is turned on, the number chosen will be increased by 4."""
display_name = "Cerulean Cave Condition"
range_start = 0
range_end = 25
default = 20
class SecondFossilCheckCondition(Range):
"""After choosing one of the fossil location items, you can obtain the remaining item from the Cinnabar Lab
Scientist after reviving this number of fossils."""
display_name = "Second Fossil Check Condition"
range_start = 0
range_end = 3
default = 3
class BadgeSanity(Toggle):
"""Shuffle gym badges into the general item pool. If turned off, badges will be shuffled across the 8 gyms."""
display_name = "Badgesanity"
default = 0
class BadgesNeededForHMMoves(Choice):
"""Off will remove the requirement for badges to use HM moves. Extra will give the Marsh, Volcano, and Earth
Badges a random HM move to enable. Extra Plus will additionally pick two random badges to enable a second HM move.
A man in Cerulean City will reveal the moves enabled by each Badge."""
display_name = "Badges Needed For HM Moves"
default = 1
option_on = 1
alias_true = 1
option_off = 0
alias_false = 0
option_extra = 2
option_extra_plus = 3
class OldMan(Choice):
"""With Open Viridian City, the Old Man will let you through without needing to turn in Oak's Parcel."""
"""Early Parcel will ensure Oak's Parcel is available at the beginning of your game."""
display_name = "Old Man"
option_vanilla = 0
option_early_parcel = 1
option_open_viridian_city = 2
default = 1
class Tea(Toggle):
"""Adds a Tea item to the item pool which the Saffron guards require instead of the vending machine drinks.
Adds a location check to the Celadon Mansion 1F, where Tea is acquired in FireRed and LeafGreen."""
display_name = "Tea"
default = 0
class ExtraKeyItems(Toggle):
"""Adds key items that are required to access the Rocket Hideout, Cinnabar Mansion, Safari Zone, and Power Plant.
Adds four item pickups to Rock Tunnel B1F."""
display_name = "Extra Key Items"
default = 0
class ExtraStrengthBoulders(Toggle):
"""Adds Strength Boulders blocking the Route 11 gate, and in Route 13 (can be bypassed with Surf).
This potentially increases the usefulness of Strength as well as the Bicycle."""
display_name = "Extra Strength Boulders"
default = 0
class RequireItemFinder(Toggle):
"""Require Item Finder to pick up hidden items."""
display_name = "Require Item Finder"
default = 0
class RandomizeHiddenItems(Choice):
"""Randomize hidden items. If you choose exclude, they will be randomized but will be guaranteed junk items."""
display_name = "Randomize Hidden Items"
option_on = 1
option_off = 0
alias_true = 1
alias_false = 0
option_exclude = 2
default = 0
class FreeFlyLocation(Toggle):
"""One random fly destination will be unlocked by default."""
display_name = "Free Fly Location"
default = 1
class OaksAidRt2(Range):
"""Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 2"""
display_name = "Oak's Aide Route 2"
range_start = 0
range_end = 80
default = 10
class OaksAidRt11(Range):
"""Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 11"""
display_name = "Oak's Aide Route 11"
range_start = 0
range_end = 80
default = 30
class OaksAidRt15(Range):
"""Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 15"""
display_name = "Oak's Aide Route 15"
range_start = 0
range_end = 80
default = 50
class ExpModifier(SpecialRange):
"""Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16."""
display_name = "Exp Modifier"
range_start = 0
range_end = 255
default = 16
special_range_names = {
"half": default / 2,
"normal": default,
"double": default * 2,
"triple": default * 3,
"quadruple": default * 4,
"quintuple": default * 5,
"sextuple": default * 6,
"septuple": default * 7,
"octuple": default * 8,
}
class RandomizeWildPokemon(Choice):
"""Randomize all wild Pokemon and game corner prize Pokemon. match_types will select a Pokemon with at least one
type matching the original type of the original Pokemon. match_base_stats will prefer Pokemon with closer base stat
totals. match_types_and_base_stats will match types and will weight towards similar base stats, but there may not be
many to choose from."""
display_name = "Randomize Wild Pokemon"
default = 0
option_vanilla = 0
option_match_types = 1
option_match_base_stats = 2
option_match_types_and_base_stats = 3
option_completely_random = 4
class RandomizeStarterPokemon(Choice):
"""Randomize the starter Pokemon choices."""
display_name = "Randomize Starter Pokemon"
default = 0
option_vanilla = 0
option_match_types = 1
option_match_base_stats = 2
option_match_types_and_base_stats = 3
option_completely_random = 4
class RandomizeStaticPokemon(Choice):
"""Randomize one-time gift and encountered Pokemon. These will always be first evolution stage Pokemon."""
display_name = "Randomize Static Pokemon"
default = 0
option_vanilla = 0
option_match_types = 1
option_match_base_stats = 2
option_match_types_and_base_stats = 3
option_completely_random = 4
class RandomizeLegendaryPokemon(Choice):
"""Randomize Legendaries. Mew has been added as an encounter at the Vermilion dock truck.
Shuffle will shuffle the legendaries with each other. Static will shuffle them into other static Pokemon locations.
'Any' will allow legendaries to appear anywhere based on wild and static randomization options, and their locations
will be randomized according to static Pokemon randomization options."""
display_name = "Randomize Legendary Pokemon"
default = 0
option_vanilla = 0
option_shuffle = 1
option_static = 2
option_any = 3
class CatchEmAll(Choice):
"""Guarantee all first evolution stage Pokemon are available, or all Pokemon of all stages.
Currently only has an effect if wild Pokemon are randomized."""
display_name = "Catch 'Em All"
default = 0
option_off = 0
alias_false = 0
option_first_stage = 1
option_all_pokemon = 2
class RandomizeTrainerParties(Choice):
"""Randomize enemy Pokemon encountered in trainer battles."""
display_name = "Randomize Trainer Parties"
default = 0
option_vanilla = 0
option_match_types = 1
option_match_base_stats = 2
option_match_types_and_base_stats = 3
option_completely_random = 4
class TrainerLegendaries(Toggle):
"""Allow legendary Pokemon in randomized trainer parties."""
display_name = "Trainer Legendaries"
default = 0
class BlindTrainers(Range):
"""Chance each frame that you are standing on a tile in a trainer's line of sight that they will fail to initiate a
battle. If you move into and out of their line of sight without stopping, this chance will only trigger once."""
display_name = "Blind Trainers"
range_start = 0
range_end = 100
default = 0
class MinimumStepsBetweenEncounters(Range):
"""Minimum number of steps between wild Pokemon encounters."""
display_name = "Minimum Steps Between Encounters"
default = 3
range_start = 0
range_end = 255
class RandomizePokemonStats(Choice):
"""Randomize base stats for each Pokemon. Shuffle will shuffle the 5 base stat values amongst each other. Randomize
will completely randomize each stat, but will still add up to the same base stat total."""
display_name = "Randomize Pokemon Stats"
default = 0
option_vanilla = 0
option_shuffle = 1
option_randomize = 2
class RandomizePokemonCatchRates(Toggle):
"""Randomize the catch rate for each Pokemon."""
display_name = "Randomize Catch Rates"
default = 0
class MinimumCatchRate(Range):
"""Minimum catch rate for each Pokemon. If randomize_catch_rates is on, this will be the minimum value that can be
chosen. Otherwise, it will raise any Pokemon's catch rate up to this value if its normal catch rate is lower."""
display_name = "Minimum Catch Rate"
range_start = 1
range_end = 255
default = 3
class RandomizePokemonMovesets(Choice):
"""Randomize the moves learned by Pokemon. prefer_types will prefer moves that match the type of the Pokemon."""
display_name = "Randomize Pokemon Movesets"
option_vanilla = 0
option_prefer_types = 1
option_completely_random = 2
default = 0
class StartWithFourMoves(Toggle):
"""If movesets are randomized, this will give all Pokemon 4 starting moves."""
display_name = "Start With Four Moves"
default = 0
class TMCompatibility(Choice):
"""Randomize which Pokemon can learn each TM. prefer_types: 90% chance if Pokemon's type matches the move,
50% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same
TM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn
every TM."""
display_name = "TM Compatibility"
default = 0
option_vanilla = 0
option_prefer_types = 1
option_completely_random = 2
option_full_compatibility = 3
class HMCompatibility(Choice):
"""Randomize which Pokemon can learn each HM. prefer_types: 100% chance if Pokemon's type matches the move,
75% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same
HM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn
every HM."""
display_name = "HM Compatibility"
default = 0
option_vanilla = 0
option_prefer_types = 1
option_completely_random = 2
option_full_compatibility = 3
class RandomizePokemonTypes(Choice):
"""Randomize the types of each Pokemon. Follow Evolutions will ensure Pokemon's types remain the same when evolving
(except possibly gaining a type)."""
display_name = "Pokemon Types"
option_vanilla = 0
option_follow_evolutions = 1
option_randomize = 2
default = 0
class SecondaryTypeChance(SpecialRange):
"""If randomize_pokemon_types is on, this is the chance each Pokemon will have a secondary type. If follow_evolutions
is selected, it is the chance a second type will be added at each evolution stage. vanilla will give secondary types
to Pokemon that normally have a secondary type."""
display_name = "Secondary Type Chance"
range_start = -1
range_end = 100
default = -1
special_range_names = {
"vanilla": -1
}
class RandomizeTypeChartTypes(Choice):
"""The game's type chart consists of 3 columns: attacking type, defending type, and type effectiveness.
Matchups that have regular type effectiveness are not in the chart. Shuffle will shuffle the attacking types
across the attacking type column and the defending types across the defending type column (so for example Normal
type will still have exactly 2 types that it receives non-regular damage from, and 2 types it deals non-regular
damage to). Randomize will randomize each type in both columns to any random type."""
display_name = "Randomize Type Chart Types"
option_vanilla = 0
option_shuffle = 1
option_randomize = 2
default = 0
class RandomizeTypeChartTypeEffectiveness(Choice):
"""The game's type chart consists of 3 columns: attacking type, defending type, and type effectiveness.
Matchups that have regular type effectiveness are not in the chart. Shuffle will shuffle the type effectiveness
across the type effectiveness column (so for example there will always be 6 type immunities). Randomize will
randomize each entry in the table to no effect, not very effective, or super effective; with no effect occurring
at a low chance. Chaos will randomize the values to anywhere between 0% and 200% damage, in 10% increments."""
display_name = "Randomize Type Chart Type Effectiveness"
option_vanilla = 0
option_shuffle = 1
option_randomize = 2
option_chaos = 3
default = 0
class SafariZoneNormalBattles(Toggle):
"""Change the Safari Zone to have standard wild pokemon battles."""
display_name = "Safari Zone Normal Battles"
default = 0
class NormalizeEncounterChances(Toggle):
"""Each wild encounter table has 10 slots for Pokemon. Normally the chance for each being chosen ranges from
19.9% to 1.2%. Turn this on to normalize them all to 10% each."""
display_name = "Normalize Encounter Chances"
default = 0
class ReusableTMs(Toggle):
"""Makes TMs reusable, so they will not be consumed upon use."""
display_name = "Reusable TMs"
default = 0
class StartingMoney(Range):
"""The amount of money you start with."""
display_name = "Starting Money"
default = 3000
range_start = 0
range_end = 999999
pokemon_rb_options = {
"game_version": GameVersion,
"trainer_name": TrainerName,
"rival_name": RivalName,
#"goal": Goal,
"elite_four_condition": EliteFourCondition,
"victory_road_condition": VictoryRoadCondition,
"viridian_gym_condition": ViridianGymCondition,
"cerulean_cave_condition": CeruleanCaveCondition,
"second_fossil_check_condition": SecondFossilCheckCondition,
"badgesanity": BadgeSanity,
"old_man": OldMan,
"tea": Tea,
"extra_key_items": ExtraKeyItems,
"extra_strength_boulders": ExtraStrengthBoulders,
"require_item_finder": RequireItemFinder,
"randomize_hidden_items": RandomizeHiddenItems,
"badges_needed_for_hm_moves": BadgesNeededForHMMoves,
"free_fly_location": FreeFlyLocation,
"oaks_aide_rt_2": OaksAidRt2,
"oaks_aide_rt_11": OaksAidRt11,
"oaks_aide_rt_15": OaksAidRt15,
"blind_trainers": BlindTrainers,
"minimum_steps_between_encounters": MinimumStepsBetweenEncounters,
"exp_modifier": ExpModifier,
"randomize_wild_pokemon": RandomizeWildPokemon,
"randomize_starter_pokemon": RandomizeStarterPokemon,
"randomize_static_pokemon": RandomizeStaticPokemon,
"randomize_legendary_pokemon": RandomizeLegendaryPokemon,
"catch_em_all": CatchEmAll,
"randomize_pokemon_stats": RandomizePokemonStats,
"randomize_pokemon_catch_rates": RandomizePokemonCatchRates,
"minimum_catch_rate": MinimumCatchRate,
"randomize_trainer_parties": RandomizeTrainerParties,
"trainer_legendaries": TrainerLegendaries,
"randomize_pokemon_movesets": RandomizePokemonMovesets,
"start_with_four_moves": StartWithFourMoves,
"tm_compatibility": TMCompatibility,
"hm_compatibility": HMCompatibility,
"randomize_pokemon_types": RandomizePokemonTypes,
"secondary_type_chance": SecondaryTypeChance,
"randomize_type_matchup_types": RandomizeTypeChartTypes,
"randomize_type_matchup_type_effectiveness": RandomizeTypeChartTypeEffectiveness,
"safari_zone_normal_battles": SafariZoneNormalBattles,
"normalize_encounter_chances": NormalizeEncounterChances,
"reusable_tms": ReusableTMs,
"starting_money": StartingMoney,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,305 @@
from BaseClasses import MultiWorld, Region, Entrance, RegionType, LocationProgressType
from worlds.generic.Rules import add_item_rule
from .locations import location_data, PokemonRBLocation
def create_region(world: MultiWorld, player: int, name: str, locations_per_region=None, exits=None):
ret = Region(name, RegionType.Generic, name, player, world)
for location in locations_per_region.get(name, []):
if (world.randomize_hidden_items[player].value or "Hidden" not in location.name) and \
(world.extra_key_items[player].value or name != "Rock Tunnel B1F" or "Item" not in location.name) and \
(world.tea[player].value or location.name != "Celadon City - Mansion Lady"):
location.parent_region = ret
ret.locations.append(location)
if world.randomize_hidden_items[player].value == 2 and "Hidden" in location.name:
location.progress_type = LocationProgressType.EXCLUDED
add_item_rule(location, lambda i: not (i.advancement or i.useful))
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
locations_per_region[name] = []
return ret
def create_regions(world: MultiWorld, player: int):
locations_per_region = {}
for location in location_data:
locations_per_region.setdefault(location.region, [])
locations_per_region[location.region].append(PokemonRBLocation(player, location.name, location.address,
location.rom_address))
regions = [
create_region(world, player, "Menu", locations_per_region),
create_region(world, player, "Anywhere", locations_per_region),
create_region(world, player, "Fossil", locations_per_region),
create_region(world, player, "Pallet Town", locations_per_region),
create_region(world, player, "Route 1", locations_per_region),
create_region(world, player, "Viridian City", locations_per_region),
create_region(world, player, "Viridian City North", locations_per_region),
create_region(world, player, "Viridian Gym", locations_per_region),
create_region(world, player, "Route 2", locations_per_region),
create_region(world, player, "Route 2 East", locations_per_region),
create_region(world, player, "Diglett's Cave", locations_per_region),
create_region(world, player, "Route 22", locations_per_region),
create_region(world, player, "Route 23 South", locations_per_region),
create_region(world, player, "Route 23 North", locations_per_region),
create_region(world, player, "Viridian Forest", locations_per_region),
create_region(world, player, "Pewter City", locations_per_region),
create_region(world, player, "Pewter Gym", locations_per_region),
create_region(world, player, "Route 3", locations_per_region),
create_region(world, player, "Mt Moon 1F", locations_per_region),
create_region(world, player, "Mt Moon B1F", locations_per_region),
create_region(world, player, "Mt Moon B2F", locations_per_region),
create_region(world, player, "Route 4", locations_per_region),
create_region(world, player, "Cerulean City", locations_per_region),
create_region(world, player, "Cerulean Gym", locations_per_region),
create_region(world, player, "Route 24", locations_per_region),
create_region(world, player, "Route 25", locations_per_region),
create_region(world, player, "Route 9", locations_per_region),
create_region(world, player, "Route 10 North", locations_per_region),
create_region(world, player, "Rock Tunnel 1F", locations_per_region),
create_region(world, player, "Rock Tunnel B1F", locations_per_region),
create_region(world, player, "Power Plant", locations_per_region),
create_region(world, player, "Route 10 South", locations_per_region),
create_region(world, player, "Lavender Town", locations_per_region),
create_region(world, player, "Pokemon Tower 1F", locations_per_region),
create_region(world, player, "Pokemon Tower 2F", locations_per_region),
create_region(world, player, "Pokemon Tower 3F", locations_per_region),
create_region(world, player, "Pokemon Tower 4F", locations_per_region),
create_region(world, player, "Pokemon Tower 5F", locations_per_region),
create_region(world, player, "Pokemon Tower 6F", locations_per_region),
create_region(world, player, "Pokemon Tower 7F", locations_per_region),
create_region(world, player, "Route 5", locations_per_region),
create_region(world, player, "Saffron City", locations_per_region),
create_region(world, player, "Saffron Gym", locations_per_region),
create_region(world, player, "Copycat's House", locations_per_region),
create_region(world, player, "Underground Tunnel North-South", locations_per_region),
create_region(world, player, "Route 6", locations_per_region),
create_region(world, player, "Vermilion City", locations_per_region),
create_region(world, player, "Vermilion Gym", locations_per_region),
create_region(world, player, "S.S. Anne 1F", locations_per_region),
create_region(world, player, "S.S. Anne B1F", locations_per_region),
create_region(world, player, "S.S. Anne 2F", locations_per_region),
create_region(world, player, "Route 11", locations_per_region),
create_region(world, player, "Route 11 East", locations_per_region),
create_region(world, player, "Route 12 North", locations_per_region),
create_region(world, player, "Route 12 South", locations_per_region),
create_region(world, player, "Route 12 Grass", locations_per_region),
create_region(world, player, "Route 12 West", locations_per_region),
create_region(world, player, "Route 7", locations_per_region),
create_region(world, player, "Underground Tunnel West-East", locations_per_region),
create_region(world, player, "Route 8", locations_per_region),
create_region(world, player, "Route 8 Grass", locations_per_region),
create_region(world, player, "Celadon City", locations_per_region),
create_region(world, player, "Celadon Prize Corner", locations_per_region),
create_region(world, player, "Celadon Gym", locations_per_region),
create_region(world, player, "Route 16", locations_per_region),
create_region(world, player, "Route 16 North", locations_per_region),
create_region(world, player, "Route 17", locations_per_region),
create_region(world, player, "Route 18", locations_per_region),
create_region(world, player, "Fuchsia City", locations_per_region),
create_region(world, player, "Fuchsia Gym", locations_per_region),
create_region(world, player, "Safari Zone Gate", locations_per_region),
create_region(world, player, "Safari Zone Center", locations_per_region),
create_region(world, player, "Safari Zone East", locations_per_region),
create_region(world, player, "Safari Zone North", locations_per_region),
create_region(world, player, "Safari Zone West", locations_per_region),
create_region(world, player, "Route 15", locations_per_region),
create_region(world, player, "Route 14", locations_per_region),
create_region(world, player, "Route 13", locations_per_region),
create_region(world, player, "Route 19", locations_per_region),
create_region(world, player, "Route 20 East", locations_per_region),
create_region(world, player, "Route 20 West", locations_per_region),
create_region(world, player, "Seafoam Islands 1F", locations_per_region),
create_region(world, player, "Seafoam Islands B1F", locations_per_region),
create_region(world, player, "Seafoam Islands B2F", locations_per_region),
create_region(world, player, "Seafoam Islands B3F", locations_per_region),
create_region(world, player, "Seafoam Islands B4F", locations_per_region),
create_region(world, player, "Cinnabar Island", locations_per_region),
create_region(world, player, "Cinnabar Gym", locations_per_region),
create_region(world, player, "Route 21", locations_per_region),
create_region(world, player, "Silph Co 1F", locations_per_region),
create_region(world, player, "Silph Co 2F", locations_per_region),
create_region(world, player, "Silph Co 3F", locations_per_region),
create_region(world, player, "Silph Co 4F", locations_per_region),
create_region(world, player, "Silph Co 5F", locations_per_region),
create_region(world, player, "Silph Co 6F", locations_per_region),
create_region(world, player, "Silph Co 7F", locations_per_region),
create_region(world, player, "Silph Co 8F", locations_per_region),
create_region(world, player, "Silph Co 9F", locations_per_region),
create_region(world, player, "Silph Co 10F", locations_per_region),
create_region(world, player, "Silph Co 11F", locations_per_region),
create_region(world, player, "Rocket Hideout B1F", locations_per_region),
create_region(world, player, "Rocket Hideout B2F", locations_per_region),
create_region(world, player, "Rocket Hideout B3F", locations_per_region),
create_region(world, player, "Rocket Hideout B4F", locations_per_region),
create_region(world, player, "Pokemon Mansion 1F", locations_per_region),
create_region(world, player, "Pokemon Mansion 2F", locations_per_region),
create_region(world, player, "Pokemon Mansion 3F", locations_per_region),
create_region(world, player, "Pokemon Mansion B1F", locations_per_region),
create_region(world, player, "Victory Road 1F", locations_per_region),
create_region(world, player, "Victory Road 2F", locations_per_region),
create_region(world, player, "Victory Road 3F", locations_per_region),
create_region(world, player, "Indigo Plateau", locations_per_region),
create_region(world, player, "Cerulean Cave 1F", locations_per_region),
create_region(world, player, "Cerulean Cave 2F", locations_per_region),
create_region(world, player, "Cerulean Cave B1F", locations_per_region),
create_region(world, player, "Evolution", locations_per_region),
]
world.regions += regions
connect(world, player, "Menu", "Anywhere", one_way=True)
connect(world, player, "Menu", "Pallet Town", one_way=True)
connect(world, player, "Menu", "Fossil", lambda state: state.pokemon_rb_fossil_checks(
state.world.second_fossil_check_condition[player].value, player), one_way=True)
connect(world, player, "Pallet Town", "Route 1")
connect(world, player, "Route 1", "Viridian City")
connect(world, player, "Viridian City", "Route 22")
connect(world, player, "Route 22", "Route 23 South",
lambda state: state.pokemon_rb_has_badges(state.world.victory_road_condition[player].value, player))
connect(world, player, "Route 23 South", "Route 23 North", lambda state: state.pokemon_rb_can_surf(player))
connect(world, player, "Viridian City North", "Viridian Gym", lambda state:
state.pokemon_rb_has_badges(state.world.viridian_gym_condition[player].value, player), one_way=True)
connect(world, player, "Route 2", "Route 2 East", lambda state: state.pokemon_rb_can_cut(player))
connect(world, player, "Route 2 East", "Diglett's Cave", lambda state: state.pokemon_rb_can_cut(player))
connect(world, player, "Route 2", "Viridian City North")
connect(world, player, "Route 2", "Viridian Forest")
connect(world, player, "Route 2", "Pewter City")
connect(world, player, "Pewter City", "Pewter Gym", one_way=True)
connect(world, player, "Pewter City", "Route 3")
connect(world, player, "Route 4", "Route 3", one_way=True)
connect(world, player, "Mt Moon 1F", "Mt Moon B1F", one_way=True)
connect(world, player, "Mt Moon B1F", "Mt Moon B2F", one_way=True)
connect(world, player, "Mt Moon B1F", "Route 4", one_way=True)
connect(world, player, "Route 4", "Cerulean City")
connect(world, player, "Cerulean City", "Cerulean Gym", one_way=True)
connect(world, player, "Cerulean City", "Route 24", one_way=True)
connect(world, player, "Route 24", "Route 25", one_way=True)
connect(world, player, "Cerulean City", "Route 9", lambda state: state.pokemon_rb_can_cut(player))
connect(world, player, "Route 9", "Route 10 North")
connect(world, player, "Route 10 North", "Rock Tunnel 1F", lambda state: state.pokemon_rb_can_flash(player))
connect(world, player, "Route 10 North", "Power Plant", lambda state: state.pokemon_rb_can_surf(player) and
(state.has("Plant Key", player) or not state.world.extra_key_items[player].value), one_way=True)
connect(world, player, "Rock Tunnel 1F", "Route 10 South", lambda state: state.pokemon_rb_can_flash(player))
connect(world, player, "Rock Tunnel 1F", "Rock Tunnel B1F")
connect(world, player, "Lavender Town", "Pokemon Tower 1F", one_way=True)
connect(world, player, "Lavender Town", "Pokemon Tower 1F", one_way=True)
connect(world, player, "Pokemon Tower 1F", "Pokemon Tower 2F", one_way=True)
connect(world, player, "Pokemon Tower 2F", "Pokemon Tower 3F", one_way=True)
connect(world, player, "Pokemon Tower 3F", "Pokemon Tower 4F", one_way=True)
connect(world, player, "Pokemon Tower 4F", "Pokemon Tower 5F", one_way=True)
connect(world, player, "Pokemon Tower 5F", "Pokemon Tower 6F", one_way=True)
connect(world, player, "Pokemon Tower 6F", "Pokemon Tower 7F", lambda state: state.has("Silph Scope", player))
connect(world, player, "Cerulean City", "Route 5")
connect(world, player, "Route 5", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
connect(world, player, "Route 5", "Underground Tunnel North-South")
connect(world, player, "Route 6", "Underground Tunnel North-South")
connect(world, player, "Route 6", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
connect(world, player, "Route 7", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
connect(world, player, "Route 8", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
connect(world, player, "Saffron City", "Copycat's House", lambda state: state.has("Silph Co Liberated", player), one_way=True)
connect(world, player, "Saffron City", "Saffron Gym", lambda state: state.has("Silph Co Liberated", player), one_way=True)
connect(world, player, "Route 6", "Vermilion City")
connect(world, player, "Vermilion City", "Vermilion Gym", lambda state: state.pokemon_rb_can_surf(player) or state.pokemon_rb_can_cut(player), one_way=True)
connect(world, player, "Vermilion City", "S.S. Anne 1F", lambda state: state.has("S.S. Ticket", player), one_way=True)
connect(world, player, "S.S. Anne 1F", "S.S. Anne 2F", one_way=True)
connect(world, player, "S.S. Anne 1F", "S.S. Anne B1F", one_way=True)
connect(world, player, "Vermilion City", "Route 11")
connect(world, player, "Vermilion City", "Diglett's Cave")
connect(world, player, "Route 12 West", "Route 11 East", lambda state: state.pokemon_rb_can_strength(player) or not state.world.extra_strength_boulders[player].value)
connect(world, player, "Route 12 North", "Route 12 South", lambda state: state.has("Poke Flute", player) or state.pokemon_rb_can_surf( player))
connect(world, player, "Route 12 West", "Route 12 North", lambda state: state.has("Poke Flute", player))
connect(world, player, "Route 12 West", "Route 12 South", lambda state: state.has("Poke Flute", player))
connect(world, player, "Route 12 South", "Route 12 Grass", lambda state: state.pokemon_rb_can_cut(player))
connect(world, player, "Route 12 North", "Lavender Town")
connect(world, player, "Route 7", "Lavender Town")
connect(world, player, "Route 10 South", "Lavender Town")
connect(world, player, "Route 7", "Underground Tunnel West-East")
connect(world, player, "Route 8", "Underground Tunnel West-East")
connect(world, player, "Route 8", "Celadon City")
connect(world, player, "Route 8", "Route 8 Grass", lambda state: state.pokemon_rb_can_cut(player), one_way=True)
connect(world, player, "Route 7", "Celadon City")
connect(world, player, "Celadon City", "Celadon Gym", lambda state: state.pokemon_rb_can_cut(player), one_way=True)
connect(world, player, "Celadon City", "Celadon Prize Corner")
connect(world, player, "Celadon City", "Route 16")
connect(world, player, "Route 16", "Route 16 North", lambda state: state.pokemon_rb_can_cut(player), one_way=True)
connect(world, player, "Route 16", "Route 17", lambda state: state.has("Poke Flute", player) and state.has("Bicycle", player))
connect(world, player, "Route 17", "Route 18", lambda state: state.has("Bicycle", player))
connect(world, player, "Fuchsia City", "Fuchsia Gym", one_way=True)
connect(world, player, "Fuchsia City", "Route 18")
connect(world, player, "Fuchsia City", "Safari Zone Gate", one_way=True)
connect(world, player, "Safari Zone Gate", "Safari Zone Center", lambda state: state.has("Safari Pass", player) or not state.world.extra_key_items[player].value, one_way=True)
connect(world, player, "Safari Zone Center", "Safari Zone East", one_way=True)
connect(world, player, "Safari Zone Center", "Safari Zone West", one_way=True)
connect(world, player, "Safari Zone Center", "Safari Zone North", one_way=True)
connect(world, player, "Fuchsia City", "Route 15")
connect(world, player, "Route 15", "Route 14")
connect(world, player, "Route 14", "Route 13")
connect(world, player, "Route 13", "Route 12 South", lambda state: state.pokemon_rb_can_strength(player) or state.pokemon_rb_can_surf(player) or not state.world.extra_strength_boulders[player].value)
connect(world, player, "Fuchsia City", "Route 19", lambda state: state.pokemon_rb_can_surf(player))
connect(world, player, "Route 20 East", "Route 19")
connect(world, player, "Route 20 West", "Cinnabar Island", lambda state: state.pokemon_rb_can_surf(player))
connect(world, player, "Route 20 West", "Seafoam Islands 1F")
connect(world, player, "Route 20 East", "Seafoam Islands 1F", one_way=True)
connect(world, player, "Seafoam Islands 1F", "Route 20 East", lambda state: state.pokemon_rb_can_strength(player), one_way=True)
connect(world, player, "Viridian City", "Viridian City North", lambda state: state.has("Oak's Parcel", player) or state.world.old_man[player].value == 2 or state.pokemon_rb_can_cut(player))
connect(world, player, "Route 3", "Mt Moon 1F", one_way=True)
connect(world, player, "Route 11", "Route 11 East", lambda state: state.pokemon_rb_can_strength(player))
connect(world, player, "Cinnabar Island", "Cinnabar Gym", lambda state: state.has("Secret Key", player), one_way=True)
connect(world, player, "Cinnabar Island", "Pokemon Mansion 1F", lambda state: state.has("Mansion Key", player) or not state.world.extra_key_items[player].value, one_way=True)
connect(world, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True)
connect(world, player, "Seafoam Islands B1F", "Seafoam Islands B2F", one_way=True)
connect(world, player, "Seafoam Islands B2F", "Seafoam Islands B3F", one_way=True)
connect(world, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True)
connect(world, player, "Route 21", "Cinnabar Island", lambda state: state.pokemon_rb_can_surf(player))
connect(world, player, "Pallet Town", "Route 21", lambda state: state.pokemon_rb_can_surf(player))
connect(world, player, "Saffron City", "Silph Co 1F", lambda state: state.has("Fuji Saved", player), one_way=True)
connect(world, player, "Silph Co 1F", "Silph Co 2F", one_way=True)
connect(world, player, "Silph Co 2F", "Silph Co 3F", one_way=True)
connect(world, player, "Silph Co 3F", "Silph Co 4F", one_way=True)
connect(world, player, "Silph Co 4F", "Silph Co 5F", one_way=True)
connect(world, player, "Silph Co 5F", "Silph Co 6F", one_way=True)
connect(world, player, "Silph Co 6F", "Silph Co 7F", one_way=True)
connect(world, player, "Silph Co 7F", "Silph Co 8F", one_way=True)
connect(world, player, "Silph Co 8F", "Silph Co 9F", one_way=True)
connect(world, player, "Silph Co 9F", "Silph Co 10F", one_way=True)
connect(world, player, "Silph Co 10F", "Silph Co 11F", one_way=True)
connect(world, player, "Celadon City", "Rocket Hideout B1F", lambda state: state.has("Hideout Key", player) or not state.world.extra_key_items[player].value, one_way=True)
connect(world, player, "Rocket Hideout B1F", "Rocket Hideout B2F", one_way=True)
connect(world, player, "Rocket Hideout B2F", "Rocket Hideout B3F", one_way=True)
connect(world, player, "Rocket Hideout B3F", "Rocket Hideout B4F", one_way=True)
connect(world, player, "Pokemon Mansion 1F", "Pokemon Mansion 2F", one_way=True)
connect(world, player, "Pokemon Mansion 2F", "Pokemon Mansion 3F", one_way=True)
connect(world, player, "Pokemon Mansion 1F", "Pokemon Mansion B1F", one_way=True)
connect(world, player, "Route 23 North", "Victory Road 1F", lambda state: state.pokemon_rb_can_strength(player), one_way=True)
connect(world, player, "Victory Road 1F", "Victory Road 2F", one_way=True)
connect(world, player, "Victory Road 2F", "Victory Road 3F", one_way=True)
connect(world, player, "Victory Road 2F", "Indigo Plateau", lambda state: state.pokemon_rb_has_badges(state.world.elite_four_condition[player], player), one_way=True)
connect(world, player, "Cerulean City", "Cerulean Cave 1F", lambda state:
state.pokemon_rb_cerulean_cave(state.world.cerulean_cave_condition[player].value + (state.world.extra_key_items[player].value * 4), player) and
state.pokemon_rb_can_surf(player), one_way=True)
connect(world, player, "Cerulean Cave 1F", "Cerulean Cave 2F", one_way=True)
connect(world, player, "Cerulean Cave 1F", "Cerulean Cave B1F", lambda state: state.pokemon_rb_can_surf(player), one_way=True)
if world.worlds[player].fly_map != "Pallet Town":
connect(world, player, "Menu", world.worlds[player].fly_map, lambda state: state.pokemon_rb_can_fly(player), one_way=True,
name="Fly to " + world.worlds[player].fly_map)
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, one_way=False, name=None):
source_region = world.get_region(source, player)
target_region = world.get_region(target, player)
if name is None:
name = source + " to " + target
connection = Entrance(
player,
name,
source_region
)
connection.access_rule = rule
source_region.exits.append(connection)
connection.connect(target_region)
if not one_way:
connect(world, player, target, source, rule, True)

631
worlds/pokemon_rb/rom.py Normal file
View File

@@ -0,0 +1,631 @@
import os
import hashlib
import Utils
import bsdiff4
from copy import deepcopy
from Patch import APDeltaPatch
from .text import encode_text
from .rom_addresses import rom_addresses
from .locations import location_data
import worlds.pokemon_rb.poke_data as poke_data
def choose_forced_type(chances, random):
n = random.randint(1, 100)
for chance in chances:
if chance[0] >= n:
return chance[1]
return None
def filter_moves(moves, type, random):
ret = []
for move in moves:
if poke_data.moves[move]["type"] == type or type is None:
ret.append(move)
random.shuffle(ret)
return ret
def get_move(moves, chances, random, starting_move=False):
type = choose_forced_type(chances, random)
filtered_moves = filter_moves(moves, type, random)
for move in filtered_moves:
if poke_data.moves[move]["accuracy"] > 80 and poke_data.moves[move]["power"] > 0 or not starting_move:
moves.remove(move)
return move
else:
return get_move(moves, [], random, starting_move)
def get_encounter_slots(self):
encounter_slots = [location for location in location_data if location.type == "Wild Encounter"]
for location in encounter_slots:
if isinstance(location.original_item, list):
location.original_item = location.original_item[not self.world.game_version[self.player].value]
return encounter_slots
def get_base_stat_total(mon):
return (poke_data.pokemon_data[mon]["atk"] + poke_data.pokemon_data[mon]["def"]
+ poke_data.pokemon_data[mon]["hp"] + poke_data.pokemon_data[mon]["spd"]
+ poke_data.pokemon_data[mon]["spc"])
def randomize_pokemon(self, mon, mons_list, randomize_type):
if randomize_type in [1, 3]:
type_mons = [pokemon for pokemon in mons_list if any([poke_data.pokemon_data[mon][
"type1"] in [self.local_poke_data[pokemon]["type1"], self.local_poke_data[pokemon]["type2"]],
poke_data.pokemon_data[mon]["type2"] in [self.local_poke_data[pokemon]["type1"],
self.local_poke_data[pokemon]["type2"]]])]
if not type_mons:
type_mons = mons_list.copy()
if randomize_type == 3:
stat_base = get_base_stat_total(mon)
type_mons.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base))
mon = type_mons[round(self.world.random.triangular(0, len(type_mons) - 1, 0))]
if randomize_type == 2:
stat_base = get_base_stat_total(mon)
mons_list.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base))
mon = mons_list[round(self.world.random.triangular(0, 50, 0))]
elif randomize_type == 4:
mon = self.world.random.choice(mons_list)
return mon
def process_trainer_data(self, data):
mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon
or self.world.trainer_legendaries[self.player].value]
address = rom_addresses["Trainer_Data"]
while address < rom_addresses["Trainer_Data_End"]:
if data[address] == 255:
mode = 1
else:
mode = 0
while True:
address += 1
if data[address] == 0:
address += 1
break
address += mode
mon = None
for i in range(1, 4):
for l in ["A", "B", "C", "D", "E", "F", "G", "H"]:
if rom_addresses[f"Rival_Starter{i}_{l}"] == address:
mon = " ".join(self.world.get_location(f"Pallet Town - Starter {i}", self.player).item.name.split()[1:])
if l in ["D", "E", "F", "G", "H"] and mon in poke_data.evolves_to:
mon = poke_data.evolves_to[mon]
if l in ["F", "G", "H"] and mon in poke_data.evolves_to:
mon = poke_data.evolves_to[mon]
if mon is None and self.world.randomize_trainer_parties[self.player].value:
mon = poke_data.id_to_mon[data[address]]
mon = randomize_pokemon(self, mon, mons_list, self.world.randomize_trainer_parties[self.player].value)
if mon is not None:
data[address] = poke_data.pokemon_data[mon]["id"]
def process_static_pokemon(self):
starter_slots = [location for location in location_data if location.type == "Starter Pokemon"]
legendary_slots = [location for location in location_data if location.type == "Legendary Pokemon"]
static_slots = [location for location in location_data if location.type in
["Static Pokemon", "Missable Pokemon"]]
legendary_mons = [slot.original_item for slot in legendary_slots]
tower_6F_mons = set()
for i in range(1, 11):
tower_6F_mons.add(self.world.get_location(f"Pokemon Tower 6F - Wild Pokemon - {i}", self.player).item.name)
mons_list = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon
or self.world.randomize_legendary_pokemon[self.player].value == 3]
if self.world.randomize_legendary_pokemon[self.player].value == 0:
for slot in legendary_slots:
location = self.world.get_location(slot.name, self.player)
location.place_locked_item(self.create_item("Missable " + slot.original_item))
elif self.world.randomize_legendary_pokemon[self.player].value == 1:
self.world.random.shuffle(legendary_mons)
for slot in legendary_slots:
location = self.world.get_location(slot.name, self.player)
location.place_locked_item(self.create_item("Missable " + legendary_mons.pop()))
elif self.world.randomize_legendary_pokemon[self.player].value == 2:
static_slots = static_slots + legendary_slots
self.world.random.shuffle(static_slots)
static_slots.sort(key=lambda s: 0 if s.name == "Pokemon Tower 6F - Restless Soul" else 1)
while legendary_slots:
swap_slot = legendary_slots.pop()
slot = static_slots.pop()
slot_type = slot.type.split()[0]
if slot_type == "Legendary":
slot_type = "Missable"
location = self.world.get_location(slot.name, self.player)
location.place_locked_item(self.create_item(slot_type + " " + swap_slot.original_item))
swap_slot.original_item = slot.original_item
elif self.world.randomize_legendary_pokemon[self.player].value == 3:
static_slots = static_slots + legendary_slots
for slot in static_slots:
location = self.world.get_location(slot.name, self.player)
randomize_type = self.world.randomize_static_pokemon[self.player].value
slot_type = slot.type.split()[0]
if slot_type == "Legendary":
slot_type = "Missable"
if not randomize_type:
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
else:
mon = self.create_item(slot_type + " " +
randomize_pokemon(self, slot.original_item, mons_list, randomize_type))
while location.name == "Pokemon Tower 6F - Restless Soul" and mon in tower_6F_mons:
mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list,
randomize_type))
location.place_locked_item(mon)
for slot in starter_slots:
location = self.world.get_location(slot.name, self.player)
randomize_type = self.world.randomize_starter_pokemon[self.player].value
slot_type = "Missable"
if not randomize_type:
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
else:
location.place_locked_item(self.create_item(slot_type + " " +
randomize_pokemon(self, slot.original_item, mons_list, randomize_type)))
def process_wild_pokemon(self):
encounter_slots = get_encounter_slots(self)
placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()}
if self.world.randomize_wild_pokemon[self.player].value:
mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon
or self.world.randomize_legendary_pokemon[self.player].value == 3]
self.world.random.shuffle(encounter_slots)
locations = []
for slot in encounter_slots:
mon = randomize_pokemon(self, slot.original_item, mons_list, self.world.randomize_wild_pokemon[self.player].value)
# if static Pokemon are not randomized, we make sure nothing on Pokemon Tower 6F is a Marowak
# if static Pokemon are randomized we deal with that during static encounter randomization
while (self.world.randomize_static_pokemon[self.player].value == 0 and mon == "Marowak"
and "Pokemon Tower 6F" in slot.name):
# to account for the possibility that only one ground type Pokemon exists, match only stats for this fix
mon = randomize_pokemon(self, slot.original_item, mons_list, 2)
placed_mons[mon] += 1
location = self.world.get_location(slot.name, self.player)
location.item = self.create_item(mon)
location.event = True
location.locked = True
location.item.location = location
locations.append(location)
mons_to_add = []
remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and
(pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3)]
if self.world.catch_em_all[self.player].value == 1:
mons_to_add = [pokemon for pokemon in poke_data.first_stage_pokemon if placed_mons[pokemon] == 0 and
(pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3)]
elif self.world.catch_em_all[self.player].value == 2:
mons_to_add = remaining_pokemon.copy()
logic_needed_mons = max(self.world.oaks_aide_rt_2[self.player].value,
self.world.oaks_aide_rt_11[self.player].value,
self.world.oaks_aide_rt_15[self.player].value)
if self.world.accessibility[self.player] == "minimal":
logic_needed_mons = 0
self.world.random.shuffle(remaining_pokemon)
while (len([pokemon for pokemon in placed_mons if placed_mons[pokemon] > 0])
+ len(mons_to_add) < logic_needed_mons):
mons_to_add.append(remaining_pokemon.pop())
for mon in mons_to_add:
stat_base = get_base_stat_total(mon)
candidate_locations = get_encounter_slots(self)
if self.world.randomize_wild_pokemon[self.player].value in [1, 3]:
candidate_locations = [slot for slot in candidate_locations if any([poke_data.pokemon_data[slot.original_item][
"type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]],
poke_data.pokemon_data[slot.original_item]["type2"] in [self.local_poke_data[mon]["type1"],
self.local_poke_data[mon]["type2"]]])]
if not candidate_locations:
candidate_locations = location_data
candidate_locations = [self.world.get_location(location.name, self.player) for location in candidate_locations]
candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.item.name) - stat_base))
for location in candidate_locations:
if placed_mons[location.item.name] > 1 or location.item.name not in poke_data.first_stage_pokemon:
placed_mons[location.item.name] -= 1
location.item = self.create_item(mon)
location.item.location = location
placed_mons[mon] += 1
break
else:
for slot in encounter_slots:
location = self.world.get_location(slot.name, self.player)
location.item = self.create_item(slot.original_item)
location.event = True
location.locked = True
location.item.location = location
placed_mons[location.item.name] += 1
def process_pokemon_data(self):
local_poke_data = deepcopy(poke_data.pokemon_data)
learnsets = deepcopy(poke_data.learnsets)
for mon, mon_data in local_poke_data.items():
if self.world.randomize_pokemon_stats[self.player].value == 1:
stats = [mon_data["hp"], mon_data["atk"], mon_data["def"], mon_data["spd"], mon_data["spc"]]
self.world.random.shuffle(stats)
mon_data["hp"] = stats[0]
mon_data["atk"] = stats[1]
mon_data["def"] = stats[2]
mon_data["spd"] = stats[3]
mon_data["spc"] = stats[4]
elif self.world.randomize_pokemon_stats[self.player].value == 2:
old_stats = mon_data["hp"] + mon_data["atk"] + mon_data["def"] + mon_data["spd"] + mon_data["spc"] - 5
stats = [1, 1, 1, 1, 1]
while old_stats > 0:
stat = self.world.random.randint(0, 4)
if stats[stat] < 255:
old_stats -= 1
stats[stat] += 1
mon_data["hp"] = stats[0]
mon_data["atk"] = stats[1]
mon_data["def"] = stats[2]
mon_data["spd"] = stats[3]
mon_data["spc"] = stats[4]
if self.world.randomize_pokemon_types[self.player].value:
if self.world.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from:
type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"]
type2 = local_poke_data[poke_data.evolves_from[mon]]["type2"]
if type1 == type2:
if self.world.secondary_type_chance[self.player].value == -1:
if mon_data["type1"] != mon_data["type2"]:
while type2 == type1:
type2 = self.world.random.choice(list(poke_data.type_names.values()))
elif self.world.random.randint(1, 100) <= self.world.secondary_type_chance[self.player].value:
type2 = self.world.random.choice(list(poke_data.type_names.values()))
else:
type1 = self.world.random.choice(list(poke_data.type_names.values()))
type2 = type1
if ((self.world.secondary_type_chance[self.player].value == -1 and mon_data["type1"]
!= mon_data["type2"]) or self.world.random.randint(1, 100)
<= self.world.secondary_type_chance[self.player].value):
while type2 == type1:
type2 = self.world.random.choice(list(poke_data.type_names.values()))
mon_data["type1"] = type1
mon_data["type2"] = type2
if self.world.randomize_pokemon_movesets[self.player].value:
if self.world.randomize_pokemon_movesets[self.player].value == 1:
if mon_data["type1"] == "Normal" and mon_data["type2"] == "Normal":
chances = [[75, "Normal"]]
elif mon_data["type1"] == "Normal" or mon_data["type2"] == "Normal":
if mon_data["type1"] == "Normal":
second_type = mon_data["type2"]
else:
second_type = mon_data["type1"]
chances = [[30, "Normal"], [85, second_type]]
elif mon_data["type1"] == mon_data["type2"]:
chances = [[60, mon_data["type1"]], [80, "Normal"]]
else:
chances = [[50, mon_data["type1"]], [80, mon_data["type2"]], [85, "Normal"]]
else:
chances = []
moves = list(poke_data.moves.keys())
for move in ["No Move"] + poke_data.hm_moves:
moves.remove(move)
mon_data["start move 1"] = get_move(moves, chances, self.world.random, True)
for i in range(2, 5):
if mon_data[f"start move {i}"] != "No Move" or self.world.start_with_four_moves[
self.player].value == 1:
mon_data[f"start move {i}"] = get_move(moves, chances, self.world.random)
if mon in learnsets:
for move_num in range(0, len(learnsets[mon])):
learnsets[mon][move_num] = get_move(moves, chances, self.world.random)
if self.world.randomize_pokemon_catch_rates[self.player].value:
mon_data["catch rate"] = self.world.random.randint(self.world.minimum_catch_rate[self.player], 255)
else:
mon_data["catch rate"] = max(self.world.minimum_catch_rate[self.player], mon_data["catch rate"])
if mon in poke_data.evolves_from.keys() and mon_data["type1"] == local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] == local_poke_data[poke_data.evolves_from[mon]]["type2"]:
mon_data["tms"] = local_poke_data[poke_data.evolves_from[mon]]["tms"]
elif mon != "Mew":
tms_hms = poke_data.tm_moves + poke_data.hm_moves
for flag, tm_move in enumerate(tms_hms):
if (flag < 50 and self.world.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 1):
type_match = poke_data.moves[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]]
bit = int(self.world.random.randint(1, 100) < [[90, 50, 25], [100, 75, 25]][flag >= 50][0 if type_match else 1 if poke_data.moves[tm_move]["type"] == "Normal" else 2])
elif (flag < 50 and self.world.tm_compatibility[self.player].value == 2) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 2):
bit = [0, 1][self.world.random.randint(0, 1)]
elif (flag < 50 and self.world.tm_compatibility[self.player].value == 3) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 3):
bit = 1
else:
continue
if bit:
mon_data["tms"][int(flag / 8)] |= 1 << (flag % 8)
else:
mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8))
self.local_poke_data = local_poke_data
self.learnsets = learnsets
def generate_output(self, output_directory: str):
random = self.world.slot_seeds[self.player]
game_version = self.world.game_version[self.player].current_key
data = bytearray(get_base_rom_bytes(game_version))
basemd5 = hashlib.md5()
basemd5.update(data)
for location in self.world.get_locations():
if location.player != self.player or location.rom_address is None:
continue
if location.item and location.item.player == self.player:
if location.rom_address:
rom_address = location.rom_address
if not isinstance(rom_address, list):
rom_address = [rom_address]
for address in rom_address:
if location.item.name in poke_data.pokemon_data.keys():
data[address] = poke_data.pokemon_data[location.item.name]["id"]
elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys():
data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"]
else:
data[address] = self.item_name_to_id[location.item.name] - 172000000
else:
data[location.rom_address] = 0x2C # AP Item
data[rom_addresses['Fly_Location']] = self.fly_map_code
if self.world.tea[self.player].value:
data[rom_addresses["Option_Tea"]] = 1
data[rom_addresses["Guard_Drink_List"]] = 0x54
data[rom_addresses["Guard_Drink_List"] + 1] = 0
data[rom_addresses["Guard_Drink_List"] + 2] = 0
if self.world.extra_key_items[self.player].value:
data[rom_addresses['Options']] |= 4
data[rom_addresses["Option_Blind_Trainers"]] = round(self.world.blind_trainers[self.player].value * 2.55)
data[rom_addresses['Option_Cerulean_Cave_Condition']] = self.world.cerulean_cave_condition[self.player].value
data[rom_addresses['Option_Encounter_Minimum_Steps']] = self.world.minimum_steps_between_encounters[self.player].value
data[rom_addresses['Option_Victory_Road_Badges']] = self.world.victory_road_condition[self.player].value
data[rom_addresses['Option_Pokemon_League_Badges']] = self.world.elite_four_condition[self.player].value
data[rom_addresses['Option_Viridian_Gym_Badges']] = self.world.viridian_gym_condition[self.player].value
data[rom_addresses['Option_EXP_Modifier']] = self.world.exp_modifier[self.player].value
if not self.world.require_item_finder[self.player].value:
data[rom_addresses['Option_Itemfinder']] = 0
if self.world.extra_strength_boulders[self.player].value:
for i in range(0, 3):
data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15
if self.world.extra_key_items[self.player].value:
for i in range(0, 4):
data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15
if self.world.old_man[self.player].value == 2:
data[rom_addresses['Option_Old_Man']] = 0x11
data[rom_addresses['Option_Old_Man_Lying']] = 0x15
money = str(self.world.starting_money[self.player].value)
while len(money) < 6:
money = "0" + money
data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16)
data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16)
data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16)
data[rom_addresses["Text_Badges_Needed"]] = encode_text(
str(max(self.world.victory_road_condition[self.player].value,
self.world.elite_four_condition[self.player].value)))[0]
if self.world.badges_needed_for_hm_moves[self.player].value == 0:
for hm_move in poke_data.hm_moves:
write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
rom_addresses["HM_" + hm_move + "_Badge_a"])
elif self.extra_badges:
written_badges = {}
for hm_move, badge in self.extra_badges.items():
data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F,
"Thunder Badge": 0x57, "Rainbow Badge": 0x5F,
"Soul Badge": 0x67, "Marsh Badge": 0x6F,
"Volcano Badge": 0x77, "Earth Badge": 0x7F}[badge]
move_text = hm_move
if badge not in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
move_text = ", " + move_text
rom_address = rom_addresses["Badge_Text_" + badge.replace(" ", "_")]
if badge in written_badges:
rom_address += len(written_badges[badge])
move_text = ", " + move_text
write_bytes(data, encode_text(move_text.upper()), rom_address)
written_badges[badge] = move_text
for badge in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
if badge not in written_badges:
write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")])
chart = deepcopy(poke_data.type_chart)
if self.world.randomize_type_matchup_types[self.player].value == 1:
attacking_types = []
defending_types = []
for matchup in chart:
attacking_types.append(matchup[0])
defending_types.append(matchup[1])
random.shuffle(attacking_types)
random.shuffle(defending_types)
matchups = []
while len(attacking_types) > 0:
if [attacking_types[0], defending_types[0]] not in matchups:
matchups.append([attacking_types.pop(0), defending_types.pop(0)])
else:
matchup = matchups.pop(0)
attacking_types.append(matchup[0])
defending_types.append(matchup[1])
random.shuffle(attacking_types)
random.shuffle(defending_types)
for matchup, chart_row in zip(matchups, chart):
chart_row[0] = matchup[0]
chart_row[1] = matchup[1]
elif self.world.randomize_type_matchup_types[self.player].value == 2:
used_matchups = []
for matchup in chart:
matchup[0] = random.choice(list(poke_data.type_names.values()))
matchup[1] = random.choice(list(poke_data.type_names.values()))
while [matchup[0], matchup[1]] in used_matchups:
matchup[0] = random.choice(list(poke_data.type_names.values()))
matchup[1] = random.choice(list(poke_data.type_names.values()))
used_matchups.append([matchup[0], matchup[1]])
if self.world.randomize_type_matchup_type_effectiveness[self.player].value == 1:
effectiveness_list = []
for matchup in chart:
effectiveness_list.append(matchup[2])
random.shuffle(effectiveness_list)
for (matchup, effectiveness) in zip(chart, effectiveness_list):
matchup[2] = effectiveness
elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 2:
for matchup in chart:
matchup[2] = random.choice([0] + ([5, 20] * 5))
elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 3:
for matchup in chart:
matchup[2] = random.choice([i for i in range(0, 21) if i != 10])
type_loc = rom_addresses["Type_Chart"]
for matchup in chart:
data[type_loc] = poke_data.type_ids[matchup[0]]
data[type_loc + 1] = poke_data.type_ids[matchup[1]]
data[type_loc + 2] = matchup[2]
type_loc += 3
# sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective"
# matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to
# damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes
# to the way effectiveness messages are generated.
self.type_chart = sorted(chart, key=lambda matchup: 0 - matchup[2])
if self.world.normalize_encounter_chances[self.player].value:
chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255]
for i, chance in enumerate(chances):
data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance
for mon, mon_data in self.local_poke_data.items():
if mon == "Mew":
address = rom_addresses["Base_Stats_Mew"]
else:
address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1))
data[address + 1] = self.local_poke_data[mon]["hp"]
data[address + 2] = self.local_poke_data[mon]["atk"]
data[address + 3] = self.local_poke_data[mon]["def"]
data[address + 4] = self.local_poke_data[mon]["spd"]
data[address + 5] = self.local_poke_data[mon]["spc"]
data[address + 6] = poke_data.type_ids[self.local_poke_data[mon]["type1"]]
data[address + 7] = poke_data.type_ids[self.local_poke_data[mon]["type2"]]
data[address + 8] = self.local_poke_data[mon]["catch rate"]
data[address + 15] = poke_data.moves[self.local_poke_data[mon]["start move 1"]]["id"]
data[address + 16] = poke_data.moves[self.local_poke_data[mon]["start move 2"]]["id"]
data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"]
data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"]
write_bytes(data, self.local_poke_data[mon]["tms"], address + 20)
if mon in self.learnsets:
address = rom_addresses["Learnset_" + mon.replace(" ", "")]
for i, move in enumerate(self.learnsets[mon]):
data[(address + 1) + i * 2] = poke_data.moves[move]["id"]
data[rom_addresses["Option_Aide_Rt2"]] = self.world.oaks_aide_rt_2[self.player]
data[rom_addresses["Option_Aide_Rt11"]] = self.world.oaks_aide_rt_11[self.player]
data[rom_addresses["Option_Aide_Rt15"]] = self.world.oaks_aide_rt_15[self.player]
if self.world.safari_zone_normal_battles[self.player].value == 1:
data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255
if self.world.reusable_tms[self.player].value:
data[rom_addresses["Option_Reusable_TMs"]] = 0xC9
process_trainer_data(self, data)
mons = [mon["id"] for mon in poke_data.pokemon_data.values()]
random.shuffle(mons)
data[rom_addresses['Title_Mon_First']] = mons.pop()
for mon in range(0, 16):
data[rom_addresses['Title_Mons'] + mon] = mons.pop()
if self.world.game_version[self.player].value:
mons.sort(key=lambda mon: 0 if mon == self.world.get_location("Pallet Town - Starter 1", self.player).item.name
else 1 if mon == self.world.get_location("Pallet Town - Starter 2", self.player).item.name else
2 if mon == self.world.get_location("Pallet Town - Starter 3", self.player).item.name else 3)
else:
mons.sort(key=lambda mon: 0 if mon == self.world.get_location("Pallet Town - Starter 2", self.player).item.name
else 1 if mon == self.world.get_location("Pallet Town - Starter 1", self.player).item.name else
2 if mon == self.world.get_location("Pallet Town - Starter 3", self.player).item.name else 3)
write_bytes(data, encode_text(self.world.seed_name, 20, True), rom_addresses['Title_Seed'])
slot_name = self.world.player_name[self.player]
slot_name.replace("@", " ")
slot_name.replace("<", " ")
slot_name.replace(">", " ")
write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name'])
write_bytes(data, self.trainer_name, rom_addresses['Player_Name'])
write_bytes(data, self.rival_name, rom_addresses['Rival_Name'])
write_bytes(data, basemd5.digest(), 0xFFCC)
write_bytes(data, self.world.seed_name.encode(), 0xFFDC)
write_bytes(data, self.world.player_name[self.player].encode(), 0xFFF0)
outfilepname = f'_P{self.player}'
outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}" \
if self.world.player_name[self.player] != 'Player%d' % self.player else ''
rompath = os.path.join(output_directory, f'AP_{self.world.seed_name}{outfilepname}.gb')
with open(rompath, 'wb') as outfile:
outfile.write(data)
if self.world.game_version[self.player].current_key == "red":
patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=self.player,
player_name=self.world.player_name[self.player], patched_path=rompath)
else:
patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=self.player,
player_name=self.world.player_name[self.player], patched_path=rompath)
patch.write()
os.unlink(rompath)
def write_bytes(data, byte_array, address):
for byte in byte_array:
data[address] = byte
address += 1
def get_base_rom_bytes(game_version: str, hash: str="") -> bytes:
file_name = get_base_rom_path(game_version)
with open(file_name, "rb") as file:
base_rom_bytes = bytes(file.read())
if hash:
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if hash != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. '
'Get the correct game and version, then dump it')
with open(os.path.join(os.path.dirname(__file__), f'basepatch_{game_version}.bsdiff4'), 'rb') as stream:
base_patch = bytes(stream.read())
base_rom_bytes = bsdiff4.patch(base_rom_bytes, base_patch)
return base_rom_bytes
def get_base_rom_path(game_version: str) -> str:
options = Utils.get_options()
file_name = options["pokemon_rb_options"][f"{game_version}_rom_file"]
if not os.path.exists(file_name):
file_name = Utils.local_path(file_name)
return file_name
class BlueDeltaPatch(APDeltaPatch):
patch_file_ending = ".apblue"
hash = "50927e843568814f7ed45ec4f944bd8b"
game_version = "blue"
game = "Pokemon Red and Blue"
result_file_ending = ".gb"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes(cls.game_version, cls.hash)
class RedDeltaPatch(APDeltaPatch):
patch_file_ending = ".apred"
hash = "3d45c1ee9abd5738df46d2bdda8b57dc"
game_version = "red"
game = "Pokemon Red and Blue"
result_file_ending = ".gb"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes(cls.game_version, cls.hash)

View File

@@ -0,0 +1,588 @@
rom_addresses = {
"Option_Encounter_Minimum_Steps": 0x3c3,
"Option_Blind_Trainers": 0x317e,
"Base_Stats_Mew": 0x425b,
"Title_Mon_First": 0x436e,
"Title_Mons": 0x4547,
"Player_Name": 0x4569,
"Rival_Name": 0x4571,
"Title_Seed": 0x5dfe,
"Title_Slot_Name": 0x5e1e,
"PC_Item": 0x61ec,
"PC_Item_Quantity": 0x61f1,
"Options": 0x61f9,
"Fly_Location": 0x61fe,
"Option_Old_Man": 0xcaef,
"Option_Old_Man_Lying": 0xcaf2,
"Option_Boulders": 0xcd98,
"Option_Rock_Tunnel_Extra_Items": 0xcda1,
"Wild_Route1": 0xd0fb,
"Wild_Route2": 0xd111,
"Wild_Route22": 0xd127,
"Wild_ViridianForest": 0xd13d,
"Wild_Route3": 0xd153,
"Wild_MtMoon1F": 0xd169,
"Wild_MtMoonB1F": 0xd17f,
"Wild_MtMoonB2F": 0xd195,
"Wild_Route4": 0xd1ab,
"Wild_Route24": 0xd1c1,
"Wild_Route25": 0xd1d7,
"Wild_Route9": 0xd1ed,
"Wild_Route5": 0xd203,
"Wild_Route6": 0xd219,
"Wild_Route11": 0xd22f,
"Wild_RockTunnel1F": 0xd245,
"Wild_RockTunnelB1F": 0xd25b,
"Wild_Route10": 0xd271,
"Wild_Route12": 0xd287,
"Wild_Route8": 0xd29d,
"Wild_Route7": 0xd2b3,
"Wild_PokemonTower3F": 0xd2cd,
"Wild_PokemonTower4F": 0xd2e3,
"Wild_PokemonTower5F": 0xd2f9,
"Wild_PokemonTower6F": 0xd30f,
"Wild_PokemonTower7F": 0xd325,
"Wild_Route13": 0xd33b,
"Wild_Route14": 0xd351,
"Wild_Route15": 0xd367,
"Wild_Route16": 0xd37d,
"Wild_Route17": 0xd393,
"Wild_Route18": 0xd3a9,
"Wild_SafariZoneCenter": 0xd3bf,
"Wild_SafariZoneEast": 0xd3d5,
"Wild_SafariZoneNorth": 0xd3eb,
"Wild_SafariZoneWest": 0xd401,
"Wild_SeaRoutes": 0xd418,
"Wild_SeafoamIslands1F": 0xd42d,
"Wild_SeafoamIslandsB1F": 0xd443,
"Wild_SeafoamIslandsB2F": 0xd459,
"Wild_SeafoamIslandsB3F": 0xd46f,
"Wild_SeafoamIslandsB4F": 0xd485,
"Wild_PokemonMansion1F": 0xd49b,
"Wild_PokemonMansion2F": 0xd4b1,
"Wild_PokemonMansion3F": 0xd4c7,
"Wild_PokemonMansionB1F": 0xd4dd,
"Wild_Route21": 0xd4f3,
"Wild_Surf_Route21": 0xd508,
"Wild_CeruleanCave1F": 0xd51d,
"Wild_CeruleanCave2F": 0xd533,
"Wild_CeruleanCaveB1F": 0xd549,
"Wild_PowerPlant": 0xd55f,
"Wild_Route23": 0xd575,
"Wild_VictoryRoad2F": 0xd58b,
"Wild_VictoryRoad3F": 0xd5a1,
"Wild_VictoryRoad1F": 0xd5b7,
"Wild_DiglettsCave": 0xd5cd,
"Ghost_Battle5": 0xd723,
"HM_Surf_Badge_a": 0xda11,
"HM_Surf_Badge_b": 0xda16,
"Wild_Old_Rod": 0xe313,
"Wild_Good_Rod": 0xe340,
"Option_Reusable_TMs": 0xe60c,
"Wild_Super_Rod_A": 0xea40,
"Wild_Super_Rod_B": 0xea45,
"Wild_Super_Rod_C": 0xea4a,
"Wild_Super_Rod_D": 0xea51,
"Wild_Super_Rod_E": 0xea56,
"Wild_Super_Rod_F": 0xea5b,
"Wild_Super_Rod_G": 0xea64,
"Wild_Super_Rod_H": 0xea6d,
"Wild_Super_Rod_I": 0xea76,
"Wild_Super_Rod_J": 0xea7f,
"Starting_Money_High": 0xf949,
"Starting_Money_Middle": 0xf94c,
"Starting_Money_Low": 0xf94f,
"HM_Fly_Badge_a": 0x1318e,
"HM_Fly_Badge_b": 0x13193,
"HM_Cut_Badge_a": 0x131c4,
"HM_Cut_Badge_b": 0x131c9,
"HM_Strength_Badge_a": 0x131f4,
"HM_Strength_Badge_b": 0x131f9,
"HM_Flash_Badge_a": 0x13208,
"HM_Flash_Badge_b": 0x1320d,
"Encounter_Chances": 0x13911,
"Option_Viridian_Gym_Badges": 0x1901d,
"Event_Sleepy_Guy": 0x191bc,
"Starter2_K": 0x195a8,
"Starter3_K": 0x195b0,
"Event_Rocket_Thief": 0x196cc,
"Option_Cerulean_Cave_Condition": 0x1986c,
"Event_Stranded_Man": 0x19b2b,
"Event_Rivals_Sister": 0x19cf9,
"Option_Pokemon_League_Badges": 0x19e16,
"Missable_Silph_Co_4F_Item_1": 0x1a0d7,
"Missable_Silph_Co_4F_Item_2": 0x1a0de,
"Missable_Silph_Co_4F_Item_3": 0x1a0e5,
"Missable_Silph_Co_5F_Item_1": 0x1a337,
"Missable_Silph_Co_5F_Item_2": 0x1a33e,
"Missable_Silph_Co_5F_Item_3": 0x1a345,
"Missable_Silph_Co_6F_Item_1": 0x1a5ad,
"Missable_Silph_Co_6F_Item_2": 0x1a5b4,
"Event_Free_Sample": 0x1cade,
"Starter1_F": 0x1cca5,
"Starter2_F": 0x1cca9,
"Starter2_G": 0x1cde2,
"Starter3_G": 0x1cdea,
"Starter2_H": 0x1d0e5,
"Starter1_H": 0x1d0ef,
"Starter3_I": 0x1d0f6,
"Starter2_I": 0x1d100,
"Starter1_D": 0x1d107,
"Starter3_D": 0x1d111,
"Starter2_E": 0x1d2eb,
"Starter3_E": 0x1d2f3,
"Event_Oaks_Gift": 0x1d373,
"Event_Pokemart_Quest": 0x1d566,
"Event_Bicycle_Shop": 0x1d805,
"Text_Bicycle": 0x1d898,
"Event_Fuji": 0x1d9cd,
"Static_Encounter_Mew": 0x1dc4e,
"Gift_Eevee": 0x1dcc7,
"Event_Mr_Psychic": 0x1ddcf,
"Static_Encounter_Voltorb_A": 0x1e397,
"Static_Encounter_Voltorb_B": 0x1e39f,
"Static_Encounter_Voltorb_C": 0x1e3a7,
"Static_Encounter_Electrode_A": 0x1e3af,
"Static_Encounter_Voltorb_D": 0x1e3b7,
"Static_Encounter_Voltorb_E": 0x1e3bf,
"Static_Encounter_Electrode_B": 0x1e3c7,
"Static_Encounter_Voltorb_F": 0x1e3cf,
"Static_Encounter_Zapdos": 0x1e3d7,
"Missable_Power_Plant_Item_1": 0x1e3df,
"Missable_Power_Plant_Item_2": 0x1e3e6,
"Missable_Power_Plant_Item_3": 0x1e3ed,
"Missable_Power_Plant_Item_4": 0x1e3f4,
"Missable_Power_Plant_Item_5": 0x1e3fb,
"Event_Rt16_House_Woman": 0x1e5d4,
"Option_Victory_Road_Badges": 0x1e6a5,
"Event_Bill": 0x1e8d6,
"Starter1_O": 0x372b0,
"Starter2_O": 0x372b4,
"Starter3_O": 0x372b8,
"Base_Stats": 0x383de,
"Starter3_C": 0x39cf2,
"Starter1_C": 0x39cf8,
"Trainer_Data": 0x39d99,
"Rival_Starter2_A": 0x3a1e5,
"Rival_Starter3_A": 0x3a1e8,
"Rival_Starter1_A": 0x3a1eb,
"Rival_Starter2_B": 0x3a1f1,
"Rival_Starter3_B": 0x3a1f7,
"Rival_Starter1_B": 0x3a1fd,
"Rival_Starter2_C": 0x3a207,
"Rival_Starter3_C": 0x3a211,
"Rival_Starter1_C": 0x3a21b,
"Rival_Starter2_D": 0x3a409,
"Rival_Starter3_D": 0x3a413,
"Rival_Starter1_D": 0x3a41d,
"Rival_Starter2_E": 0x3a429,
"Rival_Starter3_E": 0x3a435,
"Rival_Starter1_E": 0x3a441,
"Rival_Starter2_F": 0x3a44d,
"Rival_Starter3_F": 0x3a459,
"Rival_Starter1_F": 0x3a465,
"Rival_Starter2_G": 0x3a473,
"Rival_Starter3_G": 0x3a481,
"Rival_Starter1_G": 0x3a48f,
"Rival_Starter2_H": 0x3a49d,
"Rival_Starter3_H": 0x3a4ab,
"Rival_Starter1_H": 0x3a4b9,
"Trainer_Data_End": 0x3a52e,
"Learnset_Rhydon": 0x3b1d9,
"Learnset_Kangaskhan": 0x3b1e7,
"Learnset_NidoranM": 0x3b1f6,
"Learnset_Clefairy": 0x3b208,
"Learnset_Spearow": 0x3b219,
"Learnset_Voltorb": 0x3b228,
"Learnset_Nidoking": 0x3b234,
"Learnset_Slowbro": 0x3b23c,
"Learnset_Ivysaur": 0x3b24f,
"Learnset_Exeggutor": 0x3b25f,
"Learnset_Lickitung": 0x3b263,
"Learnset_Exeggcute": 0x3b273,
"Learnset_Grimer": 0x3b284,
"Learnset_Gengar": 0x3b292,
"Learnset_NidoranF": 0x3b29b,
"Learnset_Nidoqueen": 0x3b2a9,
"Learnset_Cubone": 0x3b2b4,
"Learnset_Rhyhorn": 0x3b2c3,
"Learnset_Lapras": 0x3b2d1,
"Learnset_Mew": 0x3b2e1,
"Learnset_Gyarados": 0x3b2eb,
"Learnset_Shellder": 0x3b2fb,
"Learnset_Tentacool": 0x3b30a,
"Learnset_Gastly": 0x3b31f,
"Learnset_Scyther": 0x3b325,
"Learnset_Staryu": 0x3b337,
"Learnset_Blastoise": 0x3b347,
"Learnset_Pinsir": 0x3b355,
"Learnset_Tangela": 0x3b363,
"Learnset_Growlithe": 0x3b379,
"Learnset_Onix": 0x3b385,
"Learnset_Fearow": 0x3b391,
"Learnset_Pidgey": 0x3b3a0,
"Learnset_Slowpoke": 0x3b3b1,
"Learnset_Kadabra": 0x3b3c9,
"Learnset_Graveler": 0x3b3e1,
"Learnset_Chansey": 0x3b3ef,
"Learnset_Machoke": 0x3b407,
"Learnset_MrMime": 0x3b413,
"Learnset_Hitmonlee": 0x3b41f,
"Learnset_Hitmonchan": 0x3b42b,
"Learnset_Arbok": 0x3b437,
"Learnset_Parasect": 0x3b443,
"Learnset_Psyduck": 0x3b452,
"Learnset_Drowzee": 0x3b461,
"Learnset_Golem": 0x3b46f,
"Learnset_Magmar": 0x3b47f,
"Learnset_Electabuzz": 0x3b48f,
"Learnset_Magneton": 0x3b49b,
"Learnset_Koffing": 0x3b4ac,
"Learnset_Mankey": 0x3b4bd,
"Learnset_Seel": 0x3b4cc,
"Learnset_Diglett": 0x3b4db,
"Learnset_Tauros": 0x3b4e7,
"Learnset_Farfetchd": 0x3b4f9,
"Learnset_Venonat": 0x3b508,
"Learnset_Dragonite": 0x3b516,
"Learnset_Doduo": 0x3b52b,
"Learnset_Poliwag": 0x3b53c,
"Learnset_Jynx": 0x3b54a,
"Learnset_Moltres": 0x3b558,
"Learnset_Articuno": 0x3b560,
"Learnset_Zapdos": 0x3b568,
"Learnset_Meowth": 0x3b575,
"Learnset_Krabby": 0x3b584,
"Learnset_Vulpix": 0x3b59a,
"Learnset_Pikachu": 0x3b5ac,
"Learnset_Dratini": 0x3b5c1,
"Learnset_Dragonair": 0x3b5d0,
"Learnset_Kabuto": 0x3b5df,
"Learnset_Kabutops": 0x3b5e9,
"Learnset_Horsea": 0x3b5f6,
"Learnset_Seadra": 0x3b602,
"Learnset_Sandshrew": 0x3b615,
"Learnset_Sandslash": 0x3b621,
"Learnset_Omanyte": 0x3b630,
"Learnset_Omastar": 0x3b63a,
"Learnset_Jigglypuff": 0x3b648,
"Learnset_Eevee": 0x3b666,
"Learnset_Flareon": 0x3b670,
"Learnset_Jolteon": 0x3b682,
"Learnset_Vaporeon": 0x3b694,
"Learnset_Machop": 0x3b6a9,
"Learnset_Zubat": 0x3b6b8,
"Learnset_Ekans": 0x3b6c7,
"Learnset_Paras": 0x3b6d6,
"Learnset_Poliwhirl": 0x3b6e6,
"Learnset_Poliwrath": 0x3b6f4,
"Learnset_Beedrill": 0x3b704,
"Learnset_Dodrio": 0x3b714,
"Learnset_Primeape": 0x3b722,
"Learnset_Dugtrio": 0x3b72e,
"Learnset_Venomoth": 0x3b73a,
"Learnset_Dewgong": 0x3b748,
"Learnset_Butterfree": 0x3b762,
"Learnset_Machamp": 0x3b772,
"Learnset_Golduck": 0x3b780,
"Learnset_Hypno": 0x3b78c,
"Learnset_Golbat": 0x3b79a,
"Learnset_Mewtwo": 0x3b7a6,
"Learnset_Snorlax": 0x3b7b2,
"Learnset_Magikarp": 0x3b7bf,
"Learnset_Muk": 0x3b7c7,
"Learnset_Kingler": 0x3b7d7,
"Learnset_Cloyster": 0x3b7e3,
"Learnset_Electrode": 0x3b7e9,
"Learnset_Weezing": 0x3b7f7,
"Learnset_Persian": 0x3b803,
"Learnset_Marowak": 0x3b80f,
"Learnset_Haunter": 0x3b827,
"Learnset_Alakazam": 0x3b832,
"Learnset_Pidgeotto": 0x3b843,
"Learnset_Pidgeot": 0x3b851,
"Learnset_Bulbasaur": 0x3b864,
"Learnset_Venusaur": 0x3b874,
"Learnset_Tentacruel": 0x3b884,
"Learnset_Goldeen": 0x3b89b,
"Learnset_Seaking": 0x3b8a9,
"Learnset_Ponyta": 0x3b8c2,
"Learnset_Rapidash": 0x3b8d0,
"Learnset_Rattata": 0x3b8e1,
"Learnset_Raticate": 0x3b8eb,
"Learnset_Nidorino": 0x3b8f9,
"Learnset_Nidorina": 0x3b90b,
"Learnset_Geodude": 0x3b91c,
"Learnset_Porygon": 0x3b92a,
"Learnset_Aerodactyl": 0x3b934,
"Learnset_Magnemite": 0x3b942,
"Learnset_Charmander": 0x3b957,
"Learnset_Squirtle": 0x3b968,
"Learnset_Charmeleon": 0x3b979,
"Learnset_Wartortle": 0x3b98a,
"Learnset_Charizard": 0x3b998,
"Learnset_Oddish": 0x3b9b1,
"Learnset_Gloom": 0x3b9c3,
"Learnset_Vileplume": 0x3b9d1,
"Learnset_Bellsprout": 0x3b9dc,
"Learnset_Weepinbell": 0x3b9f0,
"Learnset_Victreebel": 0x3ba00,
"Type_Chart": 0x3e4b6,
"Type_Chart_Divider": 0x3e5ac,
"Ghost_Battle3": 0x3efd9,
"Missable_Pokemon_Mansion_1F_Item_1": 0x443d6,
"Missable_Pokemon_Mansion_1F_Item_2": 0x443dd,
"Map_Rock_TunnelF": 0x44676,
"Missable_Victory_Road_3F_Item_1": 0x44b07,
"Missable_Victory_Road_3F_Item_2": 0x44b0e,
"Missable_Rocket_Hideout_B1F_Item_1": 0x44d2d,
"Missable_Rocket_Hideout_B1F_Item_2": 0x44d34,
"Missable_Rocket_Hideout_B2F_Item_1": 0x4511d,
"Missable_Rocket_Hideout_B2F_Item_2": 0x45124,
"Missable_Rocket_Hideout_B2F_Item_3": 0x4512b,
"Missable_Rocket_Hideout_B2F_Item_4": 0x45132,
"Missable_Rocket_Hideout_B3F_Item_1": 0x4536f,
"Missable_Rocket_Hideout_B3F_Item_2": 0x45376,
"Missable_Rocket_Hideout_B4F_Item_1": 0x45627,
"Missable_Rocket_Hideout_B4F_Item_2": 0x4562e,
"Missable_Rocket_Hideout_B4F_Item_3": 0x45635,
"Missable_Rocket_Hideout_B4F_Item_4": 0x4563c,
"Missable_Rocket_Hideout_B4F_Item_5": 0x45643,
"Missable_Safari_Zone_East_Item_1": 0x458b2,
"Missable_Safari_Zone_East_Item_2": 0x458b9,
"Missable_Safari_Zone_East_Item_3": 0x458c0,
"Missable_Safari_Zone_East_Item_4": 0x458c7,
"Missable_Safari_Zone_North_Item_1": 0x45a12,
"Missable_Safari_Zone_North_Item_2": 0x45a19,
"Missable_Safari_Zone_Center_Item": 0x45bf9,
"Missable_Cerulean_Cave_2F_Item_1": 0x45e36,
"Missable_Cerulean_Cave_2F_Item_2": 0x45e3d,
"Missable_Cerulean_Cave_2F_Item_3": 0x45e44,
"Static_Encounter_Mewtwo": 0x45f44,
"Missable_Cerulean_Cave_B1F_Item_1": 0x45f4c,
"Missable_Cerulean_Cave_B1F_Item_2": 0x45f53,
"Missable_Rock_Tunnel_B1F_Item_1": 0x4619f,
"Missable_Rock_Tunnel_B1F_Item_2": 0x461a6,
"Missable_Rock_Tunnel_B1F_Item_3": 0x461ad,
"Missable_Rock_Tunnel_B1F_Item_4": 0x461b4,
"Static_Encounter_Articuno": 0x4690c,
"Hidden_Item_Viridian_Forest_1": 0x46e6d,
"Hidden_Item_Viridian_Forest_2": 0x46e73,
"Hidden_Item_MtMoonB2F_1": 0x46e7a,
"Hidden_Item_MtMoonB2F_2": 0x46e80,
"Hidden_Item_Route_25_1": 0x46e94,
"Hidden_Item_Route_25_2": 0x46e9a,
"Hidden_Item_Route_9": 0x46ea1,
"Hidden_Item_SS_Anne_Kitchen": 0x46eb4,
"Hidden_Item_SS_Anne_B1F": 0x46ebb,
"Hidden_Item_Route_10_1": 0x46ec2,
"Hidden_Item_Route_10_2": 0x46ec8,
"Hidden_Item_Rocket_Hideout_B1F": 0x46ecf,
"Hidden_Item_Rocket_Hideout_B3F": 0x46ed6,
"Hidden_Item_Rocket_Hideout_B4F": 0x46edd,
"Hidden_Item_Pokemon_Tower_5F": 0x46ef1,
"Hidden_Item_Route_13_1": 0x46ef8,
"Hidden_Item_Route_13_2": 0x46efe,
"Hidden_Item_Safari_Zone_West": 0x46f0c,
"Hidden_Item_Silph_Co_5F": 0x46f13,
"Hidden_Item_Silph_Co_9F": 0x46f1a,
"Hidden_Item_Copycats_House": 0x46f21,
"Hidden_Item_Cerulean_Cave_1F": 0x46f28,
"Hidden_Item_Cerulean_Cave_B1F": 0x46f2f,
"Hidden_Item_Power_Plant_1": 0x46f36,
"Hidden_Item_Power_Plant_2": 0x46f3c,
"Hidden_Item_Seafoam_Islands_B2F": 0x46f43,
"Hidden_Item_Seafoam_Islands_B4F": 0x46f4a,
"Hidden_Item_Pokemon_Mansion_1F": 0x46f51,
"Hidden_Item_Pokemon_Mansion_3F": 0x46f65,
"Hidden_Item_Pokemon_Mansion_B1F": 0x46f72,
"Hidden_Item_Route_23_1": 0x46f85,
"Hidden_Item_Route_23_2": 0x46f8b,
"Hidden_Item_Route_23_3": 0x46f91,
"Hidden_Item_Victory_Road_2F_1": 0x46f98,
"Hidden_Item_Victory_Road_2F_2": 0x46f9e,
"Hidden_Item_Unused_6F": 0x46fa5,
"Hidden_Item_Viridian_City": 0x46fb3,
"Hidden_Item_Route_11": 0x47060,
"Hidden_Item_Route_12": 0x47067,
"Hidden_Item_Route_17_1": 0x47075,
"Hidden_Item_Route_17_2": 0x4707b,
"Hidden_Item_Route_17_3": 0x47081,
"Hidden_Item_Route_17_4": 0x47087,
"Hidden_Item_Route_17_5": 0x4708d,
"Hidden_Item_Underground_Path_NS_1": 0x47094,
"Hidden_Item_Underground_Path_NS_2": 0x4709a,
"Hidden_Item_Underground_Path_WE_1": 0x470a1,
"Hidden_Item_Underground_Path_WE_2": 0x470a7,
"Hidden_Item_Celadon_City": 0x470ae,
"Hidden_Item_Seafoam_Islands_B3F": 0x470b5,
"Hidden_Item_Vermilion_City": 0x470bc,
"Hidden_Item_Cerulean_City": 0x470c3,
"Hidden_Item_Route_4": 0x470ca,
"Event_Counter": 0x482d3,
"Event_Thirsty_Girl_Lemonade": 0x484f9,
"Event_Thirsty_Girl_Soda": 0x4851d,
"Event_Thirsty_Girl_Water": 0x48541,
"Option_Tea": 0x4871d,
"Event_Mansion_Lady": 0x4872a,
"Badge_Celadon_Gym": 0x48a1b,
"Event_Celadon_Gym": 0x48a2f,
"Event_Gambling_Addict": 0x49293,
"Gift_Magikarp": 0x49430,
"Option_Aide_Rt11": 0x4958d,
"Event_Rt11_Oaks_Aide": 0x49591,
"Event_Mourning_Girl": 0x4968b,
"Option_Aide_Rt15": 0x49776,
"Event_Rt_15_Oaks_Aide": 0x4977a,
"Missable_Mt_Moon_1F_Item_1": 0x49c75,
"Missable_Mt_Moon_1F_Item_2": 0x49c7c,
"Missable_Mt_Moon_1F_Item_3": 0x49c83,
"Missable_Mt_Moon_1F_Item_4": 0x49c8a,
"Missable_Mt_Moon_1F_Item_5": 0x49c91,
"Missable_Mt_Moon_1F_Item_6": 0x49c98,
"Dome_Fossil_Text": 0x4a001,
"Event_Dome_Fossil": 0x4a021,
"Helix_Fossil_Text": 0x4a05d,
"Event_Helix_Fossil": 0x4a07d,
"Missable_Mt_Moon_B2F_Item_1": 0x4a166,
"Missable_Mt_Moon_B2F_Item_2": 0x4a16d,
"Missable_Safari_Zone_West_Item_1": 0x4a34f,
"Missable_Safari_Zone_West_Item_2": 0x4a356,
"Missable_Safari_Zone_West_Item_3": 0x4a35d,
"Missable_Safari_Zone_West_Item_4": 0x4a364,
"Event_Safari_Zone_Secret_House": 0x4a469,
"Missable_Route_24_Item": 0x506e6,
"Missable_Route_25_Item": 0x5080b,
"Starter2_B": 0x50fce,
"Starter3_B": 0x50fd0,
"Starter1_B": 0x50fd2,
"Starter2_A": 0x510f1,
"Starter3_A": 0x510f3,
"Starter1_A": 0x510f5,
"Option_Badge_Goal": 0x51317,
"Event_Nugget_Bridge": 0x5148f,
"Static_Encounter_Moltres": 0x51939,
"Missable_Victory_Road_2F_Item_1": 0x51941,
"Missable_Victory_Road_2F_Item_2": 0x51948,
"Missable_Victory_Road_2F_Item_3": 0x5194f,
"Missable_Victory_Road_2F_Item_4": 0x51956,
"Starter2_L": 0x51c85,
"Starter3_L": 0x51c8d,
"Gift_Lapras": 0x51d83,
"Missable_Silph_Co_7F_Item_1": 0x51f0d,
"Missable_Silph_Co_7F_Item_2": 0x51f14,
"Missable_Pokemon_Mansion_2F_Item": 0x520c9,
"Missable_Pokemon_Mansion_3F_Item_1": 0x522e2,
"Missable_Pokemon_Mansion_3F_Item_2": 0x522e9,
"Missable_Pokemon_Mansion_B1F_Item_1": 0x5248c,
"Missable_Pokemon_Mansion_B1F_Item_2": 0x52493,
"Missable_Pokemon_Mansion_B1F_Item_3": 0x5249a,
"Missable_Pokemon_Mansion_B1F_Item_4": 0x524a1,
"Missable_Pokemon_Mansion_B1F_Item_5": 0x524ae,
"Option_Safari_Zone_Battle_Type": 0x525c3,
"Prize_Mon_A2": 0x5282f,
"Prize_Mon_B2": 0x52830,
"Prize_Mon_C2": 0x52831,
"Prize_Mon_D2": 0x5283a,
"Prize_Mon_E2": 0x5283b,
"Prize_Mon_F2": 0x5283c,
"Prize_Mon_A": 0x52960,
"Prize_Mon_B": 0x52962,
"Prize_Mon_C": 0x52964,
"Prize_Mon_D": 0x52966,
"Prize_Mon_E": 0x52968,
"Prize_Mon_F": 0x5296a,
"Missable_Route_2_Item_1": 0x5404a,
"Missable_Route_2_Item_2": 0x54051,
"Missable_Route_4_Item": 0x543df,
"Missable_Route_9_Item": 0x546fd,
"Option_EXP_Modifier": 0x552c5,
"Rod_Vermilion_City_Fishing_Guru": 0x560df,
"Rod_Fuchsia_City_Fishing_Brother": 0x561eb,
"Rod_Route12_Fishing_Brother": 0x564ee,
"Missable_Route_12_Item_1": 0x58704,
"Missable_Route_12_Item_2": 0x5870b,
"Missable_Route_15_Item": 0x589c7,
"Ghost_Battle6": 0x58df0,
"Static_Encounter_Snorlax_A": 0x5969b,
"Static_Encounter_Snorlax_B": 0x599db,
"Event_Pokemon_Fan_Club": 0x59c8b,
"Event_Scared_Woman": 0x59e1f,
"Missable_Silph_Co_3F_Item": 0x5a0cb,
"Missable_Silph_Co_10F_Item_1": 0x5a281,
"Missable_Silph_Co_10F_Item_2": 0x5a288,
"Missable_Silph_Co_10F_Item_3": 0x5a28f,
"Guard_Drink_List": 0x5a600,
"Event_Museum": 0x5c266,
"Badge_Pewter_Gym": 0x5c3ed,
"Event_Pewter_Gym": 0x5c401,
"Badge_Cerulean_Gym": 0x5c716,
"Event_Cerulean_Gym": 0x5c72a,
"Badge_Vermilion_Gym": 0x5caba,
"Event_Vermillion_Gym": 0x5cace,
"Event_Copycat": 0x5cca9,
"Gift_Hitmonlee": 0x5cf1a,
"Gift_Hitmonchan": 0x5cf62,
"Badge_Saffron_Gym": 0x5d079,
"Event_Saffron_Gym": 0x5d08d,
"Option_Aide_Rt2": 0x5d5f2,
"Event_Route_2_Oaks_Aide": 0x5d5f6,
"Missable_Victory_Road_1F_Item_1": 0x5dae6,
"Missable_Victory_Road_1F_Item_2": 0x5daed,
"Starter2_J": 0x6060e,
"Starter3_J": 0x60616,
"Missable_Pokemon_Tower_3F_Item": 0x60787,
"Missable_Pokemon_Tower_4F_Item_1": 0x608b5,
"Missable_Pokemon_Tower_4F_Item_2": 0x608bc,
"Missable_Pokemon_Tower_4F_Item_3": 0x608c3,
"Missable_Pokemon_Tower_5F_Item": 0x60a80,
"Ghost_Battle1": 0x60b33,
"Ghost_Battle2": 0x60c0a,
"Missable_Pokemon_Tower_6F_Item_1": 0x60c85,
"Missable_Pokemon_Tower_6F_Item_2": 0x60c8c,
"Gift_Aerodactyl": 0x61064,
"Gift_Omanyte": 0x61068,
"Gift_Kabuto": 0x6106c,
"Missable_Viridian_Forest_Item_1": 0x6122c,
"Missable_Viridian_Forest_Item_2": 0x61233,
"Missable_Viridian_Forest_Item_3": 0x6123a,
"Starter2_M": 0x61450,
"Starter3_M": 0x61458,
"Event_SS_Anne_Captain": 0x618c3,
"Missable_SS_Anne_1F_Item": 0x61ac0,
"Missable_SS_Anne_2F_Item_1": 0x61ced,
"Missable_SS_Anne_2F_Item_2": 0x61d00,
"Missable_SS_Anne_B1F_Item_1": 0x61ee3,
"Missable_SS_Anne_B1F_Item_2": 0x61eea,
"Missable_SS_Anne_B1F_Item_3": 0x61ef1,
"Event_Silph_Co_President": 0x622ed,
"Ghost_Battle4": 0x708e1,
"Badge_Viridian_Gym": 0x749ca,
"Event_Viridian_Gym": 0x749de,
"Missable_Viridian_Gym_Item": 0x74c63,
"Missable_Cerulean_Cave_1F_Item_1": 0x74d68,
"Missable_Cerulean_Cave_1F_Item_2": 0x74d6f,
"Missable_Cerulean_Cave_1F_Item_3": 0x74d76,
"Event_Warden": 0x7512a,
"Missable_Wardens_House_Item": 0x751b7,
"Badge_Fuchsia_Gym": 0x755cd,
"Event_Fuschia_Gym": 0x755e1,
"Badge_Cinnabar_Gym": 0x75995,
"Event_Cinnabar_Gym": 0x759a9,
"Event_Lab_Scientist": 0x75dd6,
"Fossils_Needed_For_Second_Item": 0x75ea3,
"Event_Dome_Fossil_B": 0x75f20,
"Event_Helix_Fossil_B": 0x75f40,
"Starter2_N": 0x76169,
"Starter3_N": 0x76171,
"Option_Itemfinder": 0x76864,
"Text_Badges_Needed": 0x92304,
"Badge_Text_Boulder_Badge": 0x990b3,
"Badge_Text_Cascade_Badge": 0x990cb,
"Badge_Text_Thunder_Badge": 0x99111,
"Badge_Text_Rainbow_Badge": 0x9912e,
"Badge_Text_Soul_Badge": 0x99177,
"Badge_Text_Marsh_Badge": 0x9918c,
"Badge_Text_Volcano_Badge": 0x991d6,
"Badge_Text_Earth_Badge": 0x991f3,
}

165
worlds/pokemon_rb/rules.py Normal file
View File

@@ -0,0 +1,165 @@
from ..generic.Rules import add_item_rule, add_rule
def set_rules(world, player):
add_item_rule(world.get_location("Pallet Town - Player's PC", player),
lambda i: i.player == player and "Badge" not in i.name)
access_rules = {
"Pallet Town - Rival's Sister": lambda state: state.has("Oak's Parcel", player),
"Pallet Town - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player),
"Viridian City - Sleepy Guy": lambda state: state.pokemon_rb_can_cut(player) or state.pokemon_rb_can_surf(player),
"Route 2 - Oak's Aide": lambda state: state.pokemon_rb_has_pokemon(state.world.oaks_aide_rt_2[player].value + 5, player),
"Pewter City - Museum": lambda state: state.pokemon_rb_can_cut(player),
"Cerulean City - Bicycle Shop": lambda state: state.has("Bike Voucher", player),
"Lavender Town - Mr. Fuji": lambda state: state.has("Fuji Saved", player),
"Vermilion Gym - Lt. Surge 1": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)),
"Vermilion Gym - Lt. Surge 2": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)),
"Route 11 - Oak's Aide": lambda state: state.pokemon_rb_has_pokemon(state.world.oaks_aide_rt_11[player].value + 5, player),
"Celadon City - Stranded Man": lambda state: state.pokemon_rb_can_surf(player),
"Silph Co 11F - Silph Co President": lambda state: state.has("Card Key", player),
"Fuchsia City - Safari Zone Warden": lambda state: state.has("Gold Teeth", player),
"Route 12 - Island Item": lambda state: state.pokemon_rb_can_surf(player),
"Route 12 - Item Behind Cuttable Tree": lambda state: state.pokemon_rb_can_cut(player),
"Route 15 - Item": lambda state: state.pokemon_rb_can_cut(player),
"Route 25 - Item": lambda state: state.pokemon_rb_can_cut(player),
"Fuchsia City - Warden's House Item": lambda state: state.pokemon_rb_can_strength(player),
"Rocket Hideout B4F - Southwest Item (Lift Key)": lambda state: state.has("Lift Key", player),
"Rocket Hideout B4F - Giovanni Item (Lift Key)": lambda state: state.has("Lift Key", player),
"Silph Co 3F - Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 4F - Left Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 4F - Middle Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 4F - Right Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 5F - Northwest Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 6F - West Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 6F - Southwest Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 7F - East Item (Card Key)": lambda state: state.has("Card Key", player),
"Safari Zone Center - Island Item": lambda state: state.pokemon_rb_can_surf(player),
"Silph Co 11F - Silph Co Liberated": lambda state: state.has("Card Key", player),
"Pallet Town - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Pallet Town - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 22 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 22 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 24 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 24 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 24 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Route 6 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 6 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 10 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 10 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Safari Zone Center - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Safari Zone Center - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Safari Zone Center - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Safari Zone Center - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
"Route 12 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 12 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 12 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Route 12 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
"Route 19 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 19 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 19 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Route 19 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
"Route 23 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 23 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 23 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Route 23 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
"Fuchsia City - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Fuchsia City - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Fuchsia City - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Fuchsia City - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
"Anywhere - Good Rod Pokemon - 1": lambda state: state.has("Good Rod", player),
"Anywhere - Good Rod Pokemon - 2": lambda state: state.has("Good Rod", player),
"Anywhere - Old Rod Pokemon": lambda state: state.has("Old Rod", player),
"Celadon Prize Corner - Pokemon Prize - 1": lambda state: state.has("Coin Case", player),
"Celadon Prize Corner - Pokemon Prize - 2": lambda state: state.has("Coin Case", player),
"Celadon Prize Corner - Pokemon Prize - 3": lambda state: state.has("Coin Case", player),
"Celadon Prize Corner - Pokemon Prize - 4": lambda state: state.has("Coin Case", player),
"Celadon Prize Corner - Pokemon Prize - 5": lambda state: state.has("Coin Case", player),
"Celadon Prize Corner - Pokemon Prize - 6": lambda state: state.has("Coin Case", player),
"Cinnabar Island - Old Amber Pokemon": lambda state: state.has("Old Amber", player),
"Cinnabar Island - Helix Fossil Pokemon": lambda state: state.has("Helix Fossil", player),
"Cinnabar Island - Dome Fossil Pokemon": lambda state: state.has("Dome Fossil", player),
"Route 12 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player),
"Route 16 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player),
"Seafoam Islands B4F - Legendary Pokemon": lambda state: state.pokemon_rb_can_strength(player),
"Vermilion City - Legendary Pokemon": lambda state: state.pokemon_rb_can_surf(player) and state.has("S.S. Ticket", player)
}
hidden_item_access_rules = {
"Viridian Forest - Hidden Item Northwest by Trainer": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Viridian Forest - Hidden Item Entrance Tree": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 25 - Hidden Item Fence Outside Bill's House": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 9 - Hidden Item Rock By Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"S.S. Anne 1F - Hidden Item Kitchen Trash": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"S.S. Anne B1F - Hidden Item Under Pillow": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 10 - Hidden Item Behind Rock Tunnel Entrance Tree": lambda
state: state.pokemon_rb_can_get_hidden_items(player),
"Route 10 - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Rocket Hideout B3F - Hidden Item Near East Item": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 13 - Hidden Item Dead End Boulder": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 13 - Hidden Item Dead End By Water Corner": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Safari Zone West - Hidden Item Secret House Statue": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Silph Co 5F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Silph Co 9F - Hidden Item Nurse Bed": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Copycat's House - Hidden Item Desk": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Power Plant - Hidden Item Central Dead End": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Power Plant - Hidden Item Before Zapdos": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Seafoam Islands B2F - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Seafoam Islands B4F - Hidden Item Corner Island": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda
state: state.pokemon_rb_can_get_hidden_items(player),
"Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 23 - Hidden Item Rocks Before Final Guard": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 23 - Hidden Item East Tree After Water": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 23 - Hidden Item On Island": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Victory Road 2F - Hidden Item Rock In Final Room": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Viridian City - Hidden Item Cuttable Tree": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 11 - Hidden Item Isolated Tree Near Gate": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 12 - Hidden Item Tree Near Gate": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 17 - Hidden Item In Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 17 - Hidden Item Near Northernmost Sign": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 17 - Hidden Item East Center": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 17 - Hidden Item West Center": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 17 - Hidden Item Before Final Bridge": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Underground Tunnel North-South - Hidden Item Near Northern Stairs": lambda
state: state.pokemon_rb_can_get_hidden_items(player),
"Underground Tunnel North-South - Hidden Item Near Southern Stairs": lambda
state: state.pokemon_rb_can_get_hidden_items(player),
"Underground Tunnel West-East - Hidden Item West": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Underground Tunnel West-East - Hidden Item East": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 25 - Hidden Item Northeast Of Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Mt Moon B2F - Hidden Item Lone Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Seafoam Islands B3F - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Vermilion City - Hidden Item In Water Near Fan Club": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: state.pokemon_rb_can_get_hidden_items(player),
}
for loc, rule in access_rules.items():
add_rule(world.get_location(loc, player), rule)
if world.randomize_hidden_items[player].value != 0:
for loc, rule in hidden_item_access_rules.items():
add_rule(world.get_location(loc, player), rule)

147
worlds/pokemon_rb/text.py Normal file
View File

@@ -0,0 +1,147 @@
special_chars = {
"PKMN": 0x4A,
"'d": 0xBB,
"'l": 0xBC,
"'t": 0xBE,
"'v": 0xBF,
"PK": 0xE1,
"MN": 0xE2,
"'r": 0xE4,
"'m": 0xE5,
"MALE": 0xEF,
"FEMALE": 0xF5,
}
char_map = {
"@": 0x50, # String terminator
"#": 0x54, # Poké
"": 0x70,
"": 0x71,
"": 0x72,
"": 0x73,
"·": 0x74,
"": 0x75,
" ": 0x7F,
"A": 0x80,
"B": 0x81,
"C": 0x82,
"D": 0x83,
"E": 0x84,
"F": 0x85,
"G": 0x86,
"H": 0x87,
"I": 0x88,
"J": 0x89,
"K": 0x8A,
"L": 0x8B,
"M": 0x8C,
"N": 0x8D,
"O": 0x8E,
"P": 0x8F,
"Q": 0x90,
"R": 0x91,
"S": 0x92,
"T": 0x93,
"U": 0x94,
"V": 0x95,
"W": 0x96,
"X": 0x97,
"Y": 0x98,
"Z": 0x99,
"(": 0x9A,
")": 0x9B,
":": 0x9C,
";": 0x9D,
"[": 0x9E,
"]": 0x9F,
"a": 0xA0,
"b": 0xA1,
"c": 0xA2,
"d": 0xA3,
"e": 0xA4,
"f": 0xA5,
"g": 0xA6,
"h": 0xA7,
"i": 0xA8,
"j": 0xA9,
"k": 0xAA,
"l": 0xAB,
"m": 0xAC,
"n": 0xAD,
"o": 0xAE,
"p": 0xAF,
"q": 0xB0,
"r": 0xB1,
"s": 0xB2,
"t": 0xB3,
"u": 0xB4,
"v": 0xB5,
"w": 0xB6,
"x": 0xB7,
"y": 0xB8,
"z": 0xB9,
"é": 0xBA,
"'": 0xE0,
"-": 0xE3,
"?": 0xE6,
"!": 0xE7,
".": 0xE8,
"": 0xEF,
"¥": 0xF0,
"$": 0xF0,
"×": 0xF1,
"/": 0xF3,
",": 0xF4,
"": 0xF5,
"0": 0xF6,
"1": 0xF7,
"2": 0xF8,
"3": 0xF9,
"4": 0xFA,
"5": 0xFB,
"6": 0xFC,
"7": 0xFD,
"8": 0xFE,
"9": 0xFF,
}
unsafe_chars = ["@", "#", "PKMN"]
def encode_text(text: str, length: int=0, whitespace=False, force=False, safety=False):
encoded_text = bytearray()
spec_char = ""
special = False
for char in text:
if char == ">":
if spec_char in unsafe_chars and safety:
raise KeyError(f"Disallowed Pokemon text special character '<{spec_char}>'")
try:
encoded_text.append(special_chars[spec_char])
except KeyError:
if force:
encoded_text.append(char_map[" "])
else:
raise KeyError(f"Invalid Pokemon text special character '<{spec_char}>'")
spec_char = ""
special = False
elif char == "<":
spec_char = ""
special = True
elif special is True:
spec_char += char
else:
if char in unsafe_chars and safety:
raise KeyError(f"Disallowed Pokemon text character '{char}'")
try:
encoded_text.append(char_map[char])
except KeyError:
if force:
encoded_text.append(char_map[" "])
else:
raise KeyError(f"Invalid Pokemon text character '{char}'")
if length > 0:
encoded_text = encoded_text[:length]
while whitespace and len(encoded_text) < length:
encoded_text.append(char_map[" " if whitespace is True else whitespace])
return encoded_text

View File

@@ -15,7 +15,7 @@ missions. When you receive items, they will immediately become available, even d
notified via a text box in the top-right corner of the game screen. (The text client for StarCraft 2 also records all
items in all worlds.)
Missions are launched only through the text client. The Hyperion is never visited. Aditionally, credits are not used.
Missions are launched only through the text client. The Hyperion is never visited. Additionally, credits are not used.
## What is the goal of this game when randomized?

View File

@@ -13,9 +13,10 @@ to obtain a config file for StarCraft 2.
1. Install StarCraft 2 and Archipelago using the first two links above. (The StarCraft 2 client for Archipelago is
included by default.)
2. Click the third link above and follow the instructions there.
3. Linux users should also follow the instructions found at the bottom of this page
(["Running in Linux"](#running-in-linux)).
- Linux users should also follow the instructions found at the bottom of this page
(["Running in Linux"](#running-in-linux)).
2. Run ArchipelagoStarcraft2Client.exe.
3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above.
## Where do I get a config file (aka "YAML") for this game?
@@ -40,16 +41,9 @@ Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en
## The game isn't launching when I try to start a mission.
First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If the below fix doesn't
work for you, and you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2)
tech-support channel for help. Please include a specific description of what's going wrong and attach your log file to
your message.
### Check your installation
Make sure you've followed the installation instructions completely. Specifically, make sure that you've placed the Maps
and Mods folders directly inside the StarCraft II installation folder. They should be in the same location as the
SC2Data, Support, Support64, and Versions folders.
First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If you can't figure out
the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a
specific description of what's going wrong and attach your log file to your message.
## Running in Linux

View File

@@ -36,7 +36,7 @@ class SM64World(World):
location_name_to_id = location_table
data_version = 8
required_client_version = (0, 3, 0)
required_client_version = (0, 3, 5)
area_connections: typing.Dict[int, int]

View File

@@ -3,8 +3,10 @@
## Required Software
- Super Mario 64 US Rom (Japanese may work also. Europe and Shindou not supported)
- Either of [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or
- Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually.
- Either of
- [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or
- Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually
- Optional, for sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
NOTE: The above linked sm64pclauncher is a special version designed to work with the Archipelago build of sm64ex.
You can use other sm64-port based builds with it, but you can't use a different launcher with the Archipelago build of sm64ex.
@@ -25,7 +27,9 @@ Then follow the steps below
6. Set the location where you installed MSYS when prompted. Check the "Install Dependencies" Checkbox
7. Set the Repo link to `https://github.com/N00byKing/sm64ex` and the Branch to `archipelago` (Top two boxes). You can choose the folder (Secound Box) at will, as long as it does not exist yet
8. Point the Launcher to your Super Mario 64 US/JP Rom, and set the Region correspondingly
9. Set Build Options. Recommended: `-jn` where `n` is the Number of CPU Cores, to build faster.
9. Set Build Options and press build.
- Recommended: To build faster, use `-jn` where `n` is the number of CPU cores to use (e.g., `-j4` to use 4 cores).
- Optional: Add options from [this list](https://github.com/sm64pc/sm64ex/wiki/Build-options), separated by spaces (e.g., `-j4 BETTERCAMERA=1`).
10. SM64EX will now be compiled. The Launcher will appear to have crashed, but this is not likely the case. Best wait a bit, but there may be a problem if it takes longer than 10 Minutes
After it's done, the Build list should have another entry titled with what you named the folder in step 7.
@@ -55,6 +59,9 @@ In case you are using the Archipelago Website, the IP should be `archipelago.gg`
Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that.
Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text.
**Important:** You must start a new file for every new seed you play. Using `⭐x0` files is **not** sufficient.
Failing to use a new file may make some locations unavailable. However, this can be fixed without losing any progress by exiting and starting a new file.
# Playing offline
To play offline, first generate a seed on the game's settings page.
@@ -83,6 +90,11 @@ with its name.
When using a US Rom, the In-Game messages are missing some letters: `J Q V X Z` and `?`.
The Japanese Version should have no problem displaying these.
### Toad does not have an item for me.
This happens when you load an existing file that had already received an item from that toad.
To resolve this, exit and start from a `NEW` file. The server will automatically restore your progress.
### What happens if I lose connection?
SM64EX tries to reconnect a few times, so be patient.

Binary file not shown.

View File

@@ -7,7 +7,7 @@ from Options import Toggle, DefaultOnToggle, Option, Range, Choice
# "Play the randomizer in hardmode"
# display_name = "Hard Mode"
class DisableNonRandomizedPuzzles(DefaultOnToggle):
class DisableNonRandomizedPuzzles(Toggle):
"""Disables puzzles that cannot be randomized.
This includes many puzzles that heavily involve the environment, such as Shadows, Monastery or Orchard.
The lasers for those areas will be activated as you solve optional puzzles throughout the island."""
@@ -59,8 +59,9 @@ class ShuffleVaultBoxes(Toggle):
class ShufflePostgame(Toggle):
"""Adds locations into the pool that are guaranteed to become accessible before or at the same time as your goal.
Use this if you don't play with forfeit on victory."""
"""Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal.
Use this if you don't play with forfeit on victory. IMPORTANT NOTE: The possibility of your second
"Progressive Dots" showing up in the Caves is ignored, they will still be considered "postgame" in base settings."""
display_name = "Shuffle Postgame"
@@ -75,6 +76,13 @@ class VictoryCondition(Choice):
option_mountain_box_long = 3
class PuzzleRandomization(Choice):
"""Puzzles in this randomizer are randomly generated. This setting changes the difficulty/types of puzzles."""
display_name = "Puzzle Randomization"
option_sigma_normal = 0
option_sigma_expert = 1
class MountainLasers(Range):
"""Sets the amount of beams required to enter the final area."""
display_name = "Required Lasers for Mountain Entry"
@@ -108,8 +116,17 @@ class PuzzleSkipAmount(Range):
default = 5
class HintAmount(Range):
"""Adds hints to Audio Logs. Hints will have the same number of duplicates, as many as will fit. Remaining Audio
Logs will have junk hints."""
display_name = "Hints on Audio Logs"
range_start = 0
range_end = 49
default = 10
the_witness_options: Dict[str, type] = {
# "hard_mode": HardMode,
"puzzle_randomization": PuzzleRandomization,
"shuffle_symbols": ShuffleSymbols,
"shuffle_doors": ShuffleDoors,
"shuffle_lasers": ShuffleLasers,
@@ -123,6 +140,7 @@ the_witness_options: Dict[str, type] = {
"early_secret_area": EarlySecretArea,
"trap_percentage": TrapPercentage,
"puzzle_skip_amount": PuzzleSkipAmount,
"hint_amount": HintAmount,
}

View File

@@ -15,6 +15,8 @@ Progression:
71 - Black/White Squares
72 - Colored Squares
80 - Arrows
200 - Progressive Dots - Dots,Full Dots
260 - Progressive Stars - Stars,Stars + Same Colored Symbol
Usefuls:
101 - Functioning Brain - False

View File

@@ -120,7 +120,7 @@ Door - 0x03313 (Second Gate) - 0x032FF
Orchard End (Orchard):
Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE:
158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers
158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots
158653 - 0x0339E (Vault Box) - 0x0CC7B - True
158602 - 0x17CE7 (Discard) - True - Triangles
158076 - 0x00698 (Surface 1) - True - True
@@ -338,7 +338,7 @@ Keep 3rd Pressure Plate (Keep) - Keep 4th Pressure Plate - 0x01CD5:
158202 - 0x01CD3 (Pressure Plates 3) - 0x0A3BB - Shapers & Black/White Squares & Colored Squares
Door - 0x01CD5 (Pressure Plates 3 Exit) - 0x01CD3
Keep 4th Pressure Plate (Keep) - Keep - 0x09E3D - Keep Tower - 0x01D40:
Keep 4th Pressure Plate (Keep) - Shadows - 0x09E3D - Keep Tower - 0x01D40:
158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True
158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Shapers & Dots & Symmetry
Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F
@@ -358,7 +358,7 @@ Door - 0x04F8F (Tower Shortcut) - 0x0361B
158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots
Laser - 0x014BB (Laser) - 0x0360E | 0x03317
Outside Monastery (Monastery) - Main Island - True - Main Island - 0x0364E - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750:
Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750:
158207 - 0x03713 (Shortcut Panel) - True - True
Door - 0x0364E (Shortcut) - 0x03713
158208 - 0x00B10 (Entry Left) - True - True
@@ -390,11 +390,11 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8
158221 - 0x28AE3 (Vines) - 0x18590 - True
158222 - 0x28938 (Apple Tree) - 0x28AE3 - True
158223 - 0x079DF (Triple Exit) - 0x28938 - True
158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Rotated Shapers & Dots
158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Shapers & Dots
158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Shapers & Rotated Shapers & Dots
158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots
158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots
158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Rotated Shapers & Dots & Full Dots
158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Shapers & Dots & Full Dots
158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Shapers & Rotated Shapers & Dots & Full Dots
158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots & Full Dots
158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots & Full Dots
Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1
158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers
Door - 0x28A61 (Tinted Glass Door) - 0x28998
@@ -421,7 +421,7 @@ Town Red Rooftop (Town):
158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - True
Town Wooden Rooftop (Town):
158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser
158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser & Full Dots
Town Church (Town):
158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True
@@ -819,14 +819,14 @@ Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x
Door - 0x2D77D (Caves Entry) - 0x00FF8
158448 - 0x334E1 (Rock Control) - True - True
Caves (Caves) - Main Island - 0x2D73F - Main Island - 0x2D859 - Path to Challenge - 0x019A5:
Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Path to Challenge - 0x019A5:
158451 - 0x335AB (Elevator Inside Control) - True - Dots & Black/White Squares
158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Black/White Squares
158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Black/White Squares & Dots
158454 - 0x00190 (Blue Tunnel Right First 1) - True - Dots & Triangles
158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Dots & Triangles
158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Dots & Triangles
158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Dots & Triangles
158454 - 0x00190 (Blue Tunnel Right First 1) - True - Dots & Triangles & Full Dots
158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Dots & Triangles & Full Dots
158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Dots & Triangles & Full Dots
158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Dots & Triangles & Full Dots
158458 - 0x01A0D (Blue Tunnel Left First 1) - True - Symmetry & Triangles
158459 - 0x008B8 (Blue Tunnel Left Second 1) - True - Black/White Squares & Triangles
158460 - 0x00973 (Blue Tunnel Left Second 2) - 0x008B8 - Stars & Triangles
@@ -849,12 +849,12 @@ Caves (Caves) - Main Island - 0x2D73F - Main Island - 0x2D859 - Path to Challeng
158479 - 0x288FC (Second Wooden Beam) - True - Black/White Squares & Shapers & Rotated Shapers
158480 - 0x289E7 (Third Wooden Beam) - True - Stars & Black/White Squares
158481 - 0x288AA (Fourth Wooden Beam) - True - Stars & Shapers
158482 - 0x17FB9 (Left Upstairs Single) - True - Shapers & Dots & Negative Shapers
158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Dots
158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Stars & Dots
158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars
158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Shapers & Dots
158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Rotated Shapers & Dots
158482 - 0x17FB9 (Left Upstairs Single) - True - Shapers & Dots & Negative Shapers & Full Dots
158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Dots & Full Dots
158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Stars & Dots & Full Dots
158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars & Full Dots
158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Shapers & Dots & Full Dots
158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Rotated Shapers & Dots & Full Dots
158488 - 0x0008F (Right Upstairs Left Row 1) - True - Dots & Invisible Dots
158489 - 0x0006B (Right Upstairs Left Row 2) - 0x0008F - Dots & Invisible Dots
158490 - 0x0008B (Right Upstairs Left Row 3) - 0x0006B - Dots & Invisible Dots
@@ -913,7 +913,7 @@ Door - 0x09E87 (Town Shortcut) - 0x09E85
Final Room (Mountain Final Room) - Elevator - 0x339BB & 0x33961:
158522 - 0x0383A (Right Pillar 1) - True - Stars
158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots
158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots
158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Full Dots
158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry
158526 - 0x0383D (Left Pillar 1) - True - Dots
158527 - 0x0383F (Left Pillar 2) - 0x0383D - Black/White Squares

View File

@@ -0,0 +1,932 @@
First Hallway (First Hallway) - Entry - True - Tutorial - 0x00182:
158000 - 0x00064 (Straight) - True - True
158001 - 0x00182 (Bend) - 0x00064 - True
Tutorial (Tutorial) - Outside Tutorial - True:
158002 - 0x00293 (Front Center) - True - Dots
158003 - 0x00295 (Center Left) - 0x00293 - Dots
158004 - 0x002C2 (Front Left) - 0x00295 - Dots
158005 - 0x0A3B5 (Back Left) - True - Full Dots
158006 - 0x0A3B2 (Back Right) - True - Full Dots
158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 - Symmetry & Dots
158008 - 0x03505 (Gate Close) - 0x2FAF6 - False
158009 - 0x0C335 (Pillar) - True - Triangles - True
158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots
Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2:
158650 - 0x033D4 (Vault) - True - Full Dots & Squares & Black/White Squares
158651 - 0x03481 (Vault Box) - 0x033D4 - True
158013 - 0x0005D (Shed Row 1) - True - Full Dots
158014 - 0x0005E (Shed Row 2) - 0x0005D - Full Dots
158015 - 0x0005F (Shed Row 3) - 0x0005E - Full Dots
158016 - 0x00060 (Shed Row 4) - 0x0005F - Full Dots
158017 - 0x00061 (Shed Row 5) - 0x00060 - Full Dots
158018 - 0x018AF (Tree Row 1) - True - Squares & Black/White Squares
158019 - 0x0001B (Tree Row 2) - 0x018AF - Squares & Black/White Squares
158020 - 0x012C9 (Tree Row 3) - 0x0001B - Squares & Black/White Squares
158021 - 0x0001C (Tree Row 4) - 0x012C9 - Squares & Black/White Squares & Dots
158022 - 0x0001D (Tree Row 5) - 0x0001C - Squares & Black/White Squares & Dots
158023 - 0x0001E (Tree Row 6) - 0x0001D - Squares & Black/White Squares & Dots
158024 - 0x0001F (Tree Row 7) - 0x0001E - Squares & Black/White Squares & Full Dots
158025 - 0x00020 (Tree Row 8) - 0x0001F - Squares & Black/White Squares & Full Dots
158026 - 0x00021 (Tree Row 9) - 0x00020 - Squares & Black/White Squares & Full Dots
Door - 0x03BA2 (Outpost Path) - 0x0A3B5
Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170:
158011 - 0x0A171 (Outpost Entry Panel) - True - Full Dots & Triangles
Door - 0x0A170 (Outpost Entry) - 0x0A171
Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3:
158012 - 0x04CA4 (Outpost Exit Panel) - True - Full Dots & Shapers & Rotated Shapers
Door - 0x04CA3 (Outpost Exit) - 0x04CA4
158600 - 0x17CFB (Discard) - True - Arrows
Main Island () - Outside Tutorial - True:
Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29:
158027 - 0x01A54 (Entry Panel) - True - Symmetry
Door - 0x01A29 (Entry) - 0x01A54
158601 - 0x3C12B (Discard) - True - Arrows
Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0x0D7ED:
158028 - 0x00086 (Back Wall 1) - True - Symmetry & Dots
158029 - 0x00087 (Back Wall 2) - 0x00086 - Symmetry & Dots
158030 - 0x00059 (Back Wall 3) - 0x00087 - Symmetry & Dots
158031 - 0x00062 (Back Wall 4) - 0x00059 - Symmetry & Dots
158032 - 0x0005C (Back Wall 5) - 0x00062 - Symmetry & Dots
158033 - 0x0008D (Front 1) - 0x0005C - Symmetry
158034 - 0x00081 (Front 2) - 0x0008D - Symmetry
158035 - 0x00083 (Front 3) - 0x00081 - Symmetry
158036 - 0x00084 (Melting 1) - 0x00083 - Symmetry & Dots
158037 - 0x00082 (Melting 2) - 0x00084 - Symmetry & Dots
158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry & Dots
Door - 0x0D7ED (Back Wall) - 0x0005C
Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8:
158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat
Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E:
158040 - 0x000B0 (Lower Panel) - 0x0343A - Triangles
Door - 0x17F3E (Lower) - 0x000B0
Symmetry Island Lower (Symmetry Island) - Symmetry Island Upper - 0x18269:
158041 - 0x00022 (Right 1) - True - Symmetry & Triangles
158042 - 0x00023 (Right 2) - 0x00022 - Symmetry & Triangles
158043 - 0x00024 (Right 3) - 0x00023 - Symmetry & Triangles
158044 - 0x00025 (Right 4) - 0x00024 - Symmetry & Triangles
158045 - 0x00026 (Right 5) - 0x00025 - Symmetry & Triangles
158046 - 0x0007C (Back 1) - 0x00026 - Symmetry & Colored Dots & Dots
158047 - 0x0007E (Back 2) - 0x0007C - Symmetry & Colored Squares
158048 - 0x00075 (Back 3) - 0x0007E - Symmetry & Stars
158049 - 0x00073 (Back 4) - 0x00075 - Symmetry & Shapers
158050 - 0x00077 (Back 5) - 0x00073 - Symmetry & Triangles
158051 - 0x00079 (Back 6) - 0x00077 - Symmetry & Dots & Colored Dots & Eraser
158052 - 0x00065 (Left 1) - 0x00079 - Symmetry & Colored Dots & Triangles
158053 - 0x0006D (Left 2) - 0x00065 - Symmetry & Colored Dots & Triangles
158054 - 0x00072 (Left 3) - 0x0006D - Symmetry & Colored Dots & Triangles
158055 - 0x0006F (Left 4) - 0x00072 - Symmetry & Colored Dots & Triangles
158056 - 0x00070 (Left 5) - 0x0006F - Symmetry & Colored Dots & Triangles
158057 - 0x00071 (Left 6) - 0x00070 - Symmetry & Triangles
158058 - 0x00076 (Left 7) - 0x00071 - Symmetry & Triangles
158059 - 0x009B8 (Scenery Outlines 1) - True - Symmetry & Environment
158060 - 0x003E8 (Scenery Outlines 2) - 0x009B8 - Symmetry & Environment
158061 - 0x00A15 (Scenery Outlines 3) - 0x003E8 - Symmetry & Environment
158062 - 0x00B53 (Scenery Outlines 4) - 0x00A15 - Symmetry & Environment
158063 - 0x00B8D (Scenery Outlines 5) - 0x00B53 - Symmetry & Environment
158064 - 0x1C349 (Upper Panel) - 0x00076 - Symmetry & Triangles
Door - 0x18269 (Upper) - 0x1C349
Symmetry Island Upper (Symmetry Island):
158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots
158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots
158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots
158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots
158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots
158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots
158700 - 0x0360D (Laser Panel) - 0x00A68 - True
Laser - 0x00509 (Laser) - 0x0360D - True
Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307:
158071 - 0x00143 (Apple Tree 1) - True - Environment
158072 - 0x0003B (Apple Tree 2) - 0x00143 - Environment
158073 - 0x00055 (Apple Tree 3) - 0x0003B - Environment
Door - 0x03307 (First Gate) - 0x00055
Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313:
158074 - 0x032F7 (Apple Tree 4) - 0x00055 - Environment
158075 - 0x032FF (Apple Tree 5) - 0x032F7 - Environment
Door - 0x03313 (Second Gate) - 0x032FF
Orchard End (Orchard):
Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE:
158652 - 0x0CC7B (Vault) - True - Full Dots & Stars & Stars + Same Colored Symbol & Eraser & Triangles & Shapers & Negative Shapers & Colored Squares
158653 - 0x0339E (Vault Box) - 0x0CC7B - True
158602 - 0x17CE7 (Discard) - True - Arrows
158076 - 0x00698 (Surface 1) - True - Reflection
158077 - 0x0048F (Surface 2) - 0x00698 - Reflection
158078 - 0x09F92 (Surface 3) - 0x0048F & 0x09FA0 - Reflection
158079 - 0x09FA0 (Surface 3 Control) - 0x0048F - True
158080 - 0x0A036 (Surface 4) - 0x09F92 - Reflection
158081 - 0x09DA6 (Surface 5) - 0x09F92 - Reflection
158082 - 0x0A049 (Surface 6) - 0x09F92 - Reflection
158083 - 0x0A053 (Surface 7) - 0x0A036 & 0x09DA6 & 0x0A049 - Reflection
158084 - 0x09F94 (Surface 8) - 0x0A053 & 0x09F86 - Reflection
158085 - 0x09F86 (Surface 8 Control) - 0x0A053 - True
158086 - 0x0C339 (Light Room Entry Panel) - 0x09F94 - True
Door - 0x09FEE (Light Room Entry) - 0x0C339 - True
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
Laser - 0x012FB (Laser) - 0x03608
Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3:
158087 - 0x09FAA (Light Control) - True - True
158088 - 0x00422 (Light Room 1) - 0x09FAA - Reflection
158089 - 0x006E3 (Light Room 2) - 0x09FAA - Reflection
158090 - 0x0A02D (Light Room 3) - 0x09FAA & 0x00422 & 0x006E3 - Reflection
Door - 0x0C2C3 (Pond Room Entry) - 0x0A02D
Desert Pond Room (Desert) - Desert Water Levels Room - 0x0A24B:
158091 - 0x00C72 (Pond Room 1) - True - Reflection
158092 - 0x0129D (Pond Room 2) - 0x00C72 - Reflection
158093 - 0x008BB (Pond Room 3) - 0x0129D - Reflection
158094 - 0x0078D (Pond Room 4) - 0x008BB - Reflection
158095 - 0x18313 (Pond Room 5) - 0x0078D - Reflection
158096 - 0x0A249 (Flood Room Entry Panel) - 0x18313 - Reflection
Door - 0x0A24B (Flood Room Entry) - 0x0A249
Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316:
158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True
158098 - 0x1831E (Reduce Water Level Far Right) - True - True
158099 - 0x1C260 (Reduce Water Level Near Left) - True - True
158100 - 0x1831C (Reduce Water Level Near Right) - True - True
158101 - 0x1C2F3 (Raise Water Level Far Left) - True - True
158102 - 0x1831D (Raise Water Level Far Right) - True - True
158103 - 0x1C2B1 (Raise Water Level Near Left) - True - True
158104 - 0x1831B (Raise Water Level Near Right) - True - True
158105 - 0x04D18 (Flood Room 1) - 0x1C260 & 0x1831C - Reflection
158106 - 0x01205 (Flood Room 2) - 0x04D18 & 0x1C260 & 0x1831C - Reflection
158107 - 0x181AB (Flood Room 3) - 0x01205 & 0x1C260 & 0x1831C - Reflection
158108 - 0x0117A (Flood Room 4) - 0x181AB & 0x1C260 & 0x1831C - Reflection
158109 - 0x17ECA (Flood Room 5) - 0x0117A & 0x1C260 & 0x1831C - Reflection
158110 - 0x18076 (Flood Room 6) - 0x17ECA & 0x1C260 & 0x1831C - Reflection
Door - 0x0C316 (Elevator Room Entry) - 0x18076
Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB:
158111 - 0x17C31 (Final Transparent) - True - Reflection
158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - Reflection
158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True
158115 - 0x0A15C (Final Bent 1) - True - Reflection
158116 - 0x09FFF (Final Bent 2) - 0x0A15C - Reflection
158117 - 0x0A15F (Final Bent 3) - 0x09FFF - Reflection
Desert Lowest Level Inbetween Shortcuts (Desert):
Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F:
158118 - 0x09E57 (Entry 1 Panel) - True - Squares & Black/White Squares & Triangles
158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser
158603 - 0x17CF0 (Discard) - True - Arrows
158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Triangles & Stars & Stars + Same Colored Symbol
Laser - 0x01539 (Laser) - 0x03612
Door - 0x09D6F (Entry 1) - 0x09E57
Quarry Between Entrys (Quarry) - Quarry - 0x17C07:
158119 - 0x17C09 (Entry 2 Panel) - True - Shapers & Triangles
Door - 0x17C07 (Entry 2) - 0x17C09
Quarry (Quarry) - Quarry Mill Ground Floor - 0x02010:
158121 - 0x01E5A (Mill Entry Left Panel) - True - Squares & Black/White Squares & Stars & Stars + Same Colored Symbol
158122 - 0x01E59 (Mill Entry Right Panel) - True - Triangles
Door - 0x02010 (Mill Entry) - 0x01E59 & 0x01E5A
Quarry Mill Ground Floor (Quarry Mill) - Quarry - 0x275FF - Quarry Mill Middle Floor - 0x03678 - Outside Quarry - 0x17CE8:
158123 - 0x275ED (Side Exit Panel) - True - True
Door - 0x275FF (Side Exit) - 0x275ED
158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser
158145 - 0x17CAC (Roof Exit Panel) - True - True
Door - 0x17CE8 (Roof Exit) - 0x17CAC
Quarry Mill Middle Floor (Quarry Mill) - Quarry Mill Ground Floor - 0x03675 - Quarry Mill Upper Floor - 0x03679:
158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser
158126 - 0x01489 (Lower Row 2) - 0x00E0C - Triangles & Eraser
158127 - 0x0148A (Lower Row 3) - 0x01489 - Triangles & Eraser
158128 - 0x014D9 (Lower Row 4) - 0x0148A - Triangles & Eraser
158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Triangles & Eraser
158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Triangles & Eraser
158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser
Quarry Mill Upper Floor (Quarry Mill) - Quarry Mill Middle Floor - 0x03676 & 0x03679 - Quarry Mill Ground Floor - 0x0368A:
158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser
158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser
158134 - 0x00557 (Upper Row 1) - True - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
158135 - 0x005F1 (Upper Row 2) - 0x00557 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
158136 - 0x00620 (Upper Row 3) - 0x005F1 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
158137 - 0x009F5 (Upper Row 4) - 0x00620 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
158138 - 0x0146C (Upper Row 5) - 0x009F5 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
158139 - 0x3C12D (Upper Row 6) - 0x0146C - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
158140 - 0x03686 (Upper Row 7) - 0x3C12D - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
158141 - 0x014E9 (Upper Row 8) - 0x03686 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
158142 - 0x03677 (Stair Control) - True - Squares & Colored Squares & Eraser
Door - 0x0368A (Stairs) - 0x03677
158143 - 0x3C125 (Control Room Left) - 0x0367C - Squares & Black/White Squares & Full Dots & Eraser
158144 - 0x0367C (Control Room Right) - 0x014E9 - Squares & Colored Squares & Triangles & Eraser & Stars & Stars + Same Colored Symbol
Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B:
158146 - 0x034D4 (Intro Left) - True - Stars & Eraser
158147 - 0x021D5 (Intro Right) - True - Shapers & Eraser
158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers
158166 - 0x17CA6 (Boat Spawn) - True - Boat
Door - 0x2769B (Dock) - 0x17CA6
Door - 0x27163 (Dock Invis Barrier) - 0x17CA6
Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6:
Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50:
158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser & Negative Shapers
158150 - 0x021B4 (Front Row 2) - 0x021B3 - Shapers & Eraser & Negative Shapers
158151 - 0x021B0 (Front Row 3) - 0x021B4 - Shapers & Eraser & Negative Shapers
158152 - 0x021AF (Front Row 4) - 0x021B0 - Shapers & Eraser & Negative Shapers
158153 - 0x021AE (Front Row 5) - 0x021AF - Shapers & Eraser & Negative Shapers
Door - 0x17C50 (First Barrier) - 0x021AE
Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back - 0x03858:
158154 - 0x03858 (Ramp Horizontal Control) - True - Shapers & Eraser
Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F:
158155 - 0x38663 (Second Barrier Panel) - True - True
Door - 0x3865F (Second Barrier) - 0x38663
158156 - 0x021B5 (Back First Row 1) - True - Stars & Stars + Same Colored Symbol & Eraser
158157 - 0x021B6 (Back First Row 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser
158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser
158159 - 0x021BB (Back First Row 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser
158160 - 0x09DB5 (Back First Row 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser
158161 - 0x09DB1 (Back First Row 6) - 0x09DB5 - Eraser & Shapers
158162 - 0x3C124 (Back First Row 7) - 0x09DB1 - Eraser & Shapers
158163 - 0x09DB3 (Back First Row 8) - 0x3C124 - Eraser & Shapers & Stars & Stars + Same Colored Symbol
158164 - 0x09DB4 (Back First Row 9) - 0x09DB3 - Eraser & Shapers & Stars & Stars + Same Colored Symbol
158165 - 0x275FA (Hook Control) - True - Shapers & Eraser
158167 - 0x0A3CB (Back Second Row 1) - 0x09DB4 - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol
158168 - 0x0A3CC (Back Second Row 2) - 0x0A3CB - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol
158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol
Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665:
158170 - 0x334DB (Door Timer Outside) - True - True
Door - 0x19B24 (Timed Door) - 0x334DB
158171 - 0x0AC74 (Intro 6) - 0x0A8DC - Shadows Avoid
158172 - 0x0AC7A (Intro 7) - 0x0AC74 - Shadows Avoid
158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - Shadows Avoid
158174 - 0x386FA (Far 1) - 0x0A8E0 - Shadows Avoid & Environment
158175 - 0x1C33F (Far 2) - 0x386FA - Shadows Avoid & Environment
158176 - 0x196E2 (Far 3) - 0x1C33F - Shadows Avoid & Environment
158177 - 0x1972A (Far 4) - 0x196E2 - Shadows Avoid & Environment
158178 - 0x19809 (Far 5) - 0x1972A - Shadows Avoid & Environment
158179 - 0x19806 (Far 6) - 0x19809 - Shadows Avoid & Environment
158180 - 0x196F8 (Far 7) - 0x19806 - Shadows Avoid & Environment
158181 - 0x1972F (Far 8) - 0x196F8 - Shadows Avoid & Environment
Door - 0x194B2 (Laser Entry Right) - 0x1972F
158182 - 0x19797 (Near 1) - 0x0A8E0 - Shadows Follow
158183 - 0x1979A (Near 2) - 0x19797 - Shadows Follow
158184 - 0x197E0 (Near 3) - 0x1979A - Shadows Follow
158185 - 0x197E8 (Near 4) - 0x197E0 - Shadows Follow
158186 - 0x197E5 (Near 5) - 0x197E8 - Shadows Follow
Door - 0x19665 (Laser Entry Left) - 0x197E5
Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF:
158187 - 0x334DC (Door Timer Inside) - True - True
158188 - 0x198B5 (Intro 1) - True - Shadows Avoid
158189 - 0x198BD (Intro 2) - 0x198B5 - Shadows Avoid
158190 - 0x198BF (Intro 3) - 0x198BD & 0x334DC & 0x19B24 - Shadows Avoid
Door - 0x19865 (Quarry Barrier) - 0x198BF
Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF
158191 - 0x19771 (Intro 4) - 0x198BF - Shadows Avoid
158192 - 0x0A8DC (Intro 5) - 0x19771 - Shadows Avoid
Door - 0x1855B (Ledge Barrier) - 0x0A8DC
Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC
Shadows Laser Room (Shadows):
158703 - 0x19650 (Laser Panel) - True - Shadows Avoid & Shadows Follow
Laser - 0x181B3 (Laser) - 0x19650
Keep (Keep) - Main Island - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC:
158193 - 0x00139 (Hedge Maze 1) - True - Environment
158197 - 0x0A3A8 (Reset Pressure Plates 1) - True - True
158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Pressure Plates & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol
Door - 0x01954 (Hedge Maze 1 Exit) - 0x00139
Door - 0x01BEC (Pressure Plates 1 Exit) - 0x033EA
Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - True:
Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139
158194 - 0x019DC (Hedge Maze 2) - True - Environment
Door - 0x019D8 (Hedge Maze 2 Exit) - 0x019DC
Keep 3rd Maze (Keep) - Keep - 0x019B5 - Keep 4th Maze - 0x019E6:
Door - 0x019B5 (Hedge Maze 3 Shortcut) - 0x019DC
158195 - 0x019E7 (Hedge Maze 3) - True - Environment & Sound
Door - 0x019E6 (Hedge Maze 3 Exit) - 0x019E7
Keep 4th Maze (Keep) - Keep - 0x0199A - Keep Tower - 0x01A0E:
Door - 0x0199A (Hedge Maze 4 Shortcut) - 0x019E7
158196 - 0x01A0F (Hedge Maze 4) - True - Environment
Door - 0x01A0E (Hedge Maze 4 Exit) - 0x01A0F
Keep 2nd Pressure Plate (Keep) - Keep 3rd Pressure Plate - True:
158199 - 0x0A3B9 (Reset Pressure Plates 2) - True - True
158200 - 0x01BE9 (Pressure Plates 2) - PP2 Weirdness - Pressure Plates & Stars & Stars + Same Colored Symbol & Squares & Black/White Squares & Shapers & Rotated Shapers
Door - 0x01BEA (Pressure Plates 2 Exit) - 0x01BE9
Keep 3rd Pressure Plate (Keep) - Keep 4th Pressure Plate - 0x01CD5:
158201 - 0x0A3BB (Reset Pressure Plates 3) - True - True
158202 - 0x01CD3 (Pressure Plates 3) - 0x0A3BB - Pressure Plates & Black/White Squares & Triangles & Shapers & Rotated Shapers
Door - 0x01CD5 (Pressure Plates 3 Exit) - 0x01CD3
Keep 4th Pressure Plate (Keep) - Shadows - 0x09E3D - Keep Tower - 0x01D40:
158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True
158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Pressure Plates & Shapers & Triangles & Stars & Stars + Same Colored Symbol
Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F
158604 - 0x17D27 (Discard) - True - Arrows
158205 - 0x09E49 (Shadows Shortcut Panel) - True - True
Door - 0x09E3D (Shadows Shortcut) - 0x09E49
Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True:
158654 - 0x00AFB (Vault) - True - Symmetry & Sound & Sound Dots & Colored Dots
158655 - 0x03535 (Vault Box) - 0x00AFB - True
158605 - 0x17D28 (Discard) - True - Arrows
Keep Tower (Keep) - Keep - 0x04F8F:
158206 - 0x0361B (Tower Shortcut Panel) - True - True
Door - 0x04F8F (Tower Shortcut) - 0x0361B
158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - Environment & Sound
158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01BE9 - Shapers & Rotated Shapers & Triangles & Stars & Stars + Same Colored Symbol & Colored Squares & Black/White Squares
Laser - 0x014BB (Laser) - 0x0360E | 0x03317
Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750:
158207 - 0x03713 (Shortcut Panel) - True - True
Door - 0x0364E (Shortcut) - 0x03713
158208 - 0x00B10 (Entry Left) - True - True
158209 - 0x00C92 (Entry Right) - True - True
Door - 0x0C128 (Entry Inner) - 0x00B10
Door - 0x0C153 (Entry Outer) - 0x00C92
158210 - 0x00290 (Outside 1) - 0x09D9B - Environment
158211 - 0x00038 (Outside 2) - 0x09D9B & 0x00290 - Environment
158212 - 0x00037 (Outside 3) - 0x09D9B & 0x00038 - Environment
Door - 0x03750 (Garden Entry) - 0x00037
158706 - 0x17CA4 (Laser Panel) - 0x193A6 - True
Laser - 0x17C65 (Laser) - 0x17CA4
Inside Monastery (Monastery):
158213 - 0x09D9B (Shutters Control) - True - Dots
158214 - 0x193A7 (Inside 1) - 0x00037 - Environment
158215 - 0x193AA (Inside 2) - 0x193A7 - Environment
158216 - 0x193AB (Inside 3) - 0x193AA - Environment
158217 - 0x193A6 (Inside 4) - 0x193AB - Environment
Monastery Garden (Monastery):
Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9:
158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat
158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Squares & Black/White Squares & Shapers & Triangles
Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8
158707 - 0x09F98 (Desert Laser Redirect) - True - True
158220 - 0x18590 (Transparent) - True - Symmetry & Environment
158221 - 0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment
158222 - 0x28938 (Apple Tree) - 0x28AE3 - Environment
158223 - 0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection
158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Triangles & Full Dots
158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Triangles & Full Dots
158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Triangles & Full Dots
158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Triangles & Full Dots
158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Triangles & Full Dots
Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1
158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers & Stars + Same Colored Symbol
Door - 0x28A61 (Tinted Glass Door) - 0x28A0D
158226 - 0x28A0D (Church Entry Panel) - 0x28998 - Stars & RGB & Environment
Door - 0x03BB0 (Church Entry) - 0x03C08
158228 - 0x28A79 (Maze Stair Control) - True - Environment
Door - 0x28AA2 (Maze Stairs) - 0x28A79
158241 - 0x17F5F (Windmill Entry Panel) - True - Dots
Door - 0x1845B (Windmill Entry) - 0x17F5F
Town Inside Cargo Box (Town):
158606 - 0x17D01 (Cargo Box Discard) - True - Arrows
Town Maze Rooftop (Town) - Town Red Rooftop - 0x2896A:
158229 - 0x2896A (Maze Rooftop Bridge Control) - True - Shapers
Town Red Rooftop (Town):
158607 - 0x17C71 (Rooftop Discard) - True - Arrows
158230 - 0x28AC7 (Red Rooftop 1) - True - Symmetry & Shapers
158231 - 0x28AC8 (Red Rooftop 2) - 0x28AC7 - Symmetry & Shapers
158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Shapers
158233 - 0x28ACB (Red Rooftop 4) - 0x28ACA - Symmetry & Shapers
158234 - 0x28ACC (Red Rooftop 5) - 0x28ACB - Symmetry & Shapers
158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - Reflection
Town Wooden Rooftop (Town):
158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Triangles & Full Dots & Eraser
Town Church (Town):
158227 - 0x28A69 (Church Lattice) - 0x03BB0 - Environment
RGB House (Town) - RGB Room - 0x2897B:
158242 - 0x034E4 (Sound Room Left) - True - Sound
158243 - 0x034E3 (Sound Room Right) - True - Sound & Sound Dots
Door - 0x2897B (RGB House Stairs) - 0x034E4 & 0x034E3
RGB Room (Town):
158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & RGB & Squares & Colored Squares & Triangles
158245 - 0x03C0C (RGB Room Left) - 0x334D8 - RGB & Squares & Colored Squares & Black/White Squares & Eraser
158246 - 0x03C08 (RGB Room Right) - 0x334D8 & 0x03C0C - RGB & Symmetry & Dots & Colored Dots & Triangles
Town Tower (Town Tower) - Town - True - Town Tower Top - 0x27798 & 0x27799 & 0x2779A & 0x2779C:
Door - 0x27798 (First Door) - 0x28ACC
Door - 0x2779C (Second Door) - 0x28AD9
Door - 0x27799 (Third Door) - 0x28A69
Door - 0x2779A (Fourth Door) - 0x28B39
Town Tower Top (Town):
158708 - 0x032F5 (Laser Panel) - True - True
Laser - 0x032F9 (Laser) - 0x032F5
Windmill Interior (Windmill) - Theater - 0x17F88:
158247 - 0x17D02 (Turn Control) - True - Dots
158248 - 0x17F89 (Theater Entry Panel) - True - Squares & Black/White Squares & Eraser & Triangles
Door - 0x17F88 (Theater Entry) - 0x17F89
Theater (Theater) - Town - 0x0A16D | 0x3CCDF:
158656 - 0x00815 (Video Input) - True - True
158657 - 0x03553 (Tutorial Video) - 0x00815 & 0x03481 - True
158658 - 0x03552 (Desert Video) - 0x00815 & 0x0339E - True
158659 - 0x0354E (Jungle Video) - 0x00815 & 0x03702 - True
158660 - 0x03549 (Challenge Video) - 0x00815 & 0x0356B - True
158661 - 0x0354F (Shipwreck Video) - 0x00815 & 0x03535 - True
158662 - 0x03545 (Mountain Video) - 0x00815 & 0x03542 - True
158249 - 0x0A168 (Exit Left Panel) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Eraser
158250 - 0x33AB2 (Exit Right Panel) - True - Eraser & Triangles & Shapers
Door - 0x0A16D (Exit Left) - 0x0A168
Door - 0x3CCDF (Exit Right) - 0x33AB2
158608 - 0x17CF7 (Discard) - True - Arrows
Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF:
158251 - 0x17CDF (Shore Boat Spawn) - True - Boat
158609 - 0x17F9B (Discard) - True - Triangles
158252 - 0x002C4 (First Row 1) - True - Sound
158253 - 0x00767 (First Row 2) - 0x002C4 - Sound
158254 - 0x002C6 (First Row 3) - 0x00767 - Sound
158255 - 0x0070E (Second Row 1) - 0x002C6 - Sound
158256 - 0x0070F (Second Row 2) - 0x0070E - Sound
158257 - 0x0087D (Second Row 3) - 0x0070F - Sound
158258 - 0x002C7 (Second Row 4) - 0x0087D - Sound
158259 - 0x17CAB (Popup Wall Control) - 0x002C7 - True
Door - 0x1475B (Popup Wall) - 0x17CAB
158260 - 0x0026D (Popup Wall 1) - 0x1475B - Sound & Sound Dots & Symmetry
158261 - 0x0026E (Popup Wall 2) - 0x0026D - Sound & Sound Dots & Symmetry
158262 - 0x0026F (Popup Wall 3) - 0x0026E - Sound & Sound Dots & Symmetry
158263 - 0x00C3F (Popup Wall 4) - 0x0026F - Sound & Sound Dots & Symmetry
158264 - 0x00C41 (Popup Wall 5) - 0x00C3F - Sound & Sound Dots & Symmetry
158265 - 0x014B2 (Popup Wall 6) - 0x00C41 - Sound & Sound Dots & Symmetry
158709 - 0x03616 (Laser Panel) - 0x014B2 - True
Laser - 0x00274 (Laser) - 0x03616
158266 - 0x337FA (Laser Shortcut Panel) - True - True
Door - 0x3873B (Laser Shortcut) - 0x337FA
Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A:
158267 - 0x17CAA (Monastery Shortcut Panel) - True - Environment
Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA
158663 - 0x15ADD (Vault) - True - Environment & Black/White Squares & Dots
158664 - 0x03702 (Vault Box) - 0x15ADD - True
Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4:
158268 - 0x17C2E (Entry Panel) - True - Squares & Black/White Squares & Colored Squares
Door - 0x0C2A4 (Entry) - 0x17C2E
Bunker (Bunker) - Bunker Glass Room - 0x17C79:
158269 - 0x09F7D (Intro Left 1) - True - Squares & Colored Squares
158270 - 0x09FDC (Intro Left 2) - 0x09F7D - Squares & Colored Squares & Black/White Squares
158271 - 0x09FF7 (Intro Left 3) - 0x09FDC - Squares & Colored Squares & Black/White Squares
158272 - 0x09F82 (Intro Left 4) - 0x09FF7 - Squares & Colored Squares & Black/White Squares
158273 - 0x09FF8 (Intro Left 5) - 0x09F82 - Squares & Colored Squares & Black/White Squares
158274 - 0x09D9F (Intro Back 1) - 0x09FF8 - Squares & Colored Squares & Black/White Squares
158275 - 0x09DA1 (Intro Back 2) - 0x09D9F - Squares & Colored Squares
158276 - 0x09DA2 (Intro Back 3) - 0x09DA1 - Squares & Colored Squares
158277 - 0x09DAF (Intro Back 4) - 0x09DA2 - Squares & Colored Squares
158278 - 0x0A099 (Tinted Glass Door Panel) - 0x09DAF - True
Door - 0x17C79 (Tinted Glass Door) - 0x0A099
Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3:
158279 - 0x0A010 (Glass Room 1) - True - Squares & Colored Squares & RGB & Environment
158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares & RGB & Environment
158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares & RGB & Environment
Door - 0x0C2A3 (UV Room Entry) - 0x0A01F
Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D:
158282 - 0x34BC5 (Drop-Down Door Open) - True - True
158283 - 0x34BC6 (Drop-Down Door Close) - 0x34BC5 - True
158284 - 0x17E63 (UV Room 1) - 0x34BC5 - Squares & Colored Squares & RGB & Environment
158285 - 0x17E67 (UV Room 2) - 0x17E63 & 0x34BC6 - Squares & Colored Squares & Black/White Squares & RGB
Door - 0x0A08D (Elevator Room Entry) - 0x17E67
Bunker Elevator Section (Bunker) - Bunker Laser Platform - 0x0A079:
158286 - 0x0A079 (Elevator Control) - True - Squares & Colored Squares & Black/White Squares & RGB
Bunker Laser Platform (Bunker):
158710 - 0x09DE0 (Laser Panel) - True - True
Laser - 0x0C2B2 (Laser) - 0x09DE0
Outside Swamp (Swamp) - Swamp Entry Area - 0x00C1C - Main Island - True:
158287 - 0x0056E (Entry Panel) - True - Rotated Shapers & Black/White Squares & Triangles
Door - 0x00C1C (Entry) - 0x0056E
Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay:
158288 - 0x00469 (Intro Front 1) - True - Black/White Squares & Shapers
158289 - 0x00472 (Intro Front 2) - 0x00469 - Black/White Squares & Shapers & Rotated Shapers
158290 - 0x00262 (Intro Front 3) - 0x00472 - Black/White Squares & Rotated Shapers
158291 - 0x00474 (Intro Front 4) - 0x00262 - Black/White Squares & Shapers & Rotated Shapers
158292 - 0x00553 (Intro Front 5) - 0x00474 - Black/White Squares & Shapers & Rotated Shapers
158293 - 0x0056F (Intro Front 6) - 0x00553 - Black/White Squares & Shapers & Rotated Shapers
158294 - 0x00390 (Intro Back 1) - 0x0056F - Shapers & Triangles
158295 - 0x010CA (Intro Back 2) - 0x00390 - Shapers & Rotated Shapers & Triangles
158296 - 0x00983 (Intro Back 3) - 0x010CA - Rotated Shapers & Triangles
158297 - 0x00984 (Intro Back 4) - 0x00983 - Shapers & Rotated Shapers & Triangles
158298 - 0x00986 (Intro Back 5) - 0x00984 - Shapers & Rotated Shapers & Triangles
158299 - 0x00985 (Intro Back 6) - 0x00986 - Rotated Shapers & Triangles & Black/White Squares
158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers & Rotated Shapers & Triangles & Black/White Squares
158301 - 0x181A9 (Intro Back 8) - 0x00987 - Rotated Shapers & Triangles & Black/White Squares
Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609:
158302 - 0x00609 (Sliding Bridge) - True - Shapers & Black/White Squares
Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay:
158313 - 0x00982 (Platform Row 1) - True - Rotated Shapers
158314 - 0x0097F (Platform Row 2) - 0x00982 - Rotated Shapers
158315 - 0x0098F (Platform Row 3) - 0x0097F - Rotated Shapers
158316 - 0x00990 (Platform Row 4) - 0x0098F - Rotated Shapers
Door - 0x184B7 (Between Bridges First Door) - 0x00990
158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Rotated Shapers
158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Rotated Shapers
Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E
Door - 0x04B7F (Cyan Water Pump) - 0x00006
Swamp Cyan Underwater (Swamp):
158307 - 0x00002 (Cyan Underwater 1) - True - Shapers & Negative Shapers & Black/White Squares
158308 - 0x00004 (Cyan Underwater 2) - 0x00002 - Shapers & Negative Shapers & Triangles
158309 - 0x00005 (Cyan Underwater 3) - 0x00004 - Shapers & Negative Shapers & Triangles & Black/White Squares
158310 - 0x013E6 (Cyan Underwater 4) - 0x00005 - Shapers & Negative Shapers & Triangles & Black/White Squares
158311 - 0x00596 (Cyan Underwater 5) - 0x013E6 - Shapers & Negative Shapers & Triangles & Black/White Squares
158312 - 0x18488 (Cyan Underwater Sliding Bridge Control) - True - Shapers
Swamp Between Bridges Near (Swamp) - Swamp Between Bridges Far - 0x18507:
158303 - 0x00999 (Between Bridges Near Row 1) - 0x00990 - Rotated Shapers
158304 - 0x0099D (Between Bridges Near Row 2) - 0x00999 - Rotated Shapers
158305 - 0x009A0 (Between Bridges Near Row 3) - 0x0099D - Rotated Shapers
158306 - 0x009A1 (Between Bridges Near Row 4) - 0x009A0 - Rotated Shapers
Door - 0x18507 (Between Bridges Second Door) - 0x009A1
Swamp Between Bridges Far (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotating Bridge - TrueOneWay:
158319 - 0x00007 (Between Bridges Far Row 1) - 0x009A1 - Rotated Shapers & Full Dots
158320 - 0x00008 (Between Bridges Far Row 2) - 0x00007 - Rotated Shapers & Full Dots
158321 - 0x00009 (Between Bridges Far Row 3) - 0x00008 - Rotated Shapers & Shapers & Full Dots
158322 - 0x0000A (Between Bridges Far Row 4) - 0x00009 - Rotated Shapers & Shapers & Full Dots
Door - 0x183F2 (Red Water Pump) - 0x00596
Swamp Red Underwater (Swamp) - Swamp Maze - 0x014D1:
158323 - 0x00001 (Red Underwater 1) - True - Shapers & Negative Shapers & Full Dots
158324 - 0x014D2 (Red Underwater 2) - True - Shapers & Negative Shapers & Full Dots
158325 - 0x014D4 (Red Underwater 3) - True - Shapers & Negative Shapers & Full Dots
158326 - 0x014D1 (Red Underwater 4) - True - Shapers & Negative Shapers & Full Dots
Door - 0x305D5 (Red Underwater Exit) - 0x014D1
Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near Boat - 0x181F5 - Swamp Purple Area - 0x181F5:
158327 - 0x181F5 (Rotating Bridge) - True - Rotated Shapers & Shapers & Stars & Colored Squares & Triangles & Stars + Same Colored Symbol
Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482:
158328 - 0x09DB8 (Boat Spawn) - True - Boat
158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Shapers & Full Dots
158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Shapers & Full Dots
158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Shapers & Full Dots
158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Shapers & Full Dots
158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers
Door - 0x18482 (Blue Water Pump) - 0x00E3A
Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6:
Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A
Swamp Purple Underwater (Swamp):
158333 - 0x009A6 (Purple Underwater) - True - Shapers & Triangles & Black/White Squares & Rotated Shapers
Swamp Blue Underwater (Swamp):
158334 - 0x009AB (Blue Underwater 1) - True - Shapers & Negative Shapers
158335 - 0x009AD (Blue Underwater 2) - 0x009AB - Shapers & Negative Shapers
158336 - 0x009AE (Blue Underwater 3) - 0x009AD - Shapers & Negative Shapers
158337 - 0x009AF (Blue Underwater 4) - 0x009AE - Shapers & Negative Shapers
158338 - 0x00006 (Blue Underwater 5) - 0x009AF - Shapers & Negative Shapers
Swamp Maze (Swamp) - Swamp Laser Area - 0x17C0A & 0x17E07:
158340 - 0x17C0A (Maze Control) - True - Shapers & Negative Shapers & Rotated Shapers & Environment
158112 - 0x17E07 (Maze Control Other Side) - True - Shapers & Negative Shapers & Rotated Shapers & Environment
Swamp Laser Area (Swamp) - Outside Swamp - 0x2D880:
158711 - 0x03615 (Laser Panel) - True - True
Laser - 0x00BF6 (Laser) - 0x03615
158341 - 0x17C05 (Laser Shortcut Left Panel) - True - Shapers & Stars & Negative Shapers & Stars + Same Colored Symbol
158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers
Door - 0x2D880 (Laser Shortcut) - 0x17C02
Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309:
158343 - 0x17C95 (Boat Spawn) - True - Boat
158344 - 0x0288C (First Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles
Door - 0x0C309 (First Door) - 0x0288C
Treehouse Between Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310:
158345 - 0x02886 (Second Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles
Door - 0x0C310 (Second Door) - 0x02886
Treehouse Yellow Bridge (Treehouse) - Treehouse After Yellow Bridge - 0x17DC4:
158346 - 0x17D72 (Yellow Bridge 1) - True - Stars & Stars + Same Colored Symbol & Triangles
158347 - 0x17D8F (Yellow Bridge 2) - 0x17D72 - Stars & Stars + Same Colored Symbol & Triangles
158348 - 0x17D74 (Yellow Bridge 3) - 0x17D8F - Stars & Stars + Same Colored Symbol & Triangles
158349 - 0x17DAC (Yellow Bridge 4) - 0x17D74 - Stars & Stars + Same Colored Symbol & Triangles
158350 - 0x17D9E (Yellow Bridge 5) - 0x17DAC - Stars & Stars + Same Colored Symbol & Triangles
158351 - 0x17DB9 (Yellow Bridge 6) - 0x17D9E - Stars & Stars + Same Colored Symbol & Triangles
158352 - 0x17D9C (Yellow Bridge 7) - 0x17DB9 - Stars & Stars + Same Colored Symbol & Triangles
158353 - 0x17DC2 (Yellow Bridge 8) - 0x17D9C - Stars & Stars + Same Colored Symbol & Triangles
158354 - 0x17DC4 (Yellow Bridge 9) - 0x17DC2 - Stars & Stars + Same Colored Symbol & Triangles
Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181:
158355 - 0x0A182 (Third Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles & Colored Squares
Door - 0x0A181 (Third Door) - 0x0A182
Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True:
158356 - 0x2700B (Laser House Door Timer Outside Control) - True - True
Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C:
158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Full Dots
158358 - 0x17DC7 (First Purple Bridge 2) - 0x17DC8 - Stars & Full Dots
158359 - 0x17CE4 (First Purple Bridge 3) - 0x17DC7 - Stars & Full Dots
158360 - 0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Full Dots
158361 - 0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Full Dots
Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2:
158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars & Stars + Same Colored Symbol & Triangles
158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars & Stars + Same Colored Symbol & Triangles
158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars & Stars + Same Colored Symbol & Triangles
158394 - 0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Triangles
158395 - 0x17DCD (Right Orange Bridge 5) - 0x17CE3 - Stars & Stars + Same Colored Symbol & Triangles
158396 - 0x17DB2 (Right Orange Bridge 6) - 0x17DCD - Stars & Stars + Same Colored Symbol & Triangles
158397 - 0x17DCC (Right Orange Bridge 7) - 0x17DB2 - Stars & Stars + Same Colored Symbol & Triangles
158398 - 0x17DCA (Right Orange Bridge 8) - 0x17DCC - Stars & Stars + Same Colored Symbol & Triangles
158399 - 0x17D8E (Right Orange Bridge 9) - 0x17DCA - Stars & Stars + Same Colored Symbol & Triangles
158400 - 0x17DB7 (Right Orange Bridge 10 & Directional) - 0x17D8E - Triangles
158401 - 0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars & Stars + Same Colored Symbol & Triangles
158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars & Stars + Same Colored Symbol & Triangles
Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D:
158404 - 0x037FF (Bridge Control) - True - Stars
Door - 0x0C32D (Drawbridge) - 0x037FF
Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6:
158362 - 0x17D9B (Second Purple Bridge 1) - True - Stars & Black/White Squares & Triangles & Stars + Same Colored Symbol
158363 - 0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Black/White Squares & Triangles & Stars + Same Colored Symbol
158364 - 0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Black/White Squares & Triangles & Stars + Same Colored Symbol
158365 - 0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Black/White Squares & Colored Squares & Triangles & Stars + Same Colored Symbol
158366 - 0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Colored Squares & Triangles & Stars + Same Colored Symbol
158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Colored Squares & Triangles & Stars + Same Colored Symbol
158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Colored Squares & Triangles & Stars + Same Colored Symbol
Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room Front Platform - 0x17DDE - Treehouse Laser Room Back Platform - 0x17DDB:
158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers
158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers
158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers
158379 - 0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers
158380 - 0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers
158381 - 0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers
158382 - 0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers
158383 - 0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers
158384 - 0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares
158385 - 0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares
158386 - 0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares
158387 - 0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares
158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC & 0x03613 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Triangles
158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Black/White Squares & Stars + Same Colored Symbol
158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Black/White Squares & Stars + Same Colored Symbol
Treehouse Green Bridge (Treehouse):
158369 - 0x17E3C (Green Bridge 1) - True - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
158370 - 0x17E4D (Green Bridge 2) - 0x17E3C - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol & Triangles
158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol & Triangles
158610 - 0x17FA9 (Green Bridge Discard) - 0x17E61 - Arrows
Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323:
Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DEC
Treehouse Laser Room Back Platform (Treehouse):
158611 - 0x17FA0 (Laser Discard) - True - Arrows
Treehouse Laser Room (Treehouse):
158712 - 0x03613 (Laser Panel) - True - True
158403 - 0x17CBC (Laser House Door Timer Inside) - True - True
Laser - 0x028A4 (Laser) - 0x03613
Mountainside (Mountainside) - Main Island - True - Mountaintop - True:
158612 - 0x17C42 (Discard) - True - Arrows
158665 - 0x002A6 (Vault) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol
158666 - 0x03542 (Vault Box) - 0x002A6 - True
Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34:
158405 - 0x0042D (River Shape) - True - True
158406 - 0x09F7F (Box Short) - 7 Lasers - True
158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol & Triangles
158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True
Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39:
158408 - 0x09E39 (Light Bridge Controller) - True - Eraser & Triangles
Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots
158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Triangles
158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Triangles & Shapers
158412 - 0x09E69 (Right Row 4) - 0x09E72 - Stars & Black/White Squares & Stars + Same Colored Symbol & Rotated Shapers
158413 - 0x09E7B (Right Row 5) - 0x09E69 - Stars & Black/White Squares & Stars + Same Colored Symbol & Eraser & Dots & Triangles & Shapers
158414 - 0x09E73 (Left Row 1) - True - Black/White Squares & Triangles
158415 - 0x09E75 (Left Row 2) - 0x09E73 - Black/White Squares & Shapers & Rotated Shapers
158416 - 0x09E78 (Left Row 3) - 0x09E75 - Stars & Triangles & Stars + Same Colored Symbol & Shapers & Rotated Shapers
158417 - 0x09E79 (Left Row 4) - 0x09E78 - Stars & Colored Squares & Stars + Same Colored Symbol & Triangles
158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
158419 - 0x09E6F (Left Row 6) - 0x09E6C - Symmetry & Stars & Colored Squares & Black/White Squares & Stars + Same Colored Symbol & Symmetry
158420 - 0x09E6B (Left Row 7) - 0x09E6F - Symmetry & Full Dots & Triangles
158421 - 0x33AF5 (Back Row 1) - True - Symmetry & Black/White Squares & Triangles
158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Symmetry & Stars & Triangles & Stars + Same Colored Symbol
158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Stars & Shapers & Stars + Same Colored Symbol
158424 - 0x09EAD (Trash Pillar 1) - True - Rotated Shapers & Stars
158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Rotated Shapers & Triangles
Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B
Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Blue Bridge - 0x09E86:
158426 - 0x09FD3 (Near Row 1) - True - Stars & Colored Squares & Stars + Same Colored Symbol
158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Triangles & Stars + Same Colored Symbol
158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser
Door - 0x09FFB (Staircase Near) - 0x09FD8
Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - TrueOneWay:
Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD:
Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86
Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2):
158431 - 0x09E86 (Light Bridge Controller Near) - True - Shapers & Dots
Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07:
158432 - 0x09FCC (Far Row 1) - True - Triangles
158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares & Stars & Stars + Same Colored Symbol
158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars & Triangles & Stars + Same Colored Symbol
158435 - 0x09FD0 (Far Row 4) - 0x09FCF - Rotated Shapers & Negative Shapers
158436 - 0x09FD1 (Far Row 5) - 0x09FD0 - Dots
158437 - 0x09FD2 (Far Row 6) - 0x09FD1 - Rotated Shapers
Door - 0x09E07 (Staircase Far) - 0x09FD2
Mountain Floor 2 Light Bridge Room Far (Mountain Floor 2):
158438 - 0x09ED8 (Light Bridge Controller Far) - True - Shapers & Dots
Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - TrueOneWay:
158613 - 0x17F93 (Elevator Discard) - True - Arrows
Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB:
158439 - 0x09EEB (Elevator Control Panel) - True - Dots
Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89:
158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Negative Shapers
158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser
158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser
158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser
158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry
Door - 0x09F89 (Exit) - 0x09FDA
Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141:
158614 - 0x17FA2 (Discard) - 0xFFF00 - Arrows
158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars
158446 - 0x01987 (Final Room Entry Right) - True - Squares & Colored Squares & Dots
Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987
Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33:
Door - 0x17F33 (Rock Open) - True
Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D:
158447 - 0x00FF8 (Caves Entry Panel) - True - Arrows & Black/White Squares
Door - 0x2D77D (Caves Entry) - 0x00FF8
158448 - 0x334E1 (Rock Control) - True - True
Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Path to Challenge - 0x019A5:
158451 - 0x335AB (Elevator Inside Control) - True - Dots & Squares & Black/White Squares
158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Squares & Black/White Squares
158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Squares & Black/White Squares & Dots
158454 - 0x00190 (Blue Tunnel Right First 1) - True - Arrows
158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Arrows
158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Arrows
158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Arrows
158458 - 0x01A0D (Blue Tunnel Left First 1) - True - Arrows & Symmetry
158459 - 0x008B8 (Blue Tunnel Left Second 1) - True - Arrows & Triangles
158460 - 0x00973 (Blue Tunnel Left Second 2) - 0x008B8 - Arrows & Triangles
158461 - 0x0097B (Blue Tunnel Left Second 3) - 0x00973 - Arrows & Triangles
158462 - 0x0097D (Blue Tunnel Left Second 4) - 0x0097B - Arrows & Triangles
158463 - 0x0097E (Blue Tunnel Left Second 5) - 0x0097D - Arrows & Triangles
158464 - 0x00994 (Blue Tunnel Right Second 1) - True - Arrows & Shapers & Rotated Shapers
158465 - 0x334D5 (Blue Tunnel Right Second 2) - 0x00994 - Arrows & Rotated Shapers
158466 - 0x00995 (Blue Tunnel Right Second 3) - 0x334D5 - Arrows & Shapers & Rotated Shapers
158467 - 0x00996 (Blue Tunnel Right Second 4) - 0x00995 - Arrows & Shapers & Rotated Shapers
158468 - 0x00998 (Blue Tunnel Right Second 5) - 0x00996 - Arrows & Shapers
158469 - 0x009A4 (Blue Tunnel Left Third 1) - True - Arrows & Stars
158470 - 0x018A0 (Blue Tunnel Right Third 1) - True - Arrows & Symmetry
158471 - 0x00A72 (Blue Tunnel Left Fourth 1) - True - Arrows & Shapers & Negative Shapers
158472 - 0x32962 (First Floor Left) - True - Full Dots & Rotated Shapers
158473 - 0x32966 (First Floor Grounded) - True - Stars & Triangles & Rotated Shapers & Black/White Squares & Stars + Same Colored Symbol
158474 - 0x01A31 (First Floor Middle) - True - Stars
158475 - 0x00B71 (First Floor Right) - True - Full Dots & Eraser & Stars & Stars + Same Colored Symbol & Colored Squares & Shapers & Negative Shapers
158478 - 0x288EA (First Wooden Beam) - True - Stars
158479 - 0x288FC (Second Wooden Beam) - True - Shapers & Eraser
158480 - 0x289E7 (Third Wooden Beam) - True - Eraser & Triangles
158481 - 0x288AA (Fourth Wooden Beam) - True - Full Dots & Negative Shapers & Shapers
158482 - 0x17FB9 (Left Upstairs Single) - True - Full Dots & Arrows & Black/White Squares
158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Full Dots & Arrows
158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Full Dots & Arrows
158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Full Dots & Arrows
158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Full Dots & Arrows
158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Full Dots & Arrows
158488 - 0x0008F (Right Upstairs Left Row 1) - True - Dots & Black/White Squares & Colored Squares
158489 - 0x0006B (Right Upstairs Left Row 2) - 0x0008F - Stars & Black/White Squares & Colored Squares & Stars + Same Colored Symbol
158490 - 0x0008B (Right Upstairs Left Row 3) - 0x0006B - Stars & Black/White Squares & Colored Squares & Stars + Same Colored Symbol & Triangles
158491 - 0x0008C (Right Upstairs Left Row 4) - 0x0008B - Stars & Stars + Same Colored Symbol & Shapers & Rotated Shapers
158492 - 0x0008A (Right Upstairs Left Row 5) - 0x0008C - Stars & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Eraser & Triangles
158493 - 0x00089 (Right Upstairs Left Row 6) - 0x0008A - Shapers & Negative Shapers & Dots
158494 - 0x0006A (Right Upstairs Left Row 7) - 0x00089 - Stars & Dots
158495 - 0x0006C (Right Upstairs Left Row 8) - 0x0006A - Dots & Stars & Stars + Same Colored Symbol & Eraser
158496 - 0x00027 (Right Upstairs Right Row 1) - True - Colored Squares & Black/White Squares & Eraser
158497 - 0x00028 (Right Upstairs Right Row 2) - 0x00027 - Shapers & Symmetry
158498 - 0x00029 (Right Upstairs Right Row 3) - 0x00028 - Symmetry & Triangles & Eraser
158476 - 0x09DD5 (Lone Pillar) - True - Arrows
Door - 0x019A5 (Pillar Door) - 0x09DD5
158449 - 0x021D7 (Mountain Shortcut Panel) - True - Stars & Stars + Same Colored Symbol & Triangles & Eraser
Door - 0x2D73F (Mountain Shortcut Door) - 0x021D7
158450 - 0x17CF2 (Swamp Shortcut Panel) - True - Arrows
Door - 0x2D859 (Swamp Shortcut Door) - 0x17CF2
Path to Challenge (Caves) - Challenge - 0x0A19A:
158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Arrows & Stars + Same Colored Symbol
Door - 0x0A19A (Challenge Entry) - 0x0A16E
Challenge (Challenge) - Tunnels - 0x0348A:
158499 - 0x0A332 (Start Timer) - 11 Lasers - True
158500 - 0x0088E (Small Basic) - 0x0A332 - True
158501 - 0x00BAF (Big Basic) - 0x0088E - True
158502 - 0x00BF3 (Square) - 0x00BAF - Squares & Black/White Squares
158503 - 0x00C09 (Maze Map) - 0x00BF3 - Dots
158504 - 0x00CDB (Stars and Dots) - 0x00C09 - Stars & Dots
158505 - 0x0051F (Symmetry) - 0x00CDB - Symmetry & Colored Dots & Dots
158506 - 0x00524 (Stars and Shapers) - 0x0051F - Stars & Shapers
158507 - 0x00CD4 (Big Basic 2) - 0x00524 - True
158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Squares & Black/White Squares
158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Squares & Black/White Squares
158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Squares & Black/White Squares
158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares
158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares
158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares
158514 - 0x034F4 (Maze Hidden 1) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles
158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles
158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry & Pillar
158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Squares & Black/White Squares & Symmetry & Pillar
158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True
158518 - 0x039B4 (Tunnels Entry Panel) - True - Arrows
Door - 0x0348A (Tunnels Entry) - 0x039B4
Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87:
158668 - 0x2FAF6 (Vault Box) - True - True
158519 - 0x27732 (Theater Shortcut Panel) - True - True
Door - 0x27739 (Theater Shortcut) - 0x27732
158520 - 0x2773D (Desert Shortcut Panel) - True - True
Door - 0x27263 (Desert Shortcut) - 0x2773D
158521 - 0x09E85 (Town Shortcut Panel) - True - Arrows
Door - 0x09E87 (Town Shortcut) - 0x09E85
Final Room (Mountain Final Room) - Elevator - 0x339BB & 0x33961:
158522 - 0x0383A (Right Pillar 1) - True - Stars & Eraser & Triangles & Stars + Same Colored Symbol
158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Full Dots & Triangles
158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Shapers & Stars & Negative Shapers & Stars + Same Colored Symbol
158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Eraser & Symmetry & Stars & Stars + Same Colored Symbols & Negative Shapers & Shapers
158526 - 0x0383D (Left Pillar 1) - True - Stars & Black/White Squares & Stars + Same Colored Symbol
158527 - 0x0383F (Left Pillar 2) - 0x0383D - Triangles
158528 - 0x03859 (Left Pillar 3) - 0x0383F - Symmetry & Shapers & Black/White Squares
158529 - 0x339BB (Left Pillar 4) - 0x03859 - Symmetry & Black/White Squares & Stars & Stars + Same Colored Symbol & Triangles
Elevator (Mountain Final Room):
158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True
158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True
158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True
158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA | 0x3D9A8 - True
Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay:

View File

@@ -1,18 +1,20 @@
"""
Archipelago init file for The Witness
"""
import typing
from BaseClasses import Region, RegionType, Location, MultiWorld, Item, Entrance, Tutorial, ItemClassification
from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \
get_priority_hint_items, make_hints, generate_joke_hints
from ..AutoWorld import World, WebWorld
from .player_logic import StaticWitnessLogic, WitnessPlayerLogic
from .player_logic import WitnessPlayerLogic
from .static_logic import StaticWitnessLogic
from .locations import WitnessPlayerLocations, StaticWitnessLocations
from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems
from .rules import set_rules
from .regions import WitnessRegions
from .Options import is_option_enabled, the_witness_options, get_option_value
from .utils import best_junk_to_add_based_on_weights
from .utils import best_junk_to_add_based_on_weights, get_audio_logs
from logging import warning
@@ -36,7 +38,7 @@ class WitnessWorld(World):
"""
game = "The Witness"
topology_present = False
data_version = 7
data_version = 8
static_logic = StaticWitnessLogic()
static_locat = StaticWitnessLocations()
@@ -59,6 +61,8 @@ class WitnessWorld(World):
'door_hexes': self.items.DOORS,
'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME,
'disabled_panels': self.player_logic.COMPLETELY_DISABLED_CHECKS,
'log_ids_to_hints': self.log_ids_to_hints,
'progressive_item_lists': self.items.MULTI_LISTS_BY_CODE
}
def generate_early(self):
@@ -77,6 +81,8 @@ class WitnessWorld(World):
self.items = WitnessPlayerItems(self.locat, self.world, self.player, self.player_logic)
self.regio = WitnessRegions(self.locat)
self.log_ids_to_hints = dict()
self.junk_items_created = {key: 0 for key in self.items.JUNK_WEIGHTS.keys()}
def generate_basic(self):
@@ -84,17 +90,18 @@ class WitnessWorld(World):
pool = []
items_by_name = dict()
for item in self.items.ITEM_TABLE:
witness_item = self.create_item(item)
if item in self.items.PROGRESSION_TABLE:
pool.append(witness_item)
items_by_name[item] = witness_item
for i in range(0, self.items.PROG_ITEM_AMOUNTS[item]):
if item in self.items.PROGRESSION_TABLE:
witness_item = self.create_item(item)
pool.append(witness_item)
items_by_name[item] = witness_item
less_junk = 0
# Put good item on first check if symbol shuffle is on
symbols = is_option_enabled(self.world, self.player, "shuffle_symbols")
if symbols:
if symbols and get_option_value(self.world, self.player, "puzzle_randomization") != 1:
random_good_item = self.world.random.choice(self.items.GOOD_ITEMS)
first_check = self.world.get_location(
@@ -138,9 +145,39 @@ class WitnessWorld(World):
set_rules(self.world, self.player, self.player_logic, self.locat)
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
hint_amount = get_option_value(self.world, self.player, "hint_amount")
slot_data["hard_mode"] = False
credits_hint = ("This Randomizer", "is brought to you by", "NewSoupVi, Jarno, jbzdarkid, sigma144", -1)
audio_logs = get_audio_logs().copy()
if hint_amount != 0:
generated_hints = make_hints(self.world, self.player, hint_amount)
self.world.random.shuffle(audio_logs)
duplicates = len(audio_logs) // hint_amount
for _ in range(0, hint_amount):
hint = generated_hints.pop()
for _ in range(0, duplicates):
audio_log = audio_logs.pop()
self.log_ids_to_hints[int(audio_log, 16)] = hint
if audio_logs:
audio_log = audio_logs.pop()
self.log_ids_to_hints[int(audio_log, 16)] = credits_hint
joke_hints = generate_joke_hints(self.world, len(audio_logs))
while audio_logs:
audio_log = audio_logs.pop()
self.log_ids_to_hints[int(audio_log, 16)] = joke_hints.pop()
# generate hints done
slot_data = self._get_slot_data()
for option_name in the_witness_options:
slot_data[option_name] = get_option_value(

281
worlds/witness/hints.py Normal file
View File

@@ -0,0 +1,281 @@
from BaseClasses import MultiWorld
from .Options import is_option_enabled, get_option_value
joke_hints = [
("Quaternions", "break", "my brain"),
("Eclipse", "has nothing", "but you should do it anyway"),
("", "Beep", ""),
("Putting in custom subtitles", "shouldn't have been", "as hard as it was..."),
("BK mode", "is right", "around the corner"),
("", "You can do it!", ""),
("", "I believe in you!", ""),
("The person playing", "is", "cute <3"),
("dash dot, dash dash dash", "dash, dot dot dot dot, dot dot", "dash dot, dash dash dot"),
("When you think about it,", "there are actually a lot of", "bubbles in a stream"),
("Never gonna give you up", "Never gonna let you down", "Never gonna run around and desert you"),
("Thanks to", "the Archipelago developers", "for making this possible."),
("Have you tried ChecksFinder?", "If you like puzzles,", "you might enjoy it!"),
("Have you tried Dark Souls III?", "A tough game like this", "feels better when friends are helping you!"),
("Have you tried Donkey Kong Country 3?", "A legendary game", "from a golden age of platformers!"),
("Have you tried Factorio?", "Alone in an unknown world.", "Sound familiar?"),
("Have you tried Final Fantasy?", "Experience a classic game", "improved to fit modern standards!"),
("Have you tried Hollow Knight?", "Another independent hit", "revolutionising a genre!"),
("Have you tried A Link to the Past?", "The Archipelago game", "that started it all!"),
("Have you tried Meritous?", "You should know that obscure games", "are often groundbreaking!"),
("Have you tried Ocarine of Time?", "One of the biggest randomizers,", "big inspiration for this one's features!"),
("Have you tried Raft?", "Haven't you always wanted to explore", "the ocean surrounding this island?"),
("Have you tried Risk of Rain 2?", "I haven't either.", "But I hear it's incredible!"),
("Have you tried Rogue Legacy?", "After solving so many puzzles", "it's the perfect way to rest your brain."),
("Have you tried Secret of Evermore?", "I haven't either", "But I hear it's great!"),
("Have you tried Slay the Spire?", "Experience the thrill of combat", "without needing fast fingers!"),
("Have you tried SMZ3?", "Why play one incredible game", "when you can play 2 at once?"),
("Have you tried Starcraft 2?", "Use strategy and management", "to crush your enemies!"),
("Have you tried Super Mario 64?", "3-dimensional games like this", "owe everything to that game."),
("Have you tried Super Metroid?", "A classic game", "that started a whole genre."),
("Have you tried Timespinner?", "Everyone who plays it", "ends up loving it!"),
("Have you tried VVVVVV?", "Experience the essence of gaming", "distilled into its purest form!"),
("Have you tried The Witness?", "Oh. I guess you already have.", " Thanks for playing!"),
("One day I was fascinated", "by the subject of", "generation of waves by wind"),
("I don't like sandwiches", "Why would you think I like sandwiches?", "Have you ever seen me with a sandwich?"),
("Where are you right now?", "I'm at soup!", "What do you mean you're at soup?"),
("Remember to ask", "in the Archipelago Discord", "what the Functioning Brain does."),
("", "Don't use your puzzle skips", "you might need them later"),
("", "For an extra challenge", "Try playing blindfolded"),
("Go to the top of the mountain", "and see if you can see", "your house"),
("Yellow = Red + Green", "Cyan = Green + Blue", "Magenta = Red + Blue"),
("", "Maybe that panel really is unsolvable", ""),
("", "Did you make sure it was plugged in?", ""),
("", "Do not look into laser with remaining eye", ""),
("", "Try pressing Space to jump", ""),
("The Witness is a Doom clone.", "Just replace the demons", "with puzzles"),
("", "Test Hint please ignore", ""),
("Shapers can never be placed", "outside the panel boundaries", "even if subtracted."),
("", "The Keep laser panels use", "the same trick on both sides!"),
("Can't get past a door? Try going around.", "Can't go around? Try building a", "nether portal."),
("", "We've been trying to reach you", "about your car's extended warranty"),
("I hate this game. I hate this game.", "I hate this game.", "-chess player Bobby Fischer"),
("Dear Mario,", "Please come to the castle.", "I've baked a cake for you!"),
("Have you tried waking up?", "", "Yeah, me neither."),
("Why do they call it The Witness,", "when wit game the player view", "play of with the game."),
("", "THE WIND FISH IN NAME ONLY", "FOR IT IS NEITHER"),
("Like this game? Try The Wit.nes,", "Understand, INSIGHT, Taiji", "What the Witness?, and Tametsi."),
("", "In a race", "It's survival of the Witnesst"),
("", "This hint has been removed", "We apologize for your inconvenience."),
("", "O-----------", ""),
("Circle is draw", "Square is separate", "Line is win"),
("Circle is draw", "Star is pair", "Line is win"),
("Circle is draw", "Circle is copy", "Line is win"),
("Circle is draw", "Dot is eat", "Line is win"),
("Circle is start", "Walk is draw", "Line is win"),
("Circle is start", "Line is win", "Witness is you"),
("Can't find any items?", "Consider a relaxing boat trip", "around the island"),
("", "Don't forget to like, comment, and subscribe", ""),
("Ah crap, gimme a second.", "[papers rustling]", "Sorry, nothing."),
("", "Trying to get a hint?", "Too bad."),
("", "Here's a hint:", "Get good at the game."),
("", "I'm still not entirely sure", "what we're witnessing here."),
("Have you found a red page yet?", "No?", "Then have you found a blue page?"),
(
"And here we see the Witness player,",
"seeking answers where there are none-",
"Did someone turn on the loudspeaker?"
),
(
"Hints suggested by:",
"IHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi,",
"KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch."
),
]
def get_always_hint_items(world: MultiWorld, player: int):
priority = [
"Boat",
"Mountain Bottom Floor Final Room Entry (Door)",
"Caves Mountain Shortcut (Door)",
"Caves Swamp Shortcut (Door)",
"Caves Exits to Main Island",
]
difficulty = get_option_value(world, player, "puzzle_randomization")
discards = is_option_enabled(world, player, "shuffle_discards")
if discards:
if difficulty == 1:
priority.append("Arrows")
else:
priority.append("Triangles")
return priority
def get_always_hint_locations(world: MultiWorld, player: int):
return {
"Swamp Purple Underwater",
"Shipwreck Vault Box",
"Challenge Vault Box",
"Mountain Bottom Floor Discard",
}
def get_priority_hint_items(world: MultiWorld, player: int):
priority = {
"Negative Shapers",
"Sound Dots",
"Colored Dots",
"Stars + Same Colored Symbol",
"Swamp Entry (Panel)",
"Swamp Laser Shortcut (Door)",
}
if is_option_enabled(world, player, "shuffle_lasers"):
lasers = {
"Symmetry Laser",
"Desert Laser",
"Town Laser",
"Keep Laser",
"Swamp Laser",
"Treehouse Laser",
"Monastery Laser",
"Jungle Laser",
"Quarry Laser",
"Bunker Laser",
"Shadows Laser",
}
if get_option_value(world, player, "doors") >= 2:
priority.add("Desert Laser")
lasers.remove("Desert Laser")
priority.update(world.random.sample(lasers, 2))
else:
priority.update(world.random.sample(lasers, 3))
return priority
def get_priority_hint_locations(world: MultiWorld, player: int):
return {
"Town RGB Room Left",
"Town RGB Room Right",
"Treehouse Green Bridge 7",
"Treehouse Green Bridge Discard",
"Shipwreck Discard",
"Desert Vault Box",
"Mountainside Vault Box",
"Mountainside Discard",
}
def make_hint_from_item(world: MultiWorld, player: int, item: str):
location_obj = world.find_item(item, player).item.location
location_name = location_obj.name
if location_obj.player != player:
location_name += " (" + world.get_player_name(location_obj.player) + ")"
return location_name, item, location_obj.address if(location_obj.player == player) else -1
def make_hint_from_location(world: MultiWorld, player: int, location: str):
location_obj = world.get_location(location, player)
item_obj = world.get_location(location, player).item
item_name = item_obj.name
if item_obj.player != player:
item_name += " (" + world.get_player_name(item_obj.player) + ")"
return location, item_name, location_obj.address if(location_obj.player == player) else -1
def make_hints(world: MultiWorld, player: int, hint_amount: int):
hints = list()
prog_items_in_this_world = {
item.name for item in world.get_items()
if item.player == player and item.code and item.advancement
}
loc_in_this_world = {
location.name for location in world.get_locations()
if location.player == player and not location.event
}
always_locations = [
location for location in get_always_hint_locations(world, player)
if location in loc_in_this_world
]
always_items = [
item for item in get_always_hint_items(world, player)
if item in prog_items_in_this_world
]
priority_locations = [
location for location in get_priority_hint_locations(world, player)
if location in loc_in_this_world
]
priority_items = [
item for item in get_priority_hint_items(world, player)
if item in prog_items_in_this_world
]
always_hint_pairs = dict()
for item in always_items:
hint_pair = make_hint_from_item(world, player, item)
always_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2])
for location in always_locations:
hint_pair = make_hint_from_location(world, player, location)
always_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2])
priority_hint_pairs = dict()
for item in priority_items:
hint_pair = make_hint_from_item(world, player, item)
priority_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2])
for location in priority_locations:
hint_pair = make_hint_from_location(world, player, location)
priority_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2])
for loc, item in always_hint_pairs.items():
if item[1]:
hints.append((item[0], "can be found at", loc, item[2]))
else:
hints.append((loc, "contains", item[0], item[2]))
next_random_hint_is_item = world.random.randint(0, 2)
prog_items_in_this_world = sorted(list(prog_items_in_this_world))
locations_in_this_world = sorted(list(loc_in_this_world))
world.random.shuffle(prog_items_in_this_world)
world.random.shuffle(locations_in_this_world)
while len(hints) < hint_amount:
if priority_hint_pairs:
loc = world.random.choice(list(priority_hint_pairs.keys()))
item = priority_hint_pairs[loc]
del priority_hint_pairs[loc]
if item[1]:
hints.append((item[0], "can be found at", loc, item[2]))
else:
hints.append((loc, "contains", item[0], item[2]))
continue
if next_random_hint_is_item:
if not prog_items_in_this_world:
next_random_hint_is_item = not next_random_hint_is_item
continue
hint = make_hint_from_item(world, player, prog_items_in_this_world.pop())
hints.append((hint[1], "can be found at", hint[0], hint[2]))
else:
hint = make_hint_from_location(world, player, locations_in_this_world.pop())
hints.append((hint[0], "contains", hint[1], hint[2]))
next_random_hint_is_item = not next_random_hint_is_item
return hints
def generate_joke_hints(world: MultiWorld, amount: int):
return [(x, y, z, -1) for (x, y, z) in world.random.sample(joke_hints, amount)]

View File

@@ -2,6 +2,7 @@
Defines progression, junk and event items for The Witness
"""
import copy
from collections import defaultdict
from typing import Dict, NamedTuple, Optional, Set
from BaseClasses import Item, MultiWorld
@@ -96,6 +97,10 @@ class WitnessPlayerItems:
Class that defines Items for a single world
"""
@staticmethod
def code(item_name: str):
return StaticWitnessItems.ALL_ITEM_TABLE[item_name].code
def __init__(self, locat: WitnessPlayerLocations, world: MultiWorld, player: int, player_logic: WitnessPlayerLogic):
"""Adds event items after logic changes due to options"""
self.EVENT_ITEM_TABLE = dict()
@@ -105,6 +110,8 @@ class WitnessPlayerItems:
self.ITEM_ID_TO_DOOR_HEX = dict()
self.DOORS = set()
self.PROG_ITEM_AMOUNTS = defaultdict(lambda: 1)
self.SYMBOLS_NOT_IN_THE_GAME = set()
self.EXTRA_AMOUNTS = {
@@ -118,8 +125,17 @@ class WitnessPlayerItems:
if item in StaticWitnessLogic.ALL_SYMBOL_ITEMS:
self.SYMBOLS_NOT_IN_THE_GAME.add(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code)
else:
if item[0] in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS:
self.PROG_ITEM_AMOUNTS[item[0]] = len(player_logic.MULTI_LISTS[item[0]])
self.PROGRESSION_TABLE[item[0]] = self.ITEM_TABLE[item[0]]
self.MULTI_LISTS_BY_CODE = dict()
for item in self.PROG_ITEM_AMOUNTS:
multi_list = player_logic.MULTI_LISTS[item]
self.MULTI_LISTS_BY_CODE[self.code(item)] = [self.code(single_item) for single_item in multi_list]
for entity_hex, items in player_logic.DOOR_ITEMS_BY_ID.items():
entity_hex_int = int(entity_hex, 16)
@@ -138,11 +154,11 @@ class WitnessPlayerItems:
if doors and symbols:
self.GOOD_ITEMS = [
"Dots", "Black/White Squares", "Symmetry"
"Progressive Dots", "Black/White Squares", "Symmetry"
]
elif symbols:
self.GOOD_ITEMS = [
"Dots", "Black/White Squares", "Stars",
"Progressive Dots", "Black/White Squares", "Progressive Stars",
"Shapers", "Symmetry"
]
@@ -151,6 +167,10 @@ class WitnessPlayerItems:
if not is_option_enabled(world, player, "disable_non_randomized_puzzles"):
self.GOOD_ITEMS.append("Colored Squares")
self.GOOD_ITEMS = [
StaticWitnessLogic.ITEMS_TO_PROGRESSIVE.get(item, item) for item in self.GOOD_ITEMS
]
for event_location in locat.EVENT_LOCATION_TABLE:
location = player_logic.EVENT_ITEM_PAIRS[event_location]
self.EVENT_ITEM_TABLE[location] = ItemData(None, True, True)

View File

@@ -3,7 +3,8 @@ Defines constants for different types of locations in the game
"""
from .Options import is_option_enabled, get_option_value
from .player_logic import StaticWitnessLogic, WitnessPlayerLogic
from .player_logic import WitnessPlayerLogic
from .static_logic import StaticWitnessLogic
class StaticWitnessLocations:
@@ -52,8 +53,6 @@ class StaticWitnessLocations:
"Desert Light Room 3",
"Desert Pond Room 5",
"Desert Flood Room 6",
"Desert Final Bent 3",
"Desert Final Hexagonal",
"Desert Laser Panel",
"Quarry Mill Lower Row 6",
@@ -247,6 +246,10 @@ class WitnessPlayerLocations:
StaticWitnessLocations.GENERAL_LOCATIONS
)
if get_option_value(world, player, "puzzle_randomization") == 1:
self.CHECK_LOCATIONS.remove("Keep Pressure Plates 4")
self.CHECK_LOCATIONS.add("Keep Pressure Plates 2")
doors = get_option_value(world, player, "shuffle_doors") >= 2
earlyutm = is_option_enabled(world, player, "early_secret_area")
victory = get_option_value(world, player, "victory_condition")

View File

@@ -39,7 +39,7 @@ class WitnessPlayerLogic:
if panel_hex in self.COMPLETELY_DISABLED_CHECKS:
return frozenset()
check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex]
check_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel_hex]
these_items = frozenset({frozenset()})
@@ -47,17 +47,21 @@ class WitnessPlayerLogic:
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"]
these_items = frozenset({
subset.intersection(self.PROG_ITEMS_ACTUALLY_IN_THE_GAME)
subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI)
for subset in these_items
})
for subset in these_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset)
if panel_hex in self.DOOR_ITEMS_BY_ID:
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]})
all_options = set()
for items_option in these_items:
for dependentItem in door_items:
for dependentItem in door_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependentItem)
for items_option in these_items:
all_options.add(items_option.union(dependentItem))
if panel_hex != "0x28A0D":
@@ -76,11 +80,11 @@ class WitnessPlayerLogic:
dependent_items_for_option = frozenset({frozenset()})
for option_panel in option:
dep_obj = StaticWitnessLogic.CHECKS_BY_HEX.get(option_panel)
dep_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX.get(option_panel)
if option_panel in self.COMPLETELY_DISABLED_CHECKS:
new_items = frozenset()
elif option_panel in {"7 Lasers", "11 Lasers"}:
elif option_panel in {"7 Lasers", "11 Lasers", "PP2 Weirdness"}:
new_items = frozenset({frozenset([option_panel])})
# If a panel turns on when a panel in a different region turns on,
# the latter panel will be an "event panel", unless it ends up being
@@ -113,20 +117,26 @@ class WitnessPlayerLogic:
"""Makes a single logic adjustment based on additional logic file"""
if adj_type == "Items":
if line not in StaticWitnessItems.ALL_ITEM_TABLE:
raise RuntimeError("Item \"" + line + "\" does not exit.")
line_split = line.split(" - ")
item = line_split[0]
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(line)
if item not in StaticWitnessItems.ALL_ITEM_TABLE:
raise RuntimeError("Item \"" + item + "\" does not exit.")
if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT:
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2]
self.THEORETICAL_ITEMS.add(item)
self.THEORETICAL_ITEMS_NO_MULTI.update(StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(item, [item]))
if item in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT:
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[item][2]
for panel_hex in panel_hexes:
self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(line)
self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(item)
return
if adj_type == "Remove Items":
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.discard(line)
self.THEORETICAL_ITEMS.discard(line)
for i in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(line, [line]):
self.THEORETICAL_ITEMS_NO_MULTI.discard(i)
if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT:
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2]
@@ -265,11 +275,22 @@ class WitnessPlayerLogic:
self.REQUIREMENTS_BY_HEX[check_hex] = indep_requirement
for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI:
if item not in self.THEORETICAL_ITEMS:
corresponding_multi = StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item]
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(corresponding_multi)
multi_list = StaticWitnessLogic.PROGRESSIVE_TO_ITEMS[StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item]]
multi_list = [item for item in multi_list if item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI]
self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1
self.MULTI_LISTS[corresponding_multi] = multi_list
else:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item)
def make_event_item_pair(self, panel):
"""
Makes a pair of an event panel and its event item
"""
name = StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + " Solved"
name = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel]["checkName"] + " Solved"
pair = (name, self.EVENT_ITEM_NAMES[panel])
return pair
@@ -287,7 +308,7 @@ class WitnessPlayerLogic:
if panel == "TrueOneWay":
continue
if StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"] != region_name:
if self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel]["region"]["name"] != region_name:
self.EVENT_PANELS_FROM_REGIONS.add(panel)
self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS)
@@ -306,12 +327,24 @@ class WitnessPlayerLogic:
self.EVENT_PANELS_FROM_PANELS = set()
self.EVENT_PANELS_FROM_REGIONS = set()
self.THEORETICAL_ITEMS = set()
self.THEORETICAL_ITEMS_NO_MULTI = set()
self.MULTI_AMOUNTS = dict()
self.MULTI_LISTS = dict()
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set()
self.DOOR_ITEMS_BY_ID = dict()
self.STARTING_INVENTORY = set()
self.CONNECTIONS_BY_REGION_NAME = copy.copy(StaticWitnessLogic.STATIC_CONNECTIONS_BY_REGION_NAME)
self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(StaticWitnessLogic.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX)
self.DIFFICULTY = get_option_value(world, player, "puzzle_randomization")
if self.DIFFICULTY == 0:
self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal
elif self.DIFFICULTY == 1:
self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_expert
self.CONNECTIONS_BY_REGION_NAME = copy.copy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME)
self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX)
self.REQUIREMENTS_BY_HEX = dict()
# Determining which panels need to be events is a difficult process.
@@ -333,6 +366,7 @@ class WitnessPlayerLogic:
"0x019DC": "Keep Hedges 2 Knowledge",
"0x019E7": "Keep Hedges 3 Knowledge",
"0x01D3F": "Keep Laser Panel (Pressure Plates) Activates",
"0x01BE9": "Keep Laser Panel (Pressure Plates) Activates",
"0x09F7F": "Mountain Access",
"0x0367C": "Quarry Laser Mill Requirement Met",
"0x009A1": "Swamp Between Bridges Far 1 Activates",
@@ -374,6 +408,9 @@ class WitnessPlayerLogic:
"0x0356B": "Challenge Video Pattern Knowledge",
"0x0A15F": "Desert Laser Panel Shutters Open (1)",
"0x012D7": "Desert Laser Panel Shutters Open (2)",
"0x03613": "Treehouse Orange Bridge 13 Turns On",
"0x17DEC": "Treehouse Laser House Access Requirement",
"0x03C08": "Town Church Entry Opens",
}
self.ALWAYS_EVENT_NAMES_BY_HEX = {

View File

@@ -4,7 +4,8 @@ and connects them with the proper requirements
"""
from BaseClasses import MultiWorld, Entrance
from . import StaticWitnessLogic
from .static_logic import StaticWitnessLogic
from .Options import get_option_value
from .locations import WitnessPlayerLocations
from .player_logic import WitnessPlayerLogic
@@ -39,7 +40,7 @@ class WitnessRegions:
connection = Entrance(
player,
source + " to " + target + " via " + str(panel_hex_to_solve_set),
source + " to " + target,
source_region
)
@@ -58,16 +59,23 @@ class WitnessRegions:
create_region(world, player, 'Menu', self.locat, None, ["The Splashscreen?"]),
]
difficulty = get_option_value(world, player, "puzzle_randomization")
if difficulty == 1:
reference_logic = StaticWitnessLogic.sigma_expert
else:
reference_logic = StaticWitnessLogic.sigma_normal
all_locations = set()
for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items():
for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items():
locations_for_this_region = [
StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] for panel in region["panels"]
if StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE
reference_logic.CHECKS_BY_HEX[panel]["checkName"] for panel in region["panels"]
if reference_logic.CHECKS_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE
]
locations_for_this_region += [
StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" for panel in region["panels"]
if StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" in self.locat.EVENT_LOCATION_TABLE
reference_logic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" for panel in region["panels"]
if reference_logic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" in self.locat.EVENT_LOCATION_TABLE
]
all_locations = all_locations | set(locations_for_this_region)
@@ -76,7 +84,7 @@ class WitnessRegions:
create_region(world, player, region_name, self.locat, locations_for_this_region)
]
for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items():
for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items():
for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]:
if connection[0] == "Entry":
continue
@@ -87,7 +95,7 @@ class WitnessRegions:
for subset in connection[1]:
if all({panel in player_logic.DOOR_ITEMS_BY_ID for panel in subset}):
if all({StaticWitnessLogic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}):
if all({reference_logic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}):
self.connect(world, player, connection[0], region_name, player_logic, frozenset({subset}))
self.connect(world, player, region_name, connection[0], player_logic, connection[1])

Some files were not shown because too many files have changed in this diff Show More