mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-01 12:33:25 -07:00
Compare commits
1 Commits
NewSoupVi-
...
api-refere
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f33f19f8b2 |
@@ -1,8 +0,0 @@
|
|||||||
from worlds.ahit.Client import launch
|
|
||||||
import Utils
|
|
||||||
import ModuleUpdate
|
|
||||||
ModuleUpdate.update()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
Utils.init_logging("AHITClient", exception_logger="Client")
|
|
||||||
launch()
|
|
||||||
50
Fill.py
50
Fill.py
@@ -35,8 +35,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
|||||||
"""
|
"""
|
||||||
:param multiworld: Multiworld to be filled.
|
:param multiworld: Multiworld to be filled.
|
||||||
:param base_state: State assumed before fill.
|
:param base_state: State assumed before fill.
|
||||||
:param locations: Locations to be filled with item_pool, gets mutated by removing locations that get filled.
|
:param locations: Locations to be filled with item_pool
|
||||||
:param item_pool: Items to fill into the locations, gets mutated by removing items that get placed.
|
:param item_pool: Items to fill into the locations
|
||||||
:param single_player_placement: if true, can speed up placement if everything belongs to a single player
|
: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 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
|
:param swap: if true, swaps of already place items are done in the event of a dead end
|
||||||
@@ -220,8 +220,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
|||||||
def remaining_fill(multiworld: MultiWorld,
|
def remaining_fill(multiworld: MultiWorld,
|
||||||
locations: typing.List[Location],
|
locations: typing.List[Location],
|
||||||
itempool: typing.List[Item],
|
itempool: typing.List[Item],
|
||||||
name: str = "Remaining",
|
name: str = "Remaining") -> None:
|
||||||
move_unplaceable_to_start_inventory: bool = False) -> None:
|
|
||||||
unplaced_items: typing.List[Item] = []
|
unplaced_items: typing.List[Item] = []
|
||||||
placements: typing.List[Location] = []
|
placements: typing.List[Location] = []
|
||||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||||
@@ -285,14 +284,6 @@ def remaining_fill(multiworld: MultiWorld,
|
|||||||
|
|
||||||
if unplaced_items and locations:
|
if unplaced_items and locations:
|
||||||
# There are leftover unplaceable items and locations that won't accept them
|
# There are leftover unplaceable items and locations that won't accept them
|
||||||
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"
|
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
|
||||||
f"Unplaced items:\n"
|
f"Unplaced items:\n"
|
||||||
f"{', '.join(str(item) for item in unplaced_items)}\n"
|
f"{', '.join(str(item) for item in unplaced_items)}\n"
|
||||||
@@ -429,8 +420,7 @@ def distribute_early_items(multiworld: MultiWorld,
|
|||||||
return fill_locations, itempool
|
return fill_locations, itempool
|
||||||
|
|
||||||
|
|
||||||
def distribute_items_restrictive(multiworld: MultiWorld,
|
def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||||
panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None:
|
|
||||||
fill_locations = sorted(multiworld.get_unfilled_locations())
|
fill_locations = sorted(multiworld.get_unfilled_locations())
|
||||||
multiworld.random.shuffle(fill_locations)
|
multiworld.random.shuffle(fill_locations)
|
||||||
# get items to distribute
|
# get items to distribute
|
||||||
@@ -480,29 +470,8 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
|
|
||||||
if progitempool:
|
if progitempool:
|
||||||
# "advancement/progression fill"
|
# "advancement/progression fill"
|
||||||
if panic_method == "swap":
|
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, single_player_placement=multiworld.players == 1,
|
||||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
|
name="Progression")
|
||||||
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:
|
if progitempool:
|
||||||
raise FillError(
|
raise FillError(
|
||||||
f"Not enough locations for progression items. "
|
f"Not enough locations for progression items. "
|
||||||
@@ -517,9 +486,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
|
|
||||||
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
|
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:
|
if excludedlocations:
|
||||||
raise FillError(
|
raise FillError(
|
||||||
f"Not enough filler items for excluded locations. "
|
f"Not enough filler items for excluded locations. "
|
||||||
@@ -528,8 +495,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
|
|
||||||
restitempool = filleritempool + usefulitempool
|
restitempool = filleritempool + usefulitempool
|
||||||
|
|
||||||
remaining_fill(multiworld, defaultlocations, restitempool,
|
remaining_fill(multiworld, defaultlocations, restitempool)
|
||||||
move_unplaceable_to_start_inventory=panic_method=="start_inventory")
|
|
||||||
|
|
||||||
unplaced = restitempool
|
unplaced = restitempool
|
||||||
unfilled = defaultlocations
|
unfilled = defaultlocations
|
||||||
|
|||||||
29
Generate.py
29
Generate.py
@@ -9,7 +9,6 @@ import urllib.parse
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import Any, Dict, Tuple, Union
|
from typing import Any, Dict, Tuple, Union
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
|
||||||
@@ -320,34 +319,18 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
|||||||
logging.debug(f'Applying {new_weights}')
|
logging.debug(f'Applying {new_weights}')
|
||||||
cleaned_weights = {}
|
cleaned_weights = {}
|
||||||
for option in new_weights:
|
for option in new_weights:
|
||||||
option_name = option.lstrip("+-")
|
option_name = option.lstrip("+")
|
||||||
if option.startswith("+") and option_name in weights:
|
if option.startswith("+") and option_name in weights:
|
||||||
cleaned_value = weights[option_name]
|
cleaned_value = weights[option_name]
|
||||||
new_value = new_weights[option]
|
new_value = new_weights[option]
|
||||||
if isinstance(new_value, set):
|
if isinstance(new_value, (set, dict)):
|
||||||
cleaned_value.update(new_value)
|
cleaned_value.update(new_value)
|
||||||
elif isinstance(new_value, list):
|
elif isinstance(new_value, list):
|
||||||
cleaned_value.extend(new_value)
|
cleaned_value.extend(new_value)
|
||||||
elif isinstance(new_value, dict):
|
|
||||||
cleaned_value = dict(Counter(cleaned_value) + Counter(new_value))
|
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
|
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
|
||||||
f" received {type(new_value).__name__}.")
|
f" received {type(new_value).__name__}.")
|
||||||
cleaned_weights[option_name] = cleaned_value
|
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:
|
else:
|
||||||
cleaned_weights[option_name] = new_weights[option]
|
cleaned_weights[option_name] = new_weights[option]
|
||||||
new_options = set(cleaned_weights) - set(weights)
|
new_options = set(cleaned_weights) - set(weights)
|
||||||
@@ -483,11 +466,9 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
|||||||
world_type = AutoWorldRegister.world_types[ret.game]
|
world_type = AutoWorldRegister.world_types[ret.game]
|
||||||
game_weights = weights[ret.game]
|
game_weights = weights[ret.game]
|
||||||
|
|
||||||
for weight in chain(game_weights, weights):
|
if any(weight.startswith("+") for weight in game_weights) or \
|
||||||
if weight.startswith("+"):
|
any(weight.startswith("+") for weight in weights):
|
||||||
raise Exception(f"Merge tag cannot be used outside of trigger contexts. Found {weight}")
|
raise Exception(f"Merge tag cannot be used outside of trigger contexts.")
|
||||||
if weight.startswith("-"):
|
|
||||||
raise Exception(f"Remove tag cannot be used outside of trigger contexts. Found {weight}")
|
|
||||||
|
|
||||||
if "triggers" in game_weights:
|
if "triggers" in game_weights:
|
||||||
weights = roll_triggers(weights, game_weights["triggers"], valid_trigger_names)
|
weights = roll_triggers(weights, game_weights["triggers"], valid_trigger_names)
|
||||||
|
|||||||
4
Main.py
4
Main.py
@@ -13,7 +13,7 @@ import worlds
|
|||||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
||||||
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
||||||
from Options import StartInventoryPool
|
from Options import StartInventoryPool
|
||||||
from Utils import __version__, output_path, version_tuple, get_settings
|
from Utils import __version__, output_path, version_tuple
|
||||||
from settings import get_settings
|
from settings import get_settings
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
from worlds.generic.Rules import exclusion_rules, locality_rules
|
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':
|
if multiworld.algorithm == 'flood':
|
||||||
flood_items(multiworld) # different algo, biased towards early game progress items
|
flood_items(multiworld) # different algo, biased towards early game progress items
|
||||||
elif multiworld.algorithm == 'balanced':
|
elif multiworld.algorithm == 'balanced':
|
||||||
distribute_items_restrictive(multiworld, get_settings().generator.panic_method)
|
distribute_items_restrictive(multiworld)
|
||||||
|
|
||||||
AutoWorld.call_all(multiworld, 'post_fill')
|
AutoWorld.call_all(multiworld, 'post_fill')
|
||||||
|
|
||||||
|
|||||||
@@ -508,7 +508,7 @@ class Context:
|
|||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
self._start_async_saving()
|
self._start_async_saving()
|
||||||
|
|
||||||
def _start_async_saving(self, atexit_save: bool = True):
|
def _start_async_saving(self):
|
||||||
if not self.auto_saver_thread:
|
if not self.auto_saver_thread:
|
||||||
def save_regularly():
|
def save_regularly():
|
||||||
# time.time() is platform dependent, so using the expensive datetime method instead
|
# time.time() is platform dependent, so using the expensive datetime method instead
|
||||||
@@ -532,7 +532,6 @@ class Context:
|
|||||||
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
||||||
self.auto_saver_thread.start()
|
self.auto_saver_thread.start()
|
||||||
|
|
||||||
if atexit_save:
|
|
||||||
import atexit
|
import atexit
|
||||||
atexit.register(self._save, True) # make sure we save on exit too
|
atexit.register(self._save, True) # make sure we save on exit too
|
||||||
|
|
||||||
|
|||||||
@@ -746,7 +746,6 @@ class NamedRange(Range):
|
|||||||
|
|
||||||
class FreezeValidKeys(AssembleOptions):
|
class FreezeValidKeys(AssembleOptions):
|
||||||
def __new__(mcs, name, bases, attrs):
|
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:
|
if "valid_keys" in attrs:
|
||||||
attrs["_valid_keys"] = frozenset(attrs["valid_keys"])
|
attrs["_valid_keys"] = frozenset(attrs["valid_keys"])
|
||||||
return super(FreezeValidKeys, mcs).__new__(mcs, name, bases, attrs)
|
return super(FreezeValidKeys, mcs).__new__(mcs, name, bases, attrs)
|
||||||
|
|||||||
@@ -67,9 +67,7 @@ Currently, the following games are supported:
|
|||||||
* Yoshi's Island
|
* Yoshi's Island
|
||||||
* Mario & Luigi: Superstar Saga
|
* Mario & Luigi: Superstar Saga
|
||||||
* Bomb Rush Cyberfunk
|
* Bomb Rush Cyberfunk
|
||||||
* Aquaria
|
|
||||||
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
* 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/).
|
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
|
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||||
|
|||||||
56
Utils.py
56
Utils.py
@@ -101,7 +101,8 @@ def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[
|
|||||||
|
|
||||||
@functools.wraps(function)
|
@functools.wraps(function)
|
||||||
def wrap(self: S, arg: T) -> RetType:
|
def wrap(self: S, arg: T) -> RetType:
|
||||||
cache: Optional[Dict[T, RetType]] = getattr(self, cache_name, None)
|
cache: Optional[Dict[T, RetType]] = typing.cast(Optional[Dict[T, RetType]],
|
||||||
|
getattr(self, cache_name, None))
|
||||||
if cache is None:
|
if cache is None:
|
||||||
res = function(self, arg)
|
res = function(self, arg)
|
||||||
setattr(self, cache_name, {arg: res})
|
setattr(self, cache_name, {arg: res})
|
||||||
@@ -208,11 +209,10 @@ def output_path(*path: str) -> str:
|
|||||||
|
|
||||||
def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
|
def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
|
||||||
if is_windows:
|
if is_windows:
|
||||||
os.startfile(filename) # type: ignore
|
os.startfile(filename)
|
||||||
else:
|
else:
|
||||||
from shutil import which
|
from shutil import which
|
||||||
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
|
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])
|
subprocess.call([open_command, filename])
|
||||||
|
|
||||||
|
|
||||||
@@ -300,21 +300,21 @@ def get_options() -> Settings:
|
|||||||
return get_settings()
|
return get_settings()
|
||||||
|
|
||||||
|
|
||||||
def persistent_store(category: str, key: str, value: typing.Any):
|
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
||||||
path = user_path("_persistent_storage.yaml")
|
path = user_path("_persistent_storage.yaml")
|
||||||
storage = persistent_load()
|
storage: dict = persistent_load()
|
||||||
category_dict = storage.setdefault(category, {})
|
category = storage.setdefault(category, {})
|
||||||
category_dict[key] = value
|
category[key] = value
|
||||||
with open(path, "wt") as f:
|
with open(path, "wt") as f:
|
||||||
f.write(dump(storage, Dumper=Dumper))
|
f.write(dump(storage, Dumper=Dumper))
|
||||||
|
|
||||||
|
|
||||||
def persistent_load() -> Dict[str, Dict[str, Any]]:
|
def persistent_load() -> typing.Dict[str, dict]:
|
||||||
storage: Union[Dict[str, Dict[str, Any]], None] = getattr(persistent_load, "storage", None)
|
storage = getattr(persistent_load, "storage", None)
|
||||||
if storage:
|
if storage:
|
||||||
return storage
|
return storage
|
||||||
path = user_path("_persistent_storage.yaml")
|
path = user_path("_persistent_storage.yaml")
|
||||||
storage = {}
|
storage: dict = {}
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
try:
|
try:
|
||||||
with open(path, "r") as f:
|
with open(path, "r") as f:
|
||||||
@@ -323,7 +323,7 @@ def persistent_load() -> Dict[str, Dict[str, Any]]:
|
|||||||
logging.debug(f"Could not read store: {e}")
|
logging.debug(f"Could not read store: {e}")
|
||||||
if storage is None:
|
if storage is None:
|
||||||
storage = {}
|
storage = {}
|
||||||
setattr(persistent_load, "storage", storage)
|
persistent_load.storage = storage
|
||||||
return storage
|
return storage
|
||||||
|
|
||||||
|
|
||||||
@@ -365,7 +365,6 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug(f"Could not store data package: {e}")
|
logging.debug(f"Could not store data package: {e}")
|
||||||
|
|
||||||
|
|
||||||
def get_default_adjuster_settings(game_name: str) -> Namespace:
|
def get_default_adjuster_settings(game_name: str) -> Namespace:
|
||||||
import LttPAdjuster
|
import LttPAdjuster
|
||||||
adjuster_settings = Namespace()
|
adjuster_settings = Namespace()
|
||||||
@@ -384,9 +383,7 @@ def get_adjuster_settings(game_name: str) -> Namespace:
|
|||||||
default_settings = get_default_adjuster_settings(game_name)
|
default_settings = get_default_adjuster_settings(game_name)
|
||||||
|
|
||||||
# Fill in any arguments from the argparser that we haven't seen before
|
# Fill in any arguments from the argparser that we haven't seen before
|
||||||
return Namespace(**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)})
|
||||||
k: v for k, v in vars(default_settings).items() if k not in vars(adjuster_settings)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@cache_argsless
|
@cache_argsless
|
||||||
@@ -410,13 +407,13 @@ safe_builtins = frozenset((
|
|||||||
class RestrictedUnpickler(pickle.Unpickler):
|
class RestrictedUnpickler(pickle.Unpickler):
|
||||||
generic_properties_module: Optional[object]
|
generic_properties_module: Optional[object]
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args, **kwargs):
|
||||||
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
||||||
self.options_module = importlib.import_module("Options")
|
self.options_module = importlib.import_module("Options")
|
||||||
self.net_utils_module = importlib.import_module("NetUtils")
|
self.net_utils_module = importlib.import_module("NetUtils")
|
||||||
self.generic_properties_module = None
|
self.generic_properties_module = None
|
||||||
|
|
||||||
def find_class(self, module: str, name: str) -> type:
|
def find_class(self, module, name):
|
||||||
if module == "builtins" and name in safe_builtins:
|
if module == "builtins" and name in safe_builtins:
|
||||||
return getattr(builtins, name)
|
return getattr(builtins, name)
|
||||||
# used by MultiServer -> savegame/multidata
|
# used by MultiServer -> savegame/multidata
|
||||||
@@ -440,7 +437,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
|
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
|
||||||
|
|
||||||
|
|
||||||
def restricted_loads(s: bytes) -> Any:
|
def restricted_loads(s):
|
||||||
"""Helper function analogous to pickle.loads()."""
|
"""Helper function analogous to pickle.loads()."""
|
||||||
return RestrictedUnpickler(io.BytesIO(s)).load()
|
return RestrictedUnpickler(io.BytesIO(s)).load()
|
||||||
|
|
||||||
@@ -496,7 +493,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
|||||||
file_handler.setFormatter(logging.Formatter(log_format))
|
file_handler.setFormatter(logging.Formatter(log_format))
|
||||||
|
|
||||||
class Filter(logging.Filter):
|
class Filter(logging.Filter):
|
||||||
def __init__(self, filter_name: str, condition: typing.Callable[[logging.LogRecord], bool]) -> None:
|
def __init__(self, filter_name, condition):
|
||||||
super().__init__(filter_name)
|
super().__init__(filter_name)
|
||||||
self.condition = condition
|
self.condition = condition
|
||||||
|
|
||||||
@@ -547,7 +544,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"):
|
def stream_input(stream, queue):
|
||||||
def queuer():
|
def queuer():
|
||||||
while 1:
|
while 1:
|
||||||
try:
|
try:
|
||||||
@@ -575,7 +572,7 @@ class VersionException(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def chaining_prefix(index: int, labels: typing.Sequence[str]) -> str:
|
def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str:
|
||||||
text = ""
|
text = ""
|
||||||
max_label = len(labels) - 1
|
max_label = len(labels) - 1
|
||||||
while index > max_label:
|
while index > max_label:
|
||||||
@@ -598,7 +595,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)}"
|
return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}"
|
||||||
|
|
||||||
|
|
||||||
def get_fuzzy_results(input_word: str, word_list: typing.Collection[str], limit: typing.Optional[int] = None) \
|
def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \
|
||||||
-> typing.List[typing.Tuple[str, int]]:
|
-> typing.List[typing.Tuple[str, int]]:
|
||||||
import jellyfish
|
import jellyfish
|
||||||
|
|
||||||
@@ -606,20 +603,21 @@ def get_fuzzy_results(input_word: str, word_list: typing.Collection[str], limit:
|
|||||||
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
|
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
|
||||||
/ max(len(word1), len(word2)))
|
/ max(len(word1), len(word2)))
|
||||||
|
|
||||||
limit = limit if limit else len(word_list)
|
limit: int = limit if limit else len(wordlist)
|
||||||
return list(
|
return list(
|
||||||
map(
|
map(
|
||||||
lambda container: (container[0], int(container[1]*100)), # convert up to limit to int %
|
lambda container: (container[0], int(container[1]*100)), # convert up to limit to int %
|
||||||
sorted(
|
sorted(
|
||||||
map(lambda candidate: (candidate, get_fuzzy_ratio(input_word, candidate)), word_list),
|
map(lambda candidate:
|
||||||
|
(candidate, get_fuzzy_ratio(input_word, candidate)),
|
||||||
|
wordlist),
|
||||||
key=lambda element: element[1],
|
key=lambda element: element[1],
|
||||||
reverse=True
|
reverse=True)[0:limit]
|
||||||
)[0:limit]
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], suggest: str = "") \
|
||||||
-> typing.Optional[str]:
|
-> typing.Optional[str]:
|
||||||
logging.info(f"Opening file input dialog for {title}.")
|
logging.info(f"Opening file input dialog for {title}.")
|
||||||
|
|
||||||
@@ -736,7 +734,7 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
|||||||
root.update()
|
root.update()
|
||||||
|
|
||||||
|
|
||||||
def title_sorted(data: typing.Iterable, key=None, ignore: typing.AbstractSet[str] = frozenset(("a", "the"))):
|
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
|
||||||
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
||||||
def sorter(element: Union[str, Dict[str, Any]]) -> str:
|
def sorter(element: Union[str, Dict[str, Any]]) -> str:
|
||||||
if (not isinstance(element, str)):
|
if (not isinstance(element, str)):
|
||||||
@@ -790,7 +788,7 @@ class DeprecateDict(dict):
|
|||||||
log_message: str
|
log_message: str
|
||||||
should_error: bool
|
should_error: bool
|
||||||
|
|
||||||
def __init__(self, message: str, error: bool = False) -> None:
|
def __init__(self, message, error: bool = False) -> None:
|
||||||
self.log_message = message
|
self.log_message = message
|
||||||
self.should_error = error
|
self.should_error = error
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|||||||
10
WebHost.py
10
WebHost.py
@@ -117,7 +117,7 @@ if __name__ == "__main__":
|
|||||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
||||||
|
|
||||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||||
from WebHostLib.autolauncher import autohost, autogen, stop
|
from WebHostLib.autolauncher import autohost, autogen
|
||||||
from WebHostLib.options import create as create_options_files
|
from WebHostLib.options import create as create_options_files
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -138,11 +138,3 @@ if __name__ == "__main__":
|
|||||||
else:
|
else:
|
||||||
from waitress import serve
|
from waitress import serve
|
||||||
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
|
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
|
|
||||||
|
|||||||
@@ -3,26 +3,16 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
import time
|
||||||
import typing
|
import typing
|
||||||
from datetime import timedelta, datetime
|
|
||||||
from threading import Event, Thread
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
from pony.orm import db_session, select, commit
|
from pony.orm import db_session, select, commit
|
||||||
|
|
||||||
from Utils import restricted_loads
|
from Utils import restricted_loads
|
||||||
from .locker import Locker, AlreadyRunningException
|
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):
|
def handle_generation_success(seed_id):
|
||||||
logging.info(f"Generation finished for seed {seed_id}")
|
logging.info(f"Generation finished for seed {seed_id}")
|
||||||
@@ -73,7 +63,6 @@ def cleanup():
|
|||||||
|
|
||||||
def autohost(config: dict):
|
def autohost(config: dict):
|
||||||
def keep_running():
|
def keep_running():
|
||||||
stop_event = _stop_event
|
|
||||||
try:
|
try:
|
||||||
with Locker("autohost"):
|
with Locker("autohost"):
|
||||||
cleanup()
|
cleanup()
|
||||||
@@ -83,25 +72,26 @@ def autohost(config: dict):
|
|||||||
hosters.append(hoster)
|
hosters.append(hoster)
|
||||||
hoster.start()
|
hoster.start()
|
||||||
|
|
||||||
while not stop_event.wait(0.1):
|
while 1:
|
||||||
|
time.sleep(0.1)
|
||||||
with db_session:
|
with db_session:
|
||||||
rooms = select(
|
rooms = select(
|
||||||
room for room in Room if
|
room for room in Room if
|
||||||
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
||||||
for room in rooms:
|
for room in rooms:
|
||||||
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
|
# 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 + 5):
|
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout):
|
||||||
hosters[room.id.int % len(hosters)].start_room(room.id)
|
hosters[room.id.int % len(hosters)].start_room(room.id)
|
||||||
|
|
||||||
except AlreadyRunningException:
|
except AlreadyRunningException:
|
||||||
logging.info("Autohost reports as already running, not starting another.")
|
logging.info("Autohost reports as already running, not starting another.")
|
||||||
|
|
||||||
Thread(target=keep_running, name="AP_Autohost").start()
|
import threading
|
||||||
|
threading.Thread(target=keep_running, name="AP_Autohost").start()
|
||||||
|
|
||||||
|
|
||||||
def autogen(config: dict):
|
def autogen(config: dict):
|
||||||
def keep_running():
|
def keep_running():
|
||||||
stop_event = _stop_event
|
|
||||||
try:
|
try:
|
||||||
with Locker("autogen"):
|
with Locker("autogen"):
|
||||||
|
|
||||||
@@ -122,7 +112,8 @@ def autogen(config: dict):
|
|||||||
commit()
|
commit()
|
||||||
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
|
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
|
||||||
|
|
||||||
while not stop_event.wait(0.1):
|
while 1:
|
||||||
|
time.sleep(0.1)
|
||||||
with db_session:
|
with db_session:
|
||||||
# for update locks the database row(s) during transaction, preventing writes from elsewhere
|
# for update locks the database row(s) during transaction, preventing writes from elsewhere
|
||||||
to_start = select(
|
to_start = select(
|
||||||
@@ -133,7 +124,8 @@ def autogen(config: dict):
|
|||||||
except AlreadyRunningException:
|
except AlreadyRunningException:
|
||||||
logging.info("Autogen reports as already running, not starting another.")
|
logging.info("Autogen reports as already running, not starting another.")
|
||||||
|
|
||||||
Thread(target=keep_running, name="AP_Autogen").start()
|
import threading
|
||||||
|
threading.Thread(target=keep_running, name="AP_Autogen").start()
|
||||||
|
|
||||||
|
|
||||||
multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {}
|
multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {}
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ class WebHostContext(Context):
|
|||||||
|
|
||||||
def _load_game_data(self):
|
def _load_game_data(self):
|
||||||
for key, value in self.static_server_data.items():
|
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)
|
setattr(self, key, value)
|
||||||
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
||||||
|
|
||||||
@@ -102,37 +101,18 @@ class WebHostContext(Context):
|
|||||||
|
|
||||||
multidata = self.decompress(room.seed.multidata)
|
multidata = self.decompress(room.seed.multidata)
|
||||||
game_data_packages = {}
|
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["Archipelago"]} # this may be modified by _load
|
|
||||||
self.item_name_groups = {}
|
|
||||||
self.location_name_groups = {}
|
|
||||||
|
|
||||||
for game in list(multidata.get("datapackage", {})):
|
for game in list(multidata.get("datapackage", {})):
|
||||||
game_data = multidata["datapackage"][game]
|
game_data = multidata["datapackage"][game]
|
||||||
if "checksum" in game_data:
|
if "checksum" in game_data:
|
||||||
if static_gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
||||||
# non-custom. remove from multidata and use static data
|
# non-custom. remove from multidata
|
||||||
# games package could be dropped from static data once all rooms embed data package
|
# games package could be dropped from static data once all rooms embed data package
|
||||||
del multidata["datapackage"][game]
|
del multidata["datapackage"][game]
|
||||||
else:
|
else:
|
||||||
row = GameDataPackage.get(checksum=game_data["checksum"])
|
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
|
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)
|
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)
|
return self._load(multidata, game_data_packages, True)
|
||||||
|
|
||||||
@db_session
|
@db_session
|
||||||
@@ -142,7 +122,7 @@ class WebHostContext(Context):
|
|||||||
savegame_data = Room.get(id=self.room_id).multisave
|
savegame_data = Room.get(id=self.room_id).multisave
|
||||||
if savegame_data:
|
if savegame_data:
|
||||||
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
||||||
self._start_async_saving(atexit_save=False)
|
self._start_async_saving()
|
||||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
||||||
|
|
||||||
@db_session
|
@db_session
|
||||||
@@ -232,20 +212,17 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
async def start_room(room_id):
|
async def start_room(room_id):
|
||||||
with Locker(f"RoomLocker {room_id}"):
|
|
||||||
try:
|
try:
|
||||||
logger = set_up_logging(room_id)
|
logger = set_up_logging(room_id)
|
||||||
ctx = WebHostContext(static_server_data, logger)
|
ctx = WebHostContext(static_server_data, logger)
|
||||||
ctx.load(room_id)
|
ctx.load(room_id)
|
||||||
ctx.init_save()
|
ctx.init_save()
|
||||||
try:
|
try:
|
||||||
ctx.server = websockets.serve(
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
except OSError: # likely port in use
|
except OSError: # likely port in use
|
||||||
ctx.server = websockets.serve(
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||||
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
port = 0
|
port = 0
|
||||||
@@ -269,28 +246,24 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|||||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||||
await ctx.shutdown_task
|
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):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
if ctx.saving:
|
with db_session:
|
||||||
ctx._save()
|
room = Room.get(id=room_id)
|
||||||
except Exception as e:
|
# 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:
|
with db_session:
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
room.last_port = -1
|
room.last_port = -1
|
||||||
logger.exception(e)
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
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
|
# 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)
|
||||||
room.last_activity = datetime.datetime.utcnow() - \
|
raise
|
||||||
datetime.timedelta(minutes=1, seconds=room.timeout)
|
|
||||||
logging.info(f"Shutting down room {room_id} on {name}.")
|
|
||||||
finally:
|
finally:
|
||||||
await asyncio.sleep(5)
|
|
||||||
rooms_shutting_down.put(room_id)
|
rooms_shutting_down.put(room_id)
|
||||||
|
|
||||||
class Starter(threading.Thread):
|
class Starter(threading.Thread):
|
||||||
|
|||||||
@@ -70,12 +70,6 @@ def generate(race=False):
|
|||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
meta = get_meta(request.form, race)
|
meta = get_meta(request.form, race)
|
||||||
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"]))
|
results, gen_options = roll_options(options, set(meta["plando_options"]))
|
||||||
|
|
||||||
if any(type(result) == str for result in results.values()):
|
if any(type(result) == str for result in results.values()):
|
||||||
@@ -104,6 +98,8 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any])
|
|||||||
|
|
||||||
return redirect(url_for("view_seed", seed=seed_id))
|
return redirect(url_for("view_seed", seed=seed_id))
|
||||||
|
|
||||||
|
return render_template("generate.html", race=race, version=__version__)
|
||||||
|
|
||||||
|
|
||||||
def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
|
def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
|
||||||
if not meta:
|
if not meta:
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import collections.abc
|
import collections.abc
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from textwrap import dedent
|
|
||||||
from typing import Dict, Union
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from flask import redirect, render_template, request, Response
|
import requests
|
||||||
|
import json
|
||||||
|
import flask
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import Options
|
import Options
|
||||||
from Utils import local_path
|
from Options import Visibility
|
||||||
|
from flask import redirect, render_template, request, Response
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
from Utils import local_path
|
||||||
|
from textwrap import dedent
|
||||||
from . import app, cache
|
from . import app, cache
|
||||||
|
|
||||||
|
|
||||||
def create() -> None:
|
def create():
|
||||||
target_folder = local_path("WebHostLib", "static", "generated")
|
target_folder = local_path("WebHostLib", "static", "generated")
|
||||||
yaml_folder = os.path.join(target_folder, "configs")
|
yaml_folder = os.path.join(target_folder, "configs")
|
||||||
|
|
||||||
Options.generate_yaml_templates(yaml_folder)
|
Options.generate_yaml_templates(yaml_folder)
|
||||||
|
|
||||||
|
|
||||||
def get_world_theme(game_name: str) -> str:
|
def get_world_theme(game_name: str):
|
||||||
if game_name in AutoWorldRegister.world_types:
|
if game_name in AutoWorldRegister.world_types:
|
||||||
return AutoWorldRegister.world_types[game_name].web.theme
|
return AutoWorldRegister.world_types[game_name].web.theme
|
||||||
return 'grass'
|
return 'grass'
|
||||||
|
|
||||||
|
|
||||||
def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]:
|
def render_options_page(template: str, world_name: str, is_complex: bool = False):
|
||||||
visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui
|
|
||||||
world = AutoWorldRegister.world_types[world_name]
|
world = AutoWorldRegister.world_types[world_name]
|
||||||
if world.hidden or world.web.options_page is False:
|
if world.hidden or world.web.options_page is False:
|
||||||
return redirect("games")
|
return redirect("games")
|
||||||
@@ -39,7 +40,12 @@ def render_options_page(template: str, world_name: str, is_complex: bool = False
|
|||||||
grouped_options = {group: {} for group in ordered_groups}
|
grouped_options = {group: {} for group in ordered_groups}
|
||||||
for option_name, option in world.options_dataclass.type_hints.items():
|
for option_name, option in world.options_dataclass.type_hints.items():
|
||||||
# Exclude settings from options pages if their visibility is disabled
|
# Exclude settings from options pages if their visibility is disabled
|
||||||
if visibility_flag in option.visibility:
|
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
|
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
@@ -53,12 +59,29 @@ def render_options_page(template: str, world_name: str, is_complex: bool = False
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_game(options: Dict[str, Union[dict, str]]) -> Union[Response, str]:
|
def generate_game(player_name: str, formatted_options: dict):
|
||||||
from .generate import start_generation
|
payload = {
|
||||||
return start_generation(options, {"plando_options": ["items", "connections", "texts", "bosses"]})
|
"race": 0,
|
||||||
|
"hint_cost": 10,
|
||||||
|
"forfeit_mode": "auto",
|
||||||
|
"remaining_mode": "disabled",
|
||||||
|
"collect_mode": "goal",
|
||||||
|
"weights": {
|
||||||
|
player_name: formatted_options,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
url = urlparse(request.base_url)
|
||||||
|
port_string = f":{url.port}" if url.port else ""
|
||||||
|
r = requests.post(f"{url.scheme}://{url.hostname}{port_string}/api/generate", json=payload)
|
||||||
|
if 200 <= r.status_code <= 299:
|
||||||
|
response_data = r.json()
|
||||||
|
return redirect(response_data["url"])
|
||||||
|
else:
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
|
||||||
def send_yaml(player_name: str, formatted_options: dict) -> Response:
|
def send_yaml(player_name: str, formatted_options: dict):
|
||||||
response = Response(yaml.dump(formatted_options, sort_keys=False))
|
response = Response(yaml.dump(formatted_options, sort_keys=False))
|
||||||
response.headers["Content-Type"] = "text/yaml"
|
response.headers["Content-Type"] = "text/yaml"
|
||||||
response.headers["Content-Disposition"] = f"attachment; filename={player_name}.yaml"
|
response.headers["Content-Disposition"] = f"attachment; filename={player_name}.yaml"
|
||||||
@@ -66,7 +89,7 @@ def send_yaml(player_name: str, formatted_options: dict) -> Response:
|
|||||||
|
|
||||||
|
|
||||||
@app.template_filter("dedent")
|
@app.template_filter("dedent")
|
||||||
def filter_dedent(text: str) -> str:
|
def filter_dedent(text: str):
|
||||||
return dedent(text).strip("\n ")
|
return dedent(text).strip("\n ")
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +102,10 @@ def test_ordered(obj):
|
|||||||
@cache.cached()
|
@cache.cached()
|
||||||
def option_presets(game: str) -> Response:
|
def option_presets(game: str) -> Response:
|
||||||
world = AutoWorldRegister.world_types[game]
|
world = AutoWorldRegister.world_types[game]
|
||||||
|
presets = {}
|
||||||
|
|
||||||
|
if world.web.options_presets:
|
||||||
|
presets = presets | world.web.options_presets
|
||||||
|
|
||||||
class SetEncoder(json.JSONEncoder):
|
class SetEncoder(json.JSONEncoder):
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
@@ -87,8 +114,8 @@ def option_presets(game: str) -> Response:
|
|||||||
return list(obj)
|
return list(obj)
|
||||||
return json.JSONEncoder.default(self, obj)
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
json_data = json.dumps(world.web.options_presets, cls=SetEncoder)
|
json_data = json.dumps(presets, cls=SetEncoder)
|
||||||
response = Response(json_data)
|
response = flask.Response(json_data)
|
||||||
response.headers["Content-Type"] = "application/json"
|
response.headers["Content-Type"] = "application/json"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -146,7 +173,7 @@ def generate_weighted_yaml(game: str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if intent_generate:
|
if intent_generate:
|
||||||
return generate_game({player_name: formatted_options})
|
return generate_game(player_name, formatted_options)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return send_yaml(player_name, formatted_options)
|
return send_yaml(player_name, formatted_options)
|
||||||
@@ -220,7 +247,7 @@ def generate_yaml(game: str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if intent_generate:
|
if intent_generate:
|
||||||
return generate_game({player_name: formatted_options})
|
return generate_game(player_name, formatted_options)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return send_yaml(player_name, formatted_options)
|
return send_yaml(player_name, formatted_options)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@
|
|||||||
{% macro ItemDict(option_name, option, world) %}
|
{% macro ItemDict(option_name, option, world) %}
|
||||||
{{ OptionTitle(option_name, option) }}
|
{{ OptionTitle(option_name, option) }}
|
||||||
<div class="option-container">
|
<div class="option-container">
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
{% for item_name in world.item_names|sort %}
|
||||||
<div class="option-entry">
|
<div class="option-entry">
|
||||||
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
<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 }}" />
|
<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 %}
|
{% for group_name in world.location_name_groups.keys()|sort %}
|
||||||
{% if group_name != "Everywhere" %}
|
{% if group_name != "Everywhere" %}
|
||||||
<div class="option-entry">
|
<div class="option-entry">
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}" value="{{ group_name }}" {{ "checked" if group_name in option.default }} />
|
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}" value="{{ group_name }}" {{ "checked" if grop_name in option.default }} />
|
||||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
{% if world.location_name_groups.keys()|length > 1 %}
|
{% if world.location_name_groups.keys()|length > 1 %}
|
||||||
<div class="option-divider"> </div>
|
<div class="option-divider"> </div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for location_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.location_names|sort) %}
|
{% for location_name in world.location_names|sort %}
|
||||||
<div class="option-entry">
|
<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 }} />
|
<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>
|
<label for="{{ option_name }}-{{ location_name }}">{{ location_name }}</label>
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
{% if world.item_name_groups.keys()|length > 1 %}
|
{% if world.item_name_groups.keys()|length > 1 %}
|
||||||
<div class="option-divider"> </div>
|
<div class="option-divider"> </div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
{% for item_name in world.item_names|sort %}
|
||||||
<div class="option-entry">
|
<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 }} />
|
<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>
|
<label for="{{ option_name }}-{{ item_name }}">{{ item_name }}</label>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
|
|
||||||
{% macro ItemDict(option_name, option, world) %}
|
{% macro ItemDict(option_name, option, world) %}
|
||||||
<div class="dict-container">
|
<div class="dict-container">
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
{% for item_name in world.item_names|sort %}
|
||||||
<div class="dict-entry">
|
<div class="dict-entry">
|
||||||
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
||||||
<input
|
<input
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
{% for group_name in world.location_name_groups.keys()|sort %}
|
{% for group_name in world.location_name_groups.keys()|sort %}
|
||||||
{% if group_name != "Everywhere" %}
|
{% if group_name != "Everywhere" %}
|
||||||
<div class="set-entry">
|
<div class="set-entry">
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}||{{ group_name }}" value="1" {{ "checked" if group_name in option.default }} />
|
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}||{{ group_name }}" value="1" {{ "checked" if grop_name in option.default }} />
|
||||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
{% if world.location_name_groups.keys()|length > 1 %}
|
{% if world.location_name_groups.keys()|length > 1 %}
|
||||||
<div class="divider"> </div>
|
<div class="divider"> </div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for location_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.location_names|sort) %}
|
{% for location_name in world.location_names|sort %}
|
||||||
<div class="set-entry">
|
<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 }} />
|
<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>
|
<label for="{{ option_name }}-{{ location_name }}">{{ location_name }}</label>
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
{% if world.item_name_groups.keys()|length > 1 %}
|
{% if world.item_name_groups.keys()|length > 1 %}
|
||||||
<div class="set-divider"> </div>
|
<div class="set-divider"> </div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
{% for item_name in world.item_names|sort %}
|
||||||
<div class="set-entry">
|
<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 }} />
|
<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>
|
<label for="{{ option_name }}-{{ item_name }}">{{ item_name }}</label>
|
||||||
|
|||||||
BIN
data/yatta.ico
BIN
data/yatta.ico
Binary file not shown.
|
Before Width: | Height: | Size: 149 KiB |
BIN
data/yatta.png
BIN
data/yatta.png
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
@@ -13,9 +13,6 @@
|
|||||||
# Adventure
|
# Adventure
|
||||||
/worlds/adventure/ @JusticePS
|
/worlds/adventure/ @JusticePS
|
||||||
|
|
||||||
# A Hat in Time
|
|
||||||
/worlds/ahit/ @CookieCat45
|
|
||||||
|
|
||||||
# A Link to the Past
|
# A Link to the Past
|
||||||
/worlds/alttp/ @Berserker66
|
/worlds/alttp/ @Berserker66
|
||||||
|
|
||||||
@@ -207,7 +204,7 @@
|
|||||||
/worlds/yoshisisland/ @PinkSwitch
|
/worlds/yoshisisland/ @PinkSwitch
|
||||||
|
|
||||||
#Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
#Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
||||||
/worlds/yugioh06/ @Rensen3
|
/worlds/yugioh06/ @rensen
|
||||||
|
|
||||||
# Zillion
|
# Zillion
|
||||||
/worlds/zillion/ @beauxq
|
/worlds/zillion/ @beauxq
|
||||||
|
|||||||
@@ -121,53 +121,6 @@ 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
|
### MultiWorld Object
|
||||||
|
|
||||||
The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible
|
The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible
|
||||||
@@ -225,6 +178,36 @@ 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
|
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.
|
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
|
||||||
|
|
||||||
Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally
|
Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally
|
||||||
@@ -249,6 +232,36 @@ Other classifications include:
|
|||||||
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
|
* `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
|
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
|
### 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
|
An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to
|
||||||
|
|||||||
@@ -665,14 +665,6 @@ class GeneratorOptions(Group):
|
|||||||
OFF = 0
|
OFF = 0
|
||||||
ON = 1
|
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
|
enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
|
||||||
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
|
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
|
||||||
players: Players = Players(0)
|
players: Players = Players(0)
|
||||||
@@ -681,7 +673,6 @@ class GeneratorOptions(Group):
|
|||||||
spoiler: Spoiler = Spoiler(3)
|
spoiler: Spoiler = Spoiler(3)
|
||||||
race: Race = Race(0)
|
race: Race = Race(0)
|
||||||
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
|
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
|
||||||
panic_method: PanicMethod = PanicMethod("swap")
|
|
||||||
|
|
||||||
|
|
||||||
class SNIOptions(Group):
|
class SNIOptions(Group):
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ class TestBase(unittest.TestCase):
|
|||||||
for item in multiworld.itempool:
|
for item in multiworld.itempool:
|
||||||
self.assertIn(item.name, world_type.item_name_to_id)
|
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):
|
def test_itempool_not_modified(self):
|
||||||
"""Test that worlds don't modify the itempool after `create_items`"""
|
"""Test that worlds don't modify the itempool after `create_items`"""
|
||||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||||
|
|||||||
@@ -66,3 +66,12 @@ class TestBase(unittest.TestCase):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
self.assertIn(location, world_type.location_name_to_id)
|
self.assertIn(location, world_type.location_name_to_id)
|
||||||
self.assertNotIn(group_name, 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_2"], ["string_3"])
|
||||||
self.assertEqual(new_weights["list_1"], ["string", "string_2"])
|
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_a"], 50)
|
||||||
self.assertEqual(new_weights["dict_1"]["option_b"], 50)
|
self.assertEqual(new_weights["dict_1"]["option_b"], 0)
|
||||||
self.assertEqual(new_weights["dict_1"]["option_c"], 50)
|
self.assertEqual(new_weights["dict_1"]["option_c"], 50)
|
||||||
self.assertNotIn("option_f", new_weights["dict_2"])
|
self.assertNotIn("option_f", new_weights["dict_2"])
|
||||||
self.assertEqual(new_weights["dict_2"]["option_g"], 50)
|
self.assertEqual(new_weights["dict_2"]["option_g"], 50)
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
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")
|
|
||||||
@@ -3,12 +3,13 @@ from __future__ import annotations
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from random import Random
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from random import Random
|
|
||||||
from dataclasses import make_dataclass
|
from dataclasses import make_dataclass
|
||||||
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple,
|
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping,
|
||||||
TYPE_CHECKING, Type, Union)
|
Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union)
|
||||||
|
|
||||||
from Options import (
|
from Options import (
|
||||||
ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions,
|
ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions,
|
||||||
@@ -54,12 +55,17 @@ class AutoWorldRegister(type):
|
|||||||
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||||
in dct.get("item_name_groups", {}).items()}
|
in dct.get("item_name_groups", {}).items()}
|
||||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
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_names"] = frozenset(dct["location_name_to_id"])
|
||||||
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||||
in dct.get("location_name_groups", {}).items()}
|
in dct.get("location_name_groups", {}).items()}
|
||||||
dct["location_name_groups"]["Everywhere"] = dct["location_names"]
|
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["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
|
# move away from get_required_client_version function
|
||||||
if "game" in dct:
|
if "game" in dct:
|
||||||
@@ -220,12 +226,6 @@ class WebWorld(metaclass=WebWorldRegister):
|
|||||||
option_groups: ClassVar[List[OptionGroup]] = []
|
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"."""
|
"""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):
|
class World(metaclass=AutoWorldRegister):
|
||||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||||
@@ -252,9 +252,23 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
item_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
item_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
||||||
"""maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}"""
|
"""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]]] = {}
|
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"}}"""
|
"""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
|
data_version: ClassVar[int] = 0
|
||||||
"""
|
"""
|
||||||
Increment this every time something in your world's names/id mappings changes.
|
Increment this every time something in your world's names/id mappings changes.
|
||||||
@@ -558,3 +572,18 @@ def data_package_checksum(data: "GamesPackage") -> str:
|
|||||||
assert sorted(data) == list(data), "Data not ordered"
|
assert sorted(data) == list(data), "Data not ordered"
|
||||||
from NetUtils import encode
|
from NetUtils import encode
|
||||||
return hashlib.sha1(encode(data).encode()).hexdigest()
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
A module containing the BizHawkClient base class and metaclass
|
A module containing the BizHawkClient base class and metaclass
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
@@ -11,13 +12,14 @@ from worlds.LauncherComponents import Component, SuffixIdentifier, Type, compone
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .context import BizHawkClientContext
|
from .context import BizHawkClientContext
|
||||||
|
else:
|
||||||
|
BizHawkClientContext = object
|
||||||
|
|
||||||
|
|
||||||
def launch_client(*args) -> None:
|
def launch_client(*args) -> None:
|
||||||
from .context import launch
|
from .context import launch
|
||||||
launch_subprocess(launch, name="BizHawkClient")
|
launch_subprocess(launch, name="BizHawkClient")
|
||||||
|
|
||||||
|
|
||||||
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||||
file_identifier=SuffixIdentifier())
|
file_identifier=SuffixIdentifier())
|
||||||
components.append(component)
|
components.append(component)
|
||||||
@@ -54,7 +56,7 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
|
|||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
@staticmethod
|
@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():
|
for systems, handlers in AutoBizHawkClientRegister.game_handlers.items():
|
||||||
if system in systems:
|
if system in systems:
|
||||||
for handler in handlers.values():
|
for handler in handlers.values():
|
||||||
@@ -75,7 +77,7 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
|||||||
"""The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
|
"""The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@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
|
"""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
|
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.
|
client class, so you do not need to check the system yourself.
|
||||||
@@ -84,18 +86,18 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
|||||||
as necessary (such as setting `ctx.game = self.game`, modifying `ctx.items_handling`, etc...)."""
|
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
|
"""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
|
name in your patched ROM. If ctx.auth is not set after calling, the player will be prompted to enter their
|
||||||
username."""
|
username."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@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
|
"""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."""
|
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`."""
|
"""For handling packages from the server. Called from `BizHawkClientContext.on_package`."""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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.
|
checking or launching the client, otherwise it will probably cause circular import issues.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import enum
|
import enum
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -147,8 +148,7 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
|||||||
script_version = await get_script_version(ctx.bizhawk_ctx)
|
script_version = await get_script_version(ctx.bizhawk_ctx)
|
||||||
|
|
||||||
if script_version != EXPECTED_SCRIPT_VERSION:
|
if script_version != EXPECTED_SCRIPT_VERSION:
|
||||||
logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but "
|
logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but got {script_version}. Disconnecting.")
|
||||||
f"got {script_version}. Disconnecting.")
|
|
||||||
disconnect(ctx.bizhawk_ctx)
|
disconnect(ctx.bizhawk_ctx)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,462 +0,0 @@
|
|||||||
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",
|
|
||||||
]
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,770 +0,0 @@
|
|||||||
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",
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,959 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
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",
|
|
||||||
}
|
|
||||||
@@ -1,374 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from test.bases import WorldTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class HatInTimeTestBase(WorldTestBase):
|
|
||||||
game = "A Hat in Time"
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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)")
|
|
||||||
@@ -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))
|
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']:
|
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)
|
forbid_item(multiworld.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
||||||
add_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 - 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))
|
set_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]:
|
if multiworld.pot_shuffle[player]:
|
||||||
# key can (and probably will) be moved behind bombable wall
|
# 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))
|
set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ from typing import Optional
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from BaseClasses import Item, ItemClassification
|
from BaseClasses import Item, ItemClassification
|
||||||
|
|
||||||
|
|
||||||
class ItemType(Enum):
|
class ItemType(Enum):
|
||||||
"""
|
"""
|
||||||
Used to indicate to the multi-world if an item is useful or not
|
Used to indicate to the multi-world if an item is usefull or not
|
||||||
"""
|
"""
|
||||||
NORMAL = 0
|
NORMAL = 0
|
||||||
PROGRESSION = 1
|
PROGRESSION = 1
|
||||||
JUNK = 2
|
JUNK = 2
|
||||||
|
|
||||||
|
|
||||||
class ItemGroup(Enum):
|
class ItemGroup(Enum):
|
||||||
"""
|
"""
|
||||||
Used to group items
|
Used to group items
|
||||||
@@ -30,7 +28,6 @@ class ItemGroup(Enum):
|
|||||||
SONG = 5
|
SONG = 5
|
||||||
TURTLE = 6
|
TURTLE = 6
|
||||||
|
|
||||||
|
|
||||||
class AquariaItem(Item):
|
class AquariaItem(Item):
|
||||||
"""
|
"""
|
||||||
A single item in the Aquaria game.
|
A single item in the Aquaria game.
|
||||||
@@ -43,13 +40,12 @@ class AquariaItem(Item):
|
|||||||
"""
|
"""
|
||||||
Initialisation of the Item
|
Initialisation of the Item
|
||||||
:param name: The name of the item
|
:param name: The name of the item
|
||||||
:param classification: If the item is useful or not
|
:param classification: If the item is usefull or not
|
||||||
:param code: The ID of the item (if None, it is an event)
|
:param code: The ID of the item (if None, it is an event)
|
||||||
:param player: The ID of the player in the multiworld
|
:param player: The ID of the player in the multiworld
|
||||||
"""
|
"""
|
||||||
super().__init__(name, classification, code, player)
|
super().__init__(name, classification, code, player)
|
||||||
|
|
||||||
|
|
||||||
class ItemData:
|
class ItemData:
|
||||||
"""
|
"""
|
||||||
Data of an item.
|
Data of an item.
|
||||||
@@ -72,7 +68,6 @@ class ItemData:
|
|||||||
self.type = type
|
self.type = type
|
||||||
self.group = group
|
self.group = group
|
||||||
|
|
||||||
|
|
||||||
"""Information data for every (not event) item."""
|
"""Information data for every (not event) item."""
|
||||||
item_table = {
|
item_table = {
|
||||||
# name: ID, Nb, Item Type, Item Group
|
# name: ID, Nb, Item Type, Item Group
|
||||||
@@ -212,3 +207,4 @@ item_table = {
|
|||||||
"Transturtle Simon says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
|
"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 Arnassi ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_home_water = {
|
locations_home_water = {
|
||||||
"Home water, bulb below the grouper fish": 698058,
|
"Home water, bulb below the grouper fish": 698058,
|
||||||
"Home water, bulb in the path below Nautilus Prime": 698059,
|
"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 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 end of the left path from the verse cave": 698061,
|
||||||
"Home water, bulb in the top left path": 698062,
|
"Home water, bulb in the top left path": 698062,
|
||||||
@@ -129,7 +129,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_openwater_bl = {
|
locations_openwater_bl = {
|
||||||
"Open water bottom left area, bulb behind the chomper fish": 698011,
|
"Open water bottom left area, bulb behind the chomper fish": 698011,
|
||||||
"Open water bottom left area, bulb inside the lowest fish pass": 698010,
|
"Open water bottom left area, bulb inside the downest fish pass": 698010,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_skeleton_path = {
|
locations_skeleton_path = {
|
||||||
@@ -226,7 +226,7 @@ class AquariaLocations:
|
|||||||
"Mithalas cathedral, third urn in the path behind the flesh vein": 698146,
|
"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, one of the urns in the top right room": 698147,
|
||||||
"Mithalas cathedral, Mithalan Dress": 698189,
|
"Mithalas cathedral, Mithalan Dress": 698189,
|
||||||
"Mithalas cathedral right area, urn below the left entrance": 698198,
|
"Mithalas cathedral right area, urn bellow the left entrance": 698198,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_cathedral_underground = {
|
locations_cathedral_underground = {
|
||||||
@@ -457,7 +457,7 @@ class AquariaLocations:
|
|||||||
locations_body_l = {
|
locations_body_l = {
|
||||||
"The body left area, first bulb in the top face room": 698066,
|
"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, second bulb in the top face room": 698069,
|
||||||
"The body left area, bulb below the water stream": 698067,
|
"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 top path to the top face room": 698068,
|
||||||
"The body left area, bulb in the bottom face room": 698070,
|
"The body left area, bulb in the bottom face room": 698070,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ from Options import Toggle, Choice, Range, DeathLink, PerGameCommonOptions, Defa
|
|||||||
|
|
||||||
class IngredientRandomizer(Choice):
|
class IngredientRandomizer(Choice):
|
||||||
"""
|
"""
|
||||||
Select if the simple ingredients (that do not have a recipe) should be randomized.
|
Randomize Ingredients. Select if the simple ingredients (that does not have
|
||||||
If "Common Ingredients" is selected, the randomization will exclude the "Red Bulb", "Special Bulb" and "Rukh Egg".
|
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"
|
display_name = "Randomize Ingredients"
|
||||||
option_off = 0
|
option_off = 0
|
||||||
@@ -28,25 +29,27 @@ class DishRandomizer(Toggle):
|
|||||||
class TurtleRandomizer(Choice):
|
class TurtleRandomizer(Choice):
|
||||||
"""Randomize the transportation turtle."""
|
"""Randomize the transportation turtle."""
|
||||||
display_name = "Turtle Randomizer"
|
display_name = "Turtle Randomizer"
|
||||||
option_none = 0
|
option_no_turtle_randomization = 0
|
||||||
option_all = 1
|
option_randomize_all_turtle = 1
|
||||||
option_all_except_final = 2
|
option_randomize_turtle_other_than_the_final_one = 2
|
||||||
default = 2
|
default = 2
|
||||||
|
|
||||||
|
|
||||||
class EarlyEnergyForm(DefaultOnToggle):
|
class EarlyEnergyForm(DefaultOnToggle):
|
||||||
""" Force the Energy Form to be in a location early in the game """
|
"""
|
||||||
|
Force the Energy Form to be in a location before leaving the areas around the Home Water.
|
||||||
|
"""
|
||||||
display_name = "Early Energy Form"
|
display_name = "Early Energy Form"
|
||||||
|
|
||||||
|
|
||||||
class AquarianTranslation(Toggle):
|
class AquarianTranslation(Toggle):
|
||||||
"""Translate the Aquarian scripture in the game into English."""
|
"""Translate to English the Aquarian scripture in the game."""
|
||||||
display_name = "Translate Aquarian"
|
display_name = "Translate Aquarian"
|
||||||
|
|
||||||
|
|
||||||
class BigBossesToBeat(Range):
|
class BigBossesToBeat(Range):
|
||||||
"""
|
"""
|
||||||
The number of big bosses to beat before having access to the creator (the final boss). The big bosses are
|
A number of big bosses to beat before having access to the creator (the final boss). The big bosses are
|
||||||
"Fallen God", "Mithalan God", "Drunian God", "Sun God" and "The Golem".
|
"Fallen God", "Mithalan God", "Drunian God", "Sun God" and "The Golem".
|
||||||
"""
|
"""
|
||||||
display_name = "Big bosses to beat"
|
display_name = "Big bosses to beat"
|
||||||
@@ -57,10 +60,10 @@ class BigBossesToBeat(Range):
|
|||||||
|
|
||||||
class MiniBossesToBeat(Range):
|
class MiniBossesToBeat(Range):
|
||||||
"""
|
"""
|
||||||
The number of minibosses to beat before having access to the creator (the final boss). The minibosses are
|
A number of Minibosses to beat before having access to the creator (the final boss). Mini bosses are
|
||||||
"Nautilus Prime", "Blaster Peg Prime", "Mergog", "Mithalan priests", "Octopus Prime", "Crabbius Maximus",
|
"Nautilus Prime", "Blaster Peg Prime", "Mergog", "Mithalan priests", "Octopus Prime", "Crabbius Maximus",
|
||||||
"Mantis Shrimp Prime" and "King Jellyfish God Prime".
|
"Mantis Shrimp Prime" and "King Jellyfish God Prime". Note that the Energy statue and Simon says are not
|
||||||
Note that the Energy Statue and Simon Says are not minibosses.
|
mini bosses.
|
||||||
"""
|
"""
|
||||||
display_name = "Mini bosses to beat"
|
display_name = "Mini bosses to beat"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
@@ -70,50 +73,47 @@ class MiniBossesToBeat(Range):
|
|||||||
|
|
||||||
class Objective(Choice):
|
class Objective(Choice):
|
||||||
"""
|
"""
|
||||||
The game objective can be to kill the creator or to kill the creator after obtaining all three secret memories.
|
The game objective can be only to kill the creator or to kill the creator
|
||||||
|
and having obtained the three every secret memories
|
||||||
"""
|
"""
|
||||||
display_name = "Objective"
|
display_name = "Objective"
|
||||||
option_kill_the_creator = 0
|
option_kill_the_creator = 0
|
||||||
option_obtain_secrets_and_kill_the_creator = 1
|
option_obtain_secrets_and_kill_the_creator = 1
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
class SkipFirstVision(Toggle):
|
class SkipFirstVision(Toggle):
|
||||||
"""
|
"""
|
||||||
The first vision in the game, where Naija transforms into Energy Form and gets flooded by enemies, is quite cool but
|
The first vision in the game; where Naija transform to Energy Form and get fload by enemy; is quite cool but
|
||||||
can be quite long when you already know what is going on. This option can be used to skip this vision.
|
can be quite long when you already know what is going on. This option can be used to skip this vision.
|
||||||
"""
|
"""
|
||||||
display_name = "Skip Naija's first vision"
|
display_name = "Skip first Naija's vision"
|
||||||
|
|
||||||
|
|
||||||
class NoProgressionHardOrHiddenLocation(Toggle):
|
class NoProgressionHardOrHiddenLocation(Toggle):
|
||||||
"""
|
"""
|
||||||
Make sure that there are no progression items at hard-to-reach or hard-to-find locations.
|
Make sure that there is no progression items at hard to get or hard to find locations.
|
||||||
Those locations are very High locations (that need beast form, soup and skill to get),
|
Those locations that will be very High location (that need beast form, soup and skill to get), every
|
||||||
every location in the bubble cave, locations where need you to cross a false wall without any indication,
|
location in the bubble cave, locations that need you to cross a false wall without any indication, Arnassi
|
||||||
the Arnassi race, bosses and minibosses. Useful for those that want a more casual run.
|
race, bosses and mini-bosses. Usefull for those that want a casual run.
|
||||||
"""
|
"""
|
||||||
display_name = "No progression in hard or hidden locations"
|
display_name = "No progression in hard or hidden locations"
|
||||||
|
|
||||||
|
|
||||||
class LightNeededToGetToDarkPlaces(DefaultOnToggle):
|
class LightNeededToGetToDarkPlaces(DefaultOnToggle):
|
||||||
"""
|
"""
|
||||||
Make sure that the sun form or the dumbo pet can be acquired before getting to dark places.
|
Make sure that the sun form or the dumbo pet can be aquired before getting to dark places. Be aware that navigating
|
||||||
Be aware that navigating in dark places without light is extremely difficult.
|
in dark place without light is extremely difficult.
|
||||||
"""
|
"""
|
||||||
display_name = "Light needed to get to dark places"
|
display_name = "Light needed to get to dark places"
|
||||||
|
|
||||||
|
|
||||||
class BindSongNeededToGetUnderRockBulb(Toggle):
|
class BindSongNeededToGetUnderRockBulb(Toggle):
|
||||||
"""
|
"""
|
||||||
Make sure that the bind song can be acquired before having to obtain sing bulbs under rocks.
|
Make sure that the bind song can be aquired before having to obtain sing bulb under rocks.
|
||||||
"""
|
"""
|
||||||
display_name = "Bind song needed to get sing bulbs under rocks"
|
display_name = "Bind song needed to get sing bulbs under rocks"
|
||||||
|
|
||||||
|
|
||||||
class UnconfineHomeWater(Choice):
|
class UnconfineHomeWater(Choice):
|
||||||
"""
|
"""
|
||||||
Open the way out of the Home water area so that Naija can go to open water and beyond without the bind song.
|
Open the way out of Home water area so that Naija can go to open water and beyond without the bind song.
|
||||||
"""
|
"""
|
||||||
display_name = "Unconfine Home Water Area"
|
display_name = "Unconfine Home Water Area"
|
||||||
option_off = 0
|
option_off = 0
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Description: Used to manage Regions in the Aquaria game multiworld randomizer
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
from BaseClasses import MultiWorld, Region, Entrance, ItemClassification, CollectionState
|
from BaseClasses import MultiWorld, Region, Entrance, ItemClassification, LocationProgressType, CollectionState
|
||||||
from .Items import AquariaItem
|
from .Items import AquariaItem
|
||||||
from .Locations import AquariaLocations, AquariaLocation
|
from .Locations import AquariaLocations, AquariaLocation
|
||||||
from .Options import AquariaOptions
|
from .Options import AquariaOptions
|
||||||
@@ -223,6 +223,8 @@ class AquariaRegions:
|
|||||||
region.add_locations(locations, AquariaLocation)
|
region.add_locations(locations, AquariaLocation)
|
||||||
return region
|
return region
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def __create_home_water_area(self) -> None:
|
def __create_home_water_area(self) -> None:
|
||||||
"""
|
"""
|
||||||
Create the `verse_cave`, `home_water` and `song_cave*` regions
|
Create the `verse_cave`, `home_water` and `song_cave*` regions
|
||||||
@@ -1093,10 +1095,12 @@ class AquariaRegions:
|
|||||||
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))
|
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def __adjusting_manual_rules(self) -> None:
|
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))
|
lambda state: _has_beast_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Open water bottom left area, bulb inside the lowest fish pass", self.player),
|
add_rule(self.multiworld.get_location("Open water bottom left area, bulb inside the downest fish pass", self.player),
|
||||||
lambda state: _has_fish_form(state, 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))
|
lambda state: _has_spirit_form(state, self.player))
|
||||||
@@ -1118,7 +1122,7 @@ class AquariaRegions:
|
|||||||
self.player), lambda state: _has_energy_form(state, self.player))
|
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))
|
lambda state: _has_bind_song(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Home water, bulb in the path below Nautilus Prime", self.player),
|
add_rule(self.multiworld.get_location("Home water, bulb in the path bellow Nautilus Prime", self.player),
|
||||||
lambda state: _has_bind_song(state, 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))
|
lambda state: _has_energy_form(state, self.player))
|
||||||
@@ -1129,6 +1133,9 @@ class AquariaRegions:
|
|||||||
lambda state: _has_fish_form(state, self.player) and
|
lambda state: _has_fish_form(state, self.player) and
|
||||||
_has_spirit_form(state, self.player))
|
_has_spirit_form(state, self.player))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def __no_progression_hard_or_hidden_location(self) -> None:
|
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 =\
|
self.player).item_rule =\
|
||||||
@@ -1235,7 +1242,11 @@ class AquariaRegions:
|
|||||||
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))
|
lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player))
|
||||||
if options.early_energy_form:
|
if options.early_energy_form:
|
||||||
self.multiworld.early_items[self.player]["Energy form"] = 1
|
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))
|
||||||
|
|
||||||
if options.no_progression_hard_or_hidden_locations:
|
if options.no_progression_hard_or_hidden_locations:
|
||||||
self.__no_progression_hard_or_hidden_location()
|
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 typing import List, Dict, ClassVar, Any
|
||||||
from worlds.AutoWorld import World, WebWorld
|
from ..AutoWorld import World, WebWorld
|
||||||
from BaseClasses import Tutorial, MultiWorld, ItemClassification
|
from BaseClasses import Tutorial, MultiWorld, ItemClassification
|
||||||
from .Items import item_table, AquariaItem, ItemType, ItemGroup
|
from .Items import item_table, AquariaItem, ItemType, ItemGroup
|
||||||
from .Locations import location_table
|
from .Locations import location_table
|
||||||
@@ -114,7 +114,7 @@ class AquariaWorld(World):
|
|||||||
|
|
||||||
def create_item(self, name: str) -> AquariaItem:
|
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
|
result: AquariaItem
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -11,39 +11,39 @@ options page link: [Aquaria Player Options Page](../player-options).
|
|||||||
## What does randomization do to this game?
|
## What does randomization do to this game?
|
||||||
The locations in the randomizer are:
|
The locations in the randomizer are:
|
||||||
|
|
||||||
- All sing bulbs
|
- All sing bulbs;
|
||||||
- All Mithalas Urns
|
- All Mithalas Urns;
|
||||||
- All Sunken City crates
|
- All Sunken City crates;
|
||||||
- Collectible treasure locations (including pet eggs and costumes)
|
- Collectible treasure locations (including pet eggs and costumes);
|
||||||
- Beating Simon says
|
- Beating Simon says;
|
||||||
- Li cave
|
- Li cave;
|
||||||
- Every Transportation Turtle (also called transturtle)
|
- Every Transportation Turtle (also called transturtle);
|
||||||
- Locations where you get songs:
|
- Locations where you get songs,
|
||||||
* Erulian spirit cristal
|
* Erulian spirit cristal,
|
||||||
* Energy status mini-boss
|
* Energy status mini-boss,
|
||||||
* Beating Mithalan God boss
|
* Beating Mithalan God boss,
|
||||||
* Fish cave puzzle
|
* Fish cave puzzle,
|
||||||
* Beating Drunian God boss
|
* Beating Drunian God boss,
|
||||||
* Beating Sun God boss
|
* Beating Sun God boss,
|
||||||
* Breaking Li cage in the body
|
* Breaking Li cage in the body
|
||||||
|
|
||||||
Note that, unlike the vanilla game, when opening sing bulbs, Mithalas urns and Sunken City crates,
|
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 checked.
|
nothing will come out of them. The moment those bulbs, urns and crates are opened, the location is considered received.
|
||||||
|
|
||||||
The items in the randomizer are:
|
The items in the randomizer are:
|
||||||
- Dishes (used to learn recipes)<sup>*</sup>
|
- Dishes (used to learn recipes*);
|
||||||
- Some ingredients
|
- Some ingredients;
|
||||||
- The Wok (third plate used to cook 3-ingredient recipes everywhere)
|
- The Wok (third plate used to cook 3 ingredients recipes everywhere);
|
||||||
- All collectible treasure (including pet eggs and costumes)
|
- All collectible treasure (including pet eggs and costumes);
|
||||||
- Li and Li's song
|
- Li and Li song;
|
||||||
- All songs (other than Li's song since it is learned when Li is obtained)
|
- All songs (other than Li's song since it is learned when Li is obtained);
|
||||||
- Transportation to transturtles
|
- Transportation to transturtles.
|
||||||
|
|
||||||
Also, there is the option to randomize every ingredient drops (from fishes, monsters
|
Also, there is the option to randomize every ingredient drops (from fishes, monsters
|
||||||
or plants).
|
or plants).
|
||||||
|
|
||||||
<sup>*</sup> Note that, unlike in the vanilla game, the recipes for dishes (other than the Sea Loaf)
|
*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
|
cannot be cooked (and learn) before being obtained as randomized items. Also, enemies and plants
|
||||||
that drop dishes that have not been learned before will drop ingredients of this dish instead.
|
that drop dishes that have not been learned before will drop ingredients of this dish instead.
|
||||||
|
|
||||||
## What is the goal of the game?
|
## 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.
|
No visuals are shown when finding locations other than collectible treasure.
|
||||||
For those treasures, the visual of the treasure is visually unchanged.
|
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
|
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 the player receives an item, what happens?
|
||||||
When you receive an item, a message will pop up to inform you where you received
|
When you receive an item, a message will pop up to inform you where you received
|
||||||
the item from and which one it was.
|
the item from, and which one it is.
|
||||||
@@ -2,12 +2,9 @@
|
|||||||
|
|
||||||
## Required Software
|
## Required Software
|
||||||
|
|
||||||
- The original Aquaria Game (purchasable from most online game stores)
|
- The original Aquaria Game (buyable from a lot of online game seller);
|
||||||
- The [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
|
- 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
|
## Installation and execution Procedures
|
||||||
|
|
||||||
@@ -16,9 +13,10 @@
|
|||||||
First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
|
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.
|
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
|
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 its own save game.
|
game you play will make sure that every game has their own save game.
|
||||||
|
|
||||||
Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files are:
|
Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files
|
||||||
|
are those:
|
||||||
- aquaria_randomizer.exe
|
- aquaria_randomizer.exe
|
||||||
- OpenAL32.dll
|
- OpenAL32.dll
|
||||||
- override (directory)
|
- override (directory)
|
||||||
@@ -27,11 +25,11 @@ Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria
|
|||||||
- wrap_oal.dll
|
- wrap_oal.dll
|
||||||
- cacert.pem
|
- cacert.pem
|
||||||
|
|
||||||
If there is a conflict between files in the original game folder and the unzipped files, you should overwrite
|
If there is a conflict between file in the original game folder and the unzipped files, you should override
|
||||||
the original files with the ones from the unzipped randomizer.
|
the original files with the one of the unzipped randomizer.
|
||||||
|
|
||||||
Finally, to launch the randomizer, you must use the command line interface (you can open the command line interface
|
Finally, to launch the randomizer, you must use the command line interface (you can open the command line interface
|
||||||
by typing `cmd` in the address bar of the Windows File Explorer). Here is the command line used to start the
|
by writing `cmd` in the address bar of the Windows file explorer). Here is the command line to use to start the
|
||||||
randomizer:
|
randomizer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -46,8 +44,8 @@ aquaria_randomizer.exe --name YourName --server theServer:thePort --password th
|
|||||||
|
|
||||||
### Linux when using the AppImage
|
### Linux when using the AppImage
|
||||||
|
|
||||||
If you use the AppImage, just copy it into the Aquaria game folder. You then have to make it executable. You
|
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:
|
can do that from command line by using
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x Aquaria_Randomizer-*.AppImage
|
chmod +x Aquaria_Randomizer-*.AppImage
|
||||||
@@ -67,7 +65,7 @@ or, if the room has a password:
|
|||||||
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort --password thePassword
|
./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 occurs,
|
Note that you should not have multiple Aquaria_Randomizer AppImage file in the same folder. If this situation occurred,
|
||||||
the preceding commands will launch the game multiple times.
|
the preceding commands will launch the game multiple times.
|
||||||
|
|
||||||
### Linux when using the tar file
|
### Linux when using the tar file
|
||||||
@@ -75,23 +73,24 @@ 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
|
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.
|
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:
|
Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted
|
||||||
|
files are those:
|
||||||
- aquaria_randomizer
|
- aquaria_randomizer
|
||||||
- override (directory)
|
- override (directory)
|
||||||
- usersettings.xml
|
- usersettings.xml
|
||||||
- cacert.pem
|
- cacert.pem
|
||||||
|
|
||||||
If there is a conflict between files in the original game folder and the extracted files, you should overwrite
|
If there is a conflict between file in the original game folder and the extracted files, you should override
|
||||||
the original files with the ones from the extracted randomizer files.
|
the original files with the one of the extracted randomizer files.
|
||||||
|
|
||||||
Then, you should use your system package manager to install `liblua5`, `libogg`, `libvorbis`, `libopenal` and `libsdl2`.
|
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:
|
On Debian base system (like Ubuntu), you can use the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
|
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Also, if there are certain `.so` files in the original Aquaria game folder (`libgcc_s.so.1`, `libopenal.so.1`,
|
Also, if there is some `.so` files in the Aquaria original game folder (`libgcc_s.so.1`, `libopenal.so.1`,
|
||||||
`libSDL-1.2.so.0` and `libstdc++.so.6`), you should remove them from the Aquaria Randomizer game folder. Those are
|
`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.
|
old libraries that will not work on the recent build of the randomizer.
|
||||||
|
|
||||||
@@ -107,7 +106,7 @@ or, if the room has a password:
|
|||||||
./aquaria_randomizer --name YourName --server theServer:thePort --password thePassword
|
./aquaria_randomizer --name YourName --server theServer:thePort --password thePassword
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: If you get a permission denied error when using the command line, you can use this command to be
|
Note: If you have a permission denied error when using the command line, you can use this command line to be
|
||||||
sure that your executable has executable permission:
|
sure that your executable has executable permission:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ after_home_water_locations = [
|
|||||||
"Open water top right area, bulb in the turtle room",
|
"Open water top right area, bulb in the turtle room",
|
||||||
"Open water top right area, Transturtle",
|
"Open water top right area, Transturtle",
|
||||||
"Open water bottom left area, bulb behind the chomper fish",
|
"Open water bottom left area, bulb behind the chomper fish",
|
||||||
"Open water bottom left area, bulb inside the lowest fish pass",
|
"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 close to the right exit",
|
||||||
"Open water skeleton path, bulb behind the chomper fish",
|
"Open water skeleton path, bulb behind the chomper fish",
|
||||||
"Open water skeleton path, King skull",
|
"Open water skeleton path, King skull",
|
||||||
@@ -82,7 +82,7 @@ after_home_water_locations = [
|
|||||||
"Mithalas cathedral, third 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, one of the urns in the top right room",
|
||||||
"Mithalas cathedral, Mithalan Dress",
|
"Mithalas cathedral, Mithalan Dress",
|
||||||
"Mithalas cathedral right area, urn below the left entrance",
|
"Mithalas cathedral right area, urn bellow the left entrance",
|
||||||
"Cathedral underground, bulb in the center part",
|
"Cathedral underground, bulb in the center part",
|
||||||
"Cathedral underground, first bulb in the top left part",
|
"Cathedral underground, first bulb in the top left part",
|
||||||
"Cathedral underground, second bulb in the top left part",
|
"Cathedral underground, second bulb in the top left part",
|
||||||
@@ -178,7 +178,7 @@ after_home_water_locations = [
|
|||||||
"The body main area, bulb on the main path blocking tube",
|
"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, first bulb in the top face room",
|
||||||
"The body left area, second 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 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 top path to the top face room",
|
||||||
"The body left area, bulb in the bottom 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 face room",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class BindSongAccessTest(AquariaTestBase):
|
|||||||
"""Test locations that require Bind song"""
|
"""Test locations that require Bind song"""
|
||||||
locations = [
|
locations = [
|
||||||
"Verse cave right area, Big Seed",
|
"Verse cave right area, Big Seed",
|
||||||
"Home water, bulb in the path below Nautilus Prime",
|
"Home water, bulb in the path bellow Nautilus Prime",
|
||||||
"Home water, bulb in the bottom left room",
|
"Home water, bulb in the bottom left room",
|
||||||
"Home water, Nautilus Egg",
|
"Home water, Nautilus Egg",
|
||||||
"Song cave, Verse egg",
|
"Song cave, Verse egg",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class BindSongOptionAccessTest(AquariaTestBase):
|
|||||||
"Song cave, bulb under the rock close to the song door",
|
"Song cave, bulb under the rock close to the song door",
|
||||||
"Song cave, bulb under the rock in the path to the singing statues",
|
"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",
|
"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 path bellow Nautilus Prime",
|
||||||
"Home water, bulb in the bottom left room",
|
"Home water, bulb in the bottom left room",
|
||||||
"Home water, Nautilus Egg",
|
"Home water, Nautilus Egg",
|
||||||
"Song cave, Verse egg",
|
"Song cave, Verse egg",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class EnergyFormAccessTest(AquariaTestBase):
|
|||||||
"Mithalas cathedral, third 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, one of the urns in the top right room",
|
||||||
"Mithalas cathedral, Mithalan Dress",
|
"Mithalas cathedral, Mithalan Dress",
|
||||||
"Mithalas cathedral right area, urn below the left entrance",
|
"Mithalas cathedral right area, urn bellow the left entrance",
|
||||||
"Cathedral boss area, beating Mithalan God",
|
"Cathedral boss area, beating Mithalan God",
|
||||||
"Kelp Forest top left area, bulb close to the Verse egg",
|
"Kelp Forest top left area, bulb close to the Verse egg",
|
||||||
"Kelp forest top left area, Verse egg",
|
"Kelp forest top left area, Verse egg",
|
||||||
@@ -67,6 +67,7 @@ class EnergyFormAccessTest(AquariaTestBase):
|
|||||||
"First secret",
|
"First secret",
|
||||||
"Sunken City cleared",
|
"Sunken City cleared",
|
||||||
"Objective complete",
|
"Objective complete",
|
||||||
|
|
||||||
]
|
]
|
||||||
items = [["Energy form"]]
|
items = [["Energy form"]]
|
||||||
self.assertAccessDependency(locations, items)
|
self.assertAccessDependency(locations, items)
|
||||||
31
worlds/aquaria/test/test_energy_form_access_option.py
Normal file
31
worlds/aquaria/test/test_energy_form_access_option.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"""
|
||||||
|
Author: Louis M
|
||||||
|
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
||||||
|
Description: Unit test used to test accessibility of locations with and without the bind song (with the early
|
||||||
|
energy form option)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from worlds.aquaria.test import AquariaTestBase, after_home_water_locations
|
||||||
|
|
||||||
|
|
||||||
|
class EnergyFormAccessTest(AquariaTestBase):
|
||||||
|
"""Unit test used to test accessibility of locations with and without the energy form"""
|
||||||
|
options = {
|
||||||
|
"early_energy_form": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_energy_form_location(self) -> None:
|
||||||
|
"""Test locations that require Energy form with early energy song enable"""
|
||||||
|
locations = [
|
||||||
|
"Home water, Nautilus Egg",
|
||||||
|
"Naija's home, bulb after the energy door",
|
||||||
|
"Energy temple first area, bulb in the bottom room blocked by a rock",
|
||||||
|
"Energy temple second area, bulb under the rock",
|
||||||
|
"Energy temple bottom entrance, Krotite armor",
|
||||||
|
"Energy temple third area, bulb in the bottom path",
|
||||||
|
"Energy temple boss area, Fallen god tooth",
|
||||||
|
"Energy temple blaster room, Blaster egg",
|
||||||
|
*after_home_water_locations
|
||||||
|
]
|
||||||
|
items = [["Energy form"]]
|
||||||
|
self.assertAccessDependency(locations, items)
|
||||||
@@ -21,7 +21,7 @@ class FishFormAccessTest(AquariaTestBase):
|
|||||||
"Mithalas city, urn inside a home fish pass",
|
"Mithalas city, urn inside a home fish pass",
|
||||||
"Kelp Forest top right area, bulb in the top fish pass",
|
"Kelp Forest top right area, bulb in the top fish pass",
|
||||||
"The veil bottom area, Verse egg",
|
"The veil bottom area, Verse egg",
|
||||||
"Open water bottom left area, bulb inside the lowest fish pass",
|
"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, bulb close to the Verse egg",
|
||||||
"Kelp forest top left area, Verse egg",
|
"Kelp forest top left area, Verse egg",
|
||||||
"Mermog cave, bulb in the left part of the cave",
|
"Mermog cave, bulb in the left part of the cave",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class LiAccessTest(AquariaTestBase):
|
|||||||
"The body main area, bulb on the main path blocking tube",
|
"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, first bulb in the top face room",
|
||||||
"The body left area, second 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 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 top path to the top face room",
|
||||||
"The body left area, bulb in the bottom 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 face room",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class NatureFormAccessTest(AquariaTestBase):
|
|||||||
"The body main area, bulb on the main path blocking tube",
|
"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, first bulb in the top face room",
|
||||||
"The body left area, second 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 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 top path to the top face room",
|
||||||
"The body left area, bulb in the bottom 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 face room",
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
class Logic(Choice):
|
class Logic(Choice):
|
||||||
"""
|
"""Choose the logic used by the randomizer."""
|
||||||
Choose the logic used by the randomizer.
|
|
||||||
"""
|
|
||||||
display_name = "Logic"
|
display_name = "Logic"
|
||||||
option_glitchless = 0
|
option_glitchless = 0
|
||||||
option_glitched = 1
|
option_glitched = 1
|
||||||
@@ -19,38 +17,26 @@ class Logic(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class SkipIntro(DefaultOnToggle):
|
class SkipIntro(DefaultOnToggle):
|
||||||
"""
|
"""Skips escaping the police station.
|
||||||
Skips escaping the police station.
|
Graffiti spots tagged during the intro will not unlock items."""
|
||||||
|
|
||||||
Graffiti spots tagged during the intro will not unlock items.
|
|
||||||
"""
|
|
||||||
display_name = "Skip Intro"
|
display_name = "Skip Intro"
|
||||||
|
|
||||||
|
|
||||||
class SkipDreams(Toggle):
|
class SkipDreams(Toggle):
|
||||||
"""
|
"""Skips the dream sequences at the end of each chapter.
|
||||||
Skips the dream sequences at the end of each chapter.
|
This can be changed later in the options menu inside the Archipelago phone app."""
|
||||||
|
|
||||||
This can be changed later in the options menu inside the Archipelago phone app.
|
|
||||||
"""
|
|
||||||
display_name = "Skip Dreams"
|
display_name = "Skip Dreams"
|
||||||
|
|
||||||
|
|
||||||
class SkipHands(Toggle):
|
class SkipHands(Toggle):
|
||||||
"""
|
"""Skips spraying the lion statue hands after the dream in Chapter 5."""
|
||||||
Skips spraying the lion statue hands after the dream in Chapter 5.
|
|
||||||
"""
|
|
||||||
display_name = "Skip Statue Hands"
|
display_name = "Skip Statue Hands"
|
||||||
|
|
||||||
|
|
||||||
class TotalRep(Range):
|
class TotalRep(Range):
|
||||||
"""
|
"""Change the total amount of REP in your world.
|
||||||
Change the total amount of REP in your world.
|
|
||||||
|
|
||||||
At least 960 REP is needed to finish the game.
|
At least 960 REP is needed to finish the game.
|
||||||
|
Will be rounded to the nearest number divisible by 8."""
|
||||||
Will be rounded to the nearest number divisible by 8.
|
|
||||||
"""
|
|
||||||
display_name = "Total REP"
|
display_name = "Total REP"
|
||||||
range_start = 1000
|
range_start = 1000
|
||||||
range_end = 2000
|
range_end = 2000
|
||||||
@@ -88,16 +74,12 @@ class TotalRep(Range):
|
|||||||
|
|
||||||
|
|
||||||
class EndingREP(Toggle):
|
class EndingREP(Toggle):
|
||||||
"""
|
"""Changes the final boss to require 1000 REP instead of 960 REP to start."""
|
||||||
Changes the final boss to require 1000 REP instead of 960 REP to start.
|
|
||||||
"""
|
|
||||||
display_name = "Extra REP Required"
|
display_name = "Extra REP Required"
|
||||||
|
|
||||||
|
|
||||||
class StartStyle(Choice):
|
class StartStyle(Choice):
|
||||||
"""
|
"""Choose which movestyle to start with."""
|
||||||
Choose which movestyle to start with.
|
|
||||||
"""
|
|
||||||
display_name = "Starting Movestyle"
|
display_name = "Starting Movestyle"
|
||||||
option_skateboard = 2
|
option_skateboard = 2
|
||||||
option_inline_skates = 3
|
option_inline_skates = 3
|
||||||
@@ -106,22 +88,17 @@ class StartStyle(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class LimitedGraffiti(Toggle):
|
class LimitedGraffiti(Toggle):
|
||||||
"""
|
"""Each graffiti design can only be used a limited number of times before being removed from your inventory.
|
||||||
Each graffiti design can only be used a limited number of times before being removed from your inventory.
|
In some cases, such as completing a dream, using graffiti to defeat enemies, or spraying over your own graffiti,
|
||||||
|
uses will not be counted.
|
||||||
In some cases, such as completing a dream, using graffiti to defeat enemies, or spraying over your own graffiti, uses will not be counted.
|
If enabled, doing graffiti is disabled during crew battles, to prevent softlocking."""
|
||||||
|
|
||||||
If enabled, doing graffiti is disabled during crew battles, to prevent softlocking.
|
|
||||||
"""
|
|
||||||
display_name = "Limited Graffiti"
|
display_name = "Limited Graffiti"
|
||||||
|
|
||||||
|
|
||||||
class SGraffiti(Choice):
|
class SGraffiti(Choice):
|
||||||
"""
|
"""Choose if small graffiti should be separate, meaning that you will need to switch characters every time you run
|
||||||
Choose if small graffiti should be separate, meaning that you will need to switch characters every time you run out, or combined, meaning that unlocking new characters will add 5 uses that any character can use.
|
out, or combined, meaning that unlocking new characters will add 5 uses that any character can use.
|
||||||
|
Has no effect if Limited Graffiti is disabled."""
|
||||||
Has no effect if Limited Graffiti is disabled.
|
|
||||||
"""
|
|
||||||
display_name = "Small Graffiti Uses"
|
display_name = "Small Graffiti Uses"
|
||||||
option_separate = 0
|
option_separate = 0
|
||||||
option_combined = 1
|
option_combined = 1
|
||||||
@@ -129,27 +106,19 @@ class SGraffiti(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class JunkPhotos(Toggle):
|
class JunkPhotos(Toggle):
|
||||||
"""
|
"""Skip taking pictures of Polo for items."""
|
||||||
Skip taking pictures of Polo for items.
|
|
||||||
"""
|
|
||||||
display_name = "Skip Polo Photos"
|
display_name = "Skip Polo Photos"
|
||||||
|
|
||||||
|
|
||||||
class DontSavePhotos(Toggle):
|
class DontSavePhotos(Toggle):
|
||||||
"""
|
"""Photos taken with the Camera app will not be saved.
|
||||||
Photos taken with the Camera app will not be saved.
|
This can be changed later in the options menu inside the Archipelago phone app."""
|
||||||
|
|
||||||
This can be changed later in the options menu inside the Archipelago phone app.
|
|
||||||
"""
|
|
||||||
display_name = "Don't Save Photos"
|
display_name = "Don't Save Photos"
|
||||||
|
|
||||||
|
|
||||||
class ScoreDifficulty(Choice):
|
class ScoreDifficulty(Choice):
|
||||||
"""
|
"""Alters the score required to win score challenges and crew battles.
|
||||||
Alters the score required to win score challenges and crew battles.
|
This can be changed later in the options menu inside the Archipelago phone app."""
|
||||||
|
|
||||||
This can be changed later in the options menu inside the Archipelago phone app.
|
|
||||||
"""
|
|
||||||
display_name = "Score Difficulty"
|
display_name = "Score Difficulty"
|
||||||
option_normal = 0
|
option_normal = 0
|
||||||
option_medium = 1
|
option_medium = 1
|
||||||
@@ -160,14 +129,10 @@ class ScoreDifficulty(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class DamageMultiplier(Range):
|
class DamageMultiplier(Range):
|
||||||
"""
|
"""Multiplies all damage received.
|
||||||
Multiplies all damage received.
|
|
||||||
|
|
||||||
At 3x, most damage will OHKO the player, including falling into pits.
|
At 3x, most damage will OHKO the player, including falling into pits.
|
||||||
At 6x, all damage will OHKO the player.
|
At 6x, all damage will OHKO the player.
|
||||||
|
This can be changed later in the options menu inside the Archipelago phone app."""
|
||||||
This can be changed later in the options menu inside the Archipelago phone app.
|
|
||||||
"""
|
|
||||||
display_name = "Damage Multiplier"
|
display_name = "Damage Multiplier"
|
||||||
range_start = 1
|
range_start = 1
|
||||||
range_end = 6
|
range_end = 6
|
||||||
@@ -175,11 +140,8 @@ class DamageMultiplier(Range):
|
|||||||
|
|
||||||
|
|
||||||
class BRCDeathLink(DeathLink):
|
class BRCDeathLink(DeathLink):
|
||||||
"""
|
"""When you die, everyone dies. The reverse is also true.
|
||||||
When you die, everyone dies. The reverse is also true.
|
This can be changed later in the options menu inside the Archipelago phone app."""
|
||||||
|
|
||||||
This can be changed later in the options menu inside the Archipelago phone app.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from Options import Choice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions
|
from Options import Choice, Range, Toggle, DeathLink, PerGameCommonOptions
|
||||||
|
|
||||||
|
|
||||||
class DeathLinkAmnesty(Range):
|
class DeathLinkAmnesty(Range):
|
||||||
@@ -47,9 +47,7 @@ class MoveShuffle(Toggle):
|
|||||||
- Air Dash
|
- Air Dash
|
||||||
- Skid Jump
|
- Skid Jump
|
||||||
- Climb
|
- Climb
|
||||||
|
|
||||||
NOTE: Having Move Shuffle and Standard Logic Difficulty will guarantee that one of the four Move items will be immediately accessible
|
NOTE: Having Move Shuffle and Standard Logic Difficulty will guarantee that one of the four Move items will be immediately accessible
|
||||||
|
|
||||||
WARNING: Combining Move Shuffle and Hard Logic Difficulty can require very difficult tricks
|
WARNING: Combining Move Shuffle and Hard Logic Difficulty can require very difficult tricks
|
||||||
"""
|
"""
|
||||||
display_name = "Move Shuffle"
|
display_name = "Move Shuffle"
|
||||||
@@ -77,9 +75,7 @@ class Carsanity(Toggle):
|
|||||||
class BadelineChaserSource(Choice):
|
class BadelineChaserSource(Choice):
|
||||||
"""
|
"""
|
||||||
What type of action causes more Badeline Chasers to start spawning
|
What type of action causes more Badeline Chasers to start spawning
|
||||||
|
|
||||||
Locations: The number of locations you've checked contributes to Badeline Chasers
|
Locations: The number of locations you've checked contributes to Badeline Chasers
|
||||||
|
|
||||||
Strawberries: The number of Strawberry items you've received contributes to Badeline Chasers
|
Strawberries: The number of Strawberry items you've received contributes to Badeline Chasers
|
||||||
"""
|
"""
|
||||||
display_name = "Badeline Chaser Source"
|
display_name = "Badeline Chaser Source"
|
||||||
@@ -90,9 +86,7 @@ class BadelineChaserSource(Choice):
|
|||||||
class BadelineChaserFrequency(Range):
|
class BadelineChaserFrequency(Range):
|
||||||
"""
|
"""
|
||||||
How many of the `Badeline Chaser Source` actions must occur to make each Badeline Chaser start spawning
|
How many of the `Badeline Chaser Source` actions must occur to make each Badeline Chaser start spawning
|
||||||
|
|
||||||
NOTE: Choosing `0` disables Badeline Chasers entirely
|
NOTE: Choosing `0` disables Badeline Chasers entirely
|
||||||
|
|
||||||
WARNING: Turning on Badeline Chasers alongside Move Shuffle could result in extremely difficult situations
|
WARNING: Turning on Badeline Chasers alongside Move Shuffle could result in extremely difficult situations
|
||||||
"""
|
"""
|
||||||
display_name = "Badeline Chaser Frequency"
|
display_name = "Badeline Chaser Frequency"
|
||||||
@@ -110,24 +104,6 @@ class BadelineChaserSpeed(Range):
|
|||||||
default = 3
|
default = 3
|
||||||
|
|
||||||
|
|
||||||
celeste_64_option_groups = [
|
|
||||||
OptionGroup("Goal Options", [
|
|
||||||
TotalStrawberries,
|
|
||||||
StrawberriesRequiredPercentage,
|
|
||||||
]),
|
|
||||||
OptionGroup("Sanity Options", [
|
|
||||||
Friendsanity,
|
|
||||||
Signsanity,
|
|
||||||
Carsanity,
|
|
||||||
]),
|
|
||||||
OptionGroup("Badeline Chasers", [
|
|
||||||
BadelineChaserSource,
|
|
||||||
BadelineChaserFrequency,
|
|
||||||
BadelineChaserSpeed,
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Celeste64Options(PerGameCommonOptions):
|
class Celeste64Options(PerGameCommonOptions):
|
||||||
death_link: DeathLink
|
death_link: DeathLink
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_tab
|
|||||||
from .Locations import Celeste64Location, strawberry_location_data_table, friend_location_data_table,\
|
from .Locations import Celeste64Location, strawberry_location_data_table, friend_location_data_table,\
|
||||||
sign_location_data_table, car_location_data_table, location_table
|
sign_location_data_table, car_location_data_table, location_table
|
||||||
from .Names import ItemName, LocationName
|
from .Names import ItemName, LocationName
|
||||||
from .Options import Celeste64Options, celeste_64_option_groups
|
from .Options import Celeste64Options
|
||||||
|
|
||||||
|
|
||||||
class Celeste64WebWorld(WebWorld):
|
class Celeste64WebWorld(WebWorld):
|
||||||
@@ -24,8 +24,6 @@ class Celeste64WebWorld(WebWorld):
|
|||||||
|
|
||||||
tutorials = [setup_en]
|
tutorials = [setup_en]
|
||||||
|
|
||||||
option_groups = celeste_64_option_groups
|
|
||||||
|
|
||||||
|
|
||||||
class Celeste64World(World):
|
class Celeste64World(World):
|
||||||
"""Relive the magic of Celeste Mountain alongside Madeline in this small, heartfelt 3D platformer.
|
"""Relive the magic of Celeste Mountain alongside Madeline in this small, heartfelt 3D platformer.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from BaseClasses import Item, Region, Tutorial, ItemClassification
|
|||||||
from .items import CV64Item, filler_item_names, get_item_info, get_item_names_to_ids, get_item_counts
|
from .items import CV64Item, filler_item_names, get_item_info, get_item_names_to_ids, get_item_counts
|
||||||
from .locations import CV64Location, get_location_info, verify_locations, get_location_names_to_ids, base_id
|
from .locations import CV64Location, get_location_info, verify_locations, get_location_names_to_ids, base_id
|
||||||
from .entrances import verify_entrances, get_warp_entrances
|
from .entrances import verify_entrances, get_warp_entrances
|
||||||
from .options import CV64Options, cv64_option_groups, CharacterStages, DraculasCondition, SubWeaponShuffle
|
from .options import CV64Options, CharacterStages, DraculasCondition, SubWeaponShuffle
|
||||||
from .stages import get_locations_from_stage, get_normal_stage_exits, vanilla_stage_order, \
|
from .stages import get_locations_from_stage, get_normal_stage_exits, vanilla_stage_order, \
|
||||||
shuffle_stages, generate_warps, get_region_names
|
shuffle_stages, generate_warps, get_region_names
|
||||||
from .regions import get_region_info
|
from .regions import get_region_info
|
||||||
@@ -45,8 +45,6 @@ class CV64Web(WebWorld):
|
|||||||
["Liquid Cat"]
|
["Liquid Cat"]
|
||||||
)]
|
)]
|
||||||
|
|
||||||
option_groups = cv64_option_groups
|
|
||||||
|
|
||||||
|
|
||||||
class CV64World(World):
|
class CV64World(World):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from Options import OptionGroup, Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool
|
from Options import Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool
|
||||||
|
|
||||||
|
|
||||||
class CharacterStages(Choice):
|
class CharacterStages(Choice):
|
||||||
"""
|
"""Whether to include Reinhardt-only stages, Carrie-only stages, or both with or without branching paths at the end
|
||||||
Whether to include Reinhardt-only stages, Carrie-only stages, or both with or without branching paths at the end of Villa and Castle Center.
|
of Villa and Castle Center."""
|
||||||
"""
|
|
||||||
display_name = "Character Stages"
|
display_name = "Character Stages"
|
||||||
option_both = 0
|
option_both = 0
|
||||||
option_branchless_both = 1
|
option_branchless_both = 1
|
||||||
@@ -15,18 +14,14 @@ class CharacterStages(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class StageShuffle(Toggle):
|
class StageShuffle(Toggle):
|
||||||
"""
|
"""Shuffles which stages appear in which stage slots. Villa and Castle Center will never appear in any character
|
||||||
Shuffles which stages appear in which stage slots.
|
stage slots if Character Stages is set to Both; they can only be somewhere on the main path.
|
||||||
Villa and Castle Center will never appear in any character stage slots if Character Stages is set to Both; they can only be somewhere on the main path.
|
Castle Keep will always be at the end of the line."""
|
||||||
Castle Keep will always be at the end of the line.
|
|
||||||
"""
|
|
||||||
display_name = "Stage Shuffle"
|
display_name = "Stage Shuffle"
|
||||||
|
|
||||||
|
|
||||||
class StartingStage(Choice):
|
class StartingStage(Choice):
|
||||||
"""
|
"""Which stage to start at if Stage Shuffle is turned on."""
|
||||||
Which stage to start at if Stage Shuffle is turned on.
|
|
||||||
"""
|
|
||||||
display_name = "Starting Stage"
|
display_name = "Starting Stage"
|
||||||
option_forest_of_silence = 0
|
option_forest_of_silence = 0
|
||||||
option_castle_wall = 1
|
option_castle_wall = 1
|
||||||
@@ -44,9 +39,8 @@ class StartingStage(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class WarpOrder(Choice):
|
class WarpOrder(Choice):
|
||||||
"""
|
"""Arranges the warps in the warp menu in whichever stage order chosen,
|
||||||
Arranges the warps in the warp menu in whichever stage order chosen, thereby changing the order they are unlocked in.
|
thereby changing the order they are unlocked in."""
|
||||||
"""
|
|
||||||
display_name = "Warp Order"
|
display_name = "Warp Order"
|
||||||
option_seed_stage_order = 0
|
option_seed_stage_order = 0
|
||||||
option_vanilla_stage_order = 1
|
option_vanilla_stage_order = 1
|
||||||
@@ -55,9 +49,7 @@ class WarpOrder(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class SubWeaponShuffle(Choice):
|
class SubWeaponShuffle(Choice):
|
||||||
"""
|
"""Shuffles all sub-weapons in the game within each other in their own pool or in the main item pool."""
|
||||||
Shuffles all sub-weapons in the game within each other in their own pool or in the main item pool.
|
|
||||||
"""
|
|
||||||
display_name = "Sub-weapon Shuffle"
|
display_name = "Sub-weapon Shuffle"
|
||||||
option_off = 0
|
option_off = 0
|
||||||
option_own_pool = 1
|
option_own_pool = 1
|
||||||
@@ -66,10 +58,8 @@ class SubWeaponShuffle(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class SpareKeys(Choice):
|
class SpareKeys(Choice):
|
||||||
"""
|
"""Puts an additional copy of every non-Special key item in the pool for every key item that there is.
|
||||||
Puts an additional copy of every non-Special key item in the pool for every key item that there is.
|
Chance gives each key item a 50% chance of having a duplicate instead of guaranteeing one for all of them."""
|
||||||
Chance gives each key item a 50% chance of having a duplicate instead of guaranteeing one for all of them.
|
|
||||||
"""
|
|
||||||
display_name = "Spare Keys"
|
display_name = "Spare Keys"
|
||||||
option_off = 0
|
option_off = 0
|
||||||
option_on = 1
|
option_on = 1
|
||||||
@@ -78,17 +68,14 @@ class SpareKeys(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class HardItemPool(Toggle):
|
class HardItemPool(Toggle):
|
||||||
"""
|
"""Replaces some items in the item pool with less valuable ones, to make the item pool sort of resemble Hard Mode
|
||||||
Replaces some items in the item pool with less valuable ones, to make the item pool sort of resemble Hard Mode in the PAL version.
|
in the PAL version."""
|
||||||
"""
|
|
||||||
display_name = "Hard Item Pool"
|
display_name = "Hard Item Pool"
|
||||||
|
|
||||||
|
|
||||||
class Special1sPerWarp(Range):
|
class Special1sPerWarp(Range):
|
||||||
"""
|
"""Sets how many Special1 jewels are needed per warp menu option unlock.
|
||||||
Sets how many Special1 jewels are needed per warp menu option unlock.
|
This will decrease until the number x 7 is less than or equal to the Total Specail1s if it isn't already."""
|
||||||
This will decrease until the number x 7 is less than or equal to the Total Specail1s if it isn't already.
|
|
||||||
"""
|
|
||||||
range_start = 1
|
range_start = 1
|
||||||
range_end = 10
|
range_end = 10
|
||||||
default = 1
|
default = 1
|
||||||
@@ -96,9 +83,7 @@ class Special1sPerWarp(Range):
|
|||||||
|
|
||||||
|
|
||||||
class TotalSpecial1s(Range):
|
class TotalSpecial1s(Range):
|
||||||
"""
|
"""Sets how many Speical1 jewels are in the pool in total."""
|
||||||
Sets how many Speical1 jewels are in the pool in total.
|
|
||||||
"""
|
|
||||||
range_start = 7
|
range_start = 7
|
||||||
range_end = 70
|
range_end = 70
|
||||||
default = 7
|
default = 7
|
||||||
@@ -106,13 +91,11 @@ class TotalSpecial1s(Range):
|
|||||||
|
|
||||||
|
|
||||||
class DraculasCondition(Choice):
|
class DraculasCondition(Choice):
|
||||||
"""
|
"""Sets the requirement for unlocking and opening the door to Dracula's chamber.
|
||||||
Sets the requirement for unlocking and opening the door to Dracula's chamber.
|
|
||||||
None: No requirement. Door is unlocked from the start.
|
None: No requirement. Door is unlocked from the start.
|
||||||
Crystal: Activate the big crystal in Castle Center's basement. Neither boss afterwards has to be defeated.
|
Crystal: Activate the big crystal in Castle Center's basement. Neither boss afterwards has to be defeated.
|
||||||
Bosses: Kill a specified number of bosses with health bars and claim their Trophies.
|
Bosses: Kill a specified number of bosses with health bars and claim their Trophies.
|
||||||
Specials: Find a specified number of Special2 jewels shuffled in the main item pool.
|
Specials: Find a specified number of Special2 jewels shuffled in the main item pool."""
|
||||||
"""
|
|
||||||
display_name = "Dracula's Condition"
|
display_name = "Dracula's Condition"
|
||||||
option_none = 0
|
option_none = 0
|
||||||
option_crystal = 1
|
option_crystal = 1
|
||||||
@@ -122,9 +105,7 @@ class DraculasCondition(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class PercentSpecial2sRequired(Range):
|
class PercentSpecial2sRequired(Range):
|
||||||
"""
|
"""Percentage of Special2s required to enter Dracula's chamber when Dracula's Condition is Special2s."""
|
||||||
Percentage of Special2s required to enter Dracula's chamber when Dracula's Condition is Special2s.
|
|
||||||
"""
|
|
||||||
range_start = 1
|
range_start = 1
|
||||||
range_end = 100
|
range_end = 100
|
||||||
default = 80
|
default = 80
|
||||||
@@ -132,9 +113,7 @@ class PercentSpecial2sRequired(Range):
|
|||||||
|
|
||||||
|
|
||||||
class TotalSpecial2s(Range):
|
class TotalSpecial2s(Range):
|
||||||
"""
|
"""How many Speical2 jewels are in the pool in total when Dracula's Condition is Special2s."""
|
||||||
How many Speical2 jewels are in the pool in total when Dracula's Condition is Special2s.
|
|
||||||
"""
|
|
||||||
range_start = 1
|
range_start = 1
|
||||||
range_end = 70
|
range_end = 70
|
||||||
default = 25
|
default = 25
|
||||||
@@ -142,70 +121,58 @@ class TotalSpecial2s(Range):
|
|||||||
|
|
||||||
|
|
||||||
class BossesRequired(Range):
|
class BossesRequired(Range):
|
||||||
"""
|
"""How many bosses need to be defeated to enter Dracula's chamber when Dracula's Condition is set to Bosses.
|
||||||
How many bosses need to be defeated to enter Dracula's chamber when Dracula's Condition is set to Bosses.
|
This will automatically adjust if there are fewer available bosses than the chosen number."""
|
||||||
This will automatically adjust if there are fewer available bosses than the chosen number.
|
|
||||||
"""
|
|
||||||
range_start = 1
|
range_start = 1
|
||||||
range_end = 16
|
range_end = 16
|
||||||
default = 12
|
default = 14
|
||||||
display_name = "Bosses Required"
|
display_name = "Bosses Required"
|
||||||
|
|
||||||
|
|
||||||
class CarrieLogic(Toggle):
|
class CarrieLogic(Toggle):
|
||||||
"""
|
"""Adds the 2 checks inside Underground Waterway's crawlspace to the pool.
|
||||||
Adds the 2 checks inside Underground Waterway's crawlspace to the pool.
|
|
||||||
If you (and everyone else if racing the same seed) are planning to only ever play Reinhardt, don't enable this.
|
If you (and everyone else if racing the same seed) are planning to only ever play Reinhardt, don't enable this.
|
||||||
Can be combined with Hard Logic to include Carrie-only tricks.
|
Can be combined with Hard Logic to include Carrie-only tricks."""
|
||||||
"""
|
|
||||||
display_name = "Carrie Logic"
|
display_name = "Carrie Logic"
|
||||||
|
|
||||||
|
|
||||||
class HardLogic(Toggle):
|
class HardLogic(Toggle):
|
||||||
"""
|
"""Properly considers sequence break tricks in logic (i.e. maze skip). Can be combined with Carrie Logic to include
|
||||||
Properly considers sequence break tricks in logic (i.e. maze skip). Can be combined with Carrie Logic to include Carrie-only tricks.
|
Carrie-only tricks.
|
||||||
See the Game Page for a full list of tricks and glitches that may be logically required.
|
See the Game Page for a full list of tricks and glitches that may be logically required."""
|
||||||
"""
|
|
||||||
display_name = "Hard Logic"
|
display_name = "Hard Logic"
|
||||||
|
|
||||||
|
|
||||||
class MultiHitBreakables(Toggle):
|
class MultiHitBreakables(Toggle):
|
||||||
"""
|
"""Adds the items that drop from the objects that break in three hits to the pool. There are 18 of these throughout
|
||||||
Adds the items that drop from the objects that break in three hits to the pool.
|
the game, adding up to 79 or 80 checks (depending on sub-weapons
|
||||||
There are 18 of these throughout the game, adding up to 79 or 80 checks (depending on sub-weapons being shuffled anywhere or not) in total with all stages.
|
being shuffled anywhere or not) in total with all stages.
|
||||||
The game will be modified to remember exactly which of their items you've picked up instead of simply whether they were broken or not.
|
The game will be modified to
|
||||||
"""
|
remember exactly which of their items you've picked up instead of simply whether they were broken or not."""
|
||||||
display_name = "Multi-hit Breakables"
|
display_name = "Multi-hit Breakables"
|
||||||
|
|
||||||
|
|
||||||
class EmptyBreakables(Toggle):
|
class EmptyBreakables(Toggle):
|
||||||
"""
|
"""Adds 9 check locations in the form of breakables that normally have nothing (all empty Forest coffins, etc.)
|
||||||
Adds 9 check locations in the form of breakables that normally have nothing (all empty Forest coffins, etc.) and some additional Red Jewels and/or moneybags into the item pool to compensate.
|
and some additional Red Jewels and/or moneybags into the item pool to compensate."""
|
||||||
"""
|
|
||||||
display_name = "Empty Breakables"
|
display_name = "Empty Breakables"
|
||||||
|
|
||||||
|
|
||||||
class LizardLockerItems(Toggle):
|
class LizardLockerItems(Toggle):
|
||||||
"""
|
"""Adds the 6 items inside Castle Center 2F's Lizard-man generators to the pool.
|
||||||
Adds the 6 items inside Castle Center 2F's Lizard-man generators to the pool.
|
Picking up all of these can be a very tedious luck-based process, so they are off by default."""
|
||||||
Picking up all of these can be a very tedious luck-based process, so they are off by default.
|
|
||||||
"""
|
|
||||||
display_name = "Lizard Locker Items"
|
display_name = "Lizard Locker Items"
|
||||||
|
|
||||||
|
|
||||||
class Shopsanity(Toggle):
|
class Shopsanity(Toggle):
|
||||||
"""
|
"""Adds 7 one-time purchases from Renon's shop into the location pool. After buying an item from a slot, it will
|
||||||
Adds 7 one-time purchases from Renon's shop into the location pool.
|
revert to whatever it is in the vanilla game."""
|
||||||
After buying an item from a slot, it will revert to whatever it is in the vanilla game.
|
|
||||||
"""
|
|
||||||
display_name = "Shopsanity"
|
display_name = "Shopsanity"
|
||||||
|
|
||||||
|
|
||||||
class ShopPrices(Choice):
|
class ShopPrices(Choice):
|
||||||
"""
|
"""Randomizes the amount of gold each item costs in Renon's shop.
|
||||||
Randomizes the amount of gold each item costs in Renon's shop.
|
Use the below options to control how much or little an item can cost."""
|
||||||
Use the Minimum and Maximum Gold Price options to control how much or how little an item can cost.
|
|
||||||
"""
|
|
||||||
display_name = "Shop Prices"
|
display_name = "Shop Prices"
|
||||||
option_vanilla = 0
|
option_vanilla = 0
|
||||||
option_randomized = 1
|
option_randomized = 1
|
||||||
@@ -213,9 +180,7 @@ class ShopPrices(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class MinimumGoldPrice(Range):
|
class MinimumGoldPrice(Range):
|
||||||
"""
|
"""The lowest amount of gold an item can cost in Renon's shop, divided by 100."""
|
||||||
The lowest amount of gold an item can cost in Renon's shop, divided by 100.
|
|
||||||
"""
|
|
||||||
display_name = "Minimum Gold Price"
|
display_name = "Minimum Gold Price"
|
||||||
range_start = 1
|
range_start = 1
|
||||||
range_end = 50
|
range_end = 50
|
||||||
@@ -223,9 +188,7 @@ class MinimumGoldPrice(Range):
|
|||||||
|
|
||||||
|
|
||||||
class MaximumGoldPrice(Range):
|
class MaximumGoldPrice(Range):
|
||||||
"""
|
"""The highest amount of gold an item can cost in Renon's shop, divided by 100."""
|
||||||
The highest amount of gold an item can cost in Renon's shop, divided by 100.
|
|
||||||
"""
|
|
||||||
display_name = "Maximum Gold Price"
|
display_name = "Maximum Gold Price"
|
||||||
range_start = 1
|
range_start = 1
|
||||||
range_end = 50
|
range_end = 50
|
||||||
@@ -233,9 +196,8 @@ class MaximumGoldPrice(Range):
|
|||||||
|
|
||||||
|
|
||||||
class PostBehemothBoss(Choice):
|
class PostBehemothBoss(Choice):
|
||||||
"""
|
"""Sets which boss is fought in the vampire triplets' room in Castle Center by which characters after defeating
|
||||||
Sets which boss is fought in the vampire triplets' room in Castle Center by which characters after defeating Behemoth.
|
Behemoth."""
|
||||||
"""
|
|
||||||
display_name = "Post-Behemoth Boss"
|
display_name = "Post-Behemoth Boss"
|
||||||
option_vanilla = 0
|
option_vanilla = 0
|
||||||
option_inverted = 1
|
option_inverted = 1
|
||||||
@@ -245,9 +207,7 @@ class PostBehemothBoss(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class RoomOfClocksBoss(Choice):
|
class RoomOfClocksBoss(Choice):
|
||||||
"""
|
"""Sets which boss is fought at Room of Clocks by which characters."""
|
||||||
Sets which boss is fought at Room of Clocks by which characters.
|
|
||||||
"""
|
|
||||||
display_name = "Room of Clocks Boss"
|
display_name = "Room of Clocks Boss"
|
||||||
option_vanilla = 0
|
option_vanilla = 0
|
||||||
option_inverted = 1
|
option_inverted = 1
|
||||||
@@ -257,9 +217,7 @@ class RoomOfClocksBoss(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class RenonFightCondition(Choice):
|
class RenonFightCondition(Choice):
|
||||||
"""
|
"""Sets the condition on which the Renon fight will trigger."""
|
||||||
Sets the condition on which the Renon fight will trigger.
|
|
||||||
"""
|
|
||||||
display_name = "Renon Fight Condition"
|
display_name = "Renon Fight Condition"
|
||||||
option_never = 0
|
option_never = 0
|
||||||
option_spend_30k = 1
|
option_spend_30k = 1
|
||||||
@@ -268,9 +226,7 @@ class RenonFightCondition(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class VincentFightCondition(Choice):
|
class VincentFightCondition(Choice):
|
||||||
"""
|
"""Sets the condition on which the vampire Vincent fight will trigger."""
|
||||||
Sets the condition on which the vampire Vincent fight will trigger.
|
|
||||||
"""
|
|
||||||
display_name = "Vincent Fight Condition"
|
display_name = "Vincent Fight Condition"
|
||||||
option_never = 0
|
option_never = 0
|
||||||
option_wait_16_days = 1
|
option_wait_16_days = 1
|
||||||
@@ -279,9 +235,7 @@ class VincentFightCondition(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class BadEndingCondition(Choice):
|
class BadEndingCondition(Choice):
|
||||||
"""
|
"""Sets the condition on which the currently-controlled character's Bad Ending will trigger."""
|
||||||
Sets the condition on which the currently-controlled character's Bad Ending will trigger.
|
|
||||||
"""
|
|
||||||
display_name = "Bad Ending Condition"
|
display_name = "Bad Ending Condition"
|
||||||
option_never = 0
|
option_never = 0
|
||||||
option_kill_vincent = 1
|
option_kill_vincent = 1
|
||||||
@@ -290,32 +244,24 @@ class BadEndingCondition(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class IncreaseItemLimit(DefaultOnToggle):
|
class IncreaseItemLimit(DefaultOnToggle):
|
||||||
"""
|
"""Increases the holding limit of usable items from 10 to 99 of each item."""
|
||||||
Increases the holding limit of usable items from 10 to 99 of each item.
|
|
||||||
"""
|
|
||||||
display_name = "Increase Item Limit"
|
display_name = "Increase Item Limit"
|
||||||
|
|
||||||
|
|
||||||
class NerfHealingItems(Toggle):
|
class NerfHealingItems(Toggle):
|
||||||
"""
|
"""Decreases the amount of health healed by Roast Chickens to 25%, Roast Beefs to 50%, and Healing Kits to 80%."""
|
||||||
Decreases the amount of health healed by Roast Chickens to 25%, Roast Beefs to 50%, and Healing Kits to 80%.
|
|
||||||
"""
|
|
||||||
display_name = "Nerf Healing Items"
|
display_name = "Nerf Healing Items"
|
||||||
|
|
||||||
|
|
||||||
class LoadingZoneHeals(DefaultOnToggle):
|
class LoadingZoneHeals(DefaultOnToggle):
|
||||||
"""
|
"""Whether end-of-level loading zones restore health and cure status aliments or not.
|
||||||
Whether end-of-level loading zones restore health and cure status aliments or not.
|
Recommended off for those looking for more of a survival horror experience!"""
|
||||||
Recommended off for those looking for more of a survival horror experience!
|
|
||||||
"""
|
|
||||||
display_name = "Loading Zone Heals"
|
display_name = "Loading Zone Heals"
|
||||||
|
|
||||||
|
|
||||||
class InvisibleItems(Choice):
|
class InvisibleItems(Choice):
|
||||||
"""
|
"""Sets which items are visible in their locations and which are invisible until picked up.
|
||||||
Sets which items are visible in their locations and which are invisible until picked up.
|
'Chance' gives each item a 50/50 chance of being visible or invisible."""
|
||||||
'Chance' gives each item a 50/50 chance of being visible or invisible.
|
|
||||||
"""
|
|
||||||
display_name = "Invisible Items"
|
display_name = "Invisible Items"
|
||||||
option_vanilla = 0
|
option_vanilla = 0
|
||||||
option_reveal_all = 1
|
option_reveal_all = 1
|
||||||
@@ -325,25 +271,21 @@ class InvisibleItems(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class DropPreviousSubWeapon(Toggle):
|
class DropPreviousSubWeapon(Toggle):
|
||||||
"""
|
"""When receiving a sub-weapon, the one you had before will drop behind you, so it can be taken back if desired."""
|
||||||
When receiving a sub-weapon, the one you had before will drop behind you, so it can be taken back if desired.
|
|
||||||
"""
|
|
||||||
display_name = "Drop Previous Sub-weapon"
|
display_name = "Drop Previous Sub-weapon"
|
||||||
|
|
||||||
|
|
||||||
class PermanentPowerUps(Toggle):
|
class PermanentPowerUps(Toggle):
|
||||||
"""
|
"""Replaces PowerUps with PermaUps, which upgrade your B weapon level permanently and will stay even after
|
||||||
Replaces PowerUps with PermaUps, which upgrade your B weapon level permanently and will stay even after dying and/or continuing.
|
dying and/or continuing.
|
||||||
To compensate, only two will be in the pool overall, and they will not drop from any enemy or projectile.
|
To compensate, only two will be in the pool overall, and they will not drop from any enemy or projectile."""
|
||||||
"""
|
|
||||||
display_name = "Permanent PowerUps"
|
display_name = "Permanent PowerUps"
|
||||||
|
|
||||||
|
|
||||||
class IceTrapPercentage(Range):
|
class IceTrapPercentage(Range):
|
||||||
"""
|
"""Replaces a percentage of junk items with Ice Traps.
|
||||||
Replaces a percentage of junk items with Ice Traps.
|
These will be visibly disguised as other items, and receiving one will freeze you
|
||||||
These will be visibly disguised as other items, and receiving one will freeze you as if you were hit by Camilla's ice cloud attack.
|
as if you were hit by Camilla's ice cloud attack."""
|
||||||
"""
|
|
||||||
display_name = "Ice Trap Percentage"
|
display_name = "Ice Trap Percentage"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 100
|
range_end = 100
|
||||||
@@ -351,9 +293,7 @@ class IceTrapPercentage(Range):
|
|||||||
|
|
||||||
|
|
||||||
class IceTrapAppearance(Choice):
|
class IceTrapAppearance(Choice):
|
||||||
"""
|
"""What items Ice Traps can possibly be disguised as."""
|
||||||
What items Ice Traps can possibly be disguised as.
|
|
||||||
"""
|
|
||||||
display_name = "Ice Trap Appearance"
|
display_name = "Ice Trap Appearance"
|
||||||
option_major_only = 0
|
option_major_only = 0
|
||||||
option_junk_only = 1
|
option_junk_only = 1
|
||||||
@@ -362,34 +302,31 @@ class IceTrapAppearance(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class DisableTimeRestrictions(Toggle):
|
class DisableTimeRestrictions(Toggle):
|
||||||
"""
|
"""Disables the restriction on every event and door that requires the current time
|
||||||
Disables the restriction on every event and door that requires the current time to be within a specific range, so they can be triggered at any time.
|
to be within a specific range, so they can be triggered at any time.
|
||||||
This includes all sun/moon doors and, in the Villa, the meeting with Rosa and the fountain pillar.
|
This includes all sun/moon doors and, in the Villa, the meeting with Rosa and the fountain pillar.
|
||||||
The Villa coffin is not affected by this.
|
The Villa coffin is not affected by this."""
|
||||||
"""
|
|
||||||
display_name = "Disable Time Requirements"
|
display_name = "Disable Time Requirements"
|
||||||
|
|
||||||
|
|
||||||
class SkipGondolas(Toggle):
|
class SkipGondolas(Toggle):
|
||||||
"""
|
"""Makes jumping on and activating a gondola in Tunnel instantly teleport you
|
||||||
Makes jumping on and activating a gondola in Tunnel instantly teleport you to the other station, thereby skipping the entire three-minute ride.
|
to the other station, thereby skipping the entire three-minute ride.
|
||||||
The item normally at the gondola transfer point is moved to instead be near the red gondola at its station.
|
The item normally at the gondola transfer point is moved to instead be
|
||||||
"""
|
near the red gondola at its station."""
|
||||||
display_name = "Skip Gondolas"
|
display_name = "Skip Gondolas"
|
||||||
|
|
||||||
|
|
||||||
class SkipWaterwayBlocks(Toggle):
|
class SkipWaterwayBlocks(Toggle):
|
||||||
"""
|
"""Opens the door to the third switch in Underground Waterway from the start so that the jumping across floating
|
||||||
Opens the door to the third switch in Underground Waterway from the start so that the jumping across floating brick platforms won't have to be done.
|
brick platforms won't have to be done. Shopping at the Contract on the other side of them may still be logically
|
||||||
Shopping at the Contract on the other side of them may still be logically required if Shopsanity is on.
|
required if Shopsanity is on."""
|
||||||
"""
|
|
||||||
display_name = "Skip Waterway Blocks"
|
display_name = "Skip Waterway Blocks"
|
||||||
|
|
||||||
|
|
||||||
class Countdown(Choice):
|
class Countdown(Choice):
|
||||||
"""
|
"""Displays, near the HUD clock and below the health bar, the number of unobtained progression-marked items
|
||||||
Displays, near the HUD clock and below the health bar, the number of unobtained progression-marked items or the total check locations remaining in the stage you are currently in.
|
or the total check locations remaining in the stage you are currently in."""
|
||||||
"""
|
|
||||||
display_name = "Countdown"
|
display_name = "Countdown"
|
||||||
option_none = 0
|
option_none = 0
|
||||||
option_majors = 1
|
option_majors = 1
|
||||||
@@ -398,21 +335,19 @@ class Countdown(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class BigToss(Toggle):
|
class BigToss(Toggle):
|
||||||
"""
|
"""Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge.
|
||||||
Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge.
|
|
||||||
Press A while tossed to cancel the launch momentum and avoid being thrown off ledges.
|
Press A while tossed to cancel the launch momentum and avoid being thrown off ledges.
|
||||||
Hold Z to have all incoming damage be treated as it normally would.
|
Hold Z to have all incoming damage be treated as it normally would.
|
||||||
Any tricks that might be possible with it are not in logic.
|
Any tricks that might be possible with it are NOT considered in logic by any options."""
|
||||||
"""
|
|
||||||
display_name = "Big Toss"
|
display_name = "Big Toss"
|
||||||
|
|
||||||
|
|
||||||
class PantherDash(Choice):
|
class PantherDash(Choice):
|
||||||
"""
|
"""Hold C-right at any time to sprint way faster. Any tricks that might be
|
||||||
Hold C-right at any time to sprint way faster.
|
possible with it are NOT considered in logic by any options and any boss
|
||||||
Any tricks that are possible with it are not in logic and any boss fights with boss health meters, if started, are expected to be finished before leaving their arenas if Dracula's Condition is bosses.
|
fights with boss health meters, if started, are expected to be finished
|
||||||
Jumpless will prevent jumping while moving at the increased speed to make logic harder to break with it.
|
before leaving their arenas if Dracula's Condition is bosses. Jumpless will
|
||||||
"""
|
prevent jumping while moving at the increased speed to ensure logic cannot be broken with it."""
|
||||||
display_name = "Panther Dash"
|
display_name = "Panther Dash"
|
||||||
option_off = 0
|
option_off = 0
|
||||||
option_on = 1
|
option_on = 1
|
||||||
@@ -421,25 +356,19 @@ class PantherDash(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class IncreaseShimmySpeed(Toggle):
|
class IncreaseShimmySpeed(Toggle):
|
||||||
"""
|
"""Increases the speed at which characters shimmy left and right while hanging on ledges."""
|
||||||
Increases the speed at which characters shimmy left and right while hanging on ledges.
|
|
||||||
"""
|
|
||||||
display_name = "Increase Shimmy Speed"
|
display_name = "Increase Shimmy Speed"
|
||||||
|
|
||||||
|
|
||||||
class FallGuard(Toggle):
|
class FallGuard(Toggle):
|
||||||
"""
|
"""Removes fall damage from landing too hard. Note that falling for too long will still result in instant death."""
|
||||||
Removes fall damage from landing too hard. Note that falling for too long will still result in instant death.
|
|
||||||
"""
|
|
||||||
display_name = "Fall Guard"
|
display_name = "Fall Guard"
|
||||||
|
|
||||||
|
|
||||||
class BackgroundMusic(Choice):
|
class BackgroundMusic(Choice):
|
||||||
"""
|
"""Randomizes or disables the music heard throughout the game.
|
||||||
Randomizes or disables the music heard throughout the game.
|
|
||||||
Randomized music is split into two pools: songs that loop and songs that don't.
|
Randomized music is split into two pools: songs that loop and songs that don't.
|
||||||
The "lead-in" versions of some songs will be paired accordingly.
|
The "lead-in" versions of some songs will be paired accordingly."""
|
||||||
"""
|
|
||||||
display_name = "Background Music"
|
display_name = "Background Music"
|
||||||
option_normal = 0
|
option_normal = 0
|
||||||
option_disabled = 1
|
option_disabled = 1
|
||||||
@@ -448,10 +377,8 @@ class BackgroundMusic(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class MapLighting(Choice):
|
class MapLighting(Choice):
|
||||||
"""
|
"""Randomizes the lighting color RGB values on every map during every time of day to be literally anything.
|
||||||
Randomizes the lighting color RGB values on every map during every time of day to be literally anything.
|
The colors and/or shading of the following things are affected: fog, maps, player, enemies, and some objects."""
|
||||||
The colors and/or shading of the following things are affected: fog, maps, player, enemies, and some objects.
|
|
||||||
"""
|
|
||||||
display_name = "Map Lighting"
|
display_name = "Map Lighting"
|
||||||
option_normal = 0
|
option_normal = 0
|
||||||
option_randomized = 1
|
option_randomized = 1
|
||||||
@@ -459,16 +386,12 @@ class MapLighting(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class CinematicExperience(Toggle):
|
class CinematicExperience(Toggle):
|
||||||
"""
|
"""Enables an unused film reel effect on every cutscene in the game. Purely cosmetic."""
|
||||||
Enables an unused film reel effect on every cutscene in the game. Purely cosmetic.
|
|
||||||
"""
|
|
||||||
display_name = "Cinematic Experience"
|
display_name = "Cinematic Experience"
|
||||||
|
|
||||||
|
|
||||||
class WindowColorR(Range):
|
class WindowColorR(Range):
|
||||||
"""
|
"""The red value for the background color of the text windows during gameplay."""
|
||||||
The red value for the background color of the text windows during gameplay.
|
|
||||||
"""
|
|
||||||
display_name = "Window Color R"
|
display_name = "Window Color R"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 15
|
range_end = 15
|
||||||
@@ -476,9 +399,7 @@ class WindowColorR(Range):
|
|||||||
|
|
||||||
|
|
||||||
class WindowColorG(Range):
|
class WindowColorG(Range):
|
||||||
"""
|
"""The green value for the background color of the text windows during gameplay."""
|
||||||
The green value for the background color of the text windows during gameplay.
|
|
||||||
"""
|
|
||||||
display_name = "Window Color G"
|
display_name = "Window Color G"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 15
|
range_end = 15
|
||||||
@@ -486,9 +407,7 @@ class WindowColorG(Range):
|
|||||||
|
|
||||||
|
|
||||||
class WindowColorB(Range):
|
class WindowColorB(Range):
|
||||||
"""
|
"""The blue value for the background color of the text windows during gameplay."""
|
||||||
The blue value for the background color of the text windows during gameplay.
|
|
||||||
"""
|
|
||||||
display_name = "Window Color B"
|
display_name = "Window Color B"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 15
|
range_end = 15
|
||||||
@@ -496,9 +415,7 @@ class WindowColorB(Range):
|
|||||||
|
|
||||||
|
|
||||||
class WindowColorA(Range):
|
class WindowColorA(Range):
|
||||||
"""
|
"""The alpha value for the background color of the text windows during gameplay."""
|
||||||
The alpha value for the background color of the text windows during gameplay.
|
|
||||||
"""
|
|
||||||
display_name = "Window Color A"
|
display_name = "Window Color A"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 15
|
range_end = 15
|
||||||
@@ -506,10 +423,9 @@ class WindowColorA(Range):
|
|||||||
|
|
||||||
|
|
||||||
class DeathLink(Choice):
|
class DeathLink(Choice):
|
||||||
"""
|
"""When you die, everyone dies. Of course the reverse is true too.
|
||||||
When you die, everyone dies. Of course the reverse is true too.
|
Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion
|
||||||
Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion instead of the normal death animation.
|
instead of the normal death animation."""
|
||||||
"""
|
|
||||||
display_name = "DeathLink"
|
display_name = "DeathLink"
|
||||||
option_off = 0
|
option_off = 0
|
||||||
alias_no = 0
|
alias_no = 0
|
||||||
@@ -521,7 +437,6 @@ class DeathLink(Choice):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CV64Options(PerGameCommonOptions):
|
class CV64Options(PerGameCommonOptions):
|
||||||
start_inventory_from_pool: StartInventoryPool
|
|
||||||
character_stages: CharacterStages
|
character_stages: CharacterStages
|
||||||
stage_shuffle: StageShuffle
|
stage_shuffle: StageShuffle
|
||||||
starting_stage: StartingStage
|
starting_stage: StartingStage
|
||||||
@@ -564,26 +479,13 @@ class CV64Options(PerGameCommonOptions):
|
|||||||
big_toss: BigToss
|
big_toss: BigToss
|
||||||
panther_dash: PantherDash
|
panther_dash: PantherDash
|
||||||
increase_shimmy_speed: IncreaseShimmySpeed
|
increase_shimmy_speed: IncreaseShimmySpeed
|
||||||
window_color_r: WindowColorR
|
|
||||||
window_color_g: WindowColorG
|
|
||||||
window_color_b: WindowColorB
|
|
||||||
window_color_a: WindowColorA
|
|
||||||
background_music: BackgroundMusic
|
background_music: BackgroundMusic
|
||||||
map_lighting: MapLighting
|
map_lighting: MapLighting
|
||||||
fall_guard: FallGuard
|
fall_guard: FallGuard
|
||||||
cinematic_experience: CinematicExperience
|
cinematic_experience: CinematicExperience
|
||||||
|
window_color_r: WindowColorR
|
||||||
|
window_color_g: WindowColorG
|
||||||
|
window_color_b: WindowColorB
|
||||||
|
window_color_a: WindowColorA
|
||||||
death_link: DeathLink
|
death_link: DeathLink
|
||||||
|
start_inventory_from_pool: StartInventoryPool
|
||||||
|
|
||||||
cv64_option_groups = [
|
|
||||||
OptionGroup("gameplay tweaks", [
|
|
||||||
HardItemPool, ShopPrices, MinimumGoldPrice, MaximumGoldPrice, PostBehemothBoss, RoomOfClocksBoss,
|
|
||||||
RenonFightCondition, VincentFightCondition, BadEndingCondition, IncreaseItemLimit, NerfHealingItems,
|
|
||||||
LoadingZoneHeals, InvisibleItems, DropPreviousSubWeapon, PermanentPowerUps, IceTrapPercentage,
|
|
||||||
IceTrapAppearance, DisableTimeRestrictions, SkipGondolas, SkipWaterwayBlocks, Countdown, BigToss, PantherDash,
|
|
||||||
IncreaseShimmySpeed, FallGuard, DeathLink
|
|
||||||
]),
|
|
||||||
OptionGroup("cosmetics", [
|
|
||||||
WindowColorR, WindowColorG, WindowColorB, WindowColorA, BackgroundMusic, MapLighting, CinematicExperience
|
|
||||||
])
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1272,7 +1272,11 @@ _cut_content_items = [DS3ItemData(row[0], row[1], False, row[2]) for row in [
|
|||||||
]]
|
]]
|
||||||
|
|
||||||
item_descriptions = {
|
item_descriptions = {
|
||||||
"Cinders": "All four Cinders of a Lord.\n\nOnce you have these four, you can fight Soul of Cinder and win the game.",
|
"Cinders": """
|
||||||
|
All four Cinders of a Lord.
|
||||||
|
|
||||||
|
Once you have these four, you can fight Soul of Cinder and win the game.
|
||||||
|
""",
|
||||||
}
|
}
|
||||||
|
|
||||||
_all_items = _vanilla_items + _dlc_items
|
_all_items = _vanilla_items + _dlc_items
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ class DarkSouls3Web(WebWorld):
|
|||||||
|
|
||||||
tutorials = [setup_en, setup_fr]
|
tutorials = [setup_en, setup_fr]
|
||||||
|
|
||||||
item_descriptions = item_descriptions
|
|
||||||
|
|
||||||
|
|
||||||
class DarkSouls3World(World):
|
class DarkSouls3World(World):
|
||||||
"""
|
"""
|
||||||
@@ -63,6 +61,8 @@ class DarkSouls3World(World):
|
|||||||
"Cinders of a Lord - Lothric Prince"
|
"Cinders of a Lord - Lothric Prince"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
item_descriptions = item_descriptions
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, multiworld: MultiWorld, player: int):
|
def __init__(self, multiworld: MultiWorld, player: int):
|
||||||
super().__init__(multiworld, player)
|
super().__init__(multiworld, player)
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
|
from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList, PerGameCommonOptions
|
||||||
|
|
||||||
|
|
||||||
class Goal(Choice):
|
class Goal(Choice):
|
||||||
"""
|
"""
|
||||||
Determines the goal of the seed
|
Determines the goal of the seed
|
||||||
|
|
||||||
Knautilus: Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein
|
Knautilus: Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein
|
||||||
|
|
||||||
Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother
|
Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother
|
||||||
"""
|
"""
|
||||||
display_name = "Goal"
|
display_name = "Goal"
|
||||||
@@ -28,7 +26,6 @@ class IncludeTradeSequence(Toggle):
|
|||||||
class DKCoinsForGyrocopter(Range):
|
class DKCoinsForGyrocopter(Range):
|
||||||
"""
|
"""
|
||||||
How many DK Coins are needed to unlock the Gyrocopter
|
How many DK Coins are needed to unlock the Gyrocopter
|
||||||
|
|
||||||
Note: Achieving this number before unlocking the Turbo Ski will cause the game to grant you a
|
Note: Achieving this number before unlocking the Turbo Ski will cause the game to grant you a
|
||||||
one-time upgrade to the next non-unlocked boat, until you return to Funky. Logic does not assume
|
one-time upgrade to the next non-unlocked boat, until you return to Funky. Logic does not assume
|
||||||
that you will use this.
|
that you will use this.
|
||||||
@@ -96,7 +93,6 @@ class LevelShuffle(Toggle):
|
|||||||
class Difficulty(Choice):
|
class Difficulty(Choice):
|
||||||
"""
|
"""
|
||||||
Which Difficulty Level to use
|
Which Difficulty Level to use
|
||||||
|
|
||||||
NORML: The Normal Difficulty
|
NORML: The Normal Difficulty
|
||||||
HARDR: Many DK Barrels are removed
|
HARDR: Many DK Barrels are removed
|
||||||
TUFST: Most DK Barrels and all Midway Barrels are removed
|
TUFST: Most DK Barrels and all Midway Barrels are removed
|
||||||
@@ -163,40 +159,19 @@ class StartingLifeCount(Range):
|
|||||||
default = 5
|
default = 5
|
||||||
|
|
||||||
|
|
||||||
dkc3_option_groups = [
|
|
||||||
OptionGroup("Goal Options", [
|
|
||||||
Goal,
|
|
||||||
KrematoaBonusCoinCost,
|
|
||||||
PercentageOfExtraBonusCoins,
|
|
||||||
NumberOfBananaBirds,
|
|
||||||
PercentageOfBananaBirds,
|
|
||||||
]),
|
|
||||||
OptionGroup("Aesthetics", [
|
|
||||||
Autosave,
|
|
||||||
MERRY,
|
|
||||||
MusicShuffle,
|
|
||||||
KongPaletteSwap,
|
|
||||||
StartingLifeCount,
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DKC3Options(PerGameCommonOptions):
|
class DKC3Options(PerGameCommonOptions):
|
||||||
#death_link: DeathLink # Disabled
|
#death_link: DeathLink # Disabled
|
||||||
#include_trade_sequence: IncludeTradeSequence # Disabled
|
|
||||||
|
|
||||||
goal: Goal
|
goal: Goal
|
||||||
|
#include_trade_sequence: IncludeTradeSequence # Disabled
|
||||||
|
dk_coins_for_gyrocopter: DKCoinsForGyrocopter
|
||||||
krematoa_bonus_coin_cost: KrematoaBonusCoinCost
|
krematoa_bonus_coin_cost: KrematoaBonusCoinCost
|
||||||
percentage_of_extra_bonus_coins: PercentageOfExtraBonusCoins
|
percentage_of_extra_bonus_coins: PercentageOfExtraBonusCoins
|
||||||
number_of_banana_birds: NumberOfBananaBirds
|
number_of_banana_birds: NumberOfBananaBirds
|
||||||
percentage_of_banana_birds: PercentageOfBananaBirds
|
percentage_of_banana_birds: PercentageOfBananaBirds
|
||||||
|
|
||||||
dk_coins_for_gyrocopter: DKCoinsForGyrocopter
|
|
||||||
kongsanity: KONGsanity
|
kongsanity: KONGsanity
|
||||||
level_shuffle: LevelShuffle
|
level_shuffle: LevelShuffle
|
||||||
difficulty: Difficulty
|
difficulty: Difficulty
|
||||||
|
|
||||||
autosave: Autosave
|
autosave: Autosave
|
||||||
merry: MERRY
|
merry: MERRY
|
||||||
music_shuffle: MusicShuffle
|
music_shuffle: MusicShuffle
|
||||||
|
|||||||
@@ -4,21 +4,20 @@ import typing
|
|||||||
import math
|
import math
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
import settings
|
||||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
||||||
from Options import PerGameCommonOptions
|
from Options import PerGameCommonOptions
|
||||||
import Patch
|
|
||||||
import settings
|
|
||||||
from worlds.AutoWorld import WebWorld, World
|
|
||||||
|
|
||||||
from .Client import DKC3SNIClient
|
|
||||||
from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
|
from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
|
||||||
from .Levels import level_list
|
|
||||||
from .Locations import DKC3Location, all_locations, setup_locations
|
from .Locations import DKC3Location, all_locations, setup_locations
|
||||||
from .Names import ItemName, LocationName
|
from .Options import DKC3Options
|
||||||
from .Options import DKC3Options, dkc3_option_groups
|
|
||||||
from .Regions import create_regions, connect_regions
|
from .Regions import create_regions, connect_regions
|
||||||
from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
|
from .Levels import level_list
|
||||||
from .Rules import set_rules
|
from .Rules import set_rules
|
||||||
|
from .Names import ItemName, LocationName
|
||||||
|
from .Client import DKC3SNIClient
|
||||||
|
from worlds.AutoWorld import WebWorld, World
|
||||||
|
from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
|
||||||
|
import Patch
|
||||||
|
|
||||||
|
|
||||||
class DK3Settings(settings.Group):
|
class DK3Settings(settings.Group):
|
||||||
@@ -45,8 +44,6 @@ class DKC3Web(WebWorld):
|
|||||||
|
|
||||||
tutorials = [setup_en]
|
tutorials = [setup_en]
|
||||||
|
|
||||||
option_groups = dkc3_option_groups
|
|
||||||
|
|
||||||
|
|
||||||
class DKC3World(World):
|
class DKC3World(World):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
factorio-rcon-py>=2.1.2
|
factorio-rcon-py>=2.1.1; python_version >= '3.9'
|
||||||
|
factorio-rcon-py==2.0.1; python_version <= '3.8'
|
||||||
|
|||||||
@@ -123,21 +123,10 @@ again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and
|
|||||||
new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard`
|
new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard`
|
||||||
and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery".
|
and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery".
|
||||||
|
|
||||||
## Adding or Removing from a List, Set, or Dict Option
|
Options that define a list, set, or dict can additionally have the character `+` added to the start of their name, which applies the contents of
|
||||||
|
the activated trigger to the already present equivalents in the game options.
|
||||||
List, set, and dict options can additionally have values added to or removed from itself without overriding the existing
|
|
||||||
option value by prefixing the option name in the trigger block with `+` (add) or `-` (remove). The exact behavior for
|
|
||||||
each will depend on the option type.
|
|
||||||
|
|
||||||
- For sets, `+` will add the value(s) to the set and `-` will remove any value(s) of the set. Sets do not allow
|
|
||||||
duplicates.
|
|
||||||
- For lists, `+` will add new values(s) to the list and `-` will remove the first matching values(s) it comes across.
|
|
||||||
Lists allow duplicate values.
|
|
||||||
- For dicts, `+` will add the value(s) to the given key(s) inside the dict if it exists, or add it otherwise. `-` is the
|
|
||||||
inverse operation of addition (and negative values are allowed).
|
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
Super Metroid:
|
Super Metroid:
|
||||||
start_location:
|
start_location:
|
||||||
@@ -145,8 +134,6 @@ Super Metroid:
|
|||||||
aqueduct: 50
|
aqueduct: 50
|
||||||
start_hints:
|
start_hints:
|
||||||
- Morph Ball
|
- Morph Ball
|
||||||
start_inventory:
|
|
||||||
Power Bombs: 1
|
|
||||||
triggers:
|
triggers:
|
||||||
- option_category: Super Metroid
|
- option_category: Super Metroid
|
||||||
option_name: start_location
|
option_name: start_location
|
||||||
@@ -157,9 +144,8 @@ Super Metroid:
|
|||||||
- Gravity Suit
|
- Gravity Suit
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, if the `start_location` option rolls `landing_site`, only a starting hint for Morph Ball will be
|
In this example, if the `start_location` option rolls `landing_site`, only a starting hint for Morph Ball will be created.
|
||||||
created. If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph
|
If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph Ball.
|
||||||
Ball.
|
|
||||||
|
|
||||||
Note that for lists, items can only be added, not removed or replaced. For dicts, defining a value for a present key
|
Note that for lists, items can only be added, not removed or replaced. For dicts, defining a value for a present key will
|
||||||
will replace that value within the dict.
|
replace that value within the dict.
|
||||||
|
|||||||
@@ -1,38 +1,25 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from Options import Choice, Removed, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions
|
from Options import Choice, Removed, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions
|
||||||
|
|
||||||
|
|
||||||
class PartyShuffle(Toggle):
|
class PartyShuffle(Toggle):
|
||||||
"""
|
"""Shuffles party members into the pool.
|
||||||
Shuffles party members into the item pool.
|
Note that enabling this can potentially increase both the difficulty and length of a run."""
|
||||||
|
|
||||||
Note that enabling this can significantly increase both the difficulty and length of a run.
|
|
||||||
"""
|
|
||||||
display_name = "Shuffle Party Members"
|
display_name = "Shuffle Party Members"
|
||||||
|
|
||||||
|
|
||||||
class GestureShuffle(Choice):
|
class GestureShuffle(Choice):
|
||||||
"""
|
"""Choose where gestures will appear in the item pool."""
|
||||||
Choose where gestures will appear in the item pool.
|
|
||||||
"""
|
|
||||||
display_name = "Shuffle Gestures"
|
display_name = "Shuffle Gestures"
|
||||||
option_anywhere = 0
|
option_anywhere = 0
|
||||||
option_tvs_only = 1
|
option_tvs_only = 1
|
||||||
option_default_locations = 2
|
option_default_locations = 2
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
class MedallionShuffle(Toggle):
|
class MedallionShuffle(Toggle):
|
||||||
"""
|
"""Shuffles red medallions into the pool."""
|
||||||
Shuffles red medallions into the item pool.
|
|
||||||
"""
|
|
||||||
display_name = "Shuffle Red Medallions"
|
display_name = "Shuffle Red Medallions"
|
||||||
|
|
||||||
|
|
||||||
class StartLocation(Choice):
|
class StartLocation(Choice):
|
||||||
"""
|
"""Select the starting location from 1 of 4 positions."""
|
||||||
Select the starting location from 1 of 4 positions.
|
|
||||||
"""
|
|
||||||
display_name = "Start Location"
|
display_name = "Start Location"
|
||||||
option_waynehouse = 0
|
option_waynehouse = 0
|
||||||
option_viewaxs_edifice = 1
|
option_viewaxs_edifice = 1
|
||||||
@@ -48,23 +35,14 @@ class StartLocation(Choice):
|
|||||||
return "TV Island"
|
return "TV Island"
|
||||||
return super().get_option_name(value)
|
return super().get_option_name(value)
|
||||||
|
|
||||||
|
|
||||||
class ExtraLogic(DefaultOnToggle):
|
class ExtraLogic(DefaultOnToggle):
|
||||||
"""
|
"""Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult."""
|
||||||
Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult.
|
|
||||||
"""
|
|
||||||
display_name = "Extra Items in Logic"
|
display_name = "Extra Items in Logic"
|
||||||
|
|
||||||
|
|
||||||
class Hylics2DeathLink(DeathLink):
|
class Hylics2DeathLink(DeathLink):
|
||||||
"""
|
"""When you die, everyone dies. The reverse is also true.
|
||||||
When you die, everyone dies. The reverse is also true.
|
|
||||||
|
|
||||||
Note that this also includes death by using the PERISH gesture.
|
Note that this also includes death by using the PERISH gesture.
|
||||||
|
Can be toggled via in-game console command "/deathlink"."""
|
||||||
Can be toggled via in-game console command "/deathlink".
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Hylics2Options(PerGameCommonOptions):
|
class Hylics2Options(PerGameCommonOptions):
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ from worlds.AutoWorld import WebWorld, World
|
|||||||
from .datatypes import Room, RoomEntrance
|
from .datatypes import Room, RoomEntrance
|
||||||
from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem
|
from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem
|
||||||
from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP
|
from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP
|
||||||
from .options import LingoOptions, lingo_option_groups
|
from .options import LingoOptions
|
||||||
from .player_logic import LingoPlayerLogic
|
from .player_logic import LingoPlayerLogic
|
||||||
from .regions import create_regions
|
from .regions import create_regions
|
||||||
|
|
||||||
|
|
||||||
class LingoWebWorld(WebWorld):
|
class LingoWebWorld(WebWorld):
|
||||||
option_groups = lingo_option_groups
|
|
||||||
theme = "grass"
|
theme = "grass"
|
||||||
tutorials = [Tutorial(
|
tutorials = [Tutorial(
|
||||||
"Multiworld Setup Guide",
|
"Multiworld Setup Guide",
|
||||||
|
|||||||
@@ -2052,7 +2052,6 @@
|
|||||||
door: Rhyme Room Entrance
|
door: Rhyme Room Entrance
|
||||||
Art Gallery:
|
Art Gallery:
|
||||||
warp: True
|
warp: True
|
||||||
Roof: True # by parkouring through the Bearer shortcut
|
|
||||||
panels:
|
panels:
|
||||||
RED:
|
RED:
|
||||||
id: Color Arrow Room/Panel_red_afar
|
id: Color Arrow Room/Panel_red_afar
|
||||||
@@ -2334,7 +2333,6 @@
|
|||||||
# This is the MASTERY on the other side of THE FEARLESS. It can only be
|
# This is the MASTERY on the other side of THE FEARLESS. It can only be
|
||||||
# accessed by jumping from the top of the tower.
|
# accessed by jumping from the top of the tower.
|
||||||
id: Master Room/Panel_mastery_mastery8
|
id: Master Room/Panel_mastery_mastery8
|
||||||
location_name: The Fearless - MASTERY
|
|
||||||
tag: midwhite
|
tag: midwhite
|
||||||
hunt: True
|
hunt: True
|
||||||
required_door:
|
required_door:
|
||||||
@@ -4100,7 +4098,6 @@
|
|||||||
Number Hunt:
|
Number Hunt:
|
||||||
room: Number Hunt
|
room: Number Hunt
|
||||||
door: Door to Directional Gallery
|
door: Door to Directional Gallery
|
||||||
Roof: True # through ceiling of sunwarp
|
|
||||||
panels:
|
panels:
|
||||||
PEPPER:
|
PEPPER:
|
||||||
id: Backside Room/Panel_pepper_salt
|
id: Backside Room/Panel_pepper_salt
|
||||||
@@ -5393,7 +5390,6 @@
|
|||||||
- The Artistic (Apple)
|
- The Artistic (Apple)
|
||||||
- The Artistic (Lattice)
|
- The Artistic (Lattice)
|
||||||
check: True
|
check: True
|
||||||
location_name: The Artistic - Achievement
|
|
||||||
achievement: The Artistic
|
achievement: The Artistic
|
||||||
FINE:
|
FINE:
|
||||||
id: Ceiling Room/Panel_yellow_top_5
|
id: Ceiling Room/Panel_yellow_top_5
|
||||||
@@ -6050,7 +6046,7 @@
|
|||||||
paintings:
|
paintings:
|
||||||
- id: symmetry_painting_a_5
|
- id: symmetry_painting_a_5
|
||||||
orientation: east
|
orientation: east
|
||||||
- id: symmetry_painting_b_5
|
- id: symmetry_painting_a_5
|
||||||
disable: True
|
disable: True
|
||||||
The Wondrous (Window):
|
The Wondrous (Window):
|
||||||
entrances:
|
entrances:
|
||||||
@@ -6818,6 +6814,9 @@
|
|||||||
tag: syn rhyme
|
tag: syn rhyme
|
||||||
subtag: bot
|
subtag: bot
|
||||||
link: rhyme FALL
|
link: rhyme FALL
|
||||||
|
LEAP:
|
||||||
|
id: Double Room/Panel_leap_leap
|
||||||
|
tag: midwhite
|
||||||
doors:
|
doors:
|
||||||
Exit:
|
Exit:
|
||||||
id: Double Room Area Doors/Door_room_exit
|
id: Double Room Area Doors/Door_room_exit
|
||||||
@@ -7066,9 +7065,6 @@
|
|||||||
tag: syn rhyme
|
tag: syn rhyme
|
||||||
subtag: bot
|
subtag: bot
|
||||||
link: rhyme CREATIVE
|
link: rhyme CREATIVE
|
||||||
LEAP:
|
|
||||||
id: Double Room/Panel_leap_leap
|
|
||||||
tag: midwhite
|
|
||||||
doors:
|
doors:
|
||||||
Door to Cross:
|
Door to Cross:
|
||||||
id: Double Room Area Doors/Door_room_4a
|
id: Double Room Area Doors/Door_room_4a
|
||||||
@@ -7276,7 +7272,6 @@
|
|||||||
MASTERY:
|
MASTERY:
|
||||||
id: Master Room/Panel_mastery_mastery
|
id: Master Room/Panel_mastery_mastery
|
||||||
tag: midwhite
|
tag: midwhite
|
||||||
hunt: True
|
|
||||||
required_door:
|
required_door:
|
||||||
room: Orange Tower Seventh Floor
|
room: Orange Tower Seventh Floor
|
||||||
door: Mastery
|
door: Mastery
|
||||||
|
|||||||
Binary file not shown.
@@ -766,6 +766,7 @@ panels:
|
|||||||
BOUNCE: 445010
|
BOUNCE: 445010
|
||||||
SCRAWL: 445011
|
SCRAWL: 445011
|
||||||
PLUNGE: 445012
|
PLUNGE: 445012
|
||||||
|
LEAP: 445013
|
||||||
Rhyme Room (Circle):
|
Rhyme Room (Circle):
|
||||||
BIRD: 445014
|
BIRD: 445014
|
||||||
LETTER: 445015
|
LETTER: 445015
|
||||||
@@ -789,7 +790,6 @@ panels:
|
|||||||
GEM: 445031
|
GEM: 445031
|
||||||
INNOVATIVE (Top): 445032
|
INNOVATIVE (Top): 445032
|
||||||
INNOVATIVE (Bottom): 445033
|
INNOVATIVE (Bottom): 445033
|
||||||
LEAP: 445013
|
|
||||||
Room Room:
|
Room Room:
|
||||||
DOOR (1): 445034
|
DOOR (1): 445034
|
||||||
DOOR (2): 445035
|
DOOR (2): 445035
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ class Panel(NamedTuple):
|
|||||||
exclude_reduce: bool
|
exclude_reduce: bool
|
||||||
achievement: bool
|
achievement: bool
|
||||||
non_counting: bool
|
non_counting: bool
|
||||||
location_name: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
class Painting(NamedTuple):
|
class Painting(NamedTuple):
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ def load_location_data():
|
|||||||
|
|
||||||
for room_name, panels in PANELS_BY_ROOM.items():
|
for room_name, panels in PANELS_BY_ROOM.items():
|
||||||
for panel_name, panel in panels.items():
|
for panel_name, panel in panels.items():
|
||||||
location_name = f"{room_name} - {panel_name}" if panel.location_name is None else panel.location_name
|
location_name = f"{room_name} - {panel_name}"
|
||||||
|
|
||||||
classification = LocationClassification.insanity
|
classification = LocationClassification.insanity
|
||||||
if panel.check:
|
if panel.check:
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from schema import And, Schema
|
from schema import And, Schema
|
||||||
|
|
||||||
from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict, \
|
from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict
|
||||||
OptionGroup
|
|
||||||
from .items import TRAP_ITEMS
|
from .items import TRAP_ITEMS
|
||||||
|
|
||||||
|
|
||||||
@@ -33,8 +32,8 @@ class ProgressiveColorful(DefaultOnToggle):
|
|||||||
|
|
||||||
|
|
||||||
class LocationChecks(Choice):
|
class LocationChecks(Choice):
|
||||||
"""Determines what locations are available.
|
"""On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for
|
||||||
On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for achievement panels and a small handful of other panels.
|
achievement panels and a small handful of other panels.
|
||||||
On "reduced", many of the locations that are associated with opening doors are removed.
|
On "reduced", many of the locations that are associated with opening doors are removed.
|
||||||
On "insanity", every individual panel in the game is a location check."""
|
On "insanity", every individual panel in the game is a location check."""
|
||||||
display_name = "Location Checks"
|
display_name = "Location Checks"
|
||||||
@@ -44,10 +43,8 @@ class LocationChecks(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class ShuffleColors(DefaultOnToggle):
|
class ShuffleColors(DefaultOnToggle):
|
||||||
"""
|
"""If on, an item is added to the pool for every puzzle color (besides White).
|
||||||
If on, an item is added to the pool for every puzzle color (besides White).
|
You will need to unlock the requisite colors in order to be able to solve puzzles of that color."""
|
||||||
You will need to unlock the requisite colors in order to be able to solve puzzles of that color.
|
|
||||||
"""
|
|
||||||
display_name = "Shuffle Colors"
|
display_name = "Shuffle Colors"
|
||||||
|
|
||||||
|
|
||||||
@@ -65,25 +62,20 @@ class ShufflePaintings(Toggle):
|
|||||||
|
|
||||||
|
|
||||||
class EnablePilgrimage(Toggle):
|
class EnablePilgrimage(Toggle):
|
||||||
"""Determines how the pilgrimage works.
|
"""If on, you are required to complete a pilgrimage in order to access the Pilgrim Antechamber.
|
||||||
If on, you are required to complete a pilgrimage in order to access the Pilgrim Antechamber.
|
|
||||||
If off, the pilgrimage will be deactivated, and the sun painting will be added to the pool, even if door shuffle is off."""
|
If off, the pilgrimage will be deactivated, and the sun painting will be added to the pool, even if door shuffle is off."""
|
||||||
display_name = "Enable Pilgrimage"
|
display_name = "Enable Pilgrimage"
|
||||||
|
|
||||||
|
|
||||||
class PilgrimageAllowsRoofAccess(DefaultOnToggle):
|
class PilgrimageAllowsRoofAccess(DefaultOnToggle):
|
||||||
"""
|
"""If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so).
|
||||||
If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so).
|
Otherwise, pilgrimage will be deactivated when going up the stairs."""
|
||||||
Otherwise, pilgrimage will be deactivated when going up the stairs.
|
|
||||||
"""
|
|
||||||
display_name = "Allow Roof Access for Pilgrimage"
|
display_name = "Allow Roof Access for Pilgrimage"
|
||||||
|
|
||||||
|
|
||||||
class PilgrimageAllowsPaintings(DefaultOnToggle):
|
class PilgrimageAllowsPaintings(DefaultOnToggle):
|
||||||
"""
|
"""If on, you may use paintings during a pilgrimage (and you may be expected to do so).
|
||||||
If on, you may use paintings during a pilgrimage (and you may be expected to do so).
|
Otherwise, pilgrimage will be deactivated when going through a painting."""
|
||||||
Otherwise, pilgrimage will be deactivated when going through a painting.
|
|
||||||
"""
|
|
||||||
display_name = "Allow Paintings for Pilgrimage"
|
display_name = "Allow Paintings for Pilgrimage"
|
||||||
|
|
||||||
|
|
||||||
@@ -145,10 +137,8 @@ class Level2Requirement(Range):
|
|||||||
|
|
||||||
|
|
||||||
class EarlyColorHallways(Toggle):
|
class EarlyColorHallways(Toggle):
|
||||||
"""
|
"""When on, a painting warp to the color hallways area will appear in the starting room.
|
||||||
When on, a painting warp to the color hallways area will appear in the starting room.
|
This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on."""
|
||||||
This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on.
|
|
||||||
"""
|
|
||||||
display_name = "Early Color Hallways"
|
display_name = "Early Color Hallways"
|
||||||
|
|
||||||
|
|
||||||
@@ -161,10 +151,8 @@ class TrapPercentage(Range):
|
|||||||
|
|
||||||
|
|
||||||
class TrapWeights(OptionDict):
|
class TrapWeights(OptionDict):
|
||||||
"""
|
"""Specify the distribution of traps that should be placed into the pool.
|
||||||
Specify the distribution of traps that should be placed into the pool.
|
If you don't want a specific type of trap, set the weight to zero."""
|
||||||
If you don't want a specific type of trap, set the weight to zero.
|
|
||||||
"""
|
|
||||||
display_name = "Trap Weights"
|
display_name = "Trap Weights"
|
||||||
schema = Schema({trap_name: And(int, lambda n: n >= 0) for trap_name in TRAP_ITEMS})
|
schema = Schema({trap_name: And(int, lambda n: n >= 0) for trap_name in TRAP_ITEMS})
|
||||||
default = {trap_name: 1 for trap_name in TRAP_ITEMS}
|
default = {trap_name: 1 for trap_name in TRAP_ITEMS}
|
||||||
@@ -183,26 +171,6 @@ class DeathLink(Toggle):
|
|||||||
display_name = "Death Link"
|
display_name = "Death Link"
|
||||||
|
|
||||||
|
|
||||||
lingo_option_groups = [
|
|
||||||
OptionGroup("Pilgrimage", [
|
|
||||||
EnablePilgrimage,
|
|
||||||
PilgrimageAllowsRoofAccess,
|
|
||||||
PilgrimageAllowsPaintings,
|
|
||||||
SunwarpAccess,
|
|
||||||
ShuffleSunwarps,
|
|
||||||
]),
|
|
||||||
OptionGroup("Fine-tuning", [
|
|
||||||
ProgressiveOrangeTower,
|
|
||||||
ProgressiveColorful,
|
|
||||||
MasteryAchievements,
|
|
||||||
Level2Requirement,
|
|
||||||
TrapPercentage,
|
|
||||||
TrapWeights,
|
|
||||||
PuzzleSkipPercentage,
|
|
||||||
])
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LingoOptions(PerGameCommonOptions):
|
class LingoOptions(PerGameCommonOptions):
|
||||||
shuffle_doors: ShuffleDoors
|
shuffle_doors: ShuffleDoors
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from enum import Enum
|
|||||||
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING
|
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
from Options import OptionError
|
from Options import OptionError
|
||||||
from .datatypes import Door, DoorType, Painting, RoomAndDoor, RoomAndPanel
|
from .datatypes import Door, DoorType, RoomAndDoor, RoomAndPanel
|
||||||
from .items import ALL_ITEM_TABLE, ItemType
|
from .items import ALL_ITEM_TABLE, ItemType
|
||||||
from .locations import ALL_LOCATION_TABLE, LocationClassification
|
from .locations import ALL_LOCATION_TABLE, LocationClassification
|
||||||
from .options import LocationChecks, ShuffleDoors, SunwarpAccess, VictoryCondition
|
from .options import LocationChecks, ShuffleDoors, SunwarpAccess, VictoryCondition
|
||||||
@@ -361,29 +361,13 @@ class LingoPlayerLogic:
|
|||||||
if door_shuffle == ShuffleDoors.option_none:
|
if door_shuffle == ShuffleDoors.option_none:
|
||||||
required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
|
required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
|
||||||
req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors]
|
req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors]
|
||||||
|
|
||||||
def is_req_enterable(painting_id: str, painting: Painting) -> bool:
|
|
||||||
if painting.exit_only or painting.disable or painting.req_blocked\
|
|
||||||
or painting.room in required_painting_rooms:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if world.options.shuffle_doors == ShuffleDoors.option_none:
|
|
||||||
if painting.req_blocked_when_no_doors:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Special case for the paintings in Color Hunt and Champion's Rest. These are req blocked when not on
|
|
||||||
# doors mode, and when sunwarps are disabled or sunwarp shuffle is on and the Color Hunt sunwarp is not
|
|
||||||
# an exit. This is because these two rooms would then be inaccessible without roof access, and we can't
|
|
||||||
# hide the Owl Hallway entrance behind roof access.
|
|
||||||
if painting.room in ["Color Hunt", "Champion's Rest"]:
|
|
||||||
if world.options.sunwarp_access == SunwarpAccess.option_disabled\
|
|
||||||
or (world.options.shuffle_sunwarps and "Color Hunt" not in self.sunwarp_exits):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
req_enterable = [painting_id for painting_id, painting in PAINTINGS.items()
|
req_enterable = [painting_id for painting_id, painting in PAINTINGS.items()
|
||||||
if is_req_enterable(painting_id, painting)]
|
if not painting.exit_only and not painting.disable and not painting.req_blocked and
|
||||||
|
not painting.req_blocked_when_no_doors and painting.room not in required_painting_rooms]
|
||||||
|
else:
|
||||||
|
req_enterable = [painting_id for painting_id, painting in PAINTINGS.items()
|
||||||
|
if not painting.exit_only and not painting.disable and not painting.req_blocked and
|
||||||
|
painting.room not in required_painting_rooms]
|
||||||
req_exits += [painting_id for painting_id, painting in PAINTINGS.items()
|
req_exits += [painting_id for painting_id, painting in PAINTINGS.items()
|
||||||
if painting.exit_only and painting.required]
|
if painting.exit_only and painting.required]
|
||||||
req_entrances = world.random.sample(req_enterable, len(req_exits))
|
req_entrances = world.random.sample(req_enterable, len(req_exits))
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ def process_entrance(source_room, doors, room_obj):
|
|||||||
def process_panel(room_name, panel_name, panel_data):
|
def process_panel(room_name, panel_name, panel_data):
|
||||||
global PANELS_BY_ROOM
|
global PANELS_BY_ROOM
|
||||||
|
|
||||||
|
full_name = f"{room_name} - {panel_name}"
|
||||||
|
|
||||||
# required_room can either be a single room or a list of rooms.
|
# required_room can either be a single room or a list of rooms.
|
||||||
if "required_room" in panel_data:
|
if "required_room" in panel_data:
|
||||||
if isinstance(panel_data["required_room"], list):
|
if isinstance(panel_data["required_room"], list):
|
||||||
@@ -227,13 +229,8 @@ def process_panel(room_name, panel_name, panel_data):
|
|||||||
else:
|
else:
|
||||||
non_counting = False
|
non_counting = False
|
||||||
|
|
||||||
if "location_name" in panel_data:
|
|
||||||
location_name = panel_data["location_name"]
|
|
||||||
else:
|
|
||||||
location_name = None
|
|
||||||
|
|
||||||
panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, exclude_reduce,
|
panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, exclude_reduce,
|
||||||
achievement, non_counting, location_name)
|
achievement, non_counting)
|
||||||
PANELS_BY_ROOM[room_name][panel_name] = panel_obj
|
PANELS_BY_ROOM[room_name][panel_name] = panel_obj
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,11 @@ mentioned_doors = Set[]
|
|||||||
mentioned_panels = Set[]
|
mentioned_panels = Set[]
|
||||||
mentioned_sunwarp_entrances = Set[]
|
mentioned_sunwarp_entrances = Set[]
|
||||||
mentioned_sunwarp_exits = Set[]
|
mentioned_sunwarp_exits = Set[]
|
||||||
mentioned_paintings = Set[]
|
|
||||||
|
|
||||||
door_groups = {}
|
door_groups = {}
|
||||||
|
|
||||||
directives = Set["entrances", "panels", "doors", "paintings", "sunwarps", "progression"]
|
directives = Set["entrances", "panels", "doors", "paintings", "sunwarps", "progression"]
|
||||||
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt", "location_name"]
|
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"]
|
||||||
door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "event", "warp_id"]
|
door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "event", "warp_id"]
|
||||||
painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]
|
painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]
|
||||||
|
|
||||||
@@ -258,12 +257,6 @@ config.each do |room_name, room|
|
|||||||
unless paintings.include? painting["id"] then
|
unless paintings.include? painting["id"] then
|
||||||
puts "#{room_name} :::: Invalid Painting ID #{painting["id"]}"
|
puts "#{room_name} :::: Invalid Painting ID #{painting["id"]}"
|
||||||
end
|
end
|
||||||
|
|
||||||
if mentioned_paintings.include?(painting["id"]) then
|
|
||||||
puts "Painting #{painting["id"]} is mentioned more than once"
|
|
||||||
else
|
|
||||||
mentioned_paintings.add(painting["id"])
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
puts "#{room_name} :::: Painting is missing an ID"
|
puts "#{room_name} :::: Painting is missing an ID"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ class MLSSClient(BizHawkClient):
|
|||||||
rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")])
|
rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")])
|
||||||
rom_name = bytes([byte for byte in rom_name_bytes[0] if byte != 0]).decode("UTF-8")
|
rom_name = bytes([byte for byte in rom_name_bytes[0] if byte != 0]).decode("UTF-8")
|
||||||
if not rom_name.startswith("MARIO&LUIGIUA8"):
|
if not rom_name.startswith("MARIO&LUIGIUA8"):
|
||||||
|
logger.info(
|
||||||
|
"ERROR: You have opened a game that is not Mario & Luigi Superstar Saga. "
|
||||||
|
"Please make sure you are opening the correct ROM."
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -538,16 +538,10 @@ Reality Show|71-2|Valentine Stage|False|5|7|10|
|
|||||||
SIG feat.Tobokegao|71-3|Valentine Stage|True|3|6|8|
|
SIG feat.Tobokegao|71-3|Valentine Stage|True|3|6|8|
|
||||||
Rose Love|71-4|Valentine Stage|True|2|4|7|
|
Rose Love|71-4|Valentine Stage|True|2|4|7|
|
||||||
Euphoria|71-5|Valentine Stage|True|1|3|6|
|
Euphoria|71-5|Valentine Stage|True|1|3|6|
|
||||||
P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|True|0|?|0|
|
P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|False|0|?|0|
|
||||||
PA PPA PANIC|72-1|Legends of Muse Warriors|False|4|8|10|
|
PA PPA PANIC|72-1|Legends of Muse Warriors|False|4|8|10|
|
||||||
How To Make Music Game Song!|72-2|Legends of Muse Warriors|True|6|8|10|11
|
How To Make Music Game Song!|72-2|Legends of Muse Warriors|False|6|8|10|11
|
||||||
Re Re|72-3|Legends of Muse Warriors|True|7|9|11|12
|
Re Re|72-3|Legends of Muse Warriors|False|7|9|11|12
|
||||||
Marmalade Twins|72-4|Legends of Muse Warriors|True|5|8|10|
|
Marmalade Twins|72-4|Legends of Muse Warriors|False|5|8|10|
|
||||||
DOMINATOR|72-5|Legends of Muse Warriors|True|7|9|11|
|
DOMINATOR|72-5|Legends of Muse Warriors|False|7|9|11|
|
||||||
Teshikani TESHiKANi|72-6|Legends of Muse Warriors|True|5|7|9|
|
Teshikani TESHiKANi|72-6|Legends of Muse Warriors|False|5|7|9|
|
||||||
Urban Magic|73-0|Happy Otaku Pack Vol.19|True|3|5|7|
|
|
||||||
Maid's Prank|73-1|Happy Otaku Pack Vol.19|True|5|7|10|
|
|
||||||
Dance Dance Good Night Dance|73-2|Happy Otaku Pack Vol.19|True|2|4|7|
|
|
||||||
Ops Limone|73-3|Happy Otaku Pack Vol.19|True|5|8|11|
|
|
||||||
NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10|
|
|
||||||
Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10|
|
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
|
|
||||||
class PathOption(Choice):
|
class PathOption(Choice):
|
||||||
"""
|
"""Choose where you would like Hidden Chest and Pedestal checks to be placed.
|
||||||
Choose where you would like Hidden Chest and Pedestal checks to be placed.
|
|
||||||
Main Path includes the main 7 biomes you typically go through to get to the final boss.
|
Main Path includes the main 7 biomes you typically go through to get to the final boss.
|
||||||
Side Path includes the Lukki Lair and Fungal Caverns. 9 biomes total.
|
Side Path includes the Lukki Lair and Fungal Caverns. 9 biomes total.
|
||||||
Main World includes the full world (excluding parallel worlds). 15 biomes total.
|
Main World includes the full world (excluding parallel worlds). 15 biomes total.
|
||||||
Note: The Collapsed Mines have been combined into the Mines as the biome is tiny.
|
Note: The Collapsed Mines have been combined into the Mines as the biome is tiny."""
|
||||||
"""
|
|
||||||
display_name = "Path Option"
|
display_name = "Path Option"
|
||||||
option_main_path = 1
|
option_main_path = 1
|
||||||
option_side_path = 2
|
option_side_path = 2
|
||||||
@@ -18,9 +16,7 @@ class PathOption(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class HiddenChests(Range):
|
class HiddenChests(Range):
|
||||||
"""
|
"""Number of hidden chest checks added to the applicable biomes."""
|
||||||
Number of hidden chest checks added to the applicable biomes.
|
|
||||||
"""
|
|
||||||
display_name = "Hidden Chests per Biome"
|
display_name = "Hidden Chests per Biome"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 20
|
range_end = 20
|
||||||
@@ -28,9 +24,7 @@ class HiddenChests(Range):
|
|||||||
|
|
||||||
|
|
||||||
class PedestalChecks(Range):
|
class PedestalChecks(Range):
|
||||||
"""
|
"""Number of checks that will spawn on pedestals in the applicable biomes."""
|
||||||
Number of checks that will spawn on pedestals in the applicable biomes.
|
|
||||||
"""
|
|
||||||
display_name = "Pedestal Checks per Biome"
|
display_name = "Pedestal Checks per Biome"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 20
|
range_end = 20
|
||||||
@@ -38,19 +32,15 @@ class PedestalChecks(Range):
|
|||||||
|
|
||||||
|
|
||||||
class Traps(DefaultOnToggle):
|
class Traps(DefaultOnToggle):
|
||||||
"""
|
"""Whether negative effects on the Noita world are added to the item pool."""
|
||||||
Whether negative effects on the Noita world are added to the item pool.
|
|
||||||
"""
|
|
||||||
display_name = "Traps"
|
display_name = "Traps"
|
||||||
|
|
||||||
|
|
||||||
class OrbsAsChecks(Choice):
|
class OrbsAsChecks(Choice):
|
||||||
"""
|
"""Decides whether finding the orbs that naturally spawn in the world count as checks.
|
||||||
Decides whether finding the orbs that naturally spawn in the world count as checks.
|
|
||||||
The Main Path option includes only the Floating Island and Abyss Orb Room orbs.
|
The Main Path option includes only the Floating Island and Abyss Orb Room orbs.
|
||||||
The Side Path option includes the Main Path, Magical Temple, Lukki Lair, and Lava Lake orbs.
|
The Side Path option includes the Main Path, Magical Temple, Lukki Lair, and Lava Lake orbs.
|
||||||
The Main World option includes all 11 orbs.
|
The Main World option includes all 11 orbs."""
|
||||||
"""
|
|
||||||
display_name = "Orbs as Location Checks"
|
display_name = "Orbs as Location Checks"
|
||||||
option_no_orbs = 0
|
option_no_orbs = 0
|
||||||
option_main_path = 1
|
option_main_path = 1
|
||||||
@@ -60,12 +50,10 @@ class OrbsAsChecks(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class BossesAsChecks(Choice):
|
class BossesAsChecks(Choice):
|
||||||
"""
|
"""Makes bosses count as location checks. The boss only needs to die, you do not need the kill credit.
|
||||||
Makes bosses count as location checks. The boss only needs to die, you do not need the kill credit.
|
|
||||||
The Main Path option includes Gate Guardian, Suomuhauki, and Kolmisilmä.
|
The Main Path option includes Gate Guardian, Suomuhauki, and Kolmisilmä.
|
||||||
The Side Path option includes the Main Path bosses, Sauvojen Tuntija, and Ylialkemisti.
|
The Side Path option includes the Main Path bosses, Sauvojen Tuntija, and Ylialkemisti.
|
||||||
The All Bosses option includes all 15 bosses.
|
The All Bosses option includes all 15 bosses."""
|
||||||
"""
|
|
||||||
display_name = "Bosses as Location Checks"
|
display_name = "Bosses as Location Checks"
|
||||||
option_no_bosses = 0
|
option_no_bosses = 0
|
||||||
option_main_path = 1
|
option_main_path = 1
|
||||||
@@ -77,13 +65,11 @@ class BossesAsChecks(Choice):
|
|||||||
# Note: the Sampo is an item that is picked up to trigger the boss fight at the normal ending location.
|
# Note: the Sampo is an item that is picked up to trigger the boss fight at the normal ending location.
|
||||||
# The sampo is required for every ending (having orbs and bringing the sampo to a different spot changes the ending).
|
# The sampo is required for every ending (having orbs and bringing the sampo to a different spot changes the ending).
|
||||||
class VictoryCondition(Choice):
|
class VictoryCondition(Choice):
|
||||||
"""
|
"""Greed is to get to the bottom, beat the boss, and win the game.
|
||||||
Greed is to get to the bottom, beat the boss, and win the game.
|
|
||||||
Pure is to get 11 orbs, grab the sampo, and bring it to the mountain altar.
|
Pure is to get 11 orbs, grab the sampo, and bring it to the mountain altar.
|
||||||
Peaceful is to get all 33 orbs, grab the sampo, and bring it to the mountain altar.
|
Peaceful is to get all 33 orbs, grab the sampo, and bring it to the mountain altar.
|
||||||
Orbs will be added to the randomizer pool based on which victory condition you chose.
|
Orbs will be added to the randomizer pool based on which victory condition you chose.
|
||||||
The base game orbs will not count towards these victory conditions.
|
The base game orbs will not count towards these victory conditions."""
|
||||||
"""
|
|
||||||
display_name = "Victory Condition"
|
display_name = "Victory Condition"
|
||||||
option_greed_ending = 0
|
option_greed_ending = 0
|
||||||
option_pure_ending = 1
|
option_pure_ending = 1
|
||||||
@@ -92,11 +78,9 @@ class VictoryCondition(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class ExtraOrbs(Range):
|
class ExtraOrbs(Range):
|
||||||
"""
|
"""Add extra orbs to your item pool, to prevent you from needing to wait as long for the last orb you need for your victory condition.
|
||||||
Add extra orbs to your item pool, to prevent you from needing to wait as long for the last orb you need for your victory condition.
|
|
||||||
Extra orbs received past your victory condition's amount will be received as hearts instead.
|
Extra orbs received past your victory condition's amount will be received as hearts instead.
|
||||||
Can be turned on for the Greed Ending goal, but will only really make it harder.
|
Can be turned on for the Greed Ending goal, but will only really make it harder."""
|
||||||
"""
|
|
||||||
display_name = "Extra Orbs"
|
display_name = "Extra Orbs"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 10
|
range_end = 10
|
||||||
@@ -104,10 +88,8 @@ class ExtraOrbs(Range):
|
|||||||
|
|
||||||
|
|
||||||
class ShopPrice(Choice):
|
class ShopPrice(Choice):
|
||||||
"""
|
"""Reduce the costs of Archipelago items in shops.
|
||||||
Reduce the costs of Archipelago items in shops.
|
By default, the price of Archipelago items matches the price of wands at that shop."""
|
||||||
By default, the price of Archipelago items matches the price of wands at that shop.
|
|
||||||
"""
|
|
||||||
display_name = "Shop Price Reduction"
|
display_name = "Shop Price Reduction"
|
||||||
option_full_price = 100
|
option_full_price = 100
|
||||||
option_25_percent_off = 75
|
option_25_percent_off = 75
|
||||||
@@ -116,17 +98,10 @@ class ShopPrice(Choice):
|
|||||||
default = 100
|
default = 100
|
||||||
|
|
||||||
|
|
||||||
class NoitaDeathLink(DeathLink):
|
|
||||||
"""
|
|
||||||
When you die, everyone dies. Of course, the reverse is true too.
|
|
||||||
You can disable this in the in-game mod options.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NoitaOptions(PerGameCommonOptions):
|
class NoitaOptions(PerGameCommonOptions):
|
||||||
start_inventory_from_pool: StartInventoryPool
|
start_inventory_from_pool: StartInventoryPool
|
||||||
death_link: NoitaDeathLink
|
death_link: DeathLink
|
||||||
bad_effects: Traps
|
bad_effects: Traps
|
||||||
victory_condition: VictoryCondition
|
victory_condition: VictoryCondition
|
||||||
path_option: PathOption
|
path_option: PathOption
|
||||||
|
|||||||
@@ -21,16 +21,16 @@ limpiarlas, selecciona el atajo y presiona la tecla Esc.
|
|||||||
|
|
||||||
## Software Opcional
|
## Software Opcional
|
||||||
|
|
||||||
- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), para usar
|
- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), para usar con
|
||||||
con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
[PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||||
|
|
||||||
## Generando y Parcheando el Juego
|
## Generando y Parcheando el Juego
|
||||||
|
|
||||||
1. Crea tu archivo de configuración (YAML). Puedes hacerlo en
|
1. Crea tu archivo de configuración (YAML). Puedes hacerlo en
|
||||||
[Página de Opciones de Pokémon Emerald](../../../games/Pokemon%20Emerald/player-options).
|
[Página de Opciones de Pokémon Emerald](../../../games/Pokemon%20Emerald/player-options).
|
||||||
2. Sigue las instrucciones generales de Archipelago para
|
2. Sigue las instrucciones generales de Archipelago para [Generar un juego]
|
||||||
[Generar un juego](../../Archipelago/setup/en#generating-a-game). Esto generará un archivo de salida (output file) para
|
(../../Archipelago/setup/en#generating-a-game). Esto generará un archivo de salida (output file) para ti. Tu archivo
|
||||||
ti. Tu archivo de parche tendrá la extensión de archivo `.apemerald`.
|
de parche tendrá la extensión de archivo`.apemerald`.
|
||||||
3. Abre `ArchipelagoLauncher.exe`
|
3. Abre `ArchipelagoLauncher.exe`
|
||||||
4. Selecciona "Open Patch" en el lado derecho y elige tu archivo de parcheo.
|
4. Selecciona "Open Patch" en el lado derecho y elige tu archivo de parcheo.
|
||||||
5. Si esta es la primera vez que vas a parchear, se te pedirá que selecciones la ROM sin parchear.
|
5. Si esta es la primera vez que vas a parchear, se te pedirá que selecciones la ROM sin parchear.
|
||||||
@@ -40,7 +40,7 @@ ti. Tu archivo de parche tendrá la extensión de archivo `.apemerald`.
|
|||||||
|
|
||||||
Si estás jugando una seed Single-Player y no te interesa el auto-tracking o las pistas, puedes parar aquí, cierra el
|
Si estás jugando una seed Single-Player y no te interesa el auto-tracking o las pistas, puedes parar aquí, cierra el
|
||||||
cliente, y carga la ROM ya parcheada en cualquier emulador. Pero para partidas multi-worlds y para otras
|
cliente, y carga la ROM ya parcheada en cualquier emulador. Pero para partidas multi-worlds y para otras
|
||||||
implementaciones de Archipelago, continúa usando BizHawk como tu emulador.
|
implementaciones de Archipelago, continúa usando BizHawk como tu emulador
|
||||||
|
|
||||||
## Conectando con el Servidor
|
## Conectando con el Servidor
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
from dataclasses import dataclass
|
import typing
|
||||||
|
|
||||||
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
|
from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList
|
||||||
|
|
||||||
|
|
||||||
class Goal(Choice):
|
class Goal(Choice):
|
||||||
"""
|
"""
|
||||||
Determines the goal of the seed
|
Determines the goal of the seed
|
||||||
|
|
||||||
Biolizard: Finish Cannon's Core and defeat the Biolizard and Finalhazard
|
Biolizard: Finish Cannon's Core and defeat the Biolizard and Finalhazard
|
||||||
|
|
||||||
Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone
|
Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone
|
||||||
|
|
||||||
Finalhazard Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone, then defeat Finalhazard
|
Finalhazard Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone, then defeat Finalhazard
|
||||||
|
|
||||||
Grand Prix: Win every race in Kart Race Mode (all standard levels are disabled)
|
Grand Prix: Win every race in Kart Race Mode (all standard levels are disabled)
|
||||||
|
|
||||||
Boss Rush: Beat all of the bosses in the Boss Rush, ending with Finalhazard
|
Boss Rush: Beat all of the bosses in the Boss Rush, ending with Finalhazard
|
||||||
|
|
||||||
Cannon's Core Boss Rush: Beat Cannon's Core, then beat all of the bosses in the Boss Rush, ending with Finalhazard
|
Cannon's Core Boss Rush: Beat Cannon's Core, then beat all of the bosses in the Boss Rush, ending with Finalhazard
|
||||||
|
|
||||||
Boss Rush Chaos Emerald Hunt: Find the Seven Chaos Emeralds, then beat all of the bosses in the Boss Rush, ending with Finalhazard
|
Boss Rush Chaos Emerald Hunt: Find the Seven Chaos Emeralds, then beat all of the bosses in the Boss Rush, ending with Finalhazard
|
||||||
|
|
||||||
Chaos Chao: Raise a Chaos Chao to win
|
Chaos Chao: Raise a Chaos Chao to win
|
||||||
"""
|
"""
|
||||||
display_name = "Goal"
|
display_name = "Goal"
|
||||||
@@ -54,13 +46,9 @@ class MissionShuffle(Toggle):
|
|||||||
class BossRushShuffle(Choice):
|
class BossRushShuffle(Choice):
|
||||||
"""
|
"""
|
||||||
Determines how bosses in Boss Rush Mode are shuffled
|
Determines how bosses in Boss Rush Mode are shuffled
|
||||||
|
|
||||||
Vanilla: Bosses appear in the Vanilla ordering
|
Vanilla: Bosses appear in the Vanilla ordering
|
||||||
|
|
||||||
Shuffled: The same bosses appear, but in a random order
|
Shuffled: The same bosses appear, but in a random order
|
||||||
|
|
||||||
Chaos: Each boss is randomly chosen separately (one will always be King Boom Boo)
|
Chaos: Each boss is randomly chosen separately (one will always be King Boom Boo)
|
||||||
|
|
||||||
Singularity: One boss is chosen and placed in every slot (one will always be replaced with King Boom Boo)
|
Singularity: One boss is chosen and placed in every slot (one will always be replaced with King Boom Boo)
|
||||||
"""
|
"""
|
||||||
display_name = "Boss Rush Shuffle"
|
display_name = "Boss Rush Shuffle"
|
||||||
@@ -208,13 +196,9 @@ class Keysanity(Toggle):
|
|||||||
class Whistlesanity(Choice):
|
class Whistlesanity(Choice):
|
||||||
"""
|
"""
|
||||||
Determines whether whistling at various spots grants checks
|
Determines whether whistling at various spots grants checks
|
||||||
|
|
||||||
None: No Whistle Spots grant checks
|
None: No Whistle Spots grant checks
|
||||||
|
|
||||||
Pipes: Whistling at Pipes grants checks (97 Locations)
|
Pipes: Whistling at Pipes grants checks (97 Locations)
|
||||||
|
|
||||||
Hidden: Whistling at Hidden Whistle Spots grants checks (32 Locations)
|
Hidden: Whistling at Hidden Whistle Spots grants checks (32 Locations)
|
||||||
|
|
||||||
Both: Whistling at both Pipes and Hidden Whistle Spots grants checks (129 Locations)
|
Both: Whistling at both Pipes and Hidden Whistle Spots grants checks (129 Locations)
|
||||||
"""
|
"""
|
||||||
display_name = "Whistlesanity"
|
display_name = "Whistlesanity"
|
||||||
@@ -244,9 +228,8 @@ class Omosanity(Toggle):
|
|||||||
class Animalsanity(Toggle):
|
class Animalsanity(Toggle):
|
||||||
"""
|
"""
|
||||||
Determines whether unique counts of animals grant checks.
|
Determines whether unique counts of animals grant checks.
|
||||||
(421 Locations)
|
|
||||||
|
|
||||||
ALL animals must be collected in a single run of a mission to get all checks.
|
ALL animals must be collected in a single run of a mission to get all checks.
|
||||||
|
(421 Locations)
|
||||||
"""
|
"""
|
||||||
display_name = "Animalsanity"
|
display_name = "Animalsanity"
|
||||||
|
|
||||||
@@ -254,11 +237,8 @@ class Animalsanity(Toggle):
|
|||||||
class KartRaceChecks(Choice):
|
class KartRaceChecks(Choice):
|
||||||
"""
|
"""
|
||||||
Determines whether Kart Race Mode grants checks
|
Determines whether Kart Race Mode grants checks
|
||||||
|
|
||||||
None: No Kart Races grant checks
|
None: No Kart Races grant checks
|
||||||
|
|
||||||
Mini: Each Kart Race difficulty must be beaten only once
|
Mini: Each Kart Race difficulty must be beaten only once
|
||||||
|
|
||||||
Full: Every Character must separately beat each Kart Race difficulty
|
Full: Every Character must separately beat each Kart Race difficulty
|
||||||
"""
|
"""
|
||||||
display_name = "Kart Race Checks"
|
display_name = "Kart Race Checks"
|
||||||
@@ -291,11 +271,8 @@ class NumberOfLevelGates(Range):
|
|||||||
class LevelGateDistribution(Choice):
|
class LevelGateDistribution(Choice):
|
||||||
"""
|
"""
|
||||||
Determines how levels are distributed between level gate regions
|
Determines how levels are distributed between level gate regions
|
||||||
|
|
||||||
Early: Earlier regions will have more levels than later regions
|
Early: Earlier regions will have more levels than later regions
|
||||||
|
|
||||||
Even: Levels will be evenly distributed between all regions
|
Even: Levels will be evenly distributed between all regions
|
||||||
|
|
||||||
Late: Later regions will have more levels than earlier regions
|
Late: Later regions will have more levels than earlier regions
|
||||||
"""
|
"""
|
||||||
display_name = "Level Gate Distribution"
|
display_name = "Level Gate Distribution"
|
||||||
@@ -319,9 +296,7 @@ class LevelGateCosts(Choice):
|
|||||||
class MaximumEmblemCap(Range):
|
class MaximumEmblemCap(Range):
|
||||||
"""
|
"""
|
||||||
Determines the maximum number of emblems that can be in the item pool.
|
Determines the maximum number of emblems that can be in the item pool.
|
||||||
|
|
||||||
If fewer available locations exist in the pool than this number, the number of available locations will be used instead.
|
If fewer available locations exist in the pool than this number, the number of available locations will be used instead.
|
||||||
|
|
||||||
Gate and Cannon's Core costs will be calculated based off of that number.
|
Gate and Cannon's Core costs will be calculated based off of that number.
|
||||||
"""
|
"""
|
||||||
display_name = "Max Emblem Cap"
|
display_name = "Max Emblem Cap"
|
||||||
@@ -346,13 +321,9 @@ class RequiredRank(Choice):
|
|||||||
class ChaoRaceDifficulty(Choice):
|
class ChaoRaceDifficulty(Choice):
|
||||||
"""
|
"""
|
||||||
Determines the number of Chao Race difficulty levels included. Easier difficulty settings means fewer Chao Race checks
|
Determines the number of Chao Race difficulty levels included. Easier difficulty settings means fewer Chao Race checks
|
||||||
|
|
||||||
None: No Chao Races have checks
|
None: No Chao Races have checks
|
||||||
|
|
||||||
Beginner: Beginner Races
|
Beginner: Beginner Races
|
||||||
|
|
||||||
Intermediate: Beginner, Challenge, Hero, and Dark Races
|
Intermediate: Beginner, Challenge, Hero, and Dark Races
|
||||||
|
|
||||||
Expert: Beginner, Challenge, Hero, Dark and Jewel Races
|
Expert: Beginner, Challenge, Hero, Dark and Jewel Races
|
||||||
"""
|
"""
|
||||||
display_name = "Chao Race Difficulty"
|
display_name = "Chao Race Difficulty"
|
||||||
@@ -379,10 +350,9 @@ class ChaoKarateDifficulty(Choice):
|
|||||||
class ChaoStadiumChecks(Choice):
|
class ChaoStadiumChecks(Choice):
|
||||||
"""
|
"""
|
||||||
Determines which Chao Stadium activities grant checks
|
Determines which Chao Stadium activities grant checks
|
||||||
|
|
||||||
All: Each individual race and karate fight grants a check
|
All: Each individual race and karate fight grants a check
|
||||||
|
Prize: Only the races which grant Chao Toys grant checks (final race of each Beginner and Jewel cup, 4th, 8th, and
|
||||||
Prize: Only the races which grant Chao Toys grant checks (final race of each Beginner and Jewel cup, 4th, 8th, and 12th Challenge Races, 2nd and 4th Hero and Dark Races, final fight of each Karate difficulty)
|
12th Challenge Races, 2nd and 4th Hero and Dark Races, final fight of each Karate difficulty)
|
||||||
"""
|
"""
|
||||||
display_name = "Chao Stadium Checks"
|
display_name = "Chao Stadium Checks"
|
||||||
option_all = 0
|
option_all = 0
|
||||||
@@ -404,7 +374,6 @@ class ChaoStats(Range):
|
|||||||
class ChaoStatsFrequency(Range):
|
class ChaoStatsFrequency(Range):
|
||||||
"""
|
"""
|
||||||
Determines how many levels in each Chao Stat grant checks (up to the maximum set in the `chao_stats` option)
|
Determines how many levels in each Chao Stat grant checks (up to the maximum set in the `chao_stats` option)
|
||||||
|
|
||||||
`1` means every level is included, `2` means every other level is included, `3` means every third, and so on
|
`1` means every level is included, `2` means every other level is included, `3` means every third, and so on
|
||||||
"""
|
"""
|
||||||
display_name = "Chao Stats Frequency"
|
display_name = "Chao Stats Frequency"
|
||||||
@@ -439,11 +408,8 @@ class ChaoKindergarten(Choice):
|
|||||||
"""
|
"""
|
||||||
Determines whether learning the lessons from the Kindergarten Classroom grants checks
|
Determines whether learning the lessons from the Kindergarten Classroom grants checks
|
||||||
(WARNING: VERY SLOW)
|
(WARNING: VERY SLOW)
|
||||||
|
|
||||||
None: No Kindergarten classes have checks
|
None: No Kindergarten classes have checks
|
||||||
|
|
||||||
Basics: One class from each category (Drawing, Dance, Song, and Instrument) is a check (4 Locations)
|
Basics: One class from each category (Drawing, Dance, Song, and Instrument) is a check (4 Locations)
|
||||||
|
|
||||||
Full: Every class is a check (23 Locations)
|
Full: Every class is a check (23 Locations)
|
||||||
"""
|
"""
|
||||||
display_name = "Chao Kindergarten Checks"
|
display_name = "Chao Kindergarten Checks"
|
||||||
@@ -477,8 +443,8 @@ class BlackMarketUnlockCosts(Choice):
|
|||||||
class BlackMarketPriceMultiplier(Range):
|
class BlackMarketPriceMultiplier(Range):
|
||||||
"""
|
"""
|
||||||
Determines how many rings the Black Market items cost
|
Determines how many rings the Black Market items cost
|
||||||
|
The base ring costs of items in the Black Market range from 50-100,
|
||||||
The base ring costs of items in the Black Market range from 50-100, and are then multiplied by this value
|
and are then multiplied by this value
|
||||||
"""
|
"""
|
||||||
display_name = "Black Market Price Multiplier"
|
display_name = "Black Market Price Multiplier"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
@@ -503,9 +469,7 @@ class ChaoEntranceRandomization(Toggle):
|
|||||||
class RequiredCannonsCoreMissions(Choice):
|
class RequiredCannonsCoreMissions(Choice):
|
||||||
"""
|
"""
|
||||||
Determines how many Cannon's Core missions must be completed (for Biolizard or Cannon's Core goals)
|
Determines how many Cannon's Core missions must be completed (for Biolizard or Cannon's Core goals)
|
||||||
|
|
||||||
First: Only the first mission must be completed
|
First: Only the first mission must be completed
|
||||||
|
|
||||||
All Active: All active Cannon's Core missions must be completed
|
All Active: All active Cannon's Core missions must be completed
|
||||||
"""
|
"""
|
||||||
display_name = "Required Cannon's Core Missions"
|
display_name = "Required Cannon's Core Missions"
|
||||||
@@ -701,11 +665,8 @@ class CannonsCoreMission5(DefaultOnToggle):
|
|||||||
class RingLoss(Choice):
|
class RingLoss(Choice):
|
||||||
"""
|
"""
|
||||||
How taking damage is handled
|
How taking damage is handled
|
||||||
|
|
||||||
Classic: You lose all of your rings when hit
|
Classic: You lose all of your rings when hit
|
||||||
|
|
||||||
Modern: You lose 20 rings when hit
|
Modern: You lose 20 rings when hit
|
||||||
|
|
||||||
OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks may require damage boosts!)
|
OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks may require damage boosts!)
|
||||||
"""
|
"""
|
||||||
display_name = "Ring Loss"
|
display_name = "Ring Loss"
|
||||||
@@ -732,13 +693,9 @@ class RingLink(Toggle):
|
|||||||
class SADXMusic(Choice):
|
class SADXMusic(Choice):
|
||||||
"""
|
"""
|
||||||
Whether the randomizer will include Sonic Adventure DX Music in the music pool
|
Whether the randomizer will include Sonic Adventure DX Music in the music pool
|
||||||
|
|
||||||
SA2B: Only SA2B music will be played
|
SA2B: Only SA2B music will be played
|
||||||
|
|
||||||
SADX: Only SADX music will be played
|
SADX: Only SADX music will be played
|
||||||
|
|
||||||
Both: Both SA2B and SADX music will be played
|
Both: Both SA2B and SADX music will be played
|
||||||
|
|
||||||
NOTE: This option requires the player to own a PC copy of SADX and to follow the addition steps in the setup guide.
|
NOTE: This option requires the player to own a PC copy of SADX and to follow the addition steps in the setup guide.
|
||||||
"""
|
"""
|
||||||
display_name = "SADX Music"
|
display_name = "SADX Music"
|
||||||
@@ -758,13 +715,9 @@ class SADXMusic(Choice):
|
|||||||
class MusicShuffle(Choice):
|
class MusicShuffle(Choice):
|
||||||
"""
|
"""
|
||||||
What type of Music Shuffle is used
|
What type of Music Shuffle is used
|
||||||
|
|
||||||
None: No music is shuffled.
|
None: No music is shuffled.
|
||||||
|
|
||||||
Levels: Level music is shuffled.
|
Levels: Level music is shuffled.
|
||||||
|
|
||||||
Full: Level, Menu, and Additional music is shuffled.
|
Full: Level, Menu, and Additional music is shuffled.
|
||||||
|
|
||||||
Singularity: Level, Menu, and Additional music is all replaced with a single random song.
|
Singularity: Level, Menu, and Additional music is all replaced with a single random song.
|
||||||
"""
|
"""
|
||||||
display_name = "Music Shuffle Type"
|
display_name = "Music Shuffle Type"
|
||||||
@@ -778,15 +731,10 @@ class MusicShuffle(Choice):
|
|||||||
class VoiceShuffle(Choice):
|
class VoiceShuffle(Choice):
|
||||||
"""
|
"""
|
||||||
What type of Voice Shuffle is used
|
What type of Voice Shuffle is used
|
||||||
|
|
||||||
None: No voices are shuffled.
|
None: No voices are shuffled.
|
||||||
|
|
||||||
Shuffled: Voices are shuffled.
|
Shuffled: Voices are shuffled.
|
||||||
|
|
||||||
Rude: Voices are shuffled, but some are replaced with rude words.
|
Rude: Voices are shuffled, but some are replaced with rude words.
|
||||||
|
|
||||||
Chao: All voices are replaced with chao sounds.
|
Chao: All voices are replaced with chao sounds.
|
||||||
|
|
||||||
Singularity: All voices are replaced with a single random voice.
|
Singularity: All voices are replaced with a single random voice.
|
||||||
"""
|
"""
|
||||||
display_name = "Voice Shuffle Type"
|
display_name = "Voice Shuffle Type"
|
||||||
@@ -820,9 +768,7 @@ class Narrator(Choice):
|
|||||||
class LogicDifficulty(Choice):
|
class LogicDifficulty(Choice):
|
||||||
"""
|
"""
|
||||||
What set of Upgrade Requirement logic to use
|
What set of Upgrade Requirement logic to use
|
||||||
|
|
||||||
Standard: The logic assumes the "intended" usage of Upgrades to progress through levels
|
Standard: The logic assumes the "intended" usage of Upgrades to progress through levels
|
||||||
|
|
||||||
Hard: Some simple skips or sequence breaks may be required
|
Hard: Some simple skips or sequence breaks may be required
|
||||||
"""
|
"""
|
||||||
display_name = "Logic Difficulty"
|
display_name = "Logic Difficulty"
|
||||||
@@ -831,195 +777,96 @@ class LogicDifficulty(Choice):
|
|||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
sa2b_option_groups = [
|
sa2b_options: typing.Dict[str, type(Option)] = {
|
||||||
OptionGroup("General Options", [
|
"goal": Goal,
|
||||||
Goal,
|
|
||||||
BossRushShuffle,
|
|
||||||
LogicDifficulty,
|
|
||||||
RequiredRank,
|
|
||||||
MaximumEmblemCap,
|
|
||||||
RingLoss,
|
|
||||||
]),
|
|
||||||
OptionGroup("Stages", [
|
|
||||||
MissionShuffle,
|
|
||||||
EmblemPercentageForCannonsCore,
|
|
||||||
RequiredCannonsCoreMissions,
|
|
||||||
NumberOfLevelGates,
|
|
||||||
LevelGateCosts,
|
|
||||||
LevelGateDistribution,
|
|
||||||
]),
|
|
||||||
OptionGroup("Sanity Options", [
|
|
||||||
Keysanity,
|
|
||||||
Whistlesanity,
|
|
||||||
Beetlesanity,
|
|
||||||
Omosanity,
|
|
||||||
Animalsanity,
|
|
||||||
KartRaceChecks,
|
|
||||||
]),
|
|
||||||
OptionGroup("Chao", [
|
|
||||||
BlackMarketSlots,
|
|
||||||
BlackMarketUnlockCosts,
|
|
||||||
BlackMarketPriceMultiplier,
|
|
||||||
ChaoRaceDifficulty,
|
|
||||||
ChaoKarateDifficulty,
|
|
||||||
ChaoStadiumChecks,
|
|
||||||
ChaoAnimalParts,
|
|
||||||
ChaoStats,
|
|
||||||
ChaoStatsFrequency,
|
|
||||||
ChaoStatsStamina,
|
|
||||||
ChaoStatsHidden,
|
|
||||||
ChaoKindergarten,
|
|
||||||
ShuffleStartingChaoEggs,
|
|
||||||
ChaoEntranceRandomization,
|
|
||||||
]),
|
|
||||||
OptionGroup("Junk and Traps", [
|
|
||||||
JunkFillPercentage,
|
|
||||||
TrapFillPercentage,
|
|
||||||
OmochaoTrapWeight,
|
|
||||||
TimestopTrapWeight,
|
|
||||||
ConfusionTrapWeight,
|
|
||||||
TinyTrapWeight,
|
|
||||||
GravityTrapWeight,
|
|
||||||
ExpositionTrapWeight,
|
|
||||||
IceTrapWeight,
|
|
||||||
SlowTrapWeight,
|
|
||||||
CutsceneTrapWeight,
|
|
||||||
ReverseTrapWeight,
|
|
||||||
PongTrapWeight,
|
|
||||||
MinigameTrapDifficulty,
|
|
||||||
]),
|
|
||||||
OptionGroup("Speed Missions", [
|
|
||||||
SpeedMissionCount,
|
|
||||||
SpeedMission2,
|
|
||||||
SpeedMission3,
|
|
||||||
SpeedMission4,
|
|
||||||
SpeedMission5,
|
|
||||||
]),
|
|
||||||
OptionGroup("Mech Missions", [
|
|
||||||
MechMissionCount,
|
|
||||||
MechMission2,
|
|
||||||
MechMission3,
|
|
||||||
MechMission4,
|
|
||||||
MechMission5,
|
|
||||||
]),
|
|
||||||
OptionGroup("Hunt Missions", [
|
|
||||||
HuntMissionCount,
|
|
||||||
HuntMission2,
|
|
||||||
HuntMission3,
|
|
||||||
HuntMission4,
|
|
||||||
HuntMission5,
|
|
||||||
]),
|
|
||||||
OptionGroup("Kart Missions", [
|
|
||||||
KartMissionCount,
|
|
||||||
KartMission2,
|
|
||||||
KartMission3,
|
|
||||||
KartMission4,
|
|
||||||
KartMission5,
|
|
||||||
]),
|
|
||||||
OptionGroup("Cannon's Core Missions", [
|
|
||||||
CannonsCoreMissionCount,
|
|
||||||
CannonsCoreMission2,
|
|
||||||
CannonsCoreMission3,
|
|
||||||
CannonsCoreMission4,
|
|
||||||
CannonsCoreMission5,
|
|
||||||
]),
|
|
||||||
OptionGroup("Aesthetics", [
|
|
||||||
SADXMusic,
|
|
||||||
MusicShuffle,
|
|
||||||
VoiceShuffle,
|
|
||||||
Narrator,
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
"mission_shuffle": MissionShuffle,
|
||||||
|
"boss_rush_shuffle": BossRushShuffle,
|
||||||
|
|
||||||
@dataclass
|
"keysanity": Keysanity,
|
||||||
class SA2BOptions(PerGameCommonOptions):
|
"whistlesanity": Whistlesanity,
|
||||||
goal: Goal
|
"beetlesanity": Beetlesanity,
|
||||||
boss_rush_shuffle: BossRushShuffle
|
"omosanity": Omosanity,
|
||||||
logic_difficulty: LogicDifficulty
|
"animalsanity": Animalsanity,
|
||||||
required_rank: RequiredRank
|
"kart_race_checks": KartRaceChecks,
|
||||||
max_emblem_cap: MaximumEmblemCap
|
|
||||||
ring_loss: RingLoss
|
|
||||||
|
|
||||||
mission_shuffle: MissionShuffle
|
"logic_difficulty": LogicDifficulty,
|
||||||
required_cannons_core_missions: RequiredCannonsCoreMissions
|
"required_rank": RequiredRank,
|
||||||
emblem_percentage_for_cannons_core: EmblemPercentageForCannonsCore
|
"required_cannons_core_missions": RequiredCannonsCoreMissions,
|
||||||
number_of_level_gates: NumberOfLevelGates
|
|
||||||
level_gate_distribution: LevelGateDistribution
|
|
||||||
level_gate_costs: LevelGateCosts
|
|
||||||
|
|
||||||
keysanity: Keysanity
|
"emblem_percentage_for_cannons_core": EmblemPercentageForCannonsCore,
|
||||||
whistlesanity: Whistlesanity
|
"number_of_level_gates": NumberOfLevelGates,
|
||||||
beetlesanity: Beetlesanity
|
"level_gate_distribution": LevelGateDistribution,
|
||||||
omosanity: Omosanity
|
"level_gate_costs": LevelGateCosts,
|
||||||
animalsanity: Animalsanity
|
"max_emblem_cap": MaximumEmblemCap,
|
||||||
kart_race_checks: KartRaceChecks
|
|
||||||
|
|
||||||
black_market_slots: BlackMarketSlots
|
"chao_race_difficulty": ChaoRaceDifficulty,
|
||||||
black_market_unlock_costs: BlackMarketUnlockCosts
|
"chao_karate_difficulty": ChaoKarateDifficulty,
|
||||||
black_market_price_multiplier: BlackMarketPriceMultiplier
|
"chao_stadium_checks": ChaoStadiumChecks,
|
||||||
chao_race_difficulty: ChaoRaceDifficulty
|
"chao_stats": ChaoStats,
|
||||||
chao_karate_difficulty: ChaoKarateDifficulty
|
"chao_stats_frequency": ChaoStatsFrequency,
|
||||||
chao_stadium_checks: ChaoStadiumChecks
|
"chao_stats_stamina": ChaoStatsStamina,
|
||||||
chao_animal_parts: ChaoAnimalParts
|
"chao_stats_hidden": ChaoStatsHidden,
|
||||||
chao_stats: ChaoStats
|
"chao_animal_parts": ChaoAnimalParts,
|
||||||
chao_stats_frequency: ChaoStatsFrequency
|
"chao_kindergarten": ChaoKindergarten,
|
||||||
chao_stats_stamina: ChaoStatsStamina
|
"black_market_slots": BlackMarketSlots,
|
||||||
chao_stats_hidden: ChaoStatsHidden
|
"black_market_unlock_costs": BlackMarketUnlockCosts,
|
||||||
chao_kindergarten: ChaoKindergarten
|
"black_market_price_multiplier": BlackMarketPriceMultiplier,
|
||||||
shuffle_starting_chao_eggs: ShuffleStartingChaoEggs
|
"shuffle_starting_chao_eggs": ShuffleStartingChaoEggs,
|
||||||
chao_entrance_randomization: ChaoEntranceRandomization
|
"chao_entrance_randomization": ChaoEntranceRandomization,
|
||||||
|
|
||||||
junk_fill_percentage: JunkFillPercentage
|
"junk_fill_percentage": JunkFillPercentage,
|
||||||
trap_fill_percentage: TrapFillPercentage
|
"trap_fill_percentage": TrapFillPercentage,
|
||||||
omochao_trap_weight: OmochaoTrapWeight
|
"omochao_trap_weight": OmochaoTrapWeight,
|
||||||
timestop_trap_weight: TimestopTrapWeight
|
"timestop_trap_weight": TimestopTrapWeight,
|
||||||
confusion_trap_weight: ConfusionTrapWeight
|
"confusion_trap_weight": ConfusionTrapWeight,
|
||||||
tiny_trap_weight: TinyTrapWeight
|
"tiny_trap_weight": TinyTrapWeight,
|
||||||
gravity_trap_weight: GravityTrapWeight
|
"gravity_trap_weight": GravityTrapWeight,
|
||||||
exposition_trap_weight: ExpositionTrapWeight
|
"exposition_trap_weight": ExpositionTrapWeight,
|
||||||
#darkness_trap_weight: DarknessTrapWeight
|
#"darkness_trap_weight": DarknessTrapWeight,
|
||||||
ice_trap_weight: IceTrapWeight
|
"ice_trap_weight": IceTrapWeight,
|
||||||
slow_trap_weight: SlowTrapWeight
|
"slow_trap_weight": SlowTrapWeight,
|
||||||
cutscene_trap_weight: CutsceneTrapWeight
|
"cutscene_trap_weight": CutsceneTrapWeight,
|
||||||
reverse_trap_weight: ReverseTrapWeight
|
"reverse_trap_weight": ReverseTrapWeight,
|
||||||
pong_trap_weight: PongTrapWeight
|
"pong_trap_weight": PongTrapWeight,
|
||||||
minigame_trap_difficulty: MinigameTrapDifficulty
|
"minigame_trap_difficulty": MinigameTrapDifficulty,
|
||||||
|
|
||||||
sadx_music: SADXMusic
|
"sadx_music": SADXMusic,
|
||||||
music_shuffle: MusicShuffle
|
"music_shuffle": MusicShuffle,
|
||||||
voice_shuffle: VoiceShuffle
|
"voice_shuffle": VoiceShuffle,
|
||||||
narrator: Narrator
|
"narrator": Narrator,
|
||||||
|
"ring_loss": RingLoss,
|
||||||
|
|
||||||
speed_mission_count: SpeedMissionCount
|
"speed_mission_count": SpeedMissionCount,
|
||||||
speed_mission_2: SpeedMission2
|
"speed_mission_2": SpeedMission2,
|
||||||
speed_mission_3: SpeedMission3
|
"speed_mission_3": SpeedMission3,
|
||||||
speed_mission_4: SpeedMission4
|
"speed_mission_4": SpeedMission4,
|
||||||
speed_mission_5: SpeedMission5
|
"speed_mission_5": SpeedMission5,
|
||||||
|
|
||||||
mech_mission_count: MechMissionCount
|
"mech_mission_count": MechMissionCount,
|
||||||
mech_mission_2: MechMission2
|
"mech_mission_2": MechMission2,
|
||||||
mech_mission_3: MechMission3
|
"mech_mission_3": MechMission3,
|
||||||
mech_mission_4: MechMission4
|
"mech_mission_4": MechMission4,
|
||||||
mech_mission_5: MechMission5
|
"mech_mission_5": MechMission5,
|
||||||
|
|
||||||
hunt_mission_count: HuntMissionCount
|
"hunt_mission_count": HuntMissionCount,
|
||||||
hunt_mission_2: HuntMission2
|
"hunt_mission_2": HuntMission2,
|
||||||
hunt_mission_3: HuntMission3
|
"hunt_mission_3": HuntMission3,
|
||||||
hunt_mission_4: HuntMission4
|
"hunt_mission_4": HuntMission4,
|
||||||
hunt_mission_5: HuntMission5
|
"hunt_mission_5": HuntMission5,
|
||||||
|
|
||||||
kart_mission_count: KartMissionCount
|
"kart_mission_count": KartMissionCount,
|
||||||
kart_mission_2: KartMission2
|
"kart_mission_2": KartMission2,
|
||||||
kart_mission_3: KartMission3
|
"kart_mission_3": KartMission3,
|
||||||
kart_mission_4: KartMission4
|
"kart_mission_4": KartMission4,
|
||||||
kart_mission_5: KartMission5
|
"kart_mission_5": KartMission5,
|
||||||
|
|
||||||
cannons_core_mission_count: CannonsCoreMissionCount
|
"cannons_core_mission_count": CannonsCoreMissionCount,
|
||||||
cannons_core_mission_2: CannonsCoreMission2
|
"cannons_core_mission_2": CannonsCoreMission2,
|
||||||
cannons_core_mission_3: CannonsCoreMission3
|
"cannons_core_mission_3": CannonsCoreMission3,
|
||||||
cannons_core_mission_4: CannonsCoreMission4
|
"cannons_core_mission_4": CannonsCoreMission4,
|
||||||
cannons_core_mission_5: CannonsCoreMission5
|
"cannons_core_mission_5": CannonsCoreMission5,
|
||||||
|
|
||||||
ring_link: RingLink
|
"ring_link": RingLink,
|
||||||
death_link: DeathLink
|
"death_link": DeathLink,
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ import math
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
||||||
from worlds.AutoWorld import WebWorld, World
|
|
||||||
|
|
||||||
from .AestheticData import chao_name_conversion, sample_chao_names, totally_real_item_names, \
|
|
||||||
all_exits, all_destinations, multi_rooms, single_rooms, room_to_exits_map, exit_to_room_map, valid_kindergarten_exits
|
|
||||||
from .GateBosses import get_gate_bosses, get_boss_rush_bosses, get_boss_name
|
|
||||||
from .Items import SA2BItem, ItemData, item_table, upgrades_table, emeralds_table, junk_table, trap_table, item_groups, \
|
from .Items import SA2BItem, ItemData, item_table, upgrades_table, emeralds_table, junk_table, trap_table, item_groups, \
|
||||||
eggs_table, fruits_table, seeds_table, hats_table, animals_table, chaos_drives_table
|
eggs_table, fruits_table, seeds_table, hats_table, animals_table, chaos_drives_table
|
||||||
from .Locations import SA2BLocation, all_locations, setup_locations, chao_animal_event_location_table, black_market_location_table
|
from .Locations import SA2BLocation, all_locations, setup_locations, chao_animal_event_location_table, black_market_location_table
|
||||||
from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions
|
from .Options import sa2b_options
|
||||||
from .Names import ItemName, LocationName
|
|
||||||
from .Options import SA2BOptions, sa2b_option_groups
|
|
||||||
from .Regions import create_regions, shuffleable_regions, connect_regions, LevelGate, gate_0_whitelist_regions, \
|
from .Regions import create_regions, shuffleable_regions, connect_regions, LevelGate, gate_0_whitelist_regions, \
|
||||||
gate_0_blacklist_regions
|
gate_0_blacklist_regions
|
||||||
from .Rules import set_rules
|
from .Rules import set_rules
|
||||||
|
from .Names import ItemName, LocationName
|
||||||
|
from .AestheticData import chao_name_conversion, sample_chao_names, totally_real_item_names, \
|
||||||
|
all_exits, all_destinations, multi_rooms, single_rooms, room_to_exits_map, exit_to_room_map, valid_kindergarten_exits
|
||||||
|
from worlds.AutoWorld import WebWorld, World
|
||||||
|
from .GateBosses import get_gate_bosses, get_boss_rush_bosses, get_boss_name
|
||||||
|
from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions
|
||||||
|
import Patch
|
||||||
|
|
||||||
|
|
||||||
class SA2BWeb(WebWorld):
|
class SA2BWeb(WebWorld):
|
||||||
@@ -32,7 +32,6 @@ class SA2BWeb(WebWorld):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tutorials = [setup_en]
|
tutorials = [setup_en]
|
||||||
option_groups = sa2b_option_groups
|
|
||||||
|
|
||||||
|
|
||||||
def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range: int, multiworld: MultiWorld):
|
def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range: int, multiworld: MultiWorld):
|
||||||
@@ -55,8 +54,7 @@ class SA2BWorld(World):
|
|||||||
Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rouge, and Eggman across 31 stages and prevent the destruction of the earth.
|
Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rouge, and Eggman across 31 stages and prevent the destruction of the earth.
|
||||||
"""
|
"""
|
||||||
game: str = "Sonic Adventure 2 Battle"
|
game: str = "Sonic Adventure 2 Battle"
|
||||||
options_dataclass = SA2BOptions
|
option_definitions = sa2b_options
|
||||||
options: SA2BOptions
|
|
||||||
topology_present = False
|
topology_present = False
|
||||||
data_version = 7
|
data_version = 7
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
|
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, PerGameCommonOptions
|
||||||
|
|
||||||
|
|
||||||
class Goal(Choice):
|
class Goal(Choice):
|
||||||
"""
|
"""
|
||||||
Determines the goal of the seed
|
Determines the goal of the seed
|
||||||
|
|
||||||
Bowser: Defeat Koopalings, reach Bowser's Castle and defeat Bowser
|
Bowser: Defeat Koopalings, reach Bowser's Castle and defeat Bowser
|
||||||
|
|
||||||
Yoshi Egg Hunt: Find a certain number of Yoshi Eggs
|
Yoshi Egg Hunt: Find a certain number of Yoshi Eggs
|
||||||
"""
|
"""
|
||||||
display_name = "Goal"
|
display_name = "Goal"
|
||||||
@@ -30,9 +28,7 @@ class BossesRequired(Range):
|
|||||||
class NumberOfYoshiEggs(Range):
|
class NumberOfYoshiEggs(Range):
|
||||||
"""
|
"""
|
||||||
Maximum possible number of Yoshi Eggs that will be in the item pool
|
Maximum possible number of Yoshi Eggs that will be in the item pool
|
||||||
|
|
||||||
If fewer available locations exist in the pool than this number, the number of available locations will be used instead.
|
If fewer available locations exist in the pool than this number, the number of available locations will be used instead.
|
||||||
|
|
||||||
Required Percentage of Yoshi Eggs will be calculated based off of that number.
|
Required Percentage of Yoshi Eggs will be calculated based off of that number.
|
||||||
"""
|
"""
|
||||||
display_name = "Max Number of Yoshi Eggs"
|
display_name = "Max Number of Yoshi Eggs"
|
||||||
@@ -68,9 +64,7 @@ class MoonChecks(Toggle):
|
|||||||
class Hidden1UpChecks(Toggle):
|
class Hidden1UpChecks(Toggle):
|
||||||
"""
|
"""
|
||||||
Whether collecting a hidden 1-Up mushroom in a level will grant a check
|
Whether collecting a hidden 1-Up mushroom in a level will grant a check
|
||||||
|
|
||||||
These checks are considered cryptic as there's no actual indicator that they're in their respective places
|
These checks are considered cryptic as there's no actual indicator that they're in their respective places
|
||||||
|
|
||||||
Enable this option at your own risk
|
Enable this option at your own risk
|
||||||
"""
|
"""
|
||||||
display_name = "Hidden 1-Up Checks"
|
display_name = "Hidden 1-Up Checks"
|
||||||
@@ -86,9 +80,7 @@ class BonusBlockChecks(Toggle):
|
|||||||
class Blocksanity(Toggle):
|
class Blocksanity(Toggle):
|
||||||
"""
|
"""
|
||||||
Whether hitting a block with an item or coin inside will grant a check
|
Whether hitting a block with an item or coin inside will grant a check
|
||||||
|
|
||||||
Note that some blocks are excluded due to how the option and the game works!
|
Note that some blocks are excluded due to how the option and the game works!
|
||||||
|
|
||||||
Exclusion list:
|
Exclusion list:
|
||||||
* Blocks in Top Secret Area & Front Door/Bowser Castle
|
* Blocks in Top Secret Area & Front Door/Bowser Castle
|
||||||
* Blocks that are unreachable unless you glitch your way in
|
* Blocks that are unreachable unless you glitch your way in
|
||||||
@@ -99,15 +91,10 @@ class Blocksanity(Toggle):
|
|||||||
class BowserCastleDoors(Choice):
|
class BowserCastleDoors(Choice):
|
||||||
"""
|
"""
|
||||||
How the doors of Bowser's Castle behave
|
How the doors of Bowser's Castle behave
|
||||||
|
|
||||||
Vanilla: Front and Back Doors behave as vanilla
|
Vanilla: Front and Back Doors behave as vanilla
|
||||||
|
|
||||||
Fast: Both doors behave as the Back Door
|
Fast: Both doors behave as the Back Door
|
||||||
|
|
||||||
Slow: Both doors behave as the Front Door
|
Slow: Both doors behave as the Front Door
|
||||||
|
|
||||||
"Front Door" rooms depend on the `bowser_castle_rooms` option
|
"Front Door" rooms depend on the `bowser_castle_rooms` option
|
||||||
|
|
||||||
"Back Door" only requires going through the dark hallway to Bowser
|
"Back Door" only requires going through the dark hallway to Bowser
|
||||||
"""
|
"""
|
||||||
display_name = "Bowser Castle Doors"
|
display_name = "Bowser Castle Doors"
|
||||||
@@ -120,15 +107,10 @@ class BowserCastleDoors(Choice):
|
|||||||
class BowserCastleRooms(Choice):
|
class BowserCastleRooms(Choice):
|
||||||
"""
|
"""
|
||||||
How the rooms of Bowser's Castle Front Door behave
|
How the rooms of Bowser's Castle Front Door behave
|
||||||
|
|
||||||
Vanilla: You can choose which rooms to enter, as in vanilla
|
Vanilla: You can choose which rooms to enter, as in vanilla
|
||||||
|
|
||||||
Random Two Room: Two random rooms are chosen
|
Random Two Room: Two random rooms are chosen
|
||||||
|
|
||||||
Random Five Room: Five random rooms are chosen
|
Random Five Room: Five random rooms are chosen
|
||||||
|
|
||||||
Gauntlet: All eight rooms must be cleared
|
Gauntlet: All eight rooms must be cleared
|
||||||
|
|
||||||
Labyrinth: Which room leads to Bowser?
|
Labyrinth: Which room leads to Bowser?
|
||||||
"""
|
"""
|
||||||
display_name = "Bowser Castle Rooms"
|
display_name = "Bowser Castle Rooms"
|
||||||
@@ -143,13 +125,9 @@ class BowserCastleRooms(Choice):
|
|||||||
class BossShuffle(Choice):
|
class BossShuffle(Choice):
|
||||||
"""
|
"""
|
||||||
How bosses are shuffled
|
How bosses are shuffled
|
||||||
|
|
||||||
None: Bosses are not shuffled
|
None: Bosses are not shuffled
|
||||||
|
|
||||||
Simple: Four Reznors and the seven Koopalings are shuffled around
|
Simple: Four Reznors and the seven Koopalings are shuffled around
|
||||||
|
|
||||||
Full: Each boss location gets a fully random boss
|
Full: Each boss location gets a fully random boss
|
||||||
|
|
||||||
Singularity: One or two bosses are chosen and placed at every boss location
|
Singularity: One or two bosses are chosen and placed at every boss location
|
||||||
"""
|
"""
|
||||||
display_name = "Boss Shuffle"
|
display_name = "Boss Shuffle"
|
||||||
@@ -170,7 +148,6 @@ class LevelShuffle(Toggle):
|
|||||||
class ExcludeSpecialZone(Toggle):
|
class ExcludeSpecialZone(Toggle):
|
||||||
"""
|
"""
|
||||||
If active, this option will prevent any progression items from being placed in Special Zone levels.
|
If active, this option will prevent any progression items from being placed in Special Zone levels.
|
||||||
|
|
||||||
Additionally, if Level Shuffle is active, Special Zone levels will not be shuffled away from their vanilla tiles.
|
Additionally, if Level Shuffle is active, Special Zone levels will not be shuffled away from their vanilla tiles.
|
||||||
"""
|
"""
|
||||||
display_name = "Exclude Special Zone"
|
display_name = "Exclude Special Zone"
|
||||||
@@ -178,10 +155,9 @@ class ExcludeSpecialZone(Toggle):
|
|||||||
|
|
||||||
class SwapDonutGhostHouseExits(Toggle):
|
class SwapDonutGhostHouseExits(Toggle):
|
||||||
"""
|
"""
|
||||||
If enabled, this option will swap which overworld direction the two exits of the level at the Donut Ghost House overworld tile go:
|
If enabled, this option will swap which overworld direction the two exits of the level at the Donut Ghost House
|
||||||
|
overworld tile go:
|
||||||
False: Normal Exit goes up, Secret Exit goes right.
|
False: Normal Exit goes up, Secret Exit goes right.
|
||||||
|
|
||||||
True: Normal Exit goes right, Secret Exit goes up.
|
True: Normal Exit goes right, Secret Exit goes up.
|
||||||
"""
|
"""
|
||||||
display_name = "Swap Donut GH Exits"
|
display_name = "Swap Donut GH Exits"
|
||||||
@@ -282,7 +258,6 @@ class Autosave(DefaultOnToggle):
|
|||||||
class EarlyClimb(Toggle):
|
class EarlyClimb(Toggle):
|
||||||
"""
|
"""
|
||||||
Force Climb to appear early in the seed as a local item.
|
Force Climb to appear early in the seed as a local item.
|
||||||
|
|
||||||
This is particularly useful to prevent BK when Level Shuffle is disabled
|
This is particularly useful to prevent BK when Level Shuffle is disabled
|
||||||
"""
|
"""
|
||||||
display_name = "Early Climb"
|
display_name = "Early Climb"
|
||||||
@@ -302,13 +277,9 @@ class OverworldSpeed(Choice):
|
|||||||
class MusicShuffle(Choice):
|
class MusicShuffle(Choice):
|
||||||
"""
|
"""
|
||||||
Music shuffle type
|
Music shuffle type
|
||||||
|
|
||||||
None: No Music is shuffled
|
None: No Music is shuffled
|
||||||
|
|
||||||
Consistent: Each music track is consistently shuffled throughout the game
|
Consistent: Each music track is consistently shuffled throughout the game
|
||||||
|
|
||||||
Full: Each individual level has a random music track
|
Full: Each individual level has a random music track
|
||||||
|
|
||||||
Singularity: The entire game uses one song for overworld and one song for levels
|
Singularity: The entire game uses one song for overworld and one song for levels
|
||||||
"""
|
"""
|
||||||
display_name = "Music Shuffle"
|
display_name = "Music Shuffle"
|
||||||
@@ -322,13 +293,9 @@ class MusicShuffle(Choice):
|
|||||||
class SFXShuffle(Choice):
|
class SFXShuffle(Choice):
|
||||||
"""
|
"""
|
||||||
Shuffles almost every instance of sound effect playback
|
Shuffles almost every instance of sound effect playback
|
||||||
|
|
||||||
Archipelago elements that play sound effects aren't randomized
|
Archipelago elements that play sound effects aren't randomized
|
||||||
|
|
||||||
None: No SFX are shuffled
|
None: No SFX are shuffled
|
||||||
|
|
||||||
Full: Each individual SFX call has a random SFX
|
Full: Each individual SFX call has a random SFX
|
||||||
|
|
||||||
Singularity: The entire game uses one SFX for every SFX call
|
Singularity: The entire game uses one SFX for every SFX call
|
||||||
"""
|
"""
|
||||||
display_name = "Sound Effect Shuffle"
|
display_name = "Sound Effect Shuffle"
|
||||||
@@ -357,11 +324,8 @@ class MarioPalette(Choice):
|
|||||||
class LevelPaletteShuffle(Choice):
|
class LevelPaletteShuffle(Choice):
|
||||||
"""
|
"""
|
||||||
Whether to shuffle level palettes
|
Whether to shuffle level palettes
|
||||||
|
|
||||||
Off: Do not shuffle palettes
|
Off: Do not shuffle palettes
|
||||||
|
|
||||||
On Legacy: Uses only the palette sets from the original game
|
On Legacy: Uses only the palette sets from the original game
|
||||||
|
|
||||||
On Curated: Uses custom, hand-crafted palette sets
|
On Curated: Uses custom, hand-crafted palette sets
|
||||||
"""
|
"""
|
||||||
display_name = "Level Palette Shuffle"
|
display_name = "Level Palette Shuffle"
|
||||||
@@ -374,11 +338,8 @@ class LevelPaletteShuffle(Choice):
|
|||||||
class OverworldPaletteShuffle(Choice):
|
class OverworldPaletteShuffle(Choice):
|
||||||
"""
|
"""
|
||||||
Whether to shuffle overworld palettes
|
Whether to shuffle overworld palettes
|
||||||
|
|
||||||
Off: Do not shuffle palettes
|
Off: Do not shuffle palettes
|
||||||
|
|
||||||
On Legacy: Uses only the palette sets from the original game
|
On Legacy: Uses only the palette sets from the original game
|
||||||
|
|
||||||
On Curated: Uses custom, hand-crafted palette sets
|
On Curated: Uses custom, hand-crafted palette sets
|
||||||
"""
|
"""
|
||||||
display_name = "Overworld Palette Shuffle"
|
display_name = "Overworld Palette Shuffle"
|
||||||
@@ -398,52 +359,6 @@ class StartingLifeCount(Range):
|
|||||||
default = 5
|
default = 5
|
||||||
|
|
||||||
|
|
||||||
smw_option_groups = [
|
|
||||||
OptionGroup("Goal Options", [
|
|
||||||
Goal,
|
|
||||||
BossesRequired,
|
|
||||||
NumberOfYoshiEggs,
|
|
||||||
PercentageOfYoshiEggs,
|
|
||||||
]),
|
|
||||||
OptionGroup("Sanity Options", [
|
|
||||||
DragonCoinChecks,
|
|
||||||
MoonChecks,
|
|
||||||
Hidden1UpChecks,
|
|
||||||
BonusBlockChecks,
|
|
||||||
Blocksanity,
|
|
||||||
]),
|
|
||||||
OptionGroup("Level Shuffling", [
|
|
||||||
LevelShuffle,
|
|
||||||
ExcludeSpecialZone,
|
|
||||||
BowserCastleDoors,
|
|
||||||
BowserCastleRooms,
|
|
||||||
BossShuffle,
|
|
||||||
SwapDonutGhostHouseExits,
|
|
||||||
]),
|
|
||||||
OptionGroup("Junk and Traps", [
|
|
||||||
JunkFillPercentage,
|
|
||||||
TrapFillPercentage,
|
|
||||||
IceTrapWeight,
|
|
||||||
StunTrapWeight,
|
|
||||||
LiteratureTrapWeight,
|
|
||||||
TimerTrapWeight,
|
|
||||||
ReverseTrapWeight,
|
|
||||||
ThwimpTrapWeight,
|
|
||||||
]),
|
|
||||||
OptionGroup("Aesthetics", [
|
|
||||||
DisplayReceivedItemPopups,
|
|
||||||
Autosave,
|
|
||||||
OverworldSpeed,
|
|
||||||
MusicShuffle,
|
|
||||||
SFXShuffle,
|
|
||||||
MarioPalette,
|
|
||||||
LevelPaletteShuffle,
|
|
||||||
OverworldPaletteShuffle,
|
|
||||||
StartingLifeCount,
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SMWOptions(PerGameCommonOptions):
|
class SMWOptions(PerGameCommonOptions):
|
||||||
death_link: DeathLink
|
death_link: DeathLink
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
from typing import Dict, Any
|
|
||||||
|
|
||||||
all_random = {
|
|
||||||
"goal": "random",
|
|
||||||
"bosses_required": "random",
|
|
||||||
"max_yoshi_egg_cap": "random",
|
|
||||||
"percentage_of_yoshi_eggs": "random",
|
|
||||||
"dragon_coin_checks": "random",
|
|
||||||
"moon_checks": "random",
|
|
||||||
"hidden_1up_checks": "random",
|
|
||||||
"bonus_block_checks": "random",
|
|
||||||
"blocksanity": "random",
|
|
||||||
"bowser_castle_doors": "random",
|
|
||||||
"bowser_castle_rooms": "random",
|
|
||||||
"level_shuffle": "random",
|
|
||||||
"exclude_special_zone": "random",
|
|
||||||
"boss_shuffle": "random",
|
|
||||||
"swap_donut_gh_exits": "random",
|
|
||||||
"display_received_item_popups": "random",
|
|
||||||
"junk_fill_percentage": "random",
|
|
||||||
"trap_fill_percentage": "random",
|
|
||||||
"ice_trap_weight": "random",
|
|
||||||
"stun_trap_weight": "random",
|
|
||||||
"literature_trap_weight": "random",
|
|
||||||
"timer_trap_weight": "random",
|
|
||||||
"reverse_trap_weight": "random",
|
|
||||||
"thwimp_trap_weight": "random",
|
|
||||||
"autosave": "random",
|
|
||||||
"early_climb": "random",
|
|
||||||
"overworld_speed": "random",
|
|
||||||
"music_shuffle": "random",
|
|
||||||
"sfx_shuffle": "random",
|
|
||||||
"mario_palette": "random",
|
|
||||||
"level_palette_shuffle": "random",
|
|
||||||
"overworld_palette_shuffle": "random",
|
|
||||||
"starting_life_count": "random",
|
|
||||||
}
|
|
||||||
|
|
||||||
allsanity = {
|
|
||||||
"dragon_coin_checks": True,
|
|
||||||
"moon_checks": True,
|
|
||||||
"hidden_1up_checks": True,
|
|
||||||
"bonus_block_checks": True,
|
|
||||||
"blocksanity": True,
|
|
||||||
"level_shuffle": True,
|
|
||||||
"boss_shuffle": "full",
|
|
||||||
"music_shuffle": "full",
|
|
||||||
"sfx_shuffle": "full",
|
|
||||||
"mario_palette": "random",
|
|
||||||
"level_palette_shuffle": "on_curated",
|
|
||||||
"overworld_palette_shuffle": "on_curated",
|
|
||||||
}
|
|
||||||
|
|
||||||
smw_options_presets: Dict[str, Dict[str, Any]] = {
|
|
||||||
"All Random": all_random,
|
|
||||||
"Allsanity": allsanity,
|
|
||||||
}
|
|
||||||
@@ -6,19 +6,17 @@ import settings
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
||||||
from worlds.AutoWorld import WebWorld, World
|
|
||||||
from worlds.generic.Rules import add_rule, exclusion_rules
|
|
||||||
|
|
||||||
from .Client import SMWSNIClient
|
|
||||||
from .Items import SMWItem, ItemData, item_table, junk_table
|
from .Items import SMWItem, ItemData, item_table, junk_table
|
||||||
from .Levels import full_level_list, generate_level_list, location_id_to_level_id
|
|
||||||
from .Locations import SMWLocation, all_locations, setup_locations, special_zone_level_names, special_zone_dragon_coin_names, special_zone_hidden_1up_names, special_zone_blocksanity_names
|
from .Locations import SMWLocation, all_locations, setup_locations, special_zone_level_names, special_zone_dragon_coin_names, special_zone_hidden_1up_names, special_zone_blocksanity_names
|
||||||
from .Names import ItemName, LocationName
|
from .Options import SMWOptions
|
||||||
from .Options import SMWOptions, smw_option_groups
|
|
||||||
from .Presets import smw_options_presets
|
|
||||||
from .Regions import create_regions, connect_regions
|
from .Regions import create_regions, connect_regions
|
||||||
from .Rom import LocalRom, patch_rom, get_base_rom_path, SMWDeltaPatch
|
from .Levels import full_level_list, generate_level_list, location_id_to_level_id
|
||||||
from .Rules import set_rules
|
from .Rules import set_rules
|
||||||
|
from worlds.generic.Rules import add_rule, exclusion_rules
|
||||||
|
from .Names import ItemName, LocationName
|
||||||
|
from .Client import SMWSNIClient
|
||||||
|
from worlds.AutoWorld import WebWorld, World
|
||||||
|
from .Rom import LocalRom, patch_rom, get_base_rom_path, SMWDeltaPatch
|
||||||
|
|
||||||
|
|
||||||
class SMWSettings(settings.Group):
|
class SMWSettings(settings.Group):
|
||||||
@@ -45,9 +43,6 @@ class SMWWeb(WebWorld):
|
|||||||
|
|
||||||
tutorials = [setup_en]
|
tutorials = [setup_en]
|
||||||
|
|
||||||
option_groups = smw_option_groups
|
|
||||||
options_presets = smw_options_presets
|
|
||||||
|
|
||||||
|
|
||||||
class SMWWorld(World):
|
class SMWWorld(World):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from .locations import location_table, create_locations, LocationData, locations
|
|||||||
from .logic.bundle_logic import BundleLogic
|
from .logic.bundle_logic import BundleLogic
|
||||||
from .logic.logic import StardewLogic
|
from .logic.logic import StardewLogic
|
||||||
from .logic.time_logic import MAX_MONTHS
|
from .logic.time_logic import MAX_MONTHS
|
||||||
from .option_groups import sv_option_groups
|
|
||||||
from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \
|
from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \
|
||||||
BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization
|
BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization
|
||||||
from .presets import sv_options_presets
|
from .presets import sv_options_presets
|
||||||
@@ -40,7 +39,6 @@ class StardewWebWorld(WebWorld):
|
|||||||
theme = "dirt"
|
theme = "dirt"
|
||||||
bug_report_page = "https://github.com/agilbert1412/StardewArchipelago/issues/new?labels=bug&title=%5BBug%5D%3A+Brief+Description+of+bug+here"
|
bug_report_page = "https://github.com/agilbert1412/StardewArchipelago/issues/new?labels=bug&title=%5BBug%5D%3A+Brief+Description+of+bug+here"
|
||||||
options_presets = sv_options_presets
|
options_presets = sv_options_presets
|
||||||
option_groups = sv_option_groups
|
|
||||||
|
|
||||||
tutorials = [
|
tutorials = [
|
||||||
Tutorial(
|
Tutorial(
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
from Options import OptionGroup, DeathLink, ProgressionBalancing, Accessibility
|
|
||||||
from .options import (Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice,
|
|
||||||
EntranceRandomization, SeasonRandomization, Cropsanity, BackpackProgression,
|
|
||||||
ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression,
|
|
||||||
FestivalLocations, ArcadeMachineLocations, SpecialOrderLocations,
|
|
||||||
QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize,
|
|
||||||
NumberOfMovementBuffs, NumberOfLuckBuffs, ExcludeGingerIsland, TrapItems,
|
|
||||||
MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier,
|
|
||||||
FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, FarmType,
|
|
||||||
Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Mods)
|
|
||||||
|
|
||||||
sv_option_groups = [
|
|
||||||
OptionGroup("General", [
|
|
||||||
Goal,
|
|
||||||
FarmType,
|
|
||||||
BundleRandomization,
|
|
||||||
BundlePrice,
|
|
||||||
EntranceRandomization,
|
|
||||||
ExcludeGingerIsland,
|
|
||||||
]),
|
|
||||||
OptionGroup("Major Unlocks", [
|
|
||||||
SeasonRandomization,
|
|
||||||
Cropsanity,
|
|
||||||
BackpackProgression,
|
|
||||||
ToolProgression,
|
|
||||||
ElevatorProgression,
|
|
||||||
SkillProgression,
|
|
||||||
BuildingProgression,
|
|
||||||
]),
|
|
||||||
OptionGroup("Extra Shuffling", [
|
|
||||||
FestivalLocations,
|
|
||||||
ArcadeMachineLocations,
|
|
||||||
SpecialOrderLocations,
|
|
||||||
QuestLocations,
|
|
||||||
Fishsanity,
|
|
||||||
Museumsanity,
|
|
||||||
Friendsanity,
|
|
||||||
FriendsanityHeartSize,
|
|
||||||
Monstersanity,
|
|
||||||
Shipsanity,
|
|
||||||
Cooksanity,
|
|
||||||
Chefsanity,
|
|
||||||
Craftsanity,
|
|
||||||
]),
|
|
||||||
OptionGroup("Multipliers and Buffs", [
|
|
||||||
StartingMoney,
|
|
||||||
ProfitMargin,
|
|
||||||
ExperienceMultiplier,
|
|
||||||
FriendshipMultiplier,
|
|
||||||
DebrisMultiplier,
|
|
||||||
NumberOfMovementBuffs,
|
|
||||||
NumberOfLuckBuffs,
|
|
||||||
TrapItems,
|
|
||||||
MultipleDaySleepEnabled,
|
|
||||||
MultipleDaySleepCost,
|
|
||||||
QuickStart,
|
|
||||||
]),
|
|
||||||
OptionGroup("Advanced Options", [
|
|
||||||
Gifting,
|
|
||||||
DeathLink,
|
|
||||||
Mods,
|
|
||||||
ProgressionBalancing,
|
|
||||||
Accessibility,
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
@@ -697,6 +697,8 @@ class Mods(OptionSet):
|
|||||||
class StardewValleyOptions(PerGameCommonOptions):
|
class StardewValleyOptions(PerGameCommonOptions):
|
||||||
goal: Goal
|
goal: Goal
|
||||||
farm_type: FarmType
|
farm_type: FarmType
|
||||||
|
starting_money: StartingMoney
|
||||||
|
profit_margin: ProfitMargin
|
||||||
bundle_randomization: BundleRandomization
|
bundle_randomization: BundleRandomization
|
||||||
bundle_price: BundlePrice
|
bundle_price: BundlePrice
|
||||||
entrance_randomization: EntranceRandomization
|
entrance_randomization: EntranceRandomization
|
||||||
@@ -720,18 +722,16 @@ class StardewValleyOptions(PerGameCommonOptions):
|
|||||||
craftsanity: Craftsanity
|
craftsanity: Craftsanity
|
||||||
friendsanity: Friendsanity
|
friendsanity: Friendsanity
|
||||||
friendsanity_heart_size: FriendsanityHeartSize
|
friendsanity_heart_size: FriendsanityHeartSize
|
||||||
exclude_ginger_island: ExcludeGingerIsland
|
|
||||||
quick_start: QuickStart
|
|
||||||
starting_money: StartingMoney
|
|
||||||
profit_margin: ProfitMargin
|
|
||||||
experience_multiplier: ExperienceMultiplier
|
|
||||||
friendship_multiplier: FriendshipMultiplier
|
|
||||||
debris_multiplier: DebrisMultiplier
|
|
||||||
movement_buff_number: NumberOfMovementBuffs
|
movement_buff_number: NumberOfMovementBuffs
|
||||||
luck_buff_number: NumberOfLuckBuffs
|
luck_buff_number: NumberOfLuckBuffs
|
||||||
|
exclude_ginger_island: ExcludeGingerIsland
|
||||||
trap_items: TrapItems
|
trap_items: TrapItems
|
||||||
multiple_day_sleep_enabled: MultipleDaySleepEnabled
|
multiple_day_sleep_enabled: MultipleDaySleepEnabled
|
||||||
multiple_day_sleep_cost: MultipleDaySleepCost
|
multiple_day_sleep_cost: MultipleDaySleepCost
|
||||||
|
experience_multiplier: ExperienceMultiplier
|
||||||
|
friendship_multiplier: FriendshipMultiplier
|
||||||
|
debris_multiplier: DebrisMultiplier
|
||||||
|
quick_start: QuickStart
|
||||||
gifting: Gifting
|
gifting: Gifting
|
||||||
mods: Mods
|
mods: Mods
|
||||||
death_link: DeathLink
|
death_link: DeathLink
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class FillerItemsDistribution(ItemDict):
|
|||||||
"""Random chance weights of various filler resources that can be obtained.
|
"""Random chance weights of various filler resources that can be obtained.
|
||||||
Available items: """
|
Available items: """
|
||||||
__doc__ += ", ".join(f"\"{item_name}\"" for item_name in item_names_by_type[ItemType.resource])
|
__doc__ += ", ".join(f"\"{item_name}\"" for item_name in item_names_by_type[ItemType.resource])
|
||||||
valid_keys = sorted(item_names_by_type[ItemType.resource])
|
_valid_keys = frozenset(item_names_by_type[ItemType.resource])
|
||||||
default = {item_name: 1 for item_name in item_names_by_type[ItemType.resource]}
|
default = {item_name: 1 for item_name in item_names_by_type[ItemType.resource]}
|
||||||
display_name = "Filler Items Distribution"
|
display_name = "Filler Items Distribution"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from .er_rules import set_er_location_rules
|
|||||||
from .regions import tunic_regions
|
from .regions import tunic_regions
|
||||||
from .er_scripts import create_er_regions
|
from .er_scripts import create_er_regions
|
||||||
from .er_data import portal_mapping
|
from .er_data import portal_mapping
|
||||||
from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets
|
from .options import TunicOptions, EntranceRando
|
||||||
from worlds.AutoWorld import WebWorld, World
|
from worlds.AutoWorld import WebWorld, World
|
||||||
from worlds.generic import PlandoConnection
|
from worlds.generic import PlandoConnection
|
||||||
from decimal import Decimal, ROUND_HALF_UP
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
@@ -27,8 +27,6 @@ class TunicWeb(WebWorld):
|
|||||||
]
|
]
|
||||||
theme = "grassFlowers"
|
theme = "grassFlowers"
|
||||||
game = "TUNIC"
|
game = "TUNIC"
|
||||||
option_groups = tunic_option_groups
|
|
||||||
options_presets = tunic_option_presets
|
|
||||||
|
|
||||||
|
|
||||||
class TunicItem(Item):
|
class TunicItem(Item):
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ Download [BepInEx](https://github.com/BepInEx/BepInEx/releases/download/v6.0.0-p
|
|||||||
|
|
||||||
If playing on Steam Deck, follow this [guide to set up BepInEx via Proton](https://docs.bepinex.dev/articles/advanced/proton_wine.html).
|
If playing on Steam Deck, follow this [guide to set up BepInEx via Proton](https://docs.bepinex.dev/articles/advanced/proton_wine.html).
|
||||||
|
|
||||||
If playing on Linux, you may be able to add `WINEDLLOVERRIDES="winhttp=n,b" %command%` to your Steam launch options. If this does not work, follow the guide for Steam Deck above.
|
|
||||||
|
|
||||||
Extract the contents of the BepInEx .zip file into your TUNIC game directory:<br>
|
Extract the contents of the BepInEx .zip file into your TUNIC game directory:<br>
|
||||||
- **Steam**: Steam\steamapps\common\TUNIC<br>
|
- **Steam**: Steam\steamapps\common\TUNIC<br>
|
||||||
- **PC Game Pass**: XboxGames\Tunic\Content<br>
|
- **PC Game Pass**: XboxGames\Tunic\Content<br>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -268,8 +268,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
|
|||||||
connecting_region=regions["Overworld Well Ladder"],
|
connecting_region=regions["Overworld Well Ladder"],
|
||||||
rule=lambda state: has_ladder("Ladders in Well", state, player, options))
|
rule=lambda state: has_ladder("Ladders in Well", state, player, options))
|
||||||
regions["Overworld Well Ladder"].connect(
|
regions["Overworld Well Ladder"].connect(
|
||||||
connecting_region=regions["Overworld"],
|
connecting_region=regions["Overworld"])
|
||||||
rule=lambda state: has_ladder("Ladders in Well", state, player, options))
|
|
||||||
|
|
||||||
# nmg: can ice grapple through the door
|
# nmg: can ice grapple through the door
|
||||||
regions["Overworld"].connect(
|
regions["Overworld"].connect(
|
||||||
@@ -707,18 +706,17 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
|
|||||||
connecting_region=regions["Fortress Exterior from Overworld"])
|
connecting_region=regions["Fortress Exterior from Overworld"])
|
||||||
|
|
||||||
regions["Beneath the Vault Ladder Exit"].connect(
|
regions["Beneath the Vault Ladder Exit"].connect(
|
||||||
connecting_region=regions["Beneath the Vault Main"],
|
connecting_region=regions["Beneath the Vault Front"],
|
||||||
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)
|
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options))
|
||||||
and has_lantern(state, player, options))
|
regions["Beneath the Vault Front"].connect(
|
||||||
regions["Beneath the Vault Main"].connect(
|
|
||||||
connecting_region=regions["Beneath the Vault Ladder Exit"],
|
connecting_region=regions["Beneath the Vault Ladder Exit"],
|
||||||
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options))
|
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options))
|
||||||
|
|
||||||
regions["Beneath the Vault Main"].connect(
|
regions["Beneath the Vault Front"].connect(
|
||||||
connecting_region=regions["Beneath the Vault Back"])
|
connecting_region=regions["Beneath the Vault Back"],
|
||||||
regions["Beneath the Vault Back"].connect(
|
|
||||||
connecting_region=regions["Beneath the Vault Main"],
|
|
||||||
rule=lambda state: has_lantern(state, player, options))
|
rule=lambda state: has_lantern(state, player, options))
|
||||||
|
regions["Beneath the Vault Back"].connect(
|
||||||
|
connecting_region=regions["Beneath the Vault Front"])
|
||||||
|
|
||||||
regions["Fortress East Shortcut Upper"].connect(
|
regions["Fortress East Shortcut Upper"].connect(
|
||||||
connecting_region=regions["Fortress East Shortcut Lower"])
|
connecting_region=regions["Fortress East Shortcut Lower"])
|
||||||
@@ -872,9 +870,6 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
|
|||||||
regions["Rooted Ziggurat Portal Room Entrance"].connect(
|
regions["Rooted Ziggurat Portal Room Entrance"].connect(
|
||||||
connecting_region=regions["Rooted Ziggurat Lower Back"])
|
connecting_region=regions["Rooted Ziggurat Lower Back"])
|
||||||
|
|
||||||
regions["Zig Skip Exit"].connect(
|
|
||||||
connecting_region=regions["Rooted Ziggurat Lower Front"])
|
|
||||||
|
|
||||||
regions["Rooted Ziggurat Portal"].connect(
|
regions["Rooted Ziggurat Portal"].connect(
|
||||||
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
|
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
|
||||||
rule=lambda state: state.has("Activate Ziggurat Fuse", player))
|
rule=lambda state: state.has("Activate Ziggurat Fuse", player))
|
||||||
@@ -1458,6 +1453,8 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int])
|
|||||||
# Beneath the Vault
|
# Beneath the Vault
|
||||||
set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player),
|
set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player),
|
||||||
lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player))
|
lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player))
|
||||||
|
set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player),
|
||||||
|
lambda state: has_lantern(state, player, options))
|
||||||
|
|
||||||
# Quarry
|
# Quarry
|
||||||
set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player),
|
set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player),
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from typing import Dict, List, Set, TYPE_CHECKING
|
from typing import Dict, List, Set, TYPE_CHECKING
|
||||||
from BaseClasses import Region, ItemClassification, Item, Location
|
from BaseClasses import Region, ItemClassification, Item, Location
|
||||||
from .locations import location_table
|
from .locations import location_table
|
||||||
from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd
|
from .er_data import Portal, tunic_er_regions, portal_mapping, \
|
||||||
|
dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur
|
||||||
from .er_rules import set_er_region_rules
|
from .er_rules import set_er_region_rules
|
||||||
from .options import EntranceRando
|
from .options import EntranceRando
|
||||||
from worlds.generic import PlandoConnection
|
from worlds.generic import PlandoConnection
|
||||||
from random import Random
|
from random import Random
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import TunicWorld
|
from . import TunicWorld
|
||||||
@@ -95,8 +95,7 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None:
|
|||||||
|
|
||||||
def vanilla_portals() -> Dict[Portal, Portal]:
|
def vanilla_portals() -> Dict[Portal, Portal]:
|
||||||
portal_pairs: Dict[Portal, Portal] = {}
|
portal_pairs: Dict[Portal, Portal] = {}
|
||||||
# we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here
|
portal_map = portal_mapping.copy()
|
||||||
portal_map = [portal for portal in portal_mapping if portal.name != "Ziggurat Lower Falling Entrance"]
|
|
||||||
|
|
||||||
while portal_map:
|
while portal_map:
|
||||||
portal1 = portal_map[0]
|
portal1 = portal_map[0]
|
||||||
@@ -131,13 +130,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||||||
dead_ends: List[Portal] = []
|
dead_ends: List[Portal] = []
|
||||||
two_plus: List[Portal] = []
|
two_plus: List[Portal] = []
|
||||||
player_name = world.multiworld.get_player_name(world.player)
|
player_name = world.multiworld.get_player_name(world.player)
|
||||||
portal_map = portal_mapping.copy()
|
|
||||||
logic_rules = world.options.logic_rules.value
|
logic_rules = world.options.logic_rules.value
|
||||||
fixed_shop = world.options.fixed_shop
|
fixed_shop = world.options.fixed_shop
|
||||||
laurels_location = world.options.laurels_location
|
laurels_location = world.options.laurels_location
|
||||||
traversal_reqs = deepcopy(traversal_requirements)
|
|
||||||
has_laurels = True
|
|
||||||
waterfall_plando = False
|
|
||||||
|
|
||||||
# if it's not one of the EntranceRando options, it's a custom seed
|
# if it's not one of the EntranceRando options, it's a custom seed
|
||||||
if world.options.entrance_rando.value not in EntranceRando.options:
|
if world.options.entrance_rando.value not in EntranceRando.options:
|
||||||
@@ -146,52 +141,37 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||||||
fixed_shop = seed_group["fixed_shop"]
|
fixed_shop = seed_group["fixed_shop"]
|
||||||
laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
|
laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
|
||||||
|
|
||||||
# marking that you don't immediately have laurels
|
|
||||||
if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
|
|
||||||
has_laurels = False
|
|
||||||
|
|
||||||
shop_scenes: Set[str] = set()
|
shop_scenes: Set[str] = set()
|
||||||
shop_count = 6
|
shop_count = 6
|
||||||
if fixed_shop:
|
if fixed_shop:
|
||||||
shop_count = 0
|
shop_count = 1
|
||||||
shop_scenes.add("Overworld Redux")
|
shop_scenes.add("Overworld Redux")
|
||||||
|
|
||||||
|
if not logic_rules:
|
||||||
|
dependent_regions = dependent_regions_restricted
|
||||||
|
elif logic_rules == 1:
|
||||||
|
dependent_regions = dependent_regions_nmg
|
||||||
else:
|
else:
|
||||||
# if fixed shop is off, remove this portal
|
dependent_regions = dependent_regions_ur
|
||||||
for portal in portal_map:
|
|
||||||
if portal.region == "Zig Skip Exit":
|
|
||||||
portal_map.remove(portal)
|
|
||||||
break
|
|
||||||
|
|
||||||
# create separate lists for dead ends and non-dead ends
|
# create separate lists for dead ends and non-dead ends
|
||||||
for portal in portal_map:
|
|
||||||
dead_end_status = tunic_er_regions[portal.region].dead_end
|
|
||||||
if dead_end_status == DeadEnd.free:
|
|
||||||
two_plus.append(portal)
|
|
||||||
elif dead_end_status == DeadEnd.all_cats:
|
|
||||||
dead_ends.append(portal)
|
|
||||||
elif dead_end_status == DeadEnd.restricted:
|
|
||||||
if logic_rules:
|
if logic_rules:
|
||||||
|
for portal in portal_mapping:
|
||||||
|
if tunic_er_regions[portal.region].dead_end == 1:
|
||||||
|
dead_ends.append(portal)
|
||||||
|
else:
|
||||||
two_plus.append(portal)
|
two_plus.append(portal)
|
||||||
else:
|
else:
|
||||||
|
for portal in portal_mapping:
|
||||||
|
if tunic_er_regions[portal.region].dead_end:
|
||||||
dead_ends.append(portal)
|
dead_ends.append(portal)
|
||||||
# these two get special handling
|
|
||||||
elif dead_end_status == DeadEnd.special:
|
|
||||||
if portal.region == "Secret Gathering Place":
|
|
||||||
if laurels_location == "10_fairies":
|
|
||||||
two_plus.append(portal)
|
|
||||||
else:
|
else:
|
||||||
dead_ends.append(portal)
|
|
||||||
if portal.region == "Zig Skip Exit":
|
|
||||||
if fixed_shop:
|
|
||||||
two_plus.append(portal)
|
two_plus.append(portal)
|
||||||
else:
|
|
||||||
dead_ends.append(portal)
|
|
||||||
|
|
||||||
connected_regions: Set[str] = set()
|
connected_regions: Set[str] = set()
|
||||||
# make better start region stuff when/if implementing random start
|
# make better start region stuff when/if implementing random start
|
||||||
start_region = "Overworld"
|
start_region = "Overworld"
|
||||||
connected_regions.add(start_region)
|
connected_regions.update(add_dependent_regions(start_region, logic_rules))
|
||||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
|
|
||||||
|
|
||||||
if world.options.entrance_rando.value in EntranceRando.options:
|
if world.options.entrance_rando.value in EntranceRando.options:
|
||||||
plando_connections = world.multiworld.plando_connections[world.player]
|
plando_connections = world.multiworld.plando_connections[world.player]
|
||||||
@@ -225,17 +205,11 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||||||
non_dead_end_regions.add(region_name)
|
non_dead_end_regions.add(region_name)
|
||||||
elif region_info.dead_end == 2 and logic_rules:
|
elif region_info.dead_end == 2 and logic_rules:
|
||||||
non_dead_end_regions.add(region_name)
|
non_dead_end_regions.add(region_name)
|
||||||
elif region_info.dead_end == 3:
|
|
||||||
if (region_name == "Secret Gathering Place" and laurels_location == "10_fairies") \
|
|
||||||
or (region_name == "Zig Skip Exit" and fixed_shop):
|
|
||||||
non_dead_end_regions.add(region_name)
|
|
||||||
|
|
||||||
if plando_connections:
|
if plando_connections:
|
||||||
for connection in plando_connections:
|
for connection in plando_connections:
|
||||||
p_entrance = connection.entrance
|
p_entrance = connection.entrance
|
||||||
p_exit = connection.exit
|
p_exit = connection.exit
|
||||||
portal1_dead_end = True
|
|
||||||
portal2_dead_end = True
|
|
||||||
|
|
||||||
portal1 = None
|
portal1 = None
|
||||||
portal2 = None
|
portal2 = None
|
||||||
@@ -244,10 +218,8 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||||||
for portal in two_plus:
|
for portal in two_plus:
|
||||||
if p_entrance == portal.name:
|
if p_entrance == portal.name:
|
||||||
portal1 = portal
|
portal1 = portal
|
||||||
portal1_dead_end = False
|
|
||||||
if p_exit == portal.name:
|
if p_exit == portal.name:
|
||||||
portal2 = portal
|
portal2 = portal
|
||||||
portal2_dead_end = False
|
|
||||||
|
|
||||||
# search dead_ends individually since we can't really remove items from two_plus during the loop
|
# search dead_ends individually since we can't really remove items from two_plus during the loop
|
||||||
if portal1:
|
if portal1:
|
||||||
@@ -274,6 +246,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||||||
if portal2:
|
if portal2:
|
||||||
two_plus.remove(portal2)
|
two_plus.remove(portal2)
|
||||||
else:
|
else:
|
||||||
|
# check if portal2 is a dead end
|
||||||
for portal in dead_ends:
|
for portal in dead_ends:
|
||||||
if p_exit == portal.name:
|
if p_exit == portal.name:
|
||||||
portal2 = portal
|
portal2 = portal
|
||||||
@@ -283,7 +256,6 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||||||
portal2 = Portal(name="Shop Portal", region="Shop",
|
portal2 = Portal(name="Shop Portal", region="Shop",
|
||||||
destination="Previous Region", tag="_")
|
destination="Previous Region", tag="_")
|
||||||
shop_count -= 1
|
shop_count -= 1
|
||||||
# need to maintain an even number of portals total
|
|
||||||
if shop_count < 0:
|
if shop_count < 0:
|
||||||
shop_count += 2
|
shop_count += 2
|
||||||
for p in portal_mapping:
|
for p in portal_mapping:
|
||||||
@@ -297,36 +269,48 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||||||
f"plando connections in {player_name}'s YAML.")
|
f"plando connections in {player_name}'s YAML.")
|
||||||
dead_ends.remove(portal2)
|
dead_ends.remove(portal2)
|
||||||
|
|
||||||
# update the traversal chart to say you can get from portal1's region to portal2's and vice versa
|
|
||||||
if not portal1_dead_end and not portal2_dead_end:
|
|
||||||
traversal_reqs.setdefault(portal1.region, dict())[portal2.region] = []
|
|
||||||
traversal_reqs.setdefault(portal2.region, dict())[portal1.region] = []
|
|
||||||
|
|
||||||
if portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit":
|
|
||||||
if portal1_dead_end or portal2_dead_end or \
|
|
||||||
portal1.region == "Secret Gathering Place" or portal2.region == "Secret Gathering Place":
|
|
||||||
if world.options.entrance_rando.value not in EntranceRando.options:
|
|
||||||
raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
|
|
||||||
"end to a dead end in their plando connections.")
|
|
||||||
else:
|
|
||||||
raise Exception(f"{player_name} paired a dead end to a dead end in their "
|
|
||||||
"plando connections.")
|
|
||||||
|
|
||||||
if portal1.region == "Secret Gathering Place" or portal2.region == "Secret Gathering Place":
|
|
||||||
# need to make sure you didn't pair this to a dead end or zig skip
|
|
||||||
if portal1_dead_end or portal2_dead_end or \
|
|
||||||
portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit":
|
|
||||||
if world.options.entrance_rando.value not in EntranceRando.options:
|
|
||||||
raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
|
|
||||||
"end to a dead end in their plando connections.")
|
|
||||||
else:
|
|
||||||
raise Exception(f"{player_name} paired a dead end to a dead end in their "
|
|
||||||
"plando connections.")
|
|
||||||
waterfall_plando = True
|
|
||||||
portal_pairs[portal1] = portal2
|
portal_pairs[portal1] = portal2
|
||||||
|
|
||||||
|
# update dependent regions based on the plando'd connections, to ensure the portals connect well, logically
|
||||||
|
for origins, destinations in dependent_regions.items():
|
||||||
|
if portal1.region in origins:
|
||||||
|
if portal2.region in non_dead_end_regions:
|
||||||
|
destinations.append(portal2.region)
|
||||||
|
if portal2.region in origins:
|
||||||
|
if portal1.region in non_dead_end_regions:
|
||||||
|
destinations.append(portal1.region)
|
||||||
|
|
||||||
# if we have plando connections, our connected regions may change somewhat
|
# if we have plando connections, our connected regions may change somewhat
|
||||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
|
while True:
|
||||||
|
test1 = len(connected_regions)
|
||||||
|
for region in connected_regions.copy():
|
||||||
|
connected_regions.update(add_dependent_regions(region, logic_rules))
|
||||||
|
test2 = len(connected_regions)
|
||||||
|
if test1 == test2:
|
||||||
|
break
|
||||||
|
|
||||||
|
# need to plando fairy cave, or it could end up laurels locked
|
||||||
|
# fix this later to be random after adding some item logic to dependent regions
|
||||||
|
if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
|
||||||
|
portal1 = None
|
||||||
|
portal2 = None
|
||||||
|
for portal in two_plus:
|
||||||
|
if portal.scene_destination() == "Overworld Redux, Waterfall_":
|
||||||
|
portal1 = portal
|
||||||
|
break
|
||||||
|
for portal in dead_ends:
|
||||||
|
if portal.scene_destination() == "Waterfall, Overworld Redux_":
|
||||||
|
portal2 = portal
|
||||||
|
break
|
||||||
|
if not portal1:
|
||||||
|
raise Exception(f"Failed to do Laurels Location at 10 Fairies option. "
|
||||||
|
f"Did {player_name} plando connection the Secret Gathering Place Entrance?")
|
||||||
|
if not portal2:
|
||||||
|
raise Exception(f"Failed to do Laurels Location at 10 Fairies option. "
|
||||||
|
f"Did {player_name} plando connection the Secret Gathering Place Exit?")
|
||||||
|
portal_pairs[portal1] = portal2
|
||||||
|
two_plus.remove(portal1)
|
||||||
|
dead_ends.remove(portal2)
|
||||||
|
|
||||||
if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
|
if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
|
||||||
portal1 = None
|
portal1 = None
|
||||||
@@ -355,54 +339,47 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||||||
previous_conn_num = 0
|
previous_conn_num = 0
|
||||||
fail_count = 0
|
fail_count = 0
|
||||||
while len(connected_regions) < len(non_dead_end_regions):
|
while len(connected_regions) < len(non_dead_end_regions):
|
||||||
# if this is universal tracker, just break immediately and move on
|
|
||||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
|
||||||
break
|
|
||||||
# if the connected regions length stays unchanged for too long, it's stuck in a loop
|
# if the connected regions length stays unchanged for too long, it's stuck in a loop
|
||||||
# should, hopefully, only ever occur if someone plandos connections poorly
|
# should, hopefully, only ever occur if someone plandos connections poorly
|
||||||
|
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||||
|
break
|
||||||
if previous_conn_num == len(connected_regions):
|
if previous_conn_num == len(connected_regions):
|
||||||
fail_count += 1
|
fail_count += 1
|
||||||
if fail_count >= 500:
|
if fail_count >= 500:
|
||||||
raise Exception(f"Failed to pair regions. Check plando connections for {player_name} for errors. "
|
raise Exception(f"Failed to pair regions. Check plando connections for {player_name} for loops.")
|
||||||
"Unconnected regions:", non_dead_end_regions - connected_regions)
|
|
||||||
else:
|
else:
|
||||||
fail_count = 0
|
fail_count = 0
|
||||||
previous_conn_num = len(connected_regions)
|
previous_conn_num = len(connected_regions)
|
||||||
|
|
||||||
# find a portal in a connected region
|
# find a portal in an inaccessible region
|
||||||
if check_success == 0:
|
if check_success == 0:
|
||||||
for portal in two_plus:
|
for portal in two_plus:
|
||||||
if portal.region in connected_regions:
|
if portal.region in connected_regions:
|
||||||
|
# if there's risk of self-locking, start over
|
||||||
|
if gate_before_switch(portal, two_plus):
|
||||||
|
random_object.shuffle(two_plus)
|
||||||
|
break
|
||||||
portal1 = portal
|
portal1 = portal
|
||||||
two_plus.remove(portal)
|
two_plus.remove(portal)
|
||||||
check_success = 1
|
check_success = 1
|
||||||
break
|
break
|
||||||
|
|
||||||
# then we find a portal in an inaccessible region
|
# then we find a portal in a connected region
|
||||||
if check_success == 1:
|
if check_success == 1:
|
||||||
for portal in two_plus:
|
for portal in two_plus:
|
||||||
if portal.region not in connected_regions:
|
if portal.region not in connected_regions:
|
||||||
# if secret gathering place happens to get paired really late, you can end up running out
|
# if there's risk of self-locking, shuffle and try again
|
||||||
if not has_laurels and len(two_plus) < 80:
|
if gate_before_switch(portal, two_plus):
|
||||||
# if you plando'd secret gathering place with laurels at 10 fairies, you're the reason for this
|
random_object.shuffle(two_plus)
|
||||||
if waterfall_plando:
|
break
|
||||||
cr = connected_regions.copy()
|
|
||||||
cr.add(portal.region)
|
|
||||||
if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_rules):
|
|
||||||
continue
|
|
||||||
elif portal.region != "Secret Gathering Place":
|
|
||||||
continue
|
|
||||||
portal2 = portal
|
portal2 = portal
|
||||||
connected_regions.add(portal.region)
|
|
||||||
two_plus.remove(portal)
|
two_plus.remove(portal)
|
||||||
check_success = 2
|
check_success = 2
|
||||||
break
|
break
|
||||||
|
|
||||||
# once we have both portals, connect them and add the new region(s) to connected_regions
|
# once we have both portals, connect them and add the new region(s) to connected_regions
|
||||||
if check_success == 2:
|
if check_success == 2:
|
||||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
|
connected_regions.update(add_dependent_regions(portal2.region, logic_rules))
|
||||||
if "Secret Gathering Place" in connected_regions:
|
|
||||||
has_laurels = True
|
|
||||||
portal_pairs[portal1] = portal2
|
portal_pairs[portal1] = portal2
|
||||||
check_success = 0
|
check_success = 0
|
||||||
random_object.shuffle(two_plus)
|
random_object.shuffle(two_plus)
|
||||||
@@ -434,6 +411,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||||||
portal1 = two_plus.pop()
|
portal1 = two_plus.pop()
|
||||||
portal2 = dead_ends.pop()
|
portal2 = dead_ends.pop()
|
||||||
portal_pairs[portal1] = portal2
|
portal_pairs[portal1] = portal2
|
||||||
|
|
||||||
# then randomly connect the remaining portals to each other
|
# then randomly connect the remaining portals to each other
|
||||||
# every region is accessible, so gate_before_switch is not necessary
|
# every region is accessible, so gate_before_switch is not necessary
|
||||||
while len(two_plus) > 1:
|
while len(two_plus) > 1:
|
||||||
@@ -460,42 +438,126 @@ def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dic
|
|||||||
region2.connect(connecting_region=region1, name=portal2.name)
|
region2.connect(connecting_region=region1, name=portal2.name)
|
||||||
|
|
||||||
|
|
||||||
def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]],
|
# loop through the static connections, return regions you can reach from this region
|
||||||
has_laurels: bool, logic: int) -> Set[str]:
|
# todo: refactor to take region_name and dependent_regions
|
||||||
# starting count, so we can run it again if this changes
|
def add_dependent_regions(region_name: str, logic_rules: int) -> Set[str]:
|
||||||
region_count = len(connected_regions)
|
region_set = set()
|
||||||
for origin, destinations in traversal_reqs.items():
|
if not logic_rules:
|
||||||
if origin not in connected_regions:
|
regions_to_add = dependent_regions_restricted
|
||||||
continue
|
elif logic_rules == 1:
|
||||||
# check if we can traverse to any of the destinations
|
regions_to_add = dependent_regions_nmg
|
||||||
for destination, req_lists in destinations.items():
|
|
||||||
if destination in connected_regions:
|
|
||||||
continue
|
|
||||||
met_traversal_reqs = False
|
|
||||||
if len(req_lists) == 0:
|
|
||||||
met_traversal_reqs = True
|
|
||||||
# loop through each set of possible requirements, with a fancy for else loop
|
|
||||||
for reqs in req_lists:
|
|
||||||
for req in reqs:
|
|
||||||
if req == "Hyperdash":
|
|
||||||
if not has_laurels:
|
|
||||||
break
|
|
||||||
elif req == "NMG":
|
|
||||||
if not logic:
|
|
||||||
break
|
|
||||||
elif req == "UR":
|
|
||||||
if logic < 2:
|
|
||||||
break
|
|
||||||
elif req not in connected_regions:
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
met_traversal_reqs = True
|
regions_to_add = dependent_regions_ur
|
||||||
|
for origin_regions, destination_regions in regions_to_add.items():
|
||||||
|
if region_name in origin_regions:
|
||||||
|
# if you matched something in the first set, you get the regions in its paired set
|
||||||
|
region_set.update(destination_regions)
|
||||||
|
return region_set
|
||||||
|
# if you didn't match anything in the first sets, just gives you the region
|
||||||
|
region_set = {region_name}
|
||||||
|
return region_set
|
||||||
|
|
||||||
|
|
||||||
|
# we're checking if an event-locked portal is being placed before the regions where its key(s) is/are
|
||||||
|
# doing this ensures the keys will not be locked behind the event-locked portal
|
||||||
|
def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool:
|
||||||
|
# the western belltower cannot be locked since you can access it with laurels
|
||||||
|
# so we only need to make sure the forest belltower isn't locked
|
||||||
|
if check_portal.scene_destination() == "Overworld Redux, Temple_main":
|
||||||
|
i = 0
|
||||||
|
for portal in two_plus:
|
||||||
|
if portal.region == "Forest Belltower Upper":
|
||||||
|
i += 1
|
||||||
break
|
break
|
||||||
if met_traversal_reqs:
|
if i == 1:
|
||||||
connected_regions.add(destination)
|
return True
|
||||||
|
|
||||||
# if the length of connected_regions changed, we got new regions, so we want to check those new origins
|
# fortress big gold door needs 2 scenes and one of the two upper portals of the courtyard
|
||||||
if region_count != len(connected_regions):
|
elif check_portal.scene_destination() == "Fortress Main, Fortress Arena_":
|
||||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic)
|
i = j = k = 0
|
||||||
|
for portal in two_plus:
|
||||||
|
if portal.region == "Fortress Courtyard Upper":
|
||||||
|
i += 1
|
||||||
|
if portal.scene() == "Fortress Basement":
|
||||||
|
j += 1
|
||||||
|
if portal.region == "Eastern Vault Fortress":
|
||||||
|
k += 1
|
||||||
|
if i == 2 or j == 2 or k == 5:
|
||||||
|
return True
|
||||||
|
|
||||||
return connected_regions
|
# fortress teleporter needs only the left fuses
|
||||||
|
elif check_portal.scene_destination() in {"Fortress Arena, Transit_teleporter_spidertank",
|
||||||
|
"Transit, Fortress Arena_teleporter_spidertank"}:
|
||||||
|
i = j = k = 0
|
||||||
|
for portal in two_plus:
|
||||||
|
if portal.scene() == "Fortress Courtyard":
|
||||||
|
i += 1
|
||||||
|
if portal.scene() == "Fortress Basement":
|
||||||
|
j += 1
|
||||||
|
if portal.region == "Eastern Vault Fortress":
|
||||||
|
k += 1
|
||||||
|
if i == 8 or j == 2 or k == 5:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Cathedral door needs Overworld and the front of Swamp
|
||||||
|
# Overworld is currently guaranteed, so no need to check it
|
||||||
|
elif check_portal.scene_destination() == "Swamp Redux 2, Cathedral Redux_main":
|
||||||
|
i = 0
|
||||||
|
for portal in two_plus:
|
||||||
|
if portal.region in {"Swamp Front", "Swamp to Cathedral Treasure Room",
|
||||||
|
"Swamp to Cathedral Main Entrance Region"}:
|
||||||
|
i += 1
|
||||||
|
if i == 4:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Zig portal room exit needs Zig 3 to be accessible to hit the fuse
|
||||||
|
elif check_portal.scene_destination() == "ziggurat2020_FTRoom, ziggurat2020_3_":
|
||||||
|
i = 0
|
||||||
|
for portal in two_plus:
|
||||||
|
if portal.scene() == "ziggurat2020_3":
|
||||||
|
i += 1
|
||||||
|
if i == 2:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Quarry teleporter needs you to hit the Darkwoods fuse
|
||||||
|
# Since it's physically in Quarry, we don't need to check for it
|
||||||
|
elif check_portal.scene_destination() in {"Quarry Redux, Transit_teleporter_quarry teleporter",
|
||||||
|
"Quarry Redux, ziggurat2020_0_"}:
|
||||||
|
i = 0
|
||||||
|
for portal in two_plus:
|
||||||
|
if portal.scene() == "Darkwoods Tunnel":
|
||||||
|
i += 1
|
||||||
|
if i == 2:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Same as above, but Quarry isn't guaranteed here
|
||||||
|
elif check_portal.scene_destination() == "Transit, Quarry Redux_teleporter_quarry teleporter":
|
||||||
|
i = j = 0
|
||||||
|
for portal in two_plus:
|
||||||
|
if portal.scene() == "Darkwoods Tunnel":
|
||||||
|
i += 1
|
||||||
|
if portal.scene() == "Quarry Redux":
|
||||||
|
j += 1
|
||||||
|
if i == 2 or j == 7:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Need Library fuse to use this teleporter
|
||||||
|
elif check_portal.scene_destination() == "Transit, Library Lab_teleporter_library teleporter":
|
||||||
|
i = 0
|
||||||
|
for portal in two_plus:
|
||||||
|
if portal.scene() == "Library Lab":
|
||||||
|
i += 1
|
||||||
|
if i == 3:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Need West Garden fuse to use this teleporter
|
||||||
|
elif check_portal.scene_destination() == "Transit, Archipelagos Redux_teleporter_archipelagos_teleporter":
|
||||||
|
i = 0
|
||||||
|
for portal in two_plus:
|
||||||
|
if portal.scene() == "Archipelagos Redux":
|
||||||
|
i += 1
|
||||||
|
if i == 6:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# false means you're good to place the portal
|
||||||
|
return False
|
||||||
|
|||||||
@@ -237,8 +237,6 @@ extra_groups: Dict[str, Set[str]] = {
|
|||||||
"Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't
|
"Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't
|
||||||
"Ladders to Bell": {"Ladders to West Bell"},
|
"Ladders to Bell": {"Ladders to West Bell"},
|
||||||
"Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell
|
"Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell
|
||||||
"Ladders in Atoll": {"Ladders in South Atoll"},
|
|
||||||
"Ladders in Ruined Atoll": {"Ladders in South Atoll"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item_name_groups.update(extra_groups)
|
item_name_groups.update(extra_groups)
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ location_table: Dict[str, TunicLocationData] = {
|
|||||||
"Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"),
|
"Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"),
|
||||||
"Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
"Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||||
"Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
"Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||||
"Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Main"),
|
"Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Front"),
|
||||||
"Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
"Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||||
"Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
"Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||||
"Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
"Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||||
|
|||||||
@@ -1,36 +1,28 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, Any
|
|
||||||
from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PerGameCommonOptions,
|
from Options import DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PerGameCommonOptions
|
||||||
OptionGroup)
|
|
||||||
|
|
||||||
|
|
||||||
class SwordProgression(DefaultOnToggle):
|
class SwordProgression(DefaultOnToggle):
|
||||||
"""
|
"""Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new swords with increased range and attack power."""
|
||||||
Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new swords with increased range and attack power.
|
|
||||||
"""
|
|
||||||
internal_name = "sword_progression"
|
internal_name = "sword_progression"
|
||||||
display_name = "Sword Progression"
|
display_name = "Sword Progression"
|
||||||
|
|
||||||
|
|
||||||
class StartWithSword(Toggle):
|
class StartWithSword(Toggle):
|
||||||
"""
|
"""Start with a sword in the player's inventory. Does not count towards Sword Progression."""
|
||||||
Start with a sword in the player's inventory. Does not count towards Sword Progression.
|
|
||||||
"""
|
|
||||||
internal_name = "start_with_sword"
|
internal_name = "start_with_sword"
|
||||||
display_name = "Start With Sword"
|
display_name = "Start With Sword"
|
||||||
|
|
||||||
|
|
||||||
class KeysBehindBosses(Toggle):
|
class KeysBehindBosses(Toggle):
|
||||||
"""
|
"""Places the three hexagon keys behind their respective boss fight in your world."""
|
||||||
Places the three hexagon keys behind their respective boss fight in your world.
|
|
||||||
"""
|
|
||||||
internal_name = "keys_behind_bosses"
|
internal_name = "keys_behind_bosses"
|
||||||
display_name = "Keys Behind Bosses"
|
display_name = "Keys Behind Bosses"
|
||||||
|
|
||||||
|
|
||||||
class AbilityShuffling(Toggle):
|
class AbilityShuffling(Toggle):
|
||||||
"""
|
"""Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found.
|
||||||
Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found.
|
|
||||||
If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required Hexagon goal amount.
|
If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required Hexagon goal amount.
|
||||||
*Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes.
|
*Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes.
|
||||||
"""
|
"""
|
||||||
@@ -60,27 +52,21 @@ class LogicRules(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class Lanternless(Toggle):
|
class Lanternless(Toggle):
|
||||||
"""
|
"""Choose whether you require the Lantern for dark areas.
|
||||||
Choose whether you require the Lantern for dark areas.
|
When enabled, the Lantern is marked as Useful instead of Progression."""
|
||||||
When enabled, the Lantern is marked as Useful instead of Progression.
|
|
||||||
"""
|
|
||||||
internal_name = "lanternless"
|
internal_name = "lanternless"
|
||||||
display_name = "Lanternless"
|
display_name = "Lanternless"
|
||||||
|
|
||||||
|
|
||||||
class Maskless(Toggle):
|
class Maskless(Toggle):
|
||||||
"""
|
"""Choose whether you require the Scavenger's Mask for Lower Quarry.
|
||||||
Choose whether you require the Scavenger's Mask for Lower Quarry.
|
When enabled, the Scavenger's Mask is marked as Useful instead of Progression."""
|
||||||
When enabled, the Scavenger's Mask is marked as Useful instead of Progression.
|
|
||||||
"""
|
|
||||||
internal_name = "maskless"
|
internal_name = "maskless"
|
||||||
display_name = "Maskless"
|
display_name = "Maskless"
|
||||||
|
|
||||||
|
|
||||||
class FoolTraps(Choice):
|
class FoolTraps(Choice):
|
||||||
"""
|
"""Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative effects to the player."""
|
||||||
Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative effects to the player.
|
|
||||||
"""
|
|
||||||
internal_name = "fool_traps"
|
internal_name = "fool_traps"
|
||||||
display_name = "Fool Traps"
|
display_name = "Fool Traps"
|
||||||
option_off = 0
|
option_off = 0
|
||||||
@@ -91,17 +77,13 @@ class FoolTraps(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class HexagonQuest(Toggle):
|
class HexagonQuest(Toggle):
|
||||||
"""
|
"""An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed after collecting the required number of them."""
|
||||||
An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed after collecting the required number of them.
|
|
||||||
"""
|
|
||||||
internal_name = "hexagon_quest"
|
internal_name = "hexagon_quest"
|
||||||
display_name = "Hexagon Quest"
|
display_name = "Hexagon Quest"
|
||||||
|
|
||||||
|
|
||||||
class HexagonGoal(Range):
|
class HexagonGoal(Range):
|
||||||
"""
|
"""How many Gold Questagons are required to complete the game on Hexagon Quest."""
|
||||||
How many Gold Questagons are required to complete the game on Hexagon Quest.
|
|
||||||
"""
|
|
||||||
internal_name = "hexagon_goal"
|
internal_name = "hexagon_goal"
|
||||||
display_name = "Gold Hexagons Required"
|
display_name = "Gold Hexagons Required"
|
||||||
range_start = 15
|
range_start = 15
|
||||||
@@ -110,9 +92,7 @@ class HexagonGoal(Range):
|
|||||||
|
|
||||||
|
|
||||||
class ExtraHexagonPercentage(Range):
|
class ExtraHexagonPercentage(Range):
|
||||||
"""
|
"""How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount."""
|
||||||
How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount.
|
|
||||||
"""
|
|
||||||
internal_name = "extra_hexagon_percentage"
|
internal_name = "extra_hexagon_percentage"
|
||||||
display_name = "Percentage of Extra Gold Hexagons"
|
display_name = "Percentage of Extra Gold Hexagons"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
@@ -138,20 +118,16 @@ class EntranceRando(TextChoice):
|
|||||||
|
|
||||||
|
|
||||||
class FixedShop(Toggle):
|
class FixedShop(Toggle):
|
||||||
"""
|
"""Forces the Windmill entrance to lead to a shop, and places only one other shop in the pool.
|
||||||
Forces the Windmill entrance to lead to a shop, and removes the remaining shops from the pool.
|
Has no effect if Entrance Rando is not enabled."""
|
||||||
Adds another entrance in Rooted Ziggurat Lower to keep an even number of entrances.
|
|
||||||
Has no effect if Entrance Rando is not enabled.
|
|
||||||
"""
|
|
||||||
internal_name = "fixed_shop"
|
internal_name = "fixed_shop"
|
||||||
display_name = "Fewer Shops in Entrance Rando"
|
display_name = "Fewer Shops in Entrance Rando"
|
||||||
|
|
||||||
|
|
||||||
class LaurelsLocation(Choice):
|
class LaurelsLocation(Choice):
|
||||||
"""
|
"""Force the Hero's Laurels to be placed at a location in your world.
|
||||||
Force the Hero's Laurels to be placed at a location in your world.
|
|
||||||
For if you want to avoid or specify early or late Laurels.
|
For if you want to avoid or specify early or late Laurels.
|
||||||
"""
|
If you use the 10 Fairies option in Entrance Rando, Secret Gathering Place will be at its vanilla entrance."""
|
||||||
internal_name = "laurels_location"
|
internal_name = "laurels_location"
|
||||||
display_name = "Laurels Location"
|
display_name = "Laurels Location"
|
||||||
option_anywhere = 0
|
option_anywhere = 0
|
||||||
@@ -162,19 +138,15 @@ class LaurelsLocation(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class ShuffleLadders(Toggle):
|
class ShuffleLadders(Toggle):
|
||||||
"""
|
"""Turns several ladders in the game into items that must be found before they can be climbed on.
|
||||||
Turns several ladders in the game into items that must be found before they can be climbed on.
|
|
||||||
Adds more layers of progression to the game by blocking access to many areas early on.
|
Adds more layers of progression to the game by blocking access to many areas early on.
|
||||||
"Ladders were a mistake."
|
"Ladders were a mistake." —Andrew Shouldice"""
|
||||||
—Andrew Shouldice
|
|
||||||
"""
|
|
||||||
internal_name = "shuffle_ladders"
|
internal_name = "shuffle_ladders"
|
||||||
display_name = "Shuffle Ladders"
|
display_name = "Shuffle Ladders"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TunicOptions(PerGameCommonOptions):
|
class TunicOptions(PerGameCommonOptions):
|
||||||
start_inventory_from_pool: StartInventoryPool
|
|
||||||
sword_progression: SwordProgression
|
sword_progression: SwordProgression
|
||||||
start_with_sword: StartWithSword
|
start_with_sword: StartWithSword
|
||||||
keys_behind_bosses: KeysBehindBosses
|
keys_behind_bosses: KeysBehindBosses
|
||||||
@@ -190,33 +162,4 @@ class TunicOptions(PerGameCommonOptions):
|
|||||||
lanternless: Lanternless
|
lanternless: Lanternless
|
||||||
maskless: Maskless
|
maskless: Maskless
|
||||||
laurels_location: LaurelsLocation
|
laurels_location: LaurelsLocation
|
||||||
|
start_inventory_from_pool: StartInventoryPool
|
||||||
|
|
||||||
tunic_option_groups = [
|
|
||||||
OptionGroup("Logic Options", [
|
|
||||||
LogicRules,
|
|
||||||
Lanternless,
|
|
||||||
Maskless,
|
|
||||||
])
|
|
||||||
]
|
|
||||||
|
|
||||||
tunic_option_presets: Dict[str, Dict[str, Any]] = {
|
|
||||||
"Sync": {
|
|
||||||
"ability_shuffling": True,
|
|
||||||
},
|
|
||||||
"Async": {
|
|
||||||
"progression_balancing": 0,
|
|
||||||
"ability_shuffling": True,
|
|
||||||
"shuffle_ladders": True,
|
|
||||||
"laurels_location": "10_fairies",
|
|
||||||
},
|
|
||||||
"Glace Mode": {
|
|
||||||
"accessibility": "minimal",
|
|
||||||
"ability_shuffling": True,
|
|
||||||
"entrance_rando": "yes",
|
|
||||||
"fool_traps": "onslaught",
|
|
||||||
"logic_rules": "unrestricted",
|
|
||||||
"maskless": True,
|
|
||||||
"lanternless": True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user