mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-23 07:53:23 -07:00
Compare commits
3 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d04239504 | ||
|
|
59a000033e | ||
|
|
baca95d49d |
@@ -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()
|
|
||||||
@@ -718,6 +718,10 @@ class CollectionState():
|
|||||||
def count(self, item: str, player: int) -> int:
|
def count(self, item: str, player: int) -> int:
|
||||||
return self.prog_items[player][item]
|
return self.prog_items[player][item]
|
||||||
|
|
||||||
|
def item_count(self, item: str, player: int) -> int:
|
||||||
|
Utils.deprecate("Use count instead.")
|
||||||
|
return self.count(item, player)
|
||||||
|
|
||||||
def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
|
def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
|
||||||
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
|
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
|
||||||
found: int = 0
|
found: int = 0
|
||||||
@@ -728,25 +732,10 @@ class CollectionState():
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_from_list_exclusive(self, items: Iterable[str], player: int, count: int) -> bool:
|
|
||||||
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
|
|
||||||
Ignores duplicates of the same item."""
|
|
||||||
found: int = 0
|
|
||||||
player_prog_items = self.prog_items[player]
|
|
||||||
for item_name in items:
|
|
||||||
found += player_prog_items[item_name] > 0
|
|
||||||
if found >= count:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def count_from_list(self, items: Iterable[str], player: int) -> int:
|
def count_from_list(self, items: Iterable[str], player: int) -> int:
|
||||||
"""Returns the cumulative count of items from a list present in state."""
|
"""Returns the cumulative count of items from a list present in state."""
|
||||||
return sum(self.prog_items[player][item_name] for item_name in items)
|
return sum(self.prog_items[player][item_name] for item_name in items)
|
||||||
|
|
||||||
def count_from_list_exclusive(self, items: Iterable[str], player: int) -> int:
|
|
||||||
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
|
|
||||||
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
|
|
||||||
|
|
||||||
# item name group related
|
# item name group related
|
||||||
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
||||||
"""Returns True if the state contains at least `count` items present in a specified item group."""
|
"""Returns True if the state contains at least `count` items present in a specified item group."""
|
||||||
@@ -758,18 +747,6 @@ class CollectionState():
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_group_exclusive(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
|
||||||
"""Returns True if the state contains at least `count` items present in a specified item group.
|
|
||||||
Ignores duplicates of the same item.
|
|
||||||
"""
|
|
||||||
found: int = 0
|
|
||||||
player_prog_items = self.prog_items[player]
|
|
||||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
|
|
||||||
found += player_prog_items[item_name] > 0
|
|
||||||
if found >= count:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def count_group(self, item_name_group: str, player: int) -> int:
|
def count_group(self, item_name_group: str, player: int) -> int:
|
||||||
"""Returns the cumulative count of items from an item group present in state."""
|
"""Returns the cumulative count of items from an item group present in state."""
|
||||||
player_prog_items = self.prog_items[player]
|
player_prog_items = self.prog_items[player]
|
||||||
@@ -778,15 +755,6 @@ class CollectionState():
|
|||||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
|
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
|
||||||
)
|
)
|
||||||
|
|
||||||
def count_group_exclusive(self, item_name_group: str, player: int) -> int:
|
|
||||||
"""Returns the cumulative count of items from an item group present in state.
|
|
||||||
Ignores duplicates of the same item."""
|
|
||||||
player_prog_items = self.prog_items[player]
|
|
||||||
return sum(
|
|
||||||
player_prog_items[item_name] > 0
|
|
||||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Item related
|
# Item related
|
||||||
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
||||||
if location:
|
if location:
|
||||||
@@ -1046,7 +1014,7 @@ class Location:
|
|||||||
self.parent_region = parent
|
self.parent_region = parent
|
||||||
|
|
||||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||||
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
|
return ((self.always_allow(state, item) and item.name not in state.multiworld.non_local_items[item.player])
|
||||||
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||||
and self.item_rule(item)
|
and self.item_rule(item)
|
||||||
and (not check_access or self.can_reach(state))))
|
and (not check_access or self.can_reach(state))))
|
||||||
@@ -1242,7 +1210,7 @@ class Spoiler:
|
|||||||
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
|
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
|
||||||
location.item.name, location.item.player, location.name, location.player) for location in
|
location.item.name, location.item.player, location.name, location.player) for location in
|
||||||
sphere_candidates])
|
sphere_candidates])
|
||||||
if any([multiworld.worlds[location.item.player].options.accessibility != 'minimal' for location in sphere_candidates]):
|
if any([multiworld.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
|
||||||
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
|
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
|
||||||
f'Something went terribly wrong here.')
|
f'Something went terribly wrong here.')
|
||||||
else:
|
else:
|
||||||
|
|||||||
64
Fill.py
64
Fill.py
@@ -35,8 +35,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
|||||||
"""
|
"""
|
||||||
:param multiworld: Multiworld to be filled.
|
:param 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,21 +284,13 @@ 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:
|
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
|
||||||
last_batch = []
|
f"Unplaced items:\n"
|
||||||
for item in unplaced_items:
|
f"{', '.join(str(item) for item in unplaced_items)}\n"
|
||||||
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
|
f"Unfilled locations:\n"
|
||||||
multiworld.push_precollected(item)
|
f"{', '.join(str(location) for location in locations)}\n"
|
||||||
last_batch.append(multiworld.worlds[item.player].create_filler())
|
f"Already placed {len(placements)}:\n"
|
||||||
remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry")
|
f"{', '.join(str(place) for place in placements)}")
|
||||||
else:
|
|
||||||
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
|
|
||||||
f"Unplaced items:\n"
|
|
||||||
f"{', '.join(str(item) for item in unplaced_items)}\n"
|
|
||||||
f"Unfilled locations:\n"
|
|
||||||
f"{', '.join(str(location) for location in locations)}\n"
|
|
||||||
f"Already placed {len(placements)}:\n"
|
|
||||||
f"{', '.join(str(place) for place in placements)}")
|
|
||||||
|
|
||||||
itempool.extend(unplaced_items)
|
itempool.extend(unplaced_items)
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
43
Generate.py
43
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)
|
||||||
@@ -395,7 +378,7 @@ def roll_linked_options(weights: dict) -> dict:
|
|||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
|
||||||
def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict:
|
def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||||
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
|
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
|
||||||
weights["_Generator_Version"] = Utils.__version__
|
weights["_Generator_Version"] = Utils.__version__
|
||||||
for i, option_set in enumerate(triggers):
|
for i, option_set in enumerate(triggers):
|
||||||
@@ -418,7 +401,7 @@ def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict:
|
|||||||
if category_name:
|
if category_name:
|
||||||
currently_targeted_weights = currently_targeted_weights[category_name]
|
currently_targeted_weights = currently_targeted_weights[category_name]
|
||||||
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
|
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
|
||||||
valid_keys.add(key)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Your trigger number {i + 1} is invalid. "
|
raise ValueError(f"Your trigger number {i + 1} is invalid. "
|
||||||
f"Please fix your triggers.") from e
|
f"Please fix your triggers.") from e
|
||||||
@@ -432,7 +415,6 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
|
|||||||
player_option = option.from_any(game_weights[option_key])
|
player_option = option.from_any(game_weights[option_key])
|
||||||
else:
|
else:
|
||||||
player_option = option.from_any(get_choice(option_key, game_weights))
|
player_option = option.from_any(get_choice(option_key, game_weights))
|
||||||
del game_weights[option_key]
|
|
||||||
else:
|
else:
|
||||||
player_option = option.from_any(option.default) # call the from_any here to support default "random"
|
player_option = option.from_any(option.default) # call the from_any here to support default "random"
|
||||||
setattr(ret, option_key, player_option)
|
setattr(ret, option_key, player_option)
|
||||||
@@ -446,9 +428,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
|||||||
if "linked_options" in weights:
|
if "linked_options" in weights:
|
||||||
weights = roll_linked_options(weights)
|
weights = roll_linked_options(weights)
|
||||||
|
|
||||||
valid_trigger_names = set()
|
|
||||||
if "triggers" in weights:
|
if "triggers" in weights:
|
||||||
weights = roll_triggers(weights, weights["triggers"], valid_trigger_names)
|
weights = roll_triggers(weights, weights["triggers"])
|
||||||
|
|
||||||
requirements = weights.get("requires", {})
|
requirements = weights.get("requires", {})
|
||||||
if requirements:
|
if requirements:
|
||||||
@@ -483,14 +464,12 @@ 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"])
|
||||||
game_weights = weights[ret.game]
|
game_weights = weights[ret.game]
|
||||||
|
|
||||||
ret.name = get_choice('name', weights)
|
ret.name = get_choice('name', weights)
|
||||||
@@ -499,10 +478,6 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
|||||||
|
|
||||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||||
for option_key in game_weights:
|
|
||||||
if option_key in {"triggers", *valid_trigger_names}:
|
|
||||||
continue
|
|
||||||
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
|
|
||||||
if PlandoOptions.items in plando_options:
|
if PlandoOptions.items in plando_options:
|
||||||
ret.plando_items = game_weights.get("plando_items", [])
|
ret.plando_items = game_weights.get("plando_items", [])
|
||||||
if ret.game == "A Link to the Past":
|
if ret.game == "A Link to the Past":
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
elif not args:
|
elif not args:
|
||||||
args = {}
|
args = {}
|
||||||
|
|
||||||
if args.get("Patch|Game|Component", None) is not None:
|
if "Patch|Game|Component" in args:
|
||||||
file, component = identify(args["Patch|Game|Component"])
|
file, component = identify(args["Patch|Game|Component"])
|
||||||
if file:
|
if file:
|
||||||
args['file'] = file
|
args['file'] = file
|
||||||
|
|||||||
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')
|
||||||
|
|
||||||
|
|||||||
@@ -175,13 +175,11 @@ class Context:
|
|||||||
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||||
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
|
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||||
non_hintable_names: typing.Dict[str, typing.Set[str]]
|
non_hintable_names: typing.Dict[str, typing.Set[str]]
|
||||||
logger: logging.Logger
|
|
||||||
|
|
||||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||||
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
|
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
|
||||||
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
||||||
log_network: bool = False, logger: logging.Logger = logging.getLogger()):
|
log_network: bool = False):
|
||||||
self.logger = logger
|
|
||||||
super(Context, self).__init__()
|
super(Context, self).__init__()
|
||||||
self.slot_info = {}
|
self.slot_info = {}
|
||||||
self.log_network = log_network
|
self.log_network = log_network
|
||||||
@@ -289,12 +287,12 @@ class Context:
|
|||||||
try:
|
try:
|
||||||
await endpoint.socket.send(msg)
|
await endpoint.socket.send(msg)
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
self.logger.exception(f"Exception during send_msgs, could not send {msg}")
|
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||||
await self.disconnect(endpoint)
|
await self.disconnect(endpoint)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if self.log_network:
|
if self.log_network:
|
||||||
self.logger.info(f"Outgoing message: {msg}")
|
logging.info(f"Outgoing message: {msg}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
|
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
|
||||||
@@ -303,12 +301,12 @@ class Context:
|
|||||||
try:
|
try:
|
||||||
await endpoint.socket.send(msg)
|
await endpoint.socket.send(msg)
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
self.logger.exception("Exception during send_encoded_msgs")
|
logging.exception("Exception during send_encoded_msgs")
|
||||||
await self.disconnect(endpoint)
|
await self.disconnect(endpoint)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if self.log_network:
|
if self.log_network:
|
||||||
self.logger.info(f"Outgoing message: {msg}")
|
logging.info(f"Outgoing message: {msg}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool:
|
async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool:
|
||||||
@@ -319,11 +317,11 @@ class Context:
|
|||||||
try:
|
try:
|
||||||
websockets.broadcast(sockets, msg)
|
websockets.broadcast(sockets, msg)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
self.logger.exception("Exception during broadcast_send_encoded_msgs")
|
logging.exception("Exception during broadcast_send_encoded_msgs")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if self.log_network:
|
if self.log_network:
|
||||||
self.logger.info(f"Outgoing broadcast: {msg}")
|
logging.info(f"Outgoing broadcast: {msg}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def broadcast_all(self, msgs: typing.List[dict]):
|
def broadcast_all(self, msgs: typing.List[dict]):
|
||||||
@@ -332,7 +330,7 @@ class Context:
|
|||||||
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||||
|
|
||||||
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
|
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
|
||||||
self.logger.info("Notice (all): %s" % text)
|
logging.info("Notice (all): %s" % text)
|
||||||
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
|
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
|
||||||
|
|
||||||
def broadcast_team(self, team: int, msgs: typing.List[dict]):
|
def broadcast_team(self, team: int, msgs: typing.List[dict]):
|
||||||
@@ -354,7 +352,7 @@ class Context:
|
|||||||
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
|
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
|
||||||
if not client.auth:
|
if not client.auth:
|
||||||
return
|
return
|
||||||
self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||||
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
|
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
|
||||||
|
|
||||||
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
|
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
|
||||||
@@ -453,7 +451,7 @@ class Context:
|
|||||||
for game_name, data in decoded_obj.get("datapackage", {}).items():
|
for game_name, data in decoded_obj.get("datapackage", {}).items():
|
||||||
if game_name in game_data_packages:
|
if game_name in game_data_packages:
|
||||||
data = game_data_packages[game_name]
|
data = game_data_packages[game_name]
|
||||||
self.logger.info(f"Loading embedded data package for game {game_name}")
|
logging.info(f"Loading embedded data package for game {game_name}")
|
||||||
self.gamespackage[game_name] = data
|
self.gamespackage[game_name] = data
|
||||||
self.item_name_groups[game_name] = data["item_name_groups"]
|
self.item_name_groups[game_name] = data["item_name_groups"]
|
||||||
if "location_name_groups" in data:
|
if "location_name_groups" in data:
|
||||||
@@ -485,7 +483,7 @@ class Context:
|
|||||||
with open(self.save_filename, "wb") as f:
|
with open(self.save_filename, "wb") as f:
|
||||||
f.write(zlib.compress(encoded_save))
|
f.write(zlib.compress(encoded_save))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(e)
|
logging.exception(e)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
@@ -503,12 +501,12 @@ class Context:
|
|||||||
save_data = restricted_loads(zlib.decompress(f.read()))
|
save_data = restricted_loads(zlib.decompress(f.read()))
|
||||||
self.set_save(save_data)
|
self.set_save(save_data)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
self.logger.error('No save data found, starting a new game')
|
logging.error('No save data found, starting a new game')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(e)
|
logging.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
|
||||||
@@ -522,19 +520,18 @@ class Context:
|
|||||||
next_wakeup = (second - get_datetime_second()) % self.auto_save_interval
|
next_wakeup = (second - get_datetime_second()) % self.auto_save_interval
|
||||||
time.sleep(max(1.0, next_wakeup))
|
time.sleep(max(1.0, next_wakeup))
|
||||||
if self.save_dirty:
|
if self.save_dirty:
|
||||||
self.logger.debug("Saving via thread.")
|
logging.debug("Saving via thread.")
|
||||||
self._save()
|
self._save()
|
||||||
except OperationalError as e:
|
except OperationalError as e:
|
||||||
self.logger.exception(e)
|
logging.exception(e)
|
||||||
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
|
logging.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
|
||||||
else:
|
else:
|
||||||
self.save_dirty = False
|
self.save_dirty = False
|
||||||
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
|
|
||||||
|
|
||||||
def get_save(self) -> dict:
|
def get_save(self) -> dict:
|
||||||
self.recheck_hints()
|
self.recheck_hints()
|
||||||
@@ -601,7 +598,7 @@ class Context:
|
|||||||
if "stored_data" in savedata:
|
if "stored_data" in savedata:
|
||||||
self.stored_data = savedata["stored_data"]
|
self.stored_data = savedata["stored_data"]
|
||||||
# count items and slots from lists for items_handling = remote
|
# count items and slots from lists for items_handling = remote
|
||||||
self.logger.info(
|
logging.info(
|
||||||
f'Loaded save file with {sum([len(v) for k, v in self.received_items.items() if k[2]])} received items '
|
f'Loaded save file with {sum([len(v) for k, v in self.received_items.items() if k[2]])} received items '
|
||||||
f'for {sum(k[2] for k in self.received_items)} players')
|
f'for {sum(k[2] for k in self.received_items)} players')
|
||||||
|
|
||||||
@@ -643,13 +640,13 @@ class Context:
|
|||||||
try:
|
try:
|
||||||
raise Exception(f"Could not set server option {key}, skipping.") from e
|
raise Exception(f"Could not set server option {key}, skipping.") from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(e)
|
logging.exception(e)
|
||||||
self.logger.debug(f"Setting server option {key} to {value} from supplied multidata")
|
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
elif key == "disable_item_cheat":
|
elif key == "disable_item_cheat":
|
||||||
self.item_cheat = not bool(value)
|
self.item_cheat = not bool(value)
|
||||||
else:
|
else:
|
||||||
self.logger.debug(f"Unrecognized server option {key}")
|
logging.debug(f"Unrecognized server option {key}")
|
||||||
|
|
||||||
def get_aliased_name(self, team: int, slot: int):
|
def get_aliased_name(self, team: int, slot: int):
|
||||||
if (team, slot) in self.name_aliases:
|
if (team, slot) in self.name_aliases:
|
||||||
@@ -683,7 +680,7 @@ class Context:
|
|||||||
self.hints[team, player].add(hint)
|
self.hints[team, player].add(hint)
|
||||||
new_hint_events.add(player)
|
new_hint_events.add(player)
|
||||||
|
|
||||||
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
|
logging.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
|
||||||
for slot in new_hint_events:
|
for slot in new_hint_events:
|
||||||
self.on_new_hint(team, slot)
|
self.on_new_hint(team, slot)
|
||||||
for slot, hint_data in concerns.items():
|
for slot, hint_data in concerns.items():
|
||||||
@@ -742,21 +739,21 @@ async def server(websocket, path: str = "/", ctx: Context = None):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if ctx.log_network:
|
if ctx.log_network:
|
||||||
ctx.logger.info("Incoming connection")
|
logging.info("Incoming connection")
|
||||||
await on_client_connected(ctx, client)
|
await on_client_connected(ctx, client)
|
||||||
if ctx.log_network:
|
if ctx.log_network:
|
||||||
ctx.logger.info("Sent Room Info")
|
logging.info("Sent Room Info")
|
||||||
async for data in websocket:
|
async for data in websocket:
|
||||||
if ctx.log_network:
|
if ctx.log_network:
|
||||||
ctx.logger.info(f"Incoming message: {data}")
|
logging.info(f"Incoming message: {data}")
|
||||||
for msg in decode(data):
|
for msg in decode(data):
|
||||||
await process_client_cmd(ctx, client, msg)
|
await process_client_cmd(ctx, client, msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not isinstance(e, websockets.WebSocketException):
|
if not isinstance(e, websockets.WebSocketException):
|
||||||
ctx.logger.exception(e)
|
logging.exception(e)
|
||||||
finally:
|
finally:
|
||||||
if ctx.log_network:
|
if ctx.log_network:
|
||||||
ctx.logger.info("Disconnected")
|
logging.info("Disconnected")
|
||||||
await ctx.disconnect(client)
|
await ctx.disconnect(client)
|
||||||
|
|
||||||
|
|
||||||
@@ -988,7 +985,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
|||||||
new_item = NetworkItem(item_id, location, slot, flags)
|
new_item = NetworkItem(item_id, location, slot, flags)
|
||||||
send_items_to(ctx, team, target_player, new_item)
|
send_items_to(ctx, team, target_player, new_item)
|
||||||
|
|
||||||
ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % (
|
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||||
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
|
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
|
||||||
ctx.player_names[(team, target_player)], ctx.location_names[location]))
|
ctx.player_names[(team, target_player)], ctx.location_names[location]))
|
||||||
info_text = json_format_send_event(new_item, target_player)
|
info_text = json_format_send_event(new_item, target_player)
|
||||||
@@ -1628,7 +1625,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
try:
|
try:
|
||||||
cmd: str = args["cmd"]
|
cmd: str = args["cmd"]
|
||||||
except:
|
except:
|
||||||
ctx.logger.exception(f"Could not get command from {args}")
|
logging.exception(f"Could not get command from {args}")
|
||||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
|
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
|
||||||
"text": f"Could not get command from {args} at `cmd`"}])
|
"text": f"Could not get command from {args} at `cmd`"}])
|
||||||
raise
|
raise
|
||||||
@@ -1671,7 +1668,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
if errors:
|
if errors:
|
||||||
ctx.logger.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
|
logging.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
|
||||||
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
|
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
|
||||||
else:
|
else:
|
||||||
team, slot = ctx.connect_names[args['name']]
|
team, slot = ctx.connect_names[args['name']]
|
||||||
@@ -2289,7 +2286,7 @@ async def auto_shutdown(ctx, to_cancel=None):
|
|||||||
if to_cancel:
|
if to_cancel:
|
||||||
for task in to_cancel:
|
for task in to_cancel:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
ctx.logger.info("Shutting down due to inactivity.")
|
logging.info("Shutting down due to inactivity.")
|
||||||
|
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
if not ctx.client_activity_timers.values():
|
if not ctx.client_activity_timers.values():
|
||||||
|
|||||||
62
Options.py
62
Options.py
@@ -140,6 +140,12 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
|||||||
def current_key(self) -> str:
|
def current_key(self) -> str:
|
||||||
return self.name_lookup[self.value]
|
return self.name_lookup[self.value]
|
||||||
|
|
||||||
|
def get_current_option_name(self) -> str:
|
||||||
|
"""Deprecated. use current_option_name instead. TODO remove around 0.4"""
|
||||||
|
logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated."
|
||||||
|
f" use current_option_name instead. Worlds should use {self}.current_key"))
|
||||||
|
return self.current_option_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_option_name(self) -> str:
|
def current_option_name(self) -> str:
|
||||||
"""For display purposes. Worlds should be using current_key."""
|
"""For display purposes. Worlds should be using current_key."""
|
||||||
@@ -744,9 +750,39 @@ class NamedRange(Range):
|
|||||||
return super().from_text(text)
|
return super().from_text(text)
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialRange(NamedRange):
|
||||||
|
special_range_cutoff = 0
|
||||||
|
|
||||||
|
# TODO: remove class SpecialRange, earliest 3 releases after 0.4.3
|
||||||
|
def __new__(cls, value: int) -> SpecialRange:
|
||||||
|
from Utils import deprecate
|
||||||
|
deprecate(f"Option type {cls.__name__} is a subclass of SpecialRange, which is deprecated and pending removal. "
|
||||||
|
"Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In "
|
||||||
|
"NamedRange, range_start specifies the lower end of the regular range, while special values can be "
|
||||||
|
"placed anywhere (below, inside, or above the regular range).")
|
||||||
|
return super().__new__(cls)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def weighted_range(cls, text) -> Range:
|
||||||
|
if text == "random-low":
|
||||||
|
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.special_range_cutoff))
|
||||||
|
elif text == "random-high":
|
||||||
|
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.range_end))
|
||||||
|
elif text == "random-middle":
|
||||||
|
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end))
|
||||||
|
elif text.startswith("random-range-"):
|
||||||
|
return cls.custom_range(text)
|
||||||
|
elif text == "random":
|
||||||
|
return cls(random.randint(cls.special_range_cutoff, cls.range_end))
|
||||||
|
else:
|
||||||
|
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||||
|
f"Acceptable values are: random, random-high, random-middle, random-low, "
|
||||||
|
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
|
||||||
|
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
|
||||||
|
|
||||||
|
|
||||||
class FreezeValidKeys(AssembleOptions):
|
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)
|
||||||
@@ -1124,14 +1160,6 @@ class DeathLinkMixin:
|
|||||||
death_link: DeathLink
|
death_link: DeathLink
|
||||||
|
|
||||||
|
|
||||||
class OptionGroup(typing.NamedTuple):
|
|
||||||
"""Define a grouping of options."""
|
|
||||||
name: str
|
|
||||||
"""Name of the group to categorize these options in for display on the WebHost and in generated YAMLS."""
|
|
||||||
options: typing.List[typing.Type[Option[typing.Any]]]
|
|
||||||
"""Options to be in the defined group."""
|
|
||||||
|
|
||||||
|
|
||||||
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
|
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -1170,21 +1198,15 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
|||||||
|
|
||||||
for game_name, world in AutoWorldRegister.world_types.items():
|
for game_name, world in AutoWorldRegister.world_types.items():
|
||||||
if not world.hidden or generate_hidden:
|
if not world.hidden or generate_hidden:
|
||||||
|
all_options: typing.Dict[str, AssembleOptions] = {
|
||||||
option_groups = {option: option_group.name
|
option_name: option for option_name, option in world.options_dataclass.type_hints.items()
|
||||||
for option_group in world.web.option_groups
|
if option.visibility & Visibility.template
|
||||||
for option in option_group.options}
|
}
|
||||||
ordered_groups = ["Game Options"]
|
|
||||||
[ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups]
|
|
||||||
grouped_options = {group: {} for group in ordered_groups}
|
|
||||||
for option_name, option in world.options_dataclass.type_hints.items():
|
|
||||||
if option.visibility >= Visibility.template:
|
|
||||||
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
|
||||||
|
|
||||||
with open(local_path("data", "options.yaml")) as f:
|
with open(local_path("data", "options.yaml")) as f:
|
||||||
file_data = f.read()
|
file_data = f.read()
|
||||||
res = Template(file_data).render(
|
res = Template(file_data).render(
|
||||||
option_groups=grouped_options,
|
options=all_options,
|
||||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||||
dictify_range=dictify_range,
|
dictify_range=dictify_range,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,10 +66,6 @@ Currently, the following games are supported:
|
|||||||
* A Short Hike
|
* A Short Hike
|
||||||
* Yoshi's Island
|
* Yoshi's Island
|
||||||
* Mario & Luigi: Superstar Saga
|
* Mario & Luigi: Superstar Saga
|
||||||
* Bomb Rush Cyberfunk
|
|
||||||
* Aquaria
|
|
||||||
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
|
||||||
* A Hat in Time
|
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
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
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ app.jinja_env.filters['all'] = all
|
|||||||
|
|
||||||
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
||||||
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||||
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
|
|
||||||
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
|
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
|
||||||
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
||||||
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
||||||
@@ -84,6 +83,6 @@ def register():
|
|||||||
|
|
||||||
from WebHostLib.customserver import run_server_process
|
from WebHostLib.customserver import run_server_process
|
||||||
# to trigger app routing picking up on it
|
# to trigger app routing picking up on it
|
||||||
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options
|
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots
|
||||||
|
|
||||||
app.register_blueprint(api.api_endpoints)
|
app.register_blueprint(api.api_endpoints)
|
||||||
|
|||||||
@@ -3,25 +3,26 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
import threading
|
||||||
|
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 launch_room(room: Room, config: dict):
|
||||||
|
# requires db_session!
|
||||||
|
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout):
|
||||||
|
multiworld = multiworlds.get(room.id, None)
|
||||||
|
if not multiworld:
|
||||||
|
multiworld = MultiworldInstance(room, config)
|
||||||
|
|
||||||
def stop():
|
multiworld.start()
|
||||||
"""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):
|
||||||
@@ -58,50 +59,39 @@ def init_db(pony_config: dict):
|
|||||||
db.generate_mapping()
|
db.generate_mapping()
|
||||||
|
|
||||||
|
|
||||||
def cleanup():
|
|
||||||
"""delete unowned user-content"""
|
|
||||||
with db_session:
|
|
||||||
# >>> bool(uuid.UUID(int=0))
|
|
||||||
# True
|
|
||||||
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
|
|
||||||
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
|
|
||||||
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
|
|
||||||
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
|
|
||||||
if rooms or seeds or slots:
|
|
||||||
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
|
|
||||||
|
|
||||||
|
|
||||||
def autohost(config: dict):
|
def autohost(config: dict):
|
||||||
def keep_running():
|
def keep_running():
|
||||||
stop_event = _stop_event
|
|
||||||
try:
|
try:
|
||||||
with Locker("autohost"):
|
with Locker("autohost"):
|
||||||
cleanup()
|
# delete unowned user-content
|
||||||
hosters = []
|
with db_session:
|
||||||
for x in range(config["HOSTERS"]):
|
# >>> bool(uuid.UUID(int=0))
|
||||||
hoster = MultiworldInstance(config, x)
|
# True
|
||||||
hosters.append(hoster)
|
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
|
||||||
hoster.start()
|
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
|
||||||
|
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
|
||||||
while not stop_event.wait(0.1):
|
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
|
||||||
|
if rooms or seeds or slots:
|
||||||
|
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
|
||||||
|
run_guardian()
|
||||||
|
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.
|
launch_room(room, config)
|
||||||
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
|
|
||||||
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,45 +124,37 @@ 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] = {}
|
||||||
|
|
||||||
|
|
||||||
class MultiworldInstance():
|
class MultiworldInstance():
|
||||||
def __init__(self, config: dict, id: int):
|
def __init__(self, room: Room, config: dict):
|
||||||
self.room_ids = set()
|
self.room_id = room.id
|
||||||
self.process: typing.Optional[multiprocessing.Process] = None
|
self.process: typing.Optional[multiprocessing.Process] = None
|
||||||
|
with guardian_lock:
|
||||||
|
multiworlds[self.room_id] = self
|
||||||
self.ponyconfig = config["PONY"]
|
self.ponyconfig = config["PONY"]
|
||||||
self.cert = config["SELFLAUNCHCERT"]
|
self.cert = config["SELFLAUNCHCERT"]
|
||||||
self.key = config["SELFLAUNCHKEY"]
|
self.key = config["SELFLAUNCHKEY"]
|
||||||
self.host = config["HOST_ADDRESS"]
|
self.host = config["HOST_ADDRESS"]
|
||||||
self.rooms_to_start = multiprocessing.Queue()
|
|
||||||
self.rooms_shutting_down = multiprocessing.Queue()
|
|
||||||
self.name = f"MultiHoster{id}"
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if self.process and self.process.is_alive():
|
if self.process and self.process.is_alive():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
logging.info(f"Spinning up {self.room_id}")
|
||||||
process = multiprocessing.Process(group=None, target=run_server_process,
|
process = multiprocessing.Process(group=None, target=run_server_process,
|
||||||
args=(self.name, self.ponyconfig, get_static_server_data(),
|
args=(self.room_id, self.ponyconfig, get_static_server_data(),
|
||||||
self.cert, self.key, self.host,
|
self.cert, self.key, self.host),
|
||||||
self.rooms_to_start, self.rooms_shutting_down),
|
name="MultiHost")
|
||||||
name=self.name)
|
|
||||||
process.start()
|
process.start()
|
||||||
|
# bind after start to prevent thread sync issues with guardian.
|
||||||
self.process = process
|
self.process = process
|
||||||
|
|
||||||
def start_room(self, room_id):
|
|
||||||
while not self.rooms_shutting_down.empty():
|
|
||||||
self.room_ids.remove(self.rooms_shutting_down.get(block=True, timeout=None))
|
|
||||||
if room_id in self.room_ids:
|
|
||||||
pass # should already be hosted currently.
|
|
||||||
else:
|
|
||||||
self.room_ids.add(room_id)
|
|
||||||
self.rooms_to_start.put(room_id)
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self.process:
|
if self.process:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
@@ -185,6 +168,40 @@ class MultiworldInstance():
|
|||||||
self.process = None
|
self.process = None
|
||||||
|
|
||||||
|
|
||||||
|
guardian = None
|
||||||
|
guardian_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def run_guardian():
|
||||||
|
global guardian
|
||||||
|
global multiworlds
|
||||||
|
with guardian_lock:
|
||||||
|
if not guardian:
|
||||||
|
try:
|
||||||
|
import resource
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
pass # unix only module
|
||||||
|
else:
|
||||||
|
# Each Server is another file handle, so request as many as we can from the system
|
||||||
|
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
|
||||||
|
# set soft limit to hard limit
|
||||||
|
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
|
||||||
|
|
||||||
|
def guard():
|
||||||
|
while 1:
|
||||||
|
time.sleep(1)
|
||||||
|
done = []
|
||||||
|
with guardian_lock:
|
||||||
|
for key, instance in multiworlds.items():
|
||||||
|
if instance.done():
|
||||||
|
instance.collect()
|
||||||
|
done.append(key)
|
||||||
|
for key in done:
|
||||||
|
del (multiworlds[key])
|
||||||
|
|
||||||
|
guardian = threading.Thread(name="Guardian", target=guard)
|
||||||
|
|
||||||
|
|
||||||
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed, Slot
|
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed, Slot
|
||||||
from .customserver import run_server_process, get_static_server_data
|
from .customserver import run_server_process, get_static_server_data
|
||||||
from .generate import gen_game
|
from .generate import gen_game
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import collections
|
|||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
|
||||||
import pickle
|
import pickle
|
||||||
import random
|
import random
|
||||||
import socket
|
import socket
|
||||||
@@ -54,19 +53,17 @@ del MultiServer
|
|||||||
|
|
||||||
class DBCommandProcessor(ServerCommandProcessor):
|
class DBCommandProcessor(ServerCommandProcessor):
|
||||||
def output(self, text: str):
|
def output(self, text: str):
|
||||||
self.ctx.logger.info(text)
|
logging.info(text)
|
||||||
|
|
||||||
|
|
||||||
class WebHostContext(Context):
|
class WebHostContext(Context):
|
||||||
room_id: int
|
room_id: int
|
||||||
|
|
||||||
def __init__(self, static_server_data: dict, logger: logging.Logger):
|
def __init__(self, static_server_data: dict):
|
||||||
# static server data is used during _load_game_data to load required data,
|
# static server data is used during _load_game_data to load required data,
|
||||||
# without needing to import worlds system, which takes quite a bit of memory
|
# without needing to import worlds system, which takes quite a bit of memory
|
||||||
self.static_server_data = static_server_data
|
self.static_server_data = static_server_data
|
||||||
super(WebHostContext, self).__init__("", 0, "", "", 1,
|
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
|
||||||
40, True, "enabled", "enabled",
|
|
||||||
"enabled", 0, 2, logger=logger)
|
|
||||||
del self.static_server_data
|
del self.static_server_data
|
||||||
self.main_loop = asyncio.get_running_loop()
|
self.main_loop = asyncio.get_running_loop()
|
||||||
self.video = {}
|
self.video = {}
|
||||||
@@ -74,7 +71,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 +98,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 +119,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
|
||||||
@@ -182,125 +159,72 @@ def get_static_server_data() -> dict:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def set_up_logging(room_id) -> logging.Logger:
|
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||||
import os
|
|
||||||
# logger setup
|
|
||||||
logger = logging.getLogger(f"RoomLogger {room_id}")
|
|
||||||
|
|
||||||
# this *should* be empty, but just in case.
|
|
||||||
for handler in logger.handlers[:]:
|
|
||||||
logger.removeHandler(handler)
|
|
||||||
handler.close()
|
|
||||||
|
|
||||||
file_handler = logging.FileHandler(
|
|
||||||
os.path.join(Utils.user_path("logs"), f"{room_id}.txt"),
|
|
||||||
"a",
|
|
||||||
encoding="utf-8-sig")
|
|
||||||
file_handler.setFormatter(logging.Formatter("[%(asctime)s]: %(message)s"))
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
logger.addHandler(file_handler)
|
|
||||||
return logger
|
|
||||||
|
|
||||||
|
|
||||||
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|
||||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
host: str):
|
||||||
Utils.init_logging(name)
|
|
||||||
try:
|
|
||||||
import resource
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
pass # unix only module
|
|
||||||
else:
|
|
||||||
# Each Server is another file handle, so request as many as we can from the system
|
|
||||||
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
|
|
||||||
# set soft limit to hard limit
|
|
||||||
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
|
|
||||||
del resource, file_limit
|
|
||||||
|
|
||||||
# establish DB connection for multidata and multisave
|
# establish DB connection for multidata and multisave
|
||||||
db.bind(**ponyconfig)
|
db.bind(**ponyconfig)
|
||||||
db.generate_mapping(check_tables=False)
|
db.generate_mapping(check_tables=False)
|
||||||
|
|
||||||
if "worlds" in sys.modules:
|
async def main():
|
||||||
raise Exception("Worlds system should not be loaded in the custom server.")
|
if "worlds" in sys.modules:
|
||||||
|
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||||
|
|
||||||
import gc
|
import gc
|
||||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
Utils.init_logging(str(room_id), write_mode="a")
|
||||||
del cert_file, cert_key_file, ponyconfig
|
ctx = WebHostContext(static_server_data)
|
||||||
gc.collect() # free intermediate objects used during setup
|
ctx.load(room_id)
|
||||||
|
ctx.init_save()
|
||||||
|
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||||
|
gc.collect() # free intermediate objects used during setup
|
||||||
|
try:
|
||||||
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
await ctx.server
|
||||||
|
except OSError: # likely port in use
|
||||||
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||||
|
|
||||||
async def start_room(room_id):
|
await ctx.server
|
||||||
with Locker(f"RoomLocker {room_id}"):
|
port = 0
|
||||||
try:
|
for wssocket in ctx.server.ws_server.sockets:
|
||||||
logger = set_up_logging(room_id)
|
socketname = wssocket.getsockname()
|
||||||
ctx = WebHostContext(static_server_data, logger)
|
if wssocket.family == socket.AF_INET6:
|
||||||
ctx.load(room_id)
|
# Prefer IPv4, as most users seem to not have working ipv6 support
|
||||||
ctx.init_save()
|
if not port:
|
||||||
try:
|
port = socketname[1]
|
||||||
ctx.server = websockets.serve(
|
elif wssocket.family == socket.AF_INET:
|
||||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
port = socketname[1]
|
||||||
|
if port:
|
||||||
|
logging.info(f'Hosting game at {host}:{port}')
|
||||||
|
with db_session:
|
||||||
|
room = Room.get(id=ctx.room_id)
|
||||||
|
room.last_port = port
|
||||||
|
else:
|
||||||
|
logging.exception("Could not determine port. Likely hosting failure.")
|
||||||
|
with db_session:
|
||||||
|
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||||
|
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||||
|
await ctx.shutdown_task
|
||||||
|
|
||||||
await ctx.server
|
# ensure auto launch is on the same page in regard to room activity.
|
||||||
except OSError: # likely port in use
|
with db_session:
|
||||||
ctx.server = websockets.serve(
|
room: Room = Room.get(id=ctx.room_id)
|
||||||
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
|
||||||
|
|
||||||
await ctx.server
|
logging.info("Shutting down")
|
||||||
port = 0
|
|
||||||
for wssocket in ctx.server.ws_server.sockets:
|
|
||||||
socketname = wssocket.getsockname()
|
|
||||||
if wssocket.family == socket.AF_INET6:
|
|
||||||
# Prefer IPv4, as most users seem to not have working ipv6 support
|
|
||||||
if not port:
|
|
||||||
port = socketname[1]
|
|
||||||
elif wssocket.family == socket.AF_INET:
|
|
||||||
port = socketname[1]
|
|
||||||
if port:
|
|
||||||
ctx.logger.info(f'Hosting game at {host}:{port}')
|
|
||||||
with db_session:
|
|
||||||
room = Room.get(id=ctx.room_id)
|
|
||||||
room.last_port = port
|
|
||||||
else:
|
|
||||||
ctx.logger.exception("Could not determine port. Likely hosting failure.")
|
|
||||||
with db_session:
|
|
||||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
|
||||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
|
||||||
await ctx.shutdown_task
|
|
||||||
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
with Locker(room_id):
|
||||||
if ctx.saving:
|
try:
|
||||||
ctx._save()
|
asyncio.run(main())
|
||||||
except Exception as e:
|
except (KeyboardInterrupt, SystemExit):
|
||||||
with db_session:
|
with db_session:
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
room.last_port = -1
|
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||||
logger.exception(e)
|
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||||
raise
|
except Exception:
|
||||||
else:
|
with db_session:
|
||||||
if ctx.saving:
|
room = Room.get(id=room_id)
|
||||||
ctx._save()
|
room.last_port = -1
|
||||||
finally:
|
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||||
try:
|
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||||
with (db_session):
|
raise
|
||||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
|
||||||
room = Room.get(id=room_id)
|
|
||||||
room.last_activity = datetime.datetime.utcnow() - \
|
|
||||||
datetime.timedelta(minutes=1, seconds=room.timeout)
|
|
||||||
logging.info(f"Shutting down room {room_id} on {name}.")
|
|
||||||
finally:
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
rooms_shutting_down.put(room_id)
|
|
||||||
|
|
||||||
class Starter(threading.Thread):
|
|
||||||
def run(self):
|
|
||||||
while 1:
|
|
||||||
next_room = rooms_to_run.get(block=True, timeout=None)
|
|
||||||
asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
|
|
||||||
logging.info(f"Starting room {next_room} on {name}.")
|
|
||||||
|
|
||||||
starter = Starter()
|
|
||||||
starter.daemon = True
|
|
||||||
starter.start()
|
|
||||||
loop.run_forever()
|
|
||||||
|
|||||||
@@ -70,41 +70,37 @@ 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)
|
results, gen_options = roll_options(options, set(meta["plando_options"]))
|
||||||
|
|
||||||
|
if any(type(result) == str for result in results.values()):
|
||||||
|
return render_template("checkResult.html", results=results)
|
||||||
|
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||||
|
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
|
||||||
|
f"If you have a larger group, please generate it yourself and upload it.")
|
||||||
|
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
|
||||||
|
gen = Generation(
|
||||||
|
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||||
|
# convert to json compatible
|
||||||
|
meta=json.dumps(meta),
|
||||||
|
state=STATE_QUEUED,
|
||||||
|
owner=session["_id"])
|
||||||
|
commit()
|
||||||
|
|
||||||
|
return redirect(url_for("wait_seed", seed=gen.id))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||||
|
meta=meta, owner=session["_id"].int)
|
||||||
|
except BaseException as e:
|
||||||
|
from .autolauncher import handle_generation_failure
|
||||||
|
handle_generation_failure(e)
|
||||||
|
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
||||||
|
|
||||||
|
return redirect(url_for("view_seed", seed=seed_id))
|
||||||
|
|
||||||
return render_template("generate.html", race=race, version=__version__)
|
return render_template("generate.html", race=race, version=__version__)
|
||||||
|
|
||||||
|
|
||||||
def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any]):
|
|
||||||
results, gen_options = roll_options(options, set(meta["plando_options"]))
|
|
||||||
|
|
||||||
if any(type(result) == str for result in results.values()):
|
|
||||||
return render_template("checkResult.html", results=results)
|
|
||||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
|
||||||
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
|
|
||||||
f"If you have a larger group, please generate it yourself and upload it.")
|
|
||||||
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
|
|
||||||
gen = Generation(
|
|
||||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
|
||||||
# convert to json compatible
|
|
||||||
meta=json.dumps(meta),
|
|
||||||
state=STATE_QUEUED,
|
|
||||||
owner=session["_id"])
|
|
||||||
commit()
|
|
||||||
|
|
||||||
return redirect(url_for("wait_seed", seed=gen.id))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
|
||||||
meta=meta, owner=session["_id"].int)
|
|
||||||
except BaseException as e:
|
|
||||||
from .autolauncher import handle_generation_failure
|
|
||||||
handle_generation_failure(e)
|
|
||||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
|
||||||
|
|
||||||
return redirect(url_for("view_seed", seed=seed_id))
|
|
||||||
|
|
||||||
|
|
||||||
def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
|
def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
|
||||||
if not meta:
|
if not meta:
|
||||||
meta: Dict[str, Any] = {}
|
meta: Dict[str, Any] = {}
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ def start_playing():
|
|||||||
return render_template(f"startPlaying.html")
|
return render_template(f"startPlaying.html")
|
||||||
|
|
||||||
|
|
||||||
|
# TODO for back compat. remove around 0.4.5
|
||||||
|
@app.route("/weighted-settings")
|
||||||
|
def weighted_settings():
|
||||||
|
return redirect("weighted-options", 301)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/weighted-options")
|
||||||
|
@cache.cached()
|
||||||
|
def weighted_options():
|
||||||
|
return render_template("weighted-options.html")
|
||||||
|
|
||||||
|
|
||||||
|
# Player options pages
|
||||||
|
@app.route("/games/<string:game>/player-options")
|
||||||
|
@cache.cached()
|
||||||
|
def player_options(game: str):
|
||||||
|
return render_template("player-options.html", game=game, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
# Game Info Pages
|
# Game Info Pages
|
||||||
@app.route('/games/<string:game>/info/<string:lang>')
|
@app.route('/games/<string:game>/info/<string:lang>')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
|
|||||||
@@ -1,226 +1,205 @@
|
|||||||
import collections.abc
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from textwrap import dedent
|
import typing
|
||||||
from typing import Dict, Union
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
from flask import redirect, render_template, request, Response
|
|
||||||
|
|
||||||
import Options
|
import Options
|
||||||
from Utils import local_path
|
from Utils import local_path
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
from . import app, cache
|
|
||||||
|
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
||||||
|
"exclude_locations", "priority_locations"}
|
||||||
|
|
||||||
|
|
||||||
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_html_doc(option_type: type(Options.Option)) -> str:
|
||||||
|
if not option_type.__doc__:
|
||||||
|
return "Please document me!"
|
||||||
|
return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip()
|
||||||
|
|
||||||
def get_world_theme(game_name: str) -> str:
|
weighted_options = {
|
||||||
if game_name in AutoWorldRegister.world_types:
|
"baseOptions": {
|
||||||
return AutoWorldRegister.world_types[game_name].web.theme
|
"description": "Generated by https://archipelago.gg/",
|
||||||
return 'grass'
|
"name": "",
|
||||||
|
"game": {},
|
||||||
|
},
|
||||||
|
"games": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for game_name, world in AutoWorldRegister.world_types.items():
|
||||||
|
|
||||||
def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]:
|
all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints
|
||||||
visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui
|
|
||||||
world = AutoWorldRegister.world_types[world_name]
|
|
||||||
if world.hidden or world.web.options_page is False:
|
|
||||||
return redirect("games")
|
|
||||||
|
|
||||||
option_groups = {option: option_group.name
|
# Generate JSON files for player-options pages
|
||||||
for option_group in world.web.option_groups
|
player_options = {
|
||||||
for option in option_group.options}
|
"baseOptions": {
|
||||||
ordered_groups = ["Game Options", *[group.name for group in world.web.option_groups]]
|
"description": f"Generated by https://archipelago.gg/ for {game_name}",
|
||||||
grouped_options = {group: {} for group in ordered_groups}
|
"game": game_name,
|
||||||
for option_name, option in world.options_dataclass.type_hints.items():
|
"name": "",
|
||||||
# Exclude settings from options pages if their visibility is disabled
|
},
|
||||||
if visibility_flag in option.visibility:
|
|
||||||
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
template,
|
|
||||||
world_name=world_name,
|
|
||||||
world=world,
|
|
||||||
option_groups=grouped_options,
|
|
||||||
issubclass=issubclass,
|
|
||||||
Options=Options,
|
|
||||||
theme=get_world_theme(world_name),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_game(options: Dict[str, Union[dict, str]]) -> Union[Response, str]:
|
|
||||||
from .generate import start_generation
|
|
||||||
return start_generation(options, {"plando_options": ["items", "connections", "texts", "bosses"]})
|
|
||||||
|
|
||||||
|
|
||||||
def send_yaml(player_name: str, formatted_options: dict) -> Response:
|
|
||||||
response = Response(yaml.dump(formatted_options, sort_keys=False))
|
|
||||||
response.headers["Content-Type"] = "text/yaml"
|
|
||||||
response.headers["Content-Disposition"] = f"attachment; filename={player_name}.yaml"
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@app.template_filter("dedent")
|
|
||||||
def filter_dedent(text: str) -> str:
|
|
||||||
return dedent(text).strip("\n ")
|
|
||||||
|
|
||||||
|
|
||||||
@app.template_test("ordered")
|
|
||||||
def test_ordered(obj):
|
|
||||||
return isinstance(obj, collections.abc.Sequence)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/games/<string:game>/option-presets", methods=["GET"])
|
|
||||||
@cache.cached()
|
|
||||||
def option_presets(game: str) -> Response:
|
|
||||||
world = AutoWorldRegister.world_types[game]
|
|
||||||
|
|
||||||
class SetEncoder(json.JSONEncoder):
|
|
||||||
def default(self, obj):
|
|
||||||
from collections.abc import Set
|
|
||||||
if isinstance(obj, Set):
|
|
||||||
return list(obj)
|
|
||||||
return json.JSONEncoder.default(self, obj)
|
|
||||||
|
|
||||||
json_data = json.dumps(world.web.options_presets, cls=SetEncoder)
|
|
||||||
response = Response(json_data)
|
|
||||||
response.headers["Content-Type"] = "application/json"
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/weighted-options")
|
|
||||||
def weighted_options_old():
|
|
||||||
return redirect("games", 301)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/games/<string:game>/weighted-options")
|
|
||||||
@cache.cached()
|
|
||||||
def weighted_options(game: str):
|
|
||||||
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
|
|
||||||
def generate_weighted_yaml(game: str):
|
|
||||||
if request.method == "POST":
|
|
||||||
intent_generate = False
|
|
||||||
options = {}
|
|
||||||
|
|
||||||
for key, val in request.form.items():
|
|
||||||
if "||" not in key:
|
|
||||||
if len(str(val)) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
options[key] = val
|
|
||||||
else:
|
|
||||||
if int(val) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
[option, setting] = key.split("||")
|
|
||||||
options.setdefault(option, {})[setting] = int(val)
|
|
||||||
|
|
||||||
# Error checking
|
|
||||||
if "name" not in options:
|
|
||||||
return "Player name is required."
|
|
||||||
|
|
||||||
# Remove POST data irrelevant to YAML
|
|
||||||
if "intent-generate" in options:
|
|
||||||
intent_generate = True
|
|
||||||
del options["intent-generate"]
|
|
||||||
if "intent-export" in options:
|
|
||||||
del options["intent-export"]
|
|
||||||
|
|
||||||
# Properly format YAML output
|
|
||||||
player_name = options["name"]
|
|
||||||
del options["name"]
|
|
||||||
|
|
||||||
formatted_options = {
|
|
||||||
"name": player_name,
|
|
||||||
"game": game,
|
|
||||||
"description": f"Generated by https://archipelago.gg/ for {game}",
|
|
||||||
game: options,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if intent_generate:
|
game_options = {}
|
||||||
return generate_game({player_name: formatted_options})
|
visible: typing.Set[str] = set()
|
||||||
|
visible_weighted: typing.Set[str] = set()
|
||||||
|
|
||||||
else:
|
for option_name, option in all_options.items():
|
||||||
return send_yaml(player_name, formatted_options)
|
if option.visibility & Options.Visibility.simple_ui:
|
||||||
|
visible.add(option_name)
|
||||||
|
if option.visibility & Options.Visibility.complex_ui:
|
||||||
|
visible_weighted.add(option_name)
|
||||||
|
|
||||||
|
if option_name in handled_in_js:
|
||||||
|
pass
|
||||||
|
|
||||||
# Player options pages
|
elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle):
|
||||||
@app.route("/games/<string:game>/player-options")
|
game_options[option_name] = this_option = {
|
||||||
@cache.cached()
|
"type": "select",
|
||||||
def player_options(game: str):
|
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||||
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
|
"description": get_html_doc(option),
|
||||||
|
"defaultValue": None,
|
||||||
|
"options": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for sub_option_id, sub_option_name in option.name_lookup.items():
|
||||||
|
if sub_option_name != "random":
|
||||||
|
this_option["options"].append({
|
||||||
|
"name": option.get_option_name(sub_option_id),
|
||||||
|
"value": sub_option_name,
|
||||||
|
})
|
||||||
|
if sub_option_id == option.default:
|
||||||
|
this_option["defaultValue"] = sub_option_name
|
||||||
|
|
||||||
|
if not this_option["defaultValue"]:
|
||||||
|
this_option["defaultValue"] = "random"
|
||||||
|
|
||||||
|
elif issubclass(option, Options.Range):
|
||||||
|
game_options[option_name] = {
|
||||||
|
"type": "range",
|
||||||
|
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||||
|
"description": get_html_doc(option),
|
||||||
|
"defaultValue": option.default if hasattr(
|
||||||
|
option, "default") and option.default != "random" else option.range_start,
|
||||||
|
"min": option.range_start,
|
||||||
|
"max": option.range_end,
|
||||||
|
}
|
||||||
|
|
||||||
|
if issubclass(option, Options.NamedRange):
|
||||||
|
game_options[option_name]["type"] = 'named_range'
|
||||||
|
game_options[option_name]["value_names"] = {}
|
||||||
|
for key, val in option.special_range_names.items():
|
||||||
|
game_options[option_name]["value_names"][key] = val
|
||||||
|
|
||||||
|
elif issubclass(option, Options.ItemSet):
|
||||||
|
game_options[option_name] = {
|
||||||
|
"type": "items-list",
|
||||||
|
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||||
|
"description": get_html_doc(option),
|
||||||
|
"defaultValue": list(option.default)
|
||||||
|
}
|
||||||
|
|
||||||
|
elif issubclass(option, Options.LocationSet):
|
||||||
|
game_options[option_name] = {
|
||||||
|
"type": "locations-list",
|
||||||
|
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||||
|
"description": get_html_doc(option),
|
||||||
|
"defaultValue": list(option.default)
|
||||||
|
}
|
||||||
|
|
||||||
|
elif issubclass(option, Options.VerifyKeys) and not issubclass(option, Options.OptionDict):
|
||||||
|
if option.valid_keys:
|
||||||
|
game_options[option_name] = {
|
||||||
|
"type": "custom-list",
|
||||||
|
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||||
|
"description": get_html_doc(option),
|
||||||
|
"options": list(option.valid_keys),
|
||||||
|
"defaultValue": list(option.default) if hasattr(option, "default") else []
|
||||||
|
}
|
||||||
|
|
||||||
# YAML generator for player-options
|
|
||||||
@app.route("/games/<string:game>/generate-yaml", methods=["POST"])
|
|
||||||
def generate_yaml(game: str):
|
|
||||||
if request.method == "POST":
|
|
||||||
options = {}
|
|
||||||
intent_generate = False
|
|
||||||
for key, val in request.form.items(multi=True):
|
|
||||||
if key in options:
|
|
||||||
if not isinstance(options[key], list):
|
|
||||||
options[key] = [options[key]]
|
|
||||||
options[key].append(val)
|
|
||||||
else:
|
else:
|
||||||
options[key] = val
|
logging.debug(f"{option} not exported to Web Options.")
|
||||||
|
|
||||||
# Detect and build ItemDict options from their name pattern
|
player_options["presetOptions"] = {}
|
||||||
for key, val in options.copy().items():
|
for preset_name, preset in world.web.options_presets.items():
|
||||||
key_parts = key.rsplit("||", 2)
|
player_options["presetOptions"][preset_name] = {}
|
||||||
if key_parts[-1] == "qty":
|
for option_name, option_value in preset.items():
|
||||||
if key_parts[0] not in options:
|
# Random range type settings are not valid.
|
||||||
options[key_parts[0]] = {}
|
assert (not str(option_value).startswith("random-")), \
|
||||||
if val != "0":
|
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. Special random " \
|
||||||
options[key_parts[0]][key_parts[1]] = int(val)
|
f"values are not supported for presets."
|
||||||
del options[key]
|
|
||||||
|
|
||||||
# Detect random-* keys and set their options accordingly
|
# Normal random is supported, but needs to be handled explicitly.
|
||||||
for key, val in options.copy().items():
|
if option_value == "random":
|
||||||
if key.startswith("random-"):
|
player_options["presetOptions"][preset_name][option_name] = option_value
|
||||||
options[key.removeprefix("random-")] = "random"
|
continue
|
||||||
del options[key]
|
|
||||||
|
|
||||||
# Error checking
|
option = world.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||||
if not options["name"]:
|
if isinstance(option, Options.NamedRange) and isinstance(option_value, str):
|
||||||
return "Player name is required."
|
assert option_value in option.special_range_names, \
|
||||||
|
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \
|
||||||
|
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
|
||||||
|
|
||||||
# Remove POST data irrelevant to YAML
|
# Still use the true value for the option, not the name.
|
||||||
preset_name = 'default'
|
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||||
if "intent-generate" in options:
|
elif isinstance(option, Options.Range):
|
||||||
intent_generate = True
|
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||||
del options["intent-generate"]
|
elif isinstance(option_value, str):
|
||||||
if "intent-export" in options:
|
# For Choice and Toggle options, the value should be the name of the option. This is to prevent
|
||||||
del options["intent-export"]
|
# setting a preset for an option with an overridden from_text method that would normally be okay,
|
||||||
if "game-options-preset" in options:
|
# but would not be okay for the webhost's current implementation of player options UI.
|
||||||
preset_name = options["game-options-preset"]
|
assert option.name_lookup[option.value] == option_value, \
|
||||||
del options["game-options-preset"]
|
f"Invalid option value '{option_value}' for '{option_name}' in preset '{preset_name}'. " \
|
||||||
|
f"Values must not be resolved to a different option via option.from_text (or an alias)."
|
||||||
|
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||||
|
else:
|
||||||
|
# int and bool values are fine, just resolve them to the current key for webhost.
|
||||||
|
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||||
|
|
||||||
# Properly format YAML output
|
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
|
||||||
player_name = options["name"]
|
|
||||||
del options["name"]
|
|
||||||
|
|
||||||
description = f"Generated by https://archipelago.gg/ for {game}"
|
filtered_player_options = player_options
|
||||||
if preset_name != 'default' and preset_name != 'custom':
|
filtered_player_options["gameOptions"] = {
|
||||||
description += f" using {preset_name} preset"
|
option_name: option_data for option_name, option_data in game_options.items()
|
||||||
|
if option_name in visible
|
||||||
formatted_options = {
|
|
||||||
"name": player_name,
|
|
||||||
"game": game,
|
|
||||||
"description": description,
|
|
||||||
game: options,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if intent_generate:
|
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
|
||||||
return generate_game({player_name: formatted_options})
|
json.dump(filtered_player_options, f, indent=2, separators=(',', ': '))
|
||||||
|
|
||||||
|
filtered_player_options["gameOptions"] = {
|
||||||
|
option_name: option_data for option_name, option_data in game_options.items()
|
||||||
|
if option_name in visible_weighted
|
||||||
|
}
|
||||||
|
|
||||||
|
if not world.hidden and world.web.options_page is True:
|
||||||
|
# Add the random option to Choice, TextChoice, and Toggle options
|
||||||
|
for option in filtered_player_options["gameOptions"].values():
|
||||||
|
if option["type"] == "select":
|
||||||
|
option["options"].append({"name": "Random", "value": "random"})
|
||||||
|
|
||||||
|
if not option["defaultValue"]:
|
||||||
|
option["defaultValue"] = "random"
|
||||||
|
|
||||||
|
weighted_options["baseOptions"]["game"][game_name] = 0
|
||||||
|
weighted_options["games"][game_name] = {
|
||||||
|
"gameSettings": filtered_player_options["gameOptions"],
|
||||||
|
"gameItems": tuple(world.item_names),
|
||||||
|
"gameItemGroups": [
|
||||||
|
group for group in world.item_name_groups.keys() if group != "Everything"
|
||||||
|
],
|
||||||
|
"gameItemDescriptions": world.item_descriptions,
|
||||||
|
"gameLocations": tuple(world.location_names),
|
||||||
|
"gameLocationGroups": [
|
||||||
|
group for group in world.location_name_groups.keys() if group != "Everywhere"
|
||||||
|
],
|
||||||
|
"gameLocationDescriptions": world.location_descriptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f:
|
||||||
|
json.dump(weighted_options, f, indent=2, separators=(',', ': '))
|
||||||
|
|
||||||
else:
|
|
||||||
return send_yaml(player_name, formatted_options)
|
|
||||||
|
|||||||
523
WebHostLib/static/assets/player-options.js
Normal file
523
WebHostLib/static/assets/player-options.js
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
let gameName = null;
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
gameName = document.getElementById('player-options').getAttribute('data-game');
|
||||||
|
|
||||||
|
// Update game name on page
|
||||||
|
document.getElementById('game-name').innerText = gameName;
|
||||||
|
|
||||||
|
fetchOptionData().then((results) => {
|
||||||
|
let optionHash = localStorage.getItem(`${gameName}-hash`);
|
||||||
|
if (!optionHash) {
|
||||||
|
// If no hash data has been set before, set it now
|
||||||
|
optionHash = md5(JSON.stringify(results));
|
||||||
|
localStorage.setItem(`${gameName}-hash`, optionHash);
|
||||||
|
localStorage.removeItem(gameName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionHash !== md5(JSON.stringify(results))) {
|
||||||
|
showUserMessage(
|
||||||
|
'Your options are out of date! Click here to update them! Be aware this will reset them all to default.'
|
||||||
|
);
|
||||||
|
document.getElementById('user-message').addEventListener('click', resetOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page setup
|
||||||
|
createDefaultOptions(results);
|
||||||
|
buildUI(results);
|
||||||
|
adjustHeaderWidth();
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
document.getElementById('export-options').addEventListener('click', () => exportOptions());
|
||||||
|
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||||
|
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||||
|
|
||||||
|
// Name input field
|
||||||
|
const playerOptions = JSON.parse(localStorage.getItem(gameName));
|
||||||
|
const nameInput = document.getElementById('player-name');
|
||||||
|
nameInput.addEventListener('keyup', (event) => updateBaseOption(event));
|
||||||
|
nameInput.value = playerOptions.name;
|
||||||
|
|
||||||
|
// Presets
|
||||||
|
const presetSelect = document.getElementById('game-options-preset');
|
||||||
|
presetSelect.addEventListener('change', (event) => setPresets(results, event.target.value));
|
||||||
|
for (const preset in results['presetOptions']) {
|
||||||
|
const presetOption = document.createElement('option');
|
||||||
|
presetOption.innerText = preset;
|
||||||
|
presetSelect.appendChild(presetOption);
|
||||||
|
}
|
||||||
|
presetSelect.value = localStorage.getItem(`${gameName}-preset`);
|
||||||
|
results['presetOptions']['__default'] = {};
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetOptions = () => {
|
||||||
|
localStorage.removeItem(gameName);
|
||||||
|
localStorage.removeItem(`${gameName}-hash`);
|
||||||
|
localStorage.removeItem(`${gameName}-preset`);
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOptionData = () => new Promise((resolve, reject) => {
|
||||||
|
const ajax = new XMLHttpRequest();
|
||||||
|
ajax.onreadystatechange = () => {
|
||||||
|
if (ajax.readyState !== 4) { return; }
|
||||||
|
if (ajax.status !== 200) {
|
||||||
|
reject(ajax.responseText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||||
|
catch(error){ reject(error); }
|
||||||
|
};
|
||||||
|
ajax.open('GET', `${window.location.origin}/static/generated/player-options/${gameName}.json`, true);
|
||||||
|
ajax.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDefaultOptions = (optionData) => {
|
||||||
|
if (!localStorage.getItem(gameName)) {
|
||||||
|
const newOptions = {
|
||||||
|
[gameName]: {},
|
||||||
|
};
|
||||||
|
for (let baseOption of Object.keys(optionData.baseOptions)){
|
||||||
|
newOptions[baseOption] = optionData.baseOptions[baseOption];
|
||||||
|
}
|
||||||
|
for (let gameOption of Object.keys(optionData.gameOptions)){
|
||||||
|
newOptions[gameName][gameOption] = optionData.gameOptions[gameOption].defaultValue;
|
||||||
|
}
|
||||||
|
localStorage.setItem(gameName, JSON.stringify(newOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localStorage.getItem(`${gameName}-preset`)) {
|
||||||
|
localStorage.setItem(`${gameName}-preset`, '__default');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildUI = (optionData) => {
|
||||||
|
// Game Options
|
||||||
|
const leftGameOpts = {};
|
||||||
|
const rightGameOpts = {};
|
||||||
|
Object.keys(optionData.gameOptions).forEach((key, index) => {
|
||||||
|
if (index < Object.keys(optionData.gameOptions).length / 2) {
|
||||||
|
leftGameOpts[key] = optionData.gameOptions[key];
|
||||||
|
} else {
|
||||||
|
rightGameOpts[key] = optionData.gameOptions[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
|
||||||
|
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildOptionsTable = (options, romOpts = false) => {
|
||||||
|
const currentOptions = JSON.parse(localStorage.getItem(gameName));
|
||||||
|
const table = document.createElement('table');
|
||||||
|
const tbody = document.createElement('tbody');
|
||||||
|
|
||||||
|
Object.keys(options).forEach((option) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
|
||||||
|
// td Left
|
||||||
|
const tdl = document.createElement('td');
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.textContent = `${options[option].displayName}: `;
|
||||||
|
label.setAttribute('for', option);
|
||||||
|
|
||||||
|
const questionSpan = document.createElement('span');
|
||||||
|
questionSpan.classList.add('interactive');
|
||||||
|
questionSpan.setAttribute('data-tooltip', options[option].description);
|
||||||
|
questionSpan.innerText = '(?)';
|
||||||
|
|
||||||
|
label.appendChild(questionSpan);
|
||||||
|
tdl.appendChild(label);
|
||||||
|
tr.appendChild(tdl);
|
||||||
|
|
||||||
|
// td Right
|
||||||
|
const tdr = document.createElement('td');
|
||||||
|
let element = null;
|
||||||
|
|
||||||
|
const randomButton = document.createElement('button');
|
||||||
|
|
||||||
|
switch(options[option].type) {
|
||||||
|
case 'select':
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.classList.add('select-container');
|
||||||
|
let select = document.createElement('select');
|
||||||
|
select.setAttribute('id', option);
|
||||||
|
select.setAttribute('data-key', option);
|
||||||
|
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||||
|
options[option].options.forEach((opt) => {
|
||||||
|
const optionElement = document.createElement('option');
|
||||||
|
optionElement.setAttribute('value', opt.value);
|
||||||
|
optionElement.innerText = opt.name;
|
||||||
|
|
||||||
|
if ((isNaN(currentOptions[gameName][option]) &&
|
||||||
|
(parseInt(opt.value, 10) === parseInt(currentOptions[gameName][option]))) ||
|
||||||
|
(opt.value === currentOptions[gameName][option]))
|
||||||
|
{
|
||||||
|
optionElement.selected = true;
|
||||||
|
}
|
||||||
|
select.appendChild(optionElement);
|
||||||
|
});
|
||||||
|
select.addEventListener('change', (event) => updateGameOption(event.target));
|
||||||
|
element.appendChild(select);
|
||||||
|
|
||||||
|
// Randomize button
|
||||||
|
randomButton.innerText = '🎲';
|
||||||
|
randomButton.classList.add('randomize-button');
|
||||||
|
randomButton.setAttribute('data-key', option);
|
||||||
|
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||||
|
randomButton.addEventListener('click', (event) => toggleRandomize(event, select));
|
||||||
|
if (currentOptions[gameName][option] === 'random') {
|
||||||
|
randomButton.classList.add('active');
|
||||||
|
select.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.appendChild(randomButton);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'range':
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.classList.add('range-container');
|
||||||
|
|
||||||
|
let range = document.createElement('input');
|
||||||
|
range.setAttribute('id', option);
|
||||||
|
range.setAttribute('type', 'range');
|
||||||
|
range.setAttribute('data-key', option);
|
||||||
|
range.setAttribute('min', options[option].min);
|
||||||
|
range.setAttribute('max', options[option].max);
|
||||||
|
range.value = currentOptions[gameName][option];
|
||||||
|
range.addEventListener('change', (event) => {
|
||||||
|
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||||
|
updateGameOption(event.target);
|
||||||
|
});
|
||||||
|
element.appendChild(range);
|
||||||
|
|
||||||
|
let rangeVal = document.createElement('span');
|
||||||
|
rangeVal.classList.add('range-value');
|
||||||
|
rangeVal.setAttribute('id', `${option}-value`);
|
||||||
|
rangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||||
|
currentOptions[gameName][option] : options[option].defaultValue;
|
||||||
|
element.appendChild(rangeVal);
|
||||||
|
|
||||||
|
// Randomize button
|
||||||
|
randomButton.innerText = '🎲';
|
||||||
|
randomButton.classList.add('randomize-button');
|
||||||
|
randomButton.setAttribute('data-key', option);
|
||||||
|
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||||
|
randomButton.addEventListener('click', (event) => toggleRandomize(event, range));
|
||||||
|
if (currentOptions[gameName][option] === 'random') {
|
||||||
|
randomButton.classList.add('active');
|
||||||
|
range.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.appendChild(randomButton);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'named_range':
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.classList.add('named-range-container');
|
||||||
|
|
||||||
|
// Build the select element
|
||||||
|
let namedRangeSelect = document.createElement('select');
|
||||||
|
namedRangeSelect.setAttribute('data-key', option);
|
||||||
|
Object.keys(options[option].value_names).forEach((presetName) => {
|
||||||
|
let presetOption = document.createElement('option');
|
||||||
|
presetOption.innerText = presetName;
|
||||||
|
presetOption.value = options[option].value_names[presetName];
|
||||||
|
const words = presetOption.innerText.split('_');
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
||||||
|
}
|
||||||
|
presetOption.innerText = words.join(' ');
|
||||||
|
namedRangeSelect.appendChild(presetOption);
|
||||||
|
});
|
||||||
|
let customOption = document.createElement('option');
|
||||||
|
customOption.innerText = 'Custom';
|
||||||
|
customOption.value = 'custom';
|
||||||
|
customOption.selected = true;
|
||||||
|
namedRangeSelect.appendChild(customOption);
|
||||||
|
if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) {
|
||||||
|
namedRangeSelect.value = Number(currentOptions[gameName][option]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build range element
|
||||||
|
let namedRangeWrapper = document.createElement('div');
|
||||||
|
namedRangeWrapper.classList.add('named-range-wrapper');
|
||||||
|
let namedRange = document.createElement('input');
|
||||||
|
namedRange.setAttribute('type', 'range');
|
||||||
|
namedRange.setAttribute('data-key', option);
|
||||||
|
namedRange.setAttribute('min', options[option].min);
|
||||||
|
namedRange.setAttribute('max', options[option].max);
|
||||||
|
namedRange.value = currentOptions[gameName][option];
|
||||||
|
|
||||||
|
// Build rage value element
|
||||||
|
let namedRangeVal = document.createElement('span');
|
||||||
|
namedRangeVal.classList.add('range-value');
|
||||||
|
namedRangeVal.setAttribute('id', `${option}-value`);
|
||||||
|
namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||||
|
currentOptions[gameName][option] : options[option].defaultValue;
|
||||||
|
|
||||||
|
// Configure select event listener
|
||||||
|
namedRangeSelect.addEventListener('change', (event) => {
|
||||||
|
if (event.target.value === 'custom') { return; }
|
||||||
|
|
||||||
|
// Update range slider
|
||||||
|
namedRange.value = event.target.value;
|
||||||
|
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||||
|
updateGameOption(event.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure range event handler
|
||||||
|
namedRange.addEventListener('change', (event) => {
|
||||||
|
// Update select element
|
||||||
|
namedRangeSelect.value =
|
||||||
|
(Object.values(options[option].value_names).includes(parseInt(event.target.value))) ?
|
||||||
|
parseInt(event.target.value) : 'custom';
|
||||||
|
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||||
|
updateGameOption(event.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
element.appendChild(namedRangeSelect);
|
||||||
|
namedRangeWrapper.appendChild(namedRange);
|
||||||
|
namedRangeWrapper.appendChild(namedRangeVal);
|
||||||
|
element.appendChild(namedRangeWrapper);
|
||||||
|
|
||||||
|
// Randomize button
|
||||||
|
randomButton.innerText = '🎲';
|
||||||
|
randomButton.classList.add('randomize-button');
|
||||||
|
randomButton.setAttribute('data-key', option);
|
||||||
|
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||||
|
randomButton.addEventListener('click', (event) => toggleRandomize(
|
||||||
|
event, namedRange, namedRangeSelect)
|
||||||
|
);
|
||||||
|
if (currentOptions[gameName][option] === 'random') {
|
||||||
|
randomButton.classList.add('active');
|
||||||
|
namedRange.disabled = true;
|
||||||
|
namedRangeSelect.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
namedRangeWrapper.appendChild(randomButton);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error(`Ignoring unknown option type: ${options[option].type} with name ${option}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tdr.appendChild(element);
|
||||||
|
tr.appendChild(tdr);
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.appendChild(tbody);
|
||||||
|
return table;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPresets = (optionsData, presetName) => {
|
||||||
|
const defaults = optionsData['gameOptions'];
|
||||||
|
const preset = optionsData['presetOptions'][presetName];
|
||||||
|
|
||||||
|
localStorage.setItem(`${gameName}-preset`, presetName);
|
||||||
|
|
||||||
|
if (!preset) {
|
||||||
|
console.error(`No presets defined for preset name: '${presetName}'`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOptionElement = (option, presetValue) => {
|
||||||
|
const optionElement = document.querySelector(`#${option}[data-key='${option}']`);
|
||||||
|
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||||
|
|
||||||
|
if (presetValue === 'random') {
|
||||||
|
randomElement.classList.add('active');
|
||||||
|
optionElement.disabled = true;
|
||||||
|
updateGameOption(randomElement, false);
|
||||||
|
} else {
|
||||||
|
optionElement.value = presetValue;
|
||||||
|
randomElement.classList.remove('active');
|
||||||
|
optionElement.disabled = undefined;
|
||||||
|
updateGameOption(optionElement, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const option in defaults) {
|
||||||
|
let presetValue = preset[option];
|
||||||
|
if (presetValue === undefined) {
|
||||||
|
// Using the default value if not set in presets.
|
||||||
|
presetValue = defaults[option]['defaultValue'];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (defaults[option].type) {
|
||||||
|
case 'range':
|
||||||
|
const numberElement = document.querySelector(`#${option}-value`);
|
||||||
|
if (presetValue === 'random') {
|
||||||
|
numberElement.innerText = defaults[option]['defaultValue'] === 'random'
|
||||||
|
? defaults[option]['min'] // A fallback so we don't print 'random' in the UI.
|
||||||
|
: defaults[option]['defaultValue'];
|
||||||
|
} else {
|
||||||
|
numberElement.innerText = presetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOptionElement(option, presetValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'select': {
|
||||||
|
updateOptionElement(option, presetValue);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'named_range': {
|
||||||
|
const selectElement = document.querySelector(`select[data-key='${option}']`);
|
||||||
|
const rangeElement = document.querySelector(`input[data-key='${option}']`);
|
||||||
|
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||||
|
|
||||||
|
if (presetValue === 'random') {
|
||||||
|
randomElement.classList.add('active');
|
||||||
|
selectElement.disabled = true;
|
||||||
|
rangeElement.disabled = true;
|
||||||
|
updateGameOption(randomElement, false);
|
||||||
|
} else {
|
||||||
|
rangeElement.value = presetValue;
|
||||||
|
selectElement.value = Object.values(defaults[option]['value_names']).includes(parseInt(presetValue)) ?
|
||||||
|
parseInt(presetValue) : 'custom';
|
||||||
|
document.getElementById(`${option}-value`).innerText = presetValue;
|
||||||
|
|
||||||
|
randomElement.classList.remove('active');
|
||||||
|
selectElement.disabled = undefined;
|
||||||
|
rangeElement.disabled = undefined;
|
||||||
|
updateGameOption(rangeElement, false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`Ignoring preset value for unknown option type: ${defaults[option].type} with name ${option}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
|
||||||
|
const active = event.target.classList.contains('active');
|
||||||
|
const randomButton = event.target;
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
randomButton.classList.remove('active');
|
||||||
|
inputElement.disabled = undefined;
|
||||||
|
if (optionalSelectElement) {
|
||||||
|
optionalSelectElement.disabled = undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
randomButton.classList.add('active');
|
||||||
|
inputElement.disabled = true;
|
||||||
|
if (optionalSelectElement) {
|
||||||
|
optionalSelectElement.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateGameOption(active ? inputElement : randomButton);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBaseOption = (event) => {
|
||||||
|
const options = JSON.parse(localStorage.getItem(gameName));
|
||||||
|
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
|
||||||
|
event.target.value : parseInt(event.target.value);
|
||||||
|
localStorage.setItem(gameName, JSON.stringify(options));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateGameOption = (optionElement, toggleCustomPreset = true) => {
|
||||||
|
const options = JSON.parse(localStorage.getItem(gameName));
|
||||||
|
|
||||||
|
if (toggleCustomPreset) {
|
||||||
|
localStorage.setItem(`${gameName}-preset`, '__custom');
|
||||||
|
const presetElement = document.getElementById('game-options-preset');
|
||||||
|
presetElement.value = '__custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionElement.classList.contains('randomize-button')) {
|
||||||
|
// If the event passed in is the randomize button, then we know what we must do.
|
||||||
|
options[gameName][optionElement.getAttribute('data-key')] = 'random';
|
||||||
|
} else {
|
||||||
|
options[gameName][optionElement.getAttribute('data-key')] = isNaN(optionElement.value) ?
|
||||||
|
optionElement.value : parseInt(optionElement.value, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(gameName, JSON.stringify(options));
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportOptions = () => {
|
||||||
|
const options = JSON.parse(localStorage.getItem(gameName));
|
||||||
|
const preset = localStorage.getItem(`${gameName}-preset`);
|
||||||
|
switch (preset) {
|
||||||
|
case '__default':
|
||||||
|
options['description'] = `Generated by https://archipelago.gg with the default preset.`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '__custom':
|
||||||
|
options['description'] = `Generated by https://archipelago.gg.`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.name || options.name.toString().trim().length === 0) {
|
||||||
|
return showUserMessage('You must enter a player name!');
|
||||||
|
}
|
||||||
|
const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||||
|
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Create an anchor and trigger a download of a text file. */
|
||||||
|
const download = (filename, text) => {
|
||||||
|
const downloadLink = document.createElement('a');
|
||||||
|
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
|
||||||
|
downloadLink.setAttribute('download', filename);
|
||||||
|
downloadLink.style.display = 'none';
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
document.body.removeChild(downloadLink);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateGame = (raceMode = false) => {
|
||||||
|
const options = JSON.parse(localStorage.getItem(gameName));
|
||||||
|
if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) {
|
||||||
|
return showUserMessage('You must enter a player name!');
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.post('/api/generate', {
|
||||||
|
weights: { player: options },
|
||||||
|
presetData: { player: options },
|
||||||
|
playerCount: 1,
|
||||||
|
spoiler: 3,
|
||||||
|
race: raceMode ? '1' : '0',
|
||||||
|
}).then((response) => {
|
||||||
|
window.location.href = response.data.url;
|
||||||
|
}).catch((error) => {
|
||||||
|
let userMessage = 'Something went wrong and your game could not be generated.';
|
||||||
|
if (error.response.data.text) {
|
||||||
|
userMessage += ' ' + error.response.data.text;
|
||||||
|
}
|
||||||
|
showUserMessage(userMessage);
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showUserMessage = (message) => {
|
||||||
|
const userMessage = document.getElementById('user-message');
|
||||||
|
userMessage.innerText = message;
|
||||||
|
userMessage.classList.add('visible');
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
userMessage.addEventListener('click', () => {
|
||||||
|
userMessage.classList.remove('visible');
|
||||||
|
userMessage.addEventListener('click', hideUserMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideUserMessage = () => {
|
||||||
|
const userMessage = document.getElementById('user-message');
|
||||||
|
userMessage.classList.remove('visible');
|
||||||
|
userMessage.removeEventListener('click', hideUserMessage);
|
||||||
|
};
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
let presets = {};
|
|
||||||
|
|
||||||
window.addEventListener('load', async () => {
|
|
||||||
// Load settings from localStorage, if available
|
|
||||||
loadSettings();
|
|
||||||
|
|
||||||
// Fetch presets if available
|
|
||||||
await fetchPresets();
|
|
||||||
|
|
||||||
// Handle changes to range inputs
|
|
||||||
document.querySelectorAll('input[type=range]').forEach((range) => {
|
|
||||||
const optionName = range.getAttribute('id');
|
|
||||||
range.addEventListener('change', () => {
|
|
||||||
document.getElementById(`${optionName}-value`).innerText = range.value;
|
|
||||||
|
|
||||||
// Handle updating named range selects to "custom" if appropriate
|
|
||||||
const select = document.querySelector(`select[data-option-name=${optionName}]`);
|
|
||||||
if (select) {
|
|
||||||
let updated = false;
|
|
||||||
select?.childNodes.forEach((option) => {
|
|
||||||
if (option.value === range.value) {
|
|
||||||
select.value = range.value;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!updated) {
|
|
||||||
select.value = 'custom';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle changes to named range selects
|
|
||||||
document.querySelectorAll('.named-range-container select').forEach((select) => {
|
|
||||||
const optionName = select.getAttribute('data-option-name');
|
|
||||||
select.addEventListener('change', (evt) => {
|
|
||||||
document.getElementById(optionName).value = evt.target.value;
|
|
||||||
document.getElementById(`${optionName}-value`).innerText = evt.target.value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle changes to randomize checkboxes
|
|
||||||
document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => {
|
|
||||||
const optionName = checkbox.getAttribute('data-option-name');
|
|
||||||
checkbox.addEventListener('change', () => {
|
|
||||||
const optionInput = document.getElementById(optionName);
|
|
||||||
const namedRangeSelect = document.querySelector(`select[data-option-name=${optionName}]`);
|
|
||||||
const customInput = document.getElementById(`${optionName}-custom`);
|
|
||||||
if (checkbox.checked) {
|
|
||||||
optionInput.setAttribute('disabled', '1');
|
|
||||||
namedRangeSelect?.setAttribute('disabled', '1');
|
|
||||||
if (customInput) {
|
|
||||||
customInput.setAttribute('disabled', '1');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
optionInput.removeAttribute('disabled');
|
|
||||||
namedRangeSelect?.removeAttribute('disabled');
|
|
||||||
if (customInput) {
|
|
||||||
customInput.removeAttribute('disabled');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle changes to TextChoice input[type=text]
|
|
||||||
document.querySelectorAll('.text-choice-container input[type=text]').forEach((input) => {
|
|
||||||
const optionName = input.getAttribute('data-option-name');
|
|
||||||
input.addEventListener('input', () => {
|
|
||||||
const select = document.getElementById(optionName);
|
|
||||||
const optionValues = [];
|
|
||||||
select.childNodes.forEach((option) => optionValues.push(option.value));
|
|
||||||
select.value = (optionValues.includes(input.value)) ? input.value : 'custom';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle changes to TextChoice select
|
|
||||||
document.querySelectorAll('.text-choice-container select').forEach((select) => {
|
|
||||||
const optionName = select.getAttribute('id');
|
|
||||||
select.addEventListener('change', () => {
|
|
||||||
document.getElementById(`${optionName}-custom`).value = '';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the "Option Preset" select to read "custom" when changes are made to relevant inputs
|
|
||||||
const presetSelect = document.getElementById('game-options-preset');
|
|
||||||
document.querySelectorAll('input, select').forEach((input) => {
|
|
||||||
if ( // Ignore inputs which have no effect on yaml generation
|
|
||||||
(input.id === 'player-name') ||
|
|
||||||
(input.id === 'game-options-preset') ||
|
|
||||||
(input.classList.contains('group-toggle')) ||
|
|
||||||
(input.type === 'submit')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
input.addEventListener('change', () => {
|
|
||||||
presetSelect.value = 'custom';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle changes to presets select
|
|
||||||
document.getElementById('game-options-preset').addEventListener('change', choosePreset);
|
|
||||||
|
|
||||||
// Save settings to localStorage when form is submitted
|
|
||||||
document.getElementById('options-form').addEventListener('submit', (evt) => {
|
|
||||||
const playerName = document.getElementById('player-name');
|
|
||||||
if (!playerName.value.trim()) {
|
|
||||||
evt.preventDefault();
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
showUserMessage('You must enter a player name!');
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSettings();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save all settings to localStorage
|
|
||||||
const saveSettings = () => {
|
|
||||||
const options = {
|
|
||||||
inputs: {},
|
|
||||||
checkboxes: {},
|
|
||||||
};
|
|
||||||
document.querySelectorAll('input, select').forEach((input) => {
|
|
||||||
if (input.type === 'submit') {
|
|
||||||
// Ignore submit inputs
|
|
||||||
}
|
|
||||||
else if (input.type === 'checkbox') {
|
|
||||||
options.checkboxes[input.id] = input.checked;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
options.inputs[input.id] = input.value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
|
||||||
localStorage.setItem(game, JSON.stringify(options));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load all options from localStorage
|
|
||||||
const loadSettings = () => {
|
|
||||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
|
||||||
|
|
||||||
const options = JSON.parse(localStorage.getItem(game));
|
|
||||||
if (options) {
|
|
||||||
if (!options.inputs || !options.checkboxes) {
|
|
||||||
localStorage.removeItem(game);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore value-based inputs and selects
|
|
||||||
Object.keys(options.inputs).forEach((key) => {
|
|
||||||
try{
|
|
||||||
document.getElementById(key).value = options.inputs[key];
|
|
||||||
const rangeValue = document.getElementById(`${key}-value`);
|
|
||||||
if (rangeValue) {
|
|
||||||
rangeValue.innerText = options.inputs[key];
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Unable to restore value to input with id ${key}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restore checkboxes
|
|
||||||
Object.keys(options.checkboxes).forEach((key) => {
|
|
||||||
try{
|
|
||||||
if (options.checkboxes[key]) {
|
|
||||||
document.getElementById(key).setAttribute('checked', '1');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Unable to restore value to input with id ${key}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure any input for which the randomize checkbox is checked by default, the relevant inputs are disabled
|
|
||||||
document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => {
|
|
||||||
const optionName = checkbox.getAttribute('data-option-name');
|
|
||||||
if (checkbox.checked) {
|
|
||||||
const input = document.getElementById(optionName);
|
|
||||||
if (input) {
|
|
||||||
input.setAttribute('disabled', '1');
|
|
||||||
}
|
|
||||||
const customInput = document.getElementById(`${optionName}-custom`);
|
|
||||||
if (customInput) {
|
|
||||||
customInput.setAttribute('disabled', '1');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the preset data for this game and apply the presets if localStorage indicates one was previously chosen
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
const fetchPresets = async () => {
|
|
||||||
const response = await fetch('option-presets');
|
|
||||||
presets = await response.json();
|
|
||||||
const presetSelect = document.getElementById('game-options-preset');
|
|
||||||
presetSelect.removeAttribute('disabled');
|
|
||||||
|
|
||||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
|
||||||
const presetToApply = localStorage.getItem(`${game}-preset`);
|
|
||||||
const playerName = localStorage.getItem(`${game}-player`);
|
|
||||||
if (presetToApply) {
|
|
||||||
localStorage.removeItem(`${game}-preset`);
|
|
||||||
presetSelect.value = presetToApply;
|
|
||||||
applyPresets(presetToApply);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playerName) {
|
|
||||||
document.getElementById('player-name').value = playerName;
|
|
||||||
localStorage.removeItem(`${game}-player`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the localStorage for this game and set a preset to be loaded upon page reload
|
|
||||||
* @param evt
|
|
||||||
*/
|
|
||||||
const choosePreset = (evt) => {
|
|
||||||
if (evt.target.value === 'custom') { return; }
|
|
||||||
|
|
||||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
|
||||||
localStorage.removeItem(game);
|
|
||||||
|
|
||||||
localStorage.setItem(`${game}-player`, document.getElementById('player-name').value);
|
|
||||||
if (evt.target.value !== 'default') {
|
|
||||||
localStorage.setItem(`${game}-preset`, evt.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('#options-form input, #options-form select').forEach((input) => {
|
|
||||||
if (input.id === 'player-name') { return; }
|
|
||||||
input.removeAttribute('value');
|
|
||||||
});
|
|
||||||
|
|
||||||
window.location.replace(window.location.href);
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyPresets = (presetName) => {
|
|
||||||
// Ignore the "default" preset, because it gets set automatically by Jinja
|
|
||||||
if (presetName === 'default') {
|
|
||||||
saveSettings();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!presets[presetName]) {
|
|
||||||
console.error(`Unknown preset ${presetName} chosen`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const preset = presets[presetName];
|
|
||||||
Object.keys(preset).forEach((optionName) => {
|
|
||||||
const optionValue = preset[optionName];
|
|
||||||
|
|
||||||
// Handle List and Set options
|
|
||||||
if (Array.isArray(optionValue)) {
|
|
||||||
document.querySelectorAll(`input[type=checkbox][name=${optionName}]`).forEach((checkbox) => {
|
|
||||||
if (optionValue.includes(checkbox.value)) {
|
|
||||||
checkbox.setAttribute('checked', '1');
|
|
||||||
} else {
|
|
||||||
checkbox.removeAttribute('checked');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Dict options
|
|
||||||
if (typeof(optionValue) === 'object' && optionValue !== null) {
|
|
||||||
const itemNames = Object.keys(optionValue);
|
|
||||||
document.querySelectorAll(`input[type=number][data-option-name=${optionName}]`).forEach((input) => {
|
|
||||||
const itemName = input.getAttribute('data-item-name');
|
|
||||||
input.value = (itemNames.includes(itemName)) ? optionValue[itemName] : 0
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identify all possible elements
|
|
||||||
const normalInput = document.getElementById(optionName);
|
|
||||||
const customInput = document.getElementById(`${optionName}-custom`);
|
|
||||||
const rangeValue = document.getElementById(`${optionName}-value`);
|
|
||||||
const randomizeInput = document.getElementById(`random-${optionName}`);
|
|
||||||
const namedRangeSelect = document.getElementById(`${optionName}-select`);
|
|
||||||
|
|
||||||
// It is possible for named ranges to use name of a value rather than the value itself. This is accounted for here
|
|
||||||
let trueValue = optionValue;
|
|
||||||
if (namedRangeSelect) {
|
|
||||||
namedRangeSelect.querySelectorAll('option').forEach((opt) => {
|
|
||||||
if (opt.innerText.startsWith(optionValue)) {
|
|
||||||
trueValue = opt.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
namedRangeSelect.value = trueValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle options whose presets are "random"
|
|
||||||
if (optionValue === 'random') {
|
|
||||||
normalInput.setAttribute('disabled', '1');
|
|
||||||
randomizeInput.setAttribute('checked', '1');
|
|
||||||
if (customInput) {
|
|
||||||
customInput.setAttribute('disabled', '1');
|
|
||||||
}
|
|
||||||
if (rangeValue) {
|
|
||||||
rangeValue.innerText = normalInput.value;
|
|
||||||
}
|
|
||||||
if (namedRangeSelect) {
|
|
||||||
namedRangeSelect.setAttribute('disabled', '1');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle normal (text, number, select, etc.) and custom inputs (custom inputs exist with TextChoice only)
|
|
||||||
normalInput.value = trueValue;
|
|
||||||
normalInput.removeAttribute('disabled');
|
|
||||||
randomizeInput.removeAttribute('checked');
|
|
||||||
if (customInput) {
|
|
||||||
document.getElementById(`${optionName}-custom`).removeAttribute('disabled');
|
|
||||||
}
|
|
||||||
if (rangeValue) {
|
|
||||||
rangeValue.innerText = trueValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
saveSettings();
|
|
||||||
};
|
|
||||||
|
|
||||||
const showUserMessage = (text) => {
|
|
||||||
const userMessage = document.getElementById('user-message');
|
|
||||||
userMessage.innerText = text;
|
|
||||||
userMessage.addEventListener('click', hideUserMessage);
|
|
||||||
userMessage.style.display = 'block';
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideUserMessage = () => {
|
|
||||||
const userMessage = document.getElementById('user-message');
|
|
||||||
userMessage.removeEventListener('click', hideUserMessage);
|
|
||||||
userMessage.style.display = 'none';
|
|
||||||
};
|
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
// Add toggle listener to all elements with .collapse-toggle
|
// Add toggle listener to all elements with .collapse-toggle
|
||||||
const toggleButtons = document.querySelectorAll('details');
|
const toggleButtons = document.querySelectorAll('.collapse-toggle');
|
||||||
|
toggleButtons.forEach((e) => e.addEventListener('click', toggleCollapse));
|
||||||
|
|
||||||
// Handle game filter input
|
// Handle game filter input
|
||||||
const gameSearch = document.getElementById('game-search');
|
const gameSearch = document.getElementById('game-search');
|
||||||
gameSearch.value = '';
|
gameSearch.value = '';
|
||||||
gameSearch.addEventListener('input', (evt) => {
|
gameSearch.addEventListener('input', (evt) => {
|
||||||
if (!evt.target.value.trim()) {
|
if (!evt.target.value.trim()) {
|
||||||
// If input is empty, display all games as collapsed
|
// If input is empty, display all collapsed games
|
||||||
return toggleButtons.forEach((header) => {
|
return toggleButtons.forEach((header) => {
|
||||||
header.style.display = null;
|
header.style.display = null;
|
||||||
header.removeAttribute('open');
|
header.firstElementChild.innerText = '▶';
|
||||||
|
header.nextElementSibling.classList.add('collapsed');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,10 +21,12 @@ window.addEventListener('load', () => {
|
|||||||
// If the game name includes the search string, display the game. If not, hide it
|
// If the game name includes the search string, display the game. If not, hide it
|
||||||
if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) {
|
if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) {
|
||||||
header.style.display = null;
|
header.style.display = null;
|
||||||
header.setAttribute('open', '1');
|
header.firstElementChild.innerText = '▼';
|
||||||
|
header.nextElementSibling.classList.remove('collapsed');
|
||||||
} else {
|
} else {
|
||||||
header.style.display = 'none';
|
header.style.display = 'none';
|
||||||
header.removeAttribute('open');
|
header.firstElementChild.innerText = '▶';
|
||||||
|
header.nextElementSibling.classList.add('collapsed');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -31,14 +35,30 @@ window.addEventListener('load', () => {
|
|||||||
document.getElementById('collapse-all').addEventListener('click', collapseAll);
|
document.getElementById('collapse-all').addEventListener('click', collapseAll);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toggleCollapse = (evt) => {
|
||||||
|
const gameArrow = evt.target.firstElementChild;
|
||||||
|
const gameInfo = evt.target.nextElementSibling;
|
||||||
|
if (gameInfo.classList.contains('collapsed')) {
|
||||||
|
gameArrow.innerText = '▼';
|
||||||
|
gameInfo.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
gameArrow.innerText = '▶';
|
||||||
|
gameInfo.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const expandAll = () => {
|
const expandAll = () => {
|
||||||
document.querySelectorAll('details').forEach((detail) => {
|
document.querySelectorAll('.collapse-toggle').forEach((header) => {
|
||||||
detail.setAttribute('open', '1');
|
if (header.style.display === 'none') { return; }
|
||||||
|
header.firstElementChild.innerText = '▼';
|
||||||
|
header.nextElementSibling.classList.remove('collapsed');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const collapseAll = () => {
|
const collapseAll = () => {
|
||||||
document.querySelectorAll('details').forEach((detail) => {
|
document.querySelectorAll('.collapse-toggle').forEach((header) => {
|
||||||
detail.removeAttribute('open');
|
if (header.style.display === 'none') { return; }
|
||||||
|
header.firstElementChild.innerText = '▶';
|
||||||
|
header.nextElementSibling.classList.add('collapsed');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
1190
WebHostLib/static/assets/weighted-options.js
Normal file
1190
WebHostLib/static/assets/weighted-options.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,223 +0,0 @@
|
|||||||
let deletedOptions = {};
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const worldName = document.querySelector('#weighted-options').getAttribute('data-game');
|
|
||||||
|
|
||||||
// Generic change listener. Detecting unique qualities and acting on them here reduces initial JS initialisation time
|
|
||||||
// and handles dynamically created elements
|
|
||||||
document.addEventListener('change', (evt) => {
|
|
||||||
// Handle updates to range inputs
|
|
||||||
if (evt.target.type === 'range') {
|
|
||||||
// Update span containing range value. All ranges have a corresponding `{rangeId}-value` span
|
|
||||||
document.getElementById(`${evt.target.id}-value`).innerText = evt.target.value;
|
|
||||||
|
|
||||||
// If the changed option was the name of a game, determine whether to show or hide that game's div
|
|
||||||
if (evt.target.id.startsWith('game||')) {
|
|
||||||
const gameName = evt.target.id.split('||')[1];
|
|
||||||
const gameDiv = document.getElementById(`${gameName}-container`);
|
|
||||||
if (evt.target.value > 0) {
|
|
||||||
gameDiv.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
gameDiv.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generic click listener
|
|
||||||
document.addEventListener('click', (evt) => {
|
|
||||||
// Handle creating new rows for Range options
|
|
||||||
if (evt.target.classList.contains('add-range-option-button')) {
|
|
||||||
const optionName = evt.target.getAttribute('data-option');
|
|
||||||
addRangeRow(optionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle deleting range rows
|
|
||||||
if (evt.target.classList.contains('range-option-delete')) {
|
|
||||||
const targetRow = document.querySelector(`tr[data-row="${evt.target.getAttribute('data-target')}"]`);
|
|
||||||
setDeletedOption(
|
|
||||||
targetRow.getAttribute('data-option-name'),
|
|
||||||
targetRow.getAttribute('data-value'),
|
|
||||||
);
|
|
||||||
targetRow.parentElement.removeChild(targetRow);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for enter presses on inputs intended to add range rows
|
|
||||||
document.addEventListener('keydown', (evt) => {
|
|
||||||
if (evt.key === 'Enter') {
|
|
||||||
evt.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evt.key === 'Enter' && evt.target.classList.contains('range-option-value')) {
|
|
||||||
const optionName = evt.target.getAttribute('data-option');
|
|
||||||
addRangeRow(optionName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Detect form submission
|
|
||||||
document.getElementById('weighted-options-form').addEventListener('submit', (evt) => {
|
|
||||||
// Save data to localStorage
|
|
||||||
const weightedOptions = {};
|
|
||||||
document.querySelectorAll('input[name]').forEach((input) => {
|
|
||||||
const keys = input.getAttribute('name').split('||');
|
|
||||||
|
|
||||||
// Determine keys
|
|
||||||
const optionName = keys[0] ?? null;
|
|
||||||
const subOption = keys[1] ?? null;
|
|
||||||
|
|
||||||
// Ensure keys exist
|
|
||||||
if (!weightedOptions[optionName]) { weightedOptions[optionName] = {}; }
|
|
||||||
if (subOption && !weightedOptions[optionName][subOption]) {
|
|
||||||
weightedOptions[optionName][subOption] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subOption) { return weightedOptions[optionName][subOption] = determineValue(input); }
|
|
||||||
if (optionName) { return weightedOptions[optionName] = determineValue(input); }
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem(`${worldName}-weights`, JSON.stringify(weightedOptions));
|
|
||||||
localStorage.setItem(`${worldName}-deletedOptions`, JSON.stringify(deletedOptions));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove all deleted values as specified by localStorage
|
|
||||||
deletedOptions = JSON.parse(localStorage.getItem(`${worldName}-deletedOptions`) || '{}');
|
|
||||||
Object.keys(deletedOptions).forEach((optionName) => {
|
|
||||||
deletedOptions[optionName].forEach((value) => {
|
|
||||||
const targetRow = document.querySelector(`tr[data-row="${value}-row"]`);
|
|
||||||
targetRow.parentElement.removeChild(targetRow);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Populate all settings from localStorage on page initialisation
|
|
||||||
const previousSettingsJson = localStorage.getItem(`${worldName}-weights`);
|
|
||||||
if (previousSettingsJson) {
|
|
||||||
const previousSettings = JSON.parse(previousSettingsJson);
|
|
||||||
Object.keys(previousSettings).forEach((option) => {
|
|
||||||
if (typeof previousSettings[option] === 'string') {
|
|
||||||
return document.querySelector(`input[name="${option}"]`).value = previousSettings[option];
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(previousSettings[option]).forEach((value) => {
|
|
||||||
const input = document.querySelector(`input[name="${option}||${value}"]`);
|
|
||||||
if (!input?.type) {
|
|
||||||
return console.error(`Unable to populate option with name ${option}||${value}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (input.type) {
|
|
||||||
case 'checkbox':
|
|
||||||
input.checked = (parseInt(previousSettings[option][value], 10) === 1);
|
|
||||||
break;
|
|
||||||
case 'range':
|
|
||||||
input.value = parseInt(previousSettings[option][value], 10);
|
|
||||||
break;
|
|
||||||
case 'number':
|
|
||||||
input.value = previousSettings[option][value].toString();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.error(`Found unsupported input type: ${input.type}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const addRangeRow = (optionName) => {
|
|
||||||
const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
|
|
||||||
const inputTarget = document.querySelector(inputQuery);
|
|
||||||
const newValue = inputTarget.value;
|
|
||||||
if (!/^-?\d+$/.test(newValue)) {
|
|
||||||
alert('Range values must be a positive or negative integer!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
inputTarget.value = '';
|
|
||||||
const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.setAttribute('data-row', `${optionName}-${newValue}-row`);
|
|
||||||
tr.setAttribute('data-option-name', optionName);
|
|
||||||
tr.setAttribute('data-value', newValue);
|
|
||||||
const tdLeft = document.createElement('td');
|
|
||||||
tdLeft.classList.add('td-left');
|
|
||||||
const label = document.createElement('label');
|
|
||||||
label.setAttribute('for', `${optionName}||${newValue}`);
|
|
||||||
label.innerText = newValue.toString();
|
|
||||||
tdLeft.appendChild(label);
|
|
||||||
tr.appendChild(tdLeft);
|
|
||||||
const tdMiddle = document.createElement('td');
|
|
||||||
tdMiddle.classList.add('td-middle');
|
|
||||||
const range = document.createElement('input');
|
|
||||||
range.setAttribute('type', 'range');
|
|
||||||
range.setAttribute('min', '0');
|
|
||||||
range.setAttribute('max', '50');
|
|
||||||
range.setAttribute('value', '0');
|
|
||||||
range.setAttribute('id', `${optionName}||${newValue}`);
|
|
||||||
range.setAttribute('name', `${optionName}||${newValue}`);
|
|
||||||
tdMiddle.appendChild(range);
|
|
||||||
tr.appendChild(tdMiddle);
|
|
||||||
const tdRight = document.createElement('td');
|
|
||||||
tdRight.classList.add('td-right');
|
|
||||||
const valueSpan = document.createElement('span');
|
|
||||||
valueSpan.setAttribute('id', `${optionName}||${newValue}-value`);
|
|
||||||
valueSpan.innerText = '0';
|
|
||||||
tdRight.appendChild(valueSpan);
|
|
||||||
tr.appendChild(tdRight);
|
|
||||||
const tdDelete = document.createElement('td');
|
|
||||||
const deleteSpan = document.createElement('span');
|
|
||||||
deleteSpan.classList.add('range-option-delete');
|
|
||||||
deleteSpan.classList.add('js-required');
|
|
||||||
deleteSpan.setAttribute('data-target', `${optionName}-${newValue}-row`);
|
|
||||||
deleteSpan.innerText = '❌';
|
|
||||||
tdDelete.appendChild(deleteSpan);
|
|
||||||
tr.appendChild(tdDelete);
|
|
||||||
tBody.appendChild(tr);
|
|
||||||
|
|
||||||
// Remove this option from the set of deleted options if it exists
|
|
||||||
unsetDeletedOption(optionName, newValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the value of an input element, or returns a 1 or 0 if the element is a checkbox
|
|
||||||
*
|
|
||||||
* @param {object} input - The input element.
|
|
||||||
* @returns {number} The value of the input element.
|
|
||||||
*/
|
|
||||||
const determineValue = (input) => {
|
|
||||||
switch (input.type) {
|
|
||||||
case 'checkbox':
|
|
||||||
return (input.checked ? 1 : 0);
|
|
||||||
case 'range':
|
|
||||||
return parseInt(input.value, 10);
|
|
||||||
default:
|
|
||||||
return input.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the deleted option value for a given world and option name.
|
|
||||||
* If the world or option does not exist, it creates the necessary entries.
|
|
||||||
*
|
|
||||||
* @param {string} optionName - The name of the option.
|
|
||||||
* @param {*} value - The value to be set for the deleted option.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const setDeletedOption = (optionName, value) => {
|
|
||||||
deletedOptions[optionName] = deletedOptions[optionName] || [];
|
|
||||||
deletedOptions[optionName].push(`${optionName}-${value}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a specific value from the deletedOptions object.
|
|
||||||
*
|
|
||||||
* @param {string} optionName - The name of the option.
|
|
||||||
* @param {*} value - The value to be removed
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const unsetDeletedOption = (optionName, value) => {
|
|
||||||
if (!deletedOptions.hasOwnProperty(optionName)) { return; }
|
|
||||||
if (deletedOptions[optionName].includes(`${optionName}-${value}`)) {
|
|
||||||
deletedOptions[optionName].splice(deletedOptions[optionName].indexOf(`${optionName}-${value}`), 1);
|
|
||||||
}
|
|
||||||
if (deletedOptions[optionName].length === 0) {
|
|
||||||
delete deletedOptions[optionName];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -44,7 +44,7 @@ a{
|
|||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input[type=submit]{
|
button{
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 10px 17px 11px 16px; /* top right bottom left */
|
padding: 10px 17px 11px 16px; /* top right bottom left */
|
||||||
@@ -57,7 +57,7 @@ button, input[type=submit]{
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active, input[type=submit]:active{
|
button:active{
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.5);
|
border-right: 1px solid rgba(0, 0, 0, 0.5);
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
@@ -66,11 +66,11 @@ button:active, input[type=submit]:active{
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.button-grass, input[type=submit].button-grass{
|
button.button-grass{
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.button-dirt, input[type=submit].button-dirt{
|
button.button-dirt{
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
.markdown a{}
|
.markdown a{}
|
||||||
|
|
||||||
.markdown h1, .markdown details summary.h1{
|
.markdown h1{
|
||||||
font-size: 52px;
|
font-size: 52px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-family: LondrinaSolid-Regular, sans-serif;
|
font-family: LondrinaSolid-Regular, sans-serif;
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
text-shadow: 1px 1px 4px #000000;
|
text-shadow: 1px 1px 4px #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h2, .markdown details summary.h2{
|
.markdown h2{
|
||||||
font-size: 38px;
|
font-size: 38px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-family: LondrinaSolid-Light, sans-serif;
|
font-family: LondrinaSolid-Light, sans-serif;
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
text-shadow: 1px 1px 2px #000000;
|
text-shadow: 1px 1px 2px #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h3, .markdown details summary.h3{
|
.markdown h3{
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h4, .markdown details summary.h4{
|
.markdown h4{
|
||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
@@ -63,21 +63,21 @@
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h5, .markdown details summary.h5{
|
.markdown h5{
|
||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h6, .markdown details summary.h6{
|
.markdown h6{
|
||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
cursor: pointer;;
|
cursor: pointer;;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h4, .markdown h5, .markdown h6{
|
.markdown h4, .markdown h5,.markdown h6{
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
244
WebHostLib/static/styles/player-options.css
Normal file
244
WebHostLib/static/styles/player-options.css
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
html{
|
||||||
|
background-image: url('../static/backgrounds/grass.png');
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 650px 650px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options{
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 1024px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
color: #eeffeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options #player-options-button-row{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options code{
|
||||||
|
background-color: #d9cd8e;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options #user-message{
|
||||||
|
display: none;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
background-color: #ffe86b;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #000000;
|
||||||
|
padding: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options #user-message.visible{
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options h1{
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: normal;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-shadow: 1px 1px 4px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options h2{
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: normal;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-transform: lowercase;
|
||||||
|
text-shadow: 1px 1px 2px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options h3, #player-options h4, #player-options h5, #player-options h6{
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options input:not([type]){
|
||||||
|
border: 1px solid #000000;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options input:not([type]):focus{
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options select{
|
||||||
|
border: 1px solid #000000;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
min-width: 150px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options #game-options, #player-options #rom-options{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options #meta-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options div {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options #meta-options label {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 180px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options #meta-options input,
|
||||||
|
#player-options #meta-options select {
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 150px;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options .left, #player-options .right{
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options .left{
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options .right{
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table{
|
||||||
|
margin-bottom: 30px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .select-container{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .select-container select{
|
||||||
|
min-width: 200px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table select:disabled{
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .range-container{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .range-container input[type=range]{
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .range-value{
|
||||||
|
min-width: 20px;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .named-range-container{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .named-range-wrapper{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .named-range-wrapper input[type=range]{
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .randomize-button {
|
||||||
|
max-height: 24px;
|
||||||
|
line-height: 16px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin: 0 0 0 0.25rem;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .randomize-button.active {
|
||||||
|
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table .randomize-button[data-tooltip]::after {
|
||||||
|
left: unset;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options table label{
|
||||||
|
display: block;
|
||||||
|
min-width: 200px;
|
||||||
|
margin-right: 4px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options th, #player-options td{
|
||||||
|
border: none;
|
||||||
|
padding: 3px;
|
||||||
|
font-size: 17px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 1024px) {
|
||||||
|
#player-options {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options #meta-options {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options #game-options{
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-options .left,
|
||||||
|
#player-options .right {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-options table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-options table label{
|
||||||
|
display: block;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-options table tr td {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
@import "../markdown.css";
|
|
||||||
html {
|
|
||||||
background-image: url("../../static/backgrounds/grass.png");
|
|
||||||
background-repeat: repeat;
|
|
||||||
background-size: 650px 650px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#player-options {
|
|
||||||
box-sizing: border-box;
|
|
||||||
max-width: 1024px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
background-color: rgba(0, 0, 0, 0.15);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
color: #eeffeb;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
#player-options #player-options-header h1 {
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
#player-options #player-options-header h1:nth-child(2) {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
margin-top: -8px;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
#player-options .js-warning-banner {
|
|
||||||
width: calc(100% - 1rem);
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #f3f309;
|
|
||||||
color: #000000;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#player-options .group-container {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#player-options .group-container h2 {
|
|
||||||
user-select: none;
|
|
||||||
cursor: unset;
|
|
||||||
}
|
|
||||||
#player-options .group-container h2 label {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#player-options #player-options-button-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
#player-options #user-message {
|
|
||||||
display: none;
|
|
||||||
width: calc(100% - 8px);
|
|
||||||
background-color: #ffe86b;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #000000;
|
|
||||||
padding: 4px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#player-options h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: normal;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
text-shadow: 1px 1px 4px #000000;
|
|
||||||
}
|
|
||||||
#player-options h2 {
|
|
||||||
font-size: 40px;
|
|
||||||
font-weight: normal;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
text-transform: lowercase;
|
|
||||||
text-shadow: 1px 1px 2px #000000;
|
|
||||||
}
|
|
||||||
#player-options h3, #player-options h4, #player-options h5, #player-options h6 {
|
|
||||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
#player-options input:not([type]) {
|
|
||||||
border: 1px solid #000000;
|
|
||||||
padding: 3px;
|
|
||||||
border-radius: 3px;
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
#player-options input:not([type]):focus {
|
|
||||||
border: 1px solid #ffffff;
|
|
||||||
}
|
|
||||||
#player-options select {
|
|
||||||
border: 1px solid #000000;
|
|
||||||
padding: 3px;
|
|
||||||
border-radius: 3px;
|
|
||||||
min-width: 150px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
#player-options .game-options {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
#player-options .game-options .left, #player-options .game-options .right {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 12rem auto;
|
|
||||||
grid-row-gap: 0.5rem;
|
|
||||||
grid-auto-rows: min-content;
|
|
||||||
align-items: start;
|
|
||||||
min-width: 480px;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
#player-options #meta-options {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
#player-options #meta-options input, #player-options #meta-options select {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
#player-options .left, #player-options .right {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
#player-options .left {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
#player-options .select-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
max-width: 270px;
|
|
||||||
}
|
|
||||||
#player-options .select-container select {
|
|
||||||
min-width: 200px;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
#player-options .select-container select:disabled {
|
|
||||||
background-color: lightgray;
|
|
||||||
}
|
|
||||||
#player-options .range-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
max-width: 270px;
|
|
||||||
}
|
|
||||||
#player-options .range-container input[type=range] {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
#player-options .range-container .range-value {
|
|
||||||
min-width: 20px;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
#player-options .named-range-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 270px;
|
|
||||||
}
|
|
||||||
#player-options .named-range-container .named-range-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
#player-options .named-range-container .named-range-wrapper input[type=range] {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
#player-options .free-text-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 270px;
|
|
||||||
}
|
|
||||||
#player-options .free-text-container input[type=text] {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
#player-options .text-choice-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 270px;
|
|
||||||
}
|
|
||||||
#player-options .text-choice-container .text-choice-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
#player-options .text-choice-container .text-choice-wrapper select {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
#player-options .option-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
|
||||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #ffffff;
|
|
||||||
max-height: 10rem;
|
|
||||||
min-width: 14.5rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 0.25rem;
|
|
||||||
padding-left: 0.25rem;
|
|
||||||
}
|
|
||||||
#player-options .option-container .option-divider {
|
|
||||||
width: 100%;
|
|
||||||
height: 2px;
|
|
||||||
background-color: rgba(20, 20, 20, 0.25);
|
|
||||||
margin-top: 0.125rem;
|
|
||||||
margin-bottom: 0.125rem;
|
|
||||||
}
|
|
||||||
#player-options .option-container .option-entry {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 0.125rem;
|
|
||||||
margin-top: 0.125rem;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
#player-options .option-container .option-entry:hover {
|
|
||||||
background-color: rgba(20, 20, 20, 0.25);
|
|
||||||
}
|
|
||||||
#player-options .option-container .option-entry input[type=checkbox] {
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
#player-options .option-container .option-entry input[type=number] {
|
|
||||||
max-width: 1.5rem;
|
|
||||||
max-height: 1rem;
|
|
||||||
margin-left: 0.125rem;
|
|
||||||
text-align: center;
|
|
||||||
/* Hide arrows on input[type=number] fields */
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
#player-options .option-container .option-entry input[type=number]::-webkit-outer-spin-button, #player-options .option-container .option-entry input[type=number]::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#player-options .option-container .option-entry label {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-right: 0;
|
|
||||||
min-width: unset;
|
|
||||||
display: unset;
|
|
||||||
}
|
|
||||||
#player-options .randomize-button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
height: 22px;
|
|
||||||
max-width: 30px;
|
|
||||||
margin: 0 0 0 0.25rem;
|
|
||||||
font-size: 14px;
|
|
||||||
border: 1px solid black;
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: #d3d3d3;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
#player-options .randomize-button:hover {
|
|
||||||
background-color: #c0c0c0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#player-options .randomize-button label {
|
|
||||||
line-height: 22px;
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 2px;
|
|
||||||
margin-right: 4px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-width: unset;
|
|
||||||
}
|
|
||||||
#player-options .randomize-button label:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#player-options .randomize-button input[type=checkbox] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#player-options .randomize-button:has(input[type=checkbox]:checked) {
|
|
||||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
|
||||||
}
|
|
||||||
#player-options .randomize-button:has(input[type=checkbox]:checked):hover {
|
|
||||||
background-color: #eedd27;
|
|
||||||
}
|
|
||||||
#player-options .randomize-button[data-tooltip]::after {
|
|
||||||
left: unset;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
#player-options label {
|
|
||||||
display: block;
|
|
||||||
margin-right: 4px;
|
|
||||||
cursor: default;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
#player-options th, #player-options td {
|
|
||||||
border: none;
|
|
||||||
padding: 3px;
|
|
||||||
font-size: 17px;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 1024px) {
|
|
||||||
#player-options {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
#player-options #meta-options {
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
#player-options .game-options {
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*# sourceMappingURL=playerOptions.css.map */
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sourceRoot":"","sources":["playerOptions.scss"],"names":[],"mappings":"AAAQ;AAER;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAEA;EACI;;AAIR;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAIR;EACI;IACI;;EAEA;IACI;IACA;IACA;;EAGJ;IACI;IACA","file":"playerOptions.css"}
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
@import "../markdown.css";
|
|
||||||
|
|
||||||
html{
|
|
||||||
background-image: url('../../static/backgrounds/grass.png');
|
|
||||||
background-repeat: repeat;
|
|
||||||
background-size: 650px 650px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#player-options{
|
|
||||||
box-sizing: border-box;
|
|
||||||
max-width: 1024px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
background-color: rgba(0, 0, 0, 0.15);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
color: #eeffeb;
|
|
||||||
word-break: break-all;
|
|
||||||
|
|
||||||
#player-options-header{
|
|
||||||
h1{
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1:nth-child(2){
|
|
||||||
font-size: 1.4rem;
|
|
||||||
margin-top: -8px;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-warning-banner{
|
|
||||||
width: calc(100% - 1rem);
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #f3f309;
|
|
||||||
color: #000000;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-container{
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
h2{
|
|
||||||
user-select: none;
|
|
||||||
cursor: unset;
|
|
||||||
|
|
||||||
label{
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#player-options-button-row{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#user-message{
|
|
||||||
display: none;
|
|
||||||
width: calc(100% - 8px);
|
|
||||||
background-color: #ffe86b;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #000000;
|
|
||||||
padding: 4px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1{
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: normal;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
text-shadow: 1px 1px 4px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2{
|
|
||||||
font-size: 40px;
|
|
||||||
font-weight: normal;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
text-transform: lowercase;
|
|
||||||
text-shadow: 1px 1px 2px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3, h4, h5, h6{
|
|
||||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
input:not([type]){
|
|
||||||
border: 1px solid #000000;
|
|
||||||
padding: 3px;
|
|
||||||
border-radius: 3px;
|
|
||||||
min-width: 150px;
|
|
||||||
|
|
||||||
&:focus{
|
|
||||||
border: 1px solid #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select{
|
|
||||||
border: 1px solid #000000;
|
|
||||||
padding: 3px;
|
|
||||||
border-radius: 3px;
|
|
||||||
min-width: 150px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-options{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.left, .right{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 12rem auto;
|
|
||||||
grid-row-gap: 0.5rem;
|
|
||||||
grid-auto-rows: min-content;
|
|
||||||
align-items: start;
|
|
||||||
min-width: 480px;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#meta-options{
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 3px;
|
|
||||||
|
|
||||||
input, select{
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.left, .right{
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left{
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-container{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
max-width: 270px;
|
|
||||||
|
|
||||||
select{
|
|
||||||
min-width: 200px;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
&:disabled{
|
|
||||||
background-color: lightgray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-container{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
max-width: 270px;
|
|
||||||
|
|
||||||
input[type=range]{
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-value{
|
|
||||||
min-width: 20px;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.named-range-container{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 270px;
|
|
||||||
|
|
||||||
.named-range-wrapper{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
|
|
||||||
input[type=range]{
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.free-text-container{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 270px;
|
|
||||||
|
|
||||||
input[type=text]{
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-choice-container{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 270px;
|
|
||||||
|
|
||||||
.text-choice-wrapper{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
|
|
||||||
select{
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-container{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
|
||||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #ffffff;
|
|
||||||
max-height: 10rem;
|
|
||||||
min-width: 14.5rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 0.25rem;
|
|
||||||
padding-left: 0.25rem;
|
|
||||||
|
|
||||||
.option-divider{
|
|
||||||
width: 100%;
|
|
||||||
height: 2px;
|
|
||||||
background-color: rgba(20, 20, 20, 0.25);
|
|
||||||
margin-top: 0.125rem;
|
|
||||||
margin-bottom: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-entry{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 0.125rem;
|
|
||||||
margin-top: 0.125rem;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&:hover{
|
|
||||||
background-color: rgba(20, 20, 20, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=checkbox]{
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=number]{
|
|
||||||
max-width: 1.5rem;
|
|
||||||
max-height: 1rem;
|
|
||||||
margin-left: 0.125rem;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
/* Hide arrows on input[type=number] fields */
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
label{
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-right: 0;
|
|
||||||
min-width: unset;
|
|
||||||
display: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.randomize-button{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
height: 22px;
|
|
||||||
max-width: 30px;
|
|
||||||
margin: 0 0 0 0.25rem;
|
|
||||||
font-size: 14px;
|
|
||||||
border: 1px solid black;
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: #d3d3d3;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&:hover{
|
|
||||||
background-color: #c0c0c0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
label{
|
|
||||||
line-height: 22px;
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 2px;
|
|
||||||
margin-right: 4px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-width: unset;
|
|
||||||
&:hover{
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=checkbox]{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(input[type=checkbox]:checked){
|
|
||||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
|
||||||
|
|
||||||
&:hover{
|
|
||||||
background-color: #eedd27;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-tooltip]::after{
|
|
||||||
left: unset;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
label{
|
|
||||||
display: block;
|
|
||||||
margin-right: 4px;
|
|
||||||
cursor: default;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td{
|
|
||||||
border: none;
|
|
||||||
padding: 3px;
|
|
||||||
font-size: 17px;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 1024px) {
|
|
||||||
#player-options {
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
#meta-options {
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-options{
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,15 +8,30 @@
|
|||||||
cursor: unset;
|
cursor: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
#games h1, #games details summary.h1{
|
#games h1{
|
||||||
font-size: 60px;
|
font-size: 60px;
|
||||||
cursor: unset;
|
cursor: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
#games h2, #games details summary.h2{
|
#games h2{
|
||||||
color: #93dcff;
|
color: #93dcff;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
text-transform: none;
|
}
|
||||||
|
|
||||||
|
#games .collapse-toggle{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#games h2 .collapse-arrow{
|
||||||
|
font-size: 20px;
|
||||||
|
display: inline-block; /* make vertical-align work */
|
||||||
|
padding-bottom: 9px;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#games p.collapsed{
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#games a{
|
#games a{
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
|||||||
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{
|
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
word-break: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Directional arrow styles */
|
/** Directional arrow styles */
|
||||||
|
|||||||
315
WebHostLib/static/styles/weighted-options.css
Normal file
315
WebHostLib/static/styles/weighted-options.css
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
html{
|
||||||
|
background-image: url('../static/backgrounds/grass.png');
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 650px 650px;
|
||||||
|
scroll-padding-top: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings{
|
||||||
|
max-width: 1000px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
color: #eeffeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings #games-wrapper{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .setting-wrapper{
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .setting-wrapper .add-option-div{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .setting-wrapper .add-option-div button{
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
margin: 0 0 0 0.15rem;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .setting-wrapper .add-option-div button:active{
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings p.setting-description{
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings p.hint-text{
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .jump-link{
|
||||||
|
color: #ffef00;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings table{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings table th, #weighted-settings table td{
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings table td{
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings table .td-left{
|
||||||
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
|
padding-right: 1rem;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings table .td-middle{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings table .td-right{
|
||||||
|
width: 4rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings table .td-delete{
|
||||||
|
width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings table .range-option-delete{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .items-wrapper{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .items-div h3{
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .items-wrapper .item-set-wrapper{
|
||||||
|
width: 24%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container{
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
border-radius: 2px;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container .item-div{
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container .item-div:hover{
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container .item-qty-div{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container .item-qty-div .item-qty-input-wrapper{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container .item-qty-div input{
|
||||||
|
min-width: unset;
|
||||||
|
width: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container .item-qty-div:hover{
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-div, #weighted-settings .locations-div{
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-container, #weighted-settings .locations-container{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{
|
||||||
|
width: calc(50% - 0.5rem);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
height: 300px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings #weighted-settings-button-row{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings code{
|
||||||
|
background-color: #d9cd8e;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings #user-message{
|
||||||
|
display: none;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
background-color: #ffe86b;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #000000;
|
||||||
|
padding: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings #user-message.visible{
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings h1{
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: normal;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 1px 1px 4px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings h2{
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: normal;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #ffe993;
|
||||||
|
text-transform: none;
|
||||||
|
text-shadow: 1px 1px 2px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings a{
|
||||||
|
color: #ffef00;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings input:not([type]){
|
||||||
|
border: 1px solid #000000;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings input:not([type]):focus{
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings select{
|
||||||
|
border: 1px solid #000000;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
min-width: 150px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .game-options, #weighted-settings .rom-options{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .simple-list{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .simple-list .list-row label{
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 0.5rem);
|
||||||
|
padding: 0.0625rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .simple-list .list-row label:hover{
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .simple-list .list-row label input[type=checkbox]{
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .simple-list hr{
|
||||||
|
width: calc(100% - 2px);
|
||||||
|
margin: 2px auto;
|
||||||
|
border-bottom: 1px solid rgb(255 255 255 / 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .invisible{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||||
|
#weighted-settings .game-options{
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-options table label{
|
||||||
|
display: block;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
html {
|
|
||||||
background-image: url("../../static/backgrounds/grass.png");
|
|
||||||
background-repeat: repeat;
|
|
||||||
background-size: 650px 650px;
|
|
||||||
scroll-padding-top: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#weighted-options {
|
|
||||||
max-width: 1000px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
background-color: rgba(0, 0, 0, 0.15);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
color: #eeffeb;
|
|
||||||
}
|
|
||||||
#weighted-options #weighted-options-header h1 {
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
#weighted-options #weighted-options-header h1:nth-child(2) {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
margin-top: -8px;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
#weighted-options .js-warning-banner {
|
|
||||||
width: calc(100% - 1rem);
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #f3f309;
|
|
||||||
color: #000000;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#weighted-options .option-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
#weighted-options .option-wrapper .add-option-div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
#weighted-options .option-wrapper .add-option-div button {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
margin: 0 0 0 0.15rem;
|
|
||||||
padding: 0 0.25rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
#weighted-options .option-wrapper .add-option-div button:active {
|
|
||||||
margin-bottom: 1px;
|
|
||||||
}
|
|
||||||
#weighted-options p.option-description {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
}
|
|
||||||
#weighted-options p.hint-text {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
#weighted-options table {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
#weighted-options table th, #weighted-options table td {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
#weighted-options table td {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
#weighted-options table .td-left {
|
|
||||||
font-family: LexendDeca-Regular, sans-serif;
|
|
||||||
padding-right: 1rem;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
#weighted-options table .td-middle {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
#weighted-options table .td-right {
|
|
||||||
width: 4rem;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
#weighted-options table .td-delete {
|
|
||||||
width: 50px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
#weighted-options table .range-option-delete {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#weighted-options #weighted-options-button-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
#weighted-options #user-message {
|
|
||||||
display: none;
|
|
||||||
width: calc(100% - 8px);
|
|
||||||
background-color: #ffe86b;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #000000;
|
|
||||||
padding: 4px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#weighted-options #user-message.visible {
|
|
||||||
display: block;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#weighted-options h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: normal;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #ffffff;
|
|
||||||
text-shadow: 1px 1px 4px #000000;
|
|
||||||
}
|
|
||||||
#weighted-options h2, #weighted-options details summary.h2 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: normal;
|
|
||||||
border-bottom: 1px solid #ffffff;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #ffe993;
|
|
||||||
text-transform: none;
|
|
||||||
text-shadow: 1px 1px 2px #000000;
|
|
||||||
}
|
|
||||||
#weighted-options h3, #weighted-options h4, #weighted-options h5, #weighted-options h6 {
|
|
||||||
color: #ffffff;
|
|
||||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
|
||||||
text-transform: none;
|
|
||||||
cursor: unset;
|
|
||||||
}
|
|
||||||
#weighted-options h3.option-group-header {
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
#weighted-options a {
|
|
||||||
color: #ffef00;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#weighted-options input:not([type]) {
|
|
||||||
border: 1px solid #000000;
|
|
||||||
padding: 3px;
|
|
||||||
border-radius: 3px;
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
#weighted-options input:not([type]):focus {
|
|
||||||
border: 1px solid #ffffff;
|
|
||||||
}
|
|
||||||
#weighted-options .invisible {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#weighted-options .unsupported-option {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
#weighted-options .set-container, #weighted-options .dict-container, #weighted-options .list-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
|
||||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #ffffff;
|
|
||||||
max-height: 15rem;
|
|
||||||
min-width: 14.5rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 0.25rem;
|
|
||||||
padding-left: 0.25rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
#weighted-options .set-container .divider, #weighted-options .dict-container .divider, #weighted-options .list-container .divider {
|
|
||||||
width: 100%;
|
|
||||||
height: 2px;
|
|
||||||
background-color: rgba(20, 20, 20, 0.25);
|
|
||||||
margin-top: 0.125rem;
|
|
||||||
margin-bottom: 0.125rem;
|
|
||||||
}
|
|
||||||
#weighted-options .set-container .set-entry, #weighted-options .set-container .dict-entry, #weighted-options .set-container .list-entry, #weighted-options .dict-container .set-entry, #weighted-options .dict-container .dict-entry, #weighted-options .dict-container .list-entry, #weighted-options .list-container .set-entry, #weighted-options .list-container .dict-entry, #weighted-options .list-container .list-entry {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
user-select: none;
|
|
||||||
line-height: 1rem;
|
|
||||||
}
|
|
||||||
#weighted-options .set-container .set-entry:hover, #weighted-options .set-container .dict-entry:hover, #weighted-options .set-container .list-entry:hover, #weighted-options .dict-container .set-entry:hover, #weighted-options .dict-container .dict-entry:hover, #weighted-options .dict-container .list-entry:hover, #weighted-options .list-container .set-entry:hover, #weighted-options .list-container .dict-entry:hover, #weighted-options .list-container .list-entry:hover {
|
|
||||||
background-color: rgba(20, 20, 20, 0.25);
|
|
||||||
}
|
|
||||||
#weighted-options .set-container .set-entry input[type=checkbox], #weighted-options .set-container .dict-entry input[type=checkbox], #weighted-options .set-container .list-entry input[type=checkbox], #weighted-options .dict-container .set-entry input[type=checkbox], #weighted-options .dict-container .dict-entry input[type=checkbox], #weighted-options .dict-container .list-entry input[type=checkbox], #weighted-options .list-container .set-entry input[type=checkbox], #weighted-options .list-container .dict-entry input[type=checkbox], #weighted-options .list-container .list-entry input[type=checkbox] {
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
#weighted-options .set-container .set-entry input[type=number], #weighted-options .set-container .dict-entry input[type=number], #weighted-options .set-container .list-entry input[type=number], #weighted-options .dict-container .set-entry input[type=number], #weighted-options .dict-container .dict-entry input[type=number], #weighted-options .dict-container .list-entry input[type=number], #weighted-options .list-container .set-entry input[type=number], #weighted-options .list-container .dict-entry input[type=number], #weighted-options .list-container .list-entry input[type=number] {
|
|
||||||
max-width: 1.5rem;
|
|
||||||
max-height: 1rem;
|
|
||||||
margin-left: 0.125rem;
|
|
||||||
text-align: center;
|
|
||||||
/* Hide arrows on input[type=number] fields */
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
#weighted-options .set-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#weighted-options .set-container .set-entry label, #weighted-options .set-container .dict-entry label, #weighted-options .set-container .list-entry label, #weighted-options .dict-container .set-entry label, #weighted-options .dict-container .dict-entry label, #weighted-options .dict-container .list-entry label, #weighted-options .list-container .set-entry label, #weighted-options .list-container .dict-entry label, #weighted-options .list-container .list-entry label {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-right: 0;
|
|
||||||
min-width: unset;
|
|
||||||
display: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 1000px), all and (orientation: portrait) {
|
|
||||||
#weighted-options .game-options {
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
#game-options table label {
|
|
||||||
display: block;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*# sourceMappingURL=weightedOptions.css.map */
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sourceRoot":"","sources":["weightedOptions.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAOZ;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;;AAMhB;EACI;;;AAGJ;EACI;IACI;IACA;;EAGJ;IACI;IACA","file":"weightedOptions.css"}
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
html{
|
|
||||||
background-image: url('../../static/backgrounds/grass.png');
|
|
||||||
background-repeat: repeat;
|
|
||||||
background-size: 650px 650px;
|
|
||||||
scroll-padding-top: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#weighted-options{
|
|
||||||
max-width: 1000px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
background-color: rgba(0, 0, 0, 0.15);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
color: #eeffeb;
|
|
||||||
|
|
||||||
#weighted-options-header{
|
|
||||||
h1{
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1:nth-child(2){
|
|
||||||
font-size: 1.4rem;
|
|
||||||
margin-top: -8px;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-warning-banner{
|
|
||||||
width: calc(100% - 1rem);
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #f3f309;
|
|
||||||
color: #000000;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-wrapper{
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
|
|
||||||
.add-option-div{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
button{
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
margin: 0 0 0 0.15rem;
|
|
||||||
padding: 0 0.25rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
&:active{
|
|
||||||
margin-bottom: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p{
|
|
||||||
&.option-description{
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hint-text{
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
font-style: italic;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
table{
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
|
|
||||||
th, td{
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
td{
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.td-left{
|
|
||||||
font-family: LexendDeca-Regular, sans-serif;
|
|
||||||
padding-right: 1rem;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.td-middle{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.td-right{
|
|
||||||
width: 4rem;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.td-delete{
|
|
||||||
width: 50px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-option-delete{
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#weighted-options-button-row{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#user-message{
|
|
||||||
display: none;
|
|
||||||
width: calc(100% - 8px);
|
|
||||||
background-color: #ffe86b;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #000000;
|
|
||||||
padding: 4px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&.visible{
|
|
||||||
display: block;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1{
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: normal;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #ffffff;
|
|
||||||
text-shadow: 1px 1px 4px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2, details summary.h2{
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: normal;
|
|
||||||
border-bottom: 1px solid #ffffff;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #ffe993;
|
|
||||||
text-transform: none;
|
|
||||||
text-shadow: 1px 1px 2px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3, h4, h5, h6{
|
|
||||||
color: #ffffff;
|
|
||||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
|
||||||
text-transform: none;
|
|
||||||
cursor: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3{
|
|
||||||
&.option-group-header{
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a{
|
|
||||||
color: #ffef00;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:not([type]){
|
|
||||||
border: 1px solid #000000;
|
|
||||||
padding: 3px;
|
|
||||||
border-radius: 3px;
|
|
||||||
min-width: 150px;
|
|
||||||
|
|
||||||
&:focus{
|
|
||||||
border: 1px solid #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.invisible{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unsupported-option{
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.set-container, .dict-container, .list-container{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
|
||||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #ffffff;
|
|
||||||
max-height: 15rem;
|
|
||||||
min-width: 14.5rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 0.25rem;
|
|
||||||
padding-left: 0.25rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
|
|
||||||
.divider{
|
|
||||||
width: 100%;
|
|
||||||
height: 2px;
|
|
||||||
background-color: rgba(20, 20, 20, 0.25);
|
|
||||||
margin-top: 0.125rem;
|
|
||||||
margin-bottom: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.set-entry, .dict-entry, .list-entry{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
user-select: none;
|
|
||||||
line-height: 1rem;
|
|
||||||
|
|
||||||
&:hover{
|
|
||||||
background-color: rgba(20, 20, 20, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=checkbox]{
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=number]{
|
|
||||||
max-width: 1.5rem;
|
|
||||||
max-height: 1rem;
|
|
||||||
margin-left: 0.125rem;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
/* Hide arrows on input[type=number] fields */
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
label{
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-right: 0;
|
|
||||||
min-width: unset;
|
|
||||||
display: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
|
||||||
#weighted-options .game-options{
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#game-options table label{
|
|
||||||
display: block;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
62
WebHostLib/templates/player-options.html
Normal file
62
WebHostLib/templates/player-options.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{% extends 'pageWrapper.html' %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<title>{{ game }} Options</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-options.css") }}" />
|
||||||
|
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-options.js") }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% include 'header/'+theme+'Header.html' %}
|
||||||
|
<div id="player-options" class="markdown" data-game="{{ game }}">
|
||||||
|
<div id="user-message"></div>
|
||||||
|
<h1><span id="game-name">Player</span> Options</h1>
|
||||||
|
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||||
|
or download an options file you can use to participate in a MultiWorld.</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
A more advanced options configuration for all games can be found on the
|
||||||
|
<a href="/weighted-options">Weighted options</a> page.
|
||||||
|
<br />
|
||||||
|
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
||||||
|
<br />
|
||||||
|
You may also download the
|
||||||
|
<a href="/static/generated/configs/{{ game }}.yaml">template file for this game</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="meta-options">
|
||||||
|
<div>
|
||||||
|
<label for="player-name">
|
||||||
|
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
|
||||||
|
</label>
|
||||||
|
<input id="player-name" placeholder="Player" data-key="name" maxlength="16" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="game-options-preset">
|
||||||
|
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
|
||||||
|
</label>
|
||||||
|
<select id="game-options-preset">
|
||||||
|
<option value="__default">Defaults</option>
|
||||||
|
<option value="__custom" hidden>Custom</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Game Options</h2>
|
||||||
|
<div id="game-options">
|
||||||
|
<div id="game-options-left" class="left"></div>
|
||||||
|
<div id="game-options-right" class="right"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="player-options-button-row">
|
||||||
|
<button id="export-options">Export Options</button>
|
||||||
|
<button id="generate-game">Generate Game</button>
|
||||||
|
<button id="generate-race">Generate Race</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
{% macro Toggle(option_name, option) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="select-container">
|
|
||||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
|
||||||
{% if option.default == 1 %}
|
|
||||||
<option value="false">No</option>
|
|
||||||
<option value="true" selected>Yes</option>
|
|
||||||
{% else %}
|
|
||||||
<option value="false" selected>No</option>
|
|
||||||
<option value="true">Yes</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
{{ RandomizeButton(option_name, option) }}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro Choice(option_name, option) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="select-container">
|
|
||||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
|
||||||
{% for id, name in option.name_lookup.items() %}
|
|
||||||
{% if name != "random" %}
|
|
||||||
{% if option.default == id %}
|
|
||||||
<option value="{{ name }}" selected>{{ option.get_option_name(id) }}</option>
|
|
||||||
{% else %}
|
|
||||||
<option value="{{ name }}">{{ option.get_option_name(id) }}</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{{ RandomizeButton(option_name, option) }}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro Range(option_name, option) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="range-container">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="{{ option_name }}"
|
|
||||||
name="{{ option_name }}"
|
|
||||||
min="{{ option.range_start }}"
|
|
||||||
max="{{ option.range_end }}"
|
|
||||||
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
|
|
||||||
{{ "disabled" if option.default == "random" }}
|
|
||||||
/>
|
|
||||||
<span id="{{ option_name }}-value" class="range-value js-required">
|
|
||||||
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
|
|
||||||
</span>
|
|
||||||
{{ RandomizeButton(option_name, option) }}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro NamedRange(option_name, option) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="named-range-container">
|
|
||||||
<select id="{{ option_name }}-select" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
|
||||||
{% for key, val in option.special_range_names.items() %}
|
|
||||||
{% if option.default == val %}
|
|
||||||
<option value="{{ val }}" selected>{{ key }} ({{ val }})</option>
|
|
||||||
{% else %}
|
|
||||||
<option value="{{ val }}">{{ key }} ({{ val }})</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<option value="custom" hidden>Custom</option>
|
|
||||||
</select>
|
|
||||||
<div class="named-range-wrapper">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="{{ option_name }}"
|
|
||||||
name="{{ option_name }}"
|
|
||||||
min="{{ option.range_start }}"
|
|
||||||
max="{{ option.range_end }}"
|
|
||||||
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
|
|
||||||
{{ "disabled" if option.default == "random" }}
|
|
||||||
/>
|
|
||||||
<span id="{{ option_name }}-value" class="range-value js-required">
|
|
||||||
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
|
|
||||||
</span>
|
|
||||||
{{ RandomizeButton(option_name, option) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro FreeText(option_name, option) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="free-text-container">
|
|
||||||
<input type="text" id="{{ option_name }}" name="{{ option_name }}" value="{{ option.default }}" />
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro TextChoice(option_name, option) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="text-choice-container">
|
|
||||||
<div class="text-choice-wrapper">
|
|
||||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
|
||||||
{% for id, name in option.name_lookup.items()|sort %}
|
|
||||||
{% if name != "random" %}
|
|
||||||
{% if option.default == id %}
|
|
||||||
<option value="{{ name }}" selected>{{ option.get_option_name(id) }}</option>
|
|
||||||
{% else %}
|
|
||||||
<option value="{{ name }}">{{ option.get_option_name(id) }}</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<option value="custom" hidden>Custom</option>
|
|
||||||
</select>
|
|
||||||
{{ RandomizeButton(option_name, option) }}
|
|
||||||
</div>
|
|
||||||
<input type="text" id="{{ option_name }}-custom" name="{{ option_name }}-custom" data-option-name="{{ option_name }}" placeholder="Custom value..." />
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro ItemDict(option_name, option, world) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="option-container">
|
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
|
||||||
<div class="option-entry">
|
|
||||||
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
|
||||||
<input type="number" id="{{ option_name }}-{{ item_name }}-qty" name="{{ option_name }}||{{ item_name }}||qty" value="{{ option.default[item_name]|default("0") }}" data-option-name="{{ option_name }}" data-item-name="{{ item_name }}" />
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro OptionList(option_name, option) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="option-container">
|
|
||||||
{% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}
|
|
||||||
<div class="option-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ key }}" name="{{ option_name }}" value="{{ key }}" {{ "checked" if key in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ key }}">{{ key }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro LocationSet(option_name, option, world) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="option-container">
|
|
||||||
{% for group_name in world.location_name_groups.keys()|sort %}
|
|
||||||
{% if group_name != "Everywhere" %}
|
|
||||||
<div class="option-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}" value="{{ group_name }}" {{ "checked" if group_name in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if world.location_name_groups.keys()|length > 1 %}
|
|
||||||
<div class="option-divider"> </div>
|
|
||||||
{% endif %}
|
|
||||||
{% for location_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.location_names|sort) %}
|
|
||||||
<div class="option-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ location_name }}" name="{{ option_name }}" value="{{ location_name }}" {{ "checked" if location_name in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ location_name }}">{{ location_name }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro ItemSet(option_name, option, world) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="option-container">
|
|
||||||
{% for group_name in world.item_name_groups.keys()|sort %}
|
|
||||||
{% if group_name != "Everything" %}
|
|
||||||
<div class="option-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}" value="{{ group_name }}" {{ "checked" if group_name in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if world.item_name_groups.keys()|length > 1 %}
|
|
||||||
<div class="option-divider"> </div>
|
|
||||||
{% endif %}
|
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
|
||||||
<div class="option-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ item_name }}" name="{{ option_name }}" value="{{ item_name }}" {{ "checked" if item_name in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ item_name }}">{{ item_name }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro OptionSet(option_name, option) %}
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
|
||||||
<div class="option-container">
|
|
||||||
{% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}
|
|
||||||
<div class="option-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ key }}" name="{{ option_name }}" value="{{ key }}" {{ "checked" if key in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ key }}">{{ key }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro OptionTitle(option_name, option) %}
|
|
||||||
<label for="{{ option_name }}">
|
|
||||||
{{ option.display_name|default(option_name) }}:
|
|
||||||
<span class="interactive" data-tooltip="{% filter dedent %}{{(option.__doc__ | default("Please document me!"))|escape }}{% endfilter %}">(?)</span>
|
|
||||||
</label>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro RandomizeButton(option_name, option) %}
|
|
||||||
<div class="randomize-button" data-tooltip="Toggle randomization for this option!">
|
|
||||||
<label for="random-{{ option_name }}">
|
|
||||||
<input type="checkbox" id="random-{{ option_name }}" name="random-{{ option_name }}" class="randomize-checkbox" data-option-name="{{ option_name }}" {{ "checked" if option.default == "random" }} />
|
|
||||||
🎲
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
|
||||||
{% import 'playerOptions/macros.html' as inputs %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<title>{{ world_name }} Options</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/playerOptions/playerOptions.css") }}" />
|
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/playerOptions.js") }}"></script>
|
|
||||||
|
|
||||||
<noscript>
|
|
||||||
<style>
|
|
||||||
.js-required{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</noscript>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
{% include 'header/'+theme+'Header.html' %}
|
|
||||||
<div id="player-options" class="markdown" data-game="{{ world_name }}" data-presets="{{ presets }}">
|
|
||||||
<noscript>
|
|
||||||
<div class="js-warning-banner">
|
|
||||||
This page has reduced functionality without JavaScript.
|
|
||||||
</div>
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div id="user-message">{{ message }}</div>
|
|
||||||
|
|
||||||
<div id="player-options-header">
|
|
||||||
<h1>{{ world_name }}</h1>
|
|
||||||
<h1>Player Options</h1>
|
|
||||||
</div>
|
|
||||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
|
||||||
or download an options file you can use to participate in a MultiWorld.</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
A more advanced options configuration for all games can be found on the
|
|
||||||
<a href="weighted-options">Weighted options</a> page.
|
|
||||||
<br />
|
|
||||||
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
|
||||||
<br />
|
|
||||||
You may also download the
|
|
||||||
<a href="/static/generated/configs/{{ world_name }}.yaml">template file for this game</a>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form id="options-form" method="post" enctype="application/x-www-form-urlencoded" action="generate-yaml">
|
|
||||||
<div id="meta-options">
|
|
||||||
<div>
|
|
||||||
<label for="player-name">
|
|
||||||
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
|
|
||||||
</label>
|
|
||||||
<input id="player-name" placeholder="Player" name="name" maxlength="16" />
|
|
||||||
</div>
|
|
||||||
<div class="js-required">
|
|
||||||
<label for="game-options-preset">
|
|
||||||
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
|
|
||||||
</label>
|
|
||||||
<select id="game-options-preset" name="game-options-preset" disabled>
|
|
||||||
<option value="default">Default</option>
|
|
||||||
{% for preset_name in world.web.options_presets %}
|
|
||||||
<option value="{{ preset_name }}">{{ preset_name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
<option value="custom" hidden>Custom</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="option-groups">
|
|
||||||
{% for group_name, group_options in option_groups.items() %}
|
|
||||||
<details class="group-container" {% if loop.index == 1 %}open{% endif %}>
|
|
||||||
<summary class="h2">{{ group_name }}</summary>
|
|
||||||
<div class="game-options">
|
|
||||||
<div class="left">
|
|
||||||
{% for option_name, option in group_options.items() %}
|
|
||||||
{% if loop.index <= (loop.length / 2)|round(0,"ceil") %}
|
|
||||||
{% if issubclass(option, Options.Toggle) %}
|
|
||||||
{{ inputs.Toggle(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.TextChoice) %}
|
|
||||||
{{ inputs.TextChoice(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.Choice) %}
|
|
||||||
{{ inputs.Choice(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.NamedRange) %}
|
|
||||||
{{ inputs.NamedRange(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.Range) %}
|
|
||||||
{{ inputs.Range(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.FreeText) %}
|
|
||||||
{{ inputs.FreeText(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
|
||||||
{{ inputs.ItemDict(option_name, option, world) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
|
||||||
{{ inputs.OptionList(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
|
||||||
{{ inputs.LocationSet(option_name, option, world) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
|
||||||
{{ inputs.ItemSet(option_name, option, world) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
|
||||||
{{ inputs.OptionSet(option_name, option) }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="right">
|
|
||||||
{% for option_name, option in group_options.items() %}
|
|
||||||
{% if loop.index > (loop.length / 2)|round(0,"ceil") %}
|
|
||||||
{% if issubclass(option, Options.Toggle) %}
|
|
||||||
{{ inputs.Toggle(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.TextChoice) %}
|
|
||||||
{{ inputs.TextChoice(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.Choice) %}
|
|
||||||
{{ inputs.Choice(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.NamedRange) %}
|
|
||||||
{{ inputs.NamedRange(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.Range) %}
|
|
||||||
{{ inputs.Range(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.FreeText) %}
|
|
||||||
{{ inputs.FreeText(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
|
||||||
{{ inputs.ItemDict(option_name, option, world) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
|
||||||
{{ inputs.OptionList(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
|
||||||
{{ inputs.LocationSet(option_name, option, world) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
|
||||||
{{ inputs.ItemSet(option_name, option, world) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
|
||||||
{{ inputs.OptionSet(option_name, option) }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="player-options-button-row">
|
|
||||||
<input type="submit" name="intent-export" value="Export Options" />
|
|
||||||
<input type="submit" name="intent-generate" value="Generate Single-Player Game">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
<li><a href="/games">Supported Games Page</a></li>
|
<li><a href="/games">Supported Games Page</a></li>
|
||||||
<li><a href="/tutorial">Tutorials Page</a></li>
|
<li><a href="/tutorial">Tutorials Page</a></li>
|
||||||
<li><a href="/user-content">User Content</a></li>
|
<li><a href="/user-content">User Content</a></li>
|
||||||
|
<li><a href="/weighted-options">Weighted Options Page</a></li>
|
||||||
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
|
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
|
||||||
<li><a href="/glossary/en">Glossary</a></li>
|
<li><a href="/glossary/en">Glossary</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -49,11 +50,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{% for game in games | title_sorted %}
|
{% for game in games | title_sorted %}
|
||||||
{% if game['has_settings'] %}
|
{% if game['has_settings'] %}
|
||||||
<li>{{ game['title'] }}</li>
|
<li><a href="{{ url_for('player_options', game=game['title']) }}">{{ game['title'] }}</a></li>
|
||||||
<ul>
|
|
||||||
<li><a href="{{ url_for('player_options', game=game['title']) }}">Player Options</a></li>
|
|
||||||
<li><a href="{{ url_for('weighted_options', game=game['title']) }}">Weighted Options</a></li>
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -41,8 +41,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% for game_name in worlds | title_sorted %}
|
{% for game_name in worlds | title_sorted %}
|
||||||
{% set world = worlds[game_name] %}
|
{% set world = worlds[game_name] %}
|
||||||
<details data-game="{{ game_name }}">
|
<h2 class="collapse-toggle" data-game="{{ game_name }}">
|
||||||
<summary class="h2">{{ game_name }}</summary>
|
<span class="collapse-arrow">▶</span>{{ game_name }}
|
||||||
|
</h2>
|
||||||
|
<p class="collapsed">
|
||||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||||
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
||||||
{% if world.web.tutorials %}
|
{% if world.web.tutorials %}
|
||||||
@@ -51,18 +53,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if world.web.options_page is string %}
|
{% if world.web.options_page is string %}
|
||||||
<span class="link-spacer">|</span>
|
<span class="link-spacer">|</span>
|
||||||
<a href="{{ world.web.options_page }}">Options Page (External Link)</a>
|
<a href="{{ world.web.options_page }}">Options Page</a>
|
||||||
{% elif world.web.options_page %}
|
{% elif world.web.options_page %}
|
||||||
<span class="link-spacer">|</span>
|
<span class="link-spacer">|</span>
|
||||||
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>
|
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>
|
||||||
<span class="link-spacer">|</span>
|
|
||||||
<a href="{{ url_for("weighted_options", game=game_name) }}">Advanced Options</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if world.web.bug_report_page %}
|
{% if world.web.bug_report_page %}
|
||||||
<span class="link-spacer">|</span>
|
<span class="link-spacer">|</span>
|
||||||
<a href="{{ world.web.bug_report_page }}">Report a Bug</a>
|
<a href="{{ world.web.bug_report_page }}">Report a Bug</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</details>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
48
WebHostLib/templates/weighted-options.html
Normal file
48
WebHostLib/templates/weighted-options.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{% extends 'pageWrapper.html' %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<title>{{ game }} Options</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weighted-options.css") }}" />
|
||||||
|
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weighted-options.js") }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% include 'header/grassHeader.html' %}
|
||||||
|
<div id="weighted-settings" class="markdown" data-game="{{ game }}">
|
||||||
|
<div id="user-message"></div>
|
||||||
|
<h1>Weighted Options</h1>
|
||||||
|
<p>Weighted options allow you to choose how likely a particular option is to be used in game generation.
|
||||||
|
The higher an option is weighted, the more likely the option will be chosen. Think of them like
|
||||||
|
entries in a raffle.</p>
|
||||||
|
|
||||||
|
<p>Choose the games and options you would like to play with! You may generate a single-player game from
|
||||||
|
this page, or download an options file you can use to participate in a MultiWorld.</p>
|
||||||
|
|
||||||
|
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
|
||||||
|
page.</p>
|
||||||
|
|
||||||
|
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||||
|
items if you are playing in a MultiWorld.</label><br />
|
||||||
|
<input id="player-name" placeholder="Player Name" data-key="name" maxlength="16" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="game-choice">
|
||||||
|
<!-- User chooses games by weight -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- To be generated and populated per-game with weight > 0 -->
|
||||||
|
<div id="games-wrapper">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="weighted-settings-button-row">
|
||||||
|
<button id="export-options">Export Options</button>
|
||||||
|
<button id="generate-game">Generate Game</button>
|
||||||
|
<button id="generate-race">Generate Race</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
{% macro Toggle(option_name, option) %}
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{{ RangeRow(option_name, option, "No", "false") }}
|
|
||||||
{{ RangeRow(option_name, option, "Yes", "true") }}
|
|
||||||
{{ RandomRows(option_name, option) }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro DefaultOnToggle(option_name, option) %}
|
|
||||||
<!-- Toggle handles defaults properly, so we just reuse that -->
|
|
||||||
{{ Toggle(option_name, option) }}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro Choice(option_name, option) %}
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{% for id, name in option.name_lookup.items() %}
|
|
||||||
{% if name != 'random' %}
|
|
||||||
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{{ RandomRows(option_name, option) }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro Range(option_name, option) %}
|
|
||||||
<div class="hint-text js-required">
|
|
||||||
This is a range option.
|
|
||||||
<br /><br />
|
|
||||||
Accepted values:<br />
|
|
||||||
Normal range: {{ option.range_start }} - {{ option.range_end }}
|
|
||||||
{% if option.special_range_names %}
|
|
||||||
<br /><br />
|
|
||||||
The following values has special meaning, and may fall outside the normal range.
|
|
||||||
<ul>
|
|
||||||
{% for name, value in option.special_range_names.items() %}
|
|
||||||
<li>{{ value }}: {{ name }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
<div class="add-option-div">
|
|
||||||
<input type="number" class="range-option-value" data-option="{{ option_name }}" />
|
|
||||||
<button class="add-range-option-button" data-option="{{ option_name }}">Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table class="range-rows" data-option="{{ option_name }}">
|
|
||||||
<tbody>
|
|
||||||
{{ RangeRow(option_name, option, option.range_start, option.range_start, True) }}
|
|
||||||
{% if option.range_start < option.default < option.range_end %}
|
|
||||||
{{ RangeRow(option_name, option, option.default, option.default, True) }}
|
|
||||||
{% endif %}
|
|
||||||
{{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}
|
|
||||||
{{ RandomRows(option_name, option) }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro NamedRange(option_name, option) %}
|
|
||||||
<!-- Range is able to properly handle NamedDRange options -->
|
|
||||||
{{ Range(option_name, option) }}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro FreeText(option_name, option) %}
|
|
||||||
<div class="hint-text">
|
|
||||||
This option allows custom values only. Please enter your desired values below.
|
|
||||||
<div class="custom-value-wrapper">
|
|
||||||
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
|
||||||
<button data-option="{{ option_name }}">Add</button>
|
|
||||||
</div>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<!-- This table to be filled by JS -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro TextChoice(option_name, option) %}
|
|
||||||
<div class="hint-text">
|
|
||||||
Custom values are also allowed for this option. To create one, enter it into the input box below.
|
|
||||||
<div class="custom-value-wrapper">
|
|
||||||
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
|
||||||
<button data-option="{{ option_name }}">Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{% for id, name in option.name_lookup.items() %}
|
|
||||||
{% if name != 'random' %}
|
|
||||||
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{{ RandomRows(option_name, option) }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro PlandoBosses(option_name, option) %}
|
|
||||||
<!-- PlandoBosses is handled by its parent, TextChoice -->
|
|
||||||
{{ TextChoice(option_name, option) }}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro ItemDict(option_name, option, world) %}
|
|
||||||
<div class="dict-container">
|
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
|
||||||
<div class="dict-entry">
|
|
||||||
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="{{ option_name }}-{{ item_name }}-qty"
|
|
||||||
name="{{ option_name }}||{{ item_name }}"
|
|
||||||
value="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro OptionList(option_name, option) %}
|
|
||||||
<div class="list-container">
|
|
||||||
{% for key in option.valid_keys|sort %}
|
|
||||||
<div class="list-entry">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="{{ option_name }}-{{ key }}"
|
|
||||||
name="{{ option_name }}||{{ key }}"
|
|
||||||
value="1"
|
|
||||||
/>
|
|
||||||
<label for="{{ option_name }}-{{ key }}">
|
|
||||||
{{ key }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro LocationSet(option_name, option, world) %}
|
|
||||||
<div class="set-container">
|
|
||||||
{% for group_name in world.location_name_groups.keys()|sort %}
|
|
||||||
{% if group_name != "Everywhere" %}
|
|
||||||
<div class="set-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}||{{ group_name }}" value="1" {{ "checked" if group_name in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if world.location_name_groups.keys()|length > 1 %}
|
|
||||||
<div class="divider"> </div>
|
|
||||||
{% endif %}
|
|
||||||
{% for location_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.location_names|sort) %}
|
|
||||||
<div class="set-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ location_name }}" name="{{ option_name }}||{{ location_name }}" value="1" {{ "checked" if location_name in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ location_name }}">{{ location_name }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro ItemSet(option_name, option, world) %}
|
|
||||||
<div class="set-container">
|
|
||||||
{% for group_name in world.item_name_groups.keys()|sort %}
|
|
||||||
{% if group_name != "Everything" %}
|
|
||||||
<div class="set-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ group_name }}" name="{{ option_name }}||{{ group_name }}" value="1" {{ "checked" if group_name in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if world.item_name_groups.keys()|length > 1 %}
|
|
||||||
<div class="set-divider"> </div>
|
|
||||||
{% endif %}
|
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
|
||||||
<div class="set-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ item_name }}" name="{{ option_name }}||{{ item_name }}" value="1" {{ "checked" if item_name in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ item_name }}">{{ item_name }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro OptionSet(option_name, option) %}
|
|
||||||
<div class="set-container">
|
|
||||||
{% for key in option.valid_keys|sort %}
|
|
||||||
<div class="set-entry">
|
|
||||||
<input type="checkbox" id="{{ option_name }}-{{ key }}" name="{{ option_name }}||{{ key }}" value="1" {{ "checked" if key in option.default }} />
|
|
||||||
<label for="{{ option_name }}-{{ key }}">{{ key }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro OptionTitleTd(option_name, value) %}
|
|
||||||
<td class="td-left">
|
|
||||||
<label for="{{ option_name }}||{{ value }}">
|
|
||||||
{{ option.display_name|default(option_name) }}
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro RandomRows(option_name, option, extra_column=False) %}
|
|
||||||
{% for key, value in {"Random": "random", "Random (Low)": "random-low", "Random (Middle)": "random-middle", "Random (High)": "random-high"}.items() %}
|
|
||||||
{{ RangeRow(option_name, option, key, value) }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro RangeRow(option_name, option, display_value, value, can_delete=False) %}
|
|
||||||
<tr data-row="{{ option_name }}-{{ value }}-row" data-option-name="{{ option_name }}" data-value="{{ value }}">
|
|
||||||
<td class="td-left">
|
|
||||||
<label for="{{ option_name }}||{{ value }}">
|
|
||||||
{{ display_value }}
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
<td class="td-middle">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="{{ option_name }}||{{ value }}"
|
|
||||||
name="{{ option_name }}||{{ value }}"
|
|
||||||
min="0"
|
|
||||||
max="50"
|
|
||||||
{% if option.default == value %}
|
|
||||||
value="25"
|
|
||||||
{% else %}
|
|
||||||
value="0"
|
|
||||||
{% endif %}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td class="td-right">
|
|
||||||
<span id="{{ option_name }}||{{ value }}-value">
|
|
||||||
{% if option.default == value %}
|
|
||||||
25
|
|
||||||
{% else %}
|
|
||||||
0
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
{% if can_delete %}
|
|
||||||
<td>
|
|
||||||
<span class="range-option-delete js-required" data-target="{{ option_name }}-{{ value }}-row">
|
|
||||||
❌
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
{% else %}
|
|
||||||
<td><!-- This td empty on purpose --></td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endmacro %}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
|
||||||
{% import 'weightedOptions/macros.html' as inputs %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<title>{{ world_name }} Weighted Options</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weightedOptions/weightedOptions.css") }}" />
|
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weightedOptions.js") }}"></script>
|
|
||||||
<noscript>
|
|
||||||
<style>
|
|
||||||
.js-required{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</noscript>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
{% include 'header/'+theme+'Header.html' %}
|
|
||||||
<div id="weighted-options" class="markdown" data-game="{{ world_name }}">
|
|
||||||
<noscript>
|
|
||||||
<div class="js-warning-banner">
|
|
||||||
This page has reduced functionality without JavaScript.
|
|
||||||
</div>
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div id="user-message"></div>
|
|
||||||
|
|
||||||
<div id="weighted-options-header">
|
|
||||||
<h1>{{ world_name }}</h1>
|
|
||||||
<h1>Weighted Options</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="weighted-options-form" method="post" enctype="application/x-www-form-urlencoded" action="generate-weighted-yaml">
|
|
||||||
|
|
||||||
<p>Weighted options allow you to choose how likely a particular option's value is to be used in game
|
|
||||||
generation. The higher a value is weighted, the more likely the option will be chosen. Think of them like
|
|
||||||
entries in a raffle.</p>
|
|
||||||
|
|
||||||
<p>Choose the options you would like to play with! You may generate a single-player game from
|
|
||||||
this page, or download an options file you can use to participate in a MultiWorld.</p>
|
|
||||||
|
|
||||||
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
|
|
||||||
page.</p>
|
|
||||||
|
|
||||||
|
|
||||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
|
||||||
items if you are playing in a MultiWorld.</label><br />
|
|
||||||
<input id="player-name" placeholder="Player Name" name="name" maxlength="16" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div id="{{ world_name }}-container">
|
|
||||||
{% for group_name, group_options in option_groups.items() %}
|
|
||||||
<details {% if loop.index == 1 %}open{% endif %}>
|
|
||||||
<summary class="h2">{{ group_name }}</summary>
|
|
||||||
{% for option_name, option in group_options.items() %}
|
|
||||||
<div class="option-wrapper">
|
|
||||||
<h4>{{ option.display_name|default(option_name) }}</h4>
|
|
||||||
<div class="option-description">
|
|
||||||
{{ option.__doc__ }}
|
|
||||||
</div>
|
|
||||||
{% if issubclass(option, Options.Toggle) %}
|
|
||||||
{{ inputs.Toggle(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.DefaultOnToggle) %}
|
|
||||||
{{ inputs.DefaultOnToggle(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.PlandoBosses) %}
|
|
||||||
{{ inputs.PlandoBosses(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.TextChoice) %}
|
|
||||||
{{ inputs.TextChoice(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.Choice) %}
|
|
||||||
{{ inputs.Choice(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.NamedRange) %}
|
|
||||||
{{ inputs.NamedRange(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.Range) %}
|
|
||||||
{{ inputs.Range(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.FreeText) %}
|
|
||||||
{{ inputs.FreeText(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
|
||||||
{{ inputs.ItemDict(option_name, option, world) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
|
||||||
{{ inputs.OptionList(option_name, option) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
|
||||||
{{ inputs.LocationSet(option_name, option, world) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
|
||||||
{{ inputs.ItemSet(option_name, option, world) }}
|
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
|
||||||
{{ inputs.OptionSet(option_name, option) }}
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
<div class="unsupported-option">
|
|
||||||
This option is not supported. Please edit your .yaml file manually.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</details>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="weighted-options-button-row">
|
|
||||||
<input type="submit" name="intent-export" value="Export Options" />
|
|
||||||
<input type="submit" name="intent-generate" value="Generate Single-Player Game">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -45,10 +45,7 @@ requires:
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{{ game }}:
|
{{ game }}:
|
||||||
{%- for group_name, group_options in option_groups.items() %}
|
{%- for option_key, option in options.items() %}
|
||||||
# {{ group_name }}
|
|
||||||
|
|
||||||
{%- for option_key, option in group_options.items() %}
|
|
||||||
{{ option_key }}:
|
{{ option_key }}:
|
||||||
{%- if option.__doc__ %}
|
{%- if option.__doc__ %}
|
||||||
# {{ option.__doc__
|
# {{ option.__doc__
|
||||||
@@ -86,4 +83,3 @@ requires:
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{{ "\n" }}
|
{{ "\n" }}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
{%- endfor %}
|
|
||||||
|
|||||||
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,15 +13,9 @@
|
|||||||
# 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
|
||||||
|
|
||||||
# Aquaria
|
|
||||||
/worlds/aquaria/ @tioui
|
|
||||||
|
|
||||||
# ArchipIDLE
|
# ArchipIDLE
|
||||||
/worlds/archipidle/ @LegendaryLinux
|
/worlds/archipidle/ @LegendaryLinux
|
||||||
|
|
||||||
@@ -31,9 +25,6 @@
|
|||||||
# Blasphemous
|
# Blasphemous
|
||||||
/worlds/blasphemous/ @TRPG0
|
/worlds/blasphemous/ @TRPG0
|
||||||
|
|
||||||
# Bomb Rush Cyberfunk
|
|
||||||
/worlds/bomb_rush_cyberfunk/ @TRPG0
|
|
||||||
|
|
||||||
# Bumper Stickers
|
# Bumper Stickers
|
||||||
/worlds/bumpstik/ @FelicitusNeko
|
/worlds/bumpstik/ @FelicitusNeko
|
||||||
|
|
||||||
@@ -206,9 +197,6 @@
|
|||||||
# Yoshi's Island
|
# Yoshi's Island
|
||||||
/worlds/yoshisisland/ @PinkSwitch
|
/worlds/yoshisisland/ @PinkSwitch
|
||||||
|
|
||||||
#Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
|
||||||
/worlds/yugioh06/ @Rensen3
|
|
||||||
|
|
||||||
# Zillion
|
# Zillion
|
||||||
/worlds/zillion/ @beauxq
|
/worlds/zillion/ @beauxq
|
||||||
|
|
||||||
|
|||||||
@@ -85,25 +85,6 @@ class ExampleWorld(World):
|
|||||||
options: ExampleGameOptions
|
options: ExampleGameOptions
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option Groups
|
|
||||||
Options may be categorized into groups for display on the WebHost. Option groups are displayed alphabetically on the
|
|
||||||
player-options and weighted-options pages. Options without a group name are categorized into a generic "Game Options"
|
|
||||||
group.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from worlds.AutoWorld import WebWorld
|
|
||||||
from Options import OptionGroup
|
|
||||||
|
|
||||||
class MyWorldWeb(WebWorld):
|
|
||||||
option_groups = [
|
|
||||||
OptionGroup('Color Options', [
|
|
||||||
Options.ColorblindMode,
|
|
||||||
Options.FlashReduction,
|
|
||||||
Options.UIColors,
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option Checking
|
### Option Checking
|
||||||
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
|
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
|
||||||
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
|
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
|
||||||
@@ -174,12 +155,10 @@ Gives the player starting hints for where the items defined here are.
|
|||||||
Gives the player starting hints for the items on locations defined here.
|
Gives the player starting hints for the items on locations defined here.
|
||||||
|
|
||||||
### ExcludeLocations
|
### ExcludeLocations
|
||||||
Marks locations given here as `LocationProgressType.Excluded` so that neither progression nor useful items can be
|
Marks locations given here as `LocationProgressType.Excluded` so that progression items can't be placed on them.
|
||||||
placed on them.
|
|
||||||
|
|
||||||
### PriorityLocations
|
### PriorityLocations
|
||||||
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
|
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them.
|
||||||
the pool.
|
|
||||||
|
|
||||||
### ItemLinks
|
### ItemLinks
|
||||||
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
||||||
|
|||||||
@@ -17,14 +17,13 @@ Then run any of the starting point scripts, like Generate.py, and the included M
|
|||||||
required modules and after pressing enter proceed to install everything automatically.
|
required modules and after pressing enter proceed to install everything automatically.
|
||||||
After this, you should be able to run the programs.
|
After this, you should be able to run the programs.
|
||||||
|
|
||||||
* `Launcher.py` gives access to many components, including clients registered in `worlds/LauncherComponents.py`.
|
|
||||||
* The Launcher button "Generate Template Options" will generate default yamls for all worlds.
|
|
||||||
* With yaml(s) in the `Players` folder, `Generate.py` will generate the multiworld archive.
|
* With yaml(s) in the `Players` folder, `Generate.py` will generate the multiworld archive.
|
||||||
* `MultiServer.py`, with the filename of the generated archive as a command line parameter, will host the multiworld locally.
|
* `MultiServer.py`, with the filename of the generated archive as a command line parameter, will host the multiworld locally.
|
||||||
* `--log_network` is a command line parameter useful for debugging.
|
* `--log_network` is a command line parameter useful for debugging.
|
||||||
* `WebHost.py` will host the website on your computer.
|
* `WebHost.py` will host the website on your computer.
|
||||||
* You can copy `docs/webhost configuration sample.yaml` to `config.yaml`
|
* You can copy `docs/webhost configuration sample.yaml` to `config.yaml`
|
||||||
to change WebHost options (like the web hosting port number).
|
to change WebHost options (like the web hosting port number).
|
||||||
|
* As a side effect, `WebHost.py` creates the template yamls for all the games in `WebHostLib/static/generated`.
|
||||||
|
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|||||||
@@ -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,37 @@ 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 Weighted Options page. Extra
|
||||||
|
indentation and single newlines will be collapsed into spaces.
|
||||||
|
|
||||||
|
```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 +233,37 @@ 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 Weighted Options page. Extra indentation and
|
||||||
|
single newlines will be collapsed into spaces.
|
||||||
|
|
||||||
|
```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
|
||||||
|
|||||||
@@ -169,11 +169,6 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Ar
|
|||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apmlss"; ValueData: "{#MyAppName}mlsspatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}mlsspatch"; ValueData: "Archipelago Mario & Luigi Superstar Saga Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}mlsspatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}mlsspatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apcv64"; ValueData: "{#MyAppName}cv64patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: ".apcv64"; ValueData: "{#MyAppName}cv64patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Archipelago Castlevania 64 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Archipelago Castlevania 64 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
@@ -199,11 +194,6 @@ Root: HKCR; Subkey: "{#MyAppName}yipatch"; ValueData: "Archi
|
|||||||
Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apygo06"; ValueData: "{#MyAppName}ygo06patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}ygo06patch"; ValueData: "Archipelago Yu-Gi-Oh 2006 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import unittest
|
|||||||
from Fill import distribute_items_restrictive
|
from Fill import distribute_items_restrictive
|
||||||
from NetUtils import encode
|
from NetUtils import encode
|
||||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||||
from worlds import failed_world_loads
|
|
||||||
from . import setup_solo_multiworld
|
from . import setup_solo_multiworld
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +47,3 @@ class TestImplemented(unittest.TestCase):
|
|||||||
for key, data in multiworld.worlds[1].fill_slot_data().items():
|
for key, data in multiworld.worlds[1].fill_slot_data().items():
|
||||||
self.assertIsInstance(key, str, "keys in slot data must be a string")
|
self.assertIsInstance(key, str, "keys in slot data must be a string")
|
||||||
self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.")
|
self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.")
|
||||||
|
|
||||||
def test_no_failed_world_loads(self):
|
|
||||||
if failed_world_loads:
|
|
||||||
self.fail(f"The following worlds failed to load: {failed_world_loads}")
|
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ class TestBase(unittest.TestCase):
|
|||||||
{"medallions", "stones", "rewards", "logic_bottles"},
|
{"medallions", "stones", "rewards", "logic_bottles"},
|
||||||
"Starcraft 2":
|
"Starcraft 2":
|
||||||
{"Missions", "WoL Missions"},
|
{"Missions", "WoL Missions"},
|
||||||
"Yu-Gi-Oh! 2006":
|
|
||||||
{"Campaign Boss Beaten"}
|
|
||||||
}
|
}
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game_name, game_name=game_name):
|
with self.subTest(game_name, game_name=game_name):
|
||||||
@@ -64,6 +62,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,20 +3,19 @@ from __future__ import annotations
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
|
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 PerGameCommonOptions
|
||||||
ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions,
|
|
||||||
PriorityLocations, StartHints, StartInventory, StartInventoryPool, StartLocationHints
|
|
||||||
)
|
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
import random
|
||||||
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
|
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
|
||||||
from . import GamesPackage
|
from . import GamesPackage
|
||||||
from settings import Group
|
from settings import Group
|
||||||
@@ -54,12 +53,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:
|
||||||
@@ -114,33 +118,6 @@ class AutoLogicRegister(type):
|
|||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
class WebWorldRegister(type):
|
|
||||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> WebWorldRegister:
|
|
||||||
# don't allow an option to appear in multiple groups, allow "Item & Location Options" to appear anywhere by the
|
|
||||||
# dev, putting it at the end if they don't define options in it
|
|
||||||
option_groups: List[OptionGroup] = dct.get("option_groups", [])
|
|
||||||
item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints,
|
|
||||||
StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks]
|
|
||||||
seen_options = []
|
|
||||||
item_group_in_list = False
|
|
||||||
for group in option_groups:
|
|
||||||
assert group.name != "Game Options", "Game Options is a pre-determined group and can not be defined."
|
|
||||||
if group.name == "Item & Location Options":
|
|
||||||
group.options.extend(item_and_loc_options)
|
|
||||||
item_group_in_list = True
|
|
||||||
else:
|
|
||||||
for option in group.options:
|
|
||||||
assert option not in item_and_loc_options, \
|
|
||||||
f"{option} cannot be moved out of the \"Item & Location Options\" Group"
|
|
||||||
assert len(group.options) == len(set(group.options)), f"Duplicate options in option group {group.name}"
|
|
||||||
for option in group.options:
|
|
||||||
assert option not in seen_options, f"{option} found in two option groups"
|
|
||||||
seen_options.append(option)
|
|
||||||
if not item_group_in_list:
|
|
||||||
option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options))
|
|
||||||
return super().__new__(mcs, name, bases, dct)
|
|
||||||
|
|
||||||
|
|
||||||
def _timed_call(method: Callable[..., Any], *args: Any,
|
def _timed_call(method: Callable[..., Any], *args: Any,
|
||||||
multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any:
|
multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any:
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
@@ -195,7 +172,7 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
|||||||
_timed_call(stage_callable, multiworld, *args)
|
_timed_call(stage_callable, multiworld, *args)
|
||||||
|
|
||||||
|
|
||||||
class WebWorld(metaclass=WebWorldRegister):
|
class WebWorld:
|
||||||
"""Webhost integration"""
|
"""Webhost integration"""
|
||||||
|
|
||||||
options_page: Union[bool, str] = True
|
options_page: Union[bool, str] = True
|
||||||
@@ -217,15 +194,6 @@ class WebWorld(metaclass=WebWorldRegister):
|
|||||||
options_presets: Dict[str, Dict[str, Any]] = {}
|
options_presets: Dict[str, Dict[str, Any]] = {}
|
||||||
"""A dictionary containing a collection of developer-defined game option presets."""
|
"""A dictionary containing a collection of developer-defined game option presets."""
|
||||||
|
|
||||||
option_groups: ClassVar[List[OptionGroup]] = []
|
|
||||||
"""Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options"."""
|
|
||||||
|
|
||||||
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.
|
||||||
@@ -238,8 +206,8 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
|
|
||||||
game: ClassVar[str]
|
game: ClassVar[str]
|
||||||
"""name the game"""
|
"""name the game"""
|
||||||
topology_present: bool = False
|
topology_present: ClassVar[bool] = False
|
||||||
"""indicate if this world has any meaningful layout/pathing"""
|
"""indicate if world type has any meaningful layout/pathing"""
|
||||||
|
|
||||||
all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset()
|
all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset()
|
||||||
"""gets automatically populated with all item and item group names"""
|
"""gets automatically populated with all item and item group names"""
|
||||||
@@ -252,9 +220,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.
|
||||||
@@ -301,7 +283,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
location_names: ClassVar[Set[str]]
|
location_names: ClassVar[Set[str]]
|
||||||
"""set of all potential location names"""
|
"""set of all potential location names"""
|
||||||
|
|
||||||
random: Random
|
random: random.Random
|
||||||
"""This world's random object. Should be used for any randomization needed in world for this player slot."""
|
"""This world's random object. Should be used for any randomization needed in world for this player slot."""
|
||||||
|
|
||||||
settings_key: ClassVar[str]
|
settings_key: ClassVar[str]
|
||||||
@@ -318,7 +300,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
assert multiworld is not None
|
assert multiworld is not None
|
||||||
self.multiworld = multiworld
|
self.multiworld = multiworld
|
||||||
self.player = player
|
self.player = player
|
||||||
self.random = Random(multiworld.random.getrandbits(64))
|
self.random = random.Random(multiworld.random.getrandbits(64))
|
||||||
multiworld.per_slot_randoms[player] = self.random
|
multiworld.per_slot_randoms[player] = self.random
|
||||||
|
|
||||||
def __getattr__(self, item: str) -> Any:
|
def __getattr__(self, item: str) -> Any:
|
||||||
@@ -522,10 +504,6 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
def get_region(self, region_name: str) -> "Region":
|
def get_region(self, region_name: str) -> "Region":
|
||||||
return self.multiworld.get_region(region_name, self.player)
|
return self.multiworld.get_region(region_name, self.player)
|
||||||
|
|
||||||
@property
|
|
||||||
def player_name(self) -> str:
|
|
||||||
return self.multiworld.get_player_name(self.player)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_data_package_data(cls) -> "GamesPackage":
|
def get_data_package_data(cls) -> "GamesPackage":
|
||||||
sorted_item_name_groups = {
|
sorted_item_name_groups = {
|
||||||
@@ -558,3 +536,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
|
||||||
@@ -76,7 +77,7 @@ class BizHawkClientContext(CommonContext):
|
|||||||
if self.client_handler is not None:
|
if self.client_handler is not None:
|
||||||
self.client_handler.on_package(self, cmd, args)
|
self.client_handler.on_package(self, cmd, args)
|
||||||
|
|
||||||
async def server_auth(self, password_requested: bool=False):
|
async def server_auth(self, password_requested: bool = False):
|
||||||
self.password_requested = password_requested
|
self.password_requested = password_requested
|
||||||
|
|
||||||
if self.bizhawk_ctx.connection_status != ConnectionStatus.CONNECTED:
|
if self.bizhawk_ctx.connection_status != ConnectionStatus.CONNECTED:
|
||||||
@@ -102,7 +103,7 @@ class BizHawkClientContext(CommonContext):
|
|||||||
await self.send_connect()
|
await self.send_connect()
|
||||||
self.auth_status = AuthStatus.PENDING
|
self.auth_status = AuthStatus.PENDING
|
||||||
|
|
||||||
async def disconnect(self, allow_autoreconnect: bool=False):
|
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||||
self.auth_status = AuthStatus.NOT_AUTHENTICATED
|
self.auth_status = AuthStatus.NOT_AUTHENTICATED
|
||||||
await super().disconnect(allow_autoreconnect)
|
await super().disconnect(allow_autoreconnect)
|
||||||
|
|
||||||
@@ -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)")
|
|
||||||
@@ -18,7 +18,7 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import bsdiff4
|
import bsdiff4
|
||||||
from typing import Collection, Optional, List, SupportsIndex
|
from typing import Optional, List
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
||||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
||||||
@@ -52,7 +52,7 @@ except:
|
|||||||
enemizer_logger = logging.getLogger("Enemizer")
|
enemizer_logger = logging.getLogger("Enemizer")
|
||||||
|
|
||||||
|
|
||||||
class LocalRom:
|
class LocalRom(object):
|
||||||
|
|
||||||
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
|
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -71,13 +71,13 @@ class LocalRom:
|
|||||||
def read_byte(self, address: int) -> int:
|
def read_byte(self, address: int) -> int:
|
||||||
return self.buffer[address]
|
return self.buffer[address]
|
||||||
|
|
||||||
def read_bytes(self, startaddress: int, length: int) -> bytearray:
|
def read_bytes(self, startaddress: int, length: int) -> bytes:
|
||||||
return self.buffer[startaddress:startaddress + length]
|
return self.buffer[startaddress:startaddress + length]
|
||||||
|
|
||||||
def write_byte(self, address: int, value: int):
|
def write_byte(self, address: int, value: int):
|
||||||
self.buffer[address] = value
|
self.buffer[address] = value
|
||||||
|
|
||||||
def write_bytes(self, startaddress: int, values: Collection[SupportsIndex]) -> None:
|
def write_bytes(self, startaddress: int, values):
|
||||||
self.buffer[startaddress:startaddress + len(values)] = values
|
self.buffer[startaddress:startaddress + len(values)] = values
|
||||||
|
|
||||||
def encrypt_range(self, startaddress: int, length: int, key: bytes):
|
def encrypt_range(self, startaddress: int, length: int, key: bytes):
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -64,8 +64,7 @@ configuración personal y descargar un fichero "YAML".
|
|||||||
|
|
||||||
### Configuración YAML avanzada
|
### Configuración YAML avanzada
|
||||||
|
|
||||||
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina
|
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina ["Weighted settings"](/weighted-settings),
|
||||||
["Weighted settings"](/games/A Link to the Past/weighted-options),
|
|
||||||
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones
|
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones
|
||||||
representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser
|
representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser
|
||||||
elegidos sobre otros de la misma.
|
elegidos sobre otros de la misma.
|
||||||
|
|||||||
@@ -66,10 +66,9 @@ paramètres personnels et de les exporter vers un fichier YAML.
|
|||||||
### Configuration avancée du fichier YAML
|
### Configuration avancée du fichier YAML
|
||||||
|
|
||||||
Une version plus avancée du fichier YAML peut être créée en utilisant la page
|
Une version plus avancée du fichier YAML peut être créée en utilisant la page
|
||||||
des [paramètres de pondération](/games/A Link to the Past/weighted-options), qui vous permet de configurer jusqu'à
|
des [paramètres de pondération](/weighted-settings), qui vous permet de configurer jusqu'à trois préréglages. Cette page
|
||||||
trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs
|
a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants. Cela vous permet de choisir
|
||||||
glissants. Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux
|
quelles sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles dans une même catégorie.
|
||||||
autres disponibles dans une même catégorie.
|
|
||||||
|
|
||||||
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
|
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
|
||||||
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
|
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
|
||||||
|
|||||||
@@ -1,214 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Fri, 15 Mar 2024 18:41:40 +0000
|
|
||||||
Description: Manage items in the Aquaria game multiworld randomizer
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
from enum import Enum
|
|
||||||
from BaseClasses import Item, ItemClassification
|
|
||||||
|
|
||||||
|
|
||||||
class ItemType(Enum):
|
|
||||||
"""
|
|
||||||
Used to indicate to the multi-world if an item is useful or not
|
|
||||||
"""
|
|
||||||
NORMAL = 0
|
|
||||||
PROGRESSION = 1
|
|
||||||
JUNK = 2
|
|
||||||
|
|
||||||
|
|
||||||
class ItemGroup(Enum):
|
|
||||||
"""
|
|
||||||
Used to group items
|
|
||||||
"""
|
|
||||||
COLLECTIBLE = 0
|
|
||||||
INGREDIENT = 1
|
|
||||||
RECIPE = 2
|
|
||||||
HEALTH = 3
|
|
||||||
UTILITY = 4
|
|
||||||
SONG = 5
|
|
||||||
TURTLE = 6
|
|
||||||
|
|
||||||
|
|
||||||
class AquariaItem(Item):
|
|
||||||
"""
|
|
||||||
A single item in the Aquaria game.
|
|
||||||
"""
|
|
||||||
game: str = "Aquaria"
|
|
||||||
"""The name of the game"""
|
|
||||||
|
|
||||||
def __init__(self, name: str, classification: ItemClassification,
|
|
||||||
code: Optional[int], player: int):
|
|
||||||
"""
|
|
||||||
Initialisation of the Item
|
|
||||||
:param name: The name of the item
|
|
||||||
:param classification: If the item is useful or not
|
|
||||||
:param code: The ID of the item (if None, it is an event)
|
|
||||||
:param player: The ID of the player in the multiworld
|
|
||||||
"""
|
|
||||||
super().__init__(name, classification, code, player)
|
|
||||||
|
|
||||||
|
|
||||||
class ItemData:
|
|
||||||
"""
|
|
||||||
Data of an item.
|
|
||||||
"""
|
|
||||||
id: int
|
|
||||||
count: int
|
|
||||||
type: ItemType
|
|
||||||
group: ItemGroup
|
|
||||||
|
|
||||||
def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup):
|
|
||||||
"""
|
|
||||||
Initialisation of the item data
|
|
||||||
@param id: The item ID
|
|
||||||
@param count: the number of items in the pool
|
|
||||||
@param type: the importance type of the item
|
|
||||||
@param group: the usage of the item in the game
|
|
||||||
"""
|
|
||||||
self.id = id
|
|
||||||
self.count = count
|
|
||||||
self.type = type
|
|
||||||
self.group = group
|
|
||||||
|
|
||||||
|
|
||||||
"""Information data for every (not event) item."""
|
|
||||||
item_table = {
|
|
||||||
# name: ID, Nb, Item Type, Item Group
|
|
||||||
"Anemone": ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone
|
|
||||||
"Arnassi statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue
|
|
||||||
"Big seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed
|
|
||||||
"Glowing seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed
|
|
||||||
"Black pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl
|
|
||||||
"Baby blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster
|
|
||||||
"Crab armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume
|
|
||||||
"Baby dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo
|
|
||||||
"Tooth": ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss
|
|
||||||
"Energy statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue
|
|
||||||
"Krotite armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple
|
|
||||||
"Golden starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star
|
|
||||||
"Golden gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear
|
|
||||||
"Jelly beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon
|
|
||||||
"Jelly costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume
|
|
||||||
"Jelly plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant
|
|
||||||
"Mithalas doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll
|
|
||||||
"Mithalan dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume
|
|
||||||
"Mithalas banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner
|
|
||||||
"Mithalas pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot
|
|
||||||
"Mutant costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
|
|
||||||
"Baby nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
|
|
||||||
"Baby piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
|
|
||||||
"Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume
|
|
||||||
"Seed bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
|
|
||||||
"King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
|
|
||||||
"Song plant spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
|
|
||||||
"Stone head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head
|
|
||||||
"Sun key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key
|
|
||||||
"Girl costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume
|
|
||||||
"Odd container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest
|
|
||||||
"Trident": ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
|
|
||||||
"Turtle egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
|
|
||||||
"Jelly egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
|
|
||||||
"Urchin costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
|
|
||||||
"Baby walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
|
|
||||||
"Vedha's Cure-All-All": ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
|
|
||||||
"Zuuna's perogi": ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
|
|
||||||
"Arcane poultice": ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice
|
|
||||||
"Berry ice cream": ItemData(698039, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_berryicecream
|
|
||||||
"Buttery sea loaf": ItemData(698040, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_butterysealoaf
|
|
||||||
"Cold borscht": ItemData(698041, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldborscht
|
|
||||||
"Cold soup": ItemData(698042, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldsoup
|
|
||||||
"Crab cake": ItemData(698043, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_crabcake
|
|
||||||
"Divine soup": ItemData(698044, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_divinesoup
|
|
||||||
"Dumbo ice cream": ItemData(698045, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_dumboicecream
|
|
||||||
"Fish oil": ItemData(698046, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
|
|
||||||
"Glowing egg": ItemData(698047, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
|
|
||||||
"Hand roll": ItemData(698048, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_handroll
|
|
||||||
"Healing poultice": ItemData(698049, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
|
|
||||||
"Hearty soup": ItemData(698050, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_heartysoup
|
|
||||||
"Hot borscht": ItemData(698051, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_hotborscht
|
|
||||||
"Hot soup": ItemData(698052, 3, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
|
|
||||||
"Ice cream": ItemData(698053, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_icecream
|
|
||||||
"Leadership roll": ItemData(698054, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
|
|
||||||
"Leaf poultice": ItemData(698055, 5, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
|
|
||||||
"Leeching poultice": ItemData(698056, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leechingpoultice
|
|
||||||
"Legendary cake": ItemData(698057, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_legendarycake
|
|
||||||
"Loaf of life": ItemData(698058, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_loafoflife
|
|
||||||
"Long life soup": ItemData(698059, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_longlifesoup
|
|
||||||
"Magic soup": ItemData(698060, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_magicsoup
|
|
||||||
"Mushroom x 2": ItemData(698061, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_mushroom
|
|
||||||
"Perogi": ItemData(698062, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_perogi
|
|
||||||
"Plant leaf": ItemData(698063, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
|
||||||
"Plump perogi": ItemData(698064, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_plumpperogi
|
|
||||||
"Poison loaf": ItemData(698065, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonloaf
|
|
||||||
"Poison soup": ItemData(698066, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonsoup
|
|
||||||
"Rainbow mushroom": ItemData(698067, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_rainbowmushroom
|
|
||||||
"Rainbow soup": ItemData(698068, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_rainbowsoup
|
|
||||||
"Red berry": ItemData(698069, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redberry
|
|
||||||
"Red bulb x 2": ItemData(698070, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redbulb
|
|
||||||
"Rotten cake": ItemData(698071, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottencake
|
|
||||||
"Rotten loaf x 8": ItemData(698072, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottenloaf
|
|
||||||
"Rotten meat": ItemData(698073, 5, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
|
||||||
"Royal soup": ItemData(698074, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_royalsoup
|
|
||||||
"Sea cake": ItemData(698075, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_seacake
|
|
||||||
"Sea loaf": ItemData(698076, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
|
|
||||||
"Shark fin soup": ItemData(698077, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sharkfinsoup
|
|
||||||
"Sight poultice": ItemData(698078, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sightpoultice
|
|
||||||
"Small bone x 2": ItemData(698079, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
|
|
||||||
"Small egg": ItemData(698080, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
|
|
||||||
"Small tentacle x 2": ItemData(698081, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smalltentacle
|
|
||||||
"Special bulb": ItemData(698082, 5, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_specialbulb
|
|
||||||
"Special cake": ItemData(698083, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_specialcake
|
|
||||||
"Spicy meat x 2": ItemData(698084, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_spicymeat
|
|
||||||
"Spicy roll": ItemData(698085, 11, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicyroll
|
|
||||||
"Spicy soup": ItemData(698086, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicysoup
|
|
||||||
"Spider roll": ItemData(698087, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spiderroll
|
|
||||||
"Swamp cake": ItemData(698088, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_swampcake
|
|
||||||
"Tasty cake": ItemData(698089, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastycake
|
|
||||||
"Tasty roll": ItemData(698090, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastyroll
|
|
||||||
"Tough cake": ItemData(698091, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_toughcake
|
|
||||||
"Turtle soup": ItemData(698092, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_turtlesoup
|
|
||||||
"Vedha sea crisp": ItemData(698093, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_vedhaseacrisp
|
|
||||||
"Veggie cake": ItemData(698094, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiecake
|
|
||||||
"Veggie ice cream": ItemData(698095, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggieicecream
|
|
||||||
"Veggie soup": ItemData(698096, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiesoup
|
|
||||||
"Volcano roll": ItemData(698097, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_volcanoroll
|
|
||||||
"Health upgrade": ItemData(698098, 5, ItemType.NORMAL, ItemGroup.HEALTH), # upgrade_health_?
|
|
||||||
"Wok": ItemData(698099, 1, ItemType.NORMAL, ItemGroup.UTILITY), # upgrade_wok
|
|
||||||
"Eel oil x 2": ItemData(698100, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_eeloil
|
|
||||||
"Fish meat x 2": ItemData(698101, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishmeat
|
|
||||||
"Fish oil x 3": ItemData(698102, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
|
|
||||||
"Glowing egg x 2": ItemData(698103, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
|
|
||||||
"Healing poultice x 2": ItemData(698104, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
|
|
||||||
"Hot soup x 2": ItemData(698105, 1, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
|
|
||||||
"Leadership roll x 2": ItemData(698106, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
|
|
||||||
"Leaf poultice x 3": ItemData(698107, 2, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
|
|
||||||
"Plant leaf x 2": ItemData(698108, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
|
||||||
"Plant leaf x 3": ItemData(698109, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
|
||||||
"Rotten meat x 2": ItemData(698110, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
|
||||||
"Rotten meat x 8": ItemData(698111, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
|
||||||
"Sea loaf x 2": ItemData(698112, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
|
|
||||||
"Small bone x 3": ItemData(698113, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
|
|
||||||
"Small egg x 2": ItemData(698114, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
|
|
||||||
"Li and Li song": ItemData(698115, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_li
|
|
||||||
"Shield song": ItemData(698116, 1, ItemType.NORMAL, ItemGroup.SONG), # song_shield
|
|
||||||
"Beast form": ItemData(698117, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_beast
|
|
||||||
"Sun form": ItemData(698118, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_sun
|
|
||||||
"Nature form": ItemData(698119, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_nature
|
|
||||||
"Energy form": ItemData(698120, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_energy
|
|
||||||
"Bind song": ItemData(698121, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_bind
|
|
||||||
"Fish form": ItemData(698122, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_fish
|
|
||||||
"Spirit form": ItemData(698123, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_spirit
|
|
||||||
"Dual form": ItemData(698124, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_dual
|
|
||||||
"Transturtle Veil top left": ItemData(698125, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil01
|
|
||||||
"Transturtle Veil top right": ItemData(698126, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil02
|
|
||||||
"Transturtle Open Water top right": ItemData(698127, 1, ItemType.PROGRESSION,
|
|
||||||
ItemGroup.TURTLE), # transport_openwater03
|
|
||||||
"Transturtle Forest bottom left": ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest04
|
|
||||||
"Transturtle Home water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea
|
|
||||||
"Transturtle Abyss right": ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03
|
|
||||||
"Transturtle Final Boss": ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss
|
|
||||||
"Transturtle Simon says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
|
|
||||||
"Transturtle Arnassi ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
|
|
||||||
}
|
|
||||||
@@ -1,574 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Fri, 15 Mar 2024 18:41:40 +0000
|
|
||||||
Description: Manage locations in the Aquaria game multiworld randomizer
|
|
||||||
"""
|
|
||||||
|
|
||||||
from BaseClasses import Location
|
|
||||||
|
|
||||||
|
|
||||||
class AquariaLocation(Location):
|
|
||||||
"""
|
|
||||||
A location in the game.
|
|
||||||
"""
|
|
||||||
game: str = "Aquaria"
|
|
||||||
"""The name of the game"""
|
|
||||||
|
|
||||||
def __init__(self, player: int, name="", code=None, parent=None) -> None:
|
|
||||||
"""
|
|
||||||
Initialisation of the object
|
|
||||||
:param player: the ID of the player
|
|
||||||
:param name: the name of the location
|
|
||||||
:param code: the ID (or address) of the location (Event if None)
|
|
||||||
:param parent: the Region that this location belongs to
|
|
||||||
"""
|
|
||||||
super(AquariaLocation, self).__init__(player, name, code, parent)
|
|
||||||
self.event = code is None
|
|
||||||
|
|
||||||
|
|
||||||
class AquariaLocations:
|
|
||||||
|
|
||||||
locations_verse_cave_r = {
|
|
||||||
"Verse cave, bulb in the skeleton room": 698107,
|
|
||||||
"Verse cave, bulb in the path left of the skeleton room": 698108,
|
|
||||||
"Verse cave right area, Big Seed": 698175,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_verse_cave_l = {
|
|
||||||
"Verse cave, the Naija hint about here shield ability": 698200,
|
|
||||||
"Verse cave left area, bulb in the center part": 698021,
|
|
||||||
"Verse cave left area, bulb in the right part": 698022,
|
|
||||||
"Verse cave left area, bulb under the rock at the end of the path": 698023,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_home_water = {
|
|
||||||
"Home water, bulb below the grouper fish": 698058,
|
|
||||||
"Home water, bulb in the path below Nautilus Prime": 698059,
|
|
||||||
"Home water, bulb in the little room above the grouper fish": 698060,
|
|
||||||
"Home water, bulb in the end of the left path from the verse cave": 698061,
|
|
||||||
"Home water, bulb in the top left path": 698062,
|
|
||||||
"Home water, bulb in the bottom left room": 698063,
|
|
||||||
"Home water, bulb close to the Naija's home": 698064,
|
|
||||||
"Home water, bulb under the rock in the left path from the verse cave": 698065,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_home_water_nautilus = {
|
|
||||||
"Home water, Nautilus Egg": 698194,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_home_water_transturtle = {
|
|
||||||
"Home water, Transturtle": 698213,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_naija_home = {
|
|
||||||
"Naija's home, bulb after the energy door": 698119,
|
|
||||||
"Naija's home, bulb under the rock at the right of the main path": 698120,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_song_cave = {
|
|
||||||
"Song cave, Erulian spirit": 698206,
|
|
||||||
"Song cave, bulb in the top left part": 698071,
|
|
||||||
"Song cave, bulb in the big anemone room": 698072,
|
|
||||||
"Song cave, bulb in the path to the singing statues": 698073,
|
|
||||||
"Song cave, bulb under the rock in the path to the singing statues": 698074,
|
|
||||||
"Song cave, bulb under the rock close to the song door": 698075,
|
|
||||||
"Song cave, Verse egg": 698160,
|
|
||||||
"Song cave, Jelly beacon": 698178,
|
|
||||||
"Song cave, Anemone seed": 698162,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_energy_temple_1 = {
|
|
||||||
"Energy temple first area, beating the energy statue": 698205,
|
|
||||||
"Energy temple first area, bulb in the bottom room blocked by a rock": 698027,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_energy_temple_idol = {
|
|
||||||
"Energy temple first area, Energy Idol": 698170,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_energy_temple_2 = {
|
|
||||||
"Energy temple second area, bulb under the rock": 698028,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_energy_temple_altar = {
|
|
||||||
"Energy temple bottom entrance, Krotite armor": 698163,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_energy_temple_3 = {
|
|
||||||
"Energy temple third area, bulb in the bottom path": 698029,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_energy_temple_boss = {
|
|
||||||
"Energy temple boss area, Fallen god tooth": 698169,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_energy_temple_blaster_room = {
|
|
||||||
"Energy temple blaster room, Blaster egg": 698195,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_openwater_tl = {
|
|
||||||
"Open water top left area, bulb under the rock in the right path": 698001,
|
|
||||||
"Open water top left area, bulb under the rock in the left path": 698002,
|
|
||||||
"Open water top left area, bulb to the right of the save cristal": 698003,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_openwater_tr = {
|
|
||||||
"Open water top right area, bulb in the small path before Mithalas": 698004,
|
|
||||||
"Open water top right area, bulb in the path from the left entrance": 698005,
|
|
||||||
"Open water top right area, bulb in the clearing close to the bottom exit": 698006,
|
|
||||||
"Open water top right area, bulb in the big clearing close to the save cristal": 698007,
|
|
||||||
"Open water top right area, bulb in the big clearing to the top exit": 698008,
|
|
||||||
"Open water top right area, first urn in the Mithalas exit": 698148,
|
|
||||||
"Open water top right area, second urn in the Mithalas exit": 698149,
|
|
||||||
"Open water top right area, third urn in the Mithalas exit": 698150,
|
|
||||||
}
|
|
||||||
locations_openwater_tr_turtle = {
|
|
||||||
"Open water top right area, bulb in the turtle room": 698009,
|
|
||||||
"Open water top right area, Transturtle": 698211,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_openwater_bl = {
|
|
||||||
"Open water bottom left area, bulb behind the chomper fish": 698011,
|
|
||||||
"Open water bottom left area, bulb inside the lowest fish pass": 698010,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_skeleton_path = {
|
|
||||||
"Open water skeleton path, bulb close to the right exit": 698012,
|
|
||||||
"Open water skeleton path, bulb behind the chomper fish": 698013,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_skeleton_path_sc = {
|
|
||||||
"Open water skeleton path, King skull": 698177,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_arnassi = {
|
|
||||||
"Arnassi Ruins, bulb in the right part": 698014,
|
|
||||||
"Arnassi Ruins, bulb in the left part": 698015,
|
|
||||||
"Arnassi Ruins, bulb in the center part": 698016,
|
|
||||||
"Arnassi ruins, Song plant spore on the top of the ruins": 698179,
|
|
||||||
"Arnassi ruins, Arnassi Armor": 698191,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_arnassi_path = {
|
|
||||||
"Arnassi Ruins, Arnassi statue": 698164,
|
|
||||||
"Arnassi Ruins, Transturtle": 698217,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_arnassi_crab_boss = {
|
|
||||||
"Arnassi ruins, Crab armor": 698187,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_simon = {
|
|
||||||
"Kelp forest, beating Simon says": 698156,
|
|
||||||
"Simon says area, Transturtle": 698216,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_mithalas_city = {
|
|
||||||
"Mithalas city, first bulb in the left city part": 698030,
|
|
||||||
"Mithalas city, second bulb in the left city part": 698035,
|
|
||||||
"Mithalas city, bulb in the right part": 698031,
|
|
||||||
"Mithalas city, bulb at the top of the city": 698033,
|
|
||||||
"Mithalas city, first bulb in a broken home": 698034,
|
|
||||||
"Mithalas city, second bulb in a broken home": 698041,
|
|
||||||
"Mithalas city, bulb in the bottom left part": 698037,
|
|
||||||
"Mithalas city, first bulb in one of the homes": 698038,
|
|
||||||
"Mithalas city, second bulb in one of the homes": 698039,
|
|
||||||
"Mithalas city, first urn in one of the homes": 698123,
|
|
||||||
"Mithalas city, second urn in one of the homes": 698124,
|
|
||||||
"Mithalas city, first urn in the city reserve": 698125,
|
|
||||||
"Mithalas city, second urn in the city reserve": 698126,
|
|
||||||
"Mithalas city, third urn in the city reserve": 698127,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_mithalas_city_top_path = {
|
|
||||||
"Mithalas city, first bulb at the end of the top path": 698032,
|
|
||||||
"Mithalas city, second bulb at the end of the top path": 698040,
|
|
||||||
"Mithalas city, bulb in the top path": 698036,
|
|
||||||
"Mithalas city, Mithalas pot": 698174,
|
|
||||||
"Mithalas city, urn in the cathedral flower tube entrance": 698128,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_mithalas_city_fishpass = {
|
|
||||||
"Mithalas city, Doll": 698173,
|
|
||||||
"Mithalas city, urn inside a home fish pass": 698129,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_cathedral_l = {
|
|
||||||
"Mithalas city castle, bulb in the flesh hole": 698042,
|
|
||||||
"Mithalas city castle, Blue banner": 698165,
|
|
||||||
"Mithalas city castle, urn in the bedroom": 698130,
|
|
||||||
"Mithalas city castle, first urn of the single lamp path": 698131,
|
|
||||||
"Mithalas city castle, second urn of the single lamp path": 698132,
|
|
||||||
"Mithalas city castle, urn in the bottom room": 698133,
|
|
||||||
"Mithalas city castle, first urn on the entrance path": 698134,
|
|
||||||
"Mithalas city castle, second urn on the entrance path": 698135,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_cathedral_l_tube = {
|
|
||||||
"Mithalas castle, beating the priests": 698208,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_cathedral_l_sc = {
|
|
||||||
"Mithalas city castle, Trident head": 698183,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_cathedral_r = {
|
|
||||||
"Mithalas cathedral, first urn in the top right room": 698136,
|
|
||||||
"Mithalas cathedral, second urn in the top right room": 698137,
|
|
||||||
"Mithalas cathedral, third urn in the top right room": 698138,
|
|
||||||
"Mithalas cathedral, urn in the flesh room with fleas": 698139,
|
|
||||||
"Mithalas cathedral, first urn in the bottom right path": 698140,
|
|
||||||
"Mithalas cathedral, second urn in the bottom right path": 698141,
|
|
||||||
"Mithalas cathedral, urn behind the flesh vein": 698142,
|
|
||||||
"Mithalas cathedral, urn in the top left eyes boss room": 698143,
|
|
||||||
"Mithalas cathedral, first urn in the path behind the flesh vein": 698144,
|
|
||||||
"Mithalas cathedral, second urn in the path behind the flesh vein": 698145,
|
|
||||||
"Mithalas cathedral, third urn in the path behind the flesh vein": 698146,
|
|
||||||
"Mithalas cathedral, one of the urns in the top right room": 698147,
|
|
||||||
"Mithalas cathedral, Mithalan Dress": 698189,
|
|
||||||
"Mithalas cathedral right area, urn below the left entrance": 698198,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_cathedral_underground = {
|
|
||||||
"Cathedral underground, bulb in the center part": 698113,
|
|
||||||
"Cathedral underground, first bulb in the top left part": 698114,
|
|
||||||
"Cathedral underground, second bulb in the top left part": 698115,
|
|
||||||
"Cathedral underground, third bulb in the top left part": 698116,
|
|
||||||
"Cathedral underground, bulb close to the save cristal": 698117,
|
|
||||||
"Cathedral underground, bulb in the bottom right path": 698118,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_cathedral_boss = {
|
|
||||||
"Cathedral boss area, beating Mithalan God": 698202,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_tl = {
|
|
||||||
"Kelp Forest top left area, bulb in the bottom left clearing": 698044,
|
|
||||||
"Kelp Forest top left area, bulb in the path down from the top left clearing": 698045,
|
|
||||||
"Kelp Forest top left area, bulb in the top left clearing": 698046,
|
|
||||||
"Kelp Forest top left, Jelly Egg": 698185,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_tl_fp = {
|
|
||||||
"Kelp Forest top left area, bulb close to the Verse egg": 698047,
|
|
||||||
"Kelp forest top left area, Verse egg": 698158,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_tr = {
|
|
||||||
"Kelp Forest top right area, bulb under the rock in the right path": 698048,
|
|
||||||
"Kelp Forest top right area, bulb at the left of the center clearing": 698049,
|
|
||||||
"Kelp Forest top right area, bulb in the left path's big room": 698051,
|
|
||||||
"Kelp Forest top right area, bulb in the left path's small room": 698052,
|
|
||||||
"Kelp Forest top right area, bulb at the top of the center clearing": 698053,
|
|
||||||
"Kelp forest top right area, Black pearl": 698167,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_tr_fp = {
|
|
||||||
"Kelp Forest top right area, bulb in the top fish pass": 698050,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_bl = {
|
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals": 698054,
|
|
||||||
"Kelp forest bottom left area, Walker baby": 698186,
|
|
||||||
"Kelp Forest bottom left area, Transturtle": 698212,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_br = {
|
|
||||||
"Kelp forest bottom right area, Odd Container": 698168,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_boss = {
|
|
||||||
"Kelp forest boss area, beating Drunian God": 698204,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_boss_entrance = {
|
|
||||||
"Kelp Forest boss room, bulb at the bottom of the area": 698055,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_fish_cave = {
|
|
||||||
"Kelp Forest bottom left area, Fish cave puzzle": 698207,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_sprite_cave = {
|
|
||||||
"Kelp Forest sprite cave, bulb inside the fish pass": 698056,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_forest_sprite_cave_tube = {
|
|
||||||
"Kelp Forest sprite cave, bulb in the second room": 698057,
|
|
||||||
"Kelp Forest Sprite Cave, Seed bag": 698176,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_mermog_cave = {
|
|
||||||
"Mermog cave, bulb in the left part of the cave": 698121,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_mermog_boss = {
|
|
||||||
"Mermog cave, Piranha Egg": 698197,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_veil_tl = {
|
|
||||||
"The veil top left area, In the Li cave": 698199,
|
|
||||||
"The veil top left area, bulb under the rock in the top right path": 698078,
|
|
||||||
"The veil top left area, bulb hidden behind the blocking rock": 698076,
|
|
||||||
"The veil top left area, Transturtle": 698209,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_veil_tl_fp = {
|
|
||||||
"The veil top left area, bulb inside the fish pass": 698077,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_turtle_cave = {
|
|
||||||
"Turtle cave, Turtle Egg": 698184,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_turtle_cave_bubble = {
|
|
||||||
"Turtle cave, bulb in bubble cliff": 698000,
|
|
||||||
"Turtle cave, Urchin costume": 698193,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_veil_tr_r = {
|
|
||||||
"The veil top right area, bulb in the middle of the wall jump cliff": 698079,
|
|
||||||
"The veil top right area, golden starfish at the bottom right of the bottom path": 698180,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_veil_tr_l = {
|
|
||||||
"The veil top right area, bulb in the top of the water fall": 698080,
|
|
||||||
"The veil top right area, Transturtle": 698210,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_veil_bl = {
|
|
||||||
"The veil bottom area, bulb in the left path": 698082,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_veil_b_sc = {
|
|
||||||
"The veil bottom area, bulb in the spirit path": 698081,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_veil_bl_fp = {
|
|
||||||
"The veil bottom area, Verse egg": 698157,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_veil_br = {
|
|
||||||
"The veil bottom area, Stone Head": 698181,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_octo_cave_t = {
|
|
||||||
"Octopus cave, Dumbo Egg": 698196,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_octo_cave_b = {
|
|
||||||
"Octopus cave, bulb in the path below the octopus cave path": 698122,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_sun_temple_l = {
|
|
||||||
"Sun temple, bulb in the top left part": 698094,
|
|
||||||
"Sun temple, bulb in the top right part": 698095,
|
|
||||||
"Sun temple, bulb at the top of the high dark room": 698096,
|
|
||||||
"Sun temple, Golden Gear": 698171,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_sun_temple_r = {
|
|
||||||
"Sun temple, first bulb of the temple": 698091,
|
|
||||||
"Sun temple, bulb on the left part": 698092,
|
|
||||||
"Sun temple, bulb in the hidden room of the right part": 698093,
|
|
||||||
"Sun temple, Sun key": 698182,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_sun_temple_boss_path = {
|
|
||||||
"Sun Worm path, first path bulb": 698017,
|
|
||||||
"Sun Worm path, second path bulb": 698018,
|
|
||||||
"Sun Worm path, first cliff bulb": 698019,
|
|
||||||
"Sun Worm path, second cliff bulb": 698020,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_sun_temple_boss = {
|
|
||||||
"Sun temple boss area, beating Sun God": 698203,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_abyss_l = {
|
|
||||||
"Abyss left area, bulb in hidden path room": 698024,
|
|
||||||
"Abyss left area, bulb in the right part": 698025,
|
|
||||||
"Abyss left area, Glowing seed": 698166,
|
|
||||||
"Abyss left area, Glowing Plant": 698172,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_abyss_lb = {
|
|
||||||
"Abyss left area, bulb in the bottom fish pass": 698026,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_abyss_r = {
|
|
||||||
"Abyss right area, bulb behind the rock in the whale room": 698109,
|
|
||||||
"Abyss right area, bulb in the middle path": 698110,
|
|
||||||
"Abyss right area, bulb behind the rock in the middle path": 698111,
|
|
||||||
"Abyss right area, bulb in the left green room": 698112,
|
|
||||||
"Abyss right area, Transturtle": 698214,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_ice_cave = {
|
|
||||||
"Ice cave, bulb in the room to the right": 698083,
|
|
||||||
"Ice cave, First bulbs in the top exit room": 698084,
|
|
||||||
"Ice cave, Second bulbs in the top exit room": 698085,
|
|
||||||
"Ice cave, third bulbs in the top exit room": 698086,
|
|
||||||
"Ice cave, bulb in the left room": 698087,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_bubble_cave = {
|
|
||||||
"Bubble cave, bulb in the left cave wall": 698089,
|
|
||||||
"Bubble cave, bulb in the right cave wall (behind the ice cristal)": 698090,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_bubble_cave_boss = {
|
|
||||||
"Bubble cave, Verse egg": 698161,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_king_jellyfish_cave = {
|
|
||||||
"King Jellyfish cave, bulb in the right path from King Jelly": 698088,
|
|
||||||
"King Jellyfish cave, Jellyfish Costume": 698188,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_whale = {
|
|
||||||
"The whale, Verse egg": 698159,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_sunken_city_r = {
|
|
||||||
"Sunken city right area, crate close to the save cristal": 698154,
|
|
||||||
"Sunken city right area, crate in the left bottom room": 698155,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_sunken_city_l = {
|
|
||||||
"Sunken city left area, crate in the little pipe room": 698151,
|
|
||||||
"Sunken city left area, crate close to the save cristal": 698152,
|
|
||||||
"Sunken city left area, crate before the bedroom": 698153,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_sunken_city_l_bedroom = {
|
|
||||||
"Sunken city left area, Girl Costume": 698192,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_sunken_city_boss = {
|
|
||||||
"Sunken city, bulb on the top of the boss area (boiler room)": 698043,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_body_c = {
|
|
||||||
"The body center area, breaking li cage": 698201,
|
|
||||||
"The body main area, bulb on the main path blocking tube": 698097,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_body_l = {
|
|
||||||
"The body left area, first bulb in the top face room": 698066,
|
|
||||||
"The body left area, second bulb in the top face room": 698069,
|
|
||||||
"The body left area, bulb below the water stream": 698067,
|
|
||||||
"The body left area, bulb in the top path to the top face room": 698068,
|
|
||||||
"The body left area, bulb in the bottom face room": 698070,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_body_rt = {
|
|
||||||
"The body right area, bulb in the top face room": 698100,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_body_rb = {
|
|
||||||
"The body right area, bulb in the top path to the bottom face room": 698098,
|
|
||||||
"The body right area, bulb in the bottom face room": 698099,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_body_b = {
|
|
||||||
"The body bottom area, bulb in the Jelly Zap room": 698101,
|
|
||||||
"The body bottom area, bulb in the nautilus room": 698102,
|
|
||||||
"The body bottom area, Mutant Costume": 698190,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_final_boss_tube = {
|
|
||||||
"Final boss area, first bulb in the turtle room": 698103,
|
|
||||||
"Final boss area, second bulbs in the turtle room": 698104,
|
|
||||||
"Final boss area, third bulbs in the turtle room": 698105,
|
|
||||||
"Final boss area, Transturtle": 698215,
|
|
||||||
}
|
|
||||||
|
|
||||||
locations_final_boss = {
|
|
||||||
"Final boss area, bulb in the boss third form room": 698106,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
location_table = {
|
|
||||||
**AquariaLocations.locations_openwater_tl,
|
|
||||||
**AquariaLocations.locations_openwater_tr,
|
|
||||||
**AquariaLocations.locations_openwater_tr_turtle,
|
|
||||||
**AquariaLocations.locations_openwater_bl,
|
|
||||||
**AquariaLocations.locations_skeleton_path,
|
|
||||||
**AquariaLocations.locations_skeleton_path_sc,
|
|
||||||
**AquariaLocations.locations_arnassi,
|
|
||||||
**AquariaLocations.locations_arnassi_path,
|
|
||||||
**AquariaLocations.locations_arnassi_crab_boss,
|
|
||||||
**AquariaLocations.locations_sun_temple_l,
|
|
||||||
**AquariaLocations.locations_sun_temple_r,
|
|
||||||
**AquariaLocations.locations_sun_temple_boss_path,
|
|
||||||
**AquariaLocations.locations_sun_temple_boss,
|
|
||||||
**AquariaLocations.locations_verse_cave_r,
|
|
||||||
**AquariaLocations.locations_verse_cave_l,
|
|
||||||
**AquariaLocations.locations_abyss_l,
|
|
||||||
**AquariaLocations.locations_abyss_lb,
|
|
||||||
**AquariaLocations.locations_abyss_r,
|
|
||||||
**AquariaLocations.locations_energy_temple_1,
|
|
||||||
**AquariaLocations.locations_energy_temple_2,
|
|
||||||
**AquariaLocations.locations_energy_temple_3,
|
|
||||||
**AquariaLocations.locations_energy_temple_boss,
|
|
||||||
**AquariaLocations.locations_energy_temple_blaster_room,
|
|
||||||
**AquariaLocations.locations_energy_temple_altar,
|
|
||||||
**AquariaLocations.locations_energy_temple_idol,
|
|
||||||
**AquariaLocations.locations_mithalas_city,
|
|
||||||
**AquariaLocations.locations_mithalas_city_top_path,
|
|
||||||
**AquariaLocations.locations_mithalas_city_fishpass,
|
|
||||||
**AquariaLocations.locations_cathedral_l,
|
|
||||||
**AquariaLocations.locations_cathedral_l_tube,
|
|
||||||
**AquariaLocations.locations_cathedral_l_sc,
|
|
||||||
**AquariaLocations.locations_cathedral_r,
|
|
||||||
**AquariaLocations.locations_cathedral_underground,
|
|
||||||
**AquariaLocations.locations_cathedral_boss,
|
|
||||||
**AquariaLocations.locations_forest_tl,
|
|
||||||
**AquariaLocations.locations_forest_tl_fp,
|
|
||||||
**AquariaLocations.locations_forest_tr,
|
|
||||||
**AquariaLocations.locations_forest_tr_fp,
|
|
||||||
**AquariaLocations.locations_forest_bl,
|
|
||||||
**AquariaLocations.locations_forest_br,
|
|
||||||
**AquariaLocations.locations_forest_boss,
|
|
||||||
**AquariaLocations.locations_forest_boss_entrance,
|
|
||||||
**AquariaLocations.locations_forest_sprite_cave,
|
|
||||||
**AquariaLocations.locations_forest_sprite_cave_tube,
|
|
||||||
**AquariaLocations.locations_forest_fish_cave,
|
|
||||||
**AquariaLocations.locations_home_water,
|
|
||||||
**AquariaLocations.locations_home_water_transturtle,
|
|
||||||
**AquariaLocations.locations_home_water_nautilus,
|
|
||||||
**AquariaLocations.locations_body_l,
|
|
||||||
**AquariaLocations.locations_body_rt,
|
|
||||||
**AquariaLocations.locations_body_rb,
|
|
||||||
**AquariaLocations.locations_body_c,
|
|
||||||
**AquariaLocations.locations_body_b,
|
|
||||||
**AquariaLocations.locations_final_boss_tube,
|
|
||||||
**AquariaLocations.locations_final_boss,
|
|
||||||
**AquariaLocations.locations_song_cave,
|
|
||||||
**AquariaLocations.locations_veil_tl,
|
|
||||||
**AquariaLocations.locations_veil_tl_fp,
|
|
||||||
**AquariaLocations.locations_turtle_cave,
|
|
||||||
**AquariaLocations.locations_turtle_cave_bubble,
|
|
||||||
**AquariaLocations.locations_veil_tr_r,
|
|
||||||
**AquariaLocations.locations_veil_tr_l,
|
|
||||||
**AquariaLocations.locations_veil_bl,
|
|
||||||
**AquariaLocations.locations_veil_b_sc,
|
|
||||||
**AquariaLocations.locations_veil_bl_fp,
|
|
||||||
**AquariaLocations.locations_veil_br,
|
|
||||||
**AquariaLocations.locations_ice_cave,
|
|
||||||
**AquariaLocations.locations_king_jellyfish_cave,
|
|
||||||
**AquariaLocations.locations_bubble_cave,
|
|
||||||
**AquariaLocations.locations_bubble_cave_boss,
|
|
||||||
**AquariaLocations.locations_naija_home,
|
|
||||||
**AquariaLocations.locations_mermog_cave,
|
|
||||||
**AquariaLocations.locations_mermog_boss,
|
|
||||||
**AquariaLocations.locations_octo_cave_t,
|
|
||||||
**AquariaLocations.locations_octo_cave_b,
|
|
||||||
**AquariaLocations.locations_sunken_city_l,
|
|
||||||
**AquariaLocations.locations_sunken_city_r,
|
|
||||||
**AquariaLocations.locations_sunken_city_boss,
|
|
||||||
**AquariaLocations.locations_sunken_city_l_bedroom,
|
|
||||||
**AquariaLocations.locations_simon,
|
|
||||||
**AquariaLocations.locations_whale,
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Fri, 15 Mar 2024 18:41:40 +0000
|
|
||||||
Description: Manage options in the Aquaria game multiworld randomizer
|
|
||||||
"""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from Options import Toggle, Choice, Range, DeathLink, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool
|
|
||||||
|
|
||||||
|
|
||||||
class IngredientRandomizer(Choice):
|
|
||||||
"""
|
|
||||||
Select if the simple ingredients (that do not have a recipe) should be randomized.
|
|
||||||
If "Common Ingredients" is selected, the randomization will exclude the "Red Bulb", "Special Bulb" and "Rukh Egg".
|
|
||||||
"""
|
|
||||||
display_name = "Randomize Ingredients"
|
|
||||||
option_off = 0
|
|
||||||
option_common_ingredients = 1
|
|
||||||
option_all_ingredients = 2
|
|
||||||
default = 0
|
|
||||||
|
|
||||||
|
|
||||||
class DishRandomizer(Toggle):
|
|
||||||
"""Randomize the drop of Dishes (Ingredients with recipe)."""
|
|
||||||
display_name = "Dish Randomizer"
|
|
||||||
|
|
||||||
|
|
||||||
class TurtleRandomizer(Choice):
|
|
||||||
"""Randomize the transportation turtle."""
|
|
||||||
display_name = "Turtle Randomizer"
|
|
||||||
option_none = 0
|
|
||||||
option_all = 1
|
|
||||||
option_all_except_final = 2
|
|
||||||
default = 2
|
|
||||||
|
|
||||||
|
|
||||||
class EarlyEnergyForm(DefaultOnToggle):
|
|
||||||
""" Force the Energy Form to be in a location early in the game """
|
|
||||||
display_name = "Early Energy Form"
|
|
||||||
|
|
||||||
|
|
||||||
class AquarianTranslation(Toggle):
|
|
||||||
"""Translate the Aquarian scripture in the game into English."""
|
|
||||||
display_name = "Translate Aquarian"
|
|
||||||
|
|
||||||
|
|
||||||
class BigBossesToBeat(Range):
|
|
||||||
"""
|
|
||||||
The number of big bosses to beat before having access to the creator (the final boss). The big bosses are
|
|
||||||
"Fallen God", "Mithalan God", "Drunian God", "Sun God" and "The Golem".
|
|
||||||
"""
|
|
||||||
display_name = "Big bosses to beat"
|
|
||||||
range_start = 0
|
|
||||||
range_end = 5
|
|
||||||
default = 0
|
|
||||||
|
|
||||||
|
|
||||||
class MiniBossesToBeat(Range):
|
|
||||||
"""
|
|
||||||
The number of minibosses to beat before having access to the creator (the final boss). The minibosses are
|
|
||||||
"Nautilus Prime", "Blaster Peg Prime", "Mergog", "Mithalan priests", "Octopus Prime", "Crabbius Maximus",
|
|
||||||
"Mantis Shrimp Prime" and "King Jellyfish God Prime".
|
|
||||||
Note that the Energy Statue and Simon Says are not minibosses.
|
|
||||||
"""
|
|
||||||
display_name = "Minibosses to beat"
|
|
||||||
range_start = 0
|
|
||||||
range_end = 8
|
|
||||||
default = 0
|
|
||||||
|
|
||||||
|
|
||||||
class Objective(Choice):
|
|
||||||
"""
|
|
||||||
The game objective can be to kill the creator or to kill the creator after obtaining all three secret memories.
|
|
||||||
"""
|
|
||||||
display_name = "Objective"
|
|
||||||
option_kill_the_creator = 0
|
|
||||||
option_obtain_secrets_and_kill_the_creator = 1
|
|
||||||
default = 0
|
|
||||||
|
|
||||||
|
|
||||||
class SkipFirstVision(Toggle):
|
|
||||||
"""
|
|
||||||
The first vision in the game, where Naija transforms into Energy Form and gets flooded by enemies, is quite cool but
|
|
||||||
can be quite long when you already know what is going on. This option can be used to skip this vision.
|
|
||||||
"""
|
|
||||||
display_name = "Skip Naija's first vision"
|
|
||||||
|
|
||||||
|
|
||||||
class NoProgressionHardOrHiddenLocation(Toggle):
|
|
||||||
"""
|
|
||||||
Make sure that there are no progression items at hard-to-reach or hard-to-find locations.
|
|
||||||
Those locations are very High locations (that need beast form, soup and skill to get),
|
|
||||||
every location in the bubble cave, locations where need you to cross a false wall without any indication,
|
|
||||||
the Arnassi race, bosses and minibosses. Useful for those that want a more casual run.
|
|
||||||
"""
|
|
||||||
display_name = "No progression in hard or hidden locations"
|
|
||||||
|
|
||||||
|
|
||||||
class LightNeededToGetToDarkPlaces(DefaultOnToggle):
|
|
||||||
"""
|
|
||||||
Make sure that the sun form or the dumbo pet can be acquired before getting to dark places.
|
|
||||||
Be aware that navigating in dark places without light is extremely difficult.
|
|
||||||
"""
|
|
||||||
display_name = "Light needed to get to dark places"
|
|
||||||
|
|
||||||
|
|
||||||
class BindSongNeededToGetUnderRockBulb(Toggle):
|
|
||||||
"""
|
|
||||||
Make sure that the bind song can be acquired before having to obtain sing bulbs under rocks.
|
|
||||||
"""
|
|
||||||
display_name = "Bind song needed to get sing bulbs under rocks"
|
|
||||||
|
|
||||||
|
|
||||||
class UnconfineHomeWater(Choice):
|
|
||||||
"""
|
|
||||||
Open the way out of the Home water area so that Naija can go to open water and beyond without the bind song.
|
|
||||||
"""
|
|
||||||
display_name = "Unconfine Home Water Area"
|
|
||||||
option_off = 0
|
|
||||||
option_via_energy_door = 1
|
|
||||||
option_via_transturtle = 2
|
|
||||||
option_via_both = 3
|
|
||||||
default = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AquariaOptions(PerGameCommonOptions):
|
|
||||||
"""
|
|
||||||
Every option in the Aquaria randomizer
|
|
||||||
"""
|
|
||||||
start_inventory_from_pool: StartInventoryPool
|
|
||||||
objective: Objective
|
|
||||||
mini_bosses_to_beat: MiniBossesToBeat
|
|
||||||
big_bosses_to_beat: BigBossesToBeat
|
|
||||||
turtle_randomizer: TurtleRandomizer
|
|
||||||
early_energy_form: EarlyEnergyForm
|
|
||||||
light_needed_to_get_to_dark_places: LightNeededToGetToDarkPlaces
|
|
||||||
bind_song_needed_to_get_under_rock_bulb: BindSongNeededToGetUnderRockBulb
|
|
||||||
unconfine_home_water: UnconfineHomeWater
|
|
||||||
no_progression_hard_or_hidden_locations: NoProgressionHardOrHiddenLocation
|
|
||||||
ingredient_randomizer: IngredientRandomizer
|
|
||||||
dish_randomizer: DishRandomizer
|
|
||||||
aquarian_translation: AquarianTranslation
|
|
||||||
skip_first_vision: SkipFirstVision
|
|
||||||
death_link: DeathLink
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,218 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Fri, 15 Mar 2024 18:41:40 +0000
|
|
||||||
Description: Main module for Aquaria game multiworld randomizer
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import List, Dict, ClassVar, Any
|
|
||||||
from worlds.AutoWorld import World, WebWorld
|
|
||||||
from BaseClasses import Tutorial, MultiWorld, ItemClassification
|
|
||||||
from .Items import item_table, AquariaItem, ItemType, ItemGroup
|
|
||||||
from .Locations import location_table
|
|
||||||
from .Options import AquariaOptions
|
|
||||||
from .Regions import AquariaRegions
|
|
||||||
|
|
||||||
|
|
||||||
class AquariaWeb(WebWorld):
|
|
||||||
"""
|
|
||||||
Class used to generate the Aquaria Game Web pages (setup, tutorial, etc.)
|
|
||||||
"""
|
|
||||||
theme = "ocean"
|
|
||||||
|
|
||||||
bug_report_page = "https://github.com/tioui/Aquaria_Randomizer/issues"
|
|
||||||
|
|
||||||
setup = Tutorial(
|
|
||||||
"Multiworld Setup Guide",
|
|
||||||
"A guide to setting up Aquaria for MultiWorld.",
|
|
||||||
"English",
|
|
||||||
"setup_en.md",
|
|
||||||
"setup/en",
|
|
||||||
["Tioui"]
|
|
||||||
)
|
|
||||||
|
|
||||||
setup_fr = Tutorial(
|
|
||||||
"Guide de configuration Multimonde",
|
|
||||||
"Un guide pour configurer Aquaria MultiWorld",
|
|
||||||
"Français",
|
|
||||||
"setup_fr.md",
|
|
||||||
"setup/fr",
|
|
||||||
["Tioui"]
|
|
||||||
)
|
|
||||||
|
|
||||||
tutorials = [setup, setup_fr]
|
|
||||||
|
|
||||||
|
|
||||||
class AquariaWorld(World):
|
|
||||||
"""
|
|
||||||
Aquaria is a side-scrolling action-adventure game. It follows Naija, an
|
|
||||||
aquatic humanoid woman, as she explores the underwater world of Aquaria.
|
|
||||||
Along her journey, she learns about the history of the world she inhabits
|
|
||||||
as well as her own past. The gameplay focuses on a combination of swimming,
|
|
||||||
singing, and combat, through which Naija can interact with the world. Her
|
|
||||||
songs can move items, affect plants and animals, and change her physical
|
|
||||||
appearance into other forms that have different abilities, like firing
|
|
||||||
projectiles at hostile creatures, or passing through barriers inaccessible
|
|
||||||
to her in her natural form.
|
|
||||||
From: https://en.wikipedia.org/wiki/Aquaria_(video_game)
|
|
||||||
"""
|
|
||||||
|
|
||||||
game: str = "Aquaria"
|
|
||||||
"The name of the game"
|
|
||||||
|
|
||||||
topology_present = True
|
|
||||||
"show path to required location checks in spoiler"
|
|
||||||
|
|
||||||
web: WebWorld = AquariaWeb()
|
|
||||||
"The web page generation informations"
|
|
||||||
|
|
||||||
item_name_to_id: ClassVar[Dict[str, int]] =\
|
|
||||||
{name: data.id for name, data in item_table.items()}
|
|
||||||
"The name and associated ID of each item of the world"
|
|
||||||
|
|
||||||
item_name_groups = {
|
|
||||||
"Damage": {"Energy form", "Nature form", "Beast form",
|
|
||||||
"Li and Li song", "Baby nautilus", "Baby piranha",
|
|
||||||
"Baby blaster"},
|
|
||||||
"Light": {"Sun form", "Baby dumbo"}
|
|
||||||
}
|
|
||||||
"""Grouping item make it easier to find them"""
|
|
||||||
|
|
||||||
location_name_to_id = location_table
|
|
||||||
"The name and associated ID of each location of the world"
|
|
||||||
|
|
||||||
base_id = 698000
|
|
||||||
"The starting ID of the items and locations of the world"
|
|
||||||
|
|
||||||
ingredients_substitution: List[int]
|
|
||||||
"Used to randomize ingredient drop"
|
|
||||||
|
|
||||||
options_dataclass = AquariaOptions
|
|
||||||
"Used to manage world options"
|
|
||||||
|
|
||||||
options: AquariaOptions
|
|
||||||
"Every options of the world"
|
|
||||||
|
|
||||||
regions: AquariaRegions
|
|
||||||
"Used to manage Regions"
|
|
||||||
|
|
||||||
exclude: List[str]
|
|
||||||
|
|
||||||
def __init__(self, multiworld: MultiWorld, player: int):
|
|
||||||
"""Initialisation of the Aquaria World"""
|
|
||||||
super(AquariaWorld, self).__init__(multiworld, player)
|
|
||||||
self.regions = AquariaRegions(multiworld, player)
|
|
||||||
self.ingredients_substitution = []
|
|
||||||
self.exclude = []
|
|
||||||
|
|
||||||
def create_regions(self) -> None:
|
|
||||||
"""
|
|
||||||
Create every Region in `regions`
|
|
||||||
"""
|
|
||||||
self.regions.add_regions_to_world()
|
|
||||||
self.regions.connect_regions()
|
|
||||||
self.regions.add_event_locations()
|
|
||||||
|
|
||||||
def create_item(self, name: str) -> AquariaItem:
|
|
||||||
"""
|
|
||||||
Create an AquariaItem using 'name' as item name.
|
|
||||||
"""
|
|
||||||
result: AquariaItem
|
|
||||||
try:
|
|
||||||
data = item_table[name]
|
|
||||||
classification: ItemClassification = ItemClassification.useful
|
|
||||||
if data.type == ItemType.JUNK:
|
|
||||||
classification = ItemClassification.filler
|
|
||||||
elif data.type == ItemType.PROGRESSION:
|
|
||||||
classification = ItemClassification.progression
|
|
||||||
result = AquariaItem(name, classification, data.id, self.player)
|
|
||||||
except BaseException:
|
|
||||||
raise Exception('The item ' + name + ' is not valid.')
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __pre_fill_item(self, item_name: str, location_name: str, precollected) -> None:
|
|
||||||
"""Pre-assign an item to a location"""
|
|
||||||
if item_name not in precollected:
|
|
||||||
self.exclude.append(item_name)
|
|
||||||
data = item_table[item_name]
|
|
||||||
item = AquariaItem(item_name, ItemClassification.useful, data.id, self.player)
|
|
||||||
self.multiworld.get_location(location_name, self.player).place_locked_item(item)
|
|
||||||
|
|
||||||
def get_filler_item_name(self):
|
|
||||||
"""Getting a random ingredient item as filler"""
|
|
||||||
ingredients = []
|
|
||||||
for name, data in item_table.items():
|
|
||||||
if data.group == ItemGroup.INGREDIENT:
|
|
||||||
ingredients.append(name)
|
|
||||||
filler_item_name = self.random.choice(ingredients)
|
|
||||||
return filler_item_name
|
|
||||||
|
|
||||||
def create_items(self) -> None:
|
|
||||||
"""Create every item in the world"""
|
|
||||||
precollected = [item.name for item in self.multiworld.precollected_items[self.player]]
|
|
||||||
if self.options.turtle_randomizer.value > 0:
|
|
||||||
if self.options.turtle_randomizer.value == 2:
|
|
||||||
self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected)
|
|
||||||
else:
|
|
||||||
self.__pre_fill_item("Transturtle Veil top left", "The veil top left area, Transturtle", precollected)
|
|
||||||
self.__pre_fill_item("Transturtle Veil top right", "The veil top right area, Transturtle", precollected)
|
|
||||||
self.__pre_fill_item("Transturtle Open Water top right", "Open water top right area, Transturtle",
|
|
||||||
precollected)
|
|
||||||
self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle",
|
|
||||||
precollected)
|
|
||||||
self.__pre_fill_item("Transturtle Home water", "Home water, Transturtle", precollected)
|
|
||||||
self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected)
|
|
||||||
self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected)
|
|
||||||
# The last two are inverted because in the original game, they are special turtle that communicate directly
|
|
||||||
self.__pre_fill_item("Transturtle Simon says", "Arnassi Ruins, Transturtle", precollected)
|
|
||||||
self.__pre_fill_item("Transturtle Arnassi ruins", "Simon says area, Transturtle", precollected)
|
|
||||||
for name, data in item_table.items():
|
|
||||||
if name in precollected:
|
|
||||||
precollected.remove(name)
|
|
||||||
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
|
|
||||||
else:
|
|
||||||
if name not in self.exclude:
|
|
||||||
for i in range(data.count):
|
|
||||||
item = self.create_item(name)
|
|
||||||
self.multiworld.itempool.append(item)
|
|
||||||
|
|
||||||
def set_rules(self) -> None:
|
|
||||||
"""
|
|
||||||
Launched when the Multiworld generator is ready to generate rules
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.regions.adjusting_rules(self.options)
|
|
||||||
self.multiworld.completion_condition[self.player] = lambda \
|
|
||||||
state: state.has("Victory", self.player)
|
|
||||||
|
|
||||||
def generate_basic(self) -> None:
|
|
||||||
"""
|
|
||||||
Player-specific randomization that does not affect logic.
|
|
||||||
Used to fill then `ingredients_substitution` list
|
|
||||||
"""
|
|
||||||
simple_ingredients_substitution = [i for i in range(27)]
|
|
||||||
if self.options.ingredient_randomizer.value > 0:
|
|
||||||
if self.options.ingredient_randomizer.value == 1:
|
|
||||||
simple_ingredients_substitution.pop(-1)
|
|
||||||
simple_ingredients_substitution.pop(-1)
|
|
||||||
simple_ingredients_substitution.pop(-1)
|
|
||||||
self.random.shuffle(simple_ingredients_substitution)
|
|
||||||
if self.options.ingredient_randomizer.value == 1:
|
|
||||||
simple_ingredients_substitution.extend([24, 25, 26])
|
|
||||||
dishes_substitution = [i for i in range(27, 76)]
|
|
||||||
if self.options.dish_randomizer:
|
|
||||||
self.random.shuffle(dishes_substitution)
|
|
||||||
self.ingredients_substitution.clear()
|
|
||||||
self.ingredients_substitution.extend(simple_ingredients_substitution)
|
|
||||||
self.ingredients_substitution.extend(dishes_substitution)
|
|
||||||
|
|
||||||
def fill_slot_data(self) -> Dict[str, Any]:
|
|
||||||
return {"ingredientReplacement": self.ingredients_substitution,
|
|
||||||
"aquarianTranslate": bool(self.options.aquarian_translation.value),
|
|
||||||
"secret_needed": self.options.objective.value > 0,
|
|
||||||
"minibosses_to_kill": self.options.mini_bosses_to_beat.value,
|
|
||||||
"bigbosses_to_kill": self.options.big_bosses_to_beat.value,
|
|
||||||
"skip_first_vision": bool(self.options.skip_first_vision.value),
|
|
||||||
"unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3],
|
|
||||||
"unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3],
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
# Aquaria
|
|
||||||
|
|
||||||
## Game page in other languages:
|
|
||||||
* [Français](/games/Aquaria/info/fr)
|
|
||||||
|
|
||||||
## Where is the options page?
|
|
||||||
|
|
||||||
The player options page for this game contains all the options you need to configure and export a config file. Player
|
|
||||||
options page link: [Aquaria Player Options Page](../player-options).
|
|
||||||
|
|
||||||
## What does randomization do to this game?
|
|
||||||
The locations in the randomizer are:
|
|
||||||
|
|
||||||
- All sing bulbs
|
|
||||||
- All Mithalas Urns
|
|
||||||
- All Sunken City crates
|
|
||||||
- Collectible treasure locations (including pet eggs and costumes)
|
|
||||||
- Beating Simon says
|
|
||||||
- Li cave
|
|
||||||
- Every Transportation Turtle (also called transturtle)
|
|
||||||
- Locations where you get songs:
|
|
||||||
* Erulian spirit cristal
|
|
||||||
* Energy status mini-boss
|
|
||||||
* Beating Mithalan God boss
|
|
||||||
* Fish cave puzzle
|
|
||||||
* Beating Drunian God boss
|
|
||||||
* Beating Sun God boss
|
|
||||||
* Breaking Li cage in the body
|
|
||||||
|
|
||||||
Note that, unlike the vanilla game, when opening sing bulbs, Mithalas urns and Sunken City crates,
|
|
||||||
nothing will come out of them. The moment those bulbs, urns and crates are opened, the location is considered checked.
|
|
||||||
|
|
||||||
The items in the randomizer are:
|
|
||||||
- Dishes (used to learn recipes)<sup>*</sup>
|
|
||||||
- Some ingredients
|
|
||||||
- The Wok (third plate used to cook 3-ingredient recipes everywhere)
|
|
||||||
- All collectible treasure (including pet eggs and costumes)
|
|
||||||
- Li and Li's song
|
|
||||||
- All songs (other than Li's song since it is learned when Li is obtained)
|
|
||||||
- Transportation to transturtles
|
|
||||||
|
|
||||||
Also, there is the option to randomize every ingredient drops (from fishes, monsters
|
|
||||||
or plants).
|
|
||||||
|
|
||||||
<sup>*</sup> Note that, unlike in the vanilla game, the recipes for dishes (other than the Sea Loaf)
|
|
||||||
cannot be cooked (or learned) before being obtained as randomized items. Also, enemies and plants
|
|
||||||
that drop dishes that have not been learned before will drop ingredients of this dish instead.
|
|
||||||
|
|
||||||
## What is the goal of the game?
|
|
||||||
The goal of the Aquaria game is to beat the creator. You can also add other goals like getting
|
|
||||||
secret memories, beating a number of mini-bosses and beating a number of bosses.
|
|
||||||
|
|
||||||
## Which items can be in another player's world?
|
|
||||||
Any items specified above can be in another player's world.
|
|
||||||
|
|
||||||
## What does another world's item look like in Aquaria?
|
|
||||||
No visuals are shown when finding locations other than collectible treasure.
|
|
||||||
For those treasures, the visual of the treasure is visually unchanged.
|
|
||||||
After collecting a location check, a message will be shown to inform the player
|
|
||||||
what has been collected and who will receive it.
|
|
||||||
|
|
||||||
## When the player receives an item, what happens?
|
|
||||||
When you receive an item, a message will pop up to inform you where you received
|
|
||||||
the item from and which one it was.
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# Aquaria
|
|
||||||
|
|
||||||
## Où se trouve la page des options ?
|
|
||||||
|
|
||||||
La [page des options du joueur pour ce jeu](../player-options) contient tous
|
|
||||||
les options dont vous avez besoin pour configurer et exporter le fichier.
|
|
||||||
|
|
||||||
## Quel est l'effet de la randomisation sur ce jeu ?
|
|
||||||
|
|
||||||
Les localisations du "Ransomizer" sont:
|
|
||||||
|
|
||||||
- tous les bulbes musicaux;
|
|
||||||
- toutes les urnes de Mithalas;
|
|
||||||
- toutes les caisses de la cité engloutie;
|
|
||||||
- les localisations des trésors de collections (incluant les oeufs d'animaux de compagnie et les costumes);
|
|
||||||
- Battre Simom dit;
|
|
||||||
- La caverne de Li;
|
|
||||||
- Les tortues de transportation (transturtle);
|
|
||||||
- Localisation ou on obtient normalement les musiques,
|
|
||||||
* cristal de l'esprit Erulien,
|
|
||||||
* le mini-boss de la statue de l'énergie,
|
|
||||||
* battre le dieu de Mithalas,
|
|
||||||
* résoudre l'énigme de la caverne des poissons,
|
|
||||||
* battre le dieu Drunien,
|
|
||||||
* battre le dieu du soleil,
|
|
||||||
* détruire la cage de Li dans le corps,
|
|
||||||
|
|
||||||
À noter que, contrairement au jeu original, lors de l'ouverture d'un bulbe musical, d'une urne de Mithalas ou
|
|
||||||
d'une caisse de la cité engloutie, aucun objet n'en sortira. La localisation représentée par l'objet ouvert est reçue
|
|
||||||
dès l'ouverture.
|
|
||||||
|
|
||||||
Les objets pouvant être obtenus sont:
|
|
||||||
- les recettes (permettant d'apprendre les recettes*);
|
|
||||||
- certains ingrédients;
|
|
||||||
- le Wok (la troisième assiette permettant de cuisiner avec trois ingrédients n'importe où);
|
|
||||||
- Tous les trésors de collection (incluant les oeufs d'animal de compagnie et les costumes);
|
|
||||||
- Li et la musique de Li;
|
|
||||||
- Toutes les musiques (autre que la musique de Li puisque cette dernière est apprise en obtenant Li);
|
|
||||||
- Les localisations de transportation.
|
|
||||||
|
|
||||||
Il y a également l'option pour mélanger les ingrédients obtenus en éliminant des monstres, des poissons ou des plantes.
|
|
||||||
|
|
||||||
*À noter que, contrairement au jeu original, il est impossible de cuisiner une recette qui n'a pas préalablement
|
|
||||||
été apprise en obtenant un repas en tant qu'objet. À noter également que les ennemies et plantes qui
|
|
||||||
donnent un repas dont la recette n'a pas préalablement été apprise vont donner les ingrédients de cette
|
|
||||||
recette.
|
|
||||||
|
|
||||||
## Quel est le but de Aquaria ?
|
|
||||||
|
|
||||||
Dans Aquaria, le but est de battre le monstre final (le créateur). Il est également possible d'ajouter
|
|
||||||
des buts comme obtenir les trois souvenirs secrets, ou devoir battre une quantité de boss ou de mini-boss.
|
|
||||||
|
|
||||||
## Quels objets peuvent se trouver dans le monde d'un autre joueur ?
|
|
||||||
|
|
||||||
Tous les objets indiqués plus haut peuvent être obtenus à partir du monde d'un autre joueur.
|
|
||||||
|
|
||||||
## À quoi ressemble un objet d'un autre monde dans ce jeu
|
|
||||||
|
|
||||||
Autre que pour les trésors de collection (dont le visuel demeure inchangé),
|
|
||||||
les autres localisations n'ont aucun visuel. Lorsqu'une localisation randomisée est obtenue,
|
|
||||||
un message est affiché à l'écran pour indiquer quel objet a été trouvé et pour quel joueur.
|
|
||||||
|
|
||||||
## Que se passe-t-il lorsque le joueur reçoit un objet ?
|
|
||||||
|
|
||||||
Chaque fois qu'un objet est reçu, un message apparaît à l'écran pour en informer le joueur.
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
# Aquaria Randomizer Setup Guide
|
|
||||||
|
|
||||||
## Required Software
|
|
||||||
|
|
||||||
- The original Aquaria Game (purchasable from most online game stores)
|
|
||||||
- The [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/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
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
|
|
||||||
the original game will stop working. Copying the folder will guarantee that the original game keeps on working.
|
|
||||||
Also, in Windows, the save files are stored in the Aquaria folder. So copying the Aquaria folder for every Multiworld
|
|
||||||
game you play will make sure that every game has its own save game.
|
|
||||||
|
|
||||||
Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files are:
|
|
||||||
- aquaria_randomizer.exe
|
|
||||||
- OpenAL32.dll
|
|
||||||
- override (directory)
|
|
||||||
- SDL2.dll
|
|
||||||
- usersettings.xml
|
|
||||||
- wrap_oal.dll
|
|
||||||
- cacert.pem
|
|
||||||
|
|
||||||
If there is a conflict between files in the original game folder and the unzipped files, you should overwrite
|
|
||||||
the original files with the ones from the unzipped randomizer.
|
|
||||||
|
|
||||||
Finally, to launch the randomizer, you must use the command line interface (you can open the command line interface
|
|
||||||
by typing `cmd` in the address bar of the Windows File Explorer). Here is the command line used to start the
|
|
||||||
randomizer:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aquaria_randomizer.exe --name YourName --server theServer:thePort
|
|
||||||
```
|
|
||||||
|
|
||||||
or, if the room has a password:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aquaria_randomizer.exe --name YourName --server theServer:thePort --password thePassword
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux when using the AppImage
|
|
||||||
|
|
||||||
If you use the AppImage, just copy it into the Aquaria game folder. You then have to make it executable. You
|
|
||||||
can do that from command line by using:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x Aquaria_Randomizer-*.AppImage
|
|
||||||
```
|
|
||||||
|
|
||||||
or by using the Graphical Explorer of your system.
|
|
||||||
|
|
||||||
To launch the randomizer, just launch in command line:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort
|
|
||||||
```
|
|
||||||
|
|
||||||
or, if the room has a password:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort --password thePassword
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that you should not have multiple Aquaria_Randomizer AppImage file in the same folder. If this situation occurs,
|
|
||||||
the preceding commands will launch the game multiple times.
|
|
||||||
|
|
||||||
### Linux when using the tar file
|
|
||||||
|
|
||||||
First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
|
|
||||||
the original game will stop working. Copying the folder will guarantee that the original game keeps on working.
|
|
||||||
|
|
||||||
Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted files are:
|
|
||||||
- aquaria_randomizer
|
|
||||||
- override (directory)
|
|
||||||
- usersettings.xml
|
|
||||||
- cacert.pem
|
|
||||||
|
|
||||||
If there is a conflict between files in the original game folder and the extracted files, you should overwrite
|
|
||||||
the original files with the ones from the extracted randomizer files.
|
|
||||||
|
|
||||||
Then, you should use your system package manager to install `liblua5`, `libogg`, `libvorbis`, `libopenal` and `libsdl2`.
|
|
||||||
On Debian base system (like Ubuntu), you can use the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Also, if there are certain `.so` files in the original Aquaria game folder (`libgcc_s.so.1`, `libopenal.so.1`,
|
|
||||||
`libSDL-1.2.so.0` and `libstdc++.so.6`), you should remove them from the Aquaria Randomizer game folder. Those are
|
|
||||||
old libraries that will not work on the recent build of the randomizer.
|
|
||||||
|
|
||||||
To launch the randomizer, just launch in command line:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./aquaria_randomizer --name YourName --server theServer:thePort
|
|
||||||
```
|
|
||||||
|
|
||||||
or, if the room has a password:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./aquaria_randomizer --name YourName --server theServer:thePort --password thePassword
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: If you get a permission denied error when using the command line, you can use this command to be
|
|
||||||
sure that your executable has executable permission:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x aquaria_randomizer
|
|
||||||
```
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
# Guide de configuration MultiWorld d'Aquaria
|
|
||||||
|
|
||||||
## Logiciels nécessaires
|
|
||||||
|
|
||||||
- Le jeu Aquaria original (trouvable sur la majorité des sites de ventes de jeux vidéo en ligne)
|
|
||||||
- Le client Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
|
|
||||||
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
|
||||||
|
|
||||||
## Procédures d'installation et d'exécution
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
En premier lieu, vous devriez effectuer une nouvelle copie du jeu d'Aquaria original à chaque fois que vous effectuez une
|
|
||||||
nouvelle partie. La première raison de cette copie est que le randomizer modifie des fichiers qui rendront possiblement
|
|
||||||
le jeu original non fonctionnel. La seconde raison d'effectuer cette copie est que les sauvegardes sont créées
|
|
||||||
directement dans le répertoire du jeu. Donc, la copie permet d'éviter de perdre vos sauvegardes du jeu d'origine ou
|
|
||||||
encore de charger une sauvegarde d'une ancienne partie de multiworld (ce qui pourrait avoir comme conséquence de briser
|
|
||||||
la logique du multiworld).
|
|
||||||
|
|
||||||
Désarchiver le randomizer d'Aquaria et copier tous les fichiers de l'archive dans le répertoire du jeu d'Aquaria. Le
|
|
||||||
fichier d'archive devrait contenir les fichiers suivants:
|
|
||||||
- aquaria_randomizer.exe
|
|
||||||
- OpenAL32.dll
|
|
||||||
- override (directory)
|
|
||||||
- SDL2.dll
|
|
||||||
- usersettings.xml
|
|
||||||
- wrap_oal.dll
|
|
||||||
- cacert.pem
|
|
||||||
|
|
||||||
S'il y a des conflits entre les fichiers de l'archive zip et les fichiers du jeu original, vous devez utiliser
|
|
||||||
les fichiers contenus dans l'archive zip.
|
|
||||||
|
|
||||||
Finalement, pour lancer le randomizer, vous devez utiliser la ligne de commande (vous pouvez ouvrir une interface de
|
|
||||||
ligne de commande, entrez l'adresse `cmd` dans la barre d'adresse de l'explorateur de fichier de Windows). Voici
|
|
||||||
la ligne de commande à utiliser pour lancer le randomizer:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aquaria_randomizer.exe --name VotreNom --server leServeur:LePort
|
|
||||||
```
|
|
||||||
|
|
||||||
ou, si vous devez entrer un mot de passe:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aquaria_randomizer.exe --name VotreNom --server leServeur:LePort --password leMotDePasse
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux avec le fichier AppImage
|
|
||||||
|
|
||||||
Si vous utilisez le fichier AppImage, copiez le fichier dans le répertoire du jeu d'Aquaria. Ensuite, assurez-vous de
|
|
||||||
le mettre exécutable. Vous pouvez mettre le fichier exécutable avec la commande suivante:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x Aquaria_Randomizer-*.AppImage
|
|
||||||
```
|
|
||||||
|
|
||||||
ou bien en utilisant l'explorateur graphique de votre système.
|
|
||||||
|
|
||||||
Pour lancer le randomizer, utiliser la commande suivante:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort
|
|
||||||
```
|
|
||||||
|
|
||||||
Si vous devez entrer un mot de passe:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort --password LeMotDePasse
|
|
||||||
```
|
|
||||||
|
|
||||||
À noter que vous ne devez pas avoir plusieurs fichiers AppImage différents dans le même répertoire. Si cette situation
|
|
||||||
survient, le jeu sera lancé plusieurs fois.
|
|
||||||
|
|
||||||
### Linux avec le fichier tar
|
|
||||||
|
|
||||||
En premier lieu, assurez-vous de faire une copie du répertoire du jeu d'origine d'Aquaria. Les fichiers contenus
|
|
||||||
dans le randomizer auront comme impact de rendre le jeu d'origine non fonctionnel. Donc, effectuer la copie du jeu
|
|
||||||
avant de déposer le randomizer à l'intérieur permet de vous assurer de garder une version du jeu d'origine fonctionnel.
|
|
||||||
|
|
||||||
Désarchiver le fichier tar et copier tous les fichiers qu'il contient dans le répertoire du jeu d'origine d'Aquaria. Les
|
|
||||||
fichiers extraient du fichier tar devraient être les suivants:
|
|
||||||
- aquaria_randomizer
|
|
||||||
- override (directory)
|
|
||||||
- usersettings.xml
|
|
||||||
- cacert.pem
|
|
||||||
|
|
||||||
S'il y a des conflits entre les fichiers de l'archive tar et les fichiers du jeu original, vous devez utiliser
|
|
||||||
les fichiers contenus dans l'archive tar.
|
|
||||||
|
|
||||||
Ensuite, vous devez installer manuellement les librairies dont dépend le jeu: liblua5, libogg, libvorbis, libopenal and
|
|
||||||
libsdl2. Vous pouvez utiliser le système de "package" de votre système pour les installer. Voici un exemple avec
|
|
||||||
Debian (et Ubuntu):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Notez également que s'il y a des fichiers ".so" dans le répertoire d'Aquaria (`libgcc_s.so.1`, `libopenal.so.1`,
|
|
||||||
`libSDL-1.2.so.0` and `libstdc++.so.6`), vous devriez les retirer. Il s'agit de vieille version des librairies qui
|
|
||||||
ne sont plus fonctionnelles dans les systèmes modernes et qui pourrait empêcher le randomizer de fonctionner.
|
|
||||||
|
|
||||||
Pour lancer le randomizer, utiliser la commande suivante:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./aquaria_randomizer --name VotreNom --server LeServeur:LePort
|
|
||||||
```
|
|
||||||
|
|
||||||
Si vous devez entrer un mot de passe:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./aquaria_randomizer --name VotreNom --server LeServeur:LePort --password LeMotDePasse
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: Si vous avez une erreur de permission lors de l'exécution du randomizer, vous pouvez utiliser cette commande
|
|
||||||
pour vous assurer que votre fichier est exécutable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x aquaria_randomizer
|
|
||||||
```
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Base class for the Aquaria randomizer unit tests
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
from test.bases import WorldTestBase
|
|
||||||
|
|
||||||
# Every location accessible after the home water.
|
|
||||||
after_home_water_locations = [
|
|
||||||
"Sun Crystal",
|
|
||||||
"Home water, Transturtle",
|
|
||||||
"Open water top left area, bulb under the rock in the right path",
|
|
||||||
"Open water top left area, bulb under the rock in the left path",
|
|
||||||
"Open water top left area, bulb to the right of the save cristal",
|
|
||||||
"Open water top right area, bulb in the small path before Mithalas",
|
|
||||||
"Open water top right area, bulb in the path from the left entrance",
|
|
||||||
"Open water top right area, bulb in the clearing close to the bottom exit",
|
|
||||||
"Open water top right area, bulb in the big clearing close to the save cristal",
|
|
||||||
"Open water top right area, bulb in the big clearing to the top exit",
|
|
||||||
"Open water top right area, first urn in the Mithalas exit",
|
|
||||||
"Open water top right area, second urn in the Mithalas exit",
|
|
||||||
"Open water top right area, third urn in the Mithalas exit",
|
|
||||||
"Open water top right area, bulb in the turtle room",
|
|
||||||
"Open water top right area, Transturtle",
|
|
||||||
"Open water bottom left area, bulb behind the chomper fish",
|
|
||||||
"Open water bottom left area, bulb inside the lowest fish pass",
|
|
||||||
"Open water skeleton path, bulb close to the right exit",
|
|
||||||
"Open water skeleton path, bulb behind the chomper fish",
|
|
||||||
"Open water skeleton path, King skull",
|
|
||||||
"Arnassi Ruins, bulb in the right part",
|
|
||||||
"Arnassi Ruins, bulb in the left part",
|
|
||||||
"Arnassi Ruins, bulb in the center part",
|
|
||||||
"Arnassi ruins, Song plant spore on the top of the ruins",
|
|
||||||
"Arnassi ruins, Arnassi Armor",
|
|
||||||
"Arnassi Ruins, Arnassi statue",
|
|
||||||
"Arnassi Ruins, Transturtle",
|
|
||||||
"Arnassi ruins, Crab armor",
|
|
||||||
"Simon says area, Transturtle",
|
|
||||||
"Mithalas city, first bulb in the left city part",
|
|
||||||
"Mithalas city, second bulb in the left city part",
|
|
||||||
"Mithalas city, bulb in the right part",
|
|
||||||
"Mithalas city, bulb at the top of the city",
|
|
||||||
"Mithalas city, first bulb in a broken home",
|
|
||||||
"Mithalas city, second bulb in a broken home",
|
|
||||||
"Mithalas city, bulb in the bottom left part",
|
|
||||||
"Mithalas city, first bulb in one of the homes",
|
|
||||||
"Mithalas city, second bulb in one of the homes",
|
|
||||||
"Mithalas city, first urn in one of the homes",
|
|
||||||
"Mithalas city, second urn in one of the homes",
|
|
||||||
"Mithalas city, first urn in the city reserve",
|
|
||||||
"Mithalas city, second urn in the city reserve",
|
|
||||||
"Mithalas city, third urn in the city reserve",
|
|
||||||
"Mithalas city, first bulb at the end of the top path",
|
|
||||||
"Mithalas city, second bulb at the end of the top path",
|
|
||||||
"Mithalas city, bulb in the top path",
|
|
||||||
"Mithalas city, Mithalas pot",
|
|
||||||
"Mithalas city, urn in the cathedral flower tube entrance",
|
|
||||||
"Mithalas city, Doll",
|
|
||||||
"Mithalas city, urn inside a home fish pass",
|
|
||||||
"Mithalas city castle, bulb in the flesh hole",
|
|
||||||
"Mithalas city castle, Blue banner",
|
|
||||||
"Mithalas city castle, urn in the bedroom",
|
|
||||||
"Mithalas city castle, first urn of the single lamp path",
|
|
||||||
"Mithalas city castle, second urn of the single lamp path",
|
|
||||||
"Mithalas city castle, urn in the bottom room",
|
|
||||||
"Mithalas city castle, first urn on the entrance path",
|
|
||||||
"Mithalas city castle, second urn on the entrance path",
|
|
||||||
"Mithalas castle, beating the priests",
|
|
||||||
"Mithalas city castle, Trident head",
|
|
||||||
"Mithalas cathedral, first urn in the top right room",
|
|
||||||
"Mithalas cathedral, second urn in the top right room",
|
|
||||||
"Mithalas cathedral, third urn in the top right room",
|
|
||||||
"Mithalas cathedral, urn in the flesh room with fleas",
|
|
||||||
"Mithalas cathedral, first urn in the bottom right path",
|
|
||||||
"Mithalas cathedral, second urn in the bottom right path",
|
|
||||||
"Mithalas cathedral, urn behind the flesh vein",
|
|
||||||
"Mithalas cathedral, urn in the top left eyes boss room",
|
|
||||||
"Mithalas cathedral, first urn in the path behind the flesh vein",
|
|
||||||
"Mithalas cathedral, second urn in the path behind the flesh vein",
|
|
||||||
"Mithalas cathedral, third urn in the path behind the flesh vein",
|
|
||||||
"Mithalas cathedral, one of the urns in the top right room",
|
|
||||||
"Mithalas cathedral, Mithalan Dress",
|
|
||||||
"Mithalas cathedral right area, urn below the left entrance",
|
|
||||||
"Cathedral underground, bulb in the center part",
|
|
||||||
"Cathedral underground, first bulb in the top left part",
|
|
||||||
"Cathedral underground, second bulb in the top left part",
|
|
||||||
"Cathedral underground, third bulb in the top left part",
|
|
||||||
"Cathedral underground, bulb close to the save cristal",
|
|
||||||
"Cathedral underground, bulb in the bottom right path",
|
|
||||||
"Cathedral boss area, beating Mithalan God",
|
|
||||||
"Kelp Forest top left area, bulb in the bottom left clearing",
|
|
||||||
"Kelp Forest top left area, bulb in the path down from the top left clearing",
|
|
||||||
"Kelp Forest top left area, bulb in the top left clearing",
|
|
||||||
"Kelp Forest top left, Jelly Egg",
|
|
||||||
"Kelp Forest top left area, bulb close to the Verse egg",
|
|
||||||
"Kelp forest top left area, Verse egg",
|
|
||||||
"Kelp Forest top right area, bulb under the rock in the right path",
|
|
||||||
"Kelp Forest top right area, bulb at the left of the center clearing",
|
|
||||||
"Kelp Forest top right area, bulb in the left path's big room",
|
|
||||||
"Kelp Forest top right area, bulb in the left path's small room",
|
|
||||||
"Kelp Forest top right area, bulb at the top of the center clearing",
|
|
||||||
"Kelp forest top right area, Black pearl",
|
|
||||||
"Kelp Forest top right area, bulb in the top fish pass",
|
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
|
||||||
"Kelp forest bottom left area, Walker baby",
|
|
||||||
"Kelp Forest bottom left area, Transturtle",
|
|
||||||
"Kelp forest bottom right area, Odd Container",
|
|
||||||
"Kelp forest boss area, beating Drunian God",
|
|
||||||
"Kelp Forest boss room, bulb at the bottom of the area",
|
|
||||||
"Kelp Forest bottom left area, Fish cave puzzle",
|
|
||||||
"Kelp Forest sprite cave, bulb inside the fish pass",
|
|
||||||
"Kelp Forest sprite cave, bulb in the second room",
|
|
||||||
"Kelp Forest Sprite Cave, Seed bag",
|
|
||||||
"Mermog cave, bulb in the left part of the cave",
|
|
||||||
"Mermog cave, Piranha Egg",
|
|
||||||
"The veil top left area, In the Li cave",
|
|
||||||
"The veil top left area, bulb under the rock in the top right path",
|
|
||||||
"The veil top left area, bulb hidden behind the blocking rock",
|
|
||||||
"The veil top left area, Transturtle",
|
|
||||||
"The veil top left area, bulb inside the fish pass",
|
|
||||||
"Turtle cave, Turtle Egg",
|
|
||||||
"Turtle cave, bulb in bubble cliff",
|
|
||||||
"Turtle cave, Urchin costume",
|
|
||||||
"The veil top right area, bulb in the middle of the wall jump cliff",
|
|
||||||
"The veil top right area, golden starfish at the bottom right of the bottom path",
|
|
||||||
"The veil top right area, bulb in the top of the water fall",
|
|
||||||
"The veil top right area, Transturtle",
|
|
||||||
"The veil bottom area, bulb in the left path",
|
|
||||||
"The veil bottom area, bulb in the spirit path",
|
|
||||||
"The veil bottom area, Verse egg",
|
|
||||||
"The veil bottom area, Stone Head",
|
|
||||||
"Octopus cave, Dumbo Egg",
|
|
||||||
"Octopus cave, bulb in the path below the octopus cave path",
|
|
||||||
"Bubble cave, bulb in the left cave wall",
|
|
||||||
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
|
|
||||||
"Bubble cave, Verse egg",
|
|
||||||
"Sun temple, bulb in the top left part",
|
|
||||||
"Sun temple, bulb in the top right part",
|
|
||||||
"Sun temple, bulb at the top of the high dark room",
|
|
||||||
"Sun temple, Golden Gear",
|
|
||||||
"Sun temple, first bulb of the temple",
|
|
||||||
"Sun temple, bulb on the left part",
|
|
||||||
"Sun temple, bulb in the hidden room of the right part",
|
|
||||||
"Sun temple, Sun key",
|
|
||||||
"Sun Worm path, first path bulb",
|
|
||||||
"Sun Worm path, second path bulb",
|
|
||||||
"Sun Worm path, first cliff bulb",
|
|
||||||
"Sun Worm path, second cliff bulb",
|
|
||||||
"Sun temple boss area, beating Sun God",
|
|
||||||
"Abyss left area, bulb in hidden path room",
|
|
||||||
"Abyss left area, bulb in the right part",
|
|
||||||
"Abyss left area, Glowing seed",
|
|
||||||
"Abyss left area, Glowing Plant",
|
|
||||||
"Abyss left area, bulb in the bottom fish pass",
|
|
||||||
"Abyss right area, bulb behind the rock in the whale room",
|
|
||||||
"Abyss right area, bulb in the middle path",
|
|
||||||
"Abyss right area, bulb behind the rock in the middle path",
|
|
||||||
"Abyss right area, bulb in the left green room",
|
|
||||||
"Abyss right area, Transturtle",
|
|
||||||
"Ice cave, bulb in the room to the right",
|
|
||||||
"Ice cave, First bulbs in the top exit room",
|
|
||||||
"Ice cave, Second bulbs in the top exit room",
|
|
||||||
"Ice cave, third bulbs in the top exit room",
|
|
||||||
"Ice cave, bulb in the left room",
|
|
||||||
"King Jellyfish cave, bulb in the right path from King Jelly",
|
|
||||||
"King Jellyfish cave, Jellyfish Costume",
|
|
||||||
"The whale, Verse egg",
|
|
||||||
"Sunken city right area, crate close to the save cristal",
|
|
||||||
"Sunken city right area, crate in the left bottom room",
|
|
||||||
"Sunken city left area, crate in the little pipe room",
|
|
||||||
"Sunken city left area, crate close to the save cristal",
|
|
||||||
"Sunken city left area, crate before the bedroom",
|
|
||||||
"Sunken city left area, Girl Costume",
|
|
||||||
"Sunken city, bulb on the top of the boss area (boiler room)",
|
|
||||||
"The body center area, breaking li cage",
|
|
||||||
"The body main area, bulb on the main path blocking tube",
|
|
||||||
"The body left area, first bulb in the top face room",
|
|
||||||
"The body left area, second bulb in the top face room",
|
|
||||||
"The body left area, bulb below the water stream",
|
|
||||||
"The body left area, bulb in the top path to the top face room",
|
|
||||||
"The body left area, bulb in the bottom face room",
|
|
||||||
"The body right area, bulb in the top face room",
|
|
||||||
"The body right area, bulb in the top path to the bottom face room",
|
|
||||||
"The body right area, bulb in the bottom face room",
|
|
||||||
"The body bottom area, bulb in the Jelly Zap room",
|
|
||||||
"The body bottom area, bulb in the nautilus room",
|
|
||||||
"The body bottom area, Mutant Costume",
|
|
||||||
"Final boss area, first bulb in the turtle room",
|
|
||||||
"Final boss area, second bulbs in the turtle room",
|
|
||||||
"Final boss area, third bulbs in the turtle room",
|
|
||||||
"Final boss area, Transturtle",
|
|
||||||
"Final boss area, bulb in the boss third form room",
|
|
||||||
"Kelp forest, beating Simon says",
|
|
||||||
"Beating Fallen God",
|
|
||||||
"Beating Mithalan God",
|
|
||||||
"Beating Drunian God",
|
|
||||||
"Beating Sun God",
|
|
||||||
"Beating the Golem",
|
|
||||||
"Beating Nautilus Prime",
|
|
||||||
"Beating Blaster Peg Prime",
|
|
||||||
"Beating Mergog",
|
|
||||||
"Beating Mithalan priests",
|
|
||||||
"Beating Octopus Prime",
|
|
||||||
"Beating Crabbius Maximus",
|
|
||||||
"Beating Mantis Shrimp Prime",
|
|
||||||
"Beating King Jellyfish God Prime",
|
|
||||||
"First secret",
|
|
||||||
"Second secret",
|
|
||||||
"Third secret",
|
|
||||||
"Sunken City cleared",
|
|
||||||
"Objective complete",
|
|
||||||
]
|
|
||||||
|
|
||||||
class AquariaTestBase(WorldTestBase):
|
|
||||||
"""Base class for Aquaria unit tests"""
|
|
||||||
game = "Aquaria"
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without the beast form
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class BeastFormAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without the beast form"""
|
|
||||||
|
|
||||||
def test_beast_form_location(self) -> None:
|
|
||||||
"""Test locations that require beast form"""
|
|
||||||
locations = [
|
|
||||||
"Mithalas castle, beating the priests",
|
|
||||||
"Arnassi ruins, Crab armor",
|
|
||||||
"Arnassi ruins, Song plant spore on the top of the ruins",
|
|
||||||
"Mithalas city, first bulb at the end of the top path",
|
|
||||||
"Mithalas city, second bulb at the end of the top path",
|
|
||||||
"Mithalas city, bulb in the top path",
|
|
||||||
"Mithalas city, Mithalas pot",
|
|
||||||
"Mithalas city, urn in the cathedral flower tube entrance",
|
|
||||||
"Mermog cave, Piranha Egg",
|
|
||||||
"Mithalas cathedral, Mithalan Dress",
|
|
||||||
"Turtle cave, bulb in bubble cliff",
|
|
||||||
"Turtle cave, Urchin costume",
|
|
||||||
"Sun Worm path, first cliff bulb",
|
|
||||||
"Sun Worm path, second cliff bulb",
|
|
||||||
"The veil top right area, bulb in the top of the water fall",
|
|
||||||
"Bubble cave, bulb in the left cave wall",
|
|
||||||
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
|
|
||||||
"Bubble cave, Verse egg",
|
|
||||||
"Sunken city, bulb on the top of the boss area (boiler room)",
|
|
||||||
"Octopus cave, Dumbo Egg",
|
|
||||||
"Beating the Golem",
|
|
||||||
"Beating Mergog",
|
|
||||||
"Beating Crabbius Maximus",
|
|
||||||
"Beating Octopus Prime",
|
|
||||||
"Beating Mantis Shrimp Prime",
|
|
||||||
"King Jellyfish cave, Jellyfish Costume",
|
|
||||||
"King Jellyfish cave, bulb in the right path from King Jelly",
|
|
||||||
"Beating King Jellyfish God Prime",
|
|
||||||
"Beating Mithalan priests",
|
|
||||||
"Sunken City cleared"
|
|
||||||
]
|
|
||||||
items = [["Beast form"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without the bind song (without the location
|
|
||||||
under rock needing bind song option)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase, after_home_water_locations
|
|
||||||
|
|
||||||
|
|
||||||
class BindSongAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without the bind song"""
|
|
||||||
options = {
|
|
||||||
"bind_song_needed_to_get_under_rock_bulb": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_bind_song_location(self) -> None:
|
|
||||||
"""Test locations that require Bind song"""
|
|
||||||
locations = [
|
|
||||||
"Verse cave right area, Big Seed",
|
|
||||||
"Home water, bulb in the path below Nautilus Prime",
|
|
||||||
"Home water, bulb in the bottom left room",
|
|
||||||
"Home water, Nautilus Egg",
|
|
||||||
"Song cave, Verse egg",
|
|
||||||
"Energy temple first area, beating the energy statue",
|
|
||||||
"Energy temple first area, bulb in the bottom room blocked by a rock",
|
|
||||||
"Energy temple first area, Energy Idol",
|
|
||||||
"Energy temple second area, bulb under the rock",
|
|
||||||
"Energy temple bottom entrance, Krotite armor",
|
|
||||||
"Energy temple third area, bulb in the bottom path",
|
|
||||||
"Energy temple boss area, Fallen god tooth",
|
|
||||||
"Energy temple blaster room, Blaster egg",
|
|
||||||
*after_home_water_locations
|
|
||||||
]
|
|
||||||
items = [["Bind song"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without the bind song (with the location
|
|
||||||
under rock needing bind song option)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
from worlds.aquaria.test.test_bind_song_access import after_home_water_locations
|
|
||||||
|
|
||||||
|
|
||||||
class BindSongOptionAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without the bind song"""
|
|
||||||
options = {
|
|
||||||
"bind_song_needed_to_get_under_rock_bulb": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_bind_song_location(self) -> None:
|
|
||||||
"""Test locations that require Bind song with the bind song needed option activated"""
|
|
||||||
locations = [
|
|
||||||
"Verse cave right area, Big Seed",
|
|
||||||
"Verse cave left area, bulb under the rock at the end of the path",
|
|
||||||
"Home water, bulb under the rock in the left path from the verse cave",
|
|
||||||
"Song cave, bulb under the rock close to the song door",
|
|
||||||
"Song cave, bulb under the rock in the path to the singing statues",
|
|
||||||
"Naija's home, bulb under the rock at the right of the main path",
|
|
||||||
"Home water, bulb in the path below Nautilus Prime",
|
|
||||||
"Home water, bulb in the bottom left room",
|
|
||||||
"Home water, Nautilus Egg",
|
|
||||||
"Song cave, Verse egg",
|
|
||||||
"Energy temple first area, beating the energy statue",
|
|
||||||
"Energy temple first area, bulb in the bottom room blocked by a rock",
|
|
||||||
"Energy temple first area, Energy Idol",
|
|
||||||
"Energy temple second area, bulb under the rock",
|
|
||||||
"Energy temple bottom entrance, Krotite armor",
|
|
||||||
"Energy temple third area, bulb in the bottom path",
|
|
||||||
"Energy temple boss area, Fallen god tooth",
|
|
||||||
"Energy temple blaster room, Blaster egg",
|
|
||||||
*after_home_water_locations
|
|
||||||
]
|
|
||||||
items = [["Bind song"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
|
||||||
Description: Unit test used to test accessibility of region with the home water confine via option
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class ConfinedHomeWaterAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of region with the unconfine home water option disabled"""
|
|
||||||
options = {
|
|
||||||
"unconfine_home_water": 0,
|
|
||||||
"early_energy_form": False
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_confine_home_water_location(self) -> None:
|
|
||||||
"""Test region accessible with confined home water"""
|
|
||||||
self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area")
|
|
||||||
self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without the dual song
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class LiAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without the dual song"""
|
|
||||||
options = {
|
|
||||||
"turtle_randomizer": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_li_song_location(self) -> None:
|
|
||||||
"""Test locations that require the dual song"""
|
|
||||||
locations = [
|
|
||||||
"The body bottom area, bulb in the Jelly Zap room",
|
|
||||||
"The body bottom area, bulb in the nautilus room",
|
|
||||||
"The body bottom area, Mutant Costume",
|
|
||||||
"Final boss area, bulb in the boss third form room",
|
|
||||||
"Objective complete"
|
|
||||||
]
|
|
||||||
items = [["Dual form"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without the bind song (without the early
|
|
||||||
energy form option)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class EnergyFormAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without the energy form"""
|
|
||||||
options = {
|
|
||||||
"early_energy_form": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_energy_form_location(self) -> None:
|
|
||||||
"""Test locations that require Energy form"""
|
|
||||||
locations = [
|
|
||||||
"Home water, Nautilus Egg",
|
|
||||||
"Naija's home, bulb after the energy door",
|
|
||||||
"Energy temple first area, bulb in the bottom room blocked by a rock",
|
|
||||||
"Energy temple second area, bulb under the rock",
|
|
||||||
"Energy temple bottom entrance, Krotite armor",
|
|
||||||
"Energy temple third area, bulb in the bottom path",
|
|
||||||
"Energy temple boss area, Fallen god tooth",
|
|
||||||
"Energy temple blaster room, Blaster egg",
|
|
||||||
"Mithalas castle, beating the priests",
|
|
||||||
"Mithalas cathedral, first urn in the top right room",
|
|
||||||
"Mithalas cathedral, second urn in the top right room",
|
|
||||||
"Mithalas cathedral, third urn in the top right room",
|
|
||||||
"Mithalas cathedral, urn in the flesh room with fleas",
|
|
||||||
"Mithalas cathedral, first urn in the bottom right path",
|
|
||||||
"Mithalas cathedral, second urn in the bottom right path",
|
|
||||||
"Mithalas cathedral, urn behind the flesh vein",
|
|
||||||
"Mithalas cathedral, urn in the top left eyes boss room",
|
|
||||||
"Mithalas cathedral, first urn in the path behind the flesh vein",
|
|
||||||
"Mithalas cathedral, second urn in the path behind the flesh vein",
|
|
||||||
"Mithalas cathedral, third urn in the path behind the flesh vein",
|
|
||||||
"Mithalas cathedral, one of the urns in the top right room",
|
|
||||||
"Mithalas cathedral, Mithalan Dress",
|
|
||||||
"Mithalas cathedral right area, urn below the left entrance",
|
|
||||||
"Cathedral boss area, beating Mithalan God",
|
|
||||||
"Kelp Forest top left area, bulb close to the Verse egg",
|
|
||||||
"Kelp forest top left area, Verse egg",
|
|
||||||
"Kelp forest boss area, beating Drunian God",
|
|
||||||
"Mermog cave, Piranha Egg",
|
|
||||||
"Octopus cave, Dumbo Egg",
|
|
||||||
"Sun temple boss area, beating Sun God",
|
|
||||||
"Arnassi ruins, Crab armor",
|
|
||||||
"King Jellyfish cave, bulb in the right path from King Jelly",
|
|
||||||
"King Jellyfish cave, Jellyfish Costume",
|
|
||||||
"Sunken city, bulb on the top of the boss area (boiler room)",
|
|
||||||
"Final boss area, bulb in the boss third form room",
|
|
||||||
"Beating Fallen God",
|
|
||||||
"Beating Mithalan God",
|
|
||||||
"Beating Drunian God",
|
|
||||||
"Beating Sun God",
|
|
||||||
"Beating the Golem",
|
|
||||||
"Beating Nautilus Prime",
|
|
||||||
"Beating Blaster Peg Prime",
|
|
||||||
"Beating Mergog",
|
|
||||||
"Beating Mithalan priests",
|
|
||||||
"Beating Octopus Prime",
|
|
||||||
"Beating Crabbius Maximus",
|
|
||||||
"Beating King Jellyfish God Prime",
|
|
||||||
"First secret",
|
|
||||||
"Sunken City cleared",
|
|
||||||
"Objective complete",
|
|
||||||
]
|
|
||||||
items = [["Energy form"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without the fish form
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class FishFormAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without the fish form"""
|
|
||||||
options = {
|
|
||||||
"turtle_randomizer": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_fish_form_location(self) -> None:
|
|
||||||
"""Test locations that require fish form"""
|
|
||||||
locations = [
|
|
||||||
"The veil top left area, bulb inside the fish pass",
|
|
||||||
"Mithalas city, Doll",
|
|
||||||
"Mithalas city, urn inside a home fish pass",
|
|
||||||
"Kelp Forest top right area, bulb in the top fish pass",
|
|
||||||
"The veil bottom area, Verse egg",
|
|
||||||
"Open water bottom left area, bulb inside the lowest fish pass",
|
|
||||||
"Kelp Forest top left area, bulb close to the Verse egg",
|
|
||||||
"Kelp forest top left area, Verse egg",
|
|
||||||
"Mermog cave, bulb in the left part of the cave",
|
|
||||||
"Mermog cave, Piranha Egg",
|
|
||||||
"Beating Mergog",
|
|
||||||
"Octopus cave, Dumbo Egg",
|
|
||||||
"Octopus cave, bulb in the path below the octopus cave path",
|
|
||||||
"Beating Octopus Prime",
|
|
||||||
"Abyss left area, bulb in the bottom fish pass",
|
|
||||||
"Arnassi ruins, Arnassi Armor"
|
|
||||||
]
|
|
||||||
items = [["Fish form"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without Li
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class LiAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without Li"""
|
|
||||||
options = {
|
|
||||||
"turtle_randomizer": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_li_song_location(self) -> None:
|
|
||||||
"""Test locations that require Li"""
|
|
||||||
locations = [
|
|
||||||
"Sunken city right area, crate close to the save cristal",
|
|
||||||
"Sunken city right area, crate in the left bottom room",
|
|
||||||
"Sunken city left area, crate in the little pipe room",
|
|
||||||
"Sunken city left area, crate close to the save cristal",
|
|
||||||
"Sunken city left area, crate before the bedroom",
|
|
||||||
"Sunken city left area, Girl Costume",
|
|
||||||
"Sunken city, bulb on the top of the boss area (boiler room)",
|
|
||||||
"The body center area, breaking li cage",
|
|
||||||
"The body main area, bulb on the main path blocking tube",
|
|
||||||
"The body left area, first bulb in the top face room",
|
|
||||||
"The body left area, second bulb in the top face room",
|
|
||||||
"The body left area, bulb below the water stream",
|
|
||||||
"The body left area, bulb in the top path to the top face room",
|
|
||||||
"The body left area, bulb in the bottom face room",
|
|
||||||
"The body right area, bulb in the top face room",
|
|
||||||
"The body right area, bulb in the top path to the bottom face room",
|
|
||||||
"The body right area, bulb in the bottom face room",
|
|
||||||
"The body bottom area, bulb in the Jelly Zap room",
|
|
||||||
"The body bottom area, bulb in the nautilus room",
|
|
||||||
"The body bottom area, Mutant Costume",
|
|
||||||
"Final boss area, bulb in the boss third form room",
|
|
||||||
"Beating the Golem",
|
|
||||||
"Sunken City cleared",
|
|
||||||
"Objective complete"
|
|
||||||
]
|
|
||||||
items = [["Li and Li song", "Body tongue cleared"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without a light (Dumbo pet or sun form)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class LightAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without light"""
|
|
||||||
options = {
|
|
||||||
"turtle_randomizer": 1,
|
|
||||||
"light_needed_to_get_to_dark_places": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_light_location(self) -> None:
|
|
||||||
"""Test locations that require light"""
|
|
||||||
locations = [
|
|
||||||
# Since the `assertAccessDependency` sweep for events even if I tell it not to, those location cannot be
|
|
||||||
# tested.
|
|
||||||
# "Third secret",
|
|
||||||
# "Sun temple, bulb in the top left part",
|
|
||||||
# "Sun temple, bulb in the top right part",
|
|
||||||
# "Sun temple, bulb at the top of the high dark room",
|
|
||||||
# "Sun temple, Golden Gear",
|
|
||||||
# "Sun Worm path, first path bulb",
|
|
||||||
# "Sun Worm path, second path bulb",
|
|
||||||
# "Sun Worm path, first cliff bulb",
|
|
||||||
"Octopus cave, Dumbo Egg",
|
|
||||||
"Kelp forest bottom right area, Odd Container",
|
|
||||||
"Kelp forest top right area, Black pearl",
|
|
||||||
"Abyss left area, bulb in hidden path room",
|
|
||||||
"Abyss left area, bulb in the right part",
|
|
||||||
"Abyss left area, Glowing seed",
|
|
||||||
"Abyss left area, Glowing Plant",
|
|
||||||
"Abyss left area, bulb in the bottom fish pass",
|
|
||||||
"Abyss right area, bulb behind the rock in the whale room",
|
|
||||||
"Abyss right area, bulb in the middle path",
|
|
||||||
"Abyss right area, bulb behind the rock in the middle path",
|
|
||||||
"Abyss right area, bulb in the left green room",
|
|
||||||
"Abyss right area, Transturtle",
|
|
||||||
"Ice cave, bulb in the room to the right",
|
|
||||||
"Ice cave, First bulbs in the top exit room",
|
|
||||||
"Ice cave, Second bulbs in the top exit room",
|
|
||||||
"Ice cave, third bulbs in the top exit room",
|
|
||||||
"Ice cave, bulb in the left room",
|
|
||||||
"Bubble cave, bulb in the left cave wall",
|
|
||||||
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
|
|
||||||
"Bubble cave, Verse egg",
|
|
||||||
"Beating Mantis Shrimp Prime",
|
|
||||||
"King Jellyfish cave, bulb in the right path from King Jelly",
|
|
||||||
"King Jellyfish cave, Jellyfish Costume",
|
|
||||||
"Beating King Jellyfish God Prime",
|
|
||||||
"The whale, Verse egg",
|
|
||||||
"First secret",
|
|
||||||
"Sunken city right area, crate close to the save cristal",
|
|
||||||
"Sunken city right area, crate in the left bottom room",
|
|
||||||
"Sunken city left area, crate in the little pipe room",
|
|
||||||
"Sunken city left area, crate close to the save cristal",
|
|
||||||
"Sunken city left area, crate before the bedroom",
|
|
||||||
"Sunken city left area, Girl Costume",
|
|
||||||
"Sunken city, bulb on the top of the boss area (boiler room)",
|
|
||||||
"Sunken City cleared",
|
|
||||||
"Beating the Golem",
|
|
||||||
"Beating Octopus Prime",
|
|
||||||
"Final boss area, bulb in the boss third form room",
|
|
||||||
"Objective complete",
|
|
||||||
]
|
|
||||||
items = [["Sun form", "Baby dumbo", "Has sun crystal"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without the nature form
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class NatureFormAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without the nature form"""
|
|
||||||
options = {
|
|
||||||
"turtle_randomizer": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_nature_form_location(self) -> None:
|
|
||||||
"""Test locations that require nature form"""
|
|
||||||
locations = [
|
|
||||||
"Song cave, Anemone seed",
|
|
||||||
"Energy temple blaster room, Blaster egg",
|
|
||||||
"Beating Blaster Peg Prime",
|
|
||||||
"Kelp forest top left area, Verse egg",
|
|
||||||
"Kelp Forest top left area, bulb close to the Verse egg",
|
|
||||||
"Mithalas castle, beating the priests",
|
|
||||||
"Kelp Forest sprite cave, bulb in the second room",
|
|
||||||
"Kelp Forest Sprite Cave, Seed bag",
|
|
||||||
"Beating Mithalan priests",
|
|
||||||
"Abyss left area, bulb in the bottom fish pass",
|
|
||||||
"Bubble cave, Verse egg",
|
|
||||||
"Beating Mantis Shrimp Prime",
|
|
||||||
"Sunken city right area, crate close to the save cristal",
|
|
||||||
"Sunken city right area, crate in the left bottom room",
|
|
||||||
"Sunken city left area, crate in the little pipe room",
|
|
||||||
"Sunken city left area, crate close to the save cristal",
|
|
||||||
"Sunken city left area, crate before the bedroom",
|
|
||||||
"Sunken city left area, Girl Costume",
|
|
||||||
"Sunken city, bulb on the top of the boss area (boiler room)",
|
|
||||||
"Beating the Golem",
|
|
||||||
"Sunken City cleared",
|
|
||||||
"The body center area, breaking li cage",
|
|
||||||
"The body main area, bulb on the main path blocking tube",
|
|
||||||
"The body left area, first bulb in the top face room",
|
|
||||||
"The body left area, second bulb in the top face room",
|
|
||||||
"The body left area, bulb below the water stream",
|
|
||||||
"The body left area, bulb in the top path to the top face room",
|
|
||||||
"The body left area, bulb in the bottom face room",
|
|
||||||
"The body right area, bulb in the top face room",
|
|
||||||
"The body right area, bulb in the top path to the bottom face room",
|
|
||||||
"The body right area, bulb in the bottom face room",
|
|
||||||
"The body bottom area, bulb in the Jelly Zap room",
|
|
||||||
"The body bottom area, bulb in the nautilus room",
|
|
||||||
"The body bottom area, Mutant Costume",
|
|
||||||
"Final boss area, bulb in the boss third form room",
|
|
||||||
"Objective complete"
|
|
||||||
]
|
|
||||||
items = [["Nature form"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
|
||||||
Description: Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
from BaseClasses import ItemClassification
|
|
||||||
|
|
||||||
|
|
||||||
class UNoProgressionHardHiddenTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test that no progression items can be put in hard or hidden locations when option enabled"""
|
|
||||||
options = {
|
|
||||||
"no_progression_hard_or_hidden_locations": True
|
|
||||||
}
|
|
||||||
|
|
||||||
unfillable_locations = [
|
|
||||||
"Energy temple boss area, Fallen god tooth",
|
|
||||||
"Cathedral boss area, beating Mithalan God",
|
|
||||||
"Kelp forest boss area, beating Drunian God",
|
|
||||||
"Sun temple boss area, beating Sun God",
|
|
||||||
"Sunken city, bulb on the top of the boss area (boiler room)",
|
|
||||||
"Home water, Nautilus Egg",
|
|
||||||
"Energy temple blaster room, Blaster egg",
|
|
||||||
"Mithalas castle, beating the priests",
|
|
||||||
"Mermog cave, Piranha Egg",
|
|
||||||
"Octopus cave, Dumbo Egg",
|
|
||||||
"King Jellyfish cave, bulb in the right path from King Jelly",
|
|
||||||
"King Jellyfish cave, Jellyfish Costume",
|
|
||||||
"Final boss area, bulb in the boss third form room",
|
|
||||||
"Sun Worm path, first cliff bulb",
|
|
||||||
"Sun Worm path, second cliff bulb",
|
|
||||||
"The veil top right area, bulb in the top of the water fall",
|
|
||||||
"Bubble cave, bulb in the left cave wall",
|
|
||||||
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
|
|
||||||
"Bubble cave, Verse egg",
|
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
|
||||||
"Kelp forest bottom left area, Walker baby",
|
|
||||||
"Sun temple, Sun key",
|
|
||||||
"The body bottom area, Mutant Costume",
|
|
||||||
"Sun temple, bulb in the hidden room of the right part",
|
|
||||||
"Arnassi ruins, Arnassi Armor",
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_unconfine_home_water_both_location_fillable(self) -> None:
|
|
||||||
"""
|
|
||||||
Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
|
|
||||||
"""
|
|
||||||
for location in self.unfillable_locations:
|
|
||||||
for item_name in self.world.item_names:
|
|
||||||
item = self.get_item_by_name(item_name)
|
|
||||||
if item.classification == ItemClassification.progression:
|
|
||||||
self.assertFalse(
|
|
||||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
|
||||||
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
|
|
||||||
else:
|
|
||||||
self.assertTrue(
|
|
||||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
|
||||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
|
||||||
Description: Unit test used to test that progression items can be put in hard or hidden locations when option disabled
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
from BaseClasses import ItemClassification
|
|
||||||
|
|
||||||
|
|
||||||
class UNoProgressionHardHiddenTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test that no progression items can be put in hard or hidden locations when option disabled"""
|
|
||||||
options = {
|
|
||||||
"no_progression_hard_or_hidden_locations": False
|
|
||||||
}
|
|
||||||
|
|
||||||
unfillable_locations = [
|
|
||||||
"Energy temple boss area, Fallen god tooth",
|
|
||||||
"Cathedral boss area, beating Mithalan God",
|
|
||||||
"Kelp forest boss area, beating Drunian God",
|
|
||||||
"Sun temple boss area, beating Sun God",
|
|
||||||
"Sunken city, bulb on the top of the boss area (boiler room)",
|
|
||||||
"Home water, Nautilus Egg",
|
|
||||||
"Energy temple blaster room, Blaster egg",
|
|
||||||
"Mithalas castle, beating the priests",
|
|
||||||
"Mermog cave, Piranha Egg",
|
|
||||||
"Octopus cave, Dumbo Egg",
|
|
||||||
"King Jellyfish cave, bulb in the right path from King Jelly",
|
|
||||||
"King Jellyfish cave, Jellyfish Costume",
|
|
||||||
"Final boss area, bulb in the boss third form room",
|
|
||||||
"Sun Worm path, first cliff bulb",
|
|
||||||
"Sun Worm path, second cliff bulb",
|
|
||||||
"The veil top right area, bulb in the top of the water fall",
|
|
||||||
"Bubble cave, bulb in the left cave wall",
|
|
||||||
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
|
|
||||||
"Bubble cave, Verse egg",
|
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
|
||||||
"Kelp forest bottom left area, Walker baby",
|
|
||||||
"Sun temple, Sun key",
|
|
||||||
"The body bottom area, Mutant Costume",
|
|
||||||
"Sun temple, bulb in the hidden room of the right part",
|
|
||||||
"Arnassi ruins, Arnassi Armor",
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_unconfine_home_water_both_location_fillable(self) -> None:
|
|
||||||
"""Unit test used to test that progression items can be put in hard or hidden locations when option disabled"""
|
|
||||||
for location in self.unfillable_locations:
|
|
||||||
for item_name in self.world.item_names:
|
|
||||||
item = self.get_item_by_name(item_name)
|
|
||||||
self.assertTrue(
|
|
||||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
|
||||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Louis M
|
|
||||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|
||||||
Description: Unit test used to test accessibility of locations with and without the spirit form
|
|
||||||
"""
|
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
|
||||||
|
|
||||||
|
|
||||||
class SpiritFormAccessTest(AquariaTestBase):
|
|
||||||
"""Unit test used to test accessibility of locations with and without the spirit form"""
|
|
||||||
|
|
||||||
def test_spirit_form_location(self) -> None:
|
|
||||||
"""Test locations that require spirit form"""
|
|
||||||
locations = [
|
|
||||||
"The veil bottom area, bulb in the spirit path",
|
|
||||||
"Mithalas city castle, Trident head",
|
|
||||||
"Open water skeleton path, King skull",
|
|
||||||
"Kelp forest bottom left area, Walker baby",
|
|
||||||
"Abyss right area, bulb behind the rock in the whale room",
|
|
||||||
"The whale, Verse egg",
|
|
||||||
"Ice cave, bulb in the room to the right",
|
|
||||||
"Ice cave, First bulbs in the top exit room",
|
|
||||||
"Ice cave, Second bulbs in the top exit room",
|
|
||||||
"Ice cave, third bulbs in the top exit room",
|
|
||||||
"Ice cave, bulb in the left room",
|
|
||||||
"Bubble cave, bulb in the left cave wall",
|
|
||||||
"Bubble cave, bulb in the right cave wall (behind the ice cristal)",
|
|
||||||
"Bubble cave, Verse egg",
|
|
||||||
"Sunken city left area, Girl Costume",
|
|
||||||
"Beating Mantis Shrimp Prime",
|
|
||||||
"First secret",
|
|
||||||
"Arnassi ruins, Arnassi Armor",
|
|
||||||
]
|
|
||||||
items = [["Spirit form"]]
|
|
||||||
self.assertAccessDependency(locations, items)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user