mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-07 15:13:52 -08:00
Compare commits
93 Commits
api-refere
...
multiserve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba3f695a41 | ||
|
|
fb2c194e37 | ||
|
|
cff7327558 | ||
|
|
70e9ccb13c | ||
|
|
d9120f0bea | ||
|
|
424c8b0be9 | ||
|
|
6432560fe5 | ||
|
|
dedabad290 | ||
|
|
e49b1f9fbb | ||
|
|
da33d1576a | ||
|
|
13bc121c27 | ||
|
|
bbc79a5b99 | ||
|
|
3cb5452455 | ||
|
|
8dbc8d2d41 | ||
|
|
1e205f9d73 | ||
|
|
97c9c5310b | ||
|
|
4e5b6bb3d2 | ||
|
|
f40b10dc97 | ||
|
|
4cab3b6371 | ||
|
|
67cd32b37c | ||
|
|
91c89604a5 | ||
|
|
f2587d5d27 | ||
|
|
2a5de8567e | ||
|
|
5aa6ad63ca | ||
|
|
f3003ff147 | ||
|
|
15e06e1779 | ||
|
|
b055a39454 | ||
|
|
7058575c95 | ||
|
|
2fe8c43351 | ||
|
|
6f6bf3c62d | ||
|
|
378af4b07c | ||
|
|
34f903e97a | ||
|
|
e31a7093de | ||
|
|
527559395c | ||
|
|
649ee117da | ||
|
|
5b34e06c8b | ||
|
|
04e9f5c47a | ||
|
|
dfc347cd24 | ||
|
|
74aa4eca9d | ||
|
|
df877a9254 | ||
|
|
70d97a0eb4 | ||
|
|
f249c36f8b | ||
|
|
61e88526cf | ||
|
|
18390ecc09 | ||
|
|
8045c8717c | ||
|
|
613e76689e | ||
|
|
2a47f03e72 | ||
|
|
8b992cbf00 | ||
|
|
d09b214309 | ||
|
|
860ab10b0b | ||
|
|
3f8c348a49 | ||
|
|
e1ff5073b5 | ||
|
|
8b6eae0a14 | ||
|
|
89d0dae299 | ||
|
|
56d01f3913 | ||
|
|
a43e294786 | ||
|
|
92392c0e65 | ||
|
|
893a157b23 | ||
|
|
02d3fdf2a6 | ||
|
|
cd160842ba | ||
|
|
93f63a3e31 | ||
|
|
b4fec93c82 | ||
|
|
1ae0a9b76f | ||
|
|
0ea20f3929 | ||
|
|
20134d3b1e | ||
|
|
a1c2e8715e | ||
|
|
61be79b7ea | ||
|
|
e7544d835c | ||
|
|
62e68ba1cc | ||
|
|
9441cc31b7 | ||
|
|
5c66681233 | ||
|
|
92b1f3cd19 | ||
|
|
514ad69f44 | ||
|
|
461f5db35a | ||
|
|
fe7bc8784d | ||
|
|
c792ae76ca | ||
|
|
bfe215d5a7 | ||
|
|
5910b94deb | ||
|
|
14ffd1c70c | ||
|
|
754fc11c1b | ||
|
|
12cde88f95 | ||
|
|
e0b6889634 | ||
|
|
14321d6ba2 | ||
|
|
e978109410 | ||
|
|
019dfb8242 | ||
|
|
8e9a050889 | ||
|
|
2801e21296 | ||
|
|
e97eddcdaf | ||
|
|
d3f4ee4994 | ||
|
|
cf34f125d6 | ||
|
|
663b50b33e | ||
|
|
03a31ee3b8 | ||
|
|
001fdfbe9f |
8
AHITClient.py
Normal file
8
AHITClient.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from worlds.ahit.Client import launch
|
||||
import Utils
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("AHITClient", exception_logger="Client")
|
||||
launch()
|
||||
@@ -112,7 +112,7 @@ class AdventureContext(CommonContext):
|
||||
if ': !' not in msg:
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "ReceivedItems":
|
||||
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||
msg = f"Received {', '.join([self.item_names.lookup_in_slot(item.item) for item in args['items']])}"
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "Retrieved":
|
||||
if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
|
||||
|
||||
@@ -728,7 +728,7 @@ class CollectionState():
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_from_list_exclusive(self, items: Iterable[str], player: int, count: int) -> bool:
|
||||
def has_from_list_unique(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.
|
||||
Ignores duplicates of the same item."""
|
||||
found: int = 0
|
||||
@@ -743,7 +743,7 @@ class CollectionState():
|
||||
"""Returns the cumulative count of items from a list present in state."""
|
||||
return sum(self.prog_items[player][item_name] for item_name in items)
|
||||
|
||||
def count_from_list_exclusive(self, items: Iterable[str], player: int) -> int:
|
||||
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
|
||||
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
|
||||
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
|
||||
|
||||
@@ -758,7 +758,7 @@ class CollectionState():
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_group_exclusive(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
||||
def has_group_unique(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
||||
"""Returns True if the state contains at least `count` items present in a specified item group.
|
||||
Ignores duplicates of the same item.
|
||||
"""
|
||||
@@ -778,7 +778,7 @@ class CollectionState():
|
||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
|
||||
)
|
||||
|
||||
def count_group_exclusive(self, item_name_group: str, player: int) -> int:
|
||||
def count_group_unique(self, item_name_group: str, player: int) -> int:
|
||||
"""Returns the cumulative count of items from an item group present in state.
|
||||
Ignores duplicates of the same item."""
|
||||
player_prog_items = self.prog_items[player]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import logging
|
||||
import asyncio
|
||||
@@ -8,6 +9,7 @@ import sys
|
||||
import typing
|
||||
import time
|
||||
import functools
|
||||
import warnings
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
@@ -173,10 +175,74 @@ class CommonContext:
|
||||
items_handling: typing.Optional[int] = None
|
||||
want_slot_data: bool = True # should slot_data be retrieved via Connect
|
||||
|
||||
# data package
|
||||
# Contents in flux until connection to server is made, to download correct data for this multiworld.
|
||||
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||
class NameLookupDict:
|
||||
"""A specialized dict, with helper methods, for id -> name item/location data package lookups by game."""
|
||||
def __init__(self, ctx: CommonContext, lookup_type: typing.Literal["item", "location"]):
|
||||
self.ctx: CommonContext = ctx
|
||||
self.lookup_type: typing.Literal["item", "location"] = lookup_type
|
||||
self._unknown_item: typing.Callable[[int], str] = lambda key: f"Unknown {lookup_type} (ID: {key})"
|
||||
self._archipelago_lookup: typing.Dict[int, str] = {}
|
||||
self._flat_store: typing.Dict[int, str] = Utils.KeyedDefaultDict(self._unknown_item)
|
||||
self._game_store: typing.Dict[str, typing.ChainMap[int, str]] = collections.defaultdict(
|
||||
lambda: collections.ChainMap(self._archipelago_lookup, Utils.KeyedDefaultDict(self._unknown_item)))
|
||||
self.warned: bool = False
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
def __getitem__(self, key: str) -> typing.Mapping[int, str]:
|
||||
# TODO: In a future version (0.6.0?) this should be simplified by removing implicit id lookups support.
|
||||
if isinstance(key, int):
|
||||
if not self.warned:
|
||||
# Use warnings instead of logger to avoid deprecation message from appearing on user side.
|
||||
self.warned = True
|
||||
warnings.warn(f"Implicit name lookup by id only is deprecated and only supported to maintain "
|
||||
f"backwards compatibility for now. If multiple games share the same id for a "
|
||||
f"{self.lookup_type}, name could be incorrect. Please use "
|
||||
f"`{self.lookup_type}_names.lookup_in_game()` or "
|
||||
f"`{self.lookup_type}_names.lookup_in_slot()` instead.")
|
||||
return self._flat_store[key] # type: ignore
|
||||
|
||||
return self._game_store[key]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._game_store)
|
||||
|
||||
def __iter__(self) -> typing.Iterator[str]:
|
||||
return iter(self._game_store)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self._game_store.__repr__()
|
||||
|
||||
def lookup_in_game(self, code: int, game_name: typing.Optional[str] = None) -> str:
|
||||
"""Returns the name for an item/location id in the context of a specific game or own game if `game` is
|
||||
omitted.
|
||||
"""
|
||||
if game_name is None:
|
||||
game_name = self.ctx.game
|
||||
assert game_name is not None, f"Attempted to lookup {self.lookup_type} with no game name available."
|
||||
|
||||
return self._game_store[game_name][code]
|
||||
|
||||
def lookup_in_slot(self, code: int, slot: typing.Optional[int] = None) -> str:
|
||||
"""Returns the name for an item/location id in the context of a specific slot or own slot if `slot` is
|
||||
omitted.
|
||||
"""
|
||||
if slot is None:
|
||||
slot = self.ctx.slot
|
||||
assert slot is not None, f"Attempted to lookup {self.lookup_type} with no slot info available."
|
||||
|
||||
return self.lookup_in_game(code, self.ctx.slot_info[slot].game)
|
||||
|
||||
def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int]) -> None:
|
||||
"""Overrides existing lookup tables for a particular game."""
|
||||
id_to_name_lookup_table = Utils.KeyedDefaultDict(self._unknown_item)
|
||||
id_to_name_lookup_table.update({code: name for name, code in name_to_id_lookup_table.items()})
|
||||
self._game_store[game] = collections.ChainMap(self._archipelago_lookup, id_to_name_lookup_table)
|
||||
self._flat_store.update(id_to_name_lookup_table) # Only needed for legacy lookup method.
|
||||
if game == "Archipelago":
|
||||
# Keep track of the Archipelago data package separately so if it gets updated in a custom datapackage,
|
||||
# it updates in all chain maps automatically.
|
||||
self._archipelago_lookup.clear()
|
||||
self._archipelago_lookup.update(id_to_name_lookup_table)
|
||||
|
||||
# defaults
|
||||
starting_reconnect_delay: int = 5
|
||||
@@ -231,7 +297,7 @@ class CommonContext:
|
||||
# message box reporting a loss of connection
|
||||
_messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None
|
||||
|
||||
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
|
||||
def __init__(self, server_address: typing.Optional[str] = None, password: typing.Optional[str] = None) -> None:
|
||||
# server state
|
||||
self.server_address = server_address
|
||||
self.username = None
|
||||
@@ -271,6 +337,9 @@ class CommonContext:
|
||||
self.exit_event = asyncio.Event()
|
||||
self.watcher_event = asyncio.Event()
|
||||
|
||||
self.item_names = self.NameLookupDict(self, "item")
|
||||
self.location_names = self.NameLookupDict(self, "location")
|
||||
|
||||
self.jsontotextparser = JSONtoTextParser(self)
|
||||
self.rawjsontotextparser = RawJSONtoTextParser(self)
|
||||
self.update_data_package(network_data_package)
|
||||
@@ -486,19 +555,17 @@ class CommonContext:
|
||||
or remote_checksum != cache_checksum:
|
||||
needed_updates.add(game)
|
||||
else:
|
||||
self.update_game(cached_game)
|
||||
self.update_game(cached_game, game)
|
||||
if needed_updates:
|
||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
|
||||
|
||||
def update_game(self, game_package: dict):
|
||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||
self.item_names[item_id] = item_name
|
||||
for location_name, location_id in game_package["location_name_to_id"].items():
|
||||
self.location_names[location_id] = location_name
|
||||
def update_game(self, game_package: dict, game: str):
|
||||
self.item_names.update_game(game, game_package["item_name_to_id"])
|
||||
self.location_names.update_game(game, game_package["location_name_to_id"])
|
||||
|
||||
def update_data_package(self, data_package: dict):
|
||||
for game, game_data in data_package["games"].items():
|
||||
self.update_game(game_data)
|
||||
self.update_game(game_data, game)
|
||||
|
||||
def consume_network_data_package(self, data_package: dict):
|
||||
self.update_data_package(data_package)
|
||||
|
||||
64
Fill.py
64
Fill.py
@@ -35,8 +35,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
"""
|
||||
:param multiworld: Multiworld to be filled.
|
||||
:param base_state: State assumed before fill.
|
||||
:param locations: Locations to be filled with item_pool
|
||||
:param item_pool: Items to fill into the locations
|
||||
:param locations: Locations to be filled with item_pool, gets mutated by removing locations that get filled.
|
||||
:param item_pool: Items to fill into the locations, gets mutated by removing items that get placed.
|
||||
:param single_player_placement: if true, can speed up placement if everything belongs to a single player
|
||||
:param lock: locations are set to locked as they are filled
|
||||
:param swap: if true, swaps of already place items are done in the event of a dead end
|
||||
@@ -220,7 +220,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
def remaining_fill(multiworld: MultiWorld,
|
||||
locations: typing.List[Location],
|
||||
itempool: typing.List[Item],
|
||||
name: str = "Remaining") -> None:
|
||||
name: str = "Remaining",
|
||||
move_unplaceable_to_start_inventory: bool = False) -> None:
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||
@@ -284,13 +285,21 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
|
||||
if unplaced_items and locations:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
|
||||
f"Unplaced items:\n"
|
||||
f"{', '.join(str(item) for item in unplaced_items)}\n"
|
||||
f"Unfilled locations:\n"
|
||||
f"{', '.join(str(location) for location in locations)}\n"
|
||||
f"Already placed {len(placements)}:\n"
|
||||
f"{', '.join(str(place) for place in placements)}")
|
||||
if move_unplaceable_to_start_inventory:
|
||||
last_batch = []
|
||||
for item in unplaced_items:
|
||||
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
|
||||
multiworld.push_precollected(item)
|
||||
last_batch.append(multiworld.worlds[item.player].create_filler())
|
||||
remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry")
|
||||
else:
|
||||
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
|
||||
f"Unplaced items:\n"
|
||||
f"{', '.join(str(item) for item in unplaced_items)}\n"
|
||||
f"Unfilled locations:\n"
|
||||
f"{', '.join(str(location) for location in locations)}\n"
|
||||
f"Already placed {len(placements)}:\n"
|
||||
f"{', '.join(str(place) for place in placements)}")
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
@@ -420,7 +429,8 @@ def distribute_early_items(multiworld: MultiWorld,
|
||||
return fill_locations, itempool
|
||||
|
||||
|
||||
def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None:
|
||||
fill_locations = sorted(multiworld.get_unfilled_locations())
|
||||
multiworld.random.shuffle(fill_locations)
|
||||
# get items to distribute
|
||||
@@ -470,8 +480,29 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
|
||||
if progitempool:
|
||||
# "advancement/progression fill"
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, single_player_placement=multiworld.players == 1,
|
||||
name="Progression")
|
||||
if panic_method == "swap":
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
|
||||
swap=True,
|
||||
on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
|
||||
elif panic_method == "raise":
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
|
||||
swap=False,
|
||||
on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
|
||||
elif panic_method == "start_inventory":
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
|
||||
swap=False, allow_partial=True,
|
||||
on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
|
||||
if progitempool:
|
||||
for item in progitempool:
|
||||
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
|
||||
multiworld.push_precollected(item)
|
||||
filleritempool.append(multiworld.worlds[item.player].create_filler())
|
||||
logging.warning(f"{len(progitempool)} items moved to start inventory,"
|
||||
f" due to failure in Progression fill step.")
|
||||
progitempool[:] = []
|
||||
|
||||
else:
|
||||
raise ValueError(f"Generator Panic Method {panic_method} not recognized.")
|
||||
if progitempool:
|
||||
raise FillError(
|
||||
f"Not enough locations for progression items. "
|
||||
@@ -486,7 +517,9 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
|
||||
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
|
||||
|
||||
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded")
|
||||
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded",
|
||||
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
|
||||
|
||||
if excludedlocations:
|
||||
raise FillError(
|
||||
f"Not enough filler items for excluded locations. "
|
||||
@@ -495,7 +528,8 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
|
||||
restitempool = filleritempool + usefulitempool
|
||||
|
||||
remaining_fill(multiworld, defaultlocations, restitempool)
|
||||
remaining_fill(multiworld, defaultlocations, restitempool,
|
||||
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
|
||||
|
||||
unplaced = restitempool
|
||||
unfilled = defaultlocations
|
||||
|
||||
68
Generate.py
68
Generate.py
@@ -9,6 +9,7 @@ import urllib.parse
|
||||
import urllib.request
|
||||
from collections import Counter
|
||||
from typing import Any, Dict, Tuple, Union
|
||||
from itertools import chain
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -22,9 +23,7 @@ from Main import main as ERmain
|
||||
from settings import get_settings
|
||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.generic import PlandoConnection
|
||||
from worlds import failed_world_loads
|
||||
|
||||
|
||||
@@ -319,18 +318,34 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
||||
logging.debug(f'Applying {new_weights}')
|
||||
cleaned_weights = {}
|
||||
for option in new_weights:
|
||||
option_name = option.lstrip("+")
|
||||
option_name = option.lstrip("+-")
|
||||
if option.startswith("+") and option_name in weights:
|
||||
cleaned_value = weights[option_name]
|
||||
new_value = new_weights[option]
|
||||
if isinstance(new_value, (set, dict)):
|
||||
if isinstance(new_value, set):
|
||||
cleaned_value.update(new_value)
|
||||
elif isinstance(new_value, list):
|
||||
cleaned_value.extend(new_value)
|
||||
elif isinstance(new_value, dict):
|
||||
cleaned_value = dict(Counter(cleaned_value) + Counter(new_value))
|
||||
else:
|
||||
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
|
||||
f" received {type(new_value).__name__}.")
|
||||
cleaned_weights[option_name] = cleaned_value
|
||||
elif option.startswith("-") and option_name in weights:
|
||||
cleaned_value = weights[option_name]
|
||||
new_value = new_weights[option]
|
||||
if isinstance(new_value, set):
|
||||
cleaned_value.difference_update(new_value)
|
||||
elif isinstance(new_value, list):
|
||||
for element in new_value:
|
||||
cleaned_value.remove(element)
|
||||
elif isinstance(new_value, dict):
|
||||
cleaned_value = dict(Counter(cleaned_value) - Counter(new_value))
|
||||
else:
|
||||
raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name},"
|
||||
f" received {type(new_value).__name__}.")
|
||||
cleaned_weights[option_name] = cleaned_value
|
||||
else:
|
||||
cleaned_weights[option_name] = new_weights[option]
|
||||
new_options = set(cleaned_weights) - set(weights)
|
||||
@@ -415,7 +430,6 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
|
||||
player_option = option.from_any(game_weights[option_key])
|
||||
else:
|
||||
player_option = option.from_any(get_choice(option_key, game_weights))
|
||||
del game_weights[option_key]
|
||||
else:
|
||||
player_option = option.from_any(option.default) # call the from_any here to support default "random"
|
||||
setattr(ret, option_key, player_option)
|
||||
@@ -429,9 +443,9 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
if "linked_options" in weights:
|
||||
weights = roll_linked_options(weights)
|
||||
|
||||
valid_trigger_names = set()
|
||||
valid_keys = set()
|
||||
if "triggers" in weights:
|
||||
weights = roll_triggers(weights, weights["triggers"], valid_trigger_names)
|
||||
weights = roll_triggers(weights, weights["triggers"], valid_keys)
|
||||
|
||||
requirements = weights.get("requires", {})
|
||||
if requirements:
|
||||
@@ -466,12 +480,14 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
world_type = AutoWorldRegister.world_types[ret.game]
|
||||
game_weights = weights[ret.game]
|
||||
|
||||
if any(weight.startswith("+") for weight in game_weights) or \
|
||||
any(weight.startswith("+") for weight in weights):
|
||||
raise Exception(f"Merge tag cannot be used outside of trigger contexts.")
|
||||
for weight in chain(game_weights, weights):
|
||||
if weight.startswith("+"):
|
||||
raise Exception(f"Merge tag cannot be used outside of trigger contexts. Found {weight}")
|
||||
if weight.startswith("-"):
|
||||
raise Exception(f"Remove tag cannot be used outside of trigger contexts. Found {weight}")
|
||||
|
||||
if "triggers" in game_weights:
|
||||
weights = roll_triggers(weights, game_weights["triggers"], valid_trigger_names)
|
||||
weights = roll_triggers(weights, game_weights["triggers"], valid_keys)
|
||||
game_weights = weights[ret.game]
|
||||
|
||||
ret.name = get_choice('name', weights)
|
||||
@@ -480,42 +496,20 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
valid_keys.add(option_key)
|
||||
for option_key in game_weights:
|
||||
if option_key in {"triggers", *valid_trigger_names}:
|
||||
if option_key in {"triggers", *valid_keys}:
|
||||
continue
|
||||
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
|
||||
if PlandoOptions.items in plando_options:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
if ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, game_weights, plando_options)
|
||||
if PlandoOptions.connections in plando_options:
|
||||
ret.plando_connections = []
|
||||
options = game_weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
ret.plando_connections.append(PlandoConnection(
|
||||
get_choice("entrance", placement),
|
||||
get_choice("exit", placement),
|
||||
get_choice("direction", placement, "both")
|
||||
))
|
||||
roll_alttp_settings(ret, game_weights)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
ret.plando_texts = {}
|
||||
if PlandoOptions.texts in plando_options:
|
||||
tt = TextTable()
|
||||
tt.removeUnwantedText()
|
||||
options = weights.get("plando_texts", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
at = str(get_choice_legacy("at", placement))
|
||||
if at not in tt:
|
||||
raise Exception(f"No text target \"{at}\" found.")
|
||||
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights):
|
||||
ret.sprite_pool = weights.get('sprite_pool', [])
|
||||
ret.sprite = get_choice_legacy('sprite', weights, "Link")
|
||||
if 'random_sprite_on_event' in weights:
|
||||
|
||||
16
Main.py
16
Main.py
@@ -13,7 +13,7 @@ import worlds
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
||||
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
||||
from Options import StartInventoryPool
|
||||
from Utils import __version__, output_path, version_tuple
|
||||
from Utils import __version__, output_path, version_tuple, get_settings
|
||||
from settings import get_settings
|
||||
from worlds import AutoWorld
|
||||
from worlds.generic.Rules import exclusion_rules, locality_rules
|
||||
@@ -272,7 +272,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
if multiworld.algorithm == 'flood':
|
||||
flood_items(multiworld) # different algo, biased towards early game progress items
|
||||
elif multiworld.algorithm == 'balanced':
|
||||
distribute_items_restrictive(multiworld)
|
||||
distribute_items_restrictive(multiworld, get_settings().generator.panic_method)
|
||||
|
||||
AutoWorld.call_all(multiworld, 'post_fill')
|
||||
|
||||
@@ -372,6 +372,17 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
|
||||
|
||||
# get spheres -> filter address==None -> skip empty
|
||||
spheres: List[Dict[int, Set[int]]] = []
|
||||
for sphere in multiworld.get_spheres():
|
||||
current_sphere: Dict[int, Set[int]] = collections.defaultdict(set)
|
||||
for sphere_location in sphere:
|
||||
if type(sphere_location.address) is int:
|
||||
current_sphere[sphere_location.player].add(sphere_location.address)
|
||||
|
||||
if current_sphere:
|
||||
spheres.append(dict(current_sphere))
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
"slot_info": slot_info,
|
||||
@@ -386,6 +397,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
"seed_name": multiworld.seed_name,
|
||||
"spheres": spheres,
|
||||
"datapackage": data_package,
|
||||
}
|
||||
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
|
||||
|
||||
137
MultiServer.py
137
MultiServer.py
@@ -37,7 +37,7 @@ except ImportError:
|
||||
|
||||
import NetUtils
|
||||
import Utils
|
||||
from Utils import version_tuple, restricted_loads, Version, async_start
|
||||
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||
SlotType, LocationStore
|
||||
|
||||
@@ -168,13 +168,18 @@ class Context:
|
||||
slot_info: typing.Dict[int, NetworkSlot]
|
||||
generator_version = Version(0, 0, 0)
|
||||
checksums: typing.Dict[str, str]
|
||||
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
||||
item_names: typing.Dict[str, typing.Dict[int, str]] = (
|
||||
collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')))
|
||||
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||
location_names: typing.Dict[str, typing.Dict[int, str]] = (
|
||||
collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')))
|
||||
location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||
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]]
|
||||
public_stored_data_keys: typing.Set[str] # keys that can be retrieved by a client that has not reached "auth" yet
|
||||
spheres: typing.List[typing.Dict[int, typing.Set[int]]]
|
||||
""" each sphere is { player: { location_id, ... } } """
|
||||
logger: logging.Logger
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
@@ -238,6 +243,7 @@ class Context:
|
||||
self.stored_data = {}
|
||||
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
|
||||
self.read_data = {}
|
||||
self.spheres = []
|
||||
|
||||
# init empty to satisfy linter, I suppose
|
||||
self.gamespackage = {}
|
||||
@@ -247,6 +253,7 @@ class Context:
|
||||
self.all_item_and_group_names = {}
|
||||
self.all_location_and_group_names = {}
|
||||
self.non_hintable_names = collections.defaultdict(frozenset)
|
||||
self.public_stored_data_keys = set()
|
||||
|
||||
self._load_game_data()
|
||||
|
||||
@@ -267,14 +274,21 @@ class Context:
|
||||
if "checksum" in game_package:
|
||||
self.checksums[game_name] = game_package["checksum"]
|
||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||
self.item_names[item_id] = item_name
|
||||
self.item_names[game_name][item_id] = item_name
|
||||
for location_name, location_id in game_package["location_name_to_id"].items():
|
||||
self.location_names[location_id] = location_name
|
||||
self.location_names[game_name][location_id] = location_name
|
||||
self.all_item_and_group_names[game_name] = \
|
||||
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
|
||||
self.all_location_and_group_names[game_name] = \
|
||||
set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, []))
|
||||
|
||||
archipelago_item_names = self.item_names["Archipelago"]
|
||||
archipelago_location_names = self.location_names["Archipelago"]
|
||||
for game in [game_name for game_name in self.gamespackage if game_name != "Archipelago"]:
|
||||
# Add Archipelago items and locations to each data package.
|
||||
self.item_names[game].update(archipelago_item_names)
|
||||
self.location_names[game].update(archipelago_location_names)
|
||||
|
||||
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
||||
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
|
||||
|
||||
@@ -466,6 +480,9 @@ class Context:
|
||||
for game_name, data in self.location_name_groups.items():
|
||||
self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame]
|
||||
|
||||
# sorted access spheres
|
||||
self.spheres = decoded_obj.get("spheres", [])
|
||||
|
||||
# saving
|
||||
|
||||
def save(self, now=False) -> bool:
|
||||
@@ -508,7 +525,7 @@ class Context:
|
||||
self.logger.exception(e)
|
||||
self._start_async_saving()
|
||||
|
||||
def _start_async_saving(self):
|
||||
def _start_async_saving(self, atexit_save: bool = True):
|
||||
if not self.auto_saver_thread:
|
||||
def save_regularly():
|
||||
# time.time() is platform dependent, so using the expensive datetime method instead
|
||||
@@ -532,8 +549,9 @@ class Context:
|
||||
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
||||
self.auto_saver_thread.start()
|
||||
|
||||
import atexit
|
||||
atexit.register(self._save, True) # make sure we save on exit too
|
||||
if atexit_save:
|
||||
import atexit
|
||||
atexit.register(self._save, True) # make sure we save on exit too
|
||||
|
||||
def get_save(self) -> dict:
|
||||
self.recheck_hints()
|
||||
@@ -623,6 +641,16 @@ class Context:
|
||||
self.recheck_hints(team, slot)
|
||||
return self.hints[team, slot]
|
||||
|
||||
def get_sphere(self, player: int, location_id: int) -> int:
|
||||
"""Get sphere of a location, -1 if spheres are not available."""
|
||||
if self.spheres:
|
||||
for i, sphere in enumerate(self.spheres):
|
||||
if location_id in sphere.get(player, set()):
|
||||
return i
|
||||
raise KeyError(f"No Sphere found for location ID {location_id} belonging to player {player}. "
|
||||
f"Location or player may not exist.")
|
||||
return -1
|
||||
|
||||
def get_players_package(self):
|
||||
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||
|
||||
@@ -765,10 +793,7 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
for slot, connected_clients in clients.items():
|
||||
if connected_clients:
|
||||
name = ctx.player_names[team, slot]
|
||||
players.append(
|
||||
NetworkPlayer(team, slot,
|
||||
ctx.name_aliases.get((team, slot), name), name)
|
||||
)
|
||||
players.append(NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name))
|
||||
games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)}
|
||||
games.add("Archipelago")
|
||||
await ctx.send_msgs(client, [{
|
||||
@@ -783,8 +808,6 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
'permissions': get_permissions(ctx),
|
||||
'hint_cost': ctx.hint_cost,
|
||||
'location_check_points': ctx.location_check_points,
|
||||
'datapackage_versions': {game: game_data["version"] for game, game_data
|
||||
in ctx.gamespackage.items() if game in games},
|
||||
'datapackage_checksums': {game: game_data["checksum"] for game, game_data
|
||||
in ctx.gamespackage.items() if game in games and "checksum" in game_data},
|
||||
'seed_name': ctx.seed_name,
|
||||
@@ -988,8 +1011,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
send_items_to(ctx, team, target_player, new_item)
|
||||
|
||||
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]))
|
||||
team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id],
|
||||
ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location]))
|
||||
info_text = json_format_send_event(new_item, target_player)
|
||||
ctx.broadcast_team(team, [info_text])
|
||||
|
||||
@@ -1043,8 +1066,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location
|
||||
|
||||
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||
f"{ctx.item_names[hint.item]} is " \
|
||||
f"at {ctx.location_names[hint.location]} " \
|
||||
f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \
|
||||
f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \
|
||||
f"in {ctx.player_names[team, hint.finding_player]}'s World"
|
||||
|
||||
if hint.entrance:
|
||||
@@ -1073,28 +1096,6 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
||||
"item": net_item}
|
||||
|
||||
|
||||
def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bool, str]:
|
||||
picks = Utils.get_fuzzy_results(input_text, possible_answers, limit=2)
|
||||
if len(picks) > 1:
|
||||
dif = picks[0][1] - picks[1][1]
|
||||
if picks[0][1] == 100:
|
||||
return picks[0][0], True, "Perfect Match"
|
||||
elif picks[0][1] < 75:
|
||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
elif dif > 5:
|
||||
return picks[0][0], True, "Close Match"
|
||||
else:
|
||||
return picks[0][0], False, f"Too many close matches for '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
else:
|
||||
if picks[0][1] > 90:
|
||||
return picks[0][0], True, "Only Option Match"
|
||||
else:
|
||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
|
||||
|
||||
class CommandMeta(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
commands = attrs["commands"] = {}
|
||||
@@ -1346,7 +1347,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if self.ctx.remaining_mode == "enabled":
|
||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
if remaining_item_ids:
|
||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
|
||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id]
|
||||
for item_id in remaining_item_ids))
|
||||
else:
|
||||
self.output("No remaining items found.")
|
||||
@@ -1359,7 +1360,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
if remaining_item_ids:
|
||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
|
||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id]
|
||||
for item_id in remaining_item_ids))
|
||||
else:
|
||||
self.output("No remaining items found.")
|
||||
@@ -1377,7 +1378,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
|
||||
|
||||
if locations:
|
||||
names = [self.ctx.location_names[location] for location in locations]
|
||||
game = self.ctx.slot_info[self.client.slot].game
|
||||
names = [self.ctx.location_names[game][location] for location in locations]
|
||||
if filter_text:
|
||||
location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]]
|
||||
if filter_text in location_groups: # location group name
|
||||
@@ -1402,7 +1404,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
|
||||
|
||||
if locations:
|
||||
names = [self.ctx.location_names[location] for location in locations]
|
||||
game = self.ctx.slot_info[self.client.slot].game
|
||||
names = [self.ctx.location_names[game][location] for location in locations]
|
||||
if filter_text:
|
||||
location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]]
|
||||
if filter_text in location_groups: # location group name
|
||||
@@ -1483,10 +1486,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
elif input_text.isnumeric():
|
||||
game = self.ctx.games[self.client.slot]
|
||||
hint_id = int(input_text)
|
||||
hint_name = self.ctx.item_names[hint_id] \
|
||||
if not for_location and hint_id in self.ctx.item_names \
|
||||
else self.ctx.location_names[hint_id] \
|
||||
if for_location and hint_id in self.ctx.location_names \
|
||||
hint_name = self.ctx.item_names[game][hint_id] \
|
||||
if not for_location and hint_id in self.ctx.item_names[game] \
|
||||
else self.ctx.location_names[game][hint_id] \
|
||||
if for_location and hint_id in self.ctx.location_names[game] \
|
||||
else None
|
||||
if hint_name in self.ctx.non_hintable_names[game]:
|
||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||
@@ -1548,6 +1551,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.ctx.random.shuffle(not_found_hints)
|
||||
# By popular vote, make hints prefer non-local placements
|
||||
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
|
||||
# By another popular vote, prefer early sphere
|
||||
not_found_hints.sort(key=lambda hint: self.ctx.get_sphere(hint.finding_player, hint.location),
|
||||
reverse=True)
|
||||
|
||||
hints = found_hints + old_hints
|
||||
while can_pay > 0:
|
||||
@@ -1557,10 +1563,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
hints.append(hint)
|
||||
can_pay -= 1
|
||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
|
||||
self.ctx.notify_hints(self.client.team, hints)
|
||||
if not_found_hints:
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
if hints and cost and int((points_available // cost) == 0):
|
||||
self.output(
|
||||
f"There may be more hintables, however, you cannot afford to pay for any more. "
|
||||
@@ -1623,10 +1629,25 @@ def get_slot_points(ctx: Context, team: int, slot: int) -> int:
|
||||
ctx.get_hint_cost(slot) * ctx.hints_used[team, slot])
|
||||
|
||||
|
||||
async def process_get(ctx: Context, client: Client, args: dict, cmd: dict):
|
||||
if "keys" not in args or not isinstance(args["keys"], list):
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments",
|
||||
"text": 'Retrieve', "original_cmd": cmd}])
|
||||
return
|
||||
args["cmd"] = "Retrieved"
|
||||
keys = args["keys"]
|
||||
args["keys"] = {
|
||||
key: ctx.read_data.get(key[6:], lambda: None)() if key.startswith("_read_") else
|
||||
ctx.stored_data.get(key, None)
|
||||
for key in keys
|
||||
}
|
||||
await ctx.send_msgs(client, [args])
|
||||
|
||||
|
||||
async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
try:
|
||||
cmd: str = args["cmd"]
|
||||
except:
|
||||
except Exception:
|
||||
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`"}])
|
||||
@@ -1729,6 +1750,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||
"data": {"games": ctx.gamespackage}}])
|
||||
|
||||
elif cmd == "Get" and args.get("keys", None) and all(key in ctx.public_stored_data_keys for key in args["keys"]):
|
||||
await process_get(ctx, client, args, cmd)
|
||||
|
||||
elif client.auth:
|
||||
if cmd == "ConnectUpdate":
|
||||
if not args:
|
||||
@@ -1824,18 +1848,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
await ctx.send_encoded_msgs(bounceclient, msg)
|
||||
|
||||
elif cmd == "Get":
|
||||
if "keys" not in args or type(args["keys"]) != list:
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments",
|
||||
"text": 'Retrieve', "original_cmd": cmd}])
|
||||
return
|
||||
args["cmd"] = "Retrieved"
|
||||
keys = args["keys"]
|
||||
args["keys"] = {
|
||||
key: ctx.read_data.get(key[6:], lambda: None)() if key.startswith("_read_") else
|
||||
ctx.stored_data.get(key, None)
|
||||
for key in keys
|
||||
}
|
||||
await ctx.send_msgs(client, [args])
|
||||
await process_get(ctx, client, args, cmd)
|
||||
|
||||
elif cmd == "Set":
|
||||
if "key" not in args or args["key"].startswith("_read_") or \
|
||||
|
||||
@@ -247,7 +247,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
|
||||
def _handle_item_id(self, node: JSONMessagePart):
|
||||
item_id = int(node["text"])
|
||||
node["text"] = self.ctx.item_names[item_id]
|
||||
node["text"] = self.ctx.item_names.lookup_in_slot(item_id, node["player"])
|
||||
return self._handle_item_name(node)
|
||||
|
||||
def _handle_location_name(self, node: JSONMessagePart):
|
||||
@@ -255,8 +255,8 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
return self._handle_color(node)
|
||||
|
||||
def _handle_location_id(self, node: JSONMessagePart):
|
||||
item_id = int(node["text"])
|
||||
node["text"] = self.ctx.location_names[item_id]
|
||||
location_id = int(node["text"])
|
||||
node["text"] = self.ctx.location_names.lookup_in_slot(location_id, node["player"])
|
||||
return self._handle_location_name(node)
|
||||
|
||||
def _handle_entrance_name(self, node: JSONMessagePart):
|
||||
|
||||
281
Options.py
281
Options.py
@@ -12,6 +12,7 @@ from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
|
||||
from schema import And, Optional, Or, Schema
|
||||
from typing_extensions import Self
|
||||
|
||||
from Utils import get_fuzzy_results, is_iterable_except_str
|
||||
|
||||
@@ -746,6 +747,7 @@ class NamedRange(Range):
|
||||
|
||||
class FreezeValidKeys(AssembleOptions):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
assert not "_valid_keys" in attrs, "'_valid_keys' gets set by FreezeValidKeys, define 'valid_keys' instead."
|
||||
if "valid_keys" in attrs:
|
||||
attrs["_valid_keys"] = frozenset(attrs["valid_keys"])
|
||||
return super(FreezeValidKeys, mcs).__new__(mcs, name, bases, attrs)
|
||||
@@ -895,6 +897,228 @@ class ItemSet(OptionSet):
|
||||
convert_name_groups = True
|
||||
|
||||
|
||||
class PlandoText(typing.NamedTuple):
|
||||
at: str
|
||||
text: typing.List[str]
|
||||
percentage: int = 100
|
||||
|
||||
|
||||
PlandoTextsFromAnyType = typing.Union[
|
||||
typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoText, typing.Any]], typing.Any
|
||||
]
|
||||
|
||||
|
||||
class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
||||
default = ()
|
||||
supports_weighting = False
|
||||
display_name = "Plando Texts"
|
||||
|
||||
def __init__(self, value: typing.Iterable[PlandoText]) -> None:
|
||||
self.value = list(deepcopy(value))
|
||||
super().__init__()
|
||||
|
||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||
from BaseClasses import PlandoOptions
|
||||
if self.value and not (PlandoOptions.texts & plando_options):
|
||||
# plando is disabled but plando options were given so overwrite the options
|
||||
self.value = []
|
||||
logging.warning(f"The plando texts module is turned off, "
|
||||
f"so text for {player_name} will be ignored.")
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: PlandoTextsFromAnyType) -> Self:
|
||||
texts: typing.List[PlandoText] = []
|
||||
if isinstance(data, typing.Iterable):
|
||||
for text in data:
|
||||
if isinstance(text, typing.Mapping):
|
||||
if random.random() < float(text.get("percentage", 100)/100):
|
||||
at = text.get("at", None)
|
||||
if at is not None:
|
||||
given_text = text.get("text", [])
|
||||
if isinstance(given_text, str):
|
||||
given_text = [given_text]
|
||||
texts.append(PlandoText(
|
||||
at,
|
||||
given_text,
|
||||
text.get("percentage", 100)
|
||||
))
|
||||
elif isinstance(text, PlandoText):
|
||||
if random.random() < float(text.percentage/100):
|
||||
texts.append(text)
|
||||
else:
|
||||
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
|
||||
cls.verify_keys([text.at for text in texts])
|
||||
return cls(texts)
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}")
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: typing.List[PlandoText]) -> str:
|
||||
return str({text.at: " ".join(text.text) for text in value})
|
||||
|
||||
def __iter__(self) -> typing.Iterator[PlandoText]:
|
||||
yield from self.value
|
||||
|
||||
def __getitem__(self, index: typing.SupportsIndex) -> PlandoText:
|
||||
return self.value.__getitem__(index)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self.value.__len__()
|
||||
|
||||
|
||||
class ConnectionsMeta(AssembleOptions):
|
||||
def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, typing.Any]):
|
||||
if name != "PlandoConnections":
|
||||
assert "entrances" in attrs, f"Please define valid entrances for {name}"
|
||||
attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"]))
|
||||
assert "exits" in attrs, f"Please define valid exits for {name}"
|
||||
attrs["exits"] = frozenset((connection.lower() for connection in attrs["exits"]))
|
||||
if "__doc__" not in attrs:
|
||||
attrs["__doc__"] = PlandoConnections.__doc__
|
||||
cls = super().__new__(mcs, name, bases, attrs)
|
||||
return cls
|
||||
|
||||
|
||||
class PlandoConnection(typing.NamedTuple):
|
||||
class Direction:
|
||||
entrance = "entrance"
|
||||
exit = "exit"
|
||||
both = "both"
|
||||
|
||||
entrance: str
|
||||
exit: str
|
||||
direction: typing.Literal["entrance", "exit", "both"] # TODO: convert Direction to StrEnum once 3.8 is dropped
|
||||
percentage: int = 100
|
||||
|
||||
|
||||
PlandoConFromAnyType = typing.Union[
|
||||
typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoConnection, typing.Any]], typing.Any
|
||||
]
|
||||
|
||||
|
||||
class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=ConnectionsMeta):
|
||||
"""Generic connections plando. Format is:
|
||||
- entrance: "Entrance Name"
|
||||
exit: "Exit Name"
|
||||
direction: "Direction"
|
||||
percentage: 100
|
||||
Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both' if omitted.
|
||||
Percentage is an integer from 1 to 100, and defaults to 100 when omitted."""
|
||||
|
||||
display_name = "Plando Connections"
|
||||
|
||||
default = ()
|
||||
supports_weighting = False
|
||||
|
||||
entrances: typing.ClassVar[typing.AbstractSet[str]]
|
||||
exits: typing.ClassVar[typing.AbstractSet[str]]
|
||||
|
||||
duplicate_exits: bool = False
|
||||
"""Whether or not exits should be allowed to be duplicate."""
|
||||
|
||||
def __init__(self, value: typing.Iterable[PlandoConnection]):
|
||||
self.value = list(deepcopy(value))
|
||||
super(PlandoConnections, self).__init__()
|
||||
|
||||
@classmethod
|
||||
def validate_entrance_name(cls, entrance: str) -> bool:
|
||||
return entrance.lower() in cls.entrances
|
||||
|
||||
@classmethod
|
||||
def validate_exit_name(cls, exit: str) -> bool:
|
||||
return exit.lower() in cls.exits
|
||||
|
||||
@classmethod
|
||||
def can_connect(cls, entrance: str, exit: str) -> bool:
|
||||
"""Checks that a given entrance can connect to a given exit.
|
||||
By default, this will always return true unless overridden."""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnection]) -> None:
|
||||
used_entrances: typing.List[str] = []
|
||||
used_exits: typing.List[str] = []
|
||||
for connection in connections:
|
||||
entrance = connection.entrance
|
||||
exit = connection.exit
|
||||
direction = connection.direction
|
||||
if direction not in (PlandoConnection.Direction.entrance,
|
||||
PlandoConnection.Direction.exit,
|
||||
PlandoConnection.Direction.both):
|
||||
raise ValueError(f"Unknown direction: {direction}")
|
||||
if entrance in used_entrances:
|
||||
raise ValueError(f"Duplicate Entrance {entrance} not allowed.")
|
||||
if not cls.duplicate_exits and exit in used_exits:
|
||||
raise ValueError(f"Duplicate Exit {exit} not allowed.")
|
||||
used_entrances.append(entrance)
|
||||
used_exits.append(exit)
|
||||
if not cls.validate_entrance_name(entrance):
|
||||
raise ValueError(f"{entrance.title()} is not a valid entrance.")
|
||||
if not cls.validate_exit_name(exit):
|
||||
raise ValueError(f"{exit.title()} is not a valid exit.")
|
||||
if not cls.can_connect(entrance, exit):
|
||||
raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.")
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: PlandoConFromAnyType) -> Self:
|
||||
if not isinstance(data, typing.Iterable):
|
||||
raise Exception(f"Cannot create plando connections from non-List value, got {type(data)}.")
|
||||
|
||||
value: typing.List[PlandoConnection] = []
|
||||
for connection in data:
|
||||
if isinstance(connection, typing.Mapping):
|
||||
percentage = connection.get("percentage", 100)
|
||||
if random.random() < float(percentage / 100):
|
||||
entrance = connection.get("entrance", None)
|
||||
if is_iterable_except_str(entrance):
|
||||
entrance = random.choice(sorted(entrance))
|
||||
exit = connection.get("exit", None)
|
||||
if is_iterable_except_str(exit):
|
||||
exit = random.choice(sorted(exit))
|
||||
direction = connection.get("direction", "both")
|
||||
|
||||
if not entrance or not exit:
|
||||
raise Exception("Plando connection must have an entrance and an exit.")
|
||||
value.append(PlandoConnection(
|
||||
entrance,
|
||||
exit,
|
||||
direction,
|
||||
percentage
|
||||
))
|
||||
elif isinstance(connection, PlandoConnection):
|
||||
if random.random() < float(connection.percentage / 100):
|
||||
value.append(connection)
|
||||
else:
|
||||
raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.")
|
||||
cls.validate_plando_connections(value)
|
||||
return cls(value)
|
||||
|
||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||
from BaseClasses import PlandoOptions
|
||||
if self.value and not (PlandoOptions.connections & plando_options):
|
||||
# plando is disabled but plando options were given so overwrite the options
|
||||
self.value = []
|
||||
logging.warning(f"The plando connections module is turned off, "
|
||||
f"so connections for {player_name} will be ignored.")
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: typing.List[PlandoConnection]) -> str:
|
||||
return ", ".join(["%s %s %s" % (connection.entrance,
|
||||
"<=>" if connection.direction == PlandoConnection.Direction.both else
|
||||
"<=" if connection.direction == PlandoConnection.Direction.exit else
|
||||
"=>",
|
||||
connection.exit) for connection in value])
|
||||
|
||||
def __getitem__(self, index: typing.SupportsIndex) -> PlandoConnection:
|
||||
return self.value.__getitem__(index)
|
||||
|
||||
def __iter__(self) -> typing.Iterator[PlandoConnection]:
|
||||
yield from self.value
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.value)
|
||||
|
||||
|
||||
class Accessibility(Choice):
|
||||
"""Set rules for reachability of your items/locations.
|
||||
Locations: ensure everything can be reached and acquired.
|
||||
@@ -909,8 +1133,10 @@ class Accessibility(Choice):
|
||||
|
||||
|
||||
class ProgressionBalancing(NamedRange):
|
||||
"""A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||
A lower setting means more getting stuck. A higher setting means less getting stuck."""
|
||||
"""
|
||||
A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||
A lower setting means more getting stuck. A higher setting means less getting stuck.
|
||||
"""
|
||||
default = 50
|
||||
range_start = 0
|
||||
range_end = 99
|
||||
@@ -983,7 +1209,7 @@ class LocalItems(ItemSet):
|
||||
|
||||
class NonLocalItems(ItemSet):
|
||||
"""Forces these items to be outside their native world."""
|
||||
display_name = "Not Local Items"
|
||||
display_name = "Non-local Items"
|
||||
|
||||
|
||||
class StartInventory(ItemDict):
|
||||
@@ -1046,7 +1272,8 @@ class ItemLinks(OptionList):
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, allow_item_groups: bool = True) -> typing.Set:
|
||||
def verify_items(items: typing.List[str], item_link: str, pool_name: str, world,
|
||||
allow_item_groups: bool = True) -> typing.Set:
|
||||
pool = set()
|
||||
for item_name in items:
|
||||
if item_name not in world.item_names and (not allow_item_groups or item_name not in world.item_name_groups):
|
||||
@@ -1129,9 +1356,41 @@ class OptionGroup(typing.NamedTuple):
|
||||
"""Name of the group to categorize these options in for display on the WebHost and in generated YAMLS."""
|
||||
options: typing.List[typing.Type[Option[typing.Any]]]
|
||||
"""Options to be in the defined group."""
|
||||
start_collapsed: bool = False
|
||||
"""Whether the group will start collapsed on the WebHost options pages."""
|
||||
|
||||
|
||||
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
|
||||
item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints,
|
||||
StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks]
|
||||
"""
|
||||
Options that are always populated in "Item & Location Options" Option Group. Cannot be moved to another group.
|
||||
If desired, a custom "Item & Location Options" Option Group can be defined, but only for adding additional options to
|
||||
it.
|
||||
"""
|
||||
|
||||
|
||||
def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[
|
||||
str, typing.Dict[str, typing.Type[Option[typing.Any]]]]:
|
||||
"""Generates and returns a dictionary for the option groups of a specified world."""
|
||||
option_groups = {option: option_group.name
|
||||
for option_group in world.web.option_groups
|
||||
for option in option_group.options}
|
||||
# add a default option group for uncategorized options to get thrown into
|
||||
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 visibility_level & option.visibility:
|
||||
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
||||
|
||||
# if the world doesn't have any ungrouped options, this group will be empty so just remove it
|
||||
if not grouped_options["Game Options"]:
|
||||
del grouped_options["Game Options"]
|
||||
|
||||
return grouped_options
|
||||
|
||||
|
||||
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None:
|
||||
import os
|
||||
|
||||
import yaml
|
||||
@@ -1169,17 +1428,7 @@ 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:
|
||||
|
||||
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
|
||||
|
||||
grouped_options = get_option_groups(world)
|
||||
with open(local_path("data", "options.yaml")) as f:
|
||||
file_data = f.read()
|
||||
res = Template(file_data).render(
|
||||
|
||||
47
README.md
47
README.md
@@ -1,8 +1,10 @@
|
||||
# [Archipelago](https://archipelago.gg)  | [Install](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
|
||||
Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases, presently, Archipelago is also the randomizer itself.
|
||||
Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases,
|
||||
presently, Archipelago is also the randomizer itself.
|
||||
|
||||
Currently, the following games are supported:
|
||||
|
||||
* The Legend of Zelda: A Link to the Past
|
||||
* Factorio
|
||||
* Minecraft
|
||||
@@ -67,7 +69,9 @@ Currently, the following games are supported:
|
||||
* Yoshi's Island
|
||||
* Mario & Luigi: Superstar Saga
|
||||
* Bomb Rush Cyberfunk
|
||||
* Aquaria
|
||||
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
||||
* A Hat in Time
|
||||
|
||||
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
|
||||
@@ -75,36 +79,57 @@ windows binaries.
|
||||
|
||||
## History
|
||||
|
||||
Archipelago is built upon a strong legacy of brilliant hobbyists. We want to honor that legacy by showing it here. The repositories which Archipelago is built upon, inspired by, or otherwise owes its gratitude to are:
|
||||
Archipelago is built upon a strong legacy of brilliant hobbyists. We want to honor that legacy by showing it here.
|
||||
The repositories which Archipelago is built upon, inspired by, or otherwise owes its gratitude to are:
|
||||
|
||||
* [bonta0's MultiWorld](https://github.com/Bonta0/ALttPEntranceRandomizer/tree/multiworld_31)
|
||||
* [AmazingAmpharos' Entrance Randomizer](https://github.com/AmazingAmpharos/ALttPEntranceRandomizer)
|
||||
* [VT Web Randomizer](https://github.com/sporchia/alttp_vt_randomizer)
|
||||
* [Dessyreqt's alttprandomizer](https://github.com/Dessyreqt/alttprandomizer)
|
||||
* [Zarby89's](https://github.com/Ijwu/Enemizer/commits?author=Zarby89) and [sosuke3's](https://github.com/Ijwu/Enemizer/commits?author=sosuke3) contributions to Enemizer, which make the vast majority of Enemizer contributions.
|
||||
* [Zarby89's](https://github.com/Ijwu/Enemizer/commits?author=Zarby89)
|
||||
and [sosuke3's](https://github.com/Ijwu/Enemizer/commits?author=sosuke3) contributions to Enemizer, which make up the
|
||||
vast majority of Enemizer contributions.
|
||||
|
||||
We recognize that there is a strong community of incredibly smart people that have come before us and helped pave the path. Just because one person's name may be in a repository title does not mean that only one person made that project happen. We can't hope to perfectly cover every single contribution that lead up to Archipelago but we hope to honor them fairly.
|
||||
We recognize that there is a strong community of incredibly smart people that have come before us and helped pave the
|
||||
path. Just because one person's name may be in a repository title does not mean that only one person made that project
|
||||
happen. We can't hope to perfectly cover every single contribution that lead up to Archipelago, but we hope to honor
|
||||
them fairly.
|
||||
|
||||
### Path to the Archipelago
|
||||
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
|
||||
|
||||
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a
|
||||
long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to
|
||||
_MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as
|
||||
"Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository
|
||||
(as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
|
||||
|
||||
## Running Archipelago
|
||||
For most people, all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer, or AppImage for Linux-based systems.
|
||||
|
||||
If you are a developer or are running on a platform with no compiled releases available, please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
|
||||
For most people, all you need to do is head over to
|
||||
the [releases page](https://github.com/ArchipelagoMW/Archipelago/releases), then download and run the appropriate
|
||||
installer, or AppImage for Linux-based systems.
|
||||
|
||||
If you are a developer or are running on a platform with no compiled releases available, please see our doc on
|
||||
[running Archipelago from source](docs/running%20from%20source.md).
|
||||
|
||||
## Related Repositories
|
||||
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
|
||||
|
||||
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the
|
||||
contributions of their developers, past and present.
|
||||
|
||||
* [z3randomizer](https://github.com/ArchipelagoMW/z3randomizer)
|
||||
* [Enemizer](https://github.com/Ijwu/Enemizer)
|
||||
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
|
||||
|
||||
## Contributing
|
||||
For contribution guidelines, please see our [Contributing doc.](/docs/contributing.md)
|
||||
|
||||
To contribute to Archipelago, including the WebHost, core program, or by adding a new game, see our
|
||||
[Contributing guidelines](/docs/contributing.md).
|
||||
|
||||
## FAQ
|
||||
For Frequently asked questions, please see the website's [FAQ Page.](https://archipelago.gg/faq/en/)
|
||||
|
||||
For Frequently asked questions, please see the website's [FAQ Page](https://archipelago.gg/faq/en/).
|
||||
|
||||
## Code of Conduct
|
||||
Please refer to our [code of conduct.](/docs/code_of_conduct.md)
|
||||
|
||||
Please refer to our [code of conduct](/docs/code_of_conduct.md).
|
||||
|
||||
@@ -247,8 +247,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||
toDraw = ""
|
||||
for i in range(20):
|
||||
if i < len(str(ctx.item_names[l.item])):
|
||||
toDraw += str(ctx.item_names[l.item])[i]
|
||||
if i < len(str(ctx.item_names.lookup_in_slot(l.item))):
|
||||
toDraw += str(ctx.item_names.lookup_in_slot(l.item))[i]
|
||||
else:
|
||||
break
|
||||
f.write(toDraw)
|
||||
|
||||
102
Utils.py
102
Utils.py
@@ -46,7 +46,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.4.6"
|
||||
__version__ = "0.5.0"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -101,8 +101,7 @@ def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[
|
||||
|
||||
@functools.wraps(function)
|
||||
def wrap(self: S, arg: T) -> RetType:
|
||||
cache: Optional[Dict[T, RetType]] = typing.cast(Optional[Dict[T, RetType]],
|
||||
getattr(self, cache_name, None))
|
||||
cache: Optional[Dict[T, RetType]] = getattr(self, cache_name, None)
|
||||
if cache is None:
|
||||
res = function(self, arg)
|
||||
setattr(self, cache_name, {arg: res})
|
||||
@@ -209,10 +208,11 @@ def output_path(*path: str) -> str:
|
||||
|
||||
def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
|
||||
if is_windows:
|
||||
os.startfile(filename)
|
||||
os.startfile(filename) # type: ignore
|
||||
else:
|
||||
from shutil import which
|
||||
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
|
||||
assert open_command, "Didn't find program for open_file! Please report this together with system details."
|
||||
subprocess.call([open_command, filename])
|
||||
|
||||
|
||||
@@ -300,21 +300,21 @@ def get_options() -> Settings:
|
||||
return get_settings()
|
||||
|
||||
|
||||
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
||||
def persistent_store(category: str, key: str, value: typing.Any):
|
||||
path = user_path("_persistent_storage.yaml")
|
||||
storage: dict = persistent_load()
|
||||
category = storage.setdefault(category, {})
|
||||
category[key] = value
|
||||
storage = persistent_load()
|
||||
category_dict = storage.setdefault(category, {})
|
||||
category_dict[key] = value
|
||||
with open(path, "wt") as f:
|
||||
f.write(dump(storage, Dumper=Dumper))
|
||||
|
||||
|
||||
def persistent_load() -> typing.Dict[str, dict]:
|
||||
storage = getattr(persistent_load, "storage", None)
|
||||
def persistent_load() -> Dict[str, Dict[str, Any]]:
|
||||
storage: Union[Dict[str, Dict[str, Any]], None] = getattr(persistent_load, "storage", None)
|
||||
if storage:
|
||||
return storage
|
||||
path = user_path("_persistent_storage.yaml")
|
||||
storage: dict = {}
|
||||
storage = {}
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
@@ -323,7 +323,7 @@ def persistent_load() -> typing.Dict[str, dict]:
|
||||
logging.debug(f"Could not read store: {e}")
|
||||
if storage is None:
|
||||
storage = {}
|
||||
persistent_load.storage = storage
|
||||
setattr(persistent_load, "storage", storage)
|
||||
return storage
|
||||
|
||||
|
||||
@@ -365,6 +365,7 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N
|
||||
except Exception as e:
|
||||
logging.debug(f"Could not store data package: {e}")
|
||||
|
||||
|
||||
def get_default_adjuster_settings(game_name: str) -> Namespace:
|
||||
import LttPAdjuster
|
||||
adjuster_settings = Namespace()
|
||||
@@ -383,7 +384,9 @@ def get_adjuster_settings(game_name: str) -> Namespace:
|
||||
default_settings = get_default_adjuster_settings(game_name)
|
||||
|
||||
# Fill in any arguments from the argparser that we haven't seen before
|
||||
return Namespace(**vars(adjuster_settings), **{k:v for k,v in vars(default_settings).items() if k not in vars(adjuster_settings)})
|
||||
return Namespace(**vars(adjuster_settings), **{
|
||||
k: v for k, v in vars(default_settings).items() if k not in vars(adjuster_settings)
|
||||
})
|
||||
|
||||
|
||||
@cache_argsless
|
||||
@@ -407,13 +410,13 @@ safe_builtins = frozenset((
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
generic_properties_module: Optional[object]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
||||
self.options_module = importlib.import_module("Options")
|
||||
self.net_utils_module = importlib.import_module("NetUtils")
|
||||
self.generic_properties_module = None
|
||||
|
||||
def find_class(self, module, name):
|
||||
def find_class(self, module: str, name: str) -> type:
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
return getattr(builtins, name)
|
||||
# used by MultiServer -> savegame/multidata
|
||||
@@ -437,7 +440,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
|
||||
|
||||
|
||||
def restricted_loads(s):
|
||||
def restricted_loads(s: bytes) -> Any:
|
||||
"""Helper function analogous to pickle.loads()."""
|
||||
return RestrictedUnpickler(io.BytesIO(s)).load()
|
||||
|
||||
@@ -455,6 +458,15 @@ class KeyedDefaultDict(collections.defaultdict):
|
||||
"""defaultdict variant that uses the missing key as argument to default_factory"""
|
||||
default_factory: typing.Callable[[typing.Any], typing.Any]
|
||||
|
||||
def __init__(self,
|
||||
default_factory: typing.Callable[[Any], Any] = None,
|
||||
seq: typing.Union[typing.Mapping, typing.Iterable, None] = None,
|
||||
**kwargs):
|
||||
if seq is not None:
|
||||
super().__init__(default_factory, seq, **kwargs)
|
||||
else:
|
||||
super().__init__(default_factory, **kwargs)
|
||||
|
||||
def __missing__(self, key):
|
||||
self[key] = value = self.default_factory(key)
|
||||
return value
|
||||
@@ -493,7 +505,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
||||
file_handler.setFormatter(logging.Formatter(log_format))
|
||||
|
||||
class Filter(logging.Filter):
|
||||
def __init__(self, filter_name, condition):
|
||||
def __init__(self, filter_name: str, condition: typing.Callable[[logging.LogRecord], bool]) -> None:
|
||||
super().__init__(filter_name)
|
||||
self.condition = condition
|
||||
|
||||
@@ -544,7 +556,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
||||
)
|
||||
|
||||
|
||||
def stream_input(stream, queue):
|
||||
def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"):
|
||||
def queuer():
|
||||
while 1:
|
||||
try:
|
||||
@@ -572,7 +584,7 @@ class VersionException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str:
|
||||
def chaining_prefix(index: int, labels: typing.Sequence[str]) -> str:
|
||||
text = ""
|
||||
max_label = len(labels) - 1
|
||||
while index > max_label:
|
||||
@@ -595,7 +607,7 @@ def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P
|
||||
return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}"
|
||||
|
||||
|
||||
def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \
|
||||
def get_fuzzy_results(input_word: str, word_list: typing.Collection[str], limit: typing.Optional[int] = None) \
|
||||
-> typing.List[typing.Tuple[str, int]]:
|
||||
import jellyfish
|
||||
|
||||
@@ -603,21 +615,55 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty
|
||||
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
|
||||
/ max(len(word1), len(word2)))
|
||||
|
||||
limit: int = limit if limit else len(wordlist)
|
||||
limit = limit if limit else len(word_list)
|
||||
return list(
|
||||
map(
|
||||
lambda container: (container[0], int(container[1]*100)), # convert up to limit to int %
|
||||
sorted(
|
||||
map(lambda candidate:
|
||||
(candidate, get_fuzzy_ratio(input_word, candidate)),
|
||||
wordlist),
|
||||
map(lambda candidate: (candidate, get_fuzzy_ratio(input_word, candidate)), word_list),
|
||||
key=lambda element: element[1],
|
||||
reverse=True)[0:limit]
|
||||
reverse=True
|
||||
)[0:limit]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], suggest: str = "") \
|
||||
def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bool, str]:
|
||||
picks = get_fuzzy_results(input_text, possible_answers, limit=2)
|
||||
if len(picks) > 1:
|
||||
dif = picks[0][1] - picks[1][1]
|
||||
if picks[0][1] == 100:
|
||||
return picks[0][0], True, "Perfect Match"
|
||||
elif picks[0][1] < 75:
|
||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
elif dif > 5:
|
||||
return picks[0][0], True, "Close Match"
|
||||
else:
|
||||
return picks[0][0], False, f"Too many close matches for '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
else:
|
||||
if picks[0][1] > 90:
|
||||
return picks[0][0], True, "Only Option Match"
|
||||
else:
|
||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
|
||||
|
||||
def get_input_text_from_response(text: str, command: str) -> typing.Optional[str]:
|
||||
if "did you mean " in text:
|
||||
for question in ("Didn't find something that closely matches",
|
||||
"Too many close matches"):
|
||||
if text.startswith(question):
|
||||
name = get_text_between(text, "did you mean '",
|
||||
"'? (")
|
||||
return f"!{command} {name}"
|
||||
elif text.startswith("Missing: "):
|
||||
return text.replace("Missing: ", "!hint_location ")
|
||||
return None
|
||||
|
||||
|
||||
def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
||||
-> typing.Optional[str]:
|
||||
logging.info(f"Opening file input dialog for {title}.")
|
||||
|
||||
@@ -734,7 +780,7 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
root.update()
|
||||
|
||||
|
||||
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
|
||||
def title_sorted(data: typing.Iterable, key=None, ignore: typing.AbstractSet[str] = frozenset(("a", "the"))):
|
||||
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
||||
def sorter(element: Union[str, Dict[str, Any]]) -> str:
|
||||
if (not isinstance(element, str)):
|
||||
@@ -788,7 +834,7 @@ class DeprecateDict(dict):
|
||||
log_message: str
|
||||
should_error: bool
|
||||
|
||||
def __init__(self, message, error: bool = False) -> None:
|
||||
def __init__(self, message: str, error: bool = False) -> None:
|
||||
self.log_message = message
|
||||
self.should_error = error
|
||||
super().__init__()
|
||||
|
||||
@@ -176,7 +176,7 @@ class WargrooveContext(CommonContext):
|
||||
if not os.path.isfile(path):
|
||||
open(path, 'w').close()
|
||||
# Announcing commander unlocks
|
||||
item_name = self.item_names[network_item.item]
|
||||
item_name = self.item_names.lookup_in_slot(network_item.item)
|
||||
if item_name in faction_table.keys():
|
||||
for commander in faction_table[item_name]:
|
||||
logger.info(f"{commander.name} has been unlocked!")
|
||||
@@ -197,7 +197,7 @@ class WargrooveContext(CommonContext):
|
||||
open(print_path, 'w').close()
|
||||
with open(print_path, 'w') as f:
|
||||
f.write("Received " +
|
||||
self.item_names[network_item.item] +
|
||||
self.item_names.lookup_in_slot(network_item.item) +
|
||||
" from " +
|
||||
self.player_names[network_item.player])
|
||||
f.close()
|
||||
@@ -342,7 +342,7 @@ class WargrooveContext(CommonContext):
|
||||
faction_items = 0
|
||||
faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()]
|
||||
for network_item in self.items_received:
|
||||
if self.item_names[network_item.item] in faction_item_names:
|
||||
if self.item_names.lookup_in_slot(network_item.item) in faction_item_names:
|
||||
faction_items += 1
|
||||
starting_groove = (faction_items - 1) * self.starting_groove_multiplier
|
||||
# Must be an integer larger than 0
|
||||
|
||||
10
WebHost.py
10
WebHost.py
@@ -117,7 +117,7 @@ if __name__ == "__main__":
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
||||
|
||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||
from WebHostLib.autolauncher import autohost, autogen
|
||||
from WebHostLib.autolauncher import autohost, autogen, stop
|
||||
from WebHostLib.options import create as create_options_files
|
||||
|
||||
try:
|
||||
@@ -138,3 +138,11 @@ if __name__ == "__main__":
|
||||
else:
|
||||
from waitress import serve
|
||||
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
|
||||
else:
|
||||
from time import sleep
|
||||
try:
|
||||
while True:
|
||||
sleep(1) # wait for process to be killed
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
pass
|
||||
stop() # stop worker threads
|
||||
|
||||
@@ -56,15 +56,6 @@ def get_datapackage():
|
||||
return network_data_package
|
||||
|
||||
|
||||
@api_endpoints.route('/datapackage_version')
|
||||
@cache.cached()
|
||||
def get_datapackage_versions():
|
||||
from worlds import AutoWorldRegister
|
||||
|
||||
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
|
||||
return version_package
|
||||
|
||||
|
||||
@api_endpoints.route('/datapackage_checksum')
|
||||
@cache.cached()
|
||||
def get_datapackage_checksums():
|
||||
|
||||
@@ -3,16 +3,26 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import time
|
||||
import typing
|
||||
from uuid import UUID
|
||||
from datetime import timedelta, datetime
|
||||
from threading import Event, Thread
|
||||
from uuid import UUID
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
|
||||
from Utils import restricted_loads
|
||||
from .locker import Locker, AlreadyRunningException
|
||||
|
||||
_stop_event = Event()
|
||||
|
||||
|
||||
def stop():
|
||||
"""Stops previously launched threads"""
|
||||
global _stop_event
|
||||
stop_event = _stop_event
|
||||
_stop_event = Event() # new event for new threads
|
||||
stop_event.set()
|
||||
|
||||
|
||||
def handle_generation_success(seed_id):
|
||||
logging.info(f"Generation finished for seed {seed_id}")
|
||||
@@ -63,6 +73,7 @@ def cleanup():
|
||||
|
||||
def autohost(config: dict):
|
||||
def keep_running():
|
||||
stop_event = _stop_event
|
||||
try:
|
||||
with Locker("autohost"):
|
||||
cleanup()
|
||||
@@ -72,26 +83,25 @@ def autohost(config: dict):
|
||||
hosters.append(hoster)
|
||||
hoster.start()
|
||||
|
||||
while 1:
|
||||
time.sleep(0.1)
|
||||
while not stop_event.wait(0.1):
|
||||
with db_session:
|
||||
rooms = select(
|
||||
room for room in Room if
|
||||
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
||||
for room in rooms:
|
||||
# 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):
|
||||
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
|
||||
hosters[room.id.int % len(hosters)].start_room(room.id)
|
||||
|
||||
except AlreadyRunningException:
|
||||
logging.info("Autohost reports as already running, not starting another.")
|
||||
|
||||
import threading
|
||||
threading.Thread(target=keep_running, name="AP_Autohost").start()
|
||||
Thread(target=keep_running, name="AP_Autohost").start()
|
||||
|
||||
|
||||
def autogen(config: dict):
|
||||
def keep_running():
|
||||
stop_event = _stop_event
|
||||
try:
|
||||
with Locker("autogen"):
|
||||
|
||||
@@ -112,8 +122,7 @@ def autogen(config: dict):
|
||||
commit()
|
||||
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
|
||||
|
||||
while 1:
|
||||
time.sleep(0.1)
|
||||
while not stop_event.wait(0.1):
|
||||
with db_session:
|
||||
# for update locks the database row(s) during transaction, preventing writes from elsewhere
|
||||
to_start = select(
|
||||
@@ -124,8 +133,7 @@ def autogen(config: dict):
|
||||
except AlreadyRunningException:
|
||||
logging.info("Autogen reports as already running, not starting another.")
|
||||
|
||||
import threading
|
||||
threading.Thread(target=keep_running, name="AP_Autogen").start()
|
||||
Thread(target=keep_running, name="AP_Autogen").start()
|
||||
|
||||
|
||||
multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {}
|
||||
|
||||
@@ -74,6 +74,7 @@ class WebHostContext(Context):
|
||||
|
||||
def _load_game_data(self):
|
||||
for key, value in self.static_server_data.items():
|
||||
# NOTE: attributes are mutable and shared, so they will have to be copied before being modified
|
||||
setattr(self, key, value)
|
||||
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
||||
|
||||
@@ -101,18 +102,37 @@ class WebHostContext(Context):
|
||||
|
||||
multidata = self.decompress(room.seed.multidata)
|
||||
game_data_packages = {}
|
||||
|
||||
static_gamespackage = self.gamespackage # this is shared across all rooms
|
||||
static_item_name_groups = self.item_name_groups
|
||||
static_location_name_groups = self.location_name_groups
|
||||
self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})} # this may be modified by _load
|
||||
self.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})}
|
||||
self.location_name_groups = {"Archipelago": static_location_name_groups.get("Archipelago", {})}
|
||||
|
||||
for game in list(multidata.get("datapackage", {})):
|
||||
game_data = multidata["datapackage"][game]
|
||||
if "checksum" in game_data:
|
||||
if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
||||
# non-custom. remove from multidata
|
||||
if static_gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
||||
# non-custom. remove from multidata and use static data
|
||||
# games package could be dropped from static data once all rooms embed data package
|
||||
del multidata["datapackage"][game]
|
||||
else:
|
||||
row = GameDataPackage.get(checksum=game_data["checksum"])
|
||||
if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete
|
||||
game_data_packages[game] = Utils.restricted_loads(row.data)
|
||||
continue
|
||||
else:
|
||||
self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}")
|
||||
self.gamespackage[game] = static_gamespackage.get(game, {})
|
||||
self.item_name_groups[game] = static_item_name_groups.get(game, {})
|
||||
self.location_name_groups[game] = static_location_name_groups.get(game, {})
|
||||
|
||||
if not game_data_packages:
|
||||
# all static -> use the static dicts directly
|
||||
self.gamespackage = static_gamespackage
|
||||
self.item_name_groups = static_item_name_groups
|
||||
self.location_name_groups = static_location_name_groups
|
||||
return self._load(multidata, game_data_packages, True)
|
||||
|
||||
@db_session
|
||||
@@ -122,7 +142,7 @@ class WebHostContext(Context):
|
||||
savegame_data = Room.get(id=self.room_id).multisave
|
||||
if savegame_data:
|
||||
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
||||
self._start_async_saving()
|
||||
self._start_async_saving(atexit_save=False)
|
||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
||||
|
||||
@db_session
|
||||
@@ -212,59 +232,66 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
async def start_room(room_id):
|
||||
try:
|
||||
logger = set_up_logging(room_id)
|
||||
ctx = WebHostContext(static_server_data, logger)
|
||||
ctx.load(room_id)
|
||||
ctx.init_save()
|
||||
with Locker(f"RoomLocker {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:
|
||||
ctx.logger.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
|
||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||
await ctx.shutdown_task
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
if ctx.saving:
|
||||
ctx._save()
|
||||
except Exception as e:
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
room.last_port = -1
|
||||
logger.exception(e)
|
||||
raise
|
||||
else:
|
||||
ctx.logger.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
|
||||
|
||||
# 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)
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
# 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)
|
||||
except Exception:
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
room.last_port = -1
|
||||
# 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)
|
||||
if ctx.saving:
|
||||
ctx._save()
|
||||
finally:
|
||||
try:
|
||||
with (db_session):
|
||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||
room = Room.get(id=room_id)
|
||||
room.last_activity = datetime.datetime.utcnow() - \
|
||||
datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||
logging.info(f"Shutting down room {room_id} on {name}.")
|
||||
finally:
|
||||
await asyncio.sleep(5)
|
||||
rooms_shutting_down.put(room_id)
|
||||
|
||||
class Starter(threading.Thread):
|
||||
def run(self):
|
||||
|
||||
@@ -6,7 +6,7 @@ import random
|
||||
import tempfile
|
||||
import zipfile
|
||||
from collections import Counter
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Union, Set
|
||||
|
||||
from flask import flash, redirect, render_template, request, session, url_for
|
||||
from pony.orm import commit, db_session
|
||||
@@ -16,6 +16,7 @@ from Generate import PlandoOptions, handle_name
|
||||
from Main import main as ERmain
|
||||
from Utils import __version__
|
||||
from WebHostLib import app
|
||||
from settings import ServerOptions, GeneratorOptions
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from .check import get_yaml_data, roll_options
|
||||
from .models import Generation, STATE_ERROR, STATE_QUEUED, Seed, UUID
|
||||
@@ -23,25 +24,22 @@ from .upload import upload_zip_to_db
|
||||
|
||||
|
||||
def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[str], Dict[str, Any]]]:
|
||||
plando_options = {
|
||||
options_source.get("plando_bosses", ""),
|
||||
options_source.get("plando_items", ""),
|
||||
options_source.get("plando_connections", ""),
|
||||
options_source.get("plando_texts", "")
|
||||
}
|
||||
plando_options -= {""}
|
||||
plando_options: Set[str] = set()
|
||||
for substr in ("bosses", "items", "connections", "texts"):
|
||||
if options_source.get(f"plando_{substr}", substr in GeneratorOptions.plando_options):
|
||||
plando_options.add(substr)
|
||||
|
||||
server_options = {
|
||||
"hint_cost": int(options_source.get("hint_cost", 10)),
|
||||
"release_mode": options_source.get("release_mode", "goal"),
|
||||
"remaining_mode": options_source.get("remaining_mode", "disabled"),
|
||||
"collect_mode": options_source.get("collect_mode", "disabled"),
|
||||
"item_cheat": bool(int(options_source.get("item_cheat", 1))),
|
||||
"hint_cost": int(options_source.get("hint_cost", ServerOptions.hint_cost)),
|
||||
"release_mode": options_source.get("release_mode", ServerOptions.release_mode),
|
||||
"remaining_mode": options_source.get("remaining_mode", ServerOptions.remaining_mode),
|
||||
"collect_mode": options_source.get("collect_mode", ServerOptions.collect_mode),
|
||||
"item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))),
|
||||
"server_password": options_source.get("server_password", None),
|
||||
}
|
||||
generator_options = {
|
||||
"spoiler": int(options_source.get("spoiler", 0)),
|
||||
"race": race
|
||||
"spoiler": int(options_source.get("spoiler", GeneratorOptions.spoiler)),
|
||||
"race": race,
|
||||
}
|
||||
|
||||
if race:
|
||||
@@ -70,37 +68,41 @@ def generate(race=False):
|
||||
flash(options)
|
||||
else:
|
||||
meta = get_meta(request.form, race)
|
||||
results, gen_options = roll_options(options, set(meta["plando_options"]))
|
||||
|
||||
if any(type(result) == str for result in results.values()):
|
||||
return render_template("checkResult.html", results=results)
|
||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
|
||||
f"If you have a larger group, please generate it yourself and upload it.")
|
||||
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
|
||||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# convert to json compatible
|
||||
meta=json.dumps(meta),
|
||||
state=STATE_QUEUED,
|
||||
owner=session["_id"])
|
||||
commit()
|
||||
|
||||
return redirect(url_for("wait_seed", seed=gen.id))
|
||||
else:
|
||||
try:
|
||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||
meta=meta, owner=session["_id"].int)
|
||||
except BaseException as e:
|
||||
from .autolauncher import handle_generation_failure
|
||||
handle_generation_failure(e)
|
||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
||||
|
||||
return redirect(url_for("view_seed", seed=seed_id))
|
||||
return start_generation(options, meta)
|
||||
|
||||
return render_template("generate.html", race=race, version=__version__)
|
||||
|
||||
|
||||
def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any]):
|
||||
results, gen_options = roll_options(options, set(meta["plando_options"]))
|
||||
|
||||
if any(type(result) == str for result in results.values()):
|
||||
return render_template("checkResult.html", results=results)
|
||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
|
||||
f"If you have a larger group, please generate it yourself and upload it.")
|
||||
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
|
||||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# convert to json compatible
|
||||
meta=json.dumps(meta),
|
||||
state=STATE_QUEUED,
|
||||
owner=session["_id"])
|
||||
commit()
|
||||
|
||||
return redirect(url_for("wait_seed", seed=gen.id))
|
||||
else:
|
||||
try:
|
||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||
meta=meta, owner=session["_id"].int)
|
||||
except BaseException as e:
|
||||
from .autolauncher import handle_generation_failure
|
||||
handle_generation_failure(e)
|
||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
||||
|
||||
return redirect(url_for("view_seed", seed=seed_id))
|
||||
|
||||
|
||||
def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
|
||||
if not meta:
|
||||
meta: Dict[str, Any] = {}
|
||||
|
||||
@@ -1,83 +1,60 @@
|
||||
import collections.abc
|
||||
import os
|
||||
import yaml
|
||||
import requests
|
||||
import json
|
||||
import flask
|
||||
import os
|
||||
from textwrap import dedent
|
||||
from typing import Dict, Union
|
||||
|
||||
import yaml
|
||||
from flask import redirect, render_template, request, Response
|
||||
|
||||
import Options
|
||||
from Options import Visibility
|
||||
from flask import redirect, render_template, request, Response
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from Utils import local_path
|
||||
from textwrap import dedent
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from . import app, cache
|
||||
from .generate import get_meta
|
||||
|
||||
|
||||
def create():
|
||||
def create() -> None:
|
||||
target_folder = local_path("WebHostLib", "static", "generated")
|
||||
yaml_folder = os.path.join(target_folder, "configs")
|
||||
|
||||
Options.generate_yaml_templates(yaml_folder)
|
||||
|
||||
|
||||
def get_world_theme(game_name: str):
|
||||
def get_world_theme(game_name: str) -> 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):
|
||||
def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]:
|
||||
world = AutoWorldRegister.world_types[world_name]
|
||||
if world.hidden or world.web.options_page is False:
|
||||
return redirect("games")
|
||||
visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui
|
||||
|
||||
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
|
||||
start_collapsed = {"Game Options": False}
|
||||
for group in world.web.option_groups:
|
||||
start_collapsed[group.name] = group.start_collapsed
|
||||
|
||||
return render_template(
|
||||
template,
|
||||
world_name=world_name,
|
||||
world=world,
|
||||
option_groups=grouped_options,
|
||||
option_groups=Options.get_option_groups(world, visibility_level=visibility_flag),
|
||||
start_collapsed=start_collapsed,
|
||||
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,
|
||||
},
|
||||
}
|
||||
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
|
||||
def generate_game(options: Dict[str, Union[dict, str]]) -> Union[Response, str]:
|
||||
from .generate import start_generation
|
||||
return start_generation(options, get_meta({}))
|
||||
|
||||
|
||||
def send_yaml(player_name: str, formatted_options: dict):
|
||||
def send_yaml(player_name: str, formatted_options: dict) -> Response:
|
||||
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"
|
||||
@@ -85,7 +62,7 @@ def send_yaml(player_name: str, formatted_options: dict):
|
||||
|
||||
|
||||
@app.template_filter("dedent")
|
||||
def filter_dedent(text: str):
|
||||
def filter_dedent(text: str) -> str:
|
||||
return dedent(text).strip("\n ")
|
||||
|
||||
|
||||
@@ -98,10 +75,34 @@ def test_ordered(obj):
|
||||
@cache.cached()
|
||||
def option_presets(game: str) -> Response:
|
||||
world = AutoWorldRegister.world_types[game]
|
||||
presets = {}
|
||||
|
||||
if world.web.options_presets:
|
||||
presets = presets | world.web.options_presets
|
||||
presets = {}
|
||||
for preset_name, preset in world.web.options_presets.items():
|
||||
presets[preset_name] = {}
|
||||
for preset_option_name, preset_option in preset.items():
|
||||
if preset_option == "random":
|
||||
presets[preset_name][preset_option_name] = preset_option
|
||||
continue
|
||||
|
||||
option = world.options_dataclass.type_hints[preset_option_name].from_any(preset_option)
|
||||
if isinstance(option, Options.NamedRange) and isinstance(preset_option, str):
|
||||
assert preset_option in option.special_range_names, \
|
||||
f"Invalid preset value '{preset_option}' for '{preset_option_name}' in '{preset_name}'. " \
|
||||
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
|
||||
|
||||
presets[preset_name][preset_option_name] = option.value
|
||||
elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.ItemDict)):
|
||||
presets[preset_name][preset_option_name] = option.value
|
||||
elif isinstance(preset_option, str):
|
||||
# Ensure the option value is valid for Choice and Toggle options
|
||||
assert option.name_lookup[option.value] == preset_option, \
|
||||
f"Invalid option value '{preset_option}' for '{preset_option_name}' in preset '{preset_name}'. " \
|
||||
f"Values must not be resolved to a different option via option.from_text (or an alias)."
|
||||
# Use the name of the option
|
||||
presets[preset_name][preset_option_name] = option.current_key
|
||||
else:
|
||||
# Use the name of the option
|
||||
presets[preset_name][preset_option_name] = option.current_key
|
||||
|
||||
class SetEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
@@ -111,7 +112,7 @@ def option_presets(game: str) -> Response:
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
json_data = json.dumps(presets, cls=SetEncoder)
|
||||
response = flask.Response(json_data)
|
||||
response = Response(json_data)
|
||||
response.headers["Content-Type"] = "application/json"
|
||||
return response
|
||||
|
||||
@@ -169,7 +170,7 @@ def generate_weighted_yaml(game: str):
|
||||
}
|
||||
|
||||
if intent_generate:
|
||||
return generate_game(player_name, formatted_options)
|
||||
return generate_game({player_name: formatted_options})
|
||||
|
||||
else:
|
||||
return send_yaml(player_name, formatted_options)
|
||||
@@ -196,9 +197,9 @@ def generate_yaml(game: str):
|
||||
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)
|
||||
# Detect and build ItemDict options from their name pattern
|
||||
if key_parts[-1] == "qty":
|
||||
if key_parts[0] not in options:
|
||||
options[key_parts[0]] = {}
|
||||
@@ -206,6 +207,13 @@ def generate_yaml(game: str):
|
||||
options[key_parts[0]][key_parts[1]] = int(val)
|
||||
del options[key]
|
||||
|
||||
# Detect keys which end with -custom, indicating a TextChoice with a possible custom value
|
||||
elif key_parts[-1].endswith("-custom"):
|
||||
if val:
|
||||
options[key_parts[-1][:-7]] = val
|
||||
|
||||
del options[key]
|
||||
|
||||
# Detect random-* keys and set their options accordingly
|
||||
for key, val in options.copy().items():
|
||||
if key.startswith("random-"):
|
||||
@@ -243,7 +251,7 @@ def generate_yaml(game: str):
|
||||
}
|
||||
|
||||
if intent_generate:
|
||||
return generate_game(player_name, formatted_options)
|
||||
return generate_game({player_name: formatted_options})
|
||||
|
||||
else:
|
||||
return send_yaml(player_name, formatted_options)
|
||||
|
||||
@@ -27,7 +27,7 @@ const adjustTableHeight = () => {
|
||||
* @returns {string}
|
||||
*/
|
||||
const secondsToHours = (seconds) => {
|
||||
let hours = Math.floor(seconds / 3600);
|
||||
let hours = Math.floor(seconds / 3600);
|
||||
let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
};
|
||||
@@ -38,18 +38,18 @@ window.addEventListener('load', () => {
|
||||
info: false,
|
||||
dom: "t",
|
||||
stateSave: true,
|
||||
stateSaveCallback: function(settings, data) {
|
||||
stateSaveCallback: function (settings, data) {
|
||||
delete data.search;
|
||||
localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data));
|
||||
},
|
||||
stateLoadCallback: function(settings) {
|
||||
stateLoadCallback: function (settings) {
|
||||
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
|
||||
},
|
||||
footerCallback: function(tfoot, data, start, end, display) {
|
||||
footerCallback: function (tfoot, data, start, end, display) {
|
||||
if (tfoot) {
|
||||
const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x));
|
||||
Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText =
|
||||
(activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None';
|
||||
(activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None';
|
||||
}
|
||||
},
|
||||
columnDefs: [
|
||||
@@ -123,49 +123,64 @@ window.addEventListener('load', () => {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
|
||||
const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3;
|
||||
const target_second = parseInt(document.getElementById('tracker-wrapper').getAttribute('data-second')) + 3;
|
||||
console.log("Target second of refresh: " + target_second);
|
||||
|
||||
function getSleepTimeSeconds(){
|
||||
function getSleepTimeSeconds() {
|
||||
// -40 % 60 is -40, which is absolutely wrong and should burn
|
||||
var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60;
|
||||
return sleepSeconds || 60;
|
||||
}
|
||||
|
||||
let update_on_view = false;
|
||||
const update = () => {
|
||||
const target = $("<div></div>");
|
||||
console.log("Updating Tracker...");
|
||||
target.load(location.href, function (response, status) {
|
||||
if (status === "success") {
|
||||
target.find(".table").each(function (i, new_table) {
|
||||
const new_trs = $(new_table).find("tbody>tr");
|
||||
const footer_tr = $(new_table).find("tfoot>tr");
|
||||
const old_table = tables.eq(i);
|
||||
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
|
||||
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
|
||||
old_table.clear();
|
||||
if (footer_tr.length) {
|
||||
$(old_table.table).find("tfoot").html(footer_tr);
|
||||
}
|
||||
old_table.rows.add(new_trs);
|
||||
old_table.draw();
|
||||
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
|
||||
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
|
||||
});
|
||||
$("#multi-stream-link").replaceWith(target.find("#multi-stream-link"));
|
||||
} else {
|
||||
console.log("Failed to connect to Server, in order to update Table Data.");
|
||||
console.log(response);
|
||||
}
|
||||
})
|
||||
setTimeout(update, getSleepTimeSeconds()*1000);
|
||||
if (document.hidden) {
|
||||
console.log("Document reporting as not visible, not updating Tracker...");
|
||||
update_on_view = true;
|
||||
} else {
|
||||
update_on_view = false;
|
||||
const target = $("<div></div>");
|
||||
console.log("Updating Tracker...");
|
||||
target.load(location.href, function (response, status) {
|
||||
if (status === "success") {
|
||||
target.find(".table").each(function (i, new_table) {
|
||||
const new_trs = $(new_table).find("tbody>tr");
|
||||
const footer_tr = $(new_table).find("tfoot>tr");
|
||||
const old_table = tables.eq(i);
|
||||
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
|
||||
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
|
||||
old_table.clear();
|
||||
if (footer_tr.length) {
|
||||
$(old_table.table).find("tfoot").html(footer_tr);
|
||||
}
|
||||
old_table.rows.add(new_trs);
|
||||
old_table.draw();
|
||||
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
|
||||
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
|
||||
});
|
||||
$("#multi-stream-link").replaceWith(target.find("#multi-stream-link"));
|
||||
} else {
|
||||
console.log("Failed to connect to Server, in order to update Table Data.");
|
||||
console.log(response);
|
||||
}
|
||||
})
|
||||
}
|
||||
updater = setTimeout(update, getSleepTimeSeconds() * 1000);
|
||||
}
|
||||
setTimeout(update, getSleepTimeSeconds()*1000);
|
||||
let updater = setTimeout(update, getSleepTimeSeconds() * 1000);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
adjustTableHeight();
|
||||
tables.draw();
|
||||
});
|
||||
|
||||
window.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden && update_on_view) {
|
||||
console.log("Page became visible, tracker should be refreshed.");
|
||||
clearTimeout(updater);
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
||||
adjustTableHeight();
|
||||
});
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if room.tracker %}
|
||||
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
|
||||
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a>
|
||||
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
|
||||
<br />
|
||||
{% endif %}
|
||||
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
|
||||
|
||||
72
WebHostLib/templates/multispheretracker.html
Normal file
72
WebHostLib/templates/multispheretracker.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends "tablepage.html" %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Multiworld Sphere Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="styles/tracker.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include "header/dirtHeader.html" %}
|
||||
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker | suuid }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search" />
|
||||
|
||||
<div class="info">
|
||||
{% if tracker_data.get_spheres() %}
|
||||
This tracker lists already found locations by their logical access sphere.
|
||||
It ignores items that cannot be sent
|
||||
and will therefore differ from the sphere numbers in the spoiler playthrough.
|
||||
This tracker will automatically update itself periodically.
|
||||
{% else %}
|
||||
This Multiworld has no Sphere data, likely due to being too old, cannot display data.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tables-container">
|
||||
{%- for team, players in tracker_data.get_all_players().items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="checks-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sphere</th>
|
||||
{#- Mimicking hint table header for familiarity. #}
|
||||
<th>Finder</th>
|
||||
<th>Receiver</th>
|
||||
<th>Item</th>
|
||||
<th>Location</th>
|
||||
<th>Game</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for sphere in tracker_data.get_spheres() %}
|
||||
{%- set current_sphere = loop.index %}
|
||||
{%- for player, sphere_location_ids in sphere.items() %}
|
||||
{%- set checked_locations = tracker_data.get_player_checked_locations(team, player) %}
|
||||
{%- set finder_game = tracker_data.get_player_game(team, player) %}
|
||||
{%- set player_location_data = tracker_data.get_player_locations(team, player) %}
|
||||
{%- for location_id in sphere_location_ids.intersection(checked_locations) %}
|
||||
<tr>
|
||||
{%- set item_id, receiver, item_flags = player_location_data[location_id] %}
|
||||
{%- set receiver_game = tracker_data.get_player_game(team, receiver) %}
|
||||
<td>{{ current_sphere }}</td>
|
||||
<td>{{ tracker_data.get_player_name(team, player) }}</td>
|
||||
<td>{{ tracker_data.get_player_name(team, receiver) }}</td>
|
||||
<td>{{ tracker_data.item_id_to_name[receiver_game][item_id] }}</td>
|
||||
<td>{{ tracker_data.location_id_to_name[finder_game][location_id] }}</td>
|
||||
<td>{{ finder_game }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
|
||||
{%- endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -10,7 +10,7 @@
|
||||
{% include "header/dirtHeader.html" %}
|
||||
{% include "multitrackerNavigation.html" %}
|
||||
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker | suuid }}">
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker | suuid }}" data-second="{{ saving_second }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search" />
|
||||
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/ootTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/ootTracker.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ ocarina_url }}" class="{{ 'acquired' if 'Ocarina' in acquired_items }}" title="Ocarina" /></td>
|
||||
<td><img src="{{ icons['Bombs'] }}" class="{{ 'acquired' if 'Bomb Bag' in acquired_items }}" title="Bombs" /></td>
|
||||
<td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Bow' in acquired_items }}" title="Fairy Bow" /></td>
|
||||
<td><img src="{{ icons['Fire Arrows'] }}" class="{{ 'acquired' if 'Fire Arrows' in acquired_items }}" title="Fire Arrows" /></td>
|
||||
<td><img src="{{ icons['Kokiri Sword'] }}" class="{{ 'acquired' if 'Kokiri Sword' in acquired_items }}" title="Kokiri Sword" /></td>
|
||||
<td><img src="{{ icons['Biggoron Sword'] }}" class="{{ 'acquired' if 'Biggoron Sword' in acquired_items }}" title="Biggoron's Sword" /></td>
|
||||
<td><img src="{{ icons['Mirror Shield'] }}" class="{{ 'acquired' if 'Mirror Shield' in acquired_items }}" title="Mirror Shield" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Slingshot'] }}" class="{{ 'acquired' if 'Slingshot' in acquired_items }}" title="Slingshot" /></td>
|
||||
<td><img src="{{ icons['Bombchus'] }}" class="{{ 'acquired' if has_bombchus }}" title="Bombchus" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ hookshot_url }}" class="{{ 'acquired' if 'Progressive Hookshot' in acquired_items }}" title="Progressive Hookshot" />
|
||||
<div class="item-count">{{ hookshot_length }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ icons['Ice Arrows'] }}" class="{{ 'acquired' if 'Ice Arrows' in acquired_items }}" title="Ice Arrows" /></td>
|
||||
<td><img src="{{ strength_upgrade_url }}" class="{{ 'acquired' if 'Progressive Strength Upgrade' in acquired_items }}" title="Progressive Strength Upgrade" /></td>
|
||||
<td><img src="{{ icons['Goron Tunic'] }}" class="{{ 'acquired' if 'Goron Tunic' in acquired_items }}" title="Goron Tunic" /></td>
|
||||
<td><img src="{{ icons['Zora Tunic'] }}" class="{{ 'acquired' if 'Zora Tunic' in acquired_items }}" title="Zora Tunic" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Boomerang'] }}" class="{{ 'acquired' if 'Boomerang' in acquired_items }}" title="Boomerang" /></td>
|
||||
<td><img src="{{ icons['Lens of Truth'] }}" class="{{ 'acquired' if 'Lens of Truth' in acquired_items }}" title="Lens of Truth" /></td>
|
||||
<td><img src="{{ icons['Megaton Hammer'] }}" class="{{ 'acquired' if 'Megaton Hammer' in acquired_items }}" title="Megaton Hammer" /></td>
|
||||
<td><img src="{{ icons['Light Arrows'] }}" class="{{ 'acquired' if 'Light Arrows' in acquired_items }}" title="Light Arrows" /></td>
|
||||
<td><img src="{{ scale_url }}" class="{{ 'acquired' if 'Progressive Scale' in acquired_items }}" title="Progressive Scale" /></td>
|
||||
<td><img src="{{ icons['Iron Boots'] }}" class="{{ 'acquired' if 'Iron Boots' in acquired_items }}" title="Iron Boots" /></td>
|
||||
<td><img src="{{ icons['Hover Boots'] }}" class="{{ 'acquired' if 'Hover Boots' in acquired_items }}" title="Hover Boots" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ bottle_url }}" class="{{ 'acquired' if bottle_count > 0 }}" title="Bottles" />
|
||||
<div class="item-count">{{ bottle_count if bottle_count > 0 else '' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ icons['Dins Fire'] }}" class="{{ 'acquired' if 'Dins Fire' in acquired_items }}" title="Din's Fire" /></td>
|
||||
<td><img src="{{ icons['Farores Wind'] }}" class="{{ 'acquired' if 'Farores Wind' in acquired_items }}" title="Farore's Wind" /></td>
|
||||
<td><img src="{{ icons['Nayrus Love'] }}" class="{{ 'acquired' if 'Nayrus Love' in acquired_items }}" title="Nayru's Love" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ wallet_url }}" class="{{ 'acquired' if 'Progressive Wallet' in acquired_items }}" title="Progressive Wallet" />
|
||||
<div class="item-count">{{ wallet_size }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ magic_meter_url }}" class="{{ 'acquired' if 'Magic Meter' in acquired_items }}" title="Magic Meter" /></td>
|
||||
<td><img src="{{ icons['Gerudo Membership Card'] }}" class="{{ 'acquired' if 'Gerudo Membership Card' in acquired_items }}" title="Gerudo Membership Card" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Zeldas Lullaby'] }}" class="{{ 'acquired' if 'Zeldas Lullaby' in acquired_items }}" title="Zelda's Lullaby" id="lullaby"/>
|
||||
<div class="item-count">Zelda</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Eponas Song'] }}" class="{{ 'acquired' if 'Eponas Song' in acquired_items }}" title="Epona's Song" id="epona" />
|
||||
<div class="item-count">Epona</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Sarias Song'] }}" class="{{ 'acquired' if 'Sarias Song' in acquired_items }}" title="Saria's Song" id="saria"/>
|
||||
<div class="item-count">Saria</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Suns Song'] }}" class="{{ 'acquired' if 'Suns Song' in acquired_items }}" title="Sun's Song" id="sun"/>
|
||||
<div class="item-count">Sun</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Song of Time'] }}" class="{{ 'acquired' if 'Song of Time' in acquired_items }}" title="Song of Time" id="time"/>
|
||||
<div class="item-count">Time</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Song of Storms'] }}" class="{{ 'acquired' if 'Song of Storms' in acquired_items }}" title="Song of Storms" />
|
||||
<div class="item-count">Storms</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Gold Skulltula Token'] }}" class="{{ 'acquired' if token_count > 0 }}" title="Gold Skulltula Tokens" />
|
||||
<div class="item-count">{{ token_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Minuet of Forest'] }}" class="{{ 'acquired' if 'Minuet of Forest' in acquired_items }}" title="Minuet of Forest" />
|
||||
<div class="item-count">Min</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Bolero of Fire'] }}" class="{{ 'acquired' if 'Bolero of Fire' in acquired_items }}" title="Bolero of Fire" />
|
||||
<div class="item-count">Bol</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Serenade of Water'] }}" class="{{ 'acquired' if 'Serenade of Water' in acquired_items }}" title="Serenade of Water" />
|
||||
<div class="item-count">Ser</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Requiem of Spirit'] }}" class="{{ 'acquired' if 'Requiem of Spirit' in acquired_items }}" title="Requiem of Spirit" />
|
||||
<div class="item-count">Req</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Nocturne of Shadow'] }}" class="{{ 'acquired' if 'Nocturne of Shadow' in acquired_items }}" title="Nocturne of Shadow" />
|
||||
<div class="item-count">Noc</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Prelude of Light'] }}" class="{{ 'acquired' if 'Prelude of Light' in acquired_items }}" title="Prelude of Light" />
|
||||
<div class="item-count">Pre</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Triforce'] if game_finished else icons['Triforce Piece'] }}" class="{{ 'acquired' if game_finished or piece_count > 0 }}" title="{{ 'Triforce' if game_finished else 'Triforce Pieces' }}" id=triforce />
|
||||
<div class="item-count">{{ piece_count if piece_count > 0 else '' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Small Key'] }}" title="Small Keys" /></td>
|
||||
<td><img src="{{ icons['Boss Key'] }}" title="Boss Key" /></td>
|
||||
<td class="right-align">Items</td>
|
||||
</tr>
|
||||
{% for area in checks_done %}
|
||||
<tr class="location-category" id="{{area}}-header">
|
||||
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||
<td class="smallkeys">{{ small_key_counts.get(area, '-') }}</td>
|
||||
<td class="bosskeys">{{ boss_key_counts.get(area, '-') }}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hide" id="{{area}}">
|
||||
{% for location in location_info[area] %}
|
||||
<tr>
|
||||
<td class="location-name">{{ location }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -114,7 +114,7 @@
|
||||
{% macro ItemDict(option_name, option, world) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="option-container">
|
||||
{% for item_name in world.item_names|sort %}
|
||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else 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 }}" />
|
||||
@@ -141,7 +141,7 @@
|
||||
{% 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 }} />
|
||||
<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 %}
|
||||
@@ -149,7 +149,7 @@
|
||||
{% if world.location_name_groups.keys()|length > 1 %}
|
||||
<div class="option-divider"> </div>
|
||||
{% endif %}
|
||||
{% for location_name in world.location_names|sort %}
|
||||
{% for location_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else 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>
|
||||
@@ -172,7 +172,7 @@
|
||||
{% if world.item_name_groups.keys()|length > 1 %}
|
||||
<div class="option-divider"> </div>
|
||||
{% endif %}
|
||||
{% for item_name in world.item_names|sort %}
|
||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else 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>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
<div id="option-groups">
|
||||
{% for group_name, group_options in option_groups.items() %}
|
||||
<details class="group-container" {% if loop.index == 1 %}open{% endif %}>
|
||||
<details class="group-container" {% if not start_collapsed[group_name] %}open{% endif %}>
|
||||
<summary class="h2">{{ group_name }}</summary>
|
||||
<div class="game-options">
|
||||
<div class="left">
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
|
||||
{% macro ItemDict(option_name, option, world) %}
|
||||
<div class="dict-container">
|
||||
{% for item_name in world.item_names|sort %}
|
||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
||||
<div class="dict-entry">
|
||||
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
||||
<input
|
||||
@@ -142,7 +142,7 @@
|
||||
{% 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 }} />
|
||||
<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 %}
|
||||
@@ -150,7 +150,7 @@
|
||||
{% if world.location_name_groups.keys()|length > 1 %}
|
||||
<div class="divider"> </div>
|
||||
{% endif %}
|
||||
{% for location_name in world.location_names|sort %}
|
||||
{% for location_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else 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>
|
||||
@@ -172,7 +172,7 @@
|
||||
{% if world.item_name_groups.keys()|length > 1 %}
|
||||
<div class="set-divider"> </div>
|
||||
{% endif %}
|
||||
{% for item_name in world.item_names|sort %}
|
||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else 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>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
<div id="{{ world_name }}-container">
|
||||
{% for group_name, group_options in option_groups.items() %}
|
||||
<details {% if loop.index == 1 %}open{% endif %}>
|
||||
<details {% if not start_collapsed[group_name] %}open{% endif %}>
|
||||
<summary class="h2">{{ group_name }}</summary>
|
||||
{% for option_name, option in group_options.items() %}
|
||||
<div class="option-wrapper">
|
||||
|
||||
@@ -3,8 +3,9 @@ import collections
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, NamedTuple, Counter
|
||||
from uuid import UUID
|
||||
from email.utils import parsedate_to_datetime
|
||||
|
||||
from flask import render_template
|
||||
from flask import render_template, make_response, Response, request
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from MultiServer import Context, get_saving_second
|
||||
@@ -291,47 +292,47 @@ class TrackerData:
|
||||
|
||||
return video_feeds
|
||||
|
||||
@_cache_results
|
||||
def get_spheres(self) -> List[List[int]]:
|
||||
""" each sphere is { player: { location_id, ... } } """
|
||||
return self._multidata.get("spheres", [])
|
||||
|
||||
|
||||
def _process_if_request_valid(incoming_request, room: Optional[Room]) -> Optional[Response]:
|
||||
if not room:
|
||||
abort(404)
|
||||
|
||||
if_modified = incoming_request.headers.get("If-Modified-Since", None)
|
||||
if if_modified:
|
||||
if_modified = parsedate_to_datetime(if_modified)
|
||||
# if_modified has less precision than last_activity, so we bring them to same precision
|
||||
if if_modified >= room.last_activity.replace(microsecond=0):
|
||||
return make_response("", 304)
|
||||
|
||||
|
||||
@app.route("/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>")
|
||||
def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> str:
|
||||
def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response:
|
||||
key = f"{tracker}_{tracked_team}_{tracked_player}_{generic}"
|
||||
tracker_page = cache.get(key)
|
||||
if tracker_page:
|
||||
return tracker_page
|
||||
response: Optional[Response] = cache.get(key)
|
||||
if response:
|
||||
return response
|
||||
|
||||
timeout, tracker_page = get_timeout_and_tracker(tracker, tracked_team, tracked_player, generic)
|
||||
cache.set(key, tracker_page, timeout)
|
||||
return tracker_page
|
||||
|
||||
|
||||
@app.route("/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>")
|
||||
def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> str:
|
||||
return get_player_tracker(tracker, tracked_team, tracked_player, True)
|
||||
|
||||
|
||||
@app.route("/tracker/<suuid:tracker>", defaults={"game": "Generic"})
|
||||
@app.route("/tracker/<suuid:tracker>/<game>")
|
||||
@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS)
|
||||
def get_multiworld_tracker(tracker: UUID, game: str):
|
||||
# Room must exist.
|
||||
room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
abort(404)
|
||||
|
||||
tracker_data = TrackerData(room)
|
||||
enabled_trackers = list(get_enabled_multiworld_trackers(room).keys())
|
||||
if game not in _multiworld_trackers:
|
||||
return render_generic_multiworld_tracker(tracker_data, enabled_trackers)
|
||||
response = _process_if_request_valid(request, room)
|
||||
if response:
|
||||
return response
|
||||
|
||||
return _multiworld_trackers[game](tracker_data, enabled_trackers)
|
||||
timeout, last_modified, tracker_page = get_timeout_and_player_tracker(room, tracked_team, tracked_player, generic)
|
||||
response = make_response(tracker_page)
|
||||
response.last_modified = last_modified
|
||||
cache.set(key, response, timeout)
|
||||
return response
|
||||
|
||||
|
||||
def get_timeout_and_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool) -> Tuple[int, str]:
|
||||
# Room must exist.
|
||||
room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
abort(404)
|
||||
|
||||
def get_timeout_and_player_tracker(room: Room, tracked_team: int, tracked_player: int, generic: bool)\
|
||||
-> Tuple[int, datetime.datetime, str]:
|
||||
tracker_data = TrackerData(room)
|
||||
|
||||
# Load and render the game-specific player tracker, or fallback to generic tracker if none exists.
|
||||
@@ -341,7 +342,48 @@ def get_timeout_and_tracker(tracker: UUID, tracked_team: int, tracked_player: in
|
||||
else:
|
||||
tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player)
|
||||
|
||||
return (tracker_data.get_room_saving_second() - datetime.datetime.now().second) % 60 or 60, tracker
|
||||
return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second)
|
||||
% TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker)
|
||||
|
||||
|
||||
@app.route("/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>")
|
||||
def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> Response:
|
||||
return get_player_tracker(tracker, tracked_team, tracked_player, True)
|
||||
|
||||
|
||||
@app.route("/tracker/<suuid:tracker>", defaults={"game": "Generic"})
|
||||
@app.route("/tracker/<suuid:tracker>/<game>")
|
||||
def get_multiworld_tracker(tracker: UUID, game: str) -> Response:
|
||||
key = f"{tracker}_{game}"
|
||||
response: Optional[Response] = cache.get(key)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# Room must exist.
|
||||
room = Room.get(tracker=tracker)
|
||||
|
||||
response = _process_if_request_valid(request, room)
|
||||
if response:
|
||||
return response
|
||||
|
||||
timeout, last_modified, tracker_page = get_timeout_and_multiworld_tracker(room, game)
|
||||
response = make_response(tracker_page)
|
||||
response.last_modified = last_modified
|
||||
cache.set(key, response, timeout)
|
||||
return response
|
||||
|
||||
|
||||
def get_timeout_and_multiworld_tracker(room: Room, game: str)\
|
||||
-> Tuple[int, datetime.datetime, str]:
|
||||
tracker_data = TrackerData(room)
|
||||
enabled_trackers = list(get_enabled_multiworld_trackers(room).keys())
|
||||
if game in _multiworld_trackers:
|
||||
tracker = _multiworld_trackers[game](tracker_data, enabled_trackers)
|
||||
else:
|
||||
tracker = render_generic_multiworld_tracker(tracker_data, enabled_trackers)
|
||||
|
||||
return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second)
|
||||
% TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker)
|
||||
|
||||
|
||||
def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]:
|
||||
@@ -411,9 +453,30 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
|
||||
videos=tracker_data.get_room_videos(),
|
||||
item_id_to_name=tracker_data.item_id_to_name,
|
||||
location_id_to_name=tracker_data.location_id_to_name,
|
||||
saving_second=tracker_data.get_room_saving_second(),
|
||||
)
|
||||
|
||||
|
||||
def render_generic_multiworld_sphere_tracker(tracker_data: TrackerData) -> str:
|
||||
return render_template(
|
||||
"multispheretracker.html",
|
||||
room=tracker_data.room,
|
||||
tracker_data=tracker_data,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/sphere_tracker/<suuid:tracker>")
|
||||
@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS)
|
||||
def get_multiworld_sphere_tracker(tracker: UUID):
|
||||
# Room must exist.
|
||||
room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
abort(404)
|
||||
|
||||
tracker_data = TrackerData(room)
|
||||
return render_generic_multiworld_sphere_tracker(tracker_data)
|
||||
|
||||
|
||||
# TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to
|
||||
# live in their respective world folders.
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ def get_payload(ctx: ZeldaContext):
|
||||
|
||||
|
||||
def reconcile_shops(ctx: ZeldaContext):
|
||||
checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations]
|
||||
checked_location_names = [ctx.location_names.lookup_in_slot(location) for location in ctx.checked_locations]
|
||||
shops = [location for location in checked_location_names if "Shop" in location]
|
||||
left_slots = [shop for shop in shops if "Left" in shop]
|
||||
middle_slots = [shop for shop in shops if "Middle" in shop]
|
||||
@@ -190,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone=
|
||||
locations_checked = []
|
||||
location = None
|
||||
for location in ctx.missing_locations:
|
||||
location_name = ctx.location_names[location]
|
||||
location_name = ctx.location_names.lookup_in_slot(location)
|
||||
|
||||
if location_name in Locations.overworld_locations and zone == "overworld":
|
||||
status = locations_array[Locations.major_location_offsets[location_name]]
|
||||
|
||||
BIN
data/yatta.ico
Normal file
BIN
data/yatta.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
BIN
data/yatta.png
Normal file
BIN
data/yatta.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -6,25 +6,24 @@
|
||||
#
|
||||
# All usernames must be GitHub usernames (and are case sensitive).
|
||||
|
||||
###################
|
||||
## Active Worlds ##
|
||||
###################
|
||||
|
||||
# Adventure
|
||||
/worlds/adventure/ @JusticePS
|
||||
|
||||
# A Hat in Time
|
||||
/worlds/ahit/ @CookieCat45
|
||||
|
||||
# A Link to the Past
|
||||
/worlds/alttp/ @Berserker66
|
||||
|
||||
# Sudoku (APSudoku)
|
||||
/worlds/apsudoku/ @EmilyV99
|
||||
|
||||
# Aquaria
|
||||
/worlds/aquaria/ @tioui
|
||||
|
||||
# ArchipIDLE
|
||||
/worlds/archipidle/ @LegendaryLinux
|
||||
|
||||
# Sudoku (BK Sudoku)
|
||||
/worlds/bk_sudoku/ @Jarno458
|
||||
|
||||
# Blasphemous
|
||||
/worlds/blasphemous/ @TRPG0
|
||||
|
||||
@@ -64,9 +63,6 @@
|
||||
# Factorio
|
||||
/worlds/factorio/ @Berserker66
|
||||
|
||||
# Final Fantasy
|
||||
/worlds/ff1/ @jtoyoda
|
||||
|
||||
# Final Fantasy Mystic Quest
|
||||
/worlds/ffmq/ @Alchav @wildham0
|
||||
|
||||
@@ -204,7 +200,7 @@
|
||||
/worlds/yoshisisland/ @PinkSwitch
|
||||
|
||||
#Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
||||
/worlds/yugioh06/ @rensen
|
||||
/worlds/yugioh06/ @Rensen3
|
||||
|
||||
# Zillion
|
||||
/worlds/zillion/ @beauxq
|
||||
@@ -212,9 +208,22 @@
|
||||
# Zork Grand Inquisitor
|
||||
/worlds/zork_grand_inquisitor/ @nbrochu
|
||||
|
||||
##################################
|
||||
## Disabled Unmaintained Worlds ##
|
||||
##################################
|
||||
|
||||
## Active Unmaintained Worlds
|
||||
|
||||
# The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks
|
||||
# compatibility, these worlds may be moved to `worlds_disabled`. If you are interested in stepping up as maintainer for
|
||||
# any of these worlds, please review `/docs/world maintainer.md` documentation.
|
||||
|
||||
# Final Fantasy (1)
|
||||
# /worlds/ff1/
|
||||
|
||||
|
||||
## Disabled Unmaintained Worlds
|
||||
|
||||
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
||||
# interested in stepping up as maintainer for any of these worlds, please review `/docs/world maintainer.md`
|
||||
# documentation.
|
||||
|
||||
# Ori and the Blind Forest
|
||||
# /worlds_disabled/oribf/ <Unmaintained>
|
||||
# /worlds_disabled/oribf/
|
||||
|
||||
@@ -1,43 +1,49 @@
|
||||
# Contributing
|
||||
Contributions are welcome. We have a few requests for new contributors:
|
||||
|
||||
All contributions are welcome, though we have a few requests of contributors, whether they be for core, webhost, or new
|
||||
game contributions:
|
||||
|
||||
* **Follow styling guidelines.**
|
||||
Please take a look at the [code style documentation](/docs/style.md)
|
||||
to ensure ease of communication and uniformity.
|
||||
|
||||
* **Ensure that critical changes are covered by tests.**
|
||||
It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working.
|
||||
If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/tests.md).
|
||||
If you wish to contribute to the website, please take a look at [these tests](/test/webhost).
|
||||
* **Ensure that critical changes are covered by tests.**
|
||||
It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working.
|
||||
If you wish to contribute by adding a new game, please take a look at
|
||||
the [logic unit test documentation](/docs/tests.md).
|
||||
If you wish to contribute to the website, please take a look at [these tests](/test/webhost).
|
||||
|
||||
* **Do not introduce unit test failures/regressions.**
|
||||
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
|
||||
your changes. Currently, the oldest supported version is [Python 3.8](https://www.python.org/downloads/release/python-380/).
|
||||
It is recommended that automated github actions are turned on in your fork to have github run all of the unit tests after pushing.
|
||||
You can turn them on here:
|
||||

|
||||
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
|
||||
your changes. Currently, the oldest supported version
|
||||
is [Python 3.8](https://www.python.org/downloads/release/python-380/).
|
||||
It is recommended that automated github actions are turned on in your fork to have github run unit tests after
|
||||
pushing.
|
||||
You can turn them on here:
|
||||

|
||||
|
||||
* **When reviewing PRs, please leave a message about what was done.**
|
||||
We don't have full test coverage, so manual testing can help.
|
||||
For code changes that could affect multiple worlds or that could have changes in unexpected code paths, manual testing
|
||||
or checking if all code paths are covered by automated tests is desired. The original author may not have been able
|
||||
to test all possibly affected worlds, or didn't know it would affect another world. In such cases, it is helpful to
|
||||
state which games or settings were rolled, if any.
|
||||
Please also tell us if you looked at code, just did functional testing, did both, or did neither.
|
||||
If testing the PR depends on other PRs, please state what you merged into what for testing.
|
||||
We cannot determine what "LGTM" means without additional context, so that should not be the norm.
|
||||
We don't have full test coverage, so manual testing can help.
|
||||
For code changes that could affect multiple worlds or that could have changes in unexpected code paths, manual testing
|
||||
or checking if all code paths are covered by automated tests is desired. The original author may not have been able
|
||||
to test all possibly affected worlds, or didn't know it would affect another world. In such cases, it is helpful to
|
||||
state which games or settings were rolled, if any.
|
||||
Please also tell us if you looked at code, just did functional testing, did both, or did neither.
|
||||
If testing the PR depends on other PRs, please state what you merged into what for testing.
|
||||
We cannot determine what "LGTM" means without additional context, so that should not be the norm.
|
||||
|
||||
Other than these requests, we tend to judge code on a case-by-case basis.
|
||||
Other than these requests, we tend to judge code on a case-by-case basis.
|
||||
|
||||
For contribution to the website, please refer to the [WebHost README](/WebHostLib/README.md).
|
||||
|
||||
If you want to contribute to the core, you will be subject to stricter review on your pull requests. It is recommended
|
||||
that you get in touch with other core maintainers via the [Discord](https://archipelago.gg/discord).
|
||||
|
||||
If you want to add Archipelago support for a new game, please take a look at the [adding games documentation](/docs/adding%20games.md), which details what is required
|
||||
to implement support for a game, as well as tips for how to get started.
|
||||
If you want to merge a new game into the main Archipelago repo, please make sure to read the responsibilities as a
|
||||
[world maintainer](/docs/world%20maintainer.md).
|
||||
If you want to add Archipelago support for a new game, please take a look at
|
||||
the [adding games documentation](/docs/adding%20games.md)
|
||||
which details what is required to implement support for a game, and has tips on to get started.
|
||||
If you want to merge a new game into the main Archipelago repo, please make sure to read the responsibilities as a
|
||||
[world maintainer](/docs/world%20maintainer.md).
|
||||
|
||||
For other questions, feel free to explore the [main documentation folder](/docs/) and ask us questions in the #archipelago-dev channel
|
||||
of the [Discord](https://archipelago.gg/discord).
|
||||
For other questions, feel free to explore the [main documentation folder](/docs), and ask us questions in the
|
||||
#ap-world-dev channel of the [Discord](https://archipelago.gg/discord).
|
||||
|
||||
@@ -53,7 +53,7 @@ Example:
|
||||
```
|
||||
|
||||
## (Server -> Client)
|
||||
These packets are are sent from the multiworld server to the client. They are not messages which the server accepts.
|
||||
These packets are sent from the multiworld server to the client. They are not messages which the server accepts.
|
||||
* [RoomInfo](#RoomInfo)
|
||||
* [ConnectionRefused](#ConnectionRefused)
|
||||
* [Connected](#Connected)
|
||||
@@ -80,7 +80,6 @@ Sent to clients when they connect to an Archipelago server.
|
||||
| hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. |
|
||||
| location_check_points | int | The amount of hint points you receive per item/location check completed. |
|
||||
| games | list\[str\] | List of games present in this multiworld. |
|
||||
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). **Deprecated. Use `datapackage_checksums` instead.** |
|
||||
| datapackage_checksums | dict[str, str] | Checksum hash of the individual games' data packages the server will send. Used by newer clients to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents) for more information. |
|
||||
| seed_name | str | Uniquely identifying name of this generation |
|
||||
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
|
||||
@@ -500,9 +499,9 @@ In JSON this may look like:
|
||||
{"item": 3, "location": 3, "player": 3, "flags": 0}
|
||||
]
|
||||
```
|
||||
`item` is the item id of the item. Item ids are in the range of ± 2<sup>53</sup>-1.
|
||||
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
|
||||
|
||||
`location` is the location id of the item inside the world. Location ids are in the range of ± 2<sup>53</sup>-1.
|
||||
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
|
||||
|
||||
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item
|
||||
|
||||
@@ -646,15 +645,47 @@ class Hint(typing.NamedTuple):
|
||||
```
|
||||
|
||||
### Data Package Contents
|
||||
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings.
|
||||
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago
|
||||
server most easily and not maintain their own mappings. Some contents include:
|
||||
|
||||
We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different version. A special case is datapackage version 0, where it is expected the package is custom and should not be cached.
|
||||
- Name to ID mappings for items and locations.
|
||||
- A checksum of each game's data package for clients to tell if a cached package is invalid.
|
||||
|
||||
Note:
|
||||
* Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once.
|
||||
* Any Name is unique to its type across its own Game only: Single Arrow can exist in two games.
|
||||
* The IDs from the game "Archipelago" may be used in any other game.
|
||||
Especially Location ID -1: Cheat Console and -2: Server (typically Remote Start Inventory)
|
||||
We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know
|
||||
when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different checksum
|
||||
than any locally cached ones.
|
||||
|
||||
**Important Notes about IDs and Names**:
|
||||
|
||||
* IDs ≤ 0 are reserved for "Archipelago" and should not be used by other world implementations.
|
||||
* The IDs from the game "Archipelago" (in `worlds/generic`) may be used in any world.
|
||||
* Especially Location ID `-1`: `Cheat Console` and `-2`: `Server` (typically Remote Start Inventory)
|
||||
* Any names and IDs are only unique in its own world data package, but different games may reuse these names or IDs.
|
||||
* At runtime, you will need to look up the game of the player to know which item or location ID/Name to lookup in the
|
||||
data package. This can be easily achieved by reviewing the `slot_info` for a particular player ID prior to lookup.
|
||||
* For example, a data package like this is valid (Some properties such as `checksum` were omitted):
|
||||
```json
|
||||
{
|
||||
"games": {
|
||||
"Game A": {
|
||||
"location_name_to_id": {
|
||||
"Boss Chest": 40
|
||||
},
|
||||
"item_name_to_id": {
|
||||
"Item X": 12
|
||||
}
|
||||
},
|
||||
"Game B": {
|
||||
"location_name_to_id": {
|
||||
"Minigame Prize": 40
|
||||
},
|
||||
"item_name_to_id": {
|
||||
"Item X": 40
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Contents
|
||||
| Name | Type | Notes |
|
||||
@@ -668,7 +699,6 @@ GameData is a **dict** but contains these keys and values. It's broken out into
|
||||
|---------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. |
|
||||
| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. |
|
||||
| version | int | Version number of this game's data. Deprecated. Used by older clients to request an updated datapackage if cache is outdated. |
|
||||
| checksum | str | A checksum hash of this game's data. |
|
||||
|
||||
### Tags
|
||||
|
||||
@@ -121,6 +121,53 @@ class RLWeb(WebWorld):
|
||||
# ...
|
||||
```
|
||||
|
||||
* `location_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of locations
|
||||
or location groups.
|
||||
|
||||
```python
|
||||
# locations.py
|
||||
location_descriptions = {
|
||||
"Red Potion #6": "In a secret destructible block under the second stairway",
|
||||
"L2 Spaceship": """
|
||||
The group of all items in the spaceship in Level 2.
|
||||
|
||||
This doesn't include the item on the spaceship door, since it can be
|
||||
accessed without the Spaceship Key.
|
||||
"""
|
||||
}
|
||||
|
||||
# __init__.py
|
||||
from worlds.AutoWorld import WebWorld
|
||||
from .locations import location_descriptions
|
||||
|
||||
|
||||
class MyGameWeb(WebWorld):
|
||||
location_descriptions = location_descriptions
|
||||
```
|
||||
|
||||
* `item_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of items or item
|
||||
groups.
|
||||
|
||||
```python
|
||||
# items.py
|
||||
item_descriptions = {
|
||||
"Red Potion": "A standard health potion",
|
||||
"Spaceship Key": """
|
||||
The key to the spaceship in Level 2.
|
||||
|
||||
This is necessary to get to the Star Realm.
|
||||
""",
|
||||
}
|
||||
|
||||
# __init__.py
|
||||
from worlds.AutoWorld import WebWorld
|
||||
from .items import item_descriptions
|
||||
|
||||
|
||||
class MyGameWeb(WebWorld):
|
||||
item_descriptions = item_descriptions
|
||||
```
|
||||
|
||||
### MultiWorld Object
|
||||
|
||||
The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible
|
||||
@@ -178,36 +225,6 @@ Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED
|
||||
The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being
|
||||
required, and will prevent progression and useful items from being placed at excluded locations.
|
||||
|
||||
#### 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 options pages.
|
||||
|
||||
```python
|
||||
# locations.py
|
||||
|
||||
location_descriptions = {
|
||||
"Red Potion #6": "In a secret destructible block under the second stairway",
|
||||
"L2 Spaceship":
|
||||
"""
|
||||
The group of all items in the spaceship in Level 2.
|
||||
|
||||
This doesn't include the item on the spaceship door, since it can be accessed without the Spaceship Key.
|
||||
"""
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
from worlds.AutoWorld import World
|
||||
from .locations import location_descriptions
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
location_descriptions = location_descriptions
|
||||
```
|
||||
|
||||
### Items
|
||||
|
||||
Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally
|
||||
@@ -232,36 +249,6 @@ Other classifications include:
|
||||
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
|
||||
will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres
|
||||
|
||||
#### 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 options pages.
|
||||
|
||||
```python
|
||||
# items.py
|
||||
|
||||
item_descriptions = {
|
||||
"Red Potion": "A standard health potion",
|
||||
"Spaceship Key":
|
||||
"""
|
||||
The key to the spaceship in Level 2.
|
||||
|
||||
This is necessary to get to the Star Realm.
|
||||
"""
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
from worlds.AutoWorld import World
|
||||
from .items import item_descriptions
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
item_descriptions = item_descriptions
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to
|
||||
|
||||
@@ -75,7 +75,7 @@ Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLaunc
|
||||
[Run]
|
||||
|
||||
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
|
||||
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Flags: nowait; Components: lttp_sprites
|
||||
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: lttp_sprites
|
||||
Filename: "{app}\ArchipelagoLauncher"; Parameters: "--update_settings"; StatusMsg: "Updating host.yaml..."; Flags: runasoriginaluser runhidden
|
||||
Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringChange('Launcher', '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
|
||||
30
kvui.py
30
kvui.py
@@ -64,7 +64,7 @@ from kivy.uix.popup import Popup
|
||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||
|
||||
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType
|
||||
from Utils import async_start
|
||||
from Utils import async_start, get_input_text_from_response
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import CommonClient
|
||||
@@ -285,16 +285,10 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
temp = MarkupLabel(text=self.text).markup
|
||||
text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||
cmdinput = App.get_running_app().textinput
|
||||
if not cmdinput.text and " did you mean " in text:
|
||||
for question in ("Didn't find something that closely matches, did you mean ",
|
||||
"Too many close matches, did you mean "):
|
||||
if text.startswith(question):
|
||||
name = Utils.get_text_between(text, question,
|
||||
"? (")
|
||||
cmdinput.text = f"!{App.get_running_app().last_autofillable_command} {name}"
|
||||
break
|
||||
elif not cmdinput.text and text.startswith("Missing: "):
|
||||
cmdinput.text = text.replace("Missing: ", "!hint_location ")
|
||||
if not cmdinput.text:
|
||||
input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command)
|
||||
if input_text is not None:
|
||||
cmdinput.text = input_text
|
||||
|
||||
Clipboard.copy(text.replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
@@ -683,10 +677,18 @@ class HintLog(RecycleView):
|
||||
for hint in hints:
|
||||
data.append({
|
||||
"receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
|
||||
"item": {"text": self.parser.handle_node(
|
||||
{"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})},
|
||||
"item": {"text": self.parser.handle_node({
|
||||
"type": "item_id",
|
||||
"text": hint["item"],
|
||||
"flags": hint["item_flags"],
|
||||
"player": hint["receiving_player"],
|
||||
})},
|
||||
"finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})},
|
||||
"location": {"text": self.parser.handle_node({"type": "location_id", "text": hint["location"]})},
|
||||
"location": {"text": self.parser.handle_node({
|
||||
"type": "location_id",
|
||||
"text": hint["location"],
|
||||
"player": hint["finding_player"],
|
||||
})},
|
||||
"entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text",
|
||||
"color": "blue", "text": hint["entrance"]
|
||||
if hint["entrance"] else "Vanilla"})},
|
||||
|
||||
20
settings.py
20
settings.py
@@ -643,17 +643,6 @@ class GeneratorOptions(Group):
|
||||
PLAYTHROUGH = 2
|
||||
FULL = 3
|
||||
|
||||
class GlitchTriforceRoom(IntEnum):
|
||||
"""
|
||||
Glitch to Triforce room from Ganon
|
||||
When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality
|
||||
+ hammer) and have completed the goal required for killing ganon to be able to access the triforce room.
|
||||
1 -> Enabled.
|
||||
0 -> Disabled (except in no-logic)
|
||||
"""
|
||||
OFF = 0
|
||||
ON = 1
|
||||
|
||||
class PlandoOptions(str):
|
||||
"""
|
||||
List of options that can be plando'd. Can be combined, for example "bosses, items"
|
||||
@@ -665,6 +654,14 @@ class GeneratorOptions(Group):
|
||||
OFF = 0
|
||||
ON = 1
|
||||
|
||||
class PanicMethod(str):
|
||||
"""
|
||||
What to do if the current item placements appear unsolvable.
|
||||
raise -> Raise an exception and abort.
|
||||
swap -> Attempt to fix it by swapping prior placements around. (Default)
|
||||
start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations.
|
||||
"""
|
||||
|
||||
enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
|
||||
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
|
||||
players: Players = Players(0)
|
||||
@@ -673,6 +670,7 @@ class GeneratorOptions(Group):
|
||||
spoiler: Spoiler = Spoiler(3)
|
||||
race: Race = Race(0)
|
||||
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
|
||||
panic_method: PanicMethod = PanicMethod("swap")
|
||||
|
||||
|
||||
class SNIOptions(Group):
|
||||
|
||||
2
setup.py
2
setup.py
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
try:
|
||||
requirement = 'cx-Freeze>=7.0.0'
|
||||
requirement = 'cx-Freeze==7.0.0'
|
||||
import pkg_resources
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
|
||||
23
test/general/test_client_server_interaction.py
Normal file
23
test/general/test_client_server_interaction.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import unittest
|
||||
|
||||
from Utils import get_intended_text, get_input_text_from_response
|
||||
|
||||
|
||||
class TestClient(unittest.TestCase):
|
||||
def test_autofill_hint_from_fuzzy_hint(self) -> None:
|
||||
tests = (
|
||||
("item", ["item1", "item2"]), # Multiple close matches
|
||||
("itm", ["item1", "item21"]), # No close match, multiple option
|
||||
("item", ["item1"]), # No close match, single option
|
||||
("item", ["\"item\" 'item' (item)"]), # Testing different special characters
|
||||
)
|
||||
|
||||
for input_text, possible_answers in tests:
|
||||
item_name, usable, response = get_intended_text(input_text, possible_answers)
|
||||
self.assertFalse(usable, "This test must be updated, it seems get_fuzzy_results behavior changed")
|
||||
|
||||
hint_command = get_input_text_from_response(response, "hint")
|
||||
self.assertIsNotNone(hint_command,
|
||||
"The response to fuzzy hints is no longer recognized by the hint autofill")
|
||||
self.assertEqual(hint_command, f"!hint {item_name}",
|
||||
"The hint command autofilled by the response is not correct")
|
||||
@@ -6,22 +6,6 @@ from . import setup_solo_multiworld
|
||||
|
||||
|
||||
class TestIDs(unittest.TestCase):
|
||||
def test_unique_items(self):
|
||||
"""Tests that every game has a unique ID per item in the datapackage"""
|
||||
known_item_ids = set()
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
current = len(known_item_ids)
|
||||
known_item_ids |= set(world_type.item_id_to_name)
|
||||
self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current)
|
||||
|
||||
def test_unique_locations(self):
|
||||
"""Tests that every game has a unique ID per location in the datapackage"""
|
||||
known_location_ids = set()
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
current = len(known_location_ids)
|
||||
known_location_ids |= set(world_type.location_id_to_name)
|
||||
self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current)
|
||||
|
||||
def test_range_items(self):
|
||||
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
|
||||
@@ -64,15 +64,6 @@ class TestBase(unittest.TestCase):
|
||||
for item in multiworld.itempool:
|
||||
self.assertIn(item.name, world_type.item_name_to_id)
|
||||
|
||||
def test_item_descriptions_have_valid_names(self):
|
||||
"""Ensure all item descriptions match an item name or item group name"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
valid_names = world_type.item_names.union(world_type.item_name_groups)
|
||||
for name in world_type.item_descriptions:
|
||||
with self.subTest("Name should be valid", game=game_name, item=name):
|
||||
self.assertIn(name, valid_names,
|
||||
"All item descriptions must match defined item names")
|
||||
|
||||
def test_itempool_not_modified(self):
|
||||
"""Test that worlds don't modify the itempool after `create_items`"""
|
||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||
|
||||
@@ -66,12 +66,3 @@ class TestBase(unittest.TestCase):
|
||||
for location in locations:
|
||||
self.assertIn(location, world_type.location_name_to_id)
|
||||
self.assertNotIn(group_name, world_type.location_name_to_id)
|
||||
|
||||
def test_location_descriptions_have_valid_names(self):
|
||||
"""Ensure all location descriptions match a location name or location group name"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
valid_names = world_type.location_names.union(world_type.location_name_groups)
|
||||
for name in world_type.location_descriptions:
|
||||
with self.subTest("Name should be valid", game=game_name, location=name):
|
||||
self.assertIn(name, valid_names,
|
||||
"All location descriptions must match defined location names")
|
||||
|
||||
@@ -31,7 +31,7 @@ class TestPlayerOptions(unittest.TestCase):
|
||||
self.assertEqual(new_weights["list_2"], ["string_3"])
|
||||
self.assertEqual(new_weights["list_1"], ["string", "string_2"])
|
||||
self.assertEqual(new_weights["dict_1"]["option_a"], 50)
|
||||
self.assertEqual(new_weights["dict_1"]["option_b"], 0)
|
||||
self.assertEqual(new_weights["dict_1"]["option_b"], 50)
|
||||
self.assertEqual(new_weights["dict_1"]["option_c"], 50)
|
||||
self.assertNotIn("option_f", new_weights["dict_2"])
|
||||
self.assertEqual(new_weights["dict_2"]["option_g"], 50)
|
||||
|
||||
106
test/programs/test_common_client.py
Normal file
106
test/programs/test_common_client.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import unittest
|
||||
|
||||
import NetUtils
|
||||
from CommonClient import CommonContext
|
||||
|
||||
|
||||
class TestCommonContext(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.ctx = CommonContext()
|
||||
self.ctx.slot = 1 # Pretend we're player 1 for this.
|
||||
self.ctx.slot_info.update({
|
||||
1: NetUtils.NetworkSlot("Player 1", "__TestGame1", NetUtils.SlotType.player),
|
||||
2: NetUtils.NetworkSlot("Player 2", "__TestGame1", NetUtils.SlotType.player),
|
||||
3: NetUtils.NetworkSlot("Player 3", "__TestGame2", NetUtils.SlotType.player),
|
||||
})
|
||||
self.ctx.consume_players_package([
|
||||
NetUtils.NetworkPlayer(1, 1, "Player 1", "Player 1"),
|
||||
NetUtils.NetworkPlayer(1, 2, "Player 2", "Player 2"),
|
||||
NetUtils.NetworkPlayer(1, 3, "Player 3", "Player 3"),
|
||||
])
|
||||
# Using IDs outside the "safe range" for testing purposes only. If this fails unit tests, it's because
|
||||
# another world is not following the spec for allowed ID ranges.
|
||||
self.ctx.update_data_package({
|
||||
"games": {
|
||||
"__TestGame1": {
|
||||
"location_name_to_id": {
|
||||
"Test Location 1 - Safe": 2**54 + 1,
|
||||
"Test Location 2 - Duplicate": 2**54 + 2,
|
||||
},
|
||||
"item_name_to_id": {
|
||||
"Test Item 1 - Safe": 2**54 + 1,
|
||||
"Test Item 2 - Duplicate": 2**54 + 2,
|
||||
},
|
||||
},
|
||||
"__TestGame2": {
|
||||
"location_name_to_id": {
|
||||
"Test Location 3 - Duplicate": 2**54 + 2,
|
||||
},
|
||||
"item_name_to_id": {
|
||||
"Test Item 3 - Duplicate": 2**54 + 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
async def test_archipelago_datapackage_lookups_exist(self):
|
||||
assert "Archipelago" in self.ctx.item_names, "Archipelago item names entry does not exist"
|
||||
assert "Archipelago" in self.ctx.location_names, "Archipelago location names entry does not exist"
|
||||
|
||||
async def test_implicit_name_lookups(self):
|
||||
# Items
|
||||
assert self.ctx.item_names[2**54 + 1] == "Test Item 1 - Safe"
|
||||
assert self.ctx.item_names[2**54 + 3] == f"Unknown item (ID: {2**54+3})"
|
||||
assert self.ctx.item_names[-1] == "Nothing"
|
||||
|
||||
# Locations
|
||||
assert self.ctx.location_names[2**54 + 1] == "Test Location 1 - Safe"
|
||||
assert self.ctx.location_names[2**54 + 3] == f"Unknown location (ID: {2**54+3})"
|
||||
assert self.ctx.location_names[-1] == "Cheat Console"
|
||||
|
||||
async def test_explicit_name_lookups(self):
|
||||
# Items
|
||||
assert self.ctx.item_names["__TestGame1"][2**54+1] == "Test Item 1 - Safe"
|
||||
assert self.ctx.item_names["__TestGame1"][2**54+2] == "Test Item 2 - Duplicate"
|
||||
assert self.ctx.item_names["__TestGame1"][2**54+3] == f"Unknown item (ID: {2**54+3})"
|
||||
assert self.ctx.item_names["__TestGame1"][-1] == "Nothing"
|
||||
assert self.ctx.item_names["__TestGame2"][2**54+1] == f"Unknown item (ID: {2**54+1})"
|
||||
assert self.ctx.item_names["__TestGame2"][2**54+2] == "Test Item 3 - Duplicate"
|
||||
assert self.ctx.item_names["__TestGame2"][2**54+3] == f"Unknown item (ID: {2**54+3})"
|
||||
assert self.ctx.item_names["__TestGame2"][-1] == "Nothing"
|
||||
|
||||
# Locations
|
||||
assert self.ctx.location_names["__TestGame1"][2**54+1] == "Test Location 1 - Safe"
|
||||
assert self.ctx.location_names["__TestGame1"][2**54+2] == "Test Location 2 - Duplicate"
|
||||
assert self.ctx.location_names["__TestGame1"][2**54+3] == f"Unknown location (ID: {2**54+3})"
|
||||
assert self.ctx.location_names["__TestGame1"][-1] == "Cheat Console"
|
||||
assert self.ctx.location_names["__TestGame2"][2**54+1] == f"Unknown location (ID: {2**54+1})"
|
||||
assert self.ctx.location_names["__TestGame2"][2**54+2] == "Test Location 3 - Duplicate"
|
||||
assert self.ctx.location_names["__TestGame2"][2**54+3] == f"Unknown location (ID: {2**54+3})"
|
||||
assert self.ctx.location_names["__TestGame2"][-1] == "Cheat Console"
|
||||
|
||||
async def test_lookup_helper_functions(self):
|
||||
# Checking own slot.
|
||||
assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1) == "Test Item 1 - Safe"
|
||||
assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2) == "Test Item 2 - Duplicate"
|
||||
assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 3) == f"Unknown item (ID: {2 ** 54 + 3})"
|
||||
assert self.ctx.item_names.lookup_in_slot(-1) == f"Nothing"
|
||||
|
||||
# Checking others' slots.
|
||||
assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 2) == "Test Item 1 - Safe"
|
||||
assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 2) == "Test Item 2 - Duplicate"
|
||||
assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 3) == f"Unknown item (ID: {2 ** 54 + 1})"
|
||||
assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 3) == "Test Item 3 - Duplicate"
|
||||
|
||||
# Checking by game.
|
||||
assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame1") == "Test Item 1 - Safe"
|
||||
assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame1") == "Test Item 2 - Duplicate"
|
||||
assert self.ctx.item_names.lookup_in_game(2 ** 54 + 3, "__TestGame1") == f"Unknown item (ID: {2 ** 54 + 3})"
|
||||
assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame2") == f"Unknown item (ID: {2 ** 54 + 1})"
|
||||
assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame2") == "Test Item 3 - Duplicate"
|
||||
|
||||
# Checking with Archipelago ids are valid in any game package.
|
||||
assert self.ctx.item_names.lookup_in_slot(-1, 2) == "Nothing"
|
||||
assert self.ctx.item_names.lookup_in_slot(-1, 3) == "Nothing"
|
||||
assert self.ctx.item_names.lookup_in_game(-1, "__TestGame1") == "Nothing"
|
||||
assert self.ctx.item_names.lookup_in_game(-1, "__TestGame2") == "Nothing"
|
||||
23
test/webhost/test_descriptions.py
Normal file
23
test/webhost/test_descriptions.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import unittest
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
class TestWebDescriptions(unittest.TestCase):
|
||||
def test_item_descriptions_have_valid_names(self) -> None:
|
||||
"""Ensure all item descriptions match an item name or item group name"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
valid_names = world_type.item_names.union(world_type.item_name_groups)
|
||||
for name in world_type.web.item_descriptions:
|
||||
with self.subTest("Name should be valid", game=game_name, item=name):
|
||||
self.assertIn(name, valid_names,
|
||||
"All item descriptions must match defined item names")
|
||||
|
||||
def test_location_descriptions_have_valid_names(self) -> None:
|
||||
"""Ensure all location descriptions match a location name or location group name"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
valid_names = world_type.location_names.union(world_type.location_name_groups)
|
||||
for name in world_type.web.location_descriptions:
|
||||
with self.subTest("Name should be valid", game=game_name, location=name):
|
||||
self.assertIn(name, valid_names,
|
||||
"All location descriptions must match defined location names")
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
|
||||
from worlds import AutoWorldRegister
|
||||
from Options import Choice, NamedRange, Toggle, Range
|
||||
from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet
|
||||
|
||||
|
||||
class TestOptionPresets(unittest.TestCase):
|
||||
@@ -14,7 +14,7 @@ class TestOptionPresets(unittest.TestCase):
|
||||
with self.subTest(game=game_name, preset=preset_name, option=option_name):
|
||||
try:
|
||||
option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
supported_types = [Choice, Toggle, Range, NamedRange]
|
||||
supported_types = [NumericOption, OptionSet, OptionList, ItemDict]
|
||||
if not any([issubclass(option.__class__, t) for t in supported_types]):
|
||||
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
|
||||
f"is not a supported type for webhost. "
|
||||
|
||||
@@ -3,18 +3,14 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import logging
|
||||
import pathlib
|
||||
from random import Random
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from random import Random
|
||||
from dataclasses import make_dataclass
|
||||
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping,
|
||||
Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union)
|
||||
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple,
|
||||
TYPE_CHECKING, Type, Union)
|
||||
|
||||
from Options import (
|
||||
ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions,
|
||||
PriorityLocations, StartHints, StartInventory, StartInventoryPool, StartLocationHints
|
||||
)
|
||||
from Options import item_and_loc_options, OptionGroup, PerGameCommonOptions
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -55,17 +51,12 @@ class AutoWorldRegister(type):
|
||||
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("item_name_groups", {}).items()}
|
||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
||||
dct["item_descriptions"] = {name: _normalize_description(description) for name, description
|
||||
in dct.get("item_descriptions", {}).items()}
|
||||
dct["item_descriptions"]["Everything"] = "All items in the entire game."
|
||||
|
||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("location_name_groups", {}).items()}
|
||||
dct["location_name_groups"]["Everywhere"] = dct["location_names"]
|
||||
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||
dct["location_descriptions"] = {name: _normalize_description(description) for name, description
|
||||
in dct.get("location_descriptions", {}).items()}
|
||||
dct["location_descriptions"]["Everywhere"] = "All locations in the entire game."
|
||||
|
||||
# move away from get_required_client_version function
|
||||
if "game" in dct:
|
||||
@@ -125,13 +116,19 @@ class WebWorldRegister(type):
|
||||
# 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]
|
||||
prebuilt_options = ["Game Options", "Item & Location Options"]
|
||||
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."
|
||||
assert group.options, "A custom defined Option Group must contain at least one Option."
|
||||
# catch incorrectly titled versions of the prebuilt groups so they don't create extra groups
|
||||
title_name = group.name.title()
|
||||
if title_name in prebuilt_options:
|
||||
group.name = title_name
|
||||
|
||||
if group.name == "Item & Location Options":
|
||||
assert not any(option in item_and_loc_options for option in group.options), \
|
||||
f"Item and Location Options cannot be specified multiple times"
|
||||
group.options.extend(item_and_loc_options)
|
||||
item_group_in_list = True
|
||||
else:
|
||||
@@ -143,7 +140,7 @@ class WebWorldRegister(type):
|
||||
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))
|
||||
option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options, True))
|
||||
return super().__new__(mcs, name, bases, dct)
|
||||
|
||||
|
||||
@@ -226,6 +223,12 @@ class WebWorld(metaclass=WebWorldRegister):
|
||||
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"."""
|
||||
|
||||
location_descriptions: Dict[str, str] = {}
|
||||
"""An optional map from location names (or location group names) to brief descriptions for users."""
|
||||
|
||||
item_descriptions: Dict[str, str] = {}
|
||||
"""An optional map from item names (or item group names) to brief descriptions for users."""
|
||||
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
@@ -252,35 +255,9 @@ class World(metaclass=AutoWorldRegister):
|
||||
item_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
||||
"""maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}"""
|
||||
|
||||
item_descriptions: ClassVar[Dict[str, str]] = {}
|
||||
"""An optional map from item names (or item group names) to brief descriptions for users.
|
||||
|
||||
Individual newlines and indentation will be collapsed into spaces before these descriptions are
|
||||
displayed. This may cover only a subset of items.
|
||||
"""
|
||||
|
||||
location_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
||||
"""maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}"""
|
||||
|
||||
location_descriptions: ClassVar[Dict[str, str]] = {}
|
||||
"""An optional map from location names (or location group names) to brief descriptions for users.
|
||||
|
||||
Individual newlines and indentation will be collapsed into spaces before these descriptions are
|
||||
displayed. This may cover only a subset of locations.
|
||||
"""
|
||||
|
||||
data_version: ClassVar[int] = 0
|
||||
"""
|
||||
Increment this every time something in your world's names/id mappings changes.
|
||||
|
||||
When this is set to 0, that world's DataPackage is considered in "testing mode", which signals to servers/clients
|
||||
that it should not be cached, and clients should request that world's DataPackage every connection. Not
|
||||
recommended for production-ready worlds.
|
||||
|
||||
Deprecated. Clients should utilize `checksum` to determine if DataPackage has changed since last connection and
|
||||
request a new DataPackage, if necessary.
|
||||
"""
|
||||
|
||||
required_client_version: Tuple[int, int, int] = (0, 1, 6)
|
||||
"""
|
||||
override this if changes to a world break forward-compatibility of the client
|
||||
@@ -554,7 +531,6 @@ class World(metaclass=AutoWorldRegister):
|
||||
"item_name_to_id": cls.item_name_to_id,
|
||||
"location_name_groups": sorted_location_name_groups,
|
||||
"location_name_to_id": cls.location_name_to_id,
|
||||
"version": cls.data_version,
|
||||
}
|
||||
res["checksum"] = data_package_checksum(res)
|
||||
return res
|
||||
@@ -572,18 +548,3 @@ def data_package_checksum(data: "GamesPackage") -> str:
|
||||
assert sorted(data) == list(data), "Data not ordered"
|
||||
from NetUtils import encode
|
||||
return hashlib.sha1(encode(data).encode()).hexdigest()
|
||||
|
||||
|
||||
def _normalize_description(description):
|
||||
"""
|
||||
Normalizes a description in item_descriptions or location_descriptions.
|
||||
|
||||
This allows authors to write descritions with nice indentation and line lengths in their world
|
||||
definitions without having it affect the rendered format.
|
||||
"""
|
||||
# First, collapse the whitespace around newlines and the ends of the description.
|
||||
description = re.sub(r' *\n *', '\n', description.strip())
|
||||
# Next, condense individual newlines into spaces.
|
||||
description = re.sub(r'(?<!\n)\n(?!\n)', ' ', description)
|
||||
return description
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ class GamesPackage(TypedDict, total=False):
|
||||
location_name_groups: Dict[str, List[str]]
|
||||
location_name_to_id: Dict[str, int]
|
||||
checksum: str
|
||||
version: int # TODO: Remove support after per game data packages API change.
|
||||
|
||||
|
||||
class DataPackage(TypedDict):
|
||||
|
||||
@@ -103,7 +103,7 @@ async def connect(ctx: BizHawkContext) -> bool:
|
||||
return True
|
||||
except (TimeoutError, ConnectionRefusedError):
|
||||
continue
|
||||
|
||||
|
||||
# No ports worked
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
A module containing the BizHawkClient base class and metaclass
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
@@ -12,14 +11,13 @@ from worlds.LauncherComponents import Component, SuffixIdentifier, Type, compone
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .context import BizHawkClientContext
|
||||
else:
|
||||
BizHawkClientContext = object
|
||||
|
||||
|
||||
def launch_client(*args) -> None:
|
||||
from .context import launch
|
||||
launch_subprocess(launch, name="BizHawkClient")
|
||||
|
||||
|
||||
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||
file_identifier=SuffixIdentifier())
|
||||
components.append(component)
|
||||
@@ -56,7 +54,7 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
async def get_handler(ctx: BizHawkClientContext, system: str) -> Optional[BizHawkClient]:
|
||||
async def get_handler(ctx: "BizHawkClientContext", system: str) -> Optional[BizHawkClient]:
|
||||
for systems, handlers in AutoBizHawkClientRegister.game_handlers.items():
|
||||
if system in systems:
|
||||
for handler in handlers.values():
|
||||
@@ -77,7 +75,7 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
||||
"""The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def validate_rom(self, ctx: BizHawkClientContext) -> bool:
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
"""Should return whether the currently loaded ROM should be handled by this client. You might read the game name
|
||||
from the ROM header, for example. This function will only be asked to validate ROMs from the system set by the
|
||||
client class, so you do not need to check the system yourself.
|
||||
@@ -86,18 +84,18 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
||||
as necessary (such as setting `ctx.game = self.game`, modifying `ctx.items_handling`, etc...)."""
|
||||
...
|
||||
|
||||
async def set_auth(self, ctx: BizHawkClientContext) -> None:
|
||||
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
|
||||
"""Should set ctx.auth in anticipation of sending a `Connected` packet. You may override this if you store slot
|
||||
name in your patched ROM. If ctx.auth is not set after calling, the player will be prompted to enter their
|
||||
username."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def game_watcher(self, ctx: BizHawkClientContext) -> None:
|
||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||
"""Runs on a loop with the approximate interval `ctx.watcher_timeout`. The currently loaded ROM is guaranteed
|
||||
to have passed your validator when this function is called, and the emulator is very likely to be connected."""
|
||||
...
|
||||
|
||||
def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None:
|
||||
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
|
||||
"""For handling packages from the server. Called from `BizHawkClientContext.on_package`."""
|
||||
pass
|
||||
|
||||
@@ -3,7 +3,6 @@ A module containing context and functions relevant to running the client. This m
|
||||
checking or launching the client, otherwise it will probably cause circular import issues.
|
||||
"""
|
||||
|
||||
|
||||
import asyncio
|
||||
import enum
|
||||
import subprocess
|
||||
@@ -77,7 +76,7 @@ class BizHawkClientContext(CommonContext):
|
||||
if self.client_handler is not None:
|
||||
self.client_handler.on_package(self, cmd, args)
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
async def server_auth(self, password_requested: bool=False):
|
||||
self.password_requested = password_requested
|
||||
|
||||
if self.bizhawk_ctx.connection_status != ConnectionStatus.CONNECTED:
|
||||
@@ -103,7 +102,7 @@ class BizHawkClientContext(CommonContext):
|
||||
await self.send_connect()
|
||||
self.auth_status = AuthStatus.PENDING
|
||||
|
||||
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||
async def disconnect(self, allow_autoreconnect: bool=False):
|
||||
self.auth_status = AuthStatus.NOT_AUTHENTICATED
|
||||
await super().disconnect(allow_autoreconnect)
|
||||
|
||||
@@ -148,7 +147,8 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
||||
script_version = await get_script_version(ctx.bizhawk_ctx)
|
||||
|
||||
if script_version != EXPECTED_SCRIPT_VERSION:
|
||||
logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but got {script_version}. Disconnecting.")
|
||||
logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but "
|
||||
f"got {script_version}. Disconnecting.")
|
||||
disconnect(ctx.bizhawk_ctx)
|
||||
continue
|
||||
|
||||
@@ -177,7 +177,8 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
||||
|
||||
if ctx.client_handler is None:
|
||||
if not showed_no_handler_message:
|
||||
logger.info("No handler was found for this game")
|
||||
logger.info("No handler was found for this game. Double-check that the apworld is installed "
|
||||
"correctly and that you loaded the right ROM file.")
|
||||
showed_no_handler_message = True
|
||||
continue
|
||||
else:
|
||||
|
||||
@@ -113,7 +113,6 @@ class AdventureWorld(World):
|
||||
settings: ClassVar[AdventureSettings]
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
|
||||
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
|
||||
data_version: ClassVar[int] = 1
|
||||
required_client_version: Tuple[int, int, int] = (0, 3, 9)
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
|
||||
232
worlds/ahit/Client.py
Normal file
232
worlds/ahit/Client.py
Normal file
@@ -0,0 +1,232 @@
|
||||
import asyncio
|
||||
import Utils
|
||||
import websockets
|
||||
import functools
|
||||
from copy import deepcopy
|
||||
from typing import List, Any, Iterable
|
||||
from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem
|
||||
from MultiServer import Endpoint
|
||||
from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
||||
class AHITJSONToTextParser(JSONtoTextParser):
|
||||
def _handle_color(self, node: JSONMessagePart):
|
||||
return self._handle_text(node) # No colors for the in-game text
|
||||
|
||||
|
||||
class AHITCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_ahit(self):
|
||||
"""Check AHIT Connection State"""
|
||||
if isinstance(self.ctx, AHITContext):
|
||||
logger.info(f"AHIT Status: {self.ctx.get_ahit_status()}")
|
||||
|
||||
|
||||
class AHITContext(CommonContext):
|
||||
command_processor = AHITCommandProcessor
|
||||
game = "A Hat in Time"
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
self.proxy = None
|
||||
self.proxy_task = None
|
||||
self.gamejsontotext = AHITJSONToTextParser(self)
|
||||
self.autoreconnect_task = None
|
||||
self.endpoint = None
|
||||
self.items_handling = 0b111
|
||||
self.room_info = None
|
||||
self.connected_msg = None
|
||||
self.game_connected = False
|
||||
self.awaiting_info = False
|
||||
self.full_inventory: List[Any] = []
|
||||
self.server_msgs: List[Any] = []
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(AHITContext, self).server_auth(password_requested)
|
||||
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
def get_ahit_status(self) -> str:
|
||||
if not self.is_proxy_connected():
|
||||
return "Not connected to A Hat in Time"
|
||||
|
||||
return "Connected to A Hat in Time"
|
||||
|
||||
async def send_msgs_proxy(self, msgs: Iterable[dict]) -> bool:
|
||||
""" `msgs` JSON serializable """
|
||||
if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed:
|
||||
return False
|
||||
|
||||
if DEBUG:
|
||||
logger.info(f"Outgoing message: {msgs}")
|
||||
|
||||
await self.endpoint.socket.send(msgs)
|
||||
return True
|
||||
|
||||
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||
await super().disconnect(allow_autoreconnect)
|
||||
|
||||
async def disconnect_proxy(self):
|
||||
if self.endpoint and not self.endpoint.socket.closed:
|
||||
await self.endpoint.socket.close()
|
||||
if self.proxy_task is not None:
|
||||
await self.proxy_task
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
return self.server and self.server.socket.open
|
||||
|
||||
def is_proxy_connected(self) -> bool:
|
||||
return self.endpoint and self.endpoint.socket.open
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
text = self.gamejsontotext(deepcopy(args["data"]))
|
||||
msg = {"cmd": "PrintJSON", "data": [{"text": text}], "type": "Chat"}
|
||||
self.server_msgs.append(encode([msg]))
|
||||
|
||||
if self.ui:
|
||||
self.ui.print_json(args["data"])
|
||||
else:
|
||||
text = self.jsontotextparser(args["data"])
|
||||
logger.info(text)
|
||||
|
||||
def update_items(self):
|
||||
# just to be safe - we might still have an inventory from a different room
|
||||
if not self.is_connected():
|
||||
return
|
||||
|
||||
self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}]))
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
self.connected_msg = encode([args])
|
||||
if self.awaiting_info:
|
||||
self.server_msgs.append(self.room_info)
|
||||
self.update_items()
|
||||
self.awaiting_info = False
|
||||
|
||||
elif cmd == "ReceivedItems":
|
||||
if args["index"] == 0:
|
||||
self.full_inventory.clear()
|
||||
|
||||
for item in args["items"]:
|
||||
self.full_inventory.append(NetworkItem(*item))
|
||||
|
||||
self.server_msgs.append(encode([args]))
|
||||
|
||||
elif cmd == "RoomInfo":
|
||||
self.seed_name = args["seed_name"]
|
||||
self.room_info = encode([args])
|
||||
|
||||
else:
|
||||
if cmd != "PrintJSON":
|
||||
self.server_msgs.append(encode([args]))
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class AHITManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago A Hat in Time Client"
|
||||
|
||||
self.ui = AHITManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
|
||||
async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
|
||||
ctx.endpoint = Endpoint(websocket)
|
||||
try:
|
||||
await on_client_connected(ctx)
|
||||
|
||||
if ctx.is_proxy_connected():
|
||||
async for data in websocket:
|
||||
if DEBUG:
|
||||
logger.info(f"Incoming message: {data}")
|
||||
|
||||
for msg in decode(data):
|
||||
if msg["cmd"] == "Connect":
|
||||
# Proxy is connecting, make sure it is valid
|
||||
if msg["game"] != "A Hat in Time":
|
||||
logger.info("Aborting proxy connection: game is not A Hat in Time")
|
||||
await ctx.disconnect_proxy()
|
||||
break
|
||||
|
||||
if ctx.seed_name:
|
||||
seed_name = msg.get("seed_name", "")
|
||||
if seed_name != "" and seed_name != ctx.seed_name:
|
||||
logger.info("Aborting proxy connection: seed mismatch from save file")
|
||||
logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}")
|
||||
text = encode([{"cmd": "PrintJSON",
|
||||
"data": [{"text": "Connection aborted - save file to seed mismatch"}]}])
|
||||
await ctx.send_msgs_proxy(text)
|
||||
await ctx.disconnect_proxy()
|
||||
break
|
||||
|
||||
if ctx.connected_msg and ctx.is_connected():
|
||||
await ctx.send_msgs_proxy(ctx.connected_msg)
|
||||
ctx.update_items()
|
||||
continue
|
||||
|
||||
if not ctx.is_proxy_connected():
|
||||
break
|
||||
|
||||
await ctx.send_msgs([msg])
|
||||
|
||||
except Exception as e:
|
||||
if not isinstance(e, websockets.WebSocketException):
|
||||
logger.exception(e)
|
||||
finally:
|
||||
await ctx.disconnect_proxy()
|
||||
|
||||
|
||||
async def on_client_connected(ctx: AHITContext):
|
||||
if ctx.room_info and ctx.is_connected():
|
||||
await ctx.send_msgs_proxy(ctx.room_info)
|
||||
else:
|
||||
ctx.awaiting_info = True
|
||||
|
||||
|
||||
async def proxy_loop(ctx: AHITContext):
|
||||
try:
|
||||
while not ctx.exit_event.is_set():
|
||||
if len(ctx.server_msgs) > 0:
|
||||
for msg in ctx.server_msgs:
|
||||
await ctx.send_msgs_proxy(msg)
|
||||
|
||||
ctx.server_msgs.clear()
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.info("Aborting AHIT Proxy Client due to errors")
|
||||
|
||||
|
||||
def launch():
|
||||
async def main():
|
||||
parser = get_base_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
ctx = AHITContext(args.connect, args.password)
|
||||
logger.info("Starting A Hat in Time proxy server")
|
||||
ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx),
|
||||
host="localhost", port=11311, ping_timeout=999999, ping_interval=999999)
|
||||
ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop")
|
||||
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
await ctx.proxy
|
||||
await ctx.proxy_task
|
||||
await ctx.exit_event.wait()
|
||||
|
||||
Utils.init_logging("AHITClient")
|
||||
# options = Utils.get_options()
|
||||
|
||||
import colorama
|
||||
colorama.init()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
243
worlds/ahit/DeathWishLocations.py
Normal file
243
worlds/ahit/DeathWishLocations.py
Normal file
@@ -0,0 +1,243 @@
|
||||
from .Types import HatInTimeLocation, HatInTimeItem
|
||||
from .Regions import create_region
|
||||
from BaseClasses import Region, LocationProgressType, ItemClassification
|
||||
from worlds.generic.Rules import add_rule
|
||||
from typing import List, TYPE_CHECKING
|
||||
from .Locations import death_wishes
|
||||
from .Options import EndGoal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import HatInTimeWorld
|
||||
|
||||
|
||||
dw_prereqs = {
|
||||
"So You're Back From Outer Space": ["Beat the Heat"],
|
||||
"Snatcher's Hit List": ["Beat the Heat"],
|
||||
"Snatcher Coins in Mafia Town": ["So You're Back From Outer Space"],
|
||||
"Rift Collapse: Mafia of Cooks": ["So You're Back From Outer Space"],
|
||||
"Collect-a-thon": ["So You're Back From Outer Space"],
|
||||
"She Speedran from Outer Space": ["Rift Collapse: Mafia of Cooks"],
|
||||
"Mafia's Jumps": ["She Speedran from Outer Space"],
|
||||
"Vault Codes in the Wind": ["Collect-a-thon", "She Speedran from Outer Space"],
|
||||
"Encore! Encore!": ["Collect-a-thon"],
|
||||
|
||||
"Security Breach": ["Beat the Heat"],
|
||||
"Rift Collapse: Dead Bird Studio": ["Security Breach"],
|
||||
"The Great Big Hootenanny": ["Security Breach"],
|
||||
"10 Seconds until Self-Destruct": ["The Great Big Hootenanny"],
|
||||
"Killing Two Birds": ["Rift Collapse: Dead Bird Studio", "10 Seconds until Self-Destruct"],
|
||||
"Community Rift: Rhythm Jump Studio": ["10 Seconds until Self-Destruct"],
|
||||
"Snatcher Coins in Battle of the Birds": ["The Great Big Hootenanny"],
|
||||
"Zero Jumps": ["Rift Collapse: Dead Bird Studio"],
|
||||
"Snatcher Coins in Nyakuza Metro": ["Killing Two Birds"],
|
||||
|
||||
"Speedrun Well": ["Beat the Heat"],
|
||||
"Rift Collapse: Sleepy Subcon": ["Speedrun Well"],
|
||||
"Boss Rush": ["Speedrun Well"],
|
||||
"Quality Time with Snatcher": ["Rift Collapse: Sleepy Subcon"],
|
||||
"Breaching the Contract": ["Boss Rush", "Quality Time with Snatcher"],
|
||||
"Community Rift: Twilight Travels": ["Quality Time with Snatcher"],
|
||||
"Snatcher Coins in Subcon Forest": ["Rift Collapse: Sleepy Subcon"],
|
||||
|
||||
"Bird Sanctuary": ["Beat the Heat"],
|
||||
"Snatcher Coins in Alpine Skyline": ["Bird Sanctuary"],
|
||||
"Wound-Up Windmill": ["Bird Sanctuary"],
|
||||
"Rift Collapse: Alpine Skyline": ["Bird Sanctuary"],
|
||||
"Camera Tourist": ["Rift Collapse: Alpine Skyline"],
|
||||
"Community Rift: The Mountain Rift": ["Rift Collapse: Alpine Skyline"],
|
||||
"The Illness has Speedrun": ["Rift Collapse: Alpine Skyline", "Wound-Up Windmill"],
|
||||
|
||||
"The Mustache Gauntlet": ["Wound-Up Windmill"],
|
||||
"No More Bad Guys": ["The Mustache Gauntlet"],
|
||||
"Seal the Deal": ["Encore! Encore!", "Killing Two Birds",
|
||||
"Breaching the Contract", "No More Bad Guys"],
|
||||
|
||||
"Rift Collapse: Deep Sea": ["Rift Collapse: Mafia of Cooks", "Rift Collapse: Dead Bird Studio",
|
||||
"Rift Collapse: Sleepy Subcon", "Rift Collapse: Alpine Skyline"],
|
||||
|
||||
"Cruisin' for a Bruisin'": ["Rift Collapse: Deep Sea"],
|
||||
}
|
||||
|
||||
dw_candles = [
|
||||
"Snatcher's Hit List",
|
||||
"Zero Jumps",
|
||||
"Camera Tourist",
|
||||
"Snatcher Coins in Mafia Town",
|
||||
"Snatcher Coins in Battle of the Birds",
|
||||
"Snatcher Coins in Subcon Forest",
|
||||
"Snatcher Coins in Alpine Skyline",
|
||||
"Snatcher Coins in Nyakuza Metro",
|
||||
]
|
||||
|
||||
annoying_dws = [
|
||||
"Vault Codes in the Wind",
|
||||
"Boss Rush",
|
||||
"Camera Tourist",
|
||||
"The Mustache Gauntlet",
|
||||
"Rift Collapse: Deep Sea",
|
||||
"Cruisin' for a Bruisin'",
|
||||
"Seal the Deal", # Non-excluded if goal
|
||||
]
|
||||
|
||||
# includes the above as well
|
||||
annoying_bonuses = [
|
||||
"So You're Back From Outer Space",
|
||||
"Encore! Encore!",
|
||||
"Snatcher's Hit List",
|
||||
"Vault Codes in the Wind",
|
||||
"10 Seconds until Self-Destruct",
|
||||
"Killing Two Birds",
|
||||
"Zero Jumps",
|
||||
"Boss Rush",
|
||||
"Bird Sanctuary",
|
||||
"The Mustache Gauntlet",
|
||||
"Wound-Up Windmill",
|
||||
"Camera Tourist",
|
||||
"Rift Collapse: Deep Sea",
|
||||
"Cruisin' for a Bruisin'",
|
||||
"Seal the Deal",
|
||||
]
|
||||
|
||||
dw_classes = {
|
||||
"Beat the Heat": "Hat_SnatcherContract_DeathWish_HeatingUpHarder",
|
||||
"So You're Back From Outer Space": "Hat_SnatcherContract_DeathWish_BackFromSpace",
|
||||
"Snatcher's Hit List": "Hat_SnatcherContract_DeathWish_KillEverybody",
|
||||
"Collect-a-thon": "Hat_SnatcherContract_DeathWish_PonFrenzy",
|
||||
"Rift Collapse: Mafia of Cooks": "Hat_SnatcherContract_DeathWish_RiftCollapse_MafiaTown",
|
||||
"Encore! Encore!": "Hat_SnatcherContract_DeathWish_MafiaBossEX",
|
||||
"She Speedran from Outer Space": "Hat_SnatcherContract_DeathWish_Speedrun_MafiaAlien",
|
||||
"Mafia's Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses_MafiaAlien",
|
||||
"Vault Codes in the Wind": "Hat_SnatcherContract_DeathWish_MovingVault",
|
||||
"Snatcher Coins in Mafia Town": "Hat_SnatcherContract_DeathWish_Tokens_MafiaTown",
|
||||
|
||||
"Security Breach": "Hat_SnatcherContract_DeathWish_DeadBirdStudioMoreGuards",
|
||||
"The Great Big Hootenanny": "Hat_SnatcherContract_DeathWish_DifficultParade",
|
||||
"Rift Collapse: Dead Bird Studio": "Hat_SnatcherContract_DeathWish_RiftCollapse_Birds",
|
||||
"10 Seconds until Self-Destruct": "Hat_SnatcherContract_DeathWish_TrainRushShortTime",
|
||||
"Killing Two Birds": "Hat_SnatcherContract_DeathWish_BirdBossEX",
|
||||
"Snatcher Coins in Battle of the Birds": "Hat_SnatcherContract_DeathWish_Tokens_Birds",
|
||||
"Zero Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses",
|
||||
|
||||
"Speedrun Well": "Hat_SnatcherContract_DeathWish_Speedrun_SubWell",
|
||||
"Rift Collapse: Sleepy Subcon": "Hat_SnatcherContract_DeathWish_RiftCollapse_Subcon",
|
||||
"Boss Rush": "Hat_SnatcherContract_DeathWish_BossRush",
|
||||
"Quality Time with Snatcher": "Hat_SnatcherContract_DeathWish_SurvivalOfTheFittest",
|
||||
"Breaching the Contract": "Hat_SnatcherContract_DeathWish_SnatcherEX",
|
||||
"Snatcher Coins in Subcon Forest": "Hat_SnatcherContract_DeathWish_Tokens_Subcon",
|
||||
|
||||
"Bird Sanctuary": "Hat_SnatcherContract_DeathWish_NiceBirdhouse",
|
||||
"Rift Collapse: Alpine Skyline": "Hat_SnatcherContract_DeathWish_RiftCollapse_Alps",
|
||||
"Wound-Up Windmill": "Hat_SnatcherContract_DeathWish_FastWindmill",
|
||||
"The Illness has Speedrun": "Hat_SnatcherContract_DeathWish_Speedrun_Illness",
|
||||
"Snatcher Coins in Alpine Skyline": "Hat_SnatcherContract_DeathWish_Tokens_Alps",
|
||||
"Camera Tourist": "Hat_SnatcherContract_DeathWish_CameraTourist_1",
|
||||
|
||||
"The Mustache Gauntlet": "Hat_SnatcherContract_DeathWish_HardCastle",
|
||||
"No More Bad Guys": "Hat_SnatcherContract_DeathWish_MuGirlEX",
|
||||
|
||||
"Seal the Deal": "Hat_SnatcherContract_DeathWish_BossRushEX",
|
||||
"Rift Collapse: Deep Sea": "Hat_SnatcherContract_DeathWish_RiftCollapse_Cruise",
|
||||
"Cruisin' for a Bruisin'": "Hat_SnatcherContract_DeathWish_EndlessTasks",
|
||||
|
||||
"Community Rift: Rhythm Jump Studio": "Hat_SnatcherContract_DeathWish_CommunityRift_RhythmJump",
|
||||
"Community Rift: Twilight Travels": "Hat_SnatcherContract_DeathWish_CommunityRift_TwilightTravels",
|
||||
"Community Rift: The Mountain Rift": "Hat_SnatcherContract_DeathWish_CommunityRift_MountainRift",
|
||||
|
||||
"Snatcher Coins in Nyakuza Metro": "Hat_SnatcherContract_DeathWish_Tokens_Metro",
|
||||
}
|
||||
|
||||
|
||||
def create_dw_regions(world: "HatInTimeWorld"):
|
||||
if world.options.DWExcludeAnnoyingContracts:
|
||||
for name in annoying_dws:
|
||||
world.excluded_dws.append(name)
|
||||
|
||||
if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses:
|
||||
for name in death_wishes:
|
||||
world.excluded_bonuses.append(name)
|
||||
elif world.options.DWExcludeAnnoyingBonuses:
|
||||
for name in annoying_bonuses:
|
||||
world.excluded_bonuses.append(name)
|
||||
|
||||
if world.options.DWExcludeCandles:
|
||||
for name in dw_candles:
|
||||
if name not in world.excluded_dws:
|
||||
world.excluded_dws.append(name)
|
||||
|
||||
spaceship = world.multiworld.get_region("Spaceship", world.player)
|
||||
dw_map: Region = create_region(world, "Death Wish Map")
|
||||
entrance = spaceship.connect(dw_map, "-> Death Wish Map")
|
||||
add_rule(entrance, lambda state: state.has("Time Piece", world.player, world.options.DWTimePieceRequirement))
|
||||
|
||||
if world.options.DWShuffle:
|
||||
# Connect Death Wishes randomly to one another in a linear sequence
|
||||
dw_list: List[str] = []
|
||||
for name in death_wishes.keys():
|
||||
# Don't shuffle excluded or invalid Death Wishes
|
||||
if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name):
|
||||
continue
|
||||
|
||||
dw_list.append(name)
|
||||
|
||||
world.random.shuffle(dw_list)
|
||||
count = world.random.randint(world.options.DWShuffleCountMin.value, world.options.DWShuffleCountMax.value)
|
||||
dw_shuffle: List[str] = []
|
||||
total = min(len(dw_list), count)
|
||||
for i in range(total):
|
||||
dw_shuffle.append(dw_list[i])
|
||||
|
||||
# Seal the Deal is always last if it's the goal
|
||||
if world.options.EndGoal == EndGoal.option_seal_the_deal:
|
||||
if "Seal the Deal" in dw_shuffle:
|
||||
dw_shuffle.remove("Seal the Deal")
|
||||
|
||||
dw_shuffle.append("Seal the Deal")
|
||||
|
||||
world.dw_shuffle = dw_shuffle
|
||||
prev_dw = dw_map
|
||||
for death_wish_name in dw_shuffle:
|
||||
dw = create_region(world, death_wish_name)
|
||||
prev_dw.connect(dw)
|
||||
create_dw_locations(world, dw)
|
||||
prev_dw = dw
|
||||
else:
|
||||
# DWShuffle is disabled, use vanilla connections
|
||||
for key in death_wishes.keys():
|
||||
if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
|
||||
world.excluded_dws.append(key)
|
||||
continue
|
||||
|
||||
dw = create_region(world, key)
|
||||
if key == "Beat the Heat":
|
||||
dw_map.connect(dw, f"{dw_map.name} -> Beat the Heat")
|
||||
elif key in dw_prereqs.keys():
|
||||
for name in dw_prereqs[key]:
|
||||
parent = world.multiworld.get_region(name, world.player)
|
||||
parent.connect(dw, f"{parent.name} -> {key}")
|
||||
|
||||
create_dw_locations(world, dw)
|
||||
|
||||
|
||||
def create_dw_locations(world: "HatInTimeWorld", dw: Region):
|
||||
loc_id = death_wishes[dw.name]
|
||||
main_objective = HatInTimeLocation(world.player, f"{dw.name} - Main Objective", loc_id, dw)
|
||||
full_clear = HatInTimeLocation(world.player, f"{dw.name} - All Clear", loc_id + 1, dw)
|
||||
main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {dw.name}", None, dw)
|
||||
bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {dw.name}", None, dw)
|
||||
main_stamp.show_in_spoiler = False
|
||||
bonus_stamps.show_in_spoiler = False
|
||||
dw.locations.append(main_stamp)
|
||||
dw.locations.append(bonus_stamps)
|
||||
main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {dw.name}",
|
||||
ItemClassification.progression, None, world.player))
|
||||
bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamp - {dw.name}",
|
||||
ItemClassification.progression, None, world.player))
|
||||
|
||||
if dw.name in world.excluded_dws:
|
||||
main_objective.progress_type = LocationProgressType.EXCLUDED
|
||||
full_clear.progress_type = LocationProgressType.EXCLUDED
|
||||
elif world.is_bonus_excluded(dw.name):
|
||||
full_clear.progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
dw.locations.append(main_objective)
|
||||
dw.locations.append(full_clear)
|
||||
462
worlds/ahit/DeathWishRules.py
Normal file
462
worlds/ahit/DeathWishRules.py
Normal file
@@ -0,0 +1,462 @@
|
||||
from worlds.AutoWorld import CollectionState
|
||||
from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings
|
||||
from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData, HitType
|
||||
from .DeathWishLocations import dw_prereqs, dw_candles
|
||||
from BaseClasses import Entrance, Location, ItemClassification
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from typing import List, Callable, TYPE_CHECKING
|
||||
from .Locations import death_wishes
|
||||
from .Options import EndGoal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import HatInTimeWorld
|
||||
|
||||
|
||||
# Any speedruns expect the player to have Sprint Hat
|
||||
dw_requirements = {
|
||||
"Beat the Heat": LocData(hit_type=HitType.umbrella),
|
||||
"So You're Back From Outer Space": LocData(hookshot=True),
|
||||
"Mafia's Jumps": LocData(required_hats=[HatType.ICE]),
|
||||
"Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]),
|
||||
|
||||
"Security Breach": LocData(hit_type=HitType.umbrella_or_brewing),
|
||||
"10 Seconds until Self-Destruct": LocData(hookshot=True),
|
||||
"Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]),
|
||||
|
||||
"Speedrun Well": LocData(hookshot=True, hit_type=HitType.umbrella_or_brewing),
|
||||
"Boss Rush": LocData(hit_type=HitType.umbrella, hookshot=True),
|
||||
"Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
|
||||
|
||||
"Bird Sanctuary": LocData(hookshot=True),
|
||||
"Wound-Up Windmill": LocData(hookshot=True),
|
||||
"The Illness has Speedrun": LocData(hookshot=True),
|
||||
"Community Rift: The Mountain Rift": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
|
||||
"Camera Tourist": LocData(misc_required=["Camera Badge"]),
|
||||
|
||||
"The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
|
||||
|
||||
"Rift Collapse - Deep Sea": LocData(hookshot=True),
|
||||
}
|
||||
|
||||
# Includes main objective requirements
|
||||
dw_bonus_requirements = {
|
||||
# Some One-Hit Hero requirements need badge pins as well because of Hookshot
|
||||
"So You're Back From Outer Space": LocData(required_hats=[HatType.SPRINT]),
|
||||
"Encore! Encore!": LocData(misc_required=["One-Hit Hero Badge"]),
|
||||
|
||||
"10 Seconds until Self-Destruct": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
|
||||
|
||||
"Boss Rush": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
|
||||
"Community Rift: Twilight Travels": LocData(required_hats=[HatType.BREWING]),
|
||||
|
||||
"Bird Sanctuary": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"], required_hats=[HatType.DWELLER]),
|
||||
"Wound-Up Windmill": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
|
||||
"The Illness has Speedrun": LocData(required_hats=[HatType.SPRINT]),
|
||||
|
||||
"The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]),
|
||||
|
||||
"Rift Collapse - Deep Sea": LocData(required_hats=[HatType.DWELLER]),
|
||||
}
|
||||
|
||||
dw_stamp_costs = {
|
||||
"So You're Back From Outer Space": 2,
|
||||
"Collect-a-thon": 5,
|
||||
"She Speedran from Outer Space": 8,
|
||||
"Encore! Encore!": 10,
|
||||
|
||||
"Security Breach": 4,
|
||||
"The Great Big Hootenanny": 7,
|
||||
"10 Seconds until Self-Destruct": 15,
|
||||
"Killing Two Birds": 25,
|
||||
"Snatcher Coins in Nyakuza Metro": 30,
|
||||
|
||||
"Speedrun Well": 10,
|
||||
"Boss Rush": 15,
|
||||
"Quality Time with Snatcher": 20,
|
||||
"Breaching the Contract": 40,
|
||||
|
||||
"Bird Sanctuary": 15,
|
||||
"Wound-Up Windmill": 30,
|
||||
"The Illness has Speedrun": 35,
|
||||
|
||||
"The Mustache Gauntlet": 35,
|
||||
"No More Bad Guys": 50,
|
||||
"Seal the Deal": 70,
|
||||
}
|
||||
|
||||
required_snatcher_coins = {
|
||||
"Snatcher Coins in Mafia Town": ["Snatcher Coin - Top of HQ", "Snatcher Coin - Top of Tower",
|
||||
"Snatcher Coin - Under Ruined Tower"],
|
||||
|
||||
"Snatcher Coins in Battle of the Birds": ["Snatcher Coin - Top of Red House", "Snatcher Coin - Train Rush",
|
||||
"Snatcher Coin - Picture Perfect"],
|
||||
|
||||
"Snatcher Coins in Subcon Forest": ["Snatcher Coin - Swamp Tree", "Snatcher Coin - Manor Roof",
|
||||
"Snatcher Coin - Giant Time Piece"],
|
||||
|
||||
"Snatcher Coins in Alpine Skyline": ["Snatcher Coin - Goat Village Top", "Snatcher Coin - Lava Cake",
|
||||
"Snatcher Coin - Windmill"],
|
||||
|
||||
"Snatcher Coins in Nyakuza Metro": ["Snatcher Coin - Green Clean Tower", "Snatcher Coin - Bluefin Cat Train",
|
||||
"Snatcher Coin - Pink Paw Fence"],
|
||||
}
|
||||
|
||||
|
||||
def set_dw_rules(world: "HatInTimeWorld"):
|
||||
if "Snatcher's Hit List" not in world.excluded_dws or "Camera Tourist" not in world.excluded_dws:
|
||||
set_enemy_rules(world)
|
||||
|
||||
dw_list: List[str] = []
|
||||
if world.options.DWShuffle:
|
||||
dw_list = world.dw_shuffle
|
||||
else:
|
||||
for name in death_wishes.keys():
|
||||
dw_list.append(name)
|
||||
|
||||
for name in dw_list:
|
||||
if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
|
||||
continue
|
||||
|
||||
dw = world.multiworld.get_region(name, world.player)
|
||||
if not world.options.DWShuffle and name in dw_stamp_costs.keys():
|
||||
for entrance in dw.entrances:
|
||||
add_rule(entrance, lambda state, n=name: state.has("Stamps", world.player, dw_stamp_costs[n]))
|
||||
|
||||
main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
|
||||
all_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
|
||||
main_stamp = world.multiworld.get_location(f"Main Stamp - {name}", world.player)
|
||||
bonus_stamps = world.multiworld.get_location(f"Bonus Stamps - {name}", world.player)
|
||||
if not world.options.DWEnableBonus:
|
||||
# place nothing, but let the locations exist still, so we can use them for bonus stamp rules
|
||||
all_clear.address = None
|
||||
all_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player))
|
||||
all_clear.show_in_spoiler = False
|
||||
|
||||
# No need for rules if excluded - stamps will be auto-granted
|
||||
if world.is_dw_excluded(name):
|
||||
continue
|
||||
|
||||
modify_dw_rules(world, name)
|
||||
add_dw_rules(world, main_objective)
|
||||
add_dw_rules(world, all_clear)
|
||||
add_rule(main_stamp, main_objective.access_rule)
|
||||
add_rule(all_clear, main_objective.access_rule)
|
||||
# Only set bonus stamp rules if we don't auto complete bonuses
|
||||
if not world.options.DWAutoCompleteBonuses and not world.is_bonus_excluded(all_clear.name):
|
||||
add_rule(bonus_stamps, all_clear.access_rule)
|
||||
|
||||
if world.options.DWShuffle:
|
||||
for i in range(len(world.dw_shuffle)-1):
|
||||
name = world.dw_shuffle[i+1]
|
||||
prev_dw = world.multiworld.get_region(world.dw_shuffle[i], world.player)
|
||||
entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player)
|
||||
add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player))
|
||||
else:
|
||||
for key, reqs in dw_prereqs.items():
|
||||
if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
|
||||
continue
|
||||
|
||||
access_rules: List[Callable[[CollectionState], bool]] = []
|
||||
entrances: List[Entrance] = []
|
||||
|
||||
for parent in reqs:
|
||||
entrance = world.multiworld.get_entrance(f"{parent} -> {key}", world.player)
|
||||
entrances.append(entrance)
|
||||
|
||||
if not world.is_dw_excluded(parent):
|
||||
access_rules.append(lambda state, n=parent: state.has(f"1 Stamp - {n}", world.player))
|
||||
|
||||
for entrance in entrances:
|
||||
for rule in access_rules:
|
||||
add_rule(entrance, rule)
|
||||
|
||||
if world.options.EndGoal == EndGoal.option_seal_the_deal:
|
||||
world.multiworld.completion_condition[world.player] = lambda state: \
|
||||
state.has("1 Stamp - Seal the Deal", world.player)
|
||||
|
||||
|
||||
def add_dw_rules(world: "HatInTimeWorld", loc: Location):
|
||||
bonus: bool = "All Clear" in loc.name
|
||||
if not bonus:
|
||||
data = dw_requirements.get(loc.name)
|
||||
else:
|
||||
data = dw_bonus_requirements.get(loc.name)
|
||||
|
||||
if data is None:
|
||||
return
|
||||
|
||||
if data.hookshot:
|
||||
add_rule(loc, lambda state: can_use_hookshot(state, world))
|
||||
|
||||
for hat in data.required_hats:
|
||||
add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h))
|
||||
|
||||
for misc in data.misc_required:
|
||||
add_rule(loc, lambda state, item=misc: state.has(item, world.player))
|
||||
|
||||
if data.paintings > 0 and world.options.ShuffleSubconPaintings:
|
||||
add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
|
||||
|
||||
if data.hit_type is not HitType.none and world.options.UmbrellaLogic:
|
||||
if data.hit_type == HitType.umbrella:
|
||||
add_rule(loc, lambda state: state.has("Umbrella", world.player))
|
||||
|
||||
elif data.hit_type == HitType.umbrella_or_brewing:
|
||||
add_rule(loc, lambda state: state.has("Umbrella", world.player)
|
||||
or can_use_hat(state, world, HatType.BREWING))
|
||||
|
||||
elif data.hit_type == HitType.dweller_bell:
|
||||
add_rule(loc, lambda state: state.has("Umbrella", world.player)
|
||||
or can_use_hat(state, world, HatType.BREWING)
|
||||
or can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
|
||||
def modify_dw_rules(world: "HatInTimeWorld", name: str):
|
||||
difficulty: Difficulty = get_difficulty(world)
|
||||
main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
|
||||
full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
|
||||
|
||||
if name == "The Illness has Speedrun":
|
||||
# All stamps with hookshot only in Expert
|
||||
if difficulty >= Difficulty.EXPERT:
|
||||
set_rule(full_clear, lambda state: True)
|
||||
else:
|
||||
add_rule(main_objective, lambda state: state.has("Umbrella", world.player))
|
||||
|
||||
elif name == "The Mustache Gauntlet":
|
||||
add_rule(main_objective, lambda state: state.has("Umbrella", world.player)
|
||||
or can_use_hat(state, world, HatType.ICE) or can_use_hat(state, world, HatType.BREWING))
|
||||
|
||||
elif name == "Vault Codes in the Wind":
|
||||
# Sprint is normally expected here
|
||||
if difficulty >= Difficulty.HARD:
|
||||
set_rule(main_objective, lambda state: True)
|
||||
|
||||
elif name == "Speedrun Well":
|
||||
# All stamps with nothing :)
|
||||
if difficulty >= Difficulty.EXPERT:
|
||||
set_rule(main_objective, lambda state: True)
|
||||
|
||||
elif name == "Mafia's Jumps":
|
||||
if difficulty >= Difficulty.HARD:
|
||||
set_rule(main_objective, lambda state: True)
|
||||
set_rule(full_clear, lambda state: True)
|
||||
|
||||
elif name == "So You're Back from Outer Space":
|
||||
# Without Hookshot
|
||||
if difficulty >= Difficulty.HARD:
|
||||
set_rule(main_objective, lambda state: True)
|
||||
|
||||
elif name == "Wound-Up Windmill":
|
||||
# No badge pin required. Player can switch to One Hit Hero after the checkpoint and do level without it.
|
||||
if difficulty >= Difficulty.MODERATE:
|
||||
set_rule(full_clear, lambda state: can_use_hookshot(state, world)
|
||||
and state.has("One-Hit Hero Badge", world.player))
|
||||
|
||||
if name in dw_candles:
|
||||
set_candle_dw_rules(name, world)
|
||||
|
||||
|
||||
def set_candle_dw_rules(name: str, world: "HatInTimeWorld"):
|
||||
main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
|
||||
full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
|
||||
|
||||
if name == "Zero Jumps":
|
||||
add_rule(main_objective, lambda state: state.has("Zero Jumps", world.player))
|
||||
add_rule(full_clear, lambda state: state.has("Zero Jumps", world.player, 4)
|
||||
and state.has("Train Rush (Zero Jumps)", world.player) and can_use_hat(state, world, HatType.ICE))
|
||||
|
||||
# No Ice Hat/painting required in Expert for Toilet Zero Jumps
|
||||
# This painting wall can only be skipped via cherry hover.
|
||||
if get_difficulty(world) < Difficulty.EXPERT or world.options.NoPaintingSkips:
|
||||
set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
|
||||
and has_paintings(state, world, 1, False))
|
||||
else:
|
||||
set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world))
|
||||
|
||||
set_rule(world.multiworld.get_location("Contractual Obligations (Zero Jumps)", world.player),
|
||||
lambda state: has_paintings(state, world, 1, False))
|
||||
|
||||
elif name == "Snatcher's Hit List":
|
||||
add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player))
|
||||
add_rule(full_clear, lambda state: state.has("Enemy", world.player, 12))
|
||||
|
||||
elif name == "Camera Tourist":
|
||||
add_rule(main_objective, lambda state: state.has("Enemy", world.player, 8))
|
||||
add_rule(full_clear, lambda state: state.has("Boss", world.player, 6)
|
||||
and state.has("Triple Enemy Photo", world.player))
|
||||
|
||||
elif "Snatcher Coins" in name:
|
||||
coins: List[str] = []
|
||||
for coin in required_snatcher_coins[name]:
|
||||
coins.append(coin)
|
||||
add_rule(full_clear, lambda state, c=coin: state.has(c, world.player))
|
||||
|
||||
# any coin works for the main objective
|
||||
add_rule(main_objective, lambda state: state.has(coins[0], world.player)
|
||||
or state.has(coins[1], world.player)
|
||||
or state.has(coins[2], world.player))
|
||||
|
||||
|
||||
def create_enemy_events(world: "HatInTimeWorld"):
|
||||
no_tourist = "Camera Tourist" in world.excluded_dws
|
||||
for enemy, regions in hit_list.items():
|
||||
if no_tourist and enemy in bosses:
|
||||
continue
|
||||
|
||||
for area in regions:
|
||||
if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1():
|
||||
continue
|
||||
|
||||
if area == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour):
|
||||
continue
|
||||
|
||||
if area == "Bluefin Tunnel" and not world.is_dlc2():
|
||||
continue
|
||||
|
||||
if world.options.DWShuffle and area in death_wishes.keys() and area not in world.dw_shuffle:
|
||||
continue
|
||||
|
||||
region = world.multiworld.get_region(area, world.player)
|
||||
event = HatInTimeLocation(world.player, f"{enemy} - {area}", None, region)
|
||||
event.place_locked_item(HatInTimeItem(enemy, ItemClassification.progression, None, world.player))
|
||||
region.locations.append(event)
|
||||
event.show_in_spoiler = False
|
||||
|
||||
for name in triple_enemy_locations:
|
||||
if name == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour):
|
||||
continue
|
||||
|
||||
if world.options.DWShuffle and name in death_wishes.keys() and name not in world.dw_shuffle:
|
||||
continue
|
||||
|
||||
region = world.multiworld.get_region(name, world.player)
|
||||
event = HatInTimeLocation(world.player, f"Triple Enemy Photo - {name}", None, region)
|
||||
event.place_locked_item(HatInTimeItem("Triple Enemy Photo", ItemClassification.progression, None, world.player))
|
||||
region.locations.append(event)
|
||||
event.show_in_spoiler = False
|
||||
if name == "The Mustache Gauntlet":
|
||||
add_rule(event, lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
|
||||
def set_enemy_rules(world: "HatInTimeWorld"):
|
||||
no_tourist = "Camera Tourist" in world.excluded_dws or "Camera Tourist" in world.excluded_bonuses
|
||||
|
||||
for enemy, regions in hit_list.items():
|
||||
if no_tourist and enemy in bosses:
|
||||
continue
|
||||
|
||||
for area in regions:
|
||||
if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1():
|
||||
continue
|
||||
|
||||
if area == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour):
|
||||
continue
|
||||
|
||||
if area == "Bluefin Tunnel" and not world.is_dlc2():
|
||||
continue
|
||||
|
||||
if world.options.DWShuffle and area in death_wishes and area not in world.dw_shuffle:
|
||||
continue
|
||||
|
||||
event = world.multiworld.get_location(f"{enemy} - {area}", world.player)
|
||||
|
||||
if enemy == "Toxic Flower":
|
||||
add_rule(event, lambda state: can_use_hookshot(state, world))
|
||||
|
||||
if area == "The Illness has Spread":
|
||||
add_rule(event, lambda state: not zipline_logic(world) or
|
||||
state.has("Zipline Unlock - The Birdhouse Path", world.player)
|
||||
or state.has("Zipline Unlock - The Lava Cake Path", world.player)
|
||||
or state.has("Zipline Unlock - The Windmill Path", world.player))
|
||||
|
||||
elif enemy == "Director":
|
||||
if area == "Dead Bird Studio Basement":
|
||||
add_rule(event, lambda state: can_use_hookshot(state, world))
|
||||
|
||||
elif enemy == "Snatcher" or enemy == "Mustache Girl":
|
||||
if area == "Boss Rush":
|
||||
# need to be able to kill toilet and snatcher
|
||||
add_rule(event, lambda state: can_hit(state, world) and can_use_hookshot(state, world))
|
||||
if enemy == "Mustache Girl":
|
||||
add_rule(event, lambda state: can_hit(state, world, True) and can_use_hookshot(state, world))
|
||||
|
||||
elif area == "The Finale" and enemy == "Mustache Girl":
|
||||
add_rule(event, lambda state: can_use_hookshot(state, world)
|
||||
and can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
elif enemy == "Shock Squid" or enemy == "Ninja Cat":
|
||||
if area == "Time Rift - Deep Sea":
|
||||
add_rule(event, lambda state: can_use_hookshot(state, world))
|
||||
|
||||
|
||||
# Enemies for Snatcher's Hit List/Camera Tourist, and where to find them
|
||||
hit_list = {
|
||||
"Mafia Goon": ["Mafia Town Area", "Time Rift - Mafia of Cooks", "Time Rift - Tour",
|
||||
"Bon Voyage!", "The Mustache Gauntlet", "Rift Collapse: Mafia of Cooks",
|
||||
"So You're Back From Outer Space"],
|
||||
|
||||
"Sleepy Raccoon": ["She Came from Outer Space", "Down with the Mafia!", "The Twilight Bell",
|
||||
"She Speedran from Outer Space", "Mafia's Jumps", "The Mustache Gauntlet",
|
||||
"Time Rift - Sleepy Subcon", "Rift Collapse: Sleepy Subcon"],
|
||||
|
||||
"UFO": ["Picture Perfect", "So You're Back From Outer Space", "Community Rift: Rhythm Jump Studio"],
|
||||
|
||||
"Rat": ["Down with the Mafia!", "Bluefin Tunnel"],
|
||||
|
||||
"Shock Squid": ["Bon Voyage!", "Time Rift - Sleepy Subcon", "Time Rift - Deep Sea",
|
||||
"Rift Collapse: Sleepy Subcon"],
|
||||
|
||||
"Shromb Egg": ["The Birdhouse", "Bird Sanctuary"],
|
||||
|
||||
"Spider": ["Subcon Forest Area", "The Mustache Gauntlet", "Speedrun Well",
|
||||
"The Lava Cake", "The Windmill"],
|
||||
|
||||
"Crow": ["Mafia Town Area", "The Birdhouse", "Time Rift - Tour", "Bird Sanctuary",
|
||||
"Time Rift - Alpine Skyline", "Rift Collapse: Alpine Skyline"],
|
||||
|
||||
"Pompous Crow": ["The Birdhouse", "Time Rift - The Lab", "Bird Sanctuary", "The Mustache Gauntlet"],
|
||||
|
||||
"Fiery Crow": ["The Finale", "The Lava Cake", "The Mustache Gauntlet"],
|
||||
|
||||
"Express Owl": ["The Finale", "Time Rift - The Owl Express", "Time Rift - Deep Sea"],
|
||||
|
||||
"Ninja Cat": ["The Birdhouse", "The Windmill", "Bluefin Tunnel", "The Mustache Gauntlet",
|
||||
"Time Rift - Curly Tail Trail", "Time Rift - Alpine Skyline", "Time Rift - Deep Sea",
|
||||
"Rift Collapse: Alpine Skyline"],
|
||||
|
||||
# Bosses
|
||||
"Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"],
|
||||
|
||||
"Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"],
|
||||
"Toilet": ["Toilet of Doom", "Boss Rush"],
|
||||
|
||||
"Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush",
|
||||
"Quality Time with Snatcher"],
|
||||
|
||||
"Toxic Flower": ["The Illness has Spread", "The Illness has Speedrun"],
|
||||
|
||||
"Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"],
|
||||
}
|
||||
|
||||
# Camera Tourist has a bonus that requires getting three different types of enemies in one photo.
|
||||
triple_enemy_locations = [
|
||||
"She Came from Outer Space",
|
||||
"She Speedran from Outer Space",
|
||||
"Mafia's Jumps",
|
||||
"The Mustache Gauntlet",
|
||||
"The Birdhouse",
|
||||
"Bird Sanctuary",
|
||||
"Time Rift - Tour",
|
||||
]
|
||||
|
||||
bosses = [
|
||||
"Mafia Boss",
|
||||
"Conductor",
|
||||
"Toilet",
|
||||
"Snatcher",
|
||||
"Toxic Flower",
|
||||
"Mustache Girl",
|
||||
]
|
||||
302
worlds/ahit/Items.py
Normal file
302
worlds/ahit/Items.py
Normal file
@@ -0,0 +1,302 @@
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem
|
||||
from .Locations import get_total_locations
|
||||
from .Rules import get_difficulty
|
||||
from .Options import get_total_time_pieces, CTRLogic
|
||||
from typing import List, Dict, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import HatInTimeWorld
|
||||
|
||||
|
||||
def create_itempool(world: "HatInTimeWorld") -> List[Item]:
|
||||
itempool: List[Item] = []
|
||||
if world.has_yarn():
|
||||
yarn_pool: List[Item] = create_multiple_items(world, "Yarn",
|
||||
world.options.YarnAvailable.value,
|
||||
ItemClassification.progression_skip_balancing)
|
||||
|
||||
for i in range(int(len(yarn_pool) * (0.01 * world.options.YarnBalancePercent))):
|
||||
yarn_pool[i].classification = ItemClassification.progression
|
||||
|
||||
itempool += yarn_pool
|
||||
|
||||
for name in item_table.keys():
|
||||
if name == "Yarn":
|
||||
continue
|
||||
|
||||
if not item_dlc_enabled(world, name):
|
||||
continue
|
||||
|
||||
if not world.options.HatItems and name in hat_type_to_item.values():
|
||||
continue
|
||||
|
||||
item_type: ItemClassification = item_table.get(name).classification
|
||||
|
||||
if world.is_dw_only():
|
||||
if item_type is ItemClassification.progression \
|
||||
or item_type is ItemClassification.progression_skip_balancing:
|
||||
continue
|
||||
else:
|
||||
if name == "Scooter Badge":
|
||||
if world.options.CTRLogic is CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE:
|
||||
item_type = ItemClassification.progression
|
||||
elif name == "No Bonk Badge" and world.is_dw():
|
||||
item_type = ItemClassification.progression
|
||||
|
||||
# some death wish bonuses require one hit hero + hookshot
|
||||
if world.is_dw() and name == "Badge Pin" and not world.is_dw_only():
|
||||
item_type = ItemClassification.progression
|
||||
|
||||
if item_type is ItemClassification.filler or item_type is ItemClassification.trap:
|
||||
continue
|
||||
|
||||
if name in act_contracts.keys() and not world.options.ShuffleActContracts:
|
||||
continue
|
||||
|
||||
if name in alps_hooks.keys() and not world.options.ShuffleAlpineZiplines:
|
||||
continue
|
||||
|
||||
if name == "Progressive Painting Unlock" and not world.options.ShuffleSubconPaintings:
|
||||
continue
|
||||
|
||||
if world.options.StartWithCompassBadge and name == "Compass Badge":
|
||||
continue
|
||||
|
||||
if name == "Time Piece":
|
||||
tp_list: List[Item] = create_multiple_items(world, name, get_total_time_pieces(world), item_type)
|
||||
for i in range(int(len(tp_list) * (0.01 * world.options.TimePieceBalancePercent))):
|
||||
tp_list[i].classification = ItemClassification.progression
|
||||
|
||||
itempool += tp_list
|
||||
continue
|
||||
|
||||
itempool += create_multiple_items(world, name, item_frequencies.get(name, 1), item_type)
|
||||
|
||||
itempool += create_junk_items(world, get_total_locations(world) - len(itempool))
|
||||
return itempool
|
||||
|
||||
|
||||
def calculate_yarn_costs(world: "HatInTimeWorld"):
|
||||
min_yarn_cost = int(min(world.options.YarnCostMin.value, world.options.YarnCostMax.value))
|
||||
max_yarn_cost = int(max(world.options.YarnCostMin.value, world.options.YarnCostMax.value))
|
||||
|
||||
max_cost = 0
|
||||
for i in range(5):
|
||||
hat: HatType = HatType(i)
|
||||
if not world.is_hat_precollected(hat):
|
||||
cost: int = world.random.randint(min_yarn_cost, max_yarn_cost)
|
||||
world.hat_yarn_costs[hat] = cost
|
||||
max_cost += cost
|
||||
else:
|
||||
world.hat_yarn_costs[hat] = 0
|
||||
|
||||
available_yarn: int = world.options.YarnAvailable.value
|
||||
if max_cost > available_yarn:
|
||||
world.options.YarnAvailable.value = max_cost
|
||||
available_yarn = max_cost
|
||||
|
||||
extra_yarn = max_cost + world.options.MinExtraYarn - available_yarn
|
||||
if extra_yarn > 0:
|
||||
world.options.YarnAvailable.value += extra_yarn
|
||||
|
||||
|
||||
def item_dlc_enabled(world: "HatInTimeWorld", name: str) -> bool:
|
||||
data = item_table[name]
|
||||
|
||||
if data.dlc_flags == HatDLC.none:
|
||||
return True
|
||||
elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1():
|
||||
return True
|
||||
elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2():
|
||||
return True
|
||||
elif data.dlc_flags == HatDLC.death_wish and world.is_dw():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def create_item(world: "HatInTimeWorld", name: str) -> Item:
|
||||
data = item_table[name]
|
||||
return HatInTimeItem(name, data.classification, data.code, world.player)
|
||||
|
||||
|
||||
def create_multiple_items(world: "HatInTimeWorld", name: str, count: int = 1,
|
||||
item_type: ItemClassification = ItemClassification.progression) -> List[Item]:
|
||||
|
||||
data = item_table[name]
|
||||
itemlist: List[Item] = []
|
||||
|
||||
for i in range(count):
|
||||
itemlist += [HatInTimeItem(name, item_type, data.code, world.player)]
|
||||
|
||||
return itemlist
|
||||
|
||||
|
||||
def create_junk_items(world: "HatInTimeWorld", count: int) -> List[Item]:
|
||||
trap_chance = world.options.TrapChance.value
|
||||
junk_pool: List[Item] = []
|
||||
junk_list: Dict[str, int] = {}
|
||||
trap_list: Dict[str, int] = {}
|
||||
ic: ItemClassification
|
||||
|
||||
for name in item_table.keys():
|
||||
ic = item_table[name].classification
|
||||
if ic == ItemClassification.filler:
|
||||
if world.is_dw_only() and "Pons" in name:
|
||||
continue
|
||||
|
||||
junk_list[name] = junk_weights.get(name)
|
||||
|
||||
elif trap_chance > 0 and ic == ItemClassification.trap:
|
||||
if name == "Baby Trap":
|
||||
trap_list[name] = world.options.BabyTrapWeight.value
|
||||
elif name == "Laser Trap":
|
||||
trap_list[name] = world.options.LaserTrapWeight.value
|
||||
elif name == "Parade Trap":
|
||||
trap_list[name] = world.options.ParadeTrapWeight.value
|
||||
|
||||
for i in range(count):
|
||||
if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance:
|
||||
junk_pool.append(world.create_item(
|
||||
world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0]))
|
||||
else:
|
||||
junk_pool.append(world.create_item(
|
||||
world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0]))
|
||||
|
||||
return junk_pool
|
||||
|
||||
|
||||
def get_shop_trap_name(world: "HatInTimeWorld") -> str:
|
||||
rand = world.random.randint(1, 9)
|
||||
name = ""
|
||||
if rand == 1:
|
||||
name = "Time Plece"
|
||||
elif rand == 2:
|
||||
name = "Time Piece (Trust me bro)"
|
||||
elif rand == 3:
|
||||
name = "TimePiece"
|
||||
elif rand == 4:
|
||||
name = "Time Piece?"
|
||||
elif rand == 5:
|
||||
name = "Time Pizza"
|
||||
elif rand == 6:
|
||||
name = "Time piece"
|
||||
elif rand == 7:
|
||||
name = "TIme Piece"
|
||||
elif rand == 8:
|
||||
name = "Time Piece (maybe)"
|
||||
elif rand == 9:
|
||||
name = "Time Piece ;)"
|
||||
|
||||
return name
|
||||
|
||||
|
||||
ahit_items = {
|
||||
"Yarn": ItemData(2000300001, ItemClassification.progression_skip_balancing),
|
||||
"Time Piece": ItemData(2000300002, ItemClassification.progression_skip_balancing),
|
||||
|
||||
# for HatItems option
|
||||
"Sprint Hat": ItemData(2000300049, ItemClassification.progression),
|
||||
"Brewing Hat": ItemData(2000300050, ItemClassification.progression),
|
||||
"Ice Hat": ItemData(2000300051, ItemClassification.progression),
|
||||
"Dweller Mask": ItemData(2000300052, ItemClassification.progression),
|
||||
"Time Stop Hat": ItemData(2000300053, ItemClassification.progression),
|
||||
|
||||
# Badges
|
||||
"Projectile Badge": ItemData(2000300024, ItemClassification.useful),
|
||||
"Fast Hatter Badge": ItemData(2000300025, ItemClassification.useful),
|
||||
"Hover Badge": ItemData(2000300026, ItemClassification.useful),
|
||||
"Hookshot Badge": ItemData(2000300027, ItemClassification.progression),
|
||||
"Item Magnet Badge": ItemData(2000300028, ItemClassification.useful),
|
||||
"No Bonk Badge": ItemData(2000300029, ItemClassification.useful),
|
||||
"Compass Badge": ItemData(2000300030, ItemClassification.useful),
|
||||
"Scooter Badge": ItemData(2000300031, ItemClassification.useful),
|
||||
"One-Hit Hero Badge": ItemData(2000300038, ItemClassification.progression, HatDLC.death_wish),
|
||||
"Camera Badge": ItemData(2000300042, ItemClassification.progression, HatDLC.death_wish),
|
||||
|
||||
# Relics
|
||||
"Relic (Burger Patty)": ItemData(2000300006, ItemClassification.progression),
|
||||
"Relic (Burger Cushion)": ItemData(2000300007, ItemClassification.progression),
|
||||
"Relic (Mountain Set)": ItemData(2000300008, ItemClassification.progression),
|
||||
"Relic (Train)": ItemData(2000300009, ItemClassification.progression),
|
||||
"Relic (UFO)": ItemData(2000300010, ItemClassification.progression),
|
||||
"Relic (Cow)": ItemData(2000300011, ItemClassification.progression),
|
||||
"Relic (Cool Cow)": ItemData(2000300012, ItemClassification.progression),
|
||||
"Relic (Tin-foil Hat Cow)": ItemData(2000300013, ItemClassification.progression),
|
||||
"Relic (Crayon Box)": ItemData(2000300014, ItemClassification.progression),
|
||||
"Relic (Red Crayon)": ItemData(2000300015, ItemClassification.progression),
|
||||
"Relic (Blue Crayon)": ItemData(2000300016, ItemClassification.progression),
|
||||
"Relic (Green Crayon)": ItemData(2000300017, ItemClassification.progression),
|
||||
# DLC
|
||||
"Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1),
|
||||
"Relic (Shortcake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1),
|
||||
"Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1),
|
||||
"Relic (Chocolate Cake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1),
|
||||
"Relic (Necklace Bust)": ItemData(2000300022, ItemClassification.progression, HatDLC.dlc2),
|
||||
"Relic (Necklace)": ItemData(2000300023, ItemClassification.progression, HatDLC.dlc2),
|
||||
|
||||
# Garbage items
|
||||
"25 Pons": ItemData(2000300034, ItemClassification.filler),
|
||||
"50 Pons": ItemData(2000300035, ItemClassification.filler),
|
||||
"100 Pons": ItemData(2000300036, ItemClassification.filler),
|
||||
"Health Pon": ItemData(2000300037, ItemClassification.filler),
|
||||
"Random Cosmetic": ItemData(2000300044, ItemClassification.filler),
|
||||
|
||||
# Traps
|
||||
"Baby Trap": ItemData(2000300039, ItemClassification.trap),
|
||||
"Laser Trap": ItemData(2000300040, ItemClassification.trap),
|
||||
"Parade Trap": ItemData(2000300041, ItemClassification.trap),
|
||||
|
||||
# Other
|
||||
"Badge Pin": ItemData(2000300043, ItemClassification.useful),
|
||||
"Umbrella": ItemData(2000300033, ItemClassification.progression),
|
||||
"Progressive Painting Unlock": ItemData(2000300003, ItemClassification.progression),
|
||||
# DLC
|
||||
"Metro Ticket - Yellow": ItemData(2000300045, ItemClassification.progression, HatDLC.dlc2),
|
||||
"Metro Ticket - Green": ItemData(2000300046, ItemClassification.progression, HatDLC.dlc2),
|
||||
"Metro Ticket - Blue": ItemData(2000300047, ItemClassification.progression, HatDLC.dlc2),
|
||||
"Metro Ticket - Pink": ItemData(2000300048, ItemClassification.progression, HatDLC.dlc2),
|
||||
}
|
||||
|
||||
act_contracts = {
|
||||
"Snatcher's Contract - The Subcon Well": ItemData(2000300200, ItemClassification.progression),
|
||||
"Snatcher's Contract - Toilet of Doom": ItemData(2000300201, ItemClassification.progression),
|
||||
"Snatcher's Contract - Queen Vanessa's Manor": ItemData(2000300202, ItemClassification.progression),
|
||||
"Snatcher's Contract - Mail Delivery Service": ItemData(2000300203, ItemClassification.progression),
|
||||
}
|
||||
|
||||
alps_hooks = {
|
||||
"Zipline Unlock - The Birdhouse Path": ItemData(2000300204, ItemClassification.progression),
|
||||
"Zipline Unlock - The Lava Cake Path": ItemData(2000300205, ItemClassification.progression),
|
||||
"Zipline Unlock - The Windmill Path": ItemData(2000300206, ItemClassification.progression),
|
||||
"Zipline Unlock - The Twilight Bell Path": ItemData(2000300207, ItemClassification.progression),
|
||||
}
|
||||
|
||||
relic_groups = {
|
||||
"Burger": {"Relic (Burger Patty)", "Relic (Burger Cushion)"},
|
||||
"Train": {"Relic (Mountain Set)", "Relic (Train)"},
|
||||
"UFO": {"Relic (UFO)", "Relic (Cow)", "Relic (Cool Cow)", "Relic (Tin-foil Hat Cow)"},
|
||||
"Crayon": {"Relic (Crayon Box)", "Relic (Red Crayon)", "Relic (Blue Crayon)", "Relic (Green Crayon)"},
|
||||
"Cake": {"Relic (Cake Stand)", "Relic (Chocolate Cake)", "Relic (Chocolate Cake Slice)", "Relic (Shortcake)"},
|
||||
"Necklace": {"Relic (Necklace Bust)", "Relic (Necklace)"},
|
||||
}
|
||||
|
||||
item_frequencies = {
|
||||
"Badge Pin": 2,
|
||||
"Progressive Painting Unlock": 3,
|
||||
}
|
||||
|
||||
junk_weights = {
|
||||
"25 Pons": 50,
|
||||
"50 Pons": 25,
|
||||
"100 Pons": 10,
|
||||
"Health Pon": 35,
|
||||
"Random Cosmetic": 35,
|
||||
}
|
||||
|
||||
item_table = {
|
||||
**ahit_items,
|
||||
**act_contracts,
|
||||
**alps_hooks,
|
||||
}
|
||||
1057
worlds/ahit/Locations.py
Normal file
1057
worlds/ahit/Locations.py
Normal file
File diff suppressed because it is too large
Load Diff
770
worlds/ahit/Options.py
Normal file
770
worlds/ahit/Options.py
Normal file
@@ -0,0 +1,770 @@
|
||||
from typing import List, TYPE_CHECKING, Dict, Any
|
||||
from schema import Schema, Optional
|
||||
from dataclasses import dataclass
|
||||
from worlds.AutoWorld import PerGameCommonOptions
|
||||
from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import HatInTimeWorld
|
||||
|
||||
|
||||
def create_option_groups() -> List[OptionGroup]:
|
||||
option_group_list: List[OptionGroup] = []
|
||||
for name, options in ahit_option_groups.items():
|
||||
option_group_list.append(OptionGroup(name=name, options=options))
|
||||
|
||||
return option_group_list
|
||||
|
||||
|
||||
def adjust_options(world: "HatInTimeWorld"):
|
||||
if world.options.HighestChapterCost < world.options.LowestChapterCost:
|
||||
world.options.HighestChapterCost.value, world.options.LowestChapterCost.value = \
|
||||
world.options.LowestChapterCost.value, world.options.HighestChapterCost.value
|
||||
|
||||
if world.options.FinalChapterMaxCost < world.options.FinalChapterMinCost:
|
||||
world.options.FinalChapterMaxCost.value, world.options.FinalChapterMinCost.value = \
|
||||
world.options.FinalChapterMinCost.value, world.options.FinalChapterMaxCost.value
|
||||
|
||||
if world.options.BadgeSellerMaxItems < world.options.BadgeSellerMinItems:
|
||||
world.options.BadgeSellerMaxItems.value, world.options.BadgeSellerMinItems.value = \
|
||||
world.options.BadgeSellerMinItems.value, world.options.BadgeSellerMaxItems.value
|
||||
|
||||
if world.options.NyakuzaThugMaxShopItems < world.options.NyakuzaThugMinShopItems:
|
||||
world.options.NyakuzaThugMaxShopItems.value, world.options.NyakuzaThugMinShopItems.value = \
|
||||
world.options.NyakuzaThugMinShopItems.value, world.options.NyakuzaThugMaxShopItems.value
|
||||
|
||||
if world.options.DWShuffleCountMax < world.options.DWShuffleCountMin:
|
||||
world.options.DWShuffleCountMax.value, world.options.DWShuffleCountMin.value = \
|
||||
world.options.DWShuffleCountMin.value, world.options.DWShuffleCountMax.value
|
||||
|
||||
total_tps: int = get_total_time_pieces(world)
|
||||
if world.options.HighestChapterCost > total_tps-5:
|
||||
world.options.HighestChapterCost.value = min(45, total_tps-5)
|
||||
|
||||
if world.options.LowestChapterCost > total_tps-5:
|
||||
world.options.LowestChapterCost.value = min(45, total_tps-5)
|
||||
|
||||
if world.options.FinalChapterMaxCost > total_tps:
|
||||
world.options.FinalChapterMaxCost.value = min(50, total_tps)
|
||||
|
||||
if world.options.FinalChapterMinCost > total_tps:
|
||||
world.options.FinalChapterMinCost.value = min(50, total_tps)
|
||||
|
||||
if world.is_dlc1() and world.options.ShipShapeCustomTaskGoal <= 0:
|
||||
# automatically determine task count based on Tasksanity settings
|
||||
if world.options.Tasksanity:
|
||||
world.options.ShipShapeCustomTaskGoal.value = world.options.TasksanityCheckCount * world.options.TasksanityTaskStep
|
||||
else:
|
||||
world.options.ShipShapeCustomTaskGoal.value = 18
|
||||
|
||||
# Don't allow Rush Hour goal if DLC2 content is disabled
|
||||
if world.options.EndGoal == EndGoal.option_rush_hour and not world.options.EnableDLC2:
|
||||
world.options.EndGoal.value = EndGoal.option_finale
|
||||
|
||||
# Don't allow Seal the Deal goal if Death Wish content is disabled
|
||||
if world.options.EndGoal == EndGoal.option_seal_the_deal and not world.is_dw():
|
||||
world.options.EndGoal.value = EndGoal.option_finale
|
||||
|
||||
if world.options.DWEnableBonus:
|
||||
world.options.DWAutoCompleteBonuses.value = 0
|
||||
|
||||
if world.is_dw_only():
|
||||
world.options.EndGoal.value = EndGoal.option_seal_the_deal
|
||||
world.options.ActRandomizer.value = 0
|
||||
world.options.ShuffleAlpineZiplines.value = 0
|
||||
world.options.ShuffleSubconPaintings.value = 0
|
||||
world.options.ShuffleStorybookPages.value = 0
|
||||
world.options.ShuffleActContracts.value = 0
|
||||
world.options.EnableDLC1.value = 0
|
||||
world.options.LogicDifficulty.value = LogicDifficulty.option_normal
|
||||
world.options.DWTimePieceRequirement.value = 0
|
||||
|
||||
|
||||
def get_total_time_pieces(world: "HatInTimeWorld") -> int:
|
||||
count: int = 40
|
||||
if world.is_dlc1():
|
||||
count += 6
|
||||
|
||||
if world.is_dlc2():
|
||||
count += 10
|
||||
|
||||
return min(40+world.options.MaxExtraTimePieces, count)
|
||||
|
||||
|
||||
class EndGoal(Choice):
|
||||
"""The end goal required to beat the game.
|
||||
Finale: Reach Time's End and beat Mustache Girl. The Finale will be in its vanilla location.
|
||||
|
||||
Rush Hour: Reach and complete Rush Hour. The level will be in its vanilla location and Chapter 7
|
||||
will be the final chapter. You also must find Nyakuza Metro itself and complete all of its levels.
|
||||
Requires DLC2 content to be enabled.
|
||||
|
||||
Seal the Deal: Reach and complete the Seal the Deal death wish main objective.
|
||||
Requires Death Wish content to be enabled."""
|
||||
display_name = "End Goal"
|
||||
option_finale = 1
|
||||
option_rush_hour = 2
|
||||
option_seal_the_deal = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class ActRandomizer(Choice):
|
||||
"""If enabled, shuffle the game's Acts between each other.
|
||||
Light will cause Time Rifts to only be shuffled amongst each other,
|
||||
and Blue Time Rifts and Purple Time Rifts to be shuffled separately."""
|
||||
display_name = "Shuffle Acts"
|
||||
option_false = 0
|
||||
option_light = 1
|
||||
option_insanity = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class ActPlando(OptionDict):
|
||||
"""Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\" will place Alpine Free Roam
|
||||
at Train Rush."""
|
||||
display_name = "Act Plando"
|
||||
schema = Schema({
|
||||
Optional(str): str
|
||||
})
|
||||
|
||||
|
||||
class ActBlacklist(OptionDict):
|
||||
"""Blacklist acts from being shuffled onto other acts. Multiple can be listed per act.
|
||||
For example, \"Barrel Battle\": [\"The Big Parade\", \"Dead Bird Studio\"]
|
||||
will prevent The Big Parade and Dead Bird Studio from being shuffled onto Barrel Battle."""
|
||||
display_name = "Act Blacklist"
|
||||
schema = Schema({
|
||||
Optional(str): list
|
||||
})
|
||||
|
||||
|
||||
class FinaleShuffle(Toggle):
|
||||
"""If enabled, chapter finales will only be shuffled amongst each other in act shuffle."""
|
||||
display_name = "Finale Shuffle"
|
||||
|
||||
|
||||
class LogicDifficulty(Choice):
|
||||
"""Choose the difficulty setting for logic.
|
||||
For an exhaustive list of all logic tricks for each difficulty, see this Google Doc:
|
||||
https://docs.google.com/document/d/1x9VLSQ5davfx1KGamR9T0mD5h69_lDXJ6H7Gq7knJRI/edit?usp=sharing"""
|
||||
display_name = "Logic Difficulty"
|
||||
option_normal = -1
|
||||
option_moderate = 0
|
||||
option_hard = 1
|
||||
option_expert = 2
|
||||
default = -1
|
||||
|
||||
|
||||
class CTRLogic(Choice):
|
||||
"""Choose how you want to logically clear Cheating the Race."""
|
||||
display_name = "Cheating the Race Logic"
|
||||
option_time_stop_only = 0
|
||||
option_scooter = 1
|
||||
option_sprint = 2
|
||||
option_nothing = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class RandomizeHatOrder(Choice):
|
||||
"""Randomize the order that hats are stitched in.
|
||||
Time Stop Last will force Time Stop to be the last hat in the sequence."""
|
||||
display_name = "Randomize Hat Order"
|
||||
option_false = 0
|
||||
option_true = 1
|
||||
option_time_stop_last = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class YarnBalancePercent(Range):
|
||||
"""How much (in percentage) of the yarn in the pool that will be progression balanced."""
|
||||
display_name = "Yarn Balance Percentage"
|
||||
default = 20
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
||||
|
||||
class TimePieceBalancePercent(Range):
|
||||
"""How much (in percentage) of time pieces in the pool that will be progression balanced."""
|
||||
display_name = "Time Piece Balance Percentage"
|
||||
default = 35
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
||||
|
||||
class StartWithCompassBadge(DefaultOnToggle):
|
||||
"""If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world
|
||||
(instead of just Relics). Recommended if you're not familiar with where item locations are."""
|
||||
display_name = "Start with Compass Badge"
|
||||
|
||||
|
||||
class CompassBadgeMode(Choice):
|
||||
"""closest - Compass Badge points to the closest item regardless of classification
|
||||
important_only - Compass Badge points to progression/useful items only
|
||||
important_first - Compass Badge points to progression/useful items first, then it will point to junk items"""
|
||||
display_name = "Compass Badge Mode"
|
||||
option_closest = 1
|
||||
option_important_only = 2
|
||||
option_important_first = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class UmbrellaLogic(Toggle):
|
||||
"""Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful"""
|
||||
display_name = "Umbrella Logic"
|
||||
|
||||
|
||||
class ShuffleStorybookPages(DefaultOnToggle):
|
||||
"""If enabled, each storybook page in the purple Time Rifts is an item check.
|
||||
The Compass Badge can track these down for you."""
|
||||
display_name = "Shuffle Storybook Pages"
|
||||
|
||||
|
||||
class ShuffleActContracts(DefaultOnToggle):
|
||||
"""If enabled, shuffle Snatcher's act contracts into the pool as items"""
|
||||
display_name = "Shuffle Contracts"
|
||||
|
||||
|
||||
class ShuffleAlpineZiplines(Toggle):
|
||||
"""If enabled, Alpine's zipline paths leading to the peaks will be locked behind items."""
|
||||
display_name = "Shuffle Alpine Ziplines"
|
||||
|
||||
|
||||
class ShuffleSubconPaintings(Toggle):
|
||||
"""If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings.
|
||||
These items are progressive, with the order of Village-Swamp-Courtyard."""
|
||||
display_name = "Shuffle Subcon Paintings"
|
||||
|
||||
|
||||
class NoPaintingSkips(Toggle):
|
||||
"""If enabled, prevent Subcon fire wall skips from being in logic on higher difficulty settings."""
|
||||
display_name = "No Subcon Fire Wall Skips"
|
||||
|
||||
|
||||
class StartingChapter(Choice):
|
||||
"""Determines which chapter you will be guaranteed to be able to enter at the beginning of the game."""
|
||||
display_name = "Starting Chapter"
|
||||
option_1 = 1
|
||||
option_2 = 2
|
||||
option_3 = 3
|
||||
option_4 = 4
|
||||
default = 1
|
||||
|
||||
|
||||
class ChapterCostIncrement(Range):
|
||||
"""Lower values mean chapter costs increase slower. Higher values make the cost differences more steep."""
|
||||
display_name = "Chapter Cost Increment"
|
||||
range_start = 1
|
||||
range_end = 8
|
||||
default = 4
|
||||
|
||||
|
||||
class ChapterCostMinDifference(Range):
|
||||
"""The minimum difference between chapter costs."""
|
||||
display_name = "Minimum Chapter Cost Difference"
|
||||
range_start = 1
|
||||
range_end = 8
|
||||
default = 4
|
||||
|
||||
|
||||
class LowestChapterCost(Range):
|
||||
"""Value determining the lowest possible cost for a chapter.
|
||||
Chapter costs will, progressively, be calculated based on this value (except for the final chapter)."""
|
||||
display_name = "Lowest Possible Chapter Cost"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 5
|
||||
|
||||
|
||||
class HighestChapterCost(Range):
|
||||
"""Value determining the highest possible cost for a chapter.
|
||||
Chapter costs will, progressively, be calculated based on this value (except for the final chapter)."""
|
||||
display_name = "Highest Possible Chapter Cost"
|
||||
range_start = 15
|
||||
range_end = 45
|
||||
default = 25
|
||||
|
||||
|
||||
class FinalChapterMinCost(Range):
|
||||
"""Minimum Time Pieces required to enter the final chapter. This is part of your goal."""
|
||||
display_name = "Final Chapter Minimum Time Piece Cost"
|
||||
range_start = 0
|
||||
range_end = 50
|
||||
default = 30
|
||||
|
||||
|
||||
class FinalChapterMaxCost(Range):
|
||||
"""Maximum Time Pieces required to enter the final chapter. This is part of your goal."""
|
||||
display_name = "Final Chapter Maximum Time Piece Cost"
|
||||
range_start = 0
|
||||
range_end = 50
|
||||
default = 35
|
||||
|
||||
|
||||
class MaxExtraTimePieces(Range):
|
||||
"""Maximum number of extra Time Pieces from the DLCs.
|
||||
Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56."""
|
||||
display_name = "Max Extra Time Pieces"
|
||||
range_start = 0
|
||||
range_end = 16
|
||||
default = 16
|
||||
|
||||
|
||||
class YarnCostMin(Range):
|
||||
"""The minimum possible yarn needed to stitch a hat."""
|
||||
display_name = "Minimum Yarn Cost"
|
||||
range_start = 1
|
||||
range_end = 12
|
||||
default = 4
|
||||
|
||||
|
||||
class YarnCostMax(Range):
|
||||
"""The maximum possible yarn needed to stitch a hat."""
|
||||
display_name = "Maximum Yarn Cost"
|
||||
range_start = 1
|
||||
range_end = 12
|
||||
default = 8
|
||||
|
||||
|
||||
class YarnAvailable(Range):
|
||||
"""How much yarn is available to collect in the item pool."""
|
||||
display_name = "Yarn Available"
|
||||
range_start = 30
|
||||
range_end = 80
|
||||
default = 50
|
||||
|
||||
|
||||
class MinExtraYarn(Range):
|
||||
"""The minimum number of extra yarn in the item pool.
|
||||
There must be at least this much more yarn over the total number of yarn needed to craft all hats.
|
||||
For example, if this option's value is 10, and the total yarn needed to craft all hats is 40,
|
||||
there must be at least 50 yarn in the pool."""
|
||||
display_name = "Max Extra Yarn"
|
||||
range_start = 5
|
||||
range_end = 15
|
||||
default = 10
|
||||
|
||||
|
||||
class HatItems(Toggle):
|
||||
"""Removes all yarn from the pool and turns the hats into individual items instead."""
|
||||
display_name = "Hat Items"
|
||||
|
||||
|
||||
class MinPonCost(Range):
|
||||
"""The minimum number of Pons that any item in the Badge Seller's shop can cost."""
|
||||
display_name = "Minimum Shop Pon Cost"
|
||||
range_start = 10
|
||||
range_end = 800
|
||||
default = 75
|
||||
|
||||
|
||||
class MaxPonCost(Range):
|
||||
"""The maximum number of Pons that any item in the Badge Seller's shop can cost."""
|
||||
display_name = "Maximum Shop Pon Cost"
|
||||
range_start = 10
|
||||
range_end = 800
|
||||
default = 300
|
||||
|
||||
|
||||
class BadgeSellerMinItems(Range):
|
||||
"""The smallest number of items that the Badge Seller can have for sale."""
|
||||
display_name = "Badge Seller Minimum Items"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 4
|
||||
|
||||
|
||||
class BadgeSellerMaxItems(Range):
|
||||
"""The largest number of items that the Badge Seller can have for sale."""
|
||||
display_name = "Badge Seller Maximum Items"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 8
|
||||
|
||||
|
||||
class EnableDLC1(Toggle):
|
||||
"""Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift.
|
||||
DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!"""
|
||||
display_name = "Shuffle Chapter 6"
|
||||
|
||||
|
||||
class Tasksanity(Toggle):
|
||||
"""If enabled, Ship Shape tasks will become checks. Requires DLC1 content to be enabled."""
|
||||
display_name = "Tasksanity"
|
||||
|
||||
|
||||
class TasksanityTaskStep(Range):
|
||||
"""How many tasks the player must complete in Tasksanity to send a check."""
|
||||
display_name = "Tasksanity Task Step"
|
||||
range_start = 1
|
||||
range_end = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class TasksanityCheckCount(Range):
|
||||
"""How many Tasksanity checks there will be in total."""
|
||||
display_name = "Tasksanity Check Count"
|
||||
range_start = 1
|
||||
range_end = 30
|
||||
default = 18
|
||||
|
||||
|
||||
class ExcludeTour(Toggle):
|
||||
"""Removes the Tour time rift from the game. This option is recommended if you don't want to deal with
|
||||
important levels being shuffled onto the Tour time rift, or important items being shuffled onto Tour pages
|
||||
when your goal is Time's End."""
|
||||
display_name = "Exclude Tour Time Rift"
|
||||
|
||||
|
||||
class ShipShapeCustomTaskGoal(Range):
|
||||
"""Change the number of tasks required to complete Ship Shape. If this option's value is 0, the number of tasks
|
||||
required will be TasksanityTaskStep x TasksanityCheckCount, if Tasksanity is enabled. If Tasksanity is disabled,
|
||||
it will use the game's default of 18.
|
||||
This option will not affect Cruisin' for a Bruisin'."""
|
||||
display_name = "Ship Shape Custom Task Goal"
|
||||
range_start = 0
|
||||
range_end = 90
|
||||
default = 0
|
||||
|
||||
|
||||
class EnableDLC2(Toggle):
|
||||
"""Shuffle content from Nyakuza Metro (Chapter 7) into the game.
|
||||
DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE NYAKUZA METRO DLC INSTALLED!!!"""
|
||||
display_name = "Shuffle Chapter 7"
|
||||
|
||||
|
||||
class MetroMinPonCost(Range):
|
||||
"""The cheapest an item can be in any Nyakuza Metro shop. Includes ticket booths."""
|
||||
display_name = "Metro Shops Minimum Pon Cost"
|
||||
range_start = 10
|
||||
range_end = 800
|
||||
default = 50
|
||||
|
||||
|
||||
class MetroMaxPonCost(Range):
|
||||
"""The most expensive an item can be in any Nyakuza Metro shop. Includes ticket booths."""
|
||||
display_name = "Metro Shops Maximum Pon Cost"
|
||||
range_start = 10
|
||||
range_end = 800
|
||||
default = 200
|
||||
|
||||
|
||||
class NyakuzaThugMinShopItems(Range):
|
||||
"""The smallest number of items that the thugs in Nyakuza Metro can have for sale."""
|
||||
display_name = "Nyakuza Thug Minimum Shop Items"
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 2
|
||||
|
||||
|
||||
class NyakuzaThugMaxShopItems(Range):
|
||||
"""The largest number of items that the thugs in Nyakuza Metro can have for sale."""
|
||||
display_name = "Nyakuza Thug Maximum Shop Items"
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 4
|
||||
|
||||
|
||||
class NoTicketSkips(Choice):
|
||||
"""Prevent metro gate skips from being in logic on higher difficulties.
|
||||
Rush Hour option will only consider the ticket skips for Rush Hour in logic."""
|
||||
display_name = "No Ticket Skips"
|
||||
option_false = 0
|
||||
option_true = 1
|
||||
option_rush_hour = 2
|
||||
|
||||
|
||||
class BaseballBat(Toggle):
|
||||
"""Replace the Umbrella with the baseball bat from Nyakuza Metro.
|
||||
DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed."""
|
||||
display_name = "Baseball Bat"
|
||||
|
||||
|
||||
class EnableDeathWish(Toggle):
|
||||
"""Shuffle Death Wish contracts into the game. Each contract by default will have 1 check granted upon completion.
|
||||
DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!"""
|
||||
display_name = "Enable Death Wish"
|
||||
|
||||
|
||||
class DeathWishOnly(Toggle):
|
||||
"""An alternative gameplay mode that allows you to exclusively play Death Wish in a seed.
|
||||
This has the following effects:
|
||||
- Death Wish is instantly unlocked from the start
|
||||
- All hats and other progression items are instantly given to you
|
||||
- Useful items such as Fast Hatter Badge will still be in the item pool instead of in your inventory at the start
|
||||
- All chapters and their levels are unlocked, act shuffle is forced off
|
||||
- Any checks other than Death Wish contracts are completely removed
|
||||
- All Pons in the item pool are replaced with Health Pons or random cosmetics
|
||||
- The EndGoal option is forced to complete Seal the Deal"""
|
||||
display_name = "Death Wish Only"
|
||||
|
||||
|
||||
class DWShuffle(Toggle):
|
||||
"""An alternative mode for Death Wish where each contract is unlocked one by one, in a random order.
|
||||
Stamp requirements to unlock contracts is removed. Any excluded contracts will not be shuffled into the sequence.
|
||||
If Seal the Deal is the end goal, it will always be the last Death Wish in the sequence.
|
||||
Disabling candles is highly recommended."""
|
||||
display_name = "Death Wish Shuffle"
|
||||
|
||||
|
||||
class DWShuffleCountMin(Range):
|
||||
"""The minimum number of Death Wishes that can be in the Death Wish shuffle sequence.
|
||||
The final result is clamped at the number of non-excluded Death Wishes."""
|
||||
display_name = "Death Wish Shuffle Minimum Count"
|
||||
range_start = 5
|
||||
range_end = 38
|
||||
default = 18
|
||||
|
||||
|
||||
class DWShuffleCountMax(Range):
|
||||
"""The maximum number of Death Wishes that can be in the Death Wish shuffle sequence.
|
||||
The final result is clamped at the number of non-excluded Death Wishes."""
|
||||
display_name = "Death Wish Shuffle Maximum Count"
|
||||
range_start = 5
|
||||
range_end = 38
|
||||
default = 25
|
||||
|
||||
|
||||
class DWEnableBonus(Toggle):
|
||||
"""In Death Wish, add a location for completing all of a DW contract's bonuses,
|
||||
in addition to the location for completing the DW contract normally.
|
||||
WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS!
|
||||
ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld!
|
||||
Using Peace and Tranquility to auto-complete the bonuses will NOT count!"""
|
||||
display_name = "Shuffle Death Wish Full Completions"
|
||||
|
||||
|
||||
class DWAutoCompleteBonuses(DefaultOnToggle):
|
||||
"""If enabled, auto complete all bonus stamps after completing the main objective in a Death Wish.
|
||||
This option will have no effect if bonus checks (DWEnableBonus) are turned on."""
|
||||
display_name = "Auto Complete Bonus Stamps"
|
||||
|
||||
|
||||
class DWExcludeAnnoyingContracts(DefaultOnToggle):
|
||||
"""Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear.
|
||||
Excluded Death Wishes are automatically completed as soon as they are unlocked.
|
||||
This option currently excludes the following contracts:
|
||||
- Vault Codes in the Wind
|
||||
- Boss Rush
|
||||
- Camera Tourist
|
||||
- The Mustache Gauntlet
|
||||
- Rift Collapse: Deep Sea
|
||||
- Cruisin' for a Bruisin'
|
||||
- Seal the Deal (non-excluded if goal, but the checks are still excluded)"""
|
||||
display_name = "Exclude Annoying Death Wish Contracts"
|
||||
|
||||
|
||||
class DWExcludeAnnoyingBonuses(DefaultOnToggle):
|
||||
"""If Death Wish full completions are shuffled in, exclude tedious Death Wish full completions from the pool.
|
||||
Excluded bonus Death Wishes automatically reward their bonus stamps upon completion of the main objective.
|
||||
This option currently excludes the following bonuses:
|
||||
- So You're Back From Outer Space
|
||||
- Encore! Encore!
|
||||
- Snatcher's Hit List
|
||||
- 10 Seconds until Self-Destruct
|
||||
- Killing Two Birds
|
||||
- Zero Jumps
|
||||
- Bird Sanctuary
|
||||
- Wound-Up Windmill
|
||||
- Vault Codes in the Wind
|
||||
- Boss Rush
|
||||
- Camera Tourist
|
||||
- The Mustache Gauntlet
|
||||
- Rift Collapse: Deep Sea
|
||||
- Cruisin' for a Bruisin'
|
||||
- Seal the Deal"""
|
||||
display_name = "Exclude Annoying Death Wish Full Completions"
|
||||
|
||||
|
||||
class DWExcludeCandles(DefaultOnToggle):
|
||||
"""If enabled, exclude all candle Death Wishes."""
|
||||
display_name = "Exclude Candle Death Wishes"
|
||||
|
||||
|
||||
class DWTimePieceRequirement(Range):
|
||||
"""How many Time Pieces that will be required to unlock Death Wish."""
|
||||
display_name = "Death Wish Time Piece Requirement"
|
||||
range_start = 0
|
||||
range_end = 35
|
||||
default = 15
|
||||
|
||||
|
||||
class TrapChance(Range):
|
||||
"""The chance for any junk item in the pool to be replaced by a trap."""
|
||||
display_name = "Trap Chance"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
|
||||
|
||||
class BabyTrapWeight(Range):
|
||||
"""The weight of Baby Traps in the trap pool.
|
||||
Baby Traps place a multitude of the Conductor's grandkids into Hat Kid's hands, causing her to lose her balance."""
|
||||
display_name = "Baby Trap Weight"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 40
|
||||
|
||||
|
||||
class LaserTrapWeight(Range):
|
||||
"""The weight of Laser Traps in the trap pool.
|
||||
Laser Traps will spawn multiple giant lasers (from Snatcher's boss fight) at Hat Kid's location."""
|
||||
display_name = "Laser Trap Weight"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 40
|
||||
|
||||
|
||||
class ParadeTrapWeight(Range):
|
||||
"""The weight of Parade Traps in the trap pool.
|
||||
Parade Traps will summon multiple Express Band owls with knives that chase Hat Kid by mimicking her movement."""
|
||||
display_name = "Parade Trap Weight"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 20
|
||||
|
||||
|
||||
@dataclass
|
||||
class AHITOptions(PerGameCommonOptions):
|
||||
EndGoal: EndGoal
|
||||
ActRandomizer: ActRandomizer
|
||||
ActPlando: ActPlando
|
||||
ActBlacklist: ActBlacklist
|
||||
ShuffleAlpineZiplines: ShuffleAlpineZiplines
|
||||
FinaleShuffle: FinaleShuffle
|
||||
LogicDifficulty: LogicDifficulty
|
||||
YarnBalancePercent: YarnBalancePercent
|
||||
TimePieceBalancePercent: TimePieceBalancePercent
|
||||
RandomizeHatOrder: RandomizeHatOrder
|
||||
UmbrellaLogic: UmbrellaLogic
|
||||
StartWithCompassBadge: StartWithCompassBadge
|
||||
CompassBadgeMode: CompassBadgeMode
|
||||
ShuffleStorybookPages: ShuffleStorybookPages
|
||||
ShuffleActContracts: ShuffleActContracts
|
||||
ShuffleSubconPaintings: ShuffleSubconPaintings
|
||||
NoPaintingSkips: NoPaintingSkips
|
||||
StartingChapter: StartingChapter
|
||||
CTRLogic: CTRLogic
|
||||
|
||||
EnableDLC1: EnableDLC1
|
||||
Tasksanity: Tasksanity
|
||||
TasksanityTaskStep: TasksanityTaskStep
|
||||
TasksanityCheckCount: TasksanityCheckCount
|
||||
ExcludeTour: ExcludeTour
|
||||
ShipShapeCustomTaskGoal: ShipShapeCustomTaskGoal
|
||||
|
||||
EnableDeathWish: EnableDeathWish
|
||||
DWShuffle: DWShuffle
|
||||
DWShuffleCountMin: DWShuffleCountMin
|
||||
DWShuffleCountMax: DWShuffleCountMax
|
||||
DeathWishOnly: DeathWishOnly
|
||||
DWEnableBonus: DWEnableBonus
|
||||
DWAutoCompleteBonuses: DWAutoCompleteBonuses
|
||||
DWExcludeAnnoyingContracts: DWExcludeAnnoyingContracts
|
||||
DWExcludeAnnoyingBonuses: DWExcludeAnnoyingBonuses
|
||||
DWExcludeCandles: DWExcludeCandles
|
||||
DWTimePieceRequirement: DWTimePieceRequirement
|
||||
|
||||
EnableDLC2: EnableDLC2
|
||||
BaseballBat: BaseballBat
|
||||
MetroMinPonCost: MetroMinPonCost
|
||||
MetroMaxPonCost: MetroMaxPonCost
|
||||
NyakuzaThugMinShopItems: NyakuzaThugMinShopItems
|
||||
NyakuzaThugMaxShopItems: NyakuzaThugMaxShopItems
|
||||
NoTicketSkips: NoTicketSkips
|
||||
|
||||
LowestChapterCost: LowestChapterCost
|
||||
HighestChapterCost: HighestChapterCost
|
||||
ChapterCostIncrement: ChapterCostIncrement
|
||||
ChapterCostMinDifference: ChapterCostMinDifference
|
||||
MaxExtraTimePieces: MaxExtraTimePieces
|
||||
|
||||
FinalChapterMinCost: FinalChapterMinCost
|
||||
FinalChapterMaxCost: FinalChapterMaxCost
|
||||
|
||||
YarnCostMin: YarnCostMin
|
||||
YarnCostMax: YarnCostMax
|
||||
YarnAvailable: YarnAvailable
|
||||
MinExtraYarn: MinExtraYarn
|
||||
HatItems: HatItems
|
||||
|
||||
MinPonCost: MinPonCost
|
||||
MaxPonCost: MaxPonCost
|
||||
BadgeSellerMinItems: BadgeSellerMinItems
|
||||
BadgeSellerMaxItems: BadgeSellerMaxItems
|
||||
|
||||
TrapChance: TrapChance
|
||||
BabyTrapWeight: BabyTrapWeight
|
||||
LaserTrapWeight: LaserTrapWeight
|
||||
ParadeTrapWeight: ParadeTrapWeight
|
||||
|
||||
death_link: DeathLink
|
||||
|
||||
|
||||
ahit_option_groups: Dict[str, List[Any]] = {
|
||||
"General Options": [EndGoal, ShuffleStorybookPages, ShuffleAlpineZiplines, ShuffleSubconPaintings,
|
||||
ShuffleActContracts, MinPonCost, MaxPonCost, BadgeSellerMinItems, BadgeSellerMaxItems,
|
||||
LogicDifficulty, NoPaintingSkips, CTRLogic],
|
||||
|
||||
"Act Options": [ActRandomizer, StartingChapter, LowestChapterCost, HighestChapterCost,
|
||||
ChapterCostIncrement, ChapterCostMinDifference, FinalChapterMinCost, FinalChapterMaxCost,
|
||||
FinaleShuffle, ActPlando, ActBlacklist],
|
||||
|
||||
"Item Options": [StartWithCompassBadge, CompassBadgeMode, RandomizeHatOrder, YarnAvailable, YarnCostMin,
|
||||
YarnCostMax, MinExtraYarn, HatItems, UmbrellaLogic, MaxExtraTimePieces, YarnBalancePercent,
|
||||
TimePieceBalancePercent],
|
||||
|
||||
"Arctic Cruise Options": [EnableDLC1, Tasksanity, TasksanityTaskStep, TasksanityCheckCount,
|
||||
ShipShapeCustomTaskGoal, ExcludeTour],
|
||||
|
||||
"Nyakuza Metro Options": [EnableDLC2, MetroMinPonCost, MetroMaxPonCost, NyakuzaThugMinShopItems,
|
||||
NyakuzaThugMaxShopItems, BaseballBat, NoTicketSkips],
|
||||
|
||||
"Death Wish Options": [EnableDeathWish, DWTimePieceRequirement, DWShuffle, DWShuffleCountMin, DWShuffleCountMax,
|
||||
DWEnableBonus, DWAutoCompleteBonuses, DWExcludeAnnoyingContracts, DWExcludeAnnoyingBonuses,
|
||||
DWExcludeCandles, DeathWishOnly],
|
||||
|
||||
"Trap Options": [TrapChance, BabyTrapWeight, LaserTrapWeight, ParadeTrapWeight]
|
||||
}
|
||||
|
||||
|
||||
slot_data_options: List[str] = [
|
||||
"EndGoal",
|
||||
"ActRandomizer",
|
||||
"ShuffleAlpineZiplines",
|
||||
"LogicDifficulty",
|
||||
"CTRLogic",
|
||||
"RandomizeHatOrder",
|
||||
"UmbrellaLogic",
|
||||
"StartWithCompassBadge",
|
||||
"CompassBadgeMode",
|
||||
"ShuffleStorybookPages",
|
||||
"ShuffleActContracts",
|
||||
"ShuffleSubconPaintings",
|
||||
"NoPaintingSkips",
|
||||
"HatItems",
|
||||
|
||||
"EnableDLC1",
|
||||
"Tasksanity",
|
||||
"TasksanityTaskStep",
|
||||
"TasksanityCheckCount",
|
||||
"ShipShapeCustomTaskGoal",
|
||||
"ExcludeTour",
|
||||
|
||||
"EnableDeathWish",
|
||||
"DWShuffle",
|
||||
"DeathWishOnly",
|
||||
"DWEnableBonus",
|
||||
"DWAutoCompleteBonuses",
|
||||
"DWTimePieceRequirement",
|
||||
|
||||
"EnableDLC2",
|
||||
"MetroMinPonCost",
|
||||
"MetroMaxPonCost",
|
||||
"BaseballBat",
|
||||
"NoTicketSkips",
|
||||
|
||||
"MinPonCost",
|
||||
"MaxPonCost",
|
||||
|
||||
"death_link",
|
||||
]
|
||||
1025
worlds/ahit/Regions.py
Normal file
1025
worlds/ahit/Regions.py
Normal file
File diff suppressed because it is too large
Load Diff
959
worlds/ahit/Rules.py
Normal file
959
worlds/ahit/Rules.py
Normal file
@@ -0,0 +1,959 @@
|
||||
from worlds.AutoWorld import CollectionState
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \
|
||||
shop_locations, event_locs
|
||||
from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType
|
||||
from BaseClasses import Location, Entrance, Region
|
||||
from typing import TYPE_CHECKING, List, Callable, Union, Dict
|
||||
from .Options import EndGoal, CTRLogic, NoTicketSkips
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import HatInTimeWorld
|
||||
|
||||
|
||||
act_connections = {
|
||||
"Mafia Town - Act 2": ["Mafia Town - Act 1"],
|
||||
"Mafia Town - Act 3": ["Mafia Town - Act 1"],
|
||||
"Mafia Town - Act 4": ["Mafia Town - Act 2", "Mafia Town - Act 3"],
|
||||
"Mafia Town - Act 6": ["Mafia Town - Act 4"],
|
||||
"Mafia Town - Act 7": ["Mafia Town - Act 4"],
|
||||
"Mafia Town - Act 5": ["Mafia Town - Act 6", "Mafia Town - Act 7"],
|
||||
|
||||
"Battle of the Birds - Act 2": ["Battle of the Birds - Act 1"],
|
||||
"Battle of the Birds - Act 3": ["Battle of the Birds - Act 1"],
|
||||
"Battle of the Birds - Act 4": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"],
|
||||
"Battle of the Birds - Act 5": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"],
|
||||
"Battle of the Birds - Finale A": ["Battle of the Birds - Act 4", "Battle of the Birds - Act 5"],
|
||||
"Battle of the Birds - Finale B": ["Battle of the Birds - Finale A"],
|
||||
|
||||
"Subcon Forest - Finale": ["Subcon Forest - Act 1", "Subcon Forest - Act 2",
|
||||
"Subcon Forest - Act 3", "Subcon Forest - Act 4",
|
||||
"Subcon Forest - Act 5"],
|
||||
|
||||
"The Arctic Cruise - Act 2": ["The Arctic Cruise - Act 1"],
|
||||
"The Arctic Cruise - Finale": ["The Arctic Cruise - Act 2"],
|
||||
}
|
||||
|
||||
|
||||
def can_use_hat(state: CollectionState, world: "HatInTimeWorld", hat: HatType) -> bool:
|
||||
if world.options.HatItems:
|
||||
return state.has(hat_type_to_item[hat], world.player)
|
||||
|
||||
if world.hat_yarn_costs[hat] <= 0: # this means the hat was put into starting inventory
|
||||
return True
|
||||
|
||||
return state.has("Yarn", world.player, get_hat_cost(world, hat))
|
||||
|
||||
|
||||
def get_hat_cost(world: "HatInTimeWorld", hat: HatType) -> int:
|
||||
cost = 0
|
||||
for h in world.hat_craft_order:
|
||||
cost += world.hat_yarn_costs[h]
|
||||
if h == hat:
|
||||
break
|
||||
|
||||
return cost
|
||||
|
||||
|
||||
def painting_logic(world: "HatInTimeWorld") -> bool:
|
||||
return bool(world.options.ShuffleSubconPaintings)
|
||||
|
||||
|
||||
# -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert
|
||||
def get_difficulty(world: "HatInTimeWorld") -> Difficulty:
|
||||
return Difficulty(world.options.LogicDifficulty)
|
||||
|
||||
|
||||
def has_paintings(state: CollectionState, world: "HatInTimeWorld", count: int, allow_skip: bool = True) -> bool:
|
||||
if not painting_logic(world):
|
||||
return True
|
||||
|
||||
if not world.options.NoPaintingSkips and allow_skip:
|
||||
# In Moderate there is a very easy trick to skip all the walls, except for the one guarding the boss arena
|
||||
if get_difficulty(world) >= Difficulty.MODERATE:
|
||||
return True
|
||||
|
||||
return state.has("Progressive Painting Unlock", world.player, count)
|
||||
|
||||
|
||||
def zipline_logic(world: "HatInTimeWorld") -> bool:
|
||||
return bool(world.options.ShuffleAlpineZiplines)
|
||||
|
||||
|
||||
def can_use_hookshot(state: CollectionState, world: "HatInTimeWorld"):
|
||||
return state.has("Hookshot Badge", world.player)
|
||||
|
||||
|
||||
def can_hit(state: CollectionState, world: "HatInTimeWorld", umbrella_only: bool = False):
|
||||
if not world.options.UmbrellaLogic:
|
||||
return True
|
||||
|
||||
return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING)
|
||||
|
||||
|
||||
def has_relic_combo(state: CollectionState, world: "HatInTimeWorld", relic: str) -> bool:
|
||||
return state.has_group(relic, world.player, len(world.item_name_groups[relic]))
|
||||
|
||||
|
||||
def get_relic_count(state: CollectionState, world: "HatInTimeWorld", relic: str) -> int:
|
||||
return state.count_group(relic, world.player)
|
||||
|
||||
|
||||
# This is used to determine if the player can clear an act that's required to unlock a Time Rift
|
||||
def can_clear_required_act(state: CollectionState, world: "HatInTimeWorld", act_entrance: str) -> bool:
|
||||
entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player)
|
||||
if not state.can_reach(entrance.connected_region, "Region", world.player):
|
||||
return False
|
||||
|
||||
if "Free Roam" in entrance.connected_region.name:
|
||||
return True
|
||||
|
||||
name: str = f"Act Completion ({entrance.connected_region.name})"
|
||||
return world.multiworld.get_location(name, world.player).access_rule(state)
|
||||
|
||||
|
||||
def can_clear_alpine(state: CollectionState, world: "HatInTimeWorld") -> bool:
|
||||
return state.has("Birdhouse Cleared", world.player) and state.has("Lava Cake Cleared", world.player) \
|
||||
and state.has("Windmill Cleared", world.player) and state.has("Twilight Bell Cleared", world.player)
|
||||
|
||||
|
||||
def can_clear_metro(state: CollectionState, world: "HatInTimeWorld") -> bool:
|
||||
return state.has("Nyakuza Intro Cleared", world.player) \
|
||||
and state.has("Yellow Overpass Station Cleared", world.player) \
|
||||
and state.has("Yellow Overpass Manhole Cleared", world.player) \
|
||||
and state.has("Green Clean Station Cleared", world.player) \
|
||||
and state.has("Green Clean Manhole Cleared", world.player) \
|
||||
and state.has("Bluefin Tunnel Cleared", world.player) \
|
||||
and state.has("Pink Paw Station Cleared", world.player) \
|
||||
and state.has("Pink Paw Manhole Cleared", world.player)
|
||||
|
||||
|
||||
def set_rules(world: "HatInTimeWorld"):
|
||||
# First, chapter access
|
||||
starting_chapter = ChapterIndex(world.options.StartingChapter)
|
||||
world.chapter_timepiece_costs[starting_chapter] = 0
|
||||
|
||||
# Chapter costs increase progressively. Randomly decide the chapter order, except for Finale
|
||||
chapter_list: List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS,
|
||||
ChapterIndex.SUBCON, ChapterIndex.ALPINE]
|
||||
|
||||
final_chapter = ChapterIndex.FINALE
|
||||
if world.options.EndGoal == EndGoal.option_rush_hour:
|
||||
final_chapter = ChapterIndex.METRO
|
||||
chapter_list.append(ChapterIndex.FINALE)
|
||||
elif world.options.EndGoal == EndGoal.option_seal_the_deal:
|
||||
final_chapter = None
|
||||
chapter_list.append(ChapterIndex.FINALE)
|
||||
|
||||
if world.is_dlc1():
|
||||
chapter_list.append(ChapterIndex.CRUISE)
|
||||
|
||||
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
|
||||
chapter_list.append(ChapterIndex.METRO)
|
||||
|
||||
chapter_list.remove(starting_chapter)
|
||||
world.random.shuffle(chapter_list)
|
||||
|
||||
# Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them
|
||||
if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
|
||||
index1 = 69
|
||||
index2 = 69
|
||||
pos: int
|
||||
lowest_index: int
|
||||
chapter_list.remove(ChapterIndex.ALPINE)
|
||||
|
||||
if world.is_dlc1():
|
||||
index1 = chapter_list.index(ChapterIndex.CRUISE)
|
||||
|
||||
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
|
||||
index2 = chapter_list.index(ChapterIndex.METRO)
|
||||
|
||||
lowest_index = min(index1, index2)
|
||||
if lowest_index == 0:
|
||||
pos = 0
|
||||
else:
|
||||
pos = world.random.randint(0, lowest_index)
|
||||
|
||||
chapter_list.insert(pos, ChapterIndex.ALPINE)
|
||||
|
||||
lowest_cost: int = world.options.LowestChapterCost.value
|
||||
highest_cost: int = world.options.HighestChapterCost.value
|
||||
cost_increment: int = world.options.ChapterCostIncrement.value
|
||||
min_difference: int = world.options.ChapterCostMinDifference.value
|
||||
last_cost = 0
|
||||
|
||||
for i, chapter in enumerate(chapter_list):
|
||||
min_range: int = lowest_cost + (cost_increment * i)
|
||||
if min_range >= highest_cost:
|
||||
min_range = highest_cost-1
|
||||
|
||||
value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment)))
|
||||
cost = world.random.randint(value, min(value + cost_increment, highest_cost))
|
||||
if i >= 1:
|
||||
if last_cost + min_difference > cost:
|
||||
cost = last_cost + min_difference
|
||||
|
||||
cost = min(cost, highest_cost)
|
||||
world.chapter_timepiece_costs[chapter] = cost
|
||||
last_cost = cost
|
||||
|
||||
if final_chapter is not None:
|
||||
final_chapter_cost: int
|
||||
if world.options.FinalChapterMinCost == world.options.FinalChapterMaxCost:
|
||||
final_chapter_cost = world.options.FinalChapterMaxCost.value
|
||||
else:
|
||||
final_chapter_cost = world.random.randint(world.options.FinalChapterMinCost.value,
|
||||
world.options.FinalChapterMaxCost.value)
|
||||
|
||||
world.chapter_timepiece_costs[final_chapter] = final_chapter_cost
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player),
|
||||
lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.MAFIA]))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Telescope -> Battle of the Birds", world.player),
|
||||
lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS]))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Telescope -> Subcon Forest", world.player),
|
||||
lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.SUBCON]))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Telescope -> Alpine Skyline", world.player),
|
||||
lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE]))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player),
|
||||
lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.FINALE])
|
||||
and can_use_hat(state, world, HatType.BREWING) and can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
if world.is_dlc1():
|
||||
add_rule(world.multiworld.get_entrance("Telescope -> Arctic Cruise", world.player),
|
||||
lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE])
|
||||
and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.CRUISE]))
|
||||
|
||||
if world.is_dlc2():
|
||||
add_rule(world.multiworld.get_entrance("Telescope -> Nyakuza Metro", world.player),
|
||||
lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE])
|
||||
and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.METRO])
|
||||
and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE))
|
||||
|
||||
if not world.options.ActRandomizer:
|
||||
set_default_rift_rules(world)
|
||||
|
||||
table = {**location_table, **event_locs}
|
||||
for (key, data) in table.items():
|
||||
if not is_location_valid(world, key):
|
||||
continue
|
||||
|
||||
if key in contract_locations.keys():
|
||||
continue
|
||||
|
||||
loc = world.multiworld.get_location(key, world.player)
|
||||
|
||||
for hat in data.required_hats:
|
||||
add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h))
|
||||
|
||||
if data.hookshot:
|
||||
add_rule(loc, lambda state: can_use_hookshot(state, world))
|
||||
|
||||
if data.paintings > 0 and world.options.ShuffleSubconPaintings:
|
||||
add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
|
||||
|
||||
if data.hit_type is not HitType.none and world.options.UmbrellaLogic:
|
||||
if data.hit_type == HitType.umbrella:
|
||||
add_rule(loc, lambda state: state.has("Umbrella", world.player))
|
||||
|
||||
elif data.hit_type == HitType.umbrella_or_brewing:
|
||||
add_rule(loc, lambda state: state.has("Umbrella", world.player)
|
||||
or can_use_hat(state, world, HatType.BREWING))
|
||||
|
||||
elif data.hit_type == HitType.dweller_bell:
|
||||
add_rule(loc, lambda state: state.has("Umbrella", world.player)
|
||||
or can_use_hat(state, world, HatType.BREWING)
|
||||
or can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
for misc in data.misc_required:
|
||||
add_rule(loc, lambda state, item=misc: state.has(item, world.player))
|
||||
|
||||
set_specific_rules(world)
|
||||
|
||||
# Putting all of this here, so it doesn't get overridden by anything
|
||||
# Illness starts the player past the intro
|
||||
alpine_entrance = world.multiworld.get_entrance("AFR -> Alpine Skyline Area", world.player)
|
||||
add_rule(alpine_entrance, lambda state: can_use_hookshot(state, world))
|
||||
if world.options.UmbrellaLogic:
|
||||
add_rule(alpine_entrance, lambda state: state.has("Umbrella", world.player))
|
||||
|
||||
if zipline_logic(world):
|
||||
add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player),
|
||||
lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player),
|
||||
lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("-> The Windmill", world.player),
|
||||
lambda state: state.has("Zipline Unlock - The Windmill Path", world.player))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player),
|
||||
lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player))
|
||||
|
||||
add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player),
|
||||
lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)
|
||||
and state.has("Zipline Unlock - The Lava Cake Path", world.player)
|
||||
and state.has("Zipline Unlock - The Windmill Path", world.player))
|
||||
|
||||
if zipline_logic(world):
|
||||
for (loc, zipline) in zipline_unlocks.items():
|
||||
add_rule(world.multiworld.get_location(loc, world.player),
|
||||
lambda state, z=zipline: state.has(z, world.player))
|
||||
|
||||
dummy_entrances: List[Entrance] = []
|
||||
|
||||
for (key, acts) in act_connections.items():
|
||||
if "Arctic Cruise" in key and not world.is_dlc1():
|
||||
continue
|
||||
|
||||
entrance: Entrance = world.multiworld.get_entrance(key, world.player)
|
||||
region: Region = entrance.connected_region
|
||||
access_rules: List[Callable[[CollectionState], bool]] = []
|
||||
dummy_entrances.append(entrance)
|
||||
|
||||
# Entrances to this act that we have to set access_rules on
|
||||
entrances: List[Entrance] = []
|
||||
|
||||
for i, act in enumerate(acts, start=1):
|
||||
act_entrance: Entrance = world.multiworld.get_entrance(act, world.player)
|
||||
access_rules.append(act_entrance.access_rule)
|
||||
required_region = act_entrance.connected_region
|
||||
name: str = f"{key}: Connection {i}"
|
||||
new_entrance: Entrance = required_region.connect(region, name)
|
||||
entrances.append(new_entrance)
|
||||
|
||||
# Copy access rules from act completions
|
||||
if "Free Roam" not in required_region.name:
|
||||
rule: Callable[[CollectionState], bool]
|
||||
name = f"Act Completion ({required_region.name})"
|
||||
rule = world.multiworld.get_location(name, world.player).access_rule
|
||||
access_rules.append(rule)
|
||||
|
||||
for e in entrances:
|
||||
for rules in access_rules:
|
||||
add_rule(e, rules)
|
||||
|
||||
for e in dummy_entrances:
|
||||
set_rule(e, lambda state: False)
|
||||
|
||||
set_event_rules(world)
|
||||
|
||||
if world.options.EndGoal == EndGoal.option_finale:
|
||||
world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player)
|
||||
elif world.options.EndGoal == EndGoal.option_rush_hour:
|
||||
world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player)
|
||||
|
||||
|
||||
def set_specific_rules(world: "HatInTimeWorld"):
|
||||
add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player),
|
||||
lambda state: state.has("Time Piece", world.player, 12)
|
||||
and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS]))
|
||||
|
||||
set_mafia_town_rules(world)
|
||||
set_botb_rules(world)
|
||||
set_subcon_rules(world)
|
||||
set_alps_rules(world)
|
||||
|
||||
if world.is_dlc1():
|
||||
set_dlc1_rules(world)
|
||||
|
||||
if world.is_dlc2():
|
||||
set_dlc2_rules(world)
|
||||
|
||||
difficulty: Difficulty = get_difficulty(world)
|
||||
|
||||
if difficulty >= Difficulty.MODERATE:
|
||||
set_moderate_rules(world)
|
||||
|
||||
if difficulty >= Difficulty.HARD:
|
||||
set_hard_rules(world)
|
||||
|
||||
if difficulty >= Difficulty.EXPERT:
|
||||
set_expert_rules(world)
|
||||
|
||||
|
||||
def set_moderate_rules(world: "HatInTimeWorld"):
|
||||
# Moderate: Gallery without Brewing Hat
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), lambda state: True)
|
||||
|
||||
# Moderate: Above Boats via Ice Hat Sliding
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.ICE), "or")
|
||||
|
||||
# Moderate: Clock Tower Chest + Ruined Tower with nothing
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
||||
|
||||
# Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
|
||||
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
|
||||
set_rule(loc, lambda state: has_paintings(state, world, 1))
|
||||
|
||||
# Moderate: Vanessa Manor with nothing
|
||||
for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations:
|
||||
set_rule(loc, lambda state: has_paintings(state, world, 1))
|
||||
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player),
|
||||
lambda state: has_paintings(state, world, 1))
|
||||
|
||||
# Moderate: Village Time Rift with nothing IF umbrella logic is off
|
||||
if not world.options.UmbrellaLogic:
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True)
|
||||
|
||||
# Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat
|
||||
set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player),
|
||||
lambda state: can_use_hookshot(state, world))
|
||||
set_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player),
|
||||
lambda state: can_use_hookshot(state, world))
|
||||
|
||||
# Moderate: The Birdhouse - Dweller Platforms Relic with only Birdhouse access
|
||||
set_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player),
|
||||
lambda state: True)
|
||||
|
||||
# Moderate: Twilight Path without Dweller Mask
|
||||
set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True)
|
||||
|
||||
# Moderate: Mystifying Time Mesa time trial without hats
|
||||
set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player),
|
||||
lambda state: can_use_hookshot(state, world))
|
||||
|
||||
# Moderate: Goat Refinery from TIHS with Sprint only
|
||||
add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player),
|
||||
lambda state: state.has("TIHS Access", world.player)
|
||||
and can_use_hat(state, world, HatType.SPRINT), "or")
|
||||
|
||||
# Moderate: Finale Telescope with only Ice Hat
|
||||
add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player),
|
||||
lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.FINALE])
|
||||
and can_use_hat(state, world, HatType.ICE), "or")
|
||||
|
||||
# Moderate: Finale without Hookshot
|
||||
set_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
if world.is_dlc1():
|
||||
# Moderate: clear Rock the Boat without Ice Hat
|
||||
add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
||||
add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
|
||||
|
||||
# Moderate: clear Deep Sea without Ice Hat
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
# There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw.
|
||||
# Yellow Overpass time piece can also be reached without Hookshot quite easily.
|
||||
if world.is_dlc2():
|
||||
# No Hookshot
|
||||
set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Station)", world.player),
|
||||
lambda state: True)
|
||||
|
||||
# No Dweller, Hookshot, or Time Stop for these
|
||||
set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Pink Paw Station - Pink Ticket Booth", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Act Completion (Pink Paw Station)", world.player), lambda state: True)
|
||||
for key in shop_locations.keys():
|
||||
if "Pink Paw Station Thug" in key and is_location_valid(world, key):
|
||||
set_rule(world.multiworld.get_location(key, world.player), lambda state: True)
|
||||
|
||||
# Moderate: clear Rush Hour without Hookshot
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
||||
lambda state: state.has("Metro Ticket - Pink", world.player)
|
||||
and state.has("Metro Ticket - Yellow", world.player)
|
||||
and state.has("Metro Ticket - Blue", world.player)
|
||||
and can_use_hat(state, world, HatType.ICE)
|
||||
and can_use_hat(state, world, HatType.BREWING))
|
||||
|
||||
# Moderate: Bluefin Tunnel + Pink Paw Station without tickets
|
||||
if not world.options.NoTicketSkips:
|
||||
set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True)
|
||||
|
||||
|
||||
def set_hard_rules(world: "HatInTimeWorld"):
|
||||
# Hard: clear Time Rift - The Twilight Bell with Sprint+Scooter only
|
||||
add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.SPRINT)
|
||||
and state.has("Scooter Badge", world.player), "or")
|
||||
|
||||
# No Dweller Mask required
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player),
|
||||
lambda state: has_paintings(state, world, 3))
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player),
|
||||
lambda state: has_paintings(state, world, 3))
|
||||
|
||||
# Cherry bridge over boss arena gap (painting still expected)
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
|
||||
lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
|
||||
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player),
|
||||
lambda state: has_paintings(state, world, 2, True))
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
|
||||
lambda state: has_paintings(state, world, 2, True))
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player),
|
||||
lambda state: has_paintings(state, world, 3, True))
|
||||
|
||||
# SDJ
|
||||
add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.SPRINT) and has_paintings(state, world, 2), "or")
|
||||
|
||||
add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.SPRINT), "or")
|
||||
|
||||
# Hard: Goat Refinery from TIHS with nothing
|
||||
add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player),
|
||||
lambda state: state.has("TIHS Access", world.player), "or")
|
||||
|
||||
if world.is_dlc1():
|
||||
# Hard: clear Deep Sea without Dweller Mask
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
|
||||
lambda state: can_use_hookshot(state, world))
|
||||
|
||||
if world.is_dlc2():
|
||||
# Hard: clear Green Clean Manhole without Dweller Mask
|
||||
set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.ICE))
|
||||
|
||||
# Hard: clear Rush Hour with Brewing Hat only
|
||||
if world.options.NoTicketSkips is not NoTicketSkips.option_true:
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.BREWING))
|
||||
else:
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.BREWING)
|
||||
and state.has("Metro Ticket - Yellow", world.player)
|
||||
and state.has("Metro Ticket - Blue", world.player)
|
||||
and state.has("Metro Ticket - Pink", world.player))
|
||||
|
||||
|
||||
def set_expert_rules(world: "HatInTimeWorld"):
|
||||
# Finale Telescope with no hats
|
||||
set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player),
|
||||
lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.FINALE]))
|
||||
|
||||
# Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Top of Lighthouse", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player), lambda state: True)
|
||||
|
||||
# Expert: Clear Dead Bird Studio with nothing
|
||||
for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations:
|
||||
set_rule(loc, lambda state: True)
|
||||
|
||||
set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), lambda state: True)
|
||||
|
||||
# Expert: Clear Dead Bird Studio Basement without Hookshot
|
||||
for loc in world.multiworld.get_region("Dead Bird Studio Basement", world.player).locations:
|
||||
set_rule(loc, lambda state: True)
|
||||
|
||||
# Expert: get to and clear Twilight Bell without Dweller Mask.
|
||||
# Dweller Mask OR Sprint Hat OR Brewing Hat OR Time Stop + Umbrella required to complete act.
|
||||
add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player),
|
||||
lambda state: can_use_hookshot(state, world), "or")
|
||||
|
||||
add_rule(world.multiworld.get_location("Act Completion (The Twilight Bell)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.BREWING)
|
||||
or can_use_hat(state, world, HatType.DWELLER)
|
||||
or can_use_hat(state, world, HatType.SPRINT)
|
||||
or (can_use_hat(state, world, HatType.TIME_STOP) and state.has("Umbrella", world.player)))
|
||||
|
||||
# Expert: Time Rift - Curly Tail Trail with nothing
|
||||
# Time Rift - Twilight Bell and Time Rift - Village with nothing
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player),
|
||||
lambda state: True)
|
||||
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player),
|
||||
lambda state: True)
|
||||
|
||||
# Expert: Cherry Hovering
|
||||
subcon_area = world.multiworld.get_region("Subcon Forest Area", world.player)
|
||||
yche = world.multiworld.get_region("Your Contract has Expired", world.player)
|
||||
entrance = yche.connect(subcon_area, "Subcon Forest Entrance YCHE")
|
||||
|
||||
if world.options.NoPaintingSkips:
|
||||
add_rule(entrance, lambda state: has_paintings(state, world, 1))
|
||||
|
||||
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
|
||||
and has_paintings(state, world, 1, True))
|
||||
|
||||
# Set painting rules only. Skipping paintings is determined in has_paintings
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
|
||||
lambda state: has_paintings(state, world, 1, True))
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player),
|
||||
lambda state: has_paintings(state, world, 3, True))
|
||||
|
||||
# You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him
|
||||
subcon_area.connect(yche, "Snatcher Hover")
|
||||
set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player),
|
||||
lambda state: True)
|
||||
|
||||
if world.is_dlc2():
|
||||
# Expert: clear Rush Hour with nothing
|
||||
if not world.options.NoTicketSkips:
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True)
|
||||
else:
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
||||
lambda state: state.has("Metro Ticket - Yellow", world.player)
|
||||
and state.has("Metro Ticket - Blue", world.player)
|
||||
and state.has("Metro Ticket - Pink", world.player))
|
||||
|
||||
# Expert: Yellow/Green Manhole with nothing using a Boop Clip
|
||||
set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Manhole)", world.player),
|
||||
lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player),
|
||||
lambda state: True)
|
||||
|
||||
|
||||
def set_mafia_town_rules(world: "HatInTimeWorld"):
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player),
|
||||
lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player)
|
||||
or state.can_reach("Down with the Mafia!", "Region", world.player)
|
||||
or state.can_reach("Cheating the Race", "Region", world.player)
|
||||
or state.can_reach("The Golden Vault", "Region", world.player))
|
||||
|
||||
# Old guys don't appear in SCFOS
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Old Man (Steel Beams)", world.player),
|
||||
lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
|
||||
or state.can_reach("Barrel Battle", "Region", world.player)
|
||||
or state.can_reach("Cheating the Race", "Region", world.player)
|
||||
or state.can_reach("The Golden Vault", "Region", world.player)
|
||||
or state.can_reach("Down with the Mafia!", "Region", world.player))
|
||||
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Old Man (Seaside Spaghetti)", world.player),
|
||||
lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
|
||||
or state.can_reach("Barrel Battle", "Region", world.player)
|
||||
or state.can_reach("Cheating the Race", "Region", world.player)
|
||||
or state.can_reach("The Golden Vault", "Region", world.player)
|
||||
or state.can_reach("Down with the Mafia!", "Region", world.player))
|
||||
|
||||
# Only available outside She Came from Outer Space
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Mafia Geek Platform", world.player),
|
||||
lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
|
||||
or state.can_reach("Barrel Battle", "Region", world.player)
|
||||
or state.can_reach("Down with the Mafia!", "Region", world.player)
|
||||
or state.can_reach("Cheating the Race", "Region", world.player)
|
||||
or state.can_reach("Heating Up Mafia Town", "Region", world.player)
|
||||
or state.can_reach("The Golden Vault", "Region", world.player))
|
||||
|
||||
# Only available outside Down with the Mafia! (for some reason)
|
||||
add_rule(world.multiworld.get_location("Mafia Town - On Scaffolding", world.player),
|
||||
lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
|
||||
or state.can_reach("Barrel Battle", "Region", world.player)
|
||||
or state.can_reach("She Came from Outer Space", "Region", world.player)
|
||||
or state.can_reach("Cheating the Race", "Region", world.player)
|
||||
or state.can_reach("Heating Up Mafia Town", "Region", world.player)
|
||||
or state.can_reach("The Golden Vault", "Region", world.player))
|
||||
|
||||
# For some reason, the brewing crate is removed in HUMT
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Secret Cave", world.player),
|
||||
lambda state: state.has("HUMT Access", world.player), "or")
|
||||
|
||||
# Can bounce across the lava to get this without Hookshot (need to die though)
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player),
|
||||
lambda state: state.has("HUMT Access", world.player), "or")
|
||||
|
||||
if world.options.CTRLogic == CTRLogic.option_nothing:
|
||||
set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: True)
|
||||
elif world.options.CTRLogic == CTRLogic.option_sprint:
|
||||
add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.SPRINT), "or")
|
||||
elif world.options.CTRLogic == CTRLogic.option_scooter:
|
||||
add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.SPRINT)
|
||||
and state.has("Scooter Badge", world.player), "or")
|
||||
|
||||
|
||||
def set_botb_rules(world: "HatInTimeWorld"):
|
||||
if not world.options.UmbrellaLogic and get_difficulty(world) < Difficulty.MODERATE:
|
||||
set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player),
|
||||
lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
|
||||
set_rule(world.multiworld.get_location("Dead Bird Studio - Tepee Chest", world.player),
|
||||
lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
|
||||
set_rule(world.multiworld.get_location("Dead Bird Studio - Conductor Chest", world.player),
|
||||
lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
|
||||
set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player),
|
||||
lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
|
||||
|
||||
|
||||
def set_subcon_rules(world: "HatInTimeWorld"):
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player)
|
||||
or can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
# You can't skip over the boss arena wall without cherry hover, so these two need to be set this way
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
|
||||
lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world)
|
||||
and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
|
||||
|
||||
# The painting wall can't be skipped without cherry hover, which is Expert
|
||||
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
|
||||
and has_paintings(state, world, 1, False))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Subcon Forest - Act 2", world.player),
|
||||
lambda state: state.has("Snatcher's Contract - The Subcon Well", world.player))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Subcon Forest - Act 3", world.player),
|
||||
lambda state: state.has("Snatcher's Contract - Toilet of Doom", world.player))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Subcon Forest - Act 4", world.player),
|
||||
lambda state: state.has("Snatcher's Contract - Queen Vanessa's Manor", world.player))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Subcon Forest - Act 5", world.player),
|
||||
lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player))
|
||||
|
||||
if painting_logic(world):
|
||||
add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player),
|
||||
lambda state: has_paintings(state, world, 1, False))
|
||||
|
||||
|
||||
def set_alps_rules(world: "HatInTimeWorld"):
|
||||
add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.BREWING))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player),
|
||||
lambda state: can_use_hookshot(state, world))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("-> The Windmill", world.player),
|
||||
lambda state: can_use_hookshot(state, world))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
add_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.SPRINT) or can_use_hat(state, world, HatType.TIME_STOP))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player),
|
||||
lambda state: can_clear_alpine(state, world))
|
||||
|
||||
add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player),
|
||||
lambda state: state.has("AFR Access", world.player)
|
||||
and can_use_hookshot(state, world)
|
||||
and can_hit(state, world, True))
|
||||
|
||||
|
||||
def set_dlc1_rules(world: "HatInTimeWorld"):
|
||||
add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player),
|
||||
lambda state: can_use_hookshot(state, world))
|
||||
|
||||
# This particular item isn't present in Act 3 for some reason, yes in vanilla too
|
||||
add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player),
|
||||
lambda state: state.can_reach("Bon Voyage!", "Region", world.player)
|
||||
or state.can_reach("Ship Shape", "Region", world.player))
|
||||
|
||||
|
||||
def set_dlc2_rules(world: "HatInTimeWorld"):
|
||||
add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player),
|
||||
lambda state: state.has("Metro Ticket - Green", world.player)
|
||||
or state.has("Metro Ticket - Blue", world.player))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player),
|
||||
lambda state: state.has("Metro Ticket - Pink", world.player)
|
||||
or state.has("Metro Ticket - Yellow", world.player) and state.has("Metro Ticket - Blue", world.player))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Nyakuza Metro - Finale", world.player),
|
||||
lambda state: can_clear_metro(state, world))
|
||||
|
||||
add_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
||||
lambda state: state.has("Metro Ticket - Yellow", world.player)
|
||||
and state.has("Metro Ticket - Blue", world.player)
|
||||
and state.has("Metro Ticket - Pink", world.player))
|
||||
|
||||
for key in shop_locations.keys():
|
||||
if "Green Clean Station Thug B" in key and is_location_valid(world, key):
|
||||
add_rule(world.multiworld.get_location(key, world.player),
|
||||
lambda state: state.has("Metro Ticket - Yellow", world.player), "or")
|
||||
|
||||
|
||||
def reg_act_connection(world: "HatInTimeWorld", region: Union[str, Region], unlocked_entrance: Union[str, Entrance]):
|
||||
reg: Region
|
||||
entrance: Entrance
|
||||
if isinstance(region, str):
|
||||
reg = world.multiworld.get_region(region, world.player)
|
||||
else:
|
||||
reg = region
|
||||
|
||||
if isinstance(unlocked_entrance, str):
|
||||
entrance = world.multiworld.get_entrance(unlocked_entrance, world.player)
|
||||
else:
|
||||
entrance = unlocked_entrance
|
||||
|
||||
world.multiworld.register_indirect_condition(reg, entrance)
|
||||
|
||||
|
||||
# See randomize_act_entrances in Regions.py
|
||||
# Called before set_rules
|
||||
def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]):
|
||||
|
||||
# This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances.
|
||||
for entrance in regions["Time Rift - Gallery"].entrances:
|
||||
add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING)
|
||||
and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS]))
|
||||
|
||||
for entrance in regions["Time Rift - The Lab"].entrances:
|
||||
add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER)
|
||||
and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE]))
|
||||
|
||||
for entrance in regions["Time Rift - Sewers"].entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 4"))
|
||||
reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 4",
|
||||
world.player).connected_region, entrance)
|
||||
|
||||
for entrance in regions["Time Rift - Bazaar"].entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 6"))
|
||||
reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 6",
|
||||
world.player).connected_region, entrance)
|
||||
|
||||
for entrance in regions["Time Rift - Mafia of Cooks"].entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger"))
|
||||
|
||||
for entrance in regions["Time Rift - The Owl Express"].entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 2"))
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 3"))
|
||||
reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 2",
|
||||
world.player).connected_region, entrance)
|
||||
reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 3",
|
||||
world.player).connected_region, entrance)
|
||||
|
||||
for entrance in regions["Time Rift - The Moon"].entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 4"))
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 5"))
|
||||
reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 4",
|
||||
world.player).connected_region, entrance)
|
||||
reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 5",
|
||||
world.player).connected_region, entrance)
|
||||
|
||||
for entrance in regions["Time Rift - Dead Bird Studio"].entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Train"))
|
||||
|
||||
for entrance in regions["Time Rift - Pipe"].entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 2"))
|
||||
reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 2",
|
||||
world.player).connected_region, entrance)
|
||||
if painting_logic(world):
|
||||
add_rule(entrance, lambda state: has_paintings(state, world, 2))
|
||||
|
||||
for entrance in regions["Time Rift - Village"].entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 4"))
|
||||
reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 4",
|
||||
world.player).connected_region, entrance)
|
||||
|
||||
if painting_logic(world):
|
||||
add_rule(entrance, lambda state: has_paintings(state, world, 2))
|
||||
|
||||
for entrance in regions["Time Rift - Sleepy Subcon"].entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO"))
|
||||
if painting_logic(world):
|
||||
add_rule(entrance, lambda state: has_paintings(state, world, 3))
|
||||
|
||||
for entrance in regions["Time Rift - Curly Tail Trail"].entrances:
|
||||
add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player))
|
||||
|
||||
for entrance in regions["Time Rift - The Twilight Bell"].entrances:
|
||||
add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player))
|
||||
|
||||
for entrance in regions["Time Rift - Alpine Skyline"].entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
||||
|
||||
if world.is_dlc1():
|
||||
for entrance in regions["Time Rift - Balcony"].entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
|
||||
|
||||
for entrance in regions["Time Rift - Deep Sea"].entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
|
||||
|
||||
if world.is_dlc2():
|
||||
for entrance in regions["Time Rift - Rumbi Factory"].entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace"))
|
||||
|
||||
|
||||
# Basically the same as above, but without the need of the dict since we are just setting defaults
|
||||
# Called if Act Rando is disabled
|
||||
def set_default_rift_rules(world: "HatInTimeWorld"):
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Gallery", world.player).entrances:
|
||||
add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING)
|
||||
and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS]))
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - The Lab", world.player).entrances:
|
||||
add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER)
|
||||
and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE]))
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Sewers", world.player).entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 4"))
|
||||
reg_act_connection(world, "Down with the Mafia!", entrance.name)
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Bazaar", world.player).entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 6"))
|
||||
reg_act_connection(world, "Heating Up Mafia Town", entrance.name)
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Mafia of Cooks", world.player).entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger"))
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - The Owl Express", world.player).entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 2"))
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 3"))
|
||||
reg_act_connection(world, "Murder on the Owl Express", entrance.name)
|
||||
reg_act_connection(world, "Picture Perfect", entrance.name)
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - The Moon", world.player).entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 4"))
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 5"))
|
||||
reg_act_connection(world, "Train Rush", entrance.name)
|
||||
reg_act_connection(world, "The Big Parade", entrance.name)
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Dead Bird Studio", world.player).entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Train"))
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Pipe", world.player).entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 2"))
|
||||
reg_act_connection(world, "The Subcon Well", entrance.name)
|
||||
if painting_logic(world):
|
||||
add_rule(entrance, lambda state: has_paintings(state, world, 2))
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Village", world.player).entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 4"))
|
||||
reg_act_connection(world, "Queen Vanessa's Manor", entrance.name)
|
||||
if painting_logic(world):
|
||||
add_rule(entrance, lambda state: has_paintings(state, world, 2))
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Sleepy Subcon", world.player).entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO"))
|
||||
if painting_logic(world):
|
||||
add_rule(entrance, lambda state: has_paintings(state, world, 3))
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Curly Tail Trail", world.player).entrances:
|
||||
add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player))
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - The Twilight Bell", world.player).entrances:
|
||||
add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player))
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
||||
|
||||
if world.is_dlc1():
|
||||
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:
|
||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
|
||||
|
||||
if world.is_dlc2():
|
||||
for entrance in world.multiworld.get_region("Time Rift - Rumbi Factory", world.player).entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace"))
|
||||
|
||||
|
||||
def set_event_rules(world: "HatInTimeWorld"):
|
||||
for (name, data) in event_locs.items():
|
||||
if not is_location_valid(world, name):
|
||||
continue
|
||||
|
||||
event: Location = world.multiworld.get_location(name, world.player)
|
||||
|
||||
if data.act_event:
|
||||
add_rule(event, world.multiworld.get_location(f"Act Completion ({data.region})", world.player).access_rule)
|
||||
86
worlds/ahit/Types.py
Normal file
86
worlds/ahit/Types.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from enum import IntEnum, IntFlag
|
||||
from typing import NamedTuple, Optional, List
|
||||
from BaseClasses import Location, Item, ItemClassification
|
||||
|
||||
|
||||
class HatInTimeLocation(Location):
|
||||
game = "A Hat in Time"
|
||||
|
||||
|
||||
class HatInTimeItem(Item):
|
||||
game = "A Hat in Time"
|
||||
|
||||
|
||||
class HatType(IntEnum):
|
||||
SPRINT = 0
|
||||
BREWING = 1
|
||||
ICE = 2
|
||||
DWELLER = 3
|
||||
TIME_STOP = 4
|
||||
|
||||
|
||||
class HitType(IntEnum):
|
||||
none = 0
|
||||
umbrella = 1
|
||||
umbrella_or_brewing = 2
|
||||
dweller_bell = 3
|
||||
|
||||
|
||||
class HatDLC(IntFlag):
|
||||
none = 0b000
|
||||
dlc1 = 0b001
|
||||
dlc2 = 0b010
|
||||
death_wish = 0b100
|
||||
dlc1_dw = 0b101
|
||||
dlc2_dw = 0b110
|
||||
|
||||
|
||||
class ChapterIndex(IntEnum):
|
||||
SPACESHIP = 0
|
||||
MAFIA = 1
|
||||
BIRDS = 2
|
||||
SUBCON = 3
|
||||
ALPINE = 4
|
||||
FINALE = 5
|
||||
CRUISE = 6
|
||||
METRO = 7
|
||||
|
||||
|
||||
class Difficulty(IntEnum):
|
||||
NORMAL = -1
|
||||
MODERATE = 0
|
||||
HARD = 1
|
||||
EXPERT = 2
|
||||
|
||||
|
||||
class LocData(NamedTuple):
|
||||
id: int = 0
|
||||
region: str = ""
|
||||
required_hats: List[HatType] = []
|
||||
hookshot: bool = False
|
||||
dlc_flags: HatDLC = HatDLC.none
|
||||
paintings: int = 0 # Paintings required for Subcon painting shuffle
|
||||
misc_required: List[str] = []
|
||||
|
||||
# For UmbrellaLogic setting only.
|
||||
hit_type: HitType = HitType.none
|
||||
|
||||
# Other
|
||||
act_event: bool = False # Only used for event locations. Copy access rule from act completion
|
||||
nyakuza_thug: str = "" # Name of Nyakuza thug NPC (for metro shops)
|
||||
snatcher_coin: str = "" # Only for Snatcher Coin event locations, name of the Snatcher Coin item
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
code: Optional[int]
|
||||
classification: ItemClassification
|
||||
dlc_flags: Optional[HatDLC] = HatDLC.none
|
||||
|
||||
|
||||
hat_type_to_item = {
|
||||
HatType.SPRINT: "Sprint Hat",
|
||||
HatType.BREWING: "Brewing Hat",
|
||||
HatType.ICE: "Ice Hat",
|
||||
HatType.DWELLER: "Dweller Mask",
|
||||
HatType.TIME_STOP: "Time Stop Hat",
|
||||
}
|
||||
374
worlds/ahit/__init__.py
Normal file
374
worlds/ahit/__init__.py
Normal file
@@ -0,0 +1,374 @@
|
||||
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
|
||||
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
|
||||
calculate_yarn_costs
|
||||
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
|
||||
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
||||
get_total_locations
|
||||
from .Rules import set_rules
|
||||
from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups
|
||||
from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item
|
||||
from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
|
||||
from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
|
||||
from worlds.AutoWorld import World, WebWorld, CollectionState
|
||||
from typing import List, Dict, TextIO
|
||||
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
|
||||
from Utils import local_path
|
||||
|
||||
|
||||
def launch_client():
|
||||
from .Client import launch
|
||||
launch_subprocess(launch, name="AHITClient")
|
||||
|
||||
|
||||
components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client,
|
||||
component_type=Type.CLIENT, icon='yatta'))
|
||||
|
||||
icon_paths['yatta'] = local_path('data', 'yatta.png')
|
||||
|
||||
|
||||
class AWebInTime(WebWorld):
|
||||
theme = "partyTime"
|
||||
option_groups = create_option_groups()
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide for setting up A Hat in Time to be played in Archipelago.",
|
||||
"English",
|
||||
"ahit_en.md",
|
||||
"setup/en",
|
||||
["CookieCat"]
|
||||
)]
|
||||
|
||||
|
||||
class HatInTimeWorld(World):
|
||||
"""
|
||||
A Hat in Time is a cute-as-peck 3D platformer featuring a little girl who stitches hats for wicked powers!
|
||||
Freely explore giant worlds and recover Time Pieces to travel to new heights!
|
||||
"""
|
||||
|
||||
game = "A Hat in Time"
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = get_location_names()
|
||||
options_dataclass = AHITOptions
|
||||
options: AHITOptions
|
||||
item_name_groups = relic_groups
|
||||
web = AWebInTime()
|
||||
|
||||
def __init__(self, multiworld: "MultiWorld", player: int):
|
||||
super().__init__(multiworld, player)
|
||||
self.act_connections: Dict[str, str] = {}
|
||||
self.shop_locs: List[str] = []
|
||||
|
||||
self.hat_craft_order: List[HatType] = [HatType.SPRINT, HatType.BREWING, HatType.ICE,
|
||||
HatType.DWELLER, HatType.TIME_STOP]
|
||||
|
||||
self.hat_yarn_costs: Dict[HatType, int] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1,
|
||||
HatType.DWELLER: -1, HatType.TIME_STOP: -1}
|
||||
|
||||
self.chapter_timepiece_costs: Dict[ChapterIndex, int] = {ChapterIndex.MAFIA: -1,
|
||||
ChapterIndex.BIRDS: -1,
|
||||
ChapterIndex.SUBCON: -1,
|
||||
ChapterIndex.ALPINE: -1,
|
||||
ChapterIndex.FINALE: -1,
|
||||
ChapterIndex.CRUISE: -1,
|
||||
ChapterIndex.METRO: -1}
|
||||
self.excluded_dws: List[str] = []
|
||||
self.excluded_bonuses: List[str] = []
|
||||
self.dw_shuffle: List[str] = []
|
||||
self.nyakuza_thug_items: Dict[str, int] = {}
|
||||
self.badge_seller_count: int = 0
|
||||
|
||||
def generate_early(self):
|
||||
adjust_options(self)
|
||||
|
||||
if self.options.StartWithCompassBadge:
|
||||
self.multiworld.push_precollected(self.create_item("Compass Badge"))
|
||||
|
||||
if self.is_dw_only():
|
||||
return
|
||||
|
||||
# If our starting chapter is 4 and act rando isn't on, force hookshot into inventory
|
||||
# If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock
|
||||
start_chapter: ChapterIndex = ChapterIndex(self.options.StartingChapter)
|
||||
|
||||
if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON:
|
||||
if not self.options.ActRandomizer:
|
||||
if start_chapter == ChapterIndex.ALPINE:
|
||||
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
|
||||
if self.options.UmbrellaLogic:
|
||||
self.multiworld.push_precollected(self.create_item("Umbrella"))
|
||||
|
||||
if start_chapter == ChapterIndex.SUBCON and self.options.ShuffleSubconPaintings:
|
||||
self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock"))
|
||||
|
||||
def create_regions(self):
|
||||
# noinspection PyClassVar
|
||||
self.topology_present = bool(self.options.ActRandomizer)
|
||||
|
||||
create_regions(self)
|
||||
if self.options.EnableDeathWish:
|
||||
create_dw_regions(self)
|
||||
|
||||
if self.is_dw_only():
|
||||
return
|
||||
|
||||
create_events(self)
|
||||
if self.is_dw():
|
||||
if "Snatcher's Hit List" not in self.excluded_dws or "Camera Tourist" not in self.excluded_dws:
|
||||
create_enemy_events(self)
|
||||
|
||||
# place vanilla contract locations if contract shuffle is off
|
||||
if not self.options.ShuffleActContracts:
|
||||
for name in contract_locations.keys():
|
||||
self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name))
|
||||
|
||||
def create_items(self):
|
||||
if self.has_yarn():
|
||||
calculate_yarn_costs(self)
|
||||
|
||||
if self.options.RandomizeHatOrder:
|
||||
self.random.shuffle(self.hat_craft_order)
|
||||
if self.options.RandomizeHatOrder == RandomizeHatOrder.option_time_stop_last:
|
||||
self.hat_craft_order.remove(HatType.TIME_STOP)
|
||||
self.hat_craft_order.append(HatType.TIME_STOP)
|
||||
|
||||
# move precollected hats to the start of the list
|
||||
for i in range(5):
|
||||
hat = HatType(i)
|
||||
if self.is_hat_precollected(hat):
|
||||
self.hat_craft_order.remove(hat)
|
||||
self.hat_craft_order.insert(0, hat)
|
||||
|
||||
self.multiworld.itempool += create_itempool(self)
|
||||
|
||||
def set_rules(self):
|
||||
if self.is_dw_only():
|
||||
# we already have all items if this is the case, no need for rules
|
||||
self.multiworld.push_precollected(HatInTimeItem("Death Wish Only Mode", ItemClassification.progression,
|
||||
None, self.player))
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode",
|
||||
self.player)
|
||||
|
||||
if not self.options.DWEnableBonus:
|
||||
for name in death_wishes:
|
||||
if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2():
|
||||
continue
|
||||
|
||||
if self.options.DWShuffle and name not in self.dw_shuffle:
|
||||
continue
|
||||
|
||||
full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player)
|
||||
full_clear.address = None
|
||||
full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, self.player))
|
||||
full_clear.show_in_spoiler = False
|
||||
|
||||
return
|
||||
|
||||
if self.options.ActRandomizer:
|
||||
randomize_act_entrances(self)
|
||||
|
||||
set_rules(self)
|
||||
|
||||
if self.is_dw():
|
||||
set_dw_rules(self)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return create_item(self, name)
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
slot_data: dict = {"Chapter1Cost": self.chapter_timepiece_costs[ChapterIndex.MAFIA],
|
||||
"Chapter2Cost": self.chapter_timepiece_costs[ChapterIndex.BIRDS],
|
||||
"Chapter3Cost": self.chapter_timepiece_costs[ChapterIndex.SUBCON],
|
||||
"Chapter4Cost": self.chapter_timepiece_costs[ChapterIndex.ALPINE],
|
||||
"Chapter5Cost": self.chapter_timepiece_costs[ChapterIndex.FINALE],
|
||||
"Chapter6Cost": self.chapter_timepiece_costs[ChapterIndex.CRUISE],
|
||||
"Chapter7Cost": self.chapter_timepiece_costs[ChapterIndex.METRO],
|
||||
"BadgeSellerItemCount": self.badge_seller_count,
|
||||
"SeedNumber": str(self.multiworld.seed), # For shop prices
|
||||
"SeedName": self.multiworld.seed_name,
|
||||
"TotalLocations": get_total_locations(self)}
|
||||
|
||||
if self.has_yarn():
|
||||
slot_data.setdefault("SprintYarnCost", self.hat_yarn_costs[HatType.SPRINT])
|
||||
slot_data.setdefault("BrewingYarnCost", self.hat_yarn_costs[HatType.BREWING])
|
||||
slot_data.setdefault("IceYarnCost", self.hat_yarn_costs[HatType.ICE])
|
||||
slot_data.setdefault("DwellerYarnCost", self.hat_yarn_costs[HatType.DWELLER])
|
||||
slot_data.setdefault("TimeStopYarnCost", self.hat_yarn_costs[HatType.TIME_STOP])
|
||||
slot_data.setdefault("Hat1", int(self.hat_craft_order[0]))
|
||||
slot_data.setdefault("Hat2", int(self.hat_craft_order[1]))
|
||||
slot_data.setdefault("Hat3", int(self.hat_craft_order[2]))
|
||||
slot_data.setdefault("Hat4", int(self.hat_craft_order[3]))
|
||||
slot_data.setdefault("Hat5", int(self.hat_craft_order[4]))
|
||||
|
||||
if self.options.ActRandomizer:
|
||||
for name in self.act_connections.keys():
|
||||
slot_data[name] = self.act_connections[name]
|
||||
|
||||
if self.is_dlc2() and not self.is_dw_only():
|
||||
for name in self.nyakuza_thug_items.keys():
|
||||
slot_data[name] = self.nyakuza_thug_items[name]
|
||||
|
||||
if self.is_dw():
|
||||
i = 0
|
||||
for name in self.excluded_dws:
|
||||
if self.options.EndGoal.value == EndGoal.option_seal_the_deal and name == "Seal the Deal":
|
||||
continue
|
||||
|
||||
slot_data[f"excluded_dw{i}"] = dw_classes[name]
|
||||
i += 1
|
||||
|
||||
i = 0
|
||||
if not self.options.DWAutoCompleteBonuses:
|
||||
for name in self.excluded_bonuses:
|
||||
if name in self.excluded_dws:
|
||||
continue
|
||||
|
||||
slot_data[f"excluded_bonus{i}"] = dw_classes[name]
|
||||
i += 1
|
||||
|
||||
if self.options.DWShuffle:
|
||||
shuffled_dws = self.dw_shuffle
|
||||
for i in range(len(shuffled_dws)):
|
||||
slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]]
|
||||
|
||||
shop_item_names: Dict[str, str] = {}
|
||||
for name in self.shop_locs:
|
||||
loc: Location = self.multiworld.get_location(name, self.player)
|
||||
assert loc.item
|
||||
item_name: str
|
||||
if loc.item.classification is ItemClassification.trap and loc.item.game == "A Hat in Time":
|
||||
item_name = get_shop_trap_name(self)
|
||||
else:
|
||||
item_name = loc.item.name
|
||||
|
||||
shop_item_names.setdefault(str(loc.address), item_name)
|
||||
|
||||
slot_data["ShopItemNames"] = shop_item_names
|
||||
|
||||
for name, value in self.options.as_dict(*self.options_dataclass.type_hints).items():
|
||||
if name in slot_data_options:
|
||||
slot_data[name] = value
|
||||
|
||||
return slot_data
|
||||
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
|
||||
if self.is_dw_only() or not self.options.ActRandomizer:
|
||||
return
|
||||
|
||||
new_hint_data = {}
|
||||
alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill",
|
||||
"The Twilight Bell", "Alpine Skyline Area", "Alpine Skyline Area (TIHS)"]
|
||||
|
||||
metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"]
|
||||
|
||||
for key, data in location_table.items():
|
||||
if not is_location_valid(self, key):
|
||||
continue
|
||||
|
||||
location = self.multiworld.get_location(key, self.player)
|
||||
region_name: str
|
||||
|
||||
if data.region in alpine_regions:
|
||||
region_name = "Alpine Free Roam"
|
||||
elif data.region in metro_regions:
|
||||
region_name = "Nyakuza Free Roam"
|
||||
elif "Dead Bird Studio - " in data.region:
|
||||
region_name = "Dead Bird Studio"
|
||||
elif data.region in chapter_act_info.keys():
|
||||
region_name = location.parent_region.name
|
||||
else:
|
||||
continue
|
||||
|
||||
new_hint_data[location.address] = get_shuffled_region(self, region_name)
|
||||
|
||||
if self.is_dlc1() and self.options.Tasksanity:
|
||||
ship_shape_region = get_shuffled_region(self, "Ship Shape")
|
||||
id_start: int = TASKSANITY_START_ID
|
||||
for i in range(self.options.TasksanityCheckCount):
|
||||
new_hint_data[id_start+i] = ship_shape_region
|
||||
|
||||
hint_data[self.player] = new_hint_data
|
||||
|
||||
def write_spoiler_header(self, spoiler_handle: TextIO):
|
||||
for i in self.chapter_timepiece_costs:
|
||||
spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.chapter_timepiece_costs[ChapterIndex(i)]))
|
||||
|
||||
for hat in self.hat_craft_order:
|
||||
spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, self.hat_yarn_costs[hat]))
|
||||
|
||||
def collect(self, state: "CollectionState", item: "Item") -> bool:
|
||||
old_count: int = state.count(item.name, self.player)
|
||||
change = super().collect(state, item)
|
||||
if change and old_count == 0:
|
||||
if "Stamp" in item.name:
|
||||
if "2 Stamp" in item.name:
|
||||
state.prog_items[self.player]["Stamps"] += 2
|
||||
else:
|
||||
state.prog_items[self.player]["Stamps"] += 1
|
||||
elif "(Zero Jumps)" in item.name:
|
||||
state.prog_items[self.player]["Zero Jumps"] += 1
|
||||
elif item.name in hit_list.keys():
|
||||
if item.name not in bosses:
|
||||
state.prog_items[self.player]["Enemy"] += 1
|
||||
else:
|
||||
state.prog_items[self.player]["Boss"] += 1
|
||||
|
||||
return change
|
||||
|
||||
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
||||
old_count: int = state.count(item.name, self.player)
|
||||
change = super().collect(state, item)
|
||||
if change and old_count == 1:
|
||||
if "Stamp" in item.name:
|
||||
if "2 Stamp" in item.name:
|
||||
state.prog_items[self.player]["Stamps"] -= 2
|
||||
else:
|
||||
state.prog_items[self.player]["Stamps"] -= 1
|
||||
elif "(Zero Jumps)" in item.name:
|
||||
state.prog_items[self.player]["Zero Jumps"] -= 1
|
||||
elif item.name in hit_list.keys():
|
||||
if item.name not in bosses:
|
||||
state.prog_items[self.player]["Enemy"] -= 1
|
||||
else:
|
||||
state.prog_items[self.player]["Boss"] -= 1
|
||||
|
||||
return change
|
||||
|
||||
def has_yarn(self) -> bool:
|
||||
return not self.is_dw_only() and not self.options.HatItems
|
||||
|
||||
def is_hat_precollected(self, hat: HatType) -> bool:
|
||||
for item in self.multiworld.precollected_items[self.player]:
|
||||
if item.name == hat_type_to_item[hat]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_dlc1(self) -> bool:
|
||||
return bool(self.options.EnableDLC1)
|
||||
|
||||
def is_dlc2(self) -> bool:
|
||||
return bool(self.options.EnableDLC2)
|
||||
|
||||
def is_dw(self) -> bool:
|
||||
return bool(self.options.EnableDeathWish)
|
||||
|
||||
def is_dw_only(self) -> bool:
|
||||
return self.is_dw() and bool(self.options.DeathWishOnly)
|
||||
|
||||
def is_dw_excluded(self, name: str) -> bool:
|
||||
# don't exclude Seal the Deal if it's our goal
|
||||
if self.options.EndGoal.value == EndGoal.option_seal_the_deal and name == "Seal the Deal" \
|
||||
and f"{name} - Main Objective" not in self.options.exclude_locations:
|
||||
return False
|
||||
|
||||
if name in self.excluded_dws:
|
||||
return True
|
||||
|
||||
return f"{name} - Main Objective" in self.options.exclude_locations
|
||||
|
||||
def is_bonus_excluded(self, name: str) -> bool:
|
||||
if self.is_dw_excluded(name) or name in self.excluded_bonuses:
|
||||
return True
|
||||
|
||||
return f"{name} - All Clear" in self.options.exclude_locations
|
||||
53
worlds/ahit/docs/en_A Hat in Time.md
Normal file
53
worlds/ahit/docs/en_A Hat in Time.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# A Hat in Time
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Items which the player would normally acquire throughout the game have been moved around.
|
||||
Chapter costs are randomized in a progressive order based on your options,
|
||||
so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order.
|
||||
If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well.
|
||||
|
||||
To unlock and access a chapter's Time Rift in act shuffle,
|
||||
the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed,
|
||||
and then you must enter a level that allows you to access that Time Rift.
|
||||
For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game.
|
||||
To unlock this Time Rift in act shuffle (and therefore the level it contains)
|
||||
you must complete the level that was shuffled in place of Heating Up Mafia Town
|
||||
and then enter the Time Rift through a Mafia Town level.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
|
||||
Time Pieces, Relics, Yarn, Badges, and most other items are shuffled.
|
||||
Unlike in the vanilla game, yarn is typeless, and hats will be automatically stitched
|
||||
in a set order once you gather enough yarn for each hat.
|
||||
Hats can also optionally be shuffled as individual items instead.
|
||||
Any items in the world, shops, act completions,
|
||||
and optionally storybook pages or Death Wish contracts are locations.
|
||||
|
||||
Any freestanding items that are considered to be progression or useful
|
||||
will have a rainbow streak particle attached to them.
|
||||
Filler items will have a white glow attached to them instead.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
|
||||
certain items to your own world.
|
||||
|
||||
## What does another world's item look like in A Hat in Time?
|
||||
|
||||
Items belonging to other worlds are represented by a badge with the Archipelago logo on it.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
When the player receives an item, it will play the item collect effect and information about the item
|
||||
will be printed on the screen and in the in-game developer console.
|
||||
|
||||
## Is the DLC required to play A Hat in Time in Archipelago?
|
||||
|
||||
No, the DLC expansions are not required to play. Their content can be enabled through certain options
|
||||
that are disabled by default, but please don't turn them on if you don't own the respective DLC.
|
||||
102
worlds/ahit/docs/setup_en.md
Normal file
102
worlds/ahit/docs/setup_en.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Setup Guide for A Hat in Time in Archipelago
|
||||
|
||||
## Required Software
|
||||
- [Steam release of A Hat in Time](https://store.steampowered.com/app/253230/A_Hat_in_Time/)
|
||||
|
||||
- [Archipelago Workshop Mod for A Hat in Time](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601)
|
||||
|
||||
|
||||
## Optional Software
|
||||
- [A Hat in Time Archipelago Map Tracker](https://github.com/Mysteryem/ahit-poptracker/releases), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console)
|
||||
This may not work for some browsers. If that's the case, and you're on Windows, open the Run dialog using Win+R,
|
||||
paste the link into the box, and hit Enter.
|
||||
|
||||
|
||||
2. In the Steam console, enter the following command:
|
||||
`download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!***
|
||||
This can take a while to finish (30+ minutes) depending on your connection speed, so please be patient. Additionally,
|
||||
**try to prevent your connection from being interrupted or slowed while Steam is downloading the depot,**
|
||||
or else the download may potentially become corrupted (see first FAQ issue below).
|
||||
|
||||
|
||||
3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder.
|
||||
|
||||
|
||||
4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder.
|
||||
|
||||
|
||||
5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`.
|
||||
In this new text file, input the number **253230** on the first line.
|
||||
|
||||
|
||||
6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like.
|
||||
You will use this shortcut to open the Archipelago-compatible version of A Hat in Time.
|
||||
|
||||
|
||||
7. Start up the game using your new shortcut. To confirm if you are on the correct version,
|
||||
go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running
|
||||
the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked.
|
||||
|
||||
|
||||
## Connecting to the Archipelago server
|
||||
|
||||
To connect to the multiworld server, simply run the **ArchipelagoAHITClient**
|
||||
(or run it from the Launcher if you have the apworld installed) and connect it to the Archipelago server.
|
||||
The game will connect to the client automatically when you create a new save file.
|
||||
|
||||
|
||||
## Console Commands
|
||||
|
||||
Commands will not work on the title screen, you must be in-game to use them. To use console commands,
|
||||
make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game.
|
||||
|
||||
`ap_say <message>` - Send a chat message to the server. Supports commands, such as `!hint` or `!release`.
|
||||
|
||||
`ap_deathlink` - Toggle Death Link.
|
||||
|
||||
|
||||
## FAQ/Common Issues
|
||||
### I followed the setup, but I receive an odd error message upon starting the game or creating a save file!
|
||||
If you receive an error message such as
|
||||
**"Failed to find default engine .ini to retrieve My Documents subdirectory to use. Force quitting."** or
|
||||
**"Failed to load map "hub_spaceship"** after booting up the game or creating a save file respectively, then the depot
|
||||
download was likely corrupted. The only way to fix this is to start the entire download all over again.
|
||||
Unfortunately, this appears to be an underlying issue with Steam's depot downloader. The only way to really prevent this
|
||||
from happening is to ensure that your connection is not interrupted or slowed while downloading.
|
||||
|
||||
### The game keeps crashing on startup after the splash screen!
|
||||
This issue is unfortunately very hard to fix, and the underlying cause is not known. If it does happen however,
|
||||
try the following:
|
||||
|
||||
- Close Steam **entirely**.
|
||||
- Open the downpatched version of the game (with Steam closed) and allow it to load to the titlescreen.
|
||||
- Close the game, and then open Steam again.
|
||||
- After launching the game, the issue should hopefully disappear. If not, repeat the above steps until it does.
|
||||
|
||||
### I followed the setup, but "Live Game Events" still shows up in the options menu!
|
||||
The most common cause of this is the `steam_appid.txt` file. If you're on Windows 10, file extensions are hidden by
|
||||
default (thanks Microsoft). You likely made the mistake of still naming the file `steam_appid.txt`, which, since file
|
||||
extensions are hidden, would result in the file being named `steam_appid.txt.txt`, which is incorrect.
|
||||
To show file extensions in Windows 10, open any folder, click the View tab at the top, and check
|
||||
"File name extensions". Then you can correct the name of the file. If the name of the file is correct,
|
||||
and you're still running into the issue, re-read the setup guide again in case you missed a step.
|
||||
If you still can't get it to work, ask for help in the Discord thread.
|
||||
|
||||
### The game is running on the older version, but it's not connecting when starting a new save!
|
||||
For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
|
||||
(rocket icon) in-game, and re-enable the mod.
|
||||
|
||||
### Why do relics disappear from the stands in the Spaceship after they're completed?
|
||||
This is intentional behaviour. Because of how randomizer logic works, there is no way to predict the order that
|
||||
a player will place their relics. Since there are a limited amount of relic stands in the Spaceship, relics are removed
|
||||
after being completed to allow for the placement of more relics without being potentially locked out.
|
||||
The level that the relic set unlocked will stay unlocked.
|
||||
|
||||
### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
|
||||
There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
|
||||
if you have too many save files. Delete them and it should fix the problem.
|
||||
5
worlds/ahit/test/__init__.py
Normal file
5
worlds/ahit/test/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class HatInTimeTestBase(WorldTestBase):
|
||||
game = "A Hat in Time"
|
||||
31
worlds/ahit/test/test_acts.py
Normal file
31
worlds/ahit/test/test_acts.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from ..Regions import act_chapters
|
||||
from ..Rules import act_connections
|
||||
from . import HatInTimeTestBase
|
||||
|
||||
|
||||
class TestActs(HatInTimeTestBase):
|
||||
run_default_tests = False
|
||||
|
||||
options = {
|
||||
"ActRandomizer": 2,
|
||||
"EnableDLC1": 1,
|
||||
"EnableDLC2": 1,
|
||||
"ShuffleActContracts": 0,
|
||||
}
|
||||
|
||||
def test_act_shuffle(self):
|
||||
for i in range(300):
|
||||
self.world_setup()
|
||||
self.collect_all_but([""])
|
||||
|
||||
for name in act_chapters.keys():
|
||||
region = self.multiworld.get_region(name, 1)
|
||||
for entrance in region.entrances:
|
||||
if entrance.name in act_connections.keys():
|
||||
continue
|
||||
|
||||
self.assertTrue(self.can_reach_entrance(entrance.name),
|
||||
f"Can't reach {name} from {entrance}\n"
|
||||
f"{entrance.parent_region.entrances[0]} -> {entrance.parent_region} "
|
||||
f"-> {entrance} -> {name}"
|
||||
f" (expected method of access)")
|
||||
@@ -339,7 +339,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
|
||||
def new_check(location_id):
|
||||
new_locations.append(location_id)
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_names[location_id]
|
||||
location = ctx.location_names.lookup_in_slot(location_id)
|
||||
snes_logger.info(
|
||||
f'New Check: {location} ' +
|
||||
f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' +
|
||||
@@ -552,9 +552,9 @@ class ALTTPSNIClient(SNIClient):
|
||||
item = ctx.items_received[recv_index]
|
||||
recv_index += 1
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'),
|
||||
color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
|
||||
ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
|
||||
|
||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
|
||||
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\
|
||||
FreeText, Removed
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \
|
||||
StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts, FreeText, Removed
|
||||
from .EntranceShuffle import default_connections, default_dungeon_connections, \
|
||||
inverted_default_connections, inverted_default_dungeon_connections
|
||||
from .Text import TextTable
|
||||
|
||||
|
||||
class GlitchesRequired(Choice):
|
||||
@@ -721,7 +724,27 @@ class AllowCollect(DefaultOnToggle):
|
||||
display_name = "Allow Collection of checks for other players"
|
||||
|
||||
|
||||
class ALttPPlandoConnections(PlandoConnections):
|
||||
entrances = set([connection[0] for connection in (
|
||||
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
||||
*inverted_default_dungeon_connections)])
|
||||
exits = set([connection[1] for connection in (
|
||||
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
||||
*inverted_default_dungeon_connections)])
|
||||
|
||||
|
||||
class ALttPPlandoTexts(PlandoTexts):
|
||||
"""Text plando. Format is:
|
||||
- text: 'This is your text'
|
||||
at: text_key
|
||||
percentage: 100
|
||||
Percentage is an integer from 1 to 100, and defaults to 100 when omitted."""
|
||||
valid_keys = TextTable.valid_keys
|
||||
|
||||
|
||||
alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"plando_connections": ALttPPlandoConnections,
|
||||
"plando_texts": ALttPPlandoTexts,
|
||||
"start_inventory_from_pool": StartInventoryPool,
|
||||
"goal": Goal,
|
||||
"mode": Mode,
|
||||
|
||||
@@ -1269,7 +1269,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
|
||||
|
||||
# set up goals for treasure hunt
|
||||
rom.write_int16(0x180163, local_world.treasure_hunt_required)
|
||||
rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required -
|
||||
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
|
||||
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
|
||||
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
|
||||
|
||||
@@ -1372,7 +1373,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
|
||||
'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
|
||||
'Red Mail', 'Blue Mail', 'Progressive Mail',
|
||||
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)'}:
|
||||
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}:
|
||||
continue
|
||||
|
||||
set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1),
|
||||
@@ -2475,6 +2476,9 @@ def write_strings(rom, world, player):
|
||||
tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)]
|
||||
tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)]
|
||||
|
||||
triforce_pieces_required = max(0, w.treasure_hunt_required -
|
||||
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))
|
||||
|
||||
if world.goal[player] in ['triforce_hunt', 'local_triforce_hunt']:
|
||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
|
||||
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
||||
@@ -2482,16 +2486,16 @@ def write_strings(rom, world, player):
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
|
||||
else:
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
|
||||
if w.treasure_hunt_required > 1:
|
||||
if triforce_pieces_required > 1:
|
||||
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
|
||||
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
|
||||
"hidden in a hollow tree. If you bring\n%d Triforce pieces out of %d, I can reassemble it." % \
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
else:
|
||||
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
|
||||
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
|
||||
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
elif world.goal[player] in ['pedestal']:
|
||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
|
||||
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
||||
@@ -2500,20 +2504,20 @@ def write_strings(rom, world, player):
|
||||
tt['ganon_fall_in'] = Ganon1_texts[local_random.randint(0, len(Ganon1_texts) - 1)]
|
||||
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
|
||||
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
|
||||
if w.treasure_hunt_required > 1:
|
||||
if triforce_pieces_required > 1:
|
||||
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
else:
|
||||
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
|
||||
tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)]
|
||||
|
||||
@@ -2538,12 +2542,12 @@ def write_strings(rom, world, player):
|
||||
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}"
|
||||
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
|
||||
|
||||
for at, text in world.plando_texts[player].items():
|
||||
for at, text, _ in world.plando_texts[player]:
|
||||
|
||||
if at not in tt:
|
||||
raise Exception(f"No text target \"{at}\" found.")
|
||||
else:
|
||||
tt[at] = text
|
||||
tt[at] = "\n".join(text)
|
||||
|
||||
rom.write_bytes(0xE0000, tt.getBytes())
|
||||
|
||||
|
||||
@@ -399,8 +399,8 @@ def global_rules(multiworld: MultiWorld, player: int):
|
||||
set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
|
||||
if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
||||
forbid_item(multiworld.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
||||
set_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
set_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
add_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
add_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
if multiworld.pot_shuffle[player]:
|
||||
# key can (and probably will) be moved behind bombable wall
|
||||
set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
|
||||
|
||||
@@ -9,9 +9,9 @@ from worlds.generic.Rules import add_rule
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from .SubClasses import ALttPLocation
|
||||
from .EntranceShuffle import door_addresses
|
||||
|
||||
from .Items import item_name_groups
|
||||
from .Options import small_key_shuffle, RandomizeShopInventories
|
||||
|
||||
from .StateHelpers import has_hearts, can_use_bombs, can_hold_arrows
|
||||
|
||||
logger = logging.getLogger("Shops")
|
||||
@@ -66,6 +66,7 @@ class Shop:
|
||||
return 0
|
||||
|
||||
def get_bytes(self) -> List[int]:
|
||||
from .EntranceShuffle import door_addresses
|
||||
# [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index]
|
||||
entrances = self.region.entrances
|
||||
config = self.item_count
|
||||
@@ -181,7 +182,7 @@ def push_shop_inventories(multiworld):
|
||||
|
||||
|
||||
def create_shops(multiworld, player: int):
|
||||
|
||||
from .Options import RandomizeShopInventories
|
||||
player_shop_table = shop_table.copy()
|
||||
if multiworld.include_witch_hut[player]:
|
||||
player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False)
|
||||
@@ -304,6 +305,7 @@ shop_generation_types = {
|
||||
|
||||
|
||||
def set_up_shops(multiworld, player: int):
|
||||
from .Options import small_key_shuffle
|
||||
# TODO: move hard+ mode changes for shields here, utilizing the new shops
|
||||
|
||||
if multiworld.retro_bow[player]:
|
||||
@@ -426,7 +428,7 @@ def get_price_modifier(item):
|
||||
|
||||
def get_price(multiworld, item, player: int, price_type=None):
|
||||
"""Converts a raw Rupee price into a special price type"""
|
||||
|
||||
from .Options import small_key_shuffle
|
||||
if price_type:
|
||||
price_types = [price_type]
|
||||
else:
|
||||
|
||||
@@ -1289,6 +1289,415 @@ class LargeCreditBottomMapper(CharTextMapper):
|
||||
class TextTable(object):
|
||||
SIZE = 0x7355
|
||||
|
||||
valid_keys = [
|
||||
"set_cursor",
|
||||
"set_cursor2",
|
||||
"game_over_menu",
|
||||
"var_test",
|
||||
"follower_no_enter",
|
||||
"choice_1_3",
|
||||
"choice_2_3",
|
||||
"choice_3_3",
|
||||
"choice_1_2",
|
||||
"choice_2_2",
|
||||
"uncle_leaving_text",
|
||||
"uncle_dying_sewer",
|
||||
"tutorial_guard_1",
|
||||
"tutorial_guard_2",
|
||||
"tutorial_guard_3",
|
||||
"tutorial_guard_4",
|
||||
"tutorial_guard_5",
|
||||
"tutorial_guard_6",
|
||||
"tutorial_guard_7",
|
||||
"priest_sanctuary_before_leave",
|
||||
"sanctuary_enter",
|
||||
"zelda_sanctuary_story",
|
||||
"priest_sanctuary_before_pendants",
|
||||
"priest_sanctuary_after_pendants_before_master_sword",
|
||||
"priest_sanctuary_dying",
|
||||
"zelda_save_sewers",
|
||||
"priest_info",
|
||||
"zelda_sanctuary_before_leave",
|
||||
"telepathic_intro",
|
||||
"telepathic_reminder",
|
||||
"zelda_go_to_throne",
|
||||
"zelda_push_throne",
|
||||
"zelda_switch_room_pull",
|
||||
"zelda_save_lets_go",
|
||||
"zelda_save_repeat",
|
||||
"zelda_before_pendants",
|
||||
"zelda_after_pendants_before_master_sword",
|
||||
"telepathic_zelda_right_after_master_sword",
|
||||
"zelda_sewers",
|
||||
"zelda_switch_room",
|
||||
"kakariko_saharalasa_wife",
|
||||
"kakariko_saharalasa_wife_sword_story",
|
||||
"kakariko_saharalasa_wife_closing",
|
||||
"kakariko_saharalasa_after_master_sword",
|
||||
"kakariko_alert_guards",
|
||||
"sahasrahla_quest_have_pendants",
|
||||
"sahasrahla_quest_have_master_sword",
|
||||
"sahasrahla_quest_information",
|
||||
"sahasrahla_bring_courage",
|
||||
"sahasrahla_have_ice_rod",
|
||||
"telepathic_sahasrahla_beat_agahnim",
|
||||
"telepathic_sahasrahla_beat_agahnim_no_pearl",
|
||||
"sahasrahla_have_boots_no_icerod",
|
||||
"sahasrahla_have_courage",
|
||||
"sahasrahla_found",
|
||||
"sign_rain_north_of_links_house",
|
||||
"sign_north_of_links_house",
|
||||
"sign_path_to_death_mountain",
|
||||
"sign_lost_woods",
|
||||
"sign_zoras",
|
||||
"sign_outside_magic_shop",
|
||||
"sign_death_mountain_cave_back",
|
||||
"sign_east_of_links_house",
|
||||
"sign_south_of_lumberjacks",
|
||||
"sign_east_of_desert",
|
||||
"sign_east_of_sanctuary",
|
||||
"sign_east_of_castle",
|
||||
"sign_north_of_lake",
|
||||
"sign_desert_thief",
|
||||
"sign_lumberjacks_house",
|
||||
"sign_north_kakariko",
|
||||
"witch_bring_mushroom",
|
||||
"witch_brewing_the_item",
|
||||
"witch_assistant_no_bottle",
|
||||
"witch_assistant_no_empty_bottle",
|
||||
"witch_assistant_informational",
|
||||
"witch_assistant_no_bottle_buying",
|
||||
"potion_shop_no_empty_bottles",
|
||||
"item_get_lamp",
|
||||
"item_get_boomerang",
|
||||
"item_get_bow",
|
||||
"item_get_shovel",
|
||||
"item_get_magic_cape",
|
||||
"item_get_powder",
|
||||
"item_get_flippers",
|
||||
"item_get_power_gloves",
|
||||
"item_get_pendant_courage",
|
||||
"item_get_pendant_power",
|
||||
"item_get_pendant_wisdom",
|
||||
"item_get_mushroom",
|
||||
"item_get_book",
|
||||
"item_get_moonpearl",
|
||||
"item_get_compass",
|
||||
"item_get_map",
|
||||
"item_get_ice_rod",
|
||||
"item_get_fire_rod",
|
||||
"item_get_ether",
|
||||
"item_get_bombos",
|
||||
"item_get_quake",
|
||||
"item_get_hammer",
|
||||
"item_get_flute",
|
||||
"item_get_cane_of_somaria",
|
||||
"item_get_hookshot",
|
||||
"item_get_bombs",
|
||||
"item_get_bottle",
|
||||
"item_get_big_key",
|
||||
"item_get_titans_mitts",
|
||||
"item_get_magic_mirror",
|
||||
"item_get_fake_mastersword",
|
||||
"post_item_get_mastersword",
|
||||
"item_get_red_potion",
|
||||
"item_get_green_potion",
|
||||
"item_get_blue_potion",
|
||||
"item_get_bug_net",
|
||||
"item_get_blue_mail",
|
||||
"item_get_red_mail",
|
||||
"item_get_temperedsword",
|
||||
"item_get_mirror_shield",
|
||||
"item_get_cane_of_byrna",
|
||||
"missing_big_key",
|
||||
"missing_magic",
|
||||
"item_get_pegasus_boots",
|
||||
"talking_tree_info_start",
|
||||
"talking_tree_info_1",
|
||||
"talking_tree_info_2",
|
||||
"talking_tree_info_3",
|
||||
"talking_tree_info_4",
|
||||
"talking_tree_other",
|
||||
"item_get_pendant_power_alt",
|
||||
"item_get_pendant_wisdom_alt",
|
||||
"game_shooting_choice",
|
||||
"game_shooting_yes",
|
||||
"game_shooting_no",
|
||||
"game_shooting_continue",
|
||||
"pond_of_wishing",
|
||||
"pond_item_select",
|
||||
"pond_item_test",
|
||||
"pond_will_upgrade",
|
||||
"pond_item_test_no",
|
||||
"pond_item_test_no_no",
|
||||
"pond_item_boomerang",
|
||||
"pond_item_shield",
|
||||
"pond_item_silvers",
|
||||
"pond_item_bottle_filled",
|
||||
"pond_item_sword",
|
||||
"pond_of_wishing_happiness",
|
||||
"pond_of_wishing_choice",
|
||||
"pond_of_wishing_bombs",
|
||||
"pond_of_wishing_arrows",
|
||||
"pond_of_wishing_full_upgrades",
|
||||
"mountain_old_man_first",
|
||||
"mountain_old_man_deadend",
|
||||
"mountain_old_man_turn_right",
|
||||
"mountain_old_man_lost_and_alone",
|
||||
"mountain_old_man_drop_off",
|
||||
"mountain_old_man_in_his_cave_pre_agahnim",
|
||||
"mountain_old_man_in_his_cave",
|
||||
"mountain_old_man_in_his_cave_post_agahnim",
|
||||
"tavern_old_man_awake",
|
||||
"tavern_old_man_unactivated_flute",
|
||||
"tavern_old_man_know_tree_unactivated_flute",
|
||||
"tavern_old_man_have_flute",
|
||||
"chicken_hut_lady",
|
||||
"running_man",
|
||||
"game_race_sign",
|
||||
"sign_bumper_cave",
|
||||
"sign_catfish",
|
||||
"sign_north_village_of_outcasts",
|
||||
"sign_south_of_bumper_cave",
|
||||
"sign_east_of_pyramid",
|
||||
"sign_east_of_bomb_shop",
|
||||
"sign_east_of_mire",
|
||||
"sign_village_of_outcasts",
|
||||
"sign_before_wishing_pond",
|
||||
"sign_before_catfish_area",
|
||||
"castle_wall_guard",
|
||||
"gate_guard",
|
||||
"telepathic_tile_eastern_palace",
|
||||
"telepathic_tile_tower_of_hera_floor_4",
|
||||
"hylian_text_1",
|
||||
"mastersword_pedestal_translated",
|
||||
"telepathic_tile_spectacle_rock",
|
||||
"telepathic_tile_swamp_entrance",
|
||||
"telepathic_tile_thieves_town_upstairs",
|
||||
"telepathic_tile_misery_mire",
|
||||
"hylian_text_2",
|
||||
"desert_entry_translated",
|
||||
"telepathic_tile_under_ganon",
|
||||
"telepathic_tile_palace_of_darkness",
|
||||
"telepathic_tile_desert_bonk_torch_room",
|
||||
"telepathic_tile_castle_tower",
|
||||
"telepathic_tile_ice_large_room",
|
||||
"telepathic_tile_turtle_rock",
|
||||
"telepathic_tile_ice_entrance",
|
||||
"telepathic_tile_ice_stalfos_knights_room",
|
||||
"telepathic_tile_tower_of_hera_entrance",
|
||||
"houlihan_room",
|
||||
"caught_a_bee",
|
||||
"caught_a_fairy",
|
||||
"no_empty_bottles",
|
||||
"game_race_boy_time",
|
||||
"game_race_girl",
|
||||
"game_race_boy_success",
|
||||
"game_race_boy_failure",
|
||||
"game_race_boy_already_won",
|
||||
"game_race_boy_sneaky",
|
||||
"bottle_vendor_choice",
|
||||
"bottle_vendor_get",
|
||||
"bottle_vendor_no",
|
||||
"bottle_vendor_already_collected",
|
||||
"bottle_vendor_bee",
|
||||
"bottle_vendor_fish",
|
||||
"hobo_item_get_bottle",
|
||||
"blacksmiths_what_you_want",
|
||||
"blacksmiths_paywall",
|
||||
"blacksmiths_extra_okay",
|
||||
"blacksmiths_tempered_already",
|
||||
"blacksmiths_temper_no",
|
||||
"blacksmiths_bogart_sword",
|
||||
"blacksmiths_get_sword",
|
||||
"blacksmiths_shop_before_saving",
|
||||
"blacksmiths_shop_saving",
|
||||
"blacksmiths_collect_frog",
|
||||
"blacksmiths_still_working",
|
||||
"blacksmiths_saving_bows",
|
||||
"blacksmiths_hammer_anvil",
|
||||
"dark_flute_boy_storytime",
|
||||
"dark_flute_boy_get_shovel",
|
||||
"dark_flute_boy_no_get_shovel",
|
||||
"dark_flute_boy_flute_not_found",
|
||||
"dark_flute_boy_after_shovel_get",
|
||||
"shop_fortune_teller_lw_hint_0",
|
||||
"shop_fortune_teller_lw_hint_1",
|
||||
"shop_fortune_teller_lw_hint_2",
|
||||
"shop_fortune_teller_lw_hint_3",
|
||||
"shop_fortune_teller_lw_hint_4",
|
||||
"shop_fortune_teller_lw_hint_5",
|
||||
"shop_fortune_teller_lw_hint_6",
|
||||
"shop_fortune_teller_lw_hint_7",
|
||||
"shop_fortune_teller_lw_no_rupees",
|
||||
"shop_fortune_teller_lw",
|
||||
"shop_fortune_teller_lw_post_hint",
|
||||
"shop_fortune_teller_lw_no",
|
||||
"shop_fortune_teller_lw_hint_8",
|
||||
"shop_fortune_teller_lw_hint_9",
|
||||
"shop_fortune_teller_lw_hint_10",
|
||||
"shop_fortune_teller_lw_hint_11",
|
||||
"shop_fortune_teller_lw_hint_12",
|
||||
"shop_fortune_teller_lw_hint_13",
|
||||
"shop_fortune_teller_lw_hint_14",
|
||||
"shop_fortune_teller_lw_hint_15",
|
||||
"dark_sanctuary",
|
||||
"dark_sanctuary_hint_0",
|
||||
"dark_sanctuary_no",
|
||||
"dark_sanctuary_hint_1",
|
||||
"dark_sanctuary_yes",
|
||||
"dark_sanctuary_hint_2",
|
||||
"sick_kid_no_bottle",
|
||||
"sick_kid_trade",
|
||||
"sick_kid_post_trade",
|
||||
"desert_thief_sitting",
|
||||
"desert_thief_following",
|
||||
"desert_thief_question",
|
||||
"desert_thief_question_yes",
|
||||
"desert_thief_after_item_get",
|
||||
"desert_thief_reassure",
|
||||
"hylian_text_3",
|
||||
"tablet_ether_book",
|
||||
"tablet_bombos_book",
|
||||
"magic_bat_wake",
|
||||
"magic_bat_give_half_magic",
|
||||
"intro_main",
|
||||
"intro_throne_room",
|
||||
"intro_zelda_cell",
|
||||
"intro_agahnim",
|
||||
"pickup_purple_chest",
|
||||
"bomb_shop",
|
||||
"bomb_shop_big_bomb",
|
||||
"bomb_shop_big_bomb_buy",
|
||||
"item_get_big_bomb",
|
||||
"kiki_second_extortion",
|
||||
"kiki_second_extortion_no",
|
||||
"kiki_second_extortion_yes",
|
||||
"kiki_first_extortion",
|
||||
"kiki_first_extortion_yes",
|
||||
"kiki_first_extortion_no",
|
||||
"kiki_leaving_screen",
|
||||
"blind_in_the_cell",
|
||||
"blind_by_the_light",
|
||||
"blind_not_that_way",
|
||||
"aginah_l1sword_no_book",
|
||||
"aginah_l1sword_with_pendants",
|
||||
"aginah",
|
||||
"aginah_need_better_sword",
|
||||
"aginah_have_better_sword",
|
||||
"catfish",
|
||||
"catfish_after_item",
|
||||
"lumberjack_right",
|
||||
"lumberjack_left",
|
||||
"lumberjack_left_post_agahnim",
|
||||
"fighting_brothers_right",
|
||||
"fighting_brothers_right_opened",
|
||||
"fighting_brothers_left",
|
||||
"maiden_crystal_1",
|
||||
"maiden_crystal_2",
|
||||
"maiden_crystal_3",
|
||||
"maiden_crystal_4",
|
||||
"maiden_crystal_5",
|
||||
"maiden_crystal_6",
|
||||
"maiden_crystal_7",
|
||||
"maiden_ending",
|
||||
"maiden_confirm_understood",
|
||||
"barrier_breaking",
|
||||
"maiden_crystal_7_again",
|
||||
"agahnim_zelda_teleport",
|
||||
"agahnim_magic_running_away",
|
||||
"agahnim_hide_and_seek_found",
|
||||
"agahnim_defeated",
|
||||
"agahnim_final_meeting",
|
||||
"zora_meeting",
|
||||
"zora_tells_cost",
|
||||
"zora_get_flippers",
|
||||
"zora_no_cash",
|
||||
"zora_no_buy_item",
|
||||
"kakariko_saharalasa_grandson",
|
||||
"kakariko_saharalasa_grandson_next",
|
||||
"dark_palace_tree_dude",
|
||||
"fairy_wishing_ponds",
|
||||
"fairy_wishing_ponds_no",
|
||||
"pond_of_wishing_no",
|
||||
"pond_of_wishing_return_item",
|
||||
"pond_of_wishing_throw",
|
||||
"pond_pre_item_silvers",
|
||||
"pond_of_wishing_great_luck",
|
||||
"pond_of_wishing_good_luck",
|
||||
"pond_of_wishing_meh_luck",
|
||||
"pond_of_wishing_bad_luck",
|
||||
"pond_of_wishing_fortune",
|
||||
"item_get_14_heart",
|
||||
"item_get_24_heart",
|
||||
"item_get_34_heart",
|
||||
"item_get_whole_heart",
|
||||
"item_get_sanc_heart",
|
||||
"fairy_fountain_refill",
|
||||
"death_mountain_bullied_no_pearl",
|
||||
"death_mountain_bullied_with_pearl",
|
||||
"death_mountain_bully_no_pearl",
|
||||
"death_mountain_bully_with_pearl",
|
||||
"shop_darkworld_enter",
|
||||
"game_chest_village_of_outcasts",
|
||||
"game_chest_no_cash",
|
||||
"game_chest_not_played",
|
||||
"game_chest_played",
|
||||
"game_chest_village_of_outcasts_play",
|
||||
"shop_first_time",
|
||||
"shop_already_have",
|
||||
"shop_buy_shield",
|
||||
"shop_buy_red_potion",
|
||||
"shop_buy_arrows",
|
||||
"shop_buy_bombs",
|
||||
"shop_buy_bee",
|
||||
"shop_buy_heart",
|
||||
"shop_first_no_bottle_buy",
|
||||
"shop_buy_no_space",
|
||||
"ganon_fall_in",
|
||||
"ganon_phase_3",
|
||||
"lost_woods_thief",
|
||||
"blinds_hut_dude",
|
||||
"end_triforce",
|
||||
"toppi_fallen",
|
||||
"kakariko_tavern_fisherman",
|
||||
"thief_money",
|
||||
"thief_desert_rupee_cave",
|
||||
"thief_ice_rupee_cave",
|
||||
"telepathic_tile_south_east_darkworld_cave",
|
||||
"cukeman",
|
||||
"cukeman_2",
|
||||
"potion_shop_no_cash",
|
||||
"kakariko_powdered_chicken",
|
||||
"game_chest_south_of_kakariko",
|
||||
"game_chest_play_yes",
|
||||
"game_chest_play_no",
|
||||
"game_chest_lost_woods",
|
||||
"kakariko_flophouse_man_no_flippers",
|
||||
"kakariko_flophouse_man",
|
||||
"menu_start_2",
|
||||
"menu_start_3",
|
||||
"menu_pause",
|
||||
"game_digging_choice",
|
||||
"game_digging_start",
|
||||
"game_digging_no_cash",
|
||||
"game_digging_end_time",
|
||||
"game_digging_come_back_later",
|
||||
"game_digging_no_follower",
|
||||
"menu_start_4",
|
||||
"ganon_fall_in_alt",
|
||||
"ganon_phase_3_alt",
|
||||
"sign_east_death_mountain_bridge",
|
||||
"fish_money",
|
||||
"sign_ganons_tower",
|
||||
"sign_ganon",
|
||||
"ganon_phase_3_no_bow",
|
||||
"ganon_phase_3_no_silvers_alt",
|
||||
"ganon_phase_3_no_silvers",
|
||||
"ganon_phase_3_silvers",
|
||||
"murahdahla",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self._text = OrderedDict()
|
||||
self.setDefaultText()
|
||||
|
||||
@@ -213,7 +213,6 @@ class ALTTPWorld(World):
|
||||
item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int}
|
||||
location_name_to_id = lookup_name_to_id
|
||||
|
||||
data_version = 9
|
||||
required_client_version = (0, 4, 1)
|
||||
web = ALTTPWeb()
|
||||
|
||||
|
||||
34
worlds/apsudoku/__init__.py
Normal file
34
worlds/apsudoku/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import Tutorial
|
||||
from ..AutoWorld import WebWorld, World
|
||||
|
||||
class AP_SudokuWebWorld(WebWorld):
|
||||
options_page = "games/Sudoku/info/en"
|
||||
theme = 'partyTime'
|
||||
|
||||
setup_en = Tutorial(
|
||||
tutorial_name='Setup Guide',
|
||||
description='A guide to playing APSudoku',
|
||||
language='English',
|
||||
file_name='setup_en.md',
|
||||
link='setup/en',
|
||||
authors=['EmilyV']
|
||||
)
|
||||
|
||||
tutorials = [setup_en]
|
||||
|
||||
class AP_SudokuWorld(World):
|
||||
"""
|
||||
Play a little Sudoku while you're in BK mode to maybe get some useful hints
|
||||
"""
|
||||
game = "Sudoku"
|
||||
web = AP_SudokuWebWorld()
|
||||
|
||||
item_name_to_id: Dict[str, int] = {}
|
||||
location_name_to_id: Dict[str, int] = {}
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld):
|
||||
raise Exception("APSudoku cannot be used for generating worlds, the client can instead connect to any slot from any world")
|
||||
|
||||
13
worlds/apsudoku/docs/en_Sudoku.md
Normal file
13
worlds/apsudoku/docs/en_Sudoku.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# APSudoku
|
||||
|
||||
## Hint Games
|
||||
|
||||
HintGames do not need to be added at the start of a seed, and do not create a 'slot'- instead, you connect the HintGame client to a different game's slot. By playing a HintGame, you can earn hints for the connected slot.
|
||||
|
||||
## What is this game?
|
||||
|
||||
Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle correctly solved. Harder puzzles are more likely to grant a hint towards a Progression item, though otherwise what hint is granted is random.
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
|
||||
37
worlds/apsudoku/docs/setup_en.md
Normal file
37
worlds/apsudoku/docs/setup_en.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# APSudoku Setup Guide
|
||||
|
||||
## Required Software
|
||||
- [APSudoku](https://github.com/EmilyV99/APSudoku)
|
||||
- Windows (most tested on Win10)
|
||||
- Other platforms might be able to build from source themselves; and may be included in the future.
|
||||
|
||||
## General Concept
|
||||
|
||||
This is a HintGame client, which can connect to any multiworld slot, allowing you to play Sudoku to unlock random hints for that slot's locations.
|
||||
|
||||
Does not need to be added at the start of a seed, as it does not create any slots of its own, nor does it have any YAML files.
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run APSudoku.exe
|
||||
2. Under the 'Archipelago' tab at the top-right:
|
||||
- Enter the server url & port number
|
||||
- Enter the name of the slot you wish to connect to
|
||||
- Enter the room password (optional)
|
||||
- Select DeathLink related settings (optional)
|
||||
- Press connect
|
||||
3. Go back to the 'Sudoku' tab
|
||||
- Click the various '?' buttons for information on how to play / control
|
||||
4. Choose puzzle difficulty
|
||||
5. Try to solve the Sudoku. Click 'Check' when done.
|
||||
|
||||
## DeathLink Support
|
||||
|
||||
If 'DeathLink' is enabled when you click 'Connect':
|
||||
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting).
|
||||
- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
||||
- On receiving a DeathLink from another player, your puzzle resets.
|
||||
@@ -8,14 +8,16 @@ 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
|
||||
Used to indicate to the multi-world if an item is useful or not
|
||||
"""
|
||||
NORMAL = 0
|
||||
PROGRESSION = 1
|
||||
JUNK = 2
|
||||
|
||||
|
||||
class ItemGroup(Enum):
|
||||
"""
|
||||
Used to group items
|
||||
@@ -28,6 +30,7 @@ class ItemGroup(Enum):
|
||||
SONG = 5
|
||||
TURTLE = 6
|
||||
|
||||
|
||||
class AquariaItem(Item):
|
||||
"""
|
||||
A single item in the Aquaria game.
|
||||
@@ -40,22 +43,23 @@ class AquariaItem(Item):
|
||||
"""
|
||||
Initialisation of the Item
|
||||
:param name: The name of the item
|
||||
:param classification: If the item is usefull or not
|
||||
:param classification: If the item is useful 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
|
||||
id: int
|
||||
count: int
|
||||
type: ItemType
|
||||
group: ItemGroup
|
||||
|
||||
def __init__(self, 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
|
||||
@@ -68,45 +72,46 @@ class ItemData:
|
||||
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
|
||||
"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
|
||||
"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
|
||||
"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
|
||||
"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
|
||||
"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
|
||||
@@ -201,10 +206,9 @@ item_table = {
|
||||
"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 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
|
||||
"Transturtle Simon Says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
|
||||
"Transturtle Arnassi Ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
|
||||
}
|
||||
|
||||
|
||||
@@ -29,213 +29,213 @@ class AquariaLocation(Location):
|
||||
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,
|
||||
"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,
|
||||
"Verse Cave, the Naija hint about the 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,
|
||||
"Home Water, bulb below the grouper fish": 698058,
|
||||
"Home Water, bulb in the path below 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 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,
|
||||
"Home Water, Nautilus Egg": 698194,
|
||||
}
|
||||
|
||||
locations_home_water_transturtle = {
|
||||
"Home water, Transturtle": 698213,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"Energy Temple first area, Energy Idol": 698170,
|
||||
}
|
||||
|
||||
locations_energy_temple_2 = {
|
||||
"Energy temple second area, bulb under the rock": 698028,
|
||||
"Energy Temple second area, bulb under the rock": 698028,
|
||||
}
|
||||
|
||||
locations_energy_temple_altar = {
|
||||
"Energy temple bottom entrance, Krotite armor": 698163,
|
||||
"Energy Temple bottom entrance, Krotite Armor": 698163,
|
||||
}
|
||||
|
||||
locations_energy_temple_3 = {
|
||||
"Energy temple third area, bulb in the bottom path": 698029,
|
||||
"Energy Temple third area, bulb in the bottom path": 698029,
|
||||
}
|
||||
|
||||
locations_energy_temple_boss = {
|
||||
"Energy temple boss area, Fallen god tooth": 698169,
|
||||
"Energy Temple boss area, Fallen God Tooth": 698169,
|
||||
}
|
||||
|
||||
locations_energy_temple_blaster_room = {
|
||||
"Energy temple blaster room, Blaster egg": 698195,
|
||||
"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,
|
||||
"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 crystal": 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,
|
||||
"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 crystal": 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,
|
||||
"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,
|
||||
"Open Water bottom left area, bulb behind the chomper fish": 698011,
|
||||
"Open Water bottom left area, bulb inside the lowest 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,
|
||||
"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,
|
||||
"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,
|
||||
"Arnassi Ruins, Song Plant Spore": 698179,
|
||||
"Arnassi Ruins, Arnassi Armor": 698191,
|
||||
}
|
||||
|
||||
locations_arnassi_path = {
|
||||
"Arnassi Ruins, Arnassi statue": 698164,
|
||||
"Arnassi Ruins, Arnassi Statue": 698164,
|
||||
"Arnassi Ruins, Transturtle": 698217,
|
||||
}
|
||||
|
||||
locations_arnassi_crab_boss = {
|
||||
"Arnassi ruins, Crab armor": 698187,
|
||||
"Arnassi Ruins, Crab Armor": 698187,
|
||||
}
|
||||
|
||||
locations_simon = {
|
||||
"Kelp forest, beating Simon says": 698156,
|
||||
"Simon says area, Transturtle": 698216,
|
||||
"Simon Says area, 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,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"Mithalas City Castle, beating the Priests": 698208,
|
||||
}
|
||||
|
||||
locations_cathedral_l_sc = {
|
||||
"Mithalas city castle, Trident head": 698183,
|
||||
"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,
|
||||
"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, fourth urn in the top right room": 698147,
|
||||
"Mithalas Cathedral, Mithalan Dress": 698189,
|
||||
"Mithalas Cathedral right area, urn below 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,
|
||||
"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 crystal": 698117,
|
||||
"Cathedral Underground, bulb in the bottom right path": 698118,
|
||||
}
|
||||
|
||||
locations_cathedral_boss = {
|
||||
@@ -250,8 +250,8 @@ class AquariaLocations:
|
||||
}
|
||||
|
||||
locations_forest_tl_fp = {
|
||||
"Kelp Forest top left area, bulb close to the Verse egg": 698047,
|
||||
"Kelp forest top left area, Verse egg": 698158,
|
||||
"Kelp Forest top left area, bulb close to the Verse Egg": 698047,
|
||||
"Kelp Forest top left area, Verse Egg": 698158,
|
||||
}
|
||||
|
||||
locations_forest_tr = {
|
||||
@@ -260,7 +260,7 @@ class AquariaLocations:
|
||||
"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,
|
||||
"Kelp Forest top right area, Black Pearl": 698167,
|
||||
}
|
||||
|
||||
locations_forest_tr_fp = {
|
||||
@@ -269,16 +269,16 @@ class AquariaLocations:
|
||||
|
||||
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, Walker baby": 698186,
|
||||
"Kelp Forest bottom left area, Transturtle": 698212,
|
||||
}
|
||||
|
||||
locations_forest_br = {
|
||||
"Kelp forest bottom right area, Odd Container": 698168,
|
||||
"Kelp Forest bottom right area, Odd Container": 698168,
|
||||
}
|
||||
|
||||
locations_forest_boss = {
|
||||
"Kelp forest boss area, beating Drunian God": 698204,
|
||||
"Kelp Forest boss area, beating Drunian God": 698204,
|
||||
}
|
||||
|
||||
locations_forest_boss_entrance = {
|
||||
@@ -286,7 +286,7 @@ class AquariaLocations:
|
||||
}
|
||||
|
||||
locations_forest_fish_cave = {
|
||||
"Kelp Forest bottom left area, Fish cave puzzle": 698207,
|
||||
"Kelp Forest bottom left area, Fish Cave puzzle": 698207,
|
||||
}
|
||||
|
||||
locations_forest_sprite_cave = {
|
||||
@@ -295,7 +295,7 @@ class AquariaLocations:
|
||||
|
||||
locations_forest_sprite_cave_tube = {
|
||||
"Kelp Forest sprite cave, bulb in the second room": 698057,
|
||||
"Kelp Forest Sprite Cave, Seed bag": 698176,
|
||||
"Kelp Forest sprite cave, Seed Bag": 698176,
|
||||
}
|
||||
|
||||
locations_mermog_cave = {
|
||||
@@ -307,14 +307,14 @@ class AquariaLocations:
|
||||
}
|
||||
|
||||
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,
|
||||
"The Veil top left area, In Li's 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,
|
||||
"The Veil top left area, bulb inside the fish pass": 698077,
|
||||
}
|
||||
|
||||
locations_turtle_cave = {
|
||||
@@ -322,56 +322,56 @@ class AquariaLocations:
|
||||
}
|
||||
|
||||
locations_turtle_cave_bubble = {
|
||||
"Turtle cave, bulb in bubble cliff": 698000,
|
||||
"Turtle cave, Urchin costume": 698193,
|
||||
"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,
|
||||
"The Veil top right area, bulb in the middle of the wall jump cliff": 698079,
|
||||
"The Veil top right area, Golden Starfish": 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,
|
||||
"The Veil top right area, bulb in the top of the waterfall": 698080,
|
||||
"The Veil top right area, Transturtle": 698210,
|
||||
}
|
||||
|
||||
locations_veil_bl = {
|
||||
"The veil bottom area, bulb in the left path": 698082,
|
||||
"The Veil bottom area, bulb in the left path": 698082,
|
||||
}
|
||||
|
||||
locations_veil_b_sc = {
|
||||
"The veil bottom area, bulb in the spirit path": 698081,
|
||||
"The Veil bottom area, bulb in the spirit path": 698081,
|
||||
}
|
||||
|
||||
locations_veil_bl_fp = {
|
||||
"The veil bottom area, Verse egg": 698157,
|
||||
"The Veil bottom area, Verse Egg": 698157,
|
||||
}
|
||||
|
||||
locations_veil_br = {
|
||||
"The veil bottom area, Stone Head": 698181,
|
||||
"The Veil bottom area, Stone Head": 698181,
|
||||
}
|
||||
|
||||
locations_octo_cave_t = {
|
||||
"Octopus cave, Dumbo Egg": 698196,
|
||||
"Octopus Cave, Dumbo Egg": 698196,
|
||||
}
|
||||
|
||||
locations_octo_cave_b = {
|
||||
"Octopus cave, bulb in the path below the octopus cave path": 698122,
|
||||
"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,
|
||||
"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,
|
||||
"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 = {
|
||||
@@ -382,13 +382,13 @@ class AquariaLocations:
|
||||
}
|
||||
|
||||
locations_sun_temple_boss = {
|
||||
"Sun temple boss area, beating Sun God": 698203,
|
||||
"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 Seed": 698166,
|
||||
"Abyss left area, Glowing Plant": 698172,
|
||||
}
|
||||
|
||||
@@ -405,87 +405,87 @@ class AquariaLocations:
|
||||
}
|
||||
|
||||
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,
|
||||
"Ice Cave, bulb in the room to the right": 698083,
|
||||
"Ice Cave, first bulb in the top exit room": 698084,
|
||||
"Ice Cave, second bulb in the top exit room": 698085,
|
||||
"Ice Cave, third bulb 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,
|
||||
"Bubble Cave, bulb in the left cave wall": 698089,
|
||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)": 698090,
|
||||
}
|
||||
|
||||
locations_bubble_cave_boss = {
|
||||
"Bubble cave, Verse egg": 698161,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"Sunken City right area, crate close to the save crystal": 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,
|
||||
"Sunken City left area, crate in the little pipe room": 698151,
|
||||
"Sunken City left area, crate close to the save crystal": 698152,
|
||||
"Sunken City left area, crate before the bedroom": 698153,
|
||||
}
|
||||
|
||||
locations_sunken_city_l_bedroom = {
|
||||
"Sunken city left area, Girl Costume": 698192,
|
||||
"Sunken City left area, Girl Costume": 698192,
|
||||
}
|
||||
|
||||
locations_sunken_city_boss = {
|
||||
"Sunken city, bulb on the top of the boss area (boiler room)": 698043,
|
||||
"Sunken City, bulb on top of the boss area": 698043,
|
||||
}
|
||||
|
||||
locations_body_c = {
|
||||
"The body center area, breaking li cage": 698201,
|
||||
"The body main area, bulb on the main path blocking tube": 698097,
|
||||
"The Body center area, breaking Li's 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,
|
||||
"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 below 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,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"Final Boss area, first bulb in the turtle room": 698103,
|
||||
"Final Boss area, second bulb in the turtle room": 698104,
|
||||
"Final Boss area, third bulb in the turtle room": 698105,
|
||||
"Final Boss area, Transturtle": 698215,
|
||||
}
|
||||
|
||||
locations_final_boss = {
|
||||
"Final boss area, bulb in the boss third form room": 698106,
|
||||
"Final Boss area, bulb in the boss third form room": 698106,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,9 +10,8 @@ from Options import Toggle, Choice, Range, DeathLink, PerGameCommonOptions, Defa
|
||||
|
||||
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".
|
||||
Select if the simple ingredients (that do 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
|
||||
@@ -29,27 +28,25 @@ class DishRandomizer(Toggle):
|
||||
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
|
||||
option_none = 0
|
||||
option_all = 1
|
||||
option_all_except_final = 2
|
||||
default = 2
|
||||
|
||||
|
||||
class EarlyEnergyForm(DefaultOnToggle):
|
||||
"""
|
||||
Force the Energy Form to be in a location before leaving the areas around the Home Water.
|
||||
"""
|
||||
""" Force the Energy Form to be in a location early in the game """
|
||||
display_name = "Early Energy Form"
|
||||
|
||||
|
||||
class AquarianTranslation(Toggle):
|
||||
"""Translate to English the Aquarian scripture in the game."""
|
||||
"""Translate the Aquarian scripture in the game into English."""
|
||||
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
|
||||
The 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"
|
||||
@@ -60,12 +57,12 @@ class BigBossesToBeat(Range):
|
||||
|
||||
class MiniBossesToBeat(Range):
|
||||
"""
|
||||
A number of Minibosses to beat before having access to the creator (the final boss). Mini bosses are
|
||||
The number of minibosses to beat before having access to the creator (the final boss). The minibosses 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.
|
||||
"Mantis Shrimp Prime" and "King Jellyfish God Prime".
|
||||
Note that the Energy Statue and Simon Says are not minibosses.
|
||||
"""
|
||||
display_name = "Mini bosses to beat"
|
||||
display_name = "Minibosses to beat"
|
||||
range_start = 0
|
||||
range_end = 8
|
||||
default = 0
|
||||
@@ -73,47 +70,50 @@ class MiniBossesToBeat(Range):
|
||||
|
||||
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
|
||||
The game objective can be to kill the creator or to kill the creator after obtaining all three 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
|
||||
The first vision in the game, where Naija transforms into Energy Form and gets flooded by enemies, 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"
|
||||
display_name = "Skip Naija's first 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.
|
||||
Make sure that there are no progression items at hard-to-reach or hard-to-find locations.
|
||||
Those locations are very High locations (that need beast form, soup and skill to get),
|
||||
every location in the bubble cave, locations where need you to cross a false wall without any indication,
|
||||
the Arnassi race, bosses and minibosses. Useful for those that want a more 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.
|
||||
Make sure that the sun form or the dumbo pet can be acquired before getting to dark places.
|
||||
Be aware that navigating in dark places 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.
|
||||
Make sure that the bind song can be acquired before having to obtain sing bulbs 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.
|
||||
Open the way out of the 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
|
||||
|
||||
@@ -5,7 +5,7 @@ Description: Used to manage Regions in the Aquaria game multiworld randomizer
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional
|
||||
from BaseClasses import MultiWorld, Region, Entrance, ItemClassification, LocationProgressType, CollectionState
|
||||
from BaseClasses import MultiWorld, Region, Entrance, ItemClassification, CollectionState
|
||||
from .Items import AquariaItem
|
||||
from .Locations import AquariaLocations, AquariaLocation
|
||||
from .Options import AquariaOptions
|
||||
@@ -36,8 +36,8 @@ def _has_li(state:CollectionState, player: int) -> bool:
|
||||
|
||||
def _has_damaging_item(state:CollectionState, player: int) -> bool:
|
||||
"""`player` in `state` has the shield song item"""
|
||||
return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby nautilus",
|
||||
"Baby piranha", "Baby blaster"}, player)
|
||||
return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus",
|
||||
"Baby Piranha", "Baby Blaster"}, player)
|
||||
|
||||
|
||||
def _has_shield_song(state:CollectionState, player: int) -> bool:
|
||||
@@ -72,7 +72,7 @@ def _has_sun_form(state:CollectionState, player: int) -> bool:
|
||||
|
||||
def _has_light(state:CollectionState, player: int) -> bool:
|
||||
"""`player` in `state` has the light item"""
|
||||
return state.has("Baby dumbo", player) or _has_sun_form(state, player)
|
||||
return state.has("Baby Dumbo", player) or _has_sun_form(state, player)
|
||||
|
||||
|
||||
def _has_dual_form(state:CollectionState, player: int) -> bool:
|
||||
@@ -223,8 +223,6 @@ class AquariaRegions:
|
||||
region.add_locations(locations, AquariaLocation)
|
||||
return region
|
||||
|
||||
|
||||
|
||||
def __create_home_water_area(self) -> None:
|
||||
"""
|
||||
Create the `verse_cave`, `home_water` and `song_cave*` regions
|
||||
@@ -239,26 +237,26 @@ class AquariaRegions:
|
||||
AquariaLocations.locations_home_water_nautilus)
|
||||
self.home_water_transturtle = self.__add_region("Home Water, turtle room",
|
||||
AquariaLocations.locations_home_water_transturtle)
|
||||
self.naija_home = self.__add_region("Naija's home", AquariaLocations.locations_naija_home)
|
||||
self.song_cave = self.__add_region("Song cave", AquariaLocations.locations_song_cave)
|
||||
self.naija_home = self.__add_region("Naija's Home", AquariaLocations.locations_naija_home)
|
||||
self.song_cave = self.__add_region("Song Cave", AquariaLocations.locations_song_cave)
|
||||
|
||||
def __create_energy_temple(self) -> None:
|
||||
"""
|
||||
Create the `energy_temple_*` regions
|
||||
"""
|
||||
self.energy_temple_1 = self.__add_region("Energy temple first area",
|
||||
self.energy_temple_1 = self.__add_region("Energy Temple first area",
|
||||
AquariaLocations.locations_energy_temple_1)
|
||||
self.energy_temple_2 = self.__add_region("Energy temple second area",
|
||||
self.energy_temple_2 = self.__add_region("Energy Temple second area",
|
||||
AquariaLocations.locations_energy_temple_2)
|
||||
self.energy_temple_3 = self.__add_region("Energy temple third area",
|
||||
self.energy_temple_3 = self.__add_region("Energy Temple third area",
|
||||
AquariaLocations.locations_energy_temple_3)
|
||||
self.energy_temple_altar = self.__add_region("Energy temple bottom entrance",
|
||||
self.energy_temple_altar = self.__add_region("Energy Temple bottom entrance",
|
||||
AquariaLocations.locations_energy_temple_altar)
|
||||
self.energy_temple_boss = self.__add_region("Energy temple fallen God room",
|
||||
self.energy_temple_boss = self.__add_region("Energy Temple fallen God room",
|
||||
AquariaLocations.locations_energy_temple_boss)
|
||||
self.energy_temple_idol = self.__add_region("Energy temple Idol room",
|
||||
self.energy_temple_idol = self.__add_region("Energy Temple Idol room",
|
||||
AquariaLocations.locations_energy_temple_idol)
|
||||
self.energy_temple_blaster_room = self.__add_region("Energy temple blaster room",
|
||||
self.energy_temple_blaster_room = self.__add_region("Energy Temple blaster room",
|
||||
AquariaLocations.locations_energy_temple_blaster_room)
|
||||
|
||||
def __create_openwater(self) -> None:
|
||||
@@ -266,18 +264,18 @@ class AquariaRegions:
|
||||
Create the `openwater_*`, `skeleton_path`, `arnassi*` and `simon`
|
||||
regions
|
||||
"""
|
||||
self.openwater_tl = self.__add_region("Open water top left area",
|
||||
self.openwater_tl = self.__add_region("Open Water top left area",
|
||||
AquariaLocations.locations_openwater_tl)
|
||||
self.openwater_tr = self.__add_region("Open water top right area",
|
||||
self.openwater_tr = self.__add_region("Open Water top right area",
|
||||
AquariaLocations.locations_openwater_tr)
|
||||
self.openwater_tr_turtle = self.__add_region("Open water top right area, turtle room",
|
||||
self.openwater_tr_turtle = self.__add_region("Open Water top right area, turtle room",
|
||||
AquariaLocations.locations_openwater_tr_turtle)
|
||||
self.openwater_bl = self.__add_region("Open water bottom left area",
|
||||
self.openwater_bl = self.__add_region("Open Water bottom left area",
|
||||
AquariaLocations.locations_openwater_bl)
|
||||
self.openwater_br = self.__add_region("Open water bottom right area", None)
|
||||
self.skeleton_path = self.__add_region("Open water skeleton path",
|
||||
self.openwater_br = self.__add_region("Open Water bottom right area", None)
|
||||
self.skeleton_path = self.__add_region("Open Water skeleton path",
|
||||
AquariaLocations.locations_skeleton_path)
|
||||
self.skeleton_path_sc = self.__add_region("Open water skeleton path spirit cristal",
|
||||
self.skeleton_path_sc = self.__add_region("Open Water skeleton path spirit crystal",
|
||||
AquariaLocations.locations_skeleton_path_sc)
|
||||
self.arnassi = self.__add_region("Arnassi Ruins", AquariaLocations.locations_arnassi)
|
||||
self.arnassi_path = self.__add_region("Arnassi Ruins, back entrance path",
|
||||
@@ -289,20 +287,20 @@ class AquariaRegions:
|
||||
"""
|
||||
Create the `mithalas_city*` and `cathedral_*` regions
|
||||
"""
|
||||
self.mithalas_city = self.__add_region("Mithalas city",
|
||||
self.mithalas_city = self.__add_region("Mithalas City",
|
||||
AquariaLocations.locations_mithalas_city)
|
||||
self.mithalas_city_fishpass = self.__add_region("Mithalas city fish pass",
|
||||
self.mithalas_city_fishpass = self.__add_region("Mithalas City fish pass",
|
||||
AquariaLocations.locations_mithalas_city_fishpass)
|
||||
self.mithalas_city_top_path = self.__add_region("Mithalas city top path",
|
||||
self.mithalas_city_top_path = self.__add_region("Mithalas City top path",
|
||||
AquariaLocations.locations_mithalas_city_top_path)
|
||||
self.cathedral_l = self.__add_region("Mithalas castle", AquariaLocations.locations_cathedral_l)
|
||||
self.cathedral_l_tube = self.__add_region("Mithalas castle, plant tube entrance",
|
||||
AquariaLocations.locations_cathedral_l_tube)
|
||||
self.cathedral_l_sc = self.__add_region("Mithalas castle spirit cristal",
|
||||
self.cathedral_l_sc = self.__add_region("Mithalas castle spirit crystal",
|
||||
AquariaLocations.locations_cathedral_l_sc)
|
||||
self.cathedral_r = self.__add_region("Mithalas Cathedral",
|
||||
AquariaLocations.locations_cathedral_r)
|
||||
self.cathedral_underground = self.__add_region("Mithalas Cathedral underground area",
|
||||
self.cathedral_underground = self.__add_region("Mithalas Cathedral Underground area",
|
||||
AquariaLocations.locations_cathedral_underground)
|
||||
self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room",
|
||||
AquariaLocations.locations_cathedral_boss)
|
||||
@@ -312,73 +310,73 @@ class AquariaRegions:
|
||||
"""
|
||||
Create the `forest_*` dans `mermog_cave` regions
|
||||
"""
|
||||
self.forest_tl = self.__add_region("Kelp forest top left area",
|
||||
self.forest_tl = self.__add_region("Kelp Forest top left area",
|
||||
AquariaLocations.locations_forest_tl)
|
||||
self.forest_tl_fp = self.__add_region("Kelp forest top left area fish pass",
|
||||
self.forest_tl_fp = self.__add_region("Kelp Forest top left area fish pass",
|
||||
AquariaLocations.locations_forest_tl_fp)
|
||||
self.forest_tr = self.__add_region("Kelp forest top right area",
|
||||
self.forest_tr = self.__add_region("Kelp Forest top right area",
|
||||
AquariaLocations.locations_forest_tr)
|
||||
self.forest_tr_fp = self.__add_region("Kelp forest top right area fish pass",
|
||||
self.forest_tr_fp = self.__add_region("Kelp Forest top right area fish pass",
|
||||
AquariaLocations.locations_forest_tr_fp)
|
||||
self.forest_bl = self.__add_region("Kelp forest bottom left area",
|
||||
self.forest_bl = self.__add_region("Kelp Forest bottom left area",
|
||||
AquariaLocations.locations_forest_bl)
|
||||
self.forest_br = self.__add_region("Kelp forest bottom right area",
|
||||
self.forest_br = self.__add_region("Kelp Forest bottom right area",
|
||||
AquariaLocations.locations_forest_br)
|
||||
self.forest_sprite_cave = self.__add_region("Kelp forest spirit cave",
|
||||
self.forest_sprite_cave = self.__add_region("Kelp Forest spirit cave",
|
||||
AquariaLocations.locations_forest_sprite_cave)
|
||||
self.forest_sprite_cave_tube = self.__add_region("Kelp forest spirit cave after the plant tube",
|
||||
self.forest_sprite_cave_tube = self.__add_region("Kelp Forest spirit cave after the plant tube",
|
||||
AquariaLocations.locations_forest_sprite_cave_tube)
|
||||
self.forest_boss = self.__add_region("Kelp forest Drunian God room",
|
||||
self.forest_boss = self.__add_region("Kelp Forest Drunian God room",
|
||||
AquariaLocations.locations_forest_boss)
|
||||
self.forest_boss_entrance = self.__add_region("Kelp forest Drunian God room entrance",
|
||||
self.forest_boss_entrance = self.__add_region("Kelp Forest Drunian God room entrance",
|
||||
AquariaLocations.locations_forest_boss_entrance)
|
||||
self.mermog_cave = self.__add_region("Kelp forest Mermog cave",
|
||||
self.mermog_cave = self.__add_region("Kelp Forest Mermog cave",
|
||||
AquariaLocations.locations_mermog_cave)
|
||||
self.mermog_boss = self.__add_region("Kelp forest Mermog cave boss",
|
||||
self.mermog_boss = self.__add_region("Kelp Forest Mermog cave boss",
|
||||
AquariaLocations.locations_mermog_boss)
|
||||
self.forest_fish_cave = self.__add_region("Kelp forest fish cave",
|
||||
self.forest_fish_cave = self.__add_region("Kelp Forest fish cave",
|
||||
AquariaLocations.locations_forest_fish_cave)
|
||||
self.simon = self.__add_region("Kelp forest, Simon's room", AquariaLocations.locations_simon)
|
||||
self.simon = self.__add_region("Kelp Forest, Simon's room", AquariaLocations.locations_simon)
|
||||
|
||||
def __create_veil(self) -> None:
|
||||
"""
|
||||
Create the `veil_*`, `octo_cave` and `turtle_cave` regions
|
||||
"""
|
||||
self.veil_tl = self.__add_region("The veil top left area", AquariaLocations.locations_veil_tl)
|
||||
self.veil_tl_fp = self.__add_region("The veil top left area fish pass",
|
||||
self.veil_tl = self.__add_region("The Veil top left area", AquariaLocations.locations_veil_tl)
|
||||
self.veil_tl_fp = self.__add_region("The Veil top left area fish pass",
|
||||
AquariaLocations.locations_veil_tl_fp)
|
||||
self.turtle_cave = self.__add_region("The veil top left area, turtle cave",
|
||||
self.turtle_cave = self.__add_region("The Veil top left area, turtle cave",
|
||||
AquariaLocations.locations_turtle_cave)
|
||||
self.turtle_cave_bubble = self.__add_region("The veil top left area, turtle cave bubble cliff",
|
||||
self.turtle_cave_bubble = self.__add_region("The Veil top left area, turtle cave Bubble Cliff",
|
||||
AquariaLocations.locations_turtle_cave_bubble)
|
||||
self.veil_tr_l = self.__add_region("The veil top right area, left of temple",
|
||||
self.veil_tr_l = self.__add_region("The Veil top right area, left of temple",
|
||||
AquariaLocations.locations_veil_tr_l)
|
||||
self.veil_tr_r = self.__add_region("The veil top right area, right of temple",
|
||||
self.veil_tr_r = self.__add_region("The Veil top right area, right of temple",
|
||||
AquariaLocations.locations_veil_tr_r)
|
||||
self.octo_cave_t = self.__add_region("Octopus cave top entrance",
|
||||
self.octo_cave_t = self.__add_region("Octopus Cave top entrance",
|
||||
AquariaLocations.locations_octo_cave_t)
|
||||
self.octo_cave_b = self.__add_region("Octopus cave bottom entrance",
|
||||
self.octo_cave_b = self.__add_region("Octopus Cave bottom entrance",
|
||||
AquariaLocations.locations_octo_cave_b)
|
||||
self.veil_bl = self.__add_region("The veil bottom left area",
|
||||
self.veil_bl = self.__add_region("The Veil bottom left area",
|
||||
AquariaLocations.locations_veil_bl)
|
||||
self.veil_b_sc = self.__add_region("The veil bottom spirit cristal area",
|
||||
self.veil_b_sc = self.__add_region("The Veil bottom spirit crystal area",
|
||||
AquariaLocations.locations_veil_b_sc)
|
||||
self.veil_bl_fp = self.__add_region("The veil bottom left area, in the sunken ship",
|
||||
self.veil_bl_fp = self.__add_region("The Veil bottom left area, in the sunken ship",
|
||||
AquariaLocations.locations_veil_bl_fp)
|
||||
self.veil_br = self.__add_region("The veil bottom right area",
|
||||
self.veil_br = self.__add_region("The Veil bottom right area",
|
||||
AquariaLocations.locations_veil_br)
|
||||
|
||||
def __create_sun_temple(self) -> None:
|
||||
"""
|
||||
Create the `sun_temple*` regions
|
||||
"""
|
||||
self.sun_temple_l = self.__add_region("Sun temple left area",
|
||||
self.sun_temple_l = self.__add_region("Sun Temple left area",
|
||||
AquariaLocations.locations_sun_temple_l)
|
||||
self.sun_temple_r = self.__add_region("Sun temple right area",
|
||||
self.sun_temple_r = self.__add_region("Sun Temple right area",
|
||||
AquariaLocations.locations_sun_temple_r)
|
||||
self.sun_temple_boss_path = self.__add_region("Sun temple before boss area",
|
||||
self.sun_temple_boss_path = self.__add_region("Sun Temple before boss area",
|
||||
AquariaLocations.locations_sun_temple_boss_path)
|
||||
self.sun_temple_boss = self.__add_region("Sun temple boss area",
|
||||
self.sun_temple_boss = self.__add_region("Sun Temple boss area",
|
||||
AquariaLocations.locations_sun_temple_boss)
|
||||
|
||||
def __create_abyss(self) -> None:
|
||||
@@ -390,9 +388,9 @@ class AquariaRegions:
|
||||
AquariaLocations.locations_abyss_l)
|
||||
self.abyss_lb = self.__add_region("Abyss left bottom area", AquariaLocations.locations_abyss_lb)
|
||||
self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r)
|
||||
self.ice_cave = self.__add_region("Ice cave", AquariaLocations.locations_ice_cave)
|
||||
self.bubble_cave = self.__add_region("Bubble cave", AquariaLocations.locations_bubble_cave)
|
||||
self.bubble_cave_boss = self.__add_region("Bubble cave boss area", AquariaLocations.locations_bubble_cave_boss)
|
||||
self.ice_cave = self.__add_region("Ice Cave", AquariaLocations.locations_ice_cave)
|
||||
self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave)
|
||||
self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss)
|
||||
self.king_jellyfish_cave = self.__add_region("Abyss left area, King jellyfish cave",
|
||||
AquariaLocations.locations_king_jellyfish_cave)
|
||||
self.whale = self.__add_region("Inside the whale", AquariaLocations.locations_whale)
|
||||
@@ -402,35 +400,35 @@ class AquariaRegions:
|
||||
"""
|
||||
Create the `sunken_city_*` regions
|
||||
"""
|
||||
self.sunken_city_l = self.__add_region("Sunken city left area",
|
||||
self.sunken_city_l = self.__add_region("Sunken City left area",
|
||||
AquariaLocations.locations_sunken_city_l)
|
||||
self.sunken_city_l_bedroom = self.__add_region("Sunken city left area, bedroom",
|
||||
self.sunken_city_l_bedroom = self.__add_region("Sunken City left area, bedroom",
|
||||
AquariaLocations.locations_sunken_city_l_bedroom)
|
||||
self.sunken_city_r = self.__add_region("Sunken city right area",
|
||||
self.sunken_city_r = self.__add_region("Sunken City right area",
|
||||
AquariaLocations.locations_sunken_city_r)
|
||||
self.sunken_city_boss = self.__add_region("Sunken city boss area",
|
||||
self.sunken_city_boss = self.__add_region("Sunken City boss area",
|
||||
AquariaLocations.locations_sunken_city_boss)
|
||||
|
||||
def __create_body(self) -> None:
|
||||
"""
|
||||
Create the `body_*` and `final_boss* regions
|
||||
"""
|
||||
self.body_c = self.__add_region("The body center area",
|
||||
self.body_c = self.__add_region("The Body center area",
|
||||
AquariaLocations.locations_body_c)
|
||||
self.body_l = self.__add_region("The body left area",
|
||||
self.body_l = self.__add_region("The Body left area",
|
||||
AquariaLocations.locations_body_l)
|
||||
self.body_rt = self.__add_region("The body right area, top path",
|
||||
self.body_rt = self.__add_region("The Body right area, top path",
|
||||
AquariaLocations.locations_body_rt)
|
||||
self.body_rb = self.__add_region("The body right area, bottom path",
|
||||
self.body_rb = self.__add_region("The Body right area, bottom path",
|
||||
AquariaLocations.locations_body_rb)
|
||||
self.body_b = self.__add_region("The body bottom area",
|
||||
self.body_b = self.__add_region("The Body bottom area",
|
||||
AquariaLocations.locations_body_b)
|
||||
self.final_boss_loby = self.__add_region("The body, before final boss", None)
|
||||
self.final_boss_tube = self.__add_region("The body, final boss area turtle room",
|
||||
self.final_boss_loby = self.__add_region("The Body, before final boss", None)
|
||||
self.final_boss_tube = self.__add_region("The Body, final boss area turtle room",
|
||||
AquariaLocations.locations_final_boss_tube)
|
||||
self.final_boss = self.__add_region("The body, final boss",
|
||||
self.final_boss = self.__add_region("The Body, final boss",
|
||||
AquariaLocations.locations_final_boss)
|
||||
self.final_boss_end = self.__add_region("The body, final boss area", None)
|
||||
self.final_boss_end = self.__add_region("The Body, final boss area", None)
|
||||
|
||||
def __connect_one_way_regions(self, source_name: str, destination_name: str,
|
||||
source_region: Region,
|
||||
@@ -457,99 +455,99 @@ class AquariaRegions:
|
||||
"""
|
||||
Connect entrances of the different regions around `home_water`
|
||||
"""
|
||||
self.__connect_regions("Menu", "Verse cave right area",
|
||||
self.__connect_regions("Menu", "Verse Cave right area",
|
||||
self.menu, self.verse_cave_r)
|
||||
self.__connect_regions("Verse cave left area", "Verse cave right area",
|
||||
self.__connect_regions("Verse Cave left area", "Verse Cave right area",
|
||||
self.verse_cave_l, self.verse_cave_r)
|
||||
self.__connect_regions("Verse cave", "Home water", self.verse_cave_l, self.home_water)
|
||||
self.__connect_regions("Verse Cave", "Home Water", self.verse_cave_l, self.home_water)
|
||||
self.__connect_regions("Home Water", "Haija's home", self.home_water, self.naija_home)
|
||||
self.__connect_regions("Home Water", "Song cave", self.home_water, self.song_cave)
|
||||
self.__connect_regions("Home Water", "Home water, nautilus nest",
|
||||
self.__connect_regions("Home Water", "Song Cave", self.home_water, self.song_cave)
|
||||
self.__connect_regions("Home Water", "Home Water, nautilus nest",
|
||||
self.home_water, self.home_water_nautilus,
|
||||
lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player))
|
||||
self.__connect_regions("Home Water", "Home water transturtle room",
|
||||
self.__connect_regions("Home Water", "Home Water transturtle room",
|
||||
self.home_water, self.home_water_transturtle)
|
||||
self.__connect_regions("Home Water", "Energy temple first area",
|
||||
self.__connect_regions("Home Water", "Energy Temple first area",
|
||||
self.home_water, self.energy_temple_1,
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
self.__connect_regions("Home Water", "Energy temple_altar",
|
||||
self.__connect_regions("Home Water", "Energy Temple_altar",
|
||||
self.home_water, self.energy_temple_altar,
|
||||
lambda state: _has_energy_form(state, self.player) and
|
||||
_has_bind_song(state, self.player))
|
||||
self.__connect_regions("Energy temple first area", "Energy temple second area",
|
||||
self.__connect_regions("Energy Temple first area", "Energy Temple second area",
|
||||
self.energy_temple_1, self.energy_temple_2,
|
||||
lambda state: _has_energy_form(state, self.player))
|
||||
self.__connect_regions("Energy temple first area", "Energy temple idol room",
|
||||
self.__connect_regions("Energy Temple first area", "Energy Temple idol room",
|
||||
self.energy_temple_1, self.energy_temple_idol,
|
||||
lambda state: _has_fish_form(state, self.player))
|
||||
self.__connect_regions("Energy temple idol room", "Energy temple boss area",
|
||||
self.__connect_regions("Energy Temple idol room", "Energy Temple boss area",
|
||||
self.energy_temple_idol, self.energy_temple_boss,
|
||||
lambda state: _has_energy_form(state, self.player))
|
||||
self.__connect_one_way_regions("Energy temple first area", "Energy temple boss area",
|
||||
self.__connect_one_way_regions("Energy Temple first area", "Energy Temple boss area",
|
||||
self.energy_temple_1, self.energy_temple_boss,
|
||||
lambda state: _has_beast_form(state, self.player) and
|
||||
_has_energy_form(state, self.player))
|
||||
self.__connect_one_way_regions("Energy temple boss area", "Energy temple first area",
|
||||
self.__connect_one_way_regions("Energy Temple boss area", "Energy Temple first area",
|
||||
self.energy_temple_boss, self.energy_temple_1,
|
||||
lambda state: _has_energy_form(state, self.player))
|
||||
self.__connect_regions("Energy temple second area", "Energy temple third area",
|
||||
self.__connect_regions("Energy Temple second area", "Energy Temple third area",
|
||||
self.energy_temple_2, self.energy_temple_3,
|
||||
lambda state: _has_bind_song(state, self.player) and
|
||||
_has_energy_form(state, self.player))
|
||||
self.__connect_regions("Energy temple boss area", "Energy temple blaster room",
|
||||
self.__connect_regions("Energy Temple boss area", "Energy Temple blaster room",
|
||||
self.energy_temple_boss, self.energy_temple_blaster_room,
|
||||
lambda state: _has_nature_form(state, self.player) and
|
||||
_has_bind_song(state, self.player) and
|
||||
_has_energy_form(state, self.player))
|
||||
self.__connect_regions("Energy temple first area", "Energy temple blaster room",
|
||||
self.__connect_regions("Energy Temple first area", "Energy Temple blaster room",
|
||||
self.energy_temple_1, self.energy_temple_blaster_room,
|
||||
lambda state: _has_nature_form(state, self.player) and
|
||||
_has_bind_song(state, self.player) and
|
||||
_has_energy_form(state, self.player) and
|
||||
_has_beast_form(state, self.player))
|
||||
self.__connect_regions("Home Water", "Open water top left area",
|
||||
self.__connect_regions("Home Water", "Open Water top left area",
|
||||
self.home_water, self.openwater_tl)
|
||||
|
||||
def __connect_open_water_regions(self) -> None:
|
||||
"""
|
||||
Connect entrances of the different regions around open water
|
||||
"""
|
||||
self.__connect_regions("Open water top left area", "Open water top right area",
|
||||
self.__connect_regions("Open Water top left area", "Open Water top right area",
|
||||
self.openwater_tl, self.openwater_tr)
|
||||
self.__connect_regions("Open water top left area", "Open water bottom left area",
|
||||
self.__connect_regions("Open Water top left area", "Open Water bottom left area",
|
||||
self.openwater_tl, self.openwater_bl)
|
||||
self.__connect_regions("Open water top left area", "forest bottom right area",
|
||||
self.__connect_regions("Open Water top left area", "forest bottom right area",
|
||||
self.openwater_tl, self.forest_br)
|
||||
self.__connect_regions("Open water top right area", "Open water top right area, turtle room",
|
||||
self.__connect_regions("Open Water top right area", "Open Water top right area, turtle room",
|
||||
self.openwater_tr, self.openwater_tr_turtle,
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
self.__connect_regions("Open water top right area", "Open water bottom right area",
|
||||
self.__connect_regions("Open Water top right area", "Open Water bottom right area",
|
||||
self.openwater_tr, self.openwater_br)
|
||||
self.__connect_regions("Open water top right area", "Mithalas city",
|
||||
self.__connect_regions("Open Water top right area", "Mithalas City",
|
||||
self.openwater_tr, self.mithalas_city)
|
||||
self.__connect_regions("Open water top right area", "Veil bottom left area",
|
||||
self.__connect_regions("Open Water top right area", "Veil bottom left area",
|
||||
self.openwater_tr, self.veil_bl)
|
||||
self.__connect_one_way_regions("Open water top right area", "Veil bottom right",
|
||||
self.__connect_one_way_regions("Open Water top right area", "Veil bottom right",
|
||||
self.openwater_tr, self.veil_br,
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
self.__connect_one_way_regions("Veil bottom right", "Open water top right area",
|
||||
self.__connect_one_way_regions("Veil bottom right", "Open Water top right area",
|
||||
self.veil_br, self.openwater_tr,
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
self.__connect_regions("Open water bottom left area", "Open water bottom right area",
|
||||
self.__connect_regions("Open Water bottom left area", "Open Water bottom right area",
|
||||
self.openwater_bl, self.openwater_br)
|
||||
self.__connect_regions("Open water bottom left area", "Skeleton path",
|
||||
self.__connect_regions("Open Water bottom left area", "Skeleton path",
|
||||
self.openwater_bl, self.skeleton_path)
|
||||
self.__connect_regions("Abyss left area", "Open water bottom left area",
|
||||
self.__connect_regions("Abyss left area", "Open Water bottom left area",
|
||||
self.abyss_l, self.openwater_bl)
|
||||
self.__connect_regions("Skeleton path", "skeleton_path_sc",
|
||||
self.skeleton_path, self.skeleton_path_sc,
|
||||
lambda state: _has_spirit_form(state, self.player))
|
||||
self.__connect_regions("Abyss right area", "Open water bottom right area",
|
||||
self.__connect_regions("Abyss right area", "Open Water bottom right area",
|
||||
self.abyss_r, self.openwater_br)
|
||||
self.__connect_one_way_regions("Open water bottom right area", "Arnassi",
|
||||
self.__connect_one_way_regions("Open Water bottom right area", "Arnassi",
|
||||
self.openwater_br, self.arnassi,
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
self.__connect_one_way_regions("Arnassi", "Open water bottom right area",
|
||||
self.__connect_one_way_regions("Arnassi", "Open Water bottom right area",
|
||||
self.arnassi, self.openwater_br)
|
||||
self.__connect_regions("Arnassi", "Arnassi path",
|
||||
self.arnassi, self.arnassi_path)
|
||||
@@ -564,23 +562,23 @@ class AquariaRegions:
|
||||
"""
|
||||
Connect entrances of the different regions around Mithalas
|
||||
"""
|
||||
self.__connect_one_way_regions("Mithalas city", "Mithalas city top path",
|
||||
self.__connect_one_way_regions("Mithalas City", "Mithalas City top path",
|
||||
self.mithalas_city, self.mithalas_city_top_path,
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
self.__connect_one_way_regions("Mithalas city_top_path", "Mithalas city",
|
||||
self.__connect_one_way_regions("Mithalas City_top_path", "Mithalas City",
|
||||
self.mithalas_city_top_path, self.mithalas_city)
|
||||
self.__connect_regions("Mithalas city", "Mithalas city home with fishpass",
|
||||
self.__connect_regions("Mithalas City", "Mithalas City home with fishpass",
|
||||
self.mithalas_city, self.mithalas_city_fishpass,
|
||||
lambda state: _has_fish_form(state, self.player))
|
||||
self.__connect_regions("Mithalas city", "Mithalas castle",
|
||||
self.__connect_regions("Mithalas City", "Mithalas castle",
|
||||
self.mithalas_city, self.cathedral_l,
|
||||
lambda state: _has_fish_form(state, self.player))
|
||||
self.__connect_one_way_regions("Mithalas city top path", "Mithalas castle, flower tube",
|
||||
self.__connect_one_way_regions("Mithalas City top path", "Mithalas castle, flower tube",
|
||||
self.mithalas_city_top_path,
|
||||
self.cathedral_l_tube,
|
||||
lambda state: _has_nature_form(state, self.player) and
|
||||
_has_energy_form(state, self.player))
|
||||
self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas city top path",
|
||||
self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas City top path",
|
||||
self.cathedral_l_tube,
|
||||
self.mithalas_city_top_path,
|
||||
lambda state: _has_beast_form(state, self.player) and
|
||||
@@ -692,22 +690,22 @@ class AquariaRegions:
|
||||
self.veil_tl, self.veil_tr_r)
|
||||
self.__connect_regions("Veil top left area", "Turtle cave",
|
||||
self.veil_tl, self.turtle_cave)
|
||||
self.__connect_regions("Turtle cave", "Turtle cave bubble cliff",
|
||||
self.__connect_regions("Turtle cave", "Turtle cave Bubble Cliff",
|
||||
self.turtle_cave, self.turtle_cave_bubble,
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
self.__connect_regions("Veil right of sun temple", "Sun temple right area",
|
||||
self.__connect_regions("Veil right of sun temple", "Sun Temple right area",
|
||||
self.veil_tr_r, self.sun_temple_r)
|
||||
self.__connect_regions("Sun temple right area", "Sun temple left area",
|
||||
self.__connect_regions("Sun Temple right area", "Sun Temple left area",
|
||||
self.sun_temple_r, self.sun_temple_l,
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
self.__connect_regions("Sun temple left area", "Veil left of sun temple",
|
||||
self.__connect_regions("Sun Temple left area", "Veil left of sun temple",
|
||||
self.sun_temple_l, self.veil_tr_l)
|
||||
self.__connect_regions("Sun temple left area", "Sun temple before boss area",
|
||||
self.__connect_regions("Sun Temple left area", "Sun Temple before boss area",
|
||||
self.sun_temple_l, self.sun_temple_boss_path)
|
||||
self.__connect_regions("Sun temple before boss area", "Sun temple boss area",
|
||||
self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area",
|
||||
self.sun_temple_boss_path, self.sun_temple_boss,
|
||||
lambda state: _has_energy_form(state, self.player))
|
||||
self.__connect_one_way_regions("Sun temple boss area", "Veil left of sun temple",
|
||||
self.__connect_one_way_regions("Sun Temple boss area", "Veil left of sun temple",
|
||||
self.sun_temple_boss, self.veil_tr_l)
|
||||
self.__connect_regions("Veil left of sun temple", "Octo cave top path",
|
||||
self.veil_tr_l, self.octo_cave_t,
|
||||
@@ -726,7 +724,7 @@ class AquariaRegions:
|
||||
self.__connect_regions("Abyss left area", "Abyss bottom of left area",
|
||||
self.abyss_l, self.abyss_lb,
|
||||
lambda state: _has_nature_form(state, self.player))
|
||||
self.__connect_regions("Abyss left bottom area", "Sunken city right area",
|
||||
self.__connect_regions("Abyss left bottom area", "Sunken City right area",
|
||||
self.abyss_lb, self.sunken_city_r,
|
||||
lambda state: _has_li(state, self.player))
|
||||
self.__connect_one_way_regions("Abyss left bottom area", "Body center area",
|
||||
@@ -750,13 +748,13 @@ class AquariaRegions:
|
||||
_has_sun_form(state, self.player) and
|
||||
_has_bind_song(state, self.player) and
|
||||
_has_energy_form(state, self.player))
|
||||
self.__connect_regions("Abyss right area", "Ice cave",
|
||||
self.__connect_regions("Abyss right area", "Ice Cave",
|
||||
self.abyss_r, self.ice_cave,
|
||||
lambda state: _has_spirit_form(state, self.player))
|
||||
self.__connect_regions("Abyss right area", "Bubble cave",
|
||||
self.__connect_regions("Abyss right area", "Bubble Cave",
|
||||
self.ice_cave, self.bubble_cave,
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
self.__connect_regions("Bubble cave boss area", "Bubble cave",
|
||||
self.__connect_regions("Bubble Cave boss area", "Bubble Cave",
|
||||
self.bubble_cave, self.bubble_cave_boss,
|
||||
lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player)
|
||||
)
|
||||
@@ -765,12 +763,12 @@ class AquariaRegions:
|
||||
"""
|
||||
Connect entrances of the different regions around The Sunken City
|
||||
"""
|
||||
self.__connect_regions("Sunken city right area", "Sunken city left area",
|
||||
self.__connect_regions("Sunken City right area", "Sunken City left area",
|
||||
self.sunken_city_r, self.sunken_city_l)
|
||||
self.__connect_regions("Sunken city left area", "Sunken city bedroom",
|
||||
self.__connect_regions("Sunken City left area", "Sunken City bedroom",
|
||||
self.sunken_city_l, self.sunken_city_l_bedroom,
|
||||
lambda state: _has_spirit_form(state, self.player))
|
||||
self.__connect_regions("Sunken city left area", "Sunken city boss area",
|
||||
self.__connect_regions("Sunken City left area", "Sunken City boss area",
|
||||
self.sunken_city_l, self.sunken_city_boss,
|
||||
lambda state: _has_beast_form(state, self.player) and
|
||||
_has_energy_form(state, self.player) and
|
||||
@@ -778,7 +776,7 @@ class AquariaRegions:
|
||||
|
||||
def __connect_body_regions(self) -> None:
|
||||
"""
|
||||
Connect entrances of the different regions around The body
|
||||
Connect entrances of the different regions around The Body
|
||||
"""
|
||||
self.__connect_regions("Body center area", "Body left area",
|
||||
self.body_c, self.body_l)
|
||||
@@ -789,13 +787,13 @@ class AquariaRegions:
|
||||
self.__connect_regions("Body center area", "Body bottom area",
|
||||
self.body_c, self.body_b,
|
||||
lambda state: _has_dual_form(state, self.player))
|
||||
self.__connect_regions("Body bottom area", "Final boss area",
|
||||
self.__connect_regions("Body bottom area", "Final Boss area",
|
||||
self.body_b, self.final_boss_loby,
|
||||
lambda state: _has_dual_form(state, self.player))
|
||||
self.__connect_regions("Before Final boss", "Final boss tube",
|
||||
self.__connect_regions("Before Final Boss", "Final Boss tube",
|
||||
self.final_boss_loby, self.final_boss_tube,
|
||||
lambda state: _has_nature_form(state, self.player))
|
||||
self.__connect_one_way_regions("Before Final boss", "Final boss",
|
||||
self.__connect_one_way_regions("Before Final Boss", "Final Boss",
|
||||
self.final_boss_loby, self.final_boss,
|
||||
lambda state: _has_energy_form(state, self.player) and
|
||||
_has_dual_form(state, self.player) and
|
||||
@@ -816,7 +814,7 @@ class AquariaRegions:
|
||||
|
||||
def __connect_arnassi_path_transturtle(self, item_source: str, item_target: str, region_source: Region,
|
||||
region_target: Region) -> None:
|
||||
"""Connect the Arnassi ruins transturtle to another one"""
|
||||
"""Connect the Arnassi Ruins transturtle to another one"""
|
||||
self.__connect_one_way_regions(item_source, item_target, region_source, region_target,
|
||||
lambda state: state.has(item_target, self.player) and
|
||||
_has_fish_form(state, self.player))
|
||||
@@ -827,25 +825,25 @@ class AquariaRegions:
|
||||
self.__connect_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l)
|
||||
self.__connect_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle)
|
||||
self.__connect_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl)
|
||||
self.__connect_transturtle(item, "Transturtle Home water", region, self.home_water_transturtle)
|
||||
self.__connect_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle)
|
||||
self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r)
|
||||
self.__connect_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube)
|
||||
self.__connect_transturtle(item, "Transturtle Simon says", region, self.simon)
|
||||
self.__connect_transturtle(item, "Transturtle Arnassi ruins", region, self.arnassi_path,
|
||||
lambda state: state.has("Transturtle Arnassi ruins", self.player) and
|
||||
self.__connect_transturtle(item, "Transturtle Simon Says", region, self.simon)
|
||||
self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_path,
|
||||
lambda state: state.has("Transturtle Arnassi Ruins", self.player) and
|
||||
_has_fish_form(state, self.player))
|
||||
|
||||
def _connect_arnassi_path_transturtle_to_other(self, item: str, region: Region) -> None:
|
||||
"""Connect the Arnassi ruins transturtle to all others"""
|
||||
"""Connect the Arnassi Ruins transturtle to all others"""
|
||||
self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top left", region, self.veil_tl)
|
||||
self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l)
|
||||
self.__connect_arnassi_path_transturtle(item, "Transturtle Open Water top right", region,
|
||||
self.openwater_tr_turtle)
|
||||
self.__connect_arnassi_path_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl)
|
||||
self.__connect_arnassi_path_transturtle(item, "Transturtle Home water", region, self.home_water_transturtle)
|
||||
self.__connect_arnassi_path_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle)
|
||||
self.__connect_arnassi_path_transturtle(item, "Transturtle Abyss right", region, self.abyss_r)
|
||||
self.__connect_arnassi_path_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube)
|
||||
self.__connect_arnassi_path_transturtle(item, "Transturtle Simon says", region, self.simon)
|
||||
self.__connect_arnassi_path_transturtle(item, "Transturtle Simon Says", region, self.simon)
|
||||
|
||||
def __connect_transturtles(self) -> None:
|
||||
"""Connect every transturtle with others"""
|
||||
@@ -853,11 +851,11 @@ class AquariaRegions:
|
||||
self._connect_transturtle_to_other("Transturtle Veil top right", self.veil_tr_l)
|
||||
self._connect_transturtle_to_other("Transturtle Open Water top right", self.openwater_tr_turtle)
|
||||
self._connect_transturtle_to_other("Transturtle Forest bottom left", self.forest_bl)
|
||||
self._connect_transturtle_to_other("Transturtle Home water", self.home_water_transturtle)
|
||||
self._connect_transturtle_to_other("Transturtle Home Water", self.home_water_transturtle)
|
||||
self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r)
|
||||
self._connect_transturtle_to_other("Transturtle Final Boss", self.final_boss_tube)
|
||||
self._connect_transturtle_to_other("Transturtle Simon says", self.simon)
|
||||
self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi ruins", self.arnassi_path)
|
||||
self._connect_transturtle_to_other("Transturtle Simon Says", self.simon)
|
||||
self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_path)
|
||||
|
||||
def connect_regions(self) -> None:
|
||||
"""
|
||||
@@ -909,7 +907,7 @@ class AquariaRegions:
|
||||
|
||||
def __add_event_mini_bosses(self) -> None:
|
||||
"""
|
||||
Add every mini bosses (excluding Energy statue and Simon says)
|
||||
Add every mini bosses (excluding Energy Statue and Simon Says)
|
||||
events to the `world`
|
||||
"""
|
||||
self.__add_event_location(self.home_water_nautilus,
|
||||
@@ -941,7 +939,7 @@ class AquariaRegions:
|
||||
"""
|
||||
Add secrets events to the `world`
|
||||
"""
|
||||
self.__add_event_location(self.first_secret, # Doit ajouter une région pour le "first secret"
|
||||
self.__add_event_location(self.first_secret, # Doit ajouter une région pour le "first secret"
|
||||
"First secret",
|
||||
"First secret obtained")
|
||||
self.__add_event_location(self.mithalas_city,
|
||||
@@ -969,100 +967,100 @@ class AquariaRegions:
|
||||
|
||||
def __adjusting_urns_rules(self) -> None:
|
||||
"""Since Urns need to be broken, add a damaging item to rules"""
|
||||
add_rule(self.multiworld.get_location("Open water top right area, first urn in the Mithalas exit", self.player),
|
||||
add_rule(self.multiworld.get_location("Open Water top right area, first urn in the Mithalas exit", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Open water top right area, second urn in the Mithalas exit", self.player),
|
||||
add_rule(self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Open water top right area, third urn in the Mithalas exit", self.player),
|
||||
add_rule(self.multiworld.get_location("Open Water top right area, third urn in the Mithalas exit", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city, first urn in one of the homes", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City, first urn in one of the homes", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city, second urn in one of the homes", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City, second urn in one of the homes", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city, first urn in the city reserve", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City, first urn in the city reserve", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city, second urn in the city reserve", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City, second urn in the city reserve", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city, third urn in the city reserve", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city, urn in the cathedral flower tube entrance", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City, urn in the Cathedral flower tube entrance", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city castle, urn in the bedroom", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city castle, first urn of the single lamp path", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City Castle, first urn of the single lamp path", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city castle, second urn of the single lamp path", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City Castle, second urn of the single lamp path", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city castle, urn in the bottom room", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bottom room", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city castle, first urn on the entrance path", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City Castle, first urn on the entrance path", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city castle, second urn on the entrance path", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City Castle, second urn on the entrance path", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Mithalas city, urn inside a home fish pass", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas City, urn inside a home fish pass", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
|
||||
def __adjusting_crates_rules(self) -> None:
|
||||
"""Since Crate need to be broken, add a damaging item to rules"""
|
||||
add_rule(self.multiworld.get_location("Sunken city right area, crate close to the save cristal", self.player),
|
||||
add_rule(self.multiworld.get_location("Sunken City right area, crate close to the save crystal", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Sunken city right area, crate in the left bottom room", self.player),
|
||||
add_rule(self.multiworld.get_location("Sunken City right area, crate in the left bottom room", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Sunken city left area, crate in the little pipe room", self.player),
|
||||
add_rule(self.multiworld.get_location("Sunken City left area, crate in the little pipe room", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Sunken city left area, crate close to the save cristal", self.player),
|
||||
add_rule(self.multiworld.get_location("Sunken City left area, crate close to the save crystal", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Sunken city left area, crate before the bedroom", self.player),
|
||||
add_rule(self.multiworld.get_location("Sunken City left area, crate before the bedroom", self.player),
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
|
||||
def __adjusting_soup_rules(self) -> None:
|
||||
"""
|
||||
Modify rules for location that need soup
|
||||
"""
|
||||
add_rule(self.multiworld.get_location("Turtle cave, Urchin costume", self.player),
|
||||
add_rule(self.multiworld.get_location("Turtle cave, Urchin Costume", self.player),
|
||||
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player),
|
||||
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player),
|
||||
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("The veil top right area, bulb in the top of the water fall", self.player),
|
||||
add_rule(self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player),
|
||||
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
|
||||
|
||||
def __adjusting_under_rock_location(self) -> None:
|
||||
"""
|
||||
Modify rules implying bind song needed for bulb under rocks
|
||||
"""
|
||||
add_rule(self.multiworld.get_location("Home water, bulb under the rock in the left path from the verse cave",
|
||||
add_rule(self.multiworld.get_location("Home Water, bulb under the rock in the left path from the Verse Cave",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Verse cave left area, bulb under the rock at the end of the path",
|
||||
add_rule(self.multiworld.get_location("Verse Cave left area, bulb under the rock at the end of the path",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Naija's home, bulb under the rock at the right of the main path",
|
||||
add_rule(self.multiworld.get_location("Naija's Home, bulb under the rock at the right of the main path",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Song cave, bulb under the rock in the path to the singing statues",
|
||||
add_rule(self.multiworld.get_location("Song Cave, bulb under the rock in the path to the singing statues",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Song cave, bulb under the rock close to the song door",
|
||||
add_rule(self.multiworld.get_location("Song Cave, bulb under the rock close to the song door",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Energy temple second area, bulb under the rock",
|
||||
add_rule(self.multiworld.get_location("Energy Temple second area, bulb under the rock",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Open water top left area, bulb under the rock in the right path",
|
||||
add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the right path",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Open water top left area, bulb under the rock in the left path",
|
||||
add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the left path",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Kelp Forest top right area, bulb under the rock in the right path",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("The veil top left area, bulb under the rock in the top right path",
|
||||
add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Abyss right area, bulb in the middle path",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("The veil top left area, bulb under the rock in the top right path",
|
||||
add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path",
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
|
||||
def __adjusting_light_in_dark_place_rules(self) -> None:
|
||||
add_rule(self.multiworld.get_location("Kelp forest top right area, Black pearl", self.player),
|
||||
add_rule(self.multiworld.get_location("Kelp Forest top right area, Black Pearl", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Kelp forest bottom right area, Odd Container", self.player),
|
||||
add_rule(self.multiworld.get_location("Kelp Forest bottom right area, Odd Container", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Transturtle Veil top left to Transturtle Abyss right", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
@@ -1072,108 +1070,103 @@ class AquariaRegions:
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Transturtle Forest bottom left to Transturtle Abyss right", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Transturtle Home water to Transturtle Abyss right", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Transturtle Home Water to Transturtle Abyss right", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Transturtle Final Boss to Transturtle Abyss right", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Transturtle Simon says to Transturtle Abyss right", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Transturtle Simon Says to Transturtle Abyss right", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Transturtle Arnassi ruins to Transturtle Abyss right", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Transturtle Arnassi Ruins to Transturtle Abyss right", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Body center area to Abyss left bottom area", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Veil left of sun temple to Octo cave top path", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Open water bottom right area to Abyss right area", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Open Water bottom right area to Abyss right area", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Open water bottom left area to Abyss left area", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Sun temple left area to Sun temple right area", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Sun Temple left area to Sun Temple right area", self.player),
|
||||
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Sun temple right area to Sun temple left area", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Sun Temple right area to Sun Temple left area", self.player),
|
||||
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun temple left area", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player),
|
||||
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
|
||||
|
||||
|
||||
|
||||
def __adjusting_manual_rules(self) -> None:
|
||||
add_rule(self.multiworld.get_location("Mithalas cathedral, Mithalan Dress", self.player),
|
||||
add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player),
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Open water bottom left area, bulb inside the downest fish pass", self.player),
|
||||
add_rule(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player),
|
||||
lambda state: _has_fish_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Kelp forest bottom left area, Walker baby", self.player),
|
||||
add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", self.player),
|
||||
lambda state: _has_spirit_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("The veil top left area, bulb hidden behind the blocking rock", self.player),
|
||||
add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player),
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Turtle cave, Turtle Egg", self.player),
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Abyss left area, bulb in the bottom fish pass", self.player),
|
||||
lambda state: _has_fish_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Song cave, Anemone seed", self.player),
|
||||
add_rule(self.multiworld.get_location("Song Cave, Anemone Seed", self.player),
|
||||
lambda state: _has_nature_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Song cave, Verse egg", self.player),
|
||||
add_rule(self.multiworld.get_location("Song Cave, Verse Egg", self.player),
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Verse cave right area, Big Seed", self.player),
|
||||
add_rule(self.multiworld.get_location("Verse Cave right area, Big Seed", self.player),
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Arnassi ruins, Song plant spore on the top of the ruins", self.player),
|
||||
add_rule(self.multiworld.get_location("Arnassi Ruins, Song Plant Spore", self.player),
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Energy temple first area, bulb in the bottom room blocked by a rock",
|
||||
add_rule(self.multiworld.get_location("Energy Temple first area, bulb in the bottom room blocked by a rock",
|
||||
self.player), lambda state: _has_energy_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Home water, bulb in the bottom left room", self.player),
|
||||
add_rule(self.multiworld.get_location("Home Water, bulb in the bottom left room", self.player),
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Home water, bulb in the path bellow Nautilus Prime", self.player),
|
||||
add_rule(self.multiworld.get_location("Home Water, bulb in the path below Nautilus Prime", self.player),
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Naija's home, bulb after the energy door", self.player),
|
||||
add_rule(self.multiworld.get_location("Naija's Home, bulb after the energy door", self.player),
|
||||
lambda state: _has_energy_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player),
|
||||
lambda state: _has_spirit_form(state, self.player) and
|
||||
_has_sun_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location("Arnassi ruins, Arnassi Armor", self.player),
|
||||
add_rule(self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player),
|
||||
lambda state: _has_fish_form(state, self.player) and
|
||||
_has_spirit_form(state, self.player))
|
||||
|
||||
|
||||
|
||||
|
||||
def __no_progression_hard_or_hidden_location(self) -> None:
|
||||
self.multiworld.get_location("Energy temple boss area, Fallen god tooth",
|
||||
self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Cathedral boss area, beating Mithalan God",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Kelp forest boss area, beating Drunian God",
|
||||
self.multiworld.get_location("Kelp Forest boss area, beating Drunian God",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Sun temple boss area, beating Sun God",
|
||||
self.multiworld.get_location("Sun Temple boss area, beating Sun God",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Sunken city, bulb on the top of the boss area (boiler room)",
|
||||
self.multiworld.get_location("Sunken City, bulb on top of the boss area",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Home water, Nautilus Egg",
|
||||
self.multiworld.get_location("Home Water, Nautilus Egg",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Energy temple blaster room, Blaster egg",
|
||||
self.multiworld.get_location("Energy Temple blaster room, Blaster Egg",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Mithalas castle, beating the priests",
|
||||
self.multiworld.get_location("Mithalas City Castle, beating the Priests",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Mermog cave, Piranha Egg",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Octopus cave, Dumbo Egg",
|
||||
self.multiworld.get_location("Octopus Cave, Dumbo Egg",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("King Jellyfish cave, bulb in the right path from King Jelly",
|
||||
self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("King Jellyfish cave, Jellyfish Costume",
|
||||
self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Final boss area, bulb in the boss third form room",
|
||||
self.multiworld.get_location("Final Boss area, bulb in the boss third form room",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Sun Worm path, first cliff bulb",
|
||||
@@ -1182,34 +1175,34 @@ class AquariaRegions:
|
||||
self.multiworld.get_location("Sun Worm path, second cliff bulb",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("The veil top right area, bulb in the top of the water fall",
|
||||
self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Bubble cave, bulb in the left cave wall",
|
||||
self.multiworld.get_location("Bubble Cave, bulb in the left cave wall",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Bubble cave, bulb in the right cave wall (behind the ice cristal)",
|
||||
self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Bubble cave, Verse egg",
|
||||
self.multiworld.get_location("Bubble Cave, Verse Egg",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Kelp forest bottom left area, Walker baby",
|
||||
self.multiworld.get_location("Kelp Forest bottom left area, Walker baby",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Sun temple, Sun key",
|
||||
self.multiworld.get_location("Sun Temple, Sun Key",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("The body bottom area, Mutant Costume",
|
||||
self.multiworld.get_location("The Body bottom area, Mutant Costume",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Sun temple, bulb in the hidden room of the right part",
|
||||
self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
self.multiworld.get_location("Arnassi ruins, Arnassi Armor",
|
||||
self.multiworld.get_location("Arnassi Ruins, Arnassi Armor",
|
||||
self.player).item_rule =\
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
|
||||
@@ -1227,26 +1220,22 @@ class AquariaRegions:
|
||||
self.__adjusting_under_rock_location()
|
||||
|
||||
if options.mini_bosses_to_beat.value > 0:
|
||||
add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player),
|
||||
lambda state: _has_mini_bosses(state, self.player))
|
||||
if options.big_bosses_to_beat.value > 0:
|
||||
add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player),
|
||||
lambda state: _has_big_bosses(state, self.player))
|
||||
if options.objective.value == 1:
|
||||
add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player),
|
||||
lambda state: _has_secrets(state, self.player))
|
||||
if options.unconfine_home_water.value in [0, 1]:
|
||||
add_rule(self.multiworld.get_entrance("Home Water to Home water transturtle room", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Home Water to Home Water transturtle room", self.player),
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
if options.unconfine_home_water.value in [0, 2]:
|
||||
add_rule(self.multiworld.get_entrance("Home Water to Open water top left area", self.player),
|
||||
add_rule(self.multiworld.get_entrance("Home Water to Open Water top left area", self.player),
|
||||
lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player))
|
||||
if options.early_energy_form:
|
||||
add_rule(self.multiworld.get_entrance("Home Water to Home water transturtle room", self.player),
|
||||
lambda state: _has_energy_form(state, self.player))
|
||||
if options.early_energy_form:
|
||||
add_rule(self.multiworld.get_entrance("Home Water to Open water top left area", self.player),
|
||||
lambda state: _has_energy_form(state, self.player))
|
||||
self.multiworld.early_items[self.player]["Energy form"] = 1
|
||||
|
||||
if options.no_progression_hard_or_hidden_locations:
|
||||
self.__no_progression_hard_or_hidden_location()
|
||||
|
||||
@@ -5,7 +5,7 @@ Description: Main module for Aquaria game multiworld randomizer
|
||||
"""
|
||||
|
||||
from typing import List, Dict, ClassVar, Any
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from BaseClasses import Tutorial, MultiWorld, ItemClassification
|
||||
from .Items import item_table, AquariaItem, ItemType, ItemGroup
|
||||
from .Locations import location_table
|
||||
@@ -71,9 +71,9 @@ class AquariaWorld(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"}
|
||||
"Li and Li song", "Baby Nautilus", "Baby Piranha",
|
||||
"Baby Blaster"},
|
||||
"Light": {"Sun form", "Baby Dumbo"}
|
||||
}
|
||||
"""Grouping item make it easier to find them"""
|
||||
|
||||
@@ -114,7 +114,7 @@ class AquariaWorld(World):
|
||||
|
||||
def create_item(self, name: str) -> AquariaItem:
|
||||
"""
|
||||
Create an AquariaItem using `name' as item name.
|
||||
Create an AquariaItem using 'name' as item name.
|
||||
"""
|
||||
result: AquariaItem
|
||||
try:
|
||||
@@ -152,20 +152,20 @@ class AquariaWorld(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)
|
||||
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",
|
||||
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 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)
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -11,39 +11,39 @@ 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
|
||||
- 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 crystal
|
||||
* 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.
|
||||
nothing will come out of them. The moment those bulbs, urns and crates are opened, the location is considered checked.
|
||||
|
||||
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.
|
||||
- Dishes (used to learn recipes)<sup>*</sup>
|
||||
- Some ingredients
|
||||
- The Wok (third plate used to cook 3-ingredient recipes everywhere)
|
||||
- All collectible treasure (including pet eggs and costumes)
|
||||
- Li and Li's 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
|
||||
<sup>*</sup> Note that, unlike in the vanilla game, the recipes for dishes (other than the Sea Loaf)
|
||||
cannot be cooked (or learned) 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?
|
||||
@@ -57,8 +57,8 @@ Any items specified above can be in another player's world.
|
||||
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.
|
||||
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.
|
||||
the item from and which one it was.
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- The original Aquaria Game (buyable from a lot of online game seller);
|
||||
- The original Aquaria Game (purchasable from most online game stores)
|
||||
- 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)
|
||||
|
||||
## Optional Software
|
||||
|
||||
- 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
|
||||
|
||||
@@ -13,10 +16,9 @@
|
||||
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.
|
||||
game you play will make sure that every game has its own save game.
|
||||
|
||||
Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files
|
||||
are those:
|
||||
Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files are:
|
||||
- aquaria_randomizer.exe
|
||||
- OpenAL32.dll
|
||||
- override (directory)
|
||||
@@ -25,11 +27,11 @@ are those:
|
||||
- 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.
|
||||
If there is a conflict between files in the original game folder and the unzipped files, you should overwrite
|
||||
the original files with the ones from 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
|
||||
by typing `cmd` in the address bar of the Windows File Explorer). Here is the command line used to start the
|
||||
randomizer:
|
||||
|
||||
```bash
|
||||
@@ -44,8 +46,8 @@ aquaria_randomizer.exe --name YourName --server theServer:thePort --password th
|
||||
|
||||
### 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
|
||||
If you use the AppImage, just copy it into 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
|
||||
@@ -65,7 +67,7 @@ or, if the room has a password:
|
||||
./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,
|
||||
Note that you should not have multiple Aquaria_Randomizer AppImage file in the same folder. If this situation occurs,
|
||||
the preceding commands will launch the game multiple times.
|
||||
|
||||
### Linux when using the tar file
|
||||
@@ -73,24 +75,23 @@ the preceding commands will launch the game multiple times.
|
||||
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:
|
||||
Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted files are:
|
||||
- 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.
|
||||
If there is a conflict between files in the original game folder and the extracted files, you should overwrite
|
||||
the original files with the ones from the extracted randomizer files.
|
||||
|
||||
Then, you should use your system package manager to install liblua5, libogg, libvorbis, libopenal and libsdl2.
|
||||
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`,
|
||||
Also, if there are certain `.so` files in the original Aquaria 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.
|
||||
|
||||
@@ -106,7 +107,7 @@ or, if the room has a password:
|
||||
./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
|
||||
Note: If you get a permission denied error when using the command line, you can use this command to be
|
||||
sure that your executable has executable permission:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -10,148 +10,148 @@ 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",
|
||||
"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 crystal",
|
||||
"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 crystal",
|
||||
"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 lowest 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, Song Plant Spore",
|
||||
"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",
|
||||
"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 City 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, fourth urn in the top right room",
|
||||
"Mithalas Cathedral, Mithalan Dress",
|
||||
"Mithalas Cathedral right area, urn below 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 crystal",
|
||||
"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 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, 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, Walker baby",
|
||||
"Kelp Forest bottom left area, Transturtle",
|
||||
"Kelp forest bottom right area, Odd Container",
|
||||
"Kelp forest boss area, beating Drunian God",
|
||||
"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 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",
|
||||
"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",
|
||||
"The Veil top left area, In Li's 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",
|
||||
"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",
|
||||
"The Veil top right area, bulb in the top of the waterfall",
|
||||
"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 crystal)",
|
||||
"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",
|
||||
"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 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",
|
||||
@@ -159,40 +159,40 @@ after_home_water_locations = [
|
||||
"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",
|
||||
"Ice Cave, bulb in the room to the right",
|
||||
"Ice Cave, first bulb in the top exit room",
|
||||
"Ice Cave, second bulb in the top exit room",
|
||||
"Ice Cave, third bulb 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 crystal",
|
||||
"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 crystal",
|
||||
"Sunken City left area, crate before the bedroom",
|
||||
"Sunken City left area, Girl Costume",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"The Body center area, breaking Li's 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 below 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 bulb in the turtle room",
|
||||
"Final Boss area, third bulb in the turtle room",
|
||||
"Final Boss area, Transturtle",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Simon Says area, beating Simon Says",
|
||||
"Beating Fallen God",
|
||||
"Beating Mithalan God",
|
||||
"Beating Drunian God",
|
||||
|
||||
@@ -13,33 +13,33 @@ class BeastFormAccessTest(AquariaTestBase):
|
||||
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",
|
||||
"Mithalas City Castle, beating the Priests",
|
||||
"Arnassi Ruins, Crab Armor",
|
||||
"Arnassi Ruins, Song Plant Spore",
|
||||
"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",
|
||||
"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",
|
||||
"The Veil top right area, bulb in the top of the waterfall",
|
||||
"Bubble Cave, bulb in the left cave wall",
|
||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||
"Bubble Cave, Verse Egg",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"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",
|
||||
"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"
|
||||
|
||||
@@ -17,19 +17,19 @@ class BindSongAccessTest(AquariaTestBase):
|
||||
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",
|
||||
"Verse Cave right area, Big Seed",
|
||||
"Home Water, bulb in the path below 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"]]
|
||||
|
||||
@@ -18,24 +18,24 @@ class BindSongOptionAccessTest(AquariaTestBase):
|
||||
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",
|
||||
"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 below 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"]]
|
||||
|
||||
@@ -16,5 +16,5 @@ class ConfinedHomeWaterAccessTest(AquariaTestBase):
|
||||
|
||||
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")
|
||||
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")
|
||||
|
||||
@@ -16,10 +16,10 @@ class LiAccessTest(AquariaTestBase):
|
||||
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",
|
||||
"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"]]
|
||||
|
||||
@@ -17,41 +17,41 @@ class EnergyFormAccessTest(AquariaTestBase):
|
||||
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",
|
||||
"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 City 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, fourth urn in the top right room",
|
||||
"Mithalas Cathedral, Mithalan Dress",
|
||||
"Mithalas Cathedral right area, urn below 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",
|
||||
"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",
|
||||
"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 top of the boss area",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Beating Fallen God",
|
||||
"Beating Mithalan God",
|
||||
"Beating Drunian God",
|
||||
@@ -67,7 +67,6 @@ class EnergyFormAccessTest(AquariaTestBase):
|
||||
"First secret",
|
||||
"Sunken City cleared",
|
||||
"Objective complete",
|
||||
|
||||
]
|
||||
items = [["Energy form"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -16,22 +16,22 @@ class FishFormAccessTest(AquariaTestBase):
|
||||
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",
|
||||
"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",
|
||||
"The Veil bottom area, Verse Egg",
|
||||
"Open Water bottom left area, bulb inside the lowest 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",
|
||||
"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"
|
||||
"Arnassi Ruins, Arnassi Armor"
|
||||
]
|
||||
items = [["Fish form"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -16,27 +16,27 @@ class LiAccessTest(AquariaTestBase):
|
||||
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",
|
||||
"Sunken City right area, crate close to the save crystal",
|
||||
"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 crystal",
|
||||
"Sunken City left area, crate before the bedroom",
|
||||
"Sunken City left area, Girl Costume",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"The Body center area, breaking Li's 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 below 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"
|
||||
|
||||
@@ -20,19 +20,19 @@ class LightAccessTest(AquariaTestBase):
|
||||
# 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 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",
|
||||
"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 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",
|
||||
@@ -40,32 +40,32 @@ class LightAccessTest(AquariaTestBase):
|
||||
"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",
|
||||
"Ice Cave, bulb in the room to the right",
|
||||
"Ice Cave, first bulb in the top exit room",
|
||||
"Ice Cave, second bulb in the top exit room",
|
||||
"Ice Cave, third bulb 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 crystal)",
|
||||
"Bubble Cave, Verse Egg",
|
||||
"Beating Mantis Shrimp Prime",
|
||||
"King Jellyfish cave, bulb in the right path from King Jelly",
|
||||
"King Jellyfish cave, Jellyfish Costume",
|
||||
"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",
|
||||
"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 right area, crate close to the save crystal",
|
||||
"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 crystal",
|
||||
"Sunken City left area, crate before the bedroom",
|
||||
"Sunken City left area, Girl Costume",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"Sunken City cleared",
|
||||
"Beating the Golem",
|
||||
"Beating Octopus Prime",
|
||||
"Final boss area, bulb in the boss third form room",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Objective complete",
|
||||
]
|
||||
items = [["Sun form", "Baby dumbo", "Has sun crystal"]]
|
||||
items = [["Sun form", "Baby Dumbo", "Has sun crystal"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -16,41 +16,41 @@ class NatureFormAccessTest(AquariaTestBase):
|
||||
def test_nature_form_location(self) -> None:
|
||||
"""Test locations that require nature form"""
|
||||
locations = [
|
||||
"Song cave, Anemone seed",
|
||||
"Energy temple blaster room, Blaster egg",
|
||||
"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 top left area, Verse Egg",
|
||||
"Kelp Forest top left area, bulb close to the Verse Egg",
|
||||
"Mithalas City Castle, beating the Priests",
|
||||
"Kelp Forest sprite cave, bulb in the second room",
|
||||
"Kelp Forest Sprite Cave, Seed bag",
|
||||
"Kelp Forest sprite cave, Seed Bag",
|
||||
"Beating Mithalan priests",
|
||||
"Abyss left area, bulb in the bottom fish pass",
|
||||
"Bubble cave, Verse egg",
|
||||
"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)",
|
||||
"Sunken City right area, crate close to the save crystal",
|
||||
"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 crystal",
|
||||
"Sunken City left area, crate before the bedroom",
|
||||
"Sunken City left area, Girl Costume",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"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",
|
||||
"The Body center area, breaking Li's 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 below 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"]]
|
||||
|
||||
@@ -15,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
"Energy temple boss area, Fallen god tooth",
|
||||
"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",
|
||||
"Kelp Forest boss area, beating Drunian God",
|
||||
"Sun Temple boss area, beating Sun God",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"Home Water, Nautilus Egg",
|
||||
"Energy Temple blaster room, Blaster Egg",
|
||||
"Mithalas City 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",
|
||||
"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",
|
||||
"The Veil top right area, bulb in the top of the waterfall",
|
||||
"Bubble Cave, bulb in the left cave wall",
|
||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||
"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",
|
||||
"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:
|
||||
|
||||
@@ -15,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
"Energy temple boss area, Fallen god tooth",
|
||||
"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",
|
||||
"Kelp Forest boss area, beating Drunian God",
|
||||
"Sun Temple boss area, beating Sun God",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"Home Water, Nautilus Egg",
|
||||
"Energy Temple blaster room, Blaster Egg",
|
||||
"Mithalas City 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",
|
||||
"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",
|
||||
"The Veil top right area, bulb in the top of the waterfall",
|
||||
"Bubble Cave, bulb in the left cave wall",
|
||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||
"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",
|
||||
"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:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user