mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 07:23:25 -07:00
Merge remote-tracking branch 'remotes/upstream/main'
This commit is contained in:
@@ -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
|
||||
@@ -1457,6 +1453,14 @@ class Tutorial(NamedTuple):
|
||||
authors: List[str]
|
||||
|
||||
|
||||
class OptionGroup(NamedTuple):
|
||||
"""Define a grouping of options"""
|
||||
name: str
|
||||
"""Name of the group to categorize this option in for display on the WebHost and in generated YAMLS."""
|
||||
options: List[Type[Options.Option]]
|
||||
"""Options to be in the defined group. """
|
||||
|
||||
|
||||
class PlandoOptions(IntFlag):
|
||||
none = 0b0000
|
||||
items = 0b0001
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
57
Options.py
57
Options.py
@@ -24,7 +24,7 @@ if typing.TYPE_CHECKING:
|
||||
class OptionError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Visibility(enum.IntFlag):
|
||||
none = 0b0000
|
||||
template = 0b0001
|
||||
@@ -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:
|
||||
@@ -984,7 +947,7 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
||||
def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]:
|
||||
"""
|
||||
Returns a dictionary of [str, Option.value]
|
||||
|
||||
|
||||
:param option_names: names of the options to return
|
||||
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
|
||||
"""
|
||||
@@ -1198,15 +1161,21 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden or generate_hidden:
|
||||
all_options: typing.Dict[str, AssembleOptions] = {
|
||||
option_name: option for option_name, option in world.options_dataclass.type_hints.items()
|
||||
if option.visibility & Visibility.template
|
||||
}
|
||||
|
||||
option_groups = {option: option_group.name
|
||||
for option_group in world.web.option_groups
|
||||
for option in option_group.options}
|
||||
ordered_groups = ["Game Options"]
|
||||
[ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups]
|
||||
grouped_options = {group: {} for group in ordered_groups}
|
||||
for option_name, option in world.options_dataclass.type_hints.items():
|
||||
if option.visibility >= Visibility.template:
|
||||
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
||||
|
||||
with open(local_path("data", "options.yaml")) as f:
|
||||
file_data = f.read()
|
||||
res = Template(file_data).render(
|
||||
options=all_options,
|
||||
option_groups=grouped_options,
|
||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||
dictify_range=dictify_range,
|
||||
)
|
||||
|
||||
@@ -66,6 +66,8 @@ Currently, the following games are supported:
|
||||
* A Short Hike
|
||||
* Yoshi's Island
|
||||
* Mario & Luigi: Superstar Saga
|
||||
* Bomb Rush Cyberfunk
|
||||
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -83,6 +84,6 @@ def register():
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
# to trigger app routing picking up on it
|
||||
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots
|
||||
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options
|
||||
|
||||
app.register_blueprint(api.api_endpoints)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -37,25 +37,6 @@ def start_playing():
|
||||
return render_template(f"startPlaying.html")
|
||||
|
||||
|
||||
# TODO for back compat. remove around 0.4.5
|
||||
@app.route("/weighted-settings")
|
||||
def weighted_settings():
|
||||
return redirect("weighted-options", 301)
|
||||
|
||||
|
||||
@app.route("/weighted-options")
|
||||
@cache.cached()
|
||||
def weighted_options():
|
||||
return render_template("weighted-options.html")
|
||||
|
||||
|
||||
# Player options pages
|
||||
@app.route("/games/<string:game>/player-options")
|
||||
@cache.cached()
|
||||
def player_options(game: str):
|
||||
return render_template("player-options.html", game=game, theme=get_world_theme(game))
|
||||
|
||||
|
||||
# Game Info Pages
|
||||
@app.route('/games/<string:game>/info/<string:lang>')
|
||||
@cache.cached()
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import json
|
||||
import logging
|
||||
import collections.abc
|
||||
import os
|
||||
import typing
|
||||
import yaml
|
||||
import requests
|
||||
import json
|
||||
import flask
|
||||
|
||||
import Options
|
||||
from Utils import local_path
|
||||
from Options import Visibility
|
||||
from flask import redirect, render_template, request, Response
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
||||
"exclude_locations", "priority_locations"}
|
||||
from Utils import local_path
|
||||
from textwrap import dedent
|
||||
from . import app, cache
|
||||
|
||||
|
||||
def create():
|
||||
@@ -17,189 +20,230 @@ def create():
|
||||
|
||||
Options.generate_yaml_templates(yaml_folder)
|
||||
|
||||
def get_html_doc(option_type: type(Options.Option)) -> str:
|
||||
if not option_type.__doc__:
|
||||
return "Please document me!"
|
||||
return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip()
|
||||
|
||||
weighted_options = {
|
||||
"baseOptions": {
|
||||
"description": "Generated by https://archipelago.gg/",
|
||||
"name": "",
|
||||
"game": {},
|
||||
def get_world_theme(game_name: str):
|
||||
if game_name in AutoWorldRegister.world_types:
|
||||
return AutoWorldRegister.world_types[game_name].web.theme
|
||||
return 'grass'
|
||||
|
||||
|
||||
def render_options_page(template: str, world_name: str, is_complex: bool = False):
|
||||
world = AutoWorldRegister.world_types[world_name]
|
||||
if world.hidden or world.web.options_page is False:
|
||||
return redirect("games")
|
||||
|
||||
option_groups = {option: option_group.name
|
||||
for option_group in world.web.option_groups
|
||||
for option in option_group.options}
|
||||
ordered_groups = ["Game Options", *[group.name for group in world.web.option_groups]]
|
||||
grouped_options = {group: {} for group in ordered_groups}
|
||||
for option_name, option in world.options_dataclass.type_hints.items():
|
||||
# Exclude settings from options pages if their visibility is disabled
|
||||
if not is_complex and option.visibility < Visibility.simple_ui:
|
||||
continue
|
||||
|
||||
if is_complex and option.visibility < Visibility.complex_ui:
|
||||
continue
|
||||
|
||||
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
||||
|
||||
return render_template(
|
||||
template,
|
||||
world_name=world_name,
|
||||
world=world,
|
||||
option_groups=grouped_options,
|
||||
issubclass=issubclass,
|
||||
Options=Options,
|
||||
theme=get_world_theme(world_name),
|
||||
)
|
||||
|
||||
|
||||
def generate_game(player_name: str, formatted_options: dict):
|
||||
payload = {
|
||||
"race": 0,
|
||||
"hint_cost": 10,
|
||||
"forfeit_mode": "auto",
|
||||
"remaining_mode": "disabled",
|
||||
"collect_mode": "goal",
|
||||
"weights": {
|
||||
player_name: formatted_options,
|
||||
},
|
||||
"games": {},
|
||||
}
|
||||
r = requests.post("https://archipelago.gg/api/generate", json=payload)
|
||||
if 200 <= r.status_code <= 299:
|
||||
response_data = r.json()
|
||||
return redirect(response_data["url"])
|
||||
else:
|
||||
return r.text
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
|
||||
all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints
|
||||
def send_yaml(player_name: str, formatted_options: dict):
|
||||
response = Response(yaml.dump(formatted_options, sort_keys=False))
|
||||
response.headers["Content-Type"] = "text/yaml"
|
||||
response.headers["Content-Disposition"] = f"attachment; filename={player_name}.yaml"
|
||||
return response
|
||||
|
||||
# Generate JSON files for player-options pages
|
||||
player_options = {
|
||||
"baseOptions": {
|
||||
"description": f"Generated by https://archipelago.gg/ for {game_name}",
|
||||
"game": game_name,
|
||||
"name": "",
|
||||
},
|
||||
}
|
||||
|
||||
game_options = {}
|
||||
visible: typing.Set[str] = set()
|
||||
visible_weighted: typing.Set[str] = set()
|
||||
@app.template_filter("dedent")
|
||||
def filter_dedent(text: str):
|
||||
return dedent(text).strip("\n ")
|
||||
|
||||
for option_name, option in all_options.items():
|
||||
if option.visibility & Options.Visibility.simple_ui:
|
||||
visible.add(option_name)
|
||||
if option.visibility & Options.Visibility.complex_ui:
|
||||
visible_weighted.add(option_name)
|
||||
|
||||
if option_name in handled_in_js:
|
||||
pass
|
||||
@app.template_test("ordered")
|
||||
def test_ordered(obj):
|
||||
return isinstance(obj, collections.abc.Sequence)
|
||||
|
||||
elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle):
|
||||
game_options[option_name] = this_option = {
|
||||
"type": "select",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": get_html_doc(option),
|
||||
"defaultValue": None,
|
||||
"options": []
|
||||
}
|
||||
|
||||
for sub_option_id, sub_option_name in option.name_lookup.items():
|
||||
if sub_option_name != "random":
|
||||
this_option["options"].append({
|
||||
"name": option.get_option_name(sub_option_id),
|
||||
"value": sub_option_name,
|
||||
})
|
||||
if sub_option_id == option.default:
|
||||
this_option["defaultValue"] = sub_option_name
|
||||
@app.route("/games/<string:game>/option-presets", methods=["GET"])
|
||||
@cache.cached()
|
||||
def option_presets(game: str) -> Response:
|
||||
world = AutoWorldRegister.world_types[game]
|
||||
presets = {}
|
||||
|
||||
if not this_option["defaultValue"]:
|
||||
this_option["defaultValue"] = "random"
|
||||
if world.web.options_presets:
|
||||
presets = presets | world.web.options_presets
|
||||
|
||||
elif issubclass(option, Options.Range):
|
||||
game_options[option_name] = {
|
||||
"type": "range",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": get_html_doc(option),
|
||||
"defaultValue": option.default if hasattr(
|
||||
option, "default") and option.default != "random" else option.range_start,
|
||||
"min": option.range_start,
|
||||
"max": option.range_end,
|
||||
}
|
||||
class SetEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
from collections.abc import Set
|
||||
if isinstance(obj, Set):
|
||||
return list(obj)
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
if issubclass(option, Options.NamedRange):
|
||||
game_options[option_name]["type"] = 'named_range'
|
||||
game_options[option_name]["value_names"] = {}
|
||||
for key, val in option.special_range_names.items():
|
||||
game_options[option_name]["value_names"][key] = val
|
||||
json_data = json.dumps(presets, cls=SetEncoder)
|
||||
response = flask.Response(json_data)
|
||||
response.headers["Content-Type"] = "application/json"
|
||||
return response
|
||||
|
||||
elif issubclass(option, Options.ItemSet):
|
||||
game_options[option_name] = {
|
||||
"type": "items-list",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": get_html_doc(option),
|
||||
"defaultValue": list(option.default)
|
||||
}
|
||||
|
||||
elif issubclass(option, Options.LocationSet):
|
||||
game_options[option_name] = {
|
||||
"type": "locations-list",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": get_html_doc(option),
|
||||
"defaultValue": list(option.default)
|
||||
}
|
||||
@app.route("/weighted-options")
|
||||
def weighted_options_old():
|
||||
return redirect("games", 301)
|
||||
|
||||
elif issubclass(option, Options.VerifyKeys) and not issubclass(option, Options.OptionDict):
|
||||
if option.valid_keys:
|
||||
game_options[option_name] = {
|
||||
"type": "custom-list",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": get_html_doc(option),
|
||||
"options": list(option.valid_keys),
|
||||
"defaultValue": list(option.default) if hasattr(option, "default") else []
|
||||
}
|
||||
|
||||
else:
|
||||
logging.debug(f"{option} not exported to Web Options.")
|
||||
@app.route("/games/<string:game>/weighted-options")
|
||||
@cache.cached()
|
||||
def weighted_options(game: str):
|
||||
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
|
||||
|
||||
player_options["presetOptions"] = {}
|
||||
for preset_name, preset in world.web.options_presets.items():
|
||||
player_options["presetOptions"][preset_name] = {}
|
||||
for option_name, option_value in preset.items():
|
||||
# Random range type settings are not valid.
|
||||
assert (not str(option_value).startswith("random-")), \
|
||||
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. Special random " \
|
||||
f"values are not supported for presets."
|
||||
|
||||
# Normal random is supported, but needs to be handled explicitly.
|
||||
if option_value == "random":
|
||||
player_options["presetOptions"][preset_name][option_name] = option_value
|
||||
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
|
||||
def generate_weighted_yaml(game: str):
|
||||
if request.method == "POST":
|
||||
intent_generate = False
|
||||
options = {}
|
||||
|
||||
for key, val in request.form.items():
|
||||
if "||" not in key:
|
||||
if len(str(val)) == 0:
|
||||
continue
|
||||
|
||||
option = world.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
if isinstance(option, Options.NamedRange) and isinstance(option_value, str):
|
||||
assert option_value in option.special_range_names, \
|
||||
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \
|
||||
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
|
||||
options[key] = val
|
||||
else:
|
||||
if int(val) == 0:
|
||||
continue
|
||||
|
||||
# Still use the true value for the option, not the name.
|
||||
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||
elif isinstance(option, Options.Range):
|
||||
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||
elif isinstance(option_value, str):
|
||||
# For Choice and Toggle options, the value should be the name of the option. This is to prevent
|
||||
# setting a preset for an option with an overridden from_text method that would normally be okay,
|
||||
# but would not be okay for the webhost's current implementation of player options UI.
|
||||
assert option.name_lookup[option.value] == option_value, \
|
||||
f"Invalid option value '{option_value}' for '{option_name}' in preset '{preset_name}'. " \
|
||||
f"Values must not be resolved to a different option via option.from_text (or an alias)."
|
||||
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||
else:
|
||||
# int and bool values are fine, just resolve them to the current key for webhost.
|
||||
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||
[option, setting] = key.split("||")
|
||||
options.setdefault(option, {})[setting] = int(val)
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
|
||||
# Error checking
|
||||
if "name" not in options:
|
||||
return "Player name is required."
|
||||
|
||||
filtered_player_options = player_options
|
||||
filtered_player_options["gameOptions"] = {
|
||||
option_name: option_data for option_name, option_data in game_options.items()
|
||||
if option_name in visible
|
||||
# Remove POST data irrelevant to YAML
|
||||
if "intent-generate" in options:
|
||||
intent_generate = True
|
||||
del options["intent-generate"]
|
||||
if "intent-export" in options:
|
||||
del options["intent-export"]
|
||||
|
||||
# Properly format YAML output
|
||||
player_name = options["name"]
|
||||
del options["name"]
|
||||
|
||||
formatted_options = {
|
||||
"name": player_name,
|
||||
"game": game,
|
||||
"description": f"Generated by https://archipelago.gg/ for {game}",
|
||||
game: options,
|
||||
}
|
||||
|
||||
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
|
||||
json.dump(filtered_player_options, f, indent=2, separators=(',', ': '))
|
||||
if intent_generate:
|
||||
return generate_game(player_name, formatted_options)
|
||||
|
||||
filtered_player_options["gameOptions"] = {
|
||||
option_name: option_data for option_name, option_data in game_options.items()
|
||||
if option_name in visible_weighted
|
||||
else:
|
||||
return send_yaml(player_name, formatted_options)
|
||||
|
||||
|
||||
# Player options pages
|
||||
@app.route("/games/<string:game>/player-options")
|
||||
@cache.cached()
|
||||
def player_options(game: str):
|
||||
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
|
||||
|
||||
|
||||
# YAML generator for player-options
|
||||
@app.route("/games/<string:game>/generate-yaml", methods=["POST"])
|
||||
def generate_yaml(game: str):
|
||||
if request.method == "POST":
|
||||
options = {}
|
||||
intent_generate = False
|
||||
for key, val in request.form.items(multi=True):
|
||||
if key in options:
|
||||
if not isinstance(options[key], list):
|
||||
options[key] = [options[key]]
|
||||
options[key].append(val)
|
||||
else:
|
||||
options[key] = val
|
||||
|
||||
# Detect and build ItemDict options from their name pattern
|
||||
for key, val in options.copy().items():
|
||||
key_parts = key.rsplit("||", 2)
|
||||
if key_parts[-1] == "qty":
|
||||
if key_parts[0] not in options:
|
||||
options[key_parts[0]] = {}
|
||||
if val != "0":
|
||||
options[key_parts[0]][key_parts[1]] = int(val)
|
||||
del options[key]
|
||||
|
||||
# Detect random-* keys and set their options accordingly
|
||||
for key, val in options.copy().items():
|
||||
if key.startswith("random-"):
|
||||
options[key.removeprefix("random-")] = "random"
|
||||
del options[key]
|
||||
|
||||
# Error checking
|
||||
if not options["name"]:
|
||||
return "Player name is required."
|
||||
|
||||
# Remove POST data irrelevant to YAML
|
||||
preset_name = 'default'
|
||||
if "intent-generate" in options:
|
||||
intent_generate = True
|
||||
del options["intent-generate"]
|
||||
if "intent-export" in options:
|
||||
del options["intent-export"]
|
||||
if "game-options-preset" in options:
|
||||
preset_name = options["game-options-preset"]
|
||||
del options["game-options-preset"]
|
||||
|
||||
# Properly format YAML output
|
||||
player_name = options["name"]
|
||||
del options["name"]
|
||||
|
||||
description = f"Generated by https://archipelago.gg/ for {game}"
|
||||
if preset_name != 'default' and preset_name != 'custom':
|
||||
description += f" using {preset_name} preset"
|
||||
|
||||
formatted_options = {
|
||||
"name": player_name,
|
||||
"game": game,
|
||||
"description": description,
|
||||
game: options,
|
||||
}
|
||||
|
||||
if not world.hidden and world.web.options_page is True:
|
||||
# Add the random option to Choice, TextChoice, and Toggle options
|
||||
for option in filtered_player_options["gameOptions"].values():
|
||||
if option["type"] == "select":
|
||||
option["options"].append({"name": "Random", "value": "random"})
|
||||
|
||||
if not option["defaultValue"]:
|
||||
option["defaultValue"] = "random"
|
||||
|
||||
weighted_options["baseOptions"]["game"][game_name] = 0
|
||||
weighted_options["games"][game_name] = {
|
||||
"gameSettings": filtered_player_options["gameOptions"],
|
||||
"gameItems": tuple(world.item_names),
|
||||
"gameItemGroups": [
|
||||
group for group in world.item_name_groups.keys() if group != "Everything"
|
||||
],
|
||||
"gameItemDescriptions": world.item_descriptions,
|
||||
"gameLocations": tuple(world.location_names),
|
||||
"gameLocationGroups": [
|
||||
group for group in world.location_name_groups.keys() if group != "Everywhere"
|
||||
],
|
||||
"gameLocationDescriptions": world.location_descriptions,
|
||||
}
|
||||
|
||||
with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f:
|
||||
json.dump(weighted_options, f, indent=2, separators=(',', ': '))
|
||||
if intent_generate:
|
||||
return generate_game(player_name, formatted_options)
|
||||
|
||||
else:
|
||||
return send_yaml(player_name, formatted_options)
|
||||
|
||||
@@ -1,523 +0,0 @@
|
||||
let gameName = null;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
gameName = document.getElementById('player-options').getAttribute('data-game');
|
||||
|
||||
// Update game name on page
|
||||
document.getElementById('game-name').innerText = gameName;
|
||||
|
||||
fetchOptionData().then((results) => {
|
||||
let optionHash = localStorage.getItem(`${gameName}-hash`);
|
||||
if (!optionHash) {
|
||||
// If no hash data has been set before, set it now
|
||||
optionHash = md5(JSON.stringify(results));
|
||||
localStorage.setItem(`${gameName}-hash`, optionHash);
|
||||
localStorage.removeItem(gameName);
|
||||
}
|
||||
|
||||
if (optionHash !== md5(JSON.stringify(results))) {
|
||||
showUserMessage(
|
||||
'Your options are out of date! Click here to update them! Be aware this will reset them all to default.'
|
||||
);
|
||||
document.getElementById('user-message').addEventListener('click', resetOptions);
|
||||
}
|
||||
|
||||
// Page setup
|
||||
createDefaultOptions(results);
|
||||
buildUI(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('export-options').addEventListener('click', () => exportOptions());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
|
||||
// Name input field
|
||||
const playerOptions = JSON.parse(localStorage.getItem(gameName));
|
||||
const nameInput = document.getElementById('player-name');
|
||||
nameInput.addEventListener('keyup', (event) => updateBaseOption(event));
|
||||
nameInput.value = playerOptions.name;
|
||||
|
||||
// Presets
|
||||
const presetSelect = document.getElementById('game-options-preset');
|
||||
presetSelect.addEventListener('change', (event) => setPresets(results, event.target.value));
|
||||
for (const preset in results['presetOptions']) {
|
||||
const presetOption = document.createElement('option');
|
||||
presetOption.innerText = preset;
|
||||
presetSelect.appendChild(presetOption);
|
||||
}
|
||||
presetSelect.value = localStorage.getItem(`${gameName}-preset`);
|
||||
results['presetOptions']['__default'] = {};
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
const url = new URL(window.location.href);
|
||||
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
|
||||
})
|
||||
});
|
||||
|
||||
const resetOptions = () => {
|
||||
localStorage.removeItem(gameName);
|
||||
localStorage.removeItem(`${gameName}-hash`);
|
||||
localStorage.removeItem(`${gameName}-preset`);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const fetchOptionData = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject(ajax.responseText);
|
||||
return;
|
||||
}
|
||||
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||
catch(error){ reject(error); }
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/player-options/${gameName}.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const createDefaultOptions = (optionData) => {
|
||||
if (!localStorage.getItem(gameName)) {
|
||||
const newOptions = {
|
||||
[gameName]: {},
|
||||
};
|
||||
for (let baseOption of Object.keys(optionData.baseOptions)){
|
||||
newOptions[baseOption] = optionData.baseOptions[baseOption];
|
||||
}
|
||||
for (let gameOption of Object.keys(optionData.gameOptions)){
|
||||
newOptions[gameName][gameOption] = optionData.gameOptions[gameOption].defaultValue;
|
||||
}
|
||||
localStorage.setItem(gameName, JSON.stringify(newOptions));
|
||||
}
|
||||
|
||||
if (!localStorage.getItem(`${gameName}-preset`)) {
|
||||
localStorage.setItem(`${gameName}-preset`, '__default');
|
||||
}
|
||||
};
|
||||
|
||||
const buildUI = (optionData) => {
|
||||
// Game Options
|
||||
const leftGameOpts = {};
|
||||
const rightGameOpts = {};
|
||||
Object.keys(optionData.gameOptions).forEach((key, index) => {
|
||||
if (index < Object.keys(optionData.gameOptions).length / 2) {
|
||||
leftGameOpts[key] = optionData.gameOptions[key];
|
||||
} else {
|
||||
rightGameOpts[key] = optionData.gameOptions[key];
|
||||
}
|
||||
});
|
||||
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
|
||||
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
|
||||
};
|
||||
|
||||
const buildOptionsTable = (options, romOpts = false) => {
|
||||
const currentOptions = JSON.parse(localStorage.getItem(gameName));
|
||||
const table = document.createElement('table');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
Object.keys(options).forEach((option) => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// td Left
|
||||
const tdl = document.createElement('td');
|
||||
const label = document.createElement('label');
|
||||
label.textContent = `${options[option].displayName}: `;
|
||||
label.setAttribute('for', option);
|
||||
|
||||
const questionSpan = document.createElement('span');
|
||||
questionSpan.classList.add('interactive');
|
||||
questionSpan.setAttribute('data-tooltip', options[option].description);
|
||||
questionSpan.innerText = '(?)';
|
||||
|
||||
label.appendChild(questionSpan);
|
||||
tdl.appendChild(label);
|
||||
tr.appendChild(tdl);
|
||||
|
||||
// td Right
|
||||
const tdr = document.createElement('td');
|
||||
let element = null;
|
||||
|
||||
const randomButton = document.createElement('button');
|
||||
|
||||
switch(options[option].type) {
|
||||
case 'select':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('select-container');
|
||||
let select = document.createElement('select');
|
||||
select.setAttribute('id', option);
|
||||
select.setAttribute('data-key', option);
|
||||
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||
options[option].options.forEach((opt) => {
|
||||
const optionElement = document.createElement('option');
|
||||
optionElement.setAttribute('value', opt.value);
|
||||
optionElement.innerText = opt.name;
|
||||
|
||||
if ((isNaN(currentOptions[gameName][option]) &&
|
||||
(parseInt(opt.value, 10) === parseInt(currentOptions[gameName][option]))) ||
|
||||
(opt.value === currentOptions[gameName][option]))
|
||||
{
|
||||
optionElement.selected = true;
|
||||
}
|
||||
select.appendChild(optionElement);
|
||||
});
|
||||
select.addEventListener('change', (event) => updateGameOption(event.target));
|
||||
element.appendChild(select);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, select));
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
select.disabled = true;
|
||||
}
|
||||
|
||||
element.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
case 'range':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('range-container');
|
||||
|
||||
let range = document.createElement('input');
|
||||
range.setAttribute('id', option);
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('data-key', option);
|
||||
range.setAttribute('min', options[option].min);
|
||||
range.setAttribute('max', options[option].max);
|
||||
range.value = currentOptions[gameName][option];
|
||||
range.addEventListener('change', (event) => {
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
element.appendChild(range);
|
||||
|
||||
let rangeVal = document.createElement('span');
|
||||
rangeVal.classList.add('range-value');
|
||||
rangeVal.setAttribute('id', `${option}-value`);
|
||||
rangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||
currentOptions[gameName][option] : options[option].defaultValue;
|
||||
element.appendChild(rangeVal);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, range));
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
range.disabled = true;
|
||||
}
|
||||
|
||||
element.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
case 'named_range':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('named-range-container');
|
||||
|
||||
// Build the select element
|
||||
let namedRangeSelect = document.createElement('select');
|
||||
namedRangeSelect.setAttribute('data-key', option);
|
||||
Object.keys(options[option].value_names).forEach((presetName) => {
|
||||
let presetOption = document.createElement('option');
|
||||
presetOption.innerText = presetName;
|
||||
presetOption.value = options[option].value_names[presetName];
|
||||
const words = presetOption.innerText.split('_');
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
||||
}
|
||||
presetOption.innerText = words.join(' ');
|
||||
namedRangeSelect.appendChild(presetOption);
|
||||
});
|
||||
let customOption = document.createElement('option');
|
||||
customOption.innerText = 'Custom';
|
||||
customOption.value = 'custom';
|
||||
customOption.selected = true;
|
||||
namedRangeSelect.appendChild(customOption);
|
||||
if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) {
|
||||
namedRangeSelect.value = Number(currentOptions[gameName][option]);
|
||||
}
|
||||
|
||||
// Build range element
|
||||
let namedRangeWrapper = document.createElement('div');
|
||||
namedRangeWrapper.classList.add('named-range-wrapper');
|
||||
let namedRange = document.createElement('input');
|
||||
namedRange.setAttribute('type', 'range');
|
||||
namedRange.setAttribute('data-key', option);
|
||||
namedRange.setAttribute('min', options[option].min);
|
||||
namedRange.setAttribute('max', options[option].max);
|
||||
namedRange.value = currentOptions[gameName][option];
|
||||
|
||||
// Build rage value element
|
||||
let namedRangeVal = document.createElement('span');
|
||||
namedRangeVal.classList.add('range-value');
|
||||
namedRangeVal.setAttribute('id', `${option}-value`);
|
||||
namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||
currentOptions[gameName][option] : options[option].defaultValue;
|
||||
|
||||
// Configure select event listener
|
||||
namedRangeSelect.addEventListener('change', (event) => {
|
||||
if (event.target.value === 'custom') { return; }
|
||||
|
||||
// Update range slider
|
||||
namedRange.value = event.target.value;
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
|
||||
// Configure range event handler
|
||||
namedRange.addEventListener('change', (event) => {
|
||||
// Update select element
|
||||
namedRangeSelect.value =
|
||||
(Object.values(options[option].value_names).includes(parseInt(event.target.value))) ?
|
||||
parseInt(event.target.value) : 'custom';
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
|
||||
element.appendChild(namedRangeSelect);
|
||||
namedRangeWrapper.appendChild(namedRange);
|
||||
namedRangeWrapper.appendChild(namedRangeVal);
|
||||
element.appendChild(namedRangeWrapper);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(
|
||||
event, namedRange, namedRangeSelect)
|
||||
);
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
namedRange.disabled = true;
|
||||
namedRangeSelect.disabled = true;
|
||||
}
|
||||
|
||||
namedRangeWrapper.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Ignoring unknown option type: ${options[option].type} with name ${option}`);
|
||||
return;
|
||||
}
|
||||
|
||||
tdr.appendChild(element);
|
||||
tr.appendChild(tdr);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
return table;
|
||||
};
|
||||
|
||||
const setPresets = (optionsData, presetName) => {
|
||||
const defaults = optionsData['gameOptions'];
|
||||
const preset = optionsData['presetOptions'][presetName];
|
||||
|
||||
localStorage.setItem(`${gameName}-preset`, presetName);
|
||||
|
||||
if (!preset) {
|
||||
console.error(`No presets defined for preset name: '${presetName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateOptionElement = (option, presetValue) => {
|
||||
const optionElement = document.querySelector(`#${option}[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
if (presetValue === 'random') {
|
||||
randomElement.classList.add('active');
|
||||
optionElement.disabled = true;
|
||||
updateGameOption(randomElement, false);
|
||||
} else {
|
||||
optionElement.value = presetValue;
|
||||
randomElement.classList.remove('active');
|
||||
optionElement.disabled = undefined;
|
||||
updateGameOption(optionElement, false);
|
||||
}
|
||||
};
|
||||
|
||||
for (const option in defaults) {
|
||||
let presetValue = preset[option];
|
||||
if (presetValue === undefined) {
|
||||
// Using the default value if not set in presets.
|
||||
presetValue = defaults[option]['defaultValue'];
|
||||
}
|
||||
|
||||
switch (defaults[option].type) {
|
||||
case 'range':
|
||||
const numberElement = document.querySelector(`#${option}-value`);
|
||||
if (presetValue === 'random') {
|
||||
numberElement.innerText = defaults[option]['defaultValue'] === 'random'
|
||||
? defaults[option]['min'] // A fallback so we don't print 'random' in the UI.
|
||||
: defaults[option]['defaultValue'];
|
||||
} else {
|
||||
numberElement.innerText = presetValue;
|
||||
}
|
||||
|
||||
updateOptionElement(option, presetValue);
|
||||
break;
|
||||
|
||||
case 'select': {
|
||||
updateOptionElement(option, presetValue);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'named_range': {
|
||||
const selectElement = document.querySelector(`select[data-key='${option}']`);
|
||||
const rangeElement = document.querySelector(`input[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
if (presetValue === 'random') {
|
||||
randomElement.classList.add('active');
|
||||
selectElement.disabled = true;
|
||||
rangeElement.disabled = true;
|
||||
updateGameOption(randomElement, false);
|
||||
} else {
|
||||
rangeElement.value = presetValue;
|
||||
selectElement.value = Object.values(defaults[option]['value_names']).includes(parseInt(presetValue)) ?
|
||||
parseInt(presetValue) : 'custom';
|
||||
document.getElementById(`${option}-value`).innerText = presetValue;
|
||||
|
||||
randomElement.classList.remove('active');
|
||||
selectElement.disabled = undefined;
|
||||
rangeElement.disabled = undefined;
|
||||
updateGameOption(rangeElement, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(`Ignoring preset value for unknown option type: ${defaults[option].type} with name ${option}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
|
||||
const active = event.target.classList.contains('active');
|
||||
const randomButton = event.target;
|
||||
|
||||
if (active) {
|
||||
randomButton.classList.remove('active');
|
||||
inputElement.disabled = undefined;
|
||||
if (optionalSelectElement) {
|
||||
optionalSelectElement.disabled = undefined;
|
||||
}
|
||||
} else {
|
||||
randomButton.classList.add('active');
|
||||
inputElement.disabled = true;
|
||||
if (optionalSelectElement) {
|
||||
optionalSelectElement.disabled = true;
|
||||
}
|
||||
}
|
||||
updateGameOption(active ? inputElement : randomButton);
|
||||
};
|
||||
|
||||
const updateBaseOption = (event) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value);
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const updateGameOption = (optionElement, toggleCustomPreset = true) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
|
||||
if (toggleCustomPreset) {
|
||||
localStorage.setItem(`${gameName}-preset`, '__custom');
|
||||
const presetElement = document.getElementById('game-options-preset');
|
||||
presetElement.value = '__custom';
|
||||
}
|
||||
|
||||
if (optionElement.classList.contains('randomize-button')) {
|
||||
// If the event passed in is the randomize button, then we know what we must do.
|
||||
options[gameName][optionElement.getAttribute('data-key')] = 'random';
|
||||
} else {
|
||||
options[gameName][optionElement.getAttribute('data-key')] = isNaN(optionElement.value) ?
|
||||
optionElement.value : parseInt(optionElement.value, 10);
|
||||
}
|
||||
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const exportOptions = () => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
const preset = localStorage.getItem(`${gameName}-preset`);
|
||||
switch (preset) {
|
||||
case '__default':
|
||||
options['description'] = `Generated by https://archipelago.gg with the default preset.`;
|
||||
break;
|
||||
|
||||
case '__custom':
|
||||
options['description'] = `Generated by https://archipelago.gg.`;
|
||||
break;
|
||||
|
||||
default:
|
||||
options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`;
|
||||
}
|
||||
|
||||
if (!options.name || options.name.toString().trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
|
||||
};
|
||||
|
||||
/** Create an anchor and trigger a download of a text file. */
|
||||
const download = (filename, text) => {
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
|
||||
downloadLink.setAttribute('download', filename);
|
||||
downloadLink.style.display = 'none';
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
const generateGame = (raceMode = false) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
|
||||
axios.post('/api/generate', {
|
||||
weights: { player: options },
|
||||
presetData: { player: options },
|
||||
playerCount: 1,
|
||||
spoiler: 3,
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
window.location.href = response.data.url;
|
||||
}).catch((error) => {
|
||||
let userMessage = 'Something went wrong and your game could not be generated.';
|
||||
if (error.response.data.text) {
|
||||
userMessage += ' ' + error.response.data.text;
|
||||
}
|
||||
showUserMessage(userMessage);
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const showUserMessage = (message) => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = message;
|
||||
userMessage.classList.add('visible');
|
||||
window.scrollTo(0, 0);
|
||||
userMessage.addEventListener('click', () => {
|
||||
userMessage.classList.remove('visible');
|
||||
userMessage.addEventListener('click', hideUserMessage);
|
||||
});
|
||||
};
|
||||
|
||||
const hideUserMessage = () => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.classList.remove('visible');
|
||||
userMessage.removeEventListener('click', hideUserMessage);
|
||||
};
|
||||
335
WebHostLib/static/assets/playerOptions.js
Normal file
335
WebHostLib/static/assets/playerOptions.js
Normal file
@@ -0,0 +1,335 @@
|
||||
let presets = {};
|
||||
|
||||
window.addEventListener('load', async () => {
|
||||
// Load settings from localStorage, if available
|
||||
loadSettings();
|
||||
|
||||
// Fetch presets if available
|
||||
await fetchPresets();
|
||||
|
||||
// Handle changes to range inputs
|
||||
document.querySelectorAll('input[type=range]').forEach((range) => {
|
||||
const optionName = range.getAttribute('id');
|
||||
range.addEventListener('change', () => {
|
||||
document.getElementById(`${optionName}-value`).innerText = range.value;
|
||||
|
||||
// Handle updating named range selects to "custom" if appropriate
|
||||
const select = document.querySelector(`select[data-option-name=${optionName}]`);
|
||||
if (select) {
|
||||
let updated = false;
|
||||
select?.childNodes.forEach((option) => {
|
||||
if (option.value === range.value) {
|
||||
select.value = range.value;
|
||||
updated = true;
|
||||
}
|
||||
});
|
||||
if (!updated) {
|
||||
select.value = 'custom';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle changes to named range selects
|
||||
document.querySelectorAll('.named-range-container select').forEach((select) => {
|
||||
const optionName = select.getAttribute('data-option-name');
|
||||
select.addEventListener('change', (evt) => {
|
||||
document.getElementById(optionName).value = evt.target.value;
|
||||
document.getElementById(`${optionName}-value`).innerText = evt.target.value;
|
||||
});
|
||||
});
|
||||
|
||||
// Handle changes to randomize checkboxes
|
||||
document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => {
|
||||
const optionName = checkbox.getAttribute('data-option-name');
|
||||
checkbox.addEventListener('change', () => {
|
||||
const optionInput = document.getElementById(optionName);
|
||||
const namedRangeSelect = document.querySelector(`select[data-option-name=${optionName}]`);
|
||||
const customInput = document.getElementById(`${optionName}-custom`);
|
||||
if (checkbox.checked) {
|
||||
optionInput.setAttribute('disabled', '1');
|
||||
namedRangeSelect?.setAttribute('disabled', '1');
|
||||
if (customInput) {
|
||||
customInput.setAttribute('disabled', '1');
|
||||
}
|
||||
} else {
|
||||
optionInput.removeAttribute('disabled');
|
||||
namedRangeSelect?.removeAttribute('disabled');
|
||||
if (customInput) {
|
||||
customInput.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle changes to TextChoice input[type=text]
|
||||
document.querySelectorAll('.text-choice-container input[type=text]').forEach((input) => {
|
||||
const optionName = input.getAttribute('data-option-name');
|
||||
input.addEventListener('input', () => {
|
||||
const select = document.getElementById(optionName);
|
||||
const optionValues = [];
|
||||
select.childNodes.forEach((option) => optionValues.push(option.value));
|
||||
select.value = (optionValues.includes(input.value)) ? input.value : 'custom';
|
||||
});
|
||||
});
|
||||
|
||||
// Handle changes to TextChoice select
|
||||
document.querySelectorAll('.text-choice-container select').forEach((select) => {
|
||||
const optionName = select.getAttribute('id');
|
||||
select.addEventListener('change', () => {
|
||||
document.getElementById(`${optionName}-custom`).value = '';
|
||||
});
|
||||
});
|
||||
|
||||
// Update the "Option Preset" select to read "custom" when changes are made to relevant inputs
|
||||
const presetSelect = document.getElementById('game-options-preset');
|
||||
document.querySelectorAll('input, select').forEach((input) => {
|
||||
if ( // Ignore inputs which have no effect on yaml generation
|
||||
(input.id === 'player-name') ||
|
||||
(input.id === 'game-options-preset') ||
|
||||
(input.classList.contains('group-toggle')) ||
|
||||
(input.type === 'submit')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
input.addEventListener('change', () => {
|
||||
presetSelect.value = 'custom';
|
||||
});
|
||||
});
|
||||
|
||||
// Handle changes to presets select
|
||||
document.getElementById('game-options-preset').addEventListener('change', choosePreset);
|
||||
|
||||
// Save settings to localStorage when form is submitted
|
||||
document.getElementById('options-form').addEventListener('submit', (evt) => {
|
||||
const playerName = document.getElementById('player-name');
|
||||
if (!playerName.value.trim()) {
|
||||
evt.preventDefault();
|
||||
window.scrollTo(0, 0);
|
||||
showUserMessage('You must enter a player name!');
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Save all settings to localStorage
|
||||
const saveSettings = () => {
|
||||
const options = {
|
||||
inputs: {},
|
||||
checkboxes: {},
|
||||
};
|
||||
document.querySelectorAll('input, select').forEach((input) => {
|
||||
if (input.type === 'submit') {
|
||||
// Ignore submit inputs
|
||||
}
|
||||
else if (input.type === 'checkbox') {
|
||||
options.checkboxes[input.id] = input.checked;
|
||||
}
|
||||
else {
|
||||
options.inputs[input.id] = input.value
|
||||
}
|
||||
});
|
||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
||||
localStorage.setItem(game, JSON.stringify(options));
|
||||
};
|
||||
|
||||
// Load all options from localStorage
|
||||
const loadSettings = () => {
|
||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
||||
|
||||
const options = JSON.parse(localStorage.getItem(game));
|
||||
if (options) {
|
||||
if (!options.inputs || !options.checkboxes) {
|
||||
localStorage.removeItem(game);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore value-based inputs and selects
|
||||
Object.keys(options.inputs).forEach((key) => {
|
||||
try{
|
||||
document.getElementById(key).value = options.inputs[key];
|
||||
const rangeValue = document.getElementById(`${key}-value`);
|
||||
if (rangeValue) {
|
||||
rangeValue.innerText = options.inputs[key];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Unable to restore value to input with id ${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Restore checkboxes
|
||||
Object.keys(options.checkboxes).forEach((key) => {
|
||||
try{
|
||||
if (options.checkboxes[key]) {
|
||||
document.getElementById(key).setAttribute('checked', '1');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Unable to restore value to input with id ${key}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure any input for which the randomize checkbox is checked by default, the relevant inputs are disabled
|
||||
document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => {
|
||||
const optionName = checkbox.getAttribute('data-option-name');
|
||||
if (checkbox.checked) {
|
||||
const input = document.getElementById(optionName);
|
||||
if (input) {
|
||||
input.setAttribute('disabled', '1');
|
||||
}
|
||||
const customInput = document.getElementById(`${optionName}-custom`);
|
||||
if (customInput) {
|
||||
customInput.setAttribute('disabled', '1');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the preset data for this game and apply the presets if localStorage indicates one was previously chosen
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const fetchPresets = async () => {
|
||||
const response = await fetch('option-presets');
|
||||
presets = await response.json();
|
||||
const presetSelect = document.getElementById('game-options-preset');
|
||||
presetSelect.removeAttribute('disabled');
|
||||
|
||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
||||
const presetToApply = localStorage.getItem(`${game}-preset`);
|
||||
const playerName = localStorage.getItem(`${game}-player`);
|
||||
if (presetToApply) {
|
||||
localStorage.removeItem(`${game}-preset`);
|
||||
presetSelect.value = presetToApply;
|
||||
applyPresets(presetToApply);
|
||||
}
|
||||
|
||||
if (playerName) {
|
||||
document.getElementById('player-name').value = playerName;
|
||||
localStorage.removeItem(`${game}-player`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the localStorage for this game and set a preset to be loaded upon page reload
|
||||
* @param evt
|
||||
*/
|
||||
const choosePreset = (evt) => {
|
||||
if (evt.target.value === 'custom') { return; }
|
||||
|
||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
||||
localStorage.removeItem(game);
|
||||
|
||||
localStorage.setItem(`${game}-player`, document.getElementById('player-name').value);
|
||||
if (evt.target.value !== 'default') {
|
||||
localStorage.setItem(`${game}-preset`, evt.target.value);
|
||||
}
|
||||
|
||||
document.querySelectorAll('#options-form input, #options-form select').forEach((input) => {
|
||||
if (input.id === 'player-name') { return; }
|
||||
input.removeAttribute('value');
|
||||
});
|
||||
|
||||
window.location.replace(window.location.href);
|
||||
};
|
||||
|
||||
const applyPresets = (presetName) => {
|
||||
// Ignore the "default" preset, because it gets set automatically by Jinja
|
||||
if (presetName === 'default') {
|
||||
saveSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!presets[presetName]) {
|
||||
console.error(`Unknown preset ${presetName} chosen`);
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = presets[presetName];
|
||||
Object.keys(preset).forEach((optionName) => {
|
||||
const optionValue = preset[optionName];
|
||||
|
||||
// Handle List and Set options
|
||||
if (Array.isArray(optionValue)) {
|
||||
document.querySelectorAll(`input[type=checkbox][name=${optionName}]`).forEach((checkbox) => {
|
||||
if (optionValue.includes(checkbox.value)) {
|
||||
checkbox.setAttribute('checked', '1');
|
||||
} else {
|
||||
checkbox.removeAttribute('checked');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Dict options
|
||||
if (typeof(optionValue) === 'object' && optionValue !== null) {
|
||||
const itemNames = Object.keys(optionValue);
|
||||
document.querySelectorAll(`input[type=number][data-option-name=${optionName}]`).forEach((input) => {
|
||||
const itemName = input.getAttribute('data-item-name');
|
||||
input.value = (itemNames.includes(itemName)) ? optionValue[itemName] : 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Identify all possible elements
|
||||
const normalInput = document.getElementById(optionName);
|
||||
const customInput = document.getElementById(`${optionName}-custom`);
|
||||
const rangeValue = document.getElementById(`${optionName}-value`);
|
||||
const randomizeInput = document.getElementById(`random-${optionName}`);
|
||||
const namedRangeSelect = document.getElementById(`${optionName}-select`);
|
||||
|
||||
// It is possible for named ranges to use name of a value rather than the value itself. This is accounted for here
|
||||
let trueValue = optionValue;
|
||||
if (namedRangeSelect) {
|
||||
namedRangeSelect.querySelectorAll('option').forEach((opt) => {
|
||||
if (opt.innerText.startsWith(optionValue)) {
|
||||
trueValue = opt.value;
|
||||
}
|
||||
});
|
||||
namedRangeSelect.value = trueValue;
|
||||
}
|
||||
|
||||
// Handle options whose presets are "random"
|
||||
if (optionValue === 'random') {
|
||||
normalInput.setAttribute('disabled', '1');
|
||||
randomizeInput.setAttribute('checked', '1');
|
||||
if (customInput) {
|
||||
customInput.setAttribute('disabled', '1');
|
||||
}
|
||||
if (rangeValue) {
|
||||
rangeValue.innerText = normalInput.value;
|
||||
}
|
||||
if (namedRangeSelect) {
|
||||
namedRangeSelect.setAttribute('disabled', '1');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle normal (text, number, select, etc.) and custom inputs (custom inputs exist with TextChoice only)
|
||||
normalInput.value = trueValue;
|
||||
normalInput.removeAttribute('disabled');
|
||||
randomizeInput.removeAttribute('checked');
|
||||
if (customInput) {
|
||||
document.getElementById(`${optionName}-custom`).removeAttribute('disabled');
|
||||
}
|
||||
if (rangeValue) {
|
||||
rangeValue.innerText = trueValue;
|
||||
}
|
||||
});
|
||||
|
||||
saveSettings();
|
||||
};
|
||||
|
||||
const showUserMessage = (text) => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = text;
|
||||
userMessage.addEventListener('click', hideUserMessage);
|
||||
userMessage.style.display = 'block';
|
||||
};
|
||||
|
||||
const hideUserMessage = () => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.removeEventListener('click', hideUserMessage);
|
||||
userMessage.style.display = 'none';
|
||||
};
|
||||
@@ -1,18 +1,16 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Add toggle listener to all elements with .collapse-toggle
|
||||
const toggleButtons = document.querySelectorAll('.collapse-toggle');
|
||||
toggleButtons.forEach((e) => e.addEventListener('click', toggleCollapse));
|
||||
const toggleButtons = document.querySelectorAll('details');
|
||||
|
||||
// Handle game filter input
|
||||
const gameSearch = document.getElementById('game-search');
|
||||
gameSearch.value = '';
|
||||
gameSearch.addEventListener('input', (evt) => {
|
||||
if (!evt.target.value.trim()) {
|
||||
// If input is empty, display all collapsed games
|
||||
// If input is empty, display all games as collapsed
|
||||
return toggleButtons.forEach((header) => {
|
||||
header.style.display = null;
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
header.removeAttribute('open');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,12 +19,10 @@ window.addEventListener('load', () => {
|
||||
// If the game name includes the search string, display the game. If not, hide it
|
||||
if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) {
|
||||
header.style.display = null;
|
||||
header.firstElementChild.innerText = '▼';
|
||||
header.nextElementSibling.classList.remove('collapsed');
|
||||
header.setAttribute('open', '1');
|
||||
} else {
|
||||
header.style.display = 'none';
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
header.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -35,30 +31,14 @@ window.addEventListener('load', () => {
|
||||
document.getElementById('collapse-all').addEventListener('click', collapseAll);
|
||||
});
|
||||
|
||||
const toggleCollapse = (evt) => {
|
||||
const gameArrow = evt.target.firstElementChild;
|
||||
const gameInfo = evt.target.nextElementSibling;
|
||||
if (gameInfo.classList.contains('collapsed')) {
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
} else {
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
}
|
||||
};
|
||||
|
||||
const expandAll = () => {
|
||||
document.querySelectorAll('.collapse-toggle').forEach((header) => {
|
||||
if (header.style.display === 'none') { return; }
|
||||
header.firstElementChild.innerText = '▼';
|
||||
header.nextElementSibling.classList.remove('collapsed');
|
||||
document.querySelectorAll('details').forEach((detail) => {
|
||||
detail.setAttribute('open', '1');
|
||||
});
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
document.querySelectorAll('.collapse-toggle').forEach((header) => {
|
||||
if (header.style.display === 'none') { return; }
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
document.querySelectorAll('details').forEach((detail) => {
|
||||
detail.removeAttribute('open');
|
||||
});
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
223
WebHostLib/static/assets/weightedOptions.js
Normal file
223
WebHostLib/static/assets/weightedOptions.js
Normal file
@@ -0,0 +1,223 @@
|
||||
let deletedOptions = {};
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const worldName = document.querySelector('#weighted-options').getAttribute('data-game');
|
||||
|
||||
// Generic change listener. Detecting unique qualities and acting on them here reduces initial JS initialisation time
|
||||
// and handles dynamically created elements
|
||||
document.addEventListener('change', (evt) => {
|
||||
// Handle updates to range inputs
|
||||
if (evt.target.type === 'range') {
|
||||
// Update span containing range value. All ranges have a corresponding `{rangeId}-value` span
|
||||
document.getElementById(`${evt.target.id}-value`).innerText = evt.target.value;
|
||||
|
||||
// If the changed option was the name of a game, determine whether to show or hide that game's div
|
||||
if (evt.target.id.startsWith('game||')) {
|
||||
const gameName = evt.target.id.split('||')[1];
|
||||
const gameDiv = document.getElementById(`${gameName}-container`);
|
||||
if (evt.target.value > 0) {
|
||||
gameDiv.classList.remove('hidden');
|
||||
} else {
|
||||
gameDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Generic click listener
|
||||
document.addEventListener('click', (evt) => {
|
||||
// Handle creating new rows for Range options
|
||||
if (evt.target.classList.contains('add-range-option-button')) {
|
||||
const optionName = evt.target.getAttribute('data-option');
|
||||
addRangeRow(optionName);
|
||||
}
|
||||
|
||||
// Handle deleting range rows
|
||||
if (evt.target.classList.contains('range-option-delete')) {
|
||||
const targetRow = document.querySelector(`tr[data-row="${evt.target.getAttribute('data-target')}"]`);
|
||||
setDeletedOption(
|
||||
targetRow.getAttribute('data-option-name'),
|
||||
targetRow.getAttribute('data-value'),
|
||||
);
|
||||
targetRow.parentElement.removeChild(targetRow);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for enter presses on inputs intended to add range rows
|
||||
document.addEventListener('keydown', (evt) => {
|
||||
if (evt.key === 'Enter') {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
if (evt.key === 'Enter' && evt.target.classList.contains('range-option-value')) {
|
||||
const optionName = evt.target.getAttribute('data-option');
|
||||
addRangeRow(optionName);
|
||||
}
|
||||
});
|
||||
|
||||
// Detect form submission
|
||||
document.getElementById('weighted-options-form').addEventListener('submit', (evt) => {
|
||||
// Save data to localStorage
|
||||
const weightedOptions = {};
|
||||
document.querySelectorAll('input[name]').forEach((input) => {
|
||||
const keys = input.getAttribute('name').split('||');
|
||||
|
||||
// Determine keys
|
||||
const optionName = keys[0] ?? null;
|
||||
const subOption = keys[1] ?? null;
|
||||
|
||||
// Ensure keys exist
|
||||
if (!weightedOptions[optionName]) { weightedOptions[optionName] = {}; }
|
||||
if (subOption && !weightedOptions[optionName][subOption]) {
|
||||
weightedOptions[optionName][subOption] = null;
|
||||
}
|
||||
|
||||
if (subOption) { return weightedOptions[optionName][subOption] = determineValue(input); }
|
||||
if (optionName) { return weightedOptions[optionName] = determineValue(input); }
|
||||
});
|
||||
|
||||
localStorage.setItem(`${worldName}-weights`, JSON.stringify(weightedOptions));
|
||||
localStorage.setItem(`${worldName}-deletedOptions`, JSON.stringify(deletedOptions));
|
||||
});
|
||||
|
||||
// Remove all deleted values as specified by localStorage
|
||||
deletedOptions = JSON.parse(localStorage.getItem(`${worldName}-deletedOptions`) || '{}');
|
||||
Object.keys(deletedOptions).forEach((optionName) => {
|
||||
deletedOptions[optionName].forEach((value) => {
|
||||
const targetRow = document.querySelector(`tr[data-row="${value}-row"]`);
|
||||
targetRow.parentElement.removeChild(targetRow);
|
||||
});
|
||||
});
|
||||
|
||||
// Populate all settings from localStorage on page initialisation
|
||||
const previousSettingsJson = localStorage.getItem(`${worldName}-weights`);
|
||||
if (previousSettingsJson) {
|
||||
const previousSettings = JSON.parse(previousSettingsJson);
|
||||
Object.keys(previousSettings).forEach((option) => {
|
||||
if (typeof previousSettings[option] === 'string') {
|
||||
return document.querySelector(`input[name="${option}"]`).value = previousSettings[option];
|
||||
}
|
||||
|
||||
Object.keys(previousSettings[option]).forEach((value) => {
|
||||
const input = document.querySelector(`input[name="${option}||${value}"]`);
|
||||
if (!input?.type) {
|
||||
return console.error(`Unable to populate option with name ${option}||${value}.`);
|
||||
}
|
||||
|
||||
switch (input.type) {
|
||||
case 'checkbox':
|
||||
input.checked = (parseInt(previousSettings[option][value], 10) === 1);
|
||||
break;
|
||||
case 'range':
|
||||
input.value = parseInt(previousSettings[option][value], 10);
|
||||
break;
|
||||
case 'number':
|
||||
input.value = previousSettings[option][value].toString();
|
||||
break;
|
||||
default:
|
||||
console.error(`Found unsupported input type: ${input.type}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const addRangeRow = (optionName) => {
|
||||
const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
|
||||
const inputTarget = document.querySelector(inputQuery);
|
||||
const newValue = inputTarget.value;
|
||||
if (!/^-?\d+$/.test(newValue)) {
|
||||
alert('Range values must be a positive or negative integer!');
|
||||
return;
|
||||
}
|
||||
inputTarget.value = '';
|
||||
const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
|
||||
const tr = document.createElement('tr');
|
||||
tr.setAttribute('data-row', `${optionName}-${newValue}-row`);
|
||||
tr.setAttribute('data-option-name', optionName);
|
||||
tr.setAttribute('data-value', newValue);
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
const label = document.createElement('label');
|
||||
label.setAttribute('for', `${optionName}||${newValue}`);
|
||||
label.innerText = newValue.toString();
|
||||
tdLeft.appendChild(label);
|
||||
tr.appendChild(tdLeft);
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('min', '0');
|
||||
range.setAttribute('max', '50');
|
||||
range.setAttribute('value', '0');
|
||||
range.setAttribute('id', `${optionName}||${newValue}`);
|
||||
range.setAttribute('name', `${optionName}||${newValue}`);
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.classList.add('td-right');
|
||||
const valueSpan = document.createElement('span');
|
||||
valueSpan.setAttribute('id', `${optionName}||${newValue}-value`);
|
||||
valueSpan.innerText = '0';
|
||||
tdRight.appendChild(valueSpan);
|
||||
tr.appendChild(tdRight);
|
||||
const tdDelete = document.createElement('td');
|
||||
const deleteSpan = document.createElement('span');
|
||||
deleteSpan.classList.add('range-option-delete');
|
||||
deleteSpan.classList.add('js-required');
|
||||
deleteSpan.setAttribute('data-target', `${optionName}-${newValue}-row`);
|
||||
deleteSpan.innerText = '❌';
|
||||
tdDelete.appendChild(deleteSpan);
|
||||
tr.appendChild(tdDelete);
|
||||
tBody.appendChild(tr);
|
||||
|
||||
// Remove this option from the set of deleted options if it exists
|
||||
unsetDeletedOption(optionName, newValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the value of an input element, or returns a 1 or 0 if the element is a checkbox
|
||||
*
|
||||
* @param {object} input - The input element.
|
||||
* @returns {number} The value of the input element.
|
||||
*/
|
||||
const determineValue = (input) => {
|
||||
switch (input.type) {
|
||||
case 'checkbox':
|
||||
return (input.checked ? 1 : 0);
|
||||
case 'range':
|
||||
return parseInt(input.value, 10);
|
||||
default:
|
||||
return input.value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the deleted option value for a given world and option name.
|
||||
* If the world or option does not exist, it creates the necessary entries.
|
||||
*
|
||||
* @param {string} optionName - The name of the option.
|
||||
* @param {*} value - The value to be set for the deleted option.
|
||||
* @returns {void}
|
||||
*/
|
||||
const setDeletedOption = (optionName, value) => {
|
||||
deletedOptions[optionName] = deletedOptions[optionName] || [];
|
||||
deletedOptions[optionName].push(`${optionName}-${value}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a specific value from the deletedOptions object.
|
||||
*
|
||||
* @param {string} optionName - The name of the option.
|
||||
* @param {*} value - The value to be removed
|
||||
* @returns {void}
|
||||
*/
|
||||
const unsetDeletedOption = (optionName, value) => {
|
||||
if (!deletedOptions.hasOwnProperty(optionName)) { return; }
|
||||
if (deletedOptions[optionName].includes(`${optionName}-${value}`)) {
|
||||
deletedOptions[optionName].splice(deletedOptions[optionName].indexOf(`${optionName}-${value}`), 1);
|
||||
}
|
||||
if (deletedOptions[optionName].length === 0) {
|
||||
delete deletedOptions[optionName];
|
||||
}
|
||||
};
|
||||
@@ -44,7 +44,7 @@ a{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
}
|
||||
|
||||
button{
|
||||
button, input[type=submit]{
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
padding: 10px 17px 11px 16px; /* top right bottom left */
|
||||
@@ -57,7 +57,7 @@ button{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:active{
|
||||
button:active, input[type=submit]:active{
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
|
||||
padding-right: 16px;
|
||||
@@ -66,11 +66,11 @@ button:active{
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
button.button-grass{
|
||||
button.button-grass, input[type=submit].button-grass{
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
button.button-dirt{
|
||||
button.button-dirt, input[type=submit].button-dirt{
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
@@ -111,4 +111,4 @@ h5, h6{
|
||||
|
||||
.interactive{
|
||||
color: #ffef00;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
.markdown a{}
|
||||
|
||||
.markdown h1{
|
||||
.markdown h1, .markdown details summary.h1{
|
||||
font-size: 52px;
|
||||
font-weight: normal;
|
||||
font-family: LondrinaSolid-Regular, sans-serif;
|
||||
@@ -33,7 +33,7 @@
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
.markdown h2{
|
||||
.markdown h2, .markdown details summary.h2{
|
||||
font-size: 38px;
|
||||
font-weight: normal;
|
||||
font-family: LondrinaSolid-Light, sans-serif;
|
||||
@@ -45,7 +45,7 @@
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
.markdown h3{
|
||||
.markdown h3, .markdown details summary.h3{
|
||||
font-size: 26px;
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
text-transform: none;
|
||||
@@ -55,7 +55,7 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown h4{
|
||||
.markdown h4, .markdown details summary.h4{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
text-transform: none;
|
||||
font-size: 24px;
|
||||
@@ -63,21 +63,21 @@
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown h5{
|
||||
.markdown h5, .markdown details summary.h5{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
text-transform: none;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markdown h6{
|
||||
.markdown h6, .markdown details summary.h6{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
text-transform: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;;
|
||||
}
|
||||
|
||||
.markdown h4, .markdown h5,.markdown h6{
|
||||
.markdown h4, .markdown h5, .markdown h6{
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/grass.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#player-options{
|
||||
box-sizing: border-box;
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#player-options #player-options-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#player-options code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#player-options #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#player-options #user-message.visible{
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#player-options h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#player-options h2{
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#player-options h3, #player-options h4, #player-options h5, #player-options h6{
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#player-options input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#player-options input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#player-options select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#player-options #game-options, #player-options #rom-options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-options #meta-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#player-options div {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options #meta-options label {
|
||||
display: inline-block;
|
||||
min-width: 180px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options #meta-options input,
|
||||
#player-options #meta-options select {
|
||||
box-sizing: border-box;
|
||||
min-width: 150px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#player-options .left, #player-options .right{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options .left{
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#player-options .right{
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#player-options table{
|
||||
margin-bottom: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#player-options table .select-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-options table .select-container select{
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options table select:disabled{
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
#player-options table .range-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-options table .range-container input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options table .range-value{
|
||||
min-width: 20px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
#player-options table .named-range-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#player-options table .named-range-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
#player-options table .named-range-wrapper input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options table .randomize-button {
|
||||
max-height: 24px;
|
||||
line-height: 16px;
|
||||
padding: 2px 8px;
|
||||
margin: 0 0 0 0.25rem;
|
||||
font-size: 12px;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#player-options table .randomize-button.active {
|
||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
||||
}
|
||||
|
||||
#player-options table .randomize-button[data-tooltip]::after {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#player-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#player-options th, #player-options td{
|
||||
border: none;
|
||||
padding: 3px;
|
||||
font-size: 17px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1024px) {
|
||||
#player-options {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#player-options #meta-options {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
#player-options #game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#player-options .left,
|
||||
#player-options .right {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#game-options table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#game-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
#game-options table tr td {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
310
WebHostLib/static/styles/playerOptions/playerOptions.css
Normal file
310
WebHostLib/static/styles/playerOptions/playerOptions.css
Normal file
@@ -0,0 +1,310 @@
|
||||
@import "../markdown.css";
|
||||
html {
|
||||
background-image: url("../../static/backgrounds/grass.png");
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#player-options {
|
||||
box-sizing: border-box;
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
word-break: break-all;
|
||||
}
|
||||
#player-options #player-options-header h1 {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
#player-options #player-options-header h1:nth-child(2) {
|
||||
font-size: 1.4rem;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#player-options .js-warning-banner {
|
||||
width: calc(100% - 1rem);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background-color: #f3f309;
|
||||
color: #000000;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
#player-options .group-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
#player-options .group-container h2 {
|
||||
user-select: none;
|
||||
cursor: unset;
|
||||
}
|
||||
#player-options .group-container h2 label {
|
||||
cursor: pointer;
|
||||
}
|
||||
#player-options #player-options-button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
#player-options #user-message {
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
#player-options h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
#player-options h2 {
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
#player-options h3, #player-options h4, #player-options h5, #player-options h6 {
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
#player-options input:not([type]) {
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
#player-options input:not([type]):focus {
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
#player-options select {
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#player-options .game-options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
#player-options .game-options .left, #player-options .game-options .right {
|
||||
display: grid;
|
||||
grid-template-columns: 12rem auto;
|
||||
grid-row-gap: 0.5rem;
|
||||
grid-auto-rows: min-content;
|
||||
align-items: start;
|
||||
min-width: 480px;
|
||||
width: 50%;
|
||||
}
|
||||
#player-options #meta-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 3px;
|
||||
}
|
||||
#player-options #meta-options input, #player-options #meta-options select {
|
||||
box-sizing: border-box;
|
||||
width: 200px;
|
||||
}
|
||||
#player-options .left, #player-options .right {
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#player-options .left {
|
||||
margin-right: 20px;
|
||||
}
|
||||
#player-options .select-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 270px;
|
||||
}
|
||||
#player-options .select-container select {
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
#player-options .select-container select:disabled {
|
||||
background-color: lightgray;
|
||||
}
|
||||
#player-options .range-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 270px;
|
||||
}
|
||||
#player-options .range-container input[type=range] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#player-options .range-container .range-value {
|
||||
min-width: 20px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
#player-options .named-range-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
}
|
||||
#player-options .named-range-container .named-range-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
#player-options .named-range-container .named-range-wrapper input[type=range] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#player-options .free-text-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
}
|
||||
#player-options .free-text-container input[type=text] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#player-options .text-choice-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
}
|
||||
#player-options .text-choice-container .text-choice-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
#player-options .text-choice-container .text-choice-wrapper select {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#player-options .option-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
max-height: 10rem;
|
||||
min-width: 14.5rem;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
#player-options .option-container .option-divider {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
#player-options .option-container .option-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.125rem;
|
||||
margin-top: 0.125rem;
|
||||
user-select: none;
|
||||
}
|
||||
#player-options .option-container .option-entry:hover {
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
}
|
||||
#player-options .option-container .option-entry input[type=checkbox] {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
#player-options .option-container .option-entry input[type=number] {
|
||||
max-width: 1.5rem;
|
||||
max-height: 1rem;
|
||||
margin-left: 0.125rem;
|
||||
text-align: center;
|
||||
/* Hide arrows on input[type=number] fields */
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
#player-options .option-container .option-entry input[type=number]::-webkit-outer-spin-button, #player-options .option-container .option-entry input[type=number]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
#player-options .option-container .option-entry label {
|
||||
flex-grow: 1;
|
||||
margin-right: 0;
|
||||
min-width: unset;
|
||||
display: unset;
|
||||
}
|
||||
#player-options .randomize-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 22px;
|
||||
max-width: 30px;
|
||||
margin: 0 0 0 0.25rem;
|
||||
font-size: 14px;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
background-color: #d3d3d3;
|
||||
user-select: none;
|
||||
}
|
||||
#player-options .randomize-button:hover {
|
||||
background-color: #c0c0c0;
|
||||
cursor: pointer;
|
||||
}
|
||||
#player-options .randomize-button label {
|
||||
line-height: 22px;
|
||||
padding-left: 5px;
|
||||
padding-right: 2px;
|
||||
margin-right: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: unset;
|
||||
}
|
||||
#player-options .randomize-button label:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
#player-options .randomize-button input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
#player-options .randomize-button:has(input[type=checkbox]:checked) {
|
||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
||||
}
|
||||
#player-options .randomize-button:has(input[type=checkbox]:checked):hover {
|
||||
background-color: #eedd27;
|
||||
}
|
||||
#player-options .randomize-button[data-tooltip]::after {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
#player-options label {
|
||||
display: block;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
word-break: break-word;
|
||||
}
|
||||
#player-options th, #player-options td {
|
||||
border: none;
|
||||
padding: 3px;
|
||||
font-size: 17px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1024px) {
|
||||
#player-options {
|
||||
border-radius: 0;
|
||||
}
|
||||
#player-options #meta-options {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
#player-options .game-options {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=playerOptions.css.map */
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["playerOptions.scss"],"names":[],"mappings":"AAAQ;AAER;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAEA;EACI;;AAIR;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAIR;EACI;IACI;;EAEA;IACI;IACA;IACA;;EAGJ;IACI;IACA","file":"playerOptions.css"}
|
||||
364
WebHostLib/static/styles/playerOptions/playerOptions.scss
Normal file
364
WebHostLib/static/styles/playerOptions/playerOptions.scss
Normal file
@@ -0,0 +1,364 @@
|
||||
@import "../markdown.css";
|
||||
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/grass.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#player-options{
|
||||
box-sizing: border-box;
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
word-break: break-all;
|
||||
|
||||
#player-options-header{
|
||||
h1{
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
h1:nth-child(2){
|
||||
font-size: 1.4rem;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.js-warning-banner{
|
||||
width: calc(100% - 1rem);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background-color: #f3f309;
|
||||
color: #000000;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.group-container{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
h2{
|
||||
user-select: none;
|
||||
cursor: unset;
|
||||
|
||||
label{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#player-options-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
h2{
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6{
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
|
||||
&:focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.game-options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.left, .right{
|
||||
display: grid;
|
||||
grid-template-columns: 12rem auto;
|
||||
grid-row-gap: 0.5rem;
|
||||
grid-auto-rows: min-content;
|
||||
align-items: start;
|
||||
min-width: 480px;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
#meta-options{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 3px;
|
||||
|
||||
input, select{
|
||||
box-sizing: border-box;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.left, .right{
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.left{
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.select-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 270px;
|
||||
|
||||
select{
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
|
||||
&:disabled{
|
||||
background-color: lightgray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.range-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 270px;
|
||||
|
||||
input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.range-value{
|
||||
min-width: 20px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.named-range-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
|
||||
.named-range-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.free-text-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
|
||||
input[type=text]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.text-choice-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
|
||||
.text-choice-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
select{
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
max-height: 10rem;
|
||||
min-width: 14.5rem;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
|
||||
.option-divider{
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.option-entry{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.125rem;
|
||||
margin-top: 0.125rem;
|
||||
user-select: none;
|
||||
|
||||
&:hover{
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
}
|
||||
|
||||
input[type=checkbox]{
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
input[type=number]{
|
||||
max-width: 1.5rem;
|
||||
max-height: 1rem;
|
||||
margin-left: 0.125rem;
|
||||
text-align: center;
|
||||
|
||||
/* Hide arrows on input[type=number] fields */
|
||||
-moz-appearance: textfield;
|
||||
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label{
|
||||
flex-grow: 1;
|
||||
margin-right: 0;
|
||||
min-width: unset;
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.randomize-button{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 22px;
|
||||
max-width: 30px;
|
||||
margin: 0 0 0 0.25rem;
|
||||
font-size: 14px;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
background-color: #d3d3d3;
|
||||
user-select: none;
|
||||
|
||||
&:hover{
|
||||
background-color: #c0c0c0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label{
|
||||
line-height: 22px;
|
||||
padding-left: 5px;
|
||||
padding-right: 2px;
|
||||
margin-right: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: unset;
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox]{
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:has(input[type=checkbox]:checked){
|
||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
||||
|
||||
&:hover{
|
||||
background-color: #eedd27;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-tooltip]::after{
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label{
|
||||
display: block;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
th, td{
|
||||
border: none;
|
||||
padding: 3px;
|
||||
font-size: 17px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 1024px) {
|
||||
#player-options {
|
||||
border-radius: 0;
|
||||
|
||||
#meta-options {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,30 +8,15 @@
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
#games h1{
|
||||
#games h1, #games details summary.h1{
|
||||
font-size: 60px;
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
#games h2{
|
||||
#games h2, #games details summary.h2{
|
||||
color: #93dcff;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#games .collapse-toggle{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#games h2 .collapse-arrow{
|
||||
font-size: 20px;
|
||||
display: inline-block; /* make vertical-align work */
|
||||
padding-bottom: 9px;
|
||||
vertical-align: middle;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
#games p.collapsed{
|
||||
display: none;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
#games a{
|
||||
|
||||
@@ -42,6 +42,7 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
||||
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/** Directional arrow styles */
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/grass.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
scroll-padding-top: 90px;
|
||||
}
|
||||
|
||||
#weighted-settings{
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#weighted-settings #games-wrapper{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper{
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper .add-option-div{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper .add-option-div button{
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0 0 0 0.15rem;
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper .add-option-div button:active{
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
#weighted-settings p.setting-description{
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
#weighted-settings p.hint-text{
|
||||
margin: 0 0 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#weighted-settings .jump-link{
|
||||
color: #ffef00;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#weighted-settings table{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings table th, #weighted-settings table td{
|
||||
border: none;
|
||||
}
|
||||
|
||||
#weighted-settings table td{
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-left{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
padding-right: 1rem;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-middle{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-right{
|
||||
width: 4rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-delete{
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#weighted-settings table .range-option-delete{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings .items-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#weighted-settings .items-div h3{
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .items-wrapper .item-set-wrapper{
|
||||
width: 24%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container{
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-top: 0.125rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-div{
|
||||
padding: 0.125rem 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-div:hover{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-qty-div{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0.125rem 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-qty-div .item-qty-input-wrapper{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-qty-div input{
|
||||
min-width: unset;
|
||||
width: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-qty-div:hover{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div, #weighted-settings .locations-div{
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-container, #weighted-settings .locations-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{
|
||||
width: calc(50% - 0.5rem);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{
|
||||
margin-top: 0.25rem;
|
||||
height: 300px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#weighted-settings #weighted-settings-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#weighted-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#weighted-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#weighted-settings #user-message.visible{
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: none;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
#weighted-settings a{
|
||||
color: #ffef00;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#weighted-settings select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#weighted-settings .game-options, #weighted-settings .rom-options{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list .list-row label{
|
||||
display: block;
|
||||
width: calc(100% - 0.5rem);
|
||||
padding: 0.0625rem 0.25rem;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list .list-row label:hover{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list .list-row label input[type=checkbox]{
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list hr{
|
||||
width: calc(100% - 2px);
|
||||
margin: 2px auto;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 0.6);
|
||||
}
|
||||
|
||||
#weighted-settings .invisible{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||
#weighted-settings .game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#game-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
232
WebHostLib/static/styles/weightedOptions/weightedOptions.css
Normal file
232
WebHostLib/static/styles/weightedOptions/weightedOptions.css
Normal file
@@ -0,0 +1,232 @@
|
||||
html {
|
||||
background-image: url("../../static/backgrounds/grass.png");
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
scroll-padding-top: 90px;
|
||||
}
|
||||
|
||||
#weighted-options {
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
#weighted-options #weighted-options-header h1 {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
#weighted-options #weighted-options-header h1:nth-child(2) {
|
||||
font-size: 1.4rem;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#weighted-options .js-warning-banner {
|
||||
width: calc(100% - 1rem);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background-color: #f3f309;
|
||||
color: #000000;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
#weighted-options .option-wrapper {
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
#weighted-options .option-wrapper .add-option-div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
#weighted-options .option-wrapper .add-option-div button {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0 0 0 0.15rem;
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
#weighted-options .option-wrapper .add-option-div button:active {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
#weighted-options p.option-description {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
#weighted-options p.hint-text {
|
||||
margin: 0 0 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
#weighted-options table {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
#weighted-options table th, #weighted-options table td {
|
||||
border: none;
|
||||
}
|
||||
#weighted-options table td {
|
||||
padding: 5px;
|
||||
}
|
||||
#weighted-options table .td-left {
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
padding-right: 1rem;
|
||||
width: 200px;
|
||||
}
|
||||
#weighted-options table .td-middle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#weighted-options table .td-right {
|
||||
width: 4rem;
|
||||
text-align: right;
|
||||
}
|
||||
#weighted-options table .td-delete {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
#weighted-options table .range-option-delete {
|
||||
cursor: pointer;
|
||||
}
|
||||
#weighted-options #weighted-options-button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
#weighted-options #user-message {
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
#weighted-options #user-message.visible {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
#weighted-options h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
#weighted-options h2, #weighted-options details summary.h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: none;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
#weighted-options h3, #weighted-options h4, #weighted-options h5, #weighted-options h6 {
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
text-transform: none;
|
||||
cursor: unset;
|
||||
}
|
||||
#weighted-options h3.option-group-header {
|
||||
margin-top: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
#weighted-options a {
|
||||
color: #ffef00;
|
||||
cursor: pointer;
|
||||
}
|
||||
#weighted-options input:not([type]) {
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
#weighted-options input:not([type]):focus {
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
#weighted-options .invisible {
|
||||
display: none;
|
||||
}
|
||||
#weighted-options .unsupported-option {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
#weighted-options .set-container, #weighted-options .dict-container, #weighted-options .list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
max-height: 15rem;
|
||||
min-width: 14.5rem;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
#weighted-options .set-container .divider, #weighted-options .dict-container .divider, #weighted-options .list-container .divider {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
#weighted-options .set-container .set-entry, #weighted-options .set-container .dict-entry, #weighted-options .set-container .list-entry, #weighted-options .dict-container .set-entry, #weighted-options .dict-container .dict-entry, #weighted-options .dict-container .list-entry, #weighted-options .list-container .set-entry, #weighted-options .list-container .dict-entry, #weighted-options .list-container .list-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-top: 0.25rem;
|
||||
user-select: none;
|
||||
line-height: 1rem;
|
||||
}
|
||||
#weighted-options .set-container .set-entry:hover, #weighted-options .set-container .dict-entry:hover, #weighted-options .set-container .list-entry:hover, #weighted-options .dict-container .set-entry:hover, #weighted-options .dict-container .dict-entry:hover, #weighted-options .dict-container .list-entry:hover, #weighted-options .list-container .set-entry:hover, #weighted-options .list-container .dict-entry:hover, #weighted-options .list-container .list-entry:hover {
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
}
|
||||
#weighted-options .set-container .set-entry input[type=checkbox], #weighted-options .set-container .dict-entry input[type=checkbox], #weighted-options .set-container .list-entry input[type=checkbox], #weighted-options .dict-container .set-entry input[type=checkbox], #weighted-options .dict-container .dict-entry input[type=checkbox], #weighted-options .dict-container .list-entry input[type=checkbox], #weighted-options .list-container .set-entry input[type=checkbox], #weighted-options .list-container .dict-entry input[type=checkbox], #weighted-options .list-container .list-entry input[type=checkbox] {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
#weighted-options .set-container .set-entry input[type=number], #weighted-options .set-container .dict-entry input[type=number], #weighted-options .set-container .list-entry input[type=number], #weighted-options .dict-container .set-entry input[type=number], #weighted-options .dict-container .dict-entry input[type=number], #weighted-options .dict-container .list-entry input[type=number], #weighted-options .list-container .set-entry input[type=number], #weighted-options .list-container .dict-entry input[type=number], #weighted-options .list-container .list-entry input[type=number] {
|
||||
max-width: 1.5rem;
|
||||
max-height: 1rem;
|
||||
margin-left: 0.125rem;
|
||||
text-align: center;
|
||||
/* Hide arrows on input[type=number] fields */
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
#weighted-options .set-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
#weighted-options .set-container .set-entry label, #weighted-options .set-container .dict-entry label, #weighted-options .set-container .list-entry label, #weighted-options .dict-container .set-entry label, #weighted-options .dict-container .dict-entry label, #weighted-options .dict-container .list-entry label, #weighted-options .list-container .set-entry label, #weighted-options .list-container .dict-entry label, #weighted-options .list-container .list-entry label {
|
||||
flex-grow: 1;
|
||||
margin-right: 0;
|
||||
min-width: unset;
|
||||
display: unset;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait) {
|
||||
#weighted-options .game-options {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#game-options table label {
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=weightedOptions.css.map */
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["weightedOptions.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAOZ;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;;AAMhB;EACI;;;AAGJ;EACI;IACI;IACA;;EAGJ;IACI;IACA","file":"weightedOptions.css"}
|
||||
274
WebHostLib/static/styles/weightedOptions/weightedOptions.scss
Normal file
274
WebHostLib/static/styles/weightedOptions/weightedOptions.scss
Normal file
@@ -0,0 +1,274 @@
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/grass.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
scroll-padding-top: 90px;
|
||||
}
|
||||
|
||||
#weighted-options{
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
|
||||
#weighted-options-header{
|
||||
h1{
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
h1:nth-child(2){
|
||||
font-size: 1.4rem;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.js-warning-banner{
|
||||
width: calc(100% - 1rem);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background-color: #f3f309;
|
||||
color: #000000;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.option-wrapper{
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.add-option-div{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
button{
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0 0 0 0.15rem;
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
|
||||
&:active{
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p{
|
||||
&.option-description{
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
&.hint-text{
|
||||
margin: 0 0 1rem;
|
||||
font-style: italic;
|
||||
};
|
||||
}
|
||||
|
||||
table{
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
th, td{
|
||||
border: none;
|
||||
}
|
||||
|
||||
td{
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.td-left{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
padding-right: 1rem;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.td-middle{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.td-right{
|
||||
width: 4rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.td-delete{
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.range-option-delete{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
#weighted-options-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
|
||||
&.visible{
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
h2, details summary.h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: none;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
text-transform: none;
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
h3{
|
||||
&.option-group-header{
|
||||
margin-top: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
color: #ffef00;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
|
||||
&:focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.invisible{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.unsupported-option{
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.set-container, .dict-container, .list-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
max-height: 15rem;
|
||||
min-width: 14.5rem;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
.divider{
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.set-entry, .dict-entry, .list-entry{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-top: 0.25rem;
|
||||
user-select: none;
|
||||
line-height: 1rem;
|
||||
|
||||
&:hover{
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
}
|
||||
|
||||
input[type=checkbox]{
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
input[type=number]{
|
||||
max-width: 1.5rem;
|
||||
max-height: 1rem;
|
||||
margin-left: 0.125rem;
|
||||
text-align: center;
|
||||
|
||||
/* Hide arrows on input[type=number] fields */
|
||||
-moz-appearance: textfield;
|
||||
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label{
|
||||
flex-grow: 1;
|
||||
margin-right: 0;
|
||||
min-width: unset;
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hidden{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||
#weighted-options .game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#game-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-options.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-options.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/'+theme+'Header.html' %}
|
||||
<div id="player-options" class="markdown" data-game="{{ game }}">
|
||||
<div id="user-message"></div>
|
||||
<h1><span id="game-name">Player</span> Options</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>
|
||||
A more advanced options configuration for all games can be found on the
|
||||
<a href="/weighted-options">Weighted options</a> page.
|
||||
<br />
|
||||
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
||||
<br />
|
||||
You may also download the
|
||||
<a href="/static/generated/configs/{{ game }}.yaml">template file for this game</a>.
|
||||
</p>
|
||||
|
||||
<div id="meta-options">
|
||||
<div>
|
||||
<label for="player-name">
|
||||
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
|
||||
</label>
|
||||
<input id="player-name" placeholder="Player" data-key="name" maxlength="16" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="game-options-preset">
|
||||
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
|
||||
</label>
|
||||
<select id="game-options-preset">
|
||||
<option value="__default">Defaults</option>
|
||||
<option value="__custom" hidden>Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h2>Game Options</h2>
|
||||
<div id="game-options">
|
||||
<div id="game-options-left" class="left"></div>
|
||||
<div id="game-options-right" class="right"></div>
|
||||
</div>
|
||||
|
||||
<div id="player-options-button-row">
|
||||
<button id="export-options">Export Options</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
210
WebHostLib/templates/playerOptions/macros.html
Normal file
210
WebHostLib/templates/playerOptions/macros.html
Normal file
@@ -0,0 +1,210 @@
|
||||
{% macro Toggle(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="select-container">
|
||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% if option.default == 1 %}
|
||||
<option value="false">No</option>
|
||||
<option value="true" selected>Yes</option>
|
||||
{% else %}
|
||||
<option value="false" selected>No</option>
|
||||
<option value="true">Yes</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
{{ RandomizeButton(option_name, option) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Choice(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="select-container">
|
||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% for id, name in option.name_lookup.items() %}
|
||||
{% if name != "random" %}
|
||||
{% if option.default == id %}
|
||||
<option value="{{ name }}" selected>{{ option.get_option_name(id) }}</option>
|
||||
{% else %}
|
||||
<option value="{{ name }}">{{ option.get_option_name(id) }}</option>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
{{ RandomizeButton(option_name, option) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Range(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="range-container">
|
||||
<input
|
||||
type="range"
|
||||
id="{{ option_name }}"
|
||||
name="{{ option_name }}"
|
||||
min="{{ option.range_start }}"
|
||||
max="{{ option.range_end }}"
|
||||
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
|
||||
{{ "disabled" if option.default == "random" }}
|
||||
/>
|
||||
<span id="{{ option_name }}-value" class="range-value js-required">
|
||||
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
|
||||
</span>
|
||||
{{ RandomizeButton(option_name, option) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro NamedRange(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="named-range-container">
|
||||
<select id="{{ option_name }}-select" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% for key, val in option.special_range_names.items() %}
|
||||
{% if option.default == val %}
|
||||
<option value="{{ val }}" selected>{{ key }} ({{ val }})</option>
|
||||
{% else %}
|
||||
<option value="{{ val }}">{{ key }} ({{ val }})</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<option value="custom" hidden>Custom</option>
|
||||
</select>
|
||||
<div class="named-range-wrapper">
|
||||
<input
|
||||
type="range"
|
||||
id="{{ option_name }}"
|
||||
name="{{ option_name }}"
|
||||
min="{{ option.range_start }}"
|
||||
max="{{ option.range_end }}"
|
||||
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
|
||||
{{ "disabled" if option.default == "random" }}
|
||||
/>
|
||||
<span id="{{ option_name }}-value" class="range-value js-required">
|
||||
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
|
||||
</span>
|
||||
{{ RandomizeButton(option_name, option) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro FreeText(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="free-text-container">
|
||||
<input type="text" id="{{ option_name }}" name="{{ option_name }}" value="{{ option.default }}" />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TextChoice(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="text-choice-container">
|
||||
<div class="text-choice-wrapper">
|
||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% for id, name in option.name_lookup.items()|sort %}
|
||||
{% if name != "random" %}
|
||||
{% if option.default == id %}
|
||||
<option value="{{ name }}" selected>{{ option.get_option_name(id) }}</option>
|
||||
{% else %}
|
||||
<option value="{{ name }}">{{ option.get_option_name(id) }}</option>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<option value="custom" hidden>Custom</option>
|
||||
</select>
|
||||
{{ RandomizeButton(option_name, option) }}
|
||||
</div>
|
||||
<input type="text" id="{{ option_name }}-custom" name="{{ option_name }}-custom" data-option-name="{{ option_name }}" placeholder="Custom value..." />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ItemDict(option_name, option, world) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="option-container">
|
||||
{% for item_name in world.item_names|sort %}
|
||||
<div class="option-entry">
|
||||
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
||||
<input type="number" id="{{ option_name }}-{{ item_name }}-qty" name="{{ option_name }}||{{ item_name }}||qty" value="{{ option.default[item_name]|default("0") }}" data-option-name="{{ option_name }}" data-item-name="{{ item_name }}" />
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro OptionList(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="option-container">
|
||||
{% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}
|
||||
<div class="option-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ key }}" name="{{ option_name }}" value="{{ key }}" {{ "checked" if key in option.default }} />
|
||||
<label for="{{ option_name }}-{{ key }}">{{ key }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro LocationSet(option_name, option, world) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="option-container">
|
||||
{% for group_name in world.location_name_groups.keys()|sort %}
|
||||
{% if group_name != "Everywhere" %}
|
||||
<div class="option-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}" value="{{ group_name }}" {{ "checked" if grop_name in option.default }} />
|
||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if world.location_name_groups.keys()|length > 1 %}
|
||||
<div class="option-divider"> </div>
|
||||
{% endif %}
|
||||
{% for location_name in world.location_names|sort %}
|
||||
<div class="option-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ location_name }}" name="{{ option_name }}" value="{{ location_name }}" {{ "checked" if location_name in option.default }} />
|
||||
<label for="{{ option_name }}-{{ location_name }}">{{ location_name }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ItemSet(option_name, option, world) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="option-container">
|
||||
{% for group_name in world.item_name_groups.keys()|sort %}
|
||||
{% if group_name != "Everything" %}
|
||||
<div class="option-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}" value="{{ group_name }}" {{ "checked" if group_name in option.default }} />
|
||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if world.item_name_groups.keys()|length > 1 %}
|
||||
<div class="option-divider"> </div>
|
||||
{% endif %}
|
||||
{% for item_name in world.item_names|sort %}
|
||||
<div class="option-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ item_name }}" name="{{ option_name }}" value="{{ item_name }}" {{ "checked" if item_name in option.default }} />
|
||||
<label for="{{ option_name }}-{{ item_name }}">{{ item_name }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro OptionSet(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="option-container">
|
||||
{% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}
|
||||
<div class="option-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ key }}" name="{{ option_name }}" value="{{ key }}" {{ "checked" if key in option.default }} />
|
||||
<label for="{{ option_name }}-{{ key }}">{{ key }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro OptionTitle(option_name, option) %}
|
||||
<label for="{{ option_name }}">
|
||||
{{ option.display_name|default(option_name) }}:
|
||||
<span class="interactive" data-tooltip="{% filter dedent %}{{(option.__doc__ | default("Please document me!"))|escape }}{% endfilter %}">(?)</span>
|
||||
</label>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro RandomizeButton(option_name, option) %}
|
||||
<div class="randomize-button" data-tooltip="Toggle randomization for this option!">
|
||||
<label for="random-{{ option_name }}">
|
||||
<input type="checkbox" id="random-{{ option_name }}" name="random-{{ option_name }}" class="randomize-checkbox" data-option-name="{{ option_name }}" {{ "checked" if option.default == "random" }} />
|
||||
🎲
|
||||
</label>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
166
WebHostLib/templates/playerOptions/playerOptions.html
Normal file
166
WebHostLib/templates/playerOptions/playerOptions.html
Normal file
@@ -0,0 +1,166 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import 'playerOptions/macros.html' as inputs %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ world_name }} Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/playerOptions/playerOptions.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/playerOptions.js") }}"></script>
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
.js-required{
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/'+theme+'Header.html' %}
|
||||
<div id="player-options" class="markdown" data-game="{{ world_name }}" data-presets="{{ presets }}">
|
||||
<noscript>
|
||||
<div class="js-warning-banner">
|
||||
This page has reduced functionality without JavaScript.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div id="user-message">{{ message }}</div>
|
||||
|
||||
<div id="player-options-header">
|
||||
<h1>{{ world_name }}</h1>
|
||||
<h1>Player Options</h1>
|
||||
</div>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>
|
||||
A more advanced options configuration for all games can be found on the
|
||||
<a href="weighted-options">Weighted options</a> page.
|
||||
<br />
|
||||
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
||||
<br />
|
||||
You may also download the
|
||||
<a href="/static/generated/configs/{{ world_name }}.yaml">template file for this game</a>.
|
||||
</p>
|
||||
|
||||
<form id="options-form" method="post" enctype="application/x-www-form-urlencoded" action="generate-yaml">
|
||||
<div id="meta-options">
|
||||
<div>
|
||||
<label for="player-name">
|
||||
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
|
||||
</label>
|
||||
<input id="player-name" placeholder="Player" name="name" maxlength="16" />
|
||||
</div>
|
||||
<div class="js-required">
|
||||
<label for="game-options-preset">
|
||||
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
|
||||
</label>
|
||||
<select id="game-options-preset" name="game-options-preset" disabled>
|
||||
<option value="default">Default</option>
|
||||
{% for preset_name in world.web.options_presets %}
|
||||
<option value="{{ preset_name }}">{{ preset_name }}</option>
|
||||
{% endfor %}
|
||||
<option value="custom" hidden>Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="option-groups">
|
||||
{% for group_name, group_options in option_groups.items() %}
|
||||
<details class="group-container" {% if loop.index == 1 %}open{% endif %}>
|
||||
<summary class="h2">{{ group_name }}</summary>
|
||||
<div class="game-options">
|
||||
<div class="left">
|
||||
{% for option_name, option in group_options.items() %}
|
||||
{% if loop.index <= (loop.length / 2)|round(0,"ceil") %}
|
||||
{% if issubclass(option, Options.Toggle) %}
|
||||
{{ inputs.Toggle(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.TextChoice) %}
|
||||
{{ inputs.TextChoice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Choice) %}
|
||||
{{ inputs.Choice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.NamedRange) %}
|
||||
{{ inputs.NamedRange(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Range) %}
|
||||
{{ inputs.Range(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.FreeText) %}
|
||||
{{ inputs.FreeText(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
||||
{{ inputs.ItemDict(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||
{{ inputs.OptionList(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
||||
{{ inputs.LocationSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
||||
{{ inputs.ItemSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
||||
{{ inputs.OptionSet(option_name, option) }}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="right">
|
||||
{% for option_name, option in group_options.items() %}
|
||||
{% if loop.index > (loop.length / 2)|round(0,"ceil") %}
|
||||
{% if issubclass(option, Options.Toggle) %}
|
||||
{{ inputs.Toggle(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.TextChoice) %}
|
||||
{{ inputs.TextChoice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Choice) %}
|
||||
{{ inputs.Choice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.NamedRange) %}
|
||||
{{ inputs.NamedRange(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Range) %}
|
||||
{{ inputs.Range(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.FreeText) %}
|
||||
{{ inputs.FreeText(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
||||
{{ inputs.ItemDict(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||
{{ inputs.OptionList(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
||||
{{ inputs.LocationSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
||||
{{ inputs.ItemSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
||||
{{ inputs.OptionSet(option_name, option) }}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="player-options-button-row">
|
||||
<input type="submit" name="intent-export" value="Export Options" />
|
||||
<input type="submit" name="intent-generate" value="Generate Single-Player Game">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -24,7 +24,6 @@
|
||||
<li><a href="/games">Supported Games Page</a></li>
|
||||
<li><a href="/tutorial">Tutorials Page</a></li>
|
||||
<li><a href="/user-content">User Content</a></li>
|
||||
<li><a href="/weighted-options">Weighted Options Page</a></li>
|
||||
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
|
||||
<li><a href="/glossary/en">Glossary</a></li>
|
||||
</ul>
|
||||
@@ -50,8 +49,12 @@
|
||||
<ul>
|
||||
{% for game in games | title_sorted %}
|
||||
{% if game['has_settings'] %}
|
||||
<li><a href="{{ url_for('player_options', game=game['title']) }}">{{ game['title'] }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ game['title'] }}</li>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('player_options', game=game['title']) }}">Player Options</a></li>
|
||||
<li><a href="{{ url_for('weighted_options', game=game['title']) }}">Weighted Options</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -41,28 +41,28 @@
|
||||
</div>
|
||||
{% for game_name in worlds | title_sorted %}
|
||||
{% set world = worlds[game_name] %}
|
||||
<h2 class="collapse-toggle" data-game="{{ game_name }}">
|
||||
<span class="collapse-arrow">▶</span>{{ game_name }}
|
||||
</h2>
|
||||
<p class="collapsed">
|
||||
<details data-game="{{ game_name }}">
|
||||
<summary class="h2">{{ game_name }}</summary>
|
||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||
<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>
|
||||
<a href="{{ world.web.options_page }}">Options Page</a>
|
||||
<a href="{{ world.web.options_page }}">Options Page (External Link)</a>
|
||||
{% elif world.web.options_page %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("weighted_options", game=game_name) }}">Advanced Options</a>
|
||||
{% endif %}
|
||||
{% if world.web.bug_report_page %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ world.web.bug_report_page }}">Report a Bug</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weighted-options.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weighted-options.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="weighted-settings" class="markdown" data-game="{{ game }}">
|
||||
<div id="user-message"></div>
|
||||
<h1>Weighted Options</h1>
|
||||
<p>Weighted options allow you to choose how likely a particular option is to be used in game generation.
|
||||
The higher an option is weighted, the more likely the option will be chosen. Think of them like
|
||||
entries in a raffle.</p>
|
||||
|
||||
<p>Choose the games and options you would like to play with! You may generate a single-player game from
|
||||
this page, or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
|
||||
page.</p>
|
||||
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
items if you are playing in a MultiWorld.</label><br />
|
||||
<input id="player-name" placeholder="Player Name" data-key="name" maxlength="16" />
|
||||
</p>
|
||||
|
||||
<div id="game-choice">
|
||||
<!-- User chooses games by weight -->
|
||||
</div>
|
||||
|
||||
<!-- To be generated and populated per-game with weight > 0 -->
|
||||
<div id="games-wrapper">
|
||||
|
||||
</div>
|
||||
|
||||
<div id="weighted-settings-button-row">
|
||||
<button id="export-options">Export Options</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
249
WebHostLib/templates/weightedOptions/macros.html
Normal file
249
WebHostLib/templates/weightedOptions/macros.html
Normal file
@@ -0,0 +1,249 @@
|
||||
{% macro Toggle(option_name, option) %}
|
||||
<table>
|
||||
<tbody>
|
||||
{{ RangeRow(option_name, option, "No", "false") }}
|
||||
{{ RangeRow(option_name, option, "Yes", "true") }}
|
||||
{{ RandomRows(option_name, option) }}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro DefaultOnToggle(option_name, option) %}
|
||||
<!-- Toggle handles defaults properly, so we just reuse that -->
|
||||
{{ Toggle(option_name, option) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Choice(option_name, option) %}
|
||||
<table>
|
||||
<tbody>
|
||||
{% for id, name in option.name_lookup.items() %}
|
||||
{% if name != 'random' %}
|
||||
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ RandomRows(option_name, option) }}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Range(option_name, option) %}
|
||||
<div class="hint-text js-required">
|
||||
This is a range option.
|
||||
<br /><br />
|
||||
Accepted values:<br />
|
||||
Normal range: {{ option.range_start }} - {{ option.range_end }}
|
||||
{% if option.special_range_names %}
|
||||
<br /><br />
|
||||
The following values has special meaning, and may fall outside the normal range.
|
||||
<ul>
|
||||
{% for name, value in option.special_range_names.items() %}
|
||||
<li>{{ value }}: {{ name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="add-option-div">
|
||||
<input type="number" class="range-option-value" data-option="{{ option_name }}" />
|
||||
<button class="add-range-option-button" data-option="{{ option_name }}">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="range-rows" data-option="{{ option_name }}">
|
||||
<tbody>
|
||||
{{ RangeRow(option_name, option, option.range_start, option.range_start, True) }}
|
||||
{% if option.range_start < option.default < option.range_end %}
|
||||
{{ RangeRow(option_name, option, option.default, option.default, True) }}
|
||||
{% endif %}
|
||||
{{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}
|
||||
{{ RandomRows(option_name, option) }}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro NamedRange(option_name, option) %}
|
||||
<!-- Range is able to properly handle NamedDRange options -->
|
||||
{{ Range(option_name, option) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro FreeText(option_name, option) %}
|
||||
<div class="hint-text">
|
||||
This option allows custom values only. Please enter your desired values below.
|
||||
<div class="custom-value-wrapper">
|
||||
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button data-option="{{ option_name }}">Add</button>
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<!-- This table to be filled by JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TextChoice(option_name, option) %}
|
||||
<div class="hint-text">
|
||||
Custom values are also allowed for this option. To create one, enter it into the input box below.
|
||||
<div class="custom-value-wrapper">
|
||||
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button data-option="{{ option_name }}">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for id, name in option.name_lookup.items() %}
|
||||
{% if name != 'random' %}
|
||||
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ RandomRows(option_name, option) }}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro PlandoBosses(option_name, option) %}
|
||||
<!-- PlandoBosses is handled by its parent, TextChoice -->
|
||||
{{ TextChoice(option_name, option) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ItemDict(option_name, option, world) %}
|
||||
<div class="dict-container">
|
||||
{% for item_name in world.item_names|sort %}
|
||||
<div class="dict-entry">
|
||||
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
||||
<input
|
||||
type="number"
|
||||
id="{{ option_name }}-{{ item_name }}-qty"
|
||||
name="{{ option_name }}||{{ item_name }}"
|
||||
value="0"
|
||||
/>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro OptionList(option_name, option) %}
|
||||
<div class="list-container">
|
||||
{% for key in option.valid_keys|sort %}
|
||||
<div class="list-entry">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="{{ option_name }}-{{ key }}"
|
||||
name="{{ option_name }}||{{ key }}"
|
||||
value="1"
|
||||
/>
|
||||
<label for="{{ option_name }}-{{ key }}">
|
||||
{{ key }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro LocationSet(option_name, option, world) %}
|
||||
<div class="set-container">
|
||||
{% for group_name in world.location_name_groups.keys()|sort %}
|
||||
{% if group_name != "Everywhere" %}
|
||||
<div class="set-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}||{{ group_name }}" value="1" {{ "checked" if grop_name in option.default }} />
|
||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if world.location_name_groups.keys()|length > 1 %}
|
||||
<div class="divider"> </div>
|
||||
{% endif %}
|
||||
{% for location_name in world.location_names|sort %}
|
||||
<div class="set-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ location_name }}" name="{{ option_name }}||{{ location_name }}" value="1" {{ "checked" if location_name in option.default }} />
|
||||
<label for="{{ option_name }}-{{ location_name }}">{{ location_name }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ItemSet(option_name, option, world) %}
|
||||
<div class="set-container">
|
||||
{% for group_name in world.item_name_groups.keys()|sort %}
|
||||
{% if group_name != "Everything" %}
|
||||
<div class="set-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}||{{ group_name }}" value="1" {{ "checked" if group_name in option.default }} />
|
||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if world.item_name_groups.keys()|length > 1 %}
|
||||
<div class="set-divider"> </div>
|
||||
{% endif %}
|
||||
{% for item_name in world.item_names|sort %}
|
||||
<div class="set-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ item_name }}" name="{{ option_name }}||{{ item_name }}" value="1" {{ "checked" if item_name in option.default }} />
|
||||
<label for="{{ option_name }}-{{ item_name }}">{{ item_name }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro OptionSet(option_name, option) %}
|
||||
<div class="set-container">
|
||||
{% for key in option.valid_keys|sort %}
|
||||
<div class="set-entry">
|
||||
<input type="checkbox" id="{{ option_name }}-{{ key }}" name="{{ option_name }}||{{ key }}" value="1" {{ "checked" if key in option.default }} />
|
||||
<label for="{{ option_name }}-{{ key }}">{{ key }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro OptionTitleTd(option_name, value) %}
|
||||
<td class="td-left">
|
||||
<label for="{{ option_name }}||{{ value }}">
|
||||
{{ option.display_name|default(option_name) }}
|
||||
</label>
|
||||
</td>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro RandomRows(option_name, option, extra_column=False) %}
|
||||
{% for key, value in {"Random": "random", "Random (Low)": "random-low", "Random (Middle)": "random-middle", "Random (High)": "random-high"}.items() %}
|
||||
{{ RangeRow(option_name, option, key, value) }}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro RangeRow(option_name, option, display_value, value, can_delete=False) %}
|
||||
<tr data-row="{{ option_name }}-{{ value }}-row" data-option-name="{{ option_name }}" data-value="{{ value }}">
|
||||
<td class="td-left">
|
||||
<label for="{{ option_name }}||{{ value }}">
|
||||
{{ display_value }}
|
||||
</label>
|
||||
</td>
|
||||
<td class="td-middle">
|
||||
<input
|
||||
type="range"
|
||||
id="{{ option_name }}||{{ value }}"
|
||||
name="{{ option_name }}||{{ value }}"
|
||||
min="0"
|
||||
max="50"
|
||||
{% if option.default == value %}
|
||||
value="25"
|
||||
{% else %}
|
||||
value="0"
|
||||
{% endif %}
|
||||
/>
|
||||
</td>
|
||||
<td class="td-right">
|
||||
<span id="{{ option_name }}||{{ value }}-value">
|
||||
{% if option.default == value %}
|
||||
25
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
{% if can_delete %}
|
||||
<td>
|
||||
<span class="range-option-delete js-required" data-target="{{ option_name }}-{{ value }}-row">
|
||||
❌
|
||||
</span>
|
||||
</td>
|
||||
{% else %}
|
||||
<td><!-- This td empty on purpose --></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
119
WebHostLib/templates/weightedOptions/weightedOptions.html
Normal file
119
WebHostLib/templates/weightedOptions/weightedOptions.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import 'weightedOptions/macros.html' as inputs %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ world_name }} Weighted Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weightedOptions/weightedOptions.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weightedOptions.js") }}"></script>
|
||||
<noscript>
|
||||
<style>
|
||||
.js-required{
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/'+theme+'Header.html' %}
|
||||
<div id="weighted-options" class="markdown" data-game="{{ world_name }}">
|
||||
<noscript>
|
||||
<div class="js-warning-banner">
|
||||
This page has reduced functionality without JavaScript.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div id="user-message"></div>
|
||||
|
||||
<div id="weighted-options-header">
|
||||
<h1>{{ world_name }}</h1>
|
||||
<h1>Weighted Options</h1>
|
||||
</div>
|
||||
|
||||
<form id="weighted-options-form" method="post" enctype="application/x-www-form-urlencoded" action="generate-weighted-yaml">
|
||||
|
||||
<p>Weighted options allow you to choose how likely a particular option's value is to be used in game
|
||||
generation. The higher a value is weighted, the more likely the option will be chosen. Think of them like
|
||||
entries in a raffle.</p>
|
||||
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from
|
||||
this page, or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
|
||||
page.</p>
|
||||
|
||||
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
items if you are playing in a MultiWorld.</label><br />
|
||||
<input id="player-name" placeholder="Player Name" name="name" maxlength="16" />
|
||||
</p>
|
||||
|
||||
<div id="{{ world_name }}-container">
|
||||
{% for group_name, group_options in option_groups.items() %}
|
||||
<details {% if loop.index == 1 %}open{% endif %}>
|
||||
<summary class="h2">{{ group_name }}</summary>
|
||||
{% for option_name, option in group_options.items() %}
|
||||
<div class="option-wrapper">
|
||||
<h4>{{ option.display_name|default(option_name) }}</h4>
|
||||
<div class="option-description">
|
||||
{{ option.__doc__ }}
|
||||
</div>
|
||||
{% if issubclass(option, Options.Toggle) %}
|
||||
{{ inputs.Toggle(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.DefaultOnToggle) %}
|
||||
{{ inputs.DefaultOnToggle(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.PlandoBosses) %}
|
||||
{{ inputs.PlandoBosses(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.TextChoice) %}
|
||||
{{ inputs.TextChoice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Choice) %}
|
||||
{{ inputs.Choice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.NamedRange) %}
|
||||
{{ inputs.NamedRange(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Range) %}
|
||||
{{ inputs.Range(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.FreeText) %}
|
||||
{{ inputs.FreeText(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
||||
{{ inputs.ItemDict(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||
{{ inputs.OptionList(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
||||
{{ inputs.LocationSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
||||
{{ inputs.ItemSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
||||
{{ inputs.OptionSet(option_name, option) }}
|
||||
|
||||
{% else %}
|
||||
<div class="unsupported-option">
|
||||
This option is not supported. Please edit your .yaml file manually.
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="weighted-options-button-row">
|
||||
<input type="submit" name="intent-export" value="Export Options" />
|
||||
<input type="submit" name="intent-generate" value="Generate Single-Player Game">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -45,7 +45,10 @@ requires:
|
||||
{% endmacro %}
|
||||
|
||||
{{ game }}:
|
||||
{%- for option_key, option in options.items() %}
|
||||
{%- for group_name, group_options in option_groups.items() %}
|
||||
# {{ group_name }}
|
||||
|
||||
{%- for option_key, option in group_options.items() %}
|
||||
{{ option_key }}:
|
||||
{%- if option.__doc__ %}
|
||||
# {{ option.__doc__
|
||||
@@ -83,3 +86,4 @@ requires:
|
||||
{%- endif -%}
|
||||
{{ "\n" }}
|
||||
{%- endfor %}
|
||||
{%- endfor %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -197,6 +203,9 @@
|
||||
# Yoshi's Island
|
||||
/worlds/yoshisisland/ @PinkSwitch
|
||||
|
||||
#Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
||||
/worlds/yugioh06/ @rensen
|
||||
|
||||
# Zillion
|
||||
/worlds/zillion/ @beauxq
|
||||
|
||||
|
||||
@@ -85,6 +85,25 @@ class ExampleWorld(World):
|
||||
options: ExampleGameOptions
|
||||
```
|
||||
|
||||
### Option Groups
|
||||
Options may be categorized into groups for display on the WebHost. Option groups are displayed alphabetically on the
|
||||
player-options and weighted-options pages. Options without a group name are categorized into a generic "Game Options"
|
||||
group.
|
||||
|
||||
```python
|
||||
from worlds.AutoWorld import WebWorld
|
||||
from BaseClasses import OptionGroup
|
||||
|
||||
class MyWorldWeb(WebWorld):
|
||||
option_groups = [
|
||||
OptionGroup('Color Options', [
|
||||
Options.ColorblindMode,
|
||||
Options.FlashReduction,
|
||||
Options.UIColors,
|
||||
]),
|
||||
]
|
||||
```
|
||||
|
||||
### Option Checking
|
||||
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
|
||||
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
|
||||
@@ -155,10 +174,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -181,8 +181,7 @@ required, and will prevent progression and useful items from being placed at exc
|
||||
#### Documenting Locations
|
||||
|
||||
Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and
|
||||
location groups. These descriptions will show up in location-selection options on the Weighted Options page. Extra
|
||||
indentation and single newlines will be collapsed into spaces.
|
||||
location groups. These descriptions will show up in location-selection options on the options pages.
|
||||
|
||||
```python
|
||||
# locations.py
|
||||
@@ -236,8 +235,7 @@ Other classifications include:
|
||||
#### Documenting Items
|
||||
|
||||
Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item
|
||||
groups. These descriptions will show up in item-selection options on the Weighted Options page. Extra indentation and
|
||||
single newlines will be collapsed into spaces.
|
||||
groups. These descriptions will show up in item-selection options on the options pages.
|
||||
|
||||
```python
|
||||
# items.py
|
||||
|
||||
@@ -194,6 +194,11 @@ Root: HKCR; Subkey: "{#MyAppName}yipatch"; ValueData: "Archi
|
||||
Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apygo06"; ValueData: "{#MyAppName}ygo06patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ygo06patch"; ValueData: "Archipelago Yu-Gi-Oh 2006 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import logging
|
||||
import pathlib
|
||||
import random
|
||||
from random import Random
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
@@ -11,11 +11,13 @@ from dataclasses import make_dataclass
|
||||
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping,
|
||||
Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union)
|
||||
|
||||
from Options import PerGameCommonOptions
|
||||
from BaseClasses import CollectionState
|
||||
from Options import ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, PerGameCommonOptions, \
|
||||
PriorityLocations, \
|
||||
StartHints, \
|
||||
StartInventory, StartInventoryPool, StartLocationHints
|
||||
from BaseClasses import CollectionState, OptionGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import random
|
||||
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
|
||||
from . import GamesPackage
|
||||
from settings import Group
|
||||
@@ -118,6 +120,33 @@ class AutoLogicRegister(type):
|
||||
return new_class
|
||||
|
||||
|
||||
class WebWorldRegister(type):
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> WebWorldRegister:
|
||||
# don't allow an option to appear in multiple groups, allow "Item & Location Options" to appear anywhere by the
|
||||
# dev, putting it at the end if they don't define options in it
|
||||
option_groups: List[OptionGroup] = dct.get("option_groups", [])
|
||||
item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints,
|
||||
StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks]
|
||||
seen_options = []
|
||||
item_group_in_list = False
|
||||
for group in option_groups:
|
||||
assert group.name != "Game Options", "Game Options is a pre-determined group and can not be defined."
|
||||
if group.name == "Item & Location Options":
|
||||
group.options.extend(item_and_loc_options)
|
||||
item_group_in_list = True
|
||||
else:
|
||||
for option in group.options:
|
||||
assert option not in item_and_loc_options, \
|
||||
f"{option} cannot be moved out of the \"Item & Location Options\" Group"
|
||||
assert len(group.options) == len(set(group.options)), f"Duplicate options in option group {group.name}"
|
||||
for option in group.options:
|
||||
assert option not in seen_options, f"{option} found in two option groups"
|
||||
seen_options.append(option)
|
||||
if not item_group_in_list:
|
||||
option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options))
|
||||
return super().__new__(mcs, name, bases, dct)
|
||||
|
||||
|
||||
def _timed_call(method: Callable[..., Any], *args: Any,
|
||||
multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any:
|
||||
start = time.perf_counter()
|
||||
@@ -172,7 +201,7 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
_timed_call(stage_callable, multiworld, *args)
|
||||
|
||||
|
||||
class WebWorld:
|
||||
class WebWorld(metaclass=WebWorldRegister):
|
||||
"""Webhost integration"""
|
||||
|
||||
options_page: Union[bool, str] = True
|
||||
@@ -194,6 +223,9 @@ class WebWorld:
|
||||
options_presets: Dict[str, Dict[str, Any]] = {}
|
||||
"""A dictionary containing a collection of developer-defined game option presets."""
|
||||
|
||||
option_groups: ClassVar[List[OptionGroup]] = []
|
||||
"""Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options"."""
|
||||
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
@@ -206,8 +238,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"""
|
||||
@@ -283,7 +315,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
location_names: ClassVar[Set[str]]
|
||||
"""set of all potential location names"""
|
||||
|
||||
random: random.Random
|
||||
random: Random
|
||||
"""This world's random object. Should be used for any randomization needed in world for this player slot."""
|
||||
|
||||
settings_key: ClassVar[str]
|
||||
@@ -300,7 +332,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
assert multiworld is not None
|
||||
self.multiworld = multiworld
|
||||
self.player = player
|
||||
self.random = random.Random(multiworld.random.getrandbits(64))
|
||||
self.random = Random(multiworld.random.getrandbits(64))
|
||||
multiworld.per_slot_randoms[player] = self.random
|
||||
|
||||
def __getattr__(self, item: str) -> Any:
|
||||
@@ -504,6 +536,10 @@ class World(metaclass=AutoWorldRegister):
|
||||
def get_region(self, region_name: str) -> "Region":
|
||||
return self.multiworld.get_region(region_name, self.player)
|
||||
|
||||
@property
|
||||
def player_name(self) -> str:
|
||||
return self.multiworld.get_player_name(self.player)
|
||||
|
||||
@classmethod
|
||||
def get_data_package_data(cls) -> "GamesPackage":
|
||||
sorted_item_name_groups = {
|
||||
|
||||
@@ -241,4 +241,4 @@ adventure_option_definitions: Dict[str, type(Option)] = {
|
||||
"difficulty_switch_b": DifficultySwitchB,
|
||||
"start_castle": StartCastle,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import subprocess
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
from typing import Optional, List
|
||||
from typing import Collection, Optional, List, SupportsIndex
|
||||
|
||||
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
||||
@@ -52,7 +52,7 @@ except:
|
||||
enemizer_logger = logging.getLogger("Enemizer")
|
||||
|
||||
|
||||
class LocalRom(object):
|
||||
class LocalRom:
|
||||
|
||||
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
|
||||
self.name = name
|
||||
@@ -71,13 +71,13 @@ class LocalRom(object):
|
||||
def read_byte(self, address: int) -> int:
|
||||
return self.buffer[address]
|
||||
|
||||
def read_bytes(self, startaddress: int, length: int) -> bytes:
|
||||
def read_bytes(self, startaddress: int, length: int) -> bytearray:
|
||||
return self.buffer[startaddress:startaddress + length]
|
||||
|
||||
def write_byte(self, address: int, value: int):
|
||||
self.buffer[address] = value
|
||||
|
||||
def write_bytes(self, startaddress: int, values):
|
||||
def write_bytes(self, startaddress: int, values: Collection[SupportsIndex]) -> None:
|
||||
self.buffer[startaddress:startaddress + len(values)] = values
|
||||
|
||||
def encrypt_range(self, startaddress: int, length: int, key: bytes):
|
||||
|
||||
@@ -64,7 +64,8 @@ configuración personal y descargar un fichero "YAML".
|
||||
|
||||
### Configuración YAML avanzada
|
||||
|
||||
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina ["Weighted settings"](/weighted-settings),
|
||||
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina
|
||||
["Weighted settings"](/games/A Link to the Past/weighted-options),
|
||||
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones
|
||||
representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser
|
||||
elegidos sobre otros de la misma.
|
||||
|
||||
@@ -66,9 +66,10 @@ paramètres personnels et de les exporter vers un fichier YAML.
|
||||
### Configuration avancée du fichier YAML
|
||||
|
||||
Une version plus avancée du fichier YAML peut être créée en utilisant la page
|
||||
des [paramètres de pondération](/weighted-settings), qui vous permet de configurer jusqu'à trois préréglages. Cette page
|
||||
a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants. Cela vous permet de choisir
|
||||
quelles sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles dans une même catégorie.
|
||||
des [paramètres de pondération](/games/A Link to the Past/weighted-options), qui vous permet de configurer jusqu'à
|
||||
trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs
|
||||
glissants. Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux
|
||||
autres disponibles dans une même catégorie.
|
||||
|
||||
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
|
||||
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
|
||||
|
||||
210
worlds/aquaria/Items.py
Normal file
210
worlds/aquaria/Items.py
Normal 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
574
worlds/aquaria/Locations.py
Normal 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
145
worlds/aquaria/Options.py
Normal 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
1401
worlds/aquaria/Regions.py
Executable file
File diff suppressed because it is too large
Load Diff
218
worlds/aquaria/__init__.py
Normal file
218
worlds/aquaria/__init__.py
Normal 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],
|
||||
}
|
||||
64
worlds/aquaria/docs/en_Aquaria.md
Normal file
64
worlds/aquaria/docs/en_Aquaria.md
Normal 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.
|
||||
65
worlds/aquaria/docs/fr_Aquaria.md
Normal file
65
worlds/aquaria/docs/fr_Aquaria.md
Normal 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.
|
||||
114
worlds/aquaria/docs/setup_en.md
Normal file
114
worlds/aquaria/docs/setup_en.md
Normal 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
|
||||
```
|
||||
118
worlds/aquaria/docs/setup_fr.md
Normal file
118
worlds/aquaria/docs/setup_fr.md
Normal 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
|
||||
```
|
||||
218
worlds/aquaria/test/__init__.py
Normal file
218
worlds/aquaria/test/__init__.py
Normal 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"
|
||||
48
worlds/aquaria/test/test_beast_form_access.py
Normal file
48
worlds/aquaria/test/test_beast_form_access.py
Normal 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)
|
||||
36
worlds/aquaria/test/test_bind_song_access.py
Normal file
36
worlds/aquaria/test/test_bind_song_access.py
Normal 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)
|
||||
42
worlds/aquaria/test/test_bind_song_option_access.py
Normal file
42
worlds/aquaria/test/test_bind_song_option_access.py
Normal 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)
|
||||
20
worlds/aquaria/test/test_confined_home_water.py
Normal file
20
worlds/aquaria/test/test_confined_home_water.py
Normal 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")
|
||||
26
worlds/aquaria/test/test_dual_song_access.py
Normal file
26
worlds/aquaria/test/test_dual_song_access.py
Normal 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)
|
||||
73
worlds/aquaria/test/test_energy_form_access.py
Normal file
73
worlds/aquaria/test/test_energy_form_access.py
Normal 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)
|
||||
31
worlds/aquaria/test/test_energy_form_access_option.py
Normal file
31
worlds/aquaria/test/test_energy_form_access_option.py
Normal 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)
|
||||
37
worlds/aquaria/test/test_fish_form_access.py
Normal file
37
worlds/aquaria/test/test_fish_form_access.py
Normal 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)
|
||||
45
worlds/aquaria/test/test_li_song_access.py
Normal file
45
worlds/aquaria/test/test_li_song_access.py
Normal 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)
|
||||
71
worlds/aquaria/test/test_light_access.py
Normal file
71
worlds/aquaria/test/test_light_access.py
Normal 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)
|
||||
57
worlds/aquaria/test/test_nature_form_access.py
Normal file
57
worlds/aquaria/test/test_nature_form_access.py
Normal 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)
|
||||
@@ -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 + "\"")
|
||||
|
||||
@@ -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 + "\"")
|
||||
|
||||
36
worlds/aquaria/test/test_spirit_form_access.py
Normal file
36
worlds/aquaria/test/test_spirit_form_access.py
Normal 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)
|
||||
25
worlds/aquaria/test/test_sun_form_access.py
Normal file
25
worlds/aquaria/test/test_sun_form_access.py
Normal 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)
|
||||
21
worlds/aquaria/test/test_unconfine_home_water_via_both.py
Normal file
21
worlds/aquaria/test/test_unconfine_home_water_via_both.py
Normal 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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
553
worlds/bomb_rush_cyberfunk/Items.py
Normal file
553
worlds/bomb_rush_cyberfunk/Items.py
Normal 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"}
|
||||
}
|
||||
785
worlds/bomb_rush_cyberfunk/Locations.py
Normal file
785
worlds/bomb_rush_cyberfunk/Locations.py
Normal 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"},
|
||||
]
|
||||
162
worlds/bomb_rush_cyberfunk/Options.py
Normal file
162
worlds/bomb_rush_cyberfunk/Options.py
Normal 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
|
||||
103
worlds/bomb_rush_cyberfunk/Regions.py
Normal file
103
worlds/bomb_rush_cyberfunk/Regions.py
Normal 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]
|
||||
}
|
||||
1039
worlds/bomb_rush_cyberfunk/Rules.py
Normal file
1039
worlds/bomb_rush_cyberfunk/Rules.py
Normal file
File diff suppressed because it is too large
Load Diff
203
worlds/bomb_rush_cyberfunk/__init__.py
Normal file
203
worlds/bomb_rush_cyberfunk/__init__.py
Normal 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"
|
||||
29
worlds/bomb_rush_cyberfunk/docs/en_Bomb Rush Cyberfunk.md
Normal file
29
worlds/bomb_rush_cyberfunk/docs/en_Bomb Rush Cyberfunk.md
Normal 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.
|
||||
41
worlds/bomb_rush_cyberfunk/docs/setup_en.md
Normal file
41
worlds/bomb_rush_cyberfunk/docs/setup_en.md
Normal 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.
|
||||
5
worlds/bomb_rush_cyberfunk/test/__init__.py
Normal file
5
worlds/bomb_rush_cyberfunk/test/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class BombRushCyberfunkTestBase(WorldTestBase):
|
||||
game = "Bomb Rush Cyberfunk"
|
||||
284
worlds/bomb_rush_cyberfunk/test/test_graffiti_spots.py
Normal file
284
worlds/bomb_rush_cyberfunk/test/test_graffiti_spots.py
Normal 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))
|
||||
29
worlds/bomb_rush_cyberfunk/test/test_options.py
Normal file
29
worlds/bomb_rush_cyberfunk/test/test_options.py
Normal 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
|
||||
}
|
||||
45
worlds/bomb_rush_cyberfunk/test/test_rep_items.py
Normal file
45
worlds/bomb_rush_cyberfunk/test/test_rep_items.py
Normal 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"])
|
||||
@@ -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]
|
||||
|
||||
@@ -434,7 +434,7 @@ level_music_ids = [
|
||||
0x21,
|
||||
]
|
||||
|
||||
class LocalRom(object):
|
||||
class LocalRom:
|
||||
|
||||
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
|
||||
self.name = name
|
||||
@@ -457,7 +457,7 @@ class LocalRom(object):
|
||||
def read_byte(self, address: int) -> int:
|
||||
return self.buffer[address]
|
||||
|
||||
def read_bytes(self, startaddress: int, length: int) -> bytes:
|
||||
def read_bytes(self, startaddress: int, length: int) -> bytearray:
|
||||
return self.buffer[startaddress:startaddress + length]
|
||||
|
||||
def write_byte(self, address: int, value: int):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,13 +3,12 @@ This guide covers more the more advanced options available in YAML files. This g
|
||||
to edit their YAML file manually. This guide should take about 10 minutes to read.
|
||||
|
||||
If you would like to generate a basic, fully playable YAML without editing a file, then visit the options page for the
|
||||
game you intend to play. The weighted settings page can also handle most of the advanced settings discussed here.
|
||||
game you intend to play.
|
||||
|
||||
The options page can be found on the supported games page, just click the "Options Page" link under the name of the
|
||||
game you would like.
|
||||
|
||||
* Supported games page: [Archipelago Games List](/games)
|
||||
* Weighted settings page: [Archipelago Weighted Settings](/weighted-settings)
|
||||
|
||||
Clicking on the "Export Options" button at the bottom-left will provide you with a pre-filled YAML with your options.
|
||||
The player options page also has a link to download a full template file for that game which will have every option
|
||||
@@ -132,9 +131,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -217,8 +217,6 @@ class Overcooked2World(World):
|
||||
# Autoworld Hooks
|
||||
|
||||
def generate_early(self):
|
||||
self.player_name = self.multiworld.player_name[self.player]
|
||||
|
||||
# 0.0 to 1.0 where 1.0 is World Record
|
||||
self.star_threshold_scale = self.options.star_threshold_scale / 100.0
|
||||
|
||||
|
||||
@@ -51,8 +51,6 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder
|
||||
|
||||
1. Visit the [Player Options](../../../../games/Overcooked!%202/player-options) page and configure the game-specific options to taste
|
||||
|
||||
*By default, these options will only use levels from the base game and the "Seasonal" free DLC updates. If you own any of the paid DLC, you may select individual DLC packs to include/exclude on the [Weighted Options](../../../../weighted-options) page*
|
||||
|
||||
2. Export your yaml file and use it to generate a new randomized game
|
||||
|
||||
*For instructions on how to generate an Archipelago game, refer to the [Archipelago Setup Guide](../../../../tutorial/Archipelago/setup/en)*
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
# 2.2.0
|
||||
|
||||
### Features
|
||||
|
||||
- When you blacklist species from wild encounters and turn on dexsanity, blacklisted species are not added as locations
|
||||
and won't show up in the wild. Previously they would be forced to show up exactly once.
|
||||
- Added support for some new autotracking events.
|
||||
|
||||
### Fixes
|
||||
|
||||
- The Lilycove Wailmer now logically block you from the east. Actual game behavior is still unchanged for now.
|
||||
- Water encounters in Slateport now correctly require Surf.
|
||||
- Updated the tracker link in the setup guide.
|
||||
|
||||
# 2.1.1
|
||||
|
||||
### Features
|
||||
@@ -12,10 +26,11 @@ _Separately released, branching from 2.0.0. Included procedure patch migration,
|
||||
|
||||
### Fixes
|
||||
|
||||
- Changed "Ho-oh" to "Ho-Oh" in options
|
||||
- Changed "Ho-oh" to "Ho-Oh" in options.
|
||||
- Temporary fix to alleviate problems with sometimes not receiving certain items just after connecting if `remote_items`
|
||||
is `true`.
|
||||
- Temporarily disable a possible location for Marine Cave to spawn, as its causes an overflow
|
||||
- Temporarily disable a possible location for Marine Cave to spawn, as it causes an overflow.
|
||||
- Water encounters in Dewford now correctly require Surf.
|
||||
|
||||
# 2.0.0
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user