mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-09 17:13:45 -07:00
Compare commits
158 Commits
core_repor
...
options-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a169649500 | ||
|
|
5910b94deb | ||
|
|
14ffd1c70c | ||
|
|
754fc11c1b | ||
|
|
12cde88f95 | ||
|
|
e0b6889634 | ||
|
|
14321d6ba2 | ||
|
|
e978109410 | ||
|
|
019dfb8242 | ||
|
|
8e9a050889 | ||
|
|
2801e21296 | ||
|
|
e97eddcdaf | ||
|
|
d3f4ee4994 | ||
|
|
cf34f125d6 | ||
|
|
663b50b33e | ||
|
|
230a9e620b | ||
|
|
1b6fb7b090 | ||
|
|
0e893889c7 | ||
|
|
2bc345504e | ||
|
|
5e3c5dedf3 | ||
|
|
5fb0126754 | ||
|
|
b4c263fc9d | ||
|
|
013862b068 | ||
|
|
280b67f996 | ||
|
|
9ae7083bfc | ||
|
|
bd18018852 | ||
|
|
b4b79bcd78 | ||
|
|
539ee1c5da | ||
|
|
5fb1d0f98a | ||
|
|
89a2a3c35b | ||
|
|
7900e4c9a4 | ||
|
|
3dbdd048cd | ||
|
|
68323b46a9 | ||
|
|
2447be92d8 | ||
|
|
88dd27eb3a | ||
|
|
6d8ac5d054 | ||
|
|
5a2d839412 | ||
|
|
4bd4a2c541 | ||
|
|
705cb2e816 | ||
|
|
467bbd7754 | ||
|
|
4da9cdd91c | ||
|
|
6576b069f2 | ||
|
|
b78781ab3e | ||
|
|
9a82edc931 | ||
|
|
77cce68c08 | ||
|
|
f38655d6b6 | ||
|
|
701fbab837 | ||
|
|
af83050b75 | ||
|
|
8db3e40094 | ||
|
|
d48f2ab1b4 | ||
|
|
0f1b16d640 | ||
|
|
76962b8b3b | ||
|
|
e04db57dce | ||
|
|
12b8fef1aa | ||
|
|
0ac8844f6f | ||
|
|
23eca7d747 | ||
|
|
1a563a14fc | ||
|
|
5935093615 | ||
|
|
2aa3ef372d | ||
|
|
d94cf8dcb2 | ||
|
|
5fae1c087e | ||
|
|
7e61211365 | ||
|
|
7603b4a88f | ||
|
|
005fc4e864 | ||
|
|
28262a31b8 | ||
|
|
660b068f5a | ||
|
|
879c3407d8 | ||
|
|
d5683c4326 | ||
|
|
f27d1d635b | ||
|
|
298c9fc159 | ||
|
|
26188230b7 | ||
|
|
b68be7360c | ||
|
|
255e52642e | ||
|
|
49862dca1f | ||
|
|
0d586a4467 | ||
|
|
8c8b29ae92 | ||
|
|
9d478ba2bc | ||
|
|
3cc434cd78 | ||
|
|
31a5696526 | ||
|
|
7bdf9a643c | ||
|
|
c64c80aac0 | ||
|
|
07d9d6165e | ||
|
|
fc571ba356 | ||
|
|
ea6235e0d9 | ||
|
|
6f8b8fc9c9 | ||
|
|
0ed0de3daa | ||
|
|
487a067d10 | ||
|
|
fc4e6adff5 | ||
|
|
9cdc90513b | ||
|
|
9afe45166c | ||
|
|
9e20fa48e1 | ||
|
|
e76ba928a8 | ||
|
|
4f1e696243 | ||
|
|
4756c76541 | ||
|
|
2f78860d8c | ||
|
|
cca9778871 | ||
|
|
bb16fe284a | ||
|
|
daccb30e3d | ||
|
|
747b48183c | ||
|
|
e22ac85e15 | ||
|
|
ee69fa6a8c | ||
|
|
ec18254e9e | ||
|
|
49c0268a84 | ||
|
|
6aafa6ff04 | ||
|
|
3e27b93c37 | ||
|
|
392c47dcef | ||
|
|
442c7d04db | ||
|
|
ad4451276d | ||
|
|
915ad61ecf | ||
|
|
5d8aca1b4e | ||
|
|
d4c8083be5 | ||
|
|
a3702abe38 | ||
|
|
5fcc1aa83f | ||
|
|
b8e0d4c4ee | ||
|
|
fc18f9caf9 | ||
|
|
a50e68acd1 | ||
|
|
0624ba5e81 | ||
|
|
a45fa84382 | ||
|
|
532cff1334 | ||
|
|
0a1ce5b7d8 | ||
|
|
a4acdb6ddf | ||
|
|
7a004de9a0 | ||
|
|
8021ec744f | ||
|
|
a06bca95ad | ||
|
|
b372b9da20 | ||
|
|
6087ec539b | ||
|
|
f89cee4b15 | ||
|
|
727915040d | ||
|
|
c4c4069022 | ||
|
|
5711d2c309 | ||
|
|
580c9c3943 | ||
|
|
fbfe82f57f | ||
|
|
233eba6681 | ||
|
|
ffff63e6f3 | ||
|
|
2704015eef | ||
|
|
3c70621f1b | ||
|
|
5ec342abf4 | ||
|
|
2c80a9b8f1 | ||
|
|
6f7c2fa25f | ||
|
|
8b8df9fa33 | ||
|
|
3343d4e364 | ||
|
|
1faaa0d941 | ||
|
|
fec533b65e | ||
|
|
21184b59d2 | ||
|
|
444178171b | ||
|
|
740b76ebd5 | ||
|
|
6b50c91ce2 | ||
|
|
a91105c958 | ||
|
|
c3060a8b66 | ||
|
|
5996a8163d | ||
|
|
ee1e578201 | ||
|
|
2d3f3fcc2d | ||
|
|
437843bf53 | ||
|
|
7fd7d5e492 | ||
|
|
52c1b04fc8 | ||
|
|
6e56f31398 | ||
|
|
f19a84222e | ||
|
|
801d1223ac |
160
BaseClasses.py
160
BaseClasses.py
@@ -7,14 +7,12 @@ import logging
|
||||
import random
|
||||
import secrets
|
||||
import typing # this can go away when Python 3.8 support is dropped
|
||||
import threading
|
||||
import time
|
||||
from argparse import Namespace
|
||||
from collections import Counter, deque
|
||||
from collections.abc import Collection, MutableSequence
|
||||
from enum import IntEnum, IntFlag
|
||||
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
|
||||
Type, ClassVar
|
||||
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
|
||||
TypedDict, Union, Type, ClassVar
|
||||
|
||||
import NetUtils
|
||||
import Options
|
||||
@@ -53,10 +51,6 @@ class ThreadBarrierProxy:
|
||||
class MultiWorld():
|
||||
debug_types = False
|
||||
player_name: Dict[int, str]
|
||||
difficulty_requirements: dict
|
||||
required_medallions: dict
|
||||
dark_room_logic: Dict[int, str]
|
||||
restrict_dungeon_item_on_boss: Dict[int, bool]
|
||||
plando_texts: List[Dict[str, str]]
|
||||
plando_items: List[List[Dict[str, Any]]]
|
||||
plando_connections: List
|
||||
@@ -97,42 +91,6 @@ class MultiWorld():
|
||||
def __getitem__(self, player) -> bool:
|
||||
return self.rule(player)
|
||||
|
||||
class Observer(threading.Thread):
|
||||
current_function: str
|
||||
entered: float
|
||||
shutdown: bool = False
|
||||
|
||||
def __init__(self):
|
||||
self.current_function = ""
|
||||
self.entered = 0.0
|
||||
super().__init__(name="Observer", daemon=True)
|
||||
|
||||
def __call__(self, function: typing.Callable, entered: float):
|
||||
# use str of function to avoid having a reference to a bound method
|
||||
self.current_function = str(function)
|
||||
self.entered = entered
|
||||
return self
|
||||
|
||||
def __enter__(self):
|
||||
assert self.current_function, "Entered Observer Context without current method."
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.current_function = ""
|
||||
|
||||
def run(self):
|
||||
while not self.shutdown:
|
||||
time.sleep(1)
|
||||
if self.current_function:
|
||||
now = time.perf_counter()
|
||||
elapsed = now - self.entered
|
||||
if elapsed > 60:
|
||||
logging.info(f"Generation stalling in {self.current_function}, "
|
||||
f"running since {elapsed:.0f} seconds ago.")
|
||||
self.current_function = ""
|
||||
|
||||
observer = Observer()
|
||||
observer.start()
|
||||
|
||||
class RegionManager:
|
||||
region_cache: Dict[int, Dict[str, Region]]
|
||||
entrance_cache: Dict[int, Dict[str, Entrance]]
|
||||
@@ -175,7 +133,6 @@ class MultiWorld():
|
||||
self.random = ThreadBarrierProxy(random.Random())
|
||||
self.players = players
|
||||
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
|
||||
self.glitch_triforce = False
|
||||
self.algorithm = 'balanced'
|
||||
self.groups = {}
|
||||
self.regions = self.RegionManager(players)
|
||||
@@ -202,49 +159,10 @@ class MultiWorld():
|
||||
for player in range(1, players + 1):
|
||||
def set_player_attr(attr, val):
|
||||
self.__dict__.setdefault(attr, {})[player] = val
|
||||
|
||||
set_player_attr('shuffle', "vanilla")
|
||||
set_player_attr('logic', "noglitches")
|
||||
set_player_attr('mode', 'open')
|
||||
set_player_attr('difficulty', 'normal')
|
||||
set_player_attr('item_functionality', 'normal')
|
||||
set_player_attr('timer', False)
|
||||
set_player_attr('goal', 'ganon')
|
||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||
set_player_attr('swamp_patch_required', False)
|
||||
set_player_attr('powder_patch_required', False)
|
||||
set_player_attr('ganon_at_pyramid', True)
|
||||
set_player_attr('ganonstower_vanilla', True)
|
||||
set_player_attr('can_access_trock_eyebridge', None)
|
||||
set_player_attr('can_access_trock_front', None)
|
||||
set_player_attr('can_access_trock_big_chest', None)
|
||||
set_player_attr('can_access_trock_middle', None)
|
||||
set_player_attr('fix_fake_world', True)
|
||||
set_player_attr('difficulty_requirements', None)
|
||||
set_player_attr('boss_shuffle', 'none')
|
||||
set_player_attr('enemy_health', 'default')
|
||||
set_player_attr('enemy_damage', 'default')
|
||||
set_player_attr('beemizer_total_chance', 0)
|
||||
set_player_attr('beemizer_trap_chance', 0)
|
||||
set_player_attr('escape_assist', [])
|
||||
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
||||
set_player_attr('treasure_hunt_count', 0)
|
||||
set_player_attr('clock_mode', False)
|
||||
set_player_attr('countdown_start_time', 10)
|
||||
set_player_attr('red_clock_time', -2)
|
||||
set_player_attr('blue_clock_time', 2)
|
||||
set_player_attr('green_clock_time', 4)
|
||||
set_player_attr('can_take_damage', True)
|
||||
set_player_attr('triforce_pieces_available', 30)
|
||||
set_player_attr('triforce_pieces_required', 20)
|
||||
set_player_attr('shop_shuffle', 'off')
|
||||
set_player_attr('shuffle_prizes', "g")
|
||||
set_player_attr('sprite_pool', [])
|
||||
set_player_attr('dark_room_logic', "lamp")
|
||||
set_player_attr('plando_items', [])
|
||||
set_player_attr('plando_texts', {})
|
||||
set_player_attr('plando_connections', [])
|
||||
set_player_attr('game', "A Link to the Past")
|
||||
set_player_attr('game', "Archipelago")
|
||||
set_player_attr('completion_condition', lambda state: True)
|
||||
self.worlds = {}
|
||||
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
|
||||
@@ -789,15 +707,49 @@ class CollectionState():
|
||||
"""Returns True if at least one item name of items is in state at least once."""
|
||||
return any(self.prog_items[player][item] for item in items)
|
||||
|
||||
def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool:
|
||||
"""Returns True if each item name is in the state at least as many times as specified."""
|
||||
return all(self.prog_items[player][item] >= count for item, count in item_counts.items())
|
||||
|
||||
def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool:
|
||||
"""Returns True if at least one item name is in the state at least as many times as specified."""
|
||||
return any(self.prog_items[player][item] >= count for item, count in item_counts.items())
|
||||
|
||||
def count(self, item: str, player: int) -> int:
|
||||
return self.prog_items[player][item]
|
||||
|
||||
def item_count(self, item: str, player: int) -> int:
|
||||
Utils.deprecate("Use count instead.")
|
||||
return self.count(item, player)
|
||||
def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
|
||||
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
|
||||
found: int = 0
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item_name in items:
|
||||
found += player_prog_items[item_name]
|
||||
if found >= count:
|
||||
return True
|
||||
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:
|
||||
"""Returns the cumulative count of items from a list present in state."""
|
||||
return sum(self.prog_items[player][item_name] for item_name in items)
|
||||
|
||||
def count_from_list_exclusive(self, items: Iterable[str], player: int) -> int:
|
||||
"""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
|
||||
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."""
|
||||
found: int = 0
|
||||
player_prog_items = self.prog_items[player]
|
||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
|
||||
@@ -806,12 +758,34 @@ class CollectionState():
|
||||
return True
|
||||
return False
|
||||
|
||||
def count_group(self, item_name_group: str, player: int) -> int:
|
||||
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]
|
||||
return found
|
||||
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:
|
||||
"""Returns the cumulative count of items from an item group present in state."""
|
||||
player_prog_items = self.prog_items[player]
|
||||
return sum(
|
||||
player_prog_items[item_name]
|
||||
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
|
||||
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
||||
@@ -1072,7 +1046,7 @@ class Location:
|
||||
self.parent_region = parent
|
||||
|
||||
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.non_local_items[item.player])
|
||||
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
|
||||
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||
and self.item_rule(item)
|
||||
and (not check_access or self.can_reach(state))))
|
||||
@@ -1268,7 +1242,7 @@ class Spoiler:
|
||||
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
|
||||
sphere_candidates])
|
||||
if any([multiworld.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
|
||||
if any([multiworld.worlds[location.item.player].options.accessibility != 'minimal' for location in sphere_candidates]):
|
||||
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
|
||||
f'Something went terribly wrong here.')
|
||||
else:
|
||||
|
||||
@@ -207,6 +207,8 @@ class CommonContext:
|
||||
|
||||
finished_game: bool
|
||||
ready: bool
|
||||
team: typing.Optional[int]
|
||||
slot: typing.Optional[int]
|
||||
auth: typing.Optional[str]
|
||||
seed_name: typing.Optional[str]
|
||||
|
||||
|
||||
25
Fill.py
25
Fill.py
@@ -19,11 +19,12 @@ def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
|
||||
logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.")
|
||||
|
||||
|
||||
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple()) -> CollectionState:
|
||||
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple(),
|
||||
locations: typing.Optional[typing.List[Location]] = None) -> CollectionState:
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
new_state.collect(item, True)
|
||||
new_state.sweep_for_events()
|
||||
new_state.sweep_for_events(locations=locations)
|
||||
return new_state
|
||||
|
||||
|
||||
@@ -66,7 +67,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
item_pool.pop(p)
|
||||
break
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
base_state, item_pool + unplaced_items)
|
||||
base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player)
|
||||
if single_player_placement else None)
|
||||
|
||||
has_beaten_game = multiworld.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
@@ -112,7 +114,9 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
|
||||
location.item = None
|
||||
placed_item.location = None
|
||||
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool)
|
||||
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool,
|
||||
multiworld.get_filled_locations(item.player)
|
||||
if single_player_placement else None)
|
||||
# unsafe means swap_state assumes we can somehow collect placed_item before item_to_place
|
||||
# by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic
|
||||
# to clean that up later, so there is a chance generation fails.
|
||||
@@ -170,7 +174,9 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
|
||||
if cleanup_required:
|
||||
# validate all placements and remove invalid ones
|
||||
state = sweep_from_pool(base_state, [])
|
||||
state = sweep_from_pool(
|
||||
base_state, [], multiworld.get_filled_locations(item.player)
|
||||
if single_player_placement else None)
|
||||
for placement in placements:
|
||||
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
|
||||
placement.item.location = None
|
||||
@@ -456,14 +462,16 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
|
||||
if prioritylocations:
|
||||
# "priority fill"
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
||||
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking,
|
||||
name="Priority")
|
||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||
defaultlocations = prioritylocations + defaultlocations
|
||||
|
||||
if progitempool:
|
||||
# "advancement/progression fill"
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression")
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, single_player_placement=multiworld.players == 1,
|
||||
name="Progression")
|
||||
if progitempool:
|
||||
raise FillError(
|
||||
f"Not enough locations for progression items. "
|
||||
@@ -495,10 +503,9 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
if unplaced or unfilled:
|
||||
logging.warning(
|
||||
f"Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}")
|
||||
items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item)
|
||||
items_counter = Counter(location.item.player for location in multiworld.get_filled_locations())
|
||||
locations_counter = Counter(location.player for location in multiworld.get_locations())
|
||||
items_counter.update(item.player for item in unplaced)
|
||||
locations_counter.update(location.player for location in unfilled)
|
||||
print_data = {"items": items_counter, "locations": locations_counter}
|
||||
logging.info(f"Per-Player counts: {print_data})")
|
||||
|
||||
|
||||
33
Generate.py
33
Generate.py
@@ -120,7 +120,7 @@ def main(args=None, callback=ERmain):
|
||||
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
|
||||
|
||||
# sort dict for consistent results across platforms:
|
||||
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
|
||||
weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())}
|
||||
for filename, yaml_data in weights_cache.items():
|
||||
if filename not in {args.meta_file_path, args.weights_file_path}:
|
||||
for yaml in yaml_data:
|
||||
@@ -147,7 +147,6 @@ def main(args=None, callback=ERmain):
|
||||
erargs = parse_arguments(['--multi', str(args.multi)])
|
||||
erargs.seed = seed
|
||||
erargs.plando_options = args.plando
|
||||
erargs.glitch_triforce = options.generator.glitch_triforce_room
|
||||
erargs.spoiler = args.spoiler
|
||||
erargs.race = args.race
|
||||
erargs.outputname = seed_name
|
||||
@@ -354,7 +353,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
|
||||
if options[option_key].supports_weighting:
|
||||
return get_choice(option_key, category_dict)
|
||||
return category_dict[option_key]
|
||||
raise Exception(f"Error generating meta option {option_key} for {game}.")
|
||||
raise Options.OptionError(f"Error generating meta option {option_key} for {game}.")
|
||||
|
||||
|
||||
def roll_linked_options(weights: dict) -> dict:
|
||||
@@ -379,7 +378,7 @@ def roll_linked_options(weights: dict) -> dict:
|
||||
return weights
|
||||
|
||||
|
||||
def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||
def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict:
|
||||
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
|
||||
weights["_Generator_Version"] = Utils.__version__
|
||||
for i, option_set in enumerate(triggers):
|
||||
@@ -402,7 +401,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||
if category_name:
|
||||
currently_targeted_weights = currently_targeted_weights[category_name]
|
||||
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
|
||||
|
||||
valid_keys.add(key)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Your trigger number {i + 1} is invalid. "
|
||||
f"Please fix your triggers.") from e
|
||||
@@ -410,27 +409,29 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||
|
||||
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
|
||||
if option_key in game_weights:
|
||||
try:
|
||||
try:
|
||||
if option_key in game_weights:
|
||||
if not option.supports_weighting:
|
||||
player_option = option.from_any(game_weights[option_key])
|
||||
else:
|
||||
player_option = option.from_any(get_choice(option_key, game_weights))
|
||||
setattr(ret, option_key, player_option)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
|
||||
del game_weights[option_key]
|
||||
else:
|
||||
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
|
||||
player_option = option.from_any(option.default) # call the from_any here to support default "random"
|
||||
setattr(ret, option_key, player_option)
|
||||
except Exception as e:
|
||||
raise Options.OptionError(f"Error generating option {option_key} in {ret.game}") from e
|
||||
else:
|
||||
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
|
||||
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
|
||||
|
||||
|
||||
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
|
||||
if "linked_options" in weights:
|
||||
weights = roll_linked_options(weights)
|
||||
|
||||
valid_trigger_names = set()
|
||||
if "triggers" in weights:
|
||||
weights = roll_triggers(weights, weights["triggers"])
|
||||
weights = roll_triggers(weights, weights["triggers"], valid_trigger_names)
|
||||
|
||||
requirements = weights.get("requires", {})
|
||||
if requirements:
|
||||
@@ -470,7 +471,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
raise Exception(f"Merge tag cannot be used outside of trigger contexts.")
|
||||
|
||||
if "triggers" in game_weights:
|
||||
weights = roll_triggers(weights, game_weights["triggers"])
|
||||
weights = roll_triggers(weights, game_weights["triggers"], valid_trigger_names)
|
||||
game_weights = weights[ret.game]
|
||||
|
||||
ret.name = get_choice('name', weights)
|
||||
@@ -479,6 +480,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
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:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
if ret.game == "A Link to the Past":
|
||||
|
||||
@@ -102,7 +102,7 @@ components.extend([
|
||||
Component("Open Patch", func=open_patch),
|
||||
Component("Generate Template Options", func=generate_yamls),
|
||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
||||
Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||
Component("Browse Files", func=browse_files),
|
||||
])
|
||||
|
||||
@@ -259,7 +259,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||
elif not args:
|
||||
args = {}
|
||||
|
||||
if "Patch|Game|Component" in args:
|
||||
if args.get("Patch|Game|Component", None) is not None:
|
||||
file, component = identify(args["Patch|Game|Component"])
|
||||
if file:
|
||||
args['file'] = file
|
||||
|
||||
27
Main.py
27
Main.py
@@ -36,38 +36,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
logger = logging.getLogger()
|
||||
multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||
multiworld.plando_options = args.plando_options
|
||||
|
||||
multiworld.shuffle = args.shuffle.copy()
|
||||
multiworld.logic = args.logic.copy()
|
||||
multiworld.mode = args.mode.copy()
|
||||
multiworld.difficulty = args.difficulty.copy()
|
||||
multiworld.item_functionality = args.item_functionality.copy()
|
||||
multiworld.timer = args.timer.copy()
|
||||
multiworld.goal = args.goal.copy()
|
||||
multiworld.boss_shuffle = args.shufflebosses.copy()
|
||||
multiworld.enemy_health = args.enemy_health.copy()
|
||||
multiworld.enemy_damage = args.enemy_damage.copy()
|
||||
multiworld.beemizer_total_chance = args.beemizer_total_chance.copy()
|
||||
multiworld.beemizer_trap_chance = args.beemizer_trap_chance.copy()
|
||||
multiworld.countdown_start_time = args.countdown_start_time.copy()
|
||||
multiworld.red_clock_time = args.red_clock_time.copy()
|
||||
multiworld.blue_clock_time = args.blue_clock_time.copy()
|
||||
multiworld.green_clock_time = args.green_clock_time.copy()
|
||||
multiworld.dungeon_counters = args.dungeon_counters.copy()
|
||||
multiworld.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||
multiworld.triforce_pieces_required = args.triforce_pieces_required.copy()
|
||||
multiworld.shop_shuffle = args.shop_shuffle.copy()
|
||||
multiworld.shuffle_prizes = args.shuffle_prizes.copy()
|
||||
multiworld.sprite_pool = args.sprite_pool.copy()
|
||||
multiworld.dark_room_logic = args.dark_room_logic.copy()
|
||||
multiworld.plando_items = args.plando_items.copy()
|
||||
multiworld.plando_texts = args.plando_texts.copy()
|
||||
multiworld.plando_connections = args.plando_connections.copy()
|
||||
multiworld.required_medallions = args.required_medallions.copy()
|
||||
multiworld.game = args.game.copy()
|
||||
multiworld.player_name = args.name.copy()
|
||||
multiworld.sprite = args.sprite.copy()
|
||||
multiworld.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
multiworld.sprite_pool = args.sprite_pool.copy()
|
||||
|
||||
multiworld.set_options(args)
|
||||
multiworld.set_item_links()
|
||||
|
||||
184
MultiServer.py
184
MultiServer.py
@@ -175,11 +175,13 @@ class Context:
|
||||
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||
non_hintable_names: typing.Dict[str, typing.Set[str]]
|
||||
logger: logging.Logger
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
|
||||
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
||||
log_network: bool = False):
|
||||
log_network: bool = False, logger: logging.Logger = logging.getLogger()):
|
||||
self.logger = logger
|
||||
super(Context, self).__init__()
|
||||
self.slot_info = {}
|
||||
self.log_network = log_network
|
||||
@@ -287,12 +289,12 @@ class Context:
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||
self.logger.exception(f"Exception during send_msgs, could not send {msg}")
|
||||
await self.disconnect(endpoint)
|
||||
return False
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
self.logger.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
|
||||
@@ -301,12 +303,12 @@ class Context:
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception("Exception during send_encoded_msgs")
|
||||
self.logger.exception("Exception during send_encoded_msgs")
|
||||
await self.disconnect(endpoint)
|
||||
return False
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
self.logger.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool:
|
||||
@@ -317,11 +319,11 @@ class Context:
|
||||
try:
|
||||
websockets.broadcast(sockets, msg)
|
||||
except RuntimeError:
|
||||
logging.exception("Exception during broadcast_send_encoded_msgs")
|
||||
self.logger.exception("Exception during broadcast_send_encoded_msgs")
|
||||
return False
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing broadcast: {msg}")
|
||||
self.logger.info(f"Outgoing broadcast: {msg}")
|
||||
return True
|
||||
|
||||
def broadcast_all(self, msgs: typing.List[dict]):
|
||||
@@ -330,7 +332,7 @@ class Context:
|
||||
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||
|
||||
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
|
||||
logging.info("Notice (all): %s" % text)
|
||||
self.logger.info("Notice (all): %s" % text)
|
||||
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
|
||||
|
||||
def broadcast_team(self, team: int, msgs: typing.List[dict]):
|
||||
@@ -352,7 +354,7 @@ class Context:
|
||||
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
|
||||
if not client.auth:
|
||||
return
|
||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
|
||||
|
||||
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
|
||||
@@ -451,7 +453,7 @@ class Context:
|
||||
for game_name, data in decoded_obj.get("datapackage", {}).items():
|
||||
if game_name in game_data_packages:
|
||||
data = game_data_packages[game_name]
|
||||
logging.info(f"Loading embedded data package for game {game_name}")
|
||||
self.logger.info(f"Loading embedded data package for game {game_name}")
|
||||
self.gamespackage[game_name] = data
|
||||
self.item_name_groups[game_name] = data["item_name_groups"]
|
||||
if "location_name_groups" in data:
|
||||
@@ -483,7 +485,7 @@ class Context:
|
||||
with open(self.save_filename, "wb") as f:
|
||||
f.write(zlib.compress(encoded_save))
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
self.logger.exception(e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -501,12 +503,12 @@ class Context:
|
||||
save_data = restricted_loads(zlib.decompress(f.read()))
|
||||
self.set_save(save_data)
|
||||
except FileNotFoundError:
|
||||
logging.error('No save data found, starting a new game')
|
||||
self.logger.error('No save data found, starting a new game')
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
self.logger.exception(e)
|
||||
self._start_async_saving()
|
||||
|
||||
def _start_async_saving(self):
|
||||
def _start_async_saving(self, atexit_save: bool = True):
|
||||
if not self.auto_saver_thread:
|
||||
def save_regularly():
|
||||
# time.time() is platform dependent, so using the expensive datetime method instead
|
||||
@@ -520,18 +522,19 @@ class Context:
|
||||
next_wakeup = (second - get_datetime_second()) % self.auto_save_interval
|
||||
time.sleep(max(1.0, next_wakeup))
|
||||
if self.save_dirty:
|
||||
logging.debug("Saving via thread.")
|
||||
self.logger.debug("Saving via thread.")
|
||||
self._save()
|
||||
except OperationalError as e:
|
||||
logging.exception(e)
|
||||
logging.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
|
||||
self.logger.exception(e)
|
||||
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
|
||||
else:
|
||||
self.save_dirty = False
|
||||
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
||||
self.auto_saver_thread.start()
|
||||
|
||||
import atexit
|
||||
atexit.register(self._save, True) # make sure we save on exit too
|
||||
if atexit_save:
|
||||
import atexit
|
||||
atexit.register(self._save, True) # make sure we save on exit too
|
||||
|
||||
def get_save(self) -> dict:
|
||||
self.recheck_hints()
|
||||
@@ -598,7 +601,7 @@ class Context:
|
||||
if "stored_data" in savedata:
|
||||
self.stored_data = savedata["stored_data"]
|
||||
# count items and slots from lists for items_handling = remote
|
||||
logging.info(
|
||||
self.logger.info(
|
||||
f'Loaded save file with {sum([len(v) for k, v in self.received_items.items() if k[2]])} received items '
|
||||
f'for {sum(k[2] for k in self.received_items)} players')
|
||||
|
||||
@@ -640,13 +643,13 @@ class Context:
|
||||
try:
|
||||
raise Exception(f"Could not set server option {key}, skipping.") from e
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||
self.logger.exception(e)
|
||||
self.logger.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||
setattr(self, key, value)
|
||||
elif key == "disable_item_cheat":
|
||||
self.item_cheat = not bool(value)
|
||||
else:
|
||||
logging.debug(f"Unrecognized server option {key}")
|
||||
self.logger.debug(f"Unrecognized server option {key}")
|
||||
|
||||
def get_aliased_name(self, team: int, slot: int):
|
||||
if (team, slot) in self.name_aliases:
|
||||
@@ -680,7 +683,7 @@ class Context:
|
||||
self.hints[team, player].add(hint)
|
||||
new_hint_events.add(player)
|
||||
|
||||
logging.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
|
||||
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
|
||||
for slot in new_hint_events:
|
||||
self.on_new_hint(team, slot)
|
||||
for slot, hint_data in concerns.items():
|
||||
@@ -688,7 +691,7 @@ class Context:
|
||||
clients = self.clients[team].get(slot)
|
||||
if not clients:
|
||||
continue
|
||||
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
|
||||
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)]
|
||||
for client in clients:
|
||||
async_start(self.send_msgs(client, client_hints))
|
||||
|
||||
@@ -739,21 +742,21 @@ async def server(websocket, path: str = "/", ctx: Context = None):
|
||||
|
||||
try:
|
||||
if ctx.log_network:
|
||||
logging.info("Incoming connection")
|
||||
ctx.logger.info("Incoming connection")
|
||||
await on_client_connected(ctx, client)
|
||||
if ctx.log_network:
|
||||
logging.info("Sent Room Info")
|
||||
ctx.logger.info("Sent Room Info")
|
||||
async for data in websocket:
|
||||
if ctx.log_network:
|
||||
logging.info(f"Incoming message: {data}")
|
||||
ctx.logger.info(f"Incoming message: {data}")
|
||||
for msg in decode(data):
|
||||
await process_client_cmd(ctx, client, msg)
|
||||
except Exception as e:
|
||||
if not isinstance(e, websockets.WebSocketException):
|
||||
logging.exception(e)
|
||||
ctx.logger.exception(e)
|
||||
finally:
|
||||
if ctx.log_network:
|
||||
logging.info("Disconnected")
|
||||
ctx.logger.info("Disconnected")
|
||||
await ctx.disconnect(client)
|
||||
|
||||
|
||||
@@ -803,14 +806,25 @@ async def on_client_disconnected(ctx: Context, client: Client):
|
||||
await on_client_left(ctx, client)
|
||||
|
||||
|
||||
_non_game_messages = {"HintGame": "hinting", "Tracker": "tracking", "TextOnly": "viewing"}
|
||||
""" { tag: ui_message } """
|
||||
|
||||
|
||||
async def on_client_joined(ctx: Context, client: Client):
|
||||
if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN:
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
|
||||
version_str = '.'.join(str(x) for x in client.version)
|
||||
verb = "tracking" if "Tracker" in client.tags else "playing"
|
||||
|
||||
for tag, verb in _non_game_messages.items():
|
||||
if tag in client.tags:
|
||||
final_verb = verb
|
||||
break
|
||||
else:
|
||||
final_verb = "playing"
|
||||
|
||||
ctx.broadcast_text_all(
|
||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
||||
f"{verb} {ctx.games[client.slot]} has joined. "
|
||||
f"{final_verb} {ctx.games[client.slot]} has joined. "
|
||||
f"Client({version_str}), {client.tags}.",
|
||||
{"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags})
|
||||
ctx.notify_client(client, "Now that you are connected, "
|
||||
@@ -825,8 +839,19 @@ async def on_client_left(ctx: Context, client: Client):
|
||||
if len(ctx.clients[client.team][client.slot]) < 1:
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
version_str = '.'.join(str(x) for x in client.version)
|
||||
|
||||
for tag, verb in _non_game_messages.items():
|
||||
if tag in client.tags:
|
||||
final_verb = f"stopped {verb}"
|
||||
break
|
||||
else:
|
||||
final_verb = "left"
|
||||
|
||||
ctx.broadcast_text_all(
|
||||
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1),
|
||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has {final_verb} the game. "
|
||||
f"Client({version_str}), {client.tags}.",
|
||||
{"type": "Part", "team": client.team, "slot": client.slot})
|
||||
|
||||
|
||||
@@ -963,7 +988,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
new_item = NetworkItem(item_id, location, slot, flags)
|
||||
send_items_to(ctx, team, target_player, new_item)
|
||||
|
||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
|
||||
ctx.player_names[(team, target_player)], ctx.location_names[location]))
|
||||
info_text = json_format_send_event(new_item, target_player)
|
||||
@@ -1507,15 +1532,13 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
|
||||
if hints:
|
||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||
old_hints = set(hints) - new_hints
|
||||
if old_hints:
|
||||
self.ctx.notify_hints(self.client.team, list(old_hints))
|
||||
if not new_hints:
|
||||
self.output("Hint was previously used, no points deducted.")
|
||||
old_hints = list(set(hints) - new_hints)
|
||||
if old_hints and not new_hints:
|
||||
self.ctx.notify_hints(self.client.team, old_hints)
|
||||
self.output("Hint was previously used, no points deducted.")
|
||||
if new_hints:
|
||||
found_hints = [hint for hint in new_hints if hint.found]
|
||||
not_found_hints = [hint for hint in new_hints if not hint.found]
|
||||
|
||||
if not not_found_hints: # everything's been found, no need to pay
|
||||
can_pay = 1000
|
||||
elif cost:
|
||||
@@ -1527,7 +1550,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
# By popular vote, make hints prefer non-local placements
|
||||
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
|
||||
|
||||
hints = found_hints
|
||||
hints = found_hints + old_hints
|
||||
while can_pay > 0:
|
||||
if not not_found_hints:
|
||||
break
|
||||
@@ -1537,6 +1560,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
|
||||
self.ctx.notify_hints(self.client.team, hints)
|
||||
if not_found_hints:
|
||||
if hints and cost and int((points_available // cost) == 0):
|
||||
self.output(
|
||||
@@ -1550,7 +1574,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output(f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
self.ctx.notify_hints(self.client.team, hints)
|
||||
self.ctx.save()
|
||||
return True
|
||||
|
||||
@@ -1605,7 +1628,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
try:
|
||||
cmd: str = args["cmd"]
|
||||
except:
|
||||
logging.exception(f"Could not get command from {args}")
|
||||
ctx.logger.exception(f"Could not get command from {args}")
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
|
||||
"text": f"Could not get command from {args} at `cmd`"}])
|
||||
raise
|
||||
@@ -1631,7 +1654,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
else:
|
||||
team, slot = ctx.connect_names[args['name']]
|
||||
game = ctx.games[slot]
|
||||
ignore_game = ("TextOnly" in args["tags"] or "Tracker" in args["tags"]) and not args.get("game")
|
||||
|
||||
ignore_game = not args.get("game") and any(tag in _non_game_messages for tag in args["tags"])
|
||||
|
||||
if not ignore_game and args['game'] != game:
|
||||
errors.add('InvalidGame')
|
||||
minver = min_client_version if ignore_game else ctx.minimum_client_versions[slot]
|
||||
@@ -1646,7 +1671,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
||||
errors.add('IncompatibleVersion')
|
||||
if errors:
|
||||
logging.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
|
||||
ctx.logger.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
|
||||
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
|
||||
else:
|
||||
team, slot = ctx.connect_names[args['name']]
|
||||
@@ -1905,7 +1930,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
@mark_raw
|
||||
def _cmd_alias(self, player_name_then_alias_name):
|
||||
"""Set a player's alias, by listing their base name and then their intended alias."""
|
||||
player_name, alias_name = player_name_then_alias_name.split(" ", 1)
|
||||
player_name, _, alias_name = player_name_then_alias_name.partition(" ")
|
||||
player_name, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
for (team, slot), name in self.ctx.player_names.items():
|
||||
@@ -2134,32 +2159,47 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
self.output(response)
|
||||
return False
|
||||
|
||||
def _cmd_option(self, option_name: str, option: str):
|
||||
"""Set options for the server."""
|
||||
|
||||
attrtype = self.ctx.simple_options.get(option_name, None)
|
||||
if attrtype:
|
||||
if attrtype == bool:
|
||||
def attrtype(input_text: str):
|
||||
return input_text.lower() not in {"off", "0", "false", "none", "null", "no"}
|
||||
elif attrtype == str and option_name.endswith("password"):
|
||||
def attrtype(input_text: str):
|
||||
if input_text.lower() in {"null", "none", '""', "''"}:
|
||||
return None
|
||||
return input_text
|
||||
setattr(self.ctx, option_name, attrtype(option))
|
||||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||
if option_name in {"release_mode", "remaining_mode", "collect_mode"}:
|
||||
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
|
||||
elif option_name in {"hint_cost", "location_check_points"}:
|
||||
self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}])
|
||||
return True
|
||||
else:
|
||||
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
|
||||
self.output(f"Unrecognized Option {option_name}, known: "
|
||||
f"{', '.join(known)}")
|
||||
def _cmd_option(self, option_name: str, option_value: str):
|
||||
"""Set an option for the server."""
|
||||
value_type = self.ctx.simple_options.get(option_name, None)
|
||||
if not value_type:
|
||||
known_options = (f"{option}: {option_type}" for option, option_type in self.ctx.simple_options.items())
|
||||
self.output(f"Unrecognized option '{option_name}', known: {', '.join(known_options)}")
|
||||
return False
|
||||
|
||||
if value_type == bool:
|
||||
def value_type(input_text: str):
|
||||
return input_text.lower() not in {"off", "0", "false", "none", "null", "no"}
|
||||
elif value_type == str and option_name.endswith("password"):
|
||||
def value_type(input_text: str):
|
||||
return None if input_text.lower() in {"null", "none", '""', "''"} else input_text
|
||||
elif value_type == str and option_name.endswith("mode"):
|
||||
valid_values = {"goal", "enabled", "disabled"}
|
||||
valid_values.update(("auto", "auto_enabled") if option_name != "remaining_mode" else [])
|
||||
if option_value.lower() not in valid_values:
|
||||
self.output(f"Unrecognized {option_name} value '{option_value}', known: {', '.join(valid_values)}")
|
||||
return False
|
||||
|
||||
setattr(self.ctx, option_name, value_type(option_value))
|
||||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||
if option_name in {"release_mode", "remaining_mode", "collect_mode"}:
|
||||
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
|
||||
elif option_name in {"hint_cost", "location_check_points"}:
|
||||
self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}])
|
||||
return True
|
||||
|
||||
def _cmd_datastore(self):
|
||||
"""Debug Tool: list writable datastorage keys and approximate the size of their values with pickle."""
|
||||
total: int = 0
|
||||
texts = []
|
||||
for key, value in self.ctx.stored_data.items():
|
||||
size = len(pickle.dumps(value))
|
||||
total += size
|
||||
texts.append(f"Key: {key} | Size: {size}B")
|
||||
texts.insert(0, f"Found {len(self.ctx.stored_data)} keys, "
|
||||
f"approximately totaling {Utils.format_SI_prefix(total, power=1024)}B")
|
||||
self.output("\n".join(texts))
|
||||
|
||||
|
||||
async def console(ctx: Context):
|
||||
import sys
|
||||
@@ -2183,7 +2223,7 @@ async def console(ctx: Context):
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
defaults = Utils.get_options()["server_options"].as_dict()
|
||||
defaults = Utils.get_settings()["server_options"].as_dict()
|
||||
parser.add_argument('multidata', nargs="?", default=defaults["multidata"])
|
||||
parser.add_argument('--host', default=defaults["host"])
|
||||
parser.add_argument('--port', default=defaults["port"], type=int)
|
||||
@@ -2249,7 +2289,7 @@ async def auto_shutdown(ctx, to_cancel=None):
|
||||
if to_cancel:
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
logging.info("Shutting down due to inactivity.")
|
||||
ctx.logger.info("Shutting down due to inactivity.")
|
||||
|
||||
while not ctx.exit_event.is_set():
|
||||
if not ctx.client_activity_timers.values():
|
||||
|
||||
72
Options.py
72
Options.py
@@ -21,6 +21,10 @@ if typing.TYPE_CHECKING:
|
||||
import pathlib
|
||||
|
||||
|
||||
class OptionError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Visibility(enum.IntFlag):
|
||||
none = 0b0000
|
||||
template = 0b0001
|
||||
@@ -136,12 +140,6 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
def current_key(self) -> str:
|
||||
return self.name_lookup[self.value]
|
||||
|
||||
def get_current_option_name(self) -> str:
|
||||
"""Deprecated. use current_option_name instead. TODO remove around 0.4"""
|
||||
logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated."
|
||||
f" use current_option_name instead. Worlds should use {self}.current_key"))
|
||||
return self.current_option_name
|
||||
|
||||
@property
|
||||
def current_option_name(self) -> str:
|
||||
"""For display purposes. Worlds should be using current_key."""
|
||||
@@ -384,7 +382,8 @@ class Toggle(NumericOption):
|
||||
default = 0
|
||||
|
||||
def __init__(self, value: int):
|
||||
assert value == 0 or value == 1, "value of Toggle can only be 0 or 1"
|
||||
# if user puts in an invalid value, make it valid
|
||||
value = int(bool(value))
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
@@ -745,39 +744,9 @@ class NamedRange(Range):
|
||||
return super().from_text(text)
|
||||
|
||||
|
||||
class SpecialRange(NamedRange):
|
||||
special_range_cutoff = 0
|
||||
|
||||
# TODO: remove class SpecialRange, earliest 3 releases after 0.4.3
|
||||
def __new__(cls, value: int) -> SpecialRange:
|
||||
from Utils import deprecate
|
||||
deprecate(f"Option type {cls.__name__} is a subclass of SpecialRange, which is deprecated and pending removal. "
|
||||
"Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In "
|
||||
"NamedRange, range_start specifies the lower end of the regular range, while special values can be "
|
||||
"placed anywhere (below, inside, or above the regular range).")
|
||||
return super().__new__(cls)
|
||||
|
||||
@classmethod
|
||||
def weighted_range(cls, text) -> Range:
|
||||
if text == "random-low":
|
||||
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.special_range_cutoff))
|
||||
elif text == "random-high":
|
||||
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end, cls.range_end))
|
||||
elif text == "random-middle":
|
||||
return cls(cls.triangular(cls.special_range_cutoff, cls.range_end))
|
||||
elif text.startswith("random-range-"):
|
||||
return cls.custom_range(text)
|
||||
elif text == "random":
|
||||
return cls(random.randint(cls.special_range_cutoff, cls.range_end))
|
||||
else:
|
||||
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||
f"Acceptable values are: random, random-high, random-middle, random-low, "
|
||||
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
|
||||
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
|
||||
|
||||
|
||||
class FreezeValidKeys(AssembleOptions):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
assert not "_valid_keys" in attrs, "'_valid_keys' gets set by FreezeValidKeys, define 'valid_keys' instead."
|
||||
if "valid_keys" in attrs:
|
||||
attrs["_valid_keys"] = frozenset(attrs["valid_keys"])
|
||||
return super(FreezeValidKeys, mcs).__new__(mcs, name, bases, attrs)
|
||||
@@ -979,7 +948,7 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
||||
def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]:
|
||||
"""
|
||||
Returns a dictionary of [str, Option.value]
|
||||
|
||||
|
||||
:param option_names: names of the options to return
|
||||
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
|
||||
"""
|
||||
@@ -1124,6 +1093,7 @@ class ItemLinks(OptionList):
|
||||
raise Exception(f"item_link {link['name']} has {intersection} "
|
||||
f"items in both its local_items and non_local_items pool.")
|
||||
link.setdefault("link_replacement", None)
|
||||
link["item_pool"] = list(pool)
|
||||
|
||||
|
||||
class Removed(FreeText):
|
||||
@@ -1154,6 +1124,14 @@ class DeathLinkMixin:
|
||||
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):
|
||||
import os
|
||||
|
||||
@@ -1192,15 +1170,21 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden or generate_hidden:
|
||||
all_options: typing.Dict[str, AssembleOptions] = {
|
||||
option_name: option for option_name, option in world.options_dataclass.type_hints.items()
|
||||
if option.visibility & Visibility.template
|
||||
}
|
||||
|
||||
option_groups = {option: option_group.name
|
||||
for option_group in world.web.option_groups
|
||||
for option in option_group.options}
|
||||
ordered_groups = ["Game Options"]
|
||||
[ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups]
|
||||
grouped_options = {group: {} for group in ordered_groups}
|
||||
for option_name, option in world.options_dataclass.type_hints.items():
|
||||
if option.visibility >= Visibility.template:
|
||||
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
||||
|
||||
with open(local_path("data", "options.yaml")) as f:
|
||||
file_data = f.read()
|
||||
res = Template(file_data).render(
|
||||
options=all_options,
|
||||
option_groups=grouped_options,
|
||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||
dictify_range=dictify_range,
|
||||
)
|
||||
|
||||
@@ -65,6 +65,9 @@ Currently, the following games are supported:
|
||||
* Castlevania 64
|
||||
* A Short Hike
|
||||
* Yoshi's Island
|
||||
* Mario & Luigi: Superstar Saga
|
||||
* Bomb Rush Cyberfunk
|
||||
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
@@ -85,6 +85,7 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
|
||||
"""Close connection to a currently connected snes"""
|
||||
self.ctx.snes_reconnect_address = None
|
||||
self.ctx.cancel_snes_autoreconnect()
|
||||
self.ctx.snes_state = SNESState.SNES_DISCONNECTED
|
||||
if self.ctx.snes_socket and not self.ctx.snes_socket.closed:
|
||||
async_start(self.ctx.snes_socket.close())
|
||||
return True
|
||||
@@ -281,7 +282,7 @@ class SNESState(enum.IntEnum):
|
||||
|
||||
|
||||
def launch_sni() -> None:
|
||||
sni_path = Utils.get_options()["sni_options"]["sni_path"]
|
||||
sni_path = Utils.get_settings()["sni_options"]["sni_path"]
|
||||
|
||||
if not os.path.isdir(sni_path):
|
||||
sni_path = Utils.local_path(sni_path)
|
||||
@@ -564,7 +565,7 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
|
||||
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||
try:
|
||||
for address, data in write_list:
|
||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
|
||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||
await ctx.snes_socket.send(data)
|
||||
@@ -653,7 +654,7 @@ async def game_watcher(ctx: SNIContext) -> None:
|
||||
|
||||
async def run_game(romfile: str) -> None:
|
||||
auto_start = typing.cast(typing.Union[bool, str],
|
||||
Utils.get_options()["sni_options"].get("snes_rom_start", True))
|
||||
Utils.get_settings()["sni_options"].get("snes_rom_start", True))
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
|
||||
4
Utils.py
4
Utils.py
@@ -201,7 +201,7 @@ def cache_path(*path: str) -> str:
|
||||
def output_path(*path: str) -> str:
|
||||
if hasattr(output_path, 'cached_path'):
|
||||
return os.path.join(output_path.cached_path, *path)
|
||||
output_path.cached_path = user_path(get_options()["general_options"]["output_path"])
|
||||
output_path.cached_path = user_path(get_settings()["general_options"]["output_path"])
|
||||
path = os.path.join(output_path.cached_path, *path)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
return path
|
||||
@@ -619,6 +619,8 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty
|
||||
|
||||
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], suggest: str = "") \
|
||||
-> typing.Optional[str]:
|
||||
logging.info(f"Opening file input dialog for {title}.")
|
||||
|
||||
def run(*args: str):
|
||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||
|
||||
|
||||
12
WebHost.py
12
WebHost.py
@@ -23,7 +23,6 @@ def get_app():
|
||||
from WebHostLib import register, cache, app as raw_app
|
||||
from WebHostLib.models import db
|
||||
|
||||
register()
|
||||
app = raw_app
|
||||
if os.path.exists(configpath) and not app.config["TESTING"]:
|
||||
import yaml
|
||||
@@ -34,6 +33,7 @@ def get_app():
|
||||
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
||||
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
|
||||
|
||||
register()
|
||||
cache.init_app(app)
|
||||
db.bind(**app.config["PONY"])
|
||||
db.generate_mapping(create_tables=True)
|
||||
@@ -117,7 +117,7 @@ if __name__ == "__main__":
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
||||
|
||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||
from WebHostLib.autolauncher import autohost, autogen
|
||||
from WebHostLib.autolauncher import autohost, autogen, stop
|
||||
from WebHostLib.options import create as create_options_files
|
||||
|
||||
try:
|
||||
@@ -138,3 +138,11 @@ if __name__ == "__main__":
|
||||
else:
|
||||
from waitress import serve
|
||||
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
|
||||
else:
|
||||
from time import sleep
|
||||
try:
|
||||
while True:
|
||||
sleep(1) # wait for process to be killed
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
pass
|
||||
stop() # stop worker threads
|
||||
|
||||
@@ -23,6 +23,7 @@ app.jinja_env.filters['all'] = all
|
||||
|
||||
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
||||
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
|
||||
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
|
||||
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
||||
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
||||
@@ -51,6 +52,7 @@ app.config["PONY"] = {
|
||||
app.config["MAX_ROLL"] = 20
|
||||
app.config["CACHE_TYPE"] = "SimpleCache"
|
||||
app.config["HOST_ADDRESS"] = ""
|
||||
app.config["ASSET_RIGHTS"] = False
|
||||
|
||||
cache = Cache()
|
||||
Compress(app)
|
||||
@@ -82,6 +84,6 @@ def register():
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
# to trigger app routing picking up on it
|
||||
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc
|
||||
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options
|
||||
|
||||
app.register_blueprint(api.api_endpoints)
|
||||
|
||||
@@ -3,26 +3,25 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
from uuid import UUID
|
||||
from datetime import timedelta, datetime
|
||||
from threading import Event, Thread
|
||||
from uuid import UUID
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
|
||||
from Utils import restricted_loads
|
||||
from .locker import Locker, AlreadyRunningException
|
||||
|
||||
_stop_event = Event()
|
||||
|
||||
def launch_room(room: Room, config: dict):
|
||||
# requires db_session!
|
||||
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout):
|
||||
multiworld = multiworlds.get(room.id, None)
|
||||
if not multiworld:
|
||||
multiworld = MultiworldInstance(room, config)
|
||||
|
||||
multiworld.start()
|
||||
def stop():
|
||||
"""Stops previously launched threads"""
|
||||
global _stop_event
|
||||
stop_event = _stop_event
|
||||
_stop_event = Event() # new event for new threads
|
||||
stop_event.set()
|
||||
|
||||
|
||||
def handle_generation_success(seed_id):
|
||||
@@ -59,39 +58,50 @@ def init_db(pony_config: dict):
|
||||
db.generate_mapping()
|
||||
|
||||
|
||||
def cleanup():
|
||||
"""delete unowned user-content"""
|
||||
with db_session:
|
||||
# >>> bool(uuid.UUID(int=0))
|
||||
# True
|
||||
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
|
||||
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
|
||||
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
|
||||
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
|
||||
if rooms or seeds or slots:
|
||||
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
|
||||
|
||||
|
||||
def autohost(config: dict):
|
||||
def keep_running():
|
||||
stop_event = _stop_event
|
||||
try:
|
||||
with Locker("autohost"):
|
||||
# delete unowned user-content
|
||||
with db_session:
|
||||
# >>> bool(uuid.UUID(int=0))
|
||||
# True
|
||||
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
|
||||
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
|
||||
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
|
||||
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
|
||||
if rooms or seeds or slots:
|
||||
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
|
||||
run_guardian()
|
||||
while 1:
|
||||
time.sleep(0.1)
|
||||
cleanup()
|
||||
hosters = []
|
||||
for x in range(config["HOSTERS"]):
|
||||
hoster = MultiworldInstance(config, x)
|
||||
hosters.append(hoster)
|
||||
hoster.start()
|
||||
|
||||
while not stop_event.wait(0.1):
|
||||
with db_session:
|
||||
rooms = select(
|
||||
room for room in Room if
|
||||
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
||||
for room in rooms:
|
||||
launch_room(room, config)
|
||||
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
|
||||
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
|
||||
hosters[room.id.int % len(hosters)].start_room(room.id)
|
||||
|
||||
except AlreadyRunningException:
|
||||
logging.info("Autohost reports as already running, not starting another.")
|
||||
|
||||
import threading
|
||||
threading.Thread(target=keep_running, name="AP_Autohost").start()
|
||||
Thread(target=keep_running, name="AP_Autohost").start()
|
||||
|
||||
|
||||
def autogen(config: dict):
|
||||
def keep_running():
|
||||
stop_event = _stop_event
|
||||
try:
|
||||
with Locker("autogen"):
|
||||
|
||||
@@ -112,8 +122,7 @@ def autogen(config: dict):
|
||||
commit()
|
||||
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
|
||||
|
||||
while 1:
|
||||
time.sleep(0.1)
|
||||
while not stop_event.wait(0.1):
|
||||
with db_session:
|
||||
# for update locks the database row(s) during transaction, preventing writes from elsewhere
|
||||
to_start = select(
|
||||
@@ -124,37 +133,45 @@ def autogen(config: dict):
|
||||
except AlreadyRunningException:
|
||||
logging.info("Autogen reports as already running, not starting another.")
|
||||
|
||||
import threading
|
||||
threading.Thread(target=keep_running, name="AP_Autogen").start()
|
||||
Thread(target=keep_running, name="AP_Autogen").start()
|
||||
|
||||
|
||||
multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {}
|
||||
|
||||
|
||||
class MultiworldInstance():
|
||||
def __init__(self, room: Room, config: dict):
|
||||
self.room_id = room.id
|
||||
def __init__(self, config: dict, id: int):
|
||||
self.room_ids = set()
|
||||
self.process: typing.Optional[multiprocessing.Process] = None
|
||||
with guardian_lock:
|
||||
multiworlds[self.room_id] = self
|
||||
self.ponyconfig = config["PONY"]
|
||||
self.cert = config["SELFLAUNCHCERT"]
|
||||
self.key = config["SELFLAUNCHKEY"]
|
||||
self.host = config["HOST_ADDRESS"]
|
||||
self.rooms_to_start = multiprocessing.Queue()
|
||||
self.rooms_shutting_down = multiprocessing.Queue()
|
||||
self.name = f"MultiHoster{id}"
|
||||
|
||||
def start(self):
|
||||
if self.process and self.process.is_alive():
|
||||
return False
|
||||
|
||||
logging.info(f"Spinning up {self.room_id}")
|
||||
process = multiprocessing.Process(group=None, target=run_server_process,
|
||||
args=(self.room_id, self.ponyconfig, get_static_server_data(),
|
||||
self.cert, self.key, self.host),
|
||||
name="MultiHost")
|
||||
args=(self.name, self.ponyconfig, get_static_server_data(),
|
||||
self.cert, self.key, self.host,
|
||||
self.rooms_to_start, self.rooms_shutting_down),
|
||||
name=self.name)
|
||||
process.start()
|
||||
# bind after start to prevent thread sync issues with guardian.
|
||||
self.process = process
|
||||
|
||||
def start_room(self, room_id):
|
||||
while not self.rooms_shutting_down.empty():
|
||||
self.room_ids.remove(self.rooms_shutting_down.get(block=True, timeout=None))
|
||||
if room_id in self.room_ids:
|
||||
pass # should already be hosted currently.
|
||||
else:
|
||||
self.room_ids.add(room_id)
|
||||
self.rooms_to_start.put(room_id)
|
||||
|
||||
def stop(self):
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
@@ -168,40 +185,6 @@ class MultiworldInstance():
|
||||
self.process = None
|
||||
|
||||
|
||||
guardian = None
|
||||
guardian_lock = threading.Lock()
|
||||
|
||||
|
||||
def run_guardian():
|
||||
global guardian
|
||||
global multiworlds
|
||||
with guardian_lock:
|
||||
if not guardian:
|
||||
try:
|
||||
import resource
|
||||
except ModuleNotFoundError:
|
||||
pass # unix only module
|
||||
else:
|
||||
# Each Server is another file handle, so request as many as we can from the system
|
||||
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
|
||||
# set soft limit to hard limit
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
|
||||
|
||||
def guard():
|
||||
while 1:
|
||||
time.sleep(1)
|
||||
done = []
|
||||
with guardian_lock:
|
||||
for key, instance in multiworlds.items():
|
||||
if instance.done():
|
||||
instance.collect()
|
||||
done.append(key)
|
||||
for key in done:
|
||||
del (multiworlds[key])
|
||||
|
||||
guardian = threading.Thread(name="Guardian", target=guard)
|
||||
|
||||
|
||||
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed, Slot
|
||||
from .customserver import run_server_process, get_static_server_data
|
||||
from .generate import gen_game
|
||||
|
||||
@@ -5,6 +5,7 @@ import collections
|
||||
import datetime
|
||||
import functools
|
||||
import logging
|
||||
import multiprocessing
|
||||
import pickle
|
||||
import random
|
||||
import socket
|
||||
@@ -53,17 +54,19 @@ del MultiServer
|
||||
|
||||
class DBCommandProcessor(ServerCommandProcessor):
|
||||
def output(self, text: str):
|
||||
logging.info(text)
|
||||
self.ctx.logger.info(text)
|
||||
|
||||
|
||||
class WebHostContext(Context):
|
||||
room_id: int
|
||||
|
||||
def __init__(self, static_server_data: dict):
|
||||
def __init__(self, static_server_data: dict, logger: logging.Logger):
|
||||
# static server data is used during _load_game_data to load required data,
|
||||
# without needing to import worlds system, which takes quite a bit of memory
|
||||
self.static_server_data = static_server_data
|
||||
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
|
||||
super(WebHostContext, self).__init__("", 0, "", "", 1,
|
||||
40, True, "enabled", "enabled",
|
||||
"enabled", 0, 2, logger=logger)
|
||||
del self.static_server_data
|
||||
self.main_loop = asyncio.get_running_loop()
|
||||
self.video = {}
|
||||
@@ -71,6 +74,7 @@ class WebHostContext(Context):
|
||||
|
||||
def _load_game_data(self):
|
||||
for key, value in self.static_server_data.items():
|
||||
# NOTE: attributes are mutable and shared, so they will have to be copied before being modified
|
||||
setattr(self, key, value)
|
||||
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
||||
|
||||
@@ -98,18 +102,37 @@ class WebHostContext(Context):
|
||||
|
||||
multidata = self.decompress(room.seed.multidata)
|
||||
game_data_packages = {}
|
||||
|
||||
static_gamespackage = self.gamespackage # this is shared across all rooms
|
||||
static_item_name_groups = self.item_name_groups
|
||||
static_location_name_groups = self.location_name_groups
|
||||
self.gamespackage = {"Archipelago": static_gamespackage["Archipelago"]} # this may be modified by _load
|
||||
self.item_name_groups = {}
|
||||
self.location_name_groups = {}
|
||||
|
||||
for game in list(multidata.get("datapackage", {})):
|
||||
game_data = multidata["datapackage"][game]
|
||||
if "checksum" in game_data:
|
||||
if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
||||
# non-custom. remove from multidata
|
||||
if static_gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
||||
# non-custom. remove from multidata and use static data
|
||||
# games package could be dropped from static data once all rooms embed data package
|
||||
del multidata["datapackage"][game]
|
||||
else:
|
||||
row = GameDataPackage.get(checksum=game_data["checksum"])
|
||||
if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete
|
||||
game_data_packages[game] = Utils.restricted_loads(row.data)
|
||||
continue
|
||||
else:
|
||||
self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}")
|
||||
self.gamespackage[game] = static_gamespackage.get(game, {})
|
||||
self.item_name_groups[game] = static_item_name_groups.get(game, {})
|
||||
self.location_name_groups[game] = static_location_name_groups.get(game, {})
|
||||
|
||||
if not game_data_packages:
|
||||
# all static -> use the static dicts directly
|
||||
self.gamespackage = static_gamespackage
|
||||
self.item_name_groups = static_item_name_groups
|
||||
self.location_name_groups = static_location_name_groups
|
||||
return self._load(multidata, game_data_packages, True)
|
||||
|
||||
@db_session
|
||||
@@ -119,7 +142,7 @@ class WebHostContext(Context):
|
||||
savegame_data = Room.get(id=self.room_id).multisave
|
||||
if savegame_data:
|
||||
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
||||
self._start_async_saving()
|
||||
self._start_async_saving(atexit_save=False)
|
||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
||||
|
||||
@db_session
|
||||
@@ -159,72 +182,121 @@ def get_static_server_data() -> dict:
|
||||
return data
|
||||
|
||||
|
||||
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
def set_up_logging(room_id) -> logging.Logger:
|
||||
import os
|
||||
# logger setup
|
||||
logger = logging.getLogger(f"RoomLogger {room_id}")
|
||||
|
||||
# this *should* be empty, but just in case.
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
file_handler = logging.FileHandler(
|
||||
os.path.join(Utils.user_path("logs"), f"{room_id}.txt"),
|
||||
"a",
|
||||
encoding="utf-8-sig")
|
||||
file_handler.setFormatter(logging.Formatter("[%(asctime)s]: %(message)s"))
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.addHandler(file_handler)
|
||||
return logger
|
||||
|
||||
|
||||
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||
host: str):
|
||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
||||
Utils.init_logging(name)
|
||||
try:
|
||||
import resource
|
||||
except ModuleNotFoundError:
|
||||
pass # unix only module
|
||||
else:
|
||||
# Each Server is another file handle, so request as many as we can from the system
|
||||
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
|
||||
# set soft limit to hard limit
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
|
||||
del resource, file_limit
|
||||
|
||||
# establish DB connection for multidata and multisave
|
||||
db.bind(**ponyconfig)
|
||||
db.generate_mapping(check_tables=False)
|
||||
|
||||
async def main():
|
||||
if "worlds" in sys.modules:
|
||||
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||
if "worlds" in sys.modules:
|
||||
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||
|
||||
import gc
|
||||
Utils.init_logging(str(room_id), write_mode="a")
|
||||
ctx = WebHostContext(static_server_data)
|
||||
ctx.load(room_id)
|
||||
ctx.init_save()
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||
gc.collect() # free intermediate objects used during setup
|
||||
try:
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||
import gc
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||
del cert_file, cert_key_file, ponyconfig
|
||||
gc.collect() # free intermediate objects used during setup
|
||||
|
||||
await ctx.server
|
||||
except OSError: # likely port in use
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
await ctx.server
|
||||
port = 0
|
||||
for wssocket in ctx.server.ws_server.sockets:
|
||||
socketname = wssocket.getsockname()
|
||||
if wssocket.family == socket.AF_INET6:
|
||||
# Prefer IPv4, as most users seem to not have working ipv6 support
|
||||
if not port:
|
||||
port = socketname[1]
|
||||
elif wssocket.family == socket.AF_INET:
|
||||
port = socketname[1]
|
||||
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
|
||||
async def start_room(room_id):
|
||||
with Locker(f"RoomLocker {room_id}"):
|
||||
try:
|
||||
logger = set_up_logging(room_id)
|
||||
ctx = WebHostContext(static_server_data, logger)
|
||||
ctx.load(room_id)
|
||||
ctx.init_save()
|
||||
try:
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||
|
||||
# ensure auto launch is on the same page in regard to room activity.
|
||||
with db_session:
|
||||
room: Room = Room.get(id=ctx.room_id)
|
||||
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
|
||||
await ctx.server
|
||||
except OSError: # likely port in use
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||
|
||||
logging.info("Shutting down")
|
||||
await ctx.server
|
||||
port = 0
|
||||
for wssocket in ctx.server.ws_server.sockets:
|
||||
socketname = wssocket.getsockname()
|
||||
if wssocket.family == socket.AF_INET6:
|
||||
# Prefer IPv4, as most users seem to not have working ipv6 support
|
||||
if not port:
|
||||
port = socketname[1]
|
||||
elif wssocket.family == socket.AF_INET:
|
||||
port = socketname[1]
|
||||
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
|
||||
|
||||
with Locker(room_id):
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||
except Exception:
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
room.last_port = -1
|
||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||
raise
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
except Exception:
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
room.last_port = -1
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
ctx._save()
|
||||
with (db_session):
|
||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||
room = Room.get(id=room_id)
|
||||
room.last_activity = datetime.datetime.utcnow() - \
|
||||
datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||
logging.info(f"Shutting down room {room_id} on {name}.")
|
||||
finally:
|
||||
await asyncio.sleep(5)
|
||||
rooms_shutting_down.put(room_id)
|
||||
|
||||
class Starter(threading.Thread):
|
||||
def run(self):
|
||||
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,37 +70,41 @@ def generate(race=False):
|
||||
flash(options)
|
||||
else:
|
||||
meta = get_meta(request.form, race)
|
||||
results, gen_options = roll_options(options, set(meta["plando_options"]))
|
||||
|
||||
if any(type(result) == str for result in results.values()):
|
||||
return render_template("checkResult.html", results=results)
|
||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
|
||||
f"If you have a larger group, please generate it yourself and upload it.")
|
||||
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
|
||||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# convert to json compatible
|
||||
meta=json.dumps(meta),
|
||||
state=STATE_QUEUED,
|
||||
owner=session["_id"])
|
||||
commit()
|
||||
|
||||
return redirect(url_for("wait_seed", seed=gen.id))
|
||||
else:
|
||||
try:
|
||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||
meta=meta, owner=session["_id"].int)
|
||||
except BaseException as e:
|
||||
from .autolauncher import handle_generation_failure
|
||||
handle_generation_failure(e)
|
||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
||||
|
||||
return redirect(url_for("view_seed", seed=seed_id))
|
||||
return start_generation(options, meta)
|
||||
|
||||
return render_template("generate.html", race=race, version=__version__)
|
||||
|
||||
|
||||
def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any]):
|
||||
results, gen_options = roll_options(options, set(meta["plando_options"]))
|
||||
|
||||
if any(type(result) == str for result in results.values()):
|
||||
return render_template("checkResult.html", results=results)
|
||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
|
||||
f"If you have a larger group, please generate it yourself and upload it.")
|
||||
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
|
||||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# convert to json compatible
|
||||
meta=json.dumps(meta),
|
||||
state=STATE_QUEUED,
|
||||
owner=session["_id"])
|
||||
commit()
|
||||
|
||||
return redirect(url_for("wait_seed", seed=gen.id))
|
||||
else:
|
||||
try:
|
||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||
meta=meta, owner=session["_id"].int)
|
||||
except BaseException as e:
|
||||
from .autolauncher import handle_generation_failure
|
||||
handle_generation_failure(e)
|
||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
||||
|
||||
return redirect(url_for("view_seed", seed=seed_id))
|
||||
|
||||
|
||||
def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
|
||||
if not meta:
|
||||
meta: Dict[str, Any] = {}
|
||||
|
||||
@@ -37,25 +37,6 @@ def start_playing():
|
||||
return render_template(f"startPlaying.html")
|
||||
|
||||
|
||||
# TODO for back compat. remove around 0.4.5
|
||||
@app.route("/weighted-settings")
|
||||
def weighted_settings():
|
||||
return redirect("weighted-options", 301)
|
||||
|
||||
|
||||
@app.route("/weighted-options")
|
||||
@cache.cached()
|
||||
def weighted_options():
|
||||
return render_template("weighted-options.html")
|
||||
|
||||
|
||||
# Player options pages
|
||||
@app.route("/games/<string:game>/player-options")
|
||||
@cache.cached()
|
||||
def player_options(game: str):
|
||||
return render_template("player-options.html", game=game, theme=get_world_theme(game))
|
||||
|
||||
|
||||
# Game Info Pages
|
||||
@app.route('/games/<string:game>/info/<string:lang>')
|
||||
@cache.cached()
|
||||
@@ -150,6 +131,7 @@ def host_room(room: UUID):
|
||||
if cmd:
|
||||
Command(room=room, commandtext=cmd)
|
||||
commit()
|
||||
return redirect(url_for("host_room", room=room.id))
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
# indicate that the page should reload to get the assigned port
|
||||
|
||||
@@ -1,205 +1,226 @@
|
||||
import collections.abc
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import typing
|
||||
from textwrap import dedent
|
||||
from typing import Dict, Union
|
||||
|
||||
import yaml
|
||||
from flask import redirect, render_template, request, Response
|
||||
|
||||
import Options
|
||||
from Utils import local_path
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
||||
"exclude_locations", "priority_locations"}
|
||||
from . import app, cache
|
||||
|
||||
|
||||
def create():
|
||||
def create() -> None:
|
||||
target_folder = local_path("WebHostLib", "static", "generated")
|
||||
yaml_folder = os.path.join(target_folder, "configs")
|
||||
|
||||
Options.generate_yaml_templates(yaml_folder)
|
||||
|
||||
def get_html_doc(option_type: type(Options.Option)) -> str:
|
||||
if not option_type.__doc__:
|
||||
return "Please document me!"
|
||||
return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip()
|
||||
|
||||
weighted_options = {
|
||||
"baseOptions": {
|
||||
"description": "Generated by https://archipelago.gg/",
|
||||
"name": "",
|
||||
"game": {},
|
||||
},
|
||||
"games": {},
|
||||
}
|
||||
def get_world_theme(game_name: str) -> str:
|
||||
if game_name in AutoWorldRegister.world_types:
|
||||
return AutoWorldRegister.world_types[game_name].web.theme
|
||||
return 'grass'
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
|
||||
all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints
|
||||
def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]:
|
||||
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")
|
||||
|
||||
# Generate JSON files for player-options pages
|
||||
player_options = {
|
||||
"baseOptions": {
|
||||
"description": f"Generated by https://archipelago.gg/ for {game_name}",
|
||||
"game": game_name,
|
||||
"name": "",
|
||||
},
|
||||
}
|
||||
option_groups = {option: option_group.name
|
||||
for option_group in world.web.option_groups
|
||||
for option in option_group.options}
|
||||
ordered_groups = ["Game Options", *[group.name for group in world.web.option_groups]]
|
||||
grouped_options = {group: {} for group in ordered_groups}
|
||||
for option_name, option in world.options_dataclass.type_hints.items():
|
||||
# Exclude settings from options pages if their visibility is disabled
|
||||
if visibility_flag in option.visibility:
|
||||
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
||||
|
||||
game_options = {}
|
||||
visible: typing.Set[str] = set()
|
||||
visible_weighted: typing.Set[str] = set()
|
||||
return render_template(
|
||||
template,
|
||||
world_name=world_name,
|
||||
world=world,
|
||||
option_groups=grouped_options,
|
||||
issubclass=issubclass,
|
||||
Options=Options,
|
||||
theme=get_world_theme(world_name),
|
||||
)
|
||||
|
||||
for option_name, option in all_options.items():
|
||||
if option.visibility & Options.Visibility.simple_ui:
|
||||
visible.add(option_name)
|
||||
if option.visibility & Options.Visibility.complex_ui:
|
||||
visible_weighted.add(option_name)
|
||||
|
||||
if option_name in handled_in_js:
|
||||
pass
|
||||
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"]})
|
||||
|
||||
elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle):
|
||||
game_options[option_name] = this_option = {
|
||||
"type": "select",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": get_html_doc(option),
|
||||
"defaultValue": None,
|
||||
"options": []
|
||||
}
|
||||
|
||||
for sub_option_id, sub_option_name in option.name_lookup.items():
|
||||
if sub_option_name != "random":
|
||||
this_option["options"].append({
|
||||
"name": option.get_option_name(sub_option_id),
|
||||
"value": sub_option_name,
|
||||
})
|
||||
if sub_option_id == option.default:
|
||||
this_option["defaultValue"] = sub_option_name
|
||||
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
|
||||
|
||||
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,
|
||||
}
|
||||
@app.template_filter("dedent")
|
||||
def filter_dedent(text: str) -> str:
|
||||
return dedent(text).strip("\n ")
|
||||
|
||||
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)
|
||||
}
|
||||
@app.template_test("ordered")
|
||||
def test_ordered(obj):
|
||||
return isinstance(obj, collections.abc.Sequence)
|
||||
|
||||
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 []
|
||||
}
|
||||
@app.route("/games/<string:game>/option-presets", methods=["GET"])
|
||||
@cache.cached()
|
||||
def option_presets(game: str) -> Response:
|
||||
world = AutoWorldRegister.world_types[game]
|
||||
|
||||
else:
|
||||
logging.debug(f"{option} not exported to Web Options.")
|
||||
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)
|
||||
|
||||
player_options["presetOptions"] = {}
|
||||
for preset_name, preset in world.web.options_presets.items():
|
||||
player_options["presetOptions"][preset_name] = {}
|
||||
for option_name, option_value in preset.items():
|
||||
# Random range type settings are not valid.
|
||||
assert (not str(option_value).startswith("random-")), \
|
||||
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. Special random " \
|
||||
f"values are not supported for presets."
|
||||
json_data = json.dumps(world.web.options_presets, cls=SetEncoder)
|
||||
response = Response(json_data)
|
||||
response.headers["Content-Type"] = "application/json"
|
||||
return response
|
||||
|
||||
# Normal random is supported, but needs to be handled explicitly.
|
||||
if option_value == "random":
|
||||
player_options["presetOptions"][preset_name][option_name] = option_value
|
||||
|
||||
@app.route("/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
|
||||
|
||||
option = world.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
if isinstance(option, Options.NamedRange) and isinstance(option_value, str):
|
||||
assert option_value in option.special_range_names, \
|
||||
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \
|
||||
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
|
||||
options[key] = val
|
||||
else:
|
||||
if int(val) == 0:
|
||||
continue
|
||||
|
||||
# Still use the true value for the option, not the name.
|
||||
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||
elif isinstance(option, Options.Range):
|
||||
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||
elif isinstance(option_value, str):
|
||||
# For Choice and Toggle options, the value should be the name of the option. This is to prevent
|
||||
# setting a preset for an option with an overridden from_text method that would normally be okay,
|
||||
# but would not be okay for the webhost's current implementation of player options UI.
|
||||
assert option.name_lookup[option.value] == option_value, \
|
||||
f"Invalid option value '{option_value}' for '{option_name}' in preset '{preset_name}'. " \
|
||||
f"Values must not be resolved to a different option via option.from_text (or an alias)."
|
||||
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||
else:
|
||||
# int and bool values are fine, just resolve them to the current key for webhost.
|
||||
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||
[option, setting] = key.split("||")
|
||||
options.setdefault(option, {})[setting] = int(val)
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
|
||||
# Error checking
|
||||
if "name" not in options:
|
||||
return "Player name is required."
|
||||
|
||||
filtered_player_options = player_options
|
||||
filtered_player_options["gameOptions"] = {
|
||||
option_name: option_data for option_name, option_data in game_options.items()
|
||||
if option_name in visible
|
||||
# Remove POST data irrelevant to YAML
|
||||
if "intent-generate" in options:
|
||||
intent_generate = True
|
||||
del options["intent-generate"]
|
||||
if "intent-export" in options:
|
||||
del options["intent-export"]
|
||||
|
||||
# Properly format YAML output
|
||||
player_name = options["name"]
|
||||
del options["name"]
|
||||
|
||||
formatted_options = {
|
||||
"name": player_name,
|
||||
"game": game,
|
||||
"description": f"Generated by https://archipelago.gg/ for {game}",
|
||||
game: options,
|
||||
}
|
||||
|
||||
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
|
||||
json.dump(filtered_player_options, f, indent=2, separators=(',', ': '))
|
||||
if intent_generate:
|
||||
return generate_game({player_name: formatted_options})
|
||||
|
||||
filtered_player_options["gameOptions"] = {
|
||||
option_name: option_data for option_name, option_data in game_options.items()
|
||||
if option_name in visible_weighted
|
||||
else:
|
||||
return send_yaml(player_name, formatted_options)
|
||||
|
||||
|
||||
# Player options pages
|
||||
@app.route("/games/<string:game>/player-options")
|
||||
@cache.cached()
|
||||
def player_options(game: str):
|
||||
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
|
||||
|
||||
|
||||
# YAML generator for player-options
|
||||
@app.route("/games/<string:game>/generate-yaml", methods=["POST"])
|
||||
def generate_yaml(game: str):
|
||||
if request.method == "POST":
|
||||
options = {}
|
||||
intent_generate = False
|
||||
for key, val in request.form.items(multi=True):
|
||||
if key in options:
|
||||
if not isinstance(options[key], list):
|
||||
options[key] = [options[key]]
|
||||
options[key].append(val)
|
||||
else:
|
||||
options[key] = val
|
||||
|
||||
# Detect and build ItemDict options from their name pattern
|
||||
for key, val in options.copy().items():
|
||||
key_parts = key.rsplit("||", 2)
|
||||
if key_parts[-1] == "qty":
|
||||
if key_parts[0] not in options:
|
||||
options[key_parts[0]] = {}
|
||||
if val != "0":
|
||||
options[key_parts[0]][key_parts[1]] = int(val)
|
||||
del options[key]
|
||||
|
||||
# Detect random-* keys and set their options accordingly
|
||||
for key, val in options.copy().items():
|
||||
if key.startswith("random-"):
|
||||
options[key.removeprefix("random-")] = "random"
|
||||
del options[key]
|
||||
|
||||
# Error checking
|
||||
if not options["name"]:
|
||||
return "Player name is required."
|
||||
|
||||
# Remove POST data irrelevant to YAML
|
||||
preset_name = 'default'
|
||||
if "intent-generate" in options:
|
||||
intent_generate = True
|
||||
del options["intent-generate"]
|
||||
if "intent-export" in options:
|
||||
del options["intent-export"]
|
||||
if "game-options-preset" in options:
|
||||
preset_name = options["game-options-preset"]
|
||||
del options["game-options-preset"]
|
||||
|
||||
# Properly format YAML output
|
||||
player_name = options["name"]
|
||||
del options["name"]
|
||||
|
||||
description = f"Generated by https://archipelago.gg/ for {game}"
|
||||
if preset_name != 'default' and preset_name != 'custom':
|
||||
description += f" using {preset_name} preset"
|
||||
|
||||
formatted_options = {
|
||||
"name": player_name,
|
||||
"game": game,
|
||||
"description": description,
|
||||
game: options,
|
||||
}
|
||||
|
||||
if not world.hidden and world.web.options_page is True:
|
||||
# Add the random option to Choice, TextChoice, and Toggle options
|
||||
for option in filtered_player_options["gameOptions"].values():
|
||||
if option["type"] == "select":
|
||||
option["options"].append({"name": "Random", "value": "random"})
|
||||
|
||||
if not option["defaultValue"]:
|
||||
option["defaultValue"] = "random"
|
||||
|
||||
weighted_options["baseOptions"]["game"][game_name] = 0
|
||||
weighted_options["games"][game_name] = {
|
||||
"gameSettings": filtered_player_options["gameOptions"],
|
||||
"gameItems": tuple(world.item_names),
|
||||
"gameItemGroups": [
|
||||
group for group in world.item_name_groups.keys() if group != "Everything"
|
||||
],
|
||||
"gameItemDescriptions": world.item_descriptions,
|
||||
"gameLocations": tuple(world.location_names),
|
||||
"gameLocationGroups": [
|
||||
group for group in world.location_name_groups.keys() if group != "Everywhere"
|
||||
],
|
||||
"gameLocationDescriptions": world.location_descriptions,
|
||||
}
|
||||
|
||||
with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f:
|
||||
json.dump(weighted_options, f, indent=2, separators=(',', ': '))
|
||||
if intent_generate:
|
||||
return generate_game({player_name: formatted_options})
|
||||
|
||||
else:
|
||||
return send_yaml(player_name, formatted_options)
|
||||
|
||||
14
WebHostLib/robots.py
Normal file
14
WebHostLib/robots.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from WebHostLib import app
|
||||
from flask import abort
|
||||
from . import cache
|
||||
|
||||
|
||||
@cache.cached()
|
||||
@app.route('/robots.txt')
|
||||
def robots():
|
||||
# If this host is not official, do not allow search engine crawling
|
||||
if not app.config["ASSET_RIGHTS"]:
|
||||
return app.send_static_file('robots.txt')
|
||||
|
||||
# Send 404 if the host has affirmed this to be the official WebHost
|
||||
abort(404)
|
||||
@@ -1,20 +0,0 @@
|
||||
window.addEventListener('load', () => {
|
||||
const url = window.location;
|
||||
setInterval(() => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
|
||||
// Create a fake DOM using the returned HTML
|
||||
const domParser = new DOMParser();
|
||||
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||
|
||||
// Update item and location trackers
|
||||
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||
document.getElementById('location-table').innerHTML = fakeDOM.getElementById('location-table').innerHTML;
|
||||
|
||||
};
|
||||
ajax.open('GET', url);
|
||||
ajax.send();
|
||||
}, 15000)
|
||||
});
|
||||
@@ -1,523 +0,0 @@
|
||||
let gameName = null;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
gameName = document.getElementById('player-options').getAttribute('data-game');
|
||||
|
||||
// Update game name on page
|
||||
document.getElementById('game-name').innerText = gameName;
|
||||
|
||||
fetchOptionData().then((results) => {
|
||||
let optionHash = localStorage.getItem(`${gameName}-hash`);
|
||||
if (!optionHash) {
|
||||
// If no hash data has been set before, set it now
|
||||
optionHash = md5(JSON.stringify(results));
|
||||
localStorage.setItem(`${gameName}-hash`, optionHash);
|
||||
localStorage.removeItem(gameName);
|
||||
}
|
||||
|
||||
if (optionHash !== md5(JSON.stringify(results))) {
|
||||
showUserMessage(
|
||||
'Your options are out of date! Click here to update them! Be aware this will reset them all to default.'
|
||||
);
|
||||
document.getElementById('user-message').addEventListener('click', resetOptions);
|
||||
}
|
||||
|
||||
// Page setup
|
||||
createDefaultOptions(results);
|
||||
buildUI(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('export-options').addEventListener('click', () => exportOptions());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
|
||||
// Name input field
|
||||
const playerOptions = JSON.parse(localStorage.getItem(gameName));
|
||||
const nameInput = document.getElementById('player-name');
|
||||
nameInput.addEventListener('keyup', (event) => updateBaseOption(event));
|
||||
nameInput.value = playerOptions.name;
|
||||
|
||||
// Presets
|
||||
const presetSelect = document.getElementById('game-options-preset');
|
||||
presetSelect.addEventListener('change', (event) => setPresets(results, event.target.value));
|
||||
for (const preset in results['presetOptions']) {
|
||||
const presetOption = document.createElement('option');
|
||||
presetOption.innerText = preset;
|
||||
presetSelect.appendChild(presetOption);
|
||||
}
|
||||
presetSelect.value = localStorage.getItem(`${gameName}-preset`);
|
||||
results['presetOptions']['__default'] = {};
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
const url = new URL(window.location.href);
|
||||
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
|
||||
})
|
||||
});
|
||||
|
||||
const resetOptions = () => {
|
||||
localStorage.removeItem(gameName);
|
||||
localStorage.removeItem(`${gameName}-hash`);
|
||||
localStorage.removeItem(`${gameName}-preset`);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const fetchOptionData = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject(ajax.responseText);
|
||||
return;
|
||||
}
|
||||
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||
catch(error){ reject(error); }
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/player-options/${gameName}.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const createDefaultOptions = (optionData) => {
|
||||
if (!localStorage.getItem(gameName)) {
|
||||
const newOptions = {
|
||||
[gameName]: {},
|
||||
};
|
||||
for (let baseOption of Object.keys(optionData.baseOptions)){
|
||||
newOptions[baseOption] = optionData.baseOptions[baseOption];
|
||||
}
|
||||
for (let gameOption of Object.keys(optionData.gameOptions)){
|
||||
newOptions[gameName][gameOption] = optionData.gameOptions[gameOption].defaultValue;
|
||||
}
|
||||
localStorage.setItem(gameName, JSON.stringify(newOptions));
|
||||
}
|
||||
|
||||
if (!localStorage.getItem(`${gameName}-preset`)) {
|
||||
localStorage.setItem(`${gameName}-preset`, '__default');
|
||||
}
|
||||
};
|
||||
|
||||
const buildUI = (optionData) => {
|
||||
// Game Options
|
||||
const leftGameOpts = {};
|
||||
const rightGameOpts = {};
|
||||
Object.keys(optionData.gameOptions).forEach((key, index) => {
|
||||
if (index < Object.keys(optionData.gameOptions).length / 2) {
|
||||
leftGameOpts[key] = optionData.gameOptions[key];
|
||||
} else {
|
||||
rightGameOpts[key] = optionData.gameOptions[key];
|
||||
}
|
||||
});
|
||||
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
|
||||
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
|
||||
};
|
||||
|
||||
const buildOptionsTable = (options, romOpts = false) => {
|
||||
const currentOptions = JSON.parse(localStorage.getItem(gameName));
|
||||
const table = document.createElement('table');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
Object.keys(options).forEach((option) => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// td Left
|
||||
const tdl = document.createElement('td');
|
||||
const label = document.createElement('label');
|
||||
label.textContent = `${options[option].displayName}: `;
|
||||
label.setAttribute('for', option);
|
||||
|
||||
const questionSpan = document.createElement('span');
|
||||
questionSpan.classList.add('interactive');
|
||||
questionSpan.setAttribute('data-tooltip', options[option].description);
|
||||
questionSpan.innerText = '(?)';
|
||||
|
||||
label.appendChild(questionSpan);
|
||||
tdl.appendChild(label);
|
||||
tr.appendChild(tdl);
|
||||
|
||||
// td Right
|
||||
const tdr = document.createElement('td');
|
||||
let element = null;
|
||||
|
||||
const randomButton = document.createElement('button');
|
||||
|
||||
switch(options[option].type) {
|
||||
case 'select':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('select-container');
|
||||
let select = document.createElement('select');
|
||||
select.setAttribute('id', option);
|
||||
select.setAttribute('data-key', option);
|
||||
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||
options[option].options.forEach((opt) => {
|
||||
const optionElement = document.createElement('option');
|
||||
optionElement.setAttribute('value', opt.value);
|
||||
optionElement.innerText = opt.name;
|
||||
|
||||
if ((isNaN(currentOptions[gameName][option]) &&
|
||||
(parseInt(opt.value, 10) === parseInt(currentOptions[gameName][option]))) ||
|
||||
(opt.value === currentOptions[gameName][option]))
|
||||
{
|
||||
optionElement.selected = true;
|
||||
}
|
||||
select.appendChild(optionElement);
|
||||
});
|
||||
select.addEventListener('change', (event) => updateGameOption(event.target));
|
||||
element.appendChild(select);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, select));
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
select.disabled = true;
|
||||
}
|
||||
|
||||
element.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
case 'range':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('range-container');
|
||||
|
||||
let range = document.createElement('input');
|
||||
range.setAttribute('id', option);
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('data-key', option);
|
||||
range.setAttribute('min', options[option].min);
|
||||
range.setAttribute('max', options[option].max);
|
||||
range.value = currentOptions[gameName][option];
|
||||
range.addEventListener('change', (event) => {
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
element.appendChild(range);
|
||||
|
||||
let rangeVal = document.createElement('span');
|
||||
rangeVal.classList.add('range-value');
|
||||
rangeVal.setAttribute('id', `${option}-value`);
|
||||
rangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||
currentOptions[gameName][option] : options[option].defaultValue;
|
||||
element.appendChild(rangeVal);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, range));
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
range.disabled = true;
|
||||
}
|
||||
|
||||
element.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
case 'named_range':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('named-range-container');
|
||||
|
||||
// Build the select element
|
||||
let namedRangeSelect = document.createElement('select');
|
||||
namedRangeSelect.setAttribute('data-key', option);
|
||||
Object.keys(options[option].value_names).forEach((presetName) => {
|
||||
let presetOption = document.createElement('option');
|
||||
presetOption.innerText = presetName;
|
||||
presetOption.value = options[option].value_names[presetName];
|
||||
const words = presetOption.innerText.split('_');
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
||||
}
|
||||
presetOption.innerText = words.join(' ');
|
||||
namedRangeSelect.appendChild(presetOption);
|
||||
});
|
||||
let customOption = document.createElement('option');
|
||||
customOption.innerText = 'Custom';
|
||||
customOption.value = 'custom';
|
||||
customOption.selected = true;
|
||||
namedRangeSelect.appendChild(customOption);
|
||||
if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) {
|
||||
namedRangeSelect.value = Number(currentOptions[gameName][option]);
|
||||
}
|
||||
|
||||
// Build range element
|
||||
let namedRangeWrapper = document.createElement('div');
|
||||
namedRangeWrapper.classList.add('named-range-wrapper');
|
||||
let namedRange = document.createElement('input');
|
||||
namedRange.setAttribute('type', 'range');
|
||||
namedRange.setAttribute('data-key', option);
|
||||
namedRange.setAttribute('min', options[option].min);
|
||||
namedRange.setAttribute('max', options[option].max);
|
||||
namedRange.value = currentOptions[gameName][option];
|
||||
|
||||
// Build rage value element
|
||||
let namedRangeVal = document.createElement('span');
|
||||
namedRangeVal.classList.add('range-value');
|
||||
namedRangeVal.setAttribute('id', `${option}-value`);
|
||||
namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||
currentOptions[gameName][option] : options[option].defaultValue;
|
||||
|
||||
// Configure select event listener
|
||||
namedRangeSelect.addEventListener('change', (event) => {
|
||||
if (event.target.value === 'custom') { return; }
|
||||
|
||||
// Update range slider
|
||||
namedRange.value = event.target.value;
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
|
||||
// Configure range event handler
|
||||
namedRange.addEventListener('change', (event) => {
|
||||
// Update select element
|
||||
namedRangeSelect.value =
|
||||
(Object.values(options[option].value_names).includes(parseInt(event.target.value))) ?
|
||||
parseInt(event.target.value) : 'custom';
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
|
||||
element.appendChild(namedRangeSelect);
|
||||
namedRangeWrapper.appendChild(namedRange);
|
||||
namedRangeWrapper.appendChild(namedRangeVal);
|
||||
element.appendChild(namedRangeWrapper);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(
|
||||
event, namedRange, namedRangeSelect)
|
||||
);
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
namedRange.disabled = true;
|
||||
namedRangeSelect.disabled = true;
|
||||
}
|
||||
|
||||
namedRangeWrapper.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Ignoring unknown option type: ${options[option].type} with name ${option}`);
|
||||
return;
|
||||
}
|
||||
|
||||
tdr.appendChild(element);
|
||||
tr.appendChild(tdr);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
return table;
|
||||
};
|
||||
|
||||
const setPresets = (optionsData, presetName) => {
|
||||
const defaults = optionsData['gameOptions'];
|
||||
const preset = optionsData['presetOptions'][presetName];
|
||||
|
||||
localStorage.setItem(`${gameName}-preset`, presetName);
|
||||
|
||||
if (!preset) {
|
||||
console.error(`No presets defined for preset name: '${presetName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateOptionElement = (option, presetValue) => {
|
||||
const optionElement = document.querySelector(`#${option}[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
if (presetValue === 'random') {
|
||||
randomElement.classList.add('active');
|
||||
optionElement.disabled = true;
|
||||
updateGameOption(randomElement, false);
|
||||
} else {
|
||||
optionElement.value = presetValue;
|
||||
randomElement.classList.remove('active');
|
||||
optionElement.disabled = undefined;
|
||||
updateGameOption(optionElement, false);
|
||||
}
|
||||
};
|
||||
|
||||
for (const option in defaults) {
|
||||
let presetValue = preset[option];
|
||||
if (presetValue === undefined) {
|
||||
// Using the default value if not set in presets.
|
||||
presetValue = defaults[option]['defaultValue'];
|
||||
}
|
||||
|
||||
switch (defaults[option].type) {
|
||||
case 'range':
|
||||
const numberElement = document.querySelector(`#${option}-value`);
|
||||
if (presetValue === 'random') {
|
||||
numberElement.innerText = defaults[option]['defaultValue'] === 'random'
|
||||
? defaults[option]['min'] // A fallback so we don't print 'random' in the UI.
|
||||
: defaults[option]['defaultValue'];
|
||||
} else {
|
||||
numberElement.innerText = presetValue;
|
||||
}
|
||||
|
||||
updateOptionElement(option, presetValue);
|
||||
break;
|
||||
|
||||
case 'select': {
|
||||
updateOptionElement(option, presetValue);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'named_range': {
|
||||
const selectElement = document.querySelector(`select[data-key='${option}']`);
|
||||
const rangeElement = document.querySelector(`input[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
if (presetValue === 'random') {
|
||||
randomElement.classList.add('active');
|
||||
selectElement.disabled = true;
|
||||
rangeElement.disabled = true;
|
||||
updateGameOption(randomElement, false);
|
||||
} else {
|
||||
rangeElement.value = presetValue;
|
||||
selectElement.value = Object.values(defaults[option]['value_names']).includes(parseInt(presetValue)) ?
|
||||
parseInt(presetValue) : 'custom';
|
||||
document.getElementById(`${option}-value`).innerText = presetValue;
|
||||
|
||||
randomElement.classList.remove('active');
|
||||
selectElement.disabled = undefined;
|
||||
rangeElement.disabled = undefined;
|
||||
updateGameOption(rangeElement, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(`Ignoring preset value for unknown option type: ${defaults[option].type} with name ${option}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
|
||||
const active = event.target.classList.contains('active');
|
||||
const randomButton = event.target;
|
||||
|
||||
if (active) {
|
||||
randomButton.classList.remove('active');
|
||||
inputElement.disabled = undefined;
|
||||
if (optionalSelectElement) {
|
||||
optionalSelectElement.disabled = undefined;
|
||||
}
|
||||
} else {
|
||||
randomButton.classList.add('active');
|
||||
inputElement.disabled = true;
|
||||
if (optionalSelectElement) {
|
||||
optionalSelectElement.disabled = true;
|
||||
}
|
||||
}
|
||||
updateGameOption(active ? inputElement : randomButton);
|
||||
};
|
||||
|
||||
const updateBaseOption = (event) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value);
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const updateGameOption = (optionElement, toggleCustomPreset = true) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
|
||||
if (toggleCustomPreset) {
|
||||
localStorage.setItem(`${gameName}-preset`, '__custom');
|
||||
const presetElement = document.getElementById('game-options-preset');
|
||||
presetElement.value = '__custom';
|
||||
}
|
||||
|
||||
if (optionElement.classList.contains('randomize-button')) {
|
||||
// If the event passed in is the randomize button, then we know what we must do.
|
||||
options[gameName][optionElement.getAttribute('data-key')] = 'random';
|
||||
} else {
|
||||
options[gameName][optionElement.getAttribute('data-key')] = isNaN(optionElement.value) ?
|
||||
optionElement.value : parseInt(optionElement.value, 10);
|
||||
}
|
||||
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const exportOptions = () => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
const preset = localStorage.getItem(`${gameName}-preset`);
|
||||
switch (preset) {
|
||||
case '__default':
|
||||
options['description'] = `Generated by https://archipelago.gg with the default preset.`;
|
||||
break;
|
||||
|
||||
case '__custom':
|
||||
options['description'] = `Generated by https://archipelago.gg.`;
|
||||
break;
|
||||
|
||||
default:
|
||||
options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`;
|
||||
}
|
||||
|
||||
if (!options.name || options.name.toString().trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
|
||||
};
|
||||
|
||||
/** Create an anchor and trigger a download of a text file. */
|
||||
const download = (filename, text) => {
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
|
||||
downloadLink.setAttribute('download', filename);
|
||||
downloadLink.style.display = 'none';
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
const generateGame = (raceMode = false) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
|
||||
axios.post('/api/generate', {
|
||||
weights: { player: options },
|
||||
presetData: { player: options },
|
||||
playerCount: 1,
|
||||
spoiler: 3,
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
window.location.href = response.data.url;
|
||||
}).catch((error) => {
|
||||
let userMessage = 'Something went wrong and your game could not be generated.';
|
||||
if (error.response.data.text) {
|
||||
userMessage += ' ' + error.response.data.text;
|
||||
}
|
||||
showUserMessage(userMessage);
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const showUserMessage = (message) => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = message;
|
||||
userMessage.classList.add('visible');
|
||||
window.scrollTo(0, 0);
|
||||
userMessage.addEventListener('click', () => {
|
||||
userMessage.classList.remove('visible');
|
||||
userMessage.addEventListener('click', hideUserMessage);
|
||||
});
|
||||
};
|
||||
|
||||
const hideUserMessage = () => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.classList.remove('visible');
|
||||
userMessage.removeEventListener('click', hideUserMessage);
|
||||
};
|
||||
335
WebHostLib/static/assets/playerOptions.js
Normal file
335
WebHostLib/static/assets/playerOptions.js
Normal file
@@ -0,0 +1,335 @@
|
||||
let presets = {};
|
||||
|
||||
window.addEventListener('load', async () => {
|
||||
// Load settings from localStorage, if available
|
||||
loadSettings();
|
||||
|
||||
// Fetch presets if available
|
||||
await fetchPresets();
|
||||
|
||||
// Handle changes to range inputs
|
||||
document.querySelectorAll('input[type=range]').forEach((range) => {
|
||||
const optionName = range.getAttribute('id');
|
||||
range.addEventListener('change', () => {
|
||||
document.getElementById(`${optionName}-value`).innerText = range.value;
|
||||
|
||||
// Handle updating named range selects to "custom" if appropriate
|
||||
const select = document.querySelector(`select[data-option-name=${optionName}]`);
|
||||
if (select) {
|
||||
let updated = false;
|
||||
select?.childNodes.forEach((option) => {
|
||||
if (option.value === range.value) {
|
||||
select.value = range.value;
|
||||
updated = true;
|
||||
}
|
||||
});
|
||||
if (!updated) {
|
||||
select.value = 'custom';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle changes to named range selects
|
||||
document.querySelectorAll('.named-range-container select').forEach((select) => {
|
||||
const optionName = select.getAttribute('data-option-name');
|
||||
select.addEventListener('change', (evt) => {
|
||||
document.getElementById(optionName).value = evt.target.value;
|
||||
document.getElementById(`${optionName}-value`).innerText = evt.target.value;
|
||||
});
|
||||
});
|
||||
|
||||
// Handle changes to randomize checkboxes
|
||||
document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => {
|
||||
const optionName = checkbox.getAttribute('data-option-name');
|
||||
checkbox.addEventListener('change', () => {
|
||||
const optionInput = document.getElementById(optionName);
|
||||
const namedRangeSelect = document.querySelector(`select[data-option-name=${optionName}]`);
|
||||
const customInput = document.getElementById(`${optionName}-custom`);
|
||||
if (checkbox.checked) {
|
||||
optionInput.setAttribute('disabled', '1');
|
||||
namedRangeSelect?.setAttribute('disabled', '1');
|
||||
if (customInput) {
|
||||
customInput.setAttribute('disabled', '1');
|
||||
}
|
||||
} else {
|
||||
optionInput.removeAttribute('disabled');
|
||||
namedRangeSelect?.removeAttribute('disabled');
|
||||
if (customInput) {
|
||||
customInput.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle changes to TextChoice input[type=text]
|
||||
document.querySelectorAll('.text-choice-container input[type=text]').forEach((input) => {
|
||||
const optionName = input.getAttribute('data-option-name');
|
||||
input.addEventListener('input', () => {
|
||||
const select = document.getElementById(optionName);
|
||||
const optionValues = [];
|
||||
select.childNodes.forEach((option) => optionValues.push(option.value));
|
||||
select.value = (optionValues.includes(input.value)) ? input.value : 'custom';
|
||||
});
|
||||
});
|
||||
|
||||
// Handle changes to TextChoice select
|
||||
document.querySelectorAll('.text-choice-container select').forEach((select) => {
|
||||
const optionName = select.getAttribute('id');
|
||||
select.addEventListener('change', () => {
|
||||
document.getElementById(`${optionName}-custom`).value = '';
|
||||
});
|
||||
});
|
||||
|
||||
// Update the "Option Preset" select to read "custom" when changes are made to relevant inputs
|
||||
const presetSelect = document.getElementById('game-options-preset');
|
||||
document.querySelectorAll('input, select').forEach((input) => {
|
||||
if ( // Ignore inputs which have no effect on yaml generation
|
||||
(input.id === 'player-name') ||
|
||||
(input.id === 'game-options-preset') ||
|
||||
(input.classList.contains('group-toggle')) ||
|
||||
(input.type === 'submit')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
input.addEventListener('change', () => {
|
||||
presetSelect.value = 'custom';
|
||||
});
|
||||
});
|
||||
|
||||
// Handle changes to presets select
|
||||
document.getElementById('game-options-preset').addEventListener('change', choosePreset);
|
||||
|
||||
// Save settings to localStorage when form is submitted
|
||||
document.getElementById('options-form').addEventListener('submit', (evt) => {
|
||||
const playerName = document.getElementById('player-name');
|
||||
if (!playerName.value.trim()) {
|
||||
evt.preventDefault();
|
||||
window.scrollTo(0, 0);
|
||||
showUserMessage('You must enter a player name!');
|
||||
}
|
||||
|
||||
saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Save all settings to localStorage
|
||||
const saveSettings = () => {
|
||||
const options = {
|
||||
inputs: {},
|
||||
checkboxes: {},
|
||||
};
|
||||
document.querySelectorAll('input, select').forEach((input) => {
|
||||
if (input.type === 'submit') {
|
||||
// Ignore submit inputs
|
||||
}
|
||||
else if (input.type === 'checkbox') {
|
||||
options.checkboxes[input.id] = input.checked;
|
||||
}
|
||||
else {
|
||||
options.inputs[input.id] = input.value
|
||||
}
|
||||
});
|
||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
||||
localStorage.setItem(game, JSON.stringify(options));
|
||||
};
|
||||
|
||||
// Load all options from localStorage
|
||||
const loadSettings = () => {
|
||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
||||
|
||||
const options = JSON.parse(localStorage.getItem(game));
|
||||
if (options) {
|
||||
if (!options.inputs || !options.checkboxes) {
|
||||
localStorage.removeItem(game);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore value-based inputs and selects
|
||||
Object.keys(options.inputs).forEach((key) => {
|
||||
try{
|
||||
document.getElementById(key).value = options.inputs[key];
|
||||
const rangeValue = document.getElementById(`${key}-value`);
|
||||
if (rangeValue) {
|
||||
rangeValue.innerText = options.inputs[key];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Unable to restore value to input with id ${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Restore checkboxes
|
||||
Object.keys(options.checkboxes).forEach((key) => {
|
||||
try{
|
||||
if (options.checkboxes[key]) {
|
||||
document.getElementById(key).setAttribute('checked', '1');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Unable to restore value to input with id ${key}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure any input for which the randomize checkbox is checked by default, the relevant inputs are disabled
|
||||
document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => {
|
||||
const optionName = checkbox.getAttribute('data-option-name');
|
||||
if (checkbox.checked) {
|
||||
const input = document.getElementById(optionName);
|
||||
if (input) {
|
||||
input.setAttribute('disabled', '1');
|
||||
}
|
||||
const customInput = document.getElementById(`${optionName}-custom`);
|
||||
if (customInput) {
|
||||
customInput.setAttribute('disabled', '1');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the preset data for this game and apply the presets if localStorage indicates one was previously chosen
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const fetchPresets = async () => {
|
||||
const response = await fetch('option-presets');
|
||||
presets = await response.json();
|
||||
const presetSelect = document.getElementById('game-options-preset');
|
||||
presetSelect.removeAttribute('disabled');
|
||||
|
||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
||||
const presetToApply = localStorage.getItem(`${game}-preset`);
|
||||
const playerName = localStorage.getItem(`${game}-player`);
|
||||
if (presetToApply) {
|
||||
localStorage.removeItem(`${game}-preset`);
|
||||
presetSelect.value = presetToApply;
|
||||
applyPresets(presetToApply);
|
||||
}
|
||||
|
||||
if (playerName) {
|
||||
document.getElementById('player-name').value = playerName;
|
||||
localStorage.removeItem(`${game}-player`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the localStorage for this game and set a preset to be loaded upon page reload
|
||||
* @param evt
|
||||
*/
|
||||
const choosePreset = (evt) => {
|
||||
if (evt.target.value === 'custom') { return; }
|
||||
|
||||
const game = document.getElementById('player-options').getAttribute('data-game');
|
||||
localStorage.removeItem(game);
|
||||
|
||||
localStorage.setItem(`${game}-player`, document.getElementById('player-name').value);
|
||||
if (evt.target.value !== 'default') {
|
||||
localStorage.setItem(`${game}-preset`, evt.target.value);
|
||||
}
|
||||
|
||||
document.querySelectorAll('#options-form input, #options-form select').forEach((input) => {
|
||||
if (input.id === 'player-name') { return; }
|
||||
input.removeAttribute('value');
|
||||
});
|
||||
|
||||
window.location.replace(window.location.href);
|
||||
};
|
||||
|
||||
const applyPresets = (presetName) => {
|
||||
// Ignore the "default" preset, because it gets set automatically by Jinja
|
||||
if (presetName === 'default') {
|
||||
saveSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!presets[presetName]) {
|
||||
console.error(`Unknown preset ${presetName} chosen`);
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = presets[presetName];
|
||||
Object.keys(preset).forEach((optionName) => {
|
||||
const optionValue = preset[optionName];
|
||||
|
||||
// Handle List and Set options
|
||||
if (Array.isArray(optionValue)) {
|
||||
document.querySelectorAll(`input[type=checkbox][name=${optionName}]`).forEach((checkbox) => {
|
||||
if (optionValue.includes(checkbox.value)) {
|
||||
checkbox.setAttribute('checked', '1');
|
||||
} else {
|
||||
checkbox.removeAttribute('checked');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Dict options
|
||||
if (typeof(optionValue) === 'object' && optionValue !== null) {
|
||||
const itemNames = Object.keys(optionValue);
|
||||
document.querySelectorAll(`input[type=number][data-option-name=${optionName}]`).forEach((input) => {
|
||||
const itemName = input.getAttribute('data-item-name');
|
||||
input.value = (itemNames.includes(itemName)) ? optionValue[itemName] : 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Identify all possible elements
|
||||
const normalInput = document.getElementById(optionName);
|
||||
const customInput = document.getElementById(`${optionName}-custom`);
|
||||
const rangeValue = document.getElementById(`${optionName}-value`);
|
||||
const randomizeInput = document.getElementById(`random-${optionName}`);
|
||||
const namedRangeSelect = document.getElementById(`${optionName}-select`);
|
||||
|
||||
// It is possible for named ranges to use name of a value rather than the value itself. This is accounted for here
|
||||
let trueValue = optionValue;
|
||||
if (namedRangeSelect) {
|
||||
namedRangeSelect.querySelectorAll('option').forEach((opt) => {
|
||||
if (opt.innerText.startsWith(optionValue)) {
|
||||
trueValue = opt.value;
|
||||
}
|
||||
});
|
||||
namedRangeSelect.value = trueValue;
|
||||
}
|
||||
|
||||
// Handle options whose presets are "random"
|
||||
if (optionValue === 'random') {
|
||||
normalInput.setAttribute('disabled', '1');
|
||||
randomizeInput.setAttribute('checked', '1');
|
||||
if (customInput) {
|
||||
customInput.setAttribute('disabled', '1');
|
||||
}
|
||||
if (rangeValue) {
|
||||
rangeValue.innerText = normalInput.value;
|
||||
}
|
||||
if (namedRangeSelect) {
|
||||
namedRangeSelect.setAttribute('disabled', '1');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle normal (text, number, select, etc.) and custom inputs (custom inputs exist with TextChoice only)
|
||||
normalInput.value = trueValue;
|
||||
normalInput.removeAttribute('disabled');
|
||||
randomizeInput.removeAttribute('checked');
|
||||
if (customInput) {
|
||||
document.getElementById(`${optionName}-custom`).removeAttribute('disabled');
|
||||
}
|
||||
if (rangeValue) {
|
||||
rangeValue.innerText = trueValue;
|
||||
}
|
||||
});
|
||||
|
||||
saveSettings();
|
||||
};
|
||||
|
||||
const showUserMessage = (text) => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = text;
|
||||
userMessage.addEventListener('click', hideUserMessage);
|
||||
userMessage.style.display = 'block';
|
||||
};
|
||||
|
||||
const hideUserMessage = () => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.removeEventListener('click', hideUserMessage);
|
||||
userMessage.style.display = 'none';
|
||||
};
|
||||
@@ -1,18 +1,16 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Add toggle listener to all elements with .collapse-toggle
|
||||
const toggleButtons = document.querySelectorAll('.collapse-toggle');
|
||||
toggleButtons.forEach((e) => e.addEventListener('click', toggleCollapse));
|
||||
const toggleButtons = document.querySelectorAll('details');
|
||||
|
||||
// Handle game filter input
|
||||
const gameSearch = document.getElementById('game-search');
|
||||
gameSearch.value = '';
|
||||
gameSearch.addEventListener('input', (evt) => {
|
||||
if (!evt.target.value.trim()) {
|
||||
// If input is empty, display all collapsed games
|
||||
// If input is empty, display all games as collapsed
|
||||
return toggleButtons.forEach((header) => {
|
||||
header.style.display = null;
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
header.removeAttribute('open');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,12 +19,10 @@ window.addEventListener('load', () => {
|
||||
// If the game name includes the search string, display the game. If not, hide it
|
||||
if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) {
|
||||
header.style.display = null;
|
||||
header.firstElementChild.innerText = '▼';
|
||||
header.nextElementSibling.classList.remove('collapsed');
|
||||
header.setAttribute('open', '1');
|
||||
} else {
|
||||
header.style.display = 'none';
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
header.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -35,30 +31,14 @@ window.addEventListener('load', () => {
|
||||
document.getElementById('collapse-all').addEventListener('click', collapseAll);
|
||||
});
|
||||
|
||||
const toggleCollapse = (evt) => {
|
||||
const gameArrow = evt.target.firstElementChild;
|
||||
const gameInfo = evt.target.nextElementSibling;
|
||||
if (gameInfo.classList.contains('collapsed')) {
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
} else {
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
}
|
||||
};
|
||||
|
||||
const expandAll = () => {
|
||||
document.querySelectorAll('.collapse-toggle').forEach((header) => {
|
||||
if (header.style.display === 'none') { return; }
|
||||
header.firstElementChild.innerText = '▼';
|
||||
header.nextElementSibling.classList.remove('collapsed');
|
||||
document.querySelectorAll('details').forEach((detail) => {
|
||||
detail.setAttribute('open', '1');
|
||||
});
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
document.querySelectorAll('.collapse-toggle').forEach((header) => {
|
||||
if (header.style.display === 'none') { return; }
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
document.querySelectorAll('details').forEach((detail) => {
|
||||
detail.removeAttribute('open');
|
||||
});
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
223
WebHostLib/static/assets/weightedOptions.js
Normal file
223
WebHostLib/static/assets/weightedOptions.js
Normal file
@@ -0,0 +1,223 @@
|
||||
let deletedOptions = {};
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const worldName = document.querySelector('#weighted-options').getAttribute('data-game');
|
||||
|
||||
// Generic change listener. Detecting unique qualities and acting on them here reduces initial JS initialisation time
|
||||
// and handles dynamically created elements
|
||||
document.addEventListener('change', (evt) => {
|
||||
// Handle updates to range inputs
|
||||
if (evt.target.type === 'range') {
|
||||
// Update span containing range value. All ranges have a corresponding `{rangeId}-value` span
|
||||
document.getElementById(`${evt.target.id}-value`).innerText = evt.target.value;
|
||||
|
||||
// If the changed option was the name of a game, determine whether to show or hide that game's div
|
||||
if (evt.target.id.startsWith('game||')) {
|
||||
const gameName = evt.target.id.split('||')[1];
|
||||
const gameDiv = document.getElementById(`${gameName}-container`);
|
||||
if (evt.target.value > 0) {
|
||||
gameDiv.classList.remove('hidden');
|
||||
} else {
|
||||
gameDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Generic click listener
|
||||
document.addEventListener('click', (evt) => {
|
||||
// Handle creating new rows for Range options
|
||||
if (evt.target.classList.contains('add-range-option-button')) {
|
||||
const optionName = evt.target.getAttribute('data-option');
|
||||
addRangeRow(optionName);
|
||||
}
|
||||
|
||||
// Handle deleting range rows
|
||||
if (evt.target.classList.contains('range-option-delete')) {
|
||||
const targetRow = document.querySelector(`tr[data-row="${evt.target.getAttribute('data-target')}"]`);
|
||||
setDeletedOption(
|
||||
targetRow.getAttribute('data-option-name'),
|
||||
targetRow.getAttribute('data-value'),
|
||||
);
|
||||
targetRow.parentElement.removeChild(targetRow);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for enter presses on inputs intended to add range rows
|
||||
document.addEventListener('keydown', (evt) => {
|
||||
if (evt.key === 'Enter') {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
if (evt.key === 'Enter' && evt.target.classList.contains('range-option-value')) {
|
||||
const optionName = evt.target.getAttribute('data-option');
|
||||
addRangeRow(optionName);
|
||||
}
|
||||
});
|
||||
|
||||
// Detect form submission
|
||||
document.getElementById('weighted-options-form').addEventListener('submit', (evt) => {
|
||||
// Save data to localStorage
|
||||
const weightedOptions = {};
|
||||
document.querySelectorAll('input[name]').forEach((input) => {
|
||||
const keys = input.getAttribute('name').split('||');
|
||||
|
||||
// Determine keys
|
||||
const optionName = keys[0] ?? null;
|
||||
const subOption = keys[1] ?? null;
|
||||
|
||||
// Ensure keys exist
|
||||
if (!weightedOptions[optionName]) { weightedOptions[optionName] = {}; }
|
||||
if (subOption && !weightedOptions[optionName][subOption]) {
|
||||
weightedOptions[optionName][subOption] = null;
|
||||
}
|
||||
|
||||
if (subOption) { return weightedOptions[optionName][subOption] = determineValue(input); }
|
||||
if (optionName) { return weightedOptions[optionName] = determineValue(input); }
|
||||
});
|
||||
|
||||
localStorage.setItem(`${worldName}-weights`, JSON.stringify(weightedOptions));
|
||||
localStorage.setItem(`${worldName}-deletedOptions`, JSON.stringify(deletedOptions));
|
||||
});
|
||||
|
||||
// Remove all deleted values as specified by localStorage
|
||||
deletedOptions = JSON.parse(localStorage.getItem(`${worldName}-deletedOptions`) || '{}');
|
||||
Object.keys(deletedOptions).forEach((optionName) => {
|
||||
deletedOptions[optionName].forEach((value) => {
|
||||
const targetRow = document.querySelector(`tr[data-row="${value}-row"]`);
|
||||
targetRow.parentElement.removeChild(targetRow);
|
||||
});
|
||||
});
|
||||
|
||||
// Populate all settings from localStorage on page initialisation
|
||||
const previousSettingsJson = localStorage.getItem(`${worldName}-weights`);
|
||||
if (previousSettingsJson) {
|
||||
const previousSettings = JSON.parse(previousSettingsJson);
|
||||
Object.keys(previousSettings).forEach((option) => {
|
||||
if (typeof previousSettings[option] === 'string') {
|
||||
return document.querySelector(`input[name="${option}"]`).value = previousSettings[option];
|
||||
}
|
||||
|
||||
Object.keys(previousSettings[option]).forEach((value) => {
|
||||
const input = document.querySelector(`input[name="${option}||${value}"]`);
|
||||
if (!input?.type) {
|
||||
return console.error(`Unable to populate option with name ${option}||${value}.`);
|
||||
}
|
||||
|
||||
switch (input.type) {
|
||||
case 'checkbox':
|
||||
input.checked = (parseInt(previousSettings[option][value], 10) === 1);
|
||||
break;
|
||||
case 'range':
|
||||
input.value = parseInt(previousSettings[option][value], 10);
|
||||
break;
|
||||
case 'number':
|
||||
input.value = previousSettings[option][value].toString();
|
||||
break;
|
||||
default:
|
||||
console.error(`Found unsupported input type: ${input.type}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const addRangeRow = (optionName) => {
|
||||
const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
|
||||
const inputTarget = document.querySelector(inputQuery);
|
||||
const newValue = inputTarget.value;
|
||||
if (!/^-?\d+$/.test(newValue)) {
|
||||
alert('Range values must be a positive or negative integer!');
|
||||
return;
|
||||
}
|
||||
inputTarget.value = '';
|
||||
const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
|
||||
const tr = document.createElement('tr');
|
||||
tr.setAttribute('data-row', `${optionName}-${newValue}-row`);
|
||||
tr.setAttribute('data-option-name', optionName);
|
||||
tr.setAttribute('data-value', newValue);
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
const label = document.createElement('label');
|
||||
label.setAttribute('for', `${optionName}||${newValue}`);
|
||||
label.innerText = newValue.toString();
|
||||
tdLeft.appendChild(label);
|
||||
tr.appendChild(tdLeft);
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('min', '0');
|
||||
range.setAttribute('max', '50');
|
||||
range.setAttribute('value', '0');
|
||||
range.setAttribute('id', `${optionName}||${newValue}`);
|
||||
range.setAttribute('name', `${optionName}||${newValue}`);
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.classList.add('td-right');
|
||||
const valueSpan = document.createElement('span');
|
||||
valueSpan.setAttribute('id', `${optionName}||${newValue}-value`);
|
||||
valueSpan.innerText = '0';
|
||||
tdRight.appendChild(valueSpan);
|
||||
tr.appendChild(tdRight);
|
||||
const tdDelete = document.createElement('td');
|
||||
const deleteSpan = document.createElement('span');
|
||||
deleteSpan.classList.add('range-option-delete');
|
||||
deleteSpan.classList.add('js-required');
|
||||
deleteSpan.setAttribute('data-target', `${optionName}-${newValue}-row`);
|
||||
deleteSpan.innerText = '❌';
|
||||
tdDelete.appendChild(deleteSpan);
|
||||
tr.appendChild(tdDelete);
|
||||
tBody.appendChild(tr);
|
||||
|
||||
// Remove this option from the set of deleted options if it exists
|
||||
unsetDeletedOption(optionName, newValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the value of an input element, or returns a 1 or 0 if the element is a checkbox
|
||||
*
|
||||
* @param {object} input - The input element.
|
||||
* @returns {number} The value of the input element.
|
||||
*/
|
||||
const determineValue = (input) => {
|
||||
switch (input.type) {
|
||||
case 'checkbox':
|
||||
return (input.checked ? 1 : 0);
|
||||
case 'range':
|
||||
return parseInt(input.value, 10);
|
||||
default:
|
||||
return input.value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the deleted option value for a given world and option name.
|
||||
* If the world or option does not exist, it creates the necessary entries.
|
||||
*
|
||||
* @param {string} optionName - The name of the option.
|
||||
* @param {*} value - The value to be set for the deleted option.
|
||||
* @returns {void}
|
||||
*/
|
||||
const setDeletedOption = (optionName, value) => {
|
||||
deletedOptions[optionName] = deletedOptions[optionName] || [];
|
||||
deletedOptions[optionName].push(`${optionName}-${value}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a specific value from the deletedOptions object.
|
||||
*
|
||||
* @param {string} optionName - The name of the option.
|
||||
* @param {*} value - The value to be removed
|
||||
* @returns {void}
|
||||
*/
|
||||
const unsetDeletedOption = (optionName, value) => {
|
||||
if (!deletedOptions.hasOwnProperty(optionName)) { return; }
|
||||
if (deletedOptions[optionName].includes(`${optionName}-${value}`)) {
|
||||
deletedOptions[optionName].splice(deletedOptions[optionName].indexOf(`${optionName}-${value}`), 1);
|
||||
}
|
||||
if (deletedOptions[optionName].length === 0) {
|
||||
delete deletedOptions[optionName];
|
||||
}
|
||||
};
|
||||
20
WebHostLib/static/robots.txt
Normal file
20
WebHostLib/static/robots.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
User-agent: Googlebot
|
||||
Disallow: /
|
||||
|
||||
User-agent: APIs-Google
|
||||
Disallow: /
|
||||
|
||||
User-agent: AdsBot-Google-Mobile
|
||||
Disallow: /
|
||||
|
||||
User-agent: AdsBot-Google-Mobile
|
||||
Disallow: /
|
||||
|
||||
User-agent: Mediapartners-Google
|
||||
Disallow: /
|
||||
|
||||
User-agent: Google-Safety
|
||||
Disallow: /
|
||||
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -44,7 +44,7 @@ a{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
}
|
||||
|
||||
button{
|
||||
button, input[type=submit]{
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
padding: 10px 17px 11px 16px; /* top right bottom left */
|
||||
@@ -57,7 +57,7 @@ button{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:active{
|
||||
button:active, input[type=submit]:active{
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
|
||||
padding-right: 16px;
|
||||
@@ -66,11 +66,11 @@ button:active{
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
button.button-grass{
|
||||
button.button-grass, input[type=submit].button-grass{
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
button.button-dirt{
|
||||
button.button-dirt, input[type=submit].button-dirt{
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
@@ -111,4 +111,4 @@ h5, h6{
|
||||
|
||||
.interactive{
|
||||
color: #ffef00;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
font-family: LexendDeca-Light, sans-serif;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 284px;
|
||||
background-color: #42b149;
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(75%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#inventory-table img.powder-fix{
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 284px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: #42b149;
|
||||
padding: 0 3px 3px;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-right: 5px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter{
|
||||
padding-right: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
.markdown a{}
|
||||
|
||||
.markdown h1{
|
||||
.markdown h1, .markdown details summary.h1{
|
||||
font-size: 52px;
|
||||
font-weight: normal;
|
||||
font-family: LondrinaSolid-Regular, sans-serif;
|
||||
@@ -33,7 +33,7 @@
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
.markdown h2{
|
||||
.markdown h2, .markdown details summary.h2{
|
||||
font-size: 38px;
|
||||
font-weight: normal;
|
||||
font-family: LondrinaSolid-Light, sans-serif;
|
||||
@@ -45,7 +45,7 @@
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
.markdown h3{
|
||||
.markdown h3, .markdown details summary.h3{
|
||||
font-size: 26px;
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
text-transform: none;
|
||||
@@ -55,7 +55,7 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown h4{
|
||||
.markdown h4, .markdown details summary.h4{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
text-transform: none;
|
||||
font-size: 24px;
|
||||
@@ -63,21 +63,21 @@
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown h5{
|
||||
.markdown h5, .markdown details summary.h5{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
text-transform: none;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markdown h6{
|
||||
.markdown h6, .markdown details summary.h6{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
text-transform: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;;
|
||||
}
|
||||
|
||||
.markdown h4, .markdown h5,.markdown h6{
|
||||
.markdown h4, .markdown h5, .markdown h6{
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/grass.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#player-options{
|
||||
box-sizing: border-box;
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#player-options #player-options-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#player-options code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#player-options #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#player-options #user-message.visible{
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#player-options h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#player-options h2{
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#player-options h3, #player-options h4, #player-options h5, #player-options h6{
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#player-options input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#player-options input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#player-options select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#player-options #game-options, #player-options #rom-options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-options #meta-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#player-options div {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options #meta-options label {
|
||||
display: inline-block;
|
||||
min-width: 180px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options #meta-options input,
|
||||
#player-options #meta-options select {
|
||||
box-sizing: border-box;
|
||||
min-width: 150px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#player-options .left, #player-options .right{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options .left{
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#player-options .right{
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#player-options table{
|
||||
margin-bottom: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#player-options table .select-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-options table .select-container select{
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options table select:disabled{
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
#player-options table .range-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-options table .range-container input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options table .range-value{
|
||||
min-width: 20px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
#player-options table .named-range-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#player-options table .named-range-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
#player-options table .named-range-wrapper input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options table .randomize-button {
|
||||
max-height: 24px;
|
||||
line-height: 16px;
|
||||
padding: 2px 8px;
|
||||
margin: 0 0 0 0.25rem;
|
||||
font-size: 12px;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#player-options table .randomize-button.active {
|
||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
||||
}
|
||||
|
||||
#player-options table .randomize-button[data-tooltip]::after {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#player-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#player-options th, #player-options td{
|
||||
border: none;
|
||||
padding: 3px;
|
||||
font-size: 17px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1024px) {
|
||||
#player-options {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#player-options #meta-options {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
#player-options #game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#player-options .left,
|
||||
#player-options .right {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#game-options table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#game-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
#game-options table tr td {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
310
WebHostLib/static/styles/playerOptions/playerOptions.css
Normal file
310
WebHostLib/static/styles/playerOptions/playerOptions.css
Normal file
@@ -0,0 +1,310 @@
|
||||
@import "../markdown.css";
|
||||
html {
|
||||
background-image: url("../../static/backgrounds/grass.png");
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#player-options {
|
||||
box-sizing: border-box;
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
word-break: break-all;
|
||||
}
|
||||
#player-options #player-options-header h1 {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
#player-options #player-options-header h1:nth-child(2) {
|
||||
font-size: 1.4rem;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#player-options .js-warning-banner {
|
||||
width: calc(100% - 1rem);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background-color: #f3f309;
|
||||
color: #000000;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
#player-options .group-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
#player-options .group-container h2 {
|
||||
user-select: none;
|
||||
cursor: unset;
|
||||
}
|
||||
#player-options .group-container h2 label {
|
||||
cursor: pointer;
|
||||
}
|
||||
#player-options #player-options-button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
#player-options #user-message {
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
#player-options h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
#player-options h2 {
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
#player-options h3, #player-options h4, #player-options h5, #player-options h6 {
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
#player-options input:not([type]) {
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
#player-options input:not([type]):focus {
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
#player-options select {
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#player-options .game-options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
#player-options .game-options .left, #player-options .game-options .right {
|
||||
display: grid;
|
||||
grid-template-columns: 12rem auto;
|
||||
grid-row-gap: 0.5rem;
|
||||
grid-auto-rows: min-content;
|
||||
align-items: start;
|
||||
min-width: 480px;
|
||||
width: 50%;
|
||||
}
|
||||
#player-options #meta-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 3px;
|
||||
}
|
||||
#player-options #meta-options input, #player-options #meta-options select {
|
||||
box-sizing: border-box;
|
||||
width: 200px;
|
||||
}
|
||||
#player-options .left, #player-options .right {
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#player-options .left {
|
||||
margin-right: 20px;
|
||||
}
|
||||
#player-options .select-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 270px;
|
||||
}
|
||||
#player-options .select-container select {
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
#player-options .select-container select:disabled {
|
||||
background-color: lightgray;
|
||||
}
|
||||
#player-options .range-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 270px;
|
||||
}
|
||||
#player-options .range-container input[type=range] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#player-options .range-container .range-value {
|
||||
min-width: 20px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
#player-options .named-range-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
}
|
||||
#player-options .named-range-container .named-range-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
#player-options .named-range-container .named-range-wrapper input[type=range] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#player-options .free-text-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
}
|
||||
#player-options .free-text-container input[type=text] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#player-options .text-choice-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
}
|
||||
#player-options .text-choice-container .text-choice-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
#player-options .text-choice-container .text-choice-wrapper select {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#player-options .option-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
max-height: 10rem;
|
||||
min-width: 14.5rem;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
#player-options .option-container .option-divider {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
#player-options .option-container .option-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.125rem;
|
||||
margin-top: 0.125rem;
|
||||
user-select: none;
|
||||
}
|
||||
#player-options .option-container .option-entry:hover {
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
}
|
||||
#player-options .option-container .option-entry input[type=checkbox] {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
#player-options .option-container .option-entry input[type=number] {
|
||||
max-width: 1.5rem;
|
||||
max-height: 1rem;
|
||||
margin-left: 0.125rem;
|
||||
text-align: center;
|
||||
/* Hide arrows on input[type=number] fields */
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
#player-options .option-container .option-entry input[type=number]::-webkit-outer-spin-button, #player-options .option-container .option-entry input[type=number]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
#player-options .option-container .option-entry label {
|
||||
flex-grow: 1;
|
||||
margin-right: 0;
|
||||
min-width: unset;
|
||||
display: unset;
|
||||
}
|
||||
#player-options .randomize-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 22px;
|
||||
max-width: 30px;
|
||||
margin: 0 0 0 0.25rem;
|
||||
font-size: 14px;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
background-color: #d3d3d3;
|
||||
user-select: none;
|
||||
}
|
||||
#player-options .randomize-button:hover {
|
||||
background-color: #c0c0c0;
|
||||
cursor: pointer;
|
||||
}
|
||||
#player-options .randomize-button label {
|
||||
line-height: 22px;
|
||||
padding-left: 5px;
|
||||
padding-right: 2px;
|
||||
margin-right: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: unset;
|
||||
}
|
||||
#player-options .randomize-button label:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
#player-options .randomize-button input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
#player-options .randomize-button:has(input[type=checkbox]:checked) {
|
||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
||||
}
|
||||
#player-options .randomize-button:has(input[type=checkbox]:checked):hover {
|
||||
background-color: #eedd27;
|
||||
}
|
||||
#player-options .randomize-button[data-tooltip]::after {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
#player-options label {
|
||||
display: block;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
word-break: break-word;
|
||||
}
|
||||
#player-options th, #player-options td {
|
||||
border: none;
|
||||
padding: 3px;
|
||||
font-size: 17px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1024px) {
|
||||
#player-options {
|
||||
border-radius: 0;
|
||||
}
|
||||
#player-options #meta-options {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
#player-options .game-options {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=playerOptions.css.map */
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["playerOptions.scss"],"names":[],"mappings":"AAAQ;AAER;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAEA;EACI;;AAIR;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAIR;EACI;IACI;;EAEA;IACI;IACA;IACA;;EAGJ;IACI;IACA","file":"playerOptions.css"}
|
||||
364
WebHostLib/static/styles/playerOptions/playerOptions.scss
Normal file
364
WebHostLib/static/styles/playerOptions/playerOptions.scss
Normal file
@@ -0,0 +1,364 @@
|
||||
@import "../markdown.css";
|
||||
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/grass.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#player-options{
|
||||
box-sizing: border-box;
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
word-break: break-all;
|
||||
|
||||
#player-options-header{
|
||||
h1{
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
h1:nth-child(2){
|
||||
font-size: 1.4rem;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.js-warning-banner{
|
||||
width: calc(100% - 1rem);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background-color: #f3f309;
|
||||
color: #000000;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.group-container{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
h2{
|
||||
user-select: none;
|
||||
cursor: unset;
|
||||
|
||||
label{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#player-options-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
h2{
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6{
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
|
||||
&:focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.game-options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.left, .right{
|
||||
display: grid;
|
||||
grid-template-columns: 12rem auto;
|
||||
grid-row-gap: 0.5rem;
|
||||
grid-auto-rows: min-content;
|
||||
align-items: start;
|
||||
min-width: 480px;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
#meta-options{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 3px;
|
||||
|
||||
input, select{
|
||||
box-sizing: border-box;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.left, .right{
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.left{
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.select-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 270px;
|
||||
|
||||
select{
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
|
||||
&:disabled{
|
||||
background-color: lightgray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.range-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 270px;
|
||||
|
||||
input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.range-value{
|
||||
min-width: 20px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.named-range-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
|
||||
.named-range-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.free-text-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
|
||||
input[type=text]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.text-choice-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 270px;
|
||||
|
||||
.text-choice-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
select{
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
max-height: 10rem;
|
||||
min-width: 14.5rem;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
|
||||
.option-divider{
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.option-entry{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.125rem;
|
||||
margin-top: 0.125rem;
|
||||
user-select: none;
|
||||
|
||||
&:hover{
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
}
|
||||
|
||||
input[type=checkbox]{
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
input[type=number]{
|
||||
max-width: 1.5rem;
|
||||
max-height: 1rem;
|
||||
margin-left: 0.125rem;
|
||||
text-align: center;
|
||||
|
||||
/* Hide arrows on input[type=number] fields */
|
||||
-moz-appearance: textfield;
|
||||
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label{
|
||||
flex-grow: 1;
|
||||
margin-right: 0;
|
||||
min-width: unset;
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.randomize-button{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 22px;
|
||||
max-width: 30px;
|
||||
margin: 0 0 0 0.25rem;
|
||||
font-size: 14px;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
background-color: #d3d3d3;
|
||||
user-select: none;
|
||||
|
||||
&:hover{
|
||||
background-color: #c0c0c0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label{
|
||||
line-height: 22px;
|
||||
padding-left: 5px;
|
||||
padding-right: 2px;
|
||||
margin-right: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: unset;
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox]{
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:has(input[type=checkbox]:checked){
|
||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
||||
|
||||
&:hover{
|
||||
background-color: #eedd27;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-tooltip]::after{
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label{
|
||||
display: block;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
th, td{
|
||||
border: none;
|
||||
padding: 3px;
|
||||
font-size: 17px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 1024px) {
|
||||
#player-options {
|
||||
border-radius: 0;
|
||||
|
||||
#meta-options {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,30 +8,15 @@
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
#games h1{
|
||||
#games h1, #games details summary.h1{
|
||||
font-size: 60px;
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
#games h2{
|
||||
#games h2, #games details summary.h2{
|
||||
color: #93dcff;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#games .collapse-toggle{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#games h2 .collapse-arrow{
|
||||
font-size: 20px;
|
||||
display: inline-block; /* make vertical-align work */
|
||||
padding-bottom: 9px;
|
||||
vertical-align: middle;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
#games p.collapsed{
|
||||
display: none;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
#games a{
|
||||
|
||||
@@ -42,6 +42,7 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
||||
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/** Directional arrow styles */
|
||||
|
||||
142
WebHostLib/static/styles/tracker__ALinkToThePast.css
Normal file
142
WebHostLib/static/styles/tracker__ALinkToThePast.css
Normal file
@@ -0,0 +1,142 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@100..900&display=swap');
|
||||
|
||||
.tracker-container {
|
||||
width: 440px;
|
||||
box-sizing: border-box;
|
||||
font-family: "Lexend Deca", Arial, Helvetica, sans-serif;
|
||||
border: 2px solid black;
|
||||
border-radius: 4px;
|
||||
resize: both;
|
||||
|
||||
background-color: #42b149;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/** Inventory Grid ****************************************************************************************************/
|
||||
.inventory-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.inventory-grid .item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.inventory-grid .dual-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.inventory-grid .missing {
|
||||
/* Missing items will be in full grayscale to signify "uncollected". */
|
||||
filter: grayscale(100%) contrast(75%) brightness(75%);
|
||||
}
|
||||
|
||||
.inventory-grid .item img,
|
||||
.inventory-grid .dual-item img {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
text-shadow: 0 1px 2px black;
|
||||
font-weight: bold;
|
||||
image-rendering: crisp-edges;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.inventory-grid .dual-item img {
|
||||
height: 48px;
|
||||
margin: 0 -4px;
|
||||
}
|
||||
|
||||
.inventory-grid .dual-item img:first-child {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.inventory-grid .item .quantity {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
font-size: 1.75rem;
|
||||
line-height: 1.75rem;
|
||||
text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/** Regions List ******************************************************************************************************/
|
||||
.regions-list {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.regions-list summary {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.regions-list summary::before {
|
||||
content: "⯈";
|
||||
width: 1em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.regions-list details {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.regions-list details[open] > summary::before {
|
||||
content: "⯆";
|
||||
}
|
||||
|
||||
.regions-list .region {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 20fr 8fr 2fr 2fr;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.regions-list .region :first-child {
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.regions-list .region.region-header {
|
||||
margin-left: 24px;
|
||||
width: calc(100% - 24px);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.regions-list .location-rows {
|
||||
border-top: 1px solid white;
|
||||
display: grid;
|
||||
grid-template-columns: auto 32px;
|
||||
font-weight: 300;
|
||||
padding: 2px 8px;
|
||||
margin-top: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.regions-list .location-rows :nth-child(even) {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/grass.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
scroll-padding-top: 90px;
|
||||
}
|
||||
|
||||
#weighted-settings{
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#weighted-settings #games-wrapper{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper{
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper .add-option-div{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper .add-option-div button{
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0 0 0 0.15rem;
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper .add-option-div button:active{
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
#weighted-settings p.setting-description{
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
#weighted-settings p.hint-text{
|
||||
margin: 0 0 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#weighted-settings .jump-link{
|
||||
color: #ffef00;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#weighted-settings table{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings table th, #weighted-settings table td{
|
||||
border: none;
|
||||
}
|
||||
|
||||
#weighted-settings table td{
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-left{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
padding-right: 1rem;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-middle{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-right{
|
||||
width: 4rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-delete{
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#weighted-settings table .range-option-delete{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings .items-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#weighted-settings .items-div h3{
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .items-wrapper .item-set-wrapper{
|
||||
width: 24%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container{
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-top: 0.125rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-div{
|
||||
padding: 0.125rem 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-div:hover{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-qty-div{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0.125rem 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-qty-div .item-qty-input-wrapper{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-qty-div input{
|
||||
min-width: unset;
|
||||
width: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#weighted-settings .item-container .item-qty-div:hover{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div, #weighted-settings .locations-div{
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-container, #weighted-settings .locations-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{
|
||||
width: calc(50% - 0.5rem);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{
|
||||
margin-top: 0.25rem;
|
||||
height: 300px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#weighted-settings #weighted-settings-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#weighted-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#weighted-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#weighted-settings #user-message.visible{
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: none;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
#weighted-settings a{
|
||||
color: #ffef00;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#weighted-settings select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#weighted-settings .game-options, #weighted-settings .rom-options{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list .list-row label{
|
||||
display: block;
|
||||
width: calc(100% - 0.5rem);
|
||||
padding: 0.0625rem 0.25rem;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list .list-row label:hover{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list .list-row label input[type=checkbox]{
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list hr{
|
||||
width: calc(100% - 2px);
|
||||
margin: 2px auto;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 0.6);
|
||||
}
|
||||
|
||||
#weighted-settings .invisible{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||
#weighted-settings .game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#game-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
232
WebHostLib/static/styles/weightedOptions/weightedOptions.css
Normal file
232
WebHostLib/static/styles/weightedOptions/weightedOptions.css
Normal file
@@ -0,0 +1,232 @@
|
||||
html {
|
||||
background-image: url("../../static/backgrounds/grass.png");
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
scroll-padding-top: 90px;
|
||||
}
|
||||
|
||||
#weighted-options {
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
#weighted-options #weighted-options-header h1 {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
#weighted-options #weighted-options-header h1:nth-child(2) {
|
||||
font-size: 1.4rem;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#weighted-options .js-warning-banner {
|
||||
width: calc(100% - 1rem);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background-color: #f3f309;
|
||||
color: #000000;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
#weighted-options .option-wrapper {
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
#weighted-options .option-wrapper .add-option-div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
#weighted-options .option-wrapper .add-option-div button {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0 0 0 0.15rem;
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
#weighted-options .option-wrapper .add-option-div button:active {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
#weighted-options p.option-description {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
#weighted-options p.hint-text {
|
||||
margin: 0 0 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
#weighted-options table {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
#weighted-options table th, #weighted-options table td {
|
||||
border: none;
|
||||
}
|
||||
#weighted-options table td {
|
||||
padding: 5px;
|
||||
}
|
||||
#weighted-options table .td-left {
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
padding-right: 1rem;
|
||||
width: 200px;
|
||||
}
|
||||
#weighted-options table .td-middle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#weighted-options table .td-right {
|
||||
width: 4rem;
|
||||
text-align: right;
|
||||
}
|
||||
#weighted-options table .td-delete {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
#weighted-options table .range-option-delete {
|
||||
cursor: pointer;
|
||||
}
|
||||
#weighted-options #weighted-options-button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
#weighted-options #user-message {
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
#weighted-options #user-message.visible {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
#weighted-options h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
#weighted-options h2, #weighted-options details summary.h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: none;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
#weighted-options h3, #weighted-options h4, #weighted-options h5, #weighted-options h6 {
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
text-transform: none;
|
||||
cursor: unset;
|
||||
}
|
||||
#weighted-options h3.option-group-header {
|
||||
margin-top: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
#weighted-options a {
|
||||
color: #ffef00;
|
||||
cursor: pointer;
|
||||
}
|
||||
#weighted-options input:not([type]) {
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
#weighted-options input:not([type]):focus {
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
#weighted-options .invisible {
|
||||
display: none;
|
||||
}
|
||||
#weighted-options .unsupported-option {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
#weighted-options .set-container, #weighted-options .dict-container, #weighted-options .list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
max-height: 15rem;
|
||||
min-width: 14.5rem;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
#weighted-options .set-container .divider, #weighted-options .dict-container .divider, #weighted-options .list-container .divider {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
#weighted-options .set-container .set-entry, #weighted-options .set-container .dict-entry, #weighted-options .set-container .list-entry, #weighted-options .dict-container .set-entry, #weighted-options .dict-container .dict-entry, #weighted-options .dict-container .list-entry, #weighted-options .list-container .set-entry, #weighted-options .list-container .dict-entry, #weighted-options .list-container .list-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-top: 0.25rem;
|
||||
user-select: none;
|
||||
line-height: 1rem;
|
||||
}
|
||||
#weighted-options .set-container .set-entry:hover, #weighted-options .set-container .dict-entry:hover, #weighted-options .set-container .list-entry:hover, #weighted-options .dict-container .set-entry:hover, #weighted-options .dict-container .dict-entry:hover, #weighted-options .dict-container .list-entry:hover, #weighted-options .list-container .set-entry:hover, #weighted-options .list-container .dict-entry:hover, #weighted-options .list-container .list-entry:hover {
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
}
|
||||
#weighted-options .set-container .set-entry input[type=checkbox], #weighted-options .set-container .dict-entry input[type=checkbox], #weighted-options .set-container .list-entry input[type=checkbox], #weighted-options .dict-container .set-entry input[type=checkbox], #weighted-options .dict-container .dict-entry input[type=checkbox], #weighted-options .dict-container .list-entry input[type=checkbox], #weighted-options .list-container .set-entry input[type=checkbox], #weighted-options .list-container .dict-entry input[type=checkbox], #weighted-options .list-container .list-entry input[type=checkbox] {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
#weighted-options .set-container .set-entry input[type=number], #weighted-options .set-container .dict-entry input[type=number], #weighted-options .set-container .list-entry input[type=number], #weighted-options .dict-container .set-entry input[type=number], #weighted-options .dict-container .dict-entry input[type=number], #weighted-options .dict-container .list-entry input[type=number], #weighted-options .list-container .set-entry input[type=number], #weighted-options .list-container .dict-entry input[type=number], #weighted-options .list-container .list-entry input[type=number] {
|
||||
max-width: 1.5rem;
|
||||
max-height: 1rem;
|
||||
margin-left: 0.125rem;
|
||||
text-align: center;
|
||||
/* Hide arrows on input[type=number] fields */
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
#weighted-options .set-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
#weighted-options .set-container .set-entry label, #weighted-options .set-container .dict-entry label, #weighted-options .set-container .list-entry label, #weighted-options .dict-container .set-entry label, #weighted-options .dict-container .dict-entry label, #weighted-options .dict-container .list-entry label, #weighted-options .list-container .set-entry label, #weighted-options .list-container .dict-entry label, #weighted-options .list-container .list-entry label {
|
||||
flex-grow: 1;
|
||||
margin-right: 0;
|
||||
min-width: unset;
|
||||
display: unset;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait) {
|
||||
#weighted-options .game-options {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#game-options table label {
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=weightedOptions.css.map */
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["weightedOptions.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAOZ;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;;AAMhB;EACI;;;AAGJ;EACI;IACI;IACA;;EAGJ;IACI;IACA","file":"weightedOptions.css"}
|
||||
274
WebHostLib/static/styles/weightedOptions/weightedOptions.scss
Normal file
274
WebHostLib/static/styles/weightedOptions/weightedOptions.scss
Normal file
@@ -0,0 +1,274 @@
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/grass.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
scroll-padding-top: 90px;
|
||||
}
|
||||
|
||||
#weighted-options{
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
|
||||
#weighted-options-header{
|
||||
h1{
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
h1:nth-child(2){
|
||||
font-size: 1.4rem;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.js-warning-banner{
|
||||
width: calc(100% - 1rem);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background-color: #f3f309;
|
||||
color: #000000;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.option-wrapper{
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.add-option-div{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
button{
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0 0 0 0.15rem;
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
|
||||
&:active{
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p{
|
||||
&.option-description{
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
&.hint-text{
|
||||
margin: 0 0 1rem;
|
||||
font-style: italic;
|
||||
};
|
||||
}
|
||||
|
||||
table{
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
th, td{
|
||||
border: none;
|
||||
}
|
||||
|
||||
td{
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.td-left{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
padding-right: 1rem;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.td-middle{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.td-right{
|
||||
width: 4rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.td-delete{
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.range-option-delete{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
#weighted-options-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
|
||||
&.visible{
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
h2, details summary.h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: none;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
text-transform: none;
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
h3{
|
||||
&.option-group-header{
|
||||
margin-top: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
color: #ffef00;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
|
||||
&:focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.invisible{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.unsupported-option{
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.set-container, .dict-container, .list-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(20, 20, 20, 0.25);
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
max-height: 15rem;
|
||||
min-width: 14.5rem;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
.divider{
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.set-entry, .dict-entry, .list-entry{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-top: 0.25rem;
|
||||
user-select: none;
|
||||
line-height: 1rem;
|
||||
|
||||
&:hover{
|
||||
background-color: rgba(20, 20, 20, 0.25);
|
||||
}
|
||||
|
||||
input[type=checkbox]{
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
input[type=number]{
|
||||
max-width: 1.5rem;
|
||||
max-height: 1rem;
|
||||
margin-left: 0.125rem;
|
||||
text-align: center;
|
||||
|
||||
/* Hide arrows on input[type=number] fields */
|
||||
-moz-appearance: textfield;
|
||||
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label{
|
||||
flex-grow: 1;
|
||||
margin-right: 0;
|
||||
min-width: unset;
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hidden{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||
#weighted-options .game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#game-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/lttp-tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttp-tracker.js") }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ bow_url }}" class="{{ 'acquired' if bow_acquired }}" /></td>
|
||||
<td><img src="{{ icons["Blue Boomerang"] }}" class="{{ 'acquired' if 'Blue Boomerang' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Red Boomerang"] }}" class="{{ 'acquired' if 'Red Boomerang' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Hookshot"] }}" class="{{ 'acquired' if 'Hookshot' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Magic Powder"] }}" class="powder-fix {{ 'acquired' if 'Magic Powder' in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Fire Rod"] }}" class="{{ 'acquired' if "Fire Rod" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Ice Rod"] }}" class="{{ 'acquired' if "Ice Rod" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Bombos"] }}" class="{{ 'acquired' if "Bombos" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Ether"] }}" class="{{ 'acquired' if "Ether" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Quake"] }}" class="{{ 'acquired' if "Quake" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Lamp"] }}" class="{{ 'acquired' if "Lamp" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Hammer"] }}" class="{{ 'acquired' if "Hammer" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Flute"] }}" class="{{ 'acquired' if "Flute" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Bug Catching Net"] }}" class="{{ 'acquired' if "Bug Catching Net" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Book of Mudora"] }}" class="{{ 'acquired' if "Book of Mudora" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Bottle"] }}" class="{{ 'acquired' if "Bottle" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cane of Somaria"] }}" class="{{ 'acquired' if "Cane of Somaria" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cane of Byrna"] }}" class="{{ 'acquired' if "Cane of Byrna" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cape"] }}" class="{{ 'acquired' if "Cape" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Magic Mirror"] }}" class="{{ 'acquired' if "Magic Mirror" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Pegasus Boots"] }}" class="{{ 'acquired' if "Pegasus Boots" in acquired_items }}" /></td>
|
||||
<td><img src="{{ glove_url }}" class="{{ 'acquired' if glove_acquired }}" /></td>
|
||||
<td><img src="{{ icons["Flippers"] }}" class="{{ 'acquired' if "Flippers" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Moon Pearl"] }}" class="{{ 'acquired' if "Moon Pearl" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Mushroom"] }}" class="{{ 'acquired' if "Mushroom" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ sword_url }}" class="{{ 'acquired' if sword_acquired }}" /></td>
|
||||
<td><img src="{{ shield_url }}" class="{{ 'acquired' if shield_acquired }}" /></td>
|
||||
<td><img src="{{ mail_url }}" class="acquired" /></td>
|
||||
<td><img src="{{ icons["Shovel"] }}" class="{{ 'acquired' if "Shovel" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Triforce"] }}" class="{{ 'acquired' if "Triforce" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="counter"><img src="{{ icons["Chest"] }}" /></th>
|
||||
{% if key_locations and "Universal" not in key_locations %}
|
||||
<th class="counter"><img src="{{ icons["Small Key"] }}" /></th>
|
||||
{% endif %}
|
||||
{% if big_key_locations %}
|
||||
<th><img src="{{ icons["Big Key"] }}" /></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for area in sp_areas %}
|
||||
<tr>
|
||||
<td>{{ area }}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
{% if key_locations and "Universal" not in key_locations %}
|
||||
<td class="counter">
|
||||
{{ inventory[small_key_ids[area]] if area in key_locations else '—' }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if big_key_locations %}
|
||||
<td>
|
||||
{{ '✔' if area in big_key_locations and inventory[big_key_ids[area]] else ('—' if area not in big_key_locations else '') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,52 +6,42 @@
|
||||
{% endblock %}
|
||||
|
||||
{# List all tracker-relevant icons. Format: (Name, Image URL) #}
|
||||
{%- set icons = {
|
||||
"Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
"Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
|
||||
"Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
|
||||
"Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920",
|
||||
"Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920",
|
||||
"Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920",
|
||||
"Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
|
||||
"Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
|
||||
"Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920",
|
||||
"Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920",
|
||||
"Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920",
|
||||
"Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
|
||||
"Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
|
||||
"Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
|
||||
"Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
|
||||
"Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
|
||||
"Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
|
||||
"Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
|
||||
"Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
|
||||
"Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
|
||||
"Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
|
||||
"Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
|
||||
"Magic Powder": "https://www.zeldadungeon.net/wiki/images/thumb/6/62/MagicPowder-ALttP-Sprite.png/86px-MagicPowder-ALttP-Sprite.png",
|
||||
"Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
|
||||
"Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
|
||||
"Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
|
||||
"Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
|
||||
"Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
|
||||
"Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
|
||||
"Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
|
||||
"Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
|
||||
"Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
|
||||
"Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
|
||||
"Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
|
||||
"Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
|
||||
"Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
|
||||
"Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
|
||||
"Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
|
||||
"Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
|
||||
"Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
|
||||
{% set icons = {
|
||||
"Blue Shield": "https://www.zeldadungeon.net/wiki/images/thumb/c/c3/FightersShield-ALttP-Sprite.png/100px-FightersShield-ALttP-Sprite.png",
|
||||
"Red Shield": "https://www.zeldadungeon.net/wiki/images/thumb/9/9e/FireShield-ALttP-Sprite.png/111px-FireShield-ALttP-Sprite.png",
|
||||
"Mirror Shield": "https://www.zeldadungeon.net/wiki/images/thumb/e/e3/MirrorShield-ALttP-Sprite.png/105px-MirrorShield-ALttP-Sprite.png",
|
||||
"Progressive Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/c/cc/ALttP_Master_Sword_Sprite.png",
|
||||
"Progressive Bow": "https://www.zeldadungeon.net/wiki/images/thumb/8/8c/BowArrows-ALttP-Sprite.png/120px-BowArrows-ALttP-Sprite.png",
|
||||
"Progressive Glove": "https://www.zeldadungeon.net/wiki/images/thumb/4/41/PowerGlove-ALttP-Sprite.png/105px-PowerGlove-ALttP-Sprite.png",
|
||||
"Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png",
|
||||
"Flippers": "https://www.zeldadungeon.net/wiki/images/thumb/b/bc/ZoraFlippers-ALttP-Sprite.png/112px-ZoraFlippers-ALttP-Sprite.png",
|
||||
"Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png",
|
||||
"Blue Boomerang": "https://www.zeldadungeon.net/wiki/images/thumb/f/f0/Boomerang-ALttP-Sprite.png/86px-Boomerang-ALttP-Sprite.png",
|
||||
"Red Boomerang": "https://www.zeldadungeon.net/wiki/images/thumb/3/3c/MagicalBoomerang-ALttP-Sprite.png/86px-MagicalBoomerang-ALttP-Sprite.png",
|
||||
"Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png",
|
||||
"Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png",
|
||||
"Magic Powder": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png",
|
||||
"Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png",
|
||||
"Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png",
|
||||
"Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png",
|
||||
"Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png",
|
||||
"Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png",
|
||||
"Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png",
|
||||
"Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png",
|
||||
"Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png",
|
||||
"Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png",
|
||||
"Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png",
|
||||
"Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png",
|
||||
"Bottles": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png",
|
||||
"Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png",
|
||||
"Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png",
|
||||
"Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png",
|
||||
"Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png",
|
||||
"Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png",
|
||||
"Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png",
|
||||
"Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
|
||||
"Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
|
||||
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/38/ALttP_Bomb_Sprite.png",
|
||||
"Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png",
|
||||
"Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png",
|
||||
"Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
|
||||
"Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
|
||||
"Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
|
||||
@@ -68,33 +58,93 @@
|
||||
"Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
|
||||
"Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
|
||||
"Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74",
|
||||
} -%}
|
||||
} %}
|
||||
|
||||
{% set inventory_order = [
|
||||
"Progressive Sword",
|
||||
"Progressive Bow",
|
||||
"Blue Boomerang",
|
||||
"Red Boomerang",
|
||||
"Hookshot",
|
||||
"Bombs",
|
||||
"Mushroom",
|
||||
"Magic Powder",
|
||||
"Fire Rod",
|
||||
"Ice Rod",
|
||||
"Bombos",
|
||||
"Ether",
|
||||
"Quake",
|
||||
"Lamp",
|
||||
"Hammer",
|
||||
"Flute",
|
||||
"Bug Catching Net",
|
||||
"Book of Mudora",
|
||||
"Cane of Somaria",
|
||||
"Cane of Byrna",
|
||||
"Cape",
|
||||
"Magic Mirror",
|
||||
"Shovel",
|
||||
"Pegasus Boots",
|
||||
"Flippers",
|
||||
"Progressive Glove",
|
||||
"Moon Pearl",
|
||||
"Bottles",
|
||||
"Triforce Piece",
|
||||
"Triforce",
|
||||
] %}
|
||||
|
||||
{% set dungeon_keys = {
|
||||
"Hyrule Castle": ("Small Key (Hyrule Castle)", "Big Key (Hyrule Castle)"),
|
||||
"Agahnims Tower": ("Small Key (Agahnims Tower)", "Big Key (Agahnims Tower)"),
|
||||
"Eastern Palace": ("Small Key (Eastern Palace)", "Big Key (Eastern Palace)"),
|
||||
"Desert Palace": ("Small Key (Desert Palace)", "Big Key (Desert Palace)"),
|
||||
"Tower of Hera": ("Small Key (Tower of Hera)", "Big Key (Tower of Hera)"),
|
||||
"Palace of Darkness": ("Small Key (Palace of Darkness)", "Big Key (Palace of Darkness)"),
|
||||
"Thieves Town": ("Small Key (Thieves Town)", "Big Key (Thieves Town)"),
|
||||
"Skull Woods": ("Small Key (Skull Woods)", "Big Key (Skull Woods)"),
|
||||
"Swamp Palace": ("Small Key (Swamp Palace)", "Big Key (Swamp Palace)"),
|
||||
"Ice Palace": ("Small Key (Ice Palace)", "Big Key (Ice Palace)"),
|
||||
"Misery Mire": ("Small Key (Misery Mire)", "Big Key (Misery Mire)"),
|
||||
"Turtle Rock": ("Small Key (Turtle Rock)", "Big Key (Turtle Rock)"),
|
||||
"Ganons Tower": ("Small Key (Ganons Tower)", "Big Key (Ganons Tower)"),
|
||||
} %}
|
||||
|
||||
{% set multi_items = [
|
||||
"Progressive Sword",
|
||||
"Progressive Glove",
|
||||
"Progressive Bow",
|
||||
"Bottles",
|
||||
"Triforce Piece",
|
||||
] %}
|
||||
|
||||
{%- block custom_table_headers %}
|
||||
{#- macro that creates a table header with display name and image -#}
|
||||
{%- macro make_header(name, img_src) %}
|
||||
<th class="center-column">
|
||||
<img height="24" src="{{ img_src }}" title="{{ name }}" alt="{{ name }}" />
|
||||
</th>
|
||||
{% endmacro -%}
|
||||
|
||||
{#- call the macro to build the table header -#}
|
||||
{%- for name in tracking_names %}
|
||||
{%- if name in icons -%}
|
||||
{#- macro that creates a table header with display name and image -#}
|
||||
{%- macro make_header(name, img_src) %}
|
||||
<th class="center-column">
|
||||
<img class="icon-sprite" src="{{ icons[name] }}" alt="{{ name | e }}" title="{{ name | e }}" />
|
||||
<img height="24" src="{{ img_src }}" title="{{ name }}" alt="{{ name }}">
|
||||
</th>
|
||||
{%- endif %}
|
||||
{% endfor -%}
|
||||
{% endmacro -%}
|
||||
|
||||
{#- call the macro to build the table header -#}
|
||||
{%- for item in inventory_order %}
|
||||
{%- if item in icons -%}
|
||||
<th class="center-column">
|
||||
<img class="icon-sprite" src="{{ icons[item] }}" alt="{{ item | e }}" title="{{ item | e }}">
|
||||
</th>
|
||||
{%- endif %}
|
||||
{% endfor -%}
|
||||
{% endblock %}
|
||||
|
||||
{# build each row of custom entries #}
|
||||
{% block custom_table_row scoped %}
|
||||
{%- for id in tracking_ids -%}
|
||||
{# {{ checks }}#}
|
||||
{%- if inventories[(team, player)][id] -%}
|
||||
{%- for item in inventory_order -%}
|
||||
{%- if inventories[(team, player)][item] -%}
|
||||
<td class="center-column item-acquired">
|
||||
{% if id in multi_items %}{{ inventories[(team, player)][id] }}{% else %}✔️{% endif %}
|
||||
{% if item in multi_items %}
|
||||
{{ inventories[(team, player)][item] }}
|
||||
{% else %}
|
||||
✔️
|
||||
{% endif %}
|
||||
</td>
|
||||
{%- else -%}
|
||||
<td></td>
|
||||
@@ -104,102 +154,95 @@
|
||||
|
||||
{% block custom_tables %}
|
||||
|
||||
{% for team, _ in total_team_locations.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="area-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">#</th>
|
||||
<th rowspan="2">Name</th>
|
||||
{% for area in ordered_areas %}
|
||||
{% set colspan = 1 %}
|
||||
{% if area in key_locations %}
|
||||
{% set colspan = colspan + 1 %}
|
||||
{% endif %}
|
||||
{% if area in big_key_locations %}
|
||||
{% set colspan = colspan + 1 %}
|
||||
{% endif %}
|
||||
{% if area in icons %}
|
||||
<th colspan="{{ colspan }}" class="center-column upper-row">
|
||||
<img class="icon-sprite" src="{{ icons[area] }}" alt="{{ area }}" title="{{ area }}"></th>
|
||||
{%- else -%}
|
||||
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
<th rowspan="2" class="center-column">%</th>
|
||||
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for area in ordered_areas %}
|
||||
{% for team in total_team_locations %}
|
||||
<div class="table-wrapper">
|
||||
<table class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">#</th>
|
||||
<th rowspan="2">Name</th>
|
||||
{% for region in known_regions %}
|
||||
{% set colspan = 1 %}
|
||||
{% if region == "Agahnims Tower" %}
|
||||
{% set colspan = 2 %}
|
||||
{% elif region in dungeon_keys %}
|
||||
{% set colspan = 3 %}
|
||||
{% endif %}
|
||||
|
||||
{% if region in icons %}
|
||||
<th colspan="{{ colspan }}" class="center-column upper-row">
|
||||
<img class="icon-sprite" src="{{ icons[region] }}" alt="{{ region }}" title="{{ region }}">
|
||||
</th>
|
||||
{% else %}
|
||||
<th colspan="{{ colspan }}" class="center-column">{{ region }}</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<th class="center-column">Total</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for region in known_regions %}
|
||||
<th class="center-column lower-row fraction">
|
||||
<img class="icon-sprite" src="{{ icons["Chest"] }}" alt="Checks" title="Checks Complete">
|
||||
</th>
|
||||
|
||||
{% if region in dungeon_keys %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="icon-sprite" src="{{ icons["Small Key"] }}" alt="Small Key" title="Small Keys">
|
||||
</th>
|
||||
|
||||
{# Special check just for Agahnims Tower, which has no big keys. #}
|
||||
{% if region != "Agahnims Tower" %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="icon-sprite" src="{{ icons["Big Key"] }}" alt="Big Key" title="Big Keys">
|
||||
</th>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# For "total" checks #}
|
||||
<th class="center-column lower-row fraction">
|
||||
<img class="icon-sprite" src="{{ icons["Chest"] }}" alt="Checks" title="Checks Complete">
|
||||
<img class="icon-sprite" src="{{ icons["Chest"] }}" alt="Checks" title="Total Checks Complete">
|
||||
</th>
|
||||
{% if area in key_locations %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="icon-sprite" src="{{ icons["Small Key"] }}" alt="Small Key" title="Small Keys">
|
||||
</th>
|
||||
{% endif %}
|
||||
{% if area in big_key_locations %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="icon-sprite" src="{{ icons["Big Key"] }}" alt="Big Key" title="Big Keys">
|
||||
</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for (checks_team, player), area_checks in checks_done.items() if games[(team, player)] == current_tracker and team == checks_team -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ player }}</a></td>
|
||||
<td>{{ player_names_with_alias[(team, player)] | e }}</td>
|
||||
{%- for area in ordered_areas -%}
|
||||
{% if (team, player) in checks_in_area and area in checks_in_area[(team, player)] %}
|
||||
{%- set checks_done = area_checks[area] -%}
|
||||
{%- set checks_total = checks_in_area[(team, player)][area] -%}
|
||||
{%- if checks_done == checks_total -%}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for (player_team, player), player_regions in regions.items() if team == player_team %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">
|
||||
{{ player }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ player_names_with_alias[(team, player)] | e }}</td>
|
||||
|
||||
{% for region, counts in player_regions.items() %}
|
||||
<td class="item-acquired center-column">
|
||||
{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column">{{ inventories[(team, player)][small_key_ids[area]] }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column">{% if inventories[(team, player)][big_key_ids[area]] %}✔️{% endif %}</td>
|
||||
{%- endif -%}
|
||||
{% else %}
|
||||
<td class="center-column"></td>
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
{{ counts.checked }}/{{ counts.total }}
|
||||
</td>
|
||||
|
||||
<td class="center-column">
|
||||
{% set location_count = locations[(team, player)] | length %}
|
||||
{%- if locations[(team, player)] | length > 0 -%}
|
||||
{% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %}
|
||||
{{ "{0:.2f}".format(percentage_of_completion) }}
|
||||
{%- else -%}
|
||||
100.00
|
||||
{%- endif -%}
|
||||
</td>
|
||||
{% if region in dungeon_keys %}
|
||||
<td class="center-column">
|
||||
{{ inventories[(team, player)][dungeon_keys[region][0]] }}
|
||||
</td>
|
||||
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{# Special check just for Agahnims Tower, which has no big keys. #}
|
||||
{% if region != "Agahnims Tower" %}
|
||||
<td class="center-column">
|
||||
{% if inventories[(team, player)][dungeon_keys[region][1]] %}
|
||||
✔️
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-options.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-options.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/'+theme+'Header.html' %}
|
||||
<div id="player-options" class="markdown" data-game="{{ game }}">
|
||||
<div id="user-message"></div>
|
||||
<h1><span id="game-name">Player</span> Options</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>
|
||||
A more advanced options configuration for all games can be found on the
|
||||
<a href="/weighted-options">Weighted options</a> page.
|
||||
<br />
|
||||
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
||||
<br />
|
||||
You may also download the
|
||||
<a href="/static/generated/configs/{{ game }}.yaml">template file for this game</a>.
|
||||
</p>
|
||||
|
||||
<div id="meta-options">
|
||||
<div>
|
||||
<label for="player-name">
|
||||
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
|
||||
</label>
|
||||
<input id="player-name" placeholder="Player" data-key="name" maxlength="16" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="game-options-preset">
|
||||
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
|
||||
</label>
|
||||
<select id="game-options-preset">
|
||||
<option value="__default">Defaults</option>
|
||||
<option value="__custom" hidden>Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h2>Game Options</h2>
|
||||
<div id="game-options">
|
||||
<div id="game-options-left" class="left"></div>
|
||||
<div id="game-options-right" class="right"></div>
|
||||
</div>
|
||||
|
||||
<div id="player-options-button-row">
|
||||
<button id="export-options">Export Options</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
210
WebHostLib/templates/playerOptions/macros.html
Normal file
210
WebHostLib/templates/playerOptions/macros.html
Normal file
@@ -0,0 +1,210 @@
|
||||
{% macro Toggle(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="select-container">
|
||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% if option.default == 1 %}
|
||||
<option value="false">No</option>
|
||||
<option value="true" selected>Yes</option>
|
||||
{% else %}
|
||||
<option value="false" selected>No</option>
|
||||
<option value="true">Yes</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
{{ RandomizeButton(option_name, option) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Choice(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="select-container">
|
||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% for id, name in option.name_lookup.items() %}
|
||||
{% if name != "random" %}
|
||||
{% if option.default == id %}
|
||||
<option value="{{ name }}" selected>{{ option.get_option_name(id) }}</option>
|
||||
{% else %}
|
||||
<option value="{{ name }}">{{ option.get_option_name(id) }}</option>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
{{ RandomizeButton(option_name, option) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Range(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="range-container">
|
||||
<input
|
||||
type="range"
|
||||
id="{{ option_name }}"
|
||||
name="{{ option_name }}"
|
||||
min="{{ option.range_start }}"
|
||||
max="{{ option.range_end }}"
|
||||
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
|
||||
{{ "disabled" if option.default == "random" }}
|
||||
/>
|
||||
<span id="{{ option_name }}-value" class="range-value js-required">
|
||||
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
|
||||
</span>
|
||||
{{ RandomizeButton(option_name, option) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro NamedRange(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="named-range-container">
|
||||
<select id="{{ option_name }}-select" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% for key, val in option.special_range_names.items() %}
|
||||
{% if option.default == val %}
|
||||
<option value="{{ val }}" selected>{{ key }} ({{ val }})</option>
|
||||
{% else %}
|
||||
<option value="{{ val }}">{{ key }} ({{ val }})</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<option value="custom" hidden>Custom</option>
|
||||
</select>
|
||||
<div class="named-range-wrapper">
|
||||
<input
|
||||
type="range"
|
||||
id="{{ option_name }}"
|
||||
name="{{ option_name }}"
|
||||
min="{{ option.range_start }}"
|
||||
max="{{ option.range_end }}"
|
||||
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
|
||||
{{ "disabled" if option.default == "random" }}
|
||||
/>
|
||||
<span id="{{ option_name }}-value" class="range-value js-required">
|
||||
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
|
||||
</span>
|
||||
{{ RandomizeButton(option_name, option) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro FreeText(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="free-text-container">
|
||||
<input type="text" id="{{ option_name }}" name="{{ option_name }}" value="{{ option.default }}" />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TextChoice(option_name, option) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="text-choice-container">
|
||||
<div class="text-choice-wrapper">
|
||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% for id, name in option.name_lookup.items()|sort %}
|
||||
{% if name != "random" %}
|
||||
{% if option.default == id %}
|
||||
<option value="{{ name }}" selected>{{ option.get_option_name(id) }}</option>
|
||||
{% else %}
|
||||
<option value="{{ name }}">{{ option.get_option_name(id) }}</option>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<option value="custom" hidden>Custom</option>
|
||||
</select>
|
||||
{{ RandomizeButton(option_name, option) }}
|
||||
</div>
|
||||
<input type="text" id="{{ option_name }}-custom" name="{{ option_name }}-custom" data-option-name="{{ option_name }}" placeholder="Custom value..." />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ItemDict(option_name, option, world) %}
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="option-container">
|
||||
{% for item_name in (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 grop_name in option.default }} />
|
||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if world.location_name_groups.keys()|length > 1 %}
|
||||
<div class="option-divider"> </div>
|
||||
{% endif %}
|
||||
{% for location_name in (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 %}
|
||||
166
WebHostLib/templates/playerOptions/playerOptions.html
Normal file
166
WebHostLib/templates/playerOptions/playerOptions.html
Normal file
@@ -0,0 +1,166 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import 'playerOptions/macros.html' as inputs %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ world_name }} Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/playerOptions/playerOptions.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/playerOptions.js") }}"></script>
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
.js-required{
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/'+theme+'Header.html' %}
|
||||
<div id="player-options" class="markdown" data-game="{{ world_name }}" data-presets="{{ presets }}">
|
||||
<noscript>
|
||||
<div class="js-warning-banner">
|
||||
This page has reduced functionality without JavaScript.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div id="user-message">{{ message }}</div>
|
||||
|
||||
<div id="player-options-header">
|
||||
<h1>{{ world_name }}</h1>
|
||||
<h1>Player Options</h1>
|
||||
</div>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>
|
||||
A more advanced options configuration for all games can be found on the
|
||||
<a href="weighted-options">Weighted options</a> page.
|
||||
<br />
|
||||
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
||||
<br />
|
||||
You may also download the
|
||||
<a href="/static/generated/configs/{{ world_name }}.yaml">template file for this game</a>.
|
||||
</p>
|
||||
|
||||
<form id="options-form" method="post" enctype="application/x-www-form-urlencoded" action="generate-yaml">
|
||||
<div id="meta-options">
|
||||
<div>
|
||||
<label for="player-name">
|
||||
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
|
||||
</label>
|
||||
<input id="player-name" placeholder="Player" name="name" maxlength="16" />
|
||||
</div>
|
||||
<div class="js-required">
|
||||
<label for="game-options-preset">
|
||||
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
|
||||
</label>
|
||||
<select id="game-options-preset" name="game-options-preset" disabled>
|
||||
<option value="default">Default</option>
|
||||
{% for preset_name in world.web.options_presets %}
|
||||
<option value="{{ preset_name }}">{{ preset_name }}</option>
|
||||
{% endfor %}
|
||||
<option value="custom" hidden>Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="option-groups">
|
||||
{% for group_name, group_options in option_groups.items() %}
|
||||
<details class="group-container" {% if loop.index == 1 %}open{% endif %}>
|
||||
<summary class="h2">{{ group_name }}</summary>
|
||||
<div class="game-options">
|
||||
<div class="left">
|
||||
{% for option_name, option in group_options.items() %}
|
||||
{% if loop.index <= (loop.length / 2)|round(0,"ceil") %}
|
||||
{% if issubclass(option, Options.Toggle) %}
|
||||
{{ inputs.Toggle(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.TextChoice) %}
|
||||
{{ inputs.TextChoice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Choice) %}
|
||||
{{ inputs.Choice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.NamedRange) %}
|
||||
{{ inputs.NamedRange(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Range) %}
|
||||
{{ inputs.Range(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.FreeText) %}
|
||||
{{ inputs.FreeText(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
||||
{{ inputs.ItemDict(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||
{{ inputs.OptionList(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
||||
{{ inputs.LocationSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
||||
{{ inputs.ItemSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
||||
{{ inputs.OptionSet(option_name, option) }}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="right">
|
||||
{% for option_name, option in group_options.items() %}
|
||||
{% if loop.index > (loop.length / 2)|round(0,"ceil") %}
|
||||
{% if issubclass(option, Options.Toggle) %}
|
||||
{{ inputs.Toggle(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.TextChoice) %}
|
||||
{{ inputs.TextChoice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Choice) %}
|
||||
{{ inputs.Choice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.NamedRange) %}
|
||||
{{ inputs.NamedRange(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Range) %}
|
||||
{{ inputs.Range(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.FreeText) %}
|
||||
{{ inputs.FreeText(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
||||
{{ inputs.ItemDict(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||
{{ inputs.OptionList(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
||||
{{ inputs.LocationSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
||||
{{ inputs.ItemSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
||||
{{ inputs.OptionSet(option_name, option) }}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="player-options-button-row">
|
||||
<input type="submit" name="intent-export" value="Export Options" />
|
||||
<input type="submit" name="intent-generate" value="Generate Single-Player Game">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -24,7 +24,6 @@
|
||||
<li><a href="/games">Supported Games Page</a></li>
|
||||
<li><a href="/tutorial">Tutorials Page</a></li>
|
||||
<li><a href="/user-content">User Content</a></li>
|
||||
<li><a href="/weighted-options">Weighted Options Page</a></li>
|
||||
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
|
||||
<li><a href="/glossary/en">Glossary</a></li>
|
||||
</ul>
|
||||
@@ -50,8 +49,12 @@
|
||||
<ul>
|
||||
{% for game in games | title_sorted %}
|
||||
{% if game['has_settings'] %}
|
||||
<li><a href="{{ url_for('player_options', game=game['title']) }}">{{ game['title'] }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ game['title'] }}</li>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('player_options', game=game['title']) }}">Player Options</a></li>
|
||||
<li><a href="{{ url_for('weighted_options', game=game['title']) }}">Weighted Options</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -41,28 +41,28 @@
|
||||
</div>
|
||||
{% for game_name in worlds | title_sorted %}
|
||||
{% set world = worlds[game_name] %}
|
||||
<h2 class="collapse-toggle" data-game="{{ game_name }}">
|
||||
<span class="collapse-arrow">▶</span>{{ game_name }}
|
||||
</h2>
|
||||
<p class="collapsed">
|
||||
<details data-game="{{ game_name }}">
|
||||
<summary class="h2">{{ game_name }}</summary>
|
||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
||||
{% if world.web.tutorials %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("tutorial_landing") }}#{{ game_name }}">Setup Guides</a>
|
||||
<a href="{{ url_for("tutorial_landing", _anchor = game_name | urlencode) }}">Setup Guides</a>
|
||||
{% endif %}
|
||||
{% if world.web.options_page is string %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ world.web.options_page }}">Options Page</a>
|
||||
<a href="{{ world.web.options_page }}">Options Page (External Link)</a>
|
||||
{% elif world.web.options_page %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("weighted_options", game=game_name) }}">Advanced Options</a>
|
||||
{% endif %}
|
||||
{% if world.web.bug_report_page %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ world.web.bug_report_page }}">Report a Bug</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,73 +1,89 @@
|
||||
{%- set icons = {
|
||||
"Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
"Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
|
||||
"Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
|
||||
"Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920",
|
||||
"Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920",
|
||||
"Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920",
|
||||
"Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
|
||||
"Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
|
||||
"Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920",
|
||||
"Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920",
|
||||
"Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920",
|
||||
"Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
|
||||
"Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
|
||||
"Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
|
||||
"Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
|
||||
"Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
|
||||
"Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
|
||||
"Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
|
||||
"Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
|
||||
"Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
|
||||
"Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
|
||||
"Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
|
||||
"Magic Powder": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec",
|
||||
"Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
|
||||
"Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
|
||||
"Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
|
||||
"Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
|
||||
"Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
|
||||
"Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
|
||||
"Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
|
||||
"Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
|
||||
"Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
|
||||
"Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
|
||||
"Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
|
||||
"Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
|
||||
"Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
|
||||
"Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
|
||||
"Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
|
||||
"Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
|
||||
"Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
|
||||
{% set icons = {
|
||||
"Blue Shield": "https://www.zeldadungeon.net/wiki/images/thumb/c/c3/FightersShield-ALttP-Sprite.png/100px-FightersShield-ALttP-Sprite.png",
|
||||
"Red Shield": "https://www.zeldadungeon.net/wiki/images/thumb/9/9e/FireShield-ALttP-Sprite.png/111px-FireShield-ALttP-Sprite.png",
|
||||
"Mirror Shield": "https://www.zeldadungeon.net/wiki/images/thumb/e/e3/MirrorShield-ALttP-Sprite.png/105px-MirrorShield-ALttP-Sprite.png",
|
||||
"Fighter Sword": "https://upload.wikimedia.org/wikibooks/en/8/8e/Zelda_ALttP_item_L-1_Sword.png",
|
||||
"Master Sword": "https://upload.wikimedia.org/wikibooks/en/8/87/BS_Zelda_AST_item_L-2_Sword.png",
|
||||
"Tempered Sword": "https://upload.wikimedia.org/wikibooks/en/c/cc/BS_Zelda_AST_item_L-3_Sword.png",
|
||||
"Golden Sword": "https://upload.wikimedia.org/wikibooks/en/4/40/BS_Zelda_AST_item_L-4_Sword.png",
|
||||
"Bow": "https://www.zeldadungeon.net/wiki/images/thumb/8/8c/BowArrows-ALttP-Sprite.png/120px-BowArrows-ALttP-Sprite.png",
|
||||
"Silver Bow": "https://upload.wikimedia.org/wikibooks/en/6/69/Zelda_ALttP_item_Silver_Arrows.png",
|
||||
"Green Mail": "https://upload.wikimedia.org/wikibooks/en/d/dd/Zelda_ALttP_item_Green_Mail.png",
|
||||
"Blue Mail": "https://upload.wikimedia.org/wikibooks/en/b/b5/Zelda_ALttP_item_Blue_Mail.png",
|
||||
"Red Mail": "https://upload.wikimedia.org/wikibooks/en/d/db/Zelda_ALttP_item_Red_Mail.png",
|
||||
"Power Glove": "https://www.zeldadungeon.net/wiki/images/thumb/4/41/PowerGlove-ALttP-Sprite.png/105px-PowerGlove-ALttP-Sprite.png",
|
||||
"Titan Mitts": "https://www.zeldadungeon.net/wiki/images/thumb/7/75/TitanMitt-ALttP-Sprite.png/105px-TitanMitt-ALttP-Sprite.png",
|
||||
"Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png",
|
||||
"Flippers": "https://www.zeldadungeon.net/wiki/images/thumb/b/bc/ZoraFlippers-ALttP-Sprite.png/112px-ZoraFlippers-ALttP-Sprite.png",
|
||||
"Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png",
|
||||
"Blue Boomerang": "https://www.zeldadungeon.net/wiki/images/thumb/f/f0/Boomerang-ALttP-Sprite.png/86px-Boomerang-ALttP-Sprite.png",
|
||||
"Red Boomerang": "https://www.zeldadungeon.net/wiki/images/thumb/3/3c/MagicalBoomerang-ALttP-Sprite.png/86px-MagicalBoomerang-ALttP-Sprite.png",
|
||||
"Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png",
|
||||
"Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png",
|
||||
"Magic Powder": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png",
|
||||
"Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png",
|
||||
"Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png",
|
||||
"Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png",
|
||||
"Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png",
|
||||
"Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png",
|
||||
"Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png",
|
||||
"Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png",
|
||||
"Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png",
|
||||
"Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png",
|
||||
"Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png",
|
||||
"Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png",
|
||||
"Bottles": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png",
|
||||
"Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png",
|
||||
"Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png",
|
||||
"Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png",
|
||||
"Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png",
|
||||
"Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png",
|
||||
"Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png",
|
||||
"Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
|
||||
"Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
|
||||
"Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
|
||||
"Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
|
||||
"Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
|
||||
"Hyrule Castle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
|
||||
"Agahnims Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
|
||||
"Desert Palace": "https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
|
||||
"Eastern Palace": "https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
|
||||
"Tower of Hera": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
|
||||
"Palace of Darkness": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
|
||||
"Swamp Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
|
||||
"Skull Woods": "https://alttp-wiki.net/images/6/6a/Mothula.png",
|
||||
"Thieves Town": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
|
||||
"Ice Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
|
||||
"Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
|
||||
"Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
|
||||
"Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74",
|
||||
} -%}
|
||||
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/38/ALttP_Bomb_Sprite.png",
|
||||
"Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png",
|
||||
"Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png",
|
||||
} %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
{% set inventory_order = [
|
||||
"Progressive Bow", "Boomerangs", "Hookshot", "Bombs", "Mushroom", "Magic Powder",
|
||||
"Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Progressive Mail",
|
||||
"Lamp", "Hammer", "Flute", "Bug Catching Net", "Book of Mudora", "Progressive Shield",
|
||||
"Bottles", "Cane of Somaria", "Cane of Byrna", "Cape", "Magic Mirror", "Progressive Sword",
|
||||
"Shovel", "Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Triforce Piece",
|
||||
] %}
|
||||
|
||||
{# Most have a duplicated 0th entry for when we have none of that item to still load the correct icon/name. #}
|
||||
{% set progressive_order = {
|
||||
"Progressive Bow": ["Bow", "Bow", "Silver Bow"],
|
||||
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
|
||||
"Progressive Shield": ["Blue Shield", "Blue Shield", "Red Shield", "Mirror Shield"],
|
||||
"Progressive Sword": ["Fighter Sword", "Fighter Sword", "Master Sword", "Tempered Sword", "Golden Sword"],
|
||||
"Progressive Glove": ["Power Glove", "Power Glove", "Titan Mitts"],
|
||||
} %}
|
||||
|
||||
{% set dungeon_keys = {
|
||||
"Hyrule Castle": ("Small Key (Hyrule Castle)", "Big Key (Hyrule Castle)"),
|
||||
"Agahnims Tower": ("Small Key (Agahnims Tower)", "Big Key (Agahnims Tower)"),
|
||||
"Eastern Palace": ("Small Key (Eastern Palace)", "Big Key (Eastern Palace)"),
|
||||
"Desert Palace": ("Small Key (Desert Palace)", "Big Key (Desert Palace)"),
|
||||
"Tower of Hera": ("Small Key (Tower of Hera)", "Big Key (Tower of Hera)"),
|
||||
"Palace of Darkness": ("Small Key (Palace of Darkness)", "Big Key (Palace of Darkness)"),
|
||||
"Swamp Palace": ("Small Key (Swamp Palace)", "Big Key (Swamp Palace)"),
|
||||
"Thieves Town": ("Small Key (Thieves Town)", "Big Key (Thieves Town)"),
|
||||
"Skull Woods": ("Small Key (Skull Woods)", "Big Key (Skull Woods)"),
|
||||
"Ice Palace": ("Small Key (Ice Palace)", "Big Key (Ice Palace)"),
|
||||
"Misery Mire": ("Small Key (Misery Mire)", "Big Key (Misery Mire)"),
|
||||
"Turtle Rock": ("Small Key (Turtle Rock)", "Big Key (Turtle Rock)"),
|
||||
"Ganons Tower": ("Small Key (Ganons Tower)", "Big Key (Ganons Tower)"),
|
||||
} %}
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/lttp-tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttp-tracker.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/tracker__ALinkToThePast.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -76,79 +92,128 @@
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ icons[bow_icon] }}" class="{{ 'acquired' if bow_acquired }}" /></td>
|
||||
<td><img src="{{ icons["Blue Boomerang"] }}" class="{{ 'acquired' if 'Blue Boomerang' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Red Boomerang"] }}" class="{{ 'acquired' if 'Red Boomerang' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Hookshot"] }}" class="{{ 'acquired' if 'Hookshot' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Magic Powder"] }}" class="powder-fix {{ 'acquired' if 'Magic Powder' in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Fire Rod"] }}" class="{{ 'acquired' if "Fire Rod" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Ice Rod"] }}" class="{{ 'acquired' if "Ice Rod" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Bombos"] }}" class="{{ 'acquired' if "Bombos" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Ether"] }}" class="{{ 'acquired' if "Ether" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Quake"] }}" class="{{ 'acquired' if "Quake" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Lamp"] }}" class="{{ 'acquired' if "Lamp" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Hammer"] }}" class="{{ 'acquired' if "Hammer" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Flute"] }}" class="{{ 'acquired' if "Flute" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Bug Catching Net"] }}" class="{{ 'acquired' if "Bug Catching Net" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Book of Mudora"] }}" class="{{ 'acquired' if "Book of Mudora" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Bottle"] }}" class="{{ 'acquired' if "Bottle" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cane of Somaria"] }}" class="{{ 'acquired' if "Cane of Somaria" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cane of Byrna"] }}" class="{{ 'acquired' if "Cane of Byrna" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cape"] }}" class="{{ 'acquired' if "Cape" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Magic Mirror"] }}" class="{{ 'acquired' if "Magic Mirror" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Pegasus Boots"] }}" class="{{ 'acquired' if "Pegasus Boots" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons[glove_icon] }}" class="{{ 'acquired' if glove_acquired }}" /></td>
|
||||
<td><img src="{{ icons["Flippers"] }}" class="{{ 'acquired' if "Flippers" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Moon Pearl"] }}" class="{{ 'acquired' if "Moon Pearl" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Mushroom"] }}" class="{{ 'acquired' if "Mushroom" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons[sword_icon] }}" class="{{ 'acquired' if sword_acquired }}" /></td>
|
||||
<td><img src="{{ icons[shield_icon] }}" class="{{ 'acquired' if shield_acquired }}" /></td>
|
||||
<td><img src="{{ icons[mail_icon] }}" class="acquired" /></td>
|
||||
<td><img src="{{ icons["Shovel"] }}" class="{{ 'acquired' if "Shovel" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Triforce"] }}" class="{{ 'acquired' if "Triforce" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="counter"><img src="{{ icons["Chest"] }}" /></th>
|
||||
{% if key_locations and "Universal" not in key_locations %}
|
||||
<th class="counter"><img src="{{ icons["Small Key"] }}" /></th>
|
||||
<div class="tracker-container">
|
||||
{# Inventory Grid #}
|
||||
<div class="inventory-grid">
|
||||
{% for item in inventory_order %}
|
||||
{% if item in progressive_order %}
|
||||
{% set non_prog_item = progressive_order[item][inventory[item]] %}
|
||||
<div class="item">
|
||||
<img
|
||||
src="{{ icons[non_prog_item] }}"
|
||||
alt="{{ non_prog_item }}"
|
||||
title="{{ non_prog_item }}"
|
||||
{# Progressive Mail gets a special exception, since it starts displaying green mail. #}
|
||||
class="{{ 'missing' if (item not in inventory or inventory[item] == 0) and item != 'Progressive Mail' }}"
|
||||
>
|
||||
</div>
|
||||
{% elif item == "Boomerangs" %}
|
||||
<div class="dual-item">
|
||||
<img
|
||||
src="{{ icons['Blue Boomerang'] }}"
|
||||
alt="Blue Boomerang"
|
||||
title="Blue Boomerang"
|
||||
class="{{ 'missing' if 'Blue Boomerang' not in inventory }}"
|
||||
>
|
||||
<img
|
||||
src="{{ icons['Red Boomerang'] }}"
|
||||
alt="Red Boomerang"
|
||||
title="Red Boomerang"
|
||||
class="{{ 'missing' if 'Red Boomerang' not in inventory }}"
|
||||
>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="item {{ 'hidden' if item == 'Triforce Piece' and inventory['Triforce Piece'] == 0 }}">
|
||||
<img
|
||||
src="{{ icons[item] }}"
|
||||
alt="{{ item }}"
|
||||
title="{{ item }}"
|
||||
class="{{ 'missing' if item not in inventory or inventory[item] == 0 }}"
|
||||
>
|
||||
{% if item == "Bottles" or item == "Triforce Piece" %}
|
||||
<div class="quantity">{{ inventory[item] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if big_key_locations %}
|
||||
<th><img src="{{ icons["Big Key"] }}" /></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for area in sp_areas %}
|
||||
<tr>
|
||||
<td>{{ area }}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
{% if key_locations and "Universal" not in key_locations %}
|
||||
<td class="counter">
|
||||
{{ inventory[small_key_ids[area]] if area in key_locations else '—' }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if big_key_locations %}
|
||||
<td>
|
||||
{{ '✔' if area in big_key_locations and inventory[big_key_ids[area]] else ('—' if area not in big_key_locations else '') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="regions-list">
|
||||
<div class="region region-header">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div><img src="{{ icons['Small Key'] }}" alt="SK" title="Small Keys"></div>
|
||||
<div><img src="{{ icons['Big Key'] }}" alt="BK" title="Big Keys"></div>
|
||||
</div>
|
||||
|
||||
{% for region_name in known_regions %}
|
||||
{% set region_data = regions[region_name] %}
|
||||
{% if region_data["locations"] | length > 0 %}
|
||||
<details class="region-details">
|
||||
<summary>
|
||||
{% if region_name in dungeon_keys %}
|
||||
<div class="region">
|
||||
<span>{{ region_name }}</span>
|
||||
<span>{{ region_data["checked"] }} / {{ region_data["locations"] | length }}</span>
|
||||
<span>{{ inventory[dungeon_keys[region_name][0]] }}</span>
|
||||
<span>
|
||||
{% if region_name == "Agahnims Tower" %}
|
||||
—
|
||||
{% elif inventory[dungeon_keys[region_name][1]] %}
|
||||
✔
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="region">
|
||||
<span>{{ region_name }}</span>
|
||||
<span>{{ region_data["checked"] }} / {{ region_data["locations"] | length }}</span>
|
||||
<span>—</span>
|
||||
<span>—</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</summary>
|
||||
|
||||
<div class="location-rows">
|
||||
{% for location, checked in region_data["locations"] %}
|
||||
<div>{{ location }}</div>
|
||||
<div>{% if checked %}✔{% endif %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const parser = new DOMParser();
|
||||
const interval = 15_000;
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
setInterval(() => updateTracker()
|
||||
.then(() => console.log("Refreshed tracker."))
|
||||
.catch(console.error), interval);
|
||||
});
|
||||
|
||||
async function updateTracker() {
|
||||
const response = await fetch(`${window.location}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch tracker update from ${window.location}. Received response: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const fakeDOM = parser.parseFromString(await response.text(), "text/html");
|
||||
document.querySelector(".inventory-grid").innerHTML = fakeDOM.querySelector(".inventory-grid").innerHTML;
|
||||
|
||||
const regionDetailElements = document.querySelectorAll(".region-details");
|
||||
const fakeDetailElements = fakeDOM.querySelectorAll(".region-details");
|
||||
|
||||
for (let i = 0; i < regionDetailElements.length; ++i) {
|
||||
const isOpen = regionDetailElements[i].open;
|
||||
regionDetailElements[i].innerHTML = fakeDetailElements[i].innerHTML;
|
||||
regionDetailElements[i].open = isOpen;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weighted-options.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weighted-options.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="weighted-settings" class="markdown" data-game="{{ game }}">
|
||||
<div id="user-message"></div>
|
||||
<h1>Weighted Options</h1>
|
||||
<p>Weighted options allow you to choose how likely a particular option is to be used in game generation.
|
||||
The higher an option is weighted, the more likely the option will be chosen. Think of them like
|
||||
entries in a raffle.</p>
|
||||
|
||||
<p>Choose the games and options you would like to play with! You may generate a single-player game from
|
||||
this page, or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
|
||||
page.</p>
|
||||
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
items if you are playing in a MultiWorld.</label><br />
|
||||
<input id="player-name" placeholder="Player Name" data-key="name" maxlength="16" />
|
||||
</p>
|
||||
|
||||
<div id="game-choice">
|
||||
<!-- User chooses games by weight -->
|
||||
</div>
|
||||
|
||||
<!-- To be generated and populated per-game with weight > 0 -->
|
||||
<div id="games-wrapper">
|
||||
|
||||
</div>
|
||||
|
||||
<div id="weighted-settings-button-row">
|
||||
<button id="export-options">Export Options</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
249
WebHostLib/templates/weightedOptions/macros.html
Normal file
249
WebHostLib/templates/weightedOptions/macros.html
Normal file
@@ -0,0 +1,249 @@
|
||||
{% macro Toggle(option_name, option) %}
|
||||
<table>
|
||||
<tbody>
|
||||
{{ RangeRow(option_name, option, "No", "false") }}
|
||||
{{ RangeRow(option_name, option, "Yes", "true") }}
|
||||
{{ RandomRows(option_name, option) }}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro DefaultOnToggle(option_name, option) %}
|
||||
<!-- Toggle handles defaults properly, so we just reuse that -->
|
||||
{{ Toggle(option_name, option) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Choice(option_name, option) %}
|
||||
<table>
|
||||
<tbody>
|
||||
{% for id, name in option.name_lookup.items() %}
|
||||
{% if name != 'random' %}
|
||||
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ RandomRows(option_name, option) }}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Range(option_name, option) %}
|
||||
<div class="hint-text js-required">
|
||||
This is a range option.
|
||||
<br /><br />
|
||||
Accepted values:<br />
|
||||
Normal range: {{ option.range_start }} - {{ option.range_end }}
|
||||
{% if option.special_range_names %}
|
||||
<br /><br />
|
||||
The following values has special meaning, and may fall outside the normal range.
|
||||
<ul>
|
||||
{% for name, value in option.special_range_names.items() %}
|
||||
<li>{{ value }}: {{ name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="add-option-div">
|
||||
<input type="number" class="range-option-value" data-option="{{ option_name }}" />
|
||||
<button class="add-range-option-button" data-option="{{ option_name }}">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="range-rows" data-option="{{ option_name }}">
|
||||
<tbody>
|
||||
{{ RangeRow(option_name, option, option.range_start, option.range_start, True) }}
|
||||
{% if option.range_start < option.default < option.range_end %}
|
||||
{{ RangeRow(option_name, option, option.default, option.default, True) }}
|
||||
{% endif %}
|
||||
{{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}
|
||||
{{ RandomRows(option_name, option) }}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro NamedRange(option_name, option) %}
|
||||
<!-- Range is able to properly handle NamedDRange options -->
|
||||
{{ Range(option_name, option) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro FreeText(option_name, option) %}
|
||||
<div class="hint-text">
|
||||
This option allows custom values only. Please enter your desired values below.
|
||||
<div class="custom-value-wrapper">
|
||||
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button data-option="{{ option_name }}">Add</button>
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<!-- This table to be filled by JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TextChoice(option_name, option) %}
|
||||
<div class="hint-text">
|
||||
Custom values are also allowed for this option. To create one, enter it into the input box below.
|
||||
<div class="custom-value-wrapper">
|
||||
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button data-option="{{ option_name }}">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for id, name in option.name_lookup.items() %}
|
||||
{% if name != 'random' %}
|
||||
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ RandomRows(option_name, option) }}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro PlandoBosses(option_name, option) %}
|
||||
<!-- PlandoBosses is handled by its parent, TextChoice -->
|
||||
{{ TextChoice(option_name, option) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ItemDict(option_name, option, world) %}
|
||||
<div class="dict-container">
|
||||
{% for item_name in (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 grop_name in option.default }} />
|
||||
<label for="{{ option_name }}-{{ group_name }}">{{ group_name }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if world.location_name_groups.keys()|length > 1 %}
|
||||
<div class="divider"> </div>
|
||||
{% endif %}
|
||||
{% for location_name in (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 %}
|
||||
119
WebHostLib/templates/weightedOptions/weightedOptions.html
Normal file
119
WebHostLib/templates/weightedOptions/weightedOptions.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import 'weightedOptions/macros.html' as inputs %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ world_name }} Weighted Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weightedOptions/weightedOptions.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weightedOptions.js") }}"></script>
|
||||
<noscript>
|
||||
<style>
|
||||
.js-required{
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/'+theme+'Header.html' %}
|
||||
<div id="weighted-options" class="markdown" data-game="{{ world_name }}">
|
||||
<noscript>
|
||||
<div class="js-warning-banner">
|
||||
This page has reduced functionality without JavaScript.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div id="user-message"></div>
|
||||
|
||||
<div id="weighted-options-header">
|
||||
<h1>{{ world_name }}</h1>
|
||||
<h1>Weighted Options</h1>
|
||||
</div>
|
||||
|
||||
<form id="weighted-options-form" method="post" enctype="application/x-www-form-urlencoded" action="generate-weighted-yaml">
|
||||
|
||||
<p>Weighted options allow you to choose how likely a particular option's value is to be used in game
|
||||
generation. The higher a value is weighted, the more likely the option will be chosen. Think of them like
|
||||
entries in a raffle.</p>
|
||||
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from
|
||||
this page, or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
|
||||
page.</p>
|
||||
|
||||
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
items if you are playing in a MultiWorld.</label><br />
|
||||
<input id="player-name" placeholder="Player Name" name="name" maxlength="16" />
|
||||
</p>
|
||||
|
||||
<div id="{{ world_name }}-container">
|
||||
{% for group_name, group_options in option_groups.items() %}
|
||||
<details {% if loop.index == 1 %}open{% endif %}>
|
||||
<summary class="h2">{{ group_name }}</summary>
|
||||
{% for option_name, option in group_options.items() %}
|
||||
<div class="option-wrapper">
|
||||
<h4>{{ option.display_name|default(option_name) }}</h4>
|
||||
<div class="option-description">
|
||||
{{ option.__doc__ }}
|
||||
</div>
|
||||
{% if issubclass(option, Options.Toggle) %}
|
||||
{{ inputs.Toggle(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.DefaultOnToggle) %}
|
||||
{{ inputs.DefaultOnToggle(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.PlandoBosses) %}
|
||||
{{ inputs.PlandoBosses(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.TextChoice) %}
|
||||
{{ inputs.TextChoice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Choice) %}
|
||||
{{ inputs.Choice(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.NamedRange) %}
|
||||
{{ inputs.NamedRange(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.Range) %}
|
||||
{{ inputs.Range(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.FreeText) %}
|
||||
{{ inputs.FreeText(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
||||
{{ inputs.ItemDict(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||
{{ inputs.OptionList(option_name, option) }}
|
||||
|
||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
||||
{{ inputs.LocationSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
||||
{{ inputs.ItemSet(option_name, option, world) }}
|
||||
|
||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
||||
{{ inputs.OptionSet(option_name, option) }}
|
||||
|
||||
{% else %}
|
||||
<div class="unsupported-option">
|
||||
This option is not supported. Please edit your .yaml file manually.
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="weighted-options-button-row">
|
||||
<input type="submit" name="intent-export" value="Export Options" />
|
||||
<input type="submit" name="intent-generate" value="Generate Single-Player Game">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,7 @@
|
||||
import datetime
|
||||
import collections
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, NamedTuple, Counter
|
||||
from uuid import UUID
|
||||
|
||||
from flask import render_template
|
||||
@@ -422,11 +422,11 @@ from worlds import network_data_package
|
||||
|
||||
if "Factorio" in network_data_package["games"]:
|
||||
def render_Factorio_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]):
|
||||
inventories: Dict[TeamPlayer, Dict[int, int]] = {
|
||||
(team, player): {
|
||||
inventories: Dict[TeamPlayer, collections.Counter[str]] = {
|
||||
(team, player): collections.Counter({
|
||||
tracker_data.item_id_to_name["Factorio"][item_id]: count
|
||||
for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
|
||||
} for team, players in tracker_data.get_all_slots().items() for player in players
|
||||
}) for team, players in tracker_data.get_all_slots().items() for player in players
|
||||
if tracker_data.get_player_game(team, player) == "Factorio"
|
||||
}
|
||||
|
||||
@@ -456,210 +456,111 @@ if "Factorio" in network_data_package["games"]:
|
||||
_multiworld_trackers["Factorio"] = render_Factorio_multiworld_tracker
|
||||
|
||||
if "A Link to the Past" in network_data_package["games"]:
|
||||
# Mapping from non-progressive item to progressive name and max level.
|
||||
non_progressive_items = {
|
||||
"Fighter Sword": ("Progressive Sword", 1),
|
||||
"Master Sword": ("Progressive Sword", 2),
|
||||
"Tempered Sword": ("Progressive Sword", 3),
|
||||
"Golden Sword": ("Progressive Sword", 4),
|
||||
"Power Glove": ("Progressive Glove", 1),
|
||||
"Titans Mitts": ("Progressive Glove", 2),
|
||||
"Bow": ("Progressive Bow", 1),
|
||||
"Silver Bow": ("Progressive Bow", 2),
|
||||
"Blue Mail": ("Progressive Mail", 1),
|
||||
"Red Mail": ("Progressive Mail", 2),
|
||||
"Blue Shield": ("Progressive Shield", 1),
|
||||
"Red Shield": ("Progressive Shield", 2),
|
||||
"Mirror Shield": ("Progressive Shield", 3),
|
||||
}
|
||||
|
||||
progressive_item_max = {
|
||||
"Progressive Sword": 4,
|
||||
"Progressive Glove": 2,
|
||||
"Progressive Bow": 2,
|
||||
"Progressive Mail": 2,
|
||||
"Progressive Shield": 3,
|
||||
}
|
||||
|
||||
bottle_items = [
|
||||
"Bottle",
|
||||
"Bottle (Bee)",
|
||||
"Bottle (Blue Potion)",
|
||||
"Bottle (Fairy)",
|
||||
"Bottle (Good Bee)",
|
||||
"Bottle (Green Potion)",
|
||||
"Bottle (Red Potion)",
|
||||
]
|
||||
|
||||
known_regions = [
|
||||
"Light World", "Dark World", "Hyrule Castle", "Agahnims Tower", "Eastern Palace", "Desert Palace",
|
||||
"Tower of Hera", "Palace of Darkness", "Swamp Palace", "Thieves Town", "Skull Woods", "Ice Palace",
|
||||
"Misery Mire", "Turtle Rock", "Ganons Tower"
|
||||
]
|
||||
|
||||
class RegionCounts(NamedTuple):
|
||||
total: int
|
||||
checked: int
|
||||
|
||||
def prepare_inventories(team: int, player: int, inventory: Counter[str], tracker_data: TrackerData):
|
||||
for item, (prog_item, level) in non_progressive_items.items():
|
||||
if item in inventory:
|
||||
inventory[prog_item] = min(max(inventory[prog_item], level), progressive_item_max[prog_item])
|
||||
|
||||
for bottle in bottle_items:
|
||||
inventory["Bottles"] = min(inventory["Bottles"] + inventory[bottle], 4)
|
||||
|
||||
if "Progressive Bow (Alt)" in inventory:
|
||||
inventory["Progressive Bow"] += inventory["Progressive Bow (Alt)"]
|
||||
inventory["Progressive Bow"] = min(inventory["Progressive Bow"], progressive_item_max["Progressive Bow"])
|
||||
|
||||
# Highlight 'bombs' if we received any bomb upgrades in bombless start.
|
||||
# In race mode, we'll just assume bombless start for simplicity.
|
||||
if tracker_data.get_slot_data(team, player).get("bombless_start", True):
|
||||
inventory["Bombs"] = sum(count for item, count in inventory.items() if item.startswith("Bomb Upgrade"))
|
||||
else:
|
||||
inventory["Bombs"] = 1
|
||||
|
||||
# Triforce item if we meet goal.
|
||||
if tracker_data.get_room_client_statuses()[team, player] == ClientStatus.CLIENT_GOAL:
|
||||
inventory["Triforce"] = 1
|
||||
|
||||
def render_ALinkToThePast_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]):
|
||||
# Helper objects.
|
||||
alttp_id_lookup = tracker_data.item_name_to_id["A Link to the Past"]
|
||||
inventories: Dict[Tuple[int, int], Counter[str]] = {
|
||||
(team, player): collections.Counter({
|
||||
tracker_data.item_id_to_name["A Link to the Past"][code]: count
|
||||
for code, count in tracker_data.get_player_inventory_counts(team, player).items()
|
||||
})
|
||||
for team, players in tracker_data.get_all_players().items()
|
||||
for player in players if tracker_data.get_slot_info(team, player).game == "A Link to the Past"
|
||||
}
|
||||
|
||||
multi_items = {
|
||||
alttp_id_lookup[name]
|
||||
for name in ("Progressive Sword", "Progressive Bow", "Bottle", "Progressive Glove", "Triforce Piece")
|
||||
}
|
||||
links = {
|
||||
"Bow": "Progressive Bow",
|
||||
"Silver Arrows": "Progressive Bow",
|
||||
"Silver Bow": "Progressive Bow",
|
||||
"Progressive Bow (Alt)": "Progressive Bow",
|
||||
"Bottle (Red Potion)": "Bottle",
|
||||
"Bottle (Green Potion)": "Bottle",
|
||||
"Bottle (Blue Potion)": "Bottle",
|
||||
"Bottle (Fairy)": "Bottle",
|
||||
"Bottle (Bee)": "Bottle",
|
||||
"Bottle (Good Bee)": "Bottle",
|
||||
"Fighter Sword": "Progressive Sword",
|
||||
"Master Sword": "Progressive Sword",
|
||||
"Tempered Sword": "Progressive Sword",
|
||||
"Golden Sword": "Progressive Sword",
|
||||
"Power Glove": "Progressive Glove",
|
||||
"Titans Mitts": "Progressive Glove",
|
||||
}
|
||||
links = {alttp_id_lookup[key]: alttp_id_lookup[value] for key, value in links.items()}
|
||||
levels = {
|
||||
"Fighter Sword": 1,
|
||||
"Master Sword": 2,
|
||||
"Tempered Sword": 3,
|
||||
"Golden Sword": 4,
|
||||
"Power Glove": 1,
|
||||
"Titans Mitts": 2,
|
||||
"Bow": 1,
|
||||
"Silver Bow": 2,
|
||||
"Triforce Piece": 90,
|
||||
}
|
||||
tracking_names = [
|
||||
"Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer", "Hookshot", "Magic Mirror", "Flute",
|
||||
"Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang", "Red Boomerang",
|
||||
"Bug Catching Net", "Cape", "Shovel", "Lamp", "Mushroom", "Magic Powder", "Cane of Somaria",
|
||||
"Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Bottle", "Triforce Piece", "Triforce",
|
||||
]
|
||||
default_locations = {
|
||||
"Light World": {
|
||||
1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175,
|
||||
1572880, 60049, 60178, 1572883, 60052, 60181, 1572885, 60055, 60184, 191256, 60058, 60187, 1572884,
|
||||
1572886, 1572887, 1572906, 60202, 60205, 59824, 166320, 1010170, 60208, 60211, 60214, 60217, 59836,
|
||||
60220, 60223, 59839, 1573184, 60226, 975299, 1573188, 1573189, 188229, 60229, 60232, 1573193,
|
||||
1573194, 60235, 1573187, 59845, 59854, 211407, 60238, 59857, 1573185, 1573186, 1572882, 212328,
|
||||
59881, 59761, 59890, 59770, 193020, 212605
|
||||
},
|
||||
"Dark World": {
|
||||
59776, 59779, 975237, 1572870, 60043, 1572881, 60190, 60193, 60196, 60199, 60840, 1573190, 209095,
|
||||
1573192, 1573191, 60241, 60244, 60247, 60250, 59884, 59887, 60019, 60022, 60028, 60031
|
||||
},
|
||||
"Desert Palace": {1573216, 59842, 59851, 59791, 1573201, 59830},
|
||||
"Eastern Palace": {1573200, 59827, 59893, 59767, 59833, 59773},
|
||||
"Hyrule Castle": {60256, 60259, 60169, 60172, 59758, 59764, 60025, 60253},
|
||||
"Agahnims Tower": {60082, 60085},
|
||||
"Tower of Hera": {1573218, 59878, 59821, 1573202, 59896, 59899},
|
||||
"Swamp Palace": {60064, 60067, 60070, 59782, 59785, 60073, 60076, 60079, 1573204, 60061},
|
||||
"Thieves Town": {59905, 59908, 59911, 59914, 59917, 59920, 59923, 1573206},
|
||||
"Skull Woods": {59809, 59902, 59848, 59794, 1573205, 59800, 59803, 59806},
|
||||
"Ice Palace": {59872, 59875, 59812, 59818, 59860, 59797, 1573207, 59869},
|
||||
"Misery Mire": {60001, 60004, 60007, 60010, 60013, 1573208, 59866, 59998},
|
||||
"Turtle Rock": {59938, 59941, 59944, 1573209, 59947, 59950, 59953, 59956, 59926, 59929, 59932, 59935},
|
||||
"Palace of Darkness": {
|
||||
59968, 59971, 59974, 59977, 59980, 59983, 59986, 1573203, 59989, 59959, 59992, 59962, 59995,
|
||||
59965
|
||||
},
|
||||
"Ganons Tower": {
|
||||
60160, 60163, 60166, 60088, 60091, 60094, 60097, 60100, 60103, 60106, 60109, 60112, 60115, 60118,
|
||||
60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157
|
||||
},
|
||||
"Total": set()
|
||||
}
|
||||
key_only_locations = {
|
||||
"Light World": set(),
|
||||
"Dark World": set(),
|
||||
"Desert Palace": {0x140031, 0x14002b, 0x140061, 0x140028},
|
||||
"Eastern Palace": {0x14005b, 0x140049},
|
||||
"Hyrule Castle": {0x140037, 0x140034, 0x14000d, 0x14003d},
|
||||
"Agahnims Tower": {0x140061, 0x140052},
|
||||
"Tower of Hera": set(),
|
||||
"Swamp Palace": {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a},
|
||||
"Thieves Town": {0x14005e, 0x14004f},
|
||||
"Skull Woods": {0x14002e, 0x14001c},
|
||||
"Ice Palace": {0x140004, 0x140022, 0x140025, 0x140046},
|
||||
"Misery Mire": {0x140055, 0x14004c, 0x140064},
|
||||
"Turtle Rock": {0x140058, 0x140007},
|
||||
"Palace of Darkness": set(),
|
||||
"Ganons Tower": {0x140040, 0x140043, 0x14003a, 0x14001f},
|
||||
"Total": set()
|
||||
}
|
||||
location_to_area = {}
|
||||
for area, locations in default_locations.items():
|
||||
for location in locations:
|
||||
location_to_area[location] = area
|
||||
for area, locations in key_only_locations.items():
|
||||
for location in locations:
|
||||
location_to_area[location] = area
|
||||
# Translate non-progression items to progression items for tracker simplicity.
|
||||
for (team, player), inventory in inventories.items():
|
||||
prepare_inventories(team, player, inventory, tracker_data)
|
||||
|
||||
checks_in_area = {area: len(checks) for area, checks in default_locations.items()}
|
||||
checks_in_area["Total"] = 216
|
||||
ordered_areas = (
|
||||
"Light World", "Dark World", "Hyrule Castle", "Agahnims Tower", "Eastern Palace", "Desert Palace",
|
||||
"Tower of Hera", "Palace of Darkness", "Swamp Palace", "Skull Woods", "Thieves Town", "Ice Palace",
|
||||
"Misery Mire", "Turtle Rock", "Ganons Tower", "Total"
|
||||
)
|
||||
|
||||
player_checks_in_area = {
|
||||
regions: Dict[Tuple[int, int], Dict[str, RegionCounts]] = {
|
||||
(team, player): {
|
||||
area_name: len(tracker_data._multidata["checks_in_area"][player][area_name])
|
||||
if area_name != "Total" else tracker_data._multidata["checks_in_area"][player]["Total"]
|
||||
for area_name in ordered_areas
|
||||
region_name: RegionCounts(
|
||||
total=len(tracker_data._multidata["checks_in_area"][player][region_name]),
|
||||
checked=sum(
|
||||
1 for location in tracker_data._multidata["checks_in_area"][player][region_name]
|
||||
if location in tracker_data.get_player_checked_locations(team, player)
|
||||
),
|
||||
)
|
||||
for region_name in known_regions
|
||||
}
|
||||
for team, players in tracker_data.get_all_slots().items()
|
||||
for player in players
|
||||
if tracker_data.get_slot_info(team, player).type != SlotType.group and
|
||||
tracker_data.get_slot_info(team, player).game == "A Link to the Past"
|
||||
for team, players in tracker_data.get_all_players().items()
|
||||
for player in players if tracker_data.get_slot_info(team, player).game == "A Link to the Past"
|
||||
}
|
||||
|
||||
tracking_ids = []
|
||||
for item in tracking_names:
|
||||
tracking_ids.append(alttp_id_lookup[item])
|
||||
|
||||
# Can't wait to get this into the apworld. Oof.
|
||||
from worlds.alttp import Items
|
||||
|
||||
small_key_ids = {}
|
||||
big_key_ids = {}
|
||||
ids_small_key = {}
|
||||
ids_big_key = {}
|
||||
for item_name, data in Items.item_table.items():
|
||||
if "Key" in item_name:
|
||||
area = item_name.split("(")[1][:-1]
|
||||
if "Small" in item_name:
|
||||
small_key_ids[area] = data[2]
|
||||
ids_small_key[data[2]] = area
|
||||
else:
|
||||
big_key_ids[area] = data[2]
|
||||
ids_big_key[data[2]] = area
|
||||
|
||||
def _get_location_table(checks_table: dict) -> dict:
|
||||
loc_to_area = {}
|
||||
for area, locations in checks_table.items():
|
||||
if area == "Total":
|
||||
continue
|
||||
for location in locations:
|
||||
loc_to_area[location] = area
|
||||
return loc_to_area
|
||||
|
||||
player_location_to_area = {
|
||||
(team, player): _get_location_table(tracker_data._multidata["checks_in_area"][player])
|
||||
for team, players in tracker_data.get_all_slots().items()
|
||||
for player in players
|
||||
if tracker_data.get_slot_info(team, player).type != SlotType.group and
|
||||
tracker_data.get_slot_info(team, player).game == "A Link to the Past"
|
||||
}
|
||||
|
||||
checks_done: Dict[TeamPlayer, Dict[str: int]] = {
|
||||
(team, player): {location_name: 0 for location_name in default_locations}
|
||||
for team, players in tracker_data.get_all_slots().items()
|
||||
for player in players
|
||||
if tracker_data.get_slot_info(team, player).type != SlotType.group and
|
||||
tracker_data.get_slot_info(team, player).game == "A Link to the Past"
|
||||
}
|
||||
|
||||
inventories: Dict[TeamPlayer, Dict[int, int]] = {}
|
||||
player_big_key_locations = {(player): set() for player in tracker_data.get_all_slots()[0]}
|
||||
player_small_key_locations = {player: set() for player in tracker_data.get_all_slots()[0]}
|
||||
group_big_key_locations = set()
|
||||
group_key_locations = set()
|
||||
|
||||
for (team, player), locations in checks_done.items():
|
||||
# Check if game complete.
|
||||
if tracker_data.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL:
|
||||
inventories[team, player][106] = 1 # Triforce
|
||||
|
||||
# Count number of locations checked.
|
||||
for location in tracker_data.get_player_checked_locations(team, player):
|
||||
checks_done[team, player][player_location_to_area[team, player][location]] += 1
|
||||
checks_done[team, player]["Total"] += 1
|
||||
|
||||
# Count keys.
|
||||
for location, (item, receiving, _) in tracker_data.get_player_locations(team, player).items():
|
||||
if item in ids_big_key:
|
||||
player_big_key_locations[receiving].add(ids_big_key[item])
|
||||
elif item in ids_small_key:
|
||||
player_small_key_locations[receiving].add(ids_small_key[item])
|
||||
|
||||
# Iterate over received items and build inventory/key counts.
|
||||
inventories[team, player] = collections.Counter()
|
||||
for network_item in tracker_data.get_player_received_items(team, player):
|
||||
target_item = links.get(network_item.item, network_item.item)
|
||||
if network_item.item in levels: # non-progressive
|
||||
inventories[team, player][target_item] = (max(inventories[team, player][target_item], levels[network_item.item]))
|
||||
else:
|
||||
inventories[team, player][target_item] += 1
|
||||
|
||||
group_key_locations |= player_small_key_locations[player]
|
||||
group_big_key_locations |= player_big_key_locations[player]
|
||||
# Get a totals count.
|
||||
for player, player_regions in regions.items():
|
||||
total = 0
|
||||
checked = 0
|
||||
for region, region_counts in player_regions.items():
|
||||
total += region_counts.total
|
||||
checked += region_counts.checked
|
||||
regions[player]["Total"] = RegionCounts(total, checked)
|
||||
|
||||
return render_template(
|
||||
"multitracker__ALinkToThePast.html",
|
||||
@@ -682,209 +583,39 @@ if "A Link to the Past" in network_data_package["games"]:
|
||||
item_id_to_name=tracker_data.item_id_to_name,
|
||||
location_id_to_name=tracker_data.location_id_to_name,
|
||||
inventories=inventories,
|
||||
tracking_names=tracking_names,
|
||||
tracking_ids=tracking_ids,
|
||||
multi_items=multi_items,
|
||||
checks_done=checks_done,
|
||||
ordered_areas=ordered_areas,
|
||||
checks_in_area=player_checks_in_area,
|
||||
key_locations=group_key_locations,
|
||||
big_key_locations=group_big_key_locations,
|
||||
small_key_ids=small_key_ids,
|
||||
big_key_ids=big_key_ids,
|
||||
regions=regions,
|
||||
known_regions=known_regions,
|
||||
)
|
||||
|
||||
def render_ALinkToThePast_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
|
||||
# Helper objects.
|
||||
alttp_id_lookup = tracker_data.item_name_to_id["A Link to the Past"]
|
||||
inventory = collections.Counter({
|
||||
tracker_data.item_id_to_name["A Link to the Past"][code]: count
|
||||
for code, count in tracker_data.get_player_inventory_counts(team, player).items()
|
||||
})
|
||||
|
||||
links = {
|
||||
"Bow": "Progressive Bow",
|
||||
"Silver Arrows": "Progressive Bow",
|
||||
"Silver Bow": "Progressive Bow",
|
||||
"Progressive Bow (Alt)": "Progressive Bow",
|
||||
"Bottle (Red Potion)": "Bottle",
|
||||
"Bottle (Green Potion)": "Bottle",
|
||||
"Bottle (Blue Potion)": "Bottle",
|
||||
"Bottle (Fairy)": "Bottle",
|
||||
"Bottle (Bee)": "Bottle",
|
||||
"Bottle (Good Bee)": "Bottle",
|
||||
"Fighter Sword": "Progressive Sword",
|
||||
"Master Sword": "Progressive Sword",
|
||||
"Tempered Sword": "Progressive Sword",
|
||||
"Golden Sword": "Progressive Sword",
|
||||
"Power Glove": "Progressive Glove",
|
||||
"Titans Mitts": "Progressive Glove",
|
||||
}
|
||||
links = {alttp_id_lookup[key]: alttp_id_lookup[value] for key, value in links.items()}
|
||||
levels = {
|
||||
"Fighter Sword": 1,
|
||||
"Master Sword": 2,
|
||||
"Tempered Sword": 3,
|
||||
"Golden Sword": 4,
|
||||
"Power Glove": 1,
|
||||
"Titans Mitts": 2,
|
||||
"Bow": 1,
|
||||
"Silver Bow": 2,
|
||||
"Triforce Piece": 90,
|
||||
}
|
||||
tracking_names = [
|
||||
"Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer", "Hookshot", "Magic Mirror", "Flute",
|
||||
"Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang", "Red Boomerang",
|
||||
"Bug Catching Net", "Cape", "Shovel", "Lamp", "Mushroom", "Magic Powder", "Cane of Somaria",
|
||||
"Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Bottle", "Triforce Piece", "Triforce",
|
||||
]
|
||||
default_locations = {
|
||||
"Light World": {
|
||||
1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175,
|
||||
1572880, 60049, 60178, 1572883, 60052, 60181, 1572885, 60055, 60184, 191256, 60058, 60187, 1572884,
|
||||
1572886, 1572887, 1572906, 60202, 60205, 59824, 166320, 1010170, 60208, 60211, 60214, 60217, 59836,
|
||||
60220, 60223, 59839, 1573184, 60226, 975299, 1573188, 1573189, 188229, 60229, 60232, 1573193,
|
||||
1573194, 60235, 1573187, 59845, 59854, 211407, 60238, 59857, 1573185, 1573186, 1572882, 212328,
|
||||
59881, 59761, 59890, 59770, 193020, 212605
|
||||
},
|
||||
"Dark World": {
|
||||
59776, 59779, 975237, 1572870, 60043, 1572881, 60190, 60193, 60196, 60199, 60840, 1573190, 209095,
|
||||
1573192, 1573191, 60241, 60244, 60247, 60250, 59884, 59887, 60019, 60022, 60028, 60031
|
||||
},
|
||||
"Desert Palace": {1573216, 59842, 59851, 59791, 1573201, 59830},
|
||||
"Eastern Palace": {1573200, 59827, 59893, 59767, 59833, 59773},
|
||||
"Hyrule Castle": {60256, 60259, 60169, 60172, 59758, 59764, 60025, 60253},
|
||||
"Agahnims Tower": {60082, 60085},
|
||||
"Tower of Hera": {1573218, 59878, 59821, 1573202, 59896, 59899},
|
||||
"Swamp Palace": {60064, 60067, 60070, 59782, 59785, 60073, 60076, 60079, 1573204, 60061},
|
||||
"Thieves Town": {59905, 59908, 59911, 59914, 59917, 59920, 59923, 1573206},
|
||||
"Skull Woods": {59809, 59902, 59848, 59794, 1573205, 59800, 59803, 59806},
|
||||
"Ice Palace": {59872, 59875, 59812, 59818, 59860, 59797, 1573207, 59869},
|
||||
"Misery Mire": {60001, 60004, 60007, 60010, 60013, 1573208, 59866, 59998},
|
||||
"Turtle Rock": {59938, 59941, 59944, 1573209, 59947, 59950, 59953, 59956, 59926, 59929, 59932, 59935},
|
||||
"Palace of Darkness": {
|
||||
59968, 59971, 59974, 59977, 59980, 59983, 59986, 1573203, 59989, 59959, 59992, 59962, 59995,
|
||||
59965
|
||||
},
|
||||
"Ganons Tower": {
|
||||
60160, 60163, 60166, 60088, 60091, 60094, 60097, 60100, 60103, 60106, 60109, 60112, 60115, 60118,
|
||||
60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157
|
||||
},
|
||||
"Total": set()
|
||||
}
|
||||
key_only_locations = {
|
||||
"Light World": set(),
|
||||
"Dark World": set(),
|
||||
"Desert Palace": {0x140031, 0x14002b, 0x140061, 0x140028},
|
||||
"Eastern Palace": {0x14005b, 0x140049},
|
||||
"Hyrule Castle": {0x140037, 0x140034, 0x14000d, 0x14003d},
|
||||
"Agahnims Tower": {0x140061, 0x140052},
|
||||
"Tower of Hera": set(),
|
||||
"Swamp Palace": {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a},
|
||||
"Thieves Town": {0x14005e, 0x14004f},
|
||||
"Skull Woods": {0x14002e, 0x14001c},
|
||||
"Ice Palace": {0x140004, 0x140022, 0x140025, 0x140046},
|
||||
"Misery Mire": {0x140055, 0x14004c, 0x140064},
|
||||
"Turtle Rock": {0x140058, 0x140007},
|
||||
"Palace of Darkness": set(),
|
||||
"Ganons Tower": {0x140040, 0x140043, 0x14003a, 0x14001f},
|
||||
"Total": set()
|
||||
}
|
||||
location_to_area = {}
|
||||
for area, locations in default_locations.items():
|
||||
for checked_location in locations:
|
||||
location_to_area[checked_location] = area
|
||||
for area, locations in key_only_locations.items():
|
||||
for checked_location in locations:
|
||||
location_to_area[checked_location] = area
|
||||
# Translate non-progression items to progression items for tracker simplicity.
|
||||
prepare_inventories(team, player, inventory, tracker_data)
|
||||
|
||||
checks_in_area = {area: len(checks) for area, checks in default_locations.items()}
|
||||
checks_in_area["Total"] = 216
|
||||
ordered_areas = (
|
||||
"Light World", "Dark World", "Hyrule Castle", "Agahnims Tower", "Eastern Palace", "Desert Palace",
|
||||
"Tower of Hera", "Palace of Darkness", "Swamp Palace", "Skull Woods", "Thieves Town", "Ice Palace",
|
||||
"Misery Mire", "Turtle Rock", "Ganons Tower", "Total"
|
||||
)
|
||||
|
||||
tracking_ids = []
|
||||
for item in tracking_names:
|
||||
tracking_ids.append(alttp_id_lookup[item])
|
||||
|
||||
# Can't wait to get this into the apworld. Oof.
|
||||
from worlds.alttp import Items
|
||||
|
||||
small_key_ids = {}
|
||||
big_key_ids = {}
|
||||
ids_small_key = {}
|
||||
ids_big_key = {}
|
||||
for item_name, data in Items.item_table.items():
|
||||
if "Key" in item_name:
|
||||
area = item_name.split("(")[1][:-1]
|
||||
if "Small" in item_name:
|
||||
small_key_ids[area] = data[2]
|
||||
ids_small_key[data[2]] = area
|
||||
else:
|
||||
big_key_ids[area] = data[2]
|
||||
ids_big_key[data[2]] = area
|
||||
|
||||
inventory = collections.Counter()
|
||||
checks_done = {loc_name: 0 for loc_name in default_locations}
|
||||
player_big_key_locations = set()
|
||||
player_small_key_locations = set()
|
||||
|
||||
player_locations = tracker_data.get_player_locations(team, player)
|
||||
for checked_location in tracker_data.get_player_checked_locations(team, player):
|
||||
if checked_location in player_locations:
|
||||
area_name = location_to_area.get(checked_location, None)
|
||||
if area_name:
|
||||
checks_done[area_name] += 1
|
||||
|
||||
checks_done["Total"] += 1
|
||||
|
||||
for received_item in tracker_data.get_player_received_items(team, player):
|
||||
target_item = links.get(received_item.item, received_item.item)
|
||||
if received_item.item in levels: # non-progressive
|
||||
inventory[target_item] = max(inventory[target_item], levels[received_item.item])
|
||||
else:
|
||||
inventory[target_item] += 1
|
||||
|
||||
for location, (item_id, _, _) in player_locations.items():
|
||||
if item_id in ids_big_key:
|
||||
player_big_key_locations.add(ids_big_key[item_id])
|
||||
elif item_id in ids_small_key:
|
||||
player_small_key_locations.add(ids_small_key[item_id])
|
||||
|
||||
# Note the presence of the triforce item
|
||||
if tracker_data.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL:
|
||||
inventory[106] = 1 # Triforce
|
||||
|
||||
# Progressive items need special handling for icons and class
|
||||
progressive_items = {
|
||||
"Progressive Sword": 94,
|
||||
"Progressive Glove": 97,
|
||||
"Progressive Bow": 100,
|
||||
"Progressive Mail": 96,
|
||||
"Progressive Shield": 95,
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Sword": [None, "Fighter Sword", "Master Sword", "Tempered Sword", "Golden Sword"],
|
||||
"Progressive Glove": [None, "Power Glove", "Titan Mitts"],
|
||||
"Progressive Bow": [None, "Bow", "Silver Bow"],
|
||||
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
|
||||
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
|
||||
regions = {
|
||||
region_name: {
|
||||
"checked": sum(
|
||||
1 for location in tracker_data._multidata["checks_in_area"][player][region_name]
|
||||
if location in tracker_data.get_player_checked_locations(team, player)
|
||||
),
|
||||
"locations": [
|
||||
(
|
||||
tracker_data.location_id_to_name["A Link to the Past"][location],
|
||||
location in tracker_data.get_player_checked_locations(team, player)
|
||||
)
|
||||
for location in tracker_data._multidata["checks_in_area"][player][region_name]
|
||||
],
|
||||
}
|
||||
for region_name in known_regions
|
||||
}
|
||||
|
||||
# Determine which icon to use
|
||||
display_data = {}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
acquired = True
|
||||
if not display_name:
|
||||
acquired = False
|
||||
display_name = progressive_names[item_name][level + 1]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower()
|
||||
display_data[base_name + "_acquired"] = acquired
|
||||
display_data[base_name + "_icon"] = display_name
|
||||
|
||||
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
|
||||
sp_areas = ordered_areas[2:15]
|
||||
# Sort locations in regions by name
|
||||
for region in regions:
|
||||
regions[region]["locations"].sort()
|
||||
|
||||
return render_template(
|
||||
template_name_or_list="tracker__ALinkToThePast.html",
|
||||
@@ -893,15 +624,8 @@ if "A Link to the Past" in network_data_package["games"]:
|
||||
player=player,
|
||||
inventory=inventory,
|
||||
player_name=tracker_data.get_player_name(team, player),
|
||||
checks_done=checks_done,
|
||||
checks_in_area=checks_in_area,
|
||||
acquired_items={tracker_data.item_id_to_name["A Link to the Past"][id] for id in inventory},
|
||||
sp_areas=sp_areas,
|
||||
small_key_ids=small_key_ids,
|
||||
key_locations=player_small_key_locations,
|
||||
big_key_ids=big_key_ids,
|
||||
big_key_locations=player_big_key_locations,
|
||||
**display_data,
|
||||
regions=regions,
|
||||
known_regions=known_regions,
|
||||
)
|
||||
|
||||
_multiworld_trackers["A Link to the Past"] = render_ALinkToThePast_multiworld_tracker
|
||||
|
||||
@@ -63,12 +63,13 @@ def process_multidata(compressed_multidata, files={}):
|
||||
game_data = games_package_schema.validate(game_data)
|
||||
game_data = {key: value for key, value in sorted(game_data.items())}
|
||||
game_data["checksum"] = data_package_checksum(game_data)
|
||||
game_data_package = GameDataPackage(checksum=game_data["checksum"],
|
||||
data=pickle.dumps(game_data))
|
||||
if original_checksum != game_data["checksum"]:
|
||||
raise Exception(f"Original checksum {original_checksum} != "
|
||||
f"calculated checksum {game_data['checksum']} "
|
||||
f"for game {game}.")
|
||||
|
||||
game_data_package = GameDataPackage(checksum=game_data["checksum"],
|
||||
data=pickle.dumps(game_data))
|
||||
decompressed_multidata["datapackage"][game] = {
|
||||
"version": game_data.get("version", 0),
|
||||
"checksum": game_data["checksum"],
|
||||
@@ -192,6 +193,8 @@ def uploads():
|
||||
res = upload_zip_to_db(zfile)
|
||||
except VersionException:
|
||||
flash(f"Could not load multidata. Wrong Version detected.")
|
||||
except Exception as e:
|
||||
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
|
||||
else:
|
||||
if res is str:
|
||||
return res
|
||||
|
||||
@@ -27,14 +27,9 @@ local mmbn3Socket = nil
|
||||
local frame = 0
|
||||
|
||||
-- States
|
||||
local ITEMSTATE_NONINITIALIZED = "Game Not Yet Started" -- Game has not yet started
|
||||
local ITEMSTATE_NONITEM = "Non-Itemable State" -- Do not send item now. RAM is not capable of holding
|
||||
local ITEMSTATE_IDLE = "Item State Ready" -- Ready for the next item if there are any
|
||||
local ITEMSTATE_SENT = "Item Sent Not Claimed" -- The ItemBit is set, but the dialog has not been closed yet
|
||||
local itemState = ITEMSTATE_NONINITIALIZED
|
||||
|
||||
local itemQueued = nil
|
||||
local itemQueueCounter = 120
|
||||
local itemState = ITEMSTATE_NONITEM
|
||||
|
||||
local debugEnabled = false
|
||||
local game_complete = false
|
||||
@@ -104,21 +99,24 @@ end
|
||||
local IsInBattle = function()
|
||||
return memory.read_u8(0x020097F8) == 0x08
|
||||
end
|
||||
local IsItemQueued = function()
|
||||
return memory.read_u8(0x2000224) == 0x01
|
||||
end
|
||||
|
||||
-- This function actually determines when you're on ANY full-screen menu (navi cust, link battle, etc.) but we
|
||||
-- don't want to check any locations there either so it's fine.
|
||||
local IsOnTitle = function()
|
||||
return bit.band(memory.read_u8(0x020097F8),0x04) == 0
|
||||
end
|
||||
|
||||
local IsItemable = function()
|
||||
return not IsInMenu() and not IsInTransition() and not IsInDialog() and not IsInBattle() and not IsOnTitle() and not IsItemQueued()
|
||||
return not IsInMenu() and not IsInTransition() and not IsInDialog() and not IsInBattle() and not IsOnTitle()
|
||||
end
|
||||
|
||||
local is_game_complete = function()
|
||||
if IsOnTitle() or itemState == ITEMSTATE_NONINITIALIZED then return game_complete end
|
||||
-- If the Cannary Byte is 0xFF, then the save RAM is untrustworthy
|
||||
if memory.read_u8(canary_byte) == 0xFF then
|
||||
return game_complete
|
||||
end
|
||||
|
||||
-- If on the title screen don't read RAM, RAM can't be trusted yet
|
||||
if IsOnTitle() then return game_complete end
|
||||
|
||||
-- If the game is already marked complete, do not read memory
|
||||
if game_complete then return true end
|
||||
@@ -177,14 +175,6 @@ local Check_Progressive_Undernet_ID = function()
|
||||
end
|
||||
return 9
|
||||
end
|
||||
local GenerateTextBytes = function(message)
|
||||
bytes = {}
|
||||
for i = 1, #message do
|
||||
local c = message:sub(i,i)
|
||||
table.insert(bytes, charDict[c])
|
||||
end
|
||||
return bytes
|
||||
end
|
||||
|
||||
-- Item Message Generation functions
|
||||
local Next_Progressive_Undernet_ID = function(index)
|
||||
@@ -196,150 +186,6 @@ local Next_Progressive_Undernet_ID = function(index)
|
||||
item_index=ordered_IDs[index]
|
||||
return item_index
|
||||
end
|
||||
local Extra_Progressive_Undernet = function()
|
||||
fragBytes = int32ToByteList_le(20)
|
||||
bytes = {
|
||||
0xF6, 0x50, fragBytes[1], fragBytes[2], fragBytes[3], fragBytes[4], 0xFF, 0xFF, 0xFF
|
||||
}
|
||||
bytes = TableConcat(bytes, GenerateTextBytes("The extra data\ndecompiles into:\n\"20 BugFrags\"!!"))
|
||||
return bytes
|
||||
end
|
||||
|
||||
local GenerateChipGet = function(chip, code, amt)
|
||||
chipBytes = int16ToByteList_le(chip)
|
||||
bytes = {
|
||||
0xF6, 0x10, chipBytes[1], chipBytes[2], code, amt,
|
||||
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict[' '], charDict['c'], charDict['h'], charDict['i'], charDict['p'], charDict[' '], charDict['f'], charDict['o'], charDict['r'], charDict['\n'],
|
||||
|
||||
}
|
||||
if chip < 256 then
|
||||
bytes = TableConcat(bytes, {
|
||||
charDict['\"'], 0xF9,0x00,chipBytes[1],0x01,0x00,0xF9,0x00,code,0x03, charDict['\"'],charDict['!'],charDict['!']
|
||||
})
|
||||
else
|
||||
bytes = TableConcat(bytes, {
|
||||
charDict['\"'], 0xF9,0x00,chipBytes[1],0x02,0x00,0xF9,0x00,code,0x03, charDict['\"'],charDict['!'],charDict['!']
|
||||
})
|
||||
end
|
||||
return bytes
|
||||
end
|
||||
local GenerateKeyItemGet = function(item, amt)
|
||||
bytes = {
|
||||
0xF6, 0x00, item, amt,
|
||||
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'],
|
||||
charDict['\"'], 0xF9, 0x00, item, 0x00, charDict['\"'],charDict['!'],charDict['!']
|
||||
}
|
||||
return bytes
|
||||
end
|
||||
local GenerateSubChipGet = function(subchip, amt)
|
||||
-- SubChips have an extra bit of trouble. If you have too many, they're supposed to skip to another text bank that doesn't give you the item
|
||||
-- Instead, I'm going to just let it get eaten
|
||||
bytes = {
|
||||
0xF6, 0x20, subchip, amt, 0xFF, 0xFF, 0xFF,
|
||||
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'],
|
||||
charDict['S'], charDict['u'], charDict['b'], charDict['C'], charDict['h'], charDict['i'], charDict['p'], charDict[' '], charDict['f'], charDict['o'], charDict['r'], charDict['\n'],
|
||||
charDict['\"'], 0xF9, 0x00, subchip, 0x00, charDict['\"'],charDict['!'],charDict['!']
|
||||
}
|
||||
return bytes
|
||||
end
|
||||
local GenerateZennyGet = function(amt)
|
||||
zennyBytes = int32ToByteList_le(amt)
|
||||
bytes = {
|
||||
0xF6, 0x30, zennyBytes[1], zennyBytes[2], zennyBytes[3], zennyBytes[4], 0xFF, 0xFF, 0xFF,
|
||||
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'], charDict['\"']
|
||||
}
|
||||
-- The text needs to be added one char at a time, so we need to convert the number to a string then iterate through it
|
||||
zennyStr = tostring(amt)
|
||||
for i = 1, #zennyStr do
|
||||
local c = zennyStr:sub(i,i)
|
||||
table.insert(bytes, charDict[c])
|
||||
end
|
||||
bytes = TableConcat(bytes, {
|
||||
charDict[' '], charDict['Z'], charDict['e'], charDict['n'], charDict['n'], charDict['y'], charDict['s'], charDict['\"'],charDict['!'],charDict['!']
|
||||
})
|
||||
return bytes
|
||||
end
|
||||
local GenerateProgramGet = function(program, color, amt)
|
||||
bytes = {
|
||||
0xF6, 0x40, (program * 4), amt, color,
|
||||
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict[' '], charDict['N'], charDict['a'], charDict['v'], charDict['i'], charDict['\n'],
|
||||
charDict['C'], charDict['u'], charDict['s'], charDict['t'], charDict['o'], charDict['m'], charDict['i'], charDict['z'], charDict['e'], charDict['r'], charDict[' '], charDict['P'], charDict['r'], charDict['o'], charDict['g'], charDict['r'], charDict['a'], charDict['m'], charDict[':'], charDict['\n'],
|
||||
charDict['\"'], 0xF9, 0x00, program, 0x05, charDict['\"'],charDict['!'],charDict['!']
|
||||
}
|
||||
|
||||
return bytes
|
||||
end
|
||||
local GenerateBugfragGet = function(amt)
|
||||
fragBytes = int32ToByteList_le(amt)
|
||||
bytes = {
|
||||
0xF6, 0x50, fragBytes[1], fragBytes[2], fragBytes[3], fragBytes[4], 0xFF, 0xFF, 0xFF,
|
||||
charDict['G'], charDict['o'], charDict['t'], charDict[':'], charDict['\n'], charDict['\"']
|
||||
}
|
||||
-- The text needs to be added one char at a time, so we need to convert the number to a string then iterate through it
|
||||
bugFragStr = tostring(amt)
|
||||
for i = 1, #bugFragStr do
|
||||
local c = bugFragStr:sub(i,i)
|
||||
table.insert(bytes, charDict[c])
|
||||
end
|
||||
bytes = TableConcat(bytes, {
|
||||
charDict[' '], charDict['B'], charDict['u'], charDict['g'], charDict['F'], charDict['r'], charDict['a'], charDict['g'], charDict['s'], charDict['\"'],charDict['!'],charDict['!']
|
||||
})
|
||||
return bytes
|
||||
end
|
||||
local GenerateGetMessageFromItem = function(item)
|
||||
--Special case for progressive undernet
|
||||
if item["type"] == "undernet" then
|
||||
undernet_id = Check_Progressive_Undernet_ID()
|
||||
if undernet_id > 8 then
|
||||
return Extra_Progressive_Undernet()
|
||||
end
|
||||
return GenerateKeyItemGet(Next_Progressive_Undernet_ID(undernet_id),1)
|
||||
elseif item["type"] == "chip" then
|
||||
return GenerateChipGet(item["itemID"], item["subItemID"], item["count"])
|
||||
elseif item["type"] == "key" then
|
||||
return GenerateKeyItemGet(item["itemID"], item["count"])
|
||||
elseif item["type"] == "subchip" then
|
||||
return GenerateSubChipGet(item["itemID"], item["count"])
|
||||
elseif item["type"] == "zenny" then
|
||||
return GenerateZennyGet(item["count"])
|
||||
elseif item["type"] == "program" then
|
||||
return GenerateProgramGet(item["itemID"], item["subItemID"], item["count"])
|
||||
elseif item["type"] == "bugfrag" then
|
||||
return GenerateBugfragGet(item["count"])
|
||||
end
|
||||
|
||||
return GenerateTextBytes("Empty Message")
|
||||
end
|
||||
|
||||
local GetMessage = function(item)
|
||||
startBytes = {0x02, 0x00}
|
||||
playerLockBytes = {0xF8,0x00, 0xF8, 0x10}
|
||||
msgOpenBytes = {0xF1, 0x02}
|
||||
textBytes = GenerateTextBytes("Receiving\ndata from\n"..item["sender"]..".")
|
||||
dotdotWaitBytes = {0xEA,0x00,0x0A,0x00,0x4D,0xEA,0x00,0x0A,0x00,0x4D}
|
||||
continueBytes = {0xEB, 0xE9}
|
||||
-- continueBytes = {0xE9}
|
||||
playReceiveAnimationBytes = {0xF8,0x04,0x18}
|
||||
chipGiveBytes = GenerateGetMessageFromItem(item)
|
||||
playerFinishBytes = {0xF8, 0x0C}
|
||||
playerUnlockBytes={0xEB, 0xF8, 0x08}
|
||||
-- playerUnlockBytes={0xF8, 0x08}
|
||||
endMessageBytes = {0xF8, 0x10, 0xE7}
|
||||
|
||||
bytes = {}
|
||||
bytes = TableConcat(bytes,startBytes)
|
||||
bytes = TableConcat(bytes,playerLockBytes)
|
||||
bytes = TableConcat(bytes,msgOpenBytes)
|
||||
bytes = TableConcat(bytes,textBytes)
|
||||
bytes = TableConcat(bytes,dotdotWaitBytes)
|
||||
bytes = TableConcat(bytes,continueBytes)
|
||||
bytes = TableConcat(bytes,playReceiveAnimationBytes)
|
||||
bytes = TableConcat(bytes,chipGiveBytes)
|
||||
bytes = TableConcat(bytes,playerFinishBytes)
|
||||
bytes = TableConcat(bytes,playerUnlockBytes)
|
||||
bytes = TableConcat(bytes,endMessageBytes)
|
||||
return bytes
|
||||
end
|
||||
|
||||
local getChipCodeIndex = function(chip_id, chip_code)
|
||||
chipCodeArrayStartAddress = 0x8011510 + (0x20 * chip_id)
|
||||
@@ -353,6 +199,10 @@ local getChipCodeIndex = function(chip_id, chip_code)
|
||||
end
|
||||
|
||||
local getProgramColorIndex = function(program_id, program_color)
|
||||
-- For whatever reason, OilBody (ID 24) does not follow the rules and should be color index 3
|
||||
if program_id == 24 then
|
||||
return 3
|
||||
end
|
||||
-- The general case, most programs use white pink or yellow. This is the values the enums already have
|
||||
if program_id >= 20 and program_id <= 47 then
|
||||
return program_color-1
|
||||
@@ -401,11 +251,11 @@ local changeZenny = function(val)
|
||||
return 0
|
||||
end
|
||||
if memory.read_u32_le(0x20018F4) <= math.abs(tonumber(val)) and tonumber(val) < 0 then
|
||||
memory.write_u32_le(0x20018f4, 0)
|
||||
memory.write_u32_le(0x20018F4, 0)
|
||||
val = 0
|
||||
return "empty"
|
||||
end
|
||||
memory.write_u32_le(0x20018f4, memory.read_u32_le(0x20018F4) + tonumber(val))
|
||||
memory.write_u32_le(0x20018F4, memory.read_u32_le(0x20018F4) + tonumber(val))
|
||||
if memory.read_u32_le(0x20018F4) > 999999 then
|
||||
memory.write_u32_le(0x20018F4, 999999)
|
||||
end
|
||||
@@ -417,30 +267,17 @@ local changeFrags = function(val)
|
||||
return 0
|
||||
end
|
||||
if memory.read_u16_le(0x20018F8) <= math.abs(tonumber(val)) and tonumber(val) < 0 then
|
||||
memory.write_u16_le(0x20018f8, 0)
|
||||
memory.write_u16_le(0x20018F8, 0)
|
||||
val = 0
|
||||
return "empty"
|
||||
end
|
||||
memory.write_u16_le(0x20018f8, memory.read_u16_le(0x20018F8) + tonumber(val))
|
||||
memory.write_u16_le(0x20018F8, memory.read_u16_le(0x20018F8) + tonumber(val))
|
||||
if memory.read_u16_le(0x20018F8) > 9999 then
|
||||
memory.write_u16_le(0x20018F8, 9999)
|
||||
end
|
||||
return val
|
||||
end
|
||||
|
||||
-- Fix Health Pools
|
||||
local fix_hp = function()
|
||||
-- Current Health fix
|
||||
if IsInBattle() and not (memory.read_u16_le(0x20018A0) == memory.read_u16_le(0x2037294)) then
|
||||
memory.write_u16_le(0x20018A0, memory.read_u16_le(0x2037294))
|
||||
end
|
||||
|
||||
-- Max Health Fix
|
||||
if IsInBattle() and not (memory.read_u16_le(0x20018A2) == memory.read_u16_le(0x2037296)) then
|
||||
memory.write_u16_le(0x20018A2, memory.read_u16_le(0x2037296))
|
||||
end
|
||||
end
|
||||
|
||||
local changeRegMemory = function(amt)
|
||||
regMemoryAddress = 0x02001897
|
||||
currentRegMem = memory.read_u8(regMemoryAddress)
|
||||
@@ -448,34 +285,18 @@ local changeRegMemory = function(amt)
|
||||
end
|
||||
|
||||
local changeMaxHealth = function(val)
|
||||
fix_hp()
|
||||
if val == nil then
|
||||
fix_hp()
|
||||
if val == nil then
|
||||
return 0
|
||||
end
|
||||
if math.abs(tonumber(val)) >= memory.read_u16_le(0x20018A2) and tonumber(val) < 0 then
|
||||
memory.write_u16_le(0x20018A2, 0)
|
||||
if IsInBattle() then
|
||||
memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
|
||||
if memory.read_u16_le(0x2037296) >= memory.read_u16_le(0x20018A2) then
|
||||
memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
|
||||
end
|
||||
end
|
||||
fix_hp()
|
||||
return "lethal"
|
||||
end
|
||||
|
||||
memory.write_u16_le(0x20018A2, memory.read_u16_le(0x20018A2) + tonumber(val))
|
||||
if memory.read_u16_le(0x20018A2) > 9999 then
|
||||
memory.write_u16_le(0x20018A2, 9999)
|
||||
end
|
||||
if IsInBattle() then
|
||||
memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
|
||||
end
|
||||
fix_hp()
|
||||
return val
|
||||
end
|
||||
|
||||
local SendItem = function(item)
|
||||
local SendItemToGame = function(item)
|
||||
if item["type"] == "undernet" then
|
||||
undernet_id = Check_Progressive_Undernet_ID()
|
||||
if undernet_id > 8 then
|
||||
@@ -553,13 +374,6 @@ local OpenShortcuts = function()
|
||||
end
|
||||
end
|
||||
|
||||
local RestoreItemRam = function()
|
||||
if backup_bytes ~= nil then
|
||||
memory.write_bytes_as_array(0x203fe10, backup_bytes)
|
||||
end
|
||||
backup_bytes = nil
|
||||
end
|
||||
|
||||
local process_block = function(block)
|
||||
-- Sometimes the block is nothing, if this is the case then quietly stop processing
|
||||
if block == nil then
|
||||
@@ -574,14 +388,7 @@ local process_block = function(block)
|
||||
end
|
||||
|
||||
local itemStateMachineProcess = function()
|
||||
if itemState == ITEMSTATE_NONINITIALIZED then
|
||||
itemQueueCounter = 120
|
||||
-- Only exit this state the first time a dialog window pops up. This way we know for sure that we're ready to receive
|
||||
if not IsInMenu() and (IsInDialog() or IsInTransition()) then
|
||||
itemState = ITEMSTATE_NONITEM
|
||||
end
|
||||
elseif itemState == ITEMSTATE_NONITEM then
|
||||
itemQueueCounter = 120
|
||||
if itemState == ITEMSTATE_NONITEM then
|
||||
-- Always attempt to restore the previously stored memory in this state
|
||||
-- Exit this state whenever the game is in an itemable status
|
||||
if IsItemable() then
|
||||
@@ -592,26 +399,11 @@ local itemStateMachineProcess = function()
|
||||
if not IsItemable() then
|
||||
itemState = ITEMSTATE_NONITEM
|
||||
end
|
||||
if itemQueueCounter == 0 then
|
||||
if #itemsReceived > loadItemIndexFromRAM() and not IsItemQueued() then
|
||||
itemQueued = itemsReceived[loadItemIndexFromRAM()+1]
|
||||
SendItem(itemQueued)
|
||||
itemState = ITEMSTATE_SENT
|
||||
end
|
||||
else
|
||||
itemQueueCounter = itemQueueCounter - 1
|
||||
end
|
||||
elseif itemState == ITEMSTATE_SENT then
|
||||
-- Once the item is sent, wait for the dialog to close. Then clear the item bit and be ready for the next item.
|
||||
if IsInTransition() or IsInMenu() or IsOnTitle() then
|
||||
itemState = ITEMSTATE_NONITEM
|
||||
itemQueued = nil
|
||||
RestoreItemRam()
|
||||
elseif not IsInDialog() then
|
||||
itemState = ITEMSTATE_IDLE
|
||||
if #itemsReceived > loadItemIndexFromRAM() then
|
||||
itemQueued = itemsReceived[loadItemIndexFromRAM()+1]
|
||||
SendItemToGame(itemQueued)
|
||||
saveItemIndexToRAM(itemQueued["itemIndex"])
|
||||
itemQueued = nil
|
||||
RestoreItemRam()
|
||||
itemState = ITEMSTATE_NONITEM
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -702,18 +494,8 @@ function main()
|
||||
-- Handle the debug data display
|
||||
gui.cleartext()
|
||||
if debugEnabled then
|
||||
-- gui.text(0,0,"Item Queued: "..tostring(IsItemQueued()))
|
||||
-- gui.text(0,16,"In Battle: "..tostring(IsInBattle()))
|
||||
-- gui.text(0,32,"In Dialog: "..tostring(IsInDialog()))
|
||||
-- gui.text(0,48,"In Menu: "..tostring(IsInMenu()))
|
||||
gui.text(0,48,"Item Wait Time: "..tostring(itemQueueCounter))
|
||||
gui.text(0,64,itemState)
|
||||
if itemQueued == nil then
|
||||
gui.text(0,80,"No item queued")
|
||||
else
|
||||
gui.text(0,80,itemQueued["type"].." "..itemQueued["itemID"])
|
||||
end
|
||||
gui.text(0,96,"Item Index: "..loadItemIndexFromRAM())
|
||||
gui.text(0,0,itemState)
|
||||
gui.text(0,16,"Item Index: "..loadItemIndexFromRAM())
|
||||
end
|
||||
|
||||
emu.frameadvance()
|
||||
|
||||
@@ -45,7 +45,10 @@ requires:
|
||||
{% endmacro %}
|
||||
|
||||
{{ game }}:
|
||||
{%- for option_key, option in options.items() %}
|
||||
{%- for group_name, group_options in option_groups.items() %}
|
||||
# {{ group_name }}
|
||||
|
||||
{%- for option_key, option in group_options.items() %}
|
||||
{{ option_key }}:
|
||||
{%- if option.__doc__ %}
|
||||
# {{ option.__doc__
|
||||
@@ -83,3 +86,4 @@ requires:
|
||||
{%- endif -%}
|
||||
{{ "\n" }}
|
||||
{%- endfor %}
|
||||
{%- endfor %}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
# A Link to the Past
|
||||
/worlds/alttp/ @Berserker66
|
||||
|
||||
# Aquaria
|
||||
/worlds/aquaria/ @tioui
|
||||
|
||||
# ArchipIDLE
|
||||
/worlds/archipidle/ @LegendaryLinux
|
||||
|
||||
@@ -25,6 +28,9 @@
|
||||
# Blasphemous
|
||||
/worlds/blasphemous/ @TRPG0
|
||||
|
||||
# Bomb Rush Cyberfunk
|
||||
/worlds/bomb_rush_cyberfunk/ @TRPG0
|
||||
|
||||
# Bumper Stickers
|
||||
/worlds/bumpstik/ @FelicitusNeko
|
||||
|
||||
@@ -35,7 +41,7 @@
|
||||
/worlds/celeste64/ @PoryGone
|
||||
|
||||
# ChecksFinder
|
||||
/worlds/checksfinder/ @jonloveslegos
|
||||
/worlds/checksfinder/ @SunCatMC
|
||||
|
||||
# Clique
|
||||
/worlds/clique/ @ThePhar
|
||||
@@ -92,6 +98,9 @@
|
||||
/worlds/lufia2ac/ @el-u
|
||||
/worlds/lufia2ac/docs/ @wordfcuk @el-u
|
||||
|
||||
# Mario & Luigi: Superstar Saga
|
||||
/worlds/mlss/ @jamesbrq
|
||||
|
||||
# Meritous
|
||||
/worlds/meritous/ @FelicitusNeko
|
||||
|
||||
@@ -194,6 +203,9 @@
|
||||
# Yoshi's Island
|
||||
/worlds/yoshisisland/ @PinkSwitch
|
||||
|
||||
#Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
||||
/worlds/yugioh06/ @rensen
|
||||
|
||||
# Zillion
|
||||
/worlds/zillion/ @beauxq
|
||||
|
||||
|
||||
@@ -85,6 +85,25 @@ class ExampleWorld(World):
|
||||
options: ExampleGameOptions
|
||||
```
|
||||
|
||||
### Option Groups
|
||||
Options may be categorized into groups for display on the WebHost. Option groups are displayed alphabetically on the
|
||||
player-options and weighted-options pages. Options without a group name are categorized into a generic "Game Options"
|
||||
group.
|
||||
|
||||
```python
|
||||
from worlds.AutoWorld import WebWorld
|
||||
from Options import OptionGroup
|
||||
|
||||
class MyWorldWeb(WebWorld):
|
||||
option_groups = [
|
||||
OptionGroup('Color Options', [
|
||||
Options.ColorblindMode,
|
||||
Options.FlashReduction,
|
||||
Options.UIColors,
|
||||
]),
|
||||
]
|
||||
```
|
||||
|
||||
### Option Checking
|
||||
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
|
||||
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
|
||||
@@ -155,10 +174,12 @@ Gives the player starting hints for where the items defined here are.
|
||||
Gives the player starting hints for the items on locations defined here.
|
||||
|
||||
### ExcludeLocations
|
||||
Marks locations given here as `LocationProgressType.Excluded` so that progression items can't be placed on them.
|
||||
Marks locations given here as `LocationProgressType.Excluded` so that neither progression nor useful items can be
|
||||
placed on them.
|
||||
|
||||
### PriorityLocations
|
||||
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them.
|
||||
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
|
||||
the pool.
|
||||
|
||||
### ItemLinks
|
||||
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
||||
|
||||
@@ -17,13 +17,14 @@ Then run any of the starting point scripts, like Generate.py, and the included M
|
||||
required modules and after pressing enter proceed to install everything automatically.
|
||||
After this, you should be able to run the programs.
|
||||
|
||||
* `Launcher.py` gives access to many components, including clients registered in `worlds/LauncherComponents.py`.
|
||||
* The Launcher button "Generate Template Options" will generate default yamls for all worlds.
|
||||
* With yaml(s) in the `Players` folder, `Generate.py` will generate the multiworld archive.
|
||||
* `MultiServer.py`, with the filename of the generated archive as a command line parameter, will host the multiworld locally.
|
||||
* `--log_network` is a command line parameter useful for debugging.
|
||||
* `WebHost.py` will host the website on your computer.
|
||||
* You can copy `docs/webhost configuration sample.yaml` to `config.yaml`
|
||||
to change WebHost options (like the web hosting port number).
|
||||
* As a side effect, `WebHost.py` creates the template yamls for all the games in `WebHostLib/static/generated`.
|
||||
|
||||
|
||||
## Windows
|
||||
|
||||
@@ -17,6 +17,15 @@
|
||||
* Use type annotations where possible for function signatures and class members.
|
||||
* Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the
|
||||
type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls.
|
||||
* If a line ends with an open bracket/brace/parentheses, the matching closing bracket should be at the
|
||||
beginning of a line at the same indentation as the beginning of the line with the open bracket.
|
||||
```python
|
||||
stuff = {
|
||||
x: y
|
||||
for x, y in thing
|
||||
if y > 2
|
||||
}
|
||||
```
|
||||
* New classes, attributes, and methods in core code should have docstrings that follow
|
||||
[reST style](https://peps.python.org/pep-0287/).
|
||||
* Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# This is a sample configuration for the Web host.
|
||||
# This is a sample configuration for the Web host.
|
||||
# If you wish to change any of these, rename this file to config.yaml
|
||||
# Default values are shown here. Uncomment and change the values as desired.
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
# Secret key used to determine important things like cookie authentication of room/seed page ownership.
|
||||
# If you wish to deploy, uncomment the following line and set it to something not easily guessable.
|
||||
# SECRET_KEY: "Your secret key here"
|
||||
# SECRET_KEY: "Your secret key here"
|
||||
|
||||
# TODO
|
||||
#JOB_THRESHOLD: 2
|
||||
@@ -38,15 +38,16 @@
|
||||
# provider: "sqlite"
|
||||
# filename: "ap.db3" # This MUST be the ABSOLUTE PATH to the file.
|
||||
# create_db: true
|
||||
|
||||
|
||||
# Maximum number of players that are allowed to be rolled on the server. After this limit, one should roll locally and upload the results.
|
||||
#MAX_ROLL: 20
|
||||
|
||||
# TODO
|
||||
#CACHE_TYPE: "simple"
|
||||
|
||||
# TODO
|
||||
#JSON_AS_ASCII: false
|
||||
|
||||
# Host Address. This is the address encoded into the patch that will be used for client auto-connect.
|
||||
#HOST_ADDRESS: archipelago.gg
|
||||
|
||||
# Asset redistribution rights. If true, the host affirms they have been given explicit permission to redistribute
|
||||
# the proprietary assets in WebHostLib
|
||||
#ASSET_RIGHTS: false
|
||||
|
||||
@@ -181,8 +181,7 @@ required, and will prevent progression and useful items from being placed at exc
|
||||
#### Documenting Locations
|
||||
|
||||
Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and
|
||||
location groups. These descriptions will show up in location-selection options on the Weighted Options page. Extra
|
||||
indentation and single newlines will be collapsed into spaces.
|
||||
location groups. These descriptions will show up in location-selection options on the options pages.
|
||||
|
||||
```python
|
||||
# locations.py
|
||||
@@ -236,8 +235,7 @@ Other classifications include:
|
||||
#### Documenting Items
|
||||
|
||||
Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item
|
||||
groups. These descriptions will show up in item-selection options on the Weighted Options page. Extra indentation and
|
||||
single newlines will be collapsed into spaces.
|
||||
groups. These descriptions will show up in item-selection options on the options pages.
|
||||
|
||||
```python
|
||||
# items.py
|
||||
|
||||
@@ -169,6 +169,11 @@ 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\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: "{#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: "";
|
||||
@@ -194,6 +199,11 @@ Root: HKCR; Subkey: "{#MyAppName}yipatch"; ValueData: "Archi
|
||||
Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apygo06"; ValueData: "{#MyAppName}ygo06patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ygo06patch"; ValueData: "Archipelago Yu-Gi-Oh 2006 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
@@ -200,7 +200,7 @@ class Group:
|
||||
def _dump_value(cls, value: Any, f: TextIO, indent: str) -> None:
|
||||
"""Write a single yaml line to f"""
|
||||
from Utils import dump, Dumper as BaseDumper
|
||||
yaml_line: str = dump(value, Dumper=cast(BaseDumper, cls._dumper))
|
||||
yaml_line: str = dump(value, Dumper=cast(BaseDumper, cls._dumper), width=2**31-1)
|
||||
assert yaml_line.count("\n") == 1, f"Unexpected input for yaml dumper: {value}"
|
||||
f.write(f"{indent}{yaml_line}")
|
||||
|
||||
@@ -671,7 +671,6 @@ class GeneratorOptions(Group):
|
||||
weights_file_path: WeightsFilePath = WeightsFilePath("weights.yaml")
|
||||
meta_file_path: MetaFilePath = MetaFilePath("meta.yaml")
|
||||
spoiler: Spoiler = Spoiler(3)
|
||||
glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here?
|
||||
race: Race = Race(0)
|
||||
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
|
||||
|
||||
|
||||
6
setup.py
6
setup.py
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
try:
|
||||
requirement = 'cx-Freeze>=6.15.10'
|
||||
requirement = 'cx-Freeze>=7.0.0'
|
||||
import pkg_resources
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
@@ -228,8 +228,8 @@ class BuildCommand(setuptools.command.build.build):
|
||||
|
||||
|
||||
# Override cx_Freeze's build_exe command for pre and post build steps
|
||||
class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [
|
||||
class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
user_options = cx_Freeze.command.build_exe.build_exe.user_options + [
|
||||
('yes', 'y', 'Answer "yes" to all questions.'),
|
||||
('extra-data=', None, 'Additional files to add.'),
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from argparse import Namespace
|
||||
from typing import List, Optional, Tuple, Type, Union
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
|
||||
from worlds.AutoWorld import World, call_all
|
||||
|
||||
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
||||
@@ -17,19 +17,21 @@ def setup_solo_multiworld(
|
||||
:param steps: The gen steps that should be called on the generated multiworld before returning. Default calls
|
||||
steps through pre_fill
|
||||
:param seed: The seed to be used when creating this multiworld
|
||||
:return: The generated multiworld
|
||||
"""
|
||||
return setup_multiworld(world_type, steps, seed)
|
||||
|
||||
|
||||
def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple[str, ...] = gen_steps,
|
||||
seed: Optional[int] = None) -> MultiWorld:
|
||||
seed: Optional[int] = None) -> MultiWorld:
|
||||
"""
|
||||
Creates a multiworld with a player for each provided world type, allowing duplicates, setting default options, and
|
||||
calling the provided gen steps.
|
||||
|
||||
:param worlds: type/s of worlds to generate a multiworld for
|
||||
:param steps: gen steps that should be called before returning. Default calls through pre_fill
|
||||
:param worlds: Type/s of worlds to generate a multiworld for
|
||||
:param steps: Gen steps that should be called before returning. Default calls through pre_fill
|
||||
:param seed: The seed to be used when creating this multiworld
|
||||
:return: The generated multiworld
|
||||
"""
|
||||
if not isinstance(worlds, list):
|
||||
worlds = [worlds]
|
||||
@@ -49,3 +51,59 @@ def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple
|
||||
for step in steps:
|
||||
call_all(multiworld, step)
|
||||
return multiworld
|
||||
|
||||
|
||||
class TestWorld(World):
|
||||
game = f"Test Game"
|
||||
item_name_to_id = {}
|
||||
location_name_to_id = {}
|
||||
hidden = True
|
||||
|
||||
|
||||
def generate_test_multiworld(players: int = 1) -> MultiWorld:
|
||||
"""
|
||||
Generates a multiworld using a special Test Case World class, and seed of 0.
|
||||
|
||||
:param players: Number of players to generate the multiworld for
|
||||
:return: The generated test multiworld
|
||||
"""
|
||||
multiworld = setup_multiworld([TestWorld] * players, seed=0)
|
||||
multiworld.regions += [Region("Menu", player_id + 1, multiworld) for player_id in range(players)]
|
||||
|
||||
return multiworld
|
||||
|
||||
|
||||
def generate_locations(count: int, player_id: int, region: Region, address: Optional[int] = None,
|
||||
tag: str = "") -> List[Location]:
|
||||
"""
|
||||
Generates the specified amount of locations for the player and adds them to the specified region.
|
||||
|
||||
:param count: Number of locations to create
|
||||
:param player_id: ID of the player to create the locations for
|
||||
:param address: Address for the specified locations. They will all share the same address if multiple are created
|
||||
:param region: Parent region to add these locations to
|
||||
:param tag: Tag to add to the name of the generated locations
|
||||
:return: List containing the created locations
|
||||
"""
|
||||
prefix = f"player{player_id}{tag}_location"
|
||||
|
||||
locations = [Location(player_id, f"{prefix}{i}", address, region) for i in range(count)]
|
||||
region.locations += locations
|
||||
return locations
|
||||
|
||||
|
||||
def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]:
|
||||
"""
|
||||
Generates the specified amount of items for the target player.
|
||||
|
||||
:param count: The amount of items to create
|
||||
:param player_id: ID of the player to create the items for
|
||||
:param advancement: Whether the created items should be advancement
|
||||
:param code: The code the items should be created with
|
||||
:return: List containing the created items
|
||||
"""
|
||||
item_type = "prog" if advancement else ""
|
||||
classification = ItemClassification.progression if advancement else ItemClassification.filler
|
||||
|
||||
items = [Item(f"player{player_id}_{item_type}item{i}", classification, code, player_id) for i in range(count)]
|
||||
return items
|
||||
|
||||
@@ -1,41 +1,15 @@
|
||||
from typing import List, Iterable
|
||||
import unittest
|
||||
|
||||
import Options
|
||||
from Options import Accessibility
|
||||
from worlds.AutoWorld import World
|
||||
from test.general import generate_items, generate_locations, generate_test_multiworld
|
||||
from Fill import FillError, balance_multiworld_progression, fill_restrictive, \
|
||||
distribute_early_items, distribute_items_restrictive
|
||||
from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item, Location, \
|
||||
ItemClassification, CollectionState
|
||||
ItemClassification
|
||||
from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule
|
||||
|
||||
|
||||
def generate_multiworld(players: int = 1) -> MultiWorld:
|
||||
multiworld = MultiWorld(players)
|
||||
multiworld.set_seed(0)
|
||||
multiworld.player_name = {}
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
for i in range(players):
|
||||
player_id = i+1
|
||||
world = World(multiworld, player_id)
|
||||
multiworld.game[player_id] = f"Game {player_id}"
|
||||
multiworld.worlds[player_id] = world
|
||||
multiworld.player_name[player_id] = "Test Player " + str(player_id)
|
||||
region = Region("Menu", player_id, multiworld, "Menu Region Hint")
|
||||
multiworld.regions.append(region)
|
||||
for option_key, option in Options.PerGameCommonOptions.type_hints.items():
|
||||
if hasattr(multiworld, option_key):
|
||||
getattr(multiworld, option_key).setdefault(player_id, option.from_any(getattr(option, "default")))
|
||||
else:
|
||||
setattr(multiworld, option_key, {player_id: option.from_any(getattr(option, "default"))})
|
||||
# TODO - remove this loop once all worlds use options dataclasses
|
||||
world.options = world.options_dataclass(**{option_key: getattr(multiworld, option_key)[player_id]
|
||||
for option_key in world.options_dataclass.type_hints})
|
||||
|
||||
return multiworld
|
||||
|
||||
|
||||
class PlayerDefinition(object):
|
||||
multiworld: MultiWorld
|
||||
id: int
|
||||
@@ -55,12 +29,12 @@ class PlayerDefinition(object):
|
||||
self.regions = [menu]
|
||||
|
||||
def generate_region(self, parent: Region, size: int, access_rule: CollectionRule = lambda state: True) -> Region:
|
||||
region_tag = "_region" + str(len(self.regions))
|
||||
region_name = "player" + str(self.id) + region_tag
|
||||
region = Region("player" + str(self.id) + region_tag, self.id, self.multiworld)
|
||||
self.locations += generate_locations(size, self.id, None, region, region_tag)
|
||||
region_tag = f"_region{len(self.regions)}"
|
||||
region_name = f"player{self.id}{region_tag}"
|
||||
region = Region(f"player{self.id}{region_tag}", self.id, self.multiworld)
|
||||
self.locations += generate_locations(size, self.id, region, None, region_tag)
|
||||
|
||||
entrance = Entrance(self.id, region_name + "_entrance", parent)
|
||||
entrance = Entrance(self.id, f"{region_name}_entrance", parent)
|
||||
parent.exits.append(entrance)
|
||||
entrance.connect(region)
|
||||
entrance.access_rule = access_rule
|
||||
@@ -94,7 +68,7 @@ def region_contains(region: Region, item: Item) -> bool:
|
||||
|
||||
def generate_player_data(multiworld: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition:
|
||||
menu = multiworld.get_region("Menu", player_id)
|
||||
locations = generate_locations(location_count, player_id, None, menu)
|
||||
locations = generate_locations(location_count, player_id, menu, None)
|
||||
prog_items = generate_items(prog_item_count, player_id, True)
|
||||
multiworld.itempool += prog_items
|
||||
basic_items = generate_items(basic_item_count, player_id, False)
|
||||
@@ -103,28 +77,6 @@ def generate_player_data(multiworld: MultiWorld, player_id: int, location_count:
|
||||
return PlayerDefinition(multiworld, player_id, menu, locations, prog_items, basic_items)
|
||||
|
||||
|
||||
def generate_locations(count: int, player_id: int, address: int = None, region: Region = None, tag: str = "") -> List[Location]:
|
||||
locations = []
|
||||
prefix = "player" + str(player_id) + tag + "_location"
|
||||
for i in range(count):
|
||||
name = prefix + str(i)
|
||||
location = Location(player_id, name, address, region)
|
||||
locations.append(location)
|
||||
region.locations.append(location)
|
||||
return locations
|
||||
|
||||
|
||||
def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]:
|
||||
items = []
|
||||
item_type = "prog" if advancement else ""
|
||||
for i in range(count):
|
||||
name = "player" + str(player_id) + "_" + item_type + "item" + str(i)
|
||||
items.append(Item(name,
|
||||
ItemClassification.progression if advancement else ItemClassification.filler,
|
||||
code, player_id))
|
||||
return items
|
||||
|
||||
|
||||
def names(objs: list) -> Iterable[str]:
|
||||
return map(lambda o: o.name, objs)
|
||||
|
||||
@@ -132,7 +84,7 @@ def names(objs: list) -> Iterable[str]:
|
||||
class TestFillRestrictive(unittest.TestCase):
|
||||
def test_basic_fill(self):
|
||||
"""Tests `fill_restrictive` fills and removes the locations and items from their respective lists"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
@@ -150,7 +102,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_ordered_fill(self):
|
||||
"""Tests `fill_restrictive` fulfills set rules"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
@@ -167,7 +119,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_partial_fill(self):
|
||||
"""Tests that `fill_restrictive` returns unfilled locations"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 3, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
@@ -193,7 +145,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_minimal_fill(self):
|
||||
"""Test that fill for minimal player can have unreachable items"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
items = player1.prog_items
|
||||
@@ -218,7 +170,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
the non-minimal player get all items.
|
||||
"""
|
||||
|
||||
multiworld = generate_multiworld(2)
|
||||
multiworld = generate_test_multiworld(2)
|
||||
player1 = generate_player_data(multiworld, 1, 3, 3)
|
||||
player2 = generate_player_data(multiworld, 2, 3, 3)
|
||||
|
||||
@@ -245,11 +197,11 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
# all of player2's locations and items should be accessible (not all of player1's)
|
||||
for item in player2.prog_items:
|
||||
self.assertTrue(multiworld.state.has(item.name, player2.id),
|
||||
f'{item} is unreachable in {item.location}')
|
||||
f"{item} is unreachable in {item.location}")
|
||||
|
||||
def test_reversed_fill(self):
|
||||
"""Test a different set of rules can be satisfied"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
@@ -268,7 +220,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_multi_step_fill(self):
|
||||
"""Test that fill is able to satisfy multiple spheres"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 4, 4)
|
||||
|
||||
items = player1.prog_items
|
||||
@@ -293,7 +245,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_impossible_fill(self):
|
||||
"""Test that fill raises an error when it can't place any items"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
@@ -310,7 +262,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_circular_fill(self):
|
||||
"""Test that fill raises an error when it can't place all items"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 3, 3)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
@@ -331,7 +283,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_competing_fill(self):
|
||||
"""Test that fill raises an error when it can't place items in a way to satisfy the conditions"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
@@ -348,7 +300,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_multiplayer_fill(self):
|
||||
"""Test that items can be placed across worlds"""
|
||||
multiworld = generate_multiworld(2)
|
||||
multiworld = generate_test_multiworld(2)
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
player2 = generate_player_data(multiworld, 2, 2, 2)
|
||||
|
||||
@@ -369,7 +321,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_multiplayer_rules_fill(self):
|
||||
"""Test that fill across worlds satisfies the rules"""
|
||||
multiworld = generate_multiworld(2)
|
||||
multiworld = generate_test_multiworld(2)
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
player2 = generate_player_data(multiworld, 2, 2, 2)
|
||||
|
||||
@@ -393,7 +345,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_restrictive_progress(self):
|
||||
"""Test that various spheres with different requirements can be filled"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, prog_item_count=25)
|
||||
items = player1.prog_items.copy()
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
@@ -417,7 +369,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
def test_swap_to_earlier_location_with_item_rule(self):
|
||||
"""Test that item swap happens and works as intended"""
|
||||
# test for PR#1109
|
||||
multiworld = generate_multiworld(1)
|
||||
multiworld = generate_test_multiworld(1)
|
||||
player1 = generate_player_data(multiworld, 1, 4, 4)
|
||||
locations = player1.locations[:] # copy required
|
||||
items = player1.prog_items[:] # copy required
|
||||
@@ -442,7 +394,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_swap_to_earlier_location_with_item_rule2(self):
|
||||
"""Test that swap works before all items are placed"""
|
||||
multiworld = generate_multiworld(1)
|
||||
multiworld = generate_test_multiworld(1)
|
||||
player1 = generate_player_data(multiworld, 1, 5, 5)
|
||||
locations = player1.locations[:] # copy required
|
||||
items = player1.prog_items[:] # copy required
|
||||
@@ -484,7 +436,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
def test_double_sweep(self):
|
||||
"""Test that sweep doesn't duplicate Event items when sweeping"""
|
||||
# test for PR1114
|
||||
multiworld = generate_multiworld(1)
|
||||
multiworld = generate_test_multiworld(1)
|
||||
player1 = generate_player_data(multiworld, 1, 1, 1)
|
||||
location = player1.locations[0]
|
||||
location.address = None
|
||||
@@ -498,7 +450,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_correct_item_instance_removed_from_pool(self):
|
||||
"""Test that a placed item gets removed from the submitted pool"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
player1.prog_items[0].name = "Different_item_instance_but_same_item_name"
|
||||
@@ -515,7 +467,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
def test_basic_distribute(self):
|
||||
"""Test that distribute_items_restrictive is deterministic"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
@@ -535,7 +487,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_excluded_distribute(self):
|
||||
"""Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
@@ -550,7 +502,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_non_excluded_item_distribute(self):
|
||||
"""Test that useful items aren't placed on excluded locations"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
@@ -565,7 +517,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_too_many_excluded_distribute(self):
|
||||
"""Test that fill fails if it can't place all progression items due to too many excluded locations"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
@@ -578,7 +530,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_non_excluded_item_must_distribute(self):
|
||||
"""Test that fill fails if it can't place useful items due to too many excluded locations"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
@@ -593,7 +545,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_priority_distribute(self):
|
||||
"""Test that priority locations receive advancement items"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
@@ -608,7 +560,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_excess_priority_distribute(self):
|
||||
"""Test that if there's more priority locations than advancement items, they can still fill"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
@@ -623,7 +575,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_multiple_world_priority_distribute(self):
|
||||
"""Test that priority fill can be satisfied for multiple worlds"""
|
||||
multiworld = generate_multiworld(3)
|
||||
multiworld = generate_test_multiworld(3)
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
player2 = generate_player_data(
|
||||
@@ -653,7 +605,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_can_remove_locations_in_fill_hook(self):
|
||||
"""Test that distribute_items_restrictive calls the fill hook and allows for item and location removal"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
|
||||
@@ -673,12 +625,12 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_seed_robust_to_item_order(self):
|
||||
"""Test deterministic fill"""
|
||||
mw1 = generate_multiworld()
|
||||
mw1 = generate_test_multiworld()
|
||||
gen1 = generate_player_data(
|
||||
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
distribute_items_restrictive(mw1)
|
||||
|
||||
mw2 = generate_multiworld()
|
||||
mw2 = generate_test_multiworld()
|
||||
gen2 = generate_player_data(
|
||||
mw2, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
mw2.itempool.append(mw2.itempool.pop(0))
|
||||
@@ -691,12 +643,12 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_seed_robust_to_location_order(self):
|
||||
"""Test deterministic fill even if locations in a region are reordered"""
|
||||
mw1 = generate_multiworld()
|
||||
mw1 = generate_test_multiworld()
|
||||
gen1 = generate_player_data(
|
||||
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
distribute_items_restrictive(mw1)
|
||||
|
||||
mw2 = generate_multiworld()
|
||||
mw2 = generate_test_multiworld()
|
||||
gen2 = generate_player_data(
|
||||
mw2, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
reg = mw2.get_region("Menu", gen2.id)
|
||||
@@ -710,7 +662,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_can_reserve_advancement_items_for_general_fill(self):
|
||||
"""Test that priority locations fill still satisfies item rules"""
|
||||
multiworld = generate_multiworld()
|
||||
multiworld = generate_test_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, location_count=5, prog_item_count=5)
|
||||
items = player1.prog_items
|
||||
@@ -727,7 +679,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_non_excluded_local_items(self):
|
||||
"""Test that local items get placed locally in a multiworld"""
|
||||
multiworld = generate_multiworld(2)
|
||||
multiworld = generate_test_multiworld(2)
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, location_count=5, basic_item_count=5)
|
||||
player2 = generate_player_data(
|
||||
@@ -748,7 +700,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_early_items(self) -> None:
|
||||
"""Test that the early items API successfully places items early"""
|
||||
mw = generate_multiworld(2)
|
||||
mw = generate_test_multiworld(2)
|
||||
player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5)
|
||||
player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5)
|
||||
mw.early_items[1][player1.basic_items[0].name] = 1
|
||||
@@ -803,11 +755,11 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
if location.item and location.item == item:
|
||||
return True
|
||||
|
||||
self.fail("Expected " + region.name + " to contain " + item.name +
|
||||
"\n Contains" + str(list(map(lambda location: location.item, region.locations))))
|
||||
self.fail(f"Expected {region.name} to contain {item.name}.\n"
|
||||
f"Contains{list(map(lambda location: location.item, region.locations))}")
|
||||
|
||||
def setUp(self) -> None:
|
||||
multiworld = generate_multiworld(2)
|
||||
multiworld = generate_test_multiworld(2)
|
||||
self.multiworld = multiworld
|
||||
player1 = generate_player_data(
|
||||
multiworld, 1, prog_item_count=2, basic_item_count=40)
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import os
|
||||
import unittest
|
||||
from io import StringIO
|
||||
from tempfile import TemporaryFile
|
||||
from typing import Any, Dict, List, cast
|
||||
|
||||
from settings import Settings
|
||||
import Utils
|
||||
from settings import Settings, Group
|
||||
|
||||
|
||||
class TestIDs(unittest.TestCase):
|
||||
yaml_options: Dict[Any, Any]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
with TemporaryFile("w+", encoding="utf-8") as f:
|
||||
Settings(None).dump(f)
|
||||
f.seek(0, os.SEEK_SET)
|
||||
cls.yaml_options = Utils.parse_yaml(f.read())
|
||||
yaml_options = Utils.parse_yaml(f.read())
|
||||
assert isinstance(yaml_options, dict)
|
||||
cls.yaml_options = yaml_options
|
||||
|
||||
def test_utils_in_yaml(self) -> None:
|
||||
"""Tests that the auto generated host.yaml has default settings in it"""
|
||||
@@ -30,3 +36,47 @@ class TestIDs(unittest.TestCase):
|
||||
self.assertIn(option_key, utils_options)
|
||||
for sub_option_key in option_set:
|
||||
self.assertIn(sub_option_key, utils_options[option_key])
|
||||
|
||||
|
||||
class TestSettingsDumper(unittest.TestCase):
|
||||
def test_string_format(self) -> None:
|
||||
"""Test that dumping a string will yield the expected output"""
|
||||
# By default, pyyaml has automatic line breaks in strings and quoting is optional.
|
||||
# What we want for consistency instead is single-line strings and always quote them.
|
||||
# Line breaks have to become \n in that quoting style.
|
||||
class AGroup(Group):
|
||||
key: str = " ".join(["x"] * 60) + "\n" # more than 120 chars, contains spaces and a line break
|
||||
|
||||
with StringIO() as writer:
|
||||
AGroup().dump(writer, 0)
|
||||
expected_value = AGroup.key.replace("\n", "\\n")
|
||||
self.assertEqual(writer.getvalue(), f"key: \"{expected_value}\"\n",
|
||||
"dumped string has unexpected formatting")
|
||||
|
||||
def test_indentation(self) -> None:
|
||||
"""Test that dumping items will add indentation"""
|
||||
# NOTE: we don't care how many spaces there are, but it has to be a multiple of level
|
||||
class AList(List[Any]):
|
||||
__doc__ = None # make sure we get no doc string
|
||||
|
||||
class AGroup(Group):
|
||||
key: AList = cast(AList, ["a", "b", [1]])
|
||||
|
||||
for level in range(3):
|
||||
with StringIO() as writer:
|
||||
AGroup().dump(writer, level)
|
||||
lines = writer.getvalue().split("\n", 5)
|
||||
key_line = lines[0]
|
||||
key_spaces = len(key_line) - len(key_line.lstrip(" "))
|
||||
value_lines = lines[1:-1]
|
||||
value_spaces = [len(value_line) - len(value_line.lstrip(" ")) for value_line in value_lines]
|
||||
if level == 0:
|
||||
self.assertEqual(key_spaces, 0)
|
||||
else:
|
||||
self.assertGreaterEqual(key_spaces, level)
|
||||
self.assertEqual(key_spaces % level, 0)
|
||||
self.assertGreaterEqual(value_spaces[0], key_spaces) # a
|
||||
self.assertEqual(value_spaces[1], value_spaces[0]) # b
|
||||
self.assertEqual(value_spaces[2], value_spaces[0]) # start of sub-list
|
||||
self.assertGreater(value_spaces[3], value_spaces[0],
|
||||
f"{value_lines[3]} should have more indentation than {value_lines[0]} in {lines}")
|
||||
|
||||
@@ -3,6 +3,7 @@ import unittest
|
||||
from Fill import distribute_items_restrictive
|
||||
from NetUtils import encode
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from worlds import failed_world_loads
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
@@ -47,3 +48,7 @@ class TestImplemented(unittest.TestCase):
|
||||
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(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,6 +25,8 @@ class TestBase(unittest.TestCase):
|
||||
{"medallions", "stones", "rewards", "logic_bottles"},
|
||||
"Starcraft 2":
|
||||
{"Missions", "WoL Missions"},
|
||||
"Yu-Gi-Oh! 2006":
|
||||
{"Campaign Boss Beaten"}
|
||||
}
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game_name, game_name=game_name):
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import unittest
|
||||
|
||||
from BaseClasses import PlandoOptions
|
||||
from Options import ItemLinks
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
@@ -17,3 +20,30 @@ class TestOptions(unittest.TestCase):
|
||||
with self.subTest(game=gamename):
|
||||
self.assertFalse(hasattr(world_type, "options"),
|
||||
f"Unexpected assignment to {world_type.__name__}.options!")
|
||||
|
||||
def test_item_links_name_groups(self):
|
||||
"""Tests that item links successfully unfold item_name_groups"""
|
||||
item_link_groups = [
|
||||
[{
|
||||
"name": "ItemLinkGroup",
|
||||
"item_pool": ["Everything"],
|
||||
"link_replacement": False,
|
||||
"replacement_item": None,
|
||||
}],
|
||||
[{
|
||||
"name": "ItemLinkGroup",
|
||||
"item_pool": ["Hammer", "Bow"],
|
||||
"link_replacement": False,
|
||||
"replacement_item": None,
|
||||
}]
|
||||
]
|
||||
# we really need some sort of test world but generic doesn't have enough items for this
|
||||
world = AutoWorldRegister.world_types["A Link to the Past"]
|
||||
plando_options = PlandoOptions.from_option_string("bosses")
|
||||
item_links = [ItemLinks.from_any(item_link_groups[0]), ItemLinks.from_any(item_link_groups[1])]
|
||||
for link in item_links:
|
||||
link.verify(world, "tester", plando_options)
|
||||
self.assertIn("Hammer", link.value[0]["item_pool"])
|
||||
self.assertIn("Bow", link.value[0]["item_pool"])
|
||||
|
||||
# TODO test that the group created using these options has the items
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import logging
|
||||
import pathlib
|
||||
import random
|
||||
from random import Random
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
@@ -11,11 +11,13 @@ from dataclasses import make_dataclass
|
||||
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping,
|
||||
Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union)
|
||||
|
||||
from Options import PerGameCommonOptions
|
||||
from Options import (
|
||||
ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions,
|
||||
PriorityLocations, StartHints, StartInventory, StartInventoryPool, StartLocationHints
|
||||
)
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import random
|
||||
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
|
||||
from . import GamesPackage
|
||||
from settings import Group
|
||||
@@ -118,14 +120,37 @@ class AutoLogicRegister(type):
|
||||
return new_class
|
||||
|
||||
|
||||
class WebWorldRegister(type):
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> WebWorldRegister:
|
||||
# don't allow an option to appear in multiple groups, allow "Item & Location Options" to appear anywhere by the
|
||||
# dev, putting it at the end if they don't define options in it
|
||||
option_groups: List[OptionGroup] = dct.get("option_groups", [])
|
||||
item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints,
|
||||
StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks]
|
||||
seen_options = []
|
||||
item_group_in_list = False
|
||||
for group in option_groups:
|
||||
assert group.name != "Game Options", "Game Options is a pre-determined group and can not be defined."
|
||||
if group.name == "Item & Location Options":
|
||||
group.options.extend(item_and_loc_options)
|
||||
item_group_in_list = True
|
||||
else:
|
||||
for option in group.options:
|
||||
assert option not in item_and_loc_options, \
|
||||
f"{option} cannot be moved out of the \"Item & Location Options\" Group"
|
||||
assert len(group.options) == len(set(group.options)), f"Duplicate options in option group {group.name}"
|
||||
for option in group.options:
|
||||
assert option not in seen_options, f"{option} found in two option groups"
|
||||
seen_options.append(option)
|
||||
if not item_group_in_list:
|
||||
option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options))
|
||||
return super().__new__(mcs, name, bases, dct)
|
||||
|
||||
|
||||
def _timed_call(method: Callable[..., Any], *args: Any,
|
||||
multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any:
|
||||
start = time.perf_counter()
|
||||
if multiworld:
|
||||
with multiworld.observer(method, start):
|
||||
ret = method(*args)
|
||||
else:
|
||||
ret = method(*args)
|
||||
ret = method(*args)
|
||||
taken = time.perf_counter() - start
|
||||
if taken > 1.0:
|
||||
if player and multiworld:
|
||||
@@ -173,10 +198,10 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
for world_type in sorted(world_types, key=lambda world: world.__name__):
|
||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||
if stage_callable:
|
||||
_timed_call(stage_callable, multiworld, *args, multiworld=multiworld)
|
||||
_timed_call(stage_callable, multiworld, *args)
|
||||
|
||||
|
||||
class WebWorld:
|
||||
class WebWorld(metaclass=WebWorldRegister):
|
||||
"""Webhost integration"""
|
||||
|
||||
options_page: Union[bool, str] = True
|
||||
@@ -198,6 +223,9 @@ class WebWorld:
|
||||
options_presets: Dict[str, Dict[str, Any]] = {}
|
||||
"""A dictionary containing a collection of developer-defined game option presets."""
|
||||
|
||||
option_groups: ClassVar[List[OptionGroup]] = []
|
||||
"""Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options"."""
|
||||
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
@@ -210,8 +238,8 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
game: ClassVar[str]
|
||||
"""name the game"""
|
||||
topology_present: ClassVar[bool] = False
|
||||
"""indicate if world type has any meaningful layout/pathing"""
|
||||
topology_present: bool = False
|
||||
"""indicate if this world has any meaningful layout/pathing"""
|
||||
|
||||
all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset()
|
||||
"""gets automatically populated with all item and item group names"""
|
||||
@@ -287,7 +315,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
location_names: ClassVar[Set[str]]
|
||||
"""set of all potential location names"""
|
||||
|
||||
random: random.Random
|
||||
random: Random
|
||||
"""This world's random object. Should be used for any randomization needed in world for this player slot."""
|
||||
|
||||
settings_key: ClassVar[str]
|
||||
@@ -304,7 +332,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
assert multiworld is not None
|
||||
self.multiworld = multiworld
|
||||
self.player = player
|
||||
self.random = random.Random(multiworld.random.getrandbits(64))
|
||||
self.random = Random(multiworld.random.getrandbits(64))
|
||||
multiworld.per_slot_randoms[player] = self.random
|
||||
|
||||
def __getattr__(self, item: str) -> Any:
|
||||
@@ -508,6 +536,10 @@ class World(metaclass=AutoWorldRegister):
|
||||
def get_region(self, region_name: str) -> "Region":
|
||||
return self.multiworld.get_region(region_name, self.player)
|
||||
|
||||
@property
|
||||
def player_name(self) -> str:
|
||||
return self.multiworld.get_player_name(self.player)
|
||||
|
||||
@classmethod
|
||||
def get_data_package_data(cls) -> "GamesPackage":
|
||||
sorted_item_name_groups = {
|
||||
|
||||
@@ -19,9 +19,9 @@ class WorldPosition:
|
||||
|
||||
def get_position(self, random):
|
||||
if self.room_x is None or self.room_y is None:
|
||||
return random.choice(standard_positions)
|
||||
return self.room_id, random.choice(standard_positions)
|
||||
else:
|
||||
return self.room_x, self.room_y
|
||||
return self.room_id, (self.room_x, self.room_y)
|
||||
|
||||
|
||||
class LocationData:
|
||||
@@ -46,24 +46,26 @@ class LocationData:
|
||||
self.needs_bat_logic: int = needs_bat_logic
|
||||
self.local_item: int = None
|
||||
|
||||
def get_position(self, random):
|
||||
def get_random_position(self, random):
|
||||
x: int = None
|
||||
y: int = None
|
||||
if self.world_positions is None or len(self.world_positions) == 0:
|
||||
if self.room_id is None:
|
||||
return None
|
||||
self.room_x, self.room_y = random.choice(standard_positions)
|
||||
if self.room_id is None:
|
||||
x, y = random.choice(standard_positions)
|
||||
return self.room_id, x, y
|
||||
else:
|
||||
selected_pos = random.choice(self.world_positions)
|
||||
self.room_id = selected_pos.room_id
|
||||
self.room_x, self.room_y = selected_pos.get_position(random)
|
||||
return self.room_x, self.room_y
|
||||
room_id, (x, y) = selected_pos.get_position(random)
|
||||
return self.get_random_room_id(random), x, y
|
||||
|
||||
def get_room_id(self, random):
|
||||
def get_random_room_id(self, random):
|
||||
if self.world_positions is None or len(self.world_positions) == 0:
|
||||
return None
|
||||
if self.room_id is None:
|
||||
return None
|
||||
if self.room_id is None:
|
||||
selected_pos = random.choice(self.world_positions)
|
||||
self.room_id = selected_pos.room_id
|
||||
self.room_x, self.room_y = selected_pos.get_position(random)
|
||||
return selected_pos.room_id
|
||||
return self.room_id
|
||||
|
||||
|
||||
@@ -97,7 +99,7 @@ def get_random_room_in_regions(regions: [str], random) -> int:
|
||||
possible_rooms = {}
|
||||
for locname in location_table:
|
||||
if location_table[locname].region in regions:
|
||||
room = location_table[locname].get_room_id(random)
|
||||
room = location_table[locname].get_random_room_id(random)
|
||||
if room is not None:
|
||||
possible_rooms[room] = location_table[locname].room_id
|
||||
return random.choice(list(possible_rooms.keys()))
|
||||
|
||||
@@ -241,4 +241,4 @@ adventure_option_definitions: Dict[str, type(Option)] = {
|
||||
"difficulty_switch_b": DifficultySwitchB,
|
||||
"start_castle": StartCastle,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
||||
for name, locdata in location_table.items():
|
||||
locdata.get_position(multiworld.random)
|
||||
|
||||
menu = Region("Menu", player, multiworld)
|
||||
|
||||
|
||||
@@ -371,8 +371,9 @@ class AdventureWorld(World):
|
||||
if location.item.player == self.player and \
|
||||
location.item.name == "nothing":
|
||||
location_data = location_table[location.name]
|
||||
room_id = location_data.get_random_room_id(self.random)
|
||||
auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id,
|
||||
location_data.room_id))
|
||||
room_id))
|
||||
# standard Adventure items, which are placed in the rom
|
||||
elif location.item.player == self.player and \
|
||||
location.item.name != "nothing" and \
|
||||
@@ -383,14 +384,18 @@ class AdventureWorld(World):
|
||||
item_ram_address = item_ram_addresses[item_table[location.item.name].table_index]
|
||||
item_position_data_start = item_position_table + item_ram_address - items_ram_start
|
||||
location_data = location_table[location.name]
|
||||
room_x, room_y = location_data.get_position(self.multiworld.per_slot_randoms[self.player])
|
||||
(room_id, room_x, room_y) = \
|
||||
location_data.get_random_position(self.random)
|
||||
if location_data.needs_bat_logic and bat_logic == 0x0:
|
||||
copied_location = copy.copy(location_data)
|
||||
copied_location.local_item = item_ram_address
|
||||
copied_location.room_id = room_id
|
||||
copied_location.room_x = room_x
|
||||
copied_location.room_y = room_y
|
||||
bat_no_touch_locs.append(copied_location)
|
||||
del unplaced_local_items[location.item.name]
|
||||
|
||||
rom_deltas[item_position_data_start] = location_data.room_id
|
||||
rom_deltas[item_position_data_start] = room_id
|
||||
rom_deltas[item_position_data_start + 1] = room_x
|
||||
rom_deltas[item_position_data_start + 2] = room_y
|
||||
local_item_to_location[item_table_offset] = self.location_name_to_id[location.name] \
|
||||
@@ -398,14 +403,20 @@ class AdventureWorld(World):
|
||||
# items from other worlds, and non-standard Adventure items handled by script, like difficulty switches
|
||||
elif location.item.code is not None:
|
||||
if location.item.code != nothing_item_id:
|
||||
location_data = location_table[location.name]
|
||||
location_data = copy.copy(location_table[location.name])
|
||||
(room_id, room_x, room_y) = \
|
||||
location_data.get_random_position(self.random)
|
||||
location_data.room_id = room_id
|
||||
location_data.room_x = room_x
|
||||
location_data.room_y = room_y
|
||||
foreign_item_locations.append(location_data)
|
||||
if location_data.needs_bat_logic and bat_logic == 0x0:
|
||||
bat_no_touch_locs.append(location_data)
|
||||
else:
|
||||
location_data = location_table[location.name]
|
||||
room_id = location_data.get_random_room_id(self.random)
|
||||
auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id,
|
||||
location_data.room_id))
|
||||
room_id))
|
||||
# Adventure items that are in another world get put in an invalid room until needed
|
||||
for unplaced_item_name, unplaced_item in unplaced_local_items.items():
|
||||
item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
|
||||
|
||||
@@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
||||
|
||||
if loc in all_state_base.events:
|
||||
all_state_base.events.remove(loc)
|
||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True,
|
||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True,
|
||||
name="LttP Dungeon Items")
|
||||
|
||||
|
||||
|
||||
@@ -23,170 +23,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
multiargs, _ = parser.parse_known_args(argv)
|
||||
|
||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('--logic', default=defval('no_glitches'), const='no_glitches', nargs='?', choices=['no_glitches', 'minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'],
|
||||
help='''\
|
||||
Select Enforcement of Item Requirements. (default: %(default)s)
|
||||
No Glitches:
|
||||
Minor Glitches: May require Fake Flippers, Bunny Revival
|
||||
and Dark Room Navigation.
|
||||
Overworld Glitches: May require overworld glitches.
|
||||
Hybrid Major Glitches: May require both overworld and underworld clipping.
|
||||
No Logic: Distribute items without regard for
|
||||
item requirements.
|
||||
''')
|
||||
parser.add_argument('--glitch_triforce', help='Allow glitching to Triforce from Ganon\'s room', action='store_true')
|
||||
parser.add_argument('--mode', default=defval('open'), const='open', nargs='?', choices=['standard', 'open', 'inverted'],
|
||||
help='''\
|
||||
Select game mode. (default: %(default)s)
|
||||
Open: World starts with Zelda rescued.
|
||||
Standard: Fixes Hyrule Castle Secret Entrance and Front Door
|
||||
but may lead to weird rain state issues if you exit
|
||||
through the Hyrule Castle side exits before rescuing
|
||||
Zelda in a full shuffle.
|
||||
Inverted: Starting locations are Dark Sanctuary in West Dark
|
||||
World or at Link's House, which is shuffled freely.
|
||||
Requires the moon pearl to be Link in the Light World
|
||||
instead of a bunny.
|
||||
''')
|
||||
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?',
|
||||
choices=['ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals', 'ganon_pedestal'],
|
||||
help='''\
|
||||
Select completion goal. (default: %(default)s)
|
||||
Ganon: Collect all crystals, beat Agahnim 2 then
|
||||
defeat Ganon.
|
||||
Crystals: Collect all crystals then defeat Ganon.
|
||||
Pedestal: Places the Triforce at the Master Sword Pedestal.
|
||||
Ganon Pedestal: Pull the Master Sword Pedestal, then defeat Ganon.
|
||||
All Dungeons: Collect all crystals, pendants, beat both
|
||||
Agahnim fights and then defeat Ganon.
|
||||
Triforce Hunt: Places 30 Triforce Pieces in the world, collect
|
||||
20 of them to beat the game.
|
||||
Local Triforce Hunt: Places 30 Triforce Pieces in your world, collect
|
||||
20 of them to beat the game.
|
||||
Ganon Triforce Hunt: Places 30 Triforce Pieces in the world, collect
|
||||
20 of them, then defeat Ganon.
|
||||
Local Ganon Triforce Hunt: Places 30 Triforce Pieces in your world,
|
||||
collect 20 of them, then defeat Ganon.
|
||||
''')
|
||||
parser.add_argument('--triforce_pieces_available', default=defval(30),
|
||||
type=lambda value: min(max(int(value), 1), 90),
|
||||
help='''Set Triforce Pieces available in item pool.''')
|
||||
parser.add_argument('--triforce_pieces_required', default=defval(20),
|
||||
type=lambda value: min(max(int(value), 1), 90),
|
||||
help='''Set Triforce Pieces required to win a Triforce Hunt''')
|
||||
parser.add_argument('--difficulty', default=defval('normal'), const='normal', nargs='?',
|
||||
choices=['easy', 'normal', 'hard', 'expert'],
|
||||
help='''\
|
||||
Select game difficulty. Affects available itempool. (default: %(default)s)
|
||||
Easy: An easier setting with some equipment duplicated and increased health.
|
||||
Normal: Normal difficulty.
|
||||
Hard: A harder setting with less equipment and reduced health.
|
||||
Expert: A harder yet setting with minimum equipment and health.
|
||||
''')
|
||||
parser.add_argument('--item_functionality', default=defval('normal'), const='normal', nargs='?',
|
||||
choices=['easy', 'normal', 'hard', 'expert'],
|
||||
help='''\
|
||||
Select limits on item functionality to increase difficulty. (default: %(default)s)
|
||||
Easy: Easy functionality. (Medallions usable without sword)
|
||||
Normal: Normal functionality.
|
||||
Hard: Reduced functionality.
|
||||
Expert: Greatly reduced functionality.
|
||||
''')
|
||||
parser.add_argument('--timer', default=defval('none'), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'],
|
||||
help='''\
|
||||
Select game timer setting. Affects available itempool. (default: %(default)s)
|
||||
None: No timer.
|
||||
Display: Displays a timer but does not affect
|
||||
the itempool.
|
||||
Timed: Starts with clock at zero. Green Clocks
|
||||
subtract 4 minutes (Total: 20), Blue Clocks
|
||||
subtract 2 minutes (Total: 10), Red Clocks add
|
||||
2 minutes (Total: 10). Winner is player with
|
||||
lowest time at the end.
|
||||
Timed OHKO: Starts clock at 10 minutes. Green Clocks add
|
||||
5 minutes (Total: 25). As long as clock is at 0,
|
||||
Link will die in one hit.
|
||||
OHKO: Like Timed OHKO, but no clock items are present
|
||||
and the clock is permenantly at zero.
|
||||
Timed Countdown: Starts with clock at 40 minutes. Same clocks as
|
||||
Timed mode. If time runs out, you lose (but can
|
||||
still keep playing).
|
||||
''')
|
||||
parser.add_argument('--countdown_start_time', default=defval(10), type=int,
|
||||
help='''Set amount of time, in minutes, to start with in Timed Countdown and Timed OHKO modes''')
|
||||
parser.add_argument('--red_clock_time', default=defval(-2), type=int,
|
||||
help='''Set amount of time, in minutes, to add from picking up red clocks; negative removes time instead''')
|
||||
parser.add_argument('--blue_clock_time', default=defval(2), type=int,
|
||||
help='''Set amount of time, in minutes, to add from picking up blue clocks; negative removes time instead''')
|
||||
parser.add_argument('--green_clock_time', default=defval(4), type=int,
|
||||
help='''Set amount of time, in minutes, to add from picking up green clocks; negative removes time instead''')
|
||||
parser.add_argument('--dungeon_counters', default=defval('default'), const='default', nargs='?', choices=['default', 'on', 'pickup', 'off'],
|
||||
help='''\
|
||||
Select dungeon counter display settings. (default: %(default)s)
|
||||
(Note, since timer takes up the same space on the hud as dungeon
|
||||
counters, timer settings override dungeon counter settings.)
|
||||
Default: Dungeon counters only show when the compass is
|
||||
picked up, or otherwise sent, only when compass
|
||||
shuffle is turned on.
|
||||
On: Dungeon counters are always displayed.
|
||||
Pickup: Dungeon counters are shown when the compass is
|
||||
picked up, even when compass shuffle is turned
|
||||
off.
|
||||
Off: Dungeon counters are never shown.
|
||||
''')
|
||||
|
||||
parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?',
|
||||
choices=['freshness', 'flood', 'vt25', 'vt26', 'balanced'],
|
||||
help='''\
|
||||
Select item filling algorithm. (default: %(default)s
|
||||
balanced: vt26 derivitive that aims to strike a balance between
|
||||
the overworld heavy vt25 and the dungeon heavy vt26
|
||||
algorithm.
|
||||
vt26: Shuffle items and place them in a random location
|
||||
that it is not impossible to be in. This includes
|
||||
dungeon keys and items.
|
||||
vt25: Shuffle items and place them in a random location
|
||||
that it is not impossible to be in.
|
||||
Flood: Push out items starting from Link\'s House and
|
||||
slightly biased to placing progression items with
|
||||
less restrictions.
|
||||
''')
|
||||
parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed'],
|
||||
help='''\
|
||||
Select Entrance Shuffling Algorithm. (default: %(default)s)
|
||||
Full: Mix cave and dungeon entrances freely while limiting
|
||||
multi-entrance caves to one world.
|
||||
Simple: Shuffle Dungeon Entrances/Exits between each other
|
||||
and keep all 4-entrance dungeons confined to one
|
||||
location. All caves outside of death mountain are
|
||||
shuffled in pairs and matched by original type.
|
||||
Restricted: Use Dungeons shuffling from Simple but freely
|
||||
connect remaining entrances.
|
||||
Crossed: Mix cave and dungeon entrances freely while allowing
|
||||
caves to cross between worlds.
|
||||
Insanity: Decouple entrances and exits from each other and
|
||||
shuffle them freely. Caves that used to be single
|
||||
entrance will still exit to the same location from
|
||||
which they are entered.
|
||||
Vanilla: All entrances are in the same locations they were
|
||||
in the base game.
|
||||
Legacy shuffles preserve behavior from older versions of the
|
||||
entrance randomizer including significant technical limitations.
|
||||
The dungeon variants only mix up dungeons and keep the rest of
|
||||
the overworld vanilla.
|
||||
''')
|
||||
parser.add_argument('--open_pyramid', default=defval('auto'), help='''\
|
||||
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it.
|
||||
Depending on goal, you might still need to beat Agahnim 2 in order to beat ganon.
|
||||
fast ganon goals are crystals, ganon_triforce_hunt, local_ganon_triforce_hunt, pedestalganon
|
||||
auto - Only opens pyramid hole if the goal specifies a fast ganon, and entrance shuffle
|
||||
is vanilla, dungeons_simple or dungeons_full.
|
||||
goal - Opens pyramid hole if the goal specifies a fast ganon.
|
||||
yes - Always opens the pyramid hole.
|
||||
no - Never opens the pyramid hole.
|
||||
''', choices=['auto', 'goal', 'yes', 'no'])
|
||||
|
||||
parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||
parser.add_argument('--count', help='''\
|
||||
Use to batch generate multiple seeds with same settings.
|
||||
@@ -195,16 +32,6 @@ def parse_arguments(argv, no_defaults=False):
|
||||
--seed given will produce the same 10 (different) roms each
|
||||
time).
|
||||
''', type=int)
|
||||
|
||||
parser.add_argument('--custom', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
|
||||
# included for backwards compatibility
|
||||
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(True))
|
||||
parser.add_argument('--no-shuffleganon', help='''\
|
||||
If set, the Pyramid Hole and Ganon's Tower are not
|
||||
included entrance shuffle pool.
|
||||
''', action='store_false', dest='shuffleganon')
|
||||
|
||||
parser.add_argument('--sprite', help='''\
|
||||
Path to a sprite sheet to use for Link. Needs to be in
|
||||
binary format and have a length of 0x7000 (28672) bytes,
|
||||
@@ -212,35 +39,12 @@ def parse_arguments(argv, no_defaults=False):
|
||||
Alternatively, can be a ALttP Rom patched with a Link
|
||||
sprite that will be extracted.
|
||||
''')
|
||||
|
||||
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos',
|
||||
"singularity"])
|
||||
|
||||
parser.add_argument('--enemy_health', default=defval('default'),
|
||||
choices=['default', 'easy', 'normal', 'hard', 'expert'])
|
||||
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--beemizer_total_chance', default=defval(0), type=lambda value: min(max(int(value), 0), 100))
|
||||
parser.add_argument('--beemizer_trap_chance', default=defval(0), type=lambda value: min(max(int(value), 0), 100))
|
||||
parser.add_argument('--shop_shuffle', default='', help='''\
|
||||
combine letters for options:
|
||||
g: generate default inventories for light and dark world shops, and unique shops
|
||||
f: generate default inventories for each shop individually
|
||||
i: shuffle the default inventories of the shops around
|
||||
p: randomize the prices of the items in shop inventories
|
||||
u: shuffle capacity upgrades into the item pool
|
||||
w: consider witch's hut like any other shop and shuffle/randomize it too
|
||||
''')
|
||||
parser.add_argument('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb'])
|
||||
parser.add_argument('--sprite_pool', help='''\
|
||||
Specifies a colon separated list of sprites used for random/randomonevent. If not specified, the full sprite pool is used.''')
|
||||
parser.add_argument('--dark_room_logic', default=('Lamp'), choices=["lamp", "torches", "none"], help='''\
|
||||
For unlit dark rooms, require the Lamp to be considered in logic by default.
|
||||
Torches means additionally easily accessible Torches that can be lit with Fire Rod are considered doable.
|
||||
None means full traversal through dark rooms without tools is considered doable.''')
|
||||
parser.add_argument('--multi', default=defval(1), type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--names', default=defval(''))
|
||||
parser.add_argument('--outputpath')
|
||||
parser.add_argument('--game', default="A Link to the Past")
|
||||
parser.add_argument('--game', default="Archipelago")
|
||||
parser.add_argument('--race', default=defval(False), action='store_true')
|
||||
parser.add_argument('--outputname')
|
||||
if multiargs.multi:
|
||||
@@ -249,43 +53,21 @@ def parse_arguments(argv, no_defaults=False):
|
||||
|
||||
ret = parser.parse_args(argv)
|
||||
|
||||
# shuffle medallions
|
||||
|
||||
ret.required_medallions = ("random", "random")
|
||||
# cannot be set through CLI currently
|
||||
ret.plando_items = []
|
||||
ret.plando_texts = {}
|
||||
ret.plando_connections = []
|
||||
|
||||
if ret.timer == "none":
|
||||
ret.timer = False
|
||||
if ret.dungeon_counters == 'on':
|
||||
ret.dungeon_counters = True
|
||||
elif ret.dungeon_counters == 'off':
|
||||
ret.dungeon_counters = False
|
||||
|
||||
if multiargs.multi:
|
||||
defaults = copy.deepcopy(ret)
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
|
||||
|
||||
for name in ['logic', 'mode', 'goal', 'difficulty', 'item_functionality',
|
||||
'shuffle', 'open_pyramid', 'timer',
|
||||
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
|
||||
'beemizer_total_chance', 'beemizer_trap_chance',
|
||||
'shufflebosses', 'enemy_health', 'enemy_damage',
|
||||
'sprite',
|
||||
"triforce_pieces_available",
|
||||
"triforce_pieces_required", "shop_shuffle",
|
||||
"required_medallions",
|
||||
"plando_items", "plando_texts", "plando_connections",
|
||||
'dungeon_counters',
|
||||
'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
||||
'game']:
|
||||
for name in ["plando_items", "plando_texts", "plando_connections", "game", "sprite", "sprite_pool"]:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
else:
|
||||
getattr(ret, name)[player] = value
|
||||
|
||||
return ret
|
||||
return ret
|
||||
|
||||
@@ -554,19 +554,20 @@ def link_entrances(world, player):
|
||||
|
||||
# check for swamp palace fix
|
||||
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
|
||||
world.swamp_patch_required[player] = True
|
||||
world.worlds[player].swamp_patch_required = True
|
||||
|
||||
# check for potion shop location
|
||||
if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop':
|
||||
world.powder_patch_required[player] = True
|
||||
world.worlds[player].powder_patch_required = True
|
||||
|
||||
# check for ganon location
|
||||
if world.get_entrance('Pyramid Hole', player).connected_region.name != 'Pyramid':
|
||||
world.ganon_at_pyramid[player] = False
|
||||
world.worlds[player].ganon_at_pyramid = False
|
||||
|
||||
# check for Ganon's Tower location
|
||||
if world.get_entrance('Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)':
|
||||
world.ganonstower_vanilla[player] = False
|
||||
world.worlds[player].ganonstower_vanilla = False
|
||||
|
||||
|
||||
def link_inverted_entrances(world, player):
|
||||
# Link's house shuffled freely, Houlihan set in mandatory_connections
|
||||
@@ -1261,19 +1262,19 @@ def link_inverted_entrances(world, player):
|
||||
|
||||
# patch swamp drain
|
||||
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
|
||||
world.swamp_patch_required[player] = True
|
||||
world.worlds[player].swamp_patch_required = True
|
||||
|
||||
# check for potion shop location
|
||||
if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop':
|
||||
world.powder_patch_required[player] = True
|
||||
world.worlds[player].powder_patch_required = True
|
||||
|
||||
# check for ganon location
|
||||
if world.get_entrance('Inverted Pyramid Hole', player).connected_region.name != 'Pyramid':
|
||||
world.ganon_at_pyramid[player] = False
|
||||
world.worlds[player].ganon_at_pyramid = False
|
||||
|
||||
# check for Ganon's Tower location
|
||||
if world.get_entrance('Inverted Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)':
|
||||
world.ganonstower_vanilla[player] = False
|
||||
world.worlds[player].ganonstower_vanilla = False
|
||||
|
||||
|
||||
def connect_simple(world, exitname, regionname, player):
|
||||
|
||||
@@ -238,7 +238,7 @@ def generate_itempool(world):
|
||||
raise NotImplementedError(f"Timer {multiworld.timer[player]} for player {player}")
|
||||
|
||||
if multiworld.timer[player] in ['ohko', 'timed_ohko']:
|
||||
multiworld.can_take_damage[player] = False
|
||||
world.can_take_damage = False
|
||||
if multiworld.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
|
||||
multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Nothing', world), False)
|
||||
else:
|
||||
@@ -276,13 +276,14 @@ def generate_itempool(world):
|
||||
|
||||
# set up item pool
|
||||
additional_triforce_pieces = 0
|
||||
treasure_hunt_total = 0
|
||||
if multiworld.custom:
|
||||
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count,
|
||||
treasure_hunt_icon) = make_custom_item_pool(multiworld, player)
|
||||
pool, placed_items, precollected_items, clock_mode, treasure_hunt_required = (
|
||||
make_custom_item_pool(multiworld, player))
|
||||
multiworld.rupoor_cost = min(multiworld.customitemarray[67], 9999)
|
||||
else:
|
||||
pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, \
|
||||
treasure_hunt_icon, additional_triforce_pieces = get_pool_core(multiworld, player)
|
||||
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_required, treasure_hunt_total,
|
||||
additional_triforce_pieces) = get_pool_core(multiworld, player)
|
||||
|
||||
for item in precollected_items:
|
||||
multiworld.push_precollected(item_factory(item, world))
|
||||
@@ -317,11 +318,11 @@ def generate_itempool(world):
|
||||
'Bomb Upgrade (50)', 'Cane of Somaria', 'Cane of Byrna'] and multiworld.enemy_health[player] not in ['default', 'easy']):
|
||||
if multiworld.bombless_start[player] and "Bomb Upgrade" not in placed_items["Link's Uncle"]:
|
||||
if 'Bow' in placed_items["Link's Uncle"]:
|
||||
multiworld.escape_assist[player].append('arrows')
|
||||
multiworld.worlds[player].escape_assist.append('arrows')
|
||||
elif 'Cane' in placed_items["Link's Uncle"]:
|
||||
multiworld.escape_assist[player].append('magic')
|
||||
multiworld.worlds[player].escape_assist.append('magic')
|
||||
else:
|
||||
multiworld.escape_assist[player].append('bombs')
|
||||
multiworld.worlds[player].escape_assist.append('bombs')
|
||||
|
||||
for (location, item) in placed_items.items():
|
||||
multiworld.get_location(location, player).place_locked_item(item_factory(item, world))
|
||||
@@ -334,13 +335,11 @@ def generate_itempool(world):
|
||||
item.code = 0x65 # Progressive Bow (Alt)
|
||||
break
|
||||
|
||||
if clock_mode is not None:
|
||||
multiworld.clock_mode[player] = clock_mode
|
||||
if clock_mode:
|
||||
world.clock_mode = clock_mode
|
||||
|
||||
if treasure_hunt_count is not None:
|
||||
multiworld.treasure_hunt_count[player] = treasure_hunt_count % 999
|
||||
if treasure_hunt_icon is not None:
|
||||
multiworld.treasure_hunt_icon[player] = treasure_hunt_icon
|
||||
multiworld.worlds[player].treasure_hunt_required = treasure_hunt_required % 999
|
||||
multiworld.worlds[player].treasure_hunt_total = treasure_hunt_total
|
||||
|
||||
dungeon_items = [item for item in get_dungeon_item_pool_player(world)
|
||||
if item.name not in multiworld.worlds[player].dungeon_local_item_names]
|
||||
@@ -369,7 +368,7 @@ def generate_itempool(world):
|
||||
elif "Small" in key_data[3] and multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
||||
# key drop shuffle and universal keys are on. Add universal keys in place of key drop keys.
|
||||
multiworld.itempool.append(item_factory(GetBeemizerItem(multiworld, player, 'Small Key (Universal)'), world))
|
||||
dungeon_item_replacements = sum(difficulties[multiworld.difficulty[player]].extras, []) * 2
|
||||
dungeon_item_replacements = sum(difficulties[world.options.item_pool.current_key].extras, []) * 2
|
||||
multiworld.random.shuffle(dungeon_item_replacements)
|
||||
|
||||
for x in range(len(dungeon_items)-1, -1, -1):
|
||||
@@ -464,8 +463,6 @@ def generate_itempool(world):
|
||||
while len(items) > pool_count:
|
||||
items_were_cut = False
|
||||
for reduce_item in items_reduction_table:
|
||||
if len(items) <= pool_count:
|
||||
break
|
||||
if len(reduce_item) == 2:
|
||||
items_were_cut = items_were_cut or cut_item(items, *reduce_item)
|
||||
elif len(reduce_item) == 4:
|
||||
@@ -477,7 +474,10 @@ def generate_itempool(world):
|
||||
items.remove(bottle)
|
||||
removed_filler.append(bottle)
|
||||
items_were_cut = True
|
||||
assert items_were_cut, f"Failed to limit item pool size for player {player}"
|
||||
if items_were_cut:
|
||||
break
|
||||
else:
|
||||
raise Exception(f"Failed to limit item pool size for player {player}")
|
||||
if len(items) < pool_count:
|
||||
items += removed_filler[len(items) - pool_count:]
|
||||
|
||||
@@ -498,8 +498,8 @@ def generate_itempool(world):
|
||||
for i in range(4):
|
||||
next(adv_heart_pieces).classification = ItemClassification.progression
|
||||
|
||||
multiworld.required_medallions[player] = (multiworld.misery_mire_medallion[player].current_key.title(),
|
||||
multiworld.turtle_rock_medallion[player].current_key.title())
|
||||
world.required_medallions = (multiworld.misery_mire_medallion[player].current_key.title(),
|
||||
multiworld.turtle_rock_medallion[player].current_key.title())
|
||||
|
||||
place_bosses(world)
|
||||
|
||||
@@ -591,9 +591,9 @@ def get_pool_core(world, player: int):
|
||||
pool = []
|
||||
placed_items = {}
|
||||
precollected_items = []
|
||||
clock_mode = None
|
||||
treasure_hunt_count = None
|
||||
treasure_hunt_icon = None
|
||||
clock_mode: str = ""
|
||||
treasure_hunt_required: int = 0
|
||||
treasure_hunt_total: int = 0
|
||||
|
||||
diff = difficulties[difficulty]
|
||||
pool.extend(diff.alwaysitems)
|
||||
@@ -682,21 +682,21 @@ def get_pool_core(world, player: int):
|
||||
if 'triforce_hunt' in goal:
|
||||
|
||||
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
|
||||
triforce_pieces = world.triforce_pieces_available[player].value + world.triforce_pieces_extra[player].value
|
||||
treasure_hunt_total = (world.triforce_pieces_available[player].value
|
||||
+ world.triforce_pieces_extra[player].value)
|
||||
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
|
||||
percentage = float(max(100, world.triforce_pieces_percentage[player].value)) / 100
|
||||
triforce_pieces = int(round(world.triforce_pieces_required[player].value * percentage, 0))
|
||||
percentage = float(world.triforce_pieces_percentage[player].value) / 100
|
||||
treasure_hunt_total = int(round(world.triforce_pieces_required[player].value * percentage, 0))
|
||||
else: # available
|
||||
triforce_pieces = world.triforce_pieces_available[player].value
|
||||
treasure_hunt_total = world.triforce_pieces_available[player].value
|
||||
|
||||
triforce_pieces = max(triforce_pieces, world.triforce_pieces_required[player].value)
|
||||
triforce_pieces = min(90, max(treasure_hunt_total, world.triforce_pieces_required[player].value))
|
||||
|
||||
pieces_in_core = min(extraitems, triforce_pieces)
|
||||
additional_pieces_to_place = triforce_pieces - pieces_in_core
|
||||
pool.extend(["Triforce Piece"] * pieces_in_core)
|
||||
extraitems -= pieces_in_core
|
||||
treasure_hunt_count = world.triforce_pieces_required[player].value
|
||||
treasure_hunt_icon = 'Triforce Piece'
|
||||
treasure_hunt_required = world.triforce_pieces_required[player].value
|
||||
|
||||
for extra in diff.extras:
|
||||
if extraitems >= len(extra):
|
||||
@@ -737,7 +737,7 @@ def get_pool_core(world, player: int):
|
||||
place_item(key_location, "Small Key (Universal)")
|
||||
pool = pool[:-3]
|
||||
|
||||
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
|
||||
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_required, treasure_hunt_total,
|
||||
additional_pieces_to_place)
|
||||
|
||||
|
||||
@@ -752,9 +752,9 @@ def make_custom_item_pool(world, player):
|
||||
pool = []
|
||||
placed_items = {}
|
||||
precollected_items = []
|
||||
clock_mode = None
|
||||
treasure_hunt_count = None
|
||||
treasure_hunt_icon = None
|
||||
clock_mode: str = ""
|
||||
treasure_hunt_required: int = 0
|
||||
treasure_hunt_total: int = 0
|
||||
|
||||
def place_item(loc, item):
|
||||
assert loc not in placed_items, "cannot place item twice"
|
||||
@@ -849,8 +849,7 @@ def make_custom_item_pool(world, player):
|
||||
if "triforce" in world.goal[player]:
|
||||
pool.extend(["Triforce Piece"] * world.triforce_pieces_available[player])
|
||||
itemtotal += world.triforce_pieces_available[player]
|
||||
treasure_hunt_count = world.triforce_pieces_required[player]
|
||||
treasure_hunt_icon = 'Triforce Piece'
|
||||
treasure_hunt_required = world.triforce_pieces_required[player]
|
||||
|
||||
if timer in ['display', 'timed', 'timed_countdown']:
|
||||
clock_mode = 'countdown' if timer == 'timed_countdown' else 'stopwatch'
|
||||
@@ -895,4 +894,4 @@ def make_custom_item_pool(world, player):
|
||||
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
|
||||
logging.warning(f"Pool was filled up with {total_items_to_place - itemtotal} Nothing's for player {player}")
|
||||
|
||||
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon)
|
||||
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_required)
|
||||
|
||||
@@ -18,7 +18,7 @@ import subprocess
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
from typing import Optional, List
|
||||
from typing import Collection, Optional, List, SupportsIndex
|
||||
|
||||
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
||||
@@ -52,7 +52,7 @@ except:
|
||||
enemizer_logger = logging.getLogger("Enemizer")
|
||||
|
||||
|
||||
class LocalRom(object):
|
||||
class LocalRom:
|
||||
|
||||
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
|
||||
self.name = name
|
||||
@@ -71,13 +71,13 @@ class LocalRom(object):
|
||||
def read_byte(self, address: int) -> int:
|
||||
return self.buffer[address]
|
||||
|
||||
def read_bytes(self, startaddress: int, length: int) -> bytes:
|
||||
def read_bytes(self, startaddress: int, length: int) -> bytearray:
|
||||
return self.buffer[startaddress:startaddress + length]
|
||||
|
||||
def write_byte(self, address: int, value: int):
|
||||
self.buffer[address] = value
|
||||
|
||||
def write_bytes(self, startaddress: int, values):
|
||||
def write_bytes(self, startaddress: int, values: Collection[SupportsIndex]) -> None:
|
||||
self.buffer[startaddress:startaddress + len(values)] = values
|
||||
|
||||
def encrypt_range(self, startaddress: int, length: int, key: bytes):
|
||||
@@ -433,7 +433,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
||||
if multiworld.key_drop_shuffle[player]:
|
||||
key_drop_enemies = {
|
||||
0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
|
||||
0x4E20A, 0x4E326, 0x4E4F7, 0x4E686, 0x4E70C, 0x4E7C8, 0x4E7FA
|
||||
0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
|
||||
}
|
||||
for enemy in key_drop_enemies:
|
||||
if rom.read_byte(enemy) == 0x12:
|
||||
@@ -945,22 +945,22 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_bytes(0x118C64, [first_bot, mid_bot, last_bot])
|
||||
|
||||
# patch medallion requirements
|
||||
if world.required_medallions[player][0] == 'Bombos':
|
||||
if local_world.required_medallions[0] == 'Bombos':
|
||||
rom.write_byte(0x180022, 0x00) # requirement
|
||||
rom.write_byte(0x4FF2, 0x31) # sprite
|
||||
rom.write_byte(0x50D1, 0x80)
|
||||
rom.write_byte(0x51B0, 0x00)
|
||||
elif world.required_medallions[player][0] == 'Quake':
|
||||
elif local_world.required_medallions[0] == 'Quake':
|
||||
rom.write_byte(0x180022, 0x02) # requirement
|
||||
rom.write_byte(0x4FF2, 0x31) # sprite
|
||||
rom.write_byte(0x50D1, 0x88)
|
||||
rom.write_byte(0x51B0, 0x00)
|
||||
if world.required_medallions[player][1] == 'Bombos':
|
||||
if local_world.required_medallions[1] == 'Bombos':
|
||||
rom.write_byte(0x180023, 0x00) # requirement
|
||||
rom.write_byte(0x5020, 0x31) # sprite
|
||||
rom.write_byte(0x50FF, 0x90)
|
||||
rom.write_byte(0x51DE, 0x00)
|
||||
elif world.required_medallions[player][1] == 'Ether':
|
||||
elif local_world.required_medallions[1] == 'Ether':
|
||||
rom.write_byte(0x180023, 0x01) # requirement
|
||||
rom.write_byte(0x5020, 0x31) # sprite
|
||||
rom.write_byte(0x50FF, 0x98)
|
||||
@@ -1069,7 +1069,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
# Byrna residual magic cost
|
||||
rom.write_bytes(0x45C42, [0x04, 0x02, 0x01])
|
||||
|
||||
difficulty = world.difficulty_requirements[player]
|
||||
difficulty = local_world.difficulty_requirements
|
||||
|
||||
# Set overflow items for progressive equipment
|
||||
rom.write_bytes(0x180090,
|
||||
@@ -1240,17 +1240,17 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0x180044, 0x01) # hammer activates tablets
|
||||
|
||||
# set up clocks for timed modes
|
||||
if world.clock_mode[player] in ['ohko', 'countdown-ohko']:
|
||||
if local_world.clock_mode in ['ohko', 'countdown-ohko']:
|
||||
rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality
|
||||
elif world.clock_mode[player] == 'stopwatch':
|
||||
elif local_world.clock_mode == 'stopwatch':
|
||||
rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode
|
||||
elif world.clock_mode[player] == 'countdown':
|
||||
elif local_world.clock_mode == 'countdown':
|
||||
rom.write_bytes(0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available
|
||||
else:
|
||||
rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode
|
||||
|
||||
# Set up requested clock settings
|
||||
if world.clock_mode[player] in ['countdown-ohko', 'stopwatch', 'countdown']:
|
||||
if local_world.clock_mode in ['countdown-ohko', 'stopwatch', 'countdown']:
|
||||
rom.write_int32(0x180200,
|
||||
world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32)
|
||||
rom.write_int32(0x180204,
|
||||
@@ -1263,14 +1263,14 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_int32(0x180208, 0) # green clock adjustment time (in frames, sint32)
|
||||
|
||||
# Set up requested start time for countdown modes
|
||||
if world.clock_mode[player] in ['countdown-ohko', 'countdown']:
|
||||
if local_world.clock_mode in ['countdown-ohko', 'countdown']:
|
||||
rom.write_int32(0x18020C, world.countdown_start_time[player] * 60 * 60) # starting time (in frames, sint32)
|
||||
else:
|
||||
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
|
||||
|
||||
# set up goals for treasure hunt
|
||||
rom.write_int16(0x180163, world.treasure_hunt_count[player])
|
||||
rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28])
|
||||
rom.write_int16(0x180163, local_world.treasure_hunt_required)
|
||||
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
|
||||
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
|
||||
|
||||
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
|
||||
@@ -1283,14 +1283,14 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0x180211, gametype) # Game type
|
||||
|
||||
# assorted fixes
|
||||
rom.write_byte(0x1800A2, 0x01 if world.fix_fake_world[
|
||||
player] else 0x00) # Toggle whether to be in real/fake dark world when dying in a DW dungeon before killing aga1
|
||||
# Toggle whether to be in real/fake dark world when dying in a DW dungeon before killing aga1
|
||||
rom.write_byte(0x1800A2, 0x01 if local_world.fix_fake_world else 0x00)
|
||||
# Lock or unlock aga tower door during escape sequence.
|
||||
rom.write_byte(0x180169, 0x00)
|
||||
if world.mode[player] == 'inverted':
|
||||
rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted
|
||||
rom.write_byte(0x180171,
|
||||
0x01 if world.ganon_at_pyramid[player] else 0x00) # Enable respawning on pyramid after ganon death
|
||||
0x01 if local_world.ganon_at_pyramid else 0x00) # Enable respawning on pyramid after ganon death
|
||||
rom.write_byte(0x180173, 0x01) # Bob is enabled
|
||||
rom.write_byte(0x180168, 0x08) # Spike Cave Damage
|
||||
rom.write_bytes(0x18016B, [0x04, 0x02, 0x01]) # Set spike cave and MM spike room Cape usage
|
||||
@@ -1306,7 +1306,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness
|
||||
rom.write_byte(0x1800A0, 0x01) # return to light world on s+q without mirror
|
||||
rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp
|
||||
rom.write_byte(0x180174, 0x01 if world.fix_fake_world[player] else 0x00)
|
||||
rom.write_byte(0x180174, 0x01 if local_world.fix_fake_world else 0x00)
|
||||
rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles
|
||||
|
||||
# Starting equipment
|
||||
@@ -1448,7 +1448,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
for address in keys[item.name]:
|
||||
equip[address] = min(equip[address] + 1, 99)
|
||||
elif item.name in bottles:
|
||||
if equip[0x34F] < world.difficulty_requirements[player].progressive_bottle_limit:
|
||||
if equip[0x34F] < local_world.difficulty_requirements.progressive_bottle_limit:
|
||||
equip[0x35C + equip[0x34F]] = bottles[item.name]
|
||||
equip[0x34F] += 1
|
||||
elif item.name in rupees:
|
||||
@@ -1507,9 +1507,9 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_bytes(0x180080,
|
||||
[50, 50, 70, 70]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10)
|
||||
|
||||
rom.write_byte(0x18004D, ((0x01 if 'arrows' in world.escape_assist[player] else 0x00) |
|
||||
(0x02 if 'bombs' in world.escape_assist[player] else 0x00) |
|
||||
(0x04 if 'magic' in world.escape_assist[player] else 0x00))) # Escape assist
|
||||
rom.write_byte(0x18004D, ((0x01 if 'arrows' in local_world.escape_assist else 0x00) |
|
||||
(0x02 if 'bombs' in local_world.escape_assist else 0x00) |
|
||||
(0x04 if 'magic' in local_world.escape_assist else 0x00))) # Escape assist
|
||||
|
||||
if world.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
|
||||
rom.write_byte(0x18003E, 0x01) # make ganon invincible
|
||||
@@ -1546,7 +1546,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
|
||||
|
||||
# compasses showing dungeon count
|
||||
if world.clock_mode[player] or not world.dungeon_counters[player]:
|
||||
if local_world.clock_mode or not world.dungeon_counters[player]:
|
||||
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
|
||||
elif world.dungeon_counters[player] is True:
|
||||
rom.write_byte(0x18003C, 0x02) # always on
|
||||
@@ -1616,7 +1616,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0xEFD95, digging_game_rng)
|
||||
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
|
||||
rom.write_byte(0x1800A4, 0x01 if world.glitches_required[player] != 'no_logic' else 0x00) # enable POD EG fix
|
||||
rom.write_byte(0x186383, 0x01 if world.glitch_triforce or world.glitches_required[
|
||||
rom.write_byte(0x186383, 0x01 if world.glitches_required[
|
||||
player] == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room
|
||||
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
|
||||
|
||||
@@ -1653,13 +1653,13 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
|
||||
|
||||
# patch swamp: Need to enable permanent drain of water as dam or swamp were moved
|
||||
rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00)
|
||||
rom.write_byte(0x18003D, 0x01 if local_world.swamp_patch_required else 0x00)
|
||||
|
||||
# powder patch: remove the need to leave the screen after powder, since it causes problems for potion shop at race game
|
||||
# temporarally we are just nopping out this check we will conver this to a rom fix soon.
|
||||
rom.write_bytes(0x02F539,
|
||||
[0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0,
|
||||
0x4F])
|
||||
[0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if local_world.powder_patch_required else [
|
||||
0xAD, 0xBF, 0x0A, 0xF0, 0x4F])
|
||||
|
||||
# allow smith into multi-entrance caves in appropriate shuffles
|
||||
if world.entrance_shuffle[player] in ['restricted', 'full', 'crossed', 'insanity'] or (
|
||||
@@ -1859,7 +1859,7 @@ def apply_oof_sfx(rom, oof: str):
|
||||
rom.write_bytes(0x12803A, oof_bytes)
|
||||
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
|
||||
|
||||
#Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
|
||||
# Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
|
||||
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
|
||||
|
||||
|
||||
@@ -2421,7 +2421,7 @@ def write_strings(rom, world, player):
|
||||
' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!'
|
||||
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
if world.worlds[player].has_progressive_bows and (world.difficulty_requirements[player].progressive_bow_limit >= 2 or (
|
||||
if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
|
||||
world.swordless[player] or world.glitches_required[player] == 'no_glitches')):
|
||||
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
|
||||
world.per_slot_randoms[player].shuffle(prog_bow_locs)
|
||||
@@ -2482,16 +2482,16 @@ def write_strings(rom, world, player):
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
|
||||
else:
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
|
||||
if world.treasure_hunt_count[player] > 1:
|
||||
if w.treasure_hunt_required > 1:
|
||||
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
|
||||
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
|
||||
"hidden in a hollow tree. If you bring\n%d Triforce pieces out of %d, I can reassemble it." % \
|
||||
(world.treasure_hunt_count[player], world.triforce_pieces_available[player])
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
else:
|
||||
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
|
||||
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
|
||||
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
|
||||
(world.treasure_hunt_count[player], world.triforce_pieces_available[player])
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
elif world.goal[player] in ['pedestal']:
|
||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
|
||||
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
||||
@@ -2500,20 +2500,20 @@ def write_strings(rom, world, player):
|
||||
tt['ganon_fall_in'] = Ganon1_texts[local_random.randint(0, len(Ganon1_texts) - 1)]
|
||||
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
|
||||
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
|
||||
if world.treasure_hunt_count[player] > 1:
|
||||
if w.treasure_hunt_required > 1:
|
||||
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
|
||||
(world.treasure_hunt_count[player], world.triforce_pieces_available[player])
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
|
||||
(world.treasure_hunt_count[player], world.triforce_pieces_available[player])
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
else:
|
||||
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
|
||||
(world.treasure_hunt_count[player], world.triforce_pieces_available[player])
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
|
||||
(world.treasure_hunt_count[player], world.triforce_pieces_available[player])
|
||||
(w.treasure_hunt_required, w.treasure_hunt_total)
|
||||
|
||||
tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)]
|
||||
|
||||
@@ -3021,7 +3021,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options = Utils.get_options()
|
||||
options = Utils.get_settings()
|
||||
if not file_name:
|
||||
file_name = options["lttp_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
|
||||
@@ -18,7 +18,8 @@ from .StateHelpers import (can_extend_magic, can_kill_most_things,
|
||||
can_shoot_arrows, has_beam_sword, has_crystals,
|
||||
has_fire_source, has_hearts, has_melee_weapon,
|
||||
has_misery_mire_medallion, has_sword, has_turtle_rock_medallion,
|
||||
has_triforce_pieces, can_use_bombs, can_bomb_or_bonk)
|
||||
has_triforce_pieces, can_use_bombs, can_bomb_or_bonk,
|
||||
can_activate_crystal_switch)
|
||||
from .UnderworldGlitchRules import underworld_glitches_rules
|
||||
|
||||
|
||||
@@ -63,20 +64,24 @@ def set_rules(world):
|
||||
|
||||
if world.glitches_required[player] == 'no_glitches':
|
||||
no_glitches_rules(world, player)
|
||||
forbid_bomb_jump_requirements(world, player)
|
||||
elif world.glitches_required[player] == 'overworld_glitches':
|
||||
# Initially setting no_glitches_rules to set the baseline rules for some
|
||||
# entrances. The overworld_glitches_rules set is primarily additive.
|
||||
no_glitches_rules(world, player)
|
||||
fake_flipper_rules(world, player)
|
||||
overworld_glitches_rules(world, player)
|
||||
forbid_bomb_jump_requirements(world, player)
|
||||
elif world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
|
||||
no_glitches_rules(world, player)
|
||||
fake_flipper_rules(world, player)
|
||||
overworld_glitches_rules(world, player)
|
||||
underworld_glitches_rules(world, player)
|
||||
bomb_jump_requirements(world, player)
|
||||
elif world.glitches_required[player] == 'minor_glitches':
|
||||
no_glitches_rules(world, player)
|
||||
fake_flipper_rules(world, player)
|
||||
forbid_bomb_jump_requirements(world, player)
|
||||
else:
|
||||
raise NotImplementedError(f'Not implemented yet: Logic - {world.glitches_required[player]}')
|
||||
|
||||
@@ -97,7 +102,7 @@ def set_rules(world):
|
||||
|
||||
# if swamp and dam have not been moved we require mirror for swamp palace
|
||||
# however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself.
|
||||
if not world.swamp_patch_required[player] and world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
||||
if not world.worlds[player].swamp_patch_required and world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
||||
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
|
||||
|
||||
# GT Entrance may be required for Turtle Rock for OWG and < 7 required
|
||||
@@ -186,247 +191,254 @@ def dungeon_boss_rules(world, player):
|
||||
set_defeat_dungeon_boss_rule(world.get_location(location, player))
|
||||
|
||||
|
||||
def global_rules(world, player):
|
||||
def global_rules(multiworld: MultiWorld, player: int):
|
||||
world = multiworld.worlds[player]
|
||||
# ganon can only carry triforce
|
||||
add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player)
|
||||
add_item_rule(multiworld.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player)
|
||||
# dungeon prizes can only be crystals/pendants
|
||||
crystals_and_pendants: Set[str] = \
|
||||
{item for item, item_data in item_table.items() if item_data.type == "Crystal"}
|
||||
prize_locations: Iterator[str] = \
|
||||
(locations for locations, location_data in location_table.items() if location_data[2] == True)
|
||||
for prize_location in prize_locations:
|
||||
add_item_rule(world.get_location(prize_location, player),
|
||||
add_item_rule(multiworld.get_location(prize_location, player),
|
||||
lambda item: item.name in crystals_and_pendants and item.player == player)
|
||||
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
|
||||
for exit in world.get_region('Menu', player).exits:
|
||||
for exit in multiworld.get_region('Menu', player).exits:
|
||||
exit.hide_path = True
|
||||
try:
|
||||
old_man_sq = world.get_entrance('Old Man S&Q', player)
|
||||
old_man_sq = multiworld.get_entrance('Old Man S&Q', player)
|
||||
except KeyError:
|
||||
pass # it doesn't exist, should be dungeon-only unittests
|
||||
else:
|
||||
old_man = world.get_location("Old Man", player)
|
||||
old_man = multiworld.get_location("Old Man", player)
|
||||
set_rule(old_man_sq, lambda state: old_man.can_reach(state))
|
||||
|
||||
set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))
|
||||
set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player))
|
||||
set_rule(world.get_location('Purple Chest', player),
|
||||
set_rule(multiworld.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))
|
||||
set_rule(multiworld.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player))
|
||||
set_rule(multiworld.get_location('Purple Chest', player),
|
||||
lambda state: state.has('Pick Up Purple Chest', player)) # Can S&Q with chest
|
||||
set_rule(world.get_location('Ether Tablet', player), lambda state: can_retrieve_tablet(state, player))
|
||||
set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player))
|
||||
set_rule(multiworld.get_location('Ether Tablet', player), lambda state: can_retrieve_tablet(state, player))
|
||||
set_rule(multiworld.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player))
|
||||
|
||||
set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith
|
||||
set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player))
|
||||
set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player))
|
||||
set_rule(world.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player))
|
||||
set_rule(world.get_location('Library', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(multiworld.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith
|
||||
set_rule(multiworld.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player))
|
||||
set_rule(multiworld.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player))
|
||||
set_rule(multiworld.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player))
|
||||
set_rule(multiworld.get_location('Library', player), lambda state: state.has('Pegasus Boots', player))
|
||||
|
||||
if world.enemy_shuffle[player]:
|
||||
set_rule(world.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) and
|
||||
can_kill_most_things(state, player, 4))
|
||||
if multiworld.enemy_shuffle[player]:
|
||||
set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) and
|
||||
can_kill_most_things(state, player, 4))
|
||||
else:
|
||||
set_rule(world.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player)
|
||||
and ((state.multiworld.enemy_health[player] in ("easy", "default") and can_use_bombs(state, player, 4))
|
||||
set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player)
|
||||
and ((state.multiworld.enemy_health[player] in ("easy", "default") and can_use_bombs(state, player, 4))
|
||||
or can_shoot_arrows(state, player) or state.has("Cane of Somaria", player)
|
||||
or has_beam_sword(state, player)))
|
||||
|
||||
set_rule(world.get_location('Sahasrahla', player), lambda state: state.has('Green Pendant', player))
|
||||
set_rule(multiworld.get_location('Sahasrahla', player), lambda state: state.has('Green Pendant', player))
|
||||
|
||||
set_rule(world.get_location('Aginah\'s Cave', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Blind\'s Hideout - Top', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Chicken House', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Kakariko Well - Top', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Graveyard Cave', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Sahasrahla\'s Hut - Left', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
set_rule(world.get_location('Sahasrahla\'s Hut - Middle', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
set_rule(world.get_location('Sahasrahla\'s Hut - Right', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
set_rule(world.get_location('Paradox Cave Lower - Left', player), lambda state: can_use_bombs(state, player)
|
||||
or has_beam_sword(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Fire Rod", "Cane of Somaria"], player))
|
||||
set_rule(world.get_location('Paradox Cave Lower - Right', player), lambda state: can_use_bombs(state, player)
|
||||
or has_beam_sword(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Fire Rod", "Cane of Somaria"], player))
|
||||
set_rule(world.get_location('Paradox Cave Lower - Far Right', player), lambda state: can_use_bombs(state, player)
|
||||
or has_beam_sword(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Fire Rod", "Cane of Somaria"], player))
|
||||
set_rule(world.get_location('Paradox Cave Lower - Middle', player), lambda state: can_use_bombs(state, player)
|
||||
or has_beam_sword(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Fire Rod", "Cane of Somaria"], player))
|
||||
set_rule(world.get_location('Paradox Cave Lower - Far Left', player), lambda state: can_use_bombs(state, player)
|
||||
or has_beam_sword(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Fire Rod", "Cane of Somaria"], player))
|
||||
set_rule(world.get_location('Paradox Cave Upper - Left', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Paradox Cave Upper - Right', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Mini Moldorm Cave - Far Left', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(world.get_location('Mini Moldorm Cave - Left', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(world.get_location('Mini Moldorm Cave - Far Right', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(world.get_location('Mini Moldorm Cave - Right', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(world.get_location('Mini Moldorm Cave - Generous Guy', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(world.get_location('Hype Cave - Bottom', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Hype Cave - Middle Left', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Hype Cave - Middle Right', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Hype Cave - Top', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Light World Death Mountain Shop', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Aginah\'s Cave', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Blind\'s Hideout - Top', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Chicken House', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Kakariko Well - Top', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Graveyard Cave', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Sahasrahla\'s Hut - Left', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
set_rule(multiworld.get_location('Sahasrahla\'s Hut - Middle', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
set_rule(multiworld.get_location('Sahasrahla\'s Hut - Right', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
set_rule(multiworld.get_location('Paradox Cave Lower - Left', player), lambda state: can_use_bombs(state, player)
|
||||
or has_beam_sword(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Fire Rod", "Cane of Somaria"], player))
|
||||
set_rule(multiworld.get_location('Paradox Cave Lower - Right', player), lambda state: can_use_bombs(state, player)
|
||||
or has_beam_sword(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Fire Rod", "Cane of Somaria"], player))
|
||||
set_rule(multiworld.get_location('Paradox Cave Lower - Far Right', player), lambda state: can_use_bombs(state, player)
|
||||
or has_beam_sword(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Fire Rod", "Cane of Somaria"], player))
|
||||
set_rule(multiworld.get_location('Paradox Cave Lower - Middle', player), lambda state: can_use_bombs(state, player)
|
||||
or has_beam_sword(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Fire Rod", "Cane of Somaria"], player))
|
||||
set_rule(multiworld.get_location('Paradox Cave Lower - Far Left', player), lambda state: can_use_bombs(state, player)
|
||||
or has_beam_sword(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Fire Rod", "Cane of Somaria"], player))
|
||||
set_rule(multiworld.get_location('Paradox Cave Upper - Left', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Paradox Cave Upper - Right', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Mini Moldorm Cave - Far Left', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(multiworld.get_location('Mini Moldorm Cave - Left', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(multiworld.get_location('Mini Moldorm Cave - Far Right', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(multiworld.get_location('Mini Moldorm Cave - Right', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(multiworld.get_location('Mini Moldorm Cave - Generous Guy', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(multiworld.get_location('Hype Cave - Bottom', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Hype Cave - Middle Left', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Hype Cave - Middle Right', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Hype Cave - Top', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Light World Death Mountain Shop', player), lambda state: can_use_bombs(state, player))
|
||||
|
||||
set_rule(world.get_entrance('Two Brothers House Exit (West)', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
set_rule(world.get_entrance('Two Brothers House Exit (East)', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
set_rule(multiworld.get_entrance('Two Brothers House Exit (West)', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
set_rule(multiworld.get_entrance('Two Brothers House Exit (East)', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
|
||||
set_rule(world.get_location('Spike Cave', player), lambda state:
|
||||
set_rule(multiworld.get_location('Spike Cave', player), lambda state:
|
||||
state.has('Hammer', player) and can_lift_rocks(state, player) and
|
||||
((state.has('Cape', player) and can_extend_magic(state, player, 16, True)) or
|
||||
(state.has('Cane of Byrna', player) and
|
||||
(can_extend_magic(state, player, 12, True) or
|
||||
(state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4))))))
|
||||
(world.can_take_damage and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4))))))
|
||||
)
|
||||
|
||||
set_rule(world.get_entrance('Hookshot Cave Bomb Wall (North)', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Hookshot Cave Bomb Wall (South)', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Hookshot Cave Bomb Wall (North)', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Hookshot Cave Bomb Wall (South)', player), lambda state: can_use_bombs(state, player))
|
||||
|
||||
set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Top Left', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Bottom Right', player),
|
||||
set_rule(multiworld.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(multiworld.get_location('Hookshot Cave - Top Left', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(multiworld.get_location('Hookshot Cave - Bottom Right', player),
|
||||
lambda state: state.has('Hookshot', player) or state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(multiworld.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
|
||||
|
||||
set_rule(world.get_entrance('Sewers Door', player),
|
||||
set_rule(multiworld.get_location('Hyrule Castle - Map Guard Key Drop', player),
|
||||
lambda state: can_kill_most_things(state, player, 1))
|
||||
|
||||
set_rule(multiworld.get_entrance('Sewers Door', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or (
|
||||
world.small_key_shuffle[player] == small_key_shuffle.option_universal and world.mode[
|
||||
multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal and multiworld.mode[
|
||||
player] == 'standard')) # standard universal small keys cannot access the shop
|
||||
set_rule(world.get_entrance('Sewers Back Door', player),
|
||||
set_rule(multiworld.get_entrance('Sewers Back Door', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4))
|
||||
set_rule(world.get_entrance('Sewers Secret Room', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
set_rule(multiworld.get_entrance('Sewers Secret Room', player), lambda state: can_bomb_or_bonk(state, player))
|
||||
|
||||
set_rule(world.get_entrance('Agahnim 1', player),
|
||||
set_rule(multiworld.get_entrance('Agahnim 1', player),
|
||||
lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 4))
|
||||
|
||||
set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(world.get_location('Castle Tower - Dark Maze', player),
|
||||
set_rule(multiworld.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 4))
|
||||
set_rule(multiworld.get_location('Castle Tower - Dark Maze', player),
|
||||
lambda state: can_kill_most_things(state, player, 4) and state._lttp_has_key('Small Key (Agahnims Tower)',
|
||||
player))
|
||||
set_rule(world.get_location('Castle Tower - Dark Archer Key Drop', player),
|
||||
set_rule(multiworld.get_location('Castle Tower - Dark Archer Key Drop', player),
|
||||
lambda state: can_kill_most_things(state, player, 4) and state._lttp_has_key('Small Key (Agahnims Tower)',
|
||||
player, 2))
|
||||
set_rule(world.get_location('Castle Tower - Circle of Pots Key Drop', player),
|
||||
set_rule(multiworld.get_location('Castle Tower - Circle of Pots Key Drop', player),
|
||||
lambda state: can_kill_most_things(state, player, 4) and state._lttp_has_key('Small Key (Agahnims Tower)',
|
||||
player, 3))
|
||||
set_always_allow(world.get_location('Eastern Palace - Big Key Chest', player),
|
||||
set_always_allow(multiworld.get_location('Eastern Palace - Big Key Chest', player),
|
||||
lambda state, item: item.name == 'Big Key (Eastern Palace)' and item.player == player)
|
||||
set_rule(world.get_location('Eastern Palace - Big Key Chest', player),
|
||||
set_rule(multiworld.get_location('Eastern Palace - Big Key Chest', player),
|
||||
lambda state: can_kill_most_things(state, player, 5) and (state._lttp_has_key('Small Key (Eastern Palace)',
|
||||
player, 2) or ((location_item_name(state, 'Eastern Palace - Big Key Chest', player)
|
||||
== ('Big Key (Eastern Palace)', player) and state.has('Small Key (Eastern Palace)',
|
||||
player)))))
|
||||
set_rule(world.get_location('Eastern Palace - Dark Eyegore Key Drop', player),
|
||||
set_rule(multiworld.get_location('Eastern Palace - Dark Eyegore Key Drop', player),
|
||||
lambda state: state.has('Big Key (Eastern Palace)', player) and can_kill_most_things(state, player, 1))
|
||||
set_rule(world.get_location('Eastern Palace - Big Chest', player),
|
||||
set_rule(multiworld.get_location('Eastern Palace - Big Chest', player),
|
||||
lambda state: state.has('Big Key (Eastern Palace)', player))
|
||||
# not bothering to check for can_kill_most_things in the rooms leading to boss, as if you can kill a boss you should
|
||||
# be able to get through these rooms
|
||||
ep_boss = world.get_location('Eastern Palace - Boss', player)
|
||||
ep_boss = multiworld.get_location('Eastern Palace - Boss', player)
|
||||
add_rule(ep_boss, lambda state: state.has('Big Key (Eastern Palace)', player) and
|
||||
state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and
|
||||
ep_boss.parent_region.dungeon.boss.can_defeat(state))
|
||||
ep_prize = world.get_location('Eastern Palace - Prize', player)
|
||||
ep_prize = multiworld.get_location('Eastern Palace - Prize', player)
|
||||
add_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and
|
||||
state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and
|
||||
ep_prize.parent_region.dungeon.boss.can_defeat(state))
|
||||
if not world.enemy_shuffle[player]:
|
||||
if not multiworld.enemy_shuffle[player]:
|
||||
add_rule(ep_boss, lambda state: can_shoot_arrows(state, player))
|
||||
add_rule(ep_prize, lambda state: can_shoot_arrows(state, player))
|
||||
|
||||
# You can always kill the Stalfos' with the pots on easy/normal
|
||||
if world.enemy_health[player] in ("hard", "expert") or world.enemy_shuffle[player]:
|
||||
if multiworld.enemy_health[player] in ("hard", "expert") or multiworld.enemy_shuffle[player]:
|
||||
stalfos_rule = lambda state: can_kill_most_things(state, player, 4)
|
||||
for location in ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
|
||||
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
|
||||
'Eastern Palace - Big Key Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize']:
|
||||
add_rule(world.get_location(location, player), stalfos_rule)
|
||||
add_rule(multiworld.get_location(location, player), stalfos_rule)
|
||||
|
||||
set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
|
||||
set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(multiworld.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
|
||||
set_rule(multiworld.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
|
||||
|
||||
set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4))
|
||||
set_rule(world.get_location('Desert Palace - Big Key Chest', player), lambda state: can_kill_most_things(state, player, 3))
|
||||
set_rule(world.get_location('Desert Palace - Beamos Hall Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 2) and can_kill_most_things(state, player, 4))
|
||||
set_rule(world.get_location('Desert Palace - Desert Tiles 2 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 3) and can_kill_most_things(state, player, 4))
|
||||
add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
add_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(multiworld.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4))
|
||||
set_rule(multiworld.get_location('Desert Palace - Big Key Chest', player), lambda state: can_kill_most_things(state, player, 3))
|
||||
set_rule(multiworld.get_location('Desert Palace - Beamos Hall Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 2) and can_kill_most_things(state, player, 4))
|
||||
set_rule(multiworld.get_location('Desert Palace - Desert Tiles 2 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 3) and can_kill_most_things(state, player, 4))
|
||||
add_rule(multiworld.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
add_rule(multiworld.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
|
||||
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys
|
||||
if not (world.small_key_shuffle[player] and world.big_key_shuffle[player]):
|
||||
add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state))
|
||||
if not (multiworld.small_key_shuffle[player] and multiworld.big_key_shuffle[player]):
|
||||
add_rule(multiworld.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state))
|
||||
|
||||
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
|
||||
set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
if world.enemy_shuffle[player]:
|
||||
add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_kill_most_things(state, player, 3))
|
||||
set_rule(multiworld.get_location('Tower of Hera - Basement Cage', player), lambda state: can_activate_crystal_switch(state, player))
|
||||
set_rule(multiworld.get_location('Tower of Hera - Map Chest', player), lambda state: can_activate_crystal_switch(state, player))
|
||||
set_rule(multiworld.get_entrance('Tower of Hera Small Key Door', player), lambda state: can_activate_crystal_switch(state, player) and (state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)))
|
||||
set_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_activate_crystal_switch(state, player) and state.has('Big Key (Tower of Hera)', player))
|
||||
if multiworld.enemy_shuffle[player]:
|
||||
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_kill_most_things(state, player, 3))
|
||||
else:
|
||||
add_rule(world.get_entrance('Tower of Hera Big Key Door', player),
|
||||
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player),
|
||||
lambda state: (has_melee_weapon(state, player) or (state.has('Silver Bow', player)
|
||||
and can_shoot_arrows(state, player)) or state.has("Cane of Byrna", player)
|
||||
or state.has("Cane of Somaria", player)))
|
||||
set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
||||
set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
||||
if multiworld.accessibility[player] != 'locations':
|
||||
set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
||||
|
||||
set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
||||
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
|
||||
set_rule(world.get_location('Swamp Palace - Map Chest', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2))
|
||||
set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3))
|
||||
set_rule(world.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player))
|
||||
if world.pot_shuffle[player]:
|
||||
set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
||||
set_rule(multiworld.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
|
||||
set_rule(multiworld.get_location('Swamp Palace - Map Chest', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2))
|
||||
set_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3))
|
||||
set_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player))
|
||||
if multiworld.pot_shuffle[player]:
|
||||
# it could move the key to the top right platform which can only be reached with bombs
|
||||
add_rule(world.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)
|
||||
add_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)
|
||||
if state.has('Hookshot', player)
|
||||
else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
|
||||
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
allow_self_locking_items(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
|
||||
set_rule(world.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 world.small_key_shuffle[player] and world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
||||
forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
||||
set_rule(world.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
set_rule(world.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
if world.pot_shuffle[player]:
|
||||
set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
|
||||
if multiworld.accessibility[player] != 'locations':
|
||||
allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
|
||||
set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
|
||||
if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
||||
forbid_item(multiworld.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
||||
add_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
add_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
if multiworld.pot_shuffle[player]:
|
||||
# key can (and probably will) be moved behind bombable wall
|
||||
set_rule(world.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))
|
||||
|
||||
set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||
set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||
|
||||
if world.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
||||
set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
|
||||
if multiworld.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
||||
set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
|
||||
|
||||
set_rule(world.get_location('Thieves\' Town - Big Chest', player),
|
||||
set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player),
|
||||
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
||||
|
||||
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
||||
set_rule(world.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
||||
if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
|
||||
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
||||
|
||||
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
||||
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||
|
||||
# We need so many keys in the SW doors because they are all reachable as the last door (except for the door to mothula)
|
||||
set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
allow_self_locking_items(world.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
|
||||
add_rule(world.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
add_rule(world.get_location('Skull Woods - Boss', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
set_rule(multiworld.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
set_rule(multiworld.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
|
||||
if multiworld.accessibility[player] != 'locations':
|
||||
allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
|
||||
set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
|
||||
add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
add_rule(multiworld.get_location('Skull Woods - Boss', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||
|
||||
set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_melt_things(state, player))
|
||||
set_rule(world.get_location('Ice Palace - Compass Chest', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player))
|
||||
set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player) and can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_melt_things(state, player))
|
||||
set_rule(multiworld.get_location('Ice Palace - Compass Chest', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player))
|
||||
set_rule(multiworld.get_entrance('Ice Palace (Second Section)', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player) and can_use_bombs(state, player))
|
||||
|
||||
set_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: state._lttp_has_key('Small Key (Ice Palace)', player, 2))
|
||||
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
|
||||
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 6) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 5))))
|
||||
set_rule(multiworld.get_entrance('Ice Palace (Main)', player), lambda state: state._lttp_has_key('Small Key (Ice Palace)', player, 2))
|
||||
set_rule(multiworld.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
|
||||
set_rule(multiworld.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 6) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 5))))
|
||||
# This is a complicated rule, so let's break it down.
|
||||
# Hookshot always suffices to get to the right side.
|
||||
# Also, once you get over there, you have to cross the spikes, so that's the last line.
|
||||
@@ -436,102 +448,104 @@ def global_rules(world, player):
|
||||
# Hence if big key is available then it's 6 keys, otherwise 4 keys.
|
||||
# If key_drop is off, then we have 3 drop keys available, and can never satisfy the 6 key requirement because one key is on right side,
|
||||
# so this reduces perfectly to original logic.
|
||||
set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or
|
||||
(state._lttp_has_key('Small Key (Ice Palace)', player, 4)
|
||||
set_rule(multiworld.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or
|
||||
(state._lttp_has_key('Small Key (Ice Palace)', player, 4)
|
||||
if item_name_in_location_names(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player),
|
||||
('Ice Palace - Hammer Block Key Drop', player),
|
||||
('Ice Palace - Big Key Chest', player),
|
||||
('Ice Palace - Map Chest', player)])
|
||||
else state._lttp_has_key('Small Key (Ice Palace)', player, 6))) and
|
||||
(state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
|
||||
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player))
|
||||
else state._lttp_has_key('Small Key (Ice Palace)', player, 6))) and (
|
||||
world.can_take_damage or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
|
||||
set_rule(multiworld.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player))
|
||||
|
||||
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ...
|
||||
set_rule(world.get_location('Misery Mire - Fishbone Pot Key', player), lambda state: state.has('Big Key (Misery Mire)', player) or state._lttp_has_key('Small Key (Misery Mire)', player, 4))
|
||||
set_rule(multiworld.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ...
|
||||
set_rule(multiworld.get_location('Misery Mire - Fishbone Pot Key', player), lambda state: state.has('Big Key (Misery Mire)', player) or state._lttp_has_key('Small Key (Misery Mire)', player, 4))
|
||||
|
||||
set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player))
|
||||
set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and has_hearts(state, player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
|
||||
set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
|
||||
set_rule(multiworld.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player))
|
||||
set_rule(multiworld.get_location('Misery Mire - Spike Chest', player), lambda state: (world.can_take_damage and has_hearts(state, player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
|
||||
set_rule(multiworld.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
|
||||
# How to access crystal switch:
|
||||
# If have big key: then you will need 2 small keys to be able to hit switch and return to main area, as you can burn key in dark room
|
||||
# If not big key: cannot burn key in dark room, hence need only 1 key. all doors immediately available lead to a crystal switch.
|
||||
# The listed chests are those which can be reached if you can reach a crystal switch.
|
||||
set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2))
|
||||
set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2))
|
||||
set_rule(multiworld.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2))
|
||||
set_rule(multiworld.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2))
|
||||
# we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet
|
||||
set_rule(world.get_location('Misery Mire - Conveyor Crystal Key Drop', player),
|
||||
set_rule(multiworld.get_location('Misery Mire - Conveyor Crystal Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 4)
|
||||
if location_item_name(state, 'Misery Mire - Compass Chest', player) == ('Big Key (Misery Mire)', player) or location_item_name(state, 'Misery Mire - Big Key Chest', player) == ('Big Key (Misery Mire)', player) or location_item_name(state, 'Misery Mire - Conveyor Crystal Key Drop', player) == ('Big Key (Misery Mire)', player)
|
||||
else state._lttp_has_key('Small Key (Misery Mire)', player, 5))
|
||||
set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 5)
|
||||
set_rule(multiworld.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 5)
|
||||
if ((location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or (location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)]))
|
||||
else state._lttp_has_key('Small Key (Misery Mire)', player, 6))
|
||||
set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player))
|
||||
set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
||||
set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player) and can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player))
|
||||
set_rule(multiworld.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
||||
set_rule(multiworld.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player) and can_use_bombs(state, player))
|
||||
|
||||
set_rule(world.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('Turtle Rock Entrance Gap Reverse', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_location('Turtle Rock - Pokey 1 Key Drop', player), lambda state: can_kill_most_things(state, player, 5))
|
||||
set_rule(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), lambda state: can_kill_most_things(state, player, 5))
|
||||
set_rule(world.get_location('Turtle Rock - Compass Chest', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_location('Turtle Rock - Roller Room - Left', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
|
||||
set_rule(world.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
|
||||
set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
|
||||
set_rule(world.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10))
|
||||
set_rule(world.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player)
|
||||
or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player))
|
||||
set_rule(world.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('Turtle Rock (Dark Room) (South)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Entrance Gap Reverse', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Pokey 1 Key Drop', player), lambda state: can_kill_most_things(state, player, 5))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Pokey 2 Key Drop', player), lambda state: can_kill_most_things(state, player, 5))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Compass Chest', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Roller Room - Left', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player)
|
||||
or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (South)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(multiworld.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10))
|
||||
|
||||
if not world.worlds[player].fix_trock_doors:
|
||||
add_rule(world.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
if not multiworld.worlds[player].fix_trock_doors:
|
||||
add_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
|
||||
if world.enemy_shuffle[player]:
|
||||
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3))
|
||||
if multiworld.enemy_shuffle[player]:
|
||||
set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3))
|
||||
else:
|
||||
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_shoot_arrows(state, player))
|
||||
set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and can_shoot_arrows(state, player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
|
||||
set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: can_use_bombs(state, player) and state.has('Big Key (Palace of Darkness)', player))
|
||||
set_rule(world.get_location('Palace of Darkness - The Arena - Ledge', player), lambda state: can_use_bombs(state, player))
|
||||
if world.pot_shuffle[player]:
|
||||
set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_shoot_arrows(state, player))
|
||||
set_rule(multiworld.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(multiworld.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
|
||||
set_rule(multiworld.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and can_shoot_arrows(state, player) and state.has('Hammer', player))
|
||||
set_rule(multiworld.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
|
||||
set_rule(multiworld.get_location('Palace of Darkness - Big Chest', player), lambda state: can_use_bombs(state, player) and state.has('Big Key (Palace of Darkness)', player))
|
||||
set_rule(multiworld.get_location('Palace of Darkness - The Arena - Ledge', player), lambda state: can_use_bombs(state, player))
|
||||
if multiworld.pot_shuffle[player]:
|
||||
# chest switch may be up on ledge where bombs are required
|
||||
set_rule(world.get_location('Palace of Darkness - Stalfos Basement', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_location('Palace of Darkness - Stalfos Basement', player), lambda state: can_use_bombs(state, player))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
if multiworld.accessibility[player] != 'locations':
|
||||
set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
if multiworld.accessibility[player] != 'locations':
|
||||
set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
|
||||
set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
|
||||
|
||||
# these key rules are conservative, you might be able to get away with more lenient rules
|
||||
randomizer_room_chests = ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right']
|
||||
compass_room_chests = ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - Conveyor Star Pits Pot Key']
|
||||
back_chests = ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']
|
||||
|
||||
set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
|
||||
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
|
||||
set_rule(multiworld.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(multiworld.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(multiworld.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
|
||||
if multiworld.pot_shuffle[player]:
|
||||
set_rule(multiworld.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
|
||||
set_rule(multiworld.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
|
||||
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
|
||||
|
||||
# this seemed to be causing generation failure, disable for now
|
||||
@@ -540,63 +554,63 @@ def global_rules(world, player):
|
||||
|
||||
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
|
||||
# However we need to leave these at the lower values to derive that with 7 keys it is always possible to reach Bob and Ice Armos.
|
||||
set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 6))
|
||||
set_rule(multiworld.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 6))
|
||||
# It is possible to need more than 7 keys ....
|
||||
set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
|
||||
set_rule(multiworld.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
|
||||
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests + back_chests, [player] * len(randomizer_room_chests + back_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
|
||||
|
||||
# The actual requirements for these rooms to avoid key-lock
|
||||
set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or
|
||||
((item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
|
||||
set_rule(multiworld.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or
|
||||
((item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
|
||||
for location in randomizer_room_chests:
|
||||
set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
|
||||
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
|
||||
set_rule(multiworld.get_location(location, player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
|
||||
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 6))))
|
||||
|
||||
# Once again it is possible to need more than 7 keys...
|
||||
set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
|
||||
set_rule(multiworld.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
|
||||
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5))))
|
||||
set_rule(world.get_entrance('Ganons Tower (Bottom) (East)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
|
||||
set_rule(multiworld.get_entrance('Ganons Tower (Bottom) (East)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
|
||||
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(back_chests, [player] * len(back_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
|
||||
# Actual requirements
|
||||
for location in compass_room_chests:
|
||||
set_rule(world.get_location(location, player), lambda state: (can_use_bombs(state, player) or state.has("Cane of Somaria", player)) and state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
|
||||
set_rule(multiworld.get_location(location, player), lambda state: (can_use_bombs(state, player) or state.has("Cane of Somaria", player)) and state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
|
||||
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5))))
|
||||
|
||||
set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
|
||||
set_rule(multiworld.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
|
||||
|
||||
set_rule(world.get_location('Ganons Tower - Big Key Room - Left', player),
|
||||
set_rule(multiworld.get_location('Ganons Tower - Big Key Room - Left', player),
|
||||
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Left', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
|
||||
set_rule(world.get_location('Ganons Tower - Big Key Chest', player),
|
||||
set_rule(multiworld.get_location('Ganons Tower - Big Key Chest', player),
|
||||
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
|
||||
set_rule(world.get_location('Ganons Tower - Big Key Room - Right', player),
|
||||
set_rule(multiworld.get_location('Ganons Tower - Big Key Room - Right', player),
|
||||
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
|
||||
if world.enemy_shuffle[player]:
|
||||
set_rule(world.get_entrance('Ganons Tower Big Key Door', player),
|
||||
if multiworld.enemy_shuffle[player]:
|
||||
set_rule(multiworld.get_entrance('Ganons Tower Big Key Door', player),
|
||||
lambda state: state.has('Big Key (Ganons Tower)', player))
|
||||
else:
|
||||
set_rule(world.get_entrance('Ganons Tower Big Key Door', player),
|
||||
set_rule(multiworld.get_entrance('Ganons Tower Big Key Door', player),
|
||||
lambda state: state.has('Big Key (Ganons Tower)', player) and can_shoot_arrows(state, player))
|
||||
set_rule(world.get_entrance('Ganons Tower Torch Rooms', player),
|
||||
set_rule(multiworld.get_entrance('Ganons Tower Torch Rooms', player),
|
||||
lambda state: can_kill_most_things(state, player, 8) and has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
|
||||
set_rule(world.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1))
|
||||
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player),
|
||||
set_rule(multiworld.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1))
|
||||
set_rule(multiworld.get_location('Ganons Tower - Pre-Moldorm Chest', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7))
|
||||
set_rule(world.get_entrance('Ganons Tower Moldorm Door', player),
|
||||
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Door', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
|
||||
set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player),
|
||||
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Gap', player),
|
||||
lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player))
|
||||
ganon = world.get_location('Ganon', player)
|
||||
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))
|
||||
ganon = multiworld.get_location('Ganon', player)
|
||||
set_rule(ganon, lambda state: GanonDefeatRule(state, player))
|
||||
if world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
if multiworld.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
add_rule(ganon, lambda state: has_triforce_pieces(state, player))
|
||||
elif world.goal[player] == 'ganon_pedestal':
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player))
|
||||
elif multiworld.goal[player] == 'ganon_pedestal':
|
||||
add_rule(multiworld.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player))
|
||||
else:
|
||||
add_rule(ganon, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_ganon[player], player))
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop
|
||||
set_rule(multiworld.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop
|
||||
|
||||
set_rule(world.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
|
||||
set_rule(multiworld.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
|
||||
|
||||
|
||||
def default_rules(world, player):
|
||||
@@ -895,7 +909,6 @@ def no_glitches_rules(world, player):
|
||||
|
||||
add_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override
|
||||
forbid_bomb_jump_requirements(world, player)
|
||||
add_conditional_lamps(world, player)
|
||||
|
||||
def fake_flipper_rules(world, player):
|
||||
@@ -923,12 +936,20 @@ def fake_flipper_rules(world, player):
|
||||
set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player))
|
||||
|
||||
|
||||
def forbid_bomb_jump_requirements(world, player):
|
||||
def bomb_jump_requirements(multiworld, player):
|
||||
DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right']
|
||||
for location in DMs_room_chests:
|
||||
add_rule(world.get_location(location, player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Paradox Cave Bomb Jump', player), lambda state: False)
|
||||
set_rule(world.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: False)
|
||||
add_rule(multiworld.get_location(location, player), lambda state: can_use_bombs(state, player), combine="or")
|
||||
set_rule(multiworld.get_entrance('Paradox Cave Bomb Jump', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: can_use_bombs(state, player))
|
||||
|
||||
|
||||
def forbid_bomb_jump_requirements(multiworld, player):
|
||||
DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right']
|
||||
for location in DMs_room_chests:
|
||||
add_rule(multiworld.get_location(location, player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(multiworld.get_entrance('Paradox Cave Bomb Jump', player), lambda state: False)
|
||||
set_rule(multiworld.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: False)
|
||||
|
||||
|
||||
DW_Entrances = ['Bumper Cave (Bottom)',
|
||||
@@ -1007,9 +1028,6 @@ def add_conditional_lamps(world, player):
|
||||
|
||||
def open_rules(world, player):
|
||||
|
||||
set_rule(world.get_location('Hyrule Castle - Map Guard Key Drop', player),
|
||||
lambda state: can_kill_most_things(state, player, 1))
|
||||
|
||||
def basement_key_rule(state):
|
||||
if location_item_name(state, 'Sewers - Key Rat Key Drop', player) == ("Small Key (Hyrule Castle)", player):
|
||||
return state._lttp_has_key("Small Key (Hyrule Castle)", player, 2)
|
||||
@@ -1018,7 +1036,7 @@ def open_rules(world, player):
|
||||
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
|
||||
lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 2))
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), basement_key_rule)
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 1))
|
||||
|
||||
set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3) and can_kill_most_things(state, player, 1))
|
||||
@@ -1026,8 +1044,10 @@ def open_rules(world, player):
|
||||
set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and can_kill_most_things(state, player, 1))
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and
|
||||
state.has('Big Key (Hyrule Castle)', player))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)
|
||||
and state.has('Big Key (Hyrule Castle)', player)
|
||||
and (world.enemy_health[player] in ("easy", "default")
|
||||
or can_kill_most_things(state, player, 1)))
|
||||
|
||||
|
||||
def swordless_rules(world, player):
|
||||
@@ -1057,6 +1077,7 @@ def add_connection(parent_name, target_name, entrance_name, world, player):
|
||||
parent.exits.append(connection)
|
||||
connection.connect(target)
|
||||
|
||||
|
||||
def standard_rules(world, player):
|
||||
add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player)
|
||||
world.get_entrance('Uncle S&Q', player).hide_path = True
|
||||
@@ -1068,18 +1089,23 @@ def standard_rules(world, player):
|
||||
|
||||
if world.small_key_shuffle[player] != small_key_shuffle.option_universal:
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
|
||||
and can_kill_most_things(state, player, 2))
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
|
||||
and can_kill_most_things(state, player, 1))
|
||||
|
||||
set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2))
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2) and
|
||||
state.has('Big Key (Hyrule Castle)', player))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)
|
||||
and state.has('Big Key (Hyrule Castle)', player)
|
||||
and (world.enemy_health[player] in ("easy", "default")
|
||||
or can_kill_most_things(state, player, 1)))
|
||||
|
||||
set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3)
|
||||
and can_kill_most_things(state, player, 1))
|
||||
else:
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||
lambda state: state.has('Big Key (Hyrule Castle)', player))
|
||||
@@ -1106,14 +1132,10 @@ def set_trock_key_rules(world, player):
|
||||
all_state.stale[player] = True
|
||||
|
||||
# Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon.
|
||||
can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)) if world.can_access_trock_eyebridge[player] is None else world.can_access_trock_eyebridge[player]
|
||||
world.can_access_trock_eyebridge[player] = can_reach_back
|
||||
can_reach_front = all_state.can_reach(world.get_region('Turtle Rock (Entrance)', player)) if world.can_access_trock_front[player] is None else world.can_access_trock_front[player]
|
||||
world.can_access_trock_front[player] = can_reach_front
|
||||
can_reach_big_chest = all_state.can_reach(world.get_region('Turtle Rock (Big Chest)', player)) if world.can_access_trock_big_chest[player] is None else world.can_access_trock_big_chest[player]
|
||||
world.can_access_trock_big_chest[player] = can_reach_big_chest
|
||||
can_reach_middle = all_state.can_reach(world.get_region('Turtle Rock (Second Section)', player)) if world.can_access_trock_middle[player] is None else world.can_access_trock_middle[player]
|
||||
world.can_access_trock_middle[player] = can_reach_middle
|
||||
can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player))
|
||||
can_reach_front = all_state.can_reach(world.get_region('Turtle Rock (Entrance)', player))
|
||||
can_reach_big_chest = all_state.can_reach(world.get_region('Turtle Rock (Big Chest)', player))
|
||||
can_reach_middle = all_state.can_reach(world.get_region('Turtle Rock (Second Section)', player))
|
||||
|
||||
# If you can't enter from the back, the door to the front of TR requires only 2 small keys if the big key is in one of these chests since 2 key doors are locked behind the big key door.
|
||||
# If you can only enter from the middle, this includes all locations that can only be reached by exiting the front. This can include Laser Bridge and Crystaroller if the front and back connect via Dark DM Ledge!
|
||||
|
||||
@@ -30,7 +30,7 @@ def can_shoot_arrows(state: CollectionState, player: int) -> bool:
|
||||
|
||||
|
||||
def has_triforce_pieces(state: CollectionState, player: int) -> bool:
|
||||
count = state.multiworld.treasure_hunt_count[player]
|
||||
count = state.multiworld.worlds[player].treasure_hunt_required
|
||||
return state.count('Triforce Piece', player) + state.count('Power Star', player) >= count
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ def can_lift_heavy_rocks(state: CollectionState, player: int) -> bool:
|
||||
|
||||
|
||||
def bottle_count(state: CollectionState, player: int) -> int:
|
||||
return min(state.multiworld.difficulty_requirements[player].progressive_bottle_limit,
|
||||
state.count_group("Bottles", player))
|
||||
return min(state.multiworld.worlds[player].difficulty_requirements.progressive_bottle_limit,
|
||||
state.count_group("Bottles", player))
|
||||
|
||||
|
||||
def has_hearts(state: CollectionState, player: int, count: int) -> int:
|
||||
@@ -59,7 +59,7 @@ def has_hearts(state: CollectionState, player: int, count: int) -> int:
|
||||
|
||||
def heart_count(state: CollectionState, player: int) -> int:
|
||||
# Warning: This only considers items that are marked as advancement items
|
||||
diff = state.multiworld.difficulty_requirements[player]
|
||||
diff = state.multiworld.worlds[player].difficulty_requirements
|
||||
return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
||||
+ state.count('Sanctuary Heart Container', player) \
|
||||
+ min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
||||
@@ -106,6 +106,12 @@ def can_bomb_or_bonk(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Pegasus Boots", player) or can_use_bombs(state, player)
|
||||
|
||||
|
||||
def can_activate_crystal_switch(state: CollectionState, player: int) -> bool:
|
||||
return (has_melee_weapon(state, player) or can_use_bombs(state, player) or can_shoot_arrows(state, player)
|
||||
or state.has_any(["Hookshot", "Cane of Somaria", "Cane of Byrna", "Fire Rod", "Ice Rod", "Blue Boomerang",
|
||||
"Red Boomerang"], player))
|
||||
|
||||
|
||||
def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool:
|
||||
if state.multiworld.enemy_shuffle[player]:
|
||||
# I don't fully understand Enemizer's logic for placing enemies in spots where they need to be killable, if any.
|
||||
@@ -171,10 +177,11 @@ def can_melt_things(state: CollectionState, player: int) -> bool:
|
||||
|
||||
|
||||
def has_misery_mire_medallion(state: CollectionState, player: int) -> bool:
|
||||
return state.has(state.multiworld.required_medallions[player][0], player)
|
||||
return state.has(state.multiworld.worlds[player].required_medallions[0], player)
|
||||
|
||||
|
||||
def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool:
|
||||
return state.has(state.multiworld.required_medallions[player][1], player)
|
||||
return state.has(state.multiworld.worlds[player].required_medallions[1], player)
|
||||
|
||||
|
||||
def can_boots_clip_lw(state: CollectionState, player: int) -> bool:
|
||||
|
||||
@@ -15,7 +15,7 @@ def underworld_glitch_connections(world, player):
|
||||
specrock.exits.append(kikiskip)
|
||||
mire.exits.extend([mire_to_hera, mire_to_swamp])
|
||||
|
||||
if world.fix_fake_world[player]:
|
||||
if world.worlds[player].fix_fake_world:
|
||||
kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
|
||||
mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region)
|
||||
mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region)
|
||||
@@ -38,8 +38,8 @@ def fake_pearl_state(state, player):
|
||||
# Sets the rules on where we can actually go using this clip.
|
||||
# Behavior differs based on what type of ER shuffle we're playing.
|
||||
def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, dungeon_exit: str):
|
||||
fix_dungeon_exits = world.fix_palaceofdarkness_exit[player]
|
||||
fix_fake_worlds = world.fix_fake_world[player]
|
||||
fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit
|
||||
fix_fake_worlds = world.worlds[player].fix_fake_world
|
||||
|
||||
dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
|
||||
if not fix_dungeon_exits: # vanilla, simple, restricted, dungeons_simple; should never have fake worlds fix
|
||||
@@ -52,7 +52,7 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du
|
||||
add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier
|
||||
# Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally.
|
||||
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
|
||||
elif not fix_fake_worlds: # full, dungeons_full; fixed dungeon exits, but no fake worlds fix
|
||||
elif not fix_fake_worlds: # full, dungeons_full; fixed dungeon exits, but no fake worlds fix
|
||||
# Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region.
|
||||
add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player)))
|
||||
# exiting restriction
|
||||
@@ -62,9 +62,6 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du
|
||||
|
||||
|
||||
def underworld_glitches_rules(world, player):
|
||||
fix_dungeon_exits = world.fix_palaceofdarkness_exit[player]
|
||||
fix_fake_worlds = world.fix_fake_world[player]
|
||||
|
||||
# Ice Palace Entrance Clip
|
||||
# This is the easiest one since it's a simple internal clip.
|
||||
# Need to also add melting to freezor chest since it's otherwise assumed.
|
||||
@@ -92,7 +89,7 @@ def underworld_glitches_rules(world, player):
|
||||
# Build the rule for SP moat.
|
||||
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
|
||||
# First we require a certain type of entrance shuffle, then build the rule from its pieces.
|
||||
if not world.swamp_patch_required[player]:
|
||||
if not world.worlds[player].swamp_patch_required:
|
||||
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rule_map = {
|
||||
'Misery Mire (Entrance)': (lambda state: True),
|
||||
|
||||
@@ -251,6 +251,18 @@ class ALTTPWorld(World):
|
||||
dungeons: typing.Dict[str, Dungeon]
|
||||
waterfall_fairy_bottle_fill: str
|
||||
pyramid_fairy_bottle_fill: str
|
||||
escape_assist: list
|
||||
|
||||
can_take_damage: bool = True
|
||||
swamp_patch_required: bool = False
|
||||
powder_patch_required: bool = False
|
||||
ganon_at_pyramid: bool = True
|
||||
ganonstower_vanilla: bool = True
|
||||
fix_fake_world: bool = True
|
||||
|
||||
clock_mode: str = ""
|
||||
treasure_hunt_required: int = 0
|
||||
treasure_hunt_total: int = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.dungeon_local_item_names = set()
|
||||
@@ -265,6 +277,8 @@ class ALTTPWorld(World):
|
||||
self.fix_skullwoods_exit = None
|
||||
self.fix_palaceofdarkness_exit = None
|
||||
self.fix_trock_exit = None
|
||||
self.required_medallions = ["Ether", "Quake"]
|
||||
self.escape_assist = []
|
||||
super(ALTTPWorld, self).__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
@@ -298,7 +312,7 @@ class ALTTPWorld(World):
|
||||
"Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)",
|
||||
"Bottle (Bee)", "Bottle (Good Bee)"
|
||||
]
|
||||
if multiworld.difficulty[player] not in ["hard", "expert"]:
|
||||
if multiworld.item_pool[player] not in ["hard", "expert"]:
|
||||
bottle_options.append("Bottle (Fairy)")
|
||||
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||
@@ -344,7 +358,7 @@ class ALTTPWorld(World):
|
||||
if option == "original_dungeon":
|
||||
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
|
||||
|
||||
multiworld.difficulty_requirements[player] = difficulties[multiworld.item_pool[player].current_key]
|
||||
self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]
|
||||
|
||||
# enforce pre-defined local items.
|
||||
if multiworld.goal[player] in ["local_triforce_hunt", "local_ganon_triforce_hunt"]:
|
||||
@@ -370,7 +384,7 @@ class ALTTPWorld(World):
|
||||
if (multiworld.glitches_required[player] not in ["no_glitches", "minor_glitches"] and
|
||||
multiworld.entrance_shuffle[player] in [
|
||||
"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"]):
|
||||
multiworld.fix_fake_world[player] = False
|
||||
self.fix_fake_world = False
|
||||
|
||||
# seeded entrance shuffle
|
||||
old_random = multiworld.random
|
||||
@@ -438,15 +452,16 @@ class ALTTPWorld(World):
|
||||
if 'Sword' in item_name:
|
||||
if state.has('Golden Sword', item.player):
|
||||
pass
|
||||
elif state.has('Tempered Sword', item.player) and self.multiworld.difficulty_requirements[
|
||||
item.player].progressive_sword_limit >= 4:
|
||||
elif (state.has('Tempered Sword', item.player) and
|
||||
self.difficulty_requirements.progressive_sword_limit >= 4):
|
||||
return 'Golden Sword'
|
||||
elif state.has('Master Sword', item.player) and self.multiworld.difficulty_requirements[
|
||||
item.player].progressive_sword_limit >= 3:
|
||||
elif (state.has('Master Sword', item.player) and
|
||||
self.difficulty_requirements.progressive_sword_limit >= 3):
|
||||
return 'Tempered Sword'
|
||||
elif state.has('Fighter Sword', item.player) and self.multiworld.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
||||
elif (state.has('Fighter Sword', item.player) and
|
||||
self.difficulty_requirements.progressive_sword_limit >= 2):
|
||||
return 'Master Sword'
|
||||
elif self.multiworld.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
||||
elif self.difficulty_requirements.progressive_sword_limit >= 1:
|
||||
return 'Fighter Sword'
|
||||
elif 'Glove' in item_name:
|
||||
if state.has('Titans Mitts', item.player):
|
||||
@@ -458,20 +473,22 @@ class ALTTPWorld(World):
|
||||
elif 'Shield' in item_name:
|
||||
if state.has('Mirror Shield', item.player):
|
||||
return
|
||||
elif state.has('Red Shield', item.player) and self.multiworld.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
||||
elif (state.has('Red Shield', item.player) and
|
||||
self.difficulty_requirements.progressive_shield_limit >= 3):
|
||||
return 'Mirror Shield'
|
||||
elif state.has('Blue Shield', item.player) and self.multiworld.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
||||
elif (state.has('Blue Shield', item.player) and
|
||||
self.difficulty_requirements.progressive_shield_limit >= 2):
|
||||
return 'Red Shield'
|
||||
elif self.multiworld.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
||||
elif self.difficulty_requirements.progressive_shield_limit >= 1:
|
||||
return 'Blue Shield'
|
||||
elif 'Bow' in item_name:
|
||||
if state.has('Silver Bow', item.player):
|
||||
return
|
||||
elif state.has('Bow', item.player) and (self.multiworld.difficulty_requirements[item.player].progressive_bow_limit >= 2
|
||||
or self.multiworld.glitches_required[item.player] == 'no_glitches'
|
||||
or self.multiworld.swordless[item.player]): # modes where silver bow is always required for ganon
|
||||
elif state.has('Bow', item.player) and (self.difficulty_requirements.progressive_bow_limit >= 2
|
||||
or self.multiworld.glitches_required[self.player] == 'no_glitches'
|
||||
or self.multiworld.swordless[self.player]): # modes where silver bow is always required for ganon
|
||||
return 'Silver Bow'
|
||||
elif self.multiworld.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
||||
elif self.difficulty_requirements.progressive_bow_limit >= 1:
|
||||
return 'Bow'
|
||||
elif item.advancement:
|
||||
return item_name
|
||||
@@ -660,7 +677,7 @@ class ALTTPWorld(World):
|
||||
trash_counts = {}
|
||||
for player in multiworld.get_game_players("A Link to the Past"):
|
||||
world = multiworld.worlds[player]
|
||||
if not multiworld.ganonstower_vanilla[player] or \
|
||||
if not world.ganonstower_vanilla or \
|
||||
world.options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', "no_logic"}:
|
||||
pass
|
||||
elif 'triforce_hunt' in world.options.goal.current_key and ('local' in world.options.goal.current_key or multiworld.players == 1):
|
||||
@@ -701,10 +718,10 @@ class ALTTPWorld(World):
|
||||
player_name = self.multiworld.get_player_name(self.player)
|
||||
spoiler_handle.write("\n\nMedallions:\n")
|
||||
spoiler_handle.write(f"\nMisery Mire ({player_name}):"
|
||||
f" {self.multiworld.required_medallions[self.player][0]}")
|
||||
f" {self.required_medallions[0]}")
|
||||
spoiler_handle.write(
|
||||
f"\nTurtle Rock ({player_name}):"
|
||||
f" {self.multiworld.required_medallions[self.player][1]}")
|
||||
f" {self.required_medallions[1]}")
|
||||
spoiler_handle.write("\n\nFairy Fountain Bottle Fill:\n")
|
||||
spoiler_handle.write(f"\nPyramid Fairy ({player_name}):"
|
||||
f" {self.pyramid_fairy_bottle_fill}")
|
||||
@@ -815,8 +832,8 @@ class ALTTPWorld(World):
|
||||
slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options}
|
||||
|
||||
slot_data.update({
|
||||
'mm_medalion': self.multiworld.required_medallions[self.player][0],
|
||||
'tr_medalion': self.multiworld.required_medallions[self.player][1],
|
||||
'mm_medalion': self.required_medallions[0],
|
||||
'tr_medalion': self.required_medallions[1],
|
||||
}
|
||||
)
|
||||
return slot_data
|
||||
|
||||
@@ -64,7 +64,8 @@ configuración personal y descargar un fichero "YAML".
|
||||
|
||||
### Configuración YAML avanzada
|
||||
|
||||
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina ["Weighted settings"](/weighted-settings),
|
||||
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina
|
||||
["Weighted settings"](/games/A Link to the Past/weighted-options),
|
||||
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones
|
||||
representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser
|
||||
elegidos sobre otros de la misma.
|
||||
|
||||
@@ -66,9 +66,10 @@ paramètres personnels et de les exporter vers un fichier YAML.
|
||||
### Configuration avancée du fichier YAML
|
||||
|
||||
Une version plus avancée du fichier YAML peut être créée en utilisant la page
|
||||
des [paramètres de pondération](/weighted-settings), qui vous permet de configurer jusqu'à trois préréglages. Cette page
|
||||
a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants. Cela vous permet de choisir
|
||||
quelles sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles dans une même catégorie.
|
||||
des [paramètres de pondération](/games/A Link to the Past/weighted-options), qui vous permet de configurer jusqu'à
|
||||
trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs
|
||||
glissants. Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux
|
||||
autres disponibles dans une même catégorie.
|
||||
|
||||
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
|
||||
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
|
||||
|
||||
@@ -7,7 +7,9 @@ from worlds import AutoWorldRegister
|
||||
|
||||
class LTTPTestBase(unittest.TestCase):
|
||||
def world_setup(self):
|
||||
from worlds.alttp.Options import Medallion
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.game[1] = "A Link to the Past"
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
self.multiworld.set_seed(None)
|
||||
args = Namespace()
|
||||
@@ -15,3 +17,6 @@ class LTTPTestBase(unittest.TestCase):
|
||||
setattr(args, name, {1: option.from_any(getattr(option, "default"))})
|
||||
self.multiworld.set_options(args)
|
||||
self.world = self.multiworld.worlds[1]
|
||||
# by default medallion access is randomized, for unittests we set it to vanilla
|
||||
self.world.options.misery_mire_medallion.value = Medallion.option_ether
|
||||
self.world.options.turtle_rock_medallion.value = Medallion.option_quake
|
||||
|
||||
@@ -13,9 +13,9 @@ class TestDungeon(LTTPTestBase):
|
||||
self.world_setup()
|
||||
self.starting_regions = [] # Where to start exploring
|
||||
self.remove_exits = [] # Block dungeon exits
|
||||
self.multiworld.difficulty_requirements[1] = difficulties['normal']
|
||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||
self.multiworld.bombless_start[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
||||
create_regions(self.multiworld, 1)
|
||||
self.multiworld.worlds[1].create_dungeons()
|
||||
create_shops(self.multiworld, 1)
|
||||
@@ -23,7 +23,7 @@ class TestDungeon(LTTPTestBase):
|
||||
connect_simple(self.multiworld, exitname, regionname, 1)
|
||||
connect_simple(self.multiworld, 'Big Bomb Shop', 'Big Bomb Shop', 1)
|
||||
self.multiworld.get_region('Menu', 1).exits = []
|
||||
self.multiworld.swamp_patch_required[1] = True
|
||||
self.multiworld.worlds[1].swamp_patch_required = True
|
||||
self.world.set_rules()
|
||||
self.world.create_items()
|
||||
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
|
||||
|
||||
@@ -33,22 +33,26 @@ class TestGanonsTower(TestDungeon):
|
||||
["Ganons Tower - Randomizer Room - Top Left", False, []],
|
||||
["Ganons Tower - Randomizer Room - Top Left", False, [], ['Hammer']],
|
||||
["Ganons Tower - Randomizer Room - Top Left", False, [], ['Hookshot']],
|
||||
["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
|
||||
["Ganons Tower - Randomizer Room - Top Left", False, [], ['Bomb Upgrade (50)']],
|
||||
["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Ganons Tower - Randomizer Room - Top Right", False, []],
|
||||
["Ganons Tower - Randomizer Room - Top Right", False, [], ['Hammer']],
|
||||
["Ganons Tower - Randomizer Room - Top Right", False, [], ['Hookshot']],
|
||||
["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
|
||||
["Ganons Tower - Randomizer Room - Top Right", False, [], ['Bomb Upgrade (50)']],
|
||||
["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Ganons Tower - Randomizer Room - Bottom Left", False, []],
|
||||
["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Hammer']],
|
||||
["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Hookshot']],
|
||||
["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
|
||||
["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Bomb Upgrade (50)']],
|
||||
["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Ganons Tower - Randomizer Room - Bottom Right", False, []],
|
||||
["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Hammer']],
|
||||
["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Hookshot']],
|
||||
["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
|
||||
["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Bomb Upgrade (50)']],
|
||||
["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Ganons Tower - Firesnake Room", False, []],
|
||||
["Ganons Tower - Firesnake Room", False, [], ['Hammer']],
|
||||
|
||||
@@ -9,12 +9,16 @@ class TestTowerOfHera(TestDungeon):
|
||||
["Tower of Hera - Big Key Chest", False, []],
|
||||
["Tower of Hera - Big Key Chest", False, [], ['Small Key (Tower of Hera)']],
|
||||
["Tower of Hera - Big Key Chest", False, [], ['Lamp', 'Fire Rod']],
|
||||
["Tower of Hera - Big Key Chest", True, ['Small Key (Tower of Hera)', 'Lamp']],
|
||||
["Tower of Hera - Big Key Chest", True, ['Small Key (Tower of Hera)', 'Lamp', 'Bomb Upgrade (50)']],
|
||||
["Tower of Hera - Big Key Chest", True, ['Small Key (Tower of Hera)', 'Fire Rod']],
|
||||
|
||||
["Tower of Hera - Basement Cage", True, []],
|
||||
["Tower of Hera - Basement Cage", False, []],
|
||||
["Tower of Hera - Basement Cage", True, ['Bomb Upgrade (50)']],
|
||||
["Tower of Hera - Basement Cage", True, ['Progressive Sword']],
|
||||
|
||||
["Tower of Hera - Map Chest", True, []],
|
||||
["Tower of Hera - Map Chest", False, []],
|
||||
["Tower of Hera - Map Chest", True, ['Bomb Upgrade (50)']],
|
||||
["Tower of Hera - Map Chest", True, ['Progressive Sword']],
|
||||
|
||||
["Tower of Hera - Compass Chest", False, []],
|
||||
["Tower of Hera - Compass Chest", False, [], ['Big Key (Tower of Hera)']],
|
||||
|
||||
@@ -13,16 +13,15 @@ from worlds.alttp.test import LTTPTestBase
|
||||
class TestInverted(TestBase, LTTPTestBase):
|
||||
def setUp(self):
|
||||
self.world_setup()
|
||||
self.multiworld.difficulty_requirements[1] = difficulties['normal']
|
||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||
self.multiworld.mode[1].value = 2
|
||||
self.multiworld.bombless_start[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
||||
create_inverted_regions(self.multiworld, 1)
|
||||
self.world.create_dungeons()
|
||||
create_shops(self.multiworld, 1)
|
||||
link_inverted_entrances(self.multiworld, 1)
|
||||
self.world.create_items()
|
||||
self.multiworld.required_medallions[1] = ['Ether', 'Quake']
|
||||
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
|
||||
self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
|
||||
self.multiworld.get_location('Agahnim 1', 1).item = None
|
||||
|
||||
@@ -11,7 +11,7 @@ class TestInvertedBombRules(LTTPTestBase):
|
||||
|
||||
def setUp(self):
|
||||
self.world_setup()
|
||||
self.multiworld.difficulty_requirements[1] = difficulties['normal']
|
||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||
self.multiworld.mode[1].value = 2
|
||||
create_inverted_regions(self.multiworld, 1)
|
||||
self.multiworld.worlds[1].create_dungeons()
|
||||
|
||||
@@ -17,14 +17,13 @@ class TestInvertedMinor(TestBase, LTTPTestBase):
|
||||
self.multiworld.mode[1].value = 2
|
||||
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches")
|
||||
self.multiworld.bombless_start[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = True
|
||||
self.multiworld.difficulty_requirements[1] = difficulties['normal']
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||
create_inverted_regions(self.multiworld, 1)
|
||||
self.world.create_dungeons()
|
||||
create_shops(self.multiworld, 1)
|
||||
link_inverted_entrances(self.multiworld, 1)
|
||||
self.world.create_items()
|
||||
self.multiworld.required_medallions[1] = ['Ether', 'Quake']
|
||||
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
|
||||
self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
|
||||
self.multiworld.get_location('Agahnim 1', 1).item = None
|
||||
|
||||
@@ -41,7 +41,8 @@ class TestDungeons(TestInvertedOWG):
|
||||
|
||||
["Tower of Hera - Basement Cage", False, []],
|
||||
["Tower of Hera - Basement Cage", False, [], ['Moon Pearl']],
|
||||
["Tower of Hera - Basement Cage", True, ['Pegasus Boots', 'Moon Pearl']],
|
||||
["Tower of Hera - Basement Cage", True, ['Pegasus Boots', 'Moon Pearl', 'Bomb Upgrade (50)']],
|
||||
["Tower of Hera - Basement Cage", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Sword']],
|
||||
|
||||
["Castle Tower - Room 03", False, []],
|
||||
["Castle Tower - Room 03", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
|
||||
|
||||
@@ -17,14 +17,13 @@ class TestInvertedOWG(TestBase, LTTPTestBase):
|
||||
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches")
|
||||
self.multiworld.mode[1].value = 2
|
||||
self.multiworld.bombless_start[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = True
|
||||
self.multiworld.difficulty_requirements[1] = difficulties['normal']
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||
create_inverted_regions(self.multiworld, 1)
|
||||
self.world.create_dungeons()
|
||||
create_shops(self.multiworld, 1)
|
||||
link_inverted_entrances(self.multiworld, 1)
|
||||
self.world.create_items()
|
||||
self.multiworld.required_medallions[1] = ['Ether', 'Quake']
|
||||
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
|
||||
self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
|
||||
self.multiworld.get_location('Agahnim 1', 1).item = None
|
||||
|
||||
@@ -13,12 +13,11 @@ class TestMinor(TestBase, LTTPTestBase):
|
||||
self.world_setup()
|
||||
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches")
|
||||
self.multiworld.bombless_start[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = True
|
||||
self.multiworld.difficulty_requirements[1] = difficulties['normal']
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||
self.world.er_seed = 0
|
||||
self.world.create_regions()
|
||||
self.world.create_items()
|
||||
self.multiworld.required_medallions[1] = ['Ether', 'Quake']
|
||||
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
|
||||
self.multiworld.itempool.extend(item_factory(
|
||||
['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1',
|
||||
|
||||
@@ -34,11 +34,11 @@ class TestDungeons(TestVanillaOWG):
|
||||
["Tower of Hera - Basement Cage", False, [], ['Pegasus Boots', "Flute", "Lamp"]],
|
||||
["Tower of Hera - Basement Cage", False, [], ['Pegasus Boots', "Magic Mirror", "Hammer"]],
|
||||
["Tower of Hera - Basement Cage", False, [], ['Pegasus Boots', "Magic Mirror", "Hookshot"]],
|
||||
["Tower of Hera - Basement Cage", True, ['Pegasus Boots']],
|
||||
["Tower of Hera - Basement Cage", True, ["Flute", "Magic Mirror"]],
|
||||
["Tower of Hera - Basement Cage", True, ["Progressive Glove", "Lamp", "Magic Mirror"]],
|
||||
["Tower of Hera - Basement Cage", True, ['Pegasus Boots', 'Bomb Upgrade (50)']],
|
||||
["Tower of Hera - Basement Cage", True, ["Flute", "Magic Mirror", 'Bomb Upgrade (50)']],
|
||||
["Tower of Hera - Basement Cage", True, ["Progressive Glove", "Lamp", "Magic Mirror", 'Bomb Upgrade (50)']],
|
||||
["Tower of Hera - Basement Cage", True, ["Flute", "Hookshot", "Hammer"]],
|
||||
["Tower of Hera - Basement Cage", True, ["Progressive Glove", "Lamp", "Magic Mirror"]],
|
||||
["Tower of Hera - Basement Cage", True, ["Progressive Glove", "Lamp", "Magic Mirror", 'Bomb Upgrade (50)']],
|
||||
|
||||
["Castle Tower - Room 03", False, []],
|
||||
["Castle Tower - Room 03", False, ['Progressive Sword'], ['Progressive Sword', 'Cape', 'Beat Agahnim 1']],
|
||||
|
||||
@@ -11,14 +11,13 @@ from worlds.alttp.test import LTTPTestBase
|
||||
class TestVanillaOWG(TestBase, LTTPTestBase):
|
||||
def setUp(self):
|
||||
self.world_setup()
|
||||
self.multiworld.difficulty_requirements[1] = difficulties['normal']
|
||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches")
|
||||
self.multiworld.bombless_start[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
||||
self.multiworld.worlds[1].er_seed = 0
|
||||
self.multiworld.worlds[1].create_regions()
|
||||
self.multiworld.worlds[1].create_items()
|
||||
self.multiworld.required_medallions[1] = ['Ether', 'Quake']
|
||||
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
|
||||
self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
|
||||
self.multiworld.get_location('Agahnim 1', 1).item = None
|
||||
|
||||
@@ -11,13 +11,12 @@ class TestVanilla(TestBase, LTTPTestBase):
|
||||
def setUp(self):
|
||||
self.world_setup()
|
||||
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("no_glitches")
|
||||
self.multiworld.difficulty_requirements[1] = difficulties['normal']
|
||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||
self.multiworld.bombless_start[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = True
|
||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
||||
self.multiworld.worlds[1].er_seed = 0
|
||||
self.multiworld.worlds[1].create_regions()
|
||||
self.multiworld.worlds[1].create_items()
|
||||
self.multiworld.required_medallions[1] = ['Ether', 'Quake']
|
||||
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
|
||||
self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
|
||||
self.multiworld.get_location('Agahnim 1', 1).item = None
|
||||
|
||||
210
worlds/aquaria/Items.py
Normal file
210
worlds/aquaria/Items.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 15 Mar 2024 18:41:40 +0000
|
||||
Description: Manage items in the Aquaria game multiworld randomizer
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from BaseClasses import Item, ItemClassification
|
||||
|
||||
class ItemType(Enum):
|
||||
"""
|
||||
Used to indicate to the multi-world if an item is usefull or not
|
||||
"""
|
||||
NORMAL = 0
|
||||
PROGRESSION = 1
|
||||
JUNK = 2
|
||||
|
||||
class ItemGroup(Enum):
|
||||
"""
|
||||
Used to group items
|
||||
"""
|
||||
COLLECTIBLE = 0
|
||||
INGREDIENT = 1
|
||||
RECIPE = 2
|
||||
HEALTH = 3
|
||||
UTILITY = 4
|
||||
SONG = 5
|
||||
TURTLE = 6
|
||||
|
||||
class AquariaItem(Item):
|
||||
"""
|
||||
A single item in the Aquaria game.
|
||||
"""
|
||||
game: str = "Aquaria"
|
||||
"""The name of the game"""
|
||||
|
||||
def __init__(self, name: str, classification: ItemClassification,
|
||||
code: Optional[int], player: int):
|
||||
"""
|
||||
Initialisation of the Item
|
||||
:param name: The name of the item
|
||||
:param classification: If the item is usefull or not
|
||||
:param code: The ID of the item (if None, it is an event)
|
||||
:param player: The ID of the player in the multiworld
|
||||
"""
|
||||
super().__init__(name, classification, code, player)
|
||||
|
||||
class ItemData:
|
||||
"""
|
||||
Data of an item.
|
||||
"""
|
||||
id:int
|
||||
count:int
|
||||
type:ItemType
|
||||
group:ItemGroup
|
||||
|
||||
def __init__(self, id:int, count:int, type:ItemType, group:ItemGroup):
|
||||
"""
|
||||
Initialisation of the item data
|
||||
@param id: The item ID
|
||||
@param count: the number of items in the pool
|
||||
@param type: the importance type of the item
|
||||
@param group: the usage of the item in the game
|
||||
"""
|
||||
self.id = id
|
||||
self.count = count
|
||||
self.type = type
|
||||
self.group = group
|
||||
|
||||
"""Information data for every (not event) item."""
|
||||
item_table = {
|
||||
# name: ID, Nb, Item Type, Item Group
|
||||
"Anemone": ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone
|
||||
"Arnassi statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue
|
||||
"Big seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed
|
||||
"Glowing seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed
|
||||
"Black pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl
|
||||
"Baby blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster
|
||||
"Crab armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume
|
||||
"Baby dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo
|
||||
"Tooth": ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss
|
||||
"Energy statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue
|
||||
"Krotite armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple
|
||||
"Golden starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star
|
||||
"Golden gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear
|
||||
"Jelly beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon
|
||||
"Jelly costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume
|
||||
"Jelly plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant
|
||||
"Mithalas doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll
|
||||
"Mithalan dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume
|
||||
"Mithalas banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner
|
||||
"Mithalas pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot
|
||||
"Mutant costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
|
||||
"Baby nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
|
||||
"Baby piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
|
||||
"Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume
|
||||
"Seed bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
|
||||
"King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
|
||||
"Song plant spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
|
||||
"Stone head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head
|
||||
"Sun key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key
|
||||
"Girl costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume
|
||||
"Odd container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest
|
||||
"Trident": ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
|
||||
"Turtle egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
|
||||
"Jelly egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
|
||||
"Urchin costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
|
||||
"Baby walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
|
||||
"Vedha's Cure-All-All": ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
|
||||
"Zuuna's perogi": ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
|
||||
"Arcane poultice": ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice
|
||||
"Berry ice cream": ItemData(698039, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_berryicecream
|
||||
"Buttery sea loaf": ItemData(698040, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_butterysealoaf
|
||||
"Cold borscht": ItemData(698041, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldborscht
|
||||
"Cold soup": ItemData(698042, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldsoup
|
||||
"Crab cake": ItemData(698043, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_crabcake
|
||||
"Divine soup": ItemData(698044, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_divinesoup
|
||||
"Dumbo ice cream": ItemData(698045, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_dumboicecream
|
||||
"Fish oil": ItemData(698046, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
|
||||
"Glowing egg": ItemData(698047, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
|
||||
"Hand roll": ItemData(698048, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_handroll
|
||||
"Healing poultice": ItemData(698049, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
|
||||
"Hearty soup": ItemData(698050, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_heartysoup
|
||||
"Hot borscht": ItemData(698051, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_hotborscht
|
||||
"Hot soup": ItemData(698052, 3, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
|
||||
"Ice cream": ItemData(698053, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_icecream
|
||||
"Leadership roll": ItemData(698054, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
|
||||
"Leaf poultice": ItemData(698055, 5, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
|
||||
"Leeching poultice": ItemData(698056, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leechingpoultice
|
||||
"Legendary cake": ItemData(698057, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_legendarycake
|
||||
"Loaf of life": ItemData(698058, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_loafoflife
|
||||
"Long life soup": ItemData(698059, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_longlifesoup
|
||||
"Magic soup": ItemData(698060, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_magicsoup
|
||||
"Mushroom x 2": ItemData(698061, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_mushroom
|
||||
"Perogi": ItemData(698062, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_perogi
|
||||
"Plant leaf": ItemData(698063, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
||||
"Plump perogi": ItemData(698064, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_plumpperogi
|
||||
"Poison loaf": ItemData(698065, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonloaf
|
||||
"Poison soup": ItemData(698066, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonsoup
|
||||
"Rainbow mushroom": ItemData(698067, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_rainbowmushroom
|
||||
"Rainbow soup": ItemData(698068, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_rainbowsoup
|
||||
"Red berry": ItemData(698069, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redberry
|
||||
"Red bulb x 2": ItemData(698070, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redbulb
|
||||
"Rotten cake": ItemData(698071, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottencake
|
||||
"Rotten loaf x 8": ItemData(698072, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottenloaf
|
||||
"Rotten meat": ItemData(698073, 5, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
||||
"Royal soup": ItemData(698074, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_royalsoup
|
||||
"Sea cake": ItemData(698075, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_seacake
|
||||
"Sea loaf": ItemData(698076, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
|
||||
"Shark fin soup": ItemData(698077, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sharkfinsoup
|
||||
"Sight poultice": ItemData(698078, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sightpoultice
|
||||
"Small bone x 2": ItemData(698079, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
|
||||
"Small egg": ItemData(698080, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
|
||||
"Small tentacle x 2": ItemData(698081, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smalltentacle
|
||||
"Special bulb": ItemData(698082, 5, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_specialbulb
|
||||
"Special cake": ItemData(698083, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_specialcake
|
||||
"Spicy meat x 2": ItemData(698084, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_spicymeat
|
||||
"Spicy roll": ItemData(698085, 11, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicyroll
|
||||
"Spicy soup": ItemData(698086, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicysoup
|
||||
"Spider roll": ItemData(698087, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spiderroll
|
||||
"Swamp cake": ItemData(698088, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_swampcake
|
||||
"Tasty cake": ItemData(698089, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastycake
|
||||
"Tasty roll": ItemData(698090, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastyroll
|
||||
"Tough cake": ItemData(698091, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_toughcake
|
||||
"Turtle soup": ItemData(698092, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_turtlesoup
|
||||
"Vedha sea crisp": ItemData(698093, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_vedhaseacrisp
|
||||
"Veggie cake": ItemData(698094, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiecake
|
||||
"Veggie ice cream": ItemData(698095, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggieicecream
|
||||
"Veggie soup": ItemData(698096, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiesoup
|
||||
"Volcano roll": ItemData(698097, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_volcanoroll
|
||||
"Health upgrade": ItemData(698098, 5, ItemType.NORMAL, ItemGroup.HEALTH), # upgrade_health_?
|
||||
"Wok": ItemData(698099, 1, ItemType.NORMAL, ItemGroup.UTILITY), # upgrade_wok
|
||||
"Eel oil x 2": ItemData(698100, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_eeloil
|
||||
"Fish meat x 2": ItemData(698101, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishmeat
|
||||
"Fish oil x 3": ItemData(698102, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
|
||||
"Glowing egg x 2": ItemData(698103, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
|
||||
"Healing poultice x 2": ItemData(698104, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
|
||||
"Hot soup x 2": ItemData(698105, 1, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
|
||||
"Leadership roll x 2": ItemData(698106, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
|
||||
"Leaf poultice x 3": ItemData(698107, 2, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
|
||||
"Plant leaf x 2": ItemData(698108, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
||||
"Plant leaf x 3": ItemData(698109, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
||||
"Rotten meat x 2": ItemData(698110, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
||||
"Rotten meat x 8": ItemData(698111, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
||||
"Sea loaf x 2": ItemData(698112, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
|
||||
"Small bone x 3": ItemData(698113, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
|
||||
"Small egg x 2": ItemData(698114, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
|
||||
"Li and Li song": ItemData(698115, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_li
|
||||
"Shield song": ItemData(698116, 1, ItemType.NORMAL, ItemGroup.SONG), # song_shield
|
||||
"Beast form": ItemData(698117, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_beast
|
||||
"Sun form": ItemData(698118, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_sun
|
||||
"Nature form": ItemData(698119, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_nature
|
||||
"Energy form": ItemData(698120, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_energy
|
||||
"Bind song": ItemData(698121, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_bind
|
||||
"Fish form": ItemData(698122, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_fish
|
||||
"Spirit form": ItemData(698123, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_spirit
|
||||
"Dual form": ItemData(698124, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_dual
|
||||
"Transturtle Veil top left": ItemData(698125, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil01
|
||||
"Transturtle Veil top right": ItemData(698126, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil02
|
||||
"Transturtle Open Water top right": ItemData(698127, 1, ItemType.PROGRESSION,
|
||||
ItemGroup.TURTLE), # transport_openwater03
|
||||
"Transturtle Forest bottom left": ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest04
|
||||
"Transturtle Home water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea
|
||||
"Transturtle Abyss right": ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03
|
||||
"Transturtle Final Boss": ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss
|
||||
"Transturtle Simon says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
|
||||
"Transturtle Arnassi ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user