Merge remote-tracking branch 'upstream/main' into instruction_patch_kdl3

This commit is contained in:
Silvris
2024-05-17 12:37:02 -05:00
88 changed files with 11354 additions and 283 deletions

View File

@@ -718,10 +718,6 @@ class CollectionState():
def count(self, item: str, player: int) -> int:
return self.prog_items[player][item]
def item_count(self, item: str, player: int) -> int:
Utils.deprecate("Use count instead.")
return self.count(item, player)
def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
found: int = 0

View File

@@ -259,7 +259,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
elif not args:
args = {}
if "Patch|Game|Component" in args:
if args.get("Patch|Game|Component", None) is not None:
file, component = identify(args["Patch|Game|Component"])
if file:
args['file'] = file

View File

@@ -175,11 +175,13 @@ class Context:
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
non_hintable_names: typing.Dict[str, typing.Set[str]]
logger: logging.Logger
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
log_network: bool = False):
log_network: bool = False, logger: logging.Logger = logging.getLogger()):
self.logger = logger
super(Context, self).__init__()
self.slot_info = {}
self.log_network = log_network
@@ -287,12 +289,12 @@ class Context:
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception(f"Exception during send_msgs, could not send {msg}")
self.logger.exception(f"Exception during send_msgs, could not send {msg}")
await self.disconnect(endpoint)
return False
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
self.logger.info(f"Outgoing message: {msg}")
return True
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
@@ -301,12 +303,12 @@ class Context:
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception("Exception during send_encoded_msgs")
self.logger.exception("Exception during send_encoded_msgs")
await self.disconnect(endpoint)
return False
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
self.logger.info(f"Outgoing message: {msg}")
return True
async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool:
@@ -317,11 +319,11 @@ class Context:
try:
websockets.broadcast(sockets, msg)
except RuntimeError:
logging.exception("Exception during broadcast_send_encoded_msgs")
self.logger.exception("Exception during broadcast_send_encoded_msgs")
return False
else:
if self.log_network:
logging.info(f"Outgoing broadcast: {msg}")
self.logger.info(f"Outgoing broadcast: {msg}")
return True
def broadcast_all(self, msgs: typing.List[dict]):
@@ -330,7 +332,7 @@ class Context:
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
logging.info("Notice (all): %s" % text)
self.logger.info("Notice (all): %s" % text)
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
def broadcast_team(self, team: int, msgs: typing.List[dict]):
@@ -352,7 +354,7 @@ class Context:
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
if not client.auth:
return
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
@@ -451,7 +453,7 @@ class Context:
for game_name, data in decoded_obj.get("datapackage", {}).items():
if game_name in game_data_packages:
data = game_data_packages[game_name]
logging.info(f"Loading embedded data package for game {game_name}")
self.logger.info(f"Loading embedded data package for game {game_name}")
self.gamespackage[game_name] = data
self.item_name_groups[game_name] = data["item_name_groups"]
if "location_name_groups" in data:
@@ -483,7 +485,7 @@ class Context:
with open(self.save_filename, "wb") as f:
f.write(zlib.compress(encoded_save))
except Exception as e:
logging.exception(e)
self.logger.exception(e)
return False
else:
return True
@@ -501,9 +503,9 @@ class Context:
save_data = restricted_loads(zlib.decompress(f.read()))
self.set_save(save_data)
except FileNotFoundError:
logging.error('No save data found, starting a new game')
self.logger.error('No save data found, starting a new game')
except Exception as e:
logging.exception(e)
self.logger.exception(e)
self._start_async_saving()
def _start_async_saving(self):
@@ -520,11 +522,11 @@ class Context:
next_wakeup = (second - get_datetime_second()) % self.auto_save_interval
time.sleep(max(1.0, next_wakeup))
if self.save_dirty:
logging.debug("Saving via thread.")
self.logger.debug("Saving via thread.")
self._save()
except OperationalError as e:
logging.exception(e)
logging.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
self.logger.exception(e)
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
else:
self.save_dirty = False
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
@@ -598,7 +600,7 @@ class Context:
if "stored_data" in savedata:
self.stored_data = savedata["stored_data"]
# count items and slots from lists for items_handling = remote
logging.info(
self.logger.info(
f'Loaded save file with {sum([len(v) for k, v in self.received_items.items() if k[2]])} received items '
f'for {sum(k[2] for k in self.received_items)} players')
@@ -640,13 +642,13 @@ class Context:
try:
raise Exception(f"Could not set server option {key}, skipping.") from e
except Exception as e:
logging.exception(e)
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
self.logger.exception(e)
self.logger.debug(f"Setting server option {key} to {value} from supplied multidata")
setattr(self, key, value)
elif key == "disable_item_cheat":
self.item_cheat = not bool(value)
else:
logging.debug(f"Unrecognized server option {key}")
self.logger.debug(f"Unrecognized server option {key}")
def get_aliased_name(self, team: int, slot: int):
if (team, slot) in self.name_aliases:
@@ -680,7 +682,7 @@ class Context:
self.hints[team, player].add(hint)
new_hint_events.add(player)
logging.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
for slot in new_hint_events:
self.on_new_hint(team, slot)
for slot, hint_data in concerns.items():
@@ -739,21 +741,21 @@ async def server(websocket, path: str = "/", ctx: Context = None):
try:
if ctx.log_network:
logging.info("Incoming connection")
ctx.logger.info("Incoming connection")
await on_client_connected(ctx, client)
if ctx.log_network:
logging.info("Sent Room Info")
ctx.logger.info("Sent Room Info")
async for data in websocket:
if ctx.log_network:
logging.info(f"Incoming message: {data}")
ctx.logger.info(f"Incoming message: {data}")
for msg in decode(data):
await process_client_cmd(ctx, client, msg)
except Exception as e:
if not isinstance(e, websockets.WebSocketException):
logging.exception(e)
ctx.logger.exception(e)
finally:
if ctx.log_network:
logging.info("Disconnected")
ctx.logger.info("Disconnected")
await ctx.disconnect(client)
@@ -985,7 +987,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
new_item = NetworkItem(item_id, location, slot, flags)
send_items_to(ctx, team, target_player, new_item)
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
ctx.player_names[(team, target_player)], ctx.location_names[location]))
info_text = json_format_send_event(new_item, target_player)
@@ -1625,7 +1627,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
try:
cmd: str = args["cmd"]
except:
logging.exception(f"Could not get command from {args}")
ctx.logger.exception(f"Could not get command from {args}")
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
"text": f"Could not get command from {args} at `cmd`"}])
raise
@@ -1668,7 +1670,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
if ctx.compatibility == 0 and args['version'] != version_tuple:
errors.add('IncompatibleVersion')
if errors:
logging.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
ctx.logger.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
else:
team, slot = ctx.connect_names[args['name']]
@@ -2286,7 +2288,7 @@ async def auto_shutdown(ctx, to_cancel=None):
if to_cancel:
for task in to_cancel:
task.cancel()
logging.info("Shutting down due to inactivity.")
ctx.logger.info("Shutting down due to inactivity.")
while not ctx.exit_event.is_set():
if not ctx.client_activity_timers.values():

View File

@@ -140,12 +140,6 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
def current_key(self) -> str:
return self.name_lookup[self.value]
def get_current_option_name(self) -> str:
"""Deprecated. use current_option_name instead. TODO remove around 0.4"""
logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated."
f" use current_option_name instead. Worlds should use {self}.current_key"))
return self.current_option_name
@property
def current_option_name(self) -> str:
"""For display purposes. Worlds should be using current_key."""
@@ -750,37 +744,6 @@ class NamedRange(Range):
return super().from_text(text)
class SpecialRange(NamedRange):
special_range_cutoff = 0
# TODO: remove class SpecialRange, earliest 3 releases after 0.4.3
def __new__(cls, value: int) -> SpecialRange:
from Utils import deprecate
deprecate(f"Option type {cls.__name__} is a subclass of SpecialRange, which is deprecated and pending removal. "
"Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In "
"NamedRange, range_start specifies the lower end of the regular range, while special values can be "
"placed anywhere (below, inside, or above the regular range).")
return super().__new__(cls)
@classmethod
def weighted_range(cls, text) -> Range:
if text == "random-low":
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.special_range_cutoff))
elif text == "random-high":
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.range_end))
elif text == "random-middle":
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end))
elif text.startswith("random-range-"):
return cls.custom_range(text)
elif text == "random":
return cls(random.randint(cls.special_range_cutoff, cls.range_end))
else:
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
f"Acceptable values are: random, random-high, random-middle, random-low, "
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
class FreezeValidKeys(AssembleOptions):
def __new__(mcs, name, bases, attrs):
if "valid_keys" in attrs:

View File

@@ -66,6 +66,7 @@ Currently, the following games are supported:
* A Short Hike
* Yoshi's Island
* Mario & Luigi: Superstar Saga
* Bomb Rush Cyberfunk
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

@@ -23,6 +23,7 @@ app.jinja_env.filters['all'] = all
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import json
import logging
import multiprocessing
import threading
import time
import typing
from uuid import UUID
@@ -15,16 +14,6 @@ from Utils import restricted_loads
from .locker import Locker, AlreadyRunningException
def launch_room(room: Room, config: dict):
# requires db_session!
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout):
multiworld = multiworlds.get(room.id, None)
if not multiworld:
multiworld = MultiworldInstance(room, config)
multiworld.start()
def handle_generation_success(seed_id):
logging.info(f"Generation finished for seed {seed_id}")
@@ -59,21 +48,30 @@ def init_db(pony_config: dict):
db.generate_mapping()
def cleanup():
"""delete unowned user-content"""
with db_session:
# >>> bool(uuid.UUID(int=0))
# True
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
if rooms or seeds or slots:
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
def autohost(config: dict):
def keep_running():
try:
with Locker("autohost"):
# delete unowned user-content
with db_session:
# >>> bool(uuid.UUID(int=0))
# True
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
if rooms or seeds or slots:
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
run_guardian()
cleanup()
hosters = []
for x in range(config["HOSTERS"]):
hoster = MultiworldInstance(config, x)
hosters.append(hoster)
hoster.start()
while 1:
time.sleep(0.1)
with db_session:
@@ -81,7 +79,9 @@ def autohost(config: dict):
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout):
hosters[room.id.int % len(hosters)].start_room(room.id)
except AlreadyRunningException:
logging.info("Autohost reports as already running, not starting another.")
@@ -132,29 +132,38 @@ multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {}
class MultiworldInstance():
def __init__(self, room: Room, config: dict):
self.room_id = room.id
def __init__(self, config: dict, id: int):
self.room_ids = set()
self.process: typing.Optional[multiprocessing.Process] = None
with guardian_lock:
multiworlds[self.room_id] = self
self.ponyconfig = config["PONY"]
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
self.rooms_to_start = multiprocessing.Queue()
self.rooms_shutting_down = multiprocessing.Queue()
self.name = f"MultiHoster{id}"
def start(self):
if self.process and self.process.is_alive():
return False
logging.info(f"Spinning up {self.room_id}")
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host),
name="MultiHost")
args=(self.name, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host,
self.rooms_to_start, self.rooms_shutting_down),
name=self.name)
process.start()
# bind after start to prevent thread sync issues with guardian.
self.process = process
def start_room(self, room_id):
while not self.rooms_shutting_down.empty():
self.room_ids.remove(self.rooms_shutting_down.get(block=True, timeout=None))
if room_id in self.room_ids:
pass # should already be hosted currently.
else:
self.room_ids.add(room_id)
self.rooms_to_start.put(room_id)
def stop(self):
if self.process:
self.process.terminate()
@@ -168,40 +177,6 @@ class MultiworldInstance():
self.process = None
guardian = None
guardian_lock = threading.Lock()
def run_guardian():
global guardian
global multiworlds
with guardian_lock:
if not guardian:
try:
import resource
except ModuleNotFoundError:
pass # unix only module
else:
# Each Server is another file handle, so request as many as we can from the system
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
# set soft limit to hard limit
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
def guard():
while 1:
time.sleep(1)
done = []
with guardian_lock:
for key, instance in multiworlds.items():
if instance.done():
instance.collect()
done.append(key)
for key in done:
del (multiworlds[key])
guardian = threading.Thread(name="Guardian", target=guard)
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed, Slot
from .customserver import run_server_process, get_static_server_data
from .generate import gen_game

View File

@@ -5,6 +5,7 @@ import collections
import datetime
import functools
import logging
import multiprocessing
import pickle
import random
import socket
@@ -53,17 +54,19 @@ del MultiServer
class DBCommandProcessor(ServerCommandProcessor):
def output(self, text: str):
logging.info(text)
self.ctx.logger.info(text)
class WebHostContext(Context):
room_id: int
def __init__(self, static_server_data: dict):
def __init__(self, static_server_data: dict, logger: logging.Logger):
# static server data is used during _load_game_data to load required data,
# without needing to import worlds system, which takes quite a bit of memory
self.static_server_data = static_server_data
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
super(WebHostContext, self).__init__("", 0, "", "", 1,
40, True, "enabled", "enabled",
"enabled", 0, 2, logger=logger)
del self.static_server_data
self.main_loop = asyncio.get_running_loop()
self.video = {}
@@ -159,63 +162,95 @@ def get_static_server_data() -> dict:
return data
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
def set_up_logging(room_id) -> logging.Logger:
import os
# logger setup
logger = logging.getLogger(f"RoomLogger {room_id}")
# this *should* be empty, but just in case.
for handler in logger.handlers[:]:
logger.removeHandler(handler)
handler.close()
file_handler = logging.FileHandler(
os.path.join(Utils.user_path("logs"), f"{room_id}.txt"),
"a",
encoding="utf-8-sig")
file_handler.setFormatter(logging.Formatter("[%(asctime)s]: %(message)s"))
logger.setLevel(logging.INFO)
logger.addHandler(file_handler)
return logger
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str):
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
Utils.init_logging(name)
try:
import resource
except ModuleNotFoundError:
pass # unix only module
else:
# Each Server is another file handle, so request as many as we can from the system
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
# set soft limit to hard limit
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
del resource, file_limit
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
db.generate_mapping(check_tables=False)
async def main():
if "worlds" in sys.modules:
raise Exception("Worlds system should not be loaded in the custom server.")
if "worlds" in sys.modules:
raise Exception("Worlds system should not be loaded in the custom server.")
import gc
Utils.init_logging(str(room_id), write_mode="a")
ctx = WebHostContext(static_server_data)
ctx.load(room_id)
ctx.init_save()
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
gc.collect() # free intermediate objects used during setup
import gc
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
del cert_file, cert_key_file, ponyconfig
gc.collect() # free intermediate objects used during setup
loop = asyncio.get_event_loop()
async def start_room(room_id):
try:
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
logger = set_up_logging(room_id)
ctx = WebHostContext(static_server_data, logger)
ctx.load(room_id)
ctx.init_save()
try:
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
await ctx.server
except OSError: # likely port in use
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
await ctx.server
except OSError: # likely port in use
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6:
# Prefer IPv4, as most users seem to not have working ipv6 support
if not port:
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6:
# Prefer IPv4, as most users seem to not have working ipv6 support
if not port:
port = socketname[1]
elif wssocket.family == socket.AF_INET:
port = socketname[1]
elif wssocket.family == socket.AF_INET:
port = socketname[1]
if port:
logging.info(f'Hosting game at {host}:{port}')
if port:
ctx.logger.info(f'Hosting game at {host}:{port}')
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = port
else:
ctx.logger.exception("Could not determine port. Likely hosting failure.")
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = port
else:
logging.exception("Could not determine port. Likely hosting failure.")
with db_session:
ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task
ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task
# ensure auto launch is on the same page in regard to room activity.
with db_session:
room: Room = Room.get(id=ctx.room_id)
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
# ensure auto launch is on the same page in regard to room activity.
with db_session:
room: Room = Room.get(id=ctx.room_id)
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
logging.info("Shutting down")
with Locker(room_id):
try:
asyncio.run(main())
except (KeyboardInterrupt, SystemExit):
with db_session:
room = Room.get(id=room_id)
@@ -228,3 +263,17 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
# ensure the Room does not spin up again on its own, minute of safety buffer
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
raise
finally:
rooms_shutting_down.put(room_id)
class Starter(threading.Thread):
def run(self):
while 1:
next_room = rooms_to_run.get(block=True, timeout=None)
asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
logging.info(f"Starting room {next_room} on {name}.")
starter = Starter()
starter.daemon = True
starter.start()
loop.run_forever()

View File

@@ -49,7 +49,7 @@
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
{% if world.web.tutorials %}
<span class="link-spacer">|</span>
<a href="{{ url_for("tutorial_landing") }}#{{ game_name }}">Setup Guides</a>
<a href="{{ url_for("tutorial_landing", _anchor = game_name | urlencode) }}">Setup Guides</a>
{% endif %}
{% if world.web.options_page is string %}
<span class="link-spacer">|</span>

View File

@@ -16,6 +16,9 @@
# A Link to the Past
/worlds/alttp/ @Berserker66
# Aquaria
/worlds/aquaria/ @tioui
# ArchipIDLE
/worlds/archipidle/ @LegendaryLinux
@@ -25,6 +28,9 @@
# Blasphemous
/worlds/blasphemous/ @TRPG0
# Bomb Rush Cyberfunk
/worlds/bomb_rush_cyberfunk/ @TRPG0
# Bumper Stickers
/worlds/bumpstik/ @FelicitusNeko

View File

@@ -155,10 +155,12 @@ Gives the player starting hints for where the items defined here are.
Gives the player starting hints for the items on locations defined here.
### ExcludeLocations
Marks locations given here as `LocationProgressType.Excluded` so that progression items can't be placed on them.
Marks locations given here as `LocationProgressType.Excluded` so that neither progression nor useful items can be
placed on them.
### PriorityLocations
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them.
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
the pool.
### ItemLinks
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between

View File

@@ -17,13 +17,14 @@ Then run any of the starting point scripts, like Generate.py, and the included M
required modules and after pressing enter proceed to install everything automatically.
After this, you should be able to run the programs.
* `Launcher.py` gives access to many components, including clients registered in `worlds/LauncherComponents.py`.
* The Launcher button "Generate Template Options" will generate default yamls for all worlds.
* 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

@@ -25,6 +25,8 @@ class TestBase(unittest.TestCase):
{"medallions", "stones", "rewards", "logic_bottles"},
"Starcraft 2":
{"Missions", "WoL Missions"},
"Yu-Gi-Oh! 2006":
{"Campaign Boss Beaten"}
}
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game_name, game_name=game_name):

View File

@@ -206,8 +206,8 @@ class World(metaclass=AutoWorldRegister):
game: ClassVar[str]
"""name the game"""
topology_present: ClassVar[bool] = False
"""indicate if world type has any meaningful layout/pathing"""
topology_present: bool = False
"""indicate if this world has any meaningful layout/pathing"""
all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset()
"""gets automatically populated with all item and item group names"""

210
worlds/aquaria/Items.py Normal file
View File

@@ -0,0 +1,210 @@
"""
Author: Louis M
Date: Fri, 15 Mar 2024 18:41:40 +0000
Description: Manage items in the Aquaria game multiworld randomizer
"""
from typing import Optional
from enum import Enum
from BaseClasses import Item, ItemClassification
class ItemType(Enum):
"""
Used to indicate to the multi-world if an item is usefull or not
"""
NORMAL = 0
PROGRESSION = 1
JUNK = 2
class ItemGroup(Enum):
"""
Used to group items
"""
COLLECTIBLE = 0
INGREDIENT = 1
RECIPE = 2
HEALTH = 3
UTILITY = 4
SONG = 5
TURTLE = 6
class AquariaItem(Item):
"""
A single item in the Aquaria game.
"""
game: str = "Aquaria"
"""The name of the game"""
def __init__(self, name: str, classification: ItemClassification,
code: Optional[int], player: int):
"""
Initialisation of the Item
:param name: The name of the item
:param classification: If the item is usefull or not
:param code: The ID of the item (if None, it is an event)
:param player: The ID of the player in the multiworld
"""
super().__init__(name, classification, code, player)
class ItemData:
"""
Data of an item.
"""
id:int
count:int
type:ItemType
group:ItemGroup
def __init__(self, id:int, count:int, type:ItemType, group:ItemGroup):
"""
Initialisation of the item data
@param id: The item ID
@param count: the number of items in the pool
@param type: the importance type of the item
@param group: the usage of the item in the game
"""
self.id = id
self.count = count
self.type = type
self.group = group
"""Information data for every (not event) item."""
item_table = {
# name: ID, Nb, Item Type, Item Group
"Anemone": ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone
"Arnassi statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue
"Big seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed
"Glowing seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed
"Black pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl
"Baby blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster
"Crab armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume
"Baby dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo
"Tooth": ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss
"Energy statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue
"Krotite armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple
"Golden starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star
"Golden gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear
"Jelly beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon
"Jelly costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume
"Jelly plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant
"Mithalas doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll
"Mithalan dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume
"Mithalas banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner
"Mithalas pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot
"Mutant costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
"Baby nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
"Baby piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
"Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume
"Seed bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
"King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
"Song plant spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
"Stone head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head
"Sun key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key
"Girl costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume
"Odd container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest
"Trident": ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
"Turtle egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
"Jelly egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
"Urchin costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
"Baby walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
"Vedha's Cure-All-All": ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
"Zuuna's perogi": ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
"Arcane poultice": ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice
"Berry ice cream": ItemData(698039, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_berryicecream
"Buttery sea loaf": ItemData(698040, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_butterysealoaf
"Cold borscht": ItemData(698041, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldborscht
"Cold soup": ItemData(698042, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldsoup
"Crab cake": ItemData(698043, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_crabcake
"Divine soup": ItemData(698044, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_divinesoup
"Dumbo ice cream": ItemData(698045, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_dumboicecream
"Fish oil": ItemData(698046, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
"Glowing egg": ItemData(698047, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
"Hand roll": ItemData(698048, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_handroll
"Healing poultice": ItemData(698049, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
"Hearty soup": ItemData(698050, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_heartysoup
"Hot borscht": ItemData(698051, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_hotborscht
"Hot soup": ItemData(698052, 3, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
"Ice cream": ItemData(698053, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_icecream
"Leadership roll": ItemData(698054, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
"Leaf poultice": ItemData(698055, 5, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
"Leeching poultice": ItemData(698056, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leechingpoultice
"Legendary cake": ItemData(698057, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_legendarycake
"Loaf of life": ItemData(698058, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_loafoflife
"Long life soup": ItemData(698059, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_longlifesoup
"Magic soup": ItemData(698060, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_magicsoup
"Mushroom x 2": ItemData(698061, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_mushroom
"Perogi": ItemData(698062, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_perogi
"Plant leaf": ItemData(698063, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
"Plump perogi": ItemData(698064, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_plumpperogi
"Poison loaf": ItemData(698065, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonloaf
"Poison soup": ItemData(698066, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonsoup
"Rainbow mushroom": ItemData(698067, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_rainbowmushroom
"Rainbow soup": ItemData(698068, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_rainbowsoup
"Red berry": ItemData(698069, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redberry
"Red bulb x 2": ItemData(698070, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redbulb
"Rotten cake": ItemData(698071, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottencake
"Rotten loaf x 8": ItemData(698072, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottenloaf
"Rotten meat": ItemData(698073, 5, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
"Royal soup": ItemData(698074, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_royalsoup
"Sea cake": ItemData(698075, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_seacake
"Sea loaf": ItemData(698076, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
"Shark fin soup": ItemData(698077, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sharkfinsoup
"Sight poultice": ItemData(698078, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sightpoultice
"Small bone x 2": ItemData(698079, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
"Small egg": ItemData(698080, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
"Small tentacle x 2": ItemData(698081, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smalltentacle
"Special bulb": ItemData(698082, 5, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_specialbulb
"Special cake": ItemData(698083, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_specialcake
"Spicy meat x 2": ItemData(698084, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_spicymeat
"Spicy roll": ItemData(698085, 11, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicyroll
"Spicy soup": ItemData(698086, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicysoup
"Spider roll": ItemData(698087, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spiderroll
"Swamp cake": ItemData(698088, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_swampcake
"Tasty cake": ItemData(698089, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastycake
"Tasty roll": ItemData(698090, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastyroll
"Tough cake": ItemData(698091, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_toughcake
"Turtle soup": ItemData(698092, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_turtlesoup
"Vedha sea crisp": ItemData(698093, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_vedhaseacrisp
"Veggie cake": ItemData(698094, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiecake
"Veggie ice cream": ItemData(698095, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggieicecream
"Veggie soup": ItemData(698096, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiesoup
"Volcano roll": ItemData(698097, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_volcanoroll
"Health upgrade": ItemData(698098, 5, ItemType.NORMAL, ItemGroup.HEALTH), # upgrade_health_?
"Wok": ItemData(698099, 1, ItemType.NORMAL, ItemGroup.UTILITY), # upgrade_wok
"Eel oil x 2": ItemData(698100, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_eeloil
"Fish meat x 2": ItemData(698101, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishmeat
"Fish oil x 3": ItemData(698102, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
"Glowing egg x 2": ItemData(698103, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
"Healing poultice x 2": ItemData(698104, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
"Hot soup x 2": ItemData(698105, 1, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
"Leadership roll x 2": ItemData(698106, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
"Leaf poultice x 3": ItemData(698107, 2, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
"Plant leaf x 2": ItemData(698108, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
"Plant leaf x 3": ItemData(698109, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
"Rotten meat x 2": ItemData(698110, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
"Rotten meat x 8": ItemData(698111, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
"Sea loaf x 2": ItemData(698112, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
"Small bone x 3": ItemData(698113, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
"Small egg x 2": ItemData(698114, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
"Li and Li song": ItemData(698115, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_li
"Shield song": ItemData(698116, 1, ItemType.NORMAL, ItemGroup.SONG), # song_shield
"Beast form": ItemData(698117, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_beast
"Sun form": ItemData(698118, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_sun
"Nature form": ItemData(698119, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_nature
"Energy form": ItemData(698120, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_energy
"Bind song": ItemData(698121, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_bind
"Fish form": ItemData(698122, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_fish
"Spirit form": ItemData(698123, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_spirit
"Dual form": ItemData(698124, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_dual
"Transturtle Veil top left": ItemData(698125, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil01
"Transturtle Veil top right": ItemData(698126, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil02
"Transturtle Open Water top right": ItemData(698127, 1, ItemType.PROGRESSION,
ItemGroup.TURTLE), # transport_openwater03
"Transturtle Forest bottom left": ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest04
"Transturtle Home water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea
"Transturtle Abyss right": ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03
"Transturtle Final Boss": ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss
"Transturtle Simon says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
"Transturtle Arnassi ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
}

574
worlds/aquaria/Locations.py Normal file
View File

@@ -0,0 +1,574 @@
"""
Author: Louis M
Date: Fri, 15 Mar 2024 18:41:40 +0000
Description: Manage locations in the Aquaria game multiworld randomizer
"""
from BaseClasses import Location
class AquariaLocation(Location):
"""
A location in the game.
"""
game: str = "Aquaria"
"""The name of the game"""
def __init__(self, player: int, name="", code=None, parent=None) -> None:
"""
Initialisation of the object
:param player: the ID of the player
:param name: the name of the location
:param code: the ID (or address) of the location (Event if None)
:param parent: the Region that this location belongs to
"""
super(AquariaLocation, self).__init__(player, name, code, parent)
self.event = code is None
class AquariaLocations:
locations_verse_cave_r = {
"Verse cave, bulb in the skeleton room": 698107,
"Verse cave, bulb in the path left of the skeleton room": 698108,
"Verse cave right area, Big Seed": 698175,
}
locations_verse_cave_l = {
"Verse cave, the Naija hint about here shield ability": 698200,
"Verse cave left area, bulb in the center part": 698021,
"Verse cave left area, bulb in the right part": 698022,
"Verse cave left area, bulb under the rock at the end of the path": 698023,
}
locations_home_water = {
"Home water, bulb below the grouper fish": 698058,
"Home water, bulb in the path bellow Nautilus Prime": 698059,
"Home water, bulb in the little room above the grouper fish": 698060,
"Home water, bulb in the end of the left path from the verse cave": 698061,
"Home water, bulb in the top left path": 698062,
"Home water, bulb in the bottom left room": 698063,
"Home water, bulb close to the Naija's home": 698064,
"Home water, bulb under the rock in the left path from the verse cave": 698065,
}
locations_home_water_nautilus = {
"Home water, Nautilus Egg": 698194,
}
locations_home_water_transturtle = {
"Home water, Transturtle": 698213,
}
locations_naija_home = {
"Naija's home, bulb after the energy door": 698119,
"Naija's home, bulb under the rock at the right of the main path": 698120,
}
locations_song_cave = {
"Song cave, Erulian spirit": 698206,
"Song cave, bulb in the top left part": 698071,
"Song cave, bulb in the big anemone room": 698072,
"Song cave, bulb in the path to the singing statues": 698073,
"Song cave, bulb under the rock in the path to the singing statues": 698074,
"Song cave, bulb under the rock close to the song door": 698075,
"Song cave, Verse egg": 698160,
"Song cave, Jelly beacon": 698178,
"Song cave, Anemone seed": 698162,
}
locations_energy_temple_1 = {
"Energy temple first area, beating the energy statue": 698205,
"Energy temple first area, bulb in the bottom room blocked by a rock": 698027,
}
locations_energy_temple_idol = {
"Energy temple first area, Energy Idol": 698170,
}
locations_energy_temple_2 = {
"Energy temple second area, bulb under the rock": 698028,
}
locations_energy_temple_altar = {
"Energy temple bottom entrance, Krotite armor": 698163,
}
locations_energy_temple_3 = {
"Energy temple third area, bulb in the bottom path": 698029,
}
locations_energy_temple_boss = {
"Energy temple boss area, Fallen god tooth": 698169,
}
locations_energy_temple_blaster_room = {
"Energy temple blaster room, Blaster egg": 698195,
}
locations_openwater_tl = {
"Open water top left area, bulb under the rock in the right path": 698001,
"Open water top left area, bulb under the rock in the left path": 698002,
"Open water top left area, bulb to the right of the save cristal": 698003,
}
locations_openwater_tr = {
"Open water top right area, bulb in the small path before Mithalas": 698004,
"Open water top right area, bulb in the path from the left entrance": 698005,
"Open water top right area, bulb in the clearing close to the bottom exit": 698006,
"Open water top right area, bulb in the big clearing close to the save cristal": 698007,
"Open water top right area, bulb in the big clearing to the top exit": 698008,
"Open water top right area, first urn in the Mithalas exit": 698148,
"Open water top right area, second urn in the Mithalas exit": 698149,
"Open water top right area, third urn in the Mithalas exit": 698150,
}
locations_openwater_tr_turtle = {
"Open water top right area, bulb in the turtle room": 698009,
"Open water top right area, Transturtle": 698211,
}
locations_openwater_bl = {
"Open water bottom left area, bulb behind the chomper fish": 698011,
"Open water bottom left area, bulb inside the downest fish pass": 698010,
}
locations_skeleton_path = {
"Open water skeleton path, bulb close to the right exit": 698012,
"Open water skeleton path, bulb behind the chomper fish": 698013,
}
locations_skeleton_path_sc = {
"Open water skeleton path, King skull": 698177,
}
locations_arnassi = {
"Arnassi Ruins, bulb in the right part": 698014,
"Arnassi Ruins, bulb in the left part": 698015,
"Arnassi Ruins, bulb in the center part": 698016,
"Arnassi ruins, Song plant spore on the top of the ruins": 698179,
"Arnassi ruins, Arnassi Armor": 698191,
}
locations_arnassi_path = {
"Arnassi Ruins, Arnassi statue": 698164,
"Arnassi Ruins, Transturtle": 698217,
}
locations_arnassi_crab_boss = {
"Arnassi ruins, Crab armor": 698187,
}
locations_simon = {
"Kelp forest, beating Simon says": 698156,
"Simon says area, Transturtle": 698216,
}
locations_mithalas_city = {
"Mithalas city, first bulb in the left city part": 698030,
"Mithalas city, second bulb in the left city part": 698035,
"Mithalas city, bulb in the right part": 698031,
"Mithalas city, bulb at the top of the city": 698033,
"Mithalas city, first bulb in a broken home": 698034,
"Mithalas city, second bulb in a broken home": 698041,
"Mithalas city, bulb in the bottom left part": 698037,
"Mithalas city, first bulb in one of the homes": 698038,
"Mithalas city, second bulb in one of the homes": 698039,
"Mithalas city, first urn in one of the homes": 698123,
"Mithalas city, second urn in one of the homes": 698124,
"Mithalas city, first urn in the city reserve": 698125,
"Mithalas city, second urn in the city reserve": 698126,
"Mithalas city, third urn in the city reserve": 698127,
}
locations_mithalas_city_top_path = {
"Mithalas city, first bulb at the end of the top path": 698032,
"Mithalas city, second bulb at the end of the top path": 698040,
"Mithalas city, bulb in the top path": 698036,
"Mithalas city, Mithalas pot": 698174,
"Mithalas city, urn in the cathedral flower tube entrance": 698128,
}
locations_mithalas_city_fishpass = {
"Mithalas city, Doll": 698173,
"Mithalas city, urn inside a home fish pass": 698129,
}
locations_cathedral_l = {
"Mithalas city castle, bulb in the flesh hole": 698042,
"Mithalas city castle, Blue banner": 698165,
"Mithalas city castle, urn in the bedroom": 698130,
"Mithalas city castle, first urn of the single lamp path": 698131,
"Mithalas city castle, second urn of the single lamp path": 698132,
"Mithalas city castle, urn in the bottom room": 698133,
"Mithalas city castle, first urn on the entrance path": 698134,
"Mithalas city castle, second urn on the entrance path": 698135,
}
locations_cathedral_l_tube = {
"Mithalas castle, beating the priests": 698208,
}
locations_cathedral_l_sc = {
"Mithalas city castle, Trident head": 698183,
}
locations_cathedral_r = {
"Mithalas cathedral, first urn in the top right room": 698136,
"Mithalas cathedral, second urn in the top right room": 698137,
"Mithalas cathedral, third urn in the top right room": 698138,
"Mithalas cathedral, urn in the flesh room with fleas": 698139,
"Mithalas cathedral, first urn in the bottom right path": 698140,
"Mithalas cathedral, second urn in the bottom right path": 698141,
"Mithalas cathedral, urn behind the flesh vein": 698142,
"Mithalas cathedral, urn in the top left eyes boss room": 698143,
"Mithalas cathedral, first urn in the path behind the flesh vein": 698144,
"Mithalas cathedral, second urn in the path behind the flesh vein": 698145,
"Mithalas cathedral, third urn in the path behind the flesh vein": 698146,
"Mithalas cathedral, one of the urns in the top right room": 698147,
"Mithalas cathedral, Mithalan Dress": 698189,
"Mithalas cathedral right area, urn bellow the left entrance": 698198,
}
locations_cathedral_underground = {
"Cathedral underground, bulb in the center part": 698113,
"Cathedral underground, first bulb in the top left part": 698114,
"Cathedral underground, second bulb in the top left part": 698115,
"Cathedral underground, third bulb in the top left part": 698116,
"Cathedral underground, bulb close to the save cristal": 698117,
"Cathedral underground, bulb in the bottom right path": 698118,
}
locations_cathedral_boss = {
"Cathedral boss area, beating Mithalan God": 698202,
}
locations_forest_tl = {
"Kelp Forest top left area, bulb in the bottom left clearing": 698044,
"Kelp Forest top left area, bulb in the path down from the top left clearing": 698045,
"Kelp Forest top left area, bulb in the top left clearing": 698046,
"Kelp Forest top left, Jelly Egg": 698185,
}
locations_forest_tl_fp = {
"Kelp Forest top left area, bulb close to the Verse egg": 698047,
"Kelp forest top left area, Verse egg": 698158,
}
locations_forest_tr = {
"Kelp Forest top right area, bulb under the rock in the right path": 698048,
"Kelp Forest top right area, bulb at the left of the center clearing": 698049,
"Kelp Forest top right area, bulb in the left path's big room": 698051,
"Kelp Forest top right area, bulb in the left path's small room": 698052,
"Kelp Forest top right area, bulb at the top of the center clearing": 698053,
"Kelp forest top right area, Black pearl": 698167,
}
locations_forest_tr_fp = {
"Kelp Forest top right area, bulb in the top fish pass": 698050,
}
locations_forest_bl = {
"Kelp Forest bottom left area, bulb close to the spirit crystals": 698054,
"Kelp forest bottom left area, Walker baby": 698186,
"Kelp Forest bottom left area, Transturtle": 698212,
}
locations_forest_br = {
"Kelp forest bottom right area, Odd Container": 698168,
}
locations_forest_boss = {
"Kelp forest boss area, beating Drunian God": 698204,
}
locations_forest_boss_entrance = {
"Kelp Forest boss room, bulb at the bottom of the area": 698055,
}
locations_forest_fish_cave = {
"Kelp Forest bottom left area, Fish cave puzzle": 698207,
}
locations_forest_sprite_cave = {
"Kelp Forest sprite cave, bulb inside the fish pass": 698056,
}
locations_forest_sprite_cave_tube = {
"Kelp Forest sprite cave, bulb in the second room": 698057,
"Kelp Forest Sprite Cave, Seed bag": 698176,
}
locations_mermog_cave = {
"Mermog cave, bulb in the left part of the cave": 698121,
}
locations_mermog_boss = {
"Mermog cave, Piranha Egg": 698197,
}
locations_veil_tl = {
"The veil top left area, In the Li cave": 698199,
"The veil top left area, bulb under the rock in the top right path": 698078,
"The veil top left area, bulb hidden behind the blocking rock": 698076,
"The veil top left area, Transturtle": 698209,
}
locations_veil_tl_fp = {
"The veil top left area, bulb inside the fish pass": 698077,
}
locations_turtle_cave = {
"Turtle cave, Turtle Egg": 698184,
}
locations_turtle_cave_bubble = {
"Turtle cave, bulb in bubble cliff": 698000,
"Turtle cave, Urchin costume": 698193,
}
locations_veil_tr_r = {
"The veil top right area, bulb in the middle of the wall jump cliff": 698079,
"The veil top right area, golden starfish at the bottom right of the bottom path": 698180,
}
locations_veil_tr_l = {
"The veil top right area, bulb in the top of the water fall": 698080,
"The veil top right area, Transturtle": 698210,
}
locations_veil_bl = {
"The veil bottom area, bulb in the left path": 698082,
}
locations_veil_b_sc = {
"The veil bottom area, bulb in the spirit path": 698081,
}
locations_veil_bl_fp = {
"The veil bottom area, Verse egg": 698157,
}
locations_veil_br = {
"The veil bottom area, Stone Head": 698181,
}
locations_octo_cave_t = {
"Octopus cave, Dumbo Egg": 698196,
}
locations_octo_cave_b = {
"Octopus cave, bulb in the path below the octopus cave path": 698122,
}
locations_sun_temple_l = {
"Sun temple, bulb in the top left part": 698094,
"Sun temple, bulb in the top right part": 698095,
"Sun temple, bulb at the top of the high dark room": 698096,
"Sun temple, Golden Gear": 698171,
}
locations_sun_temple_r = {
"Sun temple, first bulb of the temple": 698091,
"Sun temple, bulb on the left part": 698092,
"Sun temple, bulb in the hidden room of the right part": 698093,
"Sun temple, Sun key": 698182,
}
locations_sun_temple_boss_path = {
"Sun Worm path, first path bulb": 698017,
"Sun Worm path, second path bulb": 698018,
"Sun Worm path, first cliff bulb": 698019,
"Sun Worm path, second cliff bulb": 698020,
}
locations_sun_temple_boss = {
"Sun temple boss area, beating Sun God": 698203,
}
locations_abyss_l = {
"Abyss left area, bulb in hidden path room": 698024,
"Abyss left area, bulb in the right part": 698025,
"Abyss left area, Glowing seed": 698166,
"Abyss left area, Glowing Plant": 698172,
}
locations_abyss_lb = {
"Abyss left area, bulb in the bottom fish pass": 698026,
}
locations_abyss_r = {
"Abyss right area, bulb behind the rock in the whale room": 698109,
"Abyss right area, bulb in the middle path": 698110,
"Abyss right area, bulb behind the rock in the middle path": 698111,
"Abyss right area, bulb in the left green room": 698112,
"Abyss right area, Transturtle": 698214,
}
locations_ice_cave = {
"Ice cave, bulb in the room to the right": 698083,
"Ice cave, First bulbs in the top exit room": 698084,
"Ice cave, Second bulbs in the top exit room": 698085,
"Ice cave, third bulbs in the top exit room": 698086,
"Ice cave, bulb in the left room": 698087,
}
locations_bubble_cave = {
"Bubble cave, bulb in the left cave wall": 698089,
"Bubble cave, bulb in the right cave wall (behind the ice cristal)": 698090,
}
locations_bubble_cave_boss = {
"Bubble cave, Verse egg": 698161,
}
locations_king_jellyfish_cave = {
"King Jellyfish cave, bulb in the right path from King Jelly": 698088,
"King Jellyfish cave, Jellyfish Costume": 698188,
}
locations_whale = {
"The whale, Verse egg": 698159,
}
locations_sunken_city_r = {
"Sunken city right area, crate close to the save cristal": 698154,
"Sunken city right area, crate in the left bottom room": 698155,
}
locations_sunken_city_l = {
"Sunken city left area, crate in the little pipe room": 698151,
"Sunken city left area, crate close to the save cristal": 698152,
"Sunken city left area, crate before the bedroom": 698153,
}
locations_sunken_city_l_bedroom = {
"Sunken city left area, Girl Costume": 698192,
}
locations_sunken_city_boss = {
"Sunken city, bulb on the top of the boss area (boiler room)": 698043,
}
locations_body_c = {
"The body center area, breaking li cage": 698201,
"The body main area, bulb on the main path blocking tube": 698097,
}
locations_body_l = {
"The body left area, first bulb in the top face room": 698066,
"The body left area, second bulb in the top face room": 698069,
"The body left area, bulb bellow the water stream": 698067,
"The body left area, bulb in the top path to the top face room": 698068,
"The body left area, bulb in the bottom face room": 698070,
}
locations_body_rt = {
"The body right area, bulb in the top face room": 698100,
}
locations_body_rb = {
"The body right area, bulb in the top path to the bottom face room": 698098,
"The body right area, bulb in the bottom face room": 698099,
}
locations_body_b = {
"The body bottom area, bulb in the Jelly Zap room": 698101,
"The body bottom area, bulb in the nautilus room": 698102,
"The body bottom area, Mutant Costume": 698190,
}
locations_final_boss_tube = {
"Final boss area, first bulb in the turtle room": 698103,
"Final boss area, second bulbs in the turtle room": 698104,
"Final boss area, third bulbs in the turtle room": 698105,
"Final boss area, Transturtle": 698215,
}
locations_final_boss = {
"Final boss area, bulb in the boss third form room": 698106,
}
location_table = {
**AquariaLocations.locations_openwater_tl,
**AquariaLocations.locations_openwater_tr,
**AquariaLocations.locations_openwater_tr_turtle,
**AquariaLocations.locations_openwater_bl,
**AquariaLocations.locations_skeleton_path,
**AquariaLocations.locations_skeleton_path_sc,
**AquariaLocations.locations_arnassi,
**AquariaLocations.locations_arnassi_path,
**AquariaLocations.locations_arnassi_crab_boss,
**AquariaLocations.locations_sun_temple_l,
**AquariaLocations.locations_sun_temple_r,
**AquariaLocations.locations_sun_temple_boss_path,
**AquariaLocations.locations_sun_temple_boss,
**AquariaLocations.locations_verse_cave_r,
**AquariaLocations.locations_verse_cave_l,
**AquariaLocations.locations_abyss_l,
**AquariaLocations.locations_abyss_lb,
**AquariaLocations.locations_abyss_r,
**AquariaLocations.locations_energy_temple_1,
**AquariaLocations.locations_energy_temple_2,
**AquariaLocations.locations_energy_temple_3,
**AquariaLocations.locations_energy_temple_boss,
**AquariaLocations.locations_energy_temple_blaster_room,
**AquariaLocations.locations_energy_temple_altar,
**AquariaLocations.locations_energy_temple_idol,
**AquariaLocations.locations_mithalas_city,
**AquariaLocations.locations_mithalas_city_top_path,
**AquariaLocations.locations_mithalas_city_fishpass,
**AquariaLocations.locations_cathedral_l,
**AquariaLocations.locations_cathedral_l_tube,
**AquariaLocations.locations_cathedral_l_sc,
**AquariaLocations.locations_cathedral_r,
**AquariaLocations.locations_cathedral_underground,
**AquariaLocations.locations_cathedral_boss,
**AquariaLocations.locations_forest_tl,
**AquariaLocations.locations_forest_tl_fp,
**AquariaLocations.locations_forest_tr,
**AquariaLocations.locations_forest_tr_fp,
**AquariaLocations.locations_forest_bl,
**AquariaLocations.locations_forest_br,
**AquariaLocations.locations_forest_boss,
**AquariaLocations.locations_forest_boss_entrance,
**AquariaLocations.locations_forest_sprite_cave,
**AquariaLocations.locations_forest_sprite_cave_tube,
**AquariaLocations.locations_forest_fish_cave,
**AquariaLocations.locations_home_water,
**AquariaLocations.locations_home_water_transturtle,
**AquariaLocations.locations_home_water_nautilus,
**AquariaLocations.locations_body_l,
**AquariaLocations.locations_body_rt,
**AquariaLocations.locations_body_rb,
**AquariaLocations.locations_body_c,
**AquariaLocations.locations_body_b,
**AquariaLocations.locations_final_boss_tube,
**AquariaLocations.locations_final_boss,
**AquariaLocations.locations_song_cave,
**AquariaLocations.locations_veil_tl,
**AquariaLocations.locations_veil_tl_fp,
**AquariaLocations.locations_turtle_cave,
**AquariaLocations.locations_turtle_cave_bubble,
**AquariaLocations.locations_veil_tr_r,
**AquariaLocations.locations_veil_tr_l,
**AquariaLocations.locations_veil_bl,
**AquariaLocations.locations_veil_b_sc,
**AquariaLocations.locations_veil_bl_fp,
**AquariaLocations.locations_veil_br,
**AquariaLocations.locations_ice_cave,
**AquariaLocations.locations_king_jellyfish_cave,
**AquariaLocations.locations_bubble_cave,
**AquariaLocations.locations_bubble_cave_boss,
**AquariaLocations.locations_naija_home,
**AquariaLocations.locations_mermog_cave,
**AquariaLocations.locations_mermog_boss,
**AquariaLocations.locations_octo_cave_t,
**AquariaLocations.locations_octo_cave_b,
**AquariaLocations.locations_sunken_city_l,
**AquariaLocations.locations_sunken_city_r,
**AquariaLocations.locations_sunken_city_boss,
**AquariaLocations.locations_sunken_city_l_bedroom,
**AquariaLocations.locations_simon,
**AquariaLocations.locations_whale,
}

145
worlds/aquaria/Options.py Normal file
View File

@@ -0,0 +1,145 @@
"""
Author: Louis M
Date: Fri, 15 Mar 2024 18:41:40 +0000
Description: Manage options in the Aquaria game multiworld randomizer
"""
from dataclasses import dataclass
from Options import Toggle, Choice, Range, DeathLink, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool
class IngredientRandomizer(Choice):
"""
Randomize Ingredients. Select if the simple ingredients (that does not have
a recipe) should be randomized. If 'common_ingredients' is selected, the
randomization will exclude the "Red Bulb", "Special Bulb" and "Rukh Egg".
"""
display_name = "Randomize Ingredients"
option_off = 0
option_common_ingredients = 1
option_all_ingredients = 2
default = 0
class DishRandomizer(Toggle):
"""Randomize the drop of Dishes (Ingredients with recipe)."""
display_name = "Dish Randomizer"
class TurtleRandomizer(Choice):
"""Randomize the transportation turtle."""
display_name = "Turtle Randomizer"
option_no_turtle_randomization = 0
option_randomize_all_turtle = 1
option_randomize_turtle_other_than_the_final_one = 2
default = 2
class EarlyEnergyForm(DefaultOnToggle):
"""
Force the Energy Form to be in a location before leaving the areas around the Home Water.
"""
display_name = "Early Energy Form"
class AquarianTranslation(Toggle):
"""Translate to English the Aquarian scripture in the game."""
display_name = "Translate Aquarian"
class BigBossesToBeat(Range):
"""
A number of big bosses to beat before having access to the creator (the final boss). The big bosses are
"Fallen God", "Mithalan God", "Drunian God", "Sun God" and "The Golem".
"""
display_name = "Big bosses to beat"
range_start = 0
range_end = 5
default = 0
class MiniBossesToBeat(Range):
"""
A number of Minibosses to beat before having access to the creator (the final boss). Mini bosses are
"Nautilus Prime", "Blaster Peg Prime", "Mergog", "Mithalan priests", "Octopus Prime", "Crabbius Maximus",
"Mantis Shrimp Prime" and "King Jellyfish God Prime". Note that the Energy statue and Simon says are not
mini bosses.
"""
display_name = "Mini bosses to beat"
range_start = 0
range_end = 8
default = 0
class Objective(Choice):
"""
The game objective can be only to kill the creator or to kill the creator
and having obtained the three every secret memories
"""
display_name = "Objective"
option_kill_the_creator = 0
option_obtain_secrets_and_kill_the_creator = 1
default = 0
class SkipFirstVision(Toggle):
"""
The first vision in the game; where Naija transform to Energy Form and get fload by enemy; is quite cool but
can be quite long when you already know what is going on. This option can be used to skip this vision.
"""
display_name = "Skip first Naija's vision"
class NoProgressionHardOrHiddenLocation(Toggle):
"""
Make sure that there is no progression items at hard to get or hard to find locations.
Those locations that will be very High location (that need beast form, soup and skill to get), every
location in the bubble cave, locations that need you to cross a false wall without any indication, Arnassi
race, bosses and mini-bosses. Usefull for those that want a casual run.
"""
display_name = "No progression in hard or hidden locations"
class LightNeededToGetToDarkPlaces(DefaultOnToggle):
"""
Make sure that the sun form or the dumbo pet can be aquired before getting to dark places. Be aware that navigating
in dark place without light is extremely difficult.
"""
display_name = "Light needed to get to dark places"
class BindSongNeededToGetUnderRockBulb(Toggle):
"""
Make sure that the bind song can be aquired before having to obtain sing bulb under rocks.
"""
display_name = "Bind song needed to get sing bulbs under rocks"
class UnconfineHomeWater(Choice):
"""
Open the way out of Home water area so that Naija can go to open water and beyond without the bind song.
"""
display_name = "Unconfine Home Water Area"
option_off = 0
option_via_energy_door = 1
option_via_transturtle = 2
option_via_both = 3
default = 0
@dataclass
class AquariaOptions(PerGameCommonOptions):
"""
Every option in the Aquaria randomizer
"""
start_inventory_from_pool: StartInventoryPool
objective: Objective
mini_bosses_to_beat: MiniBossesToBeat
big_bosses_to_beat: BigBossesToBeat
turtle_randomizer: TurtleRandomizer
early_energy_form: EarlyEnergyForm
light_needed_to_get_to_dark_places: LightNeededToGetToDarkPlaces
bind_song_needed_to_get_under_rock_bulb: BindSongNeededToGetUnderRockBulb
unconfine_home_water: UnconfineHomeWater
no_progression_hard_or_hidden_locations: NoProgressionHardOrHiddenLocation
ingredient_randomizer: IngredientRandomizer
dish_randomizer: DishRandomizer
aquarian_translation: AquarianTranslation
skip_first_vision: SkipFirstVision
death_link: DeathLink

1401
worlds/aquaria/Regions.py Executable file

File diff suppressed because it is too large Load Diff

218
worlds/aquaria/__init__.py Normal file
View File

@@ -0,0 +1,218 @@
"""
Author: Louis M
Date: Fri, 15 Mar 2024 18:41:40 +0000
Description: Main module for Aquaria game multiworld randomizer
"""
from typing import List, Dict, ClassVar, Any
from ..AutoWorld import World, WebWorld
from BaseClasses import Tutorial, MultiWorld, ItemClassification
from .Items import item_table, AquariaItem, ItemType, ItemGroup
from .Locations import location_table
from .Options import AquariaOptions
from .Regions import AquariaRegions
class AquariaWeb(WebWorld):
"""
Class used to generate the Aquaria Game Web pages (setup, tutorial, etc.)
"""
theme = "ocean"
bug_report_page = "https://github.com/tioui/Aquaria_Randomizer/issues"
setup = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up Aquaria for MultiWorld.",
"English",
"setup_en.md",
"setup/en",
["Tioui"]
)
setup_fr = Tutorial(
"Guide de configuration Multimonde",
"Un guide pour configurer Aquaria MultiWorld",
"Français",
"setup_fr.md",
"setup/fr",
["Tioui"]
)
tutorials = [setup, setup_fr]
class AquariaWorld(World):
"""
Aquaria is a side-scrolling action-adventure game. It follows Naija, an
aquatic humanoid woman, as she explores the underwater world of Aquaria.
Along her journey, she learns about the history of the world she inhabits
as well as her own past. The gameplay focuses on a combination of swimming,
singing, and combat, through which Naija can interact with the world. Her
songs can move items, affect plants and animals, and change her physical
appearance into other forms that have different abilities, like firing
projectiles at hostile creatures, or passing through barriers inaccessible
to her in her natural form.
From: https://en.wikipedia.org/wiki/Aquaria_(video_game)
"""
game: str = "Aquaria"
"The name of the game"
topology_present = True
"show path to required location checks in spoiler"
web: WebWorld = AquariaWeb()
"The web page generation informations"
item_name_to_id: ClassVar[Dict[str, int]] =\
{name: data.id for name, data in item_table.items()}
"The name and associated ID of each item of the world"
item_name_groups = {
"Damage": {"Energy form", "Nature form", "Beast form",
"Li and Li song", "Baby nautilus", "Baby piranha",
"Baby blaster"},
"Light": {"Sun form", "Baby dumbo"}
}
"""Grouping item make it easier to find them"""
location_name_to_id = location_table
"The name and associated ID of each location of the world"
base_id = 698000
"The starting ID of the items and locations of the world"
ingredients_substitution: List[int]
"Used to randomize ingredient drop"
options_dataclass = AquariaOptions
"Used to manage world options"
options: AquariaOptions
"Every options of the world"
regions: AquariaRegions
"Used to manage Regions"
exclude: List[str]
def __init__(self, multiworld: MultiWorld, player: int):
"""Initialisation of the Aquaria World"""
super(AquariaWorld, self).__init__(multiworld, player)
self.regions = AquariaRegions(multiworld, player)
self.ingredients_substitution = []
self.exclude = []
def create_regions(self) -> None:
"""
Create every Region in `regions`
"""
self.regions.add_regions_to_world()
self.regions.connect_regions()
self.regions.add_event_locations()
def create_item(self, name: str) -> AquariaItem:
"""
Create an AquariaItem using `name' as item name.
"""
result: AquariaItem
try:
data = item_table[name]
classification: ItemClassification = ItemClassification.useful
if data.type == ItemType.JUNK:
classification = ItemClassification.filler
elif data.type == ItemType.PROGRESSION:
classification = ItemClassification.progression
result = AquariaItem(name, classification, data.id, self.player)
except BaseException:
raise Exception('The item ' + name + ' is not valid.')
return result
def __pre_fill_item(self, item_name: str, location_name: str, precollected) -> None:
"""Pre-assign an item to a location"""
if item_name not in precollected:
self.exclude.append(item_name)
data = item_table[item_name]
item = AquariaItem(item_name, ItemClassification.useful, data.id, self.player)
self.multiworld.get_location(location_name, self.player).place_locked_item(item)
def get_filler_item_name(self):
"""Getting a random ingredient item as filler"""
ingredients = []
for name, data in item_table.items():
if data.group == ItemGroup.INGREDIENT:
ingredients.append(name)
filler_item_name = self.random.choice(ingredients)
return filler_item_name
def create_items(self) -> None:
"""Create every item in the world"""
precollected = [item.name for item in self.multiworld.precollected_items[self.player]]
if self.options.turtle_randomizer.value > 0:
if self.options.turtle_randomizer.value == 2:
self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected)
else:
self.__pre_fill_item("Transturtle Veil top left", "The veil top left area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Veil top right", "The veil top right area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Open Water top right", "Open water top right area, Transturtle",
precollected)
self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle",
precollected)
self.__pre_fill_item("Transturtle Home water", "Home water, Transturtle", precollected)
self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected)
# The last two are inverted because in the original game, they are special turtle that communicate directly
self.__pre_fill_item("Transturtle Simon says", "Arnassi Ruins, Transturtle", precollected)
self.__pre_fill_item("Transturtle Arnassi ruins", "Simon says area, Transturtle", precollected)
for name, data in item_table.items():
if name in precollected:
precollected.remove(name)
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
else:
if name not in self.exclude:
for i in range(data.count):
item = self.create_item(name)
self.multiworld.itempool.append(item)
def set_rules(self) -> None:
"""
Launched when the Multiworld generator is ready to generate rules
"""
self.regions.adjusting_rules(self.options)
self.multiworld.completion_condition[self.player] = lambda \
state: state.has("Victory", self.player)
def generate_basic(self) -> None:
"""
Player-specific randomization that does not affect logic.
Used to fill then `ingredients_substitution` list
"""
simple_ingredients_substitution = [i for i in range(27)]
if self.options.ingredient_randomizer.value > 0:
if self.options.ingredient_randomizer.value == 1:
simple_ingredients_substitution.pop(-1)
simple_ingredients_substitution.pop(-1)
simple_ingredients_substitution.pop(-1)
self.random.shuffle(simple_ingredients_substitution)
if self.options.ingredient_randomizer.value == 1:
simple_ingredients_substitution.extend([24, 25, 26])
dishes_substitution = [i for i in range(27, 76)]
if self.options.dish_randomizer:
self.random.shuffle(dishes_substitution)
self.ingredients_substitution.clear()
self.ingredients_substitution.extend(simple_ingredients_substitution)
self.ingredients_substitution.extend(dishes_substitution)
def fill_slot_data(self) -> Dict[str, Any]:
return {"ingredientReplacement": self.ingredients_substitution,
"aquarianTranslate": bool(self.options.aquarian_translation.value),
"secret_needed": self.options.objective.value > 0,
"minibosses_to_kill": self.options.mini_bosses_to_beat.value,
"bigbosses_to_kill": self.options.big_bosses_to_beat.value,
"skip_first_vision": bool(self.options.skip_first_vision.value),
"unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3],
"unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3],
}

View File

@@ -0,0 +1,64 @@
# Aquaria
## Game page in other languages:
* [Français](/games/Aquaria/info/fr)
## Where is the options page?
The player options page for this game contains all the options you need to configure and export a config file. Player
options page link: [Aquaria Player Options Page](../player-options).
## What does randomization do to this game?
The locations in the randomizer are:
- All sing bulbs;
- All Mithalas Urns;
- All Sunken City crates;
- Collectible treasure locations (including pet eggs and costumes);
- Beating Simon says;
- Li cave;
- Every Transportation Turtle (also called transturtle);
- Locations where you get songs,
* Erulian spirit cristal,
* Energy status mini-boss,
* Beating Mithalan God boss,
* Fish cave puzzle,
* Beating Drunian God boss,
* Beating Sun God boss,
* Breaking Li cage in the body
Note that, unlike the vanilla game, when opening sing bulbs, Mithalas urns and Sunken City crates,
nothing will come out of them. The moment those bulbs, urns and crates are opened, the location is considered received.
The items in the randomizer are:
- Dishes (used to learn recipes*);
- Some ingredients;
- The Wok (third plate used to cook 3 ingredients recipes everywhere);
- All collectible treasure (including pet eggs and costumes);
- Li and Li song;
- All songs (other than Li's song since it is learned when Li is obtained);
- Transportation to transturtles.
Also, there is the option to randomize every ingredient drops (from fishes, monsters
or plants).
*Note that, unlike in the vanilla game, the recipes for dishes (other than the Sea Loaf)
cannot be cooked (and learn) before being obtained as randomized items. Also, enemies and plants
that drop dishes that have not been learned before will drop ingredients of this dish instead.
## What is the goal of the game?
The goal of the Aquaria game is to beat the creator. You can also add other goals like getting
secret memories, beating a number of mini-bosses and beating a number of bosses.
## Which items can be in another player's world?
Any items specified above can be in another player's world.
## What does another world's item look like in Aquaria?
No visuals are shown when finding locations other than collectible treasure.
For those treasures, the visual of the treasure is visually unchanged.
After collecting a location check, a message will be shown to inform the player
what has been collected, and who will receive it.
## When the player receives an item, what happens?
When you receive an item, a message will pop up to inform you where you received
the item from, and which one it is.

View File

@@ -0,0 +1,65 @@
# Aquaria
## Où se trouve la page des options ?
La [page des options du joueur pour ce jeu](../player-options) contient tous
les options dont vous avez besoin pour configurer et exporter le fichier.
## Quel est l'effet de la randomisation sur ce jeu ?
Les localisations du "Ransomizer" sont:
- tous les bulbes musicaux;
- toutes les urnes de Mithalas;
- toutes les caisses de la cité engloutie;
- les localisations des trésors de collections (incluant les oeufs d'animaux de compagnie et les costumes);
- Battre Simom dit;
- La caverne de Li;
- Les tortues de transportation (transturtle);
- Localisation ou on obtient normalement les musiques,
* cristal de l'esprit Erulien,
* le mini-boss de la statue de l'énergie,
* battre le dieu de Mithalas,
* résoudre l'énigme de la caverne des poissons,
* battre le dieu Drunien,
* battre le dieu du soleil,
* détruire la cage de Li dans le corps,
À noter que, contrairement au jeu original, lors de l'ouverture d'un bulbe musical, d'une urne de Mithalas ou
d'une caisse de la cité engloutie, aucun objet n'en sortira. La localisation représentée par l'objet ouvert est reçue
dès l'ouverture.
Les objets pouvant être obtenus sont:
- les recettes (permettant d'apprendre les recettes*);
- certains ingrédients;
- le Wok (la troisième assiette permettant de cuisiner avec trois ingrédients n'importe où);
- Tous les trésors de collection (incluant les oeufs d'animal de compagnie et les costumes);
- Li et la musique de Li;
- Toutes les musiques (autre que la musique de Li puisque cette dernière est apprise en obtenant Li);
- Les localisations de transportation.
Il y a également l'option pour mélanger les ingrédients obtenus en éliminant des monstres, des poissons ou des plantes.
*À noter que, contrairement au jeu original, il est impossible de cuisiner une recette qui n'a pas préalablement
été apprise en obtenant un repas en tant qu'objet. À noter également que les ennemies et plantes qui
donnent un repas dont la recette n'a pas préalablement été apprise vont donner les ingrédients de cette
recette.
## Quel est le but de Aquaria ?
Dans Aquaria, le but est de battre le monstre final (le créateur). Il est également possible d'ajouter
des buts comme obtenir les trois souvenirs secrets, ou devoir battre une quantité de boss ou de mini-boss.
## Quels objets peuvent se trouver dans le monde d'un autre joueur ?
Tous les objets indiqués plus haut peuvent être obtenus à partir du monde d'un autre joueur.
## À quoi ressemble un objet d'un autre monde dans ce jeu
Autre que pour les trésors de collection (dont le visuel demeure inchangé),
les autres localisations n'ont aucun visuel. Lorsqu'une localisation randomisée est obtenue,
un message est affiché à l'écran pour indiquer quel objet a été trouvé et pour quel joueur.
## Que se passe-t-il lorsque le joueur reçoit un objet ?
Chaque fois qu'un objet est reçu, un message apparaît à l'écran pour en informer le joueur.

View File

@@ -0,0 +1,114 @@
# Aquaria Randomizer Setup Guide
## Required Software
- The original Aquaria Game (buyable from a lot of online game seller);
- The [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
- Optional, for sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
## Installation and execution Procedures
### Windows
First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
the original game will stop working. Copying the folder will guarantee that the original game keeps on working.
Also, in Windows, the save files are stored in the Aquaria folder. So copying the Aquaria folder for every Multiworld
game you play will make sure that every game has their own save game.
Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files
are those:
- aquaria_randomizer.exe
- OpenAL32.dll
- override (directory)
- SDL2.dll
- usersettings.xml
- wrap_oal.dll
- cacert.pem
If there is a conflict between file in the original game folder and the unzipped files, you should override
the original files with the one of the unzipped randomizer.
Finally, to launch the randomizer, you must use the command line interface (you can open the command line interface
by writing `cmd` in the address bar of the Windows file explorer). Here is the command line to use to start the
randomizer:
```bash
aquaria_randomizer.exe --name YourName --server theServer:thePort
```
or, if the room has a password:
```bash
aquaria_randomizer.exe --name YourName --server theServer:thePort --password thePassword
```
### Linux when using the AppImage
If you use the AppImage, just copy it in the Aquaria game folder. You then have to make it executable. You
can do that from command line by using
```bash
chmod +x Aquaria_Randomizer-*.AppImage
```
or by using the Graphical Explorer of your system.
To launch the randomizer, just launch in command line:
```bash
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort
```
or, if the room has a password:
```bash
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort --password thePassword
```
Note that you should not have multiple Aquaria_Randomizer AppImage file in the same folder. If this situation occurred,
the preceding commands will launch the game multiple times.
### Linux when using the tar file
First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
the original game will stop working. Copying the folder will guarantee that the original game keeps on working.
Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted
files are those:
- aquaria_randomizer
- override (directory)
- usersettings.xml
- cacert.pem
If there is a conflict between file in the original game folder and the extracted files, you should override
the original files with the one of the extracted randomizer files.
Then, you should use your system package manager to install liblua5, libogg, libvorbis, libopenal and libsdl2.
On Debian base system (like Ubuntu), you can use the following command:
```bash
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
```
Also, if there is some `.so` files in the Aquaria original game folder (`libgcc_s.so.1`, `libopenal.so.1`,
`libSDL-1.2.so.0` and `libstdc++.so.6`), you should remove them from the Aquaria Randomizer game folder. Those are
old libraries that will not work on the recent build of the randomizer.
To launch the randomizer, just launch in command line:
```bash
./aquaria_randomizer --name YourName --server theServer:thePort
```
or, if the room has a password:
```bash
./aquaria_randomizer --name YourName --server theServer:thePort --password thePassword
```
Note: If you have a permission denied error when using the command line, you can use this command line to be
sure that your executable has executable permission:
```bash
chmod +x aquaria_randomizer
```

View File

@@ -0,0 +1,118 @@
# Guide de configuration MultiWorld d'Aquaria
## Logiciels nécessaires
- Le jeu Aquaria original (trouvable sur la majorité des sites de ventes de jeux vidéo en ligne)
- Le client Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
## Procédures d'installation et d'exécution
### Windows
En premier lieu, vous devriez effectuer une nouvelle copie du jeu d'Aquaria original à chaque fois que vous effectuez une
nouvelle partie. La première raison de cette copie est que le randomizer modifie des fichiers qui rendront possiblement
le jeu original non fonctionnel. La seconde raison d'effectuer cette copie est que les sauvegardes sont créées
directement dans le répertoire du jeu. Donc, la copie permet d'éviter de perdre vos sauvegardes du jeu d'origine ou
encore de charger une sauvegarde d'une ancienne partie de multiworld (ce qui pourrait avoir comme conséquence de briser
la logique du multiworld).
Désarchiver le randomizer d'Aquaria et copier tous les fichiers de l'archive dans le répertoire du jeu d'Aquaria. Le
fichier d'archive devrait contenir les fichiers suivants:
- aquaria_randomizer.exe
- OpenAL32.dll
- override (directory)
- SDL2.dll
- usersettings.xml
- wrap_oal.dll
- cacert.pem
S'il y a des conflits entre les fichiers de l'archive zip et les fichiers du jeu original, vous devez utiliser
les fichiers contenus dans l'archive zip.
Finalement, pour lancer le randomizer, vous devez utiliser la ligne de commande (vous pouvez ouvrir une interface de
ligne de commande, entrez l'adresse `cmd` dans la barre d'adresse de l'explorateur de fichier de Windows). Voici
la ligne de commande à utiliser pour lancer le randomizer:
```bash
aquaria_randomizer.exe --name VotreNom --server leServeur:LePort
```
ou, si vous devez entrer un mot de passe:
```bash
aquaria_randomizer.exe --name VotreNom --server leServeur:LePort --password leMotDePasse
```
### Linux avec le fichier AppImage
Si vous utilisez le fichier AppImage, copiez le fichier dans le répertoire du jeu d'Aquaria. Ensuite, assurez-vous de
le mettre exécutable. Vous pouvez mettre le fichier exécutable avec la commande suivante:
```bash
chmod +x Aquaria_Randomizer-*.AppImage
```
ou bien en utilisant l'explorateur graphique de votre système.
Pour lancer le randomizer, utiliser la commande suivante:
```bash
./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort
```
Si vous devez entrer un mot de passe:
```bash
./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort --password LeMotDePasse
```
À noter que vous ne devez pas avoir plusieurs fichiers AppImage différents dans le même répertoire. Si cette situation
survient, le jeu sera lancé plusieurs fois.
### Linux avec le fichier tar
En premier lieu, assurez-vous de faire une copie du répertoire du jeu d'origine d'Aquaria. Les fichiers contenus
dans le randomizer auront comme impact de rendre le jeu d'origine non fonctionnel. Donc, effectuer la copie du jeu
avant de déposer le randomizer à l'intérieur permet de vous assurer de garder une version du jeu d'origine fonctionnel.
Désarchiver le fichier tar et copier tous les fichiers qu'il contient dans le répertoire du jeu d'origine d'Aquaria. Les
fichiers extraient du fichier tar devraient être les suivants:
- aquaria_randomizer
- override (directory)
- usersettings.xml
- cacert.pem
S'il y a des conflits entre les fichiers de l'archive tar et les fichiers du jeu original, vous devez utiliser
les fichiers contenus dans l'archive tar.
Ensuite, vous devez installer manuellement les librairies dont dépend le jeu: liblua5, libogg, libvorbis, libopenal and
libsdl2. Vous pouvez utiliser le système de "package" de votre système pour les installer. Voici un exemple avec
Debian (et Ubuntu):
```bash
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
```
Notez également que s'il y a des fichiers ".so" dans le répertoire d'Aquaria (`libgcc_s.so.1`, `libopenal.so.1`,
`libSDL-1.2.so.0` and `libstdc++.so.6`), vous devriez les retirer. Il s'agit de vieille version des librairies qui
ne sont plus fonctionnelles dans les systèmes modernes et qui pourrait empêcher le randomizer de fonctionner.
Pour lancer le randomizer, utiliser la commande suivante:
```bash
./aquaria_randomizer --name VotreNom --server LeServeur:LePort
```
Si vous devez entrer un mot de passe:
```bash
./aquaria_randomizer --name VotreNom --server LeServeur:LePort --password LeMotDePasse
```
Note: Si vous avez une erreur de permission lors de l'exécution du randomizer, vous pouvez utiliser cette commande
pour vous assurer que votre fichier est exécutable:
```bash
chmod +x aquaria_randomizer
```

View File

@@ -0,0 +1,218 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Base class for the Aquaria randomizer unit tests
"""
from test.bases import WorldTestBase
# Every location accessible after the home water.
after_home_water_locations = [
"Sun Crystal",
"Home water, Transturtle",
"Open water top left area, bulb under the rock in the right path",
"Open water top left area, bulb under the rock in the left path",
"Open water top left area, bulb to the right of the save cristal",
"Open water top right area, bulb in the small path before Mithalas",
"Open water top right area, bulb in the path from the left entrance",
"Open water top right area, bulb in the clearing close to the bottom exit",
"Open water top right area, bulb in the big clearing close to the save cristal",
"Open water top right area, bulb in the big clearing to the top exit",
"Open water top right area, first urn in the Mithalas exit",
"Open water top right area, second urn in the Mithalas exit",
"Open water top right area, third urn in the Mithalas exit",
"Open water top right area, bulb in the turtle room",
"Open water top right area, Transturtle",
"Open water bottom left area, bulb behind the chomper fish",
"Open water bottom left area, bulb inside the downest fish pass",
"Open water skeleton path, bulb close to the right exit",
"Open water skeleton path, bulb behind the chomper fish",
"Open water skeleton path, King skull",
"Arnassi Ruins, bulb in the right part",
"Arnassi Ruins, bulb in the left part",
"Arnassi Ruins, bulb in the center part",
"Arnassi ruins, Song plant spore on the top of the ruins",
"Arnassi ruins, Arnassi Armor",
"Arnassi Ruins, Arnassi statue",
"Arnassi Ruins, Transturtle",
"Arnassi ruins, Crab armor",
"Simon says area, Transturtle",
"Mithalas city, first bulb in the left city part",
"Mithalas city, second bulb in the left city part",
"Mithalas city, bulb in the right part",
"Mithalas city, bulb at the top of the city",
"Mithalas city, first bulb in a broken home",
"Mithalas city, second bulb in a broken home",
"Mithalas city, bulb in the bottom left part",
"Mithalas city, first bulb in one of the homes",
"Mithalas city, second bulb in one of the homes",
"Mithalas city, first urn in one of the homes",
"Mithalas city, second urn in one of the homes",
"Mithalas city, first urn in the city reserve",
"Mithalas city, second urn in the city reserve",
"Mithalas city, third urn in the city reserve",
"Mithalas city, first bulb at the end of the top path",
"Mithalas city, second bulb at the end of the top path",
"Mithalas city, bulb in the top path",
"Mithalas city, Mithalas pot",
"Mithalas city, urn in the cathedral flower tube entrance",
"Mithalas city, Doll",
"Mithalas city, urn inside a home fish pass",
"Mithalas city castle, bulb in the flesh hole",
"Mithalas city castle, Blue banner",
"Mithalas city castle, urn in the bedroom",
"Mithalas city castle, first urn of the single lamp path",
"Mithalas city castle, second urn of the single lamp path",
"Mithalas city castle, urn in the bottom room",
"Mithalas city castle, first urn on the entrance path",
"Mithalas city castle, second urn on the entrance path",
"Mithalas castle, beating the priests",
"Mithalas city castle, Trident head",
"Mithalas cathedral, first urn in the top right room",
"Mithalas cathedral, second urn in the top right room",
"Mithalas cathedral, third urn in the top right room",
"Mithalas cathedral, urn in the flesh room with fleas",
"Mithalas cathedral, first urn in the bottom right path",
"Mithalas cathedral, second urn in the bottom right path",
"Mithalas cathedral, urn behind the flesh vein",
"Mithalas cathedral, urn in the top left eyes boss room",
"Mithalas cathedral, first urn in the path behind the flesh vein",
"Mithalas cathedral, second urn in the path behind the flesh vein",
"Mithalas cathedral, third urn in the path behind the flesh vein",
"Mithalas cathedral, one of the urns in the top right room",
"Mithalas cathedral, Mithalan Dress",
"Mithalas cathedral right area, urn bellow the left entrance",
"Cathedral underground, bulb in the center part",
"Cathedral underground, first bulb in the top left part",
"Cathedral underground, second bulb in the top left part",
"Cathedral underground, third bulb in the top left part",
"Cathedral underground, bulb close to the save cristal",
"Cathedral underground, bulb in the bottom right path",
"Cathedral boss area, beating Mithalan God",
"Kelp Forest top left area, bulb in the bottom left clearing",
"Kelp Forest top left area, bulb in the path down from the top left clearing",
"Kelp Forest top left area, bulb in the top left clearing",
"Kelp Forest top left, Jelly Egg",
"Kelp Forest top left area, bulb close to the Verse egg",
"Kelp forest top left area, Verse egg",
"Kelp Forest top right area, bulb under the rock in the right path",
"Kelp Forest top right area, bulb at the left of the center clearing",
"Kelp Forest top right area, bulb in the left path's big room",
"Kelp Forest top right area, bulb in the left path's small room",
"Kelp Forest top right area, bulb at the top of the center clearing",
"Kelp forest top right area, Black pearl",
"Kelp Forest top right area, bulb in the top fish pass",
"Kelp Forest bottom left area, bulb close to the spirit crystals",
"Kelp forest bottom left area, Walker baby",
"Kelp Forest bottom left area, Transturtle",
"Kelp forest bottom right area, Odd Container",
"Kelp forest boss area, beating Drunian God",
"Kelp Forest boss room, bulb at the bottom of the area",
"Kelp Forest bottom left area, Fish cave puzzle",
"Kelp Forest sprite cave, bulb inside the fish pass",
"Kelp Forest sprite cave, bulb in the second room",
"Kelp Forest Sprite Cave, Seed bag",
"Mermog cave, bulb in the left part of the cave",
"Mermog cave, Piranha Egg",
"The veil top left area, In the Li cave",
"The veil top left area, bulb under the rock in the top right path",
"The veil top left area, bulb hidden behind the blocking rock",
"The veil top left area, Transturtle",
"The veil top left area, bulb inside the fish pass",
"Turtle cave, Turtle Egg",
"Turtle cave, bulb in bubble cliff",
"Turtle cave, Urchin costume",
"The veil top right area, bulb in the middle of the wall jump cliff",
"The veil top right area, golden starfish at the bottom right of the bottom path",
"The veil top right area, bulb in the top of the water fall",
"The veil top right area, Transturtle",
"The veil bottom area, bulb in the left path",
"The veil bottom area, bulb in the spirit path",
"The veil bottom area, Verse egg",
"The veil bottom area, Stone Head",
"Octopus cave, Dumbo Egg",
"Octopus cave, bulb in the path below the octopus cave path",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Sun temple, bulb in the top left part",
"Sun temple, bulb in the top right part",
"Sun temple, bulb at the top of the high dark room",
"Sun temple, Golden Gear",
"Sun temple, first bulb of the temple",
"Sun temple, bulb on the left part",
"Sun temple, bulb in the hidden room of the right part",
"Sun temple, Sun key",
"Sun Worm path, first path bulb",
"Sun Worm path, second path bulb",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"Sun temple boss area, beating Sun God",
"Abyss left area, bulb in hidden path room",
"Abyss left area, bulb in the right part",
"Abyss left area, Glowing seed",
"Abyss left area, Glowing Plant",
"Abyss left area, bulb in the bottom fish pass",
"Abyss right area, bulb behind the rock in the whale room",
"Abyss right area, bulb in the middle path",
"Abyss right area, bulb behind the rock in the middle path",
"Abyss right area, bulb in the left green room",
"Abyss right area, Transturtle",
"Ice cave, bulb in the room to the right",
"Ice cave, First bulbs in the top exit room",
"Ice cave, Second bulbs in the top exit room",
"Ice cave, third bulbs in the top exit room",
"Ice cave, bulb in the left room",
"King Jellyfish cave, bulb in the right path from King Jelly",
"King Jellyfish cave, Jellyfish Costume",
"The whale, Verse egg",
"Sunken city right area, crate close to the save cristal",
"Sunken city right area, crate in the left bottom room",
"Sunken city left area, crate in the little pipe room",
"Sunken city left area, crate close to the save cristal",
"Sunken city left area, crate before the bedroom",
"Sunken city left area, Girl Costume",
"Sunken city, bulb on the top of the boss area (boiler room)",
"The body center area, breaking li cage",
"The body main area, bulb on the main path blocking tube",
"The body left area, first bulb in the top face room",
"The body left area, second bulb in the top face room",
"The body left area, bulb bellow the water stream",
"The body left area, bulb in the top path to the top face room",
"The body left area, bulb in the bottom face room",
"The body right area, bulb in the top face room",
"The body right area, bulb in the top path to the bottom face room",
"The body right area, bulb in the bottom face room",
"The body bottom area, bulb in the Jelly Zap room",
"The body bottom area, bulb in the nautilus room",
"The body bottom area, Mutant Costume",
"Final boss area, first bulb in the turtle room",
"Final boss area, second bulbs in the turtle room",
"Final boss area, third bulbs in the turtle room",
"Final boss area, Transturtle",
"Final boss area, bulb in the boss third form room",
"Kelp forest, beating Simon says",
"Beating Fallen God",
"Beating Mithalan God",
"Beating Drunian God",
"Beating Sun God",
"Beating the Golem",
"Beating Nautilus Prime",
"Beating Blaster Peg Prime",
"Beating Mergog",
"Beating Mithalan priests",
"Beating Octopus Prime",
"Beating Crabbius Maximus",
"Beating Mantis Shrimp Prime",
"Beating King Jellyfish God Prime",
"First secret",
"Second secret",
"Third secret",
"Sunken City cleared",
"Objective complete",
]
class AquariaTestBase(WorldTestBase):
"""Base class for Aquaria unit tests"""
game = "Aquaria"

View File

@@ -0,0 +1,48 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the beast form
"""
from worlds.aquaria.test import AquariaTestBase
class BeastFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the beast form"""
def test_beast_form_location(self) -> None:
"""Test locations that require beast form"""
locations = [
"Mithalas castle, beating the priests",
"Arnassi ruins, Crab armor",
"Arnassi ruins, Song plant spore on the top of the ruins",
"Mithalas city, first bulb at the end of the top path",
"Mithalas city, second bulb at the end of the top path",
"Mithalas city, bulb in the top path",
"Mithalas city, Mithalas pot",
"Mithalas city, urn in the cathedral flower tube entrance",
"Mermog cave, Piranha Egg",
"Mithalas cathedral, Mithalan Dress",
"Turtle cave, bulb in bubble cliff",
"Turtle cave, Urchin costume",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The veil top right area, bulb in the top of the water fall",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Octopus cave, Dumbo Egg",
"Beating the Golem",
"Beating Mergog",
"Beating Crabbius Maximus",
"Beating Octopus Prime",
"Beating Mantis Shrimp Prime",
"King Jellyfish cave, Jellyfish Costume",
"King Jellyfish cave, bulb in the right path from King Jelly",
"Beating King Jellyfish God Prime",
"Beating Mithalan priests",
"Sunken City cleared"
]
items = [["Beast form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,36 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the bind song (without the location
under rock needing bind song option)
"""
from worlds.aquaria.test import AquariaTestBase, after_home_water_locations
class BindSongAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the bind song"""
options = {
"bind_song_needed_to_get_under_rock_bulb": False,
}
def test_bind_song_location(self) -> None:
"""Test locations that require Bind song"""
locations = [
"Verse cave right area, Big Seed",
"Home water, bulb in the path bellow Nautilus Prime",
"Home water, bulb in the bottom left room",
"Home water, Nautilus Egg",
"Song cave, Verse egg",
"Energy temple first area, beating the energy statue",
"Energy temple first area, bulb in the bottom room blocked by a rock",
"Energy temple first area, Energy Idol",
"Energy temple second area, bulb under the rock",
"Energy temple bottom entrance, Krotite armor",
"Energy temple third area, bulb in the bottom path",
"Energy temple boss area, Fallen god tooth",
"Energy temple blaster room, Blaster egg",
*after_home_water_locations
]
items = [["Bind song"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,42 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the bind song (with the location
under rock needing bind song option)
"""
from worlds.aquaria.test import AquariaTestBase
from worlds.aquaria.test.test_bind_song_access import after_home_water_locations
class BindSongOptionAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the bind song"""
options = {
"bind_song_needed_to_get_under_rock_bulb": True,
}
def test_bind_song_location(self) -> None:
"""Test locations that require Bind song with the bind song needed option activated"""
locations = [
"Verse cave right area, Big Seed",
"Verse cave left area, bulb under the rock at the end of the path",
"Home water, bulb under the rock in the left path from the verse cave",
"Song cave, bulb under the rock close to the song door",
"Song cave, bulb under the rock in the path to the singing statues",
"Naija's home, bulb under the rock at the right of the main path",
"Home water, bulb in the path bellow Nautilus Prime",
"Home water, bulb in the bottom left room",
"Home water, Nautilus Egg",
"Song cave, Verse egg",
"Energy temple first area, beating the energy statue",
"Energy temple first area, bulb in the bottom room blocked by a rock",
"Energy temple first area, Energy Idol",
"Energy temple second area, bulb under the rock",
"Energy temple bottom entrance, Krotite armor",
"Energy temple third area, bulb in the bottom path",
"Energy temple boss area, Fallen god tooth",
"Energy temple blaster room, Blaster egg",
*after_home_water_locations
]
items = [["Bind song"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,20 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test accessibility of region with the home water confine via option
"""
from worlds.aquaria.test import AquariaTestBase
class ConfinedHomeWaterAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of region with the unconfine home water option disabled"""
options = {
"unconfine_home_water": 0,
"early_energy_form": False
}
def test_confine_home_water_location(self) -> None:
"""Test region accessible with confined home water"""
self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area")
self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")

View File

@@ -0,0 +1,26 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the dual song
"""
from worlds.aquaria.test import AquariaTestBase
class LiAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the dual song"""
options = {
"turtle_randomizer": 1,
}
def test_li_song_location(self) -> None:
"""Test locations that require the dual song"""
locations = [
"The body bottom area, bulb in the Jelly Zap room",
"The body bottom area, bulb in the nautilus room",
"The body bottom area, Mutant Costume",
"Final boss area, bulb in the boss third form room",
"Objective complete"
]
items = [["Dual form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,73 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the bind song (without the early
energy form option)
"""
from worlds.aquaria.test import AquariaTestBase
class EnergyFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the energy form"""
options = {
"early_energy_form": False,
}
def test_energy_form_location(self) -> None:
"""Test locations that require Energy form"""
locations = [
"Home water, Nautilus Egg",
"Naija's home, bulb after the energy door",
"Energy temple first area, bulb in the bottom room blocked by a rock",
"Energy temple second area, bulb under the rock",
"Energy temple bottom entrance, Krotite armor",
"Energy temple third area, bulb in the bottom path",
"Energy temple boss area, Fallen god tooth",
"Energy temple blaster room, Blaster egg",
"Mithalas castle, beating the priests",
"Mithalas cathedral, first urn in the top right room",
"Mithalas cathedral, second urn in the top right room",
"Mithalas cathedral, third urn in the top right room",
"Mithalas cathedral, urn in the flesh room with fleas",
"Mithalas cathedral, first urn in the bottom right path",
"Mithalas cathedral, second urn in the bottom right path",
"Mithalas cathedral, urn behind the flesh vein",
"Mithalas cathedral, urn in the top left eyes boss room",
"Mithalas cathedral, first urn in the path behind the flesh vein",
"Mithalas cathedral, second urn in the path behind the flesh vein",
"Mithalas cathedral, third urn in the path behind the flesh vein",
"Mithalas cathedral, one of the urns in the top right room",
"Mithalas cathedral, Mithalan Dress",
"Mithalas cathedral right area, urn bellow the left entrance",
"Cathedral boss area, beating Mithalan God",
"Kelp Forest top left area, bulb close to the Verse egg",
"Kelp forest top left area, Verse egg",
"Kelp forest boss area, beating Drunian God",
"Mermog cave, Piranha Egg",
"Octopus cave, Dumbo Egg",
"Sun temple boss area, beating Sun God",
"Arnassi ruins, Crab armor",
"King Jellyfish cave, bulb in the right path from King Jelly",
"King Jellyfish cave, Jellyfish Costume",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Final boss area, bulb in the boss third form room",
"Beating Fallen God",
"Beating Mithalan God",
"Beating Drunian God",
"Beating Sun God",
"Beating the Golem",
"Beating Nautilus Prime",
"Beating Blaster Peg Prime",
"Beating Mergog",
"Beating Mithalan priests",
"Beating Octopus Prime",
"Beating Crabbius Maximus",
"Beating King Jellyfish God Prime",
"First secret",
"Sunken City cleared",
"Objective complete",
]
items = [["Energy form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,31 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the bind song (with the early
energy form option)
"""
from worlds.aquaria.test import AquariaTestBase, after_home_water_locations
class EnergyFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the energy form"""
options = {
"early_energy_form": True,
}
def test_energy_form_location(self) -> None:
"""Test locations that require Energy form with early energy song enable"""
locations = [
"Home water, Nautilus Egg",
"Naija's home, bulb after the energy door",
"Energy temple first area, bulb in the bottom room blocked by a rock",
"Energy temple second area, bulb under the rock",
"Energy temple bottom entrance, Krotite armor",
"Energy temple third area, bulb in the bottom path",
"Energy temple boss area, Fallen god tooth",
"Energy temple blaster room, Blaster egg",
*after_home_water_locations
]
items = [["Energy form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,37 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the fish form
"""
from worlds.aquaria.test import AquariaTestBase
class FishFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the fish form"""
options = {
"turtle_randomizer": 1,
}
def test_fish_form_location(self) -> None:
"""Test locations that require fish form"""
locations = [
"The veil top left area, bulb inside the fish pass",
"Mithalas city, Doll",
"Mithalas city, urn inside a home fish pass",
"Kelp Forest top right area, bulb in the top fish pass",
"The veil bottom area, Verse egg",
"Open water bottom left area, bulb inside the downest fish pass",
"Kelp Forest top left area, bulb close to the Verse egg",
"Kelp forest top left area, Verse egg",
"Mermog cave, bulb in the left part of the cave",
"Mermog cave, Piranha Egg",
"Beating Mergog",
"Octopus cave, Dumbo Egg",
"Octopus cave, bulb in the path below the octopus cave path",
"Beating Octopus Prime",
"Abyss left area, bulb in the bottom fish pass",
"Arnassi ruins, Arnassi Armor"
]
items = [["Fish form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,45 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without Li
"""
from worlds.aquaria.test import AquariaTestBase
class LiAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without Li"""
options = {
"turtle_randomizer": 1,
}
def test_li_song_location(self) -> None:
"""Test locations that require Li"""
locations = [
"Sunken city right area, crate close to the save cristal",
"Sunken city right area, crate in the left bottom room",
"Sunken city left area, crate in the little pipe room",
"Sunken city left area, crate close to the save cristal",
"Sunken city left area, crate before the bedroom",
"Sunken city left area, Girl Costume",
"Sunken city, bulb on the top of the boss area (boiler room)",
"The body center area, breaking li cage",
"The body main area, bulb on the main path blocking tube",
"The body left area, first bulb in the top face room",
"The body left area, second bulb in the top face room",
"The body left area, bulb bellow the water stream",
"The body left area, bulb in the top path to the top face room",
"The body left area, bulb in the bottom face room",
"The body right area, bulb in the top face room",
"The body right area, bulb in the top path to the bottom face room",
"The body right area, bulb in the bottom face room",
"The body bottom area, bulb in the Jelly Zap room",
"The body bottom area, bulb in the nautilus room",
"The body bottom area, Mutant Costume",
"Final boss area, bulb in the boss third form room",
"Beating the Golem",
"Sunken City cleared",
"Objective complete"
]
items = [["Li and Li song", "Body tongue cleared"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,71 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without a light (Dumbo pet or sun form)
"""
from worlds.aquaria.test import AquariaTestBase
class LightAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without light"""
options = {
"turtle_randomizer": 1,
"light_needed_to_get_to_dark_places": True,
}
def test_light_location(self) -> None:
"""Test locations that require light"""
locations = [
# Since the `assertAccessDependency` sweep for events even if I tell it not to, those location cannot be
# tested.
# "Third secret",
# "Sun temple, bulb in the top left part",
# "Sun temple, bulb in the top right part",
# "Sun temple, bulb at the top of the high dark room",
# "Sun temple, Golden Gear",
# "Sun Worm path, first path bulb",
# "Sun Worm path, second path bulb",
# "Sun Worm path, first cliff bulb",
"Octopus cave, Dumbo Egg",
"Kelp forest bottom right area, Odd Container",
"Kelp forest top right area, Black pearl",
"Abyss left area, bulb in hidden path room",
"Abyss left area, bulb in the right part",
"Abyss left area, Glowing seed",
"Abyss left area, Glowing Plant",
"Abyss left area, bulb in the bottom fish pass",
"Abyss right area, bulb behind the rock in the whale room",
"Abyss right area, bulb in the middle path",
"Abyss right area, bulb behind the rock in the middle path",
"Abyss right area, bulb in the left green room",
"Abyss right area, Transturtle",
"Ice cave, bulb in the room to the right",
"Ice cave, First bulbs in the top exit room",
"Ice cave, Second bulbs in the top exit room",
"Ice cave, third bulbs in the top exit room",
"Ice cave, bulb in the left room",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Beating Mantis Shrimp Prime",
"King Jellyfish cave, bulb in the right path from King Jelly",
"King Jellyfish cave, Jellyfish Costume",
"Beating King Jellyfish God Prime",
"The whale, Verse egg",
"First secret",
"Sunken city right area, crate close to the save cristal",
"Sunken city right area, crate in the left bottom room",
"Sunken city left area, crate in the little pipe room",
"Sunken city left area, crate close to the save cristal",
"Sunken city left area, crate before the bedroom",
"Sunken city left area, Girl Costume",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Sunken City cleared",
"Beating the Golem",
"Beating Octopus Prime",
"Final boss area, bulb in the boss third form room",
"Objective complete",
]
items = [["Sun form", "Baby dumbo", "Has sun crystal"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,57 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the nature form
"""
from worlds.aquaria.test import AquariaTestBase
class NatureFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the nature form"""
options = {
"turtle_randomizer": 1,
}
def test_nature_form_location(self) -> None:
"""Test locations that require nature form"""
locations = [
"Song cave, Anemone seed",
"Energy temple blaster room, Blaster egg",
"Beating Blaster Peg Prime",
"Kelp forest top left area, Verse egg",
"Kelp Forest top left area, bulb close to the Verse egg",
"Mithalas castle, beating the priests",
"Kelp Forest sprite cave, bulb in the second room",
"Kelp Forest Sprite Cave, Seed bag",
"Beating Mithalan priests",
"Abyss left area, bulb in the bottom fish pass",
"Bubble cave, Verse egg",
"Beating Mantis Shrimp Prime",
"Sunken city right area, crate close to the save cristal",
"Sunken city right area, crate in the left bottom room",
"Sunken city left area, crate in the little pipe room",
"Sunken city left area, crate close to the save cristal",
"Sunken city left area, crate before the bedroom",
"Sunken city left area, Girl Costume",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Beating the Golem",
"Sunken City cleared",
"The body center area, breaking li cage",
"The body main area, bulb on the main path blocking tube",
"The body left area, first bulb in the top face room",
"The body left area, second bulb in the top face room",
"The body left area, bulb bellow the water stream",
"The body left area, bulb in the top path to the top face room",
"The body left area, bulb in the bottom face room",
"The body right area, bulb in the top face room",
"The body right area, bulb in the top path to the bottom face room",
"The body right area, bulb in the bottom face room",
"The body bottom area, bulb in the Jelly Zap room",
"The body bottom area, bulb in the nautilus room",
"The body bottom area, Mutant Costume",
"Final boss area, bulb in the boss third form room",
"Objective complete"
]
items = [["Nature form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,60 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
"""
from worlds.aquaria.test import AquariaTestBase
from BaseClasses import ItemClassification
class UNoProgressionHardHiddenTest(AquariaTestBase):
"""Unit test used to test that no progression items can be put in hard or hidden locations when option enabled"""
options = {
"no_progression_hard_or_hidden_locations": True
}
unfillable_locations = [
"Energy temple boss area, Fallen god tooth",
"Cathedral boss area, beating Mithalan God",
"Kelp forest boss area, beating Drunian God",
"Sun temple boss area, beating Sun God",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Home water, Nautilus Egg",
"Energy temple blaster room, Blaster egg",
"Mithalas castle, beating the priests",
"Mermog cave, Piranha Egg",
"Octopus cave, Dumbo Egg",
"King Jellyfish cave, bulb in the right path from King Jelly",
"King Jellyfish cave, Jellyfish Costume",
"Final boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The veil top right area, bulb in the top of the water fall",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Kelp Forest bottom left area, bulb close to the spirit crystals",
"Kelp forest bottom left area, Walker baby",
"Sun temple, Sun key",
"The body bottom area, Mutant Costume",
"Sun temple, bulb in the hidden room of the right part",
"Arnassi ruins, Arnassi Armor",
]
def test_unconfine_home_water_both_location_fillable(self) -> None:
"""
Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
"""
for location in self.unfillable_locations:
for item_name in self.world.item_names:
item = self.get_item_by_name(item_name)
if item.classification == ItemClassification.progression:
self.assertFalse(
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
else:
self.assertTrue(
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")

View File

@@ -0,0 +1,53 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test that progression items can be put in hard or hidden locations when option disabled
"""
from worlds.aquaria.test import AquariaTestBase
from BaseClasses import ItemClassification
class UNoProgressionHardHiddenTest(AquariaTestBase):
"""Unit test used to test that no progression items can be put in hard or hidden locations when option disabled"""
options = {
"no_progression_hard_or_hidden_locations": False
}
unfillable_locations = [
"Energy temple boss area, Fallen god tooth",
"Cathedral boss area, beating Mithalan God",
"Kelp forest boss area, beating Drunian God",
"Sun temple boss area, beating Sun God",
"Sunken city, bulb on the top of the boss area (boiler room)",
"Home water, Nautilus Egg",
"Energy temple blaster room, Blaster egg",
"Mithalas castle, beating the priests",
"Mermog cave, Piranha Egg",
"Octopus cave, Dumbo Egg",
"King Jellyfish cave, bulb in the right path from King Jelly",
"King Jellyfish cave, Jellyfish Costume",
"Final boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The veil top right area, bulb in the top of the water fall",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Kelp Forest bottom left area, bulb close to the spirit crystals",
"Kelp forest bottom left area, Walker baby",
"Sun temple, Sun key",
"The body bottom area, Mutant Costume",
"Sun temple, bulb in the hidden room of the right part",
"Arnassi ruins, Arnassi Armor",
]
def test_unconfine_home_water_both_location_fillable(self) -> None:
"""Unit test used to test that progression items can be put in hard or hidden locations when option disabled"""
for location in self.unfillable_locations:
for item_name in self.world.item_names:
item = self.get_item_by_name(item_name)
self.assertTrue(
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")

View File

@@ -0,0 +1,36 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the spirit form
"""
from worlds.aquaria.test import AquariaTestBase
class SpiritFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the spirit form"""
def test_spirit_form_location(self) -> None:
"""Test locations that require spirit form"""
locations = [
"The veil bottom area, bulb in the spirit path",
"Mithalas city castle, Trident head",
"Open water skeleton path, King skull",
"Kelp forest bottom left area, Walker baby",
"Abyss right area, bulb behind the rock in the whale room",
"The whale, Verse egg",
"Ice cave, bulb in the room to the right",
"Ice cave, First bulbs in the top exit room",
"Ice cave, Second bulbs in the top exit room",
"Ice cave, third bulbs in the top exit room",
"Ice cave, bulb in the left room",
"Bubble cave, bulb in the left cave wall",
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
"Bubble cave, Verse egg",
"Sunken city left area, Girl Costume",
"Beating Mantis Shrimp Prime",
"First secret",
"Arnassi ruins, Arnassi Armor",
]
items = [["Spirit form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,25 @@
"""
Author: Louis M
Date: Thu, 18 Apr 2024 18:45:56 +0000
Description: Unit test used to test accessibility of locations with and without the sun form
"""
from worlds.aquaria.test import AquariaTestBase
class SunFormAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of locations with and without the sun form"""
def test_sun_form_location(self) -> None:
"""Test locations that require sun form"""
locations = [
"First secret",
"The whale, Verse egg",
"Abyss right area, bulb behind the rock in the whale room",
"Octopus cave, Dumbo Egg",
"Beating Octopus Prime",
"Final boss area, bulb in the boss third form room",
"Objective complete"
]
items = [["Sun form"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,21 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test accessibility of region with the unconfined home water option via transportation
turtle and energy door
"""
from worlds.aquaria.test import AquariaTestBase
class UnconfineHomeWaterBothAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
options = {
"unconfine_home_water": 3,
"early_energy_form": False
}
def test_unconfine_home_water_both_location(self) -> None:
"""Test locations accessible with unconfined home water via energy door and transportation turtle"""
self.assertTrue(self.can_reach_region("Open water top left area"), "Cannot reach Open water top left area")
self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room")

View File

@@ -0,0 +1,20 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test accessibility of region with the unconfined home water option via the energy door
"""
from worlds.aquaria.test import AquariaTestBase
class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
options = {
"unconfine_home_water": 1,
"early_energy_form": False
}
def test_unconfine_home_water_energy_door_location(self) -> None:
"""Test locations accessible with unconfined home water via energy door"""
self.assertTrue(self.can_reach_region("Open water top left area"), "Cannot reach Open water top left area")
self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")

View File

@@ -0,0 +1,20 @@
"""
Author: Louis M
Date: Fri, 03 May 2024 14:07:35 +0000
Description: Unit test used to test accessibility of region with the unconfined home water option via transturtle
"""
from worlds.aquaria.test import AquariaTestBase
class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase):
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
options = {
"unconfine_home_water": 2,
"early_energy_form": False
}
def test_unconfine_home_water_transturtle_location(self) -> None:
"""Test locations accessible with unconfined home water via transportation turtle"""
self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room")
self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area")

View File

@@ -0,0 +1,553 @@
from typing import TypedDict, List, Dict, Set
from enum import Enum
class BRCType(Enum):
Music = 0
GraffitiM = 1
GraffitiL = 2
GraffitiXL = 3
Skateboard = 4
InlineSkates = 5
BMX = 6
Character = 7
Outfit = 8
REP = 9
Camera = 10
class ItemDict(TypedDict, total=False):
name: str
count: int
type: BRCType
base_id = 2308000
item_table: List[ItemDict] = [
# Music
{'name': "Music (GET ENUF)",
'type': BRCType.Music},
{'name': "Music (Chuckin Up)",
'type': BRCType.Music},
{'name': "Music (Spectres)",
'type': BRCType.Music},
{'name': "Music (You Can Say Hi)",
'type': BRCType.Music},
{'name': "Music (JACK DA FUNK)",
'type': BRCType.Music},
{'name': "Music (Feel The Funk (Computer Love))",
'type': BRCType.Music},
{'name': "Music (Big City Life)",
'type': BRCType.Music},
{'name': "Music (I Wanna Kno)",
'type': BRCType.Music},
{'name': "Music (Plume)",
'type': BRCType.Music},
{'name': "Music (Two Days Off)",
'type': BRCType.Music},
{'name': "Music (Scraped On The Way Out)",
'type': BRCType.Music},
{'name': "Music (Last Hoorah)",
'type': BRCType.Music},
{'name': "Music (State of Mind)",
'type': BRCType.Music},
{'name': "Music (AGUA)",
'type': BRCType.Music},
{'name': "Music (Condensed milk)",
'type': BRCType.Music},
{'name': "Music (Light Switch)",
'type': BRCType.Music},
{'name': "Music (Hair Dun Nails Dun)",
'type': BRCType.Music},
{'name': "Music (Precious Thing)",
'type': BRCType.Music},
{'name': "Music (Next To Me)",
'type': BRCType.Music},
{'name': "Music (Refuse)",
'type': BRCType.Music},
{'name': "Music (Iridium)",
'type': BRCType.Music},
{'name': "Music (Funk Express)",
'type': BRCType.Music},
{'name': "Music (In The Pocket)",
'type': BRCType.Music},
{'name': "Music (Bounce Upon A Time)",
'type': BRCType.Music},
{'name': "Music (hwbouths)",
'type': BRCType.Music},
{'name': "Music (Morning Glow)",
'type': BRCType.Music},
{'name': "Music (Chromebies)",
'type': BRCType.Music},
{'name': "Music (watchyaback!)",
'type': BRCType.Music},
{'name': "Music (Anime Break)",
'type': BRCType.Music},
{'name': "Music (DA PEOPLE)",
'type': BRCType.Music},
{'name': "Music (Trinitron)",
'type': BRCType.Music},
{'name': "Music (Operator)",
'type': BRCType.Music},
{'name': "Music (Sunshine Popping Mixtape)",
'type': BRCType.Music},
{'name': "Music (House Cats Mixtape)",
'type': BRCType.Music},
{'name': "Music (Breaking Machine Mixtape)",
'type': BRCType.Music},
{'name': "Music (Beastmode Hip Hop Mixtape)",
'type': BRCType.Music},
# Graffiti
{'name': "Graffiti (M - OVERWHELMME)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - QUICK BING)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - BLOCKY)",
'type': BRCType.GraffitiM},
#{'name': "Graffiti (M - Flow)",
# 'type': BRCType.GraffitiM},
{'name': "Graffiti (M - Pora)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - Teddy 4)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - BOMB BEATS)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - SPRAYTANICPANIC!)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - SHOGUN)",
'type': BRCType.GraffitiM},
#{'name': "Graffiti (M - EVIL DARUMA)",
# 'type': BRCType.GraffitiM},
{'name': "Graffiti (M - TeleBinge)",
'type': BRCType.GraffitiM},
#{'name': "Graffiti (M - All Screws Loose)",
# 'type': BRCType.GraffitiM},
{'name': "Graffiti (M - 0m33)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - Vom'B)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - Street classic)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - Thick Candy)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - colorBOMB)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - Zona Leste)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - Stacked Symbols)",
'type': BRCType.GraffitiM},
#{'name': "Graffiti (M - Constellation Circle)",
# 'type': BRCType.GraffitiM},
{'name': "Graffiti (M - B-boy Love)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - Devil 68)",
'type': BRCType.GraffitiM},
{'name': "Graffiti (M - pico pow)",
'type': BRCType.GraffitiM},
#{'name': "Graffiti (M - 8 MINUTES OF LEAN MEAN)",
# 'type': BRCType.GraffitiM},
{'name': "Graffiti (L - WHOLE SIXER)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - INFINITY)",
'type': BRCType.GraffitiL},
#{'name': "Graffiti (L - Dynamo)",
# 'type': BRCType.GraffitiL},
{'name': "Graffiti (L - VoodooBoy)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - Fang It Up!)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - FREAKS)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - Graffo Le Fou)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - Lauder)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - SpawningSeason)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - Moai Marathon)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - Tius)",
'type': BRCType.GraffitiL},
#{'name': "Graffiti (L - KANI-BOZU)",
# 'type': BRCType.GraffitiL},
{'name': "Graffiti (L - NOISY NINJA)",
'type': BRCType.GraffitiL},
#{'name': "Graffiti (L - Dinner On The Court)",
# 'type': BRCType.GraffitiL},
{'name': "Graffiti (L - Campaign Trail)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - skate or di3)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - Jd Vila Formosa)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - Messenger Mural)",
'type': BRCType.GraffitiL},
#{'name': "Graffiti (L - Solstice Script)",
# 'type': BRCType.GraffitiL},
{'name': "Graffiti (L - RECORD.HEAD)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - Boom)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - wild rush)",
'type': BRCType.GraffitiL},
{'name': "Graffiti (L - buttercup)",
'type': BRCType.GraffitiL},
#{'name': "Graffiti (L - DIGITAL BLOCKBUSTER)",
# 'type': BRCType.GraffitiL},
{'name': "Graffiti (XL - Gold Rush)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - WILD STRUXXA)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - VIBRATIONS)",
'type': BRCType.GraffitiXL},
#{'name': "Graffiti (XL - Bevel)",
# 'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - SECOND SIGHT)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - Bomb Croc)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - FATE)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - Web Spitter)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - MOTORCYCLE GANG)",
'type': BRCType.GraffitiXL},
#{'name': "Graffiti (XL - CYBER TENGU)",
# 'type': BRCType.GraffitiXL},
#{'name': "Graffiti (XL - Don't Screw Around)",
# 'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - Deep Dive)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - MegaHood)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - Gamex UPA ABL)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - BiGSHiNYBoMB)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - Bomb Burner)",
'type': BRCType.GraffitiXL},
#{'name': "Graffiti (XL - Astrological Augury)",
# 'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - Pirate's Life 4 Me)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - Bombing by FireMan)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - end 2 end)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - Raver Funk)",
'type': BRCType.GraffitiXL},
{'name': "Graffiti (XL - headphones on Helmet on)",
'type': BRCType.GraffitiXL},
#{'name': "Graffiti (XL - HIGH TECH WS)",
# 'type': BRCType.GraffitiXL},
# Skateboards
{'name': "Skateboard (Devon)",
'type': BRCType.Skateboard},
{'name': "Skateboard (Terrence)",
'type': BRCType.Skateboard},
{'name': "Skateboard (Maceo)",
'type': BRCType.Skateboard},
{'name': "Skateboard (Lazer Accuracy)",
'type': BRCType.Skateboard},
{'name': "Skateboard (Death Boogie)",
'type': BRCType.Skateboard},
{'name': "Skateboard (Sylk)",
'type': BRCType.Skateboard},
{'name': "Skateboard (Taiga)",
'type': BRCType.Skateboard},
{'name': "Skateboard (Just Swell)",
'type': BRCType.Skateboard},
{'name': "Skateboard (Mantra)",
'type': BRCType.Skateboard},
# Inline Skates
{'name': "Inline Skates (Glaciers)",
'type': BRCType.InlineSkates},
{'name': "Inline Skates (Sweet Royale)",
'type': BRCType.InlineSkates},
{'name': "Inline Skates (Strawberry Missiles)",
'type': BRCType.InlineSkates},
{'name': "Inline Skates (Ice Cold Killers)",
'type': BRCType.InlineSkates},
{'name': "Inline Skates (Red Industry)",
'type': BRCType.InlineSkates},
{'name': "Inline Skates (Mech Adversary)",
'type': BRCType.InlineSkates},
{'name': "Inline Skates (Orange Blasters)",
'type': BRCType.InlineSkates},
{'name': "Inline Skates (ck)",
'type': BRCType.InlineSkates},
{'name': "Inline Skates (Sharpshooters)",
'type': BRCType.InlineSkates},
# BMX
{'name': "BMX (Mr. Taupe)",
'type': BRCType.BMX},
{'name': "BMX (Gum)",
'type': BRCType.BMX},
{'name': "BMX (Steel Wheeler)",
'type': BRCType.BMX},
{'name': "BMX (oyo)",
'type': BRCType.BMX},
{'name': "BMX (Rigid No.6)",
'type': BRCType.BMX},
{'name': "BMX (Ceremony)",
'type': BRCType.BMX},
{'name': "BMX (XXX)",
'type': BRCType.BMX},
{'name': "BMX (Terrazza)",
'type': BRCType.BMX},
{'name': "BMX (Dedication)",
'type': BRCType.BMX},
# Outfits
{'name': "Outfit (Red - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Red - Winter)",
'type': BRCType.Outfit},
{'name': "Outfit (Tryce - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Tryce - Winter)",
'type': BRCType.Outfit},
{'name': "Outfit (Bel - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Bel - Winter)",
'type': BRCType.Outfit},
{'name': "Outfit (Vinyl - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Vinyl - Winter)",
'type': BRCType.Outfit},
{'name': "Outfit (Solace - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Solace - Winter)",
'type': BRCType.Outfit},
{'name': "Outfit (Felix - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Felix - Winter)",
'type': BRCType.Outfit},
{'name': "Outfit (Rave - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Rave - Winter)",
'type': BRCType.Outfit},
{'name': "Outfit (Mesh - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Mesh - Winter)",
'type': BRCType.Outfit},
{'name': "Outfit (Shine - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Shine - Winter)",
'type': BRCType.Outfit},
{'name': "Outfit (Rise - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Rise - Winter)",
'type': BRCType.Outfit},
{'name': "Outfit (Coil - Autumn)",
'type': BRCType.Outfit},
{'name': "Outfit (Coil - Winter)",
'type': BRCType.Outfit},
# Characters
{'name': "Tryce",
'type': BRCType.Character},
{'name': "Bel",
'type': BRCType.Character},
{'name': "Vinyl",
'type': BRCType.Character},
{'name': "Solace",
'type': BRCType.Character},
{'name': "Rave",
'type': BRCType.Character},
{'name': "Mesh",
'type': BRCType.Character},
{'name': "Shine",
'type': BRCType.Character},
{'name': "Rise",
'type': BRCType.Character},
{'name': "Coil",
'type': BRCType.Character},
{'name': "Frank",
'type': BRCType.Character},
{'name': "Rietveld",
'type': BRCType.Character},
{'name': "DJ Cyber",
'type': BRCType.Character},
{'name': "Eclipse",
'type': BRCType.Character},
{'name': "DOT.EXE",
'type': BRCType.Character},
{'name': "Devil Theory",
'type': BRCType.Character},
{'name': "Flesh Prince",
'type': BRCType.Character},
{'name': "Futurism",
'type': BRCType.Character},
{'name': "Oldhead",
'type': BRCType.Character},
# REP
{'name': "8 REP",
'type': BRCType.REP},
{'name': "16 REP",
'type': BRCType.REP},
{'name': "24 REP",
'type': BRCType.REP},
{'name': "32 REP",
'type': BRCType.REP},
{'name': "48 REP",
'type': BRCType.REP},
# App
{'name': "Camera App",
'type': BRCType.Camera}
]
group_table: Dict[str, Set[str]] = {
"graffitim": {"Graffiti (M - OVERWHELMME)",
"Graffiti (M - QUICK BING)",
"Graffiti (M - BLOCKY)",
"Graffiti (M - Pora)",
"Graffiti (M - Teddy 4)",
"Graffiti (M - BOMB BEATS)",
"Graffiti (M - SPRAYTANICPANIC!)",
"Graffiti (M - SHOGUN)",
"Graffiti (M - TeleBinge)",
"Graffiti (M - 0m33)",
"Graffiti (M - Vom'B)",
"Graffiti (M - Street classic)",
"Graffiti (M - Thick Candy)",
"Graffiti (M - colorBOMB)",
"Graffiti (M - Zona Leste)",
"Graffiti (M - Stacked Symbols)",
"Graffiti (M - B-boy Love)",
"Graffiti (M - Devil 68)",
"Graffiti (M - pico pow)"},
"graffitil": {"Graffiti (L - WHOLE SIXER)",
"Graffiti (L - INFINITY)",
"Graffiti (L - VoodooBoy)",
"Graffiti (L - Fang It Up!)",
"Graffiti (L - FREAKS)",
"Graffiti (L - Graffo Le Fou)",
"Graffiti (L - Lauder)",
"Graffiti (L - SpawningSeason)",
"Graffiti (L - Moai Marathon)",
"Graffiti (L - Tius)",
"Graffiti (L - NOISY NINJA)",
"Graffiti (L - Campaign Trail)",
"Graffiti (L - skate or di3)",
"Graffiti (L - Jd Vila Formosa)",
"Graffiti (L - Messenger Mural)",
"Graffiti (L - RECORD.HEAD)",
"Graffiti (L - Boom)",
"Graffiti (L - wild rush)",
"Graffiti (L - buttercup)"},
"graffitixl": {"Graffiti (XL - Gold Rush)",
"Graffiti (XL - WILD STRUXXA)",
"Graffiti (XL - VIBRATIONS)",
"Graffiti (XL - SECOND SIGHT)",
"Graffiti (XL - Bomb Croc)",
"Graffiti (XL - FATE)",
"Graffiti (XL - Web Spitter)",
"Graffiti (XL - MOTORCYCLE GANG)",
"Graffiti (XL - Deep Dive)",
"Graffiti (XL - MegaHood)",
"Graffiti (XL - Gamex UPA ABL)",
"Graffiti (XL - BiGSHiNYBoMB)",
"Graffiti (XL - Bomb Burner)",
"Graffiti (XL - Pirate's Life 4 Me)",
"Graffiti (XL - Bombing by FireMan)",
"Graffiti (XL - end 2 end)",
"Graffiti (XL - Raver Funk)",
"Graffiti (XL - headphones on Helmet on)"},
"skateboard": {"Skateboard (Devon)",
"Skateboard (Terrence)",
"Skateboard (Maceo)",
"Skateboard (Lazer Accuracy)",
"Skateboard (Death Boogie)",
"Skateboard (Sylk)",
"Skateboard (Taiga)",
"Skateboard (Just Swell)",
"Skateboard (Mantra)"},
"inline skates": {"Inline Skates (Glaciers)",
"Inline Skates (Sweet Royale)",
"Inline Skates (Strawberry Missiles)",
"Inline Skates (Ice Cold Killers)",
"Inline Skates (Red Industry)",
"Inline Skates (Mech Adversary)",
"Inline Skates (Orange Blasters)",
"Inline Skates (ck)",
"Inline Skates (Sharpshooters)"},
"skates": {"Inline Skates (Glaciers)",
"Inline Skates (Sweet Royale)",
"Inline Skates (Strawberry Missiles)",
"Inline Skates (Ice Cold Killers)",
"Inline Skates (Red Industry)",
"Inline Skates (Mech Adversary)",
"Inline Skates (Orange Blasters)",
"Inline Skates (ck)",
"Inline Skates (Sharpshooters)"},
"inline": {"Inline Skates (Glaciers)",
"Inline Skates (Sweet Royale)",
"Inline Skates (Strawberry Missiles)",
"Inline Skates (Ice Cold Killers)",
"Inline Skates (Red Industry)",
"Inline Skates (Mech Adversary)",
"Inline Skates (Orange Blasters)",
"Inline Skates (ck)",
"Inline Skates (Sharpshooters)"},
"bmx": {"BMX (Mr. Taupe)",
"BMX (Gum)",
"BMX (Steel Wheeler)",
"BMX (oyo)",
"BMX (Rigid No.6)",
"BMX (Ceremony)",
"BMX (XXX)",
"BMX (Terrazza)",
"BMX (Dedication)"},
"bike": {"BMX (Mr. Taupe)",
"BMX (Gum)",
"BMX (Steel Wheeler)",
"BMX (oyo)",
"BMX (Rigid No.6)",
"BMX (Ceremony)",
"BMX (XXX)",
"BMX (Terrazza)",
"BMX (Dedication)"},
"bicycle": {"BMX (Mr. Taupe)",
"BMX (Gum)",
"BMX (Steel Wheeler)",
"BMX (oyo)",
"BMX (Rigid No.6)",
"BMX (Ceremony)",
"BMX (XXX)",
"BMX (Terrazza)",
"BMX (Dedication)"},
"characters": {"Tryce",
"Bel",
"Vinyl",
"Solace",
"Rave",
"Mesh",
"Shine",
"Rise",
"Coil",
"Frank",
"Rietveld",
"DJ Cyber",
"Eclipse",
"DOT.EXE",
"Devil Theory",
"Flesh Prince",
"Futurism",
"Oldhead"},
"girl": {"Bel",
"Vinyl",
"Rave",
"Shine",
"Rise",
"Futurism"}
}

View File

@@ -0,0 +1,785 @@
from typing import TypedDict, List
from .Regions import Stages
class LocationDict(TypedDict):
name: str
stage: Stages
game_id: str
class EventDict(TypedDict):
name: str
stage: str
item: str
location_table: List[LocationDict] = [
{'name': "Hideout: Half pipe CD",
'stage': Stages.H,
'game_id': "MusicTrack_CondensedMilk"},
{'name': "Hideout: Garage tower CD",
'stage': Stages.H,
'game_id': "MusicTrack_MorningGlow"},
{'name': "Hideout: Rooftop CD",
'stage': Stages.H,
'game_id': "MusicTrack_LightSwitch"},
{'name': "Hideout: Under staircase graffiti",
'stage': Stages.H,
'game_id': "UnlockGraffiti_grafTex_M1"},
{'name': "Hideout: Secret area graffiti",
'stage': Stages.H,
'game_id': "UnlockGraffiti_grafTex_L1"},
{'name': "Hideout: Rear studio graffiti",
'stage': Stages.H,
'game_id': "UnlockGraffiti_grafTex_XL1"},
{'name': "Hideout: Corner ledge graffiti",
'stage': Stages.H,
'game_id': "UnlockGraffiti_grafTex_M2"},
{'name': "Hideout: Upper platform skateboard",
'stage': Stages.H,
'game_id': "SkateboardDeck3"},
{'name': "Hideout: BMX garage skateboard",
'stage': Stages.H,
'game_id': "SkateboardDeck2"},
{'name': "Hideout: Unlock phone app",
'stage': Stages.H,
'game_id': "camera"},
{'name': "Hideout: Vinyl joins the crew",
'stage': Stages.H,
'game_id': "girl1"},
{'name': "Hideout: Solace joins the crew",
'stage': Stages.H,
'game_id': "dummy"},
{'name': "Versum Hill: Main street Robo Post graffiti",
'stage': Stages.VH1,
'game_id': "UnlockGraffiti_grafTex_L4"},
{'name': "Versum Hill: Behind glass graffiti",
'stage': Stages.VH1,
'game_id': "UnlockGraffiti_grafTex_L3"},
{'name': "Versum Hill: Office room graffiti",
'stage': Stages.VH1,
'game_id': "UnlockGraffiti_grafTex_M4"},
{'name': "Versum Hill: Under bridge graffiti",
'stage': Stages.VH2,
'game_id': "UnlockGraffiti_grafTex_XL4"},
{'name': "Versum Hill: Train rail ledge skateboard",
'stage': Stages.VH2,
'game_id': "SkateboardDeck6"},
{'name': "Versum Hill: Train station CD",
'stage': Stages.VH2,
'game_id': "MusicTrack_PreciousThing"},
{'name': "Versum Hill: Billboard platform outfit",
'stage': Stages.VH2,
'game_id': "MetalheadOutfit3"},
{'name': "Versum Hill: Hilltop Robo Post CD",
'stage': Stages.VH2,
'game_id': "MusicTrack_BounceUponATime"},
{'name': "Versum Hill: Hill secret skateboard",
'stage': Stages.VH2,
'game_id': "SkateboardDeck7"},
{'name': "Versum Hill: Rooftop CD",
'stage': Stages.VH2,
'game_id': "MusicTrack_NextToMe"},
{'name': "Versum Hill: Wallrunning challenge reward",
'stage': Stages.VH2,
'game_id': "UnlockGraffiti_grafTex_M3"},
{'name': "Versum Hill: Manual challenge reward",
'stage': Stages.VH2,
'game_id': "UnlockGraffiti_grafTex_L2"},
{'name': "Versum Hill: Corner challenge reward",
'stage': Stages.VH2,
'game_id': "UnlockGraffiti_grafTex_M13"},
{'name': "Versum Hill: Side street alley outfit",
'stage': Stages.VH3,
'game_id': "MetalheadOutfit4"},
{'name': "Versum Hill: Side street secret skateboard",
'stage': Stages.VH3,
'game_id': "SkateboardDeck9"},
{'name': "Versum Hill: Basketball court alley skateboard",
'stage': Stages.VH4,
'game_id': "SkateboardDeck5"},
{'name': "Versum Hill: Basketball court Robo Post CD",
'stage': Stages.VH4,
'game_id': "MusicTrack_Operator"},
{'name': "Versum Hill: Underground mall billboard graffiti",
'stage': Stages.VHO,
'game_id': "UnlockGraffiti_grafTex_XL3"},
{'name': "Versum Hill: Underground mall vending machine skateboard",
'stage': Stages.VHO,
'game_id': "SkateboardDeck8"},
{'name': "Versum Hill: BMX gate outfit",
'stage': Stages.VH1,
'game_id': "AngelOutfit3"},
{'name': "Versum Hill: Glass floor skates",
'stage': Stages.VH2,
'game_id': "InlineSkates4"},
{'name': "Versum Hill: Basketball court shortcut CD",
'stage': Stages.VH4,
'game_id': "MusicTrack_GetEnuf"},
{'name': "Versum Hill: Rave joins the crew",
'stage': Stages.VHO,
'game_id': "angel"},
{'name': "Versum Hill: Frank joins the crew",
'stage': Stages.VH2,
'game_id': "frank"},
{'name': "Versum Hill: Rietveld joins the crew",
'stage': Stages.VH4,
'game_id': "jetpackBossPlayer"},
{'name': "Versum Hill: Big Polo",
'stage': Stages.VH1,
'game_id': "PoloBuilding/Mascot_Polo_sit_big"},
{'name': "Versum Hill: Trash Polo",
'stage': Stages.VH1,
'game_id': "TrashCluster (1)/Mascot_Polo_street"},
{'name': "Versum Hill: Fruit stand Polo",
'stage': Stages.VHO,
'game_id': "SecretRoom/Mascot_Polo_street"},
{'name': "Millennium Square: Center ramp graffiti",
'stage': Stages.MS,
'game_id': "UnlockGraffiti_grafTex_L6"},
{'name': "Millennium Square: Rooftop staircase graffiti",
'stage': Stages.MS,
'game_id': "UnlockGraffiti_grafTex_M8"},
{'name': "Millennium Square: Toilet graffiti",
'stage': Stages.MS,
'game_id': "UnlockGraffiti_grafTex_XL6"},
{'name': "Millennium Square: Trash graffiti",
'stage': Stages.MS,
'game_id': "UnlockGraffiti_grafTex_M5"},
{'name': "Millennium Square: Center tower graffiti",
'stage': Stages.MS,
'game_id': "UnlockGraffiti_grafTex_M6"},
{'name': "Millennium Square: Rooftop billboard graffiti",
'stage': Stages.MS,
'game_id': "UnlockGraffiti_grafTex_XL7"},
{'name': "Millennium Square: Center Robo Post CD",
'stage': Stages.MS,
'game_id': "MusicTrack_FeelTheFunk"},
{'name': "Millennium Square: Parking garage Robo Post CD",
'stage': Stages.MS,
'game_id': "MusicTrack_Plume"},
{'name': "Millennium Square: Mall ledge outfit",
'stage': Stages.MS,
'game_id': "BlockGuyOutfit3"},
{'name': "Millennium Square: Alley rooftop outfit",
'stage': Stages.MS,
'game_id': "BlockGuyOutfit4"},
{'name': "Millennium Square: Alley staircase skateboard",
'stage': Stages.MS,
'game_id': "SkateboardDeck4"},
{'name': "Millennium Square: Secret painting skates",
'stage': Stages.MS,
'game_id': "InlineSkates2"},
{'name': "Millennium Square: Vending machine skates",
'stage': Stages.MS,
'game_id': "InlineSkates3"},
{'name': "Millennium Square: Walkway roof skates",
'stage': Stages.MS,
'game_id': "InlineSkates5"},
{'name': "Millennium Square: Alley ledge skates",
'stage': Stages.MS,
'game_id': "InlineSkates6"},
{'name': "Millennium Square: DJ Cyber joins the crew",
'stage': Stages.MS,
'game_id': "dj"},
{'name': "Millennium Square: Half pipe Polo",
'stage': Stages.MS,
'game_id': "propsSecretArea/Mascot_Polo_street"},
{'name': "Brink Terminal: Upside grind challenge reward",
'stage': Stages.BT1,
'game_id': "UnlockGraffiti_grafTex_M10"},
{'name': "Brink Terminal: Manual challenge reward",
'stage': Stages.BT1,
'game_id': "UnlockGraffiti_grafTex_L8"},
{'name': "Brink Terminal: Score challenge reward",
'stage': Stages.BT1,
'game_id': "UnlockGraffiti_grafTex_M12"},
{'name': "Brink Terminal: Under square ledge graffiti",
'stage': Stages.BT1,
'game_id': "UnlockGraffiti_grafTex_L9"},
{'name': "Brink Terminal: Bus graffiti",
'stage': Stages.BT1,
'game_id': "UnlockGraffiti_grafTex_XL9"},
{'name': "Brink Terminal: Under square Robo Post graffiti",
'stage': Stages.BT1,
'game_id': "UnlockGraffiti_grafTex_M9"},
{'name': "Brink Terminal: BMX gate graffiti",
'stage': Stages.BT1,
'game_id': "UnlockGraffiti_grafTex_L7"},
{'name': "Brink Terminal: Square tower CD",
'stage': Stages.BT1,
'game_id': "MusicTrack_Chapter1Mixtape"},
{'name': "Brink Terminal: Trash CD",
'stage': Stages.BT1,
'game_id': "MusicTrack_HairDunNailsDun"},
{'name': "Brink Terminal: Shop roof outfit",
'stage': Stages.BT1,
'game_id': "AngelOutfit4"},
{'name': "Brink Terminal: Underground glass skates",
'stage': Stages.BTO1,
'game_id': "InlineSkates8"},
{'name': "Brink Terminal: Glass roof skates",
'stage': Stages.BT1,
'game_id': "InlineSkates10"},
{'name': "Brink Terminal: Mesh's skateboard",
'stage': Stages.BTO2,
'game_id': "SkateboardDeck10"}, # double check this one
{'name': "Brink Terminal: Underground ramp skates",
'stage': Stages.BTO1,
'game_id': "InlineSkates7"},
{'name': "Brink Terminal: Rooftop halfpipe graffiti",
'stage': Stages.BT3,
'game_id': "UnlockGraffiti_grafTex_M11"},
{'name': "Brink Terminal: Wire grind CD",
'stage': Stages.BT2,
'game_id': "MusicTrack_Watchyaback"},
{'name': "Brink Terminal: Rooftop glass CD",
'stage': Stages.BT3,
'game_id': "MusicTrack_Refuse"},
{'name': "Brink Terminal: Tower core outfit",
'stage': Stages.BT3,
'game_id': "SpacegirlOutfit4"},
{'name': "Brink Terminal: High rooftop outfit",
'stage': Stages.BT3,
'game_id': "WideKidOutfit3"},
{'name': "Brink Terminal: Ocean platform CD",
'stage': Stages.BTO2,
'game_id': "MusicTrack_ScrapedOnTheWayOut"},
{'name': "Brink Terminal: End of dock CD",
'stage': Stages.BTO2,
'game_id': "MusicTrack_Hwbouths"},
{'name': "Brink Terminal: Dock Robo Post outfit",
'stage': Stages.BTO2,
'game_id': "WideKidOutfit4"},
{'name': "Brink Terminal: Control room skates",
'stage': Stages.BTO2,
'game_id': "InlineSkates9"},
{'name': "Brink Terminal: Mesh joins the crew",
'stage': Stages.BTO2,
'game_id': "wideKid"},
{'name': "Brink Terminal: Eclipse joins the crew",
'stage': Stages.BT1,
'game_id': "medusa"},
{'name': "Brink Terminal: Behind glass Polo",
'stage': Stages.BT1,
'game_id': "KingFood (Bear)/Mascot_Polo_street"},
{'name': "Millennium Mall: Warehouse pallet graffiti",
'stage': Stages.MM1,
'game_id': "UnlockGraffiti_grafTex_L5"},
{'name': "Millennium Mall: Wall alcove graffiti",
'stage': Stages.MM1,
'game_id': "UnlockGraffiti_grafTex_XL10"},
{'name': "Millennium Mall: Maintenance shaft CD",
'stage': Stages.MM1,
'game_id': "MusicTrack_MissingBreak"},
{'name': "Millennium Mall: Glass cylinder CD",
'stage': Stages.MM1,
'game_id': "MusicTrack_DAPEOPLE"},
{'name': "Millennium Mall: Lower Robo Post outfit",
'stage': Stages.MM1,
'game_id': "SpacegirlOutfit3"},
{'name': "Millennium Mall: Atrium vending machine graffiti",
'stage': Stages.MM2,
'game_id': "UnlockGraffiti_grafTex_M15"},
{'name': "Millennium Mall: Trick challenge reward",
'stage': Stages.MM2,
'game_id': "UnlockGraffiti_grafTex_XL8"},
{'name': "Millennium Mall: Slide challenge reward",
'stage': Stages.MM2,
'game_id': "UnlockGraffiti_grafTex_L10"},
{'name': "Millennium Mall: Fish challenge reward",
'stage': Stages.MM2,
'game_id': "UnlockGraffiti_grafTex_L12"},
{'name': "Millennium Mall: Score challenge reward",
'stage': Stages.MM2,
'game_id': "UnlockGraffiti_grafTex_XL11"},
{'name': "Millennium Mall: Atrium top floor Robo Post CD",
'stage': Stages.MM2,
'game_id': "MusicTrack_TwoDaysOff"},
{'name': "Millennium Mall: Atrium top floor floating CD",
'stage': Stages.MM2,
'game_id': "MusicTrack_Spectres"},
{'name': "Millennium Mall: Atrium top floor BMX",
'stage': Stages.MM2,
'game_id': "BMXBike2"},
{'name': "Millennium Mall: Theater entrance BMX",
'stage': Stages.MM2,
'game_id': "BMXBike3"},
{'name': "Millennium Mall: Atrium BMX gate BMX",
'stage': Stages.MM2,
'game_id': "BMXBike5"},
{'name': "Millennium Mall: Upside down rail outfit",
'stage': Stages.MM2,
'game_id': "BunGirlOutfit3"},
{'name': "Millennium Mall: Theater stage corner graffiti",
'stage': Stages.MM3,
'game_id': "UnlockGraffiti_grafTex_L15"},
{'name': "Millennium Mall: Theater hanging billboards graffiti",
'stage': Stages.MM3,
'game_id': "UnlockGraffiti_grafTex_XL15"},
{'name': "Millennium Mall: Theater garage graffiti",
'stage': Stages.MM3,
'game_id': "UnlockGraffiti_grafTex_M16"},
{'name': "Millennium Mall: Theater maintenance CD",
'stage': Stages.MM3,
'game_id': "MusicTrack_WannaKno"},
{'name': "Millennium Mall: Race track Robo Post CD",
'stage': Stages.MMO2,
'game_id': "MusicTrack_StateOfMind"},
{'name': "Millennium Mall: Hanging lights CD",
'stage': Stages.MMO1,
'game_id': "MusicTrack_Chapter2Mixtape"},
{'name': "Millennium Mall: Shine joins the crew",
'stage': Stages.MM3,
'game_id': "bunGirl"},
{'name': "Millennium Mall: DOT.EXE joins the crew",
'stage': Stages.MM2,
'game_id': "eightBall"},
{'name': "Pyramid Island: Lower rooftop graffiti",
'stage': Stages.PI1,
'game_id': "UnlockGraffiti_grafTex_L18"},
{'name': "Pyramid Island: Polo graffiti",
'stage': Stages.PI1,
'game_id': "UnlockGraffiti_grafTex_L16"},
{'name': "Pyramid Island: Above entrance graffiti",
'stage': Stages.PI1,
'game_id': "UnlockGraffiti_grafTex_XL16"},
{'name': "Pyramid Island: BMX gate BMX",
'stage': Stages.PI1,
'game_id': "BMXBike6"},
{'name': "Pyramid Island: Quarter pipe rooftop graffiti",
'stage': Stages.PI2,
'game_id': "UnlockGraffiti_grafTex_M17"},
{'name': "Pyramid Island: Supply port Robo Post CD",
'stage': Stages.PI2,
'game_id': "MusicTrack_Trinitron"},
{'name': "Pyramid Island: Above gate ledge CD",
'stage': Stages.PI2,
'game_id': "MusicTrack_Agua"},
{'name': "Pyramid Island: Smoke hole BMX",
'stage': Stages.PI2,
'game_id': "BMXBike8"},
{'name': "Pyramid Island: Above gate rail outfit",
'stage': Stages.PI2,
'game_id': "VinylOutfit3"},
{'name': "Pyramid Island: Rail loop outfit",
'stage': Stages.PI2,
'game_id': "BunGirlOutfit4"},
{'name': "Pyramid Island: Score challenge reward",
'stage': Stages.PI2,
'game_id': "UnlockGraffiti_grafTex_XL2"},
{'name': "Pyramid Island: Score challenge 2 reward",
'stage': Stages.PI2,
'game_id': "UnlockGraffiti_grafTex_L13"},
{'name': "Pyramid Island: Quarter pipe challenge reward",
'stage': Stages.PI2,
'game_id': "UnlockGraffiti_grafTex_XL12"},
{'name': "Pyramid Island: Wind turbines CD",
'stage': Stages.PI3,
'game_id': "MusicTrack_YouCanSayHi"},
{'name': "Pyramid Island: Shortcut glass CD",
'stage': Stages.PI3,
'game_id': "MusicTrack_Chromebies"},
{'name': "Pyramid Island: Turret jump CD",
'stage': Stages.PI3,
'game_id': "MusicTrack_ChuckinUp"},
{'name': "Pyramid Island: Helipad BMX",
'stage': Stages.PI3,
'game_id': "BMXBike7"},
{'name': "Pyramid Island: Pipe outfit",
'stage': Stages.PI3,
'game_id': "PufferGirlOutfit3"},
{'name': "Pyramid Island: Trash outfit",
'stage': Stages.PI3,
'game_id': "PufferGirlOutfit4"},
{'name': "Pyramid Island: Pyramid top CD",
'stage': Stages.PI4,
'game_id': "MusicTrack_BigCityLife"},
{'name': "Pyramid Island: Pyramid top Robo Post CD",
'stage': Stages.PI4,
'game_id': "MusicTrack_Chapter3Mixtape"},
{'name': "Pyramid Island: Maze outfit",
'stage': Stages.PIO,
'game_id': "VinylOutfit4"},
{'name': "Pyramid Island: Rise joins the crew",
'stage': Stages.PI4,
'game_id': "pufferGirl"},
{'name': "Pyramid Island: Devil Theory joins the crew",
'stage': Stages.PI3,
'game_id': "boarder"},
{'name': "Pyramid Island: Polo pile 1",
'stage': Stages.PI1,
'game_id': "Secret01Trash/Mascot_Polo_sit_big_wave"},
{'name': "Pyramid Island: Polo pile 2",
'stage': Stages.PI1,
'game_id': "Secret01Trash/Mascot_Polo_sit_big_wave (1)"},
{'name': "Pyramid Island: Polo pile 3",
'stage': Stages.PI1,
'game_id': "Secret01Trash/Mascot_Polo_sit_big_wave (2)"},
{'name': "Pyramid Island: Polo pile 4",
'stage': Stages.PI1,
'game_id': "Secret01Trash/Mascot_Polo_sit_big_wave (3)"},
{'name': "Pyramid Island: Maze glass Polo",
'stage': Stages.PIO,
'game_id': "Start/Mascot_Polo_sit_big (1)"},
{'name': "Pyramid Island: Maze classroom Polo",
'stage': Stages.PIO,
'game_id': "PeteRoom/Mascot_Polo_sit_big_wave (1)"},
{'name': "Pyramid Island: Maze vent Polo",
'stage': Stages.PIO,
'game_id': "CheckerRoom/Mascot_Polo_street"},
{'name': "Pyramid Island: Big maze Polo",
'stage': Stages.PIO,
'game_id': "YellowPoloRoom/Mascot_Polo_sit_big"},
{'name': "Pyramid Island: Maze desk Polo",
'stage': Stages.PIO,
'game_id': "PoloRoom/Mascot_Polo_sit_big"},
{'name': "Pyramid Island: Maze forklift Polo",
'stage': Stages.PIO,
'game_id': "ForkliftRoom/Mascot_Polo_sit_big_wave"},
{'name': "Mataan: Robo Post graffiti",
'stage': Stages.MA1,
'game_id': "UnlockGraffiti_grafTex_XL17"},
{'name': "Mataan: Secret ledge BMX",
'stage': Stages.MA1,
'game_id': "BMXBike9"},
{'name': "Mataan: Highway rooftop BMX",
'stage': Stages.MA1,
'game_id': "BMXBike10"},
{'name': "Mataan: Trash CD",
'stage': Stages.MA2,
'game_id': "MusicTrack_JackDaFunk"},
{'name': "Mataan: Half pipe CD",
'stage': Stages.MA2,
'game_id': "MusicTrack_FunkExpress"},
{'name': "Mataan: Across bull horns graffiti",
'stage': Stages.MA2,
'game_id': "UnlockGraffiti_grafTex_L17"},
{'name': "Mataan: Small rooftop graffiti",
'stage': Stages.MA2,
'game_id': "UnlockGraffiti_grafTex_M18"},
{'name': "Mataan: Trash graffiti",
'stage': Stages.MA2,
'game_id': "UnlockGraffiti_grafTex_XL5"},
{'name': "Mataan: Deep city Robo Post CD",
'stage': Stages.MA3,
'game_id': "MusicTrack_LastHoorah"},
{'name': "Mataan: Deep city tower CD",
'stage': Stages.MA3,
'game_id': "MusicTrack_Chapter4Mixtape"},
{'name': "Mataan: Race challenge reward",
'stage': Stages.MA3,
'game_id': "UnlockGraffiti_grafTex_M14"},
{'name': "Mataan: Wallrunning challenge reward",
'stage': Stages.MA3,
'game_id': "UnlockGraffiti_grafTex_L14"},
{'name': "Mataan: Score challenge reward",
'stage': Stages.MA3,
'game_id': "UnlockGraffiti_grafTex_XL13"},
{'name': "Mataan: Deep city vent jump BMX",
'stage': Stages.MA3,
'game_id': "BMXBike4"},
{'name': "Mataan: Deep city side wires outfit",
'stage': Stages.MA3,
'game_id': "DummyOutfit3"},
{'name': "Mataan: Deep city center island outfit",
'stage': Stages.MA3,
'game_id': "DummyOutfit4"},
{'name': "Mataan: Red light rail graffiti",
'stage': Stages.MAO,
'game_id': "UnlockGraffiti_grafTex_XL18"},
{'name': "Mataan: Red light side alley outfit",
'stage': Stages.MAO,
'game_id': "RingDudeOutfit3"},
{'name': "Mataan: Statue hand outfit",
'stage': Stages.MA4,
'game_id': "RingDudeOutfit4"},
{'name': "Mataan: Crane CD",
'stage': Stages.MA5,
'game_id': "MusicTrack_InThePocket"},
{'name': "Mataan: Elephant tower glass outfit",
'stage': Stages.MA5,
'game_id': "LegendFaceOutfit3"},
{'name': "Mataan: Helipad outfit",
'stage': Stages.MA5,
'game_id': "LegendFaceOutfit4"},
{'name': "Mataan: Vending machine CD",
'stage': Stages.MA5,
'game_id': "MusicTrack_Iridium"},
{'name': "Mataan: Coil joins the crew",
'stage': Stages.MA5,
'game_id': "ringdude"},
{'name': "Mataan: Flesh Prince joins the crew",
'stage': Stages.MA5,
'game_id': "prince"},
{'name': "Mataan: Futurism joins the crew",
'stage': Stages.MA5,
'game_id': "futureGirl"},
{'name': "Mataan: Trash Polo",
'stage': Stages.MA2,
'game_id': "PropsMallArea/Mascot_Polo_street"},
{'name': "Mataan: Shopping Polo",
'stage': Stages.MA5,
'game_id': "propsMarket/Mascot_Polo_street"},
{'name': "Tagged 5 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf5"},
{'name': "Tagged 10 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf10"},
{'name': "Tagged 15 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf15"},
{'name': "Tagged 20 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf20"},
{'name': "Tagged 25 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf25"},
{'name': "Tagged 30 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf30"},
{'name': "Tagged 35 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf35"},
{'name': "Tagged 40 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf40"},
{'name': "Tagged 45 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf45"},
{'name': "Tagged 50 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf50"},
{'name': "Tagged 55 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf55"},
{'name': "Tagged 60 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf60"},
{'name': "Tagged 65 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf65"},
{'name': "Tagged 70 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf70"},
{'name': "Tagged 75 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf75"},
{'name': "Tagged 80 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf80"},
{'name': "Tagged 85 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf85"},
{'name': "Tagged 90 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf90"},
{'name': "Tagged 95 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf95"},
{'name': "Tagged 100 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf100"},
{'name': "Tagged 105 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf105"},
{'name': "Tagged 110 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf110"},
{'name': "Tagged 115 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf115"},
{'name': "Tagged 120 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf120"},
{'name': "Tagged 125 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf125"},
{'name': "Tagged 130 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf130"},
{'name': "Tagged 135 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf135"},
{'name': "Tagged 140 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf140"},
{'name': "Tagged 145 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf145"},
{'name': "Tagged 150 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf150"},
{'name': "Tagged 155 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf155"},
{'name': "Tagged 160 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf160"},
{'name': "Tagged 165 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf165"},
{'name': "Tagged 170 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf170"},
{'name': "Tagged 175 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf175"},
{'name': "Tagged 180 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf180"},
{'name': "Tagged 185 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf185"},
{'name': "Tagged 190 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf190"},
{'name': "Tagged 195 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf195"},
{'name': "Tagged 200 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf200"},
{'name': "Tagged 205 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf205"},
{'name': "Tagged 210 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf210"},
{'name': "Tagged 215 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf215"},
{'name': "Tagged 220 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf220"},
{'name': "Tagged 225 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf225"},
{'name': "Tagged 230 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf230"},
{'name': "Tagged 235 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf235"},
{'name': "Tagged 240 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf240"},
{'name': "Tagged 245 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf245"},
{'name': "Tagged 250 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf250"},
{'name': "Tagged 255 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf255"},
{'name': "Tagged 260 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf260"},
{'name': "Tagged 265 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf265"},
{'name': "Tagged 270 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf270"},
{'name': "Tagged 275 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf275"},
{'name': "Tagged 280 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf280"},
{'name': "Tagged 285 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf285"},
{'name': "Tagged 290 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf290"},
{'name': "Tagged 295 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf295"},
{'name': "Tagged 300 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf300"},
{'name': "Tagged 305 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf305"},
{'name': "Tagged 310 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf310"},
{'name': "Tagged 315 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf315"},
{'name': "Tagged 320 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf320"},
{'name': "Tagged 325 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf325"},
{'name': "Tagged 330 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf330"},
{'name': "Tagged 335 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf335"},
{'name': "Tagged 340 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf340"},
{'name': "Tagged 345 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf345"},
{'name': "Tagged 350 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf350"},
{'name': "Tagged 355 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf355"},
{'name': "Tagged 360 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf360"},
{'name': "Tagged 365 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf365"},
{'name': "Tagged 370 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf370"},
{'name': "Tagged 375 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf375"},
{'name': "Tagged 380 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf380"},
{'name': "Tagged 385 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf385"},
{'name': "Tagged 389 Graffiti Spots",
'stage': Stages.Misc,
'game_id': "graf379"},
]
event_table: List[EventDict] = [
{'name': "Versum Hill: Complete Chapter 1",
'stage': Stages.VH4,
'item': "Chapter Completed"},
{'name': "Brink Terminal: Complete Chapter 2",
'stage': Stages.BT3,
'item': "Chapter Completed"},
{'name': "Millennium Mall: Complete Chapter 3",
'stage': Stages.MM3,
'item': "Chapter Completed"},
{'name': "Pyramid Island: Complete Chapter 4",
'stage': Stages.PI3,
'item': "Chapter Completed"},
{'name': "Defeat Faux",
'stage': Stages.MA5,
'item': "Victory"},
]

View File

@@ -0,0 +1,162 @@
from dataclasses import dataclass
from Options import Choice, Toggle, DefaultOnToggle, Range, DeathLink, PerGameCommonOptions
import typing
if typing.TYPE_CHECKING:
from random import Random
else:
Random = typing.Any
class Logic(Choice):
"""Choose the logic used by the randomizer."""
display_name = "Logic"
option_glitchless = 0
option_glitched = 1
default = 0
class SkipIntro(DefaultOnToggle):
"""Skips escaping the police station.
Graffiti spots tagged during the intro will not unlock items."""
display_name = "Skip Intro"
class SkipDreams(Toggle):
"""Skips the dream sequences at the end of each chapter.
This can be changed later in the options menu inside the Archipelago phone app."""
display_name = "Skip Dreams"
class SkipHands(Toggle):
"""Skips spraying the lion statue hands after the dream in Chapter 5."""
display_name = "Skip Statue Hands"
class TotalRep(Range):
"""Change the total amount of REP in your world.
At least 960 REP is needed to finish the game.
Will be rounded to the nearest number divisible by 8."""
display_name = "Total REP"
range_start = 1000
range_end = 2000
default = 1400
def round_to_nearest_step(self):
rem: int = self.value % 8
if rem >= 5:
self.value = self.value - rem + 8
else:
self.value = self.value - rem
def get_rep_item_counts(self, random_source: Random, location_count: int) -> typing.List[int]:
def increment_item(item: int) -> int:
if item >= 32:
item = 48
else:
item += 8
return item
items = [8]*location_count
while sum(items) < self.value:
index = random_source.randint(0, location_count-1)
while items[index] >= 48:
index = random_source.randint(0, location_count-1)
items[index] = increment_item(items[index])
while sum(items) > self.value:
index = random_source.randint(0, location_count-1)
while not (items[index] == 16 or items[index] == 24 or items[index] == 32):
index = random_source.randint(0, location_count-1)
items[index] -= 8
return [items.count(8), items.count(16), items.count(24), items.count(32), items.count(48)]
class EndingREP(Toggle):
"""Changes the final boss to require 1000 REP instead of 960 REP to start."""
display_name = "Extra REP Required"
class StartStyle(Choice):
"""Choose which movestyle to start with."""
display_name = "Starting Movestyle"
option_skateboard = 2
option_inline_skates = 3
option_bmx = 1
default = 2
class LimitedGraffiti(Toggle):
"""Each graffiti design can only be used a limited number of times before being removed from your inventory.
In some cases, such as completing a dream, using graffiti to defeat enemies, or spraying over your own graffiti,
uses will not be counted.
If enabled, doing graffiti is disabled during crew battles, to prevent softlocking."""
display_name = "Limited Graffiti"
class SGraffiti(Choice):
"""Choose if small graffiti should be separate, meaning that you will need to switch characters every time you run
out, or combined, meaning that unlocking new characters will add 5 uses that any character can use.
Has no effect if Limited Graffiti is disabled."""
display_name = "Small Graffiti Uses"
option_separate = 0
option_combined = 1
default = 0
class JunkPhotos(Toggle):
"""Skip taking pictures of Polo for items."""
display_name = "Skip Polo Photos"
class DontSavePhotos(Toggle):
"""Photos taken with the Camera app will not be saved.
This can be changed later in the options menu inside the Archipelago phone app."""
display_name = "Don't Save Photos"
class ScoreDifficulty(Choice):
"""Alters the score required to win score challenges and crew battles.
This can be changed later in the options menu inside the Archipelago phone app."""
display_name = "Score Difficulty"
option_normal = 0
option_medium = 1
option_hard = 2
option_very_hard = 3
option_extreme = 4
default = 0
class DamageMultiplier(Range):
"""Multiplies all damage received.
At 3x, most damage will OHKO the player, including falling into pits.
At 6x, all damage will OHKO the player.
This can be changed later in the options menu inside the Archipelago phone app."""
display_name = "Damage Multiplier"
range_start = 1
range_end = 6
default = 1
class BRCDeathLink(DeathLink):
"""When you die, everyone dies. The reverse is also true.
This can be changed later in the options menu inside the Archipelago phone app."""
@dataclass
class BombRushCyberfunkOptions(PerGameCommonOptions):
logic: Logic
skip_intro: SkipIntro
skip_dreams: SkipDreams
skip_statue_hands: SkipHands
total_rep: TotalRep
extra_rep_required: EndingREP
starting_movestyle: StartStyle
limited_graffiti: LimitedGraffiti
small_graffiti_uses: SGraffiti
skip_polo_photos: JunkPhotos
dont_save_photos: DontSavePhotos
score_difficulty: ScoreDifficulty
damage_multiplier: DamageMultiplier
death_link: BRCDeathLink

View File

@@ -0,0 +1,103 @@
from typing import Dict
class Stages:
Misc = "Misc"
H = "Hideout"
VH1 = "Versum Hill"
VH2 = "Versum Hill - After Roadblock"
VHO = "Versum Hill - Underground Mall"
VH3 = "Versum Hill - Side Street"
VH4 = "Versum Hill - Basketball Court"
MS = "Millennium Square"
BT1 = "Brink Terminal"
BTO1 = "Brink Terminal - Underground"
BTO2 = "Brink Terminal - Dock"
BT2 = "Brink Terminal - Planet Plaza"
BT3 = "Brink Terminal - Tower"
MM1 = "Millennium Mall"
MMO1 = "Millennium Mall - Hanging Lights"
MM2 = "Millennium Mall - Atrium"
MMO2 = "Millennium Mall - Race Track"
MM3 = "Millennium Mall - Theater"
PI1 = "Pyramid Island - Base"
PI2 = "Pyramid Island - After Gate"
PIO = "Pyramid Island - Maze"
PI3 = "Pyramid Island - Upper Areas"
PI4 = "Pyramid Island - Top"
MA1 = "Mataan - Streets"
MA2 = "Mataan - After Smoke Wall"
MA3 = "Mataan - Deep City"
MAO = "Mataan - Red Light District"
MA4 = "Mataan - Lion Statue"
MA5 = "Mataan - Skyscrapers"
region_exits: Dict[str, str] = {
Stages.Misc: [Stages.H],
Stages.H: [Stages.Misc,
Stages.VH1,
Stages.MS,
Stages.MA1],
Stages.VH1: [Stages.H,
Stages.VH2],
Stages.VH2: [Stages.H,
Stages.VH1,
Stages.MS,
Stages.VHO,
Stages.VH3,
Stages.VH4],
Stages.VHO: [Stages.VH2],
Stages.VH3: [Stages.VH2],
Stages.VH4: [Stages.VH2,
Stages.VH1],
Stages.MS: [Stages.VH2,
Stages.BT1,
Stages.MM1,
Stages.PI1,
Stages.MA1],
Stages.BT1: [Stages.MS,
Stages.BTO1,
Stages.BTO2,
Stages.BT2],
Stages.BTO1: [Stages.BT1],
Stages.BTO2: [Stages.BT1],
Stages.BT2: [Stages.BT1,
Stages.BT3],
Stages.BT3: [Stages.BT1,
Stages.BT2],
Stages.MM1: [Stages.MS,
Stages.MMO1,
Stages.MM2],
Stages.MMO1: [Stages.MM1],
Stages.MM2: [Stages.MM1,
Stages.MMO2,
Stages.MM3],
Stages.MMO2: [Stages.MM2],
Stages.MM3: [Stages.MM2,
Stages.MM1],
Stages.PI1: [Stages.MS,
Stages.PI2],
Stages.PI2: [Stages.PI1,
Stages.PIO,
Stages.PI3],
Stages.PIO: [Stages.PI2],
Stages.PI3: [Stages.PI1,
Stages.PI2,
Stages.PI4],
Stages.PI4: [Stages.PI1,
Stages.PI2,
Stages.PI3],
Stages.MA1: [Stages.H,
Stages.MS,
Stages.MA2],
Stages.MA2: [Stages.MA1,
Stages.MA3],
Stages.MA3: [Stages.MA2,
Stages.MAO,
Stages.MA4],
Stages.MAO: [Stages.MA3],
Stages.MA4: [Stages.MA3,
Stages.MA5],
Stages.MA5: [Stages.MA1]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,203 @@
from typing import Any, Dict
from BaseClasses import MultiWorld, Region, Location, Item, Tutorial, ItemClassification, CollectionState
from worlds.AutoWorld import World, WebWorld
from .Items import base_id, item_table, group_table, BRCType
from .Locations import location_table, event_table
from .Regions import region_exits
from .Rules import rules
from .Options import BombRushCyberfunkOptions, StartStyle
class BombRushCyberfunkWeb(WebWorld):
theme = "ocean"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up Bomb Rush Cyberfunk randomizer and connecting to an Archipelago Multiworld",
"English",
"setup_en.md",
"setup/en",
["TRPG"]
)]
class BombRushCyberfunkWorld(World):
"""Bomb Rush Cyberfunk is 1 second per second of advanced funkstyle. Battle rival crews and dispatch militarized
police to conquer the five boroughs of New Amsterdam. Become All City."""
game = "Bomb Rush Cyberfunk"
web = BombRushCyberfunkWeb()
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
item_name_to_type = {item["name"]: item["type"] for item in item_table}
location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
item_name_groups = group_table
options_dataclass = BombRushCyberfunkOptions
options: BombRushCyberfunkOptions
def __init__(self, multiworld: MultiWorld, player: int):
super(BombRushCyberfunkWorld, self).__init__(multiworld, player)
self.item_classification: Dict[BRCType, ItemClassification] = {
BRCType.Music: ItemClassification.filler,
BRCType.GraffitiM: ItemClassification.progression,
BRCType.GraffitiL: ItemClassification.progression,
BRCType.GraffitiXL: ItemClassification.progression,
BRCType.Outfit: ItemClassification.filler,
BRCType.Character: ItemClassification.progression,
BRCType.REP: ItemClassification.progression_skip_balancing,
BRCType.Camera: ItemClassification.progression
}
def collect(self, state: "CollectionState", item: "Item") -> bool:
change = super().collect(state, item)
if change and "REP" in item.name:
rep: int = int(item.name[0:len(item.name)-4])
state.prog_items[item.player]["rep"] += rep
return change
def remove(self, state: "CollectionState", item: "Item") -> bool:
change = super().remove(state, item)
if change and "REP" in item.name:
rep: int = int(item.name[0:len(item.name)-4])
state.prog_items[item.player]["rep"] -= rep
return change
def set_rules(self):
rules(self)
def get_item_classification(self, item_type: BRCType) -> ItemClassification:
classification = ItemClassification.filler
if item_type in self.item_classification.keys():
classification = self.item_classification[item_type]
return classification
def create_item(self, name: str) -> "BombRushCyberfunkItem":
item_id: int = self.item_name_to_id[name]
item_type: BRCType = self.item_name_to_type[name]
classification = self.get_item_classification(item_type)
return BombRushCyberfunkItem(name, classification, item_id, self.player)
def create_event(self, event: str) -> "BombRushCyberfunkItem":
return BombRushCyberfunkItem(event, ItemClassification.progression_skip_balancing, None, self.player)
def get_filler_item_name(self) -> str:
item = self.random.choice(item_table)
while self.get_item_classification(item["type"]) == ItemClassification.progression:
item = self.random.choice(item_table)
return item["name"]
def generate_early(self):
if self.options.starting_movestyle == StartStyle.option_skateboard:
self.item_classification[BRCType.Skateboard] = ItemClassification.filler
else:
self.item_classification[BRCType.Skateboard] = ItemClassification.progression
if self.options.starting_movestyle == StartStyle.option_inline_skates:
self.item_classification[BRCType.InlineSkates] = ItemClassification.filler
else:
self.item_classification[BRCType.InlineSkates] = ItemClassification.progression
if self.options.starting_movestyle == StartStyle.option_bmx:
self.item_classification[BRCType.BMX] = ItemClassification.filler
else:
self.item_classification[BRCType.BMX] = ItemClassification.progression
def create_items(self):
rep_locations: int = 87
if self.options.skip_polo_photos:
rep_locations -= 18
self.options.total_rep.round_to_nearest_step()
rep_counts = self.options.total_rep.get_rep_item_counts(self.random, rep_locations)
#print(sum([8*rep_counts[0], 16*rep_counts[1], 24*rep_counts[2], 32*rep_counts[3], 48*rep_counts[4]]), \
# rep_counts)
pool = []
for item in item_table:
if "REP" in item["name"]:
count: int = 0
if item["name"] == "8 REP":
count = rep_counts[0]
elif item["name"] == "16 REP":
count = rep_counts[1]
elif item["name"] == "24 REP":
count = rep_counts[2]
elif item["name"] == "32 REP":
count = rep_counts[3]
elif item["name"] == "48 REP":
count = rep_counts[4]
if count > 0:
for _ in range(count):
pool.append(self.create_item(item["name"]))
else:
pool.append(self.create_item(item["name"]))
self.multiworld.itempool += pool
def create_regions(self):
multiworld = self.multiworld
player = self.player
menu = Region("Menu", player, multiworld)
multiworld.regions.append(menu)
for n in region_exits:
multiworld.regions += [Region(n, player, multiworld)]
menu.add_exits({"Hideout": "New Game"})
for n in region_exits:
self.get_region(n).add_exits(region_exits[n])
for index, loc in enumerate(location_table):
if self.options.skip_polo_photos and "Polo" in loc["name"]:
continue
stage: Region = self.get_region(loc["stage"])
stage.add_locations({loc["name"]: base_id + index})
for e in event_table:
stage: Region = self.get_region(e["stage"])
event = BombRushCyberfunkLocation(player, e["name"], None, stage)
event.show_in_spoiler = False
event.place_locked_item(self.create_event(e["item"]))
stage.locations += [event]
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
def fill_slot_data(self) -> Dict[str, Any]:
options = self.options
slot_data: Dict[str, Any] = {
"locations": {loc["game_id"]: (base_id + index) for index, loc in enumerate(location_table)},
"logic": options.logic.value,
"skip_intro": bool(options.skip_intro.value),
"skip_dreams": bool(options.skip_dreams.value),
"skip_statue_hands": bool(options.skip_statue_hands.value),
"total_rep": options.total_rep.value,
"extra_rep_required": bool(options.extra_rep_required.value),
"starting_movestyle": options.starting_movestyle.value,
"limited_graffiti": bool(options.limited_graffiti.value),
"small_graffiti_uses": options.small_graffiti_uses.value,
"skip_polo_photos": bool(options.skip_polo_photos.value),
"dont_save_photos": bool(options.dont_save_photos.value),
"score_difficulty": int(options.score_difficulty.value),
"damage_multiplier": options.damage_multiplier.value,
"death_link": bool(options.death_link.value)
}
return slot_data
class BombRushCyberfunkItem(Item):
game: str = "Bomb Rush Cyberfunk"
class BombRushCyberfunkLocation(Location):
game: str = "Bomb Rush Cyberfunk"

View File

@@ -0,0 +1,29 @@
# Bomb Rush Cyberfunk
## Where is the options page?
The [player options page for this game](../player-options) contains all the options you need to configure and export
a config file.
## What does randomization do in this game?
The goal of Bomb Rush Cyberfunk randomizer is to defeat all rival crews in each borough of New Amsterdam. REP is no
longer earned from doing graffiti, and is instead earned by finding it randomly in the multiworld.
Items can be found by picking up any type of collectible, unlocking characters, taking pictures of Polo, and for every
5 graffiti spots tagged. The types of items that can be found are Music, Graffiti (M), Graffiti (L), Graffiti (XL),
Skateboards, Inline Skates, BMX, Outfits, Characters, REP, and the Camera.
Several changes have been made to the game for a better experience as a randomizer:
- The prelude in the police station can be skipped.
- The map for each stage is always unlocked.
- The taxi is always unlocked, but you will still need to visit each stage's taxi stop before you can use them.
- No M, L, or XL graffiti is unlocked at the beginning.
- Optionally, graffiti can be depleted after a certain number of uses.
- All characters except Red are locked.
- One single REP count is used throughout the game, instead of having separate totals for each stage. REP requirements
are the same as the original game, but added together in order. At least 960 REP is needed to finish the game.
The mod also adds two new apps to the phone, an "Encounter" app which lets you retry certain events early, and the
"Archipelago" app which lets you view chat messages and change some options while playing.

View File

@@ -0,0 +1,41 @@
# Bomb Rush Cyberfunk Multiworld Setup Guide
## Quick Links
- Bomb Rush Cyberfunk: [Steam](https://store.steampowered.com/app/1353230/Bomb_Rush_Cyberfunk/)
- Archipelago Mod: [Thunderstore](https://thunderstore.io/c/bomb-rush-cyberfunk/p/TRPG/BRC_Archipelago/),
[GitHub](https://github.com/TRPG0/BRC-Archipelago/releases)
## Setup
To install the Archipelago mod, you can use a mod manager like
[r2modman](https://thunderstore.io/c/bomb-rush-cyberfunk/p/ebkr/r2modman/), or install manually by following these steps:
1. Download and install [BepInEx 5.4.22 x64](https://github.com/BepInEx/BepInEx/releases/tag/v5.4.22) in your Bomb Rush
Cyberfunk root folder. *Do not use any pre-release versions of BepInEx 6.*
2. Start Bomb Rush Cyberfunk once so that BepInEx can create its required configuration files.
3. Download the zip archive from the [releases](https://github.com/TRPG0/BRC-Archipelago/releases) page, and extract its
contents into `BepInEx\plugins`.
After installing Archipelago, there are some additional mods that can also be installed for a better experience:
- [MoreMap](https://thunderstore.io/c/bomb-rush-cyberfunk/p/TRPG/MoreMap/) by TRPG
- Adds pins to the map for every type of collectible.
- [FasterLoadTimes](https://thunderstore.io/c/bomb-rush-cyberfunk/p/cspotcode/FasterLoadTimes/) by cspotcode
- Load stages faster by skipping assets that are already loaded.
- [CutsceneSkip](https://thunderstore.io/c/bomb-rush-cyberfunk/p/Jay/CutsceneSkip/) by Jay
- Makes every cutscene skippable.
- [GimmeMyBoost](https://thunderstore.io/c/bomb-rush-cyberfunk/p/Yuri/GimmeMyBoost/) by Yuri
- Retains boost when loading into a new stage.
- [DisableAnnoyingCutscenes](https://thunderstore.io/c/bomb-rush-cyberfunk/p/viliger/DisableAnnoyingCutscenes/) by viliger
- Disables the police cutscenes when increasing your heat level.
- [FastTravel](https://thunderstore.io/c/bomb-rush-cyberfunk/p/tari/FastTravel/) by tari
- Adds an app to the phone to call for a taxi from anywhere.
## Connecting
To connect to an Archipelago server, click one of the Archipelago buttons next to the save files. If the save file is
blank or already has randomizer save data, it will open a menu where you can enter the server address and port, your
name, and a password if necessary. Then click the check mark to connect to the server.

View File

@@ -0,0 +1,5 @@
from test.bases import WorldTestBase
class BombRushCyberfunkTestBase(WorldTestBase):
game = "Bomb Rush Cyberfunk"

View File

@@ -0,0 +1,284 @@
from . import BombRushCyberfunkTestBase
from ..Rules import build_access_cache, spots_s_glitchless, spots_s_glitched, spots_m_glitchless, spots_m_glitched, \
spots_l_glitchless, spots_l_glitched, spots_xl_glitched, spots_xl_glitchless
class TestSpotsGlitchless(BombRushCyberfunkTestBase):
@property
def run_default_tests(self) -> bool:
return False
def test_spots_glitchless(self) -> None:
player = self.player
self.collect_by_name([
"Graffiti (M - OVERWHELMME)",
"Graffiti (L - WHOLE SIXER)",
"Graffiti (XL - Gold Rush)"
])
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 1 - hideout
self.assertEqual(10, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(4, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(7, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(3, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.collect_by_name("Inline Skates (Glaciers)")
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
self.assertEqual(8, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 20
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 1 - VH1-2
self.assertEqual(22, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(20, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(23, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(9, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 65
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 1 - VH3
self.assertEqual(23, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(24, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 90
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 1 - VH4
self.assertEqual(10, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["Chapter Completed"] = 1
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 2 - MS + MA1
self.assertEqual(34, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(39, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(38, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(19, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 120
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 2 - VHO
self.assertEqual(35, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(43, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(40, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.collect_by_name("Bel")
self.multiworld.state.prog_items[player]["rep"] = 180
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 2 - BT1
self.assertEqual(44, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(56, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(50, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(22, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 220
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 2 - BT2
self.assertEqual(47, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(60, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(52, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(23, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 250
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 2 - BTO1
self.assertEqual(53, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(24, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 280
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 2 - BT3 / chapter 3 - MS
self.assertEqual(58, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(28, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 320
self.multiworld.state.prog_items[player]["Chapter Completed"] = 2
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 2 - BTO2 / chapter 3 - MS
self.assertEqual(54, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(67, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(62, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(30, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 380
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 3 - MM1-2
self.assertEqual(61, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(78, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(73, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(37, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 491
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 3 - MM3
self.assertEqual(64, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(82, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(77, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(42, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["Chapter Completed"] = 3
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 4 - MS / BT / MMO1 / PI1
self.assertEqual(66, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(85, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(85, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(46, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 620
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 4 - PI2
self.assertEqual(71, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(88, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(89, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 660
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 4 - PI3
self.assertEqual(79, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(96, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(94, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(51, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 730
self.multiworld.state.prog_items[player]["Chapter Completed"] = 4
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 5 - PI4
self.assertEqual(98, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(96, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 780
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 5 - PIO
self.assertEqual(81, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(103, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(98, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(54, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 850
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 5 - MA2
self.assertEqual(84, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(99, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(56, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 864
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 5 - MA3
self.assertEqual(89, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(111, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(102, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(58, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 935
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 5 - MAO
self.assertEqual(92, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(112, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(104, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(60, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["rep"] = 960
access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
# chapter 5 - MA4-5
self.assertEqual(94, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(123, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(111, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
self.assertEqual(62, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
class TestSpotsGlitched(BombRushCyberfunkTestBase):
options = {
"logic": "glitched"
}
@property
def run_default_tests(self) -> bool:
return False
def test_spots_glitched(self) -> None:
player = self.player
self.collect_by_name([
"Graffiti (M - OVERWHELMME)",
"Graffiti (L - WHOLE SIXER)",
"Graffiti (XL - Gold Rush)"
])
access_cache = build_access_cache(self.multiworld.state, player, 2, False, True)
self.assertEqual(75, spots_s_glitched(self.multiworld.state, player, False, access_cache))
self.assertEqual(99, spots_m_glitched(self.multiworld.state, player, False, access_cache))
self.assertEqual(88, spots_l_glitched(self.multiworld.state, player, False, access_cache))
self.assertEqual(51, spots_xl_glitched(self.multiworld.state, player, False, access_cache))
self.collect_by_name("Bel")
self.multiworld.state.prog_items[player]["Chapter Completed"] = 1
self.multiworld.state.prog_items[player]["rep"] = 180
access_cache = build_access_cache(self.multiworld.state, player, 2, False, True)
# brink terminal
self.assertEqual(88, spots_s_glitched(self.multiworld.state, player, False, access_cache))
self.assertEqual(120, spots_m_glitched(self.multiworld.state, player, False, access_cache))
self.assertEqual(106, spots_l_glitched(self.multiworld.state, player, False, access_cache))
self.assertEqual(58, spots_xl_glitched(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["Chapter Completed"] = 2
access_cache = build_access_cache(self.multiworld.state, player, 2, False, True)
# chapter 3
self.assertEqual(94, spots_s_glitched(self.multiworld.state, player, False, access_cache))
self.assertEqual(123, spots_m_glitched(self.multiworld.state, player, False, access_cache))
self.assertEqual(110, spots_l_glitched(self.multiworld.state, player, False, access_cache))
self.assertEqual(61, spots_xl_glitched(self.multiworld.state, player, False, access_cache))
self.multiworld.state.prog_items[player]["Chapter Completed"] = 3
access_cache = build_access_cache(self.multiworld.state, player, 2, False, True)
# chapter 4
self.assertEqual(111, spots_l_glitched(self.multiworld.state, player, False, access_cache))
self.assertEqual(62, spots_xl_glitched(self.multiworld.state, player, False, access_cache))

View File

@@ -0,0 +1,29 @@
from . import BombRushCyberfunkTestBase
class TestRegularGraffitiGlitchless(BombRushCyberfunkTestBase):
options = {
"logic": "glitchless",
"limited_graffiti": False
}
class TestLimitedGraffitiGlitchless(BombRushCyberfunkTestBase):
options = {
"logic": "glitchless",
"limited_graffiti": True
}
class TestRegularGraffitiGlitched(BombRushCyberfunkTestBase):
options = {
"logic": "glitched",
"limited_graffiti": False
}
class TestLimitedGraffitiGlitched(BombRushCyberfunkTestBase):
options = {
"logic": "glitched",
"limited_graffiti": True
}

View File

@@ -0,0 +1,45 @@
from . import BombRushCyberfunkTestBase
from typing import List
rep_item_names: List[str] = [
"8 REP",
"16 REP",
"24 REP",
"32 REP",
"48 REP"
]
class TestCollectAndRemoveREP(BombRushCyberfunkTestBase):
@property
def run_default_tests(self) -> bool:
return False
def test_default_rep_total(self) -> None:
self.collect_by_name(rep_item_names)
self.assertEqual(1400, self.multiworld.state.prog_items[self.player]["rep"])
new_total = 1400
if self.count("8 REP") > 0:
new_total -= 8
self.remove(self.get_item_by_name("8 REP"))
if self.count("16 REP") > 0:
new_total -= 16
self.remove(self.get_item_by_name("16 REP"))
if self.count("24 REP") > 0:
new_total -= 24
self.remove(self.get_item_by_name("24 REP"))
if self.count("32 REP") > 0:
new_total -= 32
self.remove(self.get_item_by_name("32 REP"))
if self.count("48 REP") > 0:
new_total -= 48
self.remove(self.get_item_by_name("48 REP"))
self.assertEqual(new_total, self.multiworld.state.prog_items[self.player]["rep"])

View File

@@ -31,7 +31,7 @@ def cv64_string_to_bytearray(cv64text: str, a_advance: bool = False, append_end:
if char in cv64_char_dict:
text_bytes.extend([0x00, cv64_char_dict[char][0]])
else:
text_bytes.extend([0x00, 0x41])
text_bytes.extend([0x00, 0x21])
if a_advance:
text_bytes.extend([0xA3, 0x00])
@@ -45,7 +45,10 @@ def cv64_text_truncate(cv64text: str, textbox_len_limit: int) -> str:
line_len = 0
for i in range(len(cv64text)):
line_len += cv64_char_dict[cv64text[i]][1]
if cv64text[i] in cv64_char_dict:
line_len += cv64_char_dict[cv64text[i]][1]
else:
line_len += 5
if line_len > textbox_len_limit:
return cv64text[0x00:i]

View File

@@ -1,6 +1,6 @@
from typing import Dict
from dataclasses import dataclass
from Options import OptionDict
from Options import OptionDict, PerGameCommonOptions
class Locations(OptionDict):
@@ -18,8 +18,8 @@ class Rules(OptionDict):
display_name = "rules"
ff1_options: Dict[str, OptionDict] = {
"locations": Locations,
"items": Items,
"rules": Rules
}
@dataclass
class FF1Options(PerGameCommonOptions):
locations: Locations
items: Items
rules: Rules

View File

@@ -5,7 +5,7 @@ from typing import Dict
from BaseClasses import Item, Location, MultiWorld, Tutorial, ItemClassification
from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST, FF1_BRIDGE
from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT
from .Options import ff1_options
from .Options import FF1Options
from ..AutoWorld import World, WebWorld
@@ -34,7 +34,8 @@ class FF1World(World):
Part puzzle and part speed-run, it breathes new life into one of the most influential games ever made.
"""
option_definitions = ff1_options
options: FF1Options
options_dataclass = FF1Options
settings: typing.ClassVar[FF1Settings]
settings_key = "ffr_options"
game = "Final Fantasy"
@@ -58,20 +59,20 @@ class FF1World(World):
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
# Fail generation if there are no items in the pool
for player in multiworld.get_game_players(cls.game):
options = get_options(multiworld, 'items', player)
assert options,\
items = multiworld.worlds[player].options.items.value
assert items, \
f"FFR settings submitted with no key items ({multiworld.get_player_name(player)}). Please ensure you " \
f"generated the settings using finalfantasyrandomizer.com AND enabled the AP flag"
def create_regions(self):
locations = get_options(self.multiworld, 'locations', self.player)
rules = get_options(self.multiworld, 'rules', self.player)
locations = self.options.locations.value
rules = self.options.rules.value
menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules, self.multiworld)
terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region)
terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player)
terminated_event.place_locked_item(terminated_item)
items = get_options(self.multiworld, 'items', self.player)
items = self.options.items.value
goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]],
self.player)
terminated_event.access_rule = goal_rule
@@ -93,7 +94,7 @@ class FF1World(World):
self.multiworld.completion_condition[self.player] = lambda state: state.has(CHAOS_TERMINATED_EVENT, self.player)
def create_items(self):
items = get_options(self.multiworld, 'items', self.player)
items = self.options.items.value
if FF1_BRIDGE in items.keys():
self._place_locked_item_in_sphere0(FF1_BRIDGE)
if items:
@@ -109,7 +110,7 @@ class FF1World(World):
def _place_locked_item_in_sphere0(self, progression_item: str):
if progression_item:
rules = get_options(self.multiworld, 'rules', self.player)
rules = self.options.rules.value
sphere_0_locations = [name for name, rules in rules.items()
if rules and len(rules[0]) == 0 and name not in self.locked_locations]
if sphere_0_locations:
@@ -126,7 +127,3 @@ class FF1World(World):
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(["Heal", "Pure", "Soft", "Tent", "Cabin", "House"])
def get_options(world: MultiWorld, name: str, player: int):
return getattr(world, name, None)[player].value

View File

@@ -132,9 +132,10 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
the location without using any hint points.
* `start_location_hints` is the same as `start_hints` but for locations, allowing you to hint for the item contained
there without using any hint points.
* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk"
item which isn't necessary for progression to go in these locations.
* `priority_locations` is the inverse of `exclude_locations`, forcing a progression item in the defined locations.
* `exclude_locations` lets you define any locations that you don't want to do and forces a filler or trap item which
isn't necessary for progression into these locations.
* `priority_locations` lets you define any locations that you want to do and forces a progression item into these
locations.
* `item_links` allows players to link their items into a group with the same item link name and game. The items declared
in `item_pool` get combined and when an item is found for the group, all players in the group receive it. Item links
can also have local and non local items, forcing the items to either be placed within the worlds of the group or in

View File

@@ -1,5 +1,5 @@
from dataclasses import dataclass
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions
from Options import Choice, Removed, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions
class PartyShuffle(Toggle):
"""Shuffles party members into the pool.
@@ -18,10 +18,22 @@ 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 StartLocation(Choice):
"""Select the starting location from 1 of 4 positions."""
display_name = "Start Location"
option_waynehouse = 0
option_viewaxs_edifice = 1
option_tv_island = 2
option_shield_facility = 3
default = 0
@classmethod
def get_option_name(cls, value: int) -> str:
if value == 1:
return "Viewax's Edifice"
if value == 2:
return "TV Island"
return super().get_option_name(value)
class ExtraLogic(DefaultOnToggle):
"""Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult."""
@@ -37,6 +49,9 @@ class Hylics2Options(PerGameCommonOptions):
party_shuffle: PartyShuffle
gesture_shuffle: GestureShuffle
medallion_shuffle: MedallionShuffle
random_start: RandomStart
start_location: StartLocation
extra_items_in_logic: ExtraLogic
death_link: Hylics2DeathLink
death_link: Hylics2DeathLink
# Removed options
random_start: Removed

View File

@@ -132,8 +132,7 @@ def set_rules(hylics2world):
extra = hylics2world.options.extra_items_in_logic
party = hylics2world.options.party_shuffle
medallion = hylics2world.options.medallion_shuffle
random_start = hylics2world.options.random_start
start_location = hylics2world.start_location
start_location = hylics2world.options.start_location
# Afterlife
add_rule(world.get_location("Afterlife: TV", player),
@@ -499,7 +498,7 @@ def set_rules(hylics2world):
add_rule(i, lambda state: enter_hylemxylem(state, player))
# random start logic (default)
if not random_start or random_start and start_location == "Waynehouse":
if start_location == "waynehouse":
# entrances
for i in world.get_region("Viewax", player).entrances:
add_rule(i, lambda state: (
@@ -514,7 +513,7 @@ def set_rules(hylics2world):
add_rule(i, lambda state: airship(state, player))
# random start logic (Viewax's Edifice)
elif random_start and start_location == "Viewax's Edifice":
elif start_location == "viewaxs_edifice":
for i in world.get_region("Waynehouse", player).entrances:
add_rule(i, lambda state: (
air_dash(state, player)
@@ -544,8 +543,8 @@ def set_rules(hylics2world):
for i in world.get_region("Sage Labyrinth", player).entrances:
add_rule(i, lambda state: airship(state, player))
# random start logic (TV Island)
elif random_start and start_location == "TV Island":
# start logic (TV Island)
elif start_location == "tv_island":
for i in world.get_region("Waynehouse", player).entrances:
add_rule(i, lambda state: airship(state, player))
for i in world.get_region("New Muldul", player).entrances:
@@ -563,8 +562,8 @@ def set_rules(hylics2world):
for i in world.get_region("Sage Labyrinth", player).entrances:
add_rule(i, lambda state: airship(state, player))
# random start logic (Shield Facility)
elif random_start and start_location == "Shield Facility":
# start logic (Shield Facility)
elif start_location == "shield_facility":
for i in world.get_region("Waynehouse", player).entrances:
add_rule(i, lambda state: airship(state, player))
for i in world.get_region("New Muldul", player).entrances:
@@ -578,4 +577,4 @@ def set_rules(hylics2world):
for i in world.get_region("TV Island", player).entrances:
add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Sage Labyrinth", player).entrances:
add_rule(i, lambda state: airship(state, player))
add_rule(i, lambda state: airship(state, player))

View File

@@ -39,8 +39,6 @@ class Hylics2World(World):
data_version = 3
start_location = "Waynehouse"
def set_rules(self):
Rules.set_rules(self)
@@ -56,19 +54,6 @@ class Hylics2World(World):
return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player)
# set random starting location if option is enabled
def generate_early(self):
if self.options.random_start:
i = self.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 create_items(self):
# create item pool
pool = []
@@ -149,8 +134,8 @@ class Hylics2World(World):
slot_data: Dict[str, Any] = {
"party_shuffle": self.options.party_shuffle.value,
"medallion_shuffle": self.options.medallion_shuffle.value,
"random_start" : self.options.random_start.value,
"start_location" : self.start_location,
"random_start": int(self.options.start_location != "waynehouse"),
"start_location" : self.options.start_location.current_option_name,
"death_link": self.options.death_link.value
}
return slot_data
@@ -189,14 +174,14 @@ class Hylics2World(World):
# create entrance and connect it to parent and destination regions
ent = Entrance(self.player, f"{reg.name} {k}", reg)
reg.exits.append(ent)
if k == "New Game" and self.options.random_start:
if self.start_location == "Waynehouse":
if k == "New Game":
if self.options.start_location == "waynehouse":
ent.connect(region_table[2])
elif self.start_location == "Viewax's Edifice":
elif self.options.start_location == "viewaxs_edifice":
ent.connect(region_table[6])
elif self.start_location == "TV Island":
elif self.options.start_location == "tv_island":
ent.connect(region_table[9])
elif self.start_location == "Shield Facility":
elif self.options.start_location == "shield_facility":
ent.connect(region_table[11])
else:
for name, num in Exits.exit_lookup_table.items():

View File

@@ -155,7 +155,8 @@ class MessengerWorld(World):
self.starting_portals.append("Searing Crags Portal")
portals_to_strip = [portal for portal in ["Riviere Turquoise Portal", "Sunken Shrine Portal"]
if portal in self.starting_portals]
self.starting_portals.remove(self.random.choice(portals_to_strip))
if portals_to_strip:
self.starting_portals.remove(self.random.choice(portals_to_strip))
self.filler = FILLER.copy()
if self.options.traps:

View File

@@ -4,6 +4,10 @@ from ..portals import PORTALS
class PortalTestBase(MessengerTestBase):
options = {
"available_portals": 3,
}
def test_portal_reqs(self) -> None:
"""tests the paths to open a portal if only that portal is closed with vanilla connections."""
# portal and requirements to reach it if it's the only closed portal

View File

@@ -1,5 +1,6 @@
import typing
from Options import Choice, Option, Range
from dataclasses import dataclass
from Options import Choice, Option, Range, PerGameCommonOptions
class IncomeBoost(Range):
@@ -30,9 +31,8 @@ class CommanderChoice(Choice):
option_unlockable_factions = 1
option_random_starting_faction = 2
wargroove_options: typing.Dict[str, type(Option)] = {
"income_boost": IncomeBoost,
"commander_defense_boost": CommanderDefenseBoost,
"commander_choice": CommanderChoice
}
@dataclass
class WargrooveOptions(PerGameCommonOptions):
income_boost: IncomeBoost
commander_defense_boost: CommanderDefenseBoost
commander_choice: CommanderChoice

View File

@@ -7,8 +7,8 @@ from .Items import item_table, faction_table
from .Locations import location_table
from .Regions import create_regions
from .Rules import set_rules
from ..AutoWorld import World, WebWorld
from .Options import wargroove_options
from worlds.AutoWorld import World, WebWorld
from .Options import WargrooveOptions
class WargrooveSettings(settings.Group):
@@ -38,11 +38,11 @@ class WargrooveWorld(World):
Command an army, in this retro style turn based strategy game!
"""
option_definitions = wargroove_options
options: WargrooveOptions
options_dataclass = WargrooveOptions
settings: typing.ClassVar[WargrooveSettings]
game = "Wargroove"
topology_present = True
data_version = 1
web = WargrooveWeb()
item_name_to_id = {name: data.code for name, data in item_table.items()}
@@ -50,16 +50,17 @@ class WargrooveWorld(World):
def _get_slot_data(self):
return {
'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)),
'income_boost': self.multiworld.income_boost[self.player],
'commander_defense_boost': self.multiworld.commander_defense_boost[self.player],
'can_choose_commander': self.multiworld.commander_choice[self.player] != 0,
'seed': "".join(self.random.choice(string.ascii_letters) for i in range(16)),
'income_boost': self.options.income_boost.value,
'commander_defense_boost': self.options.commander_defense_boost.value,
'can_choose_commander': self.options.commander_choice.value != 0,
'commander_choice': self.options.commander_choice.value,
'starting_groove_multiplier': 20 # Backwards compatibility in case this ever becomes an option
}
def generate_early(self):
# Selecting a random starting faction
if self.multiworld.commander_choice[self.player] == 2:
if self.options.commander_choice.value == 2:
factions = [faction for faction in faction_table.keys() if faction != "Starter"]
starting_faction = WargrooveItem(self.multiworld.random.choice(factions) + ' Commanders', self.player)
self.multiworld.push_precollected(starting_faction)
@@ -68,7 +69,7 @@ class WargrooveWorld(World):
# Fill out our pool with our items from the item table
pool = []
precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]}
ignore_faction_items = self.multiworld.commander_choice[self.player] == 0
ignore_faction_items = self.options.commander_choice.value == 0
for name, data in item_table.items():
if data.code is not None and name not in precollected_item_names and not data.classification == ItemClassification.filler:
if name.endswith(' Commanders') and ignore_faction_items:
@@ -105,9 +106,6 @@ class WargrooveWorld(World):
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
for option_name in wargroove_options:
option = getattr(self.multiworld, option_name)[self.player]
slot_data[option_name] = int(option.value)
return slot_data
def get_filler_item_name(self) -> str:

View File

@@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, cast
from BaseClasses import CollectionState, Entrance, Location, Region, Tutorial
from Options import PerGameCommonOptions, Toggle
from Options import OptionError, PerGameCommonOptions, Toggle
from worlds.AutoWorld import WebWorld, World
from .data import static_items as static_witness_items
@@ -124,9 +124,9 @@ class WitnessWorld(World):
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression"
f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.")
elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1:
raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough"
f" progression items that can be placed in other players' worlds. Please turn on Symbol"
f" Shuffle, Door Shuffle, or Obelisk Keys.")
raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough"
f" progression items that can be placed in other players' worlds. Please turn on Symbol"
f" Shuffle, Door Shuffle, or Obelisk Keys.")
def generate_early(self) -> None:
disabled_locations = self.options.exclude_locations.value

View File

@@ -482,7 +482,7 @@ Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128
158207 - 0x03713 (Laser Shortcut Panel) - True - True
Door - 0x0364E (Laser Shortcut) - 0x03713
158208 - 0x00B10 (Entry Left) - True - True
158209 - 0x00C92 (Entry Right) - True - True
158209 - 0x00C92 (Entry Right) - 0x00B10 - True
Door - 0x0C128 (Entry Inner) - 0x00B10
Door - 0x0C153 (Entry Outer) - 0x00C92
158210 - 0x00290 (Outside 1) - 0x09D9B - True
@@ -1033,7 +1033,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near -
158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Colored Squares & Stars + Same Colored Symbol
158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol
158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry & Colored Dots
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Symmetry & Colored Dots
Door - 0x09FFB (Staircase Near) - 0x09FD8
Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD:

View File

@@ -482,7 +482,7 @@ Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128
158207 - 0x03713 (Laser Shortcut Panel) - True - True
Door - 0x0364E (Laser Shortcut) - 0x03713
158208 - 0x00B10 (Entry Left) - True - True
158209 - 0x00C92 (Entry Right) - True - True
158209 - 0x00C92 (Entry Right) - 0x00B10 - True
Door - 0x0C128 (Entry Inner) - 0x00B10
Door - 0x0C153 (Entry Outer) - 0x00C92
158210 - 0x00290 (Outside 1) - 0x09D9B - True

View File

@@ -482,7 +482,7 @@ Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128
158207 - 0x03713 (Laser Shortcut Panel) - True - True
Door - 0x0364E (Laser Shortcut) - 0x03713
158208 - 0x00B10 (Entry Left) - True - True
158209 - 0x00C92 (Entry Right) - True - True
158209 - 0x00C92 (Entry Right) - 0x00B10 - True
Door - 0x0C128 (Entry Inner) - 0x00B10
Door - 0x0C153 (Entry Outer) - 0x00C92
158210 - 0x00290 (Outside 1) - 0x09D9B - True

454
worlds/yugioh06/__init__.py Normal file
View File

@@ -0,0 +1,454 @@
import os
import pkgutil
from typing import Any, ClassVar, Dict, List
import settings
from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
import Utils
from worlds.AutoWorld import WebWorld, World
from .boosterpacks import booster_contents as booster_contents
from .boosterpacks import get_booster_locations
from .items import (
Banlist_Items,
booster_packs,
draft_boosters,
draft_opponents,
excluded_items,
item_to_index,
tier_1_opponents,
useful,
)
from .items import (
challenges as challenges,
)
from .locations import (
Bonuses,
Campaign_Opponents,
Limited_Duels,
Required_Cards,
Theme_Duels,
collection_events,
get_beat_challenge_events,
special,
)
from .logic import core_booster, yugioh06_difficulty
from .opponents import OpponentData, get_opponent_condition, get_opponent_locations, get_opponents
from .opponents import challenge_opponents as challenge_opponents
from .options import Yugioh06Options
from .rom import MD5America, MD5Europe, YGO06ProcedurePatch, write_tokens
from .rom import get_base_rom_path as get_base_rom_path
from .rom_values import banlist_ids as banlist_ids
from .rom_values import function_addresses as function_addresses
from .rom_values import structure_deck_selection as structure_deck_selection
from .rules import set_rules
from .structure_deck import get_deck_content_locations
from .client_bh import YuGiOh2006Client
class Yugioh06Web(WebWorld):
theme = "stone"
setup = Tutorial(
"Multiworld Setup Tutorial",
"A guide to setting up Yu-Gi-Oh! - Ultimate Masters Edition - World Championship Tournament 2006 "
"for Archipelago on your computer.",
"English",
"docs/setup_en.md",
"setup/en",
["Rensen"],
)
tutorials = [setup]
class Yugioh2006Setting(settings.Group):
class Yugioh2006RomFile(settings.UserFilePath):
"""File name of your Yu-Gi-Oh 2006 ROM"""
description = "Yu-Gi-Oh 2006 ROM File"
copy_to = "YuGiOh06.gba"
md5s = [MD5Europe, MD5America]
rom_file: Yugioh2006RomFile = Yugioh2006RomFile(Yugioh2006RomFile.copy_to)
class Yugioh06World(World):
"""
Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 is the definitive Yu-Gi-Oh
simulator on the GBA. Featuring over 2000 cards and over 90 Challenges.
"""
game = "Yu-Gi-Oh! 2006"
web = Yugioh06Web()
options: Yugioh06Options
options_dataclass = Yugioh06Options
settings_key = "yugioh06_settings"
settings: ClassVar[Yugioh2006Setting]
item_name_to_id = {}
start_id = 5730000
for k, v in item_to_index.items():
item_name_to_id[k] = v + start_id
location_name_to_id = {}
for k, v in Bonuses.items():
location_name_to_id[k] = v + start_id
for k, v in Limited_Duels.items():
location_name_to_id[k] = v + start_id
for k, v in Theme_Duels.items():
location_name_to_id[k] = v + start_id
for k, v in Campaign_Opponents.items():
location_name_to_id[k] = v + start_id
for k, v in special.items():
location_name_to_id[k] = v + start_id
for k, v in Required_Cards.items():
location_name_to_id[k] = v + start_id
item_name_groups = {
"Core Booster": core_booster,
"Campaign Boss Beaten": ["Tier 1 Beaten", "Tier 2 Beaten", "Tier 3 Beaten", "Tier 4 Beaten", "Tier 5 Beaten"],
}
removed_challenges: List[str]
starting_booster: str
starting_opponent: str
campaign_opponents: List[OpponentData]
is_draft_mode: bool
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
def generate_early(self):
self.starting_opponent = ""
self.starting_booster = ""
self.removed_challenges = []
# Universal tracker stuff, shouldn't do anything in standard gen
if hasattr(self.multiworld, "re_gen_passthrough"):
if "Yu-Gi-Oh! 2006" in self.multiworld.re_gen_passthrough:
# bypassing random yaml settings
slot_data = self.multiworld.re_gen_passthrough["Yu-Gi-Oh! 2006"]
self.options.structure_deck.value = slot_data["structure_deck"]
self.options.banlist.value = slot_data["banlist"]
self.options.final_campaign_boss_unlock_condition.value = slot_data[
"final_campaign_boss_unlock_condition"
]
self.options.fourth_tier_5_campaign_boss_unlock_condition.value = slot_data[
"fourth_tier_5_campaign_boss_unlock_condition"
]
self.options.third_tier_5_campaign_boss_unlock_condition.value = slot_data[
"third_tier_5_campaign_boss_unlock_condition"
]
self.options.final_campaign_boss_challenges.value = slot_data["final_campaign_boss_challenges"]
self.options.fourth_tier_5_campaign_boss_challenges.value = slot_data[
"fourth_tier_5_campaign_boss_challenges"
]
self.options.third_tier_5_campaign_boss_challenges.value = slot_data[
"third_tier_5_campaign_boss_challenges"
]
self.options.final_campaign_boss_campaign_opponents.value = slot_data[
"final_campaign_boss_campaign_opponents"
]
self.options.fourth_tier_5_campaign_boss_campaign_opponents.value = slot_data[
"fourth_tier_5_campaign_boss_campaign_opponents"
]
self.options.third_tier_5_campaign_boss_campaign_opponents.value = slot_data[
"third_tier_5_campaign_boss_campaign_opponents"
]
self.options.number_of_challenges.value = slot_data["number_of_challenges"]
self.removed_challenges = slot_data["removed challenges"]
self.starting_booster = slot_data["starting_booster"]
self.starting_opponent = slot_data["starting_opponent"]
if self.options.structure_deck.current_key == "none":
self.is_draft_mode = True
boosters = draft_boosters
if self.options.campaign_opponents_shuffle.value:
opponents = tier_1_opponents
else:
opponents = draft_opponents
else:
self.is_draft_mode = False
boosters = booster_packs
opponents = tier_1_opponents
if self.options.structure_deck.current_key == "random_deck":
self.options.structure_deck.value = self.random.randint(0, 5)
for item in self.options.start_inventory:
if item in opponents:
self.starting_opponent = item
if item in boosters:
self.starting_booster = item
if not self.starting_opponent:
self.starting_opponent = self.random.choice(opponents)
self.multiworld.push_precollected(self.create_item(self.starting_opponent))
if not self.starting_booster:
self.starting_booster = self.random.choice(boosters)
self.multiworld.push_precollected(self.create_item(self.starting_booster))
banlist = self.options.banlist.value
self.multiworld.push_precollected(self.create_item(Banlist_Items[banlist]))
if not self.removed_challenges:
challenge = list((Limited_Duels | Theme_Duels).keys())
noc = len(challenge) - max(
self.options.third_tier_5_campaign_boss_challenges.value
if self.options.third_tier_5_campaign_boss_unlock_condition == "challenges"
else 0,
self.options.fourth_tier_5_campaign_boss_challenges.value
if self.options.fourth_tier_5_campaign_boss_unlock_condition == "challenges"
else 0,
self.options.final_campaign_boss_challenges.value
if self.options.final_campaign_boss_unlock_condition == "challenges"
else 0,
self.options.number_of_challenges.value,
)
self.random.shuffle(challenge)
excluded = self.options.exclude_locations.value.intersection(challenge)
prio = self.options.priority_locations.value.intersection(challenge)
normal = [e for e in challenge if e not in excluded and e not in prio]
total = list(excluded) + normal + list(prio)
self.removed_challenges = total[:noc]
self.campaign_opponents = get_opponents(
self.multiworld, self.player, self.options.campaign_opponents_shuffle.value
)
def create_region(self, name: str, locations=None, exits=None):
region = Region(name, self.player, self.multiworld)
if locations:
for location_name, lid in locations.items():
if lid is not None and isinstance(lid, int):
lid = self.location_name_to_id[location_name]
else:
lid = None
location = Yugioh2006Location(self.player, location_name, lid, region)
region.locations.append(location)
if exits:
for _exit in exits:
region.exits.append(Entrance(self.player, _exit, region))
return region
def create_regions(self):
structure_deck = self.options.structure_deck.current_key
self.multiworld.regions += [
self.create_region("Menu", None, ["to Deck Edit", "to Campaign", "to Challenges", "to Card Shop"]),
self.create_region("Campaign", Bonuses | Campaign_Opponents),
self.create_region("Challenges"),
self.create_region("Card Shop", Required_Cards | collection_events),
self.create_region("Structure Deck", get_deck_content_locations(structure_deck)),
]
self.get_entrance("to Campaign").connect(self.get_region("Campaign"))
self.get_entrance("to Challenges").connect(self.get_region("Challenges"))
self.get_entrance("to Card Shop").connect(self.get_region("Card Shop"))
self.get_entrance("to Deck Edit").connect(self.get_region("Structure Deck"))
campaign = self.get_region("Campaign")
# Campaign Opponents
for opponent in self.campaign_opponents:
unlock_item = "Campaign Tier " + str(opponent.tier) + " Column " + str(opponent.column)
region = self.create_region(opponent.name, get_opponent_locations(opponent))
entrance = Entrance(self.player, unlock_item, campaign)
if opponent.tier == 5 and opponent.column > 2:
unlock_amount = 0
is_challenge = True
if opponent.column == 3:
if self.options.third_tier_5_campaign_boss_unlock_condition.value == 1:
unlock_item = "Challenge Beaten"
unlock_amount = self.options.third_tier_5_campaign_boss_challenges.value
is_challenge = True
else:
unlock_item = "Campaign Boss Beaten"
unlock_amount = self.options.third_tier_5_campaign_boss_campaign_opponents.value
is_challenge = False
if opponent.column == 4:
if self.options.fourth_tier_5_campaign_boss_unlock_condition.value == 1:
unlock_item = "Challenge Beaten"
unlock_amount = self.options.fourth_tier_5_campaign_boss_challenges.value
is_challenge = True
else:
unlock_item = "Campaign Boss Beaten"
unlock_amount = self.options.fourth_tier_5_campaign_boss_campaign_opponents.value
is_challenge = False
if opponent.column == 5:
if self.options.final_campaign_boss_unlock_condition.value == 1:
unlock_item = "Challenge Beaten"
unlock_amount = self.options.final_campaign_boss_challenges.value
is_challenge = True
else:
unlock_item = "Campaign Boss Beaten"
unlock_amount = self.options.final_campaign_boss_campaign_opponents.value
is_challenge = False
entrance.access_rule = get_opponent_condition(
opponent, unlock_item, unlock_amount, self.player, is_challenge
)
else:
entrance.access_rule = lambda state, unlock=unlock_item, opp=opponent: state.has(
unlock, self.player
) and yugioh06_difficulty(state, self.player, opp.difficulty)
campaign.exits.append(entrance)
entrance.connect(region)
self.multiworld.regions.append(region)
card_shop = self.get_region("Card Shop")
# Booster Contents
for booster in booster_packs:
region = self.create_region(booster, get_booster_locations(booster))
entrance = Entrance(self.player, booster, card_shop)
entrance.access_rule = lambda state, unlock=booster: state.has(unlock, self.player)
card_shop.exits.append(entrance)
entrance.connect(region)
self.multiworld.regions.append(region)
challenge_region = self.get_region("Challenges")
# Challenges
for challenge, lid in (Limited_Duels | Theme_Duels).items():
if challenge in self.removed_challenges:
continue
region = self.create_region(challenge, {challenge: lid, challenge + " Complete": None})
entrance = Entrance(self.player, challenge, challenge_region)
entrance.access_rule = lambda state, unlock=challenge: state.has(unlock + " Unlock", self.player)
challenge_region.exits.append(entrance)
entrance.connect(region)
self.multiworld.regions.append(region)
def create_item(self, name: str) -> Item:
classification: ItemClassification = ItemClassification.progression
if name == "5000DP":
classification = ItemClassification.filler
if name in useful:
classification = ItemClassification.useful
return Item(name, classification, self.item_name_to_id[name], self.player)
def create_filler(self) -> Item:
return self.create_item("5000DP")
def get_filler_item_name(self) -> str:
return "5000DP"
def create_items(self):
start_inventory = self.options.start_inventory.value.copy()
item_pool = []
items = item_to_index.copy()
starting_list = Banlist_Items[self.options.banlist.value]
if not self.options.add_empty_banlist.value and starting_list != "No Banlist":
items.pop("No Banlist")
for rc in self.removed_challenges:
items.pop(rc + " Unlock")
items.pop(self.starting_opponent)
items.pop(self.starting_booster)
items.pop(starting_list)
for name in items:
if name in excluded_items or name in start_inventory:
continue
item = self.create_item(name)
item_pool.append(item)
needed_item_pool_size = sum(loc not in self.removed_challenges for loc in self.location_name_to_id)
needed_filler_amount = needed_item_pool_size - len(item_pool)
item_pool += [self.create_item("5000DP") for _ in range(needed_filler_amount)]
self.multiworld.itempool += item_pool
for challenge in get_beat_challenge_events(self):
item = Yugioh2006Item("Challenge Beaten", ItemClassification.progression, None, self.player)
location = self.multiworld.get_location(challenge, self.player)
location.place_locked_item(item)
for opponent in self.campaign_opponents:
for location_name, event in get_opponent_locations(opponent).items():
if event is not None and not isinstance(event, int):
item = Yugioh2006Item(event, ItemClassification.progression, None, self.player)
location = self.multiworld.get_location(location_name, self.player)
location.place_locked_item(item)
for booster in booster_packs:
for location_name, content in get_booster_locations(booster).items():
item = Yugioh2006Item(content, ItemClassification.progression, None, self.player)
location = self.multiworld.get_location(location_name, self.player)
location.place_locked_item(item)
structure_deck = self.options.structure_deck.current_key
for location_name, content in get_deck_content_locations(structure_deck).items():
item = Yugioh2006Item(content, ItemClassification.progression, None, self.player)
location = self.multiworld.get_location(location_name, self.player)
location.place_locked_item(item)
for event in collection_events:
item = Yugioh2006Item(event, ItemClassification.progression, None, self.player)
location = self.multiworld.get_location(event, self.player)
location.place_locked_item(item)
def set_rules(self):
set_rules(self)
def generate_output(self, output_directory: str):
outfilepname = f"_P{self.player}"
outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}"
self.rom_name_text = f'YGO06{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0'
self.romName = bytearray(self.rom_name_text, "utf8")[:0x20]
self.romName.extend([0] * (0x20 - len(self.romName)))
self.rom_name = self.romName
self.playerName = bytearray(self.multiworld.player_name[self.player], "utf8")[:0x20]
self.playerName.extend([0] * (0x20 - len(self.playerName)))
patch = YGO06ProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "patch.bsdiff4"))
if self.is_draft_mode:
patch.procedure.insert(1, ("apply_bsdiff4", ["draft_patch.bsdiff4"]))
patch.write_file("draft_patch.bsdiff4", pkgutil.get_data(__name__, "patches/draft.bsdiff4"))
if self.options.ocg_arts:
patch.procedure.insert(1, ("apply_bsdiff4", ["ocg_patch.bsdiff4"]))
patch.write_file("ocg_patch.bsdiff4", pkgutil.get_data(__name__, "patches/ocg.bsdiff4"))
write_tokens(self, patch)
# Write Output
out_file_name = self.multiworld.get_out_file_name_base(self.player)
patch.write(os.path.join(output_directory, f"{out_file_name}{patch.patch_file_ending}"))
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {
"structure_deck": self.options.structure_deck.value,
"banlist": self.options.banlist.value,
"final_campaign_boss_unlock_condition": self.options.final_campaign_boss_unlock_condition.value,
"fourth_tier_5_campaign_boss_unlock_condition":
self.options.fourth_tier_5_campaign_boss_unlock_condition.value,
"third_tier_5_campaign_boss_unlock_condition":
self.options.third_tier_5_campaign_boss_unlock_condition.value,
"final_campaign_boss_challenges": self.options.final_campaign_boss_challenges.value,
"fourth_tier_5_campaign_boss_challenges":
self.options.fourth_tier_5_campaign_boss_challenges.value,
"third_tier_5_campaign_boss_challenges":
self.options.third_tier_5_campaign_boss_campaign_opponents.value,
"final_campaign_boss_campaign_opponents":
self.options.final_campaign_boss_campaign_opponents.value,
"fourth_tier_5_campaign_boss_campaign_opponents":
self.options.fourth_tier_5_campaign_boss_unlock_condition.value,
"third_tier_5_campaign_boss_campaign_opponents":
self.options.third_tier_5_campaign_boss_campaign_opponents.value,
"number_of_challenges": self.options.number_of_challenges.value,
}
slot_data["removed challenges"] = self.removed_challenges
slot_data["starting_booster"] = self.starting_booster
slot_data["starting_opponent"] = self.starting_opponent
return slot_data
# for the universal tracker, doesn't get called in standard gen
@staticmethod
def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]:
# returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough
return slot_data
class Yugioh2006Item(Item):
game: str = "Yu-Gi-Oh! 2006"
class Yugioh2006Location(Location):
game: str = "Yu-Gi-Oh! 2006"

View File

@@ -0,0 +1,923 @@
from typing import Dict, Set
booster_contents: Dict[str, Set[str]] = {
"LEGEND OF B.E.W.D.": {
"Exodia",
"Dark Magician",
"Polymerization",
"Skull Servant"
},
"METAL RAIDERS": {
"Petit Moth",
"Cocoon of Evolution",
"Time Wizard",
"Gate Guardian",
"Kazejin",
"Suijin",
"Sanga of the Thunder",
"Sangan",
"Castle of Dark Illusions",
"Soul Release",
"Magician of Faith",
"Dark Elf",
"Summoned Skull",
"Sangan",
"7 Colored Fish",
"Tribute to the Doomed",
"Horn of Heaven",
"Magic Jammer",
"Seven Tools of the Bandit",
"Solemn Judgment",
"Dream Clown",
"Heavy Storm"
},
"PHARAOH'S SERVANT": {
"Beast of Talwar",
"Jinzo",
"Gearfried the Iron Knight",
"Harpie's Brother",
"Gravity Bind",
"Solemn Wishes",
"Kiseitai",
"Morphing Jar #2",
"The Shallow Grave",
"Nobleman of Crossout",
"Magic Drain"
},
"PHARAONIC GUARDIAN": {
"Don Zaloog",
"Reasoning",
"Dark Snake Syndrome",
"Helpoemer",
"Newdoria",
"Spirit Reaper",
"Yomi Ship",
"Pyramid Turtle",
"Master Kyonshee",
"Book of Life",
"Call of the Mummy",
"Gravekeeper's Spy",
"Gravekeeper's Guard",
"A Cat of Ill Omen",
"Jowls of Dark Demise",
"Non Aggression Area",
"Terraforming",
"Des Lacooda",
"Swarm of Locusts",
"Swarm of Scarabs",
"Wandering Mummy",
"Royal Keeper",
"Book of Moon",
"Book of Taiyou",
"Dust Tornado",
"Raigeki Break"
},
"SPELL RULER": {
"Ritual",
"Messenger of Peace",
"Megamorph",
"Shining Angel",
"Mystic Tomato",
"Giant Rat",
"Mother Grizzly",
"UFO Turtle",
"Flying Kamakiri 1",
"Giant Germ",
"Nimble Momonga",
"Cyber Jar",
"Spear Cretin",
"Toon Mermaid",
"Toon Summoned Skull",
"Toon World",
"Rush Recklessly",
"The Reliable Guardian",
"Senju of the Thousand Hands",
"Sonic Bird",
"Mystical Space Typhoon"
},
"LABYRINTH OF NIGHTMARE": {
"Destiny Board",
"Spirit Message 'I'",
"Spirit Message 'N'",
"Spirit Message 'A'",
"Spirit Message 'L'",
"Fusion Gate",
"Jowgen the Spiritualist",
"Fairy Box",
"Aqua Spirit",
"Rock Spirit",
"Spirit of Flames",
"Garuda the Wind Spirit",
"Hysteric Fairy",
"Kycoo the Ghost Destroyer",
"Gemini Elf",
"Amphibian Beast",
"Revival Jam",
"Dancing Fairy",
"Cure Mermaid",
"The Last Warrior from Another Planet",
"United We Stand",
"Earthbound Spirit",
"The Masked Beast"
},
"LEGACY OF DARKNESS": {
"Last Turn",
"Yata-Garasu",
"Opticlops",
"Dark Ruler Ha Des",
"Exiled Force",
"Injection Fairy Lily",
"Spear Dragon",
"Luster Dragon #2",
"Twin-Headed Behemoth",
"Airknight Parshath",
"Freed the Matchless General",
"Marauding Captain",
"Reinforcement of the Army",
"Cave Dragon",
"Troop Dragon",
"Stamping Destruction",
"Creature Swap",
"Asura Priest",
"Fushi No Tori",
"Maharaghi",
"Susa Soldier",
"Emergency Provisions",
},
"MAGICIAN'S FORCE": {
"Huge Revolution",
"Oppressed People",
"United Resistance",
"People Running About",
"X-Head Cannon",
"Y-Dragon Head",
"Z-Metal Tank",
"XY-Dragon Cannon",
"XZ-Tank Cannon",
"YZ-Tank Dragon",
"XYZ-Dragon Cannon",
"Cliff the Trap Remover",
"Wave-Motion Cannon",
"Ritual",
"Magical Merchant",
"Poison of the Old Man",
"Chaos Command Magician",
"Skilled Dark Magician",
"Dark Blade",
"Great Angus",
"Luster Dragon",
"Breaker the magical Warrior",
"Old Vindictive Magician",
"Apprentice Magician",
"Burning Beast",
"Freezing Beast",
"Pitch-Dark Dragon",
"Giant Orc",
"Second Goblin",
"Decayed Commander",
"Zombie Tiger",
"Vampire Orchis",
"Des Dendle",
"Frontline Base",
"Formation Union",
"Pitch-Black Power Stone",
"Magical Marionette",
"Royal Magical Library",
"Spell Shield Type-8",
"Tribute Doll",
},
"DARK CRISIS": {
"Final Countdown",
"Ojama Green",
"Dark Scorpion Combination",
"Dark Scorpion - Chick the Yellow",
"Dark Scorpion - Meanae the Thorn",
"Dark Scorpion - Gorg the Strong",
"Ritual",
"Tsukuyomi",
"Ojama Trio",
"Kaiser Glider",
"D.D. Warrior Lady",
"Archfiend Soldier",
"Skull Archfiend of Lightning",
"Blindly Loyal Goblin",
"Gagagigo",
"Nin-Ken Dog",
"Zolga",
"Kelbek",
"Mudora",
"Cestus of Dagla",
"Vampire Lord",
"Metallizing Parasite - Lunatite",
"D. D. Trainer",
"Spell Reproduction",
"Contract with the Abyss",
"Dark Master - Zorc"
},
"INVASION OF CHAOS": {
"Ojama Delta Hurricane",
"Ojama Yellow",
"Ojama Black",
"Heart of the Underdog",
"Chaos Emperor Dragon - Envoy of the End",
"Self-Destruct Button",
"Manticore of Darkness",
"Dimension Fusion",
"Gigantes",
"Inferno",
"Silpheed",
"Mad Dog of Darkness",
"Ryu Kokki",
"Berserk Gorilla",
"Neo Bug",
"Dark Driceratops",
"Hyper Hammerhead",
"Sea Serpent Warrior of Darkness",
"Giga Gagagigo",
"Terrorking Salmon",
"Blazing Inpachi",
"Stealth Bird",
"Reload",
"Cursed Seal of the Forbidden Spell",
"Stray Lambs",
"Manju of the Ten Thousand Hands"
},
"ANCIENT SANCTUARY": {
"Monster Gate",
"Wall of Revealing Light",
"Mystik Wok",
"The Agent of Judgment - Saturn",
"Zaborg the Thunder Monarch",
"Regenerating Mummy",
"The End of Anubis",
"Solar Flare Dragon",
"Level Limit - Area B",
"King of the Swamp",
"Enemy Controller",
"Enchanting Fitting Room"
},
"SOUL OF THE DUELIST": {
"Ninja Grandmaster Sasuke",
"Mystic Swordsman LV2",
"Mystic Swordsman LV4",
"Enraged Muka Muka",
"Mobius the Frost Monarch",
"Horus the Black Flame Dragon LV6",
"Ultimate Baseball Kid",
"Armed Dragon LV3",
"Armed Dragon LV5",
"Masked Dragon",
"Element Dragon",
"Horus the Black Flame Dragon LV4",
"Level Up!",
"Howling Insect",
"Mobius the Frost Monarch"
},
"RISE OF DESTINY": {
"Homunculus the Alchemic Being",
"Thestalos the Firestorm Monarch",
"Roc from the Valley of Haze",
"Harpie Lady 1",
"Silent Swordsman Lv3",
"Mystic Swordsman LV6",
"Ultimate Insect Lv3",
"Divine Wrath",
"Serial Spell"
},
"FLAMING ETERNITY": {
"Insect Knight",
"Chiron the Mage",
"Granmarg the Rock Monarch",
"Silent Swordsman Lv5",
"The Dark - Hex-Sealed Fusion",
"The Earth - Hex-Sealed Fusion",
"The Light - Hex-Sealed Fusion",
"Ultimate Insect Lv5",
"Blast Magician",
"Golem Sentry",
"Rescue Cat",
"Blade Rabbit"
},
"THE LOST MILLENIUM": {
"Ritual",
"Megarock Dragon",
"D.D. Survivor",
"Hieracosphinx",
"Elemental Hero Flame Wingman",
"Elemental Hero Avian",
"Elemental Hero Burstinatrix",
"Elemental Hero Clayman",
"Elemental Hero Sparkman",
"Elemental Hero Thunder Giant",
"Aussa the Earth Charmer",
"Brain Control"
},
"CYBERNETIC REVOLUTION": {
"Power Bond",
"Cyber Dragon",
"Cyber Twin Dragon",
"Cybernetic Magician",
"Indomitable Fighter Lei Lei",
"Protective Soul Ailin",
"Miracle Fusion",
"Elemental Hero Bubbleman",
"Jerry Beans Man"
},
"ELEMENTAL ENERGY": {
"V-Tiger Jet",
"W-Wing Catapult",
"VW-Tiger Catapult",
"VWXYZ-Dragon Catapult Cannon",
"Zure, Knight of Dark World",
"Brron, Mad King of Dark World",
"Familiar-Possessed - Aussa",
"Familiar-Possessed - Eria",
"Familiar-Possessed - Hiita",
"Familiar-Possessed - Wynn",
"Oxygeddon",
"Roll Out!",
"Dark World Lightning",
"Elemental Hero Rampart Blaster",
"Elemental Hero Shining Flare Wingman",
"Elemental Hero Wildedge",
"Elemental Hero Wildheart",
"Elemental Hero Bladedge",
"Pot of Avarice",
"B.E.S. Tetran"
},
"SHADOW OF INFINITY": {
"Hamon, Lord of Striking Thunder",
"Raviel, Lord of Phantasms",
"Uria, Lord of Searing Flames",
"Ritual",
"Treeborn Frog",
"Saber Beetle",
"Tenkabito Shien",
"Princess Pikeru",
"Gokipon",
"Demise, King of Armageddon",
"Anteatereatingant"
},
"GAME GIFT COLLECTION": {
"Ritual",
"Valkyrion the Magna Warrior",
"Alpha the Magnet Warrior",
"Beta the Magnet Warrior",
"Gamma the Magnet Warrior",
"Magical Blast",
"Dunames Dark Witch",
"Vorse Raider",
"Exarion Universe",
"Abyss Soldier",
"Slate Warrior",
"Cyber-Tech Alligator",
"D.D. Assailant",
"Goblin Zombie",
"Elemental Hero Madballman",
"Mind Control",
"Toon Dark Magician Girl",
"Great Spirit",
"Graceful Dice",
"Negate Attack",
"Foolish Burial",
"Card Destruction",
"Dark Magic Ritual",
"Calamity of the Wicked"
},
"Special Gift Collection": {
"Gate Guardian",
"Scapegoat",
"Gil Garth",
"La Jinn the Mystical Genie of the Lamp",
"Summoned Skull",
"Inferno Hammer",
"Gemini Elf",
"Cyber Harpie Lady",
"Dandylion",
"Blade Knight",
"Curse of Vampire",
"Elemental Hero Flame Wingman",
"Magician of Black Chaos"
},
"Fairy Collection": {
"Silpheed",
"Dunames Dark Witch",
"Hysteric Fairy",
"The Agent of Judgment - Saturn",
"Shining Angel",
"Airknight Parshath",
"Dancing Fairy",
"Zolga",
"Kelbek",
"Mudora",
"Protective Soul Ailin",
"Marshmallon",
"Goddess with the Third Eye",
"Asura Priest",
"Manju of the Ten Thousand Hands",
"Senju of the Thousand Hands"
},
"Dragon Collection": {
"Victory D.",
"Chaos Emperor Dragon - Envoy of the End",
"Kaiser Glider",
"Horus the Black Flame Dragon LV6",
"Luster Dragon",
"Luster Dragon #2"
"Spear Dragon",
"Armed Dragon LV3",
"Armed Dragon LV5",
"Twin-Headed Behemoth",
"Cave Dragon",
"Masked Dragon",
"Element Dragon",
"Troop Dragon",
"Horus the Black Flame Dragon LV4",
"Pitch-Dark Dragon"
},
"Warrior Collection A": {
"Gate Guardian",
"Gearfried the Iron Knight",
"Dimensional Warrior",
"Command Knight",
"The Last Warrior from Another Planet",
"Dream Clown"
},
"Warrior Collection B": {
"Don Zaloog",
"Dark Scorpion - Chick the Yellow",
"Dark Scorpion - Meanae the Thorn",
"Dark Scorpion - Gorg the Strong",
"Cliff the Trap Remover",
"Ninja Grandmaster Sasuke",
"D.D. Warrior Lady",
"Mystic Swordsman LV2",
"Mystic Swordsman LV4",
"Mystic Swordsman LV6",
"Dark Blade",
"Blindly Loyal Goblin",
"Exiled Force",
"Ultimate Baseball Kid",
"Freed the Matchless General",
"Holy Knight Ishzark",
"Silent Swordsman Lv3",
"Silent Swordsman Lv5",
"Warrior Lady of the Wasteland",
"D.D. Assailant",
"Blade Knight",
"Marauding Captain",
"Toon Goblin Attack Force"
},
"Fiend Collection A": {
"Sangan",
"Castle of Dark Illusions",
"Barox",
"La Jinn the Mystical Genie of the Lamp",
"Summoned Skull",
"Beast of Talwar",
"Sangan",
"Giant Germ",
"Spear Cretin",
"Versago the Destroyer",
"Toon Summoned Skull"
},
"Fiend Collection B": {
"Raviel, Lord of Phantasms",
"Yata-Garasu",
"Helpoemer",
"Archfiend Soldier",
"Skull Descovery Knight",
"Gil Garth",
"Opticlops",
"Zure, Knight of Dark World",
"Brron, Mad King of Dark World",
"D.D. Survivor",
"Skull Archfiend of Lightning",
"The End of Anubis",
"Dark Ruler Ha Des",
"Inferno Hammer",
"Legendary Fiend",
"Newdoria",
"Slate Warrior",
"Giant Orc",
"Second Goblin",
"Kiseitai",
"Jowls of Dark Demise",
"D. D. Trainer",
"Earthbound Spirit"
},
"Machine Collection A": {
"Cyber-Stein",
"Mechanicalchaser",
"Jinzo",
"UFO Turtle",
"Cyber-Tech Alligator"
},
"Machine Collection B": {
"X-Head Cannon",
"Y-Dragon Head",
"Z-Metal Tank",
"XY-Dragon Cannon",
"XZ-Tank Cannon",
"YZ-Tank Dragon",
"XYZ-Dragon Cannon",
"V-Tiger Jet",
"W-Wing Catapult",
"VW-Tiger Catapult",
"VWXYZ-Dragon Catapult Cannon",
"Cyber Dragon",
"Cyber Twin Dragon",
"Green Gadget",
"Red Gadget",
"Yellow Gadget",
"B.E.S. Tetran"
},
"Spellcaster Collection A": {
"Exodia",
"Dark Sage",
"Dark Magician",
"Time Wizard",
"Kazejin",
"Magician of Faith",
"Dark Elf",
"Gemini Elf",
"Injection Fairy Lily",
"Cosmo Queen",
"Magician of Black Chaos"
},
"Spellcaster Collection B": {
"Jowgen the Spiritualist",
"Tsukuyomi",
"Manticore of Darkness",
"Chaos Command Magician",
"Cybernetic Magician",
"Skilled Dark Magician",
"Kycoo the Ghost Destroyer",
"Toon Gemini Elf",
"Toon Masked Sorcerer",
"Toon Dark Magician Girl",
"Familiar-Possessed - Aussa",
"Familiar-Possessed - Eria",
"Familiar-Possessed - Hiita",
"Familiar-Possessed - Wynn",
"Breaker the magical Warrior",
"The Tricky",
"Gravekeeper's Spy",
"Gravekeeper's Guard",
"Summon Priest",
"Old Vindictive Magician",
"Apprentice Magician",
"Princess Pikeru",
"Blast Magician",
"Magical Marionette",
"Mythical Beast Cerberus",
"Royal Magical Library",
"Aussa the Earth Charmer",
},
"Zombie Collection": {
"Skull Servant",
"Regenerating Mummy",
"Ryu Kokki",
"Spirit Reaper",
"Pyramid Turtle",
"Master Kyonshee",
"Curse of Vampire",
"Vampire Lord",
"Goblin Zombie",
"Decayed Commander",
"Zombie Tiger",
"Des Lacooda",
"Wandering Mummy",
"Royal Keeper"
},
"Special Monsters A": {
"X-Head Cannon",
"Y-Dragon Head",
"Z-Metal Tank",
"V-Tiger Jet",
"W-Wing Catapult",
"Yata-Garasu",
"Tsukuyomi",
"Dark Blade",
"Toon Gemini Elf",
"Toon Goblin Attack Force",
"Toon Masked Sorcerer",
"Toon Mermaid",
"Toon Dark Magician Girl",
"Toon Summoned Skull",
"Toon World",
"Burning Beast",
"Freezing Beast",
"Metallizing Parasite - Lunatite",
"Pitch-Dark Dragon",
"Giant Orc",
"Second Goblin",
"Decayed Commander",
"Zombie Tiger",
"Vampire Orchis",
"Des Dendle",
"Indomitable Fighter Lei Lei",
"Protective Soul Ailin",
"Frontline Base",
"Formation Union",
"Roll Out!",
"Asura Priest",
"Fushi No Tori",
"Maharaghi",
"Susa Soldier"
},
"Special Monsters B": {
"Polymerization",
"Mystic Swordsman LV2",
"Mystic Swordsman LV4",
"Mystic Swordsman LV6",
"Horus the Black Flame Dragon LV6",
"Horus the Black Flame Dragon LV4",
"Armed Dragon LV3"
"Armed Dragon LV5",
"Silent Swordsman Lv3",
"Silent Swordsman Lv5",
"Elemental Hero Flame Wingman",
"Elemental Hero Avian",
"Elemental Hero Burstinatrix",
"Miracle Fusion",
"Elemental Hero Madballman",
"Elemental Hero Bubbleman",
"Elemental Hero Clayman",
"Elemental Hero Rampart Blaster",
"Elemental Hero Shining Flare Wingman",
"Elemental Hero Sparkman",
"Elemental Hero Steam Healer",
"Elemental Hero Thunder Giant",
"Elemental Hero Wildedge",
"Elemental Hero Wildheart",
"Elemental Hero Bladedge",
"Level Up!",
"Ultimate Insect Lv3",
"Ultimate Insect Lv5"
},
"Reverse Collection": {
"Magical Merchant",
"Castle of Dark Illusions",
"Magician of Faith",
"Penguin Soldier",
"Blade Knight",
"Gravekeeper's Spy",
"Gravekeeper's Guard",
"Old Vindictive Magician",
"A Cat of Ill Omen",
"Jowls of Dark Demise",
"Cyber Jar",
"Morphing Jar",
"Morphing Jar #2",
"Needle Worm",
"Spear Cretin",
"Nobleman of Crossout",
"Aussa the Earth Charmer"
},
"LP Recovery Collection": {
"Mystik Wok",
"Poison of the Old Man",
"Hysteric Fairy",
"Dancing Fairy",
"Zolga",
"Cestus of Dagla",
"Nimble Momonga",
"Solemn Wishes",
"Cure Mermaid",
"Princess Pikeru",
"Kiseitai",
"Elemental Hero Steam Healer",
"Fushi No Tori",
"Emergency Provisions"
},
"Special Summon Collection A": {
"Perfectly Ultimate Great Moth",
"Dark Sage",
"Polymerization",
"Ritual",
"Cyber-Stein",
"Scapegoat",
"Aqua Spirit",
"Rock Spirit",
"Spirit of Flames",
"Garuda the Wind Spirit",
"Shining Angel",
"Mystic Tomato",
"Giant Rat",
"Mother Grizzly",
"UFO Turtle",
"Flying Kamakiri 1",
"Giant Germ",
"Revival Jam",
"Pyramid Turtle",
"Troop Dragon",
"Gravekeeper's Spy",
"Pitch-Dark Dragon",
"Decayed Commander",
"Zombie Tiger",
"Vampire Orchis",
"Des Dendle",
"Nimble Momonga",
"The Last Warrior from Another Planet",
"Embodiment of Apophis",
"Cyber Jar",
"Morphing Jar #2",
"Spear Cretin",
"Dark Magic Curtain"
},
"Special Summon Collection B": {
"Monster Gate",
"Chaos Emperor Dragon - Envoy of the End",
"Ojama Trio",
"Dimension Fusion",
"Return from the Different Dimension",
"Gigantes",
"Inferno",
"Silpheed",
"Mystic Swordsman LV2",
"Mystic Swordsman LV4",
"Skilled Dark Magician",
"Horus the Black Flame Dragon LV6",
"Armed Dragon LV3",
"Armed Dragon LV5",
"Marauding Captain",
"Masked Dragon",
"The Tricky",
"Magical Dimension",
"Frontline Base",
"Formation Union",
"Princess Pikeru",
"Skull Zoma",
"Metal Reflect Slime"
"Level Up!",
"Howling Insect",
"Tribute Doll",
"Enchanting Fitting Room",
"Stray Lambs"
},
"Special Summon Collection C": {
"Hamon, Lord of Striking Thunder",
"Raviel, Lord of Phantasms",
"Uria, Lord of Searing Flames",
"Treeborn Frog",
"Cyber Dragon",
"Familiar-Possessed - Aussa",
"Familiar-Possessed - Eria",
"Familiar-Possessed - Hiita",
"Familiar-Possessed - Wynn",
"Silent Swordsman Lv3",
"Silent Swordsman Lv5",
"Warrior Lady of the Wasteland",
"Dandylion",
"Curse of Vampire",
"Summon Priest",
"Miracle Fusion",
"Elemental Hero Bubbleman",
"The Dark - Hex-Sealed Fusion",
"The Earth - Hex-Sealed Fusion",
"The Light - Hex-Sealed Fusion",
"Ultimate Insect Lv3",
"Ultimate Insect Lv5",
"Rescue Cat",
"Anteatereatingant"
},
"Equipment Collection": {
"Megamorph",
"Cestus of Dagla",
"United We Stand"
},
"Continuous Spell/Trap A": {
"Destiny Board",
"Spirit Message 'I'",
"Spirit Message 'N'",
"Spirit Message 'A'",
"Spirit Message 'L'",
"Messenger of Peace",
"Fairy Box",
"Ultimate Offering",
"Gravity Bind",
"Solemn Wishes",
"Embodiment of Apophis",
"Toon World"
},
"Continuous Spell/Trap B": {
"Hamon, Lord of Striking Thunder",
"Uria, Lord of Searing Flames",
"Wave-Motion Cannon",
"Heart of the Underdog",
"Wall of Revealing Light",
"Dark Snake Syndrome",
"Call of the Mummy",
"Frontline Base",
"Level Limit - Area B",
"Skull Zoma",
"Pitch-Black Power Stone",
"Metal Reflect Slime"
},
"Quick/Counter Collection": {
"Mystik Wok",
"Poison of the Old Man",
"Scapegoat",
"Magical Dimension",
"Enemy Controller",
"Collapse",
"Emergency Provisions",
"Graceful Dice",
"Offerings to the Doomed",
"Reload",
"Rush Recklessly",
"The Reliable Guardian",
"Cursed Seal of the Forbidden Spell",
"Divine Wrath",
"Horn of Heaven",
"Magic Drain",
"Magic Jammer",
"Negate Attack",
"Seven Tools of the Bandit",
"Solemn Judgment",
"Spell Shield Type-8",
"Book of Moon",
"Serial Spell",
"Mystical Space Typhoon"
},
"Direct Damage Collection": {
"Hamon, Lord of Striking Thunder",
"Chaos Emperor Dragon - Envoy of the End",
"Dark Snake Syndrome",
"Inferno",
"Exarion Universe",
"Kycoo the Ghost Destroyer",
"Giant Germ",
"Familiar-Possessed - Aussa",
"Familiar-Possessed - Eria",
"Familiar-Possessed - Hiita",
"Familiar-Possessed - Wynn",
"Dark Driceratops",
"Saber Beetle",
"Thestalos the Firestorm Monarch",
"Solar Flare Dragon",
"Ultimate Baseball Kid",
"Spear Dragon",
"Oxygeddon",
"Airknight Parshath",
"Vampire Lord",
"Stamping Destruction",
"Decayed Commander",
"Jowls of Dark Demise",
"Stealth Bird",
"Elemental Hero Bladedge",
},
"Direct Attack Collection": {
"Victory D.",
"Dark Scorpion Combination",
"Spirit Reaper",
"Elemental Hero Rampart Blaster",
"Toon Gemini Elf",
"Toon Goblin Attack Force",
"Toon Masked Sorcerer",
"Toon Mermaid",
"Toon Summoned Skull",
"Toon Dark Magician Girl"
},
"Monster Destroy Collection": {
"Hamon, Lord of Striking Thunder",
"Inferno",
"Ninja Grandmaster Sasuke",
"Zaborg the Thunder Monarch",
"Mystic Swordsman LV2",
"Mystic Swordsman LV4",
"Mystic Swordsman LV6",
"Skull Descovery Knight",
"Inferno Hammer",
"Ryu Kokki",
"Newdoria",
"Exiled Force",
"Yomi Ship",
"Armed Dragon LV5",
"Element Dragon",
"Old Vindictive Magician",
"Magical Dimension",
"Des Dendle",
"Nobleman of Crossout",
"Shield Crash",
"Tribute to the Doomed",
"Elemental Hero Flame Wingman",
"Elemental Hero Shining Flare Wingman",
"Elemental Hero Steam Healer",
"Blast Magician",
"Magical Marionette",
"Swarm of Scarabs",
"Offerings to the Doomed",
"Divine Wrath",
"Dream Clown"
},
}
def get_booster_locations(booster: str) -> Dict[str, str]:
return {
f"{booster} {i}": content
for i, content in enumerate(booster_contents[booster])
}

View File

@@ -0,0 +1,139 @@
import math
from typing import TYPE_CHECKING, List, Optional, Set
from NetUtils import ClientStatus, NetworkItem
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
from worlds.yugioh06 import item_to_index
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
class YuGiOh2006Client(BizHawkClient):
game = "Yu-Gi-Oh! 2006"
system = "GBA"
patch_suffix = ".apygo06"
local_checked_locations: Set[int]
goal_flag: int
rom_slot_name: Optional[str]
def __init__(self) -> None:
super().__init__()
self.local_checked_locations = set()
self.rom_slot_name = None
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger
try:
# Check if ROM is some version of Yu-Gi-Oh! 2006
game_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 11, "ROM")]))[0]).decode("ascii")
if game_name != "YUGIOHWCT06":
return False
# Check if we can read the slot name. Doing this here instead of set_auth as a protection against
# validating a ROM where there's no slot name to read.
try:
slot_name_bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(0x30, 32, "ROM")]))[0]
self.rom_slot_name = bytes([byte for byte in slot_name_bytes if byte != 0]).decode("utf-8")
except UnicodeDecodeError:
logger.info("Could not read slot name from ROM. Are you sure this ROM matches this client version?")
return False
except UnicodeDecodeError:
return False
except bizhawk.RequestFailedError:
return False # Should verify on the next pass
ctx.game = self.game
ctx.items_handling = 0b001
ctx.want_slot_data = False
return True
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
ctx.auth = self.rom_slot_name
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
try:
read_state = await bizhawk.read(
ctx.bizhawk_ctx,
[
(0x0, 8, "EWRAM"),
(0x52E8, 32, "EWRAM"),
(0x5308, 32, "EWRAM"),
(0x5325, 1, "EWRAM"),
(0x6C38, 4, "EWRAM"),
],
)
game_state = read_state[0].decode("utf-8")
locations = read_state[1]
items = read_state[2]
amount_items = int.from_bytes(read_state[3], "little")
money = int.from_bytes(read_state[4], "little")
# make sure save was created
if game_state != "YWCT2006":
return
local_items = bytearray(items)
await bizhawk.guarded_write(
ctx.bizhawk_ctx,
[(0x5308, parse_items(bytearray(items), ctx.items_received), "EWRAM")],
[(0x5308, local_items, "EWRAM")],
)
money_received = 0
for item in ctx.items_received:
if item.item == item_to_index["5000DP"] + 5730000:
money_received += 1
if money_received > amount_items:
await bizhawk.guarded_write(
ctx.bizhawk_ctx,
[
(0x6C38, (money + (money_received - amount_items) * 5000).to_bytes(4, "little"), "EWRAM"),
(0x5325, money_received.to_bytes(2, "little"), "EWRAM"),
],
[
(0x6C38, money.to_bytes(4, "little"), "EWRAM"),
(0x5325, amount_items.to_bytes(2, "little"), "EWRAM"),
],
)
locs_to_send = set()
# Check for set location flags.
for byte_i, byte in enumerate(bytearray(locations)):
for i in range(8):
and_value = 1 << i
if byte & and_value != 0:
flag_id = byte_i * 8 + i
location_id = flag_id + 5730001
if location_id in ctx.server_locations:
locs_to_send.add(location_id)
# Send locations if there are any to send.
if locs_to_send != self.local_checked_locations:
self.local_checked_locations = locs_to_send
if locs_to_send is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locs_to_send)}])
# Send game clear if we're in either any ending cutscene or the credits state.
if not ctx.finished_game and locations[18] & (1 << 5) != 0:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
except bizhawk.RequestFailedError:
# Exit handler and return to main loop to reconnect.
pass
# Parses bit-map for local items and adds the received items to that bit-map
def parse_items(local_items: bytearray, items: List[NetworkItem]) -> bytearray:
array = local_items
for item in items:
index = item.item - 5730001
if index != 254:
byte = math.floor(index / 8)
bit = index % 8
array[byte] = array[byte] | (1 << bit)
return array

View File

@@ -0,0 +1,53 @@
# Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
## Where is the options page?
The [player options page for this game](../player-options) contains all the options you need to configure and
export a config file.
## What does randomization do to this game?
Unlocking Booster Packs, Campaign, Limited and Theme Duel Opponents has been changed.
You only need to beat each Campaign Opponent once.
Logic expects you to have access to the Booster Packs necessary to get the locations at a reasonable pace and consistency.
Logic remains, so the game is always able to be completed, but because of the shuffle, the player may need to defeat certain opponents before they
would in the vanilla game.
You can change how much money you receive and how much booster packs cost.
## What is the goal of Yu-Gi-Oh! 2006 when randomized?
Defeat a certain amount of Limited/Theme Duels to Unlock the final Campaign Opponent and beat it.
## What items and locations get shuffled?
Locations in which items can be found:
- Getting a Duel Bonus for the first time
- Beating a certain amount campaign opponents of the same level.
- Beating a Limited/Theme Duel
- Obtaining certain cards (same that unlock a theme duel in vanilla)
Items that are shuffled:
- Unlocking Booster Packs (the "ALL" Booster Packs are excluded)
- Unlocking Campaign Opponents
- Unlocking Limited/Theme Duels
- Banlists
## What items are _not_ randomized?
Certain Key Items are kept in their original locations:
- Duel Puzzles
- Survival Mode
- Booster Pack Contents
## Which items can be in another player's world?
Any shuffled item can be in other players' worlds.
## What does another world's item look like in Yu-Gi-Oh! 2006?
You can only tell when and what you got via the client.
## When the player receives an item, what happens?
The Opponent/Pack becomes available to you.

View File

@@ -0,0 +1,72 @@
# Setup Guide for Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 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.7.0 and later are supported.
- 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)
- A US or European Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 Rom
## Configuring Bizhawk
Once Bizhawk has been installed, open Bizhawk and change the following settings:
- Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
"Lua+LuaInterface". This is required for the Lua script to function correctly.
**NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs**
**of newer versions of Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
**"NLua+KopiLua" until this step is done.**
- 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, such as the Client.
- Under Config > Hotkeys, many hotkeys are listed, with many bound to common keys on the keyboard. You will likely want
to disable most of these, which you can do quickly using `Esc`.
It is strongly recommended to associate GBA rom extensions (\*.gba) to the Bizhawk we've just installed.
To do so, we simply have to search any GBA 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 customize your options by visiting the
[Yu-Gi-Oh! 2006 Player Options Page](/games/Yu-Gi-Oh!%202006/player-options)
## Joining a MultiWorld Game
### Obtain your GBA 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 `.apygo06` extension.
Double-click on your `.apygo06` 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/connector_bizhawk_generic.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]`)
Don't forget to start manipulating RNG early by shouting "Heart of the Cards!" during generation.

View File

@@ -0,0 +1,72 @@
from typing import List, NamedTuple
class FusionData(NamedTuple):
name: str
materials: List[str]
replaceable: bool
additional_spells: List[str]
fusions = {
"Elemental Hero Flame Wingman": FusionData(
"Elemental Hero Flame Wingman",
["Elemental Hero Avian", "Elemental Hero Burstinatrix"],
True,
["Miracle Fusion"]),
"Elemental Hero Madballman": FusionData(
"Elemental Hero Madballman",
["Elemental Hero Bubbleman", "Elemental Hero Clayman"],
True,
["Miracle Fusion"]),
"Elemental Hero Rampart Blaster": FusionData(
"Elemental Hero Rampart Blaster",
["Elemental Hero Burstinatrix", "Elemental Hero Clayman"],
True,
["Miracle Fusion"]),
"Elemental Hero Shining Flare Wingman": FusionData(
"Elemental Hero Shining Flare Wingman",
["Elemental Hero Flame Wingman", "Elemental Hero Sparkman"],
True,
["Miracle Fusion"]),
"Elemental Hero Steam Healer": FusionData(
"Elemental Hero Steam Healer",
["Elemental Hero Burstinatrix", "Elemental Hero Bubbleman"],
True,
["Miracle Fusion"]),
"Elemental Hero Wildedge": FusionData(
"Elemental Hero Wildedge",
["Elemental Hero Wildheart", "Elemental Hero Bladedge"],
True,
["Miracle Fusion"])
}
fusion_subs = ["The Dark - Hex-Sealed Fusion",
"The Earth - Hex-Sealed Fusion",
"The Light - Hex-Sealed Fusion",
"Goddess with the Third Eye",
"King of the Swamp",
"Versago the Destroyer",
# Only in All-packs
"Beastking of the Swamps",
"Mystical Sheep #1"]
def has_all_materials(state, monster, player):
data = fusions.get(monster)
if not state.has(monster, player):
return False
if data is None:
return True
else:
materials = data.replaceable and state.has_any(fusion_subs, player)
for material in data.materials:
materials += has_all_materials(state, material, player)
return materials >= len(data.materials)
def count_has_materials(state, monsters, player):
amount = 0
for monster in monsters:
amount += has_all_materials(state, monster, player)
return amount

369
worlds/yugioh06/items.py Normal file
View File

@@ -0,0 +1,369 @@
from typing import Dict, List
item_to_index: Dict[str, int] = {
"LEGEND OF B.E.W.D.": 1,
"METAL RAIDERS": 2,
"PHARAOH'S SERVANT": 3,
"PHARAONIC GUARDIAN": 4,
"SPELL RULER": 5,
"LABYRINTH OF NIGHTMARE": 6,
"LEGACY OF DARKNESS": 7,
"MAGICIAN'S FORCE": 8,
"DARK CRISIS": 9,
"INVASION OF CHAOS": 10,
"ANCIENT SANCTUARY": 11,
"SOUL OF THE DUELIST": 12,
"RISE OF DESTINY": 13,
"FLAMING ETERNITY": 14,
"THE LOST MILLENIUM": 15,
"CYBERNETIC REVOLUTION": 16,
"ELEMENTAL ENERGY": 17,
"SHADOW OF INFINITY": 18,
"GAME GIFT COLLECTION": 19,
"Special Gift Collection": 20,
"Fairy Collection": 21,
"Dragon Collection": 22,
"Warrior Collection A": 23,
"Warrior Collection B": 24,
"Fiend Collection A": 25,
"Fiend Collection B": 26,
"Machine Collection A": 27,
"Machine Collection B": 28,
"Spellcaster Collection A": 29,
"Spellcaster Collection B": 30,
"Zombie Collection": 31,
"Special Monsters A": 32,
"Special Monsters B": 33,
"Reverse Collection": 34,
"LP Recovery Collection": 35,
"Special Summon Collection A": 36,
"Special Summon Collection B": 37,
"Special Summon Collection C": 38,
"Equipment Collection": 39,
"Continuous Spell/Trap A": 40,
"Continuous Spell/Trap B": 41,
"Quick/Counter Collection": 42,
"Direct Damage Collection": 43,
"Direct Attack Collection": 44,
"Monster Destroy Collection": 45,
"All Normal Monsters": 46,
"All Effect Monsters": 47,
"All Fusion Monsters": 48,
"All Traps": 49,
"All Spells": 50,
"All at Random": 51,
"LD01 All except Level 4 forbidden Unlock": 52,
"LD02 Medium/high Level forbidden Unlock": 53,
"LD03 ATK 1500 or more forbidden Unlock": 54,
"LD04 Flip Effects forbidden Unlock": 55,
"LD05 Tributes forbidden Unlock": 56,
"LD06 Traps forbidden Unlock": 57,
"LD07 Large Deck A Unlock": 58,
"LD08 Large Deck B Unlock": 59,
"LD09 Sets Forbidden Unlock": 60,
"LD10 All except LV monsters forbidden Unlock": 61,
"LD11 All except Fairies forbidden Unlock": 62,
"LD12 All except Wind forbidden Unlock": 63,
"LD13 All except monsters forbidden Unlock": 64,
"LD14 Level 3 or below forbidden Unlock": 65,
"LD15 DEF 1500 or less forbidden Unlock": 66,
"LD16 Effect Monsters forbidden Unlock": 67,
"LD17 Spells forbidden Unlock": 68,
"LD18 Attacks forbidden Unlock": 69,
"LD19 All except E-Hero's forbidden Unlock": 70,
"LD20 All except Warriors forbidden Unlock": 71,
"LD21 All except Dark forbidden Unlock": 72,
"LD22 All limited cards forbidden Unlock": 73,
"LD23 Refer to Mar 05 Banlist Unlock": 74,
"LD24 Refer to Sept 04 Banlist Unlock": 75,
"LD25 Low Life Points Unlock": 76,
"LD26 All except Toons forbidden Unlock": 77,
"LD27 All except Spirits forbidden Unlock": 78,
"LD28 All except Dragons forbidden Unlock": 79,
"LD29 All except Spellcasters forbidden Unlock": 80,
"LD30 All except Light forbidden Unlock": 81,
"LD31 All except Fire forbidden Unlock": 82,
"LD32 Decks with multiples forbidden Unlock": 83,
"LD33 Special Summons forbidden Unlock": 84,
"LD34 Normal Summons forbidden Unlock": 85,
"LD35 All except Zombies forbidden Unlock": 86,
"LD36 All except Earth forbidden Unlock": 87,
"LD37 All except Water forbidden Unlock": 88,
"LD38 Refer to Mar 04 Banlist Unlock": 89,
"LD39 Monsters forbidden Unlock": 90,
"LD40 Refer to Sept 05 Banlist Unlock": 91,
"LD41 Refer to Sept 03 Banlist Unlock": 92,
"TD01 Battle Damage Unlock": 93,
"TD02 Deflected Damage Unlock": 94,
"TD03 Normal Summon Unlock": 95,
"TD04 Ritual Summon Unlock": 96,
"TD05 Special Summon A Unlock": 97,
"TD06 20x Spell Unlock": 98,
"TD07 10x Trap Unlock": 99,
"TD08 Draw Unlock": 100,
"TD09 Hand Destruction Unlock": 101,
"TD10 During Opponent's Turn Unlock": 102,
"TD11 Recover Unlock": 103,
"TD12 Remove Monsters by Effect Unlock": 104,
"TD13 Flip Summon Unlock": 105,
"TD14 Special Summon B Unlock": 106,
"TD15 Token Unlock": 107,
"TD16 Union Unlock": 108,
"TD17 10x Quick Spell Unlock": 109,
"TD18 The Forbidden Unlock": 110,
"TD19 20 Turns Unlock": 111,
"TD20 Deck Destruction Unlock": 112,
"TD21 Victory D. Unlock": 113,
"TD22 The Preventers Fight Back Unlock": 114,
"TD23 Huge Revolution Unlock": 115,
"TD24 Victory in 5 Turns Unlock": 116,
"TD25 Moth Grows Up Unlock": 117,
"TD26 Magnetic Power Unlock": 118,
"TD27 Dark Sage Unlock": 119,
"TD28 Direct Damage Unlock": 120,
"TD29 Destroy Monsters in Battle Unlock": 121,
"TD30 Tribute Summon Unlock": 122,
"TD31 Special Summon C Unlock": 123,
"TD32 Toon Unlock": 124,
"TD33 10x Counter Unlock": 125,
"TD34 Destiny Board Unlock": 126,
"TD35 Huge Damage in a Turn Unlock": 127,
"TD36 V-Z In the House Unlock": 128,
"TD37 Uria, Lord of Searing Flames Unlock": 129,
"TD38 Hamon, Lord of Striking Thunder Unlock": 130,
"TD39 Raviel, Lord of Phantasms Unlock": 131,
"TD40 Make a Chain Unlock": 132,
"TD41 The Gatekeeper Stands Tall Unlock": 133,
"TD42 Serious Damage Unlock": 134,
"TD43 Return Monsters with Effects Unlock": 135,
"TD44 Fusion Summon Unlock": 136,
"TD45 Big Damage at once Unlock": 137,
"TD46 XYZ In the House Unlock": 138,
"TD47 Spell Counter Unlock": 139,
"TD48 Destroy Monsters with Effects Unlock": 140,
"TD49 Plunder Unlock": 141,
"TD50 Dark Scorpion Combination Unlock": 142,
"Campaign Tier 1 Column 1": 143,
"Campaign Tier 1 Column 2": 144,
"Campaign Tier 1 Column 3": 145,
"Campaign Tier 1 Column 4": 146,
"Campaign Tier 1 Column 5": 147,
"Campaign Tier 2 Column 1": 148,
"Campaign Tier 2 Column 2": 149,
"Campaign Tier 2 Column 3": 150,
"Campaign Tier 2 Column 4": 151,
"Campaign Tier 2 Column 5": 152,
"Campaign Tier 3 Column 1": 153,
"Campaign Tier 3 Column 2": 154,
"Campaign Tier 3 Column 3": 155,
"Campaign Tier 3 Column 4": 156,
"Campaign Tier 3 Column 5": 157,
"Campaign Tier 4 Column 1": 158,
"Campaign Tier 4 Column 2": 159,
"Campaign Tier 4 Column 3": 160,
"Campaign Tier 4 Column 4": 161,
"Campaign Tier 4 Column 5": 162,
"Campaign Tier 5 Column 1": 163,
"Campaign Tier 5 Column 2": 164,
"No Banlist": 167,
"Banlist September 2003": 168,
"Banlist March 2004": 169,
"Banlist September 2004": 170,
"Banlist March 2005": 171,
"Banlist September 2005": 172,
"5000DP": 254,
"Remote": 255,
}
tier_1_opponents: List[str] = [
"Campaign Tier 1 Column 1",
"Campaign Tier 1 Column 2",
"Campaign Tier 1 Column 3",
"Campaign Tier 1 Column 4",
"Campaign Tier 1 Column 5",
]
Banlist_Items: List[str] = [
"No Banlist",
"Banlist September 2003",
"Banlist March 2004",
"Banlist September 2004",
"Banlist March 2005",
"Banlist September 2005",
]
draft_boosters: List[str] = [
"METAL RAIDERS",
"PHARAOH'S SERVANT",
"PHARAONIC GUARDIAN",
"LABYRINTH OF NIGHTMARE",
"LEGACY OF DARKNESS",
"MAGICIAN'S FORCE",
"DARK CRISIS",
"INVASION OF CHAOS",
"RISE OF DESTINY",
"ELEMENTAL ENERGY",
"SHADOW OF INFINITY",
]
draft_opponents: List[str] = ["Campaign Tier 1 Column 1", "Campaign Tier 1 Column 5"]
booster_packs: List[str] = [
"LEGEND OF B.E.W.D.",
"METAL RAIDERS",
"PHARAOH'S SERVANT",
"PHARAONIC GUARDIAN",
"SPELL RULER",
"LABYRINTH OF NIGHTMARE",
"LEGACY OF DARKNESS",
"MAGICIAN'S FORCE",
"DARK CRISIS",
"INVASION OF CHAOS",
"ANCIENT SANCTUARY",
"SOUL OF THE DUELIST",
"RISE OF DESTINY",
"FLAMING ETERNITY",
"THE LOST MILLENIUM",
"CYBERNETIC REVOLUTION",
"ELEMENTAL ENERGY",
"SHADOW OF INFINITY",
"GAME GIFT COLLECTION",
"Special Gift Collection",
"Fairy Collection",
"Dragon Collection",
"Warrior Collection A",
"Warrior Collection B",
"Fiend Collection A",
"Fiend Collection B",
"Machine Collection A",
"Machine Collection B",
"Spellcaster Collection A",
"Spellcaster Collection B",
"Zombie Collection",
"Special Monsters A",
"Special Monsters B",
"Reverse Collection",
"LP Recovery Collection",
"Special Summon Collection A",
"Special Summon Collection B",
"Special Summon Collection C",
"Equipment Collection",
"Continuous Spell/Trap A",
"Continuous Spell/Trap B",
"Quick/Counter Collection",
"Direct Damage Collection",
"Direct Attack Collection",
"Monster Destroy Collection",
]
challenges: List[str] = [
"LD01 All except Level 4 forbidden Unlock",
"LD02 Medium/high Level forbidden Unlock",
"LD03 ATK 1500 or more forbidden Unlock",
"LD04 Flip Effects forbidden Unlock",
"LD05 Tributes forbidden Unlock",
"LD06 Traps forbidden Unlock",
"LD07 Large Deck A Unlock",
"LD08 Large Deck B Unlock",
"LD09 Sets Forbidden Unlock",
"LD10 All except LV monsters forbidden Unlock",
"LD11 All except Fairies forbidden Unlock",
"LD12 All except Wind forbidden Unlock",
"LD13 All except monsters forbidden Unlock",
"LD14 Level 3 or below forbidden Unlock",
"LD15 DEF 1500 or less forbidden Unlock",
"LD16 Effect Monsters forbidden Unlock",
"LD17 Spells forbidden Unlock",
"LD18 Attacks forbidden Unlock",
"LD19 All except E-Hero's forbidden Unlock",
"LD20 All except Warriors forbidden Unlock",
"LD21 All except Dark forbidden Unlock",
"LD22 All limited cards forbidden Unlock",
"LD23 Refer to Mar 05 Banlist Unlock",
"LD24 Refer to Sept 04 Banlist Unlock",
"LD25 Low Life Points Unlock",
"LD26 All except Toons forbidden Unlock",
"LD27 All except Spirits forbidden Unlock",
"LD28 All except Dragons forbidden Unlock",
"LD29 All except Spellcasters forbidden Unlock",
"LD30 All except Light forbidden Unlock",
"LD31 All except Fire forbidden Unlock",
"LD32 Decks with multiples forbidden Unlock",
"LD33 Special Summons forbidden Unlock",
"LD34 Normal Summons forbidden Unlock",
"LD35 All except Zombies forbidden Unlock",
"LD36 All except Earth forbidden Unlock",
"LD37 All except Water forbidden Unlock",
"LD38 Refer to Mar 04 Banlist Unlock",
"LD39 Monsters forbidden Unlock",
"LD40 Refer to Sept 05 Banlist Unlock",
"LD41 Refer to Sept 03 Banlist Unlock",
"TD01 Battle Damage Unlock",
"TD02 Deflected Damage Unlock",
"TD03 Normal Summon Unlock",
"TD04 Ritual Summon Unlock",
"TD05 Special Summon A Unlock",
"TD06 20x Spell Unlock",
"TD07 10x Trap Unlock",
"TD08 Draw Unlock",
"TD09 Hand Destruction Unlock",
"TD10 During Opponent's Turn Unlock",
"TD11 Recover Unlock",
"TD12 Remove Monsters by Effect Unlock",
"TD13 Flip Summon Unlock",
"TD14 Special Summon B Unlock",
"TD15 Token Unlock",
"TD16 Union Unlock",
"TD17 10x Quick Spell Unlock",
"TD18 The Forbidden Unlock",
"TD19 20 Turns Unlock",
"TD20 Deck Destruction Unlock",
"TD21 Victory D. Unlock",
"TD22 The Preventers Fight Back Unlock",
"TD23 Huge Revolution Unlock",
"TD24 Victory in 5 Turns Unlock",
"TD25 Moth Grows Up Unlock",
"TD26 Magnetic Power Unlock",
"TD27 Dark Sage Unlock",
"TD28 Direct Damage Unlock",
"TD29 Destroy Monsters in Battle Unlock",
"TD30 Tribute Summon Unlock",
"TD31 Special Summon C Unlock",
"TD32 Toon Unlock",
"TD33 10x Counter Unlock",
"TD34 Destiny Board Unlock",
"TD35 Huge Damage in a Turn Unlock",
"TD36 V-Z In the House Unlock",
"TD37 Uria, Lord of Searing Flames Unlock",
"TD38 Hamon, Lord of Striking Thunder Unlock",
"TD39 Raviel, Lord of Phantasms Unlock",
"TD40 Make a Chain Unlock",
"TD41 The Gatekeeper Stands Tall Unlock",
"TD42 Serious Damage Unlock",
"TD43 Return Monsters with Effects Unlock",
"TD44 Fusion Summon Unlock",
"TD45 Big Damage at once Unlock",
"TD46 XYZ In the House Unlock",
"TD47 Spell Counter Unlock",
"TD48 Destroy Monsters with Effects Unlock",
"TD49 Plunder Unlock",
"TD50 Dark Scorpion Combination Unlock",
]
excluded_items: List[str] = [
"All Normal Monsters",
"All Effect Monsters",
"All Fusion Monsters",
"All Traps",
"All Spells",
"All at Random",
"5000DP",
"Remote",
]
useful: List[str] = [
"Banlist March 2004",
"Banlist September 2004",
"Banlist March 2005",
"Banlist September 2005",
]

View File

@@ -0,0 +1,213 @@
Bonuses = {
"Duelist Bonus Level 1": 1,
"Duelist Bonus Level 2": 2,
"Duelist Bonus Level 3": 3,
"Duelist Bonus Level 4": 4,
"Duelist Bonus Level 5": 5,
"Battle Damage": 6,
"Battle Damage Only Bonus": 7,
"Max ATK Bonus": 8,
"Max Damage Bonus": 9,
"Destroyed in Battle Bonus": 10,
"Spell Card Bonus": 11,
"Trap Card Bonus": 12,
"Tribute Summon Bonus": 13,
"Fusion Summon Bonus": 14,
"Ritual Summon Bonus": 15,
"No Special Summon Bonus": 16,
"No Spell Cards Bonus": 17,
"No Trap Cards Bonus": 18,
"No Damage Bonus": 19,
"Over 20000 LP Bonus": 20,
"Low LP Bonus": 21,
"Extremely Low LP Bonus": 22,
"Low Deck Bonus": 23,
"Extremely Low Deck Bonus": 24,
"Effect Damage Only Bonus": 25,
"No More Cards Bonus": 26,
"Opponent's Turn Finish Bonus": 27,
"Exactly 0 LP Bonus": 28,
"Reversal Finish Bonus": 29,
"Quick Finish Bonus": 30,
"Exodia Finish Bonus": 31,
"Last Turn Finish Bonus": 32,
"Final Countdown Finish Bonus": 33,
"Destiny Board Finish Bonus": 34,
"Yata-Garasu Finish Bonus": 35,
"Skull Servant Finish Bonus": 36,
"Konami Bonus": 37,
}
Limited_Duels = {
"LD01 All except Level 4 forbidden": 38,
"LD02 Medium/high Level forbidden": 39,
"LD03 ATK 1500 or more forbidden": 40,
"LD04 Flip Effects forbidden": 41,
"LD05 Tributes forbidden": 42,
"LD06 Traps forbidden": 43,
"LD07 Large Deck A": 44,
"LD08 Large Deck B": 45,
"LD09 Sets Forbidden": 46,
"LD10 All except LV monsters forbidden": 47,
"LD11 All except Fairies forbidden": 48,
"LD12 All except Wind forbidden": 49,
"LD13 All except monsters forbidden": 50,
"LD14 Level 3 or below forbidden": 51,
"LD15 DEF 1500 or less forbidden": 52,
"LD16 Effect Monsters forbidden": 53,
"LD17 Spells forbidden": 54,
"LD18 Attacks forbidden": 55,
"LD19 All except E-Hero's forbidden": 56,
"LD20 All except Warriors forbidden": 57,
"LD21 All except Dark forbidden": 58,
"LD22 All limited cards forbidden": 59,
"LD23 Refer to Mar 05 Banlist": 60,
"LD24 Refer to Sept 04 Banlist": 61,
"LD25 Low Life Points": 62,
"LD26 All except Toons forbidden": 63,
"LD27 All except Spirits forbidden": 64,
"LD28 All except Dragons forbidden": 65,
"LD29 All except Spellcasters forbidden": 66,
"LD30 All except Light forbidden": 67,
"LD31 All except Fire forbidden": 68,
"LD32 Decks with multiples forbidden": 69,
"LD33 Special Summons forbidden": 70,
"LD34 Normal Summons forbidden": 71,
"LD35 All except Zombies forbidden": 72,
"LD36 All except Earth forbidden": 73,
"LD37 All except Water forbidden": 74,
"LD38 Refer to Mar 04 Banlist": 75,
"LD39 Monsters forbidden": 76,
"LD40 Refer to Sept 05 Banlist": 77,
"LD41 Refer to Sept 03 Banlist": 78,
}
Theme_Duels = {
"TD01 Battle Damage": 79,
"TD02 Deflected Damage": 80,
"TD03 Normal Summon": 81,
"TD04 Ritual Summon": 82,
"TD05 Special Summon A": 83,
"TD06 20x Spell": 84,
"TD07 10x Trap": 85,
"TD08 Draw": 86,
"TD09 Hand Destruction": 87,
"TD10 During Opponent's Turn": 88,
"TD11 Recover": 89,
"TD12 Remove Monsters by Effect": 90,
"TD13 Flip Summon": 91,
"TD14 Special Summon B": 92,
"TD15 Token": 93,
"TD16 Union": 94,
"TD17 10x Quick Spell": 95,
"TD18 The Forbidden": 96,
"TD19 20 Turns": 97,
"TD20 Deck Destruction": 98,
"TD21 Victory D.": 99,
"TD22 The Preventers Fight Back": 100,
"TD23 Huge Revolution": 101,
"TD24 Victory in 5 Turns": 102,
"TD25 Moth Grows Up": 103,
"TD26 Magnetic Power": 104,
"TD27 Dark Sage": 105,
"TD28 Direct Damage": 106,
"TD29 Destroy Monsters in Battle": 107,
"TD30 Tribute Summon": 108,
"TD31 Special Summon C": 109,
"TD32 Toon": 110,
"TD33 10x Counter": 111,
"TD34 Destiny Board": 112,
"TD35 Huge Damage in a Turn": 113,
"TD36 V-Z In the House": 114,
"TD37 Uria, Lord of Searing Flames": 115,
"TD38 Hamon, Lord of Striking Thunder": 116,
"TD39 Raviel, Lord of Phantasms": 117,
"TD40 Make a Chain": 118,
"TD41 The Gatekeeper Stands Tall": 119,
"TD42 Serious Damage": 120,
"TD43 Return Monsters with Effects": 121,
"TD44 Fusion Summon": 122,
"TD45 Big Damage at once": 123,
"TD46 XYZ In the House": 124,
"TD47 Spell Counter": 125,
"TD48 Destroy Monsters with Effects": 126,
"TD49 Plunder": 127,
"TD50 Dark Scorpion Combination": 128,
}
Campaign_Opponents = {
"Campaign Tier 1: 1 Win": 129,
"Campaign Tier 1: 3 Wins A": 130,
"Campaign Tier 1: 3 Wins B": 131,
"Campaign Tier 1: 5 Wins A": 132,
"Campaign Tier 1: 5 Wins B": 133,
"Campaign Tier 2: 1 Win": 134,
"Campaign Tier 2: 3 Wins A": 135,
"Campaign Tier 2: 3 Wins B": 136,
"Campaign Tier 2: 5 Wins A": 137,
"Campaign Tier 2: 5 Wins B": 138,
"Campaign Tier 3: 1 Win": 139,
"Campaign Tier 3: 3 Wins A": 140,
"Campaign Tier 3: 3 Wins B": 141,
"Campaign Tier 3: 5 Wins A": 142,
"Campaign Tier 3: 5 Wins B": 143,
"Campaign Tier 4: 5 Wins A": 144,
"Campaign Tier 4: 5 Wins B": 145,
}
special = {
"Campaign Tier 5: Column 1 Win": 146,
"Campaign Tier 5: Column 2 Win": 147,
"Campaign Tier 5: Column 3 Win": 148,
"Campaign Tier 5: Column 4 Win": 149,
# "Campaign Final Boss Win": 150,
}
Required_Cards = {
"Obtain all pieces of Exodia": 154,
"Obtain Final Countdown": 155,
"Obtain Victory Dragon": 156,
"Obtain Ojama Delta Hurricane and its required cards": 157,
"Obtain Huge Revolution and its required cards": 158,
"Obtain Perfectly Ultimate Great Moth and its required cards": 159,
"Obtain Valkyrion the Magna Warrior and its pieces": 160,
"Obtain Dark Sage and its required cards": 161,
"Obtain Destiny Board and its letters": 162,
"Obtain all XYZ-Dragon Cannon fusions and their materials": 163,
"Obtain VWXYZ-Dragon Catapult Cannon and the fusion materials": 164,
"Obtain Hamon, Lord of Striking Thunder": 165,
"Obtain Raviel, Lord of Phantasms": 166,
"Obtain Uria, Lord of Searing Flames": 167,
"Obtain Gate Guardian and its pieces": 168,
"Obtain Dark Scorpion Combination and its required cards": 169,
}
collection_events = {
"Ojama Delta Hurricane and required cards": None,
"Huge Revolution and its required cards": None,
"Perfectly Ultimate Great Moth and its required cards": None,
"Valkyrion the Magna Warrior and its pieces": None,
"Dark Sage and its required cards": None,
"Destiny Board and its letters": None,
"XYZ-Dragon Cannon fusions and their materials": None,
"VWXYZ-Dragon Catapult Cannon and the fusion materials": None,
"Gate Guardian and its pieces": None,
"Dark Scorpion Combination and its required cards": None,
"Can Exodia Win": None,
"Can Yata Lock": None,
"Can Stall with Monsters": None,
"Can Stall with ST": None,
"Can Last Turn Win": None,
"Has Back-row removal": None,
}
def get_beat_challenge_events(self):
beat_events = {}
for limited in Limited_Duels.keys():
if limited not in self.removed_challenges:
beat_events[limited + " Complete"] = None
for theme in Theme_Duels.keys():
if theme not in self.removed_challenges:
beat_events[theme + " Complete"] = None
return beat_events

28
worlds/yugioh06/logic.py Normal file
View File

@@ -0,0 +1,28 @@
from typing import List
from BaseClasses import CollectionState
core_booster: List[str] = [
"LEGEND OF B.E.W.D.",
"METAL RAIDERS",
"PHARAOH'S SERVANT",
"PHARAONIC GUARDIAN",
"SPELL RULER",
"LABYRINTH OF NIGHTMARE",
"LEGACY OF DARKNESS",
"MAGICIAN'S FORCE",
"DARK CRISIS",
"INVASION OF CHAOS",
"ANCIENT SANCTUARY",
"SOUL OF THE DUELIST",
"RISE OF DESTINY",
"FLAMING ETERNITY",
"THE LOST MILLENIUM",
"CYBERNETIC REVOLUTION",
"ELEMENTAL ENERGY",
"SHADOW OF INFINITY",
]
def yugioh06_difficulty(state: CollectionState, player: int, amount: int):
return state.has_from_list(core_booster, player, amount)

View File

@@ -0,0 +1,264 @@
from typing import Dict, List, NamedTuple, Optional, Union
from BaseClasses import MultiWorld
from worlds.generic.Rules import CollectionRule
from worlds.yugioh06 import item_to_index, tier_1_opponents, yugioh06_difficulty
from worlds.yugioh06.locations import special
class OpponentData(NamedTuple):
id: int
name: str
campaign_info: List[str]
tier: int
column: int
card_id: int = 0
deck_name_id: int = 0
deck_file: str = ""
difficulty: int = 1
additional_info: List[str] = []
def tier(self, tier):
self.tier = tier
def column(self, column):
self.column = column
challenge_opponents = [
# Theme
OpponentData(27, "Exarion Universe", [], 1, 1, 5452, 13001, "deck/theme_001.ydc\x00\x00\x00\x00", 0),
OpponentData(28, "Stone Statue of the Aztecs", [], 4, 1, 4754, 13002, "deck/theme_002.ydc\x00\x00\x00\x00", 3),
OpponentData(29, "Raging Flame Sprite", [], 1, 1, 6189, 13003, "deck/theme_003.ydc\x00\x00\x00\x00", 0),
OpponentData(30, "Princess Pikeru", [], 1, 1, 6605, 13004, "deck/theme_004.ydc\x00\x00\x00\x00", 0),
OpponentData(31, "Princess Curran", ["Quick-Finish"], 1, 1, 6606, 13005, "deck/theme_005.ydc\x00\x00\x00\x00", 0,
["Has Back-row removal"]),
OpponentData(32, "Gearfried the Iron Knight", ["Quick-Finish"], 2, 1, 5059, 13006,
"deck/theme_006.ydc\x00\x00\x00\x00", 1),
OpponentData(33, "Zaborg the Thunder Monarch", [], 3, 1, 5965, 13007, "deck/theme_007.ydc\x00\x00\x00\x00", 2),
OpponentData(34, "Kycoo the Ghost Destroyer", ["Quick-Finish"], 3, 1, 5248, 13008,
"deck/theme_008.ydc\x00\x00\x00\x00"),
OpponentData(35, "Penguin Soldier", ["Quick-Finish"], 1, 1, 4608, 13009, "deck/theme_009.ydc\x00\x00\x00\x00", 0),
OpponentData(36, "Green Gadget", [], 5, 1, 6151, 13010, "deck/theme_010.ydc\x00\x00\x00\x00", 5),
OpponentData(37, "Guardian Sphinx", ["Quick-Finish"], 3, 1, 5422, 13011, "deck/theme_011.ydc\x00\x00\x00\x00", 3),
OpponentData(38, "Cyber-Tech Alligator", [], 2, 1, 4790, 13012, "deck/theme_012.ydc\x00\x00\x00\x00", 1),
OpponentData(39, "UFOroid Fighter", [], 3, 1, 6395, 13013, "deck/theme_013.ydc\x00\x00\x00\x00", 2),
OpponentData(40, "Relinquished", [], 3, 1, 4737, 13014, "deck/theme_014.ydc\x00\x00\x00\x00", 2),
OpponentData(41, "Manticore of Darkness", [], 2, 1, 5881, 13015, "deck/theme_015.ydc\x00\x00\x00\x00", 1),
OpponentData(42, "Vampire Lord", [], 3, 1, 5410, 13016, "deck/theme_016.ydc\x00\x00\x00\x00", 2),
OpponentData(43, "Gigantes", ["Quick-Finish"], 3, 1, 5831, 13017, "deck/theme_017.ydc\x00\x00\x00\x00", 2),
OpponentData(44, "Insect Queen", ["Quick-Finish"], 2, 1, 4768, 13018, "deck/theme_018.ydc\x00\x00\x00\x00", 1),
OpponentData(45, "Second Goblin", ["Quick-Finish"], 1, 1, 5587, 13019, "deck/theme_019.ydc\x00\x00\x00\x00", 0),
OpponentData(46, "Toon Summoned Skull", [], 4, 1, 4735, 13020, "deck/theme_020.ydc\x00\x00\x00\x00", 3),
OpponentData(47, "Iron Blacksmith Kotetsu", [], 2, 1, 5769, 13021, "deck/theme_021.ydc\x00\x00\x00\x00", 1),
OpponentData(48, "Magician of Faith", [], 1, 1, 4434, 13022, "deck/theme_022.ydc\x00\x00\x00\x00", 0),
OpponentData(49, "Mask of Darkness", [], 1, 1, 4108, 13023, "deck/theme_023.ydc\x00\x00\x00\x00", 0),
OpponentData(50, "Dark Ruler Vandalgyon", [], 3, 1, 6410, 13024, "deck/theme_024.ydc\x00\x00\x00\x00", 2),
OpponentData(51, "Aussa the Earth Charmer", ["Quick-Finish"], 2, 1, 6335, 13025,
"deck/theme_025.ydc\x00\x00\x00\x00", 1),
OpponentData(52, "Exodia Necross", ["Quick-Finish"], 2, 1, 5701, 13026, "deck/theme_026.ydc\x00\x00\x00\x00", 1),
OpponentData(53, "Dark Necrofear", [], 3, 1, 5222, 13027, "deck/theme_027.ydc\x00\x00\x00\x00", 2),
OpponentData(54, "Demise, King of Armageddon", [], 4, 1, 6613, 13028, "deck/theme_028.ydc\x00\x00\x00\x00", 2),
OpponentData(55, "Yamata Dragon", [], 3, 1, 5377, 13029, "deck/theme_029.ydc\x00\x00\x00\x00", 2),
OpponentData(56, "Blue-Eyes Ultimate Dragon", [], 3, 1, 4386, 13030, "deck/theme_030.ydc\x00\x00\x00\x00", 2),
OpponentData(57, "Wave-Motion Cannon", [], 4, 1, 5614, 13031, "deck/theme_031.ydc\x00\x00\x00\x00", 3,
["Has Back-row removal"]),
# Unused opponent
# OpponentData(58, "Yata-Garasu", [], 1, 1, 5375, 13032, "deck/theme_031.ydc\x00\x00\x00\x00"),
# Unused opponent
# OpponentData(59, "Makyura the Destructor", [], 1, 1, 5285, 13033, "deck/theme_031.ydc\x00\x00\x00\x00"),
OpponentData(60, "Morphing Jar", [], 5, 1, 4597, 13034, "deck/theme_034.ydc\x00\x00\x00\x00", 4),
OpponentData(61, "Spirit Reaper", [], 2, 1, 5526, 13035, "deck/theme_035.ydc\x00\x00\x00\x00", 1),
OpponentData(62, "Victory D.", [], 3, 1, 5868, 13036, "deck/theme_036.ydc\x00\x00\x00\x00", 2),
OpponentData(63, "VWXYZ-Dragon Catapult Cannon", ["Quick-Finish"], 3, 1, 6484, 13037,
"deck/theme_037.ydc\x00\x00\x00\x00", 2),
OpponentData(64, "XYZ-Dragon Cannon", [], 2, 1, 5556, 13038, "deck/theme_038.ydc\x00\x00\x00\x00", 1),
OpponentData(65, "Uria, Lord of Searing Flames", [], 4, 1, 6563, 13039, "deck/theme_039.ydc\x00\x00\x00\x00", 3),
OpponentData(66, "Hamon, Lord of Striking Thunder", [], 4, 1, 6564, 13040, "deck/theme_040.ydc\x00\x00\x00\x00", 3),
OpponentData(67, "Raviel, Lord of Phantasms TD", [], 4, 1, 6565, 13041, "deck/theme_041.ydc\x00\x00\x00\x00", 3),
OpponentData(68, "Ojama Trio", [], 1, 1, 5738, 13042, "deck/theme_042.ydc\x00\x00\x00\x00", 0),
OpponentData(69, "People Running About", ["Quick-Finish"], 1, 1, 5578, 13043, "deck/theme_043.ydc\x00\x00\x00\x00",
0),
OpponentData(70, "Cyber-Stein", [], 5, 1, 4426, 13044, "deck/theme_044.ydc\x00\x00\x00\x00", 4),
OpponentData(71, "Winged Kuriboh LV10", [], 4, 1, 6406, 13045, "deck/theme_045.ydc\x00\x00\x00\x00", 3),
OpponentData(72, "Blue-Eyes Shining Dragon", [], 3, 1, 6082, 13046, "deck/theme_046.ydc\x00\x00\x00\x00", 2),
OpponentData(73, "Perfectly Ultimate Great Moth", ["Quick-Finish"], 3, 1, 4073, 13047,
"deck/theme_047.ydc\x00\x00\x00\x00", 2),
OpponentData(74, "Gate Guardian", [], 4, 1, 4380, 13048, "deck/theme_048.ydc\x00\x00\x00\x00", 2),
OpponentData(75, "Valkyrion the Magna Warrior", [], 3, 1, 5002, 13049, "deck/theme_049.ydc\x00\x00\x00\x00", 2),
OpponentData(76, "Dark Sage", [], 4, 1, 5230, 13050, "deck/theme_050.ydc\x00\x00\x00\x00", 3),
OpponentData(77, "Don Zaloog", [], 4, 1, 5426, 13051, "deck/theme_051.ydc\x00\x00\x00\x00", 3),
OpponentData(78, "Blast Magician", ["Quick-Finish"], 2, 1, 6250, 13052, "deck/theme_052.ydc\x00\x00\x00\x00", 1),
# Limited
OpponentData(79, "Zombyra the Dark", [], 5, 1, 5245, 23000, "deck/limit_000.ydc\x00\x00\x00\x00", 5),
OpponentData(80, "Goblin Attack Force", [], 4, 1, 5145, 23001, "deck/limit_001.ydc\x00\x00\x00\x00", 3),
OpponentData(81, "Giant Kozaky", [], 4, 1, 6420, 23002, "deck/limit_002.ydc\x00\x00\x00\x00", 4),
OpponentData(82, "Big Shield Gardna", ["Quick-Finish"], 2, 1, 4764, 23003, "deck/limit_003.ydc\x00\x00\x00\x00", 1),
OpponentData(83, "Panther Warrior", [], 3, 1, 4751, 23004, "deck/limit_004.ydc\x00\x00\x00\x00", 2),
OpponentData(84, "Silent Magician LV4", ["Quick-Finish"], 2, 1, 6167, 23005, "deck/limit_005.ydc\x00\x00\x00\x00",
1),
OpponentData(85, "Summoned Skull", [], 4, 1, 4028, 23006, "deck/limit_006.ydc\x00\x00\x00\x00", 3),
OpponentData(86, "Ancient Gear Golem", [], 5, 1, 6315, 23007, "deck/limit_007.ydc\x00\x00\x00\x00", 5),
OpponentData(87, "Chaos Sorcerer", [], 5, 1, 5833, 23008, "deck/limit_008.ydc\x00\x00\x00\x00", 5),
OpponentData(88, "Breaker the Magical Warrior", [], 5, 1, 5655, 23009, "deck/limit_009.ydc\x00\x00\x00\x00", 4),
OpponentData(89, "Dark Magician of Chaos", [], 4, 1, 5880, 23010, "deck/limit_010.ydc\x00\x00\x00\x00", 3),
OpponentData(90, "Stealth Bird", ["Quick-Finish"], 2, 1, 5882, 23011, "deck/limit_011.ydc\x00\x00\x00\x00", 1),
OpponentData(91, "Rapid-Fire Magician", ["Quick-Finish"], 2, 1, 6500, 23012, "deck/limit_012.ydc\x00\x00\x00\x00",
1),
OpponentData(92, "Morphing Jar #2", [], 5, 1, 4969, 23013, "deck/limit_013.ydc\x00\x00\x00\x00", 4),
OpponentData(93, "Cyber Jar", [], 5, 1, 4913, 23014, "deck/limit_014.ydc\x00\x00\x00\x00", 4),
# Unused/Broken
# OpponentData(94, "Exodia the Forbidden One", [], 1, 1, 4027, 23015, "deck/limit_015.ydc\x00\x00\x00\x00"),
OpponentData(94, "Dark Paladin", [], 4, 1, 5628, 23016, "deck/limit_016.ydc\x00\x00\x00\x00", 3),
OpponentData(95, "F.G.D.", [], 5, 1, 5502, 23017, "deck/limit_017.ydc\x00\x00\x00\x00", 4),
OpponentData(96, "Blue-Eyes Toon Dragon", ["Quick-Finish"], 2, 1, 4773, 23018, "deck/limit_018.ydc\x00\x00\x00\x00",
1),
OpponentData(97, "Tsukuyomi", [], 3, 1, 5780, 23019, "deck/limit_019.ydc\x00\x00\x00\x00", 2),
OpponentData(98, "Silent Swordsman LV3", ["Quick-Finish"], 2, 1, 6162, 23020, "deck/limit_020.ydc\x00\x00\x00\x00",
2),
OpponentData(99, "Elemental Hero Flame Wingman", ["Quick-Finish"], 2, 1, 6344, 23021,
"deck/limit_021.ydc\x00\x00\x00\x00", 0),
OpponentData(100, "Armed Dragon LV7", ["Quick-Finish"], 2, 1, 6107, 23022, "deck/limit_022.ydc\x00\x00\x00\x00", 0),
OpponentData(101, "Alkana Knight Joker", ["Quick-Finish"], 1, 1, 6454, 23023, "deck/limit_023.ydc\x00\x00\x00\x00",
0),
OpponentData(102, "Sorcerer of Dark Magic", [], 4, 1, 6086, 23024, "deck/limit_024.ydc\x00\x00\x00\x00", 3),
OpponentData(103, "Shinato, King of a Higher Plane", [], 4, 1, 5697, 23025, "deck/limit_025.ydc\x00\x00\x00\x00",
3),
OpponentData(104, "Ryu Kokki", [], 5, 1, 5902, 23026, "deck/limit_026.ydc\x00\x00\x00\x00", 4),
OpponentData(105, "Cyber Dragon", [], 5, 1, 6390, 23027, "deck/limit_027.ydc\x00\x00\x00\x00", 4),
OpponentData(106, "Dark Dreadroute", ["Quick-Finish"], 3, 1, 6405, 23028, "deck/limit_028.ydc\x00\x00\x00\x00", 2),
OpponentData(107, "Ultimate Insect LV7", ["Quick-Finish"], 3, 1, 6319, 23029, "deck/limit_029.ydc\x00\x00\x00\x00",
2),
OpponentData(108, "Thestalos the Firestorm Monarch", ["Quick-Finish"], 3, 1, 6190, 23030,
"deck/limit_030.ydc\x00\x00\x00\x00"),
OpponentData(109, "Master of Oz", ["Quick-Finish"], 3, 1, 6127, 23031, "deck/limit_031.ydc\x00\x00\x00\x00", 2),
OpponentData(110, "Orca Mega-Fortress of Darkness", ["Quick-Finish"], 3, 1, 5896, 23032,
"deck/limit_032.ydc\x00\x00\x00\x00", 2),
OpponentData(111, "Airknight Parshath", ["Quick-Finish"], 4, 1, 5023, 23033, "deck/limit_033.ydc\x00\x00\x00\x00",
3),
OpponentData(112, "Dark Scorpion Burglars", ["Quick-Finish"], 4, 1, 5425, 23034,
"deck/limit_034.ydc\x00\x00\x00\x00", 3),
OpponentData(113, "Gilford the Lightning", [], 4, 1, 5451, 23035, "deck/limit_035.ydc\x00\x00\x00\x00", 3),
OpponentData(114, "Embodiment of Apophis", [], 2, 1, 5234, 23036, "deck/limit_036.ydc\x00\x00\x00\x00", 1),
OpponentData(115, "Great Maju Garzett", [], 5, 1, 5768, 23037, "deck/limit_037.ydc\x00\x00\x00\x00", 4),
OpponentData(116, "Black Luster Soldier - Envoy of the Beginning", [], 5, 1, 5835, 23038,
"deck/limit_038.ydc\x00\x00\x00\x00", 4),
OpponentData(117, "Red-Eyes B. Dragon", [], 4, 1, 4088, 23039, "deck/limit_039.ydc\x00\x00\x00\x00", 3),
OpponentData(118, "Blue-Eyes White Dragon", [], 4, 1, 4007, 23040, "deck/limit_040.ydc\x00\x00\x00\x00", 3),
OpponentData(119, "Dark Magician", [], 4, 1, 4041, 23041, "deck/limit_041.ydc\x00\x00\x00\x00", 3),
OpponentData(0, "Starter", ["Quick-Finish"], 1, 1, 4064, 1510, "deck/SD0_STARTER.ydc\x00\x00", 0),
OpponentData(10, "DRAGON'S ROAR", ["Quick-Finish"], 2, 1, 6292, 1511, "deck/SD1_DRAGON.ydc\x00\x00\x00", 1),
OpponentData(11, "ZOMBIE MADNESS", ["Quick-Finish"], 2, 1, 6293, 1512, "deck/SD2_UNDEAD.ydc\x00\x00\x00", 1),
OpponentData(12, "BLAZING DESTRUCTION", ["Quick-Finish"], 2, 1, 6368, 1513, "deck/SD3_FIRE.ydc\x00\x00\x00\x00\x00",
1,
["Has Back-row removal"]),
OpponentData(13, "FURY FROM THE DEEP", [], 2, 1, 6376, 1514,
"deck/SD4_UMI.ydc\x00\x00\x00\x00\x00\x00", 1, ["Has Back-row removal"]),
OpponentData(15, "WARRIORS TRIUMPH", ["Quick-Finish"], 2, 1, 6456, 1515, "deck/SD5_SOLDIER.ydc\x00\x00", 1),
OpponentData(16, "SPELLCASTERS JUDGEMENT", ["Quick-Finish"], 2, 1, 6530, 1516, "deck/SD6_MAGICIAN.ydc\x00", 1),
OpponentData(17, "INVICIBLE FORTRESS", [], 2, 1, 6640, 1517, "deck/SD7_GANSEKI.ydc\x00\x00", 1),
OpponentData(7, "Goblin King 2", ["Quick-Finish"], 3, 3, 5973, 8007, "deck/LV2_kingG2.ydc\x00\x00\x00", 2),
]
def get_opponents(multiworld: Optional[MultiWorld], player: Optional[int], randomize: bool = False) -> List[
OpponentData]:
opponents_table: List[OpponentData] = [
# Tier 1
OpponentData(0, "Kuriboh", [], 1, 1, 4064, 8000, "deck/LV1_kuriboh.ydc\x00\x00"),
OpponentData(1, "Scapegoat", [], 1, 2, 4818, 8001, "deck/LV1_sukego.ydc\x00\x00\x00", 0,
["Has Back-row removal"]),
OpponentData(2, "Skull Servant", [], 1, 3, 4030, 8002, "deck/LV1_waito.ydc\x00\x00\x00\x00", 0,
["Has Back-row removal"]),
OpponentData(3, "Watapon", [], 1, 4, 6092, 8003, "deck/LV1_watapon.ydc\x00\x00", 0, ["Has Back-row removal"]),
OpponentData(4, "White Magician Pikeru", [], 1, 5, 5975, 8004, "deck/LV1_pikeru.ydc\x00\x00\x00"),
# Tier 2
OpponentData(5, "Battery Man C", ["Quick-Finish"], 2, 1, 6428, 8005, "deck/LV2_denti.ydc\x00\x00\x00\x00", 1),
OpponentData(6, "Ojama Yellow", [], 2, 2, 5811, 8006, "deck/LV2_ojama.ydc\x00\x00\x00\x00", 1,
["Has Back-row removal"]),
OpponentData(7, "Goblin King", ["Quick-Finish"], 2, 3, 5973, 8007, "deck/LV2_kingG.ydc\x00\x00\x00\x00", 1),
OpponentData(8, "Des Frog", ["Quick-Finish"], 2, 4, 6424, 8008, "deck/LV2_kaeru.ydc\x00\x00\x00\x00", 1),
OpponentData(9, "Water Dragon", ["Quick-Finish"], 2, 5, 6481, 8009, "deck/LV2_waterD.ydc\x00\x00\x00", 1),
# Tier 3
OpponentData(10, "Red-Eyes Darkness Dragon", ["Quick-Finish"], 3, 1, 6292, 8010, "deck/LV3_RedEyes.ydc\x00\x00",
2),
OpponentData(11, "Vampire Genesis", ["Quick-Finish"], 3, 2, 6293, 8011, "deck/LV3_vamp.ydc\x00\x00\x00\x00\x00",
2),
OpponentData(12, "Infernal Flame Emperor", [], 3, 3, 6368, 8012, "deck/LV3_flame.ydc\x00\x00\x00\x00", 2,
["Has Back-row removal"]),
OpponentData(13, "Ocean Dragon Lord - Neo-Daedalus", [], 3, 4, 6376, 8013, "deck/LV3_daidaros.ydc\x00", 2,
["Has Back-row removal"]),
OpponentData(14, "Helios Duo Megiste", ["Quick-Finish"], 3, 5, 6647, 8014, "deck/LV3_heriosu.ydc\x00\x00", 2),
# Tier 4
OpponentData(15, "Gilford the Legend", ["Quick-Finish"], 4, 1, 6456, 8015, "deck/LV4_gilfo.ydc\x00\x00\x00\x00",
3),
OpponentData(16, "Dark Eradicator Warlock", ["Quick-Finish"], 4, 2, 6530, 8016, "deck/LV4_kuromadou.ydc", 3),
OpponentData(17, "Guardian Exode", [], 4, 3, 6640, 8017, "deck/LV4_exodo.ydc\x00\x00\x00\x00", 3),
OpponentData(18, "Goldd, Wu-Lord of Dark World", ["Quick-Finish"], 4, 4, 6505, 8018, "deck/LV4_ankokukai.ydc",
3),
OpponentData(19, "Elemental Hero Erikshieler", ["Quick-Finish"], 4, 5, 6639, 8019,
"deck/LV4_Ehero.ydc\x00\x00\x00\x00", 3),
# Tier 5
OpponentData(20, "Raviel, Lord of Phantasms", [], 5, 1, 6565, 8020, "deck/LV5_ravieru.ydc\x00\x00", 4),
OpponentData(21, "Horus the Black Flame Dragon LV8", [], 5, 2, 6100, 8021, "deck/LV5_horus.ydc\x00\x00\x00\x00",
4),
OpponentData(22, "Stronghold", [], 5, 3, 6153, 8022, "deck/LV5_gadget.ydc\x00\x00\x00", 5),
OpponentData(23, "Sacred Phoenix of Nephthys", [], 5, 4, 6236, 8023, "deck/LV5_nephthys.ydc\x00", 6),
OpponentData(24, "Cyber End Dragon", ["Goal"], 5, 5, 6397, 8024, "deck/LV5_cyber.ydc\x00\x00\x00\x00", 7),
]
world = multiworld.worlds[player]
if not randomize:
return opponents_table
opponents = opponents_table + challenge_opponents
start = world.random.choice([o for o in opponents if o.tier == 1 and len(o.additional_info) == 0])
opponents.remove(start)
goal = world.random.choice([o for o in opponents if "Goal" in o.campaign_info])
opponents.remove(goal)
world.random.shuffle(opponents)
chosen_ones = opponents[:23]
for item in (multiworld.precollected_items[player]):
if item.name in tier_1_opponents:
# convert item index to opponent index
chosen_ones.insert(item_to_index[item.name] - item_to_index["Campaign Tier 1 Column 1"], start)
break
chosen_ones.append(goal)
tier = 1
column = 1
recreation = []
for opp in chosen_ones:
recreation.append(OpponentData(opp.id, opp.name, opp.campaign_info, tier, column, opp.card_id,
opp.deck_name_id, opp.deck_file, opp.difficulty))
column += 1
if column > 5:
column = 1
tier += 1
return recreation
def get_opponent_locations(opponent: OpponentData) -> Dict[str, Optional[Union[str, int]]]:
location = {opponent.name + " Beaten": "Tier " + str(opponent.tier) + " Beaten"}
if opponent.tier > 4 and opponent.column != 5:
name = "Campaign Tier 5: Column " + str(opponent.column) + " Win"
# return a int instead so a item can be placed at this location later
location[name] = special[name]
for info in opponent.campaign_info:
location[opponent.name + "-> " + info] = info
return location
def get_opponent_condition(opponent: OpponentData, unlock_item: str, unlock_amount: int, player: int,
is_challenge: bool) -> CollectionRule:
if is_challenge:
return lambda state: (
state.has(unlock_item, player, unlock_amount)
and yugioh06_difficulty(state, player, opponent.difficulty)
and state.has_all(opponent.additional_info, player)
)
else:
return lambda state: (
state.has_group(unlock_item, player, unlock_amount)
and yugioh06_difficulty(state, player, opponent.difficulty)
and state.has_all(opponent.additional_info, player)
)

195
worlds/yugioh06/options.py Normal file
View File

@@ -0,0 +1,195 @@
from dataclasses import dataclass
from Options import Choice, DefaultOnToggle, PerGameCommonOptions, Range, Toggle
class StructureDeck(Choice):
"""Which Structure Deck you start with"""
display_name = "Structure Deck"
option_dragons_roar = 0
option_zombie_madness = 1
option_blazing_destruction = 2
option_fury_from_the_deep = 3
option_warriors_triumph = 4
option_spellcasters_judgement = 5
option_none = 6
option_random_deck = 7
default = 7
class Banlist(Choice):
"""Which Banlist you start with"""
display_name = "Banlist"
option_no_banlist = 0
option_september_2003 = 1
option_march_2004 = 2
option_september_2004 = 3
option_march_2005 = 4
option_september_2005 = 5
default = option_september_2005
class FinalCampaignBossUnlockCondition(Choice):
"""How to unlock the final campaign boss and goal for the world"""
display_name = "Final Campaign Boss unlock Condition"
option_campaign_opponents = 0
option_challenges = 1
class FourthTier5UnlockCondition(Choice):
"""How to unlock the fourth campaign boss"""
display_name = "Fourth Tier 5 Campaign Boss unlock Condition"
option_campaign_opponents = 0
option_challenges = 1
class ThirdTier5UnlockCondition(Choice):
"""How to unlock the third campaign boss"""
display_name = "Third Tier 5 Campaign Boss unlock Condition"
option_campaign_opponents = 0
option_challenges = 1
class FinalCampaignBossChallenges(Range):
"""Number of Limited/Theme Duels completed for the Final Campaign Boss to appear"""
display_name = "Final Campaign Boss challenges unlock amount"
range_start = 0
range_end = 91
default = 10
class FourthTier5CampaignBossChallenges(Range):
"""Number of Limited/Theme Duels completed for the Fourth Level 5 Campaign Opponent to appear"""
display_name = "Fourth Tier 5 Campaign Boss unlock amount"
range_start = 0
range_end = 91
default = 5
class ThirdTier5CampaignBossChallenges(Range):
"""Number of Limited/Theme Duels completed for the Third Level 5 Campaign Opponent to appear"""
display_name = "Third Tier 5 Campaign Boss unlock amount"
range_start = 0
range_end = 91
default = 2
class FinalCampaignBossCampaignOpponents(Range):
"""Number of Campaign Opponents Duels defeated for the Final Campaign Boss to appear"""
display_name = "Final Campaign Boss campaign opponent unlock amount"
range_start = 0
range_end = 24
default = 12
class FourthTier5CampaignBossCampaignOpponents(Range):
"""Number of Campaign Opponents Duels defeated for the Fourth Level 5 Campaign Opponent to appear"""
display_name = "Fourth Tier 5 Campaign Boss campaign opponent unlock amount"
range_start = 0
range_end = 23
default = 7
class ThirdTier5CampaignBossCampaignOpponents(Range):
"""Number of Campaign Opponents Duels defeated for the Third Level 5 Campaign Opponent to appear"""
display_name = "Third Tier 5 Campaign Boss campaign opponent unlock amount"
range_start = 0
range_end = 22
default = 3
class NumberOfChallenges(Range):
"""Number of random Limited/Theme Duels that are included. The rest will be inaccessible."""
display_name = "Number of Challenges"
range_start = 0
range_end = 91
default = 10
class StartingMoney(Range):
"""The amount of money you start with"""
display_name = "Starting Money"
range_start = 0
range_end = 100000
default = 3000
class MoneyRewardMultiplier(Range):
"""By which amount the campaign reward money is multiplied"""
display_name = "Money Reward Multiplier"
range_start = 1
range_end = 255
default = 20
class NormalizeBoostersPacks(DefaultOnToggle):
"""If enabled every booster pack costs the same otherwise vanilla cost is used"""
display_name = "Normalize Booster Packs"
class BoosterPackPrices(Range):
"""
Only Works if normalize booster packs is enabled.
Sets the amount that what every booster pack costs.
"""
display_name = "Booster Pack Prices"
range_start = 1
range_end = 3000
default = 100
class AddEmptyBanList(Toggle):
"""Adds a Ban List where everything is at 3 to the item pool"""
display_name = "Add Empty Ban List"
class CampaignOpponentsShuffle(Toggle):
"""Replaces the campaign with random opponents from the entire game"""
display_name = "Campaign Opponents Shuffle"
class OCGArts(Toggle):
"""Always use the OCG artworks for cards"""
display_name = "OCG Arts"
@dataclass
class Yugioh06Options(PerGameCommonOptions):
structure_deck: StructureDeck
banlist: Banlist
final_campaign_boss_unlock_condition: FinalCampaignBossUnlockCondition
fourth_tier_5_campaign_boss_unlock_condition: FourthTier5UnlockCondition
third_tier_5_campaign_boss_unlock_condition: ThirdTier5UnlockCondition
final_campaign_boss_challenges: FinalCampaignBossChallenges
fourth_tier_5_campaign_boss_challenges: FourthTier5CampaignBossChallenges
third_tier_5_campaign_boss_challenges: ThirdTier5CampaignBossChallenges
final_campaign_boss_campaign_opponents: FinalCampaignBossCampaignOpponents
fourth_tier_5_campaign_boss_campaign_opponents: FourthTier5CampaignBossCampaignOpponents
third_tier_5_campaign_boss_campaign_opponents: ThirdTier5CampaignBossCampaignOpponents
number_of_challenges: NumberOfChallenges
starting_money: StartingMoney
money_reward_multiplier: MoneyRewardMultiplier
normalize_boosters_packs: NormalizeBoostersPacks
booster_pack_prices: BoosterPackPrices
add_empty_banlist: AddEmptyBanList
campaign_opponents_shuffle: CampaignOpponentsShuffle
ocg_arts: OCGArts

Binary file not shown.

Binary file not shown.

Binary file not shown.

163
worlds/yugioh06/rom.py Normal file
View File

@@ -0,0 +1,163 @@
import hashlib
import math
import os
import struct
from settings import get_settings
import Utils
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
from worlds.AutoWorld import World
from .items import item_to_index
from .rom_values import banlist_ids, function_addresses, structure_deck_selection
MD5Europe = "020411d3b08f5639eb8cb878283f84bf"
MD5America = "b8a7c976b28172995fe9e465d654297a"
class YGO06ProcedurePatch(APProcedurePatch, APTokenMixin):
game = "Yu-Gi-Oh! 2006"
hash = MD5America
patch_file_ending = ".apygo06"
result_file_ending = ".gba"
procedure = [("apply_bsdiff4", ["base_patch.bsdiff4"]), ("apply_tokens", ["token_data.bin"])]
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()
def write_tokens(world: World, patch: YGO06ProcedurePatch):
structure_deck = structure_deck_selection.get(world.options.structure_deck.value)
# set structure deck
patch.write_token(APTokenTypes.WRITE, 0x000FD0AA, struct.pack("<B", structure_deck))
# set banlist
banlist = world.options.banlist
patch.write_token(APTokenTypes.WRITE, 0xF4496, struct.pack("<B", banlist_ids.get(banlist.value)))
# set items to locations map
randomizer_data_start = 0x0000F310
for location in world.multiworld.get_locations(world.player):
item = location.item.name
if location.item.player != world.player:
item = "Remote"
item_id = item_to_index.get(item)
if item_id is None:
continue
location_id = world.location_name_to_id[location.name] - 5730000
patch.write_token(APTokenTypes.WRITE, randomizer_data_start + location_id, struct.pack("<B", item_id))
# set starting inventory
inventory_map = [0 for i in range(32)]
starting_inventory = list(map(lambda i: i.name, world.multiworld.precollected_items[world.player]))
starting_inventory += world.options.start_inventory.value
for start_inventory in starting_inventory:
item_id = world.item_name_to_id[start_inventory] - 5730001
index = math.floor(item_id / 8)
bit = item_id % 8
inventory_map[index] = inventory_map[index] | (1 << bit)
for i in range(32):
patch.write_token(APTokenTypes.WRITE, 0xE9DC + i, struct.pack("<B", inventory_map[i]))
# set unlock conditions for the last 3 campaign opponents
third_tier_5 = (
world.options.third_tier_5_campaign_boss_challenges.value
if world.options.third_tier_5_campaign_boss_unlock_condition.value == 1
else world.options.third_tier_5_campaign_boss_campaign_opponents.value
)
patch.write_token(APTokenTypes.WRITE, 0xEEFA, struct.pack("<B", third_tier_5))
fourth_tier_5 = (
world.options.fourth_tier_5_campaign_boss_challenges.value
if world.options.fourth_tier_5_campaign_boss_unlock_condition.value == 1
else world.options.fourth_tier_5_campaign_boss_campaign_opponents.value
)
patch.write_token(APTokenTypes.WRITE, 0xEF10, struct.pack("<B", fourth_tier_5))
final = (
world.options.final_campaign_boss_challenges.value
if world.options.final_campaign_boss_unlock_condition.value == 1
else world.options.final_campaign_boss_campaign_opponents.value
)
patch.write_token(APTokenTypes.WRITE, 0xEF22, struct.pack("<B", final))
patch.write_token(
APTokenTypes.WRITE,
0xEEF8,
struct.pack(
"<B",
int((function_addresses.get(world.options.third_tier_5_campaign_boss_unlock_condition.value) - 0xEEFA) / 2),
),
)
patch.write_token(
APTokenTypes.WRITE,
0xEF0E,
struct.pack(
"<B",
int(
(function_addresses.get(world.options.fourth_tier_5_campaign_boss_unlock_condition.value) - 0xEF10) / 2
),
),
)
patch.write_token(
APTokenTypes.WRITE,
0xEF20,
struct.pack(
"<B", int((function_addresses.get(world.options.final_campaign_boss_unlock_condition.value) - 0xEF22) / 2)
),
)
# set starting money
patch.write_token(APTokenTypes.WRITE, 0xF4734, struct.pack("<I", world.options.starting_money))
patch.write_token(APTokenTypes.WRITE, 0xE70C, struct.pack("<B", world.options.money_reward_multiplier.value))
patch.write_token(APTokenTypes.WRITE, 0xE6E4, struct.pack("<B", world.options.money_reward_multiplier.value))
# normalize booster packs if option is set
if world.options.normalize_boosters_packs.value:
booster_pack_price = world.options.booster_pack_prices.value.to_bytes(2, "little")
for booster in range(51):
space = booster * 16
patch.write_token(APTokenTypes.WRITE, 0x1E5E2E8 + space, struct.pack("<B", booster_pack_price[0]))
patch.write_token(APTokenTypes.WRITE, 0x1E5E2E9 + space, struct.pack("<B", booster_pack_price[1]))
patch.write_token(APTokenTypes.WRITE, 0x1E5E2EA + space, struct.pack("<B", 5))
# set shuffled campaign opponents if option is set
if world.options.campaign_opponents_shuffle.value:
i = 0
for opp in world.campaign_opponents:
space = i * 32
patch.write_token(APTokenTypes.WRITE, 0x000F3BA + i, struct.pack("<B", opp.id))
patch.write_token(APTokenTypes.WRITE, 0x1E58D0E + space, struct.pack("<H", opp.card_id))
patch.write_token(APTokenTypes.WRITE, 0x1E58D10 + space, struct.pack("<H", opp.deck_name_id))
for j, b in enumerate(opp.deck_file.encode("ascii")):
patch.write_token(APTokenTypes.WRITE, 0x1E58D12 + space + j, struct.pack("<B", b))
i = i + 1
for j, b in enumerate(world.romName):
patch.write_token(APTokenTypes.WRITE, 0x10 + j, struct.pack("<B", b))
for j, b in enumerate(world.playerName):
patch.write_token(APTokenTypes.WRITE, 0x30 + j, struct.pack("<B", b))
patch.write_file("token_data.bin", patch.get_token_binary())
def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path(file_name)
base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
md5hash = basemd5.hexdigest()
if MD5Europe != md5hash and MD5America != md5hash:
raise Exception(
"Supplied Base Rom does not match known MD5 for"
"Yu-Gi-Oh! World Championship 2006 America or Europe "
"Get the correct game and version, then dump it"
)
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
if not file_name:
file_name = get_settings().yugioh06_settings.rom_file
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name

View File

@@ -0,0 +1,38 @@
structure_deck_selection = {
# DRAGON'S ROAR
0: 0x1,
# ZOMBIE MADNESS
1: 0x5,
# BLAZING DESTRUCTION
2: 0x9,
# FURY FROM THE DEEP
3: 0xD,
# Warrior'S TRIUMPH
4: 0x11,
# SPELLCASTER'S JUDGEMENT
5: 0x15,
# Draft Mode
6: 0x1,
}
banlist_ids = {
# NoList
0: 0x0,
# September 2003
1: 0x5,
# March 2004
2: 0x6,
# September 2004
3: 0x7,
# March 2005
4: 0x8,
# September 2005
5: 0x9,
}
function_addresses = {
# Count Campaign Opponents
0: 0xF0C8,
# Count Challenges
1: 0xEF3A,
}

12
worlds/yugioh06/ruff.toml Normal file
View File

@@ -0,0 +1,12 @@
line-length = 120
[lint]
preview = true
select = ["E", "F", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"]
ignore = ["RUF012", "RUF100"]
[per-file-ignores]
# The way options definitions work right now, world devs are forced to break line length requirements.
"options.py" = ["E501"]
# Yu Gi Oh specific: The structure of the Opponents.py file makes the line length violations acceptable.
"Opponents.py" = ["E501"]

868
worlds/yugioh06/rules.py Normal file
View File

@@ -0,0 +1,868 @@
from worlds.generic.Rules import add_rule
from . import yugioh06_difficulty
from .fusions import count_has_materials
def set_rules(world):
player = world.player
multiworld = world.multiworld
location_rules = {
# Campaign
"Campaign Tier 1: 1 Win": lambda state: state.has("Tier 1 Beaten", player),
"Campaign Tier 1: 3 Wins A": lambda state: state.has("Tier 1 Beaten", player, 3),
"Campaign Tier 1: 3 Wins B": lambda state: state.has("Tier 1 Beaten", player, 3),
"Campaign Tier 1: 5 Wins A": lambda state: state.has("Tier 1 Beaten", player, 5),
"Campaign Tier 1: 5 Wins B": lambda state: state.has("Tier 1 Beaten", player, 5),
"Campaign Tier 2: 1 Win": lambda state: state.has("Tier 2 Beaten", player),
"Campaign Tier 2: 3 Wins A": lambda state: state.has("Tier 2 Beaten", player, 3),
"Campaign Tier 2: 3 Wins B": lambda state: state.has("Tier 2 Beaten", player, 3),
"Campaign Tier 2: 5 Wins A": lambda state: state.has("Tier 2 Beaten", player, 5),
"Campaign Tier 2: 5 Wins B": lambda state: state.has("Tier 2 Beaten", player, 5),
"Campaign Tier 3: 1 Win": lambda state: state.has("Tier 3 Beaten", player),
"Campaign Tier 3: 3 Wins A": lambda state: state.has("Tier 3 Beaten", player, 3),
"Campaign Tier 3: 3 Wins B": lambda state: state.has("Tier 3 Beaten", player, 3),
"Campaign Tier 3: 5 Wins A": lambda state: state.has("Tier 3 Beaten", player, 5),
"Campaign Tier 3: 5 Wins B": lambda state: state.has("Tier 3 Beaten", player, 5),
"Campaign Tier 4: 5 Wins A": lambda state: state.has("Tier 4 Beaten", player, 5),
"Campaign Tier 4: 5 Wins B": lambda state: state.has("Tier 4 Beaten", player, 5),
# Bonuses
"Duelist Bonus Level 1": lambda state: state.has("Tier 1 Beaten", player),
"Duelist Bonus Level 2": lambda state: state.has("Tier 2 Beaten", player),
"Duelist Bonus Level 3": lambda state: state.has("Tier 3 Beaten", player),
"Duelist Bonus Level 4": lambda state: state.has("Tier 4 Beaten", player),
"Duelist Bonus Level 5": lambda state: state.has("Tier 5 Beaten", player),
"Max ATK Bonus": lambda state: yugioh06_difficulty(state, player, 2),
"No Spell Cards Bonus": lambda state: yugioh06_difficulty(state, player, 2),
"No Trap Cards Bonus": lambda state: yugioh06_difficulty(state, player, 2),
"No Damage Bonus": lambda state: state.has_group("Campaign Boss Beaten", player, 3),
"Low Deck Bonus": lambda state: state.has_any(["Reasoning", "Monster Gate", "Magical Merchant"], player) and
yugioh06_difficulty(state, player, 3),
"Extremely Low Deck Bonus":
lambda state: state.has_any(["Reasoning", "Monster Gate", "Magical Merchant"], player) and
yugioh06_difficulty(state, player, 2),
"Opponent's Turn Finish Bonus": lambda state: yugioh06_difficulty(state, player, 2),
"Exactly 0 LP Bonus": lambda state: yugioh06_difficulty(state, player, 2),
"Reversal Finish Bonus": lambda state: yugioh06_difficulty(state, player, 2),
"Quick Finish Bonus": lambda state: state.has("Quick-Finish", player) or yugioh06_difficulty(state, player, 6),
"Exodia Finish Bonus": lambda state: state.has("Can Exodia Win", player),
"Last Turn Finish Bonus": lambda state: state.has("Can Last Turn Win", player),
"Yata-Garasu Finish Bonus": lambda state: state.has("Can Yata Lock", player),
"Skull Servant Finish Bonus": lambda state: state.has("Skull Servant", player) and
yugioh06_difficulty(state, player, 3),
"Konami Bonus": lambda state: state.has_all(["Messenger of Peace", "Castle of Dark Illusions", "Mystik Wok"],
player) or (state.has_all(["Mystik Wok", "Barox", "Cyber-Stein",
"Poison of the Old Man"],
player) and yugioh06_difficulty(state,
player, 8)),
"Max Damage Bonus": lambda state: state.has_any(["Wave-Motion Cannon", "Megamorph", "United We Stand",
"Mage Power"], player),
"Fusion Summon Bonus": lambda state: state.has_any(["Polymerization", "Fusion Gate", "Power Bond"], player),
"Ritual Summon Bonus": lambda state: state.has("Ritual", player),
"Over 20000 LP Bonus": lambda state: can_gain_lp_every_turn(state, player)
and state.has("Can Stall with ST", player),
"Low LP Bonus": lambda state: state.has("Wall of Revealing Light", player) and yugioh06_difficulty(state, player,
2),
"Extremely Low LP Bonus": lambda state: state.has_all(["Wall of Revealing Light", "Messenger of Peace"], player)
and yugioh06_difficulty(state, player, 4),
"Effect Damage Only Bonus": lambda state: state.has_all(["Solar Flare Dragon", "UFO Turtle"], player)
or state.has("Wave-Motion Cannon", player)
or state.can_reach("Final Countdown Finish Bonus", "Location", player)
or state.can_reach("Destiny Board Finish Bonus", "Location", player)
or state.has("Can Exodia Win", player)
or state.has("Can Last Turn Win", player),
"No More Cards Bonus": lambda state: state.has_any(["Cyber Jar", "Morphing Jar",
"Morphing Jar #2", "Needle Worm"], player)
and state.has_any(["The Shallow Grave", "Spear Cretin"],
player) and yugioh06_difficulty(state, player, 5),
"Final Countdown Finish Bonus": lambda state: state.has("Final Countdown", player)
and state.has("Can Stall with ST", player),
"Destiny Board Finish Bonus": lambda state: state.has("Can Stall with Monsters", player) and
state.has("Destiny Board and its letters", player) and
state.has("A Cat of Ill Omen", player),
# Cards
"Obtain all pieces of Exodia": lambda state: state.has("Exodia", player),
"Obtain Final Countdown": lambda state: state.has("Final Countdown", player),
"Obtain Victory Dragon": lambda state: state.has("Victory D.", player),
"Obtain Ojama Delta Hurricane and its required cards":
lambda state: state.has("Ojama Delta Hurricane and required cards", player),
"Obtain Huge Revolution and its required cards":
lambda state: state.has("Huge Revolution and its required cards", player),
"Obtain Perfectly Ultimate Great Moth and its required cards":
lambda state: state.has("Perfectly Ultimate Great Moth and its required cards", player),
"Obtain Valkyrion the Magna Warrior and its pieces":
lambda state: state.has("Valkyrion the Magna Warrior and its pieces", player),
"Obtain Dark Sage and its required cards": lambda state: state.has("Dark Sage and its required cards", player),
"Obtain Destiny Board and its letters": lambda state: state.has("Destiny Board and its letters", player),
"Obtain all XYZ-Dragon Cannon fusions and their materials":
lambda state: state.has("XYZ-Dragon Cannon fusions and their materials", player),
"Obtain VWXYZ-Dragon Catapult Cannon and the fusion materials":
lambda state: state.has("VWXYZ-Dragon Catapult Cannon and the fusion materials", player),
"Obtain Hamon, Lord of Striking Thunder":
lambda state: state.has("Hamon, Lord of Striking Thunder", player),
"Obtain Raviel, Lord of Phantasms":
lambda state: state.has("Raviel, Lord of Phantasms", player),
"Obtain Uria, Lord of Searing Flames":
lambda state: state.has("Uria, Lord of Searing Flames", player),
"Obtain Gate Guardian and its pieces":
lambda state: state.has("Gate Guardian and its pieces", player),
"Obtain Dark Scorpion Combination and its required cards":
lambda state: state.has("Dark Scorpion Combination and its required cards", player),
# Collection Events
"Ojama Delta Hurricane and required cards":
lambda state: state.has_all(["Ojama Delta Hurricane", "Ojama Green", "Ojama Yellow", "Ojama Black"],
player),
"Huge Revolution and its required cards":
lambda state: state.has_all(["Huge Revolution", "Oppressed People", "United Resistance",
"People Running About"], player),
"Perfectly Ultimate Great Moth and its required cards":
lambda state: state.has_all(["Perfectly Ultimate Great Moth", "Petit Moth", "Cocoon of Evolution"], player),
"Valkyrion the Magna Warrior and its pieces":
lambda state: state.has_all(["Valkyrion the Magna Warrior", "Alpha the Magnet Warrior",
"Beta the Magnet Warrior", "Gamma the Magnet Warrior"], player),
"Dark Sage and its required cards":
lambda state: state.has_all(["Dark Sage", "Dark Magician", "Time Wizard"], player),
"Destiny Board and its letters":
lambda state: state.has_all(["Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'",
"Spirit Message 'A'", "Spirit Message 'L'"], player),
"XYZ-Dragon Cannon fusions and their materials":
lambda state: state.has_all(["X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank",
"XY-Dragon Cannon", "XZ-Tank Cannon", "YZ-Tank Dragon", "XYZ-Dragon Cannon"],
player),
"VWXYZ-Dragon Catapult Cannon and the fusion materials":
lambda state: state.has_all(["X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", "XYZ-Dragon Cannon",
"V-Tiger Jet", "W-Wing Catapult", "VW-Tiger Catapult",
"VWXYZ-Dragon Catapult Cannon"],
player),
"Gate Guardian and its pieces":
lambda state: state.has_all(["Gate Guardian", "Kazejin", "Suijin", "Sanga of the Thunder"], player),
"Dark Scorpion Combination and its required cards":
lambda state: state.has_all(["Dark Scorpion Combination", "Don Zaloog", "Dark Scorpion - Chick the Yellow",
"Dark Scorpion - Meanae the Thorn", "Dark Scorpion - Gorg the Strong",
"Cliff the Trap Remover"], player),
"Can Exodia Win":
lambda state: state.has_all(["Exodia", "Heart of the Underdog"], player),
"Can Last Turn Win":
lambda state: state.has_all(["Last Turn", "Wall of Revealing Light"], player) and
(state.has_any(["Jowgen the Spiritualist", "Jowls of Dark Demise", "Non Aggression Area"],
player)
or state.has_all(["Cyber-Stein", "The Last Warrior from Another Planet"], player)),
"Can Yata Lock":
lambda state: state.has_all(["Yata-Garasu", "Chaos Emperor Dragon - Envoy of the End", "Sangan"], player)
and state.has_any(["No Banlist", "Banlist September 2003"], player),
"Can Stall with Monsters":
lambda state: state.count_from_list_exclusive(
["Spirit Reaper", "Giant Germ", "Marshmallon", "Nimble Momonga"], player) >= 2,
"Can Stall with ST":
lambda state: state.count_from_list_exclusive(["Level Limit - Area B", "Gravity Bind", "Messenger of Peace"],
player) >= 2,
"Has Back-row removal":
lambda state: back_row_removal(state, player)
}
access_rules = {
# Limited
"LD01 All except Level 4 forbidden":
lambda state: yugioh06_difficulty(state, player, 2),
"LD02 Medium/high Level forbidden":
lambda state: yugioh06_difficulty(state, player, 1),
"LD03 ATK 1500 or more forbidden":
lambda state: yugioh06_difficulty(state, player, 4),
"LD04 Flip Effects forbidden":
lambda state: yugioh06_difficulty(state, player, 1),
"LD05 Tributes forbidden":
lambda state: yugioh06_difficulty(state, player, 1),
"LD06 Traps forbidden":
lambda state: yugioh06_difficulty(state, player, 1),
"LD07 Large Deck A":
lambda state: yugioh06_difficulty(state, player, 4),
"LD08 Large Deck B":
lambda state: yugioh06_difficulty(state, player, 4),
"LD09 Sets Forbidden":
lambda state: yugioh06_difficulty(state, player, 1),
"LD10 All except LV monsters forbidden":
lambda state: only_level(state, player) and yugioh06_difficulty(state, player, 2),
"LD11 All except Fairies forbidden":
lambda state: only_fairy(state, player) and yugioh06_difficulty(state, player, 2),
"LD12 All except Wind forbidden":
lambda state: only_wind(state, player) and yugioh06_difficulty(state, player, 2),
"LD13 All except monsters forbidden":
lambda state: yugioh06_difficulty(state, player, 3),
"LD14 Level 3 or below forbidden":
lambda state: yugioh06_difficulty(state, player, 1),
"LD15 DEF 1500 or less forbidden":
lambda state: yugioh06_difficulty(state, player, 3),
"LD16 Effect Monsters forbidden":
lambda state: only_normal(state, player) and yugioh06_difficulty(state, player, 4),
"LD17 Spells forbidden":
lambda state: yugioh06_difficulty(state, player, 3),
"LD18 Attacks forbidden":
lambda state: state.has_all(["Wave-Motion Cannon", "Stealth Bird"], player)
and state.count_from_list_exclusive(["Dark World Lightning", "Nobleman of Crossout",
"Shield Crash", "Tribute to the Doomed"], player) >= 2
and yugioh06_difficulty(state, player, 3),
"LD19 All except E-Hero's forbidden":
lambda state: state.has_any(["Polymerization", "Fusion Gate"], player) and
count_has_materials(state, ["Elemental Hero Flame Wingman",
"Elemental Hero Madballman",
"Elemental Hero Rampart Blaster",
"Elemental Hero Steam Healer",
"Elemental Hero Shining Flare Wingman",
"Elemental Hero Wildedge"], player) >= 3 and
yugioh06_difficulty(state, player, 3),
"LD20 All except Warriors forbidden":
lambda state: only_warrior(state, player) and yugioh06_difficulty(state, player, 2),
"LD21 All except Dark forbidden":
lambda state: only_dark(state, player) and yugioh06_difficulty(state, player, 2),
"LD22 All limited cards forbidden":
lambda state: yugioh06_difficulty(state, player, 3),
"LD23 Refer to Mar 05 Banlist":
lambda state: yugioh06_difficulty(state, player, 5),
"LD24 Refer to Sept 04 Banlist":
lambda state: yugioh06_difficulty(state, player, 5),
"LD25 Low Life Points":
lambda state: yugioh06_difficulty(state, player, 5),
"LD26 All except Toons forbidden":
lambda state: only_toons(state, player) and yugioh06_difficulty(state, player, 2),
"LD27 All except Spirits forbidden":
lambda state: only_spirit(state, player) and yugioh06_difficulty(state, player, 2),
"LD28 All except Dragons forbidden":
lambda state: only_dragon(state, player) and yugioh06_difficulty(state, player, 2),
"LD29 All except Spellcasters forbidden":
lambda state: only_spellcaster(state, player) and yugioh06_difficulty(state, player, 2),
"LD30 All except Light forbidden":
lambda state: only_light(state, player) and yugioh06_difficulty(state, player, 2),
"LD31 All except Fire forbidden":
lambda state: only_fire(state, player) and yugioh06_difficulty(state, player, 2),
"LD32 Decks with multiples forbidden":
lambda state: yugioh06_difficulty(state, player, 4),
"LD33 Special Summons forbidden":
lambda state: yugioh06_difficulty(state, player, 2),
"LD34 Normal Summons forbidden":
lambda state: state.has_all(["Polymerization", "King of the Swamp"], player) and
count_has_materials(state, ["Elemental Hero Flame Wingman",
"Elemental Hero Madballman",
"Elemental Hero Rampart Blaster",
"Elemental Hero Steam Healer",
"Elemental Hero Shining Flare Wingman",
"Elemental Hero Wildedge"], player) >= 3 and
yugioh06_difficulty(state, player, 4),
"LD35 All except Zombies forbidden":
lambda state: only_zombie(state, player) and yugioh06_difficulty(state, player, 2),
"LD36 All except Earth forbidden":
lambda state: only_earth(state, player) and yugioh06_difficulty(state, player, 2),
"LD37 All except Water forbidden":
lambda state: only_water(state, player) and yugioh06_difficulty(state, player, 2),
"LD38 Refer to Mar 04 Banlist":
lambda state: yugioh06_difficulty(state, player, 4),
"LD39 Monsters forbidden":
lambda state: state.has_all(["Skull Zoma", "Embodiment of Apophis"], player)
and yugioh06_difficulty(state, player, 5),
"LD40 Refer to Sept 05 Banlist":
lambda state: yugioh06_difficulty(state, player, 5),
"LD41 Refer to Sept 03 Banlist":
lambda state: yugioh06_difficulty(state, player, 5),
# Theme Duels
"TD01 Battle Damage":
lambda state: yugioh06_difficulty(state, player, 1),
"TD02 Deflected Damage":
lambda state: state.has("Fairy Box", player) and yugioh06_difficulty(state, player, 1),
"TD03 Normal Summon":
lambda state: yugioh06_difficulty(state, player, 3),
"TD04 Ritual Summon":
lambda state: yugioh06_difficulty(state, player, 3) and
state.has_all(["Contract with the Abyss",
"Manju of the Ten Thousand Hands",
"Senju of the Thousand Hands",
"Sonic Bird",
"Pot of Avarice",
"Dark Master - Zorc",
"Demise, King of Armageddon",
"The Masked Beast",
"Magician of Black Chaos",
"Dark Magic Ritual"], player),
"TD05 Special Summon A":
lambda state: yugioh06_difficulty(state, player, 3),
"TD06 20x Spell":
lambda state: state.has("Magical Blast", player) and yugioh06_difficulty(state, player, 3),
"TD07 10x Trap":
lambda state: yugioh06_difficulty(state, player, 3),
"TD08 Draw":
lambda state: state.has_any(["Self-Destruct Button", "Dark Snake Syndrome"], player) and
yugioh06_difficulty(state, player, 3),
"TD09 Hand Destruction":
lambda state: state.has_all(["Cyber Jar",
"Morphing Jar",
"Book of Moon",
"Book of Taiyou",
"Card Destruction",
"Serial Spell",
"Spell Reproduction",
"The Shallow Grave"], player) and yugioh06_difficulty(state, player, 3),
"TD10 During Opponent's Turn":
lambda state: yugioh06_difficulty(state, player, 3),
"TD11 Recover":
lambda state: can_gain_lp_every_turn(state, player) and yugioh06_difficulty(state, player, 3),
"TD12 Remove Monsters by Effect":
lambda state: state.has("Soul Release", player) and yugioh06_difficulty(state, player, 2),
"TD13 Flip Summon":
lambda state: pacman_deck(state, player) and yugioh06_difficulty(state, player, 2),
"TD14 Special Summon B":
lambda state: state.has_any(["Manticore of Darkness", "Treeborn Frog"], player) and
state.has("Foolish Burial", player) and
yugioh06_difficulty(state, player, 2),
"TD15 Token":
lambda state: state.has_all(["Dandylion", "Ojama Trio", "Stray Lambs"], player) and
yugioh06_difficulty(state, player, 3),
"TD16 Union":
lambda state: equip_unions(state, player) and
yugioh06_difficulty(state, player, 2),
"TD17 10x Quick Spell":
lambda state: quick_plays(state, player) and
yugioh06_difficulty(state, player, 3),
"TD18 The Forbidden":
lambda state: state.has("Can Exodia Win", player),
"TD19 20 Turns":
lambda state: state.has("Final Countdown", player) and state.has("Can Stall with ST", player) and
yugioh06_difficulty(state, player, 3),
"TD20 Deck Destruction":
lambda state: state.has_any(["Cyber Jar", "Morphing Jar", "Morphing Jar #2", "Needle Worm"], player)
and state.has_any(["The Shallow Grave", "Spear Cretin"],
player) and yugioh06_difficulty(state, player, 2),
"TD21 Victory D.":
lambda state: state.has("Victory D.", player) and only_dragon(state, player)
and yugioh06_difficulty(state, player, 3),
"TD22 The Preventers Fight Back":
lambda state: state.has("Ojama Delta Hurricane and required cards", player) and
state.has_all(["Rescue Cat", "Enchanting Fitting Room", "Jerry Beans Man"], player) and
yugioh06_difficulty(state, player, 3),
"TD23 Huge Revolution":
lambda state: state.has("Huge Revolution and its required cards", player) and
state.has_all(["Enchanting Fitting Room", "Jerry Beans Man"], player) and
yugioh06_difficulty(state, player, 3),
"TD24 Victory in 5 Turns":
lambda state: yugioh06_difficulty(state, player, 3),
"TD25 Moth Grows Up":
lambda state: state.has("Perfectly Ultimate Great Moth and its required cards", player) and
state.has_all(["Gokipon", "Howling Insect"], player) and
yugioh06_difficulty(state, player, 3),
"TD26 Magnetic Power":
lambda state: state.has("Valkyrion the Magna Warrior and its pieces", player) and
yugioh06_difficulty(state, player, 2),
"TD27 Dark Sage":
lambda state: state.has("Dark Sage and its required cards", player) and
state.has_any(["Skilled Dark Magician", "Dark Magic Curtain"], player) and
yugioh06_difficulty(state, player, 2),
"TD28 Direct Damage":
lambda state: yugioh06_difficulty(state, player, 2),
"TD29 Destroy Monsters in Battle":
lambda state: yugioh06_difficulty(state, player, 2),
"TD30 Tribute Summon":
lambda state: state.has("Treeborn Frog", player) and yugioh06_difficulty(state, player, 2),
"TD31 Special Summon C":
lambda state: state.count_from_list_exclusive(
["Aqua Spirit", "Rock Spirit", "Spirit of Flames",
"Garuda the Wind Spirit", "Gigantes", "Inferno", "Megarock Dragon", "Silpheed"],
player) > 4 and yugioh06_difficulty(state, player, 3),
"TD32 Toon":
lambda state: only_toons(state, player) and yugioh06_difficulty(state, player, 3),
"TD33 10x Counter":
lambda state: counter_traps(state, player) and yugioh06_difficulty(state, player, 2),
"TD34 Destiny Board":
lambda state: state.has("Destiny Board and its letters", player)
and state.has("Can Stall with Monsters", player)
and state.has("A Cat of Ill Omen", player)
and yugioh06_difficulty(state, player, 2),
"TD35 Huge Damage in a Turn":
lambda state: state.has_all(["Cyber-Stein", "Cyber Twin Dragon", "Megamorph"], player)
and yugioh06_difficulty(state, player, 3),
"TD36 V-Z In the House":
lambda state: state.has("VWXYZ-Dragon Catapult Cannon and the fusion materials", player)
and yugioh06_difficulty(state, player, 3),
"TD37 Uria, Lord of Searing Flames":
lambda state: state.has_all(["Uria, Lord of Searing Flames",
"Embodiment of Apophis",
"Skull Zoma",
"Metal Reflect Slime"], player)
and yugioh06_difficulty(state, player, 3),
"TD38 Hamon, Lord of Striking Thunder":
lambda state: state.has("Hamon, Lord of Striking Thunder", player)
and yugioh06_difficulty(state, player, 3),
"TD39 Raviel, Lord of Phantasms":
lambda state: state.has_all(["Raviel, Lord of Phantasms", "Giant Germ"], player) and
state.count_from_list_exclusive(["Archfiend Soldier",
"Skull Descovery Knight",
"Slate Warrior",
"D. D. Trainer",
"Earthbound Spirit"], player) >= 3
and yugioh06_difficulty(state, player, 3),
"TD40 Make a Chain":
lambda state: state.has("Ultimate Offering", player)
and yugioh06_difficulty(state, player, 4),
"TD41 The Gatekeeper Stands Tall":
lambda state: state.has("Gate Guardian and its pieces", player) and
state.has_all(["Treeborn Frog", "Tribute Doll"], player)
and yugioh06_difficulty(state, player, 4),
"TD42 Serious Damage":
lambda state: yugioh06_difficulty(state, player, 3),
"TD43 Return Monsters with Effects":
lambda state: state.has_all(["Penguin Soldier", "Messenger of Peace"], player)
and yugioh06_difficulty(state, player, 4),
"TD44 Fusion Summon":
lambda state: state.has_all(["Fusion Gate", "Terraforming", "Dimension Fusion",
"Return from the Different Dimension"], player) and
count_has_materials(state, ["Elemental Hero Flame Wingman",
"Elemental Hero Madballman",
"Elemental Hero Rampart Blaster",
"Elemental Hero Steam Healer",
"Elemental Hero Shining Flare Wingman",
"Elemental Hero Wildedge"], player) >= 4 and
yugioh06_difficulty(state, player, 7),
"TD45 Big Damage at once":
lambda state: state.has("Wave-Motion Cannon", player)
and yugioh06_difficulty(state, player, 3),
"TD46 XYZ In the House":
lambda state: state.has("XYZ-Dragon Cannon fusions and their materials", player) and
state.has("Dimension Fusion", player),
"TD47 Spell Counter":
lambda state: spell_counter(state, player) and yugioh06_difficulty(state, player, 3),
"TD48 Destroy Monsters with Effects":
lambda state: state.has_all(["Blade Rabbit", "Dream Clown"], player) and
state.has("Can Stall with ST", player) and
yugioh06_difficulty(state, player, 3),
"TD49 Plunder":
lambda state: take_control(state, player) and yugioh06_difficulty(state, player, 5),
"TD50 Dark Scorpion Combination":
lambda state: state.has("Dark Scorpion Combination and its required cards", player) and
state.has_all(["Reinforcement of the Army", "Mystic Tomato"], player) and
yugioh06_difficulty(state, player, 3)
}
multiworld.completion_condition[player] = lambda state: state.has("Goal", player)
for loc in multiworld.get_locations(player):
if loc.name in location_rules:
add_rule(loc, location_rules[loc.name])
if loc.name in access_rules:
add_rule(multiworld.get_entrance(loc.name, player), access_rules[loc.name])
def only_light(state, player):
return state.has_from_list_exclusive([
"Dunames Dark Witch",
"X-Head Cannon",
"Homunculus the Alchemic Being",
"Hysteric Fairy",
"Ninja Grandmaster Sasuke"], player, 2)\
and state.has_from_list_exclusive([
"Chaos Command Magician",
"Cybernetic Magician",
"Kaiser Glider",
"The Agent of Judgment - Saturn",
"Zaborg the Thunder Monarch",
"Cyber Dragon"], player, 1) \
and state.has_from_list_exclusive([
"D.D. Warrior Lady",
"Mystic Swordsman LV2",
"Y-Dragon Head",
"Z-Metal Tank",
], player, 2) and state.has("Shining Angel", player)
def only_dark(state, player):
return state.has_from_list_exclusive([
"Dark Elf",
"Archfiend Soldier",
"Mad Dog of Darkness",
"Vorse Raider",
"Skilled Dark Magician",
"Skull Descovery Knight",
"Mechanicalchaser",
"Dark Blade",
"Gil Garth",
"La Jinn the Mystical Genie of the Lamp",
"Opticlops",
"Zure, Knight of Dark World",
"Brron, Mad King of Dark World",
"D.D. Survivor",
"Exarion Universe",
"Kycoo the Ghost Destroyer",
"Regenerating Mummy"
], player, 2) \
and state.has_any([
"Summoned Skull",
"Skull Archfiend of Lightning",
"The End of Anubis",
"Dark Ruler Ha Des",
"Beast of Talwar",
"Inferno Hammer",
"Jinzo",
"Ryu Kokki"
], player) \
and state.has_from_list_exclusive([
"Legendary Fiend",
"Don Zaloog",
"Newdoria",
"Sangan",
"Spirit Reaper",
"Giant Germ"
], player, 2) and state.has("Mystic Tomato", player)
def only_earth(state, player):
return state.has_from_list_exclusive([
"Berserk Gorilla",
"Gemini Elf",
"Insect Knight",
"Toon Gemini Elf",
"Familiar-Possessed - Aussa",
"Neo Bug",
"Blindly Loyal Goblin",
"Chiron the Mage",
"Gearfried the Iron Knight"
], player, 2) and state.has_any([
"Dark Driceratops",
"Granmarg the Rock Monarch",
"Hieracosphinx",
"Saber Beetle"
], player) and state.has_from_list_exclusive([
"Hyper Hammerhead",
"Green Gadget",
"Red Gadget",
"Yellow Gadget",
"Dimensional Warrior",
"Enraged Muka Muka",
"Exiled Force"
], player, 2) and state.has("Giant Rat", player)
def only_water(state, player):
return state.has_from_list_exclusive([
"Gagagigo",
"Familiar-Possessed - Eria",
"7 Colored Fish",
"Sea Serpent Warrior of Darkness",
"Abyss Soldier"
], player, 2) and state.has_any([
"Giga Gagagigo",
"Amphibian Beast",
"Terrorking Salmon",
"Mobius the Frost Monarch"
], player) and state.has_from_list_exclusive([
"Revival Jam",
"Yomi Ship",
"Treeborn Frog"
], player, 2) and state.has("Mother Grizzly", player)
def only_fire(state, player):
return state.has_from_list_exclusive([
"Blazing Inpachi",
"Familiar-Possessed - Hiita",
"Great Angus",
"Fire Beaters"
], player, 2) and state.has_any([
"Thestalos the Firestorm Monarch",
"Horus the Black Flame Dragon LV6"
], player) and state.has_from_list_exclusive([
"Solar Flare Dragon",
"Tenkabito Shien",
"Ultimate Baseball Kid"
], player, 2) and state.has("UFO Turtle", player)
def only_wind(state, player):
return state.has_from_list_exclusive([
"Luster Dragon",
"Slate Warrior",
"Spear Dragon",
"Familiar-Possessed - Wynn",
"Harpie's Brother",
"Nin-Ken Dog",
"Cyber Harpie Lady",
"Oxygeddon"
], player, 2) and state.has_any([
"Cyber-Tech Alligator",
"Luster Dragon #2",
"Armed Dragon LV5",
"Roc from the Valley of Haze"
], player) and state.has_from_list_exclusive([
"Armed Dragon LV3",
"Twin-Headed Behemoth",
"Harpie Lady 1"
], player, 2) and state.has("Flying Kamakiri 1", player)
def only_fairy(state, player):
return state.has_any([
"Dunames Dark Witch",
"Hysteric Fairy"
], player) and (state.count_from_list_exclusive([
"Dunames Dark Witch",
"Hysteric Fairy",
"Dancing Fairy",
"Zolga",
"Shining Angel",
"Kelbek",
"Mudora",
"Asura Priest",
"Cestus of Dagla"
], player) + (state.has_any([
"The Agent of Judgment - Saturn",
"Airknight Parshath"
], player))) >= 7
def only_warrior(state, player):
return state.has_any([
"Dark Blade",
"Blindly Loyal Goblin",
"D.D. Survivor",
"Gearfried the Iron knight",
"Ninja Grandmaster Sasuke",
"Warrior Beaters"
], player) and (state.count_from_list_exclusive([
"Warrior Lady of the Wasteland",
"Exiled Force",
"Mystic Swordsman LV2",
"Dimensional Warrior",
"Dandylion",
"D.D. Assailant",
"Blade Knight",
"D.D. Warrior Lady",
"Marauding Captain",
"Command Knight",
"Reinforcement of the Army"
], player) + (state.has_any([
"Freed the Matchless General",
"Holy Knight Ishzark",
"Silent Swordsman Lv5"
], player))) >= 7
def only_zombie(state, player):
return state.has("Pyramid Turtle", player) \
and state.has_from_list_exclusive([
"Regenerating Mummy",
"Ryu Kokki",
"Spirit Reaper",
"Master Kyonshee",
"Curse of Vampire",
"Vampire Lord",
"Goblin Zombie",
"Curse of Vampire",
"Vampire Lord",
"Goblin Zombie",
"Book of Life",
"Call of the Mummy"
], player, 6)
def only_dragon(state, player):
return state.has_any([
"Luster Dragon",
"Spear Dragon",
"Cave Dragon"
], player) and (state.count_from_list_exclusive([
"Luster Dragon",
"Spear Dragon",
"Cave Dragon"
"Armed Dragon LV3",
"Masked Dragon",
"Twin-Headed Behemoth",
"Element Dragon",
"Troop Dragon",
"Horus the Black Flame Dragon LV4",
"Stamping Destruction"
], player) + (state.has_any([
"Luster Dragon #2",
"Armed Dragon LV5",
"Kaiser Glider",
"Horus the Black Flame Dragon LV6"
], player))) >= 7
def only_spellcaster(state, player):
return state.has_any([
"Dark Elf",
"Gemini Elf",
"Skilled Dark Magician",
"Toon Gemini Elf",
"Kycoo the Ghost Destroyer",
"Familiar-Possessed - Aussa"
], player) and (state.count_from_list_exclusive([
"Dark Elf",
"Gemini Elf",
"Skilled Dark Magician",
"Toon Gemini Elf",
"Kycoo the Ghost Destroyer",
"Familiar-Possessed - Aussa",
"Breaker the magical Warrior",
"The Tricky",
"Injection Fairy Lily",
"Magician of Faith",
"Tsukuyomi",
"Gravekeeper's Spy",
"Gravekeeper's Guard",
"Summon Priest",
"Old Vindictive Magician",
"Apprentice Magician",
"Magical Dimension"
], player) + (state.has_any([
"Chaos Command Magician",
"Cybernetic Magician"
], player))) >= 7
def equip_unions(state, player):
return (state.has_all(["Burning Beast", "Freezing Beast",
"Metallizing Parasite - Lunatite", "Mother Grizzly"], player) or
state.has_all(["Dark Blade", "Pitch-Dark Dragon",
"Giant Orc", "Second Goblin", "Mystic Tomato"], player) or
state.has_all(["Decayed Commander", "Zombie Tiger",
"Vampire Orchis", "Des Dendle", "Giant Rat"], player) or
state.has_all(["Indomitable Fighter Lei Lei", "Protective Soul Ailin",
"V-Tiger Jet", "W-Wing Catapult", "Shining Angel"], player) or
state.has_all(["X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", "Shining Angel"], player)) and\
state.has_any(["Frontline Base", "Formation Union", "Roll Out!"], player)
def can_gain_lp_every_turn(state, player):
return state.count_from_list_exclusive([
"Solemn Wishes",
"Cure Mermaid",
"Dancing Fairy",
"Princess Pikeru",
"Kiseitai"], player) >= 3
def only_normal(state, player):
return (state.has_from_list_exclusive([
"Archfiend Soldier",
"Gemini Elf",
"Insect Knight",
"Luster Dragon",
"Mad Dog of Darkness",
"Vorse Raider",
"Blazing Inpachi",
"Gagagigo",
"Mechanicalchaser",
"7 Colored Fish",
"Dark Blade",
"Dunames Dark Witch",
"Giant Red Snake",
"Gil Garth",
"Great Angus",
"Harpie's Brother",
"La Jinn the Mystical Genie of the Lamp",
"Neo Bug",
"Nin-Ken Dog",
"Opticlops",
"Sea Serpent Warrior of Darkness",
"X-Head Cannon",
"Zure, Knight of Dark World"], player, 6) and
state.has_any([
"Cyber-Tech Alligator",
"Summoned Skull",
"Giga Gagagigo",
"Amphibian Beast",
"Beast of Talwar",
"Luster Dragon #2",
"Terrorking Salmon"], player))
def only_level(state, player):
return (state.has("Level Up!", player) and
(state.has_all(["Armed Dragon LV3", "Armed Dragon LV5"], player) +
state.has_all(["Horus the Black Flame Dragon LV4", "Horus the Black Flame Dragon LV6"], player) +
state.has_all(["Mystic Swordsman LV4", "Mystic Swordsman LV6"], player) +
state.has_all(["Silent Swordsman Lv3", "Silent Swordsman Lv5"], player) +
state.has_all(["Ultimate Insect Lv3", "Ultimate Insect Lv5"], player)) >= 3)
def spell_counter(state, player):
return (state.has("Pitch-Black Power Stone", player) and
state.has_from_list_exclusive(["Blast Magician",
"Magical Marionette",
"Mythical Beast Cerberus",
"Royal Magical Library",
"Spell-Counter Cards"], player, 2))
def take_control(state, player):
return state.has_from_list_exclusive(["Aussa the Earth Charmer",
"Jowls of Dark Demise",
"Brain Control",
"Creature Swap",
"Enemy Controller",
"Mind Control",
"Magician of Faith"], player, 5)
def only_toons(state, player):
return state.has_all(["Toon Gemini Elf",
"Toon Goblin Attack Force",
"Toon Masked Sorcerer",
"Toon Mermaid",
"Toon Dark Magician Girl",
"Toon World"], player)
def only_spirit(state, player):
return state.has_all(["Asura Priest",
"Fushi No Tori",
"Maharaghi",
"Susa Soldier"], player)
def pacman_deck(state, player):
return state.has_from_list_exclusive(["Des Lacooda",
"Swarm of Locusts",
"Swarm of Scarabs",
"Wandering Mummy",
"Golem Sentry",
"Great Spirit",
"Royal Keeper",
"Stealth Bird"], player, 4)
def quick_plays(state, player):
return state.has_from_list_exclusive(["Collapse",
"Emergency Provisions",
"Enemy Controller",
"Graceful Dice",
"Mystik Wok",
"Offerings to the Doomed",
"Poison of the Old Man",
"Reload",
"Rush Recklessly",
"The Reliable Guardian"], player, 4)
def counter_traps(state, player):
return state.has_from_list_exclusive(["Cursed Seal of the Forbidden Spell",
"Divine Wrath",
"Horn of Heaven",
"Magic Drain",
"Magic Jammer",
"Negate Attack",
"Seven Tools of the Bandit",
"Solemn Judgment",
"Spell Shield Type-8"], player, 5)
def back_row_removal(state, player):
return state.has_from_list_exclusive(["Anteatereatingant",
"B.E.S. Tetran",
"Breaker the Magical Warrior",
"Calamity of the Wicked",
"Chiron the Mage",
"Dust Tornado",
"Heavy Storm",
"Mystical Space Typhoon",
"Mobius the Frost Monarch",
"Raigeki Break",
"Stamping Destruction",
"Swarm of Locusts"], player, 2)

View File

@@ -0,0 +1,81 @@
structure_contents: dict[str, set] = {
"dragons_roar": {
"Luster Dragon",
"Armed Dragon LV3",
"Armed Dragon LV5",
"Masked Dragon",
"Twin-Headed Behemoth",
"Stamping Destruction",
"Nobleman of Crossout",
"Creature Swap",
"Reload",
"Stamping Destruction",
"Heavy Storm",
"Dust Tornado",
"Mystical Space Typhoon",
},
"zombie_madness": {
"Pyramid Turtle",
"Regenerating Mummy",
"Ryu Kokki",
"Book of Life",
"Call of the Mummy",
"Creature Swap",
"Reload",
"Heavy Storm",
"Dust Tornado",
"Mystical Space Typhoon",
},
"blazing_destruction": {
"Inferno",
"Solar Flare Dragon",
"UFO Turtle",
"Ultimate Baseball Kid",
"Fire Beaters",
"Tribute to The Doomed",
"Level Limit - Area B",
"Heavy Storm",
"Dust Tornado",
"Mystical Space Typhoon",
},
"fury_from_the_deep": {
"Mother Grizzly",
"Water Beaters",
"Gravity Bind",
"Reload",
"Mobius the Frost Monarch",
"Heavy Storm",
"Dust Tornado",
"Mystical Space Typhoon",
},
"warriors_triumph": {
"Gearfried the Iron Knight",
"D.D. Warrior Lady",
"Marauding Captain",
"Exiled Force",
"Reinforcement of the Army",
"Warrior Beaters",
"Reload",
"Heavy Storm",
"Dust Tornado",
"Mystical Space Typhoon",
},
"spellcasters_judgement": {
"Dark Magician",
"Apprentice Magician",
"Breaker the Magical Warrior",
"Magician of Faith",
"Skilled Dark Magician",
"Tsukuyomi",
"Magical Dimension",
"Mage PowerSpell-Counter Cards",
"Heavy Storm",
"Dust Tornado",
"Mystical Space Typhoon",
},
"none": {},
}
def get_deck_content_locations(deck: str) -> dict[str, str]:
return {f"{deck} {i}": content for i, content in enumerate(structure_contents[deck])}