mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-11 18:13:48 -07:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
30cdde8605 | ||
|
|
38c54ba393 | ||
|
|
5da3a40964 | ||
|
|
a1ef25455b | ||
|
|
feb62b4af2 | ||
|
|
6dbeb6c658 | ||
|
|
09abc5beaa | ||
|
|
fb1cf26118 | ||
|
|
842a15fd3c | ||
|
|
f67e8497e0 | ||
|
|
19f1b265b1 | ||
|
|
1c14d1107f | ||
|
|
7b3727e945 | ||
|
|
d1274c12b9 | ||
|
|
7c44d749d4 | ||
|
|
f1765899c4 | ||
|
|
87b9f4a6fa | ||
|
|
480c15eea0 | ||
|
|
d4ec4d32f0 | ||
|
|
50fb70d832 | ||
|
|
ca5c0d9eb8 | ||
|
|
98e2d89a1c | ||
|
|
5bda265f43 | ||
|
|
9ef1fa825d | ||
|
|
f5ff005360 | ||
|
|
fb3035a78b | ||
|
|
3d5c21cec5 | ||
|
|
feaae1db12 | ||
|
|
56ec0e902d | ||
|
|
c8fd42f938 | ||
|
|
e5eb54fb27 | ||
|
|
fbeba1e470 | ||
|
|
11073dfdac | ||
|
|
7e660dbd23 | ||
|
|
1fc2c5ed4b | ||
|
|
242126b4b2 | ||
|
|
30dda013de | ||
|
|
ea4d0abb7f | ||
|
|
9bbc49204d | ||
|
|
b97cee4372 | ||
|
|
5dcafac861 | ||
|
|
8d28c34f95 | ||
|
|
0f2bd0fb85 | ||
|
|
cf59cfaad0 | ||
|
|
c534cb79b5 | ||
|
|
8952fbdc03 | ||
|
|
9012afeb75 | ||
|
|
401a6d9a42 | ||
|
|
5d4ed00452 | ||
|
|
0ba6d90bb8 | ||
|
|
b007a42487 | ||
|
|
32c92e03e7 | ||
|
|
14437d653f | ||
|
|
1021df8b1b | ||
|
|
569c37cb8e | ||
|
|
b296f64d3c | ||
|
|
8d9bd0135e | ||
|
|
885fb4aabe | ||
|
|
3c564d7b96 | ||
|
|
5e5792009c |
27
.github/pyright-config.json
vendored
Normal file
27
.github/pyright-config.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"include": [
|
||||
"type_check.py",
|
||||
"../worlds/AutoSNIClient.py",
|
||||
"../Patch.py"
|
||||
],
|
||||
|
||||
"exclude": [
|
||||
"**/__pycache__"
|
||||
],
|
||||
|
||||
"stubPath": "../typings",
|
||||
|
||||
"typeCheckingMode": "strict",
|
||||
"reportImplicitOverride": "error",
|
||||
"reportMissingImports": true,
|
||||
"reportMissingTypeStubs": true,
|
||||
|
||||
"pythonVersion": "3.8",
|
||||
"pythonPlatform": "Windows",
|
||||
|
||||
"executionEnvironments": [
|
||||
{
|
||||
"root": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
15
.github/type_check.py
vendored
Normal file
15
.github/type_check.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
config = Path(__file__).parent / "pyright-config.json"
|
||||
|
||||
command = ("pyright", "-p", str(config))
|
||||
print(" ".join(command))
|
||||
|
||||
try:
|
||||
result = subprocess.run(command)
|
||||
except FileNotFoundError as e:
|
||||
print(f"{e} - Is pyright installed?")
|
||||
exit(1)
|
||||
|
||||
exit(result.returncode)
|
||||
33
.github/workflows/strict-type-check.yml
vendored
Normal file
33
.github/workflows/strict-type-check.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: type check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.py"
|
||||
- ".github/pyright-config.json"
|
||||
- ".github/workflows/strict-type-check.yml"
|
||||
- "**.pyi"
|
||||
push:
|
||||
paths:
|
||||
- "**.py"
|
||||
- ".github/pyright-config.json"
|
||||
- ".github/workflows/strict-type-check.yml"
|
||||
- "**.pyi"
|
||||
|
||||
jobs:
|
||||
pyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
python -m pip install --upgrade pip pyright==1.1.358
|
||||
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
|
||||
|
||||
- name: "pyright: strict check on specific files"
|
||||
run: python .github/type_check.py
|
||||
@@ -51,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
|
||||
@@ -137,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)
|
||||
@@ -160,61 +155,14 @@ class MultiWorld():
|
||||
self.local_early_items = {player: {} for player in self.player_ids}
|
||||
self.indirect_connections = {}
|
||||
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
||||
self.fix_trock_doors = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
||||
self.fix_skullwoods_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
self.fix_palaceofdarkness_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
self.fix_trock_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
|
||||
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 "
|
||||
@@ -445,7 +393,7 @@ class MultiWorld():
|
||||
location.item = item
|
||||
item.location = location
|
||||
if collect:
|
||||
self.state.collect(item, location.event, location)
|
||||
self.state.collect(item, location.advancement, location)
|
||||
|
||||
logging.debug('Placed %s at %s', item, location)
|
||||
|
||||
@@ -592,8 +540,7 @@ class MultiWorld():
|
||||
def location_relevant(location: Location):
|
||||
"""Determine if this location is relevant to sweep."""
|
||||
if location.progress_type != LocationProgressType.EXCLUDED \
|
||||
and (location.player in players["locations"] or location.event
|
||||
or (location.item and location.item.advancement)):
|
||||
and (location.player in players["locations"] or location.advancement):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -738,7 +685,7 @@ class CollectionState():
|
||||
locations = self.multiworld.get_filled_locations()
|
||||
reachable_events = True
|
||||
# since the loop has a good chance to run more than once, only filter the events once
|
||||
locations = {location for location in locations if location.event and location not in self.events and
|
||||
locations = {location for location in locations if location.advancement and location not in self.events and
|
||||
not key_only or getattr(location.item, "locked_dungeon_item", False)}
|
||||
while reachable_events:
|
||||
reachable_events = {location for location in locations if location.can_reach(self)}
|
||||
@@ -1028,7 +975,6 @@ class Location:
|
||||
name: str
|
||||
address: Optional[int]
|
||||
parent_region: Optional[Region]
|
||||
event: bool = False
|
||||
locked: bool = False
|
||||
show_in_spoiler: bool = True
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
@@ -1059,7 +1005,6 @@ class Location:
|
||||
raise Exception(f"Location {self} already filled.")
|
||||
self.item = item
|
||||
item.location = self
|
||||
self.event = item.advancement
|
||||
self.locked = True
|
||||
|
||||
def __repr__(self):
|
||||
@@ -1075,6 +1020,15 @@ class Location:
|
||||
def __lt__(self, other: Location):
|
||||
return (self.player, self.name) < (other.player, other.name)
|
||||
|
||||
@property
|
||||
def advancement(self) -> bool:
|
||||
return self.item is not None and self.item.advancement
|
||||
|
||||
@property
|
||||
def is_event(self) -> bool:
|
||||
"""Returns True if the address of this location is None, denoting it is an Event Location."""
|
||||
return self.address is None
|
||||
|
||||
@property
|
||||
def native_item(self) -> bool:
|
||||
"""Returns True if the item in this location matches game."""
|
||||
@@ -1352,12 +1306,15 @@ class Spoiler:
|
||||
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
def to_file(self, filename: str) -> None:
|
||||
from itertools import chain
|
||||
from worlds import AutoWorld
|
||||
from Options import Visibility
|
||||
|
||||
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
|
||||
res = getattr(self.multiworld.worlds[player].options, option_key)
|
||||
display_name = getattr(option_obj, "display_name", option_key)
|
||||
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
|
||||
if res.visibility & Visibility.spoiler:
|
||||
display_name = getattr(option_obj, "display_name", option_key)
|
||||
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
|
||||
|
||||
with open(filename, 'w', encoding="utf-8-sig") as outfile:
|
||||
outfile.write(
|
||||
@@ -1388,6 +1345,14 @@ class Spoiler:
|
||||
|
||||
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
|
||||
|
||||
precollected_items = [f"{item.name} ({self.multiworld.get_player_name(item.player)})"
|
||||
if self.multiworld.players > 1
|
||||
else item.name
|
||||
for item in chain.from_iterable(self.multiworld.precollected_items.values())]
|
||||
if precollected_items:
|
||||
outfile.write("\n\nStarting Items:\n\n")
|
||||
outfile.write("\n".join([item for item in precollected_items]))
|
||||
|
||||
locations = [(str(location), str(location.item) if location.item is not None else "Nothing")
|
||||
for location in self.multiworld.get_locations() if location.show_in_spoiler]
|
||||
outfile.write('\n\nLocations:\n\n')
|
||||
|
||||
@@ -193,6 +193,7 @@ class CommonContext:
|
||||
server_version: Version = Version(0, 0, 0)
|
||||
generator_version: Version = Version(0, 0, 0)
|
||||
current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server
|
||||
max_size: int = 16*1024*1024 # 16 MB of max incoming packet size
|
||||
|
||||
last_death_link: float = time.time() # last send/received death link on AP layer
|
||||
|
||||
@@ -651,7 +652,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
try:
|
||||
port = server_url.port or 38281 # raises ValueError if invalid
|
||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
|
||||
ssl=get_ssl_context() if address.startswith("wss://") else None)
|
||||
ssl=get_ssl_context() if address.startswith("wss://") else None,
|
||||
max_size=ctx.max_size)
|
||||
if ctx.ui is not None:
|
||||
ctx.ui.update_address_bar(server_url.netloc)
|
||||
ctx.server = Endpoint(socket)
|
||||
|
||||
13
Fill.py
13
Fill.py
@@ -159,7 +159,6 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
multiworld.push_item(spot_to_fill, item_to_place, False)
|
||||
spot_to_fill.locked = lock
|
||||
placements.append(spot_to_fill)
|
||||
spot_to_fill.event = item_to_place.advancement
|
||||
placed += 1
|
||||
if not placed % 1000:
|
||||
_log_fill_progress(name, placed, total)
|
||||
@@ -310,7 +309,6 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
|
||||
pool.append(location.item)
|
||||
state.remove(location.item)
|
||||
location.item = None
|
||||
location.event = False
|
||||
if location in state.events:
|
||||
state.events.remove(location)
|
||||
locations.append(location)
|
||||
@@ -497,10 +495,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})")
|
||||
|
||||
@@ -659,7 +656,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
while True:
|
||||
# Check locations in the current sphere and gather progression items to swap earlier
|
||||
for location in balancing_sphere:
|
||||
if location.event:
|
||||
if location.advancement:
|
||||
balancing_state.collect(location.item, True, location)
|
||||
player = location.item.player
|
||||
# only replace items that end up in another player's world
|
||||
@@ -716,7 +713,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
|
||||
# sort then shuffle to maintain deterministic behaviour,
|
||||
# while allowing use of set for better algorithm growth behaviour elsewhere
|
||||
replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked)
|
||||
replacement_locations = sorted(l for l in checked_locations if not l.advancement and not l.locked)
|
||||
multiworld.random.shuffle(replacement_locations)
|
||||
items_to_replace.sort()
|
||||
multiworld.random.shuffle(items_to_replace)
|
||||
@@ -747,7 +744,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
sphere_locations.add(location)
|
||||
|
||||
for location in sphere_locations:
|
||||
if location.event:
|
||||
if location.advancement:
|
||||
state.collect(location.item, True, location)
|
||||
checked_locations |= sphere_locations
|
||||
|
||||
@@ -768,7 +765,6 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked:
|
||||
location_2.item, location_1.item = location_1.item, location_2.item
|
||||
location_1.item.location = location_1
|
||||
location_2.item.location = location_2
|
||||
location_1.event, location_2.event = location_2.event, location_1.event
|
||||
|
||||
|
||||
def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
@@ -965,7 +961,6 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
placement['force'])
|
||||
for (item, location) in successful_pairs:
|
||||
multiworld.push_item(location, item, collect=False)
|
||||
location.event = True # flag location to be checked during fill
|
||||
location.locked = True
|
||||
logging.debug(f"Plando placed {item} at {location}")
|
||||
if from_pool:
|
||||
|
||||
19
Generate.py
19
Generate.py
@@ -21,7 +21,6 @@ from BaseClasses import seeddigits, get_seed, PlandoOptions
|
||||
from Main import main as ERmain
|
||||
from settings import get_settings
|
||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
|
||||
from worlds.alttp import Options as LttPOptions
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
@@ -35,8 +34,8 @@ def mystery_argparse():
|
||||
|
||||
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
|
||||
parser.add_argument('--weights_file_path', default=defaults.weights_file_path,
|
||||
help='Path to the weights file to use for rolling game settings, urls are also valid')
|
||||
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
|
||||
help='Path to the weights file to use for rolling game options, urls are also valid')
|
||||
parser.add_argument('--sameoptions', help='Rolls options per weights file rather than per player',
|
||||
action='store_true')
|
||||
parser.add_argument('--player_files_path', default=defaults.player_files_path,
|
||||
help="Input directory for player files.")
|
||||
@@ -104,8 +103,8 @@ def main(args=None, callback=ERmain):
|
||||
del(meta_weights["meta_description"])
|
||||
except Exception as e:
|
||||
raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e
|
||||
if args.samesettings:
|
||||
raise Exception("Cannot mix --samesettings with --meta")
|
||||
if args.sameoptions:
|
||||
raise Exception("Cannot mix --sameoptions with --meta")
|
||||
else:
|
||||
meta_weights = None
|
||||
player_id = 1
|
||||
@@ -148,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
|
||||
@@ -157,7 +155,7 @@ def main(args=None, callback=ERmain):
|
||||
erargs.skip_output = args.skip_output
|
||||
|
||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
||||
for fname, yamls in weights_cache.items()}
|
||||
|
||||
if meta_weights:
|
||||
@@ -311,13 +309,6 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
||||
return new_name
|
||||
|
||||
|
||||
def prefer_int(input_data: str) -> Union[str, int]:
|
||||
try:
|
||||
return int(input_data)
|
||||
except:
|
||||
return input_data
|
||||
|
||||
|
||||
def roll_percentage(percentage: Union[int, float]) -> bool:
|
||||
"""Roll a percentage chance.
|
||||
percentage is expected to be in range [0, 100]"""
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -70,7 +70,7 @@ def install_pkg_resources(yes=False):
|
||||
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"])
|
||||
|
||||
|
||||
def update(yes=False, force=False):
|
||||
def update(yes: bool = False, force: bool = False) -> None:
|
||||
global update_ran
|
||||
if not update_ran:
|
||||
update_ran = True
|
||||
|
||||
@@ -586,7 +586,7 @@ class Context:
|
||||
self.location_check_points = savedata["game_options"]["location_check_points"]
|
||||
self.server_password = savedata["game_options"]["server_password"]
|
||||
self.password = savedata["game_options"]["password"]
|
||||
self.release_mode = savedata["game_options"].get("release_mode", savedata["game_options"].get("forfeit_mode", "goal"))
|
||||
self.release_mode = savedata["game_options"]["release_mode"]
|
||||
self.remaining_mode = savedata["game_options"]["remaining_mode"]
|
||||
self.collect_mode = savedata["game_options"]["collect_mode"]
|
||||
self.item_cheat = savedata["game_options"]["item_cheat"]
|
||||
@@ -631,8 +631,6 @@ class Context:
|
||||
|
||||
def _set_options(self, server_options: dict):
|
||||
for key, value in server_options.items():
|
||||
if key == "forfeit_mode":
|
||||
key = "release_mode"
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
@@ -1347,6 +1345,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
"Sorry, !remaining requires you to have beaten the game on this server")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_missing(self, filter_text="") -> bool:
|
||||
"""List all missing location checks from the server's perspective.
|
||||
Can be given text, which will be used as filter."""
|
||||
@@ -1356,7 +1355,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if locations:
|
||||
names = [self.ctx.location_names[location] for location in locations]
|
||||
if filter_text:
|
||||
names = [name for name in names if filter_text in name]
|
||||
location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]]
|
||||
if filter_text in location_groups: # location group name
|
||||
names = [name for name in names if name in location_groups[filter_text]]
|
||||
else:
|
||||
names = [name for name in names if filter_text in name]
|
||||
texts = [f'Missing: {name}' for name in names]
|
||||
if filter_text:
|
||||
texts.append(f"Found {len(locations)} missing location checks, displaying {len(names)} of them.")
|
||||
@@ -1367,6 +1370,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output("No missing location checks found.")
|
||||
return True
|
||||
|
||||
@mark_raw
|
||||
def _cmd_checked(self, filter_text="") -> bool:
|
||||
"""List all done location checks from the server's perspective.
|
||||
Can be given text, which will be used as filter."""
|
||||
@@ -1376,7 +1380,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if locations:
|
||||
names = [self.ctx.location_names[location] for location in locations]
|
||||
if filter_text:
|
||||
names = [name for name in names if filter_text in name]
|
||||
location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]]
|
||||
if filter_text in location_groups: # location group name
|
||||
names = [name for name in names if name in location_groups[filter_text]]
|
||||
else:
|
||||
names = [name for name in names if filter_text in name]
|
||||
texts = [f'Checked: {name}' for name in names]
|
||||
if filter_text:
|
||||
texts.append(f"Found {len(locations)} done location checks, displaying {len(names)} of them.")
|
||||
@@ -1839,6 +1847,11 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
|
||||
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
|
||||
if new_status == ClientStatus.CLIENT_GOAL:
|
||||
ctx.on_goal_achieved(client)
|
||||
# if player has yet to ever connect to the server, they will not be in client_game_state
|
||||
if all(player in ctx.client_game_state and ctx.client_game_state[player] == ClientStatus.CLIENT_GOAL
|
||||
for player in ctx.player_names
|
||||
if player[0] == client.team and player[1] != client.slot):
|
||||
ctx.broadcast_text_all(f"Team #{client.team + 1} has completed all of their games! Congratulations!")
|
||||
|
||||
ctx.client_game_state[client.team, client.slot] = new_status
|
||||
ctx.on_client_status_change(client.team, client.slot)
|
||||
@@ -1892,7 +1905,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():
|
||||
@@ -2092,8 +2105,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
|
||||
if full_name.isnumeric():
|
||||
location, usable, response = int(full_name), True, None
|
||||
elif self.ctx.location_names_for_game(game) is not None:
|
||||
location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game))
|
||||
elif game in self.ctx.all_location_and_group_names:
|
||||
location, usable, response = get_intended_text(full_name, self.ctx.all_location_and_group_names[game])
|
||||
else:
|
||||
self.output("Can't look up location for unknown game. Hint for ID instead.")
|
||||
return False
|
||||
@@ -2101,6 +2114,11 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
if usable:
|
||||
if isinstance(location, int):
|
||||
hints = collect_hint_location_id(self.ctx, team, slot, location)
|
||||
elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]:
|
||||
hints = []
|
||||
for loc_name_from_group in self.ctx.location_name_groups[game][location]:
|
||||
if loc_name_from_group in self.ctx.location_names_for_game(game):
|
||||
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group))
|
||||
else:
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location)
|
||||
if hints:
|
||||
@@ -2116,32 +2134,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
|
||||
|
||||
31
Options.py
31
Options.py
@@ -7,6 +7,7 @@ import math
|
||||
import numbers
|
||||
import random
|
||||
import typing
|
||||
import enum
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -20,6 +21,15 @@ if typing.TYPE_CHECKING:
|
||||
import pathlib
|
||||
|
||||
|
||||
class Visibility(enum.IntFlag):
|
||||
none = 0b0000
|
||||
template = 0b0001
|
||||
simple_ui = 0b0010 # show option in simple menus, such as player-options
|
||||
complex_ui = 0b0100 # show option in complex menus, such as weighted-options
|
||||
spoiler = 0b1000
|
||||
all = 0b1111
|
||||
|
||||
|
||||
class AssembleOptions(abc.ABCMeta):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
options = attrs["options"] = {}
|
||||
@@ -102,6 +112,7 @@ T = typing.TypeVar('T')
|
||||
class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
value: T
|
||||
default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type
|
||||
visibility = Visibility.all
|
||||
|
||||
# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
|
||||
# Handled in get_option_name()
|
||||
@@ -373,7 +384,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
|
||||
@@ -1113,6 +1125,18 @@ 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):
|
||||
"""This Option has been Removed."""
|
||||
default = ""
|
||||
visibility = Visibility.none
|
||||
|
||||
def __init__(self, value: str):
|
||||
if value:
|
||||
raise Exception("Option removed, please update your options file.")
|
||||
super().__init__(value)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -1170,7 +1194,10 @@ 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] = world.options_dataclass.type_hints
|
||||
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
|
||||
}
|
||||
|
||||
with open(local_path("data", "options.yaml")) as f:
|
||||
file_data = f.read()
|
||||
|
||||
17
SNIClient.py
17
SNIClient.py
@@ -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
|
||||
@@ -564,16 +565,12 @@ 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:
|
||||
while data:
|
||||
# Divide the write into packets of 256 bytes.
|
||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||
await ctx.snes_socket.send(data[:256])
|
||||
address += 256
|
||||
data = data[256:]
|
||||
else:
|
||||
snes_logger.warning(f"Could not send data to SNES: {data}")
|
||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||
await ctx.snes_socket.send(data)
|
||||
else:
|
||||
snes_logger.warning(f"Could not send data to SNES: {data}")
|
||||
except ConnectionClosed:
|
||||
return False
|
||||
|
||||
|
||||
2
Utils.py
2
Utils.py
@@ -46,7 +46,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.4.5"
|
||||
__version__ = "0.4.6"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -51,6 +51,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 +83,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
|
||||
|
||||
app.register_blueprint(api.api_endpoints)
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
from typing import List, Tuple
|
||||
from uuid import UUID
|
||||
|
||||
from flask import Blueprint, abort
|
||||
from flask import Blueprint, abort, url_for
|
||||
|
||||
import worlds.Files
|
||||
from .. import cache
|
||||
from ..models import Room, Seed
|
||||
|
||||
@@ -21,12 +22,30 @@ def room_info(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
|
||||
def supports_apdeltapatch(game: str):
|
||||
return game in worlds.Files.AutoPatchRegister.patch_types
|
||||
downloads = []
|
||||
for slot in sorted(room.seed.slots):
|
||||
if slot.data and not supports_apdeltapatch(slot.game):
|
||||
slot_download = {
|
||||
"slot": slot.player_id,
|
||||
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
|
||||
}
|
||||
downloads.append(slot_download)
|
||||
elif slot.data:
|
||||
slot_download = {
|
||||
"slot": slot.player_id,
|
||||
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
|
||||
}
|
||||
downloads.append(slot_download)
|
||||
return {
|
||||
"tracker": room.tracker,
|
||||
"players": get_players(room.seed),
|
||||
"last_port": room.last_port,
|
||||
"last_activity": room.last_activity,
|
||||
"timeout": room.timeout
|
||||
"timeout": room.timeout,
|
||||
"downloads": downloads,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import multiprocessing
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
from uuid import UUID
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
@@ -62,6 +63,16 @@ def autohost(config: dict):
|
||||
def keep_running():
|
||||
try:
|
||||
with Locker("autohost"):
|
||||
# delete unowned user-content
|
||||
with db_session:
|
||||
# >>> bool(uuid.UUID(int=0))
|
||||
# True
|
||||
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
|
||||
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
|
||||
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
|
||||
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
|
||||
if rooms or seeds or slots:
|
||||
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
|
||||
run_guardian()
|
||||
while 1:
|
||||
time.sleep(0.1)
|
||||
@@ -191,6 +202,6 @@ def run_guardian():
|
||||
guardian = threading.Thread(name="Guardian", target=guard)
|
||||
|
||||
|
||||
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
|
||||
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
|
||||
|
||||
@@ -108,7 +108,10 @@ def roll_options(options: Dict[str, Union[dict, str]],
|
||||
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
|
||||
plando_options=plando_options)
|
||||
except Exception as e:
|
||||
results[filename] = f"Failed to generate options in {filename}: {e}"
|
||||
if e.__cause__:
|
||||
results[filename] = f"Failed to generate options in {filename}: {e} - {e.__cause__}"
|
||||
else:
|
||||
results[filename] = f"Failed to generate options in {filename}: {e}"
|
||||
else:
|
||||
results[filename] = True
|
||||
return results, rolled_results
|
||||
|
||||
@@ -49,12 +49,6 @@ def weighted_options():
|
||||
return render_template("weighted-options.html")
|
||||
|
||||
|
||||
# TODO for back compat. remove around 0.4.5
|
||||
@app.route("/games/<string:game>/player-settings")
|
||||
def player_settings(game: str):
|
||||
return redirect(url_for("player_options", game=game), 301)
|
||||
|
||||
|
||||
# Player options pages
|
||||
@app.route("/games/<string:game>/player-options")
|
||||
@cache.cached()
|
||||
|
||||
@@ -45,7 +45,15 @@ def create():
|
||||
}
|
||||
|
||||
game_options = {}
|
||||
visible: typing.Set[str] = set()
|
||||
visible_weighted: typing.Set[str] = set()
|
||||
|
||||
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
|
||||
|
||||
@@ -116,8 +124,6 @@ def create():
|
||||
else:
|
||||
logging.debug(f"{option} not exported to Web Options.")
|
||||
|
||||
player_options["gameOptions"] = game_options
|
||||
|
||||
player_options["presetOptions"] = {}
|
||||
for preset_name, preset in world.web.options_presets.items():
|
||||
player_options["presetOptions"][preset_name] = {}
|
||||
@@ -156,12 +162,23 @@ def create():
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
|
||||
json.dump(player_options, f, indent=2, separators=(',', ': '))
|
||||
json.dump(filtered_player_options, f, indent=2, separators=(',', ': '))
|
||||
|
||||
filtered_player_options["gameOptions"] = {
|
||||
option_name: option_data for option_name, option_data in game_options.items()
|
||||
if option_name in visible_weighted
|
||||
}
|
||||
|
||||
if not world.hidden and world.web.options_page is True:
|
||||
# Add the random option to Choice, TextChoice, and Toggle options
|
||||
for option in game_options.values():
|
||||
for option in filtered_player_options["gameOptions"].values():
|
||||
if option["type"] == "select":
|
||||
option["options"].append({"name": "Random", "value": "random"})
|
||||
|
||||
@@ -170,7 +187,7 @@ def create():
|
||||
|
||||
weighted_options["baseOptions"]["game"][game_name] = 0
|
||||
weighted_options["games"][game_name] = {
|
||||
"gameSettings": game_options,
|
||||
"gameSettings": filtered_player_options["gameOptions"],
|
||||
"gameItems": tuple(world.item_names),
|
||||
"gameItemGroups": [
|
||||
group for group in world.item_name_groups.keys() if group != "Everything"
|
||||
|
||||
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)
|
||||
});
|
||||
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: /
|
||||
@@ -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;
|
||||
}
|
||||
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,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>
|
||||
@@ -47,9 +47,6 @@
|
||||
{% elif patch.game | supports_apdeltapatch %}
|
||||
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
|
||||
Download Patch File...</a>
|
||||
{% elif patch.game == "Dark Souls III" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download JSON File...</a>
|
||||
{% elif patch.game == "Final Fantasy Mystic Quest" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMQ File...</a>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<br /><br />
|
||||
|
||||
To start playing a game, you'll first need to <a href="/generate">generate a randomized game</a>.
|
||||
You'll need to upload either a config file or a zip file containing one more config files.
|
||||
You'll need to upload one or more config files (YAMLs) or a zip file containing one or more config files.
|
||||
<br /><br />
|
||||
|
||||
If you have already generated a game and just need to host it, this site can<br />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<th class="center">Players</th>
|
||||
<th>Created (UTC)</th>
|
||||
<th>Last Activity (UTC)</th>
|
||||
<th>Mark for deletion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -35,6 +36,7 @@
|
||||
<td>{{ room.seed.slots|length }}</td>
|
||||
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
<td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -51,6 +53,7 @@
|
||||
<th>Seed</th>
|
||||
<th class="center">Players</th>
|
||||
<th>Created (UTC)</th>
|
||||
<th>Mark for deletion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -60,6 +63,7 @@
|
||||
<td>{% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %}
|
||||
</td>
|
||||
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
<td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@ import zipfile
|
||||
import zlib
|
||||
|
||||
from io import BytesIO
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
from flask import request, flash, redirect, url_for, session, render_template, abort
|
||||
from markupsafe import Markup
|
||||
from pony.orm import commit, flush, select, rollback
|
||||
from pony.orm.core import TransactionIntegrityError
|
||||
@@ -219,3 +219,29 @@ def user_content():
|
||||
rooms = select(room for room in Room if room.owner == session["_id"])
|
||||
seeds = select(seed for seed in Seed if seed.owner == session["_id"])
|
||||
return render_template("userContent.html", rooms=rooms, seeds=seeds)
|
||||
|
||||
|
||||
@app.route("/disown_seed/<suuid:seed>", methods=["GET"])
|
||||
def disown_seed(seed):
|
||||
seed = Seed.get(id=seed)
|
||||
if not seed:
|
||||
return abort(404)
|
||||
if seed.owner != session["_id"]:
|
||||
return abort(403)
|
||||
|
||||
seed.owner = 0
|
||||
|
||||
return redirect(url_for("user_content"))
|
||||
|
||||
|
||||
@app.route("/disown_room/<suuid:room>", methods=["GET"])
|
||||
def disown_room(room):
|
||||
room = Room.get(id=room)
|
||||
if not room:
|
||||
return abort(404)
|
||||
if room.owner != session["_id"]:
|
||||
return abort(403)
|
||||
|
||||
room.owner = 0
|
||||
|
||||
return redirect(url_for("user_content"))
|
||||
|
||||
Binary file not shown.
@@ -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,19 @@ 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 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 +170,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 +181,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 +194,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 +246,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 +262,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 +280,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 +369,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 +383,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 +394,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 +489,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()
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
/worlds/celeste64/ @PoryGone
|
||||
|
||||
# ChecksFinder
|
||||
/worlds/checksfinder/ @jonloveslegos
|
||||
/worlds/checksfinder/ @SunCatMC
|
||||
|
||||
# Clique
|
||||
/worlds/clique/ @ThePhar
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Archipelago Settings API
|
||||
|
||||
The settings API describes how to use installation-wide config and let the user configure them, like paths, etc. using
|
||||
host.yaml. For the player settings / player yamls see [options api.md](options api.md).
|
||||
host.yaml. For the player options / player yamls see [options api.md](options api.md).
|
||||
|
||||
The settings API replaces `Utils.get_options()` and `Utils.get_default_options()`
|
||||
as well as the predefined `host.yaml` in the repository.
|
||||
|
||||
@@ -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,7 +38,7 @@
|
||||
# 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
|
||||
|
||||
@@ -50,3 +50,7 @@
|
||||
|
||||
# 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
|
||||
|
||||
@@ -380,11 +380,6 @@ from BaseClasses import Location
|
||||
|
||||
class MyGameLocation(Location):
|
||||
game: str = "My Game"
|
||||
|
||||
# override constructor to automatically mark event locations as such
|
||||
def __init__(self, player: int, name="", code=None, parent=None) -> None:
|
||||
super(MyGameLocation, self).__init__(player, name, code, parent)
|
||||
self.event = code is None
|
||||
```
|
||||
|
||||
in your `__init__.py` or your `locations.py`.
|
||||
|
||||
18
kvui.py
18
kvui.py
@@ -740,15 +740,17 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
||||
|
||||
def _handle_item_name(self, node: JSONMessagePart):
|
||||
flags = node.get("flags", 0)
|
||||
item_types = []
|
||||
if flags & 0b001: # advancement
|
||||
itemtype = "progression"
|
||||
elif flags & 0b010: # useful
|
||||
itemtype = "useful"
|
||||
elif flags & 0b100: # trap
|
||||
itemtype = "trap"
|
||||
else:
|
||||
itemtype = "normal"
|
||||
node.setdefault("refs", []).append("Item Class: " + itemtype)
|
||||
item_types.append("progression")
|
||||
if flags & 0b010: # useful
|
||||
item_types.append("useful")
|
||||
if flags & 0b100: # trap
|
||||
item_types.append("trap")
|
||||
if not item_types:
|
||||
item_types.append("normal")
|
||||
|
||||
node.setdefault("refs", []).append("Item Class: " + ", ".join(item_types))
|
||||
return super(KivyJSONtoTextParser, self)._handle_item_name(node)
|
||||
|
||||
def _handle_player_id(self, node: JSONMessagePart):
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
# What is this file?
|
||||
# This file contains options which allow you to configure your multiworld experience while allowing others
|
||||
# to play how they want as well.
|
||||
|
||||
# How do I use it?
|
||||
# The options in this file are weighted. This means the higher number you assign to a value, the more
|
||||
# chances you have for that option to be chosen. For example, an option like this:
|
||||
#
|
||||
# map_shuffle:
|
||||
# on: 5
|
||||
# off: 15
|
||||
#
|
||||
# Means you have 5 chances for map shuffle to occur, and 15 chances for map shuffle to be turned off
|
||||
|
||||
# I've never seen a file like this before. What characters am I allowed to use?
|
||||
# This is a .yaml file. You are allowed to use most characters.
|
||||
# To test if your yaml is valid or not, you can use this website:
|
||||
# http://www.yamllint.com/
|
||||
|
||||
description: Template Name # Used to describe your yaml. Useful if you have multiple files
|
||||
name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
#{player} will be replaced with the player's slot number.
|
||||
#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
|
||||
#{number} will be replaced with the counter value of the name.
|
||||
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
|
||||
game: # Pick a game to play
|
||||
A Link to the Past: 1
|
||||
requires:
|
||||
version: 0.4.4 # Version of Archipelago required for this yaml to work as expected.
|
||||
A Link to the Past:
|
||||
progression_balancing:
|
||||
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||
# A lower setting means more getting stuck. A higher setting means less getting stuck.
|
||||
#
|
||||
# You can define additional values between the minimum and maximum values.
|
||||
# Minimum value is 0
|
||||
# Maximum value is 99
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
disabled: 0 # equivalent to 0
|
||||
normal: 50 # equivalent to 50
|
||||
extreme: 0 # equivalent to 99
|
||||
|
||||
accessibility:
|
||||
# Set rules for reachability of your items/locations.
|
||||
# Locations: ensure everything can be reached and acquired.
|
||||
# Items: ensure all logically relevant items can be acquired.
|
||||
# Minimal: ensure what is needed to reach your goal can be acquired.
|
||||
locations: 0
|
||||
items: 50
|
||||
minimal: 0
|
||||
|
||||
local_items:
|
||||
# Forces these items to be in their native world.
|
||||
[ ]
|
||||
|
||||
non_local_items:
|
||||
# Forces these items to be outside their native world.
|
||||
[ ]
|
||||
|
||||
start_inventory:
|
||||
# Start with these items.
|
||||
{ }
|
||||
|
||||
start_hints:
|
||||
# Start with these item's locations prefilled into the !hint command.
|
||||
[ ]
|
||||
|
||||
start_location_hints:
|
||||
# Start with these locations and their item prefilled into the !hint command
|
||||
[ ]
|
||||
|
||||
exclude_locations:
|
||||
# Prevent these locations from having an important item
|
||||
[ ]
|
||||
|
||||
priority_locations:
|
||||
# Prevent these locations from having an unimportant item
|
||||
[ ]
|
||||
|
||||
item_links:
|
||||
# Share part of your item pool with other players.
|
||||
[ ]
|
||||
|
||||
### Logic Section ###
|
||||
glitches_required: # Determine the logic required to complete the seed
|
||||
none: 50 # No glitches required
|
||||
minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
|
||||
overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches
|
||||
hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons.
|
||||
no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.
|
||||
# Other players items are placed into your world under HMG logic
|
||||
dark_room_logic: # Logic for unlit dark rooms
|
||||
lamp: 50 # require the Lamp for these rooms to be considered accessible.
|
||||
torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access
|
||||
none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
|
||||
restrict_dungeon_item_on_boss: # aka ambrosia boss items
|
||||
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
|
||||
off: 50
|
||||
### End of Logic Section ###
|
||||
bigkey_shuffle: # Big Key Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
start_with: 0
|
||||
smallkey_shuffle: # Small Key Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
universal: 0
|
||||
start_with: 0
|
||||
key_drop_shuffle: # Shuffle keys found in pots or dropped from killed enemies
|
||||
off: 50
|
||||
on: 0
|
||||
compass_shuffle: # Compass Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
start_with: 0
|
||||
map_shuffle: # Map Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
start_with: 0
|
||||
dungeon_counters:
|
||||
on: 0 # Always display amount of items checked in a dungeon
|
||||
pickup: 50 # Show when compass is picked up
|
||||
default: 0 # Show when compass is picked up if the compass itself is shuffled
|
||||
off: 0 # Never show item count in dungeons
|
||||
progressive: # Enable or disable progressive items (swords, shields, bow)
|
||||
on: 50 # All items are progressive
|
||||
off: 0 # No items are progressive
|
||||
grouped_random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
|
||||
entrance_shuffle:
|
||||
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
|
||||
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
|
||||
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
|
||||
dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
|
||||
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
|
||||
restricted: 0 # Less strict than simple
|
||||
full: 0 # Less strict than restricted
|
||||
crossed: 0 # Less strict than full
|
||||
insanity: 0 # Very few grouping rules. Good luck
|
||||
# you can also define entrance shuffle seed, like so:
|
||||
crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
|
||||
# however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
|
||||
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
|
||||
goals:
|
||||
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
|
||||
crystals: 0 # Only killing Ganon is required. However, items may still be placed in GT
|
||||
bosses: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
|
||||
pedestal: 0 # Pull the Triforce from the Master Sword pedestal
|
||||
ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
|
||||
triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
|
||||
local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
|
||||
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
|
||||
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
|
||||
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
|
||||
open_pyramid:
|
||||
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
|
||||
auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
|
||||
open: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
|
||||
closed: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
|
||||
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
||||
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
||||
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
||||
available: 50 # available = triforce_pieces_available
|
||||
triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
|
||||
# Format "pieces: chance"
|
||||
0: 0
|
||||
5: 50
|
||||
10: 50
|
||||
15: 0
|
||||
20: 0
|
||||
triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
|
||||
# Format "pieces: chance"
|
||||
100: 0 #No extra
|
||||
150: 50 #Half the required will be added as extra
|
||||
200: 0 #There are the double of the required ones available.
|
||||
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
25: 0
|
||||
30: 50
|
||||
40: 0
|
||||
50: 0
|
||||
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
15: 0
|
||||
20: 50
|
||||
30: 0
|
||||
40: 0
|
||||
50: 0
|
||||
crystals_needed_for_gt: # Crystals required to open GT
|
||||
0: 0
|
||||
7: 50
|
||||
random: 0
|
||||
random-low: 0 # any valid number, weighted towards the lower end
|
||||
random-middle: 0 # any valid number, weighted towards the central range
|
||||
random-high: 0 # any valid number, weighted towards the higher end
|
||||
crystals_needed_for_ganon: # Crystals required to hurt Ganon
|
||||
0: 0
|
||||
7: 50
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-middle: 0
|
||||
random-high: 0
|
||||
mode:
|
||||
standard: 0 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
|
||||
open: 50 # Begin the game from your choice of Link's House or the Sanctuary
|
||||
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
|
||||
retro_bow:
|
||||
on: 0 # Zelda-1 like mode. You have to purchase a quiver to shoot arrows using rupees.
|
||||
off: 50
|
||||
retro_caves:
|
||||
on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion.
|
||||
off: 50
|
||||
hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
|
||||
'on': 50
|
||||
'off': 0
|
||||
full: 0
|
||||
scams: # If on, these Merchants will no longer tell you what they're selling.
|
||||
'off': 50
|
||||
'king_zora': 0
|
||||
'bottle_merchant': 0
|
||||
'all': 0
|
||||
swordless:
|
||||
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
|
||||
off: 1
|
||||
item_pool:
|
||||
easy: 0 # Doubled upgrades, progressives, and etc
|
||||
normal: 50 # Item availability remains unchanged from vanilla game
|
||||
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
|
||||
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
|
||||
item_functionality:
|
||||
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
|
||||
normal: 50 # Vanilla item functionality
|
||||
hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
|
||||
expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
|
||||
tile_shuffle: # Randomize the tile layouts in flying tile rooms
|
||||
on: 0
|
||||
off: 50
|
||||
misery_mire_medallion: # required medallion to open Misery Mire front entrance
|
||||
random: 50
|
||||
Ether: 0
|
||||
Bombos: 0
|
||||
Quake: 0
|
||||
turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
|
||||
random: 50
|
||||
Ether: 0
|
||||
Bombos: 0
|
||||
Quake: 0
|
||||
### Enemizer Section ###
|
||||
boss_shuffle:
|
||||
none: 50 # Vanilla bosses
|
||||
basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
|
||||
full: 0 # 3 bosses can occur twice
|
||||
chaos: 0 # Any boss can appear any amount of times
|
||||
singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
|
||||
enemy_shuffle: # Randomize enemy placement
|
||||
on: 0
|
||||
off: 50
|
||||
killable_thieves: # Make thieves killable
|
||||
on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
|
||||
off: 50
|
||||
bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
|
||||
on: 0
|
||||
off: 50
|
||||
enemy_damage:
|
||||
default: 50 # Vanilla enemy damage
|
||||
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
|
||||
chaos: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
|
||||
enemy_health:
|
||||
default: 50 # Vanilla enemy HP
|
||||
easy: 0 # Enemies have reduced health
|
||||
hard: 0 # Enemies have increased health
|
||||
expert: 0 # Enemies have greatly increased health
|
||||
pot_shuffle:
|
||||
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
|
||||
'off': 50 # Default pot item locations
|
||||
### End of Enemizer Section ###
|
||||
### Beemizer ###
|
||||
# can add weights for any whole number between 0 and 100
|
||||
beemizer_total_chance: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
|
||||
0: 50 # No junk fill items are replaced (Beemizer is off)
|
||||
25: 0 # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
50: 0 # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
75: 0 # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
100: 0 # All junk fill items (rupees, bombs and arrows) are replaced with bees
|
||||
beemizer_trap_chance:
|
||||
60: 50 # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee
|
||||
70: 0 # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee
|
||||
80: 0 # 80% chance for each beemizer replacement to be a trap, 20% chance to be a single bee
|
||||
90: 0 # 90% chance for each beemizer replacement to be a trap, 10% chance to be a single bee
|
||||
100: 0 # All beemizer replacements are traps
|
||||
### Shop Settings ###
|
||||
shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
|
||||
0: 50
|
||||
5: 0
|
||||
15: 0
|
||||
30: 0
|
||||
random: 0 # 0 to 30 evenly distributed
|
||||
shop_price_modifier: # Percentage modifier for shuffled item prices in shops
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
400: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
100: 50
|
||||
shop_shuffle:
|
||||
none: 50
|
||||
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
|
||||
f: 0 # Generate new default inventories for every shop independently
|
||||
i: 0 # Shuffle default inventories of the shops around
|
||||
p: 0 # Randomize the prices of the items in shop inventories
|
||||
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
|
||||
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
|
||||
P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees
|
||||
ip: 0 # Shuffle inventories and randomize prices
|
||||
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
|
||||
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
|
||||
# You can add more combos
|
||||
### End of Shop Section ###
|
||||
shuffle_prizes: # aka drops
|
||||
none: 0 # do not shuffle prize packs
|
||||
g: 50 # shuffle "general" prize packs, as in enemy, tree pull, dig etc.
|
||||
b: 0 # shuffle "bonk" prize packs
|
||||
bg: 0 # shuffle both
|
||||
timer:
|
||||
none: 50 # No timer will be displayed.
|
||||
timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
|
||||
timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
|
||||
ohko: 0 # Timer always at zero. Permanent OHKO.
|
||||
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
|
||||
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
|
||||
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
|
||||
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
|
||||
10: 50
|
||||
20: 0
|
||||
30: 0
|
||||
60: 0
|
||||
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
|
||||
-2: 50
|
||||
1: 0
|
||||
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
|
||||
1: 0
|
||||
2: 50
|
||||
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
|
||||
4: 50
|
||||
10: 0
|
||||
15: 0
|
||||
glitch_boots:
|
||||
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
|
||||
off: 0
|
||||
# rom options section
|
||||
random_sprite_on_event: # An alternative to specifying randomonhit / randomonexit / etc... in sprite down below.
|
||||
enabled: # If enabled, sprite down below is ignored completely, (although it may become the sprite pool)
|
||||
on: 0
|
||||
off: 1
|
||||
on_hit: # Random sprite on hit. Being hit by things that cause 0 damage still counts.
|
||||
on: 1
|
||||
off: 0
|
||||
on_enter: # Random sprite on underworld entry. Note that entering hobo counts.
|
||||
on: 0
|
||||
off: 1
|
||||
on_exit: # Random sprite on underworld exit. Exiting hobo does not count.
|
||||
on: 0
|
||||
off: 1
|
||||
on_slash: # Random sprite on sword slash. Note, it still counts if you attempt to slash while swordless.
|
||||
on: 0
|
||||
off: 1
|
||||
on_item: # Random sprite on getting an item. Anything that causes you to hold an item above your head counts.
|
||||
on: 0
|
||||
off: 1
|
||||
on_bonk: # Random sprite on bonk.
|
||||
on: 0
|
||||
off: 1
|
||||
on_everything: # Random sprite on ALL currently implemented events, even if not documented at present time.
|
||||
on: 0
|
||||
off: 1
|
||||
use_weighted_sprite_pool: # Always on if no sprite_pool exists, otherwise it controls whether to use sprite as a weighted sprite pool
|
||||
on: 0
|
||||
off: 1
|
||||
#sprite_pool: # When specified, limits the pool of sprites used for randomon-event to the specified pool. Uncomment to use this.
|
||||
# - link
|
||||
# - pride link
|
||||
# - penguin link
|
||||
# - random # You can specify random multiple times for however many potentially unique random sprites you want in your pool.
|
||||
sprite: # Enter the name of your preferred sprite and weight it appropriately
|
||||
random: 0
|
||||
randomonhit: 0 # Random sprite on hit
|
||||
randomonenter: 0 # Random sprite on entering the underworld.
|
||||
randomonexit: 0 # Random sprite on exiting the underworld.
|
||||
randomonslash: 0 # Random sprite on sword slashes
|
||||
randomonitem: 0 # Random sprite on getting items.
|
||||
randomonbonk: 0 # Random sprite on bonk.
|
||||
# You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit.
|
||||
randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events.
|
||||
Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
|
||||
music: # If "off", all in-game music will be disabled
|
||||
on: 50
|
||||
off: 0
|
||||
quickswap: # Enable switching items by pressing the L+R shoulder buttons
|
||||
on: 50
|
||||
off: 0
|
||||
triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala
|
||||
normal: 0 # original behavior (always visible)
|
||||
hide_goal: 50 # hide counter until a piece is collected or speaking to Murahadala
|
||||
hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala
|
||||
hide_both: 0 # Hide both under above circumstances
|
||||
reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more.
|
||||
on: 50
|
||||
off: 0
|
||||
menuspeed: # Controls how fast the item menu opens and closes
|
||||
normal: 50
|
||||
instant: 0
|
||||
double: 0
|
||||
triple: 0
|
||||
quadruple: 0
|
||||
half: 0
|
||||
heartcolor: # Controls the color of your health hearts
|
||||
red: 50
|
||||
blue: 0
|
||||
green: 0
|
||||
yellow: 0
|
||||
random: 0
|
||||
heartbeep: # Controls the frequency of the low-health beeping
|
||||
double: 0
|
||||
normal: 50
|
||||
half: 0
|
||||
quarter: 0
|
||||
off: 0
|
||||
ow_palettes: # Change the colors of the overworld
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
uw_palettes: # Change the colors of caves and dungeons
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
hud_palettes: # Change the colors of the hud
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
sword_palettes: # Change the colors of swords
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
shield_palettes: # Change the colors of shields
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
|
||||
# triggers that replace options upon rolling certain options
|
||||
legacy_weapons: # this is not an actual option, just a set of weights to trigger from
|
||||
trigger_disabled: 50
|
||||
randomized: 0 # Swords are placed randomly throughout the world
|
||||
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
|
||||
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
|
||||
swordless: 0 # swordless mode
|
||||
|
||||
death_link:
|
||||
false: 50
|
||||
true: 0
|
||||
|
||||
allow_collect: # Allows for !collect / co-op to auto-open chests containing items for other players.
|
||||
# Off by default, because it currently crashes on real hardware.
|
||||
false: 50
|
||||
true: 0
|
||||
|
||||
linked_options:
|
||||
- name: crosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
A Link to the Past:
|
||||
entrance_shuffle: crossed
|
||||
bigkey_shuffle: true
|
||||
compass_shuffle: true
|
||||
map_shuffle: true
|
||||
smallkey_shuffle: true
|
||||
percentage: 0 # Set this to the percentage chance you want crosskeys
|
||||
- name: localcrosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
A Link to the Past:
|
||||
entrance_shuffle: crossed
|
||||
bigkey_shuffle: true
|
||||
compass_shuffle: true
|
||||
map_shuffle: true
|
||||
smallkey_shuffle: true
|
||||
local_items: # Forces keys to be local to your own world
|
||||
- "Small Keys"
|
||||
- "Big Keys"
|
||||
percentage: 0 # Set this to the percentage chance you want local crosskeys
|
||||
- name: enemizer
|
||||
options:
|
||||
A Link to the Past:
|
||||
boss_shuffle: # Subchances can be injected too, which then get rolled
|
||||
basic: 1
|
||||
full: 1
|
||||
chaos: 1
|
||||
singularity: 1
|
||||
enemy_damage:
|
||||
shuffled: 1
|
||||
chaos: 1
|
||||
enemy_health:
|
||||
easy: 1
|
||||
hard: 1
|
||||
expert: 1
|
||||
percentage: 0 # Set this to the percentage chance you want enemizer
|
||||
triggers:
|
||||
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
|
||||
- option_name: legacy_weapons
|
||||
option_result: randomized
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
- option_name: legacy_weapons
|
||||
option_result: assured
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
start_inventory:
|
||||
Progressive Sword: 1
|
||||
- option_name: legacy_weapons
|
||||
option_result: vanilla
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
plando_items:
|
||||
- items:
|
||||
Progressive Sword: 4
|
||||
locations:
|
||||
- Master Sword Pedestal
|
||||
- Pyramid Fairy - Left
|
||||
- Blacksmith
|
||||
- Link's Uncle
|
||||
- option_name: legacy_weapons
|
||||
option_result: swordless
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: on
|
||||
# end of legacy weapons block
|
||||
- option_name: enemy_damage # targets enemy_damage
|
||||
option_category: A Link to the Past
|
||||
option_result: shuffled # if it rolls shuffled
|
||||
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
|
||||
options: # then inserts these options
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Application settings / host.yaml interface using type hints.
|
||||
This is different from player settings.
|
||||
This is different from player options.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
@@ -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")
|
||||
|
||||
|
||||
2
setup.py
2
setup.py
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
try:
|
||||
requirement = 'cx-Freeze>=6.15.10'
|
||||
requirement = 'cx-Freeze>=6.15.16,<7'
|
||||
import pkg_resources
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
|
||||
@@ -221,7 +221,7 @@ class WorldTestBase(unittest.TestCase):
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
if item.location and item.location.event and item.location in self.multiworld.state.events:
|
||||
if item.location and item.advancement and item.location in self.multiworld.state.events:
|
||||
self.multiworld.state.events.remove(item.location)
|
||||
self.multiworld.state.remove(item)
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ def fill_region(multiworld: MultiWorld, region: Region, items: List[Item]) -> Li
|
||||
return items
|
||||
item = items.pop(0)
|
||||
multiworld.push_item(location, item, False)
|
||||
location.event = item.advancement
|
||||
|
||||
return items
|
||||
|
||||
@@ -489,7 +488,6 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
player1 = generate_player_data(multiworld, 1, 1, 1)
|
||||
location = player1.locations[0]
|
||||
location.address = None
|
||||
location.event = True
|
||||
item = player1.prog_items[0]
|
||||
item.code = None
|
||||
location.place_locked_item(item)
|
||||
@@ -527,13 +525,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertEqual(locations[0].item, basic_items[1])
|
||||
self.assertFalse(locations[0].event)
|
||||
self.assertFalse(locations[0].advancement)
|
||||
self.assertEqual(locations[1].item, prog_items[0])
|
||||
self.assertTrue(locations[1].event)
|
||||
self.assertTrue(locations[1].advancement)
|
||||
self.assertEqual(locations[2].item, prog_items[1])
|
||||
self.assertTrue(locations[2].event)
|
||||
self.assertTrue(locations[2].advancement)
|
||||
self.assertEqual(locations[3].item, basic_items[0])
|
||||
self.assertFalse(locations[3].event)
|
||||
self.assertFalse(locations[3].advancement)
|
||||
|
||||
def test_excluded_distribute(self):
|
||||
"""Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
|
||||
@@ -746,7 +744,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
for item in multiworld.get_items():
|
||||
self.assertEqual(item.player, item.location.player)
|
||||
self.assertFalse(item.location.event, False)
|
||||
self.assertFalse(item.location.advancement, False)
|
||||
|
||||
def test_early_items(self) -> None:
|
||||
"""Test that the early items API successfully places items early"""
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
|
||||
|
||||
from typing import Any, Optional, Protocol
|
||||
from ..graphics import FillType_Drawable, FillType_Vec
|
||||
from ..graphics.texture import FillType_Drawable, FillType_Vec
|
||||
|
||||
|
||||
class FillType_BindCallback(Protocol):
|
||||
|
||||
@@ -64,7 +64,7 @@ class SuffixIdentifier:
|
||||
def __init__(self, *args: str):
|
||||
self.suffixes = args
|
||||
|
||||
def __call__(self, path: str):
|
||||
def __call__(self, path: str) -> bool:
|
||||
if isinstance(path, str):
|
||||
for suffix in self.suffixes:
|
||||
if path.endswith(suffix):
|
||||
|
||||
@@ -234,8 +234,11 @@ async def _run_game(rom: str):
|
||||
|
||||
|
||||
async def _patch_and_run_game(patch_file: str):
|
||||
metadata, output_file = Patch.create_rom_file(patch_file)
|
||||
Utils.async_start(_run_game(output_file))
|
||||
try:
|
||||
metadata, output_file = Patch.create_rom_file(patch_file)
|
||||
Utils.async_start(_run_game(output_file))
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
|
||||
|
||||
def launch() -> None:
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -43,7 +43,7 @@ an experience customized for their taste, and different players in the same mult
|
||||
|
||||
You can generate a yaml or download a template by visiting the [Adventure Options Page](/games/Adventure/player-options)
|
||||
|
||||
### What are recommended settings to tweak for beginners to the rando?
|
||||
### What are recommended options to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
|
||||
the credits room.
|
||||
|
||||
@@ -42,7 +42,7 @@ une expérience personnalisée à leur goût, et différents joueurs dans le mê
|
||||
|
||||
### Où puis-je obtenir un fichier YAML ?
|
||||
|
||||
Vous pouvez générer un yaml ou télécharger un modèle en visitant la [page des paramètres d'aventure](/games/Adventure/player-settings)
|
||||
Vous pouvez générer un yaml ou télécharger un modèle en visitant la [page des paramètres d'aventure](/games/Adventure/player-options)
|
||||
|
||||
### Quels sont les paramètres recommandés pour s'initier à la rando ?
|
||||
Régler la difficulty_switch_a et réduire la vitesse des dragons rend les dragons plus faciles à éviter. Ajouter Calice à
|
||||
@@ -72,4 +72,4 @@ configuré pour le faire automatiquement.
|
||||
Pour connecter le client au multiserveur, mettez simplement `<adresse>:<port>` dans le champ de texte en haut et appuyez sur Entrée (si le
|
||||
le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect <adresse> :<port> [mot de passe]`)
|
||||
|
||||
Appuyez sur Réinitialiser et commencez à jouer
|
||||
Appuyez sur Réinitialiser et commencez à jouer
|
||||
|
||||
@@ -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):
|
||||
@@ -2657,6 +2658,10 @@ mandatory_connections = [('Links House S&Q', 'Links House'),
|
||||
('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'),
|
||||
('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'),
|
||||
('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'),
|
||||
('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'),
|
||||
('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'),
|
||||
('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'),
|
||||
('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'),
|
||||
('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'),
|
||||
@@ -2815,6 +2820,10 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
|
||||
('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'),
|
||||
('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'),
|
||||
('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'),
|
||||
('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'),
|
||||
('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'),
|
||||
('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'),
|
||||
('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'),
|
||||
('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'),
|
||||
|
||||
@@ -408,14 +408,16 @@ def create_inverted_regions(world, player):
|
||||
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
|
||||
['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'],
|
||||
['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase',
|
||||
'Turtle Rock Big Key Door']),
|
||||
['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door',
|
||||
'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
|
||||
@@ -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:
|
||||
@@ -253,10 +253,8 @@ def generate_itempool(world):
|
||||
region.locations.append(loc)
|
||||
|
||||
multiworld.push_item(loc, item_factory('Triforce', world), False)
|
||||
loc.event = True
|
||||
loc.locked = True
|
||||
|
||||
multiworld.get_location('Ganon', player).event = True
|
||||
multiworld.get_location('Ganon', player).locked = True
|
||||
event_pairs = [
|
||||
('Agahnim 1', 'Beat Agahnim 1'),
|
||||
@@ -273,18 +271,18 @@ def generate_itempool(world):
|
||||
location = multiworld.get_location(location_name, player)
|
||||
event = item_factory(event_name, world)
|
||||
multiworld.push_item(location, event, False)
|
||||
location.event = location.locked = True
|
||||
location.locked = True
|
||||
|
||||
|
||||
# set up item pool
|
||||
additional_triforce_pieces = 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_count = (
|
||||
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_count, additional_triforce_pieces = (
|
||||
get_pool_core(multiworld, player))
|
||||
|
||||
for item in precollected_items:
|
||||
multiworld.push_precollected(item_factory(item, world))
|
||||
@@ -319,11 +317,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))
|
||||
@@ -336,13 +334,10 @@ 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_count = treasure_hunt_count % 999
|
||||
|
||||
dungeon_items = [item for item in get_dungeon_item_pool_player(world)
|
||||
if item.name not in multiworld.worlds[player].dungeon_local_item_names]
|
||||
@@ -371,7 +366,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):
|
||||
@@ -466,8 +461,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:
|
||||
@@ -479,7 +472,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:]
|
||||
|
||||
@@ -500,8 +496,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)
|
||||
|
||||
@@ -593,9 +589,8 @@ 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_count: int = 1
|
||||
|
||||
diff = difficulties[difficulty]
|
||||
pool.extend(diff.alwaysitems)
|
||||
@@ -686,19 +681,18 @@ def get_pool_core(world, player: int):
|
||||
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
|
||||
triforce_pieces = 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
|
||||
percentage = float(world.triforce_pieces_percentage[player].value) / 100
|
||||
triforce_pieces = int(round(world.triforce_pieces_required[player].value * percentage, 0))
|
||||
else: # available
|
||||
triforce_pieces = world.triforce_pieces_available[player].value
|
||||
|
||||
triforce_pieces = max(triforce_pieces, world.triforce_pieces_required[player].value)
|
||||
triforce_pieces = min(90, max(triforce_pieces, 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'
|
||||
|
||||
for extra in diff.extras:
|
||||
if extraitems >= len(extra):
|
||||
@@ -739,7 +733,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_count,
|
||||
additional_pieces_to_place)
|
||||
|
||||
|
||||
@@ -754,9 +748,8 @@ 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_count: int = 1
|
||||
|
||||
def place_item(loc, item):
|
||||
assert loc not in placed_items, "cannot place item twice"
|
||||
@@ -852,7 +845,6 @@ def make_custom_item_pool(world, 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'
|
||||
|
||||
if timer in ['display', 'timed', 'timed_countdown']:
|
||||
clock_mode = 'countdown' if timer == 'timed_countdown' else 'stopwatch'
|
||||
@@ -897,4 +889,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_count)
|
||||
|
||||
@@ -2,7 +2,7 @@ import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\
|
||||
FreeText
|
||||
FreeText, Removed
|
||||
|
||||
|
||||
class GlitchesRequired(Choice):
|
||||
@@ -716,9 +716,8 @@ class BeemizerTrapChance(BeemizerRange):
|
||||
display_name = "Beemizer Trap Chance"
|
||||
|
||||
|
||||
class AllowCollect(Toggle):
|
||||
"""Allows for !collect / co-op to auto-open chests containing items for other players.
|
||||
Off by default, because it currently crashes on real hardware."""
|
||||
class AllowCollect(DefaultOnToggle):
|
||||
"""Allows for !collect / co-op to auto-open chests containing items for other players."""
|
||||
display_name = "Allow Collection of checks for other players"
|
||||
|
||||
|
||||
@@ -796,4 +795,9 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"music": Music,
|
||||
"reduceflashing": ReduceFlashing,
|
||||
"triforcehud": TriforceHud,
|
||||
|
||||
# removed:
|
||||
"goals": Removed,
|
||||
"smallkey_shuffle": Removed,
|
||||
"bigkey_shuffle": Removed,
|
||||
}
|
||||
|
||||
@@ -336,13 +336,15 @@ def create_regions(world, player):
|
||||
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
|
||||
@@ -4,7 +4,7 @@ import Utils
|
||||
import worlds.Files
|
||||
|
||||
LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
|
||||
RANDOMIZERBASEHASH: str = "35d010bc148e0ea0ee68e81e330223f1"
|
||||
RANDOMIZERBASEHASH: str = "8704fb9b9fa4fad52d4d2f9a95fb5360"
|
||||
ROM_PLAYER_LIMIT: int = 255
|
||||
|
||||
import io
|
||||
@@ -868,11 +868,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}):
|
||||
# For exits that connot be reached from another, no need to apply offset fixes.
|
||||
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
|
||||
elif room_id == 0x0059 and world.fix_skullwoods_exit[player]:
|
||||
elif room_id == 0x0059 and local_world.fix_skullwoods_exit:
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x00F8)
|
||||
elif room_id == 0x004a and world.fix_palaceofdarkness_exit[player]:
|
||||
elif room_id == 0x004a and local_world.fix_palaceofdarkness_exit:
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x0640)
|
||||
elif room_id == 0x00d6 and world.fix_trock_exit[player]:
|
||||
elif room_id == 0x00d6 and local_world.fix_trock_exit:
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x0134)
|
||||
elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x00A4)
|
||||
@@ -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_count)
|
||||
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 (
|
||||
@@ -1674,14 +1674,14 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0x4E3BB, 0xEB)
|
||||
|
||||
# fix trock doors for reverse entrances
|
||||
if world.fix_trock_doors[player]:
|
||||
if local_world.fix_trock_doors:
|
||||
rom.write_byte(0xFED31, 0x0E) # preopen bombable exit
|
||||
rom.write_byte(0xFEE41, 0x0E) # preopen bombable exit
|
||||
# included unconditionally in base2current
|
||||
# rom.write_byte(0xFE465, 0x1E) # remove small key door on backside of big key door
|
||||
else:
|
||||
rom.write_byte(0xFED31, 0x2A) # preopen bombable exit
|
||||
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
|
||||
rom.write_byte(0xFED31, 0x2A) # bombable exit
|
||||
rom.write_byte(0xFEE41, 0x2A) # bombable exit
|
||||
|
||||
if world.tile_shuffle[player]:
|
||||
tile_set = TileSet.get_random_tile_set(world.per_slot_randoms[player])
|
||||
@@ -2397,6 +2397,9 @@ def write_strings(rom, world, player):
|
||||
if hint_count:
|
||||
locations = world.find_items_in_locations(items_to_hint, player, True)
|
||||
local_random.shuffle(locations)
|
||||
# make locked locations less likely to appear as hint,
|
||||
# chances are the lock means the player already knows.
|
||||
locations.sort(key=lambda sorting_location: not sorting_location.locked)
|
||||
for x in range(min(hint_count, len(locations))):
|
||||
this_location = locations.pop()
|
||||
this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.'
|
||||
@@ -2418,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)
|
||||
@@ -2479,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_count > 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_count, world.triforce_pieces_available[player])
|
||||
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_count, world.triforce_pieces_available[player])
|
||||
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.'
|
||||
@@ -2497,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_count > 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_count, world.triforce_pieces_available[player])
|
||||
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_count, world.triforce_pieces_available[player])
|
||||
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_count, world.triforce_pieces_available[player])
|
||||
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_count, world.triforce_pieces_available[player])
|
||||
|
||||
tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -97,7 +98,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,244 +187,251 @@ 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_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_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(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_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)
|
||||
set_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
set_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
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.
|
||||
@@ -433,96 +441,102 @@ 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_entrance('Turtle Rock Ledge Exit (West)', player), lambda state: can_use_bombs(state, 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(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 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 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 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)))
|
||||
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
|
||||
@@ -531,63 +545,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 (
|
||||
set_rule(multiworld.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)))
|
||||
|
||||
# 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):
|
||||
@@ -1097,14 +1111,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!
|
||||
@@ -1184,7 +1194,6 @@ def set_trock_key_rules(world, player):
|
||||
item = item_factory('Small Key (Turtle Rock)', world.worlds[player])
|
||||
location = world.get_location('Turtle Rock - Big Key Chest', player)
|
||||
location.place_locked_item(item)
|
||||
location.event = True
|
||||
toss_junk_item(world, player)
|
||||
|
||||
if world.accessibility[player] != 'locations':
|
||||
|
||||
@@ -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_count
|
||||
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,17 @@ 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_count: int = 1
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.dungeon_local_item_names = set()
|
||||
@@ -261,6 +272,12 @@ class ALTTPWorld(World):
|
||||
self.dungeons = {}
|
||||
self.waterfall_fairy_bottle_fill = "Bottle"
|
||||
self.pyramid_fairy_bottle_fill = "Bottle"
|
||||
self.fix_trock_doors = None
|
||||
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
|
||||
@@ -280,12 +297,21 @@ class ALTTPWorld(World):
|
||||
player = self.player
|
||||
multiworld = self.multiworld
|
||||
|
||||
self.fix_trock_doors = (multiworld.entrance_shuffle[player] != 'vanilla'
|
||||
or multiworld.mode[player] == 'inverted')
|
||||
self.fix_skullwoods_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
|
||||
'dungeons_simple']
|
||||
self.fix_palaceofdarkness_exit = multiworld.entrance_shuffle[player] not in ['dungeons_simple', 'vanilla',
|
||||
'simple', 'restricted']
|
||||
self.fix_trock_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
|
||||
'dungeons_simple']
|
||||
|
||||
# fairy bottle fills
|
||||
bottle_options = [
|
||||
"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)
|
||||
@@ -331,7 +357,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"]:
|
||||
@@ -345,42 +371,43 @@ class ALTTPWorld(World):
|
||||
|
||||
def create_regions(self):
|
||||
player = self.player
|
||||
world = self.multiworld
|
||||
multiworld = self.multiworld
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(world, player)
|
||||
if multiworld.mode[player] != 'inverted':
|
||||
create_regions(multiworld, player)
|
||||
else:
|
||||
create_inverted_regions(world, player)
|
||||
create_shops(world, player)
|
||||
create_inverted_regions(multiworld, player)
|
||||
create_shops(multiworld, player)
|
||||
self.create_dungeons()
|
||||
|
||||
if world.glitches_required[player] not in ["no_glitches", "minor_glitches"] and world.entrance_shuffle[player] in \
|
||||
{"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"}:
|
||||
world.fix_fake_world[player] = False
|
||||
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"]):
|
||||
self.fix_fake_world = False
|
||||
|
||||
# seeded entrance shuffle
|
||||
old_random = world.random
|
||||
world.random = random.Random(self.er_seed)
|
||||
old_random = multiworld.random
|
||||
multiworld.random = random.Random(self.er_seed)
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
link_entrances(world, player)
|
||||
mark_light_world_regions(world, player)
|
||||
if multiworld.mode[player] != 'inverted':
|
||||
link_entrances(multiworld, player)
|
||||
mark_light_world_regions(multiworld, player)
|
||||
for region_name, entrance_name in indirect_connections_not_inverted.items():
|
||||
world.register_indirect_condition(world.get_region(region_name, player),
|
||||
world.get_entrance(entrance_name, player))
|
||||
multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
|
||||
multiworld.get_entrance(entrance_name, player))
|
||||
else:
|
||||
link_inverted_entrances(world, player)
|
||||
mark_dark_world_regions(world, player)
|
||||
link_inverted_entrances(multiworld, player)
|
||||
mark_dark_world_regions(multiworld, player)
|
||||
for region_name, entrance_name in indirect_connections_inverted.items():
|
||||
world.register_indirect_condition(world.get_region(region_name, player),
|
||||
world.get_entrance(entrance_name, player))
|
||||
multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
|
||||
multiworld.get_entrance(entrance_name, player))
|
||||
|
||||
world.random = old_random
|
||||
plando_connect(world, player)
|
||||
multiworld.random = old_random
|
||||
plando_connect(multiworld, player)
|
||||
|
||||
for region_name, entrance_name in indirect_connections.items():
|
||||
world.register_indirect_condition(world.get_region(region_name, player),
|
||||
world.get_entrance(entrance_name, player))
|
||||
multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
|
||||
multiworld.get_entrance(entrance_name, player))
|
||||
|
||||
def collect_item(self, state: CollectionState, item: Item, remove=False):
|
||||
item_name = item.name
|
||||
@@ -424,15 +451,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):
|
||||
@@ -444,20 +472,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
|
||||
@@ -646,7 +676,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):
|
||||
@@ -687,10 +717,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}")
|
||||
@@ -801,8 +831,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
|
||||
|
||||
@@ -47,12 +47,12 @@ wählen können!
|
||||
|
||||
### Wo bekomme ich so eine YAML-Datei her?
|
||||
|
||||
Die [Player Settings](/games/A Link to the Past/player-settings) Seite auf der Website ermöglicht das einfache Erstellen
|
||||
Die [Player Options](/games/A Link to the Past/player-options) Seite auf der Website ermöglicht das einfache Erstellen
|
||||
und Herunterladen deiner eigenen `yaml` Datei. Drei verschiedene Voreinstellungen können dort gespeichert werden.
|
||||
|
||||
### Deine YAML-Datei ist gewichtet!
|
||||
|
||||
Die **Player Settings** Seite hat eine Menge Optionen, die man per Schieber einstellen kann. Das ermöglicht es,
|
||||
Die **Player Options** Seite hat eine Menge Optionen, die man per Schieber einstellen kann. Das ermöglicht es,
|
||||
verschiedene Optionen mit unterschiedlichen Wahrscheinlichkeiten in einer Kategorie ausgewürfelt zu werden
|
||||
|
||||
Als Beispiel kann man sich die Option "Map Shuffle" als einen Eimer mit Zetteln zur Abstimmung Vorstellen. So kann man
|
||||
|
||||
@@ -59,7 +59,7 @@ de multiworld puede tener diferentes opciones.
|
||||
|
||||
### Donde puedo obtener un fichero YAML?
|
||||
|
||||
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-settings)" en el sitio web te permite configurar tu
|
||||
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-options)" en el sitio web te permite configurar tu
|
||||
configuración personal y descargar un fichero "YAML".
|
||||
|
||||
### Configuración YAML avanzada
|
||||
@@ -86,7 +86,7 @@ Si quieres validar que tu fichero YAML para asegurarte que funciona correctament
|
||||
|
||||
## Generar una partida para un jugador
|
||||
|
||||
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-settings), configura tus opciones, haz
|
||||
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-options), configura tus opciones, haz
|
||||
click en el boton "Generate game".
|
||||
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
|
||||
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el Cliente no
|
||||
|
||||
@@ -60,7 +60,7 @@ peuvent avoir différentes options.
|
||||
|
||||
### Où est-ce que j'obtiens un fichier YAML ?
|
||||
|
||||
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings) vous permet de configurer vos
|
||||
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options) vous permet de configurer vos
|
||||
paramètres personnels et de les exporter vers un fichier YAML.
|
||||
|
||||
### Configuration avancée du fichier YAML
|
||||
@@ -87,7 +87,7 @@ Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous
|
||||
|
||||
## Générer une partie pour un joueur
|
||||
|
||||
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings), configurez vos options,
|
||||
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options), configurez vos options,
|
||||
et cliquez sur le bouton "Generate Game".
|
||||
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
|
||||
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
|
||||
@@ -207,4 +207,4 @@ Le logiciel recommandé pour l'auto-tracking actuellement est
|
||||
3. Sélectionnez votre appareil SNES dans la liste déroulante.
|
||||
4. Si vous voulez tracquer les petites clés ainsi que les objets des donjons, cochez la case **Race Illegal Tracking**
|
||||
5. Cliquez sur le bouton **Start Autotracking**
|
||||
6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire
|
||||
6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire
|
||||
|
||||
@@ -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,7 +13,7 @@ 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
|
||||
create_regions(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))
|
||||
|
||||
@@ -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,7 +13,7 @@ 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
|
||||
@@ -22,7 +22,6 @@ class TestInverted(TestBase, LTTPTestBase):
|
||||
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()
|
||||
|
||||
@@ -18,13 +18,12 @@ class TestInvertedMinor(TestBase, LTTPTestBase):
|
||||
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.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
|
||||
|
||||
@@ -101,20 +101,20 @@ class TestDeathMountain(TestInvertedOWG):
|
||||
["Hookshot Cave - Bottom Right", False, []],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Hookshot', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Pegasus Boots', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Hookshot Cave - Bottom Left", False, []],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Hookshot Cave - Top Left", False, []],
|
||||
["Hookshot Cave - Top Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Hookshot Cave - Top Right", False, []],
|
||||
["Hookshot Cave - Top Right", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
])
|
||||
@@ -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']],
|
||||
|
||||
@@ -18,13 +18,12 @@ class TestInvertedOWG(TestBase, LTTPTestBase):
|
||||
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.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
|
||||
|
||||
@@ -14,11 +14,10 @@ class TestMinor(TestBase, LTTPTestBase):
|
||||
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.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',
|
||||
|
||||
@@ -177,7 +177,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Bottom Right", False, []],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
|
||||
@@ -185,7 +185,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
|
||||
@@ -193,7 +193,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Top Left", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Top Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
|
||||
@@ -201,7 +201,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Top Right", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Top Right", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
])
|
||||
@@ -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.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.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
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
item_table = (
|
||||
'An Old GeoCities Profile',
|
||||
'Very Funny Joke',
|
||||
'Motivational Video',
|
||||
'Staples Easy Button',
|
||||
'One Million Dollars',
|
||||
'Replica Master Sword',
|
||||
@@ -13,7 +16,7 @@ item_table = (
|
||||
'2012 Magic the Gathering Core Set Starter Box',
|
||||
'Poke\'mon Booster Pack',
|
||||
'USB Speakers',
|
||||
'Plastic Spork',
|
||||
'Eco-Friendly Spork',
|
||||
'Cheeseburger',
|
||||
'Brand New Car',
|
||||
'Hunting Knife',
|
||||
@@ -22,7 +25,7 @@ item_table = (
|
||||
'One-Up Mushroom',
|
||||
'Nokia N-GAGE',
|
||||
'2-Liter of Sprite',
|
||||
'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward expansion up to level 60 with no restrictions on playtime!',
|
||||
'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward and Stormblood expansions up to level 70 with no restrictions on playtime!',
|
||||
'Can of Compressed Air',
|
||||
'Striped Kitten',
|
||||
'USB Power Adapter',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from BaseClasses import MultiWorld
|
||||
from ..AutoWorld import LogicMixin
|
||||
from ..generic.Rules import set_rule
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
|
||||
class ArchipIDLELogic(LogicMixin):
|
||||
@@ -10,29 +9,20 @@ class ArchipIDLELogic(LogicMixin):
|
||||
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
for i in range(16, 31):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 4)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 4)
|
||||
|
||||
for i in range(31, 51):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 10)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 10)
|
||||
|
||||
for i in range(51, 101):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 20)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 20)
|
||||
|
||||
for i in range(101, 201):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 40)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 40)
|
||||
|
||||
world.completion_condition[player] =\
|
||||
lambda state:\
|
||||
state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
|
||||
lambda state: state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from datetime import datetime
|
||||
from .Items import item_table
|
||||
from .Rules import set_rules
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ArchipIDLEWebWorld(WebWorld):
|
||||
@@ -29,11 +29,10 @@ class ArchipIDLEWebWorld(WebWorld):
|
||||
|
||||
class ArchipIDLEWorld(World):
|
||||
"""
|
||||
An idle game which sends a check every thirty seconds, up to two hundred checks.
|
||||
An idle game which sends a check every thirty to sixty seconds, up to two hundred checks.
|
||||
"""
|
||||
game = "ArchipIDLE"
|
||||
topology_present = False
|
||||
data_version = 5
|
||||
hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April
|
||||
web = ArchipIDLEWebWorld()
|
||||
|
||||
@@ -56,18 +55,40 @@ class ArchipIDLEWorld(World):
|
||||
return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player)
|
||||
|
||||
def create_items(self):
|
||||
item_table_copy = list(item_table)
|
||||
self.multiworld.random.shuffle(item_table_copy)
|
||||
|
||||
item_pool = []
|
||||
for i in range(200):
|
||||
item = ArchipIDLEItem(
|
||||
item_table_copy[i],
|
||||
ItemClassification.progression if i < 40 else ItemClassification.filler,
|
||||
self.item_name_to_id[item_table_copy[i]],
|
||||
item_pool = [
|
||||
ArchipIDLEItem(
|
||||
item_table[0],
|
||||
ItemClassification.progression,
|
||||
self.item_name_to_id[item_table[0]],
|
||||
self.player
|
||||
)
|
||||
item_pool.append(item)
|
||||
]
|
||||
|
||||
for i in range(40):
|
||||
item_pool.append(ArchipIDLEItem(
|
||||
item_table[1],
|
||||
ItemClassification.progression,
|
||||
self.item_name_to_id[item_table[1]],
|
||||
self.player
|
||||
))
|
||||
|
||||
for i in range(40):
|
||||
item_pool.append(ArchipIDLEItem(
|
||||
item_table[2],
|
||||
ItemClassification.filler,
|
||||
self.item_name_to_id[item_table[2]],
|
||||
self.player
|
||||
))
|
||||
|
||||
item_table_copy = list(item_table[3:])
|
||||
self.random.shuffle(item_table_copy)
|
||||
for i in range(119):
|
||||
item_pool.append(ArchipIDLEItem(
|
||||
item_table_copy[i],
|
||||
ItemClassification.progression if i < 9 else ItemClassification.filler,
|
||||
self.item_name_to_id[item_table_copy[i]],
|
||||
self.player
|
||||
))
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
[ArchipIDLE GitHub Releases Page](https://github.com/ArchipelagoMW/archipidle/releases)
|
||||
3. Enter the server address in the `Server Address` field and press enter
|
||||
4. Enter your slot name when prompted. This should be the same as the `name` you entered on the
|
||||
setting page above, or the `name` field in your yaml file.
|
||||
options page above, or the `name` field in your yaml file.
|
||||
5. Click the "Begin!" button.
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# Guide de configuration d'ArchipIdle
|
||||
|
||||
## Rejoindre une partie MultiWorld
|
||||
1. Générez un fichier `.yaml` à partir de la [page des paramètres du lecteur ArchipIDLE](/games/ArchipIDLE/player-settings)
|
||||
1. Générez un fichier `.yaml` à partir de la [page des paramètres du lecteur ArchipIDLE](/games/ArchipIDLE/player-options)
|
||||
2. Ouvrez le client ArchipIDLE dans votre navigateur Web en :
|
||||
- Accédez au [Client ArchipIDLE](http://idle.multiworld.link)
|
||||
- Téléchargez le client et exécutez-le localement à partir du
|
||||
[Page des versions d'ArchipIDLE GitHub](https://github.com/ArchipelagoMW/archipidle/releases)
|
||||
- Accédez au [Client ArchipIDLE](http://idle.multiworld.link)
|
||||
- Téléchargez le client et exécutez-le localement à partir du [Page des versions d'ArchipIDLE GitHub](https://github.com/ArchipelagoMW/archipidle/releases)
|
||||
3. Entrez l'adresse du serveur dans le champ `Server Address` et appuyez sur Entrée
|
||||
4. Entrez votre nom d'emplacement lorsque vous y êtes invité. Il doit être le même que le `name` que vous avez saisi sur le
|
||||
page de configuration ci-dessus, ou le champ `name` dans votre fichier yaml.
|
||||
|
||||
@@ -10,10 +10,6 @@ class AdvData(typing.NamedTuple):
|
||||
class ChecksFinderAdvancement(Location):
|
||||
game: str = "ChecksFinder"
|
||||
|
||||
def __init__(self, player: int, name: str, address: typing.Optional[int], parent):
|
||||
super().__init__(player, name, address, parent)
|
||||
self.event = not address
|
||||
|
||||
|
||||
advancement_table = {
|
||||
"Tile 1": AdvData(81000, 'Board'),
|
||||
|
||||
@@ -4,7 +4,7 @@ import settings
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from BaseClasses import Item, Region, MultiWorld, Tutorial, ItemClassification
|
||||
from BaseClasses import Item, Region, Tutorial, ItemClassification
|
||||
from .items import CV64Item, filler_item_names, get_item_info, get_item_names_to_ids, get_item_counts
|
||||
from .locations import CV64Location, get_location_info, verify_locations, get_location_names_to_ids, base_id
|
||||
from .entrances import verify_entrances, get_warp_entrances
|
||||
@@ -18,7 +18,7 @@ from ..AutoWorld import WebWorld, World
|
||||
from .aesthetics import randomize_lighting, shuffle_sub_weapons, rom_empty_breakables_flags, rom_sub_weapon_flags, \
|
||||
randomize_music, get_start_inventory_data, get_location_data, randomize_shop_prices, get_loading_zone_bytes, \
|
||||
get_countdown_numbers
|
||||
from .rom import LocalRom, patch_rom, get_base_rom_path, CV64DeltaPatch
|
||||
from .rom import RomData, write_patch, get_base_rom_path, CV64ProcedurePatch, CV64_US_10_HASH
|
||||
from .client import Castlevania64Client
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class CV64Settings(settings.Group):
|
||||
"""File name of the CV64 US 1.0 rom"""
|
||||
copy_to = "Castlevania (USA).z64"
|
||||
description = "CV64 (US 1.0) ROM File"
|
||||
md5s = [CV64DeltaPatch.hash]
|
||||
md5s = [CV64_US_10_HASH]
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
@@ -86,12 +86,6 @@ class CV64World(World):
|
||||
|
||||
web = CV64Web()
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
|
||||
rom_file = get_base_rom_path()
|
||||
if not os.path.exists(rom_file):
|
||||
raise FileNotFoundError(rom_file)
|
||||
|
||||
def generate_early(self) -> None:
|
||||
# Generate the player's unique authentication
|
||||
self.auth = bytearray(self.multiworld.random.getrandbits(8) for _ in range(16))
|
||||
@@ -276,18 +270,13 @@ class CV64World(World):
|
||||
offset_data.update(get_start_inventory_data(self.player, self.options,
|
||||
self.multiworld.precollected_items[self.player]))
|
||||
|
||||
cv64_rom = LocalRom(get_base_rom_path())
|
||||
patch = CV64ProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
|
||||
write_patch(self, patch, offset_data, shop_name_list, shop_desc_list, shop_colors_list, active_locations)
|
||||
|
||||
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.z64")
|
||||
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
|
||||
f"{patch.patch_file_ending}")
|
||||
|
||||
patch_rom(self, cv64_rom, offset_data, shop_name_list, shop_desc_list, shop_colors_list, active_locations)
|
||||
|
||||
cv64_rom.write_to_file(rompath)
|
||||
|
||||
patch = CV64DeltaPatch(os.path.splitext(rompath)[0] + CV64DeltaPatch.patch_file_ending, player=self.player,
|
||||
player_name=self.multiworld.player_name[self.player], patched_path=rompath)
|
||||
patch.write()
|
||||
os.unlink(rompath)
|
||||
patch.write(rom_path)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choice(filler_item_names)
|
||||
|
||||
@@ -14,111 +14,111 @@ if TYPE_CHECKING:
|
||||
from . import CV64World
|
||||
|
||||
rom_sub_weapon_offsets = {
|
||||
0x10C6EB: (0x10, rname.forest_of_silence), # Forest
|
||||
0x10C6F3: (0x0F, rname.forest_of_silence),
|
||||
0x10C6FB: (0x0E, rname.forest_of_silence),
|
||||
0x10C703: (0x0D, rname.forest_of_silence),
|
||||
0x10C6EB: (b"\x10", rname.forest_of_silence), # Forest
|
||||
0x10C6F3: (b"\x0F", rname.forest_of_silence),
|
||||
0x10C6FB: (b"\x0E", rname.forest_of_silence),
|
||||
0x10C703: (b"\x0D", rname.forest_of_silence),
|
||||
|
||||
0x10C81F: (0x0F, rname.castle_wall), # Castle Wall
|
||||
0x10C827: (0x10, rname.castle_wall),
|
||||
0x10C82F: (0x0E, rname.castle_wall),
|
||||
0x7F9A0F: (0x0D, rname.castle_wall),
|
||||
0x10C81F: (b"\x0F", rname.castle_wall), # Castle Wall
|
||||
0x10C827: (b"\x10", rname.castle_wall),
|
||||
0x10C82F: (b"\x0E", rname.castle_wall),
|
||||
0x7F9A0F: (b"\x0D", rname.castle_wall),
|
||||
|
||||
0x83A5D9: (0x0E, rname.villa), # Villa
|
||||
0x83A5E5: (0x0D, rname.villa),
|
||||
0x83A5F1: (0x0F, rname.villa),
|
||||
0xBFC903: (0x10, rname.villa),
|
||||
0x10C987: (0x10, rname.villa),
|
||||
0x10C98F: (0x0D, rname.villa),
|
||||
0x10C997: (0x0F, rname.villa),
|
||||
0x10CF73: (0x10, rname.villa),
|
||||
0x83A5D9: (b"\x0E", rname.villa), # Villa
|
||||
0x83A5E5: (b"\x0D", rname.villa),
|
||||
0x83A5F1: (b"\x0F", rname.villa),
|
||||
0xBFC903: (b"\x10", rname.villa),
|
||||
0x10C987: (b"\x10", rname.villa),
|
||||
0x10C98F: (b"\x0D", rname.villa),
|
||||
0x10C997: (b"\x0F", rname.villa),
|
||||
0x10CF73: (b"\x10", rname.villa),
|
||||
|
||||
0x10CA57: (0x0D, rname.tunnel), # Tunnel
|
||||
0x10CA5F: (0x0E, rname.tunnel),
|
||||
0x10CA67: (0x10, rname.tunnel),
|
||||
0x10CA6F: (0x0D, rname.tunnel),
|
||||
0x10CA77: (0x0F, rname.tunnel),
|
||||
0x10CA7F: (0x0E, rname.tunnel),
|
||||
0x10CA57: (b"\x0D", rname.tunnel), # Tunnel
|
||||
0x10CA5F: (b"\x0E", rname.tunnel),
|
||||
0x10CA67: (b"\x10", rname.tunnel),
|
||||
0x10CA6F: (b"\x0D", rname.tunnel),
|
||||
0x10CA77: (b"\x0F", rname.tunnel),
|
||||
0x10CA7F: (b"\x0E", rname.tunnel),
|
||||
|
||||
0x10CBC7: (0x0E, rname.castle_center), # Castle Center
|
||||
0x10CC0F: (0x0D, rname.castle_center),
|
||||
0x10CC5B: (0x0F, rname.castle_center),
|
||||
0x10CBC7: (b"\x0E", rname.castle_center), # Castle Center
|
||||
0x10CC0F: (b"\x0D", rname.castle_center),
|
||||
0x10CC5B: (b"\x0F", rname.castle_center),
|
||||
|
||||
0x10CD3F: (0x0E, rname.tower_of_execution), # Character towers
|
||||
0x10CD65: (0x0D, rname.tower_of_execution),
|
||||
0x10CE2B: (0x0E, rname.tower_of_science),
|
||||
0x10CE83: (0x10, rname.duel_tower),
|
||||
0x10CD3F: (b"\x0E", rname.tower_of_execution), # Character towers
|
||||
0x10CD65: (b"\x0D", rname.tower_of_execution),
|
||||
0x10CE2B: (b"\x0E", rname.tower_of_science),
|
||||
0x10CE83: (b"\x10", rname.duel_tower),
|
||||
|
||||
0x10CF8B: (0x0F, rname.room_of_clocks), # Room of Clocks
|
||||
0x10CF93: (0x0D, rname.room_of_clocks),
|
||||
0x10CF8B: (b"\x0F", rname.room_of_clocks), # Room of Clocks
|
||||
0x10CF93: (b"\x0D", rname.room_of_clocks),
|
||||
|
||||
0x99BC5A: (0x0D, rname.clock_tower), # Clock Tower
|
||||
0x10CECB: (0x10, rname.clock_tower),
|
||||
0x10CED3: (0x0F, rname.clock_tower),
|
||||
0x10CEDB: (0x0E, rname.clock_tower),
|
||||
0x10CEE3: (0x0D, rname.clock_tower),
|
||||
0x99BC5A: (b"\x0D", rname.clock_tower), # Clock Tower
|
||||
0x10CECB: (b"\x10", rname.clock_tower),
|
||||
0x10CED3: (b"\x0F", rname.clock_tower),
|
||||
0x10CEDB: (b"\x0E", rname.clock_tower),
|
||||
0x10CEE3: (b"\x0D", rname.clock_tower),
|
||||
}
|
||||
|
||||
rom_sub_weapon_flags = {
|
||||
0x10C6EC: 0x0200FF04, # Forest of Silence
|
||||
0x10C6FC: 0x0400FF04,
|
||||
0x10C6F4: 0x0800FF04,
|
||||
0x10C704: 0x4000FF04,
|
||||
0x10C6EC: b"\x02\x00\xFF\x04", # Forest of Silence
|
||||
0x10C6FC: b"\x04\x00\xFF\x04",
|
||||
0x10C6F4: b"\x08\x00\xFF\x04",
|
||||
0x10C704: b"\x40\x00\xFF\x04",
|
||||
|
||||
0x10C831: 0x08, # Castle Wall
|
||||
0x10C829: 0x10,
|
||||
0x10C821: 0x20,
|
||||
0xBFCA97: 0x04,
|
||||
0x10C831: b"\x08", # Castle Wall
|
||||
0x10C829: b"\x10",
|
||||
0x10C821: b"\x20",
|
||||
0xBFCA97: b"\x04",
|
||||
|
||||
# Villa
|
||||
0xBFC926: 0xFF04,
|
||||
0xBFC93A: 0x80,
|
||||
0xBFC93F: 0x01,
|
||||
0xBFC943: 0x40,
|
||||
0xBFC947: 0x80,
|
||||
0x10C989: 0x10,
|
||||
0x10C991: 0x20,
|
||||
0x10C999: 0x40,
|
||||
0x10CF77: 0x80,
|
||||
0xBFC926: b"\xFF\x04",
|
||||
0xBFC93A: b"\x80",
|
||||
0xBFC93F: b"\x01",
|
||||
0xBFC943: b"\x40",
|
||||
0xBFC947: b"\x80",
|
||||
0x10C989: b"\x10",
|
||||
0x10C991: b"\x20",
|
||||
0x10C999: b"\x40",
|
||||
0x10CF77: b"\x80",
|
||||
|
||||
0x10CA58: 0x4000FF0E, # Tunnel
|
||||
0x10CA6B: 0x80,
|
||||
0x10CA60: 0x1000FF05,
|
||||
0x10CA70: 0x2000FF05,
|
||||
0x10CA78: 0x4000FF05,
|
||||
0x10CA80: 0x8000FF05,
|
||||
0x10CA58: b"\x40\x00\xFF\x0E", # Tunnel
|
||||
0x10CA6B: b"\x80",
|
||||
0x10CA60: b"\x10\x00\xFF\x05",
|
||||
0x10CA70: b"\x20\x00\xFF\x05",
|
||||
0x10CA78: b"\x40\x00\xFF\x05",
|
||||
0x10CA80: b"\x80\x00\xFF\x05",
|
||||
|
||||
0x10CBCA: 0x02, # Castle Center
|
||||
0x10CC10: 0x80,
|
||||
0x10CC5C: 0x40,
|
||||
0x10CBCA: b"\x02", # Castle Center
|
||||
0x10CC10: b"\x80",
|
||||
0x10CC5C: b"\x40",
|
||||
|
||||
0x10CE86: 0x01, # Duel Tower
|
||||
0x10CD43: 0x02, # Tower of Execution
|
||||
0x10CE2E: 0x20, # Tower of Science
|
||||
0x10CE86: b"\x01", # Duel Tower
|
||||
0x10CD43: b"\x02", # Tower of Execution
|
||||
0x10CE2E: b"\x20", # Tower of Science
|
||||
|
||||
0x10CF8E: 0x04, # Room of Clocks
|
||||
0x10CF96: 0x08,
|
||||
0x10CF8E: b"\x04", # Room of Clocks
|
||||
0x10CF96: b"\x08",
|
||||
|
||||
0x10CECE: 0x08, # Clock Tower
|
||||
0x10CED6: 0x10,
|
||||
0x10CEE6: 0x20,
|
||||
0x10CEDE: 0x80,
|
||||
0x10CECE: b"\x08", # Clock Tower
|
||||
0x10CED6: b"\x10",
|
||||
0x10CEE6: b"\x20",
|
||||
0x10CEDE: b"\x80",
|
||||
}
|
||||
|
||||
rom_empty_breakables_flags = {
|
||||
0x10C74D: 0x40FF05, # Forest of Silence
|
||||
0x10C765: 0x20FF0E,
|
||||
0x10C774: 0x0800FF0E,
|
||||
0x10C755: 0x80FF05,
|
||||
0x10C784: 0x0100FF0E,
|
||||
0x10C73C: 0x0200FF0E,
|
||||
0x10C74D: b"\x40\xFF\x05", # Forest of Silence
|
||||
0x10C765: b"\x20\xFF\x0E",
|
||||
0x10C774: b"\x08\x00\xFF\x0E",
|
||||
0x10C755: b"\x80\xFF\x05",
|
||||
0x10C784: b"\x01\x00\xFF\x0E",
|
||||
0x10C73C: b"\x02\x00\xFF\x0E",
|
||||
|
||||
0x10C8D0: 0x0400FF0E, # Villa foyer
|
||||
0x10C8D0: b"\x04\x00\xFF\x0E", # Villa foyer
|
||||
|
||||
0x10CF9F: 0x08, # Room of Clocks flags
|
||||
0x10CFA7: 0x01,
|
||||
0xBFCB6F: 0x04, # Room of Clocks candle property IDs
|
||||
0xBFCB73: 0x05,
|
||||
0x10CF9F: b"\x08", # Room of Clocks flags
|
||||
0x10CFA7: b"\x01",
|
||||
0xBFCB6F: b"\x04", # Room of Clocks candle property IDs
|
||||
0xBFCB73: b"\x05",
|
||||
}
|
||||
|
||||
rom_axe_cross_lower_values = {
|
||||
@@ -269,19 +269,18 @@ renon_item_dialogue = {
|
||||
}
|
||||
|
||||
|
||||
def randomize_lighting(world: "CV64World") -> Dict[int, int]:
|
||||
def randomize_lighting(world: "CV64World") -> Dict[int, bytes]:
|
||||
"""Generates randomized data for the map lighting table."""
|
||||
randomized_lighting = {}
|
||||
for entry in range(67):
|
||||
for sub_entry in range(19):
|
||||
if sub_entry not in [3, 7, 11, 15] and entry != 4:
|
||||
# The fourth entry in the lighting table affects the lighting on some item pickups; skip it
|
||||
randomized_lighting[0x1091A0 + (entry * 28) + sub_entry] = \
|
||||
world.random.randint(0, 255)
|
||||
randomized_lighting[0x1091A0 + (entry * 28) + sub_entry] = bytes([world.random.randint(0, 255)])
|
||||
return randomized_lighting
|
||||
|
||||
|
||||
def shuffle_sub_weapons(world: "CV64World") -> Dict[int, int]:
|
||||
def shuffle_sub_weapons(world: "CV64World") -> Dict[int, bytes]:
|
||||
"""Shuffles the sub-weapons amongst themselves."""
|
||||
sub_weapon_dict = {offset: rom_sub_weapon_offsets[offset][0] for offset in rom_sub_weapon_offsets if
|
||||
rom_sub_weapon_offsets[offset][1] in world.active_stage_exits}
|
||||
@@ -295,7 +294,7 @@ def shuffle_sub_weapons(world: "CV64World") -> Dict[int, int]:
|
||||
return dict(zip(sub_weapon_dict, sub_bytes))
|
||||
|
||||
|
||||
def randomize_music(world: "CV64World") -> Dict[int, int]:
|
||||
def randomize_music(world: "CV64World") -> Dict[int, bytes]:
|
||||
"""Generates randomized or disabled data for all the music in the game."""
|
||||
music_array = bytearray(0x7A)
|
||||
for number in music_sfx_ids:
|
||||
@@ -340,15 +339,10 @@ def randomize_music(world: "CV64World") -> Dict[int, int]:
|
||||
music_array[i] = fade_in_songs[i]
|
||||
del (music_array[0x00: 0x10])
|
||||
|
||||
# Convert the music array into a data dict
|
||||
music_offsets = {}
|
||||
for i in range(len(music_array)):
|
||||
music_offsets[0xBFCD30 + i] = music_array[i]
|
||||
|
||||
return music_offsets
|
||||
return {0xBFCD30: bytes(music_array)}
|
||||
|
||||
|
||||
def randomize_shop_prices(world: "CV64World") -> Dict[int, int]:
|
||||
def randomize_shop_prices(world: "CV64World") -> Dict[int, bytes]:
|
||||
"""Randomize the shop prices based on the minimum and maximum values chosen.
|
||||
The minimum price will adjust if it's higher than the max."""
|
||||
min_price = world.options.minimum_gold_price.value
|
||||
@@ -363,21 +357,15 @@ def randomize_shop_prices(world: "CV64World") -> Dict[int, int]:
|
||||
|
||||
shop_price_list = [world.random.randint(min_price * 100, max_price * 100) for _ in range(7)]
|
||||
|
||||
# Convert the price list into a data dict. Which offset it starts from depends on how many bytes it takes up.
|
||||
# Convert the price list into a data dict.
|
||||
price_dict = {}
|
||||
for i in range(len(shop_price_list)):
|
||||
if shop_price_list[i] <= 0xFF:
|
||||
price_dict[0x103D6E + (i*12)] = 0
|
||||
price_dict[0x103D6F + (i*12)] = shop_price_list[i]
|
||||
elif shop_price_list[i] <= 0xFFFF:
|
||||
price_dict[0x103D6E + (i*12)] = shop_price_list[i]
|
||||
else:
|
||||
price_dict[0x103D6D + (i*12)] = shop_price_list[i]
|
||||
price_dict[0x103D6C + (i * 12)] = int.to_bytes(shop_price_list[i], 4, "big")
|
||||
|
||||
return price_dict
|
||||
|
||||
|
||||
def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Location]) -> Dict[int, int]:
|
||||
def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Location]) -> Dict[int, bytes]:
|
||||
"""Figures out which Countdown numbers to increase for each Location after verifying the Item on the Location should
|
||||
increase a number.
|
||||
|
||||
@@ -400,16 +388,11 @@ def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Locat
|
||||
if countdown_number is not None:
|
||||
countdown_list[countdown_number] += 1
|
||||
|
||||
# Convert the Countdown list into a data dict
|
||||
countdown_dict = {}
|
||||
for i in range(len(countdown_list)):
|
||||
countdown_dict[0xBFD818 + i] = countdown_list[i]
|
||||
|
||||
return countdown_dict
|
||||
return {0xBFD818: bytes(countdown_list)}
|
||||
|
||||
|
||||
def get_location_data(world: "CV64World", active_locations: Iterable[Location]) \
|
||||
-> Tuple[Dict[int, int], List[str], List[bytearray], List[List[Union[int, str, None]]]]:
|
||||
-> Tuple[Dict[int, bytes], List[str], List[bytearray], List[List[Union[int, str, None]]]]:
|
||||
"""Gets ALL the item data to go into the ROM. Item data consists of two bytes: the first dictates the appearance of
|
||||
the item, the second determines what the item actually is when picked up. All items from other worlds will be AP
|
||||
items that do nothing when picked up other than set their flag, and their appearance will depend on whether it's
|
||||
@@ -449,12 +432,11 @@ def get_location_data(world: "CV64World", active_locations: Iterable[Location])
|
||||
|
||||
# Figure out the item ID bytes to put in each Location here. Write the item itself if either it's the player's
|
||||
# very own, or it belongs to an Item Link that the player is a part of.
|
||||
if loc.item.player == world.player or (loc.item.player in world.multiworld.groups and
|
||||
world.player in world.multiworld.groups[loc.item.player]['players']):
|
||||
if loc.item.player == world.player:
|
||||
if loc_type not in ["npc", "shop"] and get_item_info(loc.item.name, "pickup actor id") is not None:
|
||||
location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "pickup actor id")
|
||||
else:
|
||||
location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "code")
|
||||
location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "code") & 0xFF
|
||||
else:
|
||||
# Make the item the unused Wooden Stake - our multiworld item.
|
||||
location_bytes[get_location_info(loc.name, "offset")] = 0x11
|
||||
@@ -534,11 +516,12 @@ def get_location_data(world: "CV64World", active_locations: Iterable[Location])
|
||||
|
||||
shop_colors_list.append(get_item_text_color(loc))
|
||||
|
||||
return location_bytes, shop_name_list, shop_colors_list, shop_desc_list
|
||||
return {offset: int.to_bytes(byte, 1, "big") for offset, byte in location_bytes.items()}, shop_name_list,\
|
||||
shop_colors_list, shop_desc_list
|
||||
|
||||
|
||||
def get_loading_zone_bytes(options: CV64Options, starting_stage: str,
|
||||
active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[int, int]:
|
||||
active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[int, bytes]:
|
||||
"""Figure out all the bytes for loading zones and map transitions based on which stages are where in the exit data.
|
||||
The same data was used earlier in figuring out the logic. Map transitions consist of two major components: which map
|
||||
to send the player to, and which spot within the map to spawn the player at."""
|
||||
@@ -551,8 +534,8 @@ def get_loading_zone_bytes(options: CV64Options, starting_stage: str,
|
||||
# Start loading zones
|
||||
# If the start zone is the start of the line, have it simply refresh the map.
|
||||
if active_stage_exits[stage]["prev"] == "Menu":
|
||||
loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = 0xFF
|
||||
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = 0x00
|
||||
loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = b"\xFF"
|
||||
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = b"\x00"
|
||||
elif active_stage_exits[stage]["prev"]:
|
||||
loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = \
|
||||
get_stage_info(active_stage_exits[stage]["prev"], "end map id")
|
||||
@@ -563,7 +546,7 @@ def get_loading_zone_bytes(options: CV64Options, starting_stage: str,
|
||||
if active_stage_exits[stage]["prev"] == rname.castle_center:
|
||||
if options.character_stages == CharacterStages.option_carrie_only or \
|
||||
active_stage_exits[rname.castle_center]["alt"] == stage:
|
||||
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] += 1
|
||||
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = b"\x03"
|
||||
|
||||
# End loading zones
|
||||
if active_stage_exits[stage]["next"]:
|
||||
@@ -582,16 +565,16 @@ def get_loading_zone_bytes(options: CV64Options, starting_stage: str,
|
||||
return loading_zone_bytes
|
||||
|
||||
|
||||
def get_start_inventory_data(player: int, options: CV64Options, precollected_items: List[Item]) -> Dict[int, int]:
|
||||
def get_start_inventory_data(player: int, options: CV64Options, precollected_items: List[Item]) -> Dict[int, bytes]:
|
||||
"""Calculate and return the starting inventory values. Not every Item goes into the menu inventory, so everything
|
||||
has to be handled appropriately."""
|
||||
start_inventory_data = {0xBFD867: 0, # Jewels
|
||||
0xBFD87B: 0, # PowerUps
|
||||
0xBFD883: 0, # Sub-weapon
|
||||
0xBFD88B: 0} # Ice Traps
|
||||
start_inventory_data = {}
|
||||
|
||||
inventory_items_array = [0 for _ in range(35)]
|
||||
total_money = 0
|
||||
total_jewels = 0
|
||||
total_powerups = 0
|
||||
total_ice_traps = 0
|
||||
|
||||
items_max = 10
|
||||
|
||||
@@ -615,42 +598,46 @@ def get_start_inventory_data(player: int, options: CV64Options, precollected_ite
|
||||
inventory_items_array[inventory_offset] = 2
|
||||
# Starting sub-weapon
|
||||
elif sub_equip_id is not None:
|
||||
start_inventory_data[0xBFD883] = sub_equip_id
|
||||
start_inventory_data[0xBFD883] = bytes(sub_equip_id)
|
||||
# Starting PowerUps
|
||||
elif item.name == iname.powerup:
|
||||
start_inventory_data[0xBFD87B] += 1
|
||||
if start_inventory_data[0xBFD87B] > 2:
|
||||
start_inventory_data[0xBFD87B] = 2
|
||||
total_powerups += 1
|
||||
# Can't have more than 2 PowerUps.
|
||||
if total_powerups > 2:
|
||||
total_powerups = 2
|
||||
# Starting Gold
|
||||
elif "GOLD" in item.name:
|
||||
total_money += int(item.name[0:4])
|
||||
# Money cannot be higher than 99999.
|
||||
if total_money > 99999:
|
||||
total_money = 99999
|
||||
# Starting Jewels
|
||||
elif "jewel" in item.name:
|
||||
if "L" in item.name:
|
||||
start_inventory_data[0xBFD867] += 10
|
||||
total_jewels += 10
|
||||
else:
|
||||
start_inventory_data[0xBFD867] += 5
|
||||
if start_inventory_data[0xBFD867] > 99:
|
||||
start_inventory_data[0xBFD867] = 99
|
||||
total_jewels += 5
|
||||
# Jewels cannot be higher than 99.
|
||||
if total_jewels > 99:
|
||||
total_jewels = 99
|
||||
# Starting Ice Traps
|
||||
else:
|
||||
start_inventory_data[0xBFD88B] += 1
|
||||
if start_inventory_data[0xBFD88B] > 0xFF:
|
||||
start_inventory_data[0xBFD88B] = 0xFF
|
||||
total_ice_traps += 1
|
||||
# Ice Traps cannot be higher than 255.
|
||||
if total_ice_traps > 0xFF:
|
||||
total_ice_traps = 0xFF
|
||||
|
||||
# Convert the jewels into data.
|
||||
start_inventory_data[0xBFD867] = bytes([total_jewels])
|
||||
|
||||
# Convert the Ice Traps into data.
|
||||
start_inventory_data[0xBFD88B] = bytes([total_ice_traps])
|
||||
|
||||
# Convert the inventory items into data.
|
||||
for i in range(len(inventory_items_array)):
|
||||
start_inventory_data[0xBFE518 + i] = inventory_items_array[i]
|
||||
start_inventory_data[0xBFE518] = bytes(inventory_items_array)
|
||||
|
||||
# Convert the starting money into data. Which offset it starts from depends on how many bytes it takes up.
|
||||
if total_money <= 0xFF:
|
||||
start_inventory_data[0xBFE517] = total_money
|
||||
elif total_money <= 0xFFFF:
|
||||
start_inventory_data[0xBFE516] = total_money
|
||||
else:
|
||||
start_inventory_data[0xBFE515] = total_money
|
||||
# Convert the starting money into data.
|
||||
start_inventory_data[0xBFE514] = int.to_bytes(total_money, 4, "big")
|
||||
|
||||
return start_inventory_data
|
||||
|
||||
|
||||
@@ -197,12 +197,15 @@ deathlink_nitro_edition = [
|
||||
0xA168FFFD, # SB T0, 0xFFFD (T3)
|
||||
]
|
||||
|
||||
nitro_fall_killer = [
|
||||
# Custom code to force the instant fall death if at a high enough falling speed after getting killed by the Nitro
|
||||
# explosion, since the game doesn't run the checks for the fall death after getting hit by said explosion and could
|
||||
# result in a softlock when getting blown into an abyss.
|
||||
launch_fall_killer = [
|
||||
# Custom code to force the instant fall death if at a high enough falling speed after getting killed by something
|
||||
# that launches you (whether it be the Nitro explosion or a Big Toss hit). The game doesn't normally run the check
|
||||
# that would trigger the fall death after you get killed by some other means, which could result in a softlock
|
||||
# when a killing blow launches you into an abyss.
|
||||
0x3C0C8035, # LUI T4, 0x8035
|
||||
0x918807E2, # LBU T0, 0x07E2 (T4)
|
||||
0x24090008, # ADDIU T1, R0, 0x0008
|
||||
0x11090002, # BEQ T0, T1, [forward 0x02]
|
||||
0x2409000C, # ADDIU T1, R0, 0x000C
|
||||
0x15090006, # BNE T0, T1, [forward 0x06]
|
||||
0x3C098035, # LUI T1, 0x8035
|
||||
@@ -2863,3 +2866,13 @@ big_tosser = [
|
||||
0xAD000814, # SW R0, 0x0814 (T0)
|
||||
0x03200008 # JR T9
|
||||
]
|
||||
|
||||
dog_bite_ice_trap_fix = [
|
||||
# Sets the freeze timer to 0 when a maze garden dog bites the player to ensure the ice chunk model will break if the
|
||||
# player gets bitten while frozen via Ice Trap.
|
||||
0x3C088039, # LUI T0, 0x8039
|
||||
0xA5009E76, # SH R0, 0x9E76 (T0)
|
||||
0x3C090F00, # LUI T1, 0x0F00
|
||||
0x25291CB8, # ADDIU T1, T1, 0x1CB8
|
||||
0x01200008 # JR T1
|
||||
]
|
||||
|
||||
@@ -27,7 +27,7 @@ in vanilla, contains 5 checks in rando.
|
||||
#### Bat archway rock
|
||||
After the broken bridge containing the invisible pathway to the Special1 in vanilla, this rock is off to the side in front
|
||||
of the gate frame with a swarm of bats that come at you, before the Werewolf's territory. Contains 4 checks. If you are new
|
||||
to speedrunning the vanilla game and haven't yet learned the RNG manip strats, this is a guranteed spot to find a PowerUp at.
|
||||
to speedrunning the vanilla game and haven't yet learned the RNG manip strats, this is a guaranteed spot to find a PowerUp at.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,8 @@ class HardItemPool(Toggle):
|
||||
|
||||
|
||||
class Special1sPerWarp(Range):
|
||||
"""Sets how many Special1 jewels are needed per warp menu option unlock."""
|
||||
"""Sets how many Special1 jewels are needed per warp menu option unlock.
|
||||
This will decrease until the number x 7 is less than or equal to the Total Specail1s if it isn't already."""
|
||||
range_start = 1
|
||||
range_end = 10
|
||||
default = 1
|
||||
@@ -82,8 +83,7 @@ class Special1sPerWarp(Range):
|
||||
|
||||
|
||||
class TotalSpecial1s(Range):
|
||||
"""Sets how many Speical1 jewels are in the pool in total.
|
||||
If this is set to be less than Special1s Per Warp x 7, it will decrease by 1 until it isn't."""
|
||||
"""Sets how many Speical1 jewels are in the pool in total."""
|
||||
range_start = 7
|
||||
range_end = 70
|
||||
default = 7
|
||||
|
||||
1743
worlds/cv64/rom.py
1743
worlds/cv64/rom.py
File diff suppressed because it is too large
Load Diff
@@ -47,9 +47,9 @@ if TYPE_CHECKING:
|
||||
# corresponding Locations and Entrances will all be created.
|
||||
stage_info = {
|
||||
"Forest of Silence": {
|
||||
"start region": rname.forest_start, "start map id": 0x00, "start spawn id": 0x00,
|
||||
"mid region": rname.forest_mid, "mid map id": 0x00, "mid spawn id": 0x04,
|
||||
"end region": rname.forest_end, "end map id": 0x00, "end spawn id": 0x01,
|
||||
"start region": rname.forest_start, "start map id": b"\x00", "start spawn id": b"\x00",
|
||||
"mid region": rname.forest_mid, "mid map id": b"\x00", "mid spawn id": b"\x04",
|
||||
"end region": rname.forest_end, "end map id": b"\x00", "end spawn id": b"\x01",
|
||||
"endzone map offset": 0xB6302F, "endzone spawn offset": 0xB6302B,
|
||||
"save number offsets": [0x1049C5, 0x1049CD, 0x1049D5],
|
||||
"regions": [rname.forest_start,
|
||||
@@ -58,9 +58,9 @@ stage_info = {
|
||||
},
|
||||
|
||||
"Castle Wall": {
|
||||
"start region": rname.cw_start, "start map id": 0x02, "start spawn id": 0x00,
|
||||
"mid region": rname.cw_start, "mid map id": 0x02, "mid spawn id": 0x07,
|
||||
"end region": rname.cw_exit, "end map id": 0x02, "end spawn id": 0x10,
|
||||
"start region": rname.cw_start, "start map id": b"\x02", "start spawn id": b"\x00",
|
||||
"mid region": rname.cw_start, "mid map id": b"\x02", "mid spawn id": b"\x07",
|
||||
"end region": rname.cw_exit, "end map id": b"\x02", "end spawn id": b"\x10",
|
||||
"endzone map offset": 0x109A5F, "endzone spawn offset": 0x109A61,
|
||||
"save number offsets": [0x1049DD, 0x1049E5, 0x1049ED],
|
||||
"regions": [rname.cw_start,
|
||||
@@ -69,9 +69,9 @@ stage_info = {
|
||||
},
|
||||
|
||||
"Villa": {
|
||||
"start region": rname.villa_start, "start map id": 0x03, "start spawn id": 0x00,
|
||||
"mid region": rname.villa_storeroom, "mid map id": 0x05, "mid spawn id": 0x04,
|
||||
"end region": rname.villa_crypt, "end map id": 0x1A, "end spawn id": 0x03,
|
||||
"start region": rname.villa_start, "start map id": b"\x03", "start spawn id": b"\x00",
|
||||
"mid region": rname.villa_storeroom, "mid map id": b"\x05", "mid spawn id": b"\x04",
|
||||
"end region": rname.villa_crypt, "end map id": b"\x1A", "end spawn id": b"\x03",
|
||||
"endzone map offset": 0xD9DA3, "endzone spawn offset": 0x109E81,
|
||||
"altzone map offset": 0xD9DAB, "altzone spawn offset": 0x109E81,
|
||||
"save number offsets": [0x1049F5, 0x1049FD, 0x104A05, 0x104A0D],
|
||||
@@ -85,9 +85,9 @@ stage_info = {
|
||||
},
|
||||
|
||||
"Tunnel": {
|
||||
"start region": rname.tunnel_start, "start map id": 0x07, "start spawn id": 0x00,
|
||||
"mid region": rname.tunnel_end, "mid map id": 0x07, "mid spawn id": 0x03,
|
||||
"end region": rname.tunnel_end, "end map id": 0x07, "end spawn id": 0x11,
|
||||
"start region": rname.tunnel_start, "start map id": b"\x07", "start spawn id": b"\x00",
|
||||
"mid region": rname.tunnel_end, "mid map id": b"\x07", "mid spawn id": b"\x03",
|
||||
"end region": rname.tunnel_end, "end map id": b"\x07", "end spawn id": b"\x11",
|
||||
"endzone map offset": 0x109B4F, "endzone spawn offset": 0x109B51, "character": "Reinhardt",
|
||||
"save number offsets": [0x104A15, 0x104A1D, 0x104A25, 0x104A2D],
|
||||
"regions": [rname.tunnel_start,
|
||||
@@ -95,9 +95,9 @@ stage_info = {
|
||||
},
|
||||
|
||||
"Underground Waterway": {
|
||||
"start region": rname.uw_main, "start map id": 0x08, "start spawn id": 0x00,
|
||||
"mid region": rname.uw_main, "mid map id": 0x08, "mid spawn id": 0x03,
|
||||
"end region": rname.uw_end, "end map id": 0x08, "end spawn id": 0x01,
|
||||
"start region": rname.uw_main, "start map id": b"\x08", "start spawn id": b"\x00",
|
||||
"mid region": rname.uw_main, "mid map id": b"\x08", "mid spawn id": b"\x03",
|
||||
"end region": rname.uw_end, "end map id": b"\x08", "end spawn id": b"\x01",
|
||||
"endzone map offset": 0x109B67, "endzone spawn offset": 0x109B69, "character": "Carrie",
|
||||
"save number offsets": [0x104A35, 0x104A3D],
|
||||
"regions": [rname.uw_main,
|
||||
@@ -105,9 +105,9 @@ stage_info = {
|
||||
},
|
||||
|
||||
"Castle Center": {
|
||||
"start region": rname.cc_main, "start map id": 0x19, "start spawn id": 0x00,
|
||||
"mid region": rname.cc_main, "mid map id": 0x0E, "mid spawn id": 0x03,
|
||||
"end region": rname.cc_elev_top, "end map id": 0x0F, "end spawn id": 0x02,
|
||||
"start region": rname.cc_main, "start map id": b"\x19", "start spawn id": b"\x00",
|
||||
"mid region": rname.cc_main, "mid map id": b"\x0E", "mid spawn id": b"\x03",
|
||||
"end region": rname.cc_elev_top, "end map id": b"\x0F", "end spawn id": b"\x02",
|
||||
"endzone map offset": 0x109CB7, "endzone spawn offset": 0x109CB9,
|
||||
"altzone map offset": 0x109CCF, "altzone spawn offset": 0x109CD1,
|
||||
"save number offsets": [0x104A45, 0x104A4D, 0x104A55, 0x104A5D, 0x104A65, 0x104A6D, 0x104A75],
|
||||
@@ -119,20 +119,20 @@ stage_info = {
|
||||
},
|
||||
|
||||
"Duel Tower": {
|
||||
"start region": rname.dt_main, "start map id": 0x13, "start spawn id": 0x00,
|
||||
"start region": rname.dt_main, "start map id": b"\x13", "start spawn id": b"\x00",
|
||||
"startzone map offset": 0x109DA7, "startzone spawn offset": 0x109DA9,
|
||||
"mid region": rname.dt_main, "mid map id": 0x13, "mid spawn id": 0x15,
|
||||
"end region": rname.dt_main, "end map id": 0x13, "end spawn id": 0x01,
|
||||
"mid region": rname.dt_main, "mid map id": b"\x13", "mid spawn id": b"\x15",
|
||||
"end region": rname.dt_main, "end map id": b"\x13", "end spawn id": b"\x01",
|
||||
"endzone map offset": 0x109D8F, "endzone spawn offset": 0x109D91, "character": "Reinhardt",
|
||||
"save number offsets": [0x104ACD],
|
||||
"regions": [rname.dt_main]
|
||||
},
|
||||
|
||||
"Tower of Execution": {
|
||||
"start region": rname.toe_main, "start map id": 0x10, "start spawn id": 0x00,
|
||||
"start region": rname.toe_main, "start map id": b"\x10", "start spawn id": b"\x00",
|
||||
"startzone map offset": 0x109D17, "startzone spawn offset": 0x109D19,
|
||||
"mid region": rname.toe_main, "mid map id": 0x10, "mid spawn id": 0x02,
|
||||
"end region": rname.toe_main, "end map id": 0x10, "end spawn id": 0x12,
|
||||
"mid region": rname.toe_main, "mid map id": b"\x10", "mid spawn id": b"\x02",
|
||||
"end region": rname.toe_main, "end map id": b"\x10", "end spawn id": b"\x12",
|
||||
"endzone map offset": 0x109CFF, "endzone spawn offset": 0x109D01, "character": "Reinhardt",
|
||||
"save number offsets": [0x104A7D, 0x104A85],
|
||||
"regions": [rname.toe_main,
|
||||
@@ -140,10 +140,10 @@ stage_info = {
|
||||
},
|
||||
|
||||
"Tower of Science": {
|
||||
"start region": rname.tosci_start, "start map id": 0x12, "start spawn id": 0x00,
|
||||
"start region": rname.tosci_start, "start map id": b"\x12", "start spawn id": b"\x00",
|
||||
"startzone map offset": 0x109D77, "startzone spawn offset": 0x109D79,
|
||||
"mid region": rname.tosci_conveyors, "mid map id": 0x12, "mid spawn id": 0x03,
|
||||
"end region": rname.tosci_conveyors, "end map id": 0x12, "end spawn id": 0x04,
|
||||
"mid region": rname.tosci_conveyors, "mid map id": b"\x12", "mid spawn id": b"\x03",
|
||||
"end region": rname.tosci_conveyors, "end map id": b"\x12", "end spawn id": b"\x04",
|
||||
"endzone map offset": 0x109D5F, "endzone spawn offset": 0x109D61, "character": "Carrie",
|
||||
"save number offsets": [0x104A95, 0x104A9D, 0x104AA5],
|
||||
"regions": [rname.tosci_start,
|
||||
@@ -153,28 +153,28 @@ stage_info = {
|
||||
},
|
||||
|
||||
"Tower of Sorcery": {
|
||||
"start region": rname.tosor_main, "start map id": 0x11, "start spawn id": 0x00,
|
||||
"start region": rname.tosor_main, "start map id": b"\x11", "start spawn id": b"\x00",
|
||||
"startzone map offset": 0x109D47, "startzone spawn offset": 0x109D49,
|
||||
"mid region": rname.tosor_main, "mid map id": 0x11, "mid spawn id": 0x01,
|
||||
"end region": rname.tosor_main, "end map id": 0x11, "end spawn id": 0x13,
|
||||
"mid region": rname.tosor_main, "mid map id": b"\x11", "mid spawn id": b"\x01",
|
||||
"end region": rname.tosor_main, "end map id": b"\x11", "end spawn id": b"\x13",
|
||||
"endzone map offset": 0x109D2F, "endzone spawn offset": 0x109D31, "character": "Carrie",
|
||||
"save number offsets": [0x104A8D],
|
||||
"regions": [rname.tosor_main]
|
||||
},
|
||||
|
||||
"Room of Clocks": {
|
||||
"start region": rname.roc_main, "start map id": 0x1B, "start spawn id": 0x00,
|
||||
"mid region": rname.roc_main, "mid map id": 0x1B, "mid spawn id": 0x02,
|
||||
"end region": rname.roc_main, "end map id": 0x1B, "end spawn id": 0x14,
|
||||
"start region": rname.roc_main, "start map id": b"\x1B", "start spawn id": b"\x00",
|
||||
"mid region": rname.roc_main, "mid map id": b"\x1B", "mid spawn id": b"\x02",
|
||||
"end region": rname.roc_main, "end map id": b"\x1B", "end spawn id": b"\x14",
|
||||
"endzone map offset": 0x109EAF, "endzone spawn offset": 0x109EB1,
|
||||
"save number offsets": [0x104AC5],
|
||||
"regions": [rname.roc_main]
|
||||
},
|
||||
|
||||
"Clock Tower": {
|
||||
"start region": rname.ct_start, "start map id": 0x17, "start spawn id": 0x00,
|
||||
"mid region": rname.ct_middle, "mid map id": 0x17, "mid spawn id": 0x02,
|
||||
"end region": rname.ct_end, "end map id": 0x17, "end spawn id": 0x03,
|
||||
"start region": rname.ct_start, "start map id": b"\x17", "start spawn id": b"\x00",
|
||||
"mid region": rname.ct_middle, "mid map id": b"\x17", "mid spawn id": b"\x02",
|
||||
"end region": rname.ct_end, "end map id": b"\x17", "end spawn id": b"\x03",
|
||||
"endzone map offset": 0x109E37, "endzone spawn offset": 0x109E39,
|
||||
"save number offsets": [0x104AB5, 0x104ABD],
|
||||
"regions": [rname.ct_start,
|
||||
@@ -183,8 +183,8 @@ stage_info = {
|
||||
},
|
||||
|
||||
"Castle Keep": {
|
||||
"start region": rname.ck_main, "start map id": 0x14, "start spawn id": 0x02,
|
||||
"mid region": rname.ck_main, "mid map id": 0x14, "mid spawn id": 0x03,
|
||||
"start region": rname.ck_main, "start map id": b"\x14", "start spawn id": b"\x02",
|
||||
"mid region": rname.ck_main, "mid map id": b"\x14", "mid spawn id": b"\x03",
|
||||
"end region": rname.ck_drac_chamber,
|
||||
"save number offsets": [0x104AAD],
|
||||
"regions": [rname.ck_main]
|
||||
|
||||
@@ -25,29 +25,28 @@ To downpatch DS3 for use with Archipelago, use the following instructions from t
|
||||
|
||||
1. Launch Steam (in online mode).
|
||||
2. Press the Windows Key + R. This will open the Run window.
|
||||
3. Open the Steam console by typing the following string: steam://open/console , Steam should now open in Console Mode.
|
||||
4. Insert the string of the depot you wish to download. For the AP supported v1.15, you will want to use: download_depot 374320 374321 4471176929659548333.
|
||||
5. Steam will now download the depot. Note: There is no progress bar of the download in Steam, but it is still downloading in the background.
|
||||
6. Turn off auto-updates in Steam by right-clicking Dark Souls III in your library > Properties > Updates > set "Automatic Updates" to "Only update this game when I launch it" (or change the value for AutoUpdateBehavior to 1 in "\Steam\steamapps\appmanifest_374320.acf").
|
||||
7. Back up your existing game folder in "\Steam\steamapps\common\DARK SOULS III".
|
||||
8. Return back to Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like "\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX". Back up this game folder as well.
|
||||
9. Delete your existing game folder in "\Steam\steamapps\common\DARK SOULS III", then replace it with your game folder in "\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX".
|
||||
10. Back up and delete your save file "DS30000.sl2" in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type %appdata% and hit enter or: open File Explorer > View > Hidden Items and follow "C:\Users\your username\AppData\Roaming\DarkSoulsIII\numbers".
|
||||
11. If you did all these steps correctly, you should be able to confirm your game version in the upper left corner after launching Dark Souls III.
|
||||
3. Open the Steam console by typing the following string: `steam://open/console`. Steam should now open in Console Mode.
|
||||
4. Insert the string of the depot you wish to download. For the AP-supported v1.15, you will want to use: `download_depot 374320 374321 4471176929659548333`.
|
||||
5. Steam will now download the depot. Note: There is no progress bar for the download in Steam, but it is still downloading in the background.
|
||||
6. Back up your existing game executable (`DarkSoulsIII.exe`) found in `\Steam\steamapps\common\DARK SOULS III\Game`. Easiest way to do this is to move it to another directory. If you have file extensions enabled, you can instead rename the executable to `DarkSoulsIII.exe.bak`.
|
||||
7. Return to the Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like `\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX`.
|
||||
8. Take the `DarkSoulsIII.exe` from that folder and place it in `\Steam\steamapps\common\DARK SOULS III\Game`.
|
||||
9. Back up and delete your save file (`DS30000.sl2`) in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type `%appdata%` and hit enter. Alternatively: open File Explorer > View > Hidden Items and follow `C:\Users\<your_username>\AppData\Roaming\DarkSoulsIII\<numbers>`.
|
||||
10. If you did all these steps correctly, you should be able to confirm your game version in the upper-left corner after launching Dark Souls III.
|
||||
|
||||
|
||||
## Installing the Archipelago mod
|
||||
|
||||
Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) and
|
||||
add it at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game")
|
||||
Get the `dinput8.dll` from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) and
|
||||
add it at the root folder of your game (e.g. `SteamLibrary\steamapps\common\DARK SOULS III\Game`)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run Steam in offline mode, both to avoid being banned and to prevent Steam from updating the game files
|
||||
2. Launch Dark Souls III
|
||||
3. Type in "/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}" in the "Windows Command Prompt" that opened
|
||||
4. Once connected, create a new game, choose a class and wait for the others before starting
|
||||
5. You can quit and launch at anytime during a game
|
||||
1. Run Steam in offline mode to avoid being banned.
|
||||
2. Launch Dark Souls III.
|
||||
3. Type in `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME} password:{PASSWORD}` in the "Windows Command Prompt" that opened. For example: `/connect archipelago.gg:38281 "Example Name" password:"Example Password"`. The password parameter is only necessary if your game requires one.
|
||||
4. Once connected, create a new game, choose a class and wait for the others before starting.
|
||||
5. You can quit and launch at anytime during a game.
|
||||
|
||||
## Where do I get a config file?
|
||||
|
||||
|
||||
@@ -29,5 +29,5 @@ placez-le à la racine du jeu (ex: "SteamLibrary\steamapps\common\DARK SOULS III
|
||||
|
||||
## Où trouver le fichier de configuration ?
|
||||
|
||||
La [Page de configuration](/games/Dark%20Souls%20III/player-settings) sur le site vous permez de configurer vos
|
||||
La [Page de configuration](/games/Dark%20Souls%20III/player-options) sur le site vous permez de configurer vos
|
||||
paramètres et de les exporter sous la forme d'un fichier.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user